diff options
Diffstat (limited to 'src/plugins/quota/quota.c')
-rw-r--r-- | src/plugins/quota/quota.c | 1543 |
1 files changed, 1543 insertions, 0 deletions
diff --git a/src/plugins/quota/quota.c b/src/plugins/quota/quota.c new file mode 100644 index 0000000..3d6d8e5 --- /dev/null +++ b/src/plugins/quota/quota.c @@ -0,0 +1,1543 @@ +/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "hash.h" +#include "str.h" +#include "ioloop.h" +#include "net.h" +#include "write-full.h" +#include "eacces-error.h" +#include "wildcard-match.h" +#include "dict.h" +#include "mailbox-list-private.h" +#include "quota-private.h" +#include "quota-fs.h" +#include "llist.h" +#include "program-client.h" +#include "settings-parser.h" + +#include <sys/wait.h> + +#define DEFAULT_QUOTA_EXCEEDED_MSG \ + "Quota exceeded (mailbox for user is full)" +#define QUOTA_LIMIT_SET_PATH DICT_PATH_PRIVATE"quota/limit/" + +/* How many seconds after the userdb lookup do we still want to execute the + quota_over_script. This applies to quota_over_flag_lazy_check=yes and also + after unhibernating IMAP connections. */ +#define QUOTA_OVER_FLAG_MAX_DELAY_SECS 10 + +struct quota_root_iter { + struct quota *quota; + struct mailbox *box; + + unsigned int i; +}; + +unsigned int quota_module_id = 0; + +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[] = { +#ifdef HAVE_FS_QUOTA + "a_backend_fs, +#endif + "a_backend_count, + "a_backend_dict, + "a_backend_dirsize, + "a_backend_imapc, + "a_backend_maildir +}; + +static ARRAY(const struct quota_backend*) quota_backends; + +static void hidden_param_handler(struct quota_root *_root, const char *param_value); +static void ignoreunlim_param_handler(struct quota_root *_root, const char *param_value); +static void noenforcing_param_handler(struct quota_root *_root, const char *param_value); +static void ns_param_handler(struct quota_root *_root, const char *param_value); + +struct quota_param_parser quota_param_hidden = {.param_name = "hidden", .param_handler = hidden_param_handler}; +struct quota_param_parser quota_param_ignoreunlimited = {.param_name = "ignoreunlimited", .param_handler = ignoreunlim_param_handler}; +struct quota_param_parser quota_param_noenforcing = {.param_name = "noenforcing", .param_handler = noenforcing_param_handler}; +struct quota_param_parser quota_param_ns = {.param_name = "ns=", .param_handler = ns_param_handler}; + +static enum quota_alloc_result quota_default_test_alloc( + struct quota_transaction_context *ctx, uoff_t size, + const char **error_r); +static void quota_over_flag_check_root(struct quota_root *root); + +static const struct quota_backend *quota_backend_find(const char *name) +{ + const struct quota_backend *const *backend; + + array_foreach("a_backends, backend) { + if (strcmp((*backend)->name, name) == 0) + return *backend; + } + + return NULL; +} + +void quota_backend_register(const struct quota_backend *backend) +{ + i_assert(quota_backend_find(backend->name) == NULL); + array_push_back("a_backends, &backend); +} + +void quota_backend_unregister(const struct quota_backend *backend) +{ + for(unsigned int i = 0; i < array_count("a_backends); i++) { + const struct quota_backend *be = + array_idx_elem("a_backends, i); + if (strcmp(be->name, backend->name) == 0) { + array_delete("a_backends, i, 1); + return; + } + } + + i_unreached(); +} + +void quota_backends_register(void); +void quota_backends_unregister(void); + +void quota_backends_register(void) +{ + i_array_init("a_backends, 8); + array_append("a_backends, quota_internal_backends, + N_ELEMENTS(quota_internal_backends)); +} + +void quota_backends_unregister(void) +{ + for(size_t i = 0; i < N_ELEMENTS(quota_internal_backends); i++) { + quota_backend_unregister(quota_internal_backends[i]); + } + + i_assert(array_count("a_backends) == 0); + array_free("a_backends); + +} + +static int quota_root_add_rules(struct mail_user *user, const char *root_name, + struct quota_root_settings *root_set, + const char **error_r) +{ + const char *rule_name, *rule, *error; + unsigned int i; + + rule_name = t_strconcat(root_name, "_rule", NULL); + for (i = 2;; i++) { + rule = mail_user_plugin_getenv(user, rule_name); + if (rule == NULL) + break; + + if (quota_root_add_rule(root_set, rule, &error) < 0) { + *error_r = t_strdup_printf("Invalid rule %s: %s", + rule, error); + return -1; + } + rule_name = t_strdup_printf("%s_rule%d", root_name, i); + } + return 0; +} + +static int +quota_root_add_warning_rules(struct mail_user *user, const char *root_name, + struct quota_root_settings *root_set, + const char **error_r) +{ + const char *rule_name, *rule, *error; + unsigned int i; + + rule_name = t_strconcat(root_name, "_warning", NULL); + for (i = 2;; i++) { + rule = mail_user_plugin_getenv(user, rule_name); + if (rule == NULL) + break; + + if (quota_root_add_warning_rule(root_set, rule, &error) < 0) { + *error_r = t_strdup_printf("Invalid warning rule: %s", + rule); + return -1; + } + rule_name = t_strdup_printf("%s_warning%d", root_name, i); + } + return 0; +} + +static int +quota_root_parse_set(struct mail_user *user, const char *root_name, + struct quota_root_settings *root_set, + const char **error_r) +{ + const char *name, *value; + + name = t_strconcat(root_name, "_set", NULL); + value = mail_user_plugin_getenv(user, name); + if (value == NULL) + return 0; + + if (!str_begins(value, "dict:")) { + *error_r = t_strdup_printf("%s supports only dict backend", name); + return -1; + } + root_set->limit_set = p_strdup(root_set->set->pool, value+5); + return 0; +} + +static int +quota_root_settings_init(struct quota_settings *quota_set, const char *root_def, + struct quota_root_settings **set_r, + const char **error_r) +{ + struct quota_root_settings *root_set; + const struct quota_backend *backend; + const char *p, *args, *backend_name; + + /* <backend>[:<quota root name>[:<backend args>]] */ + p = strchr(root_def, ':'); + if (p == NULL) { + backend_name = root_def; + args = NULL; + } else { + backend_name = t_strdup_until(root_def, p); + args = p + 1; + } + + backend = quota_backend_find(backend_name); + if (backend == NULL) { + *error_r = t_strdup_printf("Unknown quota backend: %s", + backend_name); + return -1; + } + + root_set = p_new(quota_set->pool, struct quota_root_settings, 1); + root_set->set = quota_set; + root_set->backend = backend; + + if (args != NULL) { + /* save root's name */ + p = strchr(args, ':'); + if (p == NULL) { + root_set->name = p_strdup(quota_set->pool, args); + args = NULL; + } else { + root_set->name = + p_strdup_until(quota_set->pool, args, p); + args = p + 1; + } + } else { + root_set->name = ""; + } + root_set->args = p_strdup(quota_set->pool, args); + + e_debug(quota_set->event, "Quota root: name=%s backend=%s args=%s", + root_set->name, backend_name, args == NULL ? "" : args); + + p_array_init(&root_set->rules, quota_set->pool, 4); + p_array_init(&root_set->warning_rules, quota_set->pool, 4); + array_push_back("a_set->root_sets, &root_set); + *set_r = root_set; + return 0; +} + +static int +quota_root_add(struct quota_settings *quota_set, struct mail_user *user, + const char *env, const char *root_name, const char **error_r) +{ + struct quota_root_settings *root_set; + const char *set_name, *value; + + if (quota_root_settings_init(quota_set, env, &root_set, error_r) < 0) + return -1; + root_set->set_name = p_strdup(quota_set->pool, root_name); + if (quota_root_add_rules(user, root_name, root_set, error_r) < 0) + return -1; + if (quota_root_add_warning_rules(user, root_name, root_set, error_r) < 0) + return -1; + if (quota_root_parse_set(user, root_name, root_set, error_r) < 0) + return -1; + + set_name = t_strconcat(root_name, "_grace", NULL); + value = mail_user_plugin_getenv(user, set_name); + if (quota_root_parse_grace(root_set, value, error_r) < 0) { + *error_r = t_strdup_printf("Invalid %s value '%s': %s", + set_name, value, *error_r); + return -1; + } + return 0; +} + +const char *quota_alloc_result_errstr(enum quota_alloc_result res, + struct quota_transaction_context *qt) +{ + switch (res) { + case QUOTA_ALLOC_RESULT_OK: + return "OK"; + case QUOTA_ALLOC_RESULT_BACKGROUND_CALC: + return "Blocked by an ongoing background quota calculation"; + case QUOTA_ALLOC_RESULT_TEMPFAIL: + return "Internal quota calculation error"; + case QUOTA_ALLOC_RESULT_OVER_MAXSIZE: + return "Mail size is larger than the maximum size allowed by " + "server configuration"; + case QUOTA_ALLOC_RESULT_OVER_QUOTA_LIMIT: + case QUOTA_ALLOC_RESULT_OVER_QUOTA: + return qt->quota->set->quota_exceeded_msg; + } + i_unreached(); +} + +int quota_user_read_settings(struct mail_user *user, + struct quota_settings **set_r, + const char **error_r) +{ + struct quota_settings *quota_set; + char root_name[5 + MAX_INT_STRLEN]; + const char *env, *error; + unsigned int i; + pool_t pool; + + pool = pool_alloconly_create("quota settings", 2048); + quota_set = p_new(pool, struct quota_settings, 1); + quota_set->pool = pool; + quota_set->event = event_create(user->event); + quota_set->test_alloc = quota_default_test_alloc; + quota_set->debug = user->mail_debug; + quota_set->quota_exceeded_msg = + mail_user_plugin_getenv(user, "quota_exceeded_message"); + if (quota_set->quota_exceeded_msg == NULL) + quota_set->quota_exceeded_msg = DEFAULT_QUOTA_EXCEEDED_MSG; + quota_set->vsizes = mail_user_plugin_getenv_bool(user, "quota_vsizes"); + + const char *max_size = mail_user_plugin_getenv(user, + "quota_max_mail_size"); + if (max_size != NULL) { + const char *error = NULL; + if (settings_get_size(max_size, "a_set->max_mail_size, + &error) < 0) { + *error_r = t_strdup_printf("quota_max_mail_size: %s", + error); + return -1; + } + } + + p_array_init("a_set->root_sets, pool, 4); + if (i_strocpy(root_name, "quota", sizeof(root_name)) < 0) + i_unreached(); + for (i = 2;; i++) { + env = mail_user_plugin_getenv(user, root_name); + if (env == NULL || *env == '\0') + break; + + if (quota_root_add(quota_set, user, env, root_name, + &error) < 0) { + *error_r = t_strdup_printf("Invalid quota root %s: %s", + root_name, error); + event_unref("a_set->event); + pool_unref(&pool); + return -1; + } + if (i_snprintf(root_name, sizeof(root_name), "quota%d", i) < 0) + i_unreached(); + } + if (quota_set->max_mail_size == 0 && + array_count("a_set->root_sets) == 0) { + event_unref("a_set->event); + pool_unref(&pool); + return 0; + } + + quota_set->initialized = TRUE; + *set_r = quota_set; + return 1; +} + +void quota_settings_deinit(struct quota_settings **_quota_set) +{ + struct quota_settings *quota_set = *_quota_set; + + *_quota_set = NULL; + + event_unref("a_set->event); + pool_unref("a_set->pool); +} + +static void quota_root_deinit(struct quota_root *root) +{ + pool_t pool = root->pool; + + if (root->limit_set_dict != NULL) + dict_deinit(&root->limit_set_dict); + event_unref(&root->backend.event); + root->backend.v.deinit(root); + pool_unref(&pool); +} + +int quota_root_default_init(struct quota_root *root, const char *args, + const char **error_r) +{ + const struct quota_param_parser default_params[] = { + quota_param_hidden, + quota_param_ignoreunlimited, + quota_param_noenforcing, + quota_param_ns, + {.param_name = NULL} + }; + return quota_parse_parameters(root, &args, error_r, default_params, TRUE); +} + +static int +quota_root_init(struct quota_root_settings *root_set, struct quota *quota, + struct quota_root **root_r, const char **error_r) +{ + struct quota_root *root; + + root = root_set->backend->v.alloc(); + root->pool = pool_alloconly_create("quota root", 512); + root->set = root_set; + root->quota = quota; + root->backend = *root_set->backend; + root->bytes_limit = root_set->default_rule.bytes_limit; + root->count_limit = root_set->default_rule.count_limit; + + array_create(&root->quota_module_contexts, root->pool, + sizeof(void *), 10); + + if (root->backend.v.init != NULL) { + root->backend.event = event_create(quota->event); + event_drop_parent_log_prefixes(root->backend.event, 1); + event_set_forced_debug(root->backend.event, root->quota->set->debug); + + if (root->backend.v.init(root, root_set->args, error_r) < 0) { + *error_r = t_strdup_printf("%s quota init failed: %s", + root->backend.name, *error_r); + + event_unref(&root->backend.event); + return -1; + } + } else { + if (quota_root_default_init(root, root_set->args, error_r) < 0) + return -1; + } + if (root_set->default_rule.bytes_limit == 0 && + root_set->default_rule.count_limit == 0 && + root->disable_unlimited_tracking) { + quota_root_deinit(root); + return 0; + } + *root_r = root; + return 1; +} + +int quota_init(struct quota_settings *quota_set, struct mail_user *user, + struct quota **quota_r, const char **error_r) +{ + struct quota *quota; + struct quota_root *root; + struct quota_root_settings *const *root_sets; + unsigned int i, count; + const char *error; + int ret; + + quota = i_new(struct quota, 1); + quota->event = event_create(user->event); + event_set_forced_debug(quota->event, quota_set->debug); + event_set_append_log_prefix(quota->event, "quota: "); + quota->user = user; + quota->set = quota_set; + i_array_init("a->roots, 8); + + root_sets = array_get("a_set->root_sets, &count); + i_array_init("a->namespaces, count); + for (i = 0; i < count; i++) { + ret = quota_root_init(root_sets[i], quota, &root, &error); + if (ret < 0) { + *error_r = t_strdup_printf("Quota root %s: %s", + root_sets[i]->name, error); + quota_deinit("a); + return -1; + } + if (ret > 0) + array_push_back("a->roots, &root); + } + *quota_r = quota; + return 0; +} + +void quota_deinit(struct quota **_quota) +{ + struct quota *quota = *_quota; + struct quota_root *const *roots; + unsigned int i, count; + + roots = array_get("a->roots, &count); + for (i = 0; i < count; i++) + quota_root_deinit(roots[i]); + + /* deinit quota roots before setting quser->quota=NULL */ + *_quota = NULL; + + array_free("a->roots); + array_free("a->namespaces); + event_unref("a->event); + i_free(quota); +} + +static int quota_root_get_rule_limits(struct quota_root *root, + const char *mailbox_name, + uint64_t *bytes_limit_r, + uint64_t *count_limit_r, + bool *ignored_r, + const char **error_r) +{ + struct quota_rule *rule; + int64_t bytes_limit, count_limit; + int ret; + + *ignored_r = FALSE; + + if (!root->set->force_default_rule) { + if (root->backend.v.init_limits != NULL) { + const char *error; + if (root->backend.v.init_limits(root, &error) < 0) { + *error_r = t_strdup_printf( + "Initializing limits failed for quota backend: %s", + error); + return -1; + } + } + } + + bytes_limit = root->bytes_limit; + count_limit = root->count_limit; + + /* if default rule limits are 0, user has unlimited quota. + ignore any specific quota rules */ + if (bytes_limit != 0 || count_limit != 0) { + (void)mail_namespace_find_unalias(root->quota->user->namespaces, + &mailbox_name); + rule = quota_root_rule_find(root->set, mailbox_name); + ret = 1; + } else { + rule = NULL; + ret = 0; + } + + if (rule != NULL) { + if (!rule->ignore) { + bytes_limit += rule->bytes_limit; + count_limit += rule->count_limit; + } else { + bytes_limit = 0; + count_limit = 0; + *ignored_r = TRUE; + } + } + + *bytes_limit_r = bytes_limit <= 0 ? 0 : bytes_limit; + *count_limit_r = count_limit <= 0 ? 0 : count_limit; + return ret; +} + +static bool +quota_is_duplicate_namespace(struct quota *quota, struct mail_namespace *ns) +{ + struct mail_namespace *const *namespaces; + unsigned int i, count; + const char *path, *path2; + + if (!mailbox_list_get_root_path(ns->list, + MAILBOX_LIST_PATH_TYPE_MAILBOX, &path)) + path = NULL; + + namespaces = array_get("a->namespaces, &count); + for (i = 0; i < count; i++) { + /* count namespace aliases only once. don't rely only on + alias_for != NULL, because the alias might have been + explicitly added as the wanted quota namespace. */ + if (ns->alias_for == namespaces[i] || + namespaces[i]->alias_for == ns) + continue; + + if (path != NULL && + mailbox_list_get_root_path(namespaces[i]->list, + MAILBOX_LIST_PATH_TYPE_MAILBOX, &path2) && + strcmp(path, path2) == 0) { + /* duplicate path */ + if ((ns->flags & NAMESPACE_FLAG_INBOX_USER) == 0) + return TRUE; + + /* this is inbox=yes namespace, but the earlier one + that had the same location was inbox=no. we need to + include the INBOX also in quota calculations, so we + can't just ignore this namespace. but since we've + already called backend's namespace_added(), we can't + just remove it either. so just mark the old one as + unwanted namespace. + + an alternative would be to do a bit larger change so + namespaces wouldn't be added until + mail_namespaces_created() hook is called */ + i_assert(quota->unwanted_ns == NULL); + quota->unwanted_ns = namespaces[i]; + return FALSE; + } + } + return FALSE; +} + +void quota_add_user_namespace(struct quota *quota, struct mail_namespace *ns) +{ + struct quota_root *const *roots; + struct quota_backend **backends; + unsigned int i, j, count; + + /* first check if there already exists a namespace with the exact same + path. we don't want to count them twice. */ + if (quota_is_duplicate_namespace(quota, ns)) + return; + + array_push_back("a->namespaces, &ns); + + roots = array_get("a->roots, &count); + /* @UNSAFE: get different backends into one array */ + backends = t_new(struct quota_backend *, count + 1); + for (i = 0; i < count; i++) { + for (j = 0; backends[j] != NULL; j++) { + if (backends[j]->name == roots[i]->backend.name) + break; + } + if (backends[j] == NULL) + backends[j] = &roots[i]->backend; + } + + for (i = 0; backends[i] != NULL; i++) { + if (backends[i]->v.namespace_added != NULL) + backends[i]->v.namespace_added(quota, ns); + } +} + +void quota_remove_user_namespace(struct mail_namespace *ns) +{ + struct quota *quota; + struct mail_namespace *const *namespaces; + unsigned int i, count; + + quota = ns->owner != NULL ? + quota_get_mail_user_quota(ns->owner) : + quota_get_mail_user_quota(ns->user); + if (quota == NULL) { + /* no quota for this namespace */ + return; + } + + namespaces = array_get("a->namespaces, &count); + for (i = 0; i < count; i++) { + if (namespaces[i] == ns) { + array_delete("a->namespaces, i, 1); + break; + } + } +} + +struct quota_root_iter * +quota_root_iter_init_user(struct mail_user *user) +{ + struct quota_root_iter *iter; + + iter = i_new(struct quota_root_iter, 1); + iter->quota = quota_get_mail_user_quota(user); + return iter; +} + +struct quota_root_iter * +quota_root_iter_init(struct mailbox *box) +{ + struct quota_root_iter *iter; + struct mail_user *user; + + user = box->list->ns->owner != NULL ? + box->list->ns->owner : box->list->ns->user; + iter = quota_root_iter_init_user(user); + iter->box = box; + return iter; +} + +bool quota_root_is_namespace_visible(struct quota_root *root, + struct mail_namespace *ns) +{ + struct mailbox_list *list = ns->list; + struct mail_storage *storage; + + /* this check works as long as there is only one storage per list */ + if (mailbox_list_get_storage(&list, "", &storage) == 0 && + (storage->class_flags & MAIL_STORAGE_CLASS_FLAG_NOQUOTA) != 0) + return FALSE; + if (root->quota->unwanted_ns == ns) + return FALSE; + + if (root->ns_prefix != NULL) { + if (root->ns != ns) + return FALSE; + } else { + if (ns->owner == NULL) + return FALSE; + } + return TRUE; +} + +static bool +quota_root_is_visible(struct quota_root *root, struct mailbox *box) +{ + if (!quota_root_is_namespace_visible(root, box->list->ns)) + return FALSE; + if (array_count(&root->quota->roots) == 1) { + /* a single quota root: don't bother checking further */ + return TRUE; + } + return root->backend.v.match_box == NULL ? TRUE : + root->backend.v.match_box(root, box); +} + +struct quota_root *quota_root_iter_next(struct quota_root_iter *iter) +{ + struct quota_root *const *roots, *root = NULL; + unsigned int count; + + if (iter->quota == NULL) + return NULL; + + roots = array_get(&iter->quota->roots, &count); + if (iter->i >= count) + return NULL; + + for (; iter->i < count; iter->i++) { + if (iter->box != NULL && + !quota_root_is_visible(roots[iter->i], iter->box)) + continue; + + root = roots[iter->i]; + break; + } + + iter->i++; + return root; +} + +void quota_root_iter_deinit(struct quota_root_iter **_iter) +{ + struct quota_root_iter *iter = *_iter; + + *_iter = NULL; + i_free(iter); +} + +struct quota_root *quota_root_lookup(struct mail_user *user, const char *name) +{ + struct quota *quota; + struct quota_root *const *roots; + unsigned int i, count; + + quota = quota_get_mail_user_quota(user); + if (quota == NULL) + return NULL; + roots = array_get("a->roots, &count); + for (i = 0; i < count; i++) { + if (strcmp(roots[i]->set->name, name) == 0) + return roots[i]; + } + return NULL; +} + +const char *quota_root_get_name(struct quota_root *root) +{ + return root->set->name; +} + +const char *const *quota_root_get_resources(struct quota_root *root) +{ + /* if we haven't checked the quota_over_flag yet, do it now */ + quota_over_flag_check_root(root); + + return root->backend.v.get_resources(root); +} + +bool quota_root_is_hidden(struct quota_root *root) +{ + return root->hidden; +} + +enum quota_get_result +quota_get_resource(struct quota_root *root, const char *mailbox_name, + const char *name, uint64_t *value_r, uint64_t *limit_r, + const char **error_r) +{ + const char *error; + uint64_t bytes_limit, count_limit; + bool ignored, kilobytes = FALSE; + enum quota_get_result ret; + + *value_r = *limit_r = 0; + + if (strcmp(name, QUOTA_NAME_STORAGE_KILOBYTES) == 0) { + name = QUOTA_NAME_STORAGE_BYTES; + kilobytes = TRUE; + } + + /* Get the value first. This call may also update quota limits if + they're defined externally. */ + ret = root->backend.v.get_resource(root, name, value_r, &error); + if (ret == QUOTA_GET_RESULT_UNLIMITED) + i_panic("Quota backend %s returned QUOTA_GET_RESULT_UNLIMITED " + "while getting resource %s from box %s", + root->backend.name, name, mailbox_name); + else if (ret != QUOTA_GET_RESULT_LIMITED) { + *error_r = t_strdup_printf( + "quota-%s: %s", root->set->backend->name, error); + return ret; + } + + if (quota_root_get_rule_limits(root, mailbox_name, + &bytes_limit, &count_limit, + &ignored, &error) < 0) { + *error_r = t_strdup_printf( + "Failed to get quota root rule limits for mailbox %s: %s", + mailbox_name, error); + return QUOTA_GET_RESULT_INTERNAL_ERROR; + } + + if (strcmp(name, QUOTA_NAME_STORAGE_BYTES) == 0) + *limit_r = bytes_limit; + else if (strcmp(name, QUOTA_NAME_MESSAGES) == 0) + *limit_r = count_limit; + else + *limit_r = 0; + + if (kilobytes) { + *value_r = (*value_r + 1023) / 1024; + *limit_r = (*limit_r + 1023) / 1024; + } + return *limit_r == 0 ? QUOTA_GET_RESULT_UNLIMITED : QUOTA_GET_RESULT_LIMITED; +} + +int quota_set_resource(struct quota_root *root, const char *name, + uint64_t value, const char **client_error_r) +{ + struct dict_transaction_context *trans; + const char *key, *error; + const struct dict_op_settings *set; + + if (root->set->limit_set == NULL) { + *client_error_r = MAIL_ERRSTR_NO_PERMISSION; + return -1; + } + if (strcasecmp(name, QUOTA_NAME_STORAGE_KILOBYTES) == 0) + key = "storage"; + else if (strcasecmp(name, QUOTA_NAME_STORAGE_BYTES) == 0) + key = "bytes"; + else if (strcasecmp(name, QUOTA_NAME_MESSAGES) == 0) + key = "messages"; + else { + *client_error_r = t_strdup_printf( + "Unsupported resource name: %s", name); + return -1; + } + + if (root->limit_set_dict == NULL) { + struct dict_settings set; + + i_zero(&set); + set.base_dir = root->quota->user->set->base_dir; + set.event_parent = root->quota->user->event; + if (dict_init(root->set->limit_set, &set, + &root->limit_set_dict, &error) < 0) { + e_error(root->quota->event, + "dict_init() failed: %s", error); + *client_error_r = "Internal quota limit update error"; + return -1; + } + } + + set = mail_user_get_dict_op_settings(root->ns->user); + trans = dict_transaction_begin(root->limit_set_dict, set); + key = t_strdup_printf(QUOTA_LIMIT_SET_PATH"%s", key); + dict_set(trans, key, dec2str(value)); + if (dict_transaction_commit(&trans, &error) < 0) { + e_error(root->quota->event, + "dict_transaction_commit() failed: %s", error); + *client_error_r = "Internal quota limit update error"; + return -1; + } + return 0; +} + +struct quota_transaction_context *quota_transaction_begin(struct mailbox *box) +{ + struct quota_transaction_context *ctx; + struct quota_root *const *rootp; + const struct quota_rule *rule; + const char *mailbox_name; + + ctx = i_new(struct quota_transaction_context, 1); + ctx->quota = box->list->ns->owner != NULL ? + quota_get_mail_user_quota(box->list->ns->owner) : + quota_get_mail_user_quota(box->list->ns->user); + i_assert(ctx->quota != NULL); + + ctx->box = box; + ctx->bytes_ceil = (uint64_t)-1; + ctx->bytes_ceil2 = (uint64_t)-1; + ctx->count_ceil = (uint64_t)-1; + + mailbox_name = mailbox_get_vname(box); + (void)mail_namespace_find_unalias(box->storage->user->namespaces, + &mailbox_name); + + ctx->auto_updating = TRUE; + array_foreach(&ctx->quota->roots, rootp) { + if (!quota_root_is_visible(*rootp, ctx->box)) + continue; + + rule = quota_root_rule_find((*rootp)->set, mailbox_name); + if (rule != NULL && rule->ignore) { + /* This mailbox isn't included in quota. This means + it's also not included in quota_warnings, so make + sure it's fully ignored. */ + continue; + } + + /* If there are reverse quota_warnings, we'll need to track + how many bytes were expunged even with auto_updating roots. + (An alternative could be to get the current quota usage + before and after the expunges, but that's more complicated + and probably isn't any better.) */ + if (!(*rootp)->auto_updating || + (*rootp)->set->have_reverse_warnings) + ctx->auto_updating = FALSE; + } + + if (box->storage->user->dsyncing) { + /* ignore quota for dsync */ + ctx->limits_set = TRUE; + } + return ctx; +} + +int quota_transaction_set_limits(struct quota_transaction_context *ctx, + enum quota_get_result *error_result_r, + const char **error_r) +{ + struct quota_root *const *roots; + const char *mailbox_name, *error; + unsigned int i, count; + uint64_t bytes_limit, count_limit, current, limit, diff; + bool use_grace, ignored; + enum quota_get_result ret; + + if (ctx->limits_set) + return 0; + ctx->limits_set = TRUE; + mailbox_name = mailbox_get_vname(ctx->box); + /* use quota_grace only for LDA/LMTP */ + use_grace = (ctx->box->flags & MAILBOX_FLAG_POST_SESSION) != 0; + ctx->no_quota_updates = TRUE; + + /* find the lowest quota limits from all roots and use them */ + roots = array_get(&ctx->quota->roots, &count); + for (i = 0; i < count; i++) { + /* make sure variables get initialized */ + bytes_limit = count_limit = 0; + if (!quota_root_is_visible(roots[i], ctx->box)) + continue; + else if (roots[i]->no_enforcing) { + ignored = FALSE; + } else if (quota_root_get_rule_limits(roots[i], mailbox_name, + &bytes_limit, &count_limit, + &ignored, &error) < 0) { + ctx->failed = TRUE; + *error_result_r = QUOTA_GET_RESULT_INTERNAL_ERROR; + *error_r = t_strdup_printf( + "Failed to get quota root rule limits for %s: %s", + mailbox_name, error); + return -1; + } + if (!ignored) + ctx->no_quota_updates = FALSE; + + if (bytes_limit > 0) { + ret = quota_get_resource(roots[i], mailbox_name, + QUOTA_NAME_STORAGE_BYTES, + ¤t, &limit, &error); + if (ret == QUOTA_GET_RESULT_LIMITED) { + if (limit <= current) { + /* over quota */ + ctx->bytes_ceil = 0; + ctx->bytes_ceil2 = 0; + diff = current - limit; + if (ctx->bytes_over < diff) + ctx->bytes_over = diff; + } else { + diff = limit - current; + if (ctx->bytes_ceil2 > diff) + ctx->bytes_ceil2 = diff; + diff += !use_grace ? 0 : + roots[i]->set->last_mail_max_extra_bytes; + if (ctx->bytes_ceil > diff) + ctx->bytes_ceil = diff; + } + } else if (ret <= QUOTA_GET_RESULT_INTERNAL_ERROR) { + ctx->failed = TRUE; + *error_result_r = ret; + *error_r = t_strdup_printf( + "Failed to get quota resource " + QUOTA_NAME_STORAGE_BYTES" for %s: %s", + mailbox_name, error); + return -1; + } + } + + if (count_limit > 0) { + ret = quota_get_resource(roots[i], mailbox_name, + QUOTA_NAME_MESSAGES, + ¤t, &limit, &error); + if (ret == QUOTA_GET_RESULT_LIMITED) { + if (limit <= current) { + /* over quota */ + ctx->count_ceil = 0; + diff = current - limit; + if (ctx->count_over < diff) + ctx->count_over = diff; + } else { + diff = limit - current; + if (ctx->count_ceil > diff) + ctx->count_ceil = diff; + } + } else if (ret <= QUOTA_GET_RESULT_INTERNAL_ERROR) { + ctx->failed = TRUE; + *error_result_r = ret; + *error_r = t_strdup_printf( + "Failed to get quota resource " + QUOTA_NAME_MESSAGES" for %s: %s", + mailbox_name, error); + return -1; + } + } + } + return 0; +} + +static void quota_warning_execute(struct quota_root *root, const char *cmd, + const char *last_arg, const char *reason) +{ + const char *socket_path, *const *args, *error, *scheme, *ptr; + + struct program_client_settings set = { + .client_connect_timeout_msecs = 1000, + .debug = root->quota->user->mail_debug, + }; + struct program_client *pc; + + restrict_access_init(&set.restrict_set); + + e_debug(root->quota->event, "Executing warning: %s (because %s)", cmd, reason); + + args = t_strsplit_spaces(cmd, " "); + if (last_arg != NULL) { + unsigned int count = str_array_length(args); + const char **new_args = t_new(const char *, count + 2); + + memcpy(new_args, args, sizeof(const char *) * count); + new_args[count] = last_arg; + args = new_args; + } + socket_path = args[0]; + + if ((ptr = strchr(socket_path, ':')) != NULL) { + scheme = t_strcut(socket_path, ':'); + socket_path = ptr+1; + } else { + scheme = "unix"; + } + + if (*socket_path != '/' && + strcmp(scheme, "unix") == 0) + socket_path = + t_strconcat(root->quota->user->set->base_dir, + "/", socket_path, NULL); + + socket_path = t_strdup_printf("%s:%s", scheme, socket_path); + + args++; + + if (program_client_create(socket_path, args, &set, TRUE, + &pc, &error) < 0) { + e_error(root->quota->event, + "program_client_create(%s) failed: %s", socket_path, + error); + return; + } + + (void)program_client_run(pc); + + program_client_destroy(&pc); +} + +static void quota_warnings_execute(struct quota_transaction_context *ctx, + struct quota_root *root) +{ + struct quota_warning_rule *warnings; + unsigned int i, count; + uint64_t bytes_current, bytes_before, bytes_limit; + uint64_t count_current, count_before, count_limit; + const char *reason, *error; + + warnings = array_get_modifiable(&root->set->warning_rules, &count); + if (count == 0) + return; + + if (quota_get_resource(root, "", QUOTA_NAME_STORAGE_BYTES, + &bytes_current, &bytes_limit, &error) == QUOTA_GET_RESULT_INTERNAL_ERROR) { + e_error(root->quota->event, + "Failed to get quota resource "QUOTA_NAME_STORAGE_BYTES + ": %s", error); + return; + } + if (quota_get_resource(root, "", QUOTA_NAME_MESSAGES, + &count_current, &count_limit, &error) == QUOTA_GET_RESULT_INTERNAL_ERROR) { + e_error(root->quota->event, + "Failed to get quota resource "QUOTA_NAME_MESSAGES + ": %s", error); + return; + } + + if (ctx->bytes_used > 0 && bytes_current < (uint64_t)ctx->bytes_used) + bytes_before = 0; + else + bytes_before = (int64_t)bytes_current - ctx->bytes_used; + + if (ctx->count_used > 0 && count_current < (uint64_t)ctx->count_used) + count_before = 0; + else + count_before = (int64_t)count_current - ctx->count_used; + for (i = 0; i < count; i++) { + if (quota_warning_match(&warnings[i], + bytes_before, bytes_current, + count_before, count_current, + &reason)) { + quota_warning_execute(root, warnings[i].command, + NULL, reason); + break; + } + } +} + +int quota_transaction_commit(struct quota_transaction_context **_ctx) +{ + struct quota_transaction_context *ctx = *_ctx; + struct quota_rule *rule; + struct quota_root *const *roots; + unsigned int i, count; + const char *mailbox_name; + int ret = 0; + + *_ctx = NULL; + + if (ctx->failed) + ret = -1; + else if (ctx->bytes_used != 0 || ctx->count_used != 0 || + ctx->recalculate != QUOTA_RECALCULATE_DONT) T_BEGIN { + ARRAY(struct quota_root *) warn_roots; + + mailbox_name = mailbox_get_vname(ctx->box); + (void)mail_namespace_find_unalias( + ctx->box->storage->user->namespaces, &mailbox_name); + + roots = array_get(&ctx->quota->roots, &count); + t_array_init(&warn_roots, count); + for (i = 0; i < count; i++) { + if (!quota_root_is_visible(roots[i], ctx->box)) + continue; + + rule = quota_root_rule_find(roots[i]->set, + mailbox_name); + if (rule != NULL && rule->ignore) { + /* mailbox not included in quota */ + continue; + } + + const char *error; + if (roots[i]->backend.v.update(roots[i], ctx, &error) < 0) { + e_error(ctx->quota->event, + "Failed to update quota for %s: %s", + mailbox_name, error); + ret = -1; + } + else if (!ctx->sync_transaction) + array_push_back(&warn_roots, &roots[i]); + } + /* execute quota warnings after all updates. this makes it + work correctly regardless of whether backend.get_resource() + returns updated values before backend.update() or not. + warnings aren't executed when dsync bring the user over, + because the user probably already got the warning on the + other replica. */ + array_foreach(&warn_roots, roots) + quota_warnings_execute(ctx, *roots); + } T_END; + + i_free(ctx); + return ret; +} + +static bool quota_over_flag_init_root(struct quota_root *root, + const char **quota_over_script_r, + const char **quota_over_flag_r, + bool *status_r) +{ + const char *name, *flag_mask; + + *quota_over_flag_r = NULL; + *status_r = FALSE; + + name = t_strconcat(root->set->set_name, "_over_script", NULL); + *quota_over_script_r = mail_user_plugin_getenv(root->quota->user, name); + if (*quota_over_script_r == NULL) { + e_debug(root->quota->event, "quota_over_flag check: " + "%s unset - skipping", name); + return FALSE; + } + + /* e.g.: quota_over_flag_value=TRUE or quota_over_flag_value=* */ + name = t_strconcat(root->set->set_name, "_over_flag_value", NULL); + flag_mask = mail_user_plugin_getenv(root->quota->user, name); + if (flag_mask == NULL) { + e_debug(root->quota->event, "quota_over_flag check: " + "%s unset - skipping", name); + return FALSE; + } + + /* compare quota_over_flag's value (that comes from userdb) to + quota_over_flag_value and save the result. */ + name = t_strconcat(root->set->set_name, "_over_flag", NULL); + *quota_over_flag_r = mail_user_plugin_getenv(root->quota->user, name); + *status_r = *quota_over_flag_r != NULL && + wildcard_match_icase(*quota_over_flag_r, flag_mask); + return TRUE; +} + +static void quota_over_flag_check_root(struct quota_root *root) +{ + const char *quota_over_script, *quota_over_flag, *error; + const char *const *resources; + unsigned int i; + uint64_t value, limit; + bool cur_overquota = FALSE; + bool quota_over_status; + enum quota_get_result ret; + + if (root->quota_over_flag_checked) + return; + if (root->quota->user->session_create_time + + QUOTA_OVER_FLAG_MAX_DELAY_SECS < ioloop_time) { + /* userdb's quota_over_flag lookup is too old. */ + e_debug(root->quota->event, "quota_over_flag check: " + "Flag lookup time is too old - skipping"); + return; + } + if (root->quota->user->session_restored) { + /* we don't know whether the quota_over_script was executed + before hibernation. just assume that it was, so we don't + unnecessarily call it too often. */ + e_debug(root->quota->event, "quota_over_flag check: " + "Session was already hibernated - skipping"); + return; + } + root->quota_over_flag_checked = TRUE; + if (!quota_over_flag_init_root(root, "a_over_script, + "a_over_flag, "a_over_status)) + return; + + resources = quota_root_get_resources(root); + for (i = 0; resources[i] != NULL; i++) { + ret = quota_get_resource(root, "", resources[i], &value, + &limit, &error); + if (ret == QUOTA_GET_RESULT_INTERNAL_ERROR) { + /* can't reliably verify this */ + e_error(root->quota->event, "Quota %s lookup failed -" + "can't verify quota_over_flag: %s", + resources[i], error); + return; + } + e_debug(root->quota->event, "quota_over_flag check: %s ret=%d" + "value=%"PRIu64" limit=%"PRIu64, resources[i], ret, + value, limit); + if (ret == QUOTA_GET_RESULT_LIMITED && value >= limit) + cur_overquota = TRUE; + } + e_debug(root->quota->event, "quota_over_flag=%d(%s) vs currently overquota=%d", + quota_over_status ? 1 : 0, + quota_over_flag == NULL ? "(null)" : quota_over_flag, + cur_overquota ? 1 : 0); + if (cur_overquota != quota_over_status) { + quota_warning_execute(root, quota_over_script, quota_over_flag, + "quota_over_flag mismatch"); + } +} + +void quota_over_flag_check_startup(struct quota *quota) +{ + struct quota_root *const *roots; + unsigned int i, count; + const char *name; + + roots = array_get("a->roots, &count); + for (i = 0; i < count; i++) { + name = t_strconcat(roots[i]->set->set_name, "_over_flag_lazy_check", NULL); + if (!mail_user_plugin_getenv_bool(roots[i]->quota->user, name)) + quota_over_flag_check_root(roots[i]); + } +} + +void quota_transaction_rollback(struct quota_transaction_context **_ctx) +{ + struct quota_transaction_context *ctx = *_ctx; + + *_ctx = NULL; + i_free(ctx); +} + +static int quota_get_mail_size(struct quota_transaction_context *ctx, + struct mail *mail, uoff_t *size_r) +{ + if (ctx->quota->set->vsizes) + return mail_get_virtual_size(mail, size_r); + else + return mail_get_physical_size(mail, size_r); +} + +enum quota_alloc_result quota_try_alloc(struct quota_transaction_context *ctx, + struct mail *mail, const char **error_r) +{ + uoff_t size; + const char *error; + enum quota_get_result error_res; + + if (quota_transaction_set_limits(ctx, &error_res, error_r) < 0) { + if (error_res == QUOTA_GET_RESULT_BACKGROUND_CALC) + return QUOTA_ALLOC_RESULT_BACKGROUND_CALC; + return QUOTA_ALLOC_RESULT_TEMPFAIL; + } + + if (ctx->no_quota_updates) + return QUOTA_ALLOC_RESULT_OK; + + if (quota_get_mail_size(ctx, mail, &size) < 0) { + enum mail_error err; + error = mailbox_get_last_internal_error(mail->box, &err); + + if (err == MAIL_ERROR_EXPUNGED) { + /* mail being copied was already expunged. it'll fail, + so just return success for the quota allocated. */ + return QUOTA_ALLOC_RESULT_OK; + } + *error_r = t_strdup_printf( + "Failed to get mail size (box=%s, uid=%u): %s", + mail->box->vname, mail->uid, error); + return QUOTA_ALLOC_RESULT_TEMPFAIL; + } + + enum quota_alloc_result ret = quota_test_alloc(ctx, size, error_r); + if (ret != QUOTA_ALLOC_RESULT_OK) + return ret; + /* with quota_try_alloc() we want to keep track of how many bytes + we've been adding/removing, so disable auto_updating=TRUE + optimization. this of course doesn't work perfectly if + quota_alloc() or quota_free_bytes() was already used within the same + transaction, but that doesn't normally happen. */ + ctx->auto_updating = FALSE; + quota_alloc(ctx, mail); + return QUOTA_ALLOC_RESULT_OK; +} + +enum quota_alloc_result quota_test_alloc(struct quota_transaction_context *ctx, + uoff_t size, const char **error_r) +{ + if (ctx->failed) { + *error_r = "Quota transaction has failed earlier"; + return QUOTA_ALLOC_RESULT_TEMPFAIL; + } + + enum quota_get_result error_res; + if (quota_transaction_set_limits(ctx, &error_res, error_r) < 0) { + if (error_res == QUOTA_GET_RESULT_BACKGROUND_CALC) + return QUOTA_ALLOC_RESULT_BACKGROUND_CALC; + return QUOTA_ALLOC_RESULT_TEMPFAIL; + } + + uoff_t max_size = ctx->quota->set->max_mail_size; + if (max_size > 0 && size > max_size) { + *error_r = t_strdup_printf( + "Requested allocation size %"PRIuUOFF_T" exceeds max " + "mail size %"PRIuUOFF_T, size, max_size); + return QUOTA_ALLOC_RESULT_OVER_MAXSIZE; + } + + if (ctx->no_quota_updates) + return QUOTA_ALLOC_RESULT_OK; + /* this is a virtual function mainly for trash plugin and similar, + which may automatically delete mails to stay under quota. */ + return ctx->quota->set->test_alloc(ctx, size, error_r); +} + +static enum quota_alloc_result quota_default_test_alloc( + struct quota_transaction_context *ctx, uoff_t size, + const char **error_r) +{ + struct quota_root *const *roots; + unsigned int i, count; + bool ignore; + int ret; + + if (!quota_transaction_is_over(ctx, size)) + return QUOTA_ALLOC_RESULT_OK; + + /* limit reached. */ + roots = array_get(&ctx->quota->roots, &count); + for (i = 0; i < count; i++) { + uint64_t bytes_limit, count_limit; + + if (!quota_root_is_visible(roots[i], ctx->box) || + roots[i]->no_enforcing) + continue; + + const char *error; + ret = quota_root_get_rule_limits(roots[i], + mailbox_get_vname(ctx->box), + &bytes_limit, &count_limit, + &ignore, &error); + if (ret < 0) { + *error_r = t_strdup_printf( + "Failed to get quota root rule limits: %s", + error); + return QUOTA_ALLOC_RESULT_TEMPFAIL; + } + + /* if size is bigger than any limit, then + it is bigger than the lowest limit */ + if (bytes_limit > 0 && size > bytes_limit) { + *error_r = t_strdup_printf( + "Allocating %"PRIuUOFF_T" bytes would exceed quota limit", + size); + return QUOTA_ALLOC_RESULT_OVER_QUOTA_LIMIT; + } + } + *error_r = t_strdup_printf( + "Allocating %"PRIuUOFF_T" bytes would exceed quota", size); + return QUOTA_ALLOC_RESULT_OVER_QUOTA; +} + +void quota_alloc(struct quota_transaction_context *ctx, struct mail *mail) +{ + uoff_t size; + + if (!ctx->auto_updating) { + if (quota_get_mail_size(ctx, mail, &size) == 0) + ctx->bytes_used += size; + } + + ctx->bytes_ceil = ctx->bytes_ceil2; + ctx->count_used++; +} + +void quota_free_bytes(struct quota_transaction_context *ctx, + uoff_t physical_size) +{ + i_assert(physical_size <= INT64_MAX); + ctx->bytes_used -= (int64_t)physical_size; + ctx->count_used--; +} + +void quota_recalculate(struct quota_transaction_context *ctx, + enum quota_recalculate recalculate) +{ + ctx->recalculate = recalculate; +} + +static void hidden_param_handler(struct quota_root *_root, const char *param_value ATTR_UNUSED) +{ + _root->hidden = TRUE; +} + +static void ignoreunlim_param_handler(struct quota_root *_root, const char *param_value ATTR_UNUSED) +{ + _root->disable_unlimited_tracking = TRUE; +} + +static void noenforcing_param_handler(struct quota_root *_root, const char *param_value ATTR_UNUSED) +{ + _root->no_enforcing = TRUE; +} + +static void ns_param_handler(struct quota_root *_root, const char *param_value) +{ + _root->ns_prefix = p_strdup(_root->pool, param_value); +} + +int quota_parse_parameters(struct quota_root *root, const char **args, const char **error_r, + const struct quota_param_parser *valid_params, bool fail_on_unknown) +{ + const char *tmp_param_name, *tmp_param_val; + size_t tmp_param_len; + + while (*args != NULL && (*args)[0] != '\0') { + for (; valid_params->param_name != NULL; ++valid_params) { + tmp_param_name = valid_params->param_name; + tmp_param_len = strlen(valid_params->param_name); + i_assert(*args != NULL); + if (strncmp(*args, tmp_param_name, tmp_param_len) == 0) { + tmp_param_val = NULL; + *args += tmp_param_len; + if (tmp_param_name[tmp_param_len - 1] == '=') { + const char *next_colon = strchr(*args, ':'); + tmp_param_val = (next_colon == NULL)? + t_strdup(*args): + t_strdup_until(*args, next_colon); + *args = (next_colon == NULL) ? NULL : next_colon + 1; + } + else if ((*args)[0] == '\0' || + (*args)[0] == ':') { + *args = ((*args)[0] == ':') ? *args + 1 : NULL; + /* in case parameter is a boolean second parameter + * string parameter value will be ignored by param_handler + * we just need some non-NULL value + * to indicate that argument is to be processed */ + tmp_param_val = ""; + } + if (tmp_param_val != NULL) { + valid_params->param_handler(root, tmp_param_val); + break; + } + } + } + if (valid_params->param_name == NULL) { + if (fail_on_unknown) { + *error_r = t_strdup_printf( + "Unknown parameter for backend %s: %s", + root->backend.name, *args); + return -1; + } + else { + break; + } + } + } + return 0; +} |