/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "buffer.h"
#include "istream.h"
#include "str.h"
#include "message-part-data.h"
#include "message-parser.h"
#include "rfc822-parser.h"
#include "rfc2231-parser.h"
#include "imap-parser.h"
#include "imap-quote.h"
#include "imap-envelope.h"
#include "imap-bodystructure.h"

/* The max level of lists nesting inside the parenthesised representation of a
   single part with no other parts inside, i.e. the max level of list nesting
   in representing a single part. According to RFC-3501, this should be in the
   order of a couple of nestings only, let's keep some margin just in case. */
#define BODYSTRUCTURE_MAX_PARENTHESIS_NESTING \
	(MESSAGE_PARSER_DEFAULT_MAX_NESTED_MIME_PARTS + 10)

#define EMPTY_BODY "(\"text\" \"plain\" " \
	"(\"charset\" \""MESSAGE_PART_DEFAULT_CHARSET"\") NIL NIL \"7bit\" 0 0)"
#define EMPTY_BODYSTRUCTURE "(\"text\" \"plain\" " \
	"(\"charset\" \""MESSAGE_PART_DEFAULT_CHARSET"\") NIL NIL \"7bit\" 0 0 " \
		"NIL NIL NIL NIL)"

/*
 * IMAP BODY/BODYSTRUCTURE write
 */

static void
params_write(const struct message_part_param *params, unsigned int params_count,
	     string_t *str, bool default_charset, enum imap_quote_flags qflags)
{
	unsigned int i;
	bool seen_charset;

	if (!default_charset && params_count == 0) {
		str_append(str, "NIL");
		return;
	}
	str_append_c(str, '(');

	seen_charset = FALSE;
	for (i = 0; i < params_count; i++) {
		i_assert(params[i].name != NULL);
		i_assert(params[i].value != NULL);

		if (i > 0)
			str_append_c(str, ' ');
		if (default_charset &&
			strcasecmp(params[i].name, "charset") == 0)
			seen_charset = TRUE;
		imap_append_string(str, params[i].name, qflags);
		str_append_c(str, ' ');
		imap_append_string(str, params[i].value, qflags);
	}
	if (default_charset && !seen_charset) {
		if (i > 0)
			str_append_c(str, ' ');
		str_append(str, "\"charset\" "
			"\""MESSAGE_PART_DEFAULT_CHARSET"\"");
	}
	str_append_c(str, ')');
}

static int
part_write_bodystructure_siblings(const struct message_part *part,
				  string_t *dest, bool extended,
				  enum imap_quote_flags qflags,
				  const char **error_r)
{
	for (; part != NULL; part = part->next) {
		str_append_c(dest, '(');
		if (imap_bodystructure_write(part, dest, extended, qflags,
					     error_r) < 0)
			return -1;
		str_append_c(dest, ')');
	}
	return 0;
}

static void
part_write_bodystructure_common(const struct message_part_data *data,
				string_t *str, enum imap_quote_flags qflags)
{
	str_append_c(str, ' ');
	if (data->content_disposition == NULL)
		str_append(str, "NIL");
	else {
		str_append_c(str, '(');
		imap_append_string(str, data->content_disposition, qflags);

		str_append_c(str, ' ');
		params_write(data->content_disposition_params,
			data->content_disposition_params_count, str,
			FALSE, qflags);

		str_append_c(str, ')');
	}

	str_append_c(str, ' ');
	if (data->content_language == NULL)
		str_append(str, "NIL");
	else {
		const char *const *lang = data->content_language;

		i_assert(*lang != NULL);
		str_append_c(str, '(');
		imap_append_string(str, *lang, qflags);
		lang++;
		while (*lang != NULL) {
			str_append_c(str, ' ');
			imap_append_string(str, *lang, qflags);
			lang++;
		}
		str_append_c(str, ')');
	}

	str_append_c(str, ' ');
	imap_append_nstring_nolf(str, data->content_location, qflags);
}

