view src/imap/cmd-list.c @ 5829:1d73153584d2 HEAD

Mailbox listing API changed to support more features. Used to implement support for half of LIST-EXTENDED.
author Timo Sirainen <tss@iki.fi>
date Fri, 29 Jun 2007 03:39:27 +0300
parents 86282604e2f5
children 1d91d36e14a4
line wrap: on
line source

/* Copyright (C) 2002-2007 Timo Sirainen */

#include "common.h"
#include "array.h"
#include "str.h"
#include "strescape.h"
#include "imap-quote.h"
#include "imap-match.h"
#include "commands.h"
#include "mail-namespace.h"

struct cmd_list_context {
	struct client_command_context *cmd;
	const char *ref;
	const char *mask;
	enum mailbox_list_flags list_flags;

	struct mail_namespace *ns;
	struct mailbox_list_iterate_context *list_iter;
	struct imap_match_glob *glob;

	ARRAY_DEFINE(ns_prefixes_listed, struct mail_namespace *);

	unsigned int lsub:1;
	unsigned int inbox_found:1;
	unsigned int seen_inbox_namespace:1;
	unsigned int cur_ns_match_inbox:1;
	unsigned int cur_ns_send_prefix:1;
	unsigned int cur_ns_skip_trailing_sep:1;
	unsigned int used_listext:1;
};

static void
mailbox_flags2str(struct cmd_list_context *ctx, string_t *str,
		  enum mailbox_info_flags flags)
{
	unsigned int 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_SUBSCRIBED) != 0 &&
	    (ctx->list_flags & MAILBOX_LIST_ITER_RETURN_SUBSCRIBED) != 0)
		str_append(str, "\\Subscribed ");

	if ((flags & MAILBOX_CHILD_SUBSCRIBED) != 0 && !ctx->used_listext) {
		/* LSUB uses \Noselect for this */
		flags |= MAILBOX_NOSELECT;
	}

	if ((flags & MAILBOX_NOSELECT) != 0)
		str_append(str, "\\Noselect ");
	if ((flags & MAILBOX_NONEXISTENT) != 0)
		str_append(str, "\\NonExistent ");

	if ((flags & MAILBOX_CHILDREN) != 0)
		str_append(str, "\\HasChildren ");
	else if ((flags & MAILBOX_NOINFERIORS) != 0)
		str_append(str, "\\NoInferiors ");
	else if ((flags & MAILBOX_NOCHILDREN) != 0)
		str_append(str, "\\HasNoChildren ");

	if ((flags & MAILBOX_MARKED) != 0)
		str_append(str, "\\Marked ");
	if ((flags & MAILBOX_UNMARKED) != 0)
		str_append(str, "\\UnMarked ");

	if (str_len(str) != orig_len)
		str_truncate(str, str_len(str)-1);
}

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)
		str_append(str, " (\"CHILDINFO\" (\"SUBSCRIBED\"))");
}

static bool
parse_select_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_ITER_SELECT_SUBSCRIBED |
				MAILBOX_LIST_ITER_RETURN_SUBSCRIBED;
		} else if (strcasecmp(atom, "RECURSIVEMATCH") == 0)
			*list_flags |= MAILBOX_LIST_ITER_SELECT_RECURSIVEMATCH;
		args++;
	}

	if ((*list_flags & MAILBOX_LIST_ITER_SELECT_RECURSIVEMATCH) != 0 &&
	    (*list_flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) == 0) {
		client_send_command_error(cmd,
			"RECURSIVEMATCH must not be the only selection.");
		return FALSE;
	}
	return TRUE;
}

static bool
parse_return_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_ITER_RETURN_SUBSCRIBED;
		else if (strcasecmp(atom, "CHILDREN") == 0)
			*list_flags |= MAILBOX_LIST_ITER_RETURN_CHILDREN;
		args++;
	}
	return TRUE;
}

