doveadm-expire.c revision 71df09024cea5f2faa93da3bb9513ee96ba6bf22
/* Copyright (c) 2005-2010 Dovecot authors, see the included COPYING file */
#include "lib.h"
#include "module-dir.h"
#include "str.h"
#include "hash.h"
#include "dict.h"
#include "imap-match.h"
#include "expire-set.h"
#include "mail-search.h"
#include "doveadm-settings.h"
#include "doveadm-mail.h"
#define DOVEADM_EXPIRE_MAIL_CMD_CONTEXT(obj) \
MODULE_CONTEXT(obj, doveadm_expire_mail_cmd_module)
struct expire_query {
const char *mailbox;
struct imap_match_glob *glob;
time_t before_time;
};
struct doveadm_expire_mail_cmd_context {
union doveadm_mail_cmd_module_context module_ctx;
struct dict *dict;
struct dict_transaction_context *trans;
struct dict_iterate_context *iter;
struct hash_table *seen_users;
ARRAY_DEFINE(queries, struct expire_query);
time_t oldest_before_time;
};
const char *doveadm_expire_plugin_version = DOVECOT_VERSION;
void doveadm_expire_plugin_init(struct module *module);
void doveadm_expire_plugin_deinit(void);
static MODULE_CONTEXT_DEFINE_INIT(doveadm_expire_mail_cmd_module,
&doveadm_mail_cmd_module_register);
static void (*next_hook_doveadm_mail_init)(struct doveadm_mail_cmd_context *ctx);
static bool
doveadm_expire_mail_match_mailbox(struct doveadm_expire_mail_cmd_context *ectx,
const char *mailbox, time_t oldest_savedate)
{
const struct expire_query *query;
array_foreach(&ectx->queries, query) {
if (oldest_savedate >= query->before_time)
continue;
if (query->glob == NULL) {
if (strcmp(query->mailbox, mailbox) == 0)
return TRUE;
} else {
if (imap_match(query->glob, mailbox) == IMAP_MATCH_YES)
return TRUE;
}
}
return FALSE;
}
static bool
doveadm_expire_mail_want(struct doveadm_mail_cmd_context *ctx,
const char *key, time_t stamp, const char **username_r)
{
struct doveadm_expire_mail_cmd_context *ectx =
DOVEADM_EXPIRE_MAIL_CMD_CONTEXT(ctx);
const char *username, *mailbox;
char *username_dup;
/* key = DICT_EXPIRE_PREFIX<user>/<mailbox> */
username = key + strlen(DICT_EXPIRE_PREFIX);
mailbox = strchr(username, '/');
if (mailbox == NULL) {
/* invalid record, ignore */
return FALSE;
}
username = t_strdup_until(username, mailbox++);
if (hash_table_lookup(ectx->seen_users, username) != NULL) {
/* seen this user already, skip the record */
return FALSE;
}
if (!doveadm_expire_mail_match_mailbox(ectx, mailbox, stamp)) {
/* this mailbox doesn't have any matching messages */
return FALSE;
}
username_dup = p_strdup(ctx->pool, username);
hash_table_insert(ectx->seen_users, username_dup, username_dup);
*username_r = username_dup;
return TRUE;
}
static int
doveadm_expire_mail_cmd_get_next_user(struct doveadm_mail_cmd_context *ctx,
const char **username_r)
{
struct doveadm_expire_mail_cmd_context *ectx =
DOVEADM_EXPIRE_MAIL_CMD_CONTEXT(ctx);
const char *key, *value;
unsigned long stamp;
bool ret;
while (dict_iterate(ectx->iter, &key, &value)) {
if (str_to_ulong(value, &stamp) < 0) {
/* invalid record */
continue;
}
if ((time_t)stamp > ectx->oldest_before_time)
break;
T_BEGIN {
ret = doveadm_expire_mail_want(ctx, key, stamp,
username_r);
} T_END;
if (ret)
return TRUE;
}
/* finished */
if (dict_iterate_deinit(&ectx->iter) < 0) {
i_error("Dictionary iteration failed");
return -1;
}
return 0;
}
static const char *const *doveadm_expire_get_patterns(void)
{
ARRAY_TYPE(const_string) patterns;
const char *str;
char set_name[20];
unsigned int i;
t_array_init(&patterns, 16);
str = doveadm_plugin_getenv("expire");
for (i = 2; str != NULL; i++) {
array_append(&patterns, &str, 1);
i_snprintf(set_name, sizeof(set_name), "expire%u", i);
str = doveadm_plugin_getenv(set_name);
}
(void)array_append_space(&patterns);
return array_idx(&patterns, 0);
}
static bool
doveadm_expire_get_or_mailboxes(struct doveadm_mail_cmd_context *ctx,
const struct mail_search_arg *args,
struct expire_query query)
{
struct doveadm_expire_mail_cmd_context *ectx =
DOVEADM_EXPIRE_MAIL_CMD_CONTEXT(ctx);
const struct mail_search_arg *arg;
unsigned int query_count;
query.mailbox = NULL;
query_count = array_count(&ectx->queries);
for (arg = args; arg != NULL; arg = arg->next) {
switch (arg->type) {
case SEARCH_MAILBOX_GLOB:
query.glob = imap_match_init(ctx->pool, arg->value.str,
TRUE, '/');
/* fall through */
case SEARCH_MAILBOX:
/* require mailbox to be in expire patterns */
query.mailbox = p_strdup(ctx->pool, arg->value.str);
array_append(&ectx->queries, &query, 1);
break;
default:
/* there are something else besides mailboxes,
can't optimize this. */
array_delete(&ectx->queries, query_count,
array_count(&ectx->queries) - query_count);
return FALSE;
}
}
return query.mailbox != NULL;
}
static bool
doveadm_expire_analyze_and_query(struct doveadm_mail_cmd_context *ctx,
const struct mail_search_arg *args)
{
struct doveadm_expire_mail_cmd_context *ectx =
DOVEADM_EXPIRE_MAIL_CMD_CONTEXT(ctx);
const struct mail_search_arg *arg;
struct expire_query query;
bool have_or = FALSE;
memset(&query, 0, sizeof(query));
query.before_time = (time_t)-1;
for (arg = args; arg != NULL; arg = arg->next) {
switch (arg->type) {
case SEARCH_OR:
have_or = TRUE;
break;
case SEARCH_MAILBOX_GLOB:
query.glob = imap_match_init(ctx->pool, arg->value.str,
TRUE, '/');
/* fall through */
case SEARCH_MAILBOX:
/* require mailbox to be in expire patterns */
query.mailbox = p_strdup(ctx->pool, arg->value.str);
break;
case SEARCH_BEFORE:
if (arg->value.date_type != MAIL_SEARCH_DATE_TYPE_SAVED)
break;
if ((arg->value.search_flags &
MAIL_SEARCH_ARG_FLAG_USE_TZ) == 0)
break;
query.before_time = arg->value.time;
break;
default:
break;
}
}
if (query.before_time == (time_t)-1) {
/* no SAVEDBEFORE, can't optimize */
return FALSE;
}
if (query.mailbox != NULL) {
/* one mailbox */
array_append(&ectx->queries, &query, 1);
return TRUE;
}
/* no MAILBOX, but check if one of the ORs lists mailboxes */
if (!have_or)
return FALSE;
for (arg = args; arg != NULL; arg = arg->next) {
if (arg->type == SEARCH_OR &&
doveadm_expire_get_or_mailboxes(ctx, arg->value.subargs,
query))
return TRUE;
}
return FALSE;
}
static time_t
doveadm_expire_analyze_or_query(struct doveadm_mail_cmd_context *ctx,
const struct mail_search_arg *args)
{
const struct mail_search_arg *arg;
/* all of the subqueries must have mailbox and savedbefore */
for (arg = args; arg != NULL; arg = arg->next) {
if (arg->type != SEARCH_SUB)
return FALSE;
if (!doveadm_expire_analyze_and_query(ctx, arg->value.subargs))
return FALSE;
}
return TRUE;
}
static bool doveadm_expire_analyze_query(struct doveadm_mail_cmd_context *ctx)
{
struct doveadm_expire_mail_cmd_context *ectx =
DOVEADM_EXPIRE_MAIL_CMD_CONTEXT(ctx);
struct mail_search_arg *args = ctx->search_args->args;
struct expire_set *set;
const struct expire_query *queries;
unsigned int i, count;
/* we support two kinds of queries:
1) mailbox-pattern savedbefore <stamp> ...
2) or 2*(mailbox-pattern savedbefore <stamp> ...)
mailbox-pattern can be:
a) mailbox <name>
b) or 2*(mailbox <name>)
*/
p_array_init(&ectx->queries, ctx->pool, 8);
if (!doveadm_expire_analyze_and_query(ctx, args) &&
(args->type != SEARCH_OR || args->next != NULL ||
!doveadm_expire_analyze_or_query(ctx, args->value.subargs))) {
if (doveadm_debug)
i_debug("expire: Couldn't optimize search query");
return FALSE;
}
/* make sure all mailboxes match expire patterns */
set = expire_set_init(doveadm_expire_get_patterns());
queries = array_get(&ectx->queries, &count);
for (i = 0; i < count; i++) {
if (!expire_set_lookup(set, queries[i].mailbox)) {
if (doveadm_debug) {
i_debug("expire: Couldn't optimize search query: "
"mailbox %s not in expire database",
queries[i].mailbox);
}
break;
}
}
expire_set_deinit(&set);
return i == count;
}
static void doveadm_expire_mail_cmd_deinit(struct doveadm_mail_cmd_context *ctx)
{
struct doveadm_expire_mail_cmd_context *ectx =
DOVEADM_EXPIRE_MAIL_CMD_CONTEXT(ctx);
if (ectx->iter != NULL) {
if (dict_iterate_deinit(&ectx->iter) < 0)
i_error("Dictionary iteration failed");
}
dict_transaction_commit(&ectx->trans);
dict_deinit(&ectx->dict);
hash_table_destroy(&ectx->seen_users);
ectx->module_ctx.super.deinit(ctx);
}
static void doveadm_expire_mail_init(struct doveadm_mail_cmd_context *ctx)
{
struct doveadm_expire_mail_cmd_context *ectx;
struct dict *dict;
const struct expire_query *query;
const char *expire_dict;
if (ctx->search_args == NULL)
return;
expire_dict = doveadm_plugin_getenv("expire_dict");
if (expire_dict == NULL)
return;
ectx = p_new(ctx->pool, struct doveadm_expire_mail_cmd_context, 1);
ectx->module_ctx.super = ctx->v;
MODULE_CONTEXT_SET(ctx, doveadm_expire_mail_cmd_module, ectx);
/* we can potentially optimize this query. see if the search args
are valid for optimization. */
if (!doveadm_expire_analyze_query(ctx))
return;
i_debug("expire: Searching only users listed in expire database");
dict = dict_init(expire_dict, DICT_DATA_TYPE_UINT32, "",
doveadm_settings->base_dir);
if (dict == NULL) {
i_error("dict_init(%s) failed, not using it", expire_dict);
return;
}
ectx->oldest_before_time = (time_t)-1;
array_foreach(&ectx->queries, query) {
if (ectx->oldest_before_time > query->before_time ||
ectx->oldest_before_time == (time_t)-1)
ectx->oldest_before_time = query->before_time;
}
ctx->v.deinit = doveadm_expire_mail_cmd_deinit;
ctx->v.get_next_user = doveadm_expire_mail_cmd_get_next_user;
ectx->seen_users =
hash_table_create(default_pool, ctx->pool, 0,
str_hash, (hash_cmp_callback_t *)strcmp);
ectx->dict = dict;
ectx->trans = dict_transaction_begin(dict);
ectx->iter = dict_iterate_init(dict, DICT_EXPIRE_PREFIX,
DICT_ITERATE_FLAG_RECURSE |
DICT_ITERATE_FLAG_SORT_BY_VALUE);
}
void doveadm_expire_plugin_init(struct module *module ATTR_UNUSED)
{
next_hook_doveadm_mail_init = hook_doveadm_mail_init;
hook_doveadm_mail_init = doveadm_expire_mail_init;
}
void doveadm_expire_plugin_deinit(void)
{
i_assert(hook_doveadm_mail_init == doveadm_expire_mail_init);
hook_doveadm_mail_init = next_hook_doveadm_mail_init;
}