static int part_write_body_multipart(const struct message_part *part,
				     string_t *str, bool extended,
				     enum imap_quote_flags qflags,
				     const char **error_r)
{
	const struct message_part_data *data = part->data;

	i_assert(part->data != NULL);

	if (part->children != NULL) {
		if (part_write_bodystructure_siblings(part->children, str,
						      extended, qflags,
						      error_r) < 0)
			return -1;
	} else {
		/* no parts in multipart message,
		   that's not allowed. write a single
		   0-length text/plain structure */
		if (!extended)
			str_append(str, EMPTY_BODY);
		else
			str_append(str, EMPTY_BODYSTRUCTURE);
	}

	str_append_c(str, ' ');
	imap_append_string(str, data->content_subtype, qflags);

	if (!extended)
		return 0;

	/* BODYSTRUCTURE data */

	str_append_c(str, ' ');
	params_write(data->content_type_params,
		data->content_type_params_count, str, FALSE, qflags);

	part_write_bodystructure_common(data, str, qflags);
	return 0;
}

static bool part_is_truncated(const struct message_part *part)
{
	const struct message_part_data *data = part->data;

	i_assert((part->flags & MESSAGE_PART_FLAG_MESSAGE_RFC822) == 0);
	i_assert((part->flags & MESSAGE_PART_FLAG_MULTIPART) == 0);

	if (data->content_type != NULL) {
		if (strcasecmp(data->content_type, "message") == 0 &&
		    strcasecmp(data->content_subtype, "rfc822") == 0) {
			/* It's message/rfc822, but without
			   MESSAGE_PART_FLAG_MESSAGE_RFC822. */
			return TRUE;
		}
		if (strcasecmp(data->content_type, "multipart") == 0) {
			/* It's multipart/, but without
			   MESSAGE_PART_FLAG_MULTIPART. */
			return TRUE;
		}
	} else {
		/* No Content-Type */
		if (part->parent != NULL &&
		    (part->parent->flags & MESSAGE_PART_FLAG_MULTIPART_DIGEST) != 0) {
			/* Parent is MESSAGE_PART_FLAG_MULTIPART_DIGEST
			   (so this should have been message/rfc822). */
			return TRUE;
		}
	}
	return FALSE;
}

static int
part_write_body(const struct message_part *part, string_t *str,
		bool extended, enum imap_quote_flags qflags,
		const char **error_r)
{
	const struct message_part_data *data = part->data;
	bool text;

	i_assert(part->data != NULL);

	if ((part->flags & MESSAGE_PART_FLAG_MESSAGE_RFC822) != 0) {
		str_append(str, "\"message\" \"rfc822\"");
		text = FALSE;
	} else if (part_is_truncated(part)) {
		/* Maximum MIME part count was reached while parsing the mail.
		   Write this part out as application/octet-stream instead.
		   We're not using text/plain, because it would require
		   message-parser to use MESSAGE_PART_FLAG_TEXT for this part
		   to avoid losing line count in message_part serialization. */
		str_append(str, "\"application\" \"octet-stream\"");
		text = FALSE;
	} else {
		/* "content type" "subtype" */
		if (data->content_type == NULL) {
			text = TRUE;
			str_append(str, "\"text\" \"plain\"");
		} else {
			text = (strcasecmp(data->content_type, "text") == 0);
			imap_append_string(str, data->content_type, qflags);
			str_append_c(str, ' ');
			imap_append_string(str, data->content_subtype, qflags);
		}
		bool part_is_text = (part->flags & MESSAGE_PART_FLAG_TEXT) != 0;
		if (text != part_is_text) {
			*error_r = "text flag mismatch";
			return -1;
		}
	}

	/* ("content type param key" "value" ...) */
	str_append_c(str, ' ');
	params_write(data->content_type_params,
		data->content_type_params_count, str, text, qflags);

	str_append_c(str, ' ');
	imap_append_nstring_nolf(str, data->content_id, qflags);
	str_append_c(str, ' ');
	imap_append_nstring_nolf(str, data->content_description, qflags);
	str_append_c(str, ' ');
	if (data->content_transfer_encoding != NULL)
		imap_append_string(str, data->content_transfer_encoding, qflags);
	else
		str_append(str, "\"7bit\"");
	str_printfa(str, " %"PRIuUOFF_T, part->body_size.virtual_size);

	if (text) {
		/* text/.. contains line count */
		str_printfa(str, " %u", part->body_size.lines);
	} else if ((part->flags & MESSAGE_PART_FLAG_MESSAGE_RFC822) != 0) {
		/* message/rfc822 contains envelope + body + line count */
		const struct message_part_data *child_data;

		i_assert(part->children != NULL);
		i_assert(part->children->next == NULL);

		child_data = part->children->data;

		str_append(str, " (");
		imap_envelope_write(child_data->envelope, str, qflags);
		str_append(str, ") ");

		if (part_write_bodystructure_siblings(part->children, str,
						      extended, qflags,
						      error_r) < 0)
			return -1;
		str_printfa(str, " %u", part->body_size.lines);
	}

	if (!extended)
		return 0;

	/* BODYSTRUCTURE data */

	/* "md5" ("content disposition" ("disposition" "params"))
	   ("body" "language" "params") "location" */
	str_append_c(str, ' ');
	imap_append_nstring_nolf(str, data->content_md5, qflags);
	part_write_bodystructure_common(data, str, qflags);
	return 0;
}