static enum mailbox_info_flags
list_get_inbox_flags(struct cmd_list_context *ctx)
{
	struct mail_namespace *ns;
	struct mailbox_list_iterate_context *list_iter;
	const struct mailbox_info *info;
	enum mailbox_info_flags flags = MAILBOX_UNMARKED;

	if (ctx->seen_inbox_namespace &&
	    (ctx->ns->flags & NAMESPACE_FLAG_INBOX) == 0) {
		/* INBOX doesn't exist. use the default INBOX flags */
		return flags;
	}

	/* find the INBOX flags */
	ns = mail_namespace_find_inbox(ctx->cmd->client->namespaces);
	list_iter = mailbox_list_iter_init(ns->list, "INBOX", 0);
	info = mailbox_list_iter_next(list_iter);
	if (info != NULL) {
		i_assert(strcasecmp(info->name, "INBOX") == 0);
		flags = info->flags;
	}
	(void)mailbox_list_iter_deinit(&list_iter);
	return flags;
}

static bool list_namespace_has_children(struct cmd_list_context *ctx)
{
	struct mailbox_list_iterate_context *list_iter;
	const struct mailbox_info *info;
	bool ret = FALSE;

	list_iter = mailbox_list_iter_init(ctx->ns->list, "%",
					   MAILBOX_LIST_ITER_RETURN_NO_FLAGS);
	info = mailbox_list_iter_next(list_iter);
	if (info != NULL)
		ret = TRUE;
	if (mailbox_list_iter_deinit(&list_iter) < 0) {
		/* safer to answer TRUE in error conditions */
		ret = TRUE;
	}
	return ret;
}

static void
list_namespace_send_prefix(struct cmd_list_context *ctx, bool have_children)
{
	struct mail_namespace *const *listed;
	unsigned int i, count, len;
	enum mailbox_info_flags flags;
	const char *name;
	string_t *str;
	
	ctx->cur_ns_send_prefix = FALSE;

	/* see if we already listed this as a valid mailbox in another
	   namespace */
	listed = array_get(&ctx->ns_prefixes_listed, &count);
	for (i = 0; i < count; i++) {
		if (listed[i] == ctx->ns)
			return;
	}

	len = strlen(ctx->ns->prefix);
	if (len == 6 && strncasecmp(ctx->ns->prefix, "INBOX", len-1) == 0 &&
	    ctx->ns->prefix[len-1] == ctx->ns->sep) {
		/* INBOX namespace needs to be handled specially. */
		if (ctx->inbox_found) {
			/* we're just now going to send it */
			return;
		}

		ctx->inbox_found = TRUE;
		flags = list_get_inbox_flags(ctx);
	} else {
		flags = MAILBOX_NONEXISTENT;
	}

	if ((flags & MAILBOX_CHILDREN) == 0) {
		if (have_children || list_namespace_has_children(ctx)) {
			flags |= MAILBOX_CHILDREN;
			flags &= ~MAILBOX_NOCHILDREN;
		} else {
			flags |= MAILBOX_NOCHILDREN;
		}
	}

	name = ctx->cur_ns_skip_trailing_sep ?
		t_strndup(ctx->ns->prefix, len-1) : ctx->ns->prefix;

	str = t_str_new(128);
	str_append(str, "* LIST (");
	mailbox_flags2str(ctx, str, flags);
	str_printfa(str, ") \"%s\" ", ctx->ns->sep_str);
	imap_quote_append_string(str, name, FALSE);
	mailbox_childinfo2str(ctx, str, flags);

	client_send_line(ctx->cmd->client, str_c(str));
}

static bool
list_insert_ns_prefix(string_t *name_str, struct cmd_list_context *ctx,
		      const struct mailbox_info *info)
{
	if (strcasecmp(info->name, "INBOX") != 0) {
		/* non-INBOX always has prefix */
	} else if ((ctx->ns->flags & NAMESPACE_FLAG_INBOX) == 0) {
		/* INBOX from non-INBOX namespace. */
		if (*ctx->ns->prefix == '\0') {
			/* no namespace prefix, we can't list this */
			return FALSE;
		}
	} else if (!ctx->cur_ns_match_inbox) {
		/* The mask doesn't match INBOX (eg. prefix.%).
		   We still want to list prefix.INBOX if it has
		   children. Otherwise we don't want to list
		   this INBOX at all. */
		if ((info->flags & MAILBOX_CHILDREN) == 0)
			return FALSE;
	} else {
		/* Listing INBOX from inbox=yes namespace.
		   Don't insert the namespace prefix. */
		return TRUE;
	}
	str_append(name_str, ctx->ns->prefix);
	return TRUE;
}

