From: Timo Sirainen Date: Sat, 2 Nov 2013 19:29:39 +0000 (+0200) Subject: imap: Added initial support for METADATA extension. X-Git-Tag: 2.2.7~13 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=02c75e04c6ff80726bb59e3ea34a7995ad1f6f7c;p=thirdparty%2Fdovecot%2Fcore.git imap: Added initial support for METADATA extension. For now this is enabled only when imap_metadata=yes setting is used. The setting will go away once the feature is complete. Also mail_attribute_dict must be set. TODO: - Metadata doesn't work for public namespaces. There should probably be a mail_attribute_public_dict setting for that. - There isn't any kind of quota or other limits - After ENABLE METADATA start sending untagged METADATA entries to clients - /shared/admin should probably return postmaster_address URL - Check if we handle ACLs correctly - RFC says that it SHOULD be possible to set METADATA entries to \NoSelect mailboxes. We probably will never allow this though. --- diff --git a/src/imap/Makefile.am b/src/imap/Makefile.am index 3beff62362..a86c8f15ca 100644 --- a/src/imap/Makefile.am +++ b/src/imap/Makefile.am @@ -39,6 +39,7 @@ cmds = \ cmd-expunge.c \ cmd-fetch.c \ cmd-genurlauth.c \ + cmd-getmetadata.c \ cmd-id.c \ cmd-idle.c \ cmd-list.c \ @@ -51,6 +52,7 @@ cmds = \ cmd-resetkey.c \ cmd-search.c \ cmd-select.c \ + cmd-setmetadata.c \ cmd-sort.c \ cmd-status.c \ cmd-store.c \ @@ -70,6 +72,7 @@ imap_SOURCES = \ imap-fetch.c \ imap-fetch-body.c \ imap-list.c \ + imap-metadata.c \ imap-notify.c \ imap-search.c \ imap-search-args.c \ @@ -87,6 +90,7 @@ headers = \ imap-expunge.h \ imap-fetch.h \ imap-list.h \ + imap-metadata.h \ imap-notify.h \ imap-search.h \ imap-search-args.h \ diff --git a/src/imap/cmd-getmetadata.c b/src/imap/cmd-getmetadata.c new file mode 100644 index 0000000000..2b16d8a611 --- /dev/null +++ b/src/imap/cmd-getmetadata.c @@ -0,0 +1,392 @@ +/* Copyright (c) 2013 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "str.h" +#include "istream.h" +#include "ostream.h" +#include "imap-quote.h" +#include "imap-metadata.h" + +struct imap_getmetadata_context { + struct client_command_context *cmd; + + struct mailbox *box; + struct mailbox_transaction_context *trans; + + ARRAY_TYPE(const_string) entries; + uint32_t maxsize; + uoff_t largest_seen_size; + unsigned int depth; + + struct istream *cur_stream; + uoff_t cur_stream_offset, cur_stream_size; + + struct mailbox_attribute_iter *iter; + string_t *iter_entry_prefix; + + const char *key_prefix; + unsigned int entry_idx; + bool first_entry_sent; + bool failed; +}; + +static bool +cmd_getmetadata_parse_options(struct imap_getmetadata_context *ctx, + const struct imap_arg *options) +{ + const char *value; + + while (!IMAP_ARG_IS_EOL(options)) { + if (imap_arg_atom_equals(options, "MAXSIZE")) { + options++; + if (!imap_arg_get_atom(options, &value) || + str_to_uint32(value, &ctx->maxsize) < 0) { + client_send_command_error(ctx->cmd, + "Invalid value for MAXSIZE option"); + return FALSE; + } + } else if (imap_arg_atom_equals(options, "DEPTH")) { + options++; + if (!imap_arg_get_atom(options, &value)) { + client_send_command_error(ctx->cmd, + "Invalid value for DEPTH option"); + return FALSE; + } + if (strcmp(value, "0") == 0) + ctx->depth = 0; + else if (strcmp(value, "1") == 0) + ctx->depth = 1; + else if (strcmp(value, "infinity") == 0) + ctx->depth = UINT_MAX; + else { + client_send_command_error(ctx->cmd, + "Invalid value for DEPTH option"); + return FALSE; + } + } else { + client_send_command_error(ctx->cmd, "Unknown option"); + return FALSE; + } + options++; + } + return TRUE; +} + +static bool +imap_metadata_parse_entry_names(struct imap_getmetadata_context *ctx, + const struct imap_arg *entries) +{ + const char *value; + + p_array_init(&ctx->entries, ctx->cmd->pool, 4); + for (; !IMAP_ARG_IS_EOL(entries); entries++) { + if (!imap_arg_get_astring(entries, &value)) { + client_send_command_error(ctx->cmd, "Entry isn't astring"); + return FALSE; + } + if (!imap_metadata_verify_entry_name(ctx->cmd, value)) + return FALSE; + + /* names are case-insensitive so we'll always lowercase them */ + value = p_strdup(ctx->cmd->pool, t_str_lcase(value)); + array_append(&ctx->entries, &value, 1); + } + return TRUE; +} + +static void cmd_getmetadata_send_entry(struct imap_getmetadata_context *ctx, + const char *entry) +{ + enum mail_attribute_type type; + struct mail_attribute_value value; + enum mail_error error; + uoff_t value_len; + const char *key; + string_t *str; + + imap_metadata_entry2key(entry, ctx->key_prefix, &type, &key); + if (ctx->key_prefix == NULL && + strncmp(key, MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT, + strlen(MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT)) == 0) { + /* skip over dovecot's internal attributes. (if key_prefix + isn't NULL, we're getting server metadata, which is handled + inside the private metadata.) */ + return; + } + + if (mailbox_attribute_get_stream(ctx->trans, type, key, &value) < 0) { + (void)mailbox_get_last_error(ctx->box, &error); + if (error != MAIL_ERROR_NOTFOUND && error != MAIL_ERROR_PERM) { + client_send_untagged_storage_error(ctx->cmd->client, + mailbox_get_storage(ctx->box)); + ctx->failed = TRUE; + } + } + if (value.value != NULL) + value_len = strlen(value.value); + else if (value.value_stream != NULL) { + if (i_stream_get_size(value.value_stream, TRUE, &value_len) < 0) { + i_error("GETMETADATA %s: i_stream_get_size(%s) failed: %s", entry, + i_stream_get_name(value.value_stream), + i_stream_get_error(value.value_stream)); + i_stream_unref(&value.value_stream); + ctx->failed = TRUE; + return; + } + } else { + /* skip nonexistent entries */ + return; + } + + if (value_len > ctx->maxsize) { + /* value length is larger than specified MAXSIZE, + skip this entry */ + if (ctx->largest_seen_size < value_len) + ctx->largest_seen_size = value_len; + if (value.value_stream != NULL) + i_stream_unref(&value.value_stream); + return; + } + + str = t_str_new(64); + if (!ctx->first_entry_sent) { + ctx->first_entry_sent = TRUE; + str_append(str, "* METADATA "); + imap_append_astring(str, mailbox_get_vname(ctx->box)); + str_append(str, " ("); + + /* nothing can be sent until untagged METADATA is finished */ + ctx->cmd->client->output_cmd_lock = ctx->cmd; + } else { + str_append_c(str, ' '); + } + imap_append_astring(str, entry); + if (value.value != NULL) { + str_printfa(str, " {%"PRIuUOFF_T"}\r\n%s", value_len, value.value); + o_stream_send(ctx->cmd->client->output, str_data(str), str_len(str)); + } else { + str_printfa(str, " ~{%"PRIuUOFF_T"}\r\n", value_len); + o_stream_send(ctx->cmd->client->output, str_data(str), str_len(str)); + + ctx->cur_stream_offset = 0; + ctx->cur_stream_size = value_len; + ctx->cur_stream = value.value_stream; + } +} + +static bool +cmd_getmetadata_stream_continue(struct imap_getmetadata_context *ctx) +{ + off_t ret; + + o_stream_set_max_buffer_size(ctx->cmd->client->output, 0); + ret = o_stream_send_istream(ctx->cmd->client->output, ctx->cur_stream); + o_stream_set_max_buffer_size(ctx->cmd->client->output, (size_t)-1); + + if (ret > 0) + ctx->cur_stream_offset += ret; + + if (ctx->cur_stream_offset == ctx->cur_stream_size) { + /* finished */ + return TRUE; + } + if (ctx->cur_stream->stream_errno != 0) { + i_error("read(%s) failed: %s", + i_stream_get_name(ctx->cur_stream), + i_stream_get_error(ctx->cur_stream)); + client_disconnect(ctx->cmd->client, + "Internal GETMETADATA failure"); + return -1; + } + if (!i_stream_have_bytes_left(ctx->cur_stream)) { + /* Input stream gave less data than expected */ + i_error("read(%s): GETMETADATA stream had less data than expected", + i_stream_get_name(ctx->cur_stream)); + client_disconnect(ctx->cmd->client, + "Internal GETMETADATA failure"); + return -1; + } + o_stream_set_flush_pending(ctx->cmd->client->output, TRUE); + return FALSE; +} + +static int cmd_getmetadata_send_entry_tree(struct imap_getmetadata_context *ctx, + const char *entry) +{ + const char *key; + enum mail_attribute_type type; + + if (o_stream_get_buffer_used_size(ctx->cmd->client->output) >= + CLIENT_OUTPUT_OPTIMAL_SIZE) { + if (o_stream_flush(ctx->cmd->client->output) <= 0) { + o_stream_set_flush_pending(ctx->cmd->client->output, TRUE); + return 0; + } + } + + if (ctx->iter != NULL) { + /* DEPTH iteration */ + do { + key = mailbox_attribute_iter_next(ctx->iter); + if (key == NULL) { + /* iteration finished, get to the next entry */ + if (mailbox_attribute_iter_deinit(&ctx->iter) < 0) { + client_send_untagged_storage_error(ctx->cmd->client, + mailbox_get_storage(ctx->box)); + ctx->failed = TRUE; + } + return -1; + } + } while (ctx->depth == 1 && strchr(key, '/') != NULL); + entry = t_strconcat(str_c(ctx->iter_entry_prefix), key, NULL); + } + cmd_getmetadata_send_entry(ctx, entry); + + if (ctx->cur_stream != NULL) { + if (!cmd_getmetadata_stream_continue(ctx)) + return 0; + i_stream_unref(&ctx->cur_stream); + } + + if (ctx->iter != NULL) { + /* already iterating the entry */ + return 1; + } else if (ctx->depth == 0) { + /* no iteration for the entry */ + return -1; + } else { + /* we just sent the entry root. iterate its children. */ + str_truncate(ctx->iter_entry_prefix, 0); + str_append(ctx->iter_entry_prefix, entry); + str_append_c(ctx->iter_entry_prefix, '/'); + + imap_metadata_entry2key(entry, ctx->key_prefix, &type, &key); + type = type; + ctx->iter = mailbox_attribute_iter_init(ctx->box, type, + key[0] == '\0' ? "" : t_strconcat(key, "/", NULL)); + return 1; + } +} + +static void cmd_getmetadata_deinit(struct imap_getmetadata_context *ctx) +{ + ctx->cmd->client->output_cmd_lock = NULL; + + if (ctx->iter != NULL) + (void)mailbox_attribute_iter_deinit(&ctx->iter); + (void)mailbox_transaction_commit(&ctx->trans); + mailbox_free(&ctx->box); +} + +static bool cmd_getmetadata_continue(struct client_command_context *cmd) +{ + struct imap_getmetadata_context *ctx = cmd->context; + const char *const *entries; + unsigned int count; + int ret; + + if (cmd->cancel) { + cmd_getmetadata_deinit(ctx); + return TRUE; + } + + if (ctx->cur_stream != NULL) { + if (!cmd_getmetadata_stream_continue(ctx)) + return FALSE; + i_stream_unref(&ctx->cur_stream); + } + + entries = array_get(&ctx->entries, &count); + for (; ctx->entry_idx < count; ctx->entry_idx++) { + do { + T_BEGIN { + ret = cmd_getmetadata_send_entry_tree(ctx, entries[ctx->entry_idx]); + } T_END; + if (ret == 0) + return FALSE; + } while (ret > 0); + } + if (ctx->first_entry_sent) + o_stream_nsend_str(cmd->client->output, ")\r\n"); + + if (ctx->failed) { + client_send_tagline(cmd, "NO Getmetadata failed to send some entries"); + } else if (ctx->largest_seen_size != 0) { + client_send_tagline(cmd, t_strdup_printf( + "OK [METADATA LONGENTRIES %"PRIuUOFF_T"] " + "Getmetadata completed.", ctx->largest_seen_size)); + } else { + client_send_tagline(cmd, "OK Getmetadata completed."); + } + cmd_getmetadata_deinit(ctx); + return TRUE; +} + +bool cmd_getmetadata(struct client_command_context *cmd) +{ + struct imap_getmetadata_context *ctx; + struct mail_namespace *ns; + const struct imap_arg *args, *options, *entries; + const char *mailbox; + + if (!client_read_args(cmd, 0, 0, &args)) + return FALSE; + + if (!cmd->client->imap_metadata_enabled) { + client_send_command_error(cmd, "METADATA disabled."); + return TRUE; + } + + ctx = p_new(cmd->pool, struct imap_getmetadata_context, 1); + ctx->cmd = cmd; + ctx->maxsize = (uint32_t)-1; + ctx->cmd->context = ctx; + + if (imap_arg_get_list(&args[0], &options)) { + if (!cmd_getmetadata_parse_options(ctx, options)) + return TRUE; + } + if (!imap_arg_get_astring(&args[1], &mailbox)) { + client_send_command_error(cmd, "Invalid arguments."); + return TRUE; + } + if (!imap_arg_get_list(&args[2], &entries)) { + if (!IMAP_ARG_IS_ASTRING(&args[2]) || + !IMAP_ARG_IS_EOL(&args[3])) { + client_send_command_error(cmd, "Invalid arguments."); + return TRUE; + } + entries = args+2; + } + if (!imap_metadata_parse_entry_names(ctx, entries)) + return TRUE; + + if (mailbox[0] == '\0') { + /* server attribute */ + ctx->key_prefix = MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT_SERVER; + ns = mail_namespace_find_inbox(cmd->client->user->namespaces); + mailbox = "INBOX"; + } else { + ns = client_find_namespace(cmd, &mailbox); + if (ns == NULL) + return TRUE; + } + + ctx->box = mailbox_alloc(ns->list, mailbox, MAILBOX_FLAG_READONLY); + if (mailbox_open(ctx->box) < 0) { + client_send_storage_error(cmd, mailbox_get_storage(ctx->box)); + mailbox_free(&ctx->box); + return TRUE; + } + ctx->trans = mailbox_transaction_begin(ctx->box, 0); + + if (ctx->depth > 0) + ctx->iter_entry_prefix = str_new(cmd->pool, 128); + + if (!cmd_getmetadata_continue(cmd)) { + cmd->state = CLIENT_COMMAND_STATE_WAIT_OUTPUT; + cmd->func = cmd_getmetadata_continue; + return FALSE; + } + return TRUE; +} diff --git a/src/imap/cmd-setmetadata.c b/src/imap/cmd-setmetadata.c new file mode 100644 index 0000000000..94cfe3638e --- /dev/null +++ b/src/imap/cmd-setmetadata.c @@ -0,0 +1,321 @@ +/* Copyright (c) 2013 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "ioloop.h" +#include "istream.h" +#include "istream-seekable.h" +#include "ostream.h" +#include "str.h" +#include "imap-metadata.h" + +#define METADATA_MAX_INMEM_SIZE (1024*128) + +struct imap_setmetadata_context { + struct client_command_context *cmd; + struct imap_parser *parser; + + const char *key_prefix; + + struct mailbox *box; + struct mailbox_transaction_context *trans; + + char *entry_name; + uoff_t entry_value_len; + struct istream *input; + bool failed; + bool cmd_error_sent; + bool storage_failure; +}; + +static void cmd_setmetadata_deinit(struct imap_setmetadata_context *ctx) +{ + ctx->cmd->client->input_lock = NULL; + imap_parser_unref(&ctx->parser); + if (ctx->trans != NULL) + mailbox_transaction_rollback(&ctx->trans); + if (ctx->box != ctx->cmd->client->mailbox) + mailbox_free(&ctx->box); + i_free(ctx->entry_name); +} + +static int +cmd_setmetadata_parse_entryvalue(struct imap_setmetadata_context *ctx, + const char **entry_r, + const struct imap_arg **value_r) +{ + const struct imap_arg *args; + const char *name, *error; + int ret; + bool fatal; + + /* parse the entry name */ + ret = imap_parser_read_args(ctx->parser, 1, + IMAP_PARSE_FLAG_INSIDE_LIST, &args); + if (ret >= 0) { + if (ret == 0) { + /* ')' found */ + *entry_r = NULL; + return 1; + } + if (!imap_arg_get_astring(args, &name)) { + client_send_command_error(ctx->cmd, + "Entry name isn't astring"); + return -1; + } + + ret = imap_parser_read_args(ctx->parser, 2, + IMAP_PARSE_FLAG_INSIDE_LIST | + IMAP_PARSE_FLAG_LITERAL_SIZE | + IMAP_PARSE_FLAG_LITERAL8, &args); + } + if (ret < 0) { + if (ret == -2) + return 0; + error = imap_parser_get_error(ctx->parser, &fatal); + if (fatal) { + client_disconnect_with_error(ctx->cmd->client, error); + return -1; + } + client_send_command_error(ctx->cmd, error); + return -1; + } + i_assert(!IMAP_ARG_IS_EOL(&args[1])); + if (args[1].type == IMAP_ARG_LIST) { + client_send_command_error(ctx->cmd, "Entry value can't be a list"); + return -1; + } + if (ctx->cmd_error_sent || + !imap_metadata_verify_entry_name(ctx->cmd, name)) { + ctx->cmd->param_error = FALSE; + ctx->cmd->state = CLIENT_COMMAND_STATE_WAIT_INPUT; + + ctx->cmd_error_sent = TRUE; + ctx->failed = TRUE; + if (args[1].type == IMAP_ARG_LITERAL_SIZE) { + /* client won't see "+ OK", so we can abort + immediately */ + ctx->cmd->client->input_skip_line = FALSE; + return -1; + } + } + + /* entry names are case-insensitive. handle this by using only + lowercase names. */ + *entry_r = t_str_lcase(name); + *value_r = &args[1]; + return 1; +} + +static int +cmd_setmetadata_entry_read_stream(struct imap_setmetadata_context *ctx) +{ + const unsigned char *data; + size_t size; + enum mail_attribute_type type; + const char *key; + struct mail_attribute_value value; + int ret; + + while ((ret = i_stream_read_data(ctx->input, &data, &size, 0)) > 0) + i_stream_skip(ctx->input, size); + if (ctx->input->v_offset == ctx->entry_value_len) { + /* finished reading the value */ + i_stream_seek(ctx->input, 0); + + if (ctx->failed) { + i_stream_unref(&ctx->input); + return 1; + } + + imap_metadata_entry2key(ctx->entry_name, ctx->key_prefix, + &type, &key); + memset(&value, 0, sizeof(value)); + value.value_stream = ctx->input; + if (mailbox_attribute_set(ctx->trans, type, key, &value) < 0) { + /* delay reporting the failure so we'll finish + reading the command input */ + ctx->storage_failure = TRUE; + ctx->failed = TRUE; + } + i_stream_unref(&ctx->input); + return 1; + } + if (ctx->input->eof) { + /* client disconnected */ + return -1; + } + return 0; +} + +static int +cmd_setmetadata_entry(struct imap_setmetadata_context *ctx, + const char *entry_name, + const struct imap_arg *entry_value) +{ + struct istream *inputs[2]; + enum mail_attribute_type type; + const char *key; + struct mail_attribute_value value; + string_t *path; + int ret; + + switch (entry_value->type) { + case IMAP_ARG_NIL: + case IMAP_ARG_ATOM: + case IMAP_ARG_STRING: + /* we have the value already */ + imap_metadata_entry2key(entry_name, ctx->key_prefix, + &type, &key); + if (ctx->failed) + return 1; + memset(&value, 0, sizeof(value)); + value.value = imap_arg_as_nstring(entry_value); + ret = value.value == NULL ? + mailbox_attribute_unset(ctx->trans, type, key) : + mailbox_attribute_set(ctx->trans, type, key, &value); + if (ret < 0) { + /* delay reporting the failure so we'll finish + reading the command input */ + ctx->storage_failure = TRUE; + ctx->failed = TRUE; + } + return 1; + case IMAP_ARG_LITERAL_SIZE: + o_stream_nsend(ctx->cmd->client->output, "+ OK\r\n", 6); + o_stream_nflush(ctx->cmd->client->output); + o_stream_uncork(ctx->cmd->client->output); + o_stream_cork(ctx->cmd->client->output); + /* fall through */ + case IMAP_ARG_LITERAL_SIZE_NONSYNC: + i_free(ctx->entry_name); + ctx->entry_name = i_strdup(entry_name); + ctx->entry_value_len = imap_arg_as_literal_size(entry_value); + + inputs[0] = i_stream_create_limit(ctx->cmd->client->input, + ctx->entry_value_len); + inputs[1] = NULL; + + path = t_str_new(128); + mail_user_set_get_temp_prefix(path, ctx->cmd->client->user->set); + ctx->input = i_stream_create_seekable_path(inputs, + METADATA_MAX_INMEM_SIZE, str_c(path)); + i_stream_set_name(ctx->input, i_stream_get_name(inputs[0])); + i_stream_unref(&inputs[0]); + return cmd_setmetadata_entry_read_stream(ctx); + case IMAP_ARG_LITERAL: + case IMAP_ARG_LIST: + case IMAP_ARG_EOL: + break; + } + i_unreached(); +} + +static bool cmd_setmetadata_continue(struct client_command_context *cmd) +{ + struct imap_setmetadata_context *ctx = cmd->context; + const char *entry; + const struct imap_arg *value; + int ret; + + if (cmd->cancel) { + cmd_setmetadata_deinit(ctx); + return TRUE; + } + + if (ctx->input != NULL) { + if ((ret = cmd_setmetadata_entry_read_stream(ctx)) == 0) + return FALSE; + if (ret < 0) { + cmd_setmetadata_deinit(ctx); + return TRUE; + } + } + + while ((ret = cmd_setmetadata_parse_entryvalue(ctx, &entry, &value)) > 0 && + entry != NULL) { + ret = cmd_setmetadata_entry(ctx, entry, value); + imap_parser_reset(ctx->parser); + if (ret <= 0) + break; + } + if (ret == 0) + return 0; + + if (ret < 0 || ctx->cmd_error_sent) + /* already sent the error to client */ ; + else if (ctx->storage_failure) + client_send_storage_error(cmd, mailbox_get_storage(ctx->box)); + else if (mailbox_transaction_commit(&ctx->trans) < 0) + client_send_storage_error(cmd, mailbox_get_storage(ctx->box)); + else + client_send_tagline(cmd, "OK Setmetadata completed."); + cmd_setmetadata_deinit(ctx); + return TRUE; +} + +bool cmd_setmetadata(struct client_command_context *cmd) +{ + struct imap_setmetadata_context *ctx; + const struct imap_arg *args; + const char *mailbox; + struct mail_namespace *ns; + int ret; + + ret = imap_parser_read_args(cmd->parser, 2, + IMAP_PARSE_FLAG_STOP_AT_LIST, &args); + if (ret == -1) { + client_send_command_error(cmd, NULL); + return TRUE; + } + if (ret == -2) + return FALSE; + if (!imap_arg_get_astring(&args[0], &mailbox) || + args[1].type != IMAP_ARG_LIST) { + client_send_command_error(cmd, "Invalid arguments."); + return TRUE; + } + + if (!cmd->client->imap_metadata_enabled) { + client_send_command_error(cmd, "METADATA disabled."); + return TRUE; + } + + ctx = p_new(cmd->pool, struct imap_setmetadata_context, 1); + ctx->cmd = cmd; + ctx->cmd->context = ctx; + + if (mailbox[0] == '\0') { + /* server attribute */ + ctx->key_prefix = MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT_SERVER; + ns = mail_namespace_find_inbox(cmd->client->user->namespaces); + mailbox = "INBOX"; + } else { + ns = client_find_namespace(cmd, &mailbox); + if (ns == NULL) + return TRUE; + } + + if (cmd->client->mailbox != NULL && !cmd->client->mailbox_examined && + mailbox_equals(cmd->client->mailbox, ns, mailbox)) + ctx->box = cmd->client->mailbox; + else { + ctx->box = mailbox_alloc(ns->list, mailbox, 0); + if (mailbox_open(ctx->box) < 0) { + client_send_storage_error(cmd, + mailbox_get_storage(ctx->box)); + mailbox_free(&ctx->box); + return TRUE; + } + } + ctx->trans = mailbox_transaction_begin(ctx->box, 0); + /* we support large literals, so read the values from client + asynchronously the same way as APPEND does. */ + cmd->client->input_lock = cmd; + ctx->parser = imap_parser_create(cmd->client->input, cmd->client->output, + cmd->client->set->imap_max_line_length); + o_stream_unset_flush_callback(cmd->client->output); + + cmd->func = cmd_setmetadata_continue; + cmd->context = ctx; + return cmd_setmetadata_continue(cmd); +} diff --git a/src/imap/imap-client.c b/src/imap/imap-client.c index fa2df6166b..e870246b4a 100644 --- a/src/imap/imap-client.c +++ b/src/imap/imap-client.c @@ -138,6 +138,11 @@ struct client *client_create(int fd_in, int fd_out, const char *session_id, if (!explicit_capability) str_append(client->capability_string, " URLAUTH URLAUTH=BINARY"); } + if (set->imap_metadata && *mail_set->mail_attribute_dict != '\0' && + !explicit_capability) { + client->imap_metadata_enabled = TRUE; + str_append(client->capability_string, " METADATA"); + } ident = mail_user_get_anvil_userip_ident(client->user); if (ident != NULL) { diff --git a/src/imap/imap-client.h b/src/imap/imap-client.h index fab8ec4764..cab11287bd 100644 --- a/src/imap/imap-client.h +++ b/src/imap/imap-client.h @@ -164,6 +164,7 @@ struct client { unsigned int notify_immediate_expunges:1; unsigned int notify_count_changes:1; unsigned int notify_flag_changes:1; + unsigned int imap_metadata_enabled:1; }; struct imap_module_register { diff --git a/src/imap/imap-commands.c b/src/imap/imap-commands.c index 4800a6e9cb..a86c874b54 100644 --- a/src/imap/imap-commands.c +++ b/src/imap/imap-commands.c @@ -52,6 +52,8 @@ static const struct command imap_ext_commands[] = { { "ID", cmd_id, 0 }, { "IDLE", cmd_idle, COMMAND_FLAG_BREAKS_SEQS | COMMAND_FLAG_REQUIRES_SYNC }, + { "GETMETADATA", cmd_getmetadata, 0 }, + { "SETMETADATA", cmd_setmetadata, 0 }, { "NAMESPACE", cmd_namespace, 0 }, { "NOTIFY", cmd_notify, COMMAND_FLAG_BREAKS_SEQS }, { "SORT", cmd_sort, COMMAND_FLAG_USES_SEQS }, diff --git a/src/imap/imap-commands.h b/src/imap/imap-commands.h index cf1631238d..a3a1350829 100644 --- a/src/imap/imap-commands.h +++ b/src/imap/imap-commands.h @@ -107,6 +107,8 @@ bool cmd_enable(struct client_command_context *cmd); bool cmd_id(struct client_command_context *cmd); bool cmd_idle(struct client_command_context *cmd); bool cmd_namespace(struct client_command_context *cmd); +bool cmd_getmetadata(struct client_command_context *cmd); +bool cmd_setmetadata(struct client_command_context *cmd); bool cmd_notify(struct client_command_context *cmd); bool cmd_sort(struct client_command_context *cmd); bool cmd_thread(struct client_command_context *cmd); diff --git a/src/imap/imap-metadata.c b/src/imap/imap-metadata.c new file mode 100644 index 0000000000..057150383c --- /dev/null +++ b/src/imap/imap-metadata.c @@ -0,0 +1,85 @@ +/* Copyright (c) 2013 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "imap-metadata.h" + +bool imap_metadata_verify_entry_name(struct client_command_context *cmd, + const char *name) +{ + unsigned int i; + bool ok; + + if (name[0] != '/') { + client_send_command_error(cmd, + "Entry name must begin with '/'"); + return FALSE; + } + for (i = 0; name[i] != '\0'; i++) { + switch (name[i]) { + case '/': + if (i > 0 && name[i-1] == '/') { + client_send_command_error(cmd, + "Entry name can't contain consecutive '/'"); + return FALSE; + } + if (name[i+1] == '\0') { + client_send_command_error(cmd, + "Entry name can't end with '/'"); + return FALSE; + } + break; + case '*': + client_send_command_error(cmd, + "Entry name can't contain '*'"); + return FALSE; + case '%': + client_send_command_error(cmd, + "Entry name can't contain '%'"); + return FALSE; + default: + if (name[i] <= 0x19) { + client_send_command_error(cmd, + "Entry name can't contain control chars"); + return FALSE; + } + break; + } + } + T_BEGIN { + const char *prefix, *p = strchr(name+1, '/'); + + prefix = p == NULL ? name : t_strdup_until(name, p); + ok = strcasecmp(prefix, IMAP_METADATA_PRIVATE_PREFIX) == 0 || + strcasecmp(prefix, IMAP_METADATA_SHARED_PREFIX) == 0; + } T_END; + if (!ok) { + client_send_command_error(cmd, + "Entry name must begin with /private or /shared"); + return FALSE; + } + return TRUE; +} + +void imap_metadata_entry2key(const char *entry, const char *key_prefix, + enum mail_attribute_type *type_r, + const char **key_r) +{ + if (strncmp(entry, IMAP_METADATA_PRIVATE_PREFIX, + strlen(IMAP_METADATA_PRIVATE_PREFIX)) == 0) { + *key_r = entry + strlen(IMAP_METADATA_PRIVATE_PREFIX); + *type_r = MAIL_ATTRIBUTE_TYPE_PRIVATE; + } else { + i_assert(strncmp(entry, IMAP_METADATA_SHARED_PREFIX, + strlen(IMAP_METADATA_SHARED_PREFIX)) == 0); + *key_r = entry + strlen(IMAP_METADATA_SHARED_PREFIX); + *type_r = MAIL_ATTRIBUTE_TYPE_SHARED; + } + if ((*key_r)[0] == '\0') { + /* /private or /shared prefix has no value itself */ + } else { + i_assert((*key_r)[0] == '/'); + *key_r += 1; + } + if (key_prefix != NULL) + *key_r = t_strconcat(key_prefix, *key_r, NULL); +} diff --git a/src/imap/imap-metadata.h b/src/imap/imap-metadata.h new file mode 100644 index 0000000000..7864652a45 --- /dev/null +++ b/src/imap/imap-metadata.h @@ -0,0 +1,13 @@ +#ifndef IMAP_METADATA_H +#define IMAP_METADATA_H + +#define IMAP_METADATA_PRIVATE_PREFIX "/private" +#define IMAP_METADATA_SHARED_PREFIX "/shared" + +bool imap_metadata_verify_entry_name(struct client_command_context *cmd, + const char *name); +void imap_metadata_entry2key(const char *entry, const char *key_prefix, + enum mail_attribute_type *type_r, + const char **key_r); + +#endif diff --git a/src/imap/imap-settings.c b/src/imap/imap-settings.c index b72e6b6168..0b4b84c825 100644 --- a/src/imap/imap-settings.c +++ b/src/imap/imap-settings.c @@ -69,6 +69,7 @@ static const struct setting_define imap_setting_defines[] = { DEF(SET_STR, imap_logout_format), DEF(SET_STR, imap_id_send), DEF(SET_STR, imap_id_log), + DEF(SET_BOOL, imap_metadata), DEF(SET_STR, imap_urlauth_host), DEF(SET_UINT, imap_urlauth_port), @@ -89,6 +90,7 @@ static const struct imap_settings imap_default_settings = { .imap_logout_format = "in=%i out=%o", .imap_id_send = "name *", .imap_id_log = "", + .imap_metadata = FALSE, .imap_urlauth_host = "", .imap_urlauth_port = 143 diff --git a/src/imap/imap-settings.h b/src/imap/imap-settings.h index 07cc2f6edf..d2da7e34e3 100644 --- a/src/imap/imap-settings.h +++ b/src/imap/imap-settings.h @@ -22,6 +22,7 @@ struct imap_settings { const char *imap_logout_format; const char *imap_id_send; const char *imap_id_log; + bool imap_metadata; /* imap urlauth: */ const char *imap_urlauth_host;