int imap_bodystructure_write(const struct message_part *part,
			     string_t *dest, bool extended,
			     enum imap_quote_flags qflags,
			     const char **error_r)
{
	if ((part->flags & MESSAGE_PART_FLAG_MULTIPART) != 0) {
		return part_write_body_multipart(part, dest, extended, qflags,
						 error_r);
	} else {
		return part_write_body(part, dest, extended, qflags, error_r);
	}
}

/*
 * IMAP BODYSTRUCTURE parsing
 */

static int
imap_bodystructure_strlist_parse(const struct imap_arg *arg,
	pool_t pool, const char *const **list_r)
{
	const char *item, **list;
	const struct imap_arg *list_args;
	unsigned int list_count, i;

	if (arg->type == IMAP_ARG_NIL) {
		*list_r = NULL;
		return 0;
	}
	if (imap_arg_get_nstring(arg, &item)) {
		list = p_new(pool, const char *, 2);
		list[0] = p_strdup(pool, item);
	} else {
		if (!imap_arg_get_list_full(arg, &list_args, &list_count))
			return -1;
		if (list_count == 0)
			return -1;

		list = p_new(pool, const char *, list_count+1);
		for (i = 0; i < list_count; i++) {
			if (!imap_arg_get_string(&list_args[i], &item))
				return -1;
			list[i] = p_strdup(pool, item);
		}
	}
	*list_r = list;
	return 0;
}

static int
imap_bodystructure_params_parse(const struct imap_arg *arg,
	pool_t pool, const struct message_part_param **params_r,
	unsigned int *count_r)
{
	struct message_part_param *params;
	const struct imap_arg *list_args;
	unsigned int list_count, params_count, i;

	if (arg->type == IMAP_ARG_NIL) {
		*params_r = NULL;
		return 0;
	}
	if (!imap_arg_get_list_full(arg, &list_args, &list_count))
		return -1;
	if ((list_count % 2) != 0)
		return -1;
	if (list_count == 0)
		return -1;

	params_count = list_count/2;
	params = p_new(pool, struct message_part_param, params_count+1);
	for (i = 0; i < params_count; i++) {
		const char *name, *value;

		if (!imap_arg_get_string(&list_args[i*2+0], &name))
			return -1;
		if (!imap_arg_get_string(&list_args[i*2+1], &value))
			return -1;
		params[i].name = p_strdup(pool, name);
		params[i].value = p_strdup(pool, value);
	}
	*params_r = params;
	*count_r = params_count;
	return 0;
}