static int
list_namespace_mailboxes(struct cmd_list_context *ctx)
{
	const struct mailbox_info *info;
	struct mail_namespace *ns;
	enum mailbox_info_flags flags;
	string_t *str, *name_str;
	const char *name;
	int ret = 0;

	t_push();
	str = t_str_new(256);
	name_str = t_str_new(256);
	while ((info = mailbox_list_iter_next(ctx->list_iter)) != NULL) {
		str_truncate(name_str, 0);

		if (!list_insert_ns_prefix(name_str, ctx, info))
			continue;
		str_append(name_str, info->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);
		flags = info->flags;

		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) {
			i_assert((ctx->ns->flags & NAMESPACE_FLAG_INBOX) != 0);
			if (ctx->inbox_found) {
				/* we already listed this at the beginning
				   of handling INBOX/ namespace */
				continue;
			}
			ctx->inbox_found = TRUE;
		}
		if (ctx->cur_ns_send_prefix)
			list_namespace_send_prefix(ctx, TRUE);

		/* if there's a namespace with this name, list it as
		   having children */
		ns = mail_namespace_find_prefix_nosep(ctx->ns, name);
		if (ns != NULL) {
			flags |= MAILBOX_CHILDREN;
			flags &= ~MAILBOX_NOCHILDREN;
			array_append(&ctx->ns_prefixes_listed, &ns, 1);
		}

		str_truncate(str, 0);
		str_printfa(str, "* %s (", ctx->lsub ? "LSUB" : "LIST");
		mailbox_flags2str(ctx, str, flags);
		str_printfa(str, ") \"%s\" ", ctx->ns->sep_str);
		imap_quote_append_string(str, name, FALSE);
		mailbox_childinfo2str(ctx, str, flags);

		if (client_send_line(ctx->cmd->client, str_c(str)) == 0) {
			/* buffer is full, continue later */
			t_pop();
			return 0;
		}
	}

	if (mailbox_list_iter_deinit(&ctx->list_iter) < 0)
		ret = -1;

	t_pop();
	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 bool
skip_namespace_prefix_refmask(struct cmd_list_context *ctx,
			      const char **cur_ns_prefix_p,
			      const char **cur_ref_p, const char **cur_mask_p)
{
	const char *cur_ns_prefix = *cur_ns_prefix_p;
	const char *cur_ref = *cur_ref_p;
	const char *cur_mask = *cur_mask_p;

	if (*ctx->ns->prefix == '\0')
		return TRUE;

	if (*cur_ref != '\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,
				      ctx->ns->sep);

		if (*cur_ref != '\0' && *cur_ns_prefix != '\0') {
			/* reference parameter didn't match with
			   namespace prefix. skip this. */
			return FALSE;
		}
	}

	if (*cur_ns_prefix != '\0') {
		/* skip namespace prefix using 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, ctx->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;
		}
	}

	*cur_ns_prefix_p = cur_ns_prefix;
	*cur_ref_p = cur_ref;
	*cur_mask_p = cur_mask;
	return TRUE;
}

static enum imap_match_result
list_use_inboxcase(struct cmd_list_context *ctx)
{
	struct imap_match_glob *inbox_glob;

	if (*ctx->ns->prefix != '\0' &&
	    (ctx->ns->flags & NAMESPACE_FLAG_INBOX) == 0)
		return IMAP_MATCH_NO;

	/* if the original reference and mask combined produces something
	   that matches INBOX, the INBOX casing is on. */
	inbox_glob = imap_match_init(ctx->cmd->pool,
				     t_strconcat(ctx->ref, ctx->mask, NULL),
				     TRUE, ctx->ns->sep);
	return imap_match(inbox_glob, "INBOX");
}

