#include "lib.h"
#include "array.h"
#include "ioloop.h"
#include "nfs-workarounds.h"
#include "file-dotlock.h"
#include "mkdir-parents.h"
#include "read-full.h"
#include "write-full.h"
#include "str.h"
#include "maildir-storage.h"
#include "quota-private.h"
#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
#include <sys/stat.h>
#define MAILDIRSIZE_FILENAME "maildirsize"
#define MAILDIRSIZE_STALE_SECS (60*15)
struct maildir_quota_root {
struct quota_root root;
struct mail_namespace *maildirsize_ns;
const char *maildirsize_path;
uint64_t total_bytes;
uint64_t total_count;
int fd;
time_t recalc_last_stamp;
off_t last_size;
unsigned int limits_initialized:1;
};
struct maildir_list_context {
struct mailbox_list *list;
struct maildir_quota_root *root;
struct mailbox_list_iterate_context *iter;
const struct mailbox_info *info;
string_t *path;
int state;
};
extern struct quota_backend quota_backend_maildir;
static struct dotlock_settings dotlock_settings = {
.timeout = 0,
.stale_timeout = 30
};
static int maildir_sum_dir(const char *dir, uint64_t *total_bytes,
uint64_t *total_count)
{
DIR *dirp;
struct dirent *dp;
string_t *path;
const char *p;
size_t len;
uoff_t num;
int ret = 0;
dirp = opendir(dir);
if (dirp == NULL) {
if (errno == ENOENT || errno == ESTALE)
return 0;
i_error("opendir(%s) failed: %m", dir);
return -1;
}
path = t_str_new(256);
str_append(path, dir);
str_append_c(path, '/');
len = str_len(path);
while ((dp = readdir(dirp)) != NULL) {
if (dp->d_name[0] == '.' &&
(dp->d_name[1] == '\0' || dp->d_name[1] == '.'))
continue;
p = strstr(dp->d_name, ",S=");
num = (uoff_t)-1;
if (p != NULL) {
p += 3;
for (num = 0; *p >= '0' && *p <= '9'; p++)
num = num * 10 + (*p - '0');
if (*p != ':' && *p != '\0' && *p != ',') {
num = (uoff_t)-1;
} else {
*total_bytes += num;
*total_count += 1;
}
}
if (num == (uoff_t)-1) {
struct stat st;
str_truncate(path, len);
str_append(path, dp->d_name);
if (stat(str_c(path), &st) == 0) {
*total_bytes += st.st_size;
*total_count += 1;
} else if (errno != ENOENT && errno != ESTALE) {
i_error("stat(%s) failed: %m", str_c(path));
ret = -1;
}
}
}
if (closedir(dirp) < 0) {
i_error("closedir(%s) failed: %m", dir);
return -1;
}
return ret;
}
static struct maildir_list_context *
maildir_list_init(struct maildir_quota_root *root, struct mailbox_list *list)
{
struct maildir_list_context *ctx;
ctx = i_new(struct maildir_list_context, 1);
ctx->root = root;
ctx->path = str_new(default_pool, 512);
ctx->list = list;
ctx->iter = mailbox_list_iter_init(list, "*",
MAILBOX_LIST_ITER_RETURN_NO_FLAGS);
return ctx;
}
static const char *
maildir_list_next(struct maildir_list_context *ctx, time_t *mtime_r)
{
struct quota_rule *rule;
struct stat st;
for (;;) {
if (ctx->state == 0) {
ctx->info = mailbox_list_iter_next(ctx->iter);
if (ctx->info == NULL)
return NULL;
rule = quota_root_rule_find(ctx->root->root.set,
ctx->info->name);
if (rule != NULL && rule->ignore) {
continue;
}
}
T_BEGIN {
const char *path, *storage_name;
storage_name = mail_namespace_get_storage_name(
ctx->info->ns, ctx->info->name);
path = mailbox_list_get_path(ctx->list, storage_name,
MAILBOX_LIST_PATH_TYPE_MAILBOX);
str_truncate(ctx->path, 0);
str_append(ctx->path, path);
str_append(ctx->path, ctx->state == 0 ?
"/new" : "/cur");
} T_END;
if (++ctx->state == 2)
ctx->state = 0;
if (stat(str_c(ctx->path), &st) == 0)
break;
if (errno != ENOENT && errno != ESTALE && errno != ENOTDIR) {
i_error("stat(%s) failed: %m", str_c(ctx->path));
ctx->state = 0;
}
}
*mtime_r = st.st_mtime;
return str_c(ctx->path);
}
static int maildir_list_deinit(struct maildir_list_context *ctx)
{
int ret = mailbox_list_iter_deinit(&ctx->iter);
str_free(&ctx->path);
i_free(ctx);
return ret;
}
static int
maildirs_check_have_changed(struct maildir_quota_root *root,
struct mail_namespace *ns, time_t latest_mtime)
{
struct maildir_list_context *ctx;
time_t mtime;
int ret = 0;
ctx = maildir_list_init(root, ns->list);
while (maildir_list_next(ctx, &mtime) != NULL) {
if (mtime > latest_mtime) {
ret = 1;
break;
}
}
if (maildir_list_deinit(ctx) < 0)
return -1;
return ret;
}
static int maildirsize_write(struct maildir_quota_root *root, const char *path)
{
const struct mail_storage_settings *set =
root->maildirsize_ns->mail_set;
struct quota_root *_root = &root->root;
struct mail_namespace *const *namespaces;
unsigned int i, count;
struct dotlock *dotlock;
const char *p, *dir, *gid_origin, *dir_gid_origin;
string_t *str;
mode_t mode, dir_mode;
gid_t gid, dir_gid;
int fd;
i_assert(root->fd == -1);
mode = 0600; dir_mode = 0700; dir_gid_origin = gid_origin = "default";
gid = dir_gid = (gid_t)-1;
namespaces = array_get(&root->root.quota->namespaces, &count);
i_assert(count > 0);
for (i = 0; i < count; i++) {
if ((namespaces[i]->flags & NAMESPACE_FLAG_INBOX_USER) != 0) {
mailbox_list_get_permissions(namespaces[i]->list,
NULL, &mode, &gid,
&gid_origin);
mailbox_list_get_dir_permissions(namespaces[i]->list,
NULL,
&dir_mode, &dir_gid,
&dir_gid_origin);
break;
}
}
dotlock_settings.use_excl_lock = set->dotlock_use_excl;
dotlock_settings.nfs_flush = set->mail_nfs_storage;
fd = file_dotlock_open_group(&dotlock_settings, path,
DOTLOCK_CREATE_FLAG_NONBLOCK,
mode, gid, gid_origin, &dotlock);
if (fd == -1 && errno == ENOENT) {
p = strrchr(path, '/');
dir = t_strdup_until(path, p);
if (mkdir_parents_chgrp(dir, dir_mode, dir_gid,
dir_gid_origin) < 0 &&
errno != EEXIST) {
i_error("mkdir_parents(%s) failed: %m", dir);
return -1;
}
fd = file_dotlock_open_group(&dotlock_settings, path,
DOTLOCK_CREATE_FLAG_NONBLOCK,
mode, gid, gid_origin, &dotlock);
}
if (fd == -1) {
if (errno == EAGAIN) {
return 1;
}
i_error("file_dotlock_open(%s) failed: %m", path);
return -1;
}
str = t_str_new(128);
if (_root->bytes_limit != 0 || _root->count_limit == 0) {
str_printfa(str, "%lluS",
(unsigned long long)_root->bytes_limit);
}
if (_root->count_limit != 0) {
if (str_len(str) > 0)
str_append_c(str, ',');
str_printfa(str, "%lluC",
(unsigned long long)_root->count_limit);
}
str_printfa(str, "\n%llu %llu\n",
(unsigned long long)root->total_bytes,
(unsigned long long)root->total_count);
if (write_full(fd, str_data(str), str_len(str)) < 0) {
i_error("write_full(%s) failed: %m", path);
file_dotlock_delete(&dotlock);
return -1;
}
if (file_dotlock_replace(&dotlock,
DOTLOCK_REPLACE_FLAG_DONT_CLOSE_FD) < 0) {
i_error("file_dotlock_replace(%s) failed: %m", path);
return -1;
}
root->fd = fd;
return 0;
}
static void maildirsize_recalculate_init(struct maildir_quota_root *root)
{
root->total_bytes = root->total_count = 0;
root->recalc_last_stamp = 0;
}
static int maildirsize_recalculate_namespace(struct maildir_quota_root *root,
struct mail_namespace *ns)
{
struct maildir_list_context *ctx;
const char *dir;
time_t mtime;
int ret = 0;
ctx = maildir_list_init(root, ns->list);
while ((dir = maildir_list_next(ctx, &mtime)) != NULL) {
if (mtime > root->recalc_last_stamp)
root->recalc_last_stamp = mtime;
T_BEGIN {
if (maildir_sum_dir(dir, &root->total_bytes,
&root->total_count) < 0)
ret = -1;
} T_END;
}
if (maildir_list_deinit(ctx) < 0)
ret = -1;
return ret;
}
static void maildirsize_rebuild_later(struct maildir_quota_root *root)
{
if (!root->root.set->force_default_rule) {
return;
}
if (unlink(root->maildirsize_path) < 0 &&
errno != ENOENT && errno != ESTALE)
i_error("unlink(%s) failed: %m", root->maildirsize_path);
}
static int maildirsize_recalculate_finish(struct maildir_quota_root *root,
int ret)
{
if (ret == 0) {
ret = maildirsize_write(root, root->maildirsize_path);
}
if (ret != 0)
maildirsize_rebuild_later(root);
return ret;
}
static int maildirsize_recalculate(struct maildir_quota_root *root)
{
struct mail_namespace *const *namespaces;
unsigned int i, count;
int ret = 0;
maildirsize_recalculate_init(root);
namespaces = array_get(&root->root.quota->namespaces, &count);
for (i = 0; i < count; i++) {
if (!quota_root_is_namespace_visible(&root->root, namespaces[i]))
continue;
if (maildirsize_recalculate_namespace(root, namespaces[i]) < 0) {
ret = -1;
break;
}
}
if (ret == 0) {
for (i = 0; i < count; i++) {
if (!quota_root_is_namespace_visible(&root->root,
namespaces[i]))
continue;
ret = maildirs_check_have_changed(root, namespaces[i],
root->recalc_last_stamp);
if (ret != 0)
break;
}
}
return maildirsize_recalculate_finish(root, ret);
}
static bool
maildir_parse_limit(const char *str, uint64_t *bytes_r, uint64_t *count_r)
{
const char *const *limit;
unsigned long long value;
char *pos;
bool ret = TRUE;
*bytes_r = 0;
*count_r = 0;
for (limit = t_strsplit(str, ","); *limit != NULL; limit++) {
value = strtoull(*limit, &pos, 10);
if (pos[0] != '\0' && pos[1] == '\0') {
switch (pos[0]) {
case 'C':
if (value != 0)
*count_r = value;
break;
case 'S':
if (value != 0)
*bytes_r = value;
break;
default:
ret = FALSE;
break;
}
} else {
ret = FALSE;
}
}
return ret;
}
static int maildirsize_parse(struct maildir_quota_root *root,
int fd, const char *const *lines)
{
struct quota_root *_root = &root->root;
uint64_t message_bytes_limit, message_count_limit;
long long bytes_diff, total_bytes;
int count_diff, total_count;
unsigned int line_count = 0;
if (*lines == NULL)
return -1;
(void)maildir_parse_limit(lines[0], &message_bytes_limit,
&message_count_limit);
if (message_bytes_limit >= (1ULL << 63))
message_bytes_limit = (1ULL << 63) - 1;
if (message_count_limit >= (1ULL << 63))
message_count_limit = (1ULL << 63) - 1;
if (root->root.bytes_limit == (int64_t)message_bytes_limit &&
root->root.count_limit == (int64_t)message_count_limit) {
} else if (root->root.set->force_default_rule) {
return 0;
} else {
root->root.bytes_limit = message_bytes_limit;
root->root.count_limit = message_count_limit;
quota_root_recalculate_relative_rules(root->root.set,
message_bytes_limit,
message_count_limit);
}
if (*lines == NULL) {
return 0;
}
total_bytes = 0; total_count = 0;
for (lines++; *lines != NULL; lines++, line_count++) {
if (sscanf(*lines, "%lld %d", &bytes_diff, &count_diff) != 2)
return -1;
total_bytes += bytes_diff;
total_count += count_diff;
}
if (total_bytes < 0 || total_count < 0) {
return -1;
}
if ((total_bytes > _root->bytes_limit && _root->bytes_limit != 0) ||
(total_count > _root->count_limit && _root->count_limit != 0)) {
struct stat st;
if (line_count > 1)
return 0;
if (fstat(fd, &st) < 0 ||
st.st_mtime < ioloop_time - MAILDIRSIZE_STALE_SECS)
return 0;
}
root->total_bytes = (uint64_t)total_bytes;
root->total_count = (uint64_t)total_count;
return 1;
}
static int maildirsize_open(struct maildir_quota_root *root)
{
if (root->fd != -1) {
if (close(root->fd) < 0)
i_error("close(%s) failed: %m", root->maildirsize_path);
}
root->fd = nfs_safe_open(root->maildirsize_path, O_RDWR | O_APPEND);
if (root->fd == -1) {
if (errno == ENOENT)
return 0;
i_error("open(%s) failed: %m", root->maildirsize_path);
return -1;
}
return 1;
}
static bool maildirsize_has_changed(struct maildir_quota_root *root)
{
struct stat st1, st2;
if (dotlock_settings.nfs_flush) {
nfs_flush_file_handle_cache(root->maildirsize_path);
nfs_flush_attr_cache_unlocked(root->maildirsize_path);
}
if (root->fd == -1)
return TRUE;
if (stat(root->maildirsize_path, &st1) < 0)
return TRUE;
if (fstat(root->fd, &st2) < 0)
return TRUE;
return root->last_size != st2.st_size || st1.st_ino != st2.st_ino ||
!CMP_DEV_T(st1.st_dev, st2.st_dev);
}
static int maildirsize_read(struct maildir_quota_root *root)
{
char buf[5120+1];
unsigned int i, size;
int ret;
if (!maildirsize_has_changed(root))
return 1;
if ((ret = maildirsize_open(root)) <= 0)
return ret;
size = 0;
while ((ret = read(root->fd, buf + size, sizeof(buf)-1 - size)) != 0) {
if (ret < 0) {
if (errno == ESTALE)
break;
i_error("read(%s) failed: %m", root->maildirsize_path);
break;
}
size += ret;
if (size >= sizeof(buf)-1) {
break;
}
}
root->total_bytes = root->total_count = 0;
root->last_size = size;
while (size > 0 && buf[size-1] != '\n') size--;
if (size > 0) size--;
buf[size] = '\0';
if (ret < 0 && size == 0) {
(void)close(root->fd);
root->fd = -1;
return -1;
}
for (i = 0; i < size; i++) {
if (buf[i] == '\0')
break;
}
if (i == size &&
maildirsize_parse(root, root->fd, t_strsplit(buf, "\n")) > 0 &&
ret == 0)
ret = 1;
else {
(void)close(root->fd);
root->fd = -1;
ret = 0;
}
return ret;
}
static bool maildirquota_limits_init(struct maildir_quota_root *root)
{
struct mailbox_list *list;
struct mail_storage *storage;
const char *name = "";
if (root->limits_initialized)
return root->maildirsize_path != NULL;
root->limits_initialized = TRUE;
if (root->maildirsize_ns == NULL) {
i_assert(root->maildirsize_path == NULL);
return FALSE;
}
i_assert(root->maildirsize_path != NULL);
list = root->maildirsize_ns->list;
if (mailbox_list_get_storage(&list, &name, &storage) == 0 &&
strcmp(storage->name, MAILDIR_STORAGE_NAME) != 0) {
if ((storage->class_flags &
MAIL_STORAGE_CLASS_FLAG_NOQUOTA) == 0) {
i_warning("quota: Namespace '%s' is not Maildir, "
"skipping for Maildir++ quota",
root->maildirsize_ns->prefix);
}
root->maildirsize_path = NULL;
return FALSE;
}
return TRUE;
}
static int maildirquota_read_limits(struct maildir_quota_root *root)
{
int ret;
if (!maildirquota_limits_init(root))
return 1;
T_BEGIN {
ret = maildirsize_read(root);
} T_END;
return ret;
}
static int
maildirquota_refresh(struct maildir_quota_root *root, bool *recalculated_r)
{
int ret;
*recalculated_r = FALSE;
ret = maildirquota_read_limits(root);
if (ret == 0) {
if (root->root.bytes_limit == 0 &&
root->root.count_limit == 0 &&
root->root.set->default_rule.bytes_limit == 0 &&
root->root.set->default_rule.count_limit == 0) {
if (!root->root.set->force_default_rule)
return 0;
}
ret = maildirsize_recalculate(root);
if (ret == 0)
*recalculated_r = TRUE;
}
return ret < 0 ? -1 : 0;
}
static int maildirsize_update(struct maildir_quota_root *root,
int count_diff, int64_t bytes_diff)
{
char str[MAX_INT_STRLEN*2 + 2];
int ret = 0;
if (count_diff == 0 && bytes_diff == 0)
return 0;
i_snprintf(str, sizeof(str), "%lld %d\n",
(long long)bytes_diff, count_diff);
if (write_full(root->fd, str, strlen(str)) < 0) {
ret = -1;
if (errno == ESTALE) {
} else {
i_error("write_full(%s) failed: %m",
root->maildirsize_path);
}
}
return ret;
}
static struct quota_root *maildir_quota_alloc(void)
{
struct maildir_quota_root *root;
root = i_new(struct maildir_quota_root, 1);
root->fd = -1;
return &root->root;
}
static int maildir_quota_init(struct quota_root *_root, const char *args)
{
const char *const *tmp;
if (args == NULL)
return 0;
for (tmp = t_strsplit(args, ":"); *tmp != NULL; tmp++) {
if (strcmp(*tmp, "noenforcing") == 0)
_root->no_enforcing = TRUE;
else if (strcmp(*tmp, "ignoreunlimited") == 0)
_root->disable_unlimited_tracking = TRUE;
else if (strncmp(*tmp, "ns=", 3) == 0)
_root->ns_prefix = p_strdup(_root->pool, *tmp + 3);
else {
i_error("maildir quota: Invalid parameter: %s", *tmp);
return -1;
}
}
return 0;
}
static void maildir_quota_deinit(struct quota_root *_root)
{
struct maildir_quota_root *root = (struct maildir_quota_root *)_root;
if (root->fd != -1)
(void)close(root->fd);
i_free(root);
}
static bool
maildir_quota_parse_rule(struct quota_root_settings *root_set ATTR_UNUSED,
struct quota_rule *rule,
const char *str, const char **error_r)
{
uint64_t bytes, count;
if (strcmp(str, "NOQUOTA") == 0) {
bytes = 0;
count = 0;
} else if (!maildir_parse_limit(str, &bytes, &count)) {
*error_r = "Invalid Maildir++ quota rule";
return FALSE;
}
rule->bytes_limit = bytes;
rule->count_limit = count;
return TRUE;
}
static int maildir_quota_init_limits(struct quota_root *_root)
{
struct maildir_quota_root *root = (struct maildir_quota_root *)_root;
return maildirquota_read_limits(root) < 0 ? -1 : 0;
}
static void
maildir_quota_root_namespace_added(struct quota_root *_root,
struct mail_namespace *ns)
{
struct maildir_quota_root *root = (struct maildir_quota_root *)_root;
const char *control_dir;
if (root->maildirsize_path != NULL)
return;
control_dir = mailbox_list_get_path(ns->list, NULL,
MAILBOX_LIST_PATH_TYPE_CONTROL);
root->maildirsize_ns = ns;
root->maildirsize_path =
p_strconcat(_root->pool, control_dir,
"/"MAILDIRSIZE_FILENAME, NULL);
}
static void
maildir_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_maildir.name &&
((roots[i]->ns_prefix == NULL &&
ns->type == NAMESPACE_PRIVATE) || roots[i]->ns == ns))
maildir_quota_root_namespace_added(roots[i], ns);
}
}
static const char *const *
maildir_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 int
maildir_quota_get_resource(struct quota_root *_root, const char *name,
uint64_t *value_r)
{
struct maildir_quota_root *root = (struct maildir_quota_root *)_root;
bool recalculated;
if (maildirquota_refresh(root, &recalculated) < 0)
return -1;
if (strcmp(name, QUOTA_NAME_STORAGE_BYTES) == 0) {
*value_r = root->total_bytes;
} else if (strcmp(name, QUOTA_NAME_MESSAGES) == 0) {
*value_r = root->total_count;
} else
return 0;
return 1;
}
static int
maildir_quota_update(struct quota_root *_root,
struct quota_transaction_context *ctx)
{
struct maildir_quota_root *root = (struct maildir_quota_root *)_root;
bool recalculated;
if (!maildirquota_limits_init(root)) {
return 0;
}
if (maildirquota_refresh(root, &recalculated) < 0)
return -1;
if (recalculated) {
} else if (root->fd == -1)
(void)maildirsize_recalculate(root);
else if (ctx->recalculate) {
(void)close(root->fd);
root->fd = -1;
(void)maildirsize_recalculate(root);
} else if (maildirsize_update(root, ctx->count_used, ctx->bytes_used) < 0)
maildirsize_rebuild_later(root);
return 0;
}
struct quota_backend quota_backend_maildir = {
"maildir",
{
maildir_quota_alloc,
maildir_quota_init,
maildir_quota_deinit,
maildir_quota_parse_rule,
maildir_quota_init_limits,
maildir_quota_namespace_added,
maildir_quota_root_get_resources,
maildir_quota_get_resource,
maildir_quota_update,
NULL
}
};