diff options
Diffstat (limited to 'src/plugins/quota/quota-imapc.c')
-rw-r--r-- | src/plugins/quota/quota-imapc.c | 494 |
1 files changed, 494 insertions, 0 deletions
diff --git a/src/plugins/quota/quota-imapc.c b/src/plugins/quota/quota-imapc.c new file mode 100644 index 0000000..d5931db --- /dev/null +++ b/src/plugins/quota/quota-imapc.c @@ -0,0 +1,494 @@ +/* Copyright (c) 2017-2018 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 void handle_box_param(struct quota_root *_root, const char *param_value) +{ + ((struct imapc_quota_root *)_root)->box_name = p_strdup(_root->pool, param_value); +} + +static void handle_root_param(struct quota_root *_root, const char *param_value) +{ + ((struct imapc_quota_root *)_root)->root_name = p_strdup(_root->pool, param_value); +} + +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 struct quota_param_parser imapc_params[] = { + {.param_name = "box=", .param_handler = handle_box_param}, + {.param_name = "root=", .param_handler = handle_root_param}, + quota_param_ns, + {.param_name = NULL} + }; + + _root->auto_updating = TRUE; + event_set_append_log_prefix(root->root.backend.event, "quota-imapc: "); + + if (quota_parse_parameters(_root, &args, error_r, imapc_params, TRUE) < 0) + return -1; + + if (root->box_name == NULL && root->root_name == NULL) + root->box_name = "INBOX"; + /* we'll never try to enforce the quota - it's just a lot of + unnecessary remote GETQUOTA calls. */ + _root->no_enforcing = TRUE; + 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("a->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) { + e_warning(root->root.backend.event, + "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) { + e_error(quota_backend_imapc.event, + "imapc didn't return any QUOTA results"); + return; + } + /* use the first quota root for everything */ + refresh_root = array_front(&refresh->roots); + + array_foreach("a->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; + + /* If limits are higher than what dovecot can handle + consider them unlimited. */ + if (refresh_root->bytes_limit > INT64_MAX) + root->root.bytes_limit = 0; + else + root->root.bytes_limit = refresh_root->bytes_limit; + if (refresh_root->count_limit > INT64_MAX) + root->root.count_limit = 0; + else + 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, + const char **error_r) +{ + 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, &cmd); + + /* 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); + if (sctx.ret < 0) + *error_r = t_strdup_printf( + "GETQUOTAROOT %s failed: %s", + root->box_name, + mail_storage_get_last_internal_error( + &root->client->_storage->storage, NULL)); + + return sctx.ret; +} + +static int imapc_quota_refresh_root(struct imapc_quota_root *root, + const char **error_r) +{ + 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, &cmd); + + /* 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_front(&root->refresh.roots); + if (strcmp(refresh_root->name, root->root_name) == 0) + break; + array_pop_front(&root->refresh.roots); + } + imapc_quota_refresh_deinit(root->root.quota, &root->refresh, + sctx.ret == 0); + if (sctx.ret < 0) + *error_r = t_strdup_printf( + "GETQUOTA %s failed: %s", + root->root_name, + mail_storage_get_last_internal_error( + &root->client->_storage->storage, NULL)); + return sctx.ret; +} + +static int imapc_quota_refresh(struct imapc_quota_root *root, + const char **error_r) +{ + enum imapc_capability capa; + int ret; + + 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) { + *error_r = "Failed to get server capabilities"; + return -1; + } + if ((capa & IMAPC_CAPABILITY_QUOTA) == 0) { + /* no QUOTA capability - disable quota */ + e_warning(root->root.backend.event, + "Remote IMAP server doesn't support QUOTA - disabling"); + root->client = NULL; + return 0; + } + + if (root->root_name == NULL) + ret = imapc_quota_refresh_mailbox(root, error_r); + else + ret = imapc_quota_refresh_root(root, error_r); + + /* set the last_refresh only after the refresh, because it changes + ioloop_timeval. */ + root->last_refresh = ioloop_timeval; + return ret; +} + +static int imapc_quota_init_limits(struct quota_root *_root, + const char **error_r) +{ + struct imapc_quota_root *root = (struct imapc_quota_root *)_root; + + return imapc_quota_refresh(root, error_r); +} + +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("a->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 enum quota_get_result +imapc_quota_get_resource(struct quota_root *_root, const char *name, + uint64_t *value_r, const char **error_r) +{ + struct imapc_quota_root *root = (struct imapc_quota_root *)_root; + + if (imapc_quota_refresh(root, error_r) < 0) + return QUOTA_GET_RESULT_INTERNAL_ERROR; + + 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 { + *error_r = QUOTA_UNKNOWN_RESOURCE_ERROR_STRING; + return QUOTA_GET_RESULT_UNKNOWN_RESOURCE; + } + return QUOTA_GET_RESULT_LIMITED; +} + +static int +imapc_quota_update(struct quota_root *root ATTR_UNUSED, + struct quota_transaction_context *ctx ATTR_UNUSED, + const char **error_r ATTR_UNUSED) +{ + return 0; +} + +struct quota_backend quota_backend_imapc = { + .name = "imapc", + + .v = { + .alloc = imapc_quota_alloc, + .init = imapc_quota_init, + .deinit = imapc_quota_deinit, + .init_limits = imapc_quota_init_limits, + .namespace_added = imapc_quota_namespace_added, + .get_resources = imapc_quota_root_get_resources, + .get_resource = imapc_quota_get_resource, + .update = imapc_quota_update, + } +}; |