static void
skip_mask_wildcard_prefix(const char *cur_ns_prefix, char sep,
			  const char **cur_mask_p)
{
	const char *cur_mask = *cur_mask_p;
	unsigned int count;

	for (count = 1; *cur_ns_prefix != '\0'; cur_ns_prefix++) {
		if (*cur_ns_prefix == sep)
			count++;
	}

	for (; count > 0; count--) {
		/* skip over one hierarchy */
		while (*cur_mask != '\0' && *cur_mask != '*' &&
		       *cur_mask != 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++;
	}

	*cur_mask_p = cur_mask;
}

static void list_namespace_init(struct cmd_list_context *ctx)
{
	struct mail_namespace *ns = ctx->ns;
	const char *cur_ns_prefix, *cur_ref, *cur_mask;
	enum imap_match_result match;
	enum imap_match_result inbox_match;
	size_t len;

	cur_ns_prefix = ns->prefix;
	cur_ref = ctx->ref;
	cur_mask = ctx->mask;

	if ((ctx->ns->flags & NAMESPACE_FLAG_HIDDEN) != 0 &&
	    (ctx->list_flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) != 0) {
		/* ignore hidden namespaces */
		return;
	}

	ctx->cur_ns_skip_trailing_sep = FALSE;

	if ((ns->flags & NAMESPACE_FLAG_INBOX) != 0)
		ctx->seen_inbox_namespace = TRUE;

	if (!skip_namespace_prefix_refmask(ctx, &cur_ns_prefix,
					   &cur_ref, &cur_mask))
		return;

	inbox_match = list_use_inboxcase(ctx);
	ctx->cur_ns_match_inbox = inbox_match == IMAP_MATCH_YES;

	ctx->glob = imap_match_init(ctx->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. */
		i_assert(*cur_ref == '\0');

		/* drop the trailing separator in namespace prefix.
		   don't do it if we're listing only the prefix itself
		   (LIST "" foo/ needs to return "foo/" entry) */
		len = strlen(cur_ns_prefix);
		if (cur_ns_prefix[len-1] == ns->sep &&
		    strcmp(cur_mask, cur_ns_prefix) != 0) {
			ctx->cur_ns_skip_trailing_sep = TRUE;
			cur_ns_prefix = t_strndup(cur_ns_prefix, len-1);
		}

		/* hidden and non-listable namespaces should still be seen
		   without wildcards. */
		match = ((ns->flags & NAMESPACE_FLAG_LIST) == 0 &&
			 list_mask_has_wildcards(cur_mask)) ?
			IMAP_MATCH_NO : imap_match(ctx->glob, cur_ns_prefix);
		if (match < 0)
			return;

		if (match == IMAP_MATCH_YES &&
		    (ns->flags & NAMESPACE_FLAG_LIST) != 0 &&
		    (ctx->list_flags &
		     MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) == 0)
			ctx->cur_ns_send_prefix = TRUE;
	}


	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. */
		i_assert(*cur_ref == '\0');
		skip_mask_wildcard_prefix(cur_ns_prefix, ns->sep, &cur_mask);

		if (*cur_mask == '\0' && ctx->cur_ns_match_inbox) {
			/* ns_prefix="INBOX/" and we wanted to list "%".
			   This is an optimization to avoid doing an empty
			   listing followed by another INBOX listing later. */
			cur_mask = "INBOX";
		}
	}

	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 = mail_namespace_fix_sep(ns, cur_ref);
	cur_mask = mail_namespace_fix_sep(ns, cur_mask);

	cur_mask = mailbox_list_join_refmask(ns->list, cur_ref, cur_mask);
	ctx->list_iter = mailbox_list_iter_init(ns->list, cur_mask,
						ctx->list_flags);
}

static void list_inbox(struct cmd_list_context *ctx)
{
	const char *str;

	/* INBOX always exists */
	if (!ctx->inbox_found && ctx->cur_ns_match_inbox &&
	    (ctx->ns->flags & NAMESPACE_FLAG_INBOX) != 0 &&
	    (ctx->list_flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) == 0) {
		str = t_strdup_printf("* LIST (\\Unmarked) \"%s\" \"INBOX\"",
				      ctx->ns->sep_str);
		client_send_line(ctx->cmd->client, str);
	}
}

