view src/imap/cmd-list.c @ 21322:5ab8dc1a4a6f

global: Change string position/length from unsigned int to size_t Mainly to avoid truncating >4GB strings, which might potentially cause some security holes. Normally there are other limits, which prevent such excessive strings from being created in the first place. I'm sure this didn't find everything. Maybe everything could be found with compiler warnings. -Wconversion kind of does it, but it gives way too many unnecessary warnings. These were mainly found with: grep " = strlen" egrep "unsigned int.*(size|len)"
author Timo Sirainen <timo.sirainen@dovecot.fi>
date Mon, 12 Dec 2016 07:19:55 +0200
parents 0f22db71df7a
children 2e2563132d5f
line wrap: on
line source

/* Copyright (c) 2002-2016 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;

	unsigned int lsub:1;
	unsigned int lsub_no_unsubscribed:1;
	unsigned int used_listext:1;
	unsigned int 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 &= ~MAILBOX_NONEXISTENT;
	}

	if ((ctx->list_flags & MAILBOX_LIST_ITER_RETURN_CHILDREN) == 0)
		flags &= ~(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 &= ~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 &&
	    (flags & MAILBOX_CHILD_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_append(&full_patterns, &pattern, 1);
	}
	array_append_zero(&full_patterns); /* NULL-terminate */
	return array_idx(&full_patterns, 0);
}

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_append(&patterns, &pattern, 1);
			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_append(&patterns, &pattern, 1);
		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_idx(&patterns, 0);
	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);
}