static int
imap_bodystructure_parse_args_common(struct message_part *part,
				     pool_t pool, const struct imap_arg *args,
				     const char **error_r)
{
	struct message_part_data *data = part->data;
	const struct imap_arg *list_args;

	if (args->type == IMAP_ARG_EOL)
		return 0;
	if (args->type == IMAP_ARG_NIL)
		args++;
	else if (!imap_arg_get_list(args, &list_args)) {
		*error_r = "Invalid content-disposition list";
		return -1;
	} else {
		if (!imap_arg_get_string
			(list_args++, &data->content_disposition)) {
			*error_r = "Invalid content-disposition";
			return -1;
		}
		data->content_disposition = p_strdup(pool, data->content_disposition);
		if (imap_bodystructure_params_parse(list_args, pool,
			&data->content_disposition_params,
			&data->content_disposition_params_count) < 0) {
			*error_r = "Invalid content-disposition params";
			return -1;
		}
		args++;
	}
	if (args->type == IMAP_ARG_EOL)
		return 0;
	if (imap_bodystructure_strlist_parse
		(args++, pool, &data->content_language) < 0) {
		*error_r = "Invalid content-language";
		return -1;
	}
	if (args->type == IMAP_ARG_EOL)
		return 0;
	if (!imap_arg_get_nstring
		(args++, &data->content_location)) {
		*error_r = "Invalid content-location";
		return -1;
	}
	data->content_location = p_strdup(pool, data->content_location);
	return 0;
}

static int
imap_bodystructure_parse_args_int(
	unsigned int nesting, const struct imap_arg *args, pool_t pool,
	struct message_part **_part, const char **error_r)
{
	struct message_part *part = *_part, *child_part;;
	struct message_part **child_part_p;
	struct message_part_data *data;
	const struct imap_arg *list_args;
	const char *value, *content_type, *subtype, *error;
	bool multipart, text, message_rfc822, parsing_tree, has_lines;
	unsigned int lines;
	uoff_t vsize;

	++nesting;
	if (nesting > BODYSTRUCTURE_MAX_PARENTHESIS_NESTING) {
		*error_r = "Parts hierarchy nested too deep";
		return -1;
	}
	if (part != NULL) {
		/* parsing with preexisting message_part tree */
		parsing_tree = FALSE;
	} else {
		/* parsing message_part tree from BODYSTRUCTURE as well */
		part = *_part = p_new(pool, struct message_part, 1);
		parsing_tree = TRUE;
	}
	part->data = data = p_new(pool, struct message_part_data, 1);

	multipart = FALSE;
	if (!parsing_tree) {
		if ((part->flags & MESSAGE_PART_FLAG_MULTIPART) != 0 &&
			part->children == NULL) {
			struct message_part_data dummy_part_data = {
				.content_type = "text",
				.content_subtype = "plain",
				.content_transfer_encoding = "7bit"
			};
			struct message_part dummy_part = {
				.parent = part,
				.data = &dummy_part_data,
				.flags = MESSAGE_PART_FLAG_TEXT
			};
			struct message_part *dummy_partp = &dummy_part;

			/* no parts in multipart message,
			   that's not allowed. expect a single
			   0-length text/plain structure */
			if (args->type != IMAP_ARG_LIST ||
				(args+1)->type == IMAP_ARG_LIST) {
				*error_r = "message_part hierarchy "
					"doesn't match BODYSTRUCTURE";
				return -1;
			}

			list_args = imap_arg_as_list(args);
			if (imap_bodystructure_parse_args_int(
				nesting, list_args, pool, &dummy_partp, error_r) < 0)
				return -1;
			child_part = NULL;

			multipart = TRUE;
			args++;

		} else {
			child_part = part->children;
			while (args->type == IMAP_ARG_LIST) {
				if ((part->flags & MESSAGE_PART_FLAG_MULTIPART) == 0 ||
						child_part == NULL) {
					*error_r = "message_part hierarchy "
						"doesn't match BODYSTRUCTURE";
					return -1;
				}

				list_args = imap_arg_as_list(args);
				if (imap_bodystructure_parse_args_int(
					nesting, list_args, pool, &child_part, error_r) < 0)
					return -1;
				child_part = child_part->next;

				multipart = TRUE;
				args++;
			}
		}
		if (multipart) {
			if (child_part != NULL) {
				*error_r = "message_part hierarchy "
					"doesn't match BODYSTRUCTURE";
				return -1;
			}
		} else if ((part->flags & MESSAGE_PART_FLAG_MULTIPART) != 0) {
			*error_r = "message_part multipart flag "
				"doesn't match BODYSTRUCTURE";
			return -1;
		}
	} else {
		child_part_p = &part->children;
		while (args->type == IMAP_ARG_LIST) {
			list_args = imap_arg_as_list(args);
			if (imap_bodystructure_parse_args_int(
				nesting, list_args, pool, child_part_p, error_r) < 0)
				return -1;
			(*child_part_p)->parent = part;
			child_part_p = &(*child_part_p)->next;

			multipart = TRUE;
			args++;
		}
		if (multipart) {
			part->flags |= MESSAGE_PART_FLAG_MULTIPART;
		}
	}

