]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
imap: Added initial support for METADATA extension.
authorTimo Sirainen <tss@iki.fi>
Sat, 2 Nov 2013 19:29:39 +0000 (21:29 +0200)
committerTimo Sirainen <tss@iki.fi>
Sat, 2 Nov 2013 19:29:39 +0000 (21:29 +0200)
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.

src/imap/Makefile.am
src/imap/cmd-getmetadata.c [new file with mode: 0644]
src/imap/cmd-setmetadata.c [new file with mode: 0644]
src/imap/imap-client.c
src/imap/imap-client.h
src/imap/imap-commands.c
src/imap/imap-commands.h
src/imap/imap-metadata.c [new file with mode: 0644]
src/imap/imap-metadata.h [new file with mode: 0644]
src/imap/imap-settings.c
src/imap/imap-settings.h

index 3beff623627ac6cd55e100d48e7e4336f0532a71..a86c8f15ca5344344f82647c8a109d1c1e395bd1 100644 (file)
@@ -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 (file)
index 0000000..2b16d8a
--- /dev/null
@@ -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 (file)
index 0000000..94cfe36
--- /dev/null
@@ -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);
+}
index fa2df6166b212909f8489fcde56331e0995ded06..e870246b4a91460555d24f308c898a9ba456d131 100644 (file)
@@ -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) {
index fab8ec4764345288d94a13c61ae1c15199265fdb..cab11287bd4c63ba7197b9c3331bca56712c5b40 100644 (file)
@@ -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 {
index 4800a6e9cb7b636b261990dac39752ab0484fe57..a86c874b541e010319180010502d0be806a0f749 100644 (file)
@@ -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 },
index cf1631238deb1ba05de11233372ce0f5c3d52051..a3a135082934009377cb2a7f70a44d1f84aa8c85 100644 (file)
@@ -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 (file)
index 0000000..0571503
--- /dev/null
@@ -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 (file)
index 0000000..7864652
--- /dev/null
@@ -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
index b72e6b6168913f20547354f48102574e2d8dc835..0b4b84c8258422b7ac2e46dce6c3fb1a90f7f6d7 100644 (file)
@@ -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
index 07cc2f6edff25db4469146c6222645118afed22b..d2da7e34e371eaed530bd773c84e63e3c5162874 100644 (file)
@@ -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;