]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
quota: Add imapc backend
authorTimo Sirainen <timo.sirainen@dovecot.fi>
Mon, 24 Apr 2017 14:59:53 +0000 (17:59 +0300)
committerTimo Sirainen <timo.sirainen@dovecot.fi>
Tue, 25 Apr 2017 09:17:45 +0000 (12:17 +0300)
This allows using imapc storage with imapc quota. The quota acts as
read-only, so it's used only when the current quota usage is explicitly
asked.

The quota can be looked up using either a mailbox name or quota root name.
By default INBOX's quota is looked up. If there are multiple quota roots
returned for the mailbox, only the first quota root returned by the
QUOTAROOT reply is used.

src/lib-imap-client/imapc-client.c
src/lib-imap-client/imapc-client.h
src/plugins/quota/Makefile.am
src/plugins/quota/quota-imapc.c [new file with mode: 0644]
src/plugins/quota/quota.c

index fa90286f9e245ea5ef798d31edaf4ae4f7819ea5..c4133dc1af3f6c60662dc184bcb39f30a798e5a4 100644 (file)
@@ -31,6 +31,7 @@ const struct imapc_capability_name imapc_capability_names[] = {
        { "UNSELECT", IMAPC_CAPABILITY_UNSELECT },
        { "ESEARCH", IMAPC_CAPABILITY_ESEARCH },
        { "WITHIN", IMAPC_CAPABILITY_WITHIN },
+       { "QUOTA", IMAPC_CAPABILITY_QUOTA },
 
        { "IMAP4REV1", IMAPC_CAPABILITY_IMAP4REV1 },
        { NULL, 0 }
index 2000af3a55094e88540395ffdec7ce5b7e026072..54dadbdff897a72b1e6dd26e7760422d259c4d4a 100644 (file)
@@ -31,6 +31,7 @@ enum imapc_capability {
        IMAPC_CAPABILITY_UNSELECT       = 0x400,
        IMAPC_CAPABILITY_ESEARCH        = 0x800,
        IMAPC_CAPABILITY_WITHIN         = 0x1000,
+       IMAPC_CAPABILITY_QUOTA          = 0x2000,
 
        IMAPC_CAPABILITY_IMAP4REV1      = 0x40000000
 };
index 329cf4719b3dc2514daf10a9f730676d55974e35..f29c5e464ebeec80f2e25b54255862ebb5a6adc7 100644 (file)
@@ -10,9 +10,12 @@ AM_CPPFLAGS = \
        -I$(top_srcdir)/src/lib-dict \
        -I$(top_srcdir)/src/lib-index \
        -I$(top_srcdir)/src/lib-mail \
+       -I$(top_srcdir)/src/lib-imap \
+       -I$(top_srcdir)/src/lib-imap-client \
        -I$(top_srcdir)/src/lib-settings \
        -I$(top_srcdir)/src/lib-storage \
        -I$(top_srcdir)/src/lib-storage/index \
+       -I$(top_srcdir)/src/lib-storage/index/imapc \
        -I$(top_srcdir)/src/lib-storage/index/maildir \
        -I$(top_srcdir)/src/doveadm
 
@@ -29,6 +32,7 @@ quota_dist_sources = \
        quota-fs.c \
        quota-dict.c \
        quota-dirsize.c \
+       quota-imapc.c \
        quota-maildir.c \
         quota-plugin.c \
        quota-storage.c \
@@ -40,6 +44,7 @@ quota_common_objects = \
        quota-fs.lo \
        quota-dict.lo \
        quota-dirsize.lo \
+       quota-imapc.lo \
        quota-maildir.lo \
         quota-plugin.lo \
        quota-storage.lo \
diff --git a/src/plugins/quota/quota-imapc.c b/src/plugins/quota/quota-imapc.c
new file mode 100644 (file)
index 0000000..264f853
--- /dev/null
@@ -0,0 +1,448 @@
+/* Copyright (c) 2017 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "imap-arg.h"
+#include "imapc-storage.h"
+#include "mailbox-list-private.h"
+#include "quota-private.h"
+
+struct imapc_quota_refresh_root {
+       const char *name;
+       unsigned int order;
+
+       uint64_t bytes_cur, count_cur;
+       uint64_t bytes_limit, count_limit;
+};
+
+struct imapc_quota_refresh {
+       pool_t pool;
+       const char *box_name;
+       ARRAY(struct imapc_quota_refresh_root) roots;
+};
+
+struct imapc_quota_root {
+       struct quota_root root;
+       const char *box_name, *root_name;
+
+       struct mail_namespace *imapc_ns;
+       struct imapc_storage_client *client;
+       bool initialized;
+
+       uint64_t bytes_last, count_last;
+
+       struct timeval last_refresh;
+       struct imapc_quota_refresh refresh;
+};
+
+extern struct quota_backend quota_backend_imapc;
+
+static struct quota_root *imapc_quota_alloc(void)
+{
+       struct imapc_quota_root *root;
+
+       root = i_new(struct imapc_quota_root, 1);
+       return &root->root;
+}
+
+static int imapc_quota_init(struct quota_root *_root, const char *args,
+                           const char **error_r)
+{
+       struct imapc_quota_root *root = (struct imapc_quota_root *)_root;
+       const char *const *tmp;
+
+       if (args == NULL)
+               args = "";
+       for (tmp = t_strsplit(args, ":"); *tmp != NULL; tmp++) {
+               if (strncmp(*tmp, "ns=", 3) == 0)
+                       _root->ns_prefix = p_strdup(_root->pool, *tmp + 3);
+               else if (strncmp(*tmp, "box=", 4) == 0)
+                       root->box_name = p_strdup(_root->pool, *tmp + 4);
+               else if (strncmp(*tmp, "root=", 5) == 0)
+                       root->root_name = p_strdup(_root->pool, *tmp + 5);
+               else {
+                       *error_r = t_strdup_printf("Invalid parameter: %s", *tmp);
+                       return -1;
+               }
+       }
+       if (root->box_name == NULL && root->root_name == NULL)
+               root->box_name = "INBOX";
+       return 0;
+}
+
+static void imapc_quota_deinit(struct quota_root *_root)
+{
+       i_free(_root);
+}
+
+static void
+imapc_quota_root_namespace_added(struct quota_root *_root,
+                                struct mail_namespace *ns)
+{
+       struct imapc_quota_root *root = (struct imapc_quota_root *)_root;
+
+       if (root->imapc_ns == NULL)
+               root->imapc_ns = ns;
+}
+
+static struct imapc_quota_refresh *
+imapc_quota_root_refresh_find(struct imapc_storage_client *client)
+{
+       struct imapc_storage *storage = client->_storage;
+       struct quota *quota;
+       struct quota_root *const *rootp;
+
+       i_assert(storage != NULL);
+       quota = quota_get_mail_user_quota(storage->storage.user);
+       i_assert(quota != NULL);
+
+       /* find the quota root that is being refreshed */
+       array_foreach(&quota->roots, rootp) {
+               if ((*rootp)->backend.name == quota_backend_imapc.name) {
+                       struct imapc_quota_root *root =
+                               (struct imapc_quota_root *)*rootp;
+
+                       if (root->refresh.pool != NULL)
+                               return &root->refresh;
+               }
+       }
+       return NULL;
+}
+
+static struct imapc_quota_refresh_root *
+imapc_quota_refresh_root_get(struct imapc_quota_refresh *refresh,
+                            const char *root_name)
+{
+       struct imapc_quota_refresh_root *refresh_root;
+
+       array_foreach_modifiable(&refresh->roots, refresh_root) {
+               if (strcmp(refresh_root->name, root_name) == 0)
+                       return refresh_root;
+       }
+
+       refresh_root = array_append_space(&refresh->roots);
+       refresh_root->order = UINT_MAX;
+       refresh_root->name = p_strdup(refresh->pool, root_name);
+       refresh_root->bytes_limit = (uint64_t)-1;
+       refresh_root->count_limit = (uint64_t)-1;
+       return refresh_root;
+}
+
+static void imapc_untagged_quotaroot(const struct imapc_untagged_reply *reply,
+                                    struct imapc_storage_client *client)
+{
+       struct imapc_quota_refresh *refresh;
+       struct imapc_quota_refresh_root *refresh_root;
+       const char *mailbox_name, *root_name;
+       unsigned int i;
+
+       if (!imap_arg_get_astring(&reply->args[0], &mailbox_name))
+               return;
+
+       if ((refresh = imapc_quota_root_refresh_find(client)) == NULL ||
+           refresh->box_name == NULL ||
+           strcmp(refresh->box_name, mailbox_name) != 0) {
+               /* unsolicited QUOTAROOT reply - ignore */
+               return;
+       }
+       if (array_count(&refresh->roots) > 0) {
+               /* duplicate QUOTAROOT reply - ignore */
+               return;
+       }
+
+       i = 1;
+       while (imap_arg_get_astring(&reply->args[i], &root_name)) {
+               refresh_root = imapc_quota_refresh_root_get(refresh, root_name);
+               refresh_root->order = i;
+               i++;
+       }
+}
+
+static void imapc_untagged_quota(const struct imapc_untagged_reply *reply,
+                                struct imapc_storage_client *client)
+{
+       const struct imap_arg *list;
+       struct imapc_quota_refresh *refresh;
+       struct imapc_quota_refresh_root *refresh_root;
+       const char *root_name, *resource, *value_str, *limit_str;
+       uint64_t value, limit;
+       unsigned int i;
+
+       if (!imap_arg_get_astring(&reply->args[0], &root_name) ||
+           !imap_arg_get_list(&reply->args[1], &list))
+               return;
+
+       if ((refresh = imapc_quota_root_refresh_find(client)) == NULL) {
+               /* unsolicited QUOTA reply - ignore */
+               return;
+       }
+       refresh_root = imapc_quota_refresh_root_get(refresh, root_name);
+
+       for (i = 0; list[i].type != IMAP_ARG_EOL; i += 3) {
+               if (!imap_arg_get_atom(&list[i], &resource) ||
+                   !imap_arg_get_atom(&list[i+1], &value_str) ||
+                   !imap_arg_get_atom(&list[i+2], &limit_str) ||
+                   /* RFC2087 uses 32bit number, but be ready for future */
+                   str_to_uint64(value_str, &value) < 0 ||
+                   str_to_uint64(limit_str, &limit) < 0)
+                       return;
+
+               if (strcasecmp(resource, QUOTA_NAME_STORAGE_KILOBYTES) == 0) {
+                       refresh_root->bytes_cur = value * 1024;
+                       refresh_root->bytes_limit = limit * 1024;
+               } else if (strcasecmp(resource, QUOTA_NAME_MESSAGES) == 0) {
+                       refresh_root->count_cur = value;
+                       refresh_root->count_limit = limit;
+               }
+       }
+}
+
+static bool imapc_quota_client_init(struct imapc_quota_root *root)
+{
+       struct mailbox_list *list;
+       struct mail_storage *storage;
+
+       if (root->initialized)
+               return root->client != NULL;
+       root->initialized = TRUE;
+
+       list = root->imapc_ns->list;
+       if (mailbox_list_get_storage(&list, "", &storage) == 0 &&
+           strcmp(storage->name, IMAPC_STORAGE_NAME) != 0) {
+               /* non-imapc namespace, skip */
+               if ((storage->class_flags &
+                    MAIL_STORAGE_CLASS_FLAG_NOQUOTA) == 0) {
+                       i_warning("quota: Namespace '%s' is not imapc, "
+                                 "skipping for imapc quota",
+                                 root->imapc_ns->prefix);
+               }
+               return FALSE;
+       }
+       root->client = ((struct imapc_storage *)storage)->client;
+
+       imapc_storage_client_register_untagged(root->client, "QUOTAROOT",
+                                              imapc_untagged_quotaroot);
+       imapc_storage_client_register_untagged(root->client, "QUOTA",
+                                              imapc_untagged_quota);
+       return TRUE;
+}
+
+static void imapc_quota_refresh_init(struct imapc_quota_refresh *refresh)
+{
+       i_assert(refresh->pool == NULL);
+
+       refresh->pool = pool_alloconly_create("imapc quota refresh", 256);
+       p_array_init(&refresh->roots, refresh->pool, 4);
+}
+
+static void
+imapc_quota_refresh_update(struct quota *quota,
+                          struct imapc_quota_refresh *refresh)
+{
+       struct quota_root *const *rootp;
+       const struct imapc_quota_refresh_root *refresh_root;
+
+       if (array_count(&refresh->roots) == 0) {
+               i_error("quota: imapc didn't return any QUOTA results");
+               return;
+       }
+       /* use the first quota root for everything */
+       refresh_root = array_idx(&refresh->roots, 0);
+
+       array_foreach(&quota->roots, rootp) {
+               if ((*rootp)->backend.name == quota_backend_imapc.name) {
+                       struct imapc_quota_root *root =
+                               (struct imapc_quota_root *)*rootp;
+
+                       root->bytes_last = refresh_root->bytes_cur;
+                       root->count_last = refresh_root->count_cur;
+                       root->root.bytes_limit = refresh_root->bytes_limit;
+                       root->root.count_limit = refresh_root->count_limit;
+               }
+       }
+}
+
+static void
+imapc_quota_refresh_deinit(struct quota *quota,
+                          struct imapc_quota_refresh *refresh, bool success)
+{
+       if (success)
+               imapc_quota_refresh_update(quota, refresh);
+       pool_unref(&refresh->pool);
+       i_zero(refresh);
+}
+
+static int
+imapc_quota_refresh_root_order_cmp(const struct imapc_quota_refresh_root *root1,
+                                  const struct imapc_quota_refresh_root *root2)
+{
+       if (root1->order < root2->order)
+               return -1;
+       else if (root1->order > root2->order)
+               return 1;
+       else
+               return 0;
+}
+
+static int imapc_quota_refresh_mailbox(struct imapc_quota_root *root)
+{
+       struct imapc_simple_context sctx;
+       struct imapc_command *cmd;
+
+       i_assert(root->box_name != NULL);
+
+       /* ask quotas for the configured mailbox */
+       imapc_quota_refresh_init(&root->refresh);
+       root->refresh.box_name = root->box_name;
+
+       imapc_simple_context_init(&sctx, root->client);
+       cmd = imapc_client_cmd(root->client->client,
+                              imapc_simple_callback, &sctx);
+       imapc_command_sendf(cmd, "GETQUOTAROOT %s", root->box_name);
+       imapc_simple_run(&sctx);
+
+       /* if there are multiple quota roots, use the first one returned by
+          the QUOTAROOT */
+       array_sort(&root->refresh.roots, imapc_quota_refresh_root_order_cmp);
+       imapc_quota_refresh_deinit(root->root.quota, &root->refresh,
+                                  sctx.ret == 0);
+       return sctx.ret;
+}
+
+static int imapc_quota_refresh_root(struct imapc_quota_root *root)
+{
+       struct imapc_simple_context sctx;
+       struct imapc_command *cmd;
+
+       i_assert(root->root_name != NULL);
+
+       /* ask quotas for the configured quota root */
+       imapc_quota_refresh_init(&root->refresh);
+
+       imapc_simple_context_init(&sctx, root->client);
+       cmd = imapc_client_cmd(root->client->client,
+                              imapc_simple_callback, &sctx);
+       imapc_command_sendf(cmd, "GETQUOTA %s", root->root_name);
+       imapc_simple_run(&sctx);
+
+       /* there shouldn't be more than one QUOTA reply, but ignore anyway
+          anything we didn't expect. */
+       while (array_count(&root->refresh.roots) > 0) {
+               const struct imapc_quota_refresh_root *refresh_root =
+                       array_idx(&root->refresh.roots, 0);
+               if (strcmp(refresh_root->name, root->root_name) == 0)
+                       break;
+               array_delete(&root->refresh.roots, 0, 1);
+       }
+       imapc_quota_refresh_deinit(root->root.quota, &root->refresh,
+                                  sctx.ret == 0);
+       return sctx.ret;
+}
+
+static int imapc_quota_refresh(struct imapc_quota_root *root)
+{
+       enum imapc_capability capa;
+
+       if (root->imapc_ns == NULL) {
+               /* imapc namespace is missing - disable this quota backend */
+               return 0;
+       }
+       if (root->last_refresh.tv_sec == ioloop_timeval.tv_sec &&
+           root->last_refresh.tv_usec == ioloop_timeval.tv_usec)
+               return 0;
+       if (!imapc_quota_client_init(root))
+               return 0;
+
+       if (imapc_client_get_capabilities(root->client->client, &capa) < 0)
+               return -1;
+       if ((capa & IMAPC_CAPABILITY_QUOTA) == 0) {
+               /* no QUOTA capability - disable quota */
+               i_warning("quota: Remote IMAP server doesn't support QUOTA - disabling");
+               root->client = NULL;
+               return 0;
+       }
+
+       root->last_refresh = ioloop_timeval;
+       if (root->root_name == NULL)
+               return imapc_quota_refresh_mailbox(root);
+       else
+               return imapc_quota_refresh_root(root);
+}
+
+static int imapc_quota_init_limits(struct quota_root *_root)
+{
+       struct imapc_quota_root *root = (struct imapc_quota_root *)_root;
+
+       return imapc_quota_refresh(root);
+}
+
+static void
+imapc_quota_namespace_added(struct quota *quota, struct mail_namespace *ns)
+{
+       struct quota_root **roots;
+       unsigned int i, count;
+
+       roots = array_get_modifiable(&quota->roots, &count);
+       for (i = 0; i < count; i++) {
+               if (roots[i]->backend.name == quota_backend_imapc.name &&
+                   ((roots[i]->ns_prefix == NULL &&
+                     ns->type == MAIL_NAMESPACE_TYPE_PRIVATE) ||
+                    roots[i]->ns == ns))
+                       imapc_quota_root_namespace_added(roots[i], ns);
+       }
+}
+
+static const char *const *
+imapc_quota_root_get_resources(struct quota_root *root ATTR_UNUSED)
+{
+       static const char *resources_both[] = {
+               QUOTA_NAME_STORAGE_KILOBYTES,
+               QUOTA_NAME_MESSAGES,
+               NULL
+       };
+       return resources_both;
+}
+
+static int
+imapc_quota_get_resource(struct quota_root *_root, const char *name,
+                        uint64_t *value_r)
+{
+       struct imapc_quota_root *root = (struct imapc_quota_root *)_root;
+
+       if (imapc_quota_refresh(root) < 0)
+               return -1;
+
+       if (strcmp(name, QUOTA_NAME_STORAGE_BYTES) == 0)
+               *value_r = root->bytes_last;
+       else if (strcmp(name, QUOTA_NAME_MESSAGES) == 0)
+               *value_r = root->count_last;
+       else
+               return 0;
+       return 1;
+}
+
+static int 
+imapc_quota_update(struct quota_root *root ATTR_UNUSED,
+                  struct quota_transaction_context *ctx ATTR_UNUSED)
+{
+       return 0;
+}
+
+struct quota_backend quota_backend_imapc = {
+       "imapc",
+
+       {
+               imapc_quota_alloc,
+               imapc_quota_init,
+               imapc_quota_deinit,
+               NULL,
+               imapc_quota_init_limits,
+               imapc_quota_namespace_added,
+               imapc_quota_root_get_resources,
+               imapc_quota_get_resource,
+               imapc_quota_update,
+               NULL,
+               NULL
+       }
+};
index b8b7b71e56eb33db37536fd10e152ee6547368e0..9bf1c12314fd37b1d6a3db5b6956125a61184bf0 100644 (file)
@@ -39,6 +39,7 @@ extern struct quota_backend quota_backend_count;
 extern struct quota_backend quota_backend_dict;
 extern struct quota_backend quota_backend_dirsize;
 extern struct quota_backend quota_backend_fs;
+extern struct quota_backend quota_backend_imapc;
 extern struct quota_backend quota_backend_maildir;
 
 static const struct quota_backend *quota_internal_backends[] = {
@@ -48,6 +49,7 @@ static const struct quota_backend *quota_internal_backends[] = {
        &quota_backend_count,
        &quota_backend_dict,
        &quota_backend_dirsize,
+       &quota_backend_imapc,
        &quota_backend_maildir
 };