cmd-list.c revision 6c462846d78e139fccfefb7c6ae0d3077b525c2d
/* Copyright (C) 2002-2004 Timo Sirainen */
#include "common.h"
#include "str.h"
#include "strescape.h"
#include "imap-quote.h"
#include "imap-match.h"
#include "commands.h"
#include "namespace.h"
enum {
_MAILBOX_LIST_HIDE_CHILDREN = 0x1000000,
_MAILBOX_LIST_LISTEXT = 0x0800000
};
struct cmd_list_context {
const char *ref;
const char *mask;
enum mailbox_list_flags list_flags;
struct namespace *ns;
struct mailbox_list_context *list_ctx;
struct imap_match_glob *glob;
unsigned int lsub:1;
unsigned int inbox_found:1;
unsigned int match_inbox:1;
};
static const char *
mailbox_flags2str(enum mailbox_flags flags, enum mailbox_list_flags list_flags)
{
const char *str;
if (flags & MAILBOX_PLACEHOLDER) {
i_assert((flags & ~MAILBOX_CHILDREN) == MAILBOX_PLACEHOLDER);
if ((list_flags & _MAILBOX_LIST_LISTEXT) == 0)
flags = MAILBOX_NOSELECT;
flags |= MAILBOX_CHILDREN;
}
if ((flags & MAILBOX_NONEXISTENT) != 0 &&
(list_flags & _MAILBOX_LIST_LISTEXT) == 0) {
flags |= MAILBOX_NOSELECT;
flags &= ~MAILBOX_NONEXISTENT;
}
if ((list_flags & _MAILBOX_LIST_HIDE_CHILDREN) != 0)
flags &= ~(MAILBOX_CHILDREN|MAILBOX_NOCHILDREN);
str = t_strconcat(
(flags & MAILBOX_NOSELECT) ? " \\Noselect" : "",
(flags & MAILBOX_NONEXISTENT) ? " \\NonExistent" : "",
(flags & MAILBOX_PLACEHOLDER) ? " \\PlaceHolder" : "",
(flags & MAILBOX_CHILDREN) ? " \\HasChildren" : "",
(flags & MAILBOX_NOCHILDREN) ? " \\HasNoChildren" : "",
(flags & MAILBOX_NOINFERIORS) ? " \\NoInferiors" : "",
(flags & MAILBOX_MARKED) ? " \\Marked" : "",
(flags & MAILBOX_UNMARKED) ? " \\UnMarked" : "",
NULL);
return *str == '\0' ? "" : str+1;
}
static bool
parse_list_flags(struct client_command_context *cmd, struct imap_arg *args,
enum mailbox_list_flags *list_flags)
{
const char *atom;
while (args->type != IMAP_ARG_EOL) {
if (args->type != IMAP_ARG_ATOM) {
client_send_command_error(cmd,
"List options contains non-atoms.");
return FALSE;
}
atom = IMAP_ARG_STR(args);
if (strcasecmp(atom, "SUBSCRIBED") == 0)
*list_flags |= MAILBOX_LIST_SUBSCRIBED;
else if (strcasecmp(atom, "CHILDREN") == 0)
*list_flags |= MAILBOX_LIST_CHILDREN;
else {
client_send_tagline(cmd, t_strconcat(
"BAD Invalid list option ", atom, NULL));
return FALSE;
}
args++;
}
return TRUE;
}
static void
list_namespace_inbox(struct client *client, struct cmd_list_context *ctx)
{
const char *str;
if (!ctx->inbox_found && ctx->ns->inbox && ctx->match_inbox &&
(ctx->list_flags & MAILBOX_LIST_SUBSCRIBED) == 0) {
/* INBOX always exists */
str = t_strdup_printf("* LIST (\\Unmarked) \"%s\" \"INBOX\"",
ctx->ns->sep_str);
client_send_line(client, str);
}
}
static int
list_namespace_mailboxes(struct client *client, struct cmd_list_context *ctx)
{
struct mailbox_list *list;
const char *name;
string_t *str, *name_str;
int ret;
if (ctx->list_ctx == NULL) {
list_namespace_inbox(client, ctx);
return 1;
}
t_push();
str = t_str_new(256);
name_str = t_str_new(256);
while ((list = mail_storage_mailbox_list_next(ctx->list_ctx)) != NULL) {
str_truncate(name_str, 0);
/* when listing INBOX from inbox=yes namespace, don't insert
the namespace prefix */
if (strcasecmp(list->name, "INBOX") != 0 || !ctx->ns->inbox)
str_append(name_str, ctx->ns->prefix);
str_append(name_str, list->name);
if (ctx->ns->sep != ctx->ns->real_sep) {
char *p = str_c_modifiable(name_str);
for (; *p != '\0'; p++) {
if (*p == ctx->ns->real_sep)
*p = ctx->ns->sep;
}
}
name = str_c(name_str);
if (*ctx->ns->prefix != '\0') {
/* With masks containing '*' we do the checks here
so prefix is included in matching */
if (ctx->glob != NULL &&
imap_match(ctx->glob, name) != IMAP_MATCH_YES)
continue;
}
if (strcasecmp(name, "INBOX") == 0) {
if (!ctx->ns->inbox)
continue;
name = "INBOX";
ctx->inbox_found = TRUE;
}
str_truncate(str, 0);
str_printfa(str, "* %s (%s) \"%s\" ",
ctx->lsub ? "LSUB" : "LIST",
mailbox_flags2str(list->flags, ctx->list_flags),
ctx->ns->sep_str);
imap_quote_append_string(str, name, FALSE);
if (client_send_line(client, str_c(str)) == 0) {
/* buffer is full, continue later */
t_pop();
return 0;
}
}
list_namespace_inbox(client, ctx);
t_pop();
ret = mail_storage_mailbox_list_deinit(&ctx->list_ctx);
ctx->list_ctx = NULL;
return ret < 0 ? -1 : 1;
}
static bool list_mask_has_wildcards(const char *mask)
{
for (; *mask != '\0'; mask++) {
if (*mask == '%' || *mask == '*')
return TRUE;
}
return FALSE;
}
static void
skip_namespace_prefix(const char **prefix, const char **mask,
bool inbox_check, char sep)
{
size_t mask_len, prefix_len;
bool match;
prefix_len = strlen(*prefix);
mask_len = strlen(*mask);
if (mask_len < prefix_len) {
/* eg. namespace prefix = "INBOX.", mask = "INBOX" */
return;
}
match = strncmp(*prefix, *mask, prefix_len) == 0;
if (!match && inbox_check) {
/* try INBOX check. */
match = prefix_len >= 5 &&
strncasecmp(*prefix, *mask, 5) == 0 &&
strncmp(*prefix + 5, *mask + 5, prefix_len - 5) == 0 &&
strncasecmp(*prefix, "INBOX", 5) == 0 &&
((*prefix)[5] == sep || (*prefix)[5] == '\0');
}
if (match) {
*prefix += prefix_len;
*mask += prefix_len;
}
}
static void
list_namespace_init(struct client_command_context *cmd,
struct cmd_list_context *ctx)
{
struct client *client = cmd->client;
struct namespace *ns = ctx->ns;
const char *cur_ns_prefix, *cur_ref, *cur_mask;
enum imap_match_result match;
enum mailbox_list_flags list_flags;
enum imap_match_result inbox_match;
struct imap_match_glob *inbox_glob;
unsigned int count;
size_t len;
cur_ns_prefix = ns->prefix;
cur_ref = ctx->ref;
cur_mask = ctx->mask;
if (*cur_ref != '\0' && *cur_ns_prefix != '\0') {
/* reference argument given. skip namespace prefix using it.
cur_ns_prefix = foo/bar/
cur_ref = foo/
-> cur_ns_prefix=bar/, cur_ref=""
cur_ref = foo/bar/baz
-> cur_ns_prefix="", cur_ref="baz"
*/
skip_namespace_prefix(&cur_ns_prefix, &cur_ref, TRUE, ns->sep);
if (*cur_ref != '\0' && *cur_ns_prefix != '\0') {
/* reference parameter didn't match with
namespace prefix. skip this. */
return;
}
}
if (*cur_ns_prefix != '\0') {
/* no reference parameter. skip namespace prefix from mask. */
const char *old_ns_prefix = cur_ns_prefix;
const char *old_mask = cur_mask;
i_assert(*cur_ref == '\0');
skip_namespace_prefix(&cur_ns_prefix, &cur_mask,
cur_ref == ctx->ref, ns->sep);
if (*cur_mask == '\0' && *cur_ns_prefix == '\0') {
/* trying to list the namespace prefix itself. */
cur_ns_prefix = old_ns_prefix;
cur_mask = old_mask;
}
}
/* if the original reference and mask combined produces something
that matches INBOX, the INBOX casing is on. */
inbox_glob = imap_match_init(cmd->pool,
t_strconcat(ctx->ref, ctx->mask, NULL),
TRUE, ns->sep);
inbox_match = imap_match(inbox_glob, "INBOX");
ctx->match_inbox = inbox_match == IMAP_MATCH_YES;
ctx->glob = imap_match_init(cmd->pool, ctx->mask,
(inbox_match == IMAP_MATCH_YES ||
inbox_match == IMAP_MATCH_PARENT) &&
cur_mask == ctx->mask,
ns->sep);
if (*cur_ns_prefix != '\0') {
/* namespace prefix still wasn't completely skipped over.
for example cur_ns_prefix=INBOX/, mask=%/% or mask=IN%.
Check that mask matches namespace prefix. */
bool skip_trailing_sep = FALSE;
i_assert(*cur_ref == '\0');
/* drop the trailing separator in namespace prefix.
don't do it if we're listing only the prefix itself. */
len = strlen(cur_ns_prefix);
if (cur_ns_prefix[len-1] == ns->sep &&
strcmp(cur_mask, cur_ns_prefix) != 0) {
skip_trailing_sep = TRUE;
cur_ns_prefix = t_strndup(cur_ns_prefix, len-1);
}
/* hidden namespaces should still be seen without wildcards.
some clients rely on this. */
match = (ns->hidden && list_mask_has_wildcards(cur_mask)) ?
IMAP_MATCH_NO : imap_match(ctx->glob, cur_ns_prefix);
if (match < 0)
return;
len = strlen(ns->prefix);
if (match == IMAP_MATCH_YES &&
(ctx->list_flags & MAILBOX_LIST_SUBSCRIBED) == 0 &&
(!ctx->ns->inbox ||
strncmp(ns->prefix, "INBOX", len-1) != 0)) {
/* The prefix itself matches */
enum mailbox_flags flags;
string_t *str = t_str_new(128);
flags = MAILBOX_PLACEHOLDER;
str_printfa(str, "* LIST (%s) \"%s\" ",
mailbox_flags2str(flags, ctx->list_flags),
ns->sep_str);
imap_quote_append_string(str, skip_trailing_sep ?
t_strndup(ns->prefix, len-1) : ns->prefix,
FALSE);
client_send_line(client, str_c(str));
}
}
if (*cur_ns_prefix != '\0') {
/* We didn't skip over the whole namespace prefix. For example
cur_ns_prefix=INBOX/ and mask=%/% or IN*.
We have already verified that the mask matches the namespace
prefix, so we'll just have to skip over as many hierarchies
from mask as there exists in namespace prefix.
The "INBOX" namespace match reply was already sent. We're
only listing the actual mailboxes now. */
i_assert(*cur_ref == '\0');
for (count = 1; *cur_ns_prefix != '\0'; cur_ns_prefix++) {
if (*cur_ns_prefix == ns->sep)
count++;
}
for (; count > 0; count--) {
/* skip over one hierarchy */
while (*cur_mask != '\0' && *cur_mask != '*' &&
*cur_mask != ns->sep)
cur_mask++;
if (*cur_mask == '*') {
/* we'll just request "*" and filter it
ourself. otherwise this gets too complex. */
cur_mask = "*";
break;
}
if (*cur_mask == '\0') {
/* mask ended too early. we won't be listing
any mailboxes. */
break;
}
cur_mask++;
}
}
if (*cur_mask != '*' || strcmp(ctx->mask, "*") == 0) {
/* a) we don't have '*' in mask
b) we want to display everything
we don't need to do separate filtering ourself */
ctx->glob = NULL;
}
cur_ref = namespace_fix_sep(ns, cur_ref);
cur_mask = namespace_fix_sep(ns, cur_mask);
list_flags = ctx->list_flags;
if ((*ns->prefix == '\0' || ns->inbox) && ctx->match_inbox)
list_flags |= MAILBOX_LIST_INBOX;
ctx->list_ctx = mail_storage_mailbox_list_init(ns->storage,
cur_ref, cur_mask,
list_flags);
}
static bool cmd_list_continue(struct client_command_context *cmd)
{
struct client *client = cmd->client;
struct cmd_list_context *ctx = cmd->context;
int ret;
for (; ctx->ns != NULL; ctx->ns = ctx->ns->next) {
if (ctx->list_ctx == NULL)
list_namespace_init(cmd, ctx);
if ((ret = list_namespace_mailboxes(client, ctx)) < 0) {
client_send_storage_error(cmd, ctx->ns->storage);
return TRUE;
}
if (ret == 0)
return FALSE;
}
client_send_tagline(cmd, !ctx->lsub ?
"OK List completed." :
"OK Lsub completed.");
return TRUE;
}
bool _cmd_list_full(struct client_command_context *cmd, bool lsub)
{
struct client *client = cmd->client;
struct namespace *ns;
struct imap_arg *args;
enum mailbox_list_flags list_flags;
struct cmd_list_context *ctx;
const char *ref, *mask;
/* [(<options>)] <reference> <mailbox wildcards> */
if (!client_read_args(cmd, 0, 0, &args))
return FALSE;
if (lsub) {
/* LSUB - we don't care about flags */
list_flags = MAILBOX_LIST_SUBSCRIBED | MAILBOX_LIST_FAST_FLAGS |
_MAILBOX_LIST_HIDE_CHILDREN;
} else if (args[0].type != IMAP_ARG_LIST) {
/* LIST - allow children flags, but don't require them */
list_flags = 0;
} else {
list_flags = _MAILBOX_LIST_LISTEXT;
if (!parse_list_flags(cmd, IMAP_ARG_LIST(&args[0])->args,
&list_flags))
return TRUE;
args++;
/* don't show children flags unless explicitly specified */
if ((list_flags & MAILBOX_LIST_CHILDREN) == 0)
list_flags |= _MAILBOX_LIST_HIDE_CHILDREN;
}
ref = imap_arg_string(&args[0]);
mask = imap_arg_string(&args[1]);
if (ref == NULL || mask == NULL) {
client_send_command_error(cmd, "Invalid arguments.");
return TRUE;
}
if (*mask == '\0' && !lsub) {
/* special request to return the hierarchy delimiter and
mailbox root name. Mailbox root name is somewhat strange
concept which probably no other client uses than Pine.
Just try our best to emulate UW-IMAP behavior and hopefully
we're fine. */
ns = namespace_find_visible(client->namespaces, &ref);
if (ns == NULL) {
const char *empty = "";
ns = namespace_find(client->namespaces, &empty);
}
if (ns != NULL) {
string_t *str = t_str_new(64);
str_printfa(str, "* LIST (\\Noselect) \"%s\" ",
ns->sep_str);
if (*ns->prefix != '\0' && !ns->hidden) {
/* public namespace, use it as the root name */
imap_quote_append_string(str, ns->prefix,
FALSE);
} else {
/* private namespace, or empty namespace
prefix. use the mailbox name's first part
as the root. */
const char *p = strchr(ref, ns->sep);
if (p == NULL)
str_append(str, "\"\"");
else {
imap_quote_append_string(str,
t_strdup_until(ref, p + 1),
FALSE);
}
}
client_send_line(client, str_c(str));
}
client_send_tagline(cmd, "OK List completed.");
} else {
ctx = p_new(cmd->pool, struct cmd_list_context, 1);
ctx->ref = ref;
ctx->mask = mask;
ctx->list_flags = list_flags;
ctx->lsub = lsub;
ctx->ns = client->namespaces;
cmd->context = ctx;
if (!cmd_list_continue(cmd)) {
/* unfinished */
client->command_pending = TRUE;
cmd->func = cmd_list_continue;
return FALSE;
}
cmd->context = NULL;
return TRUE;
}
return TRUE;
}
bool cmd_list(struct client_command_context *cmd)
{
return _cmd_list_full(cmd, FALSE);
}