/* Copyright (C) 2010 Timo Sirainen, LGPLv2.1 */ /* export DOVECOT=~/src/dovecot-1.2.0 gcc -fPIC -shared -g -Wall -I$DOVECOT -I$DOVECOT/src/lib \ -I$DOVECOT/src/lib-storage -I$DOVECOT/src/lib-mail -I$DOVECOT/src/lib-index \ -I$DOVECOT/src/lib-imap -DHAVE_CONFIG_H \ pop3-throttle-plugin.c -o pop3_throttle_plugin.so */ #include "lib.h" #include "array.h" #include "hex-dec.h" #include "read-full.h" #include "write-full.h" #include "file-dotlock.h" #include "mail-storage-private.h" #include "mail-namespace.h" #include "mail-search-build.h" #include #include #include #include #define POP3_THROTTLE_ENABLED_PATH "/etc/dovecot/pop3-throttle-enabled" #define POP3_THROTTLE_STATE_FNAME "pop3-throttle.db" /* reset state every n seconds */ #define POP3_THROTTLE_STATE_RESET_SECS (60*15) #define POP3_THROTTLE_CONTEXT(obj) \ MODULE_CONTEXT(obj, pop3_throttle_storage_module) struct pop3_throttle_state { uint32_t uid_validity; uint32_t highest_visible_uid; uint32_t start_time; uint32_t fetch_msgs; uint32_t fetch_kbytes; }; #define POP3_THROTTLE_FILE_SIZE \ (sizeof(struct pop3_throttle_state) / \ sizeof(uint32_t) * (sizeof(uint32_t)*2 + 1)) struct pop3_throttle_mailbox { union mailbox_module_context module_ctx; uint32_t session_highest_visible_seq; struct pop3_throttle_state orig_state, state; }; const char *pop3_throttle_plugin_version = PACKAGE_VERSION; static const struct dotlock_settings dotlock_set = { .timeout = 2, .stale_timeout = 60 }; static void (*next_hook_mailbox_opened)(struct mailbox *box); static MODULE_CONTEXT_DEFINE_INIT(pop3_throttle_storage_module, &mail_storage_module_register); /* allow max. n new messages per every state unit (until state is reset) */ static unsigned int set_max_msgs_per_unit; /* allow max. n kbytes of new messages per every state unit */ static unsigned int set_max_kbytes_per_unit; static bool mail_debug; static const char *pop3_throttle_get_state_path(struct mailbox *box) { const char *dir; dir = mailbox_list_get_path(box->storage->list, NULL, MAILBOX_LIST_PATH_TYPE_CONTROL); return t_strconcat(dir, "/"POP3_THROTTLE_STATE_FNAME, NULL); } static void pop3_throttle_read_state(const char *path, struct pop3_throttle_state *state_r) { #define BUF_IDX_OFFSET(idx) \ ((idx)*(sizeof(uint32_t)*2+1)) #define BUF_GET_HEX(buf, idx) \ hex2dec(buf + BUF_IDX_OFFSET(idx++), sizeof(uint32_t)*2) unsigned char buf[POP3_THROTTLE_FILE_SIZE]; unsigned int idx; int fd, ret; memset(state_r, 0, sizeof(*state_r)); fd = open(path, O_RDONLY); if (fd == -1) { if (errno != ENOENT) i_error("open(%s) failed: %m", path); return; } if ((ret = read_full(fd, buf, sizeof(buf))) <= 0) { if (ret < 0) i_error("read(%s) failed: %m", path); else i_error("read(%s) failed: Unexpected EOF", path); } else { idx = 0; state_r->uid_validity = BUF_GET_HEX(buf, idx); state_r->highest_visible_uid = BUF_GET_HEX(buf, idx); state_r->start_time = BUF_GET_HEX(buf, idx); state_r->fetch_msgs = BUF_GET_HEX(buf, idx); state_r->fetch_kbytes = BUF_GET_HEX(buf, idx); i_assert(BUF_IDX_OFFSET(idx) == sizeof(buf)); } if (close(fd) < 0) i_error("close(%s) failed: %m", path); } static int pop3_throttle_write_state(const char *path, const struct pop3_throttle_state *state) { #define BUF_ADD_HEX(buf, dec) \ dec2hex(buffer_append_space_unsafe(buf, sizeof(uint32_t)*2), \ dec, sizeof(uint32_t)*2), buffer_append_c(buf, '\n') struct dotlock *dotlock; buffer_t *buf; int fd; if ((fd = file_dotlock_open(&dotlock_set, path, 0, &dotlock)) < 0) return -1; buf = buffer_create_dynamic(pool_datastack_create(), POP3_THROTTLE_FILE_SIZE); BUF_ADD_HEX(buf, state->uid_validity); BUF_ADD_HEX(buf, state->highest_visible_uid); BUF_ADD_HEX(buf, state->start_time); BUF_ADD_HEX(buf, state->fetch_msgs); BUF_ADD_HEX(buf, state->fetch_kbytes); i_assert(buf->used == POP3_THROTTLE_FILE_SIZE); if (write_full(fd, buf->data, buf->used) < 0) { i_error("write(%s) failed: %m", file_dotlock_get_lock_path(dotlock)); (void)file_dotlock_delete(&dotlock); return -1; } else { return file_dotlock_replace(&dotlock, 0); } } static void pop3_throttle_init_state(struct mailbox *box, struct pop3_throttle_state *state) { struct mailbox_status status; time_t now, expire_time; now = time(NULL); expire_time = state->start_time + POP3_THROTTLE_STATE_RESET_SECS; mailbox_get_status(box, STATUS_UIDVALIDITY | STATUS_UIDNEXT, &status); if (state->uid_validity != status.uidvalidity) { if (mail_debug) { i_info("pop3-throttle: uidvalidity changed %u -> %u", state->uid_validity, status.uidvalidity); } memset(state, 0, sizeof(*state)); state->uid_validity = status.uidvalidity; state->start_time = now; } else if (expire_time < now) { if (mail_debug) { i_info("pop3-throttle: throttle expired %u secs ago, " "reseting state", (unsigned int)(now - expire_time)); } state->start_time = now; state->fetch_msgs = 0; state->fetch_kbytes = 0; } else { if (mail_debug) { i_info("pop3-throttle: throttle expires in %u secs", (unsigned int)(expire_time - now)); } } if (state->highest_visible_uid == 0) { /* we don't know how high uids pop3 clients have seen, so play safe and assume they've seen everything */ state->highest_visible_uid = status.uidnext - 1; if (mail_debug) { i_info("pop3-throttle: previous state unknown, " "assuming client has seen all messages"); } } } static void pop3_throttle_get_visible_msgs(struct mailbox *box) { struct pop3_throttle_mailbox *tbox = POP3_THROTTLE_CONTEXT(box); struct mail_search_args *search_args; struct mailbox_transaction_context *t; struct mail_search_context *ctx; struct mail *mail; struct stat st; uoff_t size; unsigned int max_msgs_per_unit, max_kbytes_per_unit; uint32_t seq1, seq2; (void)mailbox_sync(box, MAILBOX_SYNC_FLAG_FULL_READ, 0, NULL); if (stat(POP3_THROTTLE_ENABLED_PATH, &st) == 0) { max_msgs_per_unit = set_max_msgs_per_unit; max_kbytes_per_unit = set_max_kbytes_per_unit; if (mail_debug) i_info("pop3-throttle: throttling active"); } else { if (mail_debug) { i_info("pop3-throttle: throttling inactive (" POP3_THROTTLE_ENABLED_PATH" doesn't exist)"); } max_msgs_per_unit = 0; max_kbytes_per_unit = 0; } search_args = mail_search_build_init(); search_args->args = p_new(search_args->pool, struct mail_search_arg, 1); search_args->args->type = SEARCH_UIDSET; p_array_init(&search_args->args->value.seqset, search_args->pool, 1); seq_range_array_add_range(&search_args->args->value.seqset, tbox->state.highest_visible_uid + 1, (uint32_t)-1); t = mailbox_transaction_begin(box, 0); ctx = mailbox_search_init(t, search_args, NULL); mail_search_args_unref(&search_args); mail = mail_alloc(t, MAIL_FETCH_VIRTUAL_SIZE, NULL); while (mailbox_search_next(ctx, mail)) { if (mail_get_virtual_size(mail, &size) < 0) continue; if (tbox->state.fetch_msgs >= max_msgs_per_unit && max_msgs_per_unit > 0) { if (mail_debug) { i_info("pop3-throttle: max_msgs reached, " "uid=%u isn't visible", mail->uid); } break; } if (tbox->state.fetch_kbytes >= max_kbytes_per_unit && max_kbytes_per_unit > 0) { if (mail_debug) { i_info("pop3-throttle: max_kbytes reached, " "uid=%u isn't visible", mail->uid); } break; } tbox->state.fetch_msgs++; tbox->state.fetch_kbytes += (size + 1023) / 1024; tbox->state.highest_visible_uid = mail->uid; } mail_free(&mail); (void)mailbox_search_deinit(&ctx); (void)mailbox_transaction_commit(&t); if (tbox->state.highest_visible_uid == 0) seq2 = 0; else { mailbox_get_seq_range(box, 1, tbox->state.highest_visible_uid, &seq1, &seq2); } tbox->session_highest_visible_seq = seq2; } static void pop3_throttle_get_status(struct mailbox *box, enum mailbox_status_items items, struct mailbox_status *status_r) { struct pop3_throttle_mailbox *tbox = POP3_THROTTLE_CONTEXT(box); tbox->module_ctx.super.get_status(box, items, status_r); if (status_r->messages > tbox->session_highest_visible_seq) status_r->messages = tbox->session_highest_visible_seq; } static struct pop3_throttle_mailbox * pop3_throttle_mailbox_allocated(struct mailbox *box) { struct pop3_throttle_mailbox *tbox; struct mail_namespace *ns; ns = mail_storage_get_namespace(box->storage); if (strcmp(box->name, "INBOX") != 0 || (ns->flags & NAMESPACE_FLAG_INBOX) == 0) return NULL; tbox = p_new(box->pool, struct pop3_throttle_mailbox, 1); tbox->module_ctx.super = box->v; tbox->session_highest_visible_seq = (uint32_t)-1; box->v.get_status = pop3_throttle_get_status; MODULE_CONTEXT_SET(box, pop3_throttle_storage_module, tbox); return tbox; } static void pop3_throttle_mailbox_opened(struct mailbox *box) { struct pop3_throttle_mailbox *tbox; const char *path; if (next_hook_mailbox_opened != NULL) next_hook_mailbox_opened(box); tbox = pop3_throttle_mailbox_allocated(box); if (tbox == NULL) return; path = pop3_throttle_get_state_path(box); pop3_throttle_read_state(path, &tbox->orig_state); tbox->state = tbox->orig_state; pop3_throttle_init_state(box, &tbox->state); pop3_throttle_get_visible_msgs(box); if (memcmp(&tbox->state, &tbox->orig_state, sizeof(tbox->state)) != 0) pop3_throttle_write_state(path, &tbox->state); } void pop3_throttle_plugin_init(void) { const char *value; value = getenv("POP3_THROTTLE_MAX_MSGS"); set_max_msgs_per_unit = value == NULL ? 0 : strtoul(value, NULL, 10); value = getenv("POP3_THROTTLE_MAX_KBYTES"); set_max_kbytes_per_unit = value == NULL ? 0 : strtoul(value, NULL, 10); mail_debug = getenv("DEBUG") != NULL; if (mail_debug) { i_info("pop3-throttle: max_msgs=%u max_kbytes=%u", set_max_msgs_per_unit, set_max_kbytes_per_unit); } if (set_max_msgs_per_unit > 0 || set_max_kbytes_per_unit > 0) { next_hook_mailbox_opened = hook_mailbox_opened; hook_mailbox_opened = pop3_throttle_mailbox_opened; } } void pop3_throttle_plugin_deinit(void) { if (hook_mailbox_opened == pop3_throttle_mailbox_opened) hook_mailbox_opened = next_hook_mailbox_opened; }