static bool cmd_list_continue(struct client_command_context *cmd)
{
        struct cmd_list_context *ctx = cmd->context;
	int ret;

	if (cmd->cancel) {
		if (ctx->list_iter != NULL)
			(void)mailbox_list_iter_deinit(&ctx->list_iter);
		return TRUE;
	}
	for (; ctx->ns != NULL; ctx->ns = ctx->ns->next) {
		if (ctx->list_iter == NULL) {
			list_namespace_init(ctx);
			if (ctx->list_iter == NULL)
				continue;
		}

		if ((ret = list_namespace_mailboxes(ctx)) < 0) {
			client_send_list_error(cmd, ctx->ns->list);
			return TRUE;
		}
		if (ret == 0)
			return FALSE;

		if (ctx->cur_ns_send_prefix) {
			/* no mailboxes in this namespace */
			list_namespace_send_prefix(ctx, FALSE);
		}
		list_inbox(ctx);
	}

	client_send_tagline(cmd, !ctx->lsub ?
			    "OK List completed." :
			    "OK Lsub completed.");
	return TRUE;
}

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->namespaces, &ref);
	if (ns != NULL) {
		ns_prefix = ns->prefix;
		ns_sep = ns->sep;
	} else {
		ns_prefix = "";
		ns_sep = mail_namespace_get_root_sep(client->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_quote_append_string(str, ns_prefix, FALSE);
	} 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_quote_append_string(str,
						 t_strdup_until(ref, p + 1),
						 FALSE);
		}
	}
	client_send_line(client, str_c(str));
}

bool _cmd_list_full(struct client_command_context *cmd, bool lsub)
{
	struct client *client = cmd->client;
	struct imap_arg *args;
	enum mailbox_list_flags list_flags;
        struct cmd_list_context *ctx;
	const char *ref, *mask;
	bool used_listext = FALSE;

	/* [(<selection options>)] <reference> <pattern>|(<pattern list>)
	   [RETURN (<return options>)] */
	if (!client_read_args(cmd, 0, 0, &args))
		return FALSE;

	if (lsub) {
		/* LSUB - we don't care about flags */
		list_flags = MAILBOX_LIST_ITER_SELECT_SUBSCRIBED |
			MAILBOX_LIST_ITER_SELECT_RECURSIVEMATCH |
			MAILBOX_LIST_ITER_RETURN_NO_FLAGS;
	} else if (args[0].type != IMAP_ARG_LIST) {
		/* LIST - allow children flags, but don't require them */
		list_flags = MAILBOX_LIST_ITER_RETURN_CHILDREN;
	} else {
		/* LIST-EXTENDED extension */
		used_listext = TRUE;

		if (!parse_select_flags(cmd, IMAP_ARG_LIST(&args[0])->args,
					&list_flags))
			return TRUE;
		args++;

		if (args[0].type == IMAP_ARG_EOL ||
		    args[1].type == IMAP_ARG_EOL) {
			client_send_command_error(cmd, "Invalid arguments.");
			return TRUE;
		}

		if (args[2].type == IMAP_ARG_ATOM &&
		    strcasecmp(imap_arg_string(&args[2]), "RETURN") == 0 &&
		    args[3].type == IMAP_ARG_LIST &&
		    args[4].type == IMAP_ARG_EOL) {
			if (!parse_return_flags(cmd,
						IMAP_ARG_LIST(&args[3])->args,
						&list_flags))
				return TRUE;
		} else if (args[2].type != IMAP_ARG_EOL) {
			client_send_command_error(cmd, "Invalid arguments.");
			return TRUE;
		}
	}

	ref = imap_arg_string(&args[0]);
	mask = ref == NULL ? NULL : imap_arg_string(&args[1]);

	if (ref == NULL || (mask == NULL && args[1].type != IMAP_ARG_LIST)) {
		client_send_command_error(cmd, "Invalid arguments.");
		return TRUE;
	}

	if (mask != NULL && *mask == '\0' && !lsub) {
		/* only with mask string, not with list */
		cmd_list_ref_root(client, ref);
		client_send_tagline(cmd, "OK List completed.");
	} else {
		/* FIXME: handle mask lists */
		ctx = p_new(cmd->pool, struct cmd_list_context, 1);
		ctx->cmd = cmd;
		ctx->ref = ref;
		ctx->mask = mask;
		ctx->list_flags = list_flags;
		ctx->used_listext = used_listext;
		ctx->lsub = lsub;
		ctx->ns = client->namespaces;
		p_array_init(&ctx->ns_prefixes_listed, cmd->pool, 8);

		cmd->context = ctx;
		if (!cmd_list_continue(cmd)) {
			/* unfinished */
			cmd->output_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);
}