	if (multipart) {
		data->content_type = "multipart";
		if (!imap_arg_get_string(args++, &data->content_subtype)) {
			*error_r = "Invalid multipart content-type";
			return -1;
		}
		data->content_subtype = p_strdup(pool, data->content_subtype);
		if (args->type == IMAP_ARG_EOL)
			return 0;
		if (imap_bodystructure_params_parse(args++, pool,
			&data->content_type_params,
			&data->content_type_params_count) < 0) {
			*error_r = "Invalid content params";
			return -1;
		}
		return imap_bodystructure_parse_args_common
			(part, pool, args, error_r);
	}

	/* "content type" "subtype" */
	if (!imap_arg_get_string(&args[0], &content_type) ||
	    !imap_arg_get_string(&args[1], &subtype)) {
		*error_r = "Invalid content-type";
		return -1;
	}
	data->content_type = p_strdup(pool, content_type);
	data->content_subtype = p_strdup(pool, subtype);
	args += 2;

	text = strcasecmp(content_type, "text") == 0;
	message_rfc822 = strcasecmp(content_type, "message") == 0 &&
		strcasecmp(subtype, "rfc822") == 0;

	if (!parsing_tree) {
#if 0
		/* Disabled for now. Earlier Dovecot versions handled broken
		   Content-Type headers by writing them as "text" "plain" to
		   BODYSTRUCTURE reply, but the message_part didn't have
		   MESSAGE_PART_FLAG_TEXT. */
		if (text != ((part->flags & MESSAGE_PART_FLAG_TEXT) != 0)) {
			*error_r = "message_part text flag "
				"doesn't match BODYSTRUCTURE";
			return -1;
		}
#endif
		if (message_rfc822 !=
			((part->flags & MESSAGE_PART_FLAG_MESSAGE_RFC822) != 0)) {
			*error_r = "message_part message/rfc822 flag "
				"doesn't match BODYSTRUCTURE";
			return -1;
		}
	} else {
		if (text)
			part->flags |= MESSAGE_PART_FLAG_TEXT;
		if (message_rfc822)
			part->flags |= MESSAGE_PART_FLAG_MESSAGE_RFC822;
	}

	/* ("content type param key" "value" ...) | NIL */
	if (imap_bodystructure_params_parse(args++, pool,
		&data->content_type_params,
		&data->content_type_params_count) < 0) {
		*error_r = "Invalid content params";
		return -1;
	}

