summaryrefslogtreecommitdiffstats
path: root/src/imap/cmd-list.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/imap/cmd-list.c')
-rw-r--r--src/imap/cmd-list.c484
1 files changed, 484 insertions, 0 deletions
diff --git a/src/imap/cmd-list.c b/src/imap/cmd-list.c
new file mode 100644
index 0000000..dd6c00e
--- /dev/null
+++ b/src/imap/cmd-list.c
@@ -0,0 +1,484 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "array.h"
+#include "str.h"
+#include "strescape.h"
+#include "mailbox-list-iter.h"
+#include "imap-utf7.h"
+#include "imap-quote.h"
+#include "imap-match.h"
+#include "imap-status.h"
+#include "imap-commands.h"
+#include "imap-list.h"
+
+struct cmd_list_context {
+ struct client_command_context *cmd;
+ struct mail_user *user;
+
+ enum mailbox_list_iter_flags list_flags;
+ struct imap_status_items status_items;
+
+ struct mailbox_list_iterate_context *list_iter;
+
+ bool lsub:1;
+ bool lsub_no_unsubscribed:1;
+ bool used_listext:1;
+ bool used_status:1;
+};
+
+static void
+mailbox_flags2str(struct cmd_list_context *ctx, string_t *str,
+ const char *special_use, enum mailbox_info_flags flags)
+{
+ size_t orig_len = str_len(str);
+
+ if ((flags & MAILBOX_NONEXISTENT) != 0 && !ctx->used_listext) {
+ flags |= MAILBOX_NOSELECT;
+ flags &= ENUM_NEGATE(MAILBOX_NONEXISTENT);
+ }
+
+ if ((ctx->list_flags & MAILBOX_LIST_ITER_RETURN_CHILDREN) == 0)
+ flags &= ENUM_NEGATE(MAILBOX_CHILDREN | MAILBOX_NOCHILDREN);
+
+ if ((flags & MAILBOX_CHILD_SUBSCRIBED) != 0 &&
+ (flags & MAILBOX_SUBSCRIBED) == 0 && !ctx->used_listext) {
+ /* LSUB uses \Noselect for this */
+ flags |= MAILBOX_NOSELECT;
+ } else if ((ctx->list_flags & MAILBOX_LIST_ITER_RETURN_SUBSCRIBED) == 0)
+ flags &= ENUM_NEGATE(MAILBOX_SUBSCRIBED);
+ imap_mailbox_flags2str(str, flags);
+
+ if ((ctx->list_flags & MAILBOX_LIST_ITER_RETURN_SPECIALUSE) != 0 &&
+ special_use != NULL) {
+ if (str_len(str) != orig_len)
+ str_append_c(str, ' ');
+ str_append(str, special_use);
+ }
+}
+
+static void
+mailbox_childinfo2str(struct cmd_list_context *ctx, string_t *str,
+ enum mailbox_info_flags flags)
+{
+ if (!ctx->used_listext)
+ return;
+
+ if ((flags & MAILBOX_CHILD_SUBSCRIBED) != 0 &&
+ (ctx->list_flags & MAILBOX_LIST_ITER_SELECT_RECURSIVEMATCH) != 0)
+ str_append(str, " (CHILDINFO (\"SUBSCRIBED\"))");
+ if ((flags & MAILBOX_CHILD_SPECIALUSE) != 0 &&
+ (ctx->list_flags & MAILBOX_LIST_ITER_SELECT_RECURSIVEMATCH) != 0)
+ str_append(str, " (CHILDINFO (\"SPECIAL-USE\"))");
+}
+
+static bool
+parse_select_flags(struct cmd_list_context *ctx, const struct imap_arg *args)
+{
+ enum mailbox_list_iter_flags list_flags = 0;
+ const char *str;
+
+ while (!IMAP_ARG_IS_EOL(args)) {
+ if (!imap_arg_get_atom(args, &str)) {
+ client_send_command_error(ctx->cmd,
+ "List options contains non-atoms.");
+ return FALSE;
+ }
+
+ if (strcasecmp(str, "SUBSCRIBED") == 0) {
+ list_flags |= MAILBOX_LIST_ITER_SELECT_SUBSCRIBED |
+ MAILBOX_LIST_ITER_RETURN_SUBSCRIBED;
+ } else if (strcasecmp(str, "RECURSIVEMATCH") == 0)
+ list_flags |= MAILBOX_LIST_ITER_SELECT_RECURSIVEMATCH;
+ else if (strcasecmp(str, "SPECIAL-USE") == 0) {
+ list_flags |= MAILBOX_LIST_ITER_SELECT_SPECIALUSE |
+ MAILBOX_LIST_ITER_RETURN_SPECIALUSE;
+ } else if (strcasecmp(str, "REMOTE") == 0) {
+ /* not supported, ignore */
+ } else {
+ /* skip also optional list value */
+ client_send_command_error(ctx->cmd,
+ "Unknown select options");
+ return FALSE;
+ }
+ args++;
+ }
+
+ if ((list_flags & MAILBOX_LIST_ITER_SELECT_RECURSIVEMATCH) != 0 &&
+ (list_flags & (MAILBOX_LIST_ITER_SELECT_SUBSCRIBED |
+ MAILBOX_LIST_ITER_SELECT_SPECIALUSE)) == 0) {
+ client_send_command_error(ctx->cmd,
+ "RECURSIVEMATCH must not be the only selection.");
+ return FALSE;
+ }
+
+ ctx->list_flags = list_flags;
+ return TRUE;
+}
+
+static bool
+parse_return_flags(struct cmd_list_context *ctx, const struct imap_arg *args)
+{
+ enum mailbox_list_iter_flags list_flags = 0;
+ const struct imap_arg *list_args;
+ const char *str;
+
+ while (!IMAP_ARG_IS_EOL(args)) {
+ if (!imap_arg_get_atom(args, &str)) {
+ client_send_command_error(ctx->cmd,
+ "List options contains non-atoms.");
+ return FALSE;
+ }
+
+ if (strcasecmp(str, "SUBSCRIBED") == 0)
+ list_flags |= MAILBOX_LIST_ITER_RETURN_SUBSCRIBED;
+ else if (strcasecmp(str, "CHILDREN") == 0)
+ list_flags |= MAILBOX_LIST_ITER_RETURN_CHILDREN;
+ else if (strcasecmp(str, "SPECIAL-USE") == 0)
+ list_flags |= MAILBOX_LIST_ITER_RETURN_SPECIALUSE;
+ else if (strcasecmp(str, "STATUS") == 0 &&
+ imap_arg_get_list(&args[1], &list_args)) {
+ if (imap_status_parse_items(ctx->cmd, list_args,
+ &ctx->status_items) < 0)
+ return FALSE;
+ ctx->used_status = TRUE;
+ args++;
+ } else {
+ /* skip also optional list value */
+ client_send_command_error(ctx->cmd,
+ "Unknown return options");
+ return FALSE;
+ }
+ args++;
+ }
+
+ ctx->list_flags |= list_flags;
+ return TRUE;
+}
+
+static const char *ns_prefix_mutf7(struct mail_namespace *ns)
+{
+ string_t *str;
+
+ if (*ns->prefix == '\0')
+ return "";
+
+ str = t_str_new(64);
+ if (imap_utf8_to_utf7(ns->prefix, str) < 0)
+ i_panic("Namespace prefix not UTF-8: %s", ns->prefix);
+ return str_c(str);
+}
+
+static void list_reply_append_ns_sep_param(string_t *str, char sep)
+{
+ str_append_c(str, '"');
+ if (sep == '\\')
+ str_append(str, "\\\\");
+ else
+ str_append_c(str, sep);
+ str_append_c(str, '"');
+}
+
+static void
+list_send_status(struct cmd_list_context *ctx, const char *name,
+ const char *mutf7_name, enum mailbox_info_flags flags)
+{
+ struct imap_status_result result;
+ struct mail_namespace *ns;
+
+ if ((flags & (MAILBOX_NONEXISTENT | MAILBOX_NOSELECT)) != 0) {
+ /* doesn't exist, don't even try to get STATUS */
+ return;
+ }
+ if ((flags & MAILBOX_SUBSCRIBED) == 0 &&
+ (ctx->list_flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) != 0) {
+ /* listing subscriptions, but only child is subscribed */
+ return;
+ }
+
+ /* if we're listing subscriptions and there are subscriptions=no
+ namespaces, ctx->ns may not point to correct one */
+ ns = mail_namespace_find(ctx->user->namespaces, name);
+ if (imap_status_get(ctx->cmd, ns, name,
+ &ctx->status_items, &result) < 0) {
+ client_send_line(ctx->cmd->client,
+ t_strconcat("* ", result.errstr, NULL));
+ return;
+ }
+
+ imap_status_send(ctx->cmd->client, mutf7_name,
+ &ctx->status_items, &result);
+}
+
+static bool cmd_list_continue(struct client_command_context *cmd)
+{
+ struct cmd_list_context *ctx = cmd->context;
+ const struct mailbox_info *info;
+ enum mailbox_info_flags flags;
+ string_t *str, *mutf7_name;
+ const char *name;
+ int ret = 0;
+
+ if (cmd->cancel) {
+ if (ctx->list_iter != NULL)
+ (void)mailbox_list_iter_deinit(&ctx->list_iter);
+ return TRUE;
+ }
+ str = t_str_new(256);
+ mutf7_name = t_str_new(128);
+ while ((info = mailbox_list_iter_next(ctx->list_iter)) != NULL) {
+ name = info->vname;
+ flags = info->flags;
+
+ if ((flags & MAILBOX_CHILD_SUBSCRIBED) != 0 &&
+ (flags & MAILBOX_SUBSCRIBED) == 0 &&
+ ctx->lsub_no_unsubscribed) {
+ /* mask doesn't end with %. we don't want to show
+ any extra mailboxes. */
+ continue;
+ }
+
+ str_truncate(mutf7_name, 0);
+ if (imap_utf8_to_utf7(name, mutf7_name) < 0)
+ i_panic("LIST: Mailbox name not UTF-8: %s", name);
+
+ str_truncate(str, 0);
+ str_printfa(str, "* %s (", ctx->lsub ? "LSUB" : "LIST");
+ mailbox_flags2str(ctx, str, info->special_use, flags);
+ str_append(str, ") ");
+ list_reply_append_ns_sep_param(str,
+ mail_namespace_get_sep(info->ns));
+ str_append_c(str, ' ');
+ imap_append_astring(str, str_c(mutf7_name));
+ mailbox_childinfo2str(ctx, str, flags);
+
+ ret = client_send_line_next(ctx->cmd->client, str_c(str));
+ if (ctx->used_status) T_BEGIN {
+ list_send_status(ctx, name, str_c(mutf7_name), flags);
+ } T_END;
+ if (ret == 0) {
+ /* buffer is full, continue later */
+ return FALSE;
+ }
+ }
+
+ if (mailbox_list_iter_deinit(&ctx->list_iter) < 0) {
+ client_send_list_error(cmd, ctx->user->namespaces->list);
+ return TRUE;
+ }
+ client_send_tagline(cmd, !ctx->lsub ?
+ "OK List completed." :
+ "OK Lsub completed.");
+ return TRUE;
+}
+
+static const char *const *
+list_get_ref_patterns(struct cmd_list_context *ctx, const char *ref,
+ const char *const *patterns)
+{
+ struct mail_namespace *ns;
+ const char *const *pat, *pattern;
+ ARRAY(const char *) full_patterns;
+
+ if (*ref == '\0')
+ return patterns;
+
+ ns = mail_namespace_find(ctx->user->namespaces, ref);
+
+ t_array_init(&full_patterns, 16);
+ for (pat = patterns; *pat != NULL; pat++) {
+ pattern = mailbox_list_join_refpattern(ns->list, ref, *pat);
+ array_push_back(&full_patterns, &pattern);
+ }
+ array_append_zero(&full_patterns); /* NULL-terminate */
+ return array_front(&full_patterns);
+}
+
+static void cmd_list_init(struct cmd_list_context *ctx,
+ const char *const *patterns)
+{
+ enum mail_namespace_type type_mask = MAIL_NAMESPACE_TYPE_MASK_ALL;
+
+ ctx->list_iter =
+ mailbox_list_iter_init_namespaces(ctx->user->namespaces,
+ patterns, type_mask,
+ ctx->list_flags);
+}
+
+static void cmd_list_ref_root(struct client *client, const char *ref)
+{
+ struct mail_namespace *ns;
+ const char *ns_prefix;
+ char ns_sep;
+ string_t *str;
+
+ /* Special request to return the hierarchy delimiter and mailbox root
+ name. If namespace has a prefix, it's returned as the mailbox root.
+ Otherwise we'll emulate UW-IMAP behavior. */
+ ns = mail_namespace_find_visible(client->user->namespaces, ref);
+ if (ns != NULL) {
+ ns_prefix = ns_prefix_mutf7(ns);
+ ns_sep = mail_namespace_get_sep(ns);
+ } else {
+ ns_prefix = "";
+ ns_sep = mail_namespaces_get_root_sep(client->user->namespaces);
+ }
+
+ str = t_str_new(64);
+ str_append(str, "* LIST (\\Noselect) \"");
+ if (ns_sep == '\\' || ns_sep == '"')
+ str_append_c(str, '\\');
+ str_printfa(str, "%c\" ", ns_sep);
+ if (*ns_prefix != '\0') {
+ /* non-hidden namespace, use it as the root name */
+ imap_append_astring(str, ns_prefix);
+ } else {
+ /* Hidden namespace or empty namespace prefix. We could just
+ return an empty root name, but it's safer to emulate what
+ UW-IMAP does. With full filesystem access this might even
+ matter (root of "~user/mail/" is "~user/", not "") */
+ const char *p = strchr(ref, ns_sep);
+
+ if (p == NULL)
+ str_append(str, "\"\"");
+ else
+ imap_append_astring(str, t_strdup_until(ref, p + 1));
+ }
+ client_send_line(client, str_c(str));
+}
+
+bool cmd_list_full(struct client_command_context *cmd, bool lsub)
+{
+ struct client *client = cmd->client;
+ const struct imap_arg *args, *list_args;
+ unsigned int arg_count;
+ struct cmd_list_context *ctx;
+ ARRAY(const char *) patterns = ARRAY_INIT;
+ const char *ref, *pattern, *const *patterns_strarr;
+ string_t *str;
+
+ /* [(<selection options>)] <reference> <pattern>|(<pattern list>)
+ [RETURN (<return options>)] */
+ if (!client_read_args(cmd, 0, 0, &args))
+ return FALSE;
+
+ ctx = p_new(cmd->pool, struct cmd_list_context, 1);
+ ctx->cmd = cmd;
+ ctx->lsub = lsub;
+ ctx->user = client->user;
+
+ cmd->context = ctx;
+
+ if (!lsub && imap_arg_get_list(&args[0], &list_args)) {
+ /* LIST-EXTENDED selection options */
+ ctx->used_listext = TRUE;
+ if (!parse_select_flags(ctx, list_args))
+ return TRUE;
+ args++;
+ }
+
+ if (!imap_arg_get_astring(&args[0], &ref)) {
+ client_send_command_error(cmd, "Invalid reference.");
+ return TRUE;
+ }
+ str = t_str_new(64);
+ if (imap_utf7_to_utf8(ref, str) == 0)
+ ref = p_strdup(cmd->pool, str_c(str));
+ str_truncate(str, 0);
+
+ if (imap_arg_get_list_full(&args[1], &list_args, &arg_count)) {
+ ctx->used_listext = TRUE;
+ /* convert pattern list to string array */
+ p_array_init(&patterns, cmd->pool, arg_count);
+ for (; !IMAP_ARG_IS_EOL(list_args); list_args++) {
+ if (!imap_arg_get_astring(list_args, &pattern)) {
+ client_send_command_error(cmd,
+ "Invalid pattern list.");
+ return TRUE;
+ }
+ if (imap_utf7_to_utf8(pattern, str) == 0)
+ pattern = p_strdup(cmd->pool, str_c(str));
+ array_push_back(&patterns, &pattern);
+ str_truncate(str, 0);
+ }
+ args += 2;
+ } else {
+ if (!imap_arg_get_astring(&args[1], &pattern)) {
+ client_send_command_error(cmd, "Invalid pattern.");
+ return TRUE;
+ }
+ if (imap_utf7_to_utf8(pattern, str) == 0)
+ pattern = p_strdup(cmd->pool, str_c(str));
+
+ p_array_init(&patterns, cmd->pool, 1);
+ array_push_back(&patterns, &pattern);
+ args += 2;
+
+ if (lsub) {
+ size_t len = strlen(pattern);
+ ctx->lsub_no_unsubscribed = len == 0 ||
+ pattern[len-1] != '%';
+ }
+ }
+
+ if (imap_arg_atom_equals(&args[0], "RETURN") &&
+ imap_arg_get_list(&args[1], &list_args)) {
+ /* LIST-EXTENDED return options */
+ ctx->used_listext = TRUE;
+ if (!parse_return_flags(ctx, list_args))
+ return TRUE;
+ args += 2;
+ }
+
+ if (lsub) {
+ /* LSUB - we don't care about flags except if
+ tb-lsub-flags workaround is explicitly set */
+ ctx->list_flags |= MAILBOX_LIST_ITER_SELECT_SUBSCRIBED |
+ MAILBOX_LIST_ITER_SELECT_RECURSIVEMATCH;
+ /* Return SPECIAL-USE flags for LSUB anyway. Outlook 2013
+ does this and since it's not expensive for us to return
+ them, it's not worth the trouble of adding an explicit
+ workaround setting. */
+ ctx->list_flags |= MAILBOX_LIST_ITER_RETURN_SPECIALUSE;
+ if ((cmd->client->set->parsed_workarounds &
+ WORKAROUND_TB_LSUB_FLAGS) == 0)
+ ctx->list_flags |= MAILBOX_LIST_ITER_RETURN_NO_FLAGS;
+ } else if (!ctx->used_listext) {
+ /* non-extended LIST: use default flags */
+ ctx->list_flags |= MAILBOX_LIST_ITER_RETURN_CHILDREN |
+ MAILBOX_LIST_ITER_RETURN_SPECIALUSE;
+ }
+
+ if (!IMAP_ARG_IS_EOL(args)) {
+ client_send_command_error(cmd, "Extra arguments.");
+ return TRUE;
+ }
+
+ array_append_zero(&patterns); /* NULL-terminate */
+ patterns_strarr = array_front(&patterns);
+ if (!ctx->used_listext && !lsub && *patterns_strarr[0] == '\0') {
+ /* Only LIST ref "" gets us here */
+ cmd_list_ref_root(client, ref);
+ client_send_tagline(cmd, "OK List completed.");
+ } else {
+ patterns_strarr =
+ list_get_ref_patterns(ctx, ref, patterns_strarr);
+ cmd_list_init(ctx, patterns_strarr);
+
+ if (!cmd_list_continue(cmd)) {
+ /* unfinished */
+ cmd->state = CLIENT_COMMAND_STATE_WAIT_OUTPUT;
+ 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);
+}