diff options
Diffstat (limited to 'src/plugins/trash/trash-plugin.c')
-rw-r--r-- | src/plugins/trash/trash-plugin.c | 392 |
1 files changed, 392 insertions, 0 deletions
diff --git a/src/plugins/trash/trash-plugin.c b/src/plugins/trash/trash-plugin.c new file mode 100644 index 0000000..d918484 --- /dev/null +++ b/src/plugins/trash/trash-plugin.c @@ -0,0 +1,392 @@ +/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "unichar.h" +#include "istream.h" +#include "mail-namespace.h" +#include "mail-search-build.h" +#include "quota-private.h" +#include "quota-plugin.h" +#include "trash-plugin.h" + +#include <unistd.h> +#include <fcntl.h> + +#define INIT_TRASH_MAILBOX_COUNT 4 +#define MAX_RETRY_COUNT 3 + +#define TRASH_USER_CONTEXT(obj) \ + MODULE_CONTEXT(obj, trash_user_module) +#define TRASH_USER_CONTEXT_REQUIRE(obj) \ + MODULE_CONTEXT_REQUIRE(obj, trash_user_module) + +struct trash_mailbox { + const char *name; + int priority; /* lower number = higher priority */ + + struct mail_namespace *ns; + + /* temporarily set while cleaning: */ + struct mailbox *box; + struct mailbox_transaction_context *trans; + struct mail_search_context *search_ctx; + struct mail *mail; +}; + +struct trash_user { + union mail_user_module_context module_ctx; + + const char *config_file; + /* ordered by priority, highest first */ + ARRAY(struct trash_mailbox) trash_boxes; +}; + +const char *trash_plugin_version = DOVECOT_ABI_VERSION; + +static MODULE_CONTEXT_DEFINE_INIT(trash_user_module, + &mail_user_module_register); +static enum quota_alloc_result (*trash_next_quota_test_alloc)( + struct quota_transaction_context *, uoff_t, + const char **error_r); + +static int trash_clean_mailbox_open(struct trash_mailbox *trash) +{ + struct mail_search_args *search_args; + + trash->box = mailbox_alloc(trash->ns->list, trash->name, 0); + if (mailbox_open(trash->box) < 0) { + mailbox_free(&trash->box); + return 0; + } + + if (mailbox_sync(trash->box, MAILBOX_SYNC_FLAG_FULL_READ) < 0) + return -1; + + trash->trans = mailbox_transaction_begin(trash->box, 0, __func__); + + search_args = mail_search_build_init(); + mail_search_build_add_all(search_args); + trash->search_ctx = mailbox_search_init(trash->trans, + search_args, NULL, + MAIL_FETCH_PHYSICAL_SIZE | + MAIL_FETCH_RECEIVED_DATE, NULL); + mail_search_args_unref(&search_args); + + return mailbox_search_next(trash->search_ctx, &trash->mail) ? 1 : 0; +} + +static int trash_clean_mailbox_get_next(struct trash_mailbox *trash, + time_t *received_time_r) +{ + int ret; + + if (trash->mail == NULL) { + if (trash->box == NULL) + ret = trash_clean_mailbox_open(trash); + else { + ret = mailbox_search_next(trash->search_ctx, + &trash->mail) ? 1 : 0; + } + if (ret <= 0) { + *received_time_r = 0; + return ret; + } + } + + if (mail_get_received_date(trash->mail, received_time_r) < 0) + return -1; + return 1; +} + +static int trash_try_clean_mails(struct quota_transaction_context *ctx, + uint64_t size_needed, + unsigned int count_needed) +{ + struct trash_user *tuser = TRASH_USER_CONTEXT_REQUIRE(ctx->quota->user); + struct trash_mailbox *trashes; + struct event_reason *reason; + unsigned int i, j, count, oldest_idx; + time_t oldest, received = 0; + uint64_t size, size_expunged = 0; + unsigned int expunged_count = 0; + int ret = 0; + + reason = event_reason_begin("trash:clean"); + + trashes = array_get_modifiable(&tuser->trash_boxes, &count); + for (i = 0; i < count; ) { + /* expunge oldest mails first in all trash boxes with + same priority */ + oldest_idx = count; + oldest = (time_t)-1; + for (j = i; j < count; j++) { + if (trashes[j].priority != trashes[i].priority) + break; + + ret = trash_clean_mailbox_get_next(&trashes[j], + &received); + if (ret < 0) + goto err; + if (ret > 0) { + if (oldest == (time_t)-1 || received < oldest) { + oldest = received; + oldest_idx = j; + } + } + } + + if (oldest_idx < count) { + if (mail_get_physical_size(trashes[oldest_idx].mail, + &size) < 0) { + /* maybe expunged already? */ + trashes[oldest_idx].mail = NULL; + continue; + } + + mail_expunge(trashes[oldest_idx].mail); + expunged_count++; + size_expunged += size; + if (size_expunged >= size_needed && + expunged_count >= count_needed) + break; + trashes[oldest_idx].mail = NULL; + } else { + /* find more mails from next priority's mailbox */ + i = j; + } + } + +err: + for (i = 0; i < count; i++) { + struct trash_mailbox *trash = &trashes[i]; + + if (trash->box == NULL) + continue; + + trash->mail = NULL; + (void)mailbox_search_deinit(&trash->search_ctx); + + if (size_expunged >= size_needed && + expunged_count >= count_needed) { + (void)mailbox_transaction_commit(&trash->trans); + (void)mailbox_sync(trash->box, 0); + } else { + /* couldn't get enough space, don't expunge anything */ + mailbox_transaction_rollback(&trash->trans); + } + + mailbox_free(&trash->box); + } + event_reason_end(&reason); + + if (size_expunged < size_needed) { + e_debug(ctx->quota->user->event, + "trash plugin: Failed to remove enough messages " + "(needed %"PRIu64" bytes, expunged only %"PRIu64" bytes)", + size_needed, size_expunged); + return 0; + } + if (expunged_count < count_needed) { + e_debug(ctx->quota->user->event, + "trash plugin: Failed to remove enough messages " + "(needed %u messages, expunged only %u messages)", + count_needed, expunged_count); + return 0; + } + + if (ctx->bytes_over > 0) { + /* user is over quota. drop the over-bytes first. */ + i_assert(ctx->bytes_over <= size_expunged); + size_expunged -= ctx->bytes_over; + ctx->bytes_over = 0; + } + if (ctx->count_over > 0) { + /* user is over quota. drop the over-count first. */ + i_assert(ctx->count_over <= expunged_count); + expunged_count -= ctx->count_over; + ctx->count_over = 0; + } + + if (ctx->bytes_ceil > ((uint64_t)-1 - size_expunged)) { + ctx->bytes_ceil = (uint64_t)-1; + } else { + ctx->bytes_ceil += size_expunged; + } + if (ctx->count_ceil < ((uint64_t)-1 - expunged_count)) { + ctx->count_ceil = (uint64_t)-1; + } else { + ctx->count_ceil += expunged_count; + } + return 1; +} + +static enum quota_alloc_result +trash_quota_test_alloc(struct quota_transaction_context *ctx, + uoff_t size, const char **error_r) +{ + int i; + uint64_t size_needed = 0; + unsigned int count_needed = 0; + + for (i = 0; ; i++) { + enum quota_alloc_result ret; + ret = trash_next_quota_test_alloc(ctx, size, error_r); + if (ret != QUOTA_ALLOC_RESULT_OVER_QUOTA) { + if (ret == QUOTA_ALLOC_RESULT_OVER_QUOTA_LIMIT && + ctx->quota->user->mail_debug) + i_debug("trash plugin: Mail is larger than " + "quota, won't even try to handle"); + return ret; + } + + if (i == MAX_RETRY_COUNT) { + /* trash_try_clean_mails() should have returned 0 if + it couldn't get enough space, but allow retrying + it a couple of times if there was some extra space + that was needed.. */ + break; + } + + if (ctx->bytes_ceil != (uint64_t)-1 && + ctx->bytes_ceil < size + ctx->bytes_over) + size_needed = size + ctx->bytes_over - ctx->bytes_ceil; + if (ctx->count_ceil != (uint64_t)-1 && + ctx->count_ceil < 1 + ctx->count_over) + count_needed = 1 + ctx->count_over - ctx->count_ceil; + + /* not enough space. try deleting some from mailbox. */ + if (trash_try_clean_mails(ctx, size_needed, count_needed) <= 0) { + *error_r = t_strdup_printf( + "Allocating %"PRIuUOFF_T" bytes would exceed quota", size); + return QUOTA_ALLOC_RESULT_OVER_QUOTA; + } + } + *error_r = t_strdup_printf( + "Allocating %"PRIuUOFF_T" bytes would exceed quota", size); + return QUOTA_ALLOC_RESULT_OVER_QUOTA; +} + +static bool trash_find_storage(struct mail_user *user, + struct trash_mailbox *trash) +{ + struct mail_namespace *ns; + + ns = mail_namespace_find(user->namespaces, trash->name); + if ((ns->flags & NAMESPACE_FLAG_UNUSABLE) != 0) + return FALSE; + + trash->ns = ns; + return TRUE; +} + +static int trash_mailbox_priority_cmp(const struct trash_mailbox *t1, + const struct trash_mailbox *t2) +{ + return t1->priority - t2->priority; +} + +static int read_configuration(struct mail_user *user, const char *path) +{ + struct trash_user *tuser = TRASH_USER_CONTEXT_REQUIRE(user); + struct istream *input; + const char *line, *name; + struct trash_mailbox *trash; + int fd, ret = 0; + + fd = open(path, O_RDONLY); + if (fd == -1) { + i_error("trash plugin: open(%s) failed: %m", path); + return -1; + } + + p_array_init(&tuser->trash_boxes, user->pool, INIT_TRASH_MAILBOX_COUNT); + + input = i_stream_create_fd(fd, SIZE_MAX); + i_stream_set_return_partial_line(input, TRUE); + while ((line = i_stream_read_next_line(input)) != NULL) { + /* <priority> <mailbox name> */ + name = strchr(line, ' '); + if (name == NULL || name[1] == '\0' || *line == '#') + continue; + + trash = array_append_space(&tuser->trash_boxes); + trash->name = p_strdup(user->pool, name+1); + if (str_to_int(t_strdup_until(line, name), + &trash->priority) < 0) { + i_error("trash: Invalid priority for mailbox '%s'", + trash->name); + ret = -1; + } + + if (!uni_utf8_str_is_valid(trash->name)) { + i_error("trash: Mailbox name not UTF-8: %s", + trash->name); + ret = -1; + } + if (!trash_find_storage(user, trash)) { + i_error("trash: Namespace not found for mailbox '%s'", + trash->name); + ret = -1; + } + + e_debug(user->event, "trash plugin: Added '%s' with priority %d", + trash->name, trash->priority); + } + i_stream_destroy(&input); + i_close_fd(&fd); + + array_sort(&tuser->trash_boxes, trash_mailbox_priority_cmp); + return ret; +} + +static void +trash_mail_user_created(struct mail_user *user) +{ + struct quota_user *quser = QUOTA_USER_CONTEXT(user); + struct trash_user *tuser; + const char *env; + + env = mail_user_plugin_getenv(user, "trash"); + if (env == NULL) { + e_debug(user->event, "trash: No trash setting - plugin disabled"); + } else if (quser == NULL) { + i_error("trash plugin: quota plugin not initialized"); + } else { + tuser = p_new(user->pool, struct trash_user, 1); + tuser->config_file = env; + MODULE_CONTEXT_SET(user, trash_user_module, tuser); + } +} + +static void +trash_mail_namespaces_created(struct mail_namespace *namespaces) +{ + struct mail_user *user = namespaces->user; + struct trash_user *tuser = TRASH_USER_CONTEXT(user); + struct quota_user *quser = QUOTA_USER_CONTEXT(user); + + if (tuser != NULL && read_configuration(user, tuser->config_file) == 0) { + i_assert(quser != NULL); + trash_next_quota_test_alloc = + quser->quota->set->test_alloc; + quser->quota->set->test_alloc = trash_quota_test_alloc; + } +} + +static struct mail_storage_hooks trash_mail_storage_hooks = { + .mail_user_created = trash_mail_user_created, + .mail_namespaces_created = trash_mail_namespaces_created, +}; + +void trash_plugin_init(struct module *module) +{ + mail_storage_hooks_add(module, &trash_mail_storage_hooks); +} + +void trash_plugin_deinit(void) +{ + mail_storage_hooks_remove(&trash_mail_storage_hooks); +} + +const char *trash_plugin_dependencies[] = { "quota", NULL }; |