	/* "content id" "content description" "transfer encoding" size */
	if (!imap_arg_get_nstring(args++, &data->content_id)) {
		*error_r = "Invalid content-id";
		return -1;
	}
	if (!imap_arg_get_nstring(args++, &data->content_description)) {
		*error_r = "Invalid content-description";
		return -1;
	}
	if (!imap_arg_get_string(args++, &data->content_transfer_encoding)) {
		*error_r = "Invalid content-transfer-encoding";
		return -1;
	}
	data->content_id = p_strdup(pool, data->content_id);
	data->content_description = p_strdup(pool, data->content_description);
	data->content_transfer_encoding =
		p_strdup(pool, data->content_transfer_encoding);
	if (!imap_arg_get_atom(args++, &value) ||
	    str_to_uoff(value, &vsize) < 0) {
		*error_r = "Invalid size field";
		return -1;
	}
	if (!parsing_tree) {
		if (vsize != part->body_size.virtual_size) {
			*error_r = "message_part virtual_size doesn't match "
				"size in BODYSTRUCTURE";
			return -1;
		}
	} else {
		part->body_size.virtual_size = vsize;
	}

	if (text) {
		/* text/xxx - text lines */
		if (!imap_arg_get_atom(args++, &value) ||
		    str_to_uint(value, &lines) < 0) {
			*error_r = "Invalid lines field";
			return -1;
		}
		i_assert(part->children == NULL);
		has_lines = TRUE;
	} else if (message_rfc822) {
		/* message/rfc822 - envelope + bodystructure + text lines */

		if (!parsing_tree) {
			i_assert(part->children != NULL &&
				 part->children->next == NULL);
		}

		if (!imap_arg_get_list(&args[1], &list_args)) {
			*error_r = "Child bodystructure list expected";
			return -1;
		}
		if (imap_bodystructure_parse_args_int(
			nesting, list_args, pool, &part->children, error_r) < 0)
			return -1;
		if (parsing_tree) {
			i_assert(part->children != NULL &&
				 part->children->next == NULL);
			part->children->parent = part;
		}

		if (!imap_arg_get_list(&args[0], &list_args)) {
			*error_r = "Envelope list expected";
			return -1;
		}
		if (!imap_envelope_parse_args(list_args, pool,
			&part->children->data->envelope, &error)) {
			*error_r = t_strdup_printf
				("Invalid envelope list: %s", error);
			return -1;
		}
		args += 2;
		if (!imap_arg_get_atom(args++, &value) ||
		    str_to_uint(value, &lines) < 0) {
			*error_r = "Invalid lines field";
			return -1;
		}
		has_lines = TRUE;
	} else {
		i_assert(part->children == NULL);
		lines = 0;
		has_lines = FALSE;
	}
	if (!parsing_tree) {
		if (has_lines && lines != part->body_size.lines) {
			*error_r = "message_part lines "
				"doesn't match lines in BODYSTRUCTURE";
			return -1;
		}
	} else {
		part->body_size.lines = lines;
	}
	if (args->type == IMAP_ARG_EOL)
		return 0;
	if (!imap_arg_get_nstring(args++, &data->content_md5)) {
		*error_r = "Invalid content-md5";
		return -1;
	}
	data->content_md5 = p_strdup(pool, data->content_md5);
	return imap_bodystructure_parse_args_common
		(part, pool, args, error_r);
}

int
imap_bodystructure_parse_args(const struct imap_arg *args, pool_t pool,
			      struct message_part **_part,
			      const char **error_r)
{
	return imap_bodystructure_parse_args_int(0, args, pool, _part, error_r);
}

static void imap_bodystructure_reset_data(struct message_part *parts)
{
	for (; parts != NULL; parts = parts->next) {
		parts->data = NULL;
		imap_bodystructure_reset_data(parts->children);
	}
}

int imap_bodystructure_parse_full(const char *bodystructure,
	pool_t pool, struct message_part **parts,
	const char **error_r)
{
	struct istream *input;
	struct imap_parser *parser;
	const struct imap_arg *args;
	int ret;

	i_assert(*parts == NULL || (*parts)->next == NULL);

	input = i_stream_create_from_data(bodystructure, strlen(bodystructure));
	(void)i_stream_read(input);

	parser = imap_parser_create(input, NULL, SIZE_MAX, NULL);
	ret = imap_parser_finish_line(parser, 0,
				      IMAP_PARSE_FLAG_LITERAL_TYPE, &args);
	if (ret < 0) {
		*error_r = t_strdup_printf("IMAP parser failed: %s",
					   imap_parser_get_error(parser, NULL));
	} else if (ret == 0) {
		*error_r = "Empty bodystructure";
		ret = -1;
	} else {
		T_BEGIN {
			ret = imap_bodystructure_parse_args
				(args, pool, parts, error_r);
		} T_END_PASS_STR_IF(ret < 0, error_r);
	}

	if (ret < 0) {
		/* Don't leave partially filled data to message_parts. Some of
		   the code expects that if the first message_part->data is
		   filled, they all are. */
		imap_bodystructure_reset_data(*parts);
	}

	imap_parser_unref(&parser);
	i_stream_destroy(&input);
	return ret;
}

int imap_bodystructure_parse(const char *bodystructure,
	pool_t pool, struct message_part *parts,
	const char **error_r)
{
	i_assert(parts != NULL);

	return imap_bodystructure_parse_full(bodystructure,
		pool, &parts, error_r);
}

/*
 * IMAP BODYSTRUCTURE to BODY conversion
 */

static bool str_append_nstring(string_t *str, const struct imap_arg *arg)
{
	const char *cstr;

	if (!imap_arg_get_nstring(arg, &cstr))
		return FALSE;

	switch (arg->type) {
	case IMAP_ARG_NIL:
		str_append(str, "NIL");
		break;
	case IMAP_ARG_STRING:
		str_append_c(str, '"');
		/* NOTE: we're parsing with no-unescape flag,
		   so don't double-escape it here */
		str_append(str, cstr);
		str_append_c(str, '"');
		break;
	case IMAP_ARG_LITERAL: {
		str_printfa(str, "{%zu}\r\n", strlen(cstr));
		str_append(str, cstr);
		break;
	}
	default:
		i_unreached();
		return FALSE;
	}
	return TRUE;
}

static void
imap_write_envelope_list(const struct imap_arg *args, string_t *str,
	bool toplevel)
{
	const struct imap_arg *children;

	/* don't do any typechecking, just write it out */
	while (!IMAP_ARG_IS_EOL(args)) {
		bool list = FALSE;

		if (!str_append_nstring(str, args)) {
			if (!imap_arg_get_list(args, &children)) {
				/* everything is either nstring or list */
				i_unreached();
			}

			str_append_c(str, '(');
			imap_write_envelope_list(children, str, FALSE);
			str_append_c(str, ')');

			list = TRUE;
		}
		args++;

		if ((toplevel || !list) && !IMAP_ARG_IS_EOL(args))
			str_append_c(str, ' ');
	}
}

static void
imap_write_envelope(const struct imap_arg *args, string_t *str)
{
	imap_write_envelope_list(args, str, TRUE);
}

static int imap_parse_bodystructure_args(const struct imap_arg *args,
					 string_t *str, const char **error_r)
{
	const struct imap_arg *subargs;
	const struct imap_arg *list_args;
	const char *value, *content_type, *subtype;
	bool multipart, text, message_rfc822;
	int i;

	multipart = FALSE;
	while (args->type == IMAP_ARG_LIST) {
		str_append_c(str, '(');
		list_args = imap_arg_as_list(args);
		if (imap_parse_bodystructure_args(list_args, str, error_r) < 0)
			return -1;
		str_append_c(str, ')');

		multipart = TRUE;
		args++;
	}

	if (multipart) {
		/* next is subtype of Content-Type. rest is skipped. */
		str_append_c(str, ' ');
		if (!str_append_nstring(str, args)) {
			*error_r = "Invalid multipart content-type";
			return -1;
		}
		return 0;
	}

	/* "content type" "subtype" */
	if (!imap_arg_get_string(&args[0], &content_type) ||
	    !imap_arg_get_string(&args[1], &subtype)) {
		*error_r = "Invalid content-type";
		return -1;
	}

	if (!str_append_nstring(str, &args[0]))
		i_unreached();
	str_append_c(str, ' ');
	if (!str_append_nstring(str, &args[1]))
		i_unreached();

	text = strcasecmp(content_type, "text") == 0;
	message_rfc822 = strcasecmp(content_type, "message") == 0 &&
		strcasecmp(subtype, "rfc822") == 0;

	args += 2;

	/* ("content type param key" "value" ...) | NIL */
	if (imap_arg_get_list(args, &subargs)) {
		str_append(str, " (");
		while (!IMAP_ARG_IS_EOL(subargs)) {
			if (!str_append_nstring(str, &subargs[0])) {
				*error_r = "Invalid content param key";
				return -1;
			}
			str_append_c(str, ' ');
			if (!str_append_nstring(str, &subargs[1])) {
				*error_r = "Invalid content param value";
				return -1;
			}

			subargs += 2;
			if (IMAP_ARG_IS_EOL(subargs))
				break;
			str_append_c(str, ' ');
		}
		str_append(str, ")");
	} else if (args->type == IMAP_ARG_NIL) {
		str_append(str, " NIL");
	} else {
		*error_r = "list/NIL expected";
		return -1;
	}
	args++;

	/* "content id" "content description" "transfer encoding" size */
	for (i = 0; i < 3; i++, args++) {
		str_append_c(str, ' ');

		if (!str_append_nstring(str, args)) {
			*error_r = "nstring expected";
			return -1;
		}
	}
	if (!imap_arg_get_atom(args, &value)) {
		*error_r = "atom expected for size";
		return -1;
	}
	str_printfa(str, " %s", value);
	args++;

	if (text) {
		/* text/xxx - text lines */
		if (!imap_arg_get_atom(args, &value)) {
			*error_r = "Text lines expected";
			return -1;
		}

		str_append_c(str, ' ');
		str_append(str, value);
	} else if (message_rfc822) {
		/* message/rfc822 - envelope + bodystructure + text lines */
		str_append_c(str, ' ');

		if (!imap_arg_get_list(&args[0], &list_args)) {
			*error_r = "Envelope list expected";
			return -1;
		}
		str_append_c(str, '(');
		imap_write_envelope(list_args, str);
		str_append(str, ") (");

		if (!imap_arg_get_list(&args[1], &list_args)) {
			*error_r = "Child bodystructure list expected";
			return -1;
		}
		if (imap_parse_bodystructure_args(list_args, str, error_r) < 0)
			return -1;

		str_append(str, ") ");
		if (!imap_arg_get_atom(&args[2], &value)) {
			*error_r = "Text lines expected";
			return -1;
		}
		str_append(str, value);
	}
	return 0;
}

int imap_body_parse_from_bodystructure(const char *bodystructure,
				       string_t *dest, const char **error_r)
{
	struct istream *input;
	struct imap_parser *parser;
	const struct imap_arg *args;
	int ret;

	input = i_stream_create_from_data(bodystructure, strlen(bodystructure));
	(void)i_stream_read(input);

	parser = imap_parser_create(input, NULL, SIZE_MAX, NULL);
	ret = imap_parser_finish_line(parser, 0, IMAP_PARSE_FLAG_NO_UNESCAPE |
				      IMAP_PARSE_FLAG_LITERAL_TYPE, &args);
	if (ret < 0) {
		*error_r = t_strdup_printf("IMAP parser failed: %s",
					   imap_parser_get_error(parser, NULL));
	} else if (ret == 0) {
		*error_r = "Empty bodystructure";
		ret = -1;
	} else {
		ret = imap_parse_bodystructure_args(args, dest, error_r);
	}

	imap_parser_unref(&parser);
	i_stream_destroy(&input);
	return ret;
}
