changeset 13294:c51fbe64eae1

Initial implementation of statistics gathering daemon and plugins to feed it. Some statistics are still missing, some of the code is a bit ugly and the internal protocols will probably still change.
author Timo Sirainen <tss@iki.fi>
date Fri, 26 Aug 2011 05:15:12 +0300
parents 076a71f9a154
children 34e2190a56c6
files .hgignore configure.in src/Makefile.am src/plugins/Makefile.am src/plugins/imap-stats/Makefile.am src/plugins/imap-stats/imap-stats-plugin.c src/plugins/imap-stats/imap-stats-plugin.h src/plugins/stats/Makefile.am src/plugins/stats/stats-connection.c src/plugins/stats/stats-connection.h src/plugins/stats/stats-plugin.c src/plugins/stats/stats-plugin.h src/stats/Makefile.am src/stats/client-export.c src/stats/client-export.h src/stats/client.c src/stats/client.h src/stats/global-memory.c src/stats/global-memory.h src/stats/mail-command.c src/stats/mail-command.h src/stats/mail-domain.c src/stats/mail-domain.h src/stats/mail-ip.c src/stats/mail-ip.h src/stats/mail-server-connection.c src/stats/mail-server-connection.h src/stats/mail-session.c src/stats/mail-session.h src/stats/mail-stats.c src/stats/mail-stats.h src/stats/mail-user.c src/stats/mail-user.h src/stats/main.c src/stats/stats-settings.c src/stats/stats-settings.h
diffstat 36 files changed, 3181 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Fri Aug 26 05:10:54 2011 +0300
+++ b/.hgignore	Fri Aug 26 05:15:12 2011 +0300
@@ -82,6 +82,7 @@
 src/lmtp/lmtp
 src/master/dovecot
 src/ssl-params/ssl-params
+src/stats/stats
 src/plugins/fts-squat/squat-test
 src/pop3-login/pop3-login
 src/pop3/pop3
--- a/configure.in	Fri Aug 26 05:10:54 2011 +0300
+++ b/configure.in	Fri Aug 26 05:15:12 2011 +0300
@@ -2724,6 +2724,7 @@
 src/pop3/Makefile
 src/pop3-login/Makefile
 src/ssl-params/Makefile
+src/stats/Makefile
 src/util/Makefile
 src/plugins/Makefile
 src/plugins/acl/Makefile
@@ -2741,6 +2742,8 @@
 src/plugins/quota/Makefile
 src/plugins/imap-quota/Makefile
 src/plugins/snarf/Makefile
+src/plugins/stats/Makefile
+src/plugins/imap-stats/Makefile
 src/plugins/trash/Makefile
 src/plugins/virtual/Makefile
 src/plugins/zlib/Makefile
--- a/src/Makefile.am	Fri Aug 26 05:10:54 2011 +0300
+++ b/src/Makefile.am	Fri Aug 26 05:15:12 2011 +0300
@@ -42,4 +42,5 @@
 	doveadm \
 	dsync \
 	ssl-params \
+	stats \
 	plugins
--- a/src/plugins/Makefile.am	Fri Aug 26 05:10:54 2011 +0300
+++ b/src/plugins/Makefile.am	Fri Aug 26 05:15:12 2011 +0300
@@ -24,6 +24,8 @@
 	quota \
 	imap-quota \
 	snarf \
+	stats \
+	imap-stats \
 	trash \
 	virtual \
 	$(ZLIB) \
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/plugins/imap-stats/Makefile.am	Fri Aug 26 05:15:12 2011 +0300
@@ -0,0 +1,27 @@
+AM_CPPFLAGS = \
+	-I$(top_srcdir)/src/lib \
+	-I$(top_srcdir)/src/lib-mail \
+	-I$(top_srcdir)/src/lib-imap \
+	-I$(top_srcdir)/src/lib-index \
+	-I$(top_srcdir)/src/lib-storage \
+	-I$(top_srcdir)/src/imap \
+	-I$(top_srcdir)/src/plugins/stats
+
+imap_moduledir = $(moduledir)
+
+NOPLUGIN_LDFLAGS =
+lib95_imap_stats_plugin_la_LDFLAGS = -module -avoid-version
+
+imap_module_LTLIBRARIES = \
+	lib95_imap_stats_plugin.la
+
+if DOVECOT_PLUGIN_DEPS
+lib95_imap_stats_plugin_la_LIBADD = \
+	../stats/lib90_stats_plugin.la
+endif
+
+lib95_imap_stats_plugin_la_SOURCES = \
+	imap-stats-plugin.c
+
+noinst_HEADERS = \
+	imap-stats-plugin.h
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/plugins/imap-stats/imap-stats-plugin.c	Fri Aug 26 05:15:12 2011 +0300
@@ -0,0 +1,110 @@
+/* Copyright (c) 2011 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "str.h"
+#include "imap-commands.h"
+#include "stats-plugin.h"
+#include "stats-connection.h"
+#include "imap-stats-plugin.h"
+
+#define IMAP_STATS_IMAP_CONTEXT(obj) \
+	MODULE_CONTEXT(obj, imap_stats_imap_module)
+
+struct stats_client_command {
+	union imap_module_context module_ctx;
+
+	unsigned int id;
+	bool continued;
+	struct mail_stats stats, pre_stats;
+	struct mailbox_transaction_stats pre_trans_stats;
+};
+
+static MODULE_CONTEXT_DEFINE_INIT(imap_stats_imap_module,
+				  &imap_module_register);
+
+const char *imap_stats_plugin_version = DOVECOT_VERSION;
+
+static void stats_command_pre(struct client_command_context *cmd)
+{
+	struct stats_user *suser = STATS_USER_CONTEXT(cmd->client->user);
+	struct stats_client_command *scmd;
+	static unsigned int stats_cmd_id_counter = 0;
+
+	if (suser == NULL)
+		return;
+
+	scmd = IMAP_STATS_IMAP_CONTEXT(cmd);
+	if (scmd == NULL) {
+		scmd = p_new(cmd->pool, struct stats_client_command, 1);
+		scmd->id = ++stats_cmd_id_counter;
+		MODULE_CONTEXT_SET(cmd, imap_stats_imap_module, scmd);
+	}
+	mail_stats_get(suser, &scmd->pre_stats);
+	scmd->pre_trans_stats = suser->session_stats.trans_stats;
+}
+
+static void stats_command_post(struct client_command_context *cmd)
+{
+	struct stats_user *suser = STATS_USER_CONTEXT(cmd->client->user);
+	struct stats_client_command *scmd = IMAP_STATS_IMAP_CONTEXT(cmd);
+	struct mail_stats stats, pre_trans_stats, trans_stats;
+	unsigned int args_pos = 0;
+	string_t *str;
+	bool done;
+
+	if (scmd == NULL)
+		return;
+
+	mail_stats_get(suser, &stats);
+	mail_stats_add_diff(&scmd->stats, &scmd->pre_stats, &stats);
+
+	/* mail_stats_get() can't see the transactions that already went
+	   away, so we'll need to use the session's stats difference */
+	memset(&pre_trans_stats, 0, sizeof(pre_trans_stats));
+	memset(&trans_stats, 0, sizeof(trans_stats));
+	pre_trans_stats.trans_stats = scmd->pre_trans_stats;
+	trans_stats.trans_stats = suser->session_stats.trans_stats;
+	mail_stats_add_diff(&scmd->stats, &pre_trans_stats, &trans_stats);
+
+	str = t_str_new(128);
+	str_append(str, "UPDATE-CMD\t");
+	str_append(str, guid_128_to_string(suser->session_guid));
+
+	done = cmd->state == CLIENT_COMMAND_STATE_DONE;
+	str_printfa(str, "\t%u\t%d\t", scmd->id, done);
+	if (scmd->continued)
+		str_append_c(str, '\t');
+	else {
+		str_append(str, cmd->name);
+		str_append_c(str, '\t');
+		args_pos = str_len(str);
+		if (cmd->args != NULL)
+			str_append(str, cmd->args);
+		scmd->continued = TRUE;
+	}
+
+	mail_stats_export(str, &scmd->stats);
+	str_append_c(str, '\n');
+
+	if (str_len(str) > PIPE_BUF) {
+		/* truncate the args so it fits */
+		i_assert(args_pos != 0);
+		str_delete(str, args_pos, str_len(str) - PIPE_BUF);
+		i_assert(str_len(str) == PIPE_BUF);
+	}
+
+	stats_connection_send(suser->stats_conn, str);
+}
+
+void imap_stats_plugin_init(struct module *module ATTR_UNUSED)
+{
+	command_hook_register(stats_command_pre, stats_command_post);
+}
+
+void imap_stats_plugin_deinit(void)
+{
+	command_hook_unregister(stats_command_pre, stats_command_post);
+}
+
+const char *imap_stats_plugin_dependencies[] = { "stats", NULL };
+const char imap_stats_plugin_binary_dependency[] = "imap";
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/plugins/imap-stats/imap-stats-plugin.h	Fri Aug 26 05:15:12 2011 +0300
@@ -0,0 +1,12 @@
+#ifndef IMAP_STATS_PLUGIN_H
+#define IMAP_STATS_PLUGIN_H
+
+struct module;
+
+extern const char *imap_stats_plugin_dependencies[];
+extern const char imap_stats_plugin_binary_dependency[];
+
+void imap_stats_plugin_init(struct module *module);
+void imap_stats_plugin_deinit(void);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/plugins/stats/Makefile.am	Fri Aug 26 05:15:12 2011 +0300
@@ -0,0 +1,19 @@
+AM_CPPFLAGS = \
+	-I$(top_srcdir)/src/lib \
+	-I$(top_srcdir)/src/lib-mail \
+	-I$(top_srcdir)/src/lib-index \
+	-I$(top_srcdir)/src/lib-storage
+
+NOPLUGIN_LDFLAGS =
+lib90_stats_plugin_la_LDFLAGS = -module -avoid-version
+
+module_LTLIBRARIES = \
+	lib90_stats_plugin.la
+
+lib90_stats_plugin_la_SOURCES = \
+	stats-connection.c \
+	stats-plugin.c
+
+noinst_HEADERS = \
+	stats-connection.h \
+	stats-plugin.h
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/plugins/stats/stats-connection.c	Fri Aug 26 05:15:12 2011 +0300
@@ -0,0 +1,135 @@
+/* Copyright (c) 2011 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "network.h"
+#include "str.h"
+#include "strescape.h"
+#include "mail-storage.h"
+#include "stats-plugin.h"
+#include "stats-connection.h"
+
+struct stats_connection {
+	int refcount;
+
+	int fd;
+	char *path;
+};
+
+struct stats_connection *
+stats_connection_create(const char *path)
+{
+	struct stats_connection *conn;
+
+	conn = i_new(struct stats_connection, 1);
+	conn->refcount = 1;
+	conn->path = i_strdup(path);
+	conn->fd = open(path, O_WRONLY);
+	if (conn->fd == -1)
+		i_error("stats: open(%s) failed: %m", path);
+	return conn;
+}
+
+void stats_connection_ref(struct stats_connection *conn)
+{
+	conn->refcount++;
+}
+
+void stats_connection_unref(struct stats_connection **_conn)
+{
+	struct stats_connection *conn = *_conn;
+
+	i_assert(conn->refcount > 0);
+	if (--conn->refcount > 0)
+		return;
+
+	*_conn = NULL;
+	if (conn->fd != -1) {
+		if (close(conn->fd) < 0)
+			i_error("close(%s) failed: %m", conn->path);
+	}
+	i_free(conn->path);
+	i_free(conn);
+}
+
+void stats_connection_send(struct stats_connection *conn, const string_t *str)
+{
+	static bool pipe_warned = FALSE;
+	ssize_t ret;
+
+	if (conn->fd == -1)
+		return;
+
+	if (str_len(str) > PIPE_BUF && !pipe_warned) {
+		i_warning("stats update sent more bytes that PIPE_BUF "
+			  "(%"PRIuSIZE_T" > %u), this may break statistics",
+			  str_len(str), (unsigned int)PIPE_BUF);
+		pipe_warned = TRUE;
+	}
+
+	ret = write(conn->fd, str_data(str), str_len(str));
+	if (ret != (ssize_t)str_len(str)) {
+		if (ret < 0)
+			i_error("write(%s) failed: %m", conn->path);
+		else if ((size_t)ret != str_len(str))
+			i_error("write(%s) wrote partial update", conn->path);
+		/* this shouldn't happen, just stop sending updates */
+		if (close(conn->fd) < 0)
+			i_error("close(%s) failed: %m", conn->path);
+		conn->fd = -1;
+	}
+}
+
+void stats_connection_connect(struct stats_connection *conn,
+			      struct mail_user *user)
+{
+	struct stats_user *suser = STATS_USER_CONTEXT(user);
+	string_t *str = t_str_new(128);
+
+	str_append(str, "CONNECT\t");
+	/* required fields */
+	str_append(str, guid_128_to_string(suser->session_guid));
+	str_append_c(str, '\t');
+	str_tabescape_write(str, user->username);
+	str_append_c(str, '\t');
+	str_tabescape_write(str, user->service);
+
+	/* optional fields */
+	if (user->local_ip != NULL) {
+		str_append(str, "\tlip=");
+		str_append(str, net_ip2addr(user->local_ip));
+	}
+	if (user->remote_ip != NULL) {
+		str_append(str, "\trip=");
+		str_append(str, net_ip2addr(user->remote_ip));
+	}
+	str_append_c(str, '\n');
+	stats_connection_send(conn, str);
+}
+
+void stats_connection_disconnect(struct stats_connection *conn,
+				 struct mail_user *user)
+{
+	struct stats_user *suser = STATS_USER_CONTEXT(user);
+	string_t *str = t_str_new(128);
+
+	str_append(str, "DISCONNECT\t");
+	str_append(str, guid_128_to_string(suser->session_guid));
+	str_append_c(str, '\n');
+	stats_connection_send(conn, str);
+}
+
+void stats_connection_send_session(struct stats_connection *conn,
+				   struct mail_user *user,
+				   const struct mail_stats *stats)
+{
+	struct stats_user *suser = STATS_USER_CONTEXT(user);
+	string_t *str = t_str_new(128);
+
+	str_append(str, "UPDATE-SESSION\t");
+	str_append(str, guid_128_to_string(suser->session_guid));
+
+	mail_stats_export(str, stats);
+
+	str_append_c(str, '\n');
+	stats_connection_send(conn, str);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/plugins/stats/stats-connection.h	Fri Aug 26 05:15:12 2011 +0300
@@ -0,0 +1,21 @@
+#ifndef STATS_CONNECTION_H
+#define STATS_CONNECTION_H
+
+struct mail_stats;
+struct mail_user;
+
+struct stats_connection *stats_connection_create(const char *path);
+void stats_connection_ref(struct stats_connection *conn);
+void stats_connection_unref(struct stats_connection **conn);
+
+void stats_connection_connect(struct stats_connection *conn,
+			      struct mail_user *user);
+void stats_connection_disconnect(struct stats_connection *conn,
+				 struct mail_user *user);
+
+void stats_connection_send_session(struct stats_connection *conn,
+				   struct mail_user *user,
+				   const struct mail_stats *stats);
+void stats_connection_send(struct stats_connection *conn, const string_t *str);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/plugins/stats/stats-plugin.c	Fri Aug 26 05:15:12 2011 +0300
@@ -0,0 +1,394 @@
+/* Copyright (c) 2011 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "hostpid.h"
+#include "llist.h"
+#include "str.h"
+#include "time-util.h"
+#include "stats-connection.h"
+#include "stats-plugin.h"
+
+#include <sys/time.h>
+#include <sys/resource.h>
+
+#define STATS_CONTEXT(obj) \
+	MODULE_CONTEXT(obj, stats_storage_module)
+
+/* Refresh session every 10 seconds, if anything has changed */
+#define SESSION_STATS_REFRESH_MSECS (10*1000)
+/* If session isn't refreshed every 15 minutes, it's dropped.
+   Must be smaller than MAIL_SESSION_IDLE_TIMEOUT_MSECS in stats server */
+#define SESSION_STATS_FORCE_REFRESH_SECS (5*60)
+#define MAIL_STATS_SOCKET_NAME "stats-mail"
+
+#define USECS_PER_SEC 1000000
+
+struct stats_transaction_context {
+	union mailbox_transaction_module_context module_ctx;
+
+	struct stats_transaction_context *prev, *next;
+	struct mailbox_transaction_context *trans;
+
+	struct mailbox_transaction_stats prev_stats;
+};
+
+struct stats_mailbox {
+	union mailbox_module_context module_ctx;
+};
+
+const char *stats_plugin_version = DOVECOT_VERSION;
+
+struct stats_user_module stats_user_module =
+	MODULE_CONTEXT_INIT(&mail_user_module_register);
+static MODULE_CONTEXT_DEFINE_INIT(stats_storage_module,
+				  &mail_storage_module_register);
+
+static struct stats_connection *global_stats_conn = NULL;
+
+static int
+process_io_buffer_parse(const char *buf, uint64_t *read_bytes_r,
+			uint64_t *write_bytes_r)
+{
+	const char *const *tmp;
+	uint64_t cbytes;
+
+	tmp = t_strsplit(buf, "\n");
+	for (; *tmp != NULL; tmp++) {
+		if (strncmp(*tmp, "read_bytes: ", 12) == 0) {
+			if (str_to_uint64(*tmp + 12, read_bytes_r) < 0)
+				return -1;
+		} else if (strncmp(*tmp, "write_bytes: ", 13) == 0) {
+			if (str_to_uint64(*tmp + 13, write_bytes_r) < 0)
+				return -1;
+		} else if (strncmp(*tmp, "cancelled_write_bytes: ", 23) == 0) {
+			if (str_to_uint64(*tmp + 23, &cbytes) < 0)
+				return -1;
+			/* it's not 100% correct to simply subtract the
+			   cancelled bytes from write bytes, but it's close
+			   enough. */
+			if (*write_bytes_r < cbytes)
+				*write_bytes_r = 0;
+			else
+				*write_bytes_r -= cbytes;
+		}
+	}
+	return 0;
+}
+
+static void
+process_read_io_stats(uint64_t *read_bytes_r, uint64_t *write_bytes_r)
+{
+	static bool io_disabled = FALSE;
+	char path[100], buf[1024];
+	int fd, ret;
+
+	*read_bytes_r = *write_bytes_r = 0;
+
+	if (io_disabled)
+		return;
+
+	i_snprintf(path, sizeof(path), "/proc/%s/io", my_pid);
+	fd = open(path, O_RDONLY);
+	if (fd == -1) {
+		if (errno != ENOENT)
+			i_error("open(%s) failed: %m", path);
+		io_disabled = TRUE;
+		return;
+	}
+	ret = read(fd, buf, sizeof(buf));
+	if (ret <= 0) {
+		if (ret == -1)
+			i_error("read(%s) failed: %m", path);
+		else
+			i_error("read(%s) returned EOF", path);
+	} else if (ret == sizeof(buf)) {
+		/* just shouldn't happen.. */
+		i_error("%s is larger than expected", path);
+		io_disabled = TRUE;
+	} else {
+		buf[ret] = '\0';
+		T_BEGIN {
+			if (process_io_buffer_parse(buf, read_bytes_r,
+						    write_bytes_r) < 0) {
+				i_error("Invalid input in file %s", path);
+				io_disabled = TRUE;
+			}
+		} T_END;
+	}
+	if (close(fd) < 0)
+		i_error("close(%s) failed: %m", path);
+}
+
+static void trans_stats_dec(struct mailbox_transaction_stats *dest,
+			    const struct mailbox_transaction_stats *src)
+{
+	dest->open_lookup_count -= src->open_lookup_count;
+	dest->stat_lookup_count -= src->stat_lookup_count;
+	dest->fstat_lookup_count -= src->fstat_lookup_count;
+	dest->files_read_count -= src->files_read_count;
+	dest->files_read_bytes -= src->files_read_bytes;
+	dest->cache_hit_count -= src->cache_hit_count;
+}
+
+static void trans_stats_add(struct mailbox_transaction_stats *dest,
+			    const struct mailbox_transaction_stats *src)
+{
+	dest->open_lookup_count += src->open_lookup_count;
+	dest->stat_lookup_count += src->stat_lookup_count;
+	dest->fstat_lookup_count += src->fstat_lookup_count;
+	dest->files_read_count += src->files_read_count;
+	dest->files_read_bytes += src->files_read_bytes;
+	dest->cache_hit_count += src->cache_hit_count;
+}
+
+static void user_trans_stats_get(struct stats_user *suser,
+				 struct mailbox_transaction_stats *dest_r)
+{
+	struct stats_transaction_context *strans;
+
+	memset(dest_r, 0, sizeof(*dest_r));
+	strans = suser->transactions;
+	for (; strans != NULL; strans = strans->next)
+		trans_stats_add(dest_r, &strans->trans->stats);
+}
+
+void mail_stats_get(struct stats_user *suser, struct mail_stats *stats_r)
+{
+	struct rusage usage;
+
+	memset(stats_r, 0, sizeof(*stats_r));
+	/* cputime */
+	if (getrusage(RUSAGE_SELF, &usage) < 0)
+		memset(&usage, 0, sizeof(usage));
+	stats_r->cpu_secs = usage.ru_utime;
+	/* disk io */
+	process_read_io_stats(&stats_r->disk_input, &stats_r->disk_output);
+	user_trans_stats_get(suser, &stats_r->trans_stats);
+}
+
+static struct mailbox_transaction_context *
+stats_transaction_begin(struct mailbox *box,
+			enum mailbox_transaction_flags flags)
+{
+	struct stats_user *suser = STATS_USER_CONTEXT(box->storage->user);
+	struct stats_mailbox *sbox = STATS_CONTEXT(box);
+	struct mailbox_transaction_context *trans;
+	struct stats_transaction_context *strans;
+
+	trans = sbox->module_ctx.super.transaction_begin(box, flags);
+	trans->stats_track = TRUE;
+
+	strans = i_new(struct stats_transaction_context, 1);
+	strans->trans = trans;
+	DLLIST_PREPEND(&suser->transactions, strans);
+
+	MODULE_CONTEXT_SET(trans, stats_storage_module, strans);
+	return trans;
+}
+
+static void stats_transaction_free(struct stats_user *suser,
+				   struct stats_transaction_context *strans)
+{
+	DLLIST_REMOVE(&suser->transactions, strans);
+
+	trans_stats_add(&suser->session_stats.trans_stats,
+			&strans->trans->stats);
+}
+
+static int
+stats_transaction_commit(struct mailbox_transaction_context *ctx,
+			 struct mail_transaction_commit_changes *changes_r)
+{
+	struct stats_transaction_context *strans = STATS_CONTEXT(ctx);
+	struct stats_mailbox *sbox = STATS_CONTEXT(ctx->box);
+	struct stats_user *suser = STATS_USER_CONTEXT(ctx->box->storage->user);
+
+	stats_transaction_free(suser, strans);
+	return sbox->module_ctx.super.transaction_commit(ctx, changes_r);
+}
+
+static void
+stats_transaction_rollback(struct mailbox_transaction_context *ctx)
+{
+	struct stats_transaction_context *strans = STATS_CONTEXT(ctx);
+	struct stats_mailbox *sbox = STATS_CONTEXT(ctx->box);
+	struct stats_user *suser = STATS_USER_CONTEXT(ctx->box->storage->user);
+
+	stats_transaction_free(suser, strans);
+	sbox->module_ctx.super.transaction_rollback(ctx);
+}
+
+static void stats_mailbox_allocated(struct mailbox *box)
+{
+	struct mailbox_vfuncs *v = box->vlast;
+	struct stats_mailbox *sbox;
+
+	sbox = p_new(box->pool, struct stats_mailbox, 1);
+	sbox->module_ctx.super = *v;
+	box->vlast = &sbox->module_ctx.super;
+
+	v->transaction_begin = stats_transaction_begin;
+	v->transaction_commit = stats_transaction_commit;
+	v->transaction_rollback = stats_transaction_rollback;
+	MODULE_CONTEXT_SET(box, stats_storage_module, sbox);
+}
+
+static void stats_io_activate(void *context)
+{
+	struct mail_user *user = context;
+	struct stats_user *suser = STATS_USER_CONTEXT(user);
+
+	mail_stats_get(suser, &suser->pre_io_stats);
+}
+
+void mail_stats_add_diff(struct mail_stats *dest,
+			 const struct mail_stats *old_stats,
+			 const struct mail_stats *new_stats)
+{
+	struct timeval *tv;
+	long long usecs;
+
+	dest->disk_input += new_stats->disk_input - old_stats->disk_input;
+	dest->disk_output += new_stats->disk_output - old_stats->disk_output;
+
+	usecs = timeval_diff_usecs(&new_stats->cpu_secs, &old_stats->cpu_secs);
+	tv = &dest->cpu_secs;
+	tv->tv_sec += usecs / USECS_PER_SEC;
+	tv->tv_usec += usecs % USECS_PER_SEC;
+	if (tv->tv_usec > USECS_PER_SEC) {
+		tv->tv_usec -= USECS_PER_SEC;
+		tv->tv_sec++;
+	}
+	trans_stats_dec(&dest->trans_stats, &old_stats->trans_stats);
+	trans_stats_add(&dest->trans_stats, &new_stats->trans_stats);
+}
+
+void mail_stats_export(string_t *str, const struct mail_stats *stats)
+{
+	const struct mailbox_transaction_stats *tstats = &stats->trans_stats;
+
+	str_printfa(str, "\tcpu=%ld.%ld", (long)stats->cpu_secs.tv_sec,
+		    (long)stats->cpu_secs.tv_usec);
+	str_printfa(str, "\tdiskin=%llu",
+		    (unsigned long long)stats->disk_input);
+	str_printfa(str, "\tdiskout=%llu",
+		    (unsigned long long)stats->disk_output);
+	str_printfa(str, "\tlpath=%lu",
+		    tstats->open_lookup_count + tstats->stat_lookup_count);
+	str_printfa(str, "\tlattr=%lu",
+		    tstats->fstat_lookup_count + tstats->stat_lookup_count);
+	str_printfa(str, "\trcount=%lu", tstats->files_read_count);
+	str_printfa(str, "\trbytes=%llu", tstats->files_read_bytes);
+	str_printfa(str, "\tcache=%lu", tstats->cache_hit_count);
+}
+
+static void stats_io_deactivate(void *context)
+{
+	struct mail_user *user = context;
+	struct stats_user *suser = STATS_USER_CONTEXT(user);
+	struct mail_stats new_stats;
+
+	mail_stats_get(suser, &new_stats);
+	mail_stats_add_diff(&suser->session_stats, &suser->pre_io_stats,
+			    &new_stats);
+}
+
+static bool session_stats_need_send(struct stats_user *suser)
+{
+	if (memcmp(&suser->last_sent_session_stats, &suser->session_stats,
+		   sizeof(suser->session_stats)) != 0)
+		return TRUE;
+
+	return ioloop_time - suser->last_session_update >=
+		SESSION_STATS_FORCE_REFRESH_SECS;
+}
+
+static void session_stats_refresh(struct mail_user *user)
+{
+	struct stats_user *suser = STATS_USER_CONTEXT(user);
+
+	if (session_stats_need_send(suser)) {
+		suser->last_session_update = ioloop_time;
+		suser->last_sent_session_stats = suser->session_stats;
+		stats_connection_send_session(suser->stats_conn, user,
+					      &suser->session_stats);
+	}
+}
+
+static void stats_user_deinit(struct mail_user *user)
+{
+	struct stats_user *suser = STATS_USER_CONTEXT(user);
+	struct stats_connection *stats_conn = suser->stats_conn;
+
+	io_loop_context_remove_callbacks(suser->ioloop_ctx,
+					 stats_io_activate,
+					 stats_io_deactivate, user);
+	/* send final stats before disconnection */
+	session_stats_refresh(user);
+	stats_connection_disconnect(stats_conn, user);
+
+	timeout_remove(&suser->to_stats_timeout);
+	suser->module_ctx.super.deinit(user);
+
+	stats_connection_unref(&stats_conn);
+}
+
+static void stats_user_created(struct mail_user *user)
+{
+	struct ioloop_context *ioloop_ctx =
+		io_loop_get_current_context(current_ioloop);
+	struct stats_user *suser;
+	struct mail_user_vfuncs *v = user->vlast;
+	const char *path;
+
+	if (ioloop_ctx == NULL) {
+		/* we're probably running some test program, or at least
+		   mail-storage-service wasn't used to create this user.
+		   disable stats tracking. */
+		return;
+	}
+
+	if (global_stats_conn == NULL) {
+		path = t_strconcat(user->set->base_dir,
+				   "/"MAIL_STATS_SOCKET_NAME, NULL);
+		global_stats_conn = stats_connection_create(path);
+	} else {
+		stats_connection_ref(global_stats_conn);
+	}
+
+	suser = p_new(user->pool, struct stats_user, 1);
+	suser->module_ctx.super = *v;
+	user->vlast = &suser->module_ctx.super;
+	user->v.deinit = stats_user_deinit;
+
+	suser->stats_conn = global_stats_conn;
+	guid_128_generate(suser->session_guid);
+	suser->last_session_update = ioloop_time;
+	suser->to_stats_timeout = timeout_add(SESSION_STATS_REFRESH_MSECS,
+					      session_stats_refresh, user);
+
+	suser->ioloop_ctx = ioloop_ctx;
+	io_loop_context_add_callbacks(ioloop_ctx,
+				      stats_io_activate,
+				      stats_io_deactivate, user);
+
+	MODULE_CONTEXT_SET(user, stats_user_module, suser);
+	stats_connection_connect(suser->stats_conn, user);
+}
+
+static struct mail_storage_hooks stats_mail_storage_hooks = {
+	.mailbox_allocated = stats_mailbox_allocated,
+	.mail_user_created = stats_user_created
+};
+
+void stats_plugin_init(struct module *module)
+{
+	mail_storage_hooks_add(module, &stats_mail_storage_hooks);
+}
+
+void stats_plugin_deinit(void)
+{
+	mail_storage_hooks_remove(&stats_mail_storage_hooks);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/plugins/stats/stats-plugin.h	Fri Aug 26 05:15:12 2011 +0300
@@ -0,0 +1,51 @@
+#ifndef STATS_PLUGIN_H
+#define STATS_PLUGIN_H
+
+#include "module-context.h"
+#include "guid.h"
+#include "mail-user.h"
+#include "mail-storage-private.h"
+
+#define STATS_USER_CONTEXT(obj) \
+	MODULE_CONTEXT(obj, stats_user_module)
+
+struct mail_stats {
+	struct timeval cpu_secs;
+	uint64_t disk_input, disk_output;
+	struct mailbox_transaction_stats trans_stats;
+};
+
+struct stats_user {
+	union mail_user_module_context module_ctx;
+
+	struct ioloop_context *ioloop_ctx;
+	struct stats_connection *stats_conn;
+	guid_128_t session_guid;
+
+	/* current session statistics */
+	struct mail_stats session_stats;
+	/* stats before calling IO callback. after IO callback this value is
+	   compared to current stats to see the difference */
+	struct mail_stats pre_io_stats;
+
+	time_t last_session_update;
+	struct timeout *to_stats_timeout;
+	/* stats that were last sent to stats server */
+	struct mail_stats last_sent_session_stats;
+
+	/* list of all currently existing transactions for this user */
+	struct stats_transaction_context *transactions;
+};
+
+extern MODULE_CONTEXT_DEFINE(stats_user_module, &mail_user_module_register);
+
+void mail_stats_get(struct stats_user *suser, struct mail_stats *stats_r);
+void mail_stats_add_diff(struct mail_stats *dest,
+			 const struct mail_stats *old_stats,
+			 const struct mail_stats *new_stats);
+void mail_stats_export(string_t *str, const struct mail_stats *stats);
+
+void stats_plugin_init(struct module *module);
+void stats_plugin_deinit(void);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/stats/Makefile.am	Fri Aug 26 05:15:12 2011 +0300
@@ -0,0 +1,40 @@
+pkglibexecdir = $(libexecdir)/dovecot
+
+pkglibexec_PROGRAMS = stats
+
+AM_CPPFLAGS = \
+	-I$(top_srcdir)/src/lib \
+	-I$(top_srcdir)/src/lib-settings \
+	-I$(top_srcdir)/src/lib-master
+
+stats_LDADD = \
+	$(LIBDOVECOT) \
+	$(MODULE_LIBS)
+stats_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+
+stats_SOURCES = \
+	client.c \
+	client-export.c \
+	global-memory.c \
+	mail-command.c \
+	mail-domain.c \
+	mail-ip.c \
+	mail-server-connection.c \
+	mail-session.c \
+	mail-stats.c \
+	mail-user.c \
+	main.c \
+	stats-settings.c
+
+noinst_HEADERS = \
+	client.h \
+	client-export.h \
+	global-memory.h \
+	mail-command.h \
+	mail-domain.h \
+	mail-ip.h \
+	mail-server-connection.h \
+	mail-session.h \
+	mail-stats.h \
+	mail-user.h \
+	stats-settings.h
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/stats/client-export.c	Fri Aug 26 05:15:12 2011 +0300
@@ -0,0 +1,586 @@
+/* Copyright (c) 2011 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "network.h"
+#include "ostream.h"
+#include "str.h"
+#include "strescape.h"
+#include "wildcard-match.h"
+#include "mail-stats.h"
+#include "mail-command.h"
+#include "mail-session.h"
+#include "mail-user.h"
+#include "mail-domain.h"
+#include "mail-ip.h"
+#include "client.h"
+#include "client-export.h"
+
+enum mail_export_level {
+	MAIL_EXPORT_LEVEL_COMMAND,
+	MAIL_EXPORT_LEVEL_SESSION,
+	MAIL_EXPORT_LEVEL_USER,
+	MAIL_EXPORT_LEVEL_DOMAIN,
+	MAIL_EXPORT_LEVEL_IP
+};
+static const char *mail_export_level_names[] = {
+	"command", "session", "user", "domain", "ip"
+};
+
+struct mail_export_filter {
+	const char *user, *domain;
+	struct ip_addr ip;
+	unsigned int ip_bits;
+	time_t since;
+	bool connected;
+};
+
+struct client_export_cmd {
+	enum mail_export_level level;
+	struct mail_export_filter filter;
+	string_t *str;
+	int (*export_iter)(struct client *client);
+	bool header_sent;
+};
+
+static int
+mail_export_level_parse(const char *str, enum mail_export_level *level_r)
+{
+	unsigned int i;
+
+	for (i = 0; i < N_ELEMENTS(mail_export_level_names); i++) {
+		if (strcmp(mail_export_level_names[i], str) == 0) {
+			*level_r = (enum mail_export_level)i;
+			return 0;
+		}
+	}
+	return -1;
+}
+
+static int
+mail_export_parse_filter(const char *const *args, pool_t pool,
+			 struct mail_export_filter *filter_r,
+			 const char **error_r)
+{
+	unsigned long l;
+
+	/* filters:
+	   user=<wildcard> | domain=<wildcard>
+	   ip=<ip>[/<mask>]
+	   since=<timestamp>
+	   connected
+	*/
+	memset(filter_r, 0, sizeof(*filter_r));
+	for (; *args != NULL; args++) {
+		if (strncmp(*args, "user=", 5) == 0)
+			filter_r->user = p_strdup(pool, *args + 5);
+		else if (strncmp(*args, "domain=", 7) == 0)
+			filter_r->domain = p_strdup(pool, *args + 7);
+		else if (strncmp(*args, "ip=", 3) == 0) {
+			if (net_parse_range(*args + 3, &filter_r->ip,
+					    &filter_r->ip_bits) < 0) {
+				*error_r = "Invalid ip filter";
+				return -1;
+			}
+		} else if (strncmp(*args, "since=", 6) == 0) {
+			if (str_to_ulong(*args + 6, &l) < 0) {
+				*error_r = "Invalid since filter";
+				return -1;
+			}
+			filter_r->since = (time_t)l;
+		} else if (strcmp(*args, "connected") == 0) {
+			filter_r->connected = TRUE;
+		}
+	}
+	return 0;
+}
+
+static void
+client_export_mail_stats(string_t *str, const struct mail_stats *stats)
+{
+#define MAIL_STATS_HEADER "\tcpu\tdisk_input\tdisk_output" \
+	"\tlookup_path\tlookup_attr\tread_count\tread_bytes\tcache_hits\n"
+
+	str_printfa(str, "\t%ld.%u", (long)stats->cpu_secs.tv_sec,
+		    (unsigned int)stats->cpu_secs.tv_usec);
+	str_printfa(str, "\t%llu\t%llu",
+		    (unsigned long long)stats->disk_input,
+		    (unsigned long long)stats->disk_output);
+	str_printfa(str, "\t%u\t%u\t%u\t%llu\t%u",
+		    stats->lookup_path, stats->lookup_attr,
+		    stats->read_count,
+		    (unsigned long long)stats->read_bytes,
+		    stats->cache_hits);
+}
+
+static bool
+mail_export_filter_match_session(const struct mail_export_filter *filter,
+				 const struct mail_session *session)
+{
+	if (filter->connected && session->disconnected)
+		return FALSE;
+	if (filter->since > session->last_update)
+		return FALSE;
+	if (filter->user != NULL &&
+	    !wildcard_match(session->user->name, filter->user))
+		return FALSE;
+	if (filter->domain != NULL &&
+	    !wildcard_match(session->user->domain->name, filter->domain))
+		return FALSE;
+	if (filter->ip_bits > 0 &&
+	    !net_is_in_network(&session->ip->ip, &filter->ip, filter->ip_bits))
+		return FALSE;
+	return TRUE;
+}
+
+static bool
+mail_export_filter_match_user_common(const struct mail_export_filter *filter,
+				     const struct mail_user *user)
+{
+	struct mail_session *s;
+	bool connected = FALSE, ip_ok = FALSE;
+
+	if (filter->user != NULL &&
+	    !wildcard_match(user->name, filter->user))
+		return FALSE;
+
+	if (filter->connected || filter->ip_bits > 0) {
+		for (s = user->sessions; s != NULL; s = s->user_next) {
+			if (!s->disconnected)
+				connected = TRUE;
+			if (filter->ip_bits > 0 &&
+			    net_is_in_network(&s->ip->ip, &filter->ip,
+					      filter->ip_bits))
+				ip_ok = TRUE;
+
+		}
+		if (filter->connected && !connected)
+			return FALSE;
+		if (filter->ip_bits > 0 && !ip_ok)
+			return FALSE;
+	}
+	return TRUE;
+}
+
+static bool
+mail_export_filter_match_user(const struct mail_export_filter *filter,
+			      const struct mail_user *user)
+{
+	if (filter->since > user->last_update)
+		return FALSE;
+	if (filter->domain != NULL &&
+	    !wildcard_match(user->domain->name, filter->domain))
+		return FALSE;
+	return mail_export_filter_match_user_common(filter, user);
+}
+
+static bool
+mail_export_filter_match_domain(const struct mail_export_filter *filter,
+				const struct mail_domain *domain)
+{
+	struct mail_user *user;
+
+	if (filter->since > domain->last_update)
+		return FALSE;
+	if (filter->domain != NULL &&
+	    !wildcard_match(domain->name, filter->domain))
+		return FALSE;
+
+	if (filter->user != NULL || filter->connected || filter->ip_bits > 0) {
+		for (user = domain->users; user != NULL; user = user->domain_next) {
+			if (mail_export_filter_match_user_common(filter, user))
+				break;
+		}
+		if (user == NULL)
+			return FALSE;
+	}
+	return TRUE;
+}
+
+static bool
+mail_export_filter_match_ip(const struct mail_export_filter *filter,
+			    const struct mail_ip *ip)
+{
+	struct mail_session *s;
+	bool connected = FALSE, user_ok = FALSE, domain_ok = FALSE;
+
+	if (filter->connected || filter->ip_bits > 0) {
+		for (s = ip->sessions; s != NULL; s = s->ip_next) {
+			if (!s->disconnected)
+				connected = TRUE;
+			if (filter->user != NULL &&
+			    wildcard_match(s->user->name, filter->user))
+				user_ok = TRUE;
+			if (filter->domain != NULL &&
+			    wildcard_match(s->user->domain->name, filter->domain))
+				domain_ok = TRUE;
+		}
+		if (filter->connected && !connected)
+			return FALSE;
+		if (filter->user != NULL && !user_ok)
+			return FALSE;
+		if (filter->domain != NULL && !domain_ok)
+			return FALSE;
+	}
+	if (filter->since > ip->last_update)
+		return FALSE;
+	if (filter->ip_bits > 0 &&
+	    !net_is_in_network(&ip->ip, &filter->ip, filter->ip_bits))
+		return FALSE;
+	return TRUE;
+}
+
+static int client_export_iter_command(struct client *client)
+{
+	struct client_export_cmd *cmd = client->cmd_export;
+	struct mail_command *command = client->mail_cmd_iter;
+
+	i_assert(cmd->level == MAIL_EXPORT_LEVEL_SESSION);
+	mail_command_unref(&client->mail_cmd_iter);
+
+	if (!cmd->header_sent) {
+		o_stream_send_str(client->output,
+			"cmd\targs\tsession\tlast_update"MAIL_STATS_HEADER);
+		cmd->header_sent = TRUE;
+	}
+
+	for (; command != NULL; command = command->stable_next) {
+		if (client_is_busy(client))
+			break;
+		if (!mail_export_filter_match_session(&cmd->filter,
+						      command->session))
+			continue;
+
+		str_truncate(cmd->str, 0);
+		str_tabescape_write(cmd->str, command->name);
+		str_append_c(cmd->str, '\t');
+		str_tabescape_write(cmd->str, command->args);
+		T_BEGIN {
+			str_append(cmd->str,
+				   guid_128_to_string(command->session->guid));
+		} T_END;
+		str_printfa(cmd->str, "\t%ld", (long)command->last_update);
+		client_export_mail_stats(cmd->str, &command->stats);
+		str_append_c(cmd->str, '\n');
+		o_stream_send(client->output, str_data(cmd->str),
+			      str_len(cmd->str));
+	}
+
+	if (command != NULL) {
+		client->mail_cmd_iter = command;
+		mail_command_ref(command);
+		return 0;
+	}
+	return 1;
+}
+
+static int client_export_iter_session(struct client *client)
+{
+	struct client_export_cmd *cmd = client->cmd_export;
+	struct mail_session *session = client->mail_session_iter;
+
+	i_assert(cmd->level == MAIL_EXPORT_LEVEL_SESSION);
+	mail_session_unref(&client->mail_session_iter);
+
+	if (!cmd->header_sent) {
+		o_stream_send_str(client->output,
+			"session\tuser\tservice\tconnected"
+			"\tlast_update\tnum_cmds"
+			MAIL_STATS_HEADER);
+		cmd->header_sent = TRUE;
+	}
+
+	for (; session != NULL; session = session->stable_next) {
+		if (client_is_busy(client))
+			break;
+		if (!mail_export_filter_match_session(&cmd->filter, session))
+			continue;
+
+		str_truncate(cmd->str, 0);
+		T_BEGIN {
+			str_append(cmd->str, guid_128_to_string(session->guid));
+		} T_END;
+		str_append_c(cmd->str, '\t');
+		str_tabescape_write(cmd->str, session->user->name);
+		str_append_c(cmd->str, '\t');
+		str_tabescape_write(cmd->str, session->service);
+		str_printfa(cmd->str, "\t%d\t%ld\t%u",
+			    !session->disconnected,
+			    (long)session->last_update,
+			    session->num_cmds);
+		client_export_mail_stats(cmd->str, &session->stats);
+		str_append_c(cmd->str, '\n');
+		o_stream_send(client->output, str_data(cmd->str),
+			      str_len(cmd->str));
+	}
+
+	if (session != NULL) {
+		client->mail_session_iter = session;
+		mail_session_ref(session);
+		return 0;
+	}
+	return 1;
+}
+
+static int client_export_iter_user(struct client *client)
+{
+	struct client_export_cmd *cmd = client->cmd_export;
+	struct mail_user *user = client->mail_user_iter;
+
+	i_assert(cmd->level == MAIL_EXPORT_LEVEL_USER);
+	mail_user_unref(&client->mail_user_iter);
+
+	if (!cmd->header_sent) {
+		o_stream_send_str(client->output,
+			"user\treset_timestamp\tlast_update"
+			"\tnum_logins\tnum_cmds"MAIL_STATS_HEADER);
+		cmd->header_sent = TRUE;
+	}
+
+	for (; user != NULL; user = user->stable_next) {
+		if (client_is_busy(client))
+			break;
+		if (!mail_export_filter_match_user(&cmd->filter, user))
+			continue;
+
+		str_truncate(cmd->str, 0);
+		str_tabescape_write(cmd->str, user->name);
+		str_printfa(cmd->str, "\t%ld\t%ld\t%u\t%u",
+			    (long)user->reset_timestamp,
+			    (long)user->last_update,
+			    user->num_logins, user->num_cmds);
+		client_export_mail_stats(cmd->str, &user->stats);
+		str_append_c(cmd->str, '\n');
+		o_stream_send(client->output, str_data(cmd->str),
+			      str_len(cmd->str));
+	}
+
+	if (user != NULL) {
+		client->mail_user_iter = user;
+		mail_user_ref(user);
+		return 0;
+	}
+	return 1;
+}
+
+static int client_export_iter_domain(struct client *client)
+{
+	struct client_export_cmd *cmd = client->cmd_export;
+	struct mail_domain *domain = client->mail_domain_iter;
+
+	i_assert(cmd->level == MAIL_EXPORT_LEVEL_DOMAIN);
+	mail_domain_unref(&client->mail_domain_iter);
+
+	if (!cmd->header_sent) {
+		o_stream_send_str(client->output,
+			"domain\treset_timestamp\tlast_update"
+			"\tnum_logins\tnum_cmds"MAIL_STATS_HEADER);
+		cmd->header_sent = TRUE;
+	}
+
+	for (; domain != NULL; domain = domain->stable_next) {
+		if (client_is_busy(client))
+			break;
+		if (!mail_export_filter_match_domain(&cmd->filter, domain))
+			continue;
+
+		str_truncate(cmd->str, 0);
+		str_tabescape_write(cmd->str, domain->name);
+		str_printfa(cmd->str, "\t%ld\t%ld\t%u\t%u",
+			    (long)domain->reset_timestamp,
+			    (long)domain->last_update,
+			    domain->num_logins, domain->num_cmds);
+		client_export_mail_stats(cmd->str, &domain->stats);
+		str_append_c(cmd->str, '\n');
+		o_stream_send(client->output, str_data(cmd->str),
+			      str_len(cmd->str));
+	}
+
+	if (domain != NULL) {
+		client->mail_domain_iter = domain;
+		mail_domain_ref(domain);
+		return 0;
+	}
+	return 1;
+}
+
+static int client_export_iter_ip(struct client *client)
+{
+	struct client_export_cmd *cmd = client->cmd_export;
+	struct mail_ip *ip = client->mail_ip_iter;
+
+	i_assert(cmd->level == MAIL_EXPORT_LEVEL_IP);
+	mail_ip_unref(&client->mail_ip_iter);
+
+	for (; ip != NULL; ip = ip->stable_next) {
+		if (client_is_busy(client))
+			break;
+		if (!mail_export_filter_match_ip(&cmd->filter, ip))
+			continue;
+
+		str_truncate(cmd->str, 0);
+		T_BEGIN {
+			str_append(cmd->str, net_ip2addr(&ip->ip));
+		} T_END;
+		str_printfa(cmd->str, "\t%ld\t%ld\t%u\t%u",
+			    (long)ip->reset_timestamp,
+			    (long)ip->last_update,
+			    ip->num_logins, ip->num_cmds);
+		client_export_mail_stats(cmd->str, &ip->stats);
+		str_append_c(cmd->str, '\n');
+		o_stream_send(client->output, str_data(cmd->str),
+			      str_len(cmd->str));
+	}
+
+	if (ip != NULL) {
+		client->mail_ip_iter = ip;
+		mail_ip_ref(ip);
+		return 0;
+	}
+	return 1;
+}
+
+static int client_export_more(struct client *client)
+{
+	if (client->cmd_export->export_iter(client) == 0)
+		return 0;
+	o_stream_send_str(client->output, "\n");
+	return 1;
+}
+
+static bool client_export_iter_init(struct client *client)
+{
+	struct client_export_cmd *cmd = client->cmd_export;
+
+	if (cmd->filter.user != NULL && strchr(cmd->filter.user, '*') == NULL &&
+	    (cmd->level == MAIL_EXPORT_LEVEL_USER ||
+	     cmd->level == MAIL_EXPORT_LEVEL_SESSION)) {
+		/* exact user */
+		struct mail_user *user = mail_user_lookup(cmd->filter.user);
+		if (user == NULL)
+			return FALSE;
+		if (cmd->level == MAIL_EXPORT_LEVEL_SESSION) {
+			client->mail_session_iter = user->sessions;
+			if (client->mail_session_iter == NULL)
+				return FALSE;
+			mail_session_ref(client->mail_session_iter);
+			cmd->export_iter = client_export_iter_session;
+		} else {
+			client->mail_user_iter = user;
+			mail_user_ref(user);
+			cmd->export_iter = client_export_iter_user;
+		}
+		return TRUE;
+	}
+	if (cmd->filter.ip_bits == IPADDR_BITS(&cmd->filter.ip) &&
+	    (cmd->level == MAIL_EXPORT_LEVEL_IP ||
+	     cmd->level == MAIL_EXPORT_LEVEL_SESSION)) {
+		/* exact IP address */
+		struct mail_ip *ip = mail_ip_lookup(&cmd->filter.ip);
+		if (ip == NULL)
+			return FALSE;
+		if (cmd->level == MAIL_EXPORT_LEVEL_SESSION) {
+			client->mail_session_iter = ip->sessions;
+			if (client->mail_session_iter == NULL)
+				return FALSE;
+			mail_session_ref(client->mail_session_iter);
+			cmd->export_iter = client_export_iter_session;
+		} else {
+			client->mail_ip_iter = ip;
+			mail_ip_ref(ip);
+			cmd->export_iter = client_export_iter_ip;
+		}
+		return TRUE;
+	}
+	if (cmd->filter.domain != NULL &&
+	    strchr(cmd->filter.domain, '*') == NULL &&
+	    (cmd->level == MAIL_EXPORT_LEVEL_DOMAIN ||
+	     cmd->level == MAIL_EXPORT_LEVEL_USER)) {
+		/* exact domain */
+		struct mail_domain *domain =
+			mail_domain_lookup(cmd->filter.domain);
+		if (domain == NULL)
+			return FALSE;
+		if (cmd->level == MAIL_EXPORT_LEVEL_USER) {
+			client->mail_user_iter = domain->users;
+			mail_user_ref(client->mail_user_iter);
+			cmd->export_iter = client_export_iter_user;
+		} else {
+			client->mail_domain_iter = domain;
+			mail_domain_ref(domain);
+			cmd->export_iter = client_export_iter_domain;
+		}
+		return TRUE;
+	}
+
+	switch (cmd->level) {
+	case MAIL_EXPORT_LEVEL_COMMAND:
+		client->mail_cmd_iter = stable_mail_commands;
+		if (client->mail_cmd_iter == NULL)
+			return FALSE;
+		mail_command_ref(client->mail_cmd_iter);
+		cmd->export_iter = client_export_iter_command;
+		break;
+	case MAIL_EXPORT_LEVEL_SESSION:
+		client->mail_session_iter = stable_mail_sessions;
+		if (client->mail_session_iter == NULL)
+			return FALSE;
+		mail_session_ref(client->mail_session_iter);
+		cmd->export_iter = client_export_iter_session;
+		break;
+	case MAIL_EXPORT_LEVEL_USER:
+		client->mail_user_iter = stable_mail_users;
+		if (client->mail_user_iter == NULL)
+			return FALSE;
+		mail_user_ref(client->mail_user_iter);
+		cmd->export_iter = client_export_iter_user;
+		break;
+	case MAIL_EXPORT_LEVEL_DOMAIN:
+		client->mail_domain_iter = stable_mail_domains;
+		if (client->mail_domain_iter == NULL)
+			return FALSE;
+		mail_domain_ref(client->mail_domain_iter);
+		cmd->export_iter = client_export_iter_domain;
+		break;
+	case MAIL_EXPORT_LEVEL_IP:
+		client->mail_ip_iter = stable_mail_ips;
+		if (client->mail_ip_iter == NULL)
+			return FALSE;
+		mail_ip_ref(client->mail_ip_iter);
+		cmd->export_iter = client_export_iter_ip;
+		break;
+	}
+	i_assert(cmd->export_iter != NULL);
+	return TRUE;
+}
+
+int client_export(struct client *client, const char *const *args,
+		  const char **error_r)
+{
+	const char *level_str = args[0];
+	struct client_export_cmd *cmd;
+
+	p_clear(client->cmd_pool);
+	cmd = p_new(client->cmd_pool, struct client_export_cmd, 1);
+	cmd->str = str_new(client->cmd_pool, 256);
+
+	if (level_str == NULL) {
+		*error_r = "Missing level parameter";
+		return -1;
+	}
+	if (mail_export_level_parse(level_str, &cmd->level) < 0) {
+		*error_r = "Invalid level";
+		return -1;
+	}
+	if (mail_export_parse_filter(args + 1, client->cmd_pool,
+				     &cmd->filter, error_r) < 0)
+		return -1;
+
+	client->cmd_export = cmd;
+	if (!client_export_iter_init(client)) {
+		/* nothing to export */
+		o_stream_send_str(client->output, "\n");
+		return 1;
+	}
+	client->cmd_more = client_export_more;
+	return client_export_more(client);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/stats/client-export.h	Fri Aug 26 05:15:12 2011 +0300
@@ -0,0 +1,9 @@
+#ifndef CLIENT_EXPORT_H
+#define CLIENT_EXPORT_H
+
+struct client;
+
+int client_export(struct client *client, const char *const *args,
+		  const char **error_r);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/stats/client.c	Fri Aug 26 05:15:12 2011 +0300
@@ -0,0 +1,198 @@
+/* Copyright (c) 2011 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "llist.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "strescape.h"
+#include "master-service.h"
+#include "mail-command.h"
+#include "mail-session.h"
+#include "mail-user.h"
+#include "mail-domain.h"
+#include "mail-ip.h"
+#include "client-export.h"
+#include "client.h"
+
+#include <unistd.h>
+
+#define CLIENT_MAX_SIMULTANEOUS_ITER_COUNT 1000
+#define MAX_INBUF_SIZE 1024
+#define OUTBUF_THROTTLE_SIZE (1024*64)
+
+static struct client *clients;
+
+bool client_is_busy(struct client *client)
+{
+	client->iter_count++;
+	if (client->iter_count % CLIENT_MAX_SIMULTANEOUS_ITER_COUNT == 0)
+		return TRUE;
+	if (o_stream_get_buffer_used_size(client->output) < OUTBUF_THROTTLE_SIZE)
+		return FALSE;
+	if (o_stream_flush(client->output) < 0)
+		return TRUE;
+	return o_stream_get_buffer_used_size(client->output) >= OUTBUF_THROTTLE_SIZE;
+}
+
+static int
+client_handle_request(struct client *client, const char *const *args,
+		      const char **error_r)
+{
+	const char *cmd = args[0];
+
+	if (cmd == NULL) {
+		*error_r = "Missing command";
+		return -1;
+	}
+	args++;
+
+	if (strcmp(cmd, "EXPORT") == 0)
+		return client_export(client, args, error_r);
+
+	*error_r = "Unknown command";
+	return -1;
+}
+
+static const char *const*
+client_read_next_line(struct client *client)
+{
+	const char *line;
+	char **args;
+	unsigned int i;
+
+	line = i_stream_next_line(client->input);
+	if (line == NULL)
+		return NULL;
+
+	args = p_strsplit(pool_datastack_create(), line, "\t");
+	for (i = 0; args[i] != NULL; i++)
+		args[i] = str_tabunescape(args[i]);
+	return (void *)args;
+}
+
+static void client_input(struct client *client)
+{
+	const char *const *args, *error;
+	int ret;
+
+	if (client->to_pending != NULL)
+		timeout_remove(&client->to_pending);
+
+	switch (i_stream_read(client->input)) {
+	case -2:
+		i_error("BUG: Stats client sent too much data");
+		client_destroy(&client);
+		return;
+	case -1:
+		client_destroy(&client);
+		return;
+	}
+
+	o_stream_cork(client->output);
+	while ((args = client_read_next_line(client)) != NULL) {
+		ret = client_handle_request(client, args, &error);
+		if (ret < 0) {
+			i_error("Stats client input error: %s", error);
+			client_destroy(&client);
+			return;
+		}
+		if (ret == 0) {
+			o_stream_set_flush_pending(client->output, TRUE);
+			io_remove(&client->io);
+			break;
+		}
+		client->cmd_more = NULL;
+	}
+	o_stream_uncork(client->output);
+}
+
+static int client_output(struct client *client)
+{
+	int ret = 1;
+
+	o_stream_cork(client->output);
+	if (o_stream_flush(client->output) < 0) {
+		client_destroy(&client);
+		return 1;
+	}
+	if (client->cmd_more != NULL)
+		ret = client->cmd_more(client);
+	o_stream_uncork(client->output);
+
+	if (ret > 0) {
+		client->cmd_more = NULL;
+		if (client->io == NULL)
+			client_enable_io(client);
+	}
+	return ret;
+}
+
+void client_enable_io(struct client *client)
+{
+	i_assert(client->io == NULL);
+
+	client->io = io_add(client->fd, IO_READ, client_input, client);
+	if (client->to_pending == NULL)
+		client->to_pending = timeout_add(0, client_input, client);
+}
+
+struct client *client_create(int fd)
+{
+	struct client *client;
+
+	client = i_new(struct client, 1);
+	client->fd = fd;
+	client->io = io_add(fd, IO_READ, client_input, client);
+	client->input = i_stream_create_fd(fd, MAX_INBUF_SIZE, FALSE);
+	client->output = o_stream_create_fd(fd, (size_t)-1, FALSE);
+	o_stream_set_flush_callback(client->output, client_output, client);
+	client->cmd_pool = pool_alloconly_create("cmd pool", 1024);
+
+	DLLIST_PREPEND(&clients, client);
+	return client;
+}
+
+static void client_unref_iters(struct client *client)
+{
+	if (client->mail_cmd_iter != NULL)
+		mail_command_unref(&client->mail_cmd_iter);
+	if (client->mail_session_iter != NULL)
+		mail_session_unref(&client->mail_session_iter);
+	if (client->mail_user_iter != NULL)
+		mail_user_unref(&client->mail_user_iter);
+	if (client->mail_domain_iter != NULL)
+		mail_domain_unref(&client->mail_domain_iter);
+	if (client->mail_ip_iter != NULL)
+		mail_ip_unref(&client->mail_ip_iter);
+}
+
+void client_destroy(struct client **_client)
+{
+	struct client *client = *_client;
+
+	*_client = NULL;
+
+	DLLIST_REMOVE(&clients, client);
+	if (client->io != NULL)
+		io_remove(&client->io);
+	i_stream_destroy(&client->input);
+	o_stream_destroy(&client->output);
+	if (close(client->fd) < 0)
+		i_error("close(client) failed: %m");
+
+	client_unref_iters(client);
+	pool_unref(&client->cmd_pool);
+	i_free(client);
+
+	master_service_client_connection_destroyed(master_service);
+}
+
+void clients_destroy_all(void)
+{
+	while (clients != NULL) {
+		struct client *client = clients;
+
+		client_destroy(&client);
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/stats/client.h	Fri Aug 26 05:15:12 2011 +0300
@@ -0,0 +1,35 @@
+#ifndef CLIENT_H
+#define CLIENT_H
+
+struct client {
+	struct client *prev, *next;
+
+	int fd;
+	struct io *io;
+	struct istream *input;
+	struct ostream *output;
+	struct timeout *to_pending;
+
+	pool_t cmd_pool;
+	struct client_export_cmd *cmd_export;
+	int (*cmd_more)(struct client *client);
+
+	/* command iterators. while non-NULL, they've increased the
+	   struct's refcount so it won't be deleted during iteration */
+	unsigned int iter_count;
+	struct mail_command *mail_cmd_iter;
+	struct mail_session *mail_session_iter;
+	struct mail_user *mail_user_iter;
+	struct mail_domain *mail_domain_iter;
+	struct mail_ip *mail_ip_iter;
+};
+
+struct client *client_create(int fd);
+void client_destroy(struct client **client);
+
+bool client_is_busy(struct client *client);
+void client_enable_io(struct client *client);
+
+void clients_destroy_all(void);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/stats/global-memory.c	Fri Aug 26 05:15:12 2011 +0300
@@ -0,0 +1,46 @@
+/* Copyright (c) 2011 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "mail-command.h"
+#include "mail-session.h"
+#include "mail-user.h"
+#include "mail-domain.h"
+#include "mail-ip.h"
+#include "stats-settings.h"
+#include "global-memory.h"
+
+size_t global_used_memory = 0;
+
+static bool global_memory_free_something(void)
+{
+	size_t orig_used_memory = global_used_memory;
+
+	mail_commands_free_memory();
+	if (global_used_memory > stats_settings->memory_limit)
+		mail_sessions_free_memory();
+	if (global_used_memory > stats_settings->memory_limit)
+		mail_users_free_memory();
+	if (global_used_memory > stats_settings->memory_limit)
+		mail_ips_free_memory();
+	if (global_used_memory > stats_settings->memory_limit)
+		mail_domains_free_memory();
+
+	return global_used_memory < orig_used_memory;
+}
+
+void global_memory_alloc(size_t size)
+{
+	i_assert(size < (size_t)-1 - global_used_memory);
+	global_used_memory += size;
+
+	while (global_used_memory > stats_settings->memory_limit) {
+		if (!global_memory_free_something())
+			break;
+	}
+}
+
+void global_memory_free(size_t size)
+{
+	i_assert(size <= global_used_memory);
+	global_used_memory -= size;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/stats/global-memory.h	Fri Aug 26 05:15:12 2011 +0300
@@ -0,0 +1,9 @@
+#ifndef GLOBAL_MEMORY_H
+#define GLOBAL_MEMORY_H
+
+extern size_t global_used_memory;
+
+void global_memory_alloc(size_t size);
+void global_memory_free(size_t size);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/stats/mail-command.c	Fri Aug 26 05:15:12 2011 +0300
@@ -0,0 +1,175 @@
+/* Copyright (c) 2011 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "llist.h"
+#include "global-memory.h"
+#include "stats-settings.h"
+#include "mail-stats.h"
+#include "mail-session.h"
+#include "mail-command.h"
+
+/* commands are sorted by their last_update timestamp, oldest first */
+struct mail_command *stable_mail_commands;
+
+static size_t mail_command_memsize(const struct mail_command *cmd)
+{
+	return sizeof(*cmd) + strlen(cmd->name) + 1 + strlen(cmd->args) + 1;
+}
+
+static struct mail_command *
+mail_command_find(struct mail_session *session, unsigned int id)
+{
+	struct mail_command *cmd;
+
+	i_assert(id != 0);
+
+	if (id > session->highest_cmd_id) {
+		/* fast path for new commands */
+		return NULL;
+	}
+	for (cmd = session->commands; cmd != NULL; cmd = cmd->session_next) {
+		if (cmd->id == id)
+			return cmd;
+	}
+	/* expired */
+	return NULL;
+}
+
+static struct mail_command *
+mail_command_add(struct mail_session *session, const char *name,
+		 const char *args)
+{
+	struct mail_command *cmd;
+
+	cmd = i_new(struct mail_command, 1);
+	cmd->refcount = 1; /* unrefed at "done" */
+	cmd->session = session;
+	cmd->name = i_strdup(name);
+	cmd->args = i_strdup(args);
+	cmd->last_update = ioloop_time;
+
+	DLLIST_PREPEND_FULL(&stable_mail_commands, cmd,
+			    stable_prev, stable_next);
+	DLLIST_PREPEND_FULL(&session->commands, cmd,
+			    session_prev, session_next);
+	mail_session_ref(cmd->session);
+	global_memory_alloc(mail_command_memsize(cmd));
+	return cmd;
+}
+
+static void mail_command_free(struct mail_command *cmd)
+{
+	i_assert(cmd->refcount == 0);
+
+	global_memory_free(mail_command_memsize(cmd));
+
+	DLLIST_REMOVE_FULL(&stable_mail_commands, cmd,
+			    stable_prev, stable_next);
+	DLLIST_REMOVE_FULL(&cmd->session->commands, cmd,
+			   session_prev, session_next);
+	mail_session_unref(&cmd->session);
+	i_free(cmd->name);
+	i_free(cmd->args);
+	i_free(cmd);
+}
+
+void mail_command_ref(struct mail_command *cmd)
+{
+	cmd->refcount++;
+}
+
+void mail_command_unref(struct mail_command **_cmd)
+{
+	struct mail_command *cmd = *_cmd;
+
+	i_assert(cmd->refcount > 0);
+	cmd->refcount--;
+
+	*_cmd = NULL;
+}
+
+int mail_command_update_parse(const char *const *args, const char **error_r)
+{
+	struct mail_session *session;
+	struct mail_command *cmd;
+	struct mail_stats stats, diff_stats;
+	unsigned int cmd_id;
+	bool done;
+	int ret;
+
+	/* <session guid> <cmd id> <done> <name> <args> [key=value ..] */
+	if (str_array_length(args) < 4) {
+		*error_r = "UPDATE-CMD: Too few parameters";
+		return -1;
+	}
+	if ((ret = mail_session_lookup(args[0], &session, error_r)) <= 0)
+		return ret;
+
+	if (str_to_uint(args[1], &cmd_id) < 0 || cmd_id == 0) {
+		*error_r = "UPDATE-CMD: Invalid command id";
+		return -1;
+	}
+	if (strcmp(args[2], "0") != 0 &&
+	    strcmp(args[2], "1") != 0) {
+		*error_r = "UPDATE-CMD: Invalid done parameter";
+		return -1;
+	}
+	done = args[2][0] == '1';
+	if (mail_stats_parse(args+5, &stats, error_r) < 0) {
+		*error_r = t_strconcat("UPDATE-CMD: ", *error_r, NULL);
+		return -1;
+	}
+
+	cmd = mail_command_find(session, cmd_id);
+	if (cmd == NULL) {
+		cmd = mail_command_add(session, args[3], args[4]);
+		cmd->id = cmd_id;
+		cmd->stats = stats;
+		diff_stats = stats;
+
+		session->num_cmds++;
+		session->user->num_cmds++;
+		session->user->domain->num_cmds++;
+		if (session->ip != NULL)
+			session->ip->num_cmds++;
+	} else {
+		if (!mail_stats_diff(&cmd->stats, &stats, &diff_stats)) {
+			*error_r = "UPDATE-SESSION: stats shrank";
+			return -1;
+		}
+		cmd->last_update = ioloop_time;
+		mail_stats_add(&session->stats, &diff_stats);
+	}
+	if (done) {
+		cmd->id = 0;
+		mail_command_unref(&cmd);
+	}
+	mail_session_refresh(session, &diff_stats);
+	return 0;
+}
+
+void mail_commands_free_memory(void)
+{
+	while (stable_mail_commands != NULL &&
+	       stable_mail_commands->refcount == 0) {
+		i_assert(stable_mail_commands->id == 0);
+		mail_command_free(stable_mail_commands);
+
+		if (global_used_memory < stats_settings->memory_limit)
+			break;
+		if (ioloop_time -
+		    stable_mail_commands->last_update < stats_settings->command_min_time)
+			break;
+	}
+}
+
+void mail_commands_init(void)
+{
+}
+
+void mail_commands_deinit(void)
+{
+	while (stable_mail_commands != NULL)
+		mail_command_free(stable_mail_commands);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/stats/mail-command.h	Fri Aug 26 05:15:12 2011 +0300
@@ -0,0 +1,17 @@
+#ifndef MAIL_COMMAND_H
+#define MAIL_COMMAND_H
+
+struct mail_command;
+
+extern struct mail_command *stable_mail_commands;
+
+int mail_command_update_parse(const char *const *args, const char **error_r);
+
+void mail_command_ref(struct mail_command *cmd);
+void mail_command_unref(struct mail_command **cmd);
+
+void mail_commands_free_memory(void);
+void mail_commands_init(void);
+void mail_commands_deinit(void);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/stats/mail-domain.c	Fri Aug 26 05:15:12 2011 +0300
@@ -0,0 +1,121 @@
+/* Copyright (c) 2011 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "hash.h"
+#include "llist.h"
+#include "global-memory.h"
+#include "stats-settings.h"
+#include "mail-stats.h"
+#include "mail-domain.h"
+
+static struct hash_table *mail_domains_hash;
+/* domains are sorted by their last_update timestamp, oldest first */
+static struct mail_domain *mail_domains_head, *mail_domains_tail;
+struct mail_domain *stable_mail_domains;
+
+static size_t mail_domain_memsize(const struct mail_domain *domain)
+{
+	return sizeof(*domain) + strlen(domain->name) + 1;
+}
+
+struct mail_domain *mail_domain_login(const char *name)
+{
+	struct mail_domain *domain;
+
+	domain = hash_table_lookup(mail_domains_hash, name);
+	if (domain != NULL) {
+		domain->num_logins++;
+		mail_domain_refresh(domain, NULL);
+		return domain;
+	}
+
+	domain = i_new(struct mail_domain, 1);
+	domain->name = i_strdup(name);
+	domain->reset_timestamp = ioloop_time;
+
+	hash_table_insert(mail_domains_hash, domain->name, domain);
+	DLLIST_PREPEND_FULL(&stable_mail_domains, domain,
+			    stable_prev, stable_next);
+	DLLIST2_APPEND_FULL(&mail_domains_head, &mail_domains_tail, domain,
+			    sorted_prev, sorted_next);
+	domain->num_logins++;
+	domain->last_update = ioloop_time;
+	global_memory_alloc(mail_domain_memsize(domain));
+	return domain;
+}
+
+struct mail_domain *mail_domain_lookup(const char *name)
+{
+	return hash_table_lookup(mail_domains_hash, name);
+}
+
+void mail_domain_ref(struct mail_domain *domain)
+{
+	domain->refcount++;
+}
+
+void mail_domain_unref(struct mail_domain **_domain)
+{
+	struct mail_domain *domain = *_domain;
+
+	i_assert(domain->refcount > 0);
+	domain->refcount--;
+
+	*_domain = NULL;
+}
+
+static void mail_domain_free(struct mail_domain *domain)
+{
+	i_assert(domain->refcount == 0);
+	i_assert(domain->users == NULL);
+
+	global_memory_free(mail_domain_memsize(domain));
+	hash_table_remove(mail_domains_hash, domain->name);
+	DLLIST_REMOVE_FULL(&stable_mail_domains, domain,
+			   stable_prev, stable_next);
+	DLLIST2_REMOVE_FULL(&mail_domains_head, &mail_domains_tail, domain,
+			    sorted_prev, sorted_next);
+
+	i_free(domain->name);
+	i_free(domain);
+}
+
+void mail_domain_refresh(struct mail_domain *domain,
+			 const struct mail_stats *diff_stats)
+{
+	if (diff_stats != NULL)
+		mail_stats_add(&domain->stats, diff_stats);
+	domain->last_update = ioloop_time;
+	DLLIST2_REMOVE_FULL(&mail_domains_head, &mail_domains_tail, domain,
+			    sorted_prev, sorted_next);
+	DLLIST2_APPEND_FULL(&mail_domains_head, &mail_domains_tail, domain,
+			    sorted_prev, sorted_next);
+}
+
+void mail_domains_free_memory(void)
+{
+	while (mail_domains_head != NULL && mail_domains_head->refcount == 0) {
+		mail_domain_free(mail_domains_head);
+
+		if (global_used_memory < stats_settings->memory_limit)
+			break;
+		if (ioloop_time -
+		    mail_domains_head->last_update < stats_settings->domain_min_time)
+			break;
+	}
+}
+
+void mail_domains_init(void)
+{
+	mail_domains_hash =
+		hash_table_create(default_pool, default_pool, 0,
+				  str_hash, (hash_cmp_callback_t *)strcmp);
+}
+
+void mail_domains_deinit(void)
+{
+	while (mail_domains_head != NULL)
+		mail_domain_free(mail_domains_head);
+	hash_table_destroy(&mail_domains_hash);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/stats/mail-domain.h	Fri Aug 26 05:15:12 2011 +0300
@@ -0,0 +1,20 @@
+#ifndef MAIL_DOMAIN_H
+#define MAIL_DOMAIN_H
+
+struct mail_stats;
+
+extern struct mail_domain *stable_mail_domains;
+
+struct mail_domain *mail_domain_login(const char *name);
+struct mail_domain *mail_domain_lookup(const char *name);
+void mail_domain_refresh(struct mail_domain *domain,
+			 const struct mail_stats *diff_stats);
+
+void mail_domain_ref(struct mail_domain *domain);
+void mail_domain_unref(struct mail_domain **domain);
+
+void mail_domains_free_memory(void);
+void mail_domains_init(void);
+void mail_domains_deinit(void);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/stats/mail-ip.c	Fri Aug 26 05:15:12 2011 +0300
@@ -0,0 +1,118 @@
+/* Copyright (c) 2011 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "hash.h"
+#include "llist.h"
+#include "global-memory.h"
+#include "stats-settings.h"
+#include "mail-stats.h"
+#include "mail-ip.h"
+
+static struct hash_table *mail_ips_hash;
+/* ips are sorted by their last_update timestamp, oldest first */
+static struct mail_ip *mail_ips_head, *mail_ips_tail;
+struct mail_ip *stable_mail_ips;
+
+static size_t mail_ip_memsize(const struct mail_ip *ip)
+{
+	return sizeof(*ip);
+}
+
+struct mail_ip *mail_ip_login(const struct ip_addr *ip_addr)
+{
+	struct mail_ip *ip;
+
+	ip = hash_table_lookup(mail_ips_hash, ip_addr);
+	if (ip != NULL) {
+		ip->num_logins++;
+		mail_ip_refresh(ip, NULL);
+		return ip;
+	}
+
+	ip = i_new(struct mail_ip, 1);
+	ip->ip = *ip_addr;
+	ip->reset_timestamp = ioloop_time;
+
+	hash_table_insert(mail_ips_hash, &ip->ip, ip);
+	DLLIST_PREPEND_FULL(&stable_mail_ips, ip, stable_prev, stable_next);
+	DLLIST2_APPEND_FULL(&mail_ips_head, &mail_ips_tail, ip,
+			    sorted_prev, sorted_next);
+	ip->num_logins++;
+	ip->last_update = ioloop_time;
+	global_memory_alloc(mail_ip_memsize(ip));
+	return ip;
+}
+
+struct mail_ip *mail_ip_lookup(const struct ip_addr *ip_addr)
+{
+	return hash_table_lookup(mail_ips_hash, ip_addr);
+}
+
+void mail_ip_ref(struct mail_ip *ip)
+{
+	ip->refcount++;
+}
+
+void mail_ip_unref(struct mail_ip **_ip)
+{
+	struct mail_ip *ip = *_ip;
+
+	i_assert(ip->refcount > 0);
+	ip->refcount--;
+
+	*_ip = NULL;
+}
+
+static void mail_ip_free(struct mail_ip *ip)
+{
+	i_assert(ip->refcount == 0);
+	i_assert(ip->sessions == NULL);
+
+	global_memory_free(mail_ip_memsize(ip));
+	hash_table_remove(mail_ips_hash, &ip->ip);
+	DLLIST_REMOVE_FULL(&stable_mail_ips, ip, stable_prev, stable_next);
+	DLLIST2_REMOVE_FULL(&mail_ips_head, &mail_ips_tail, ip,
+			    sorted_prev, sorted_next);
+
+	i_free(ip);
+}
+
+void mail_ip_refresh(struct mail_ip *ip, const struct mail_stats *diff_stats)
+{
+	if (diff_stats != NULL)
+		mail_stats_add(&ip->stats, diff_stats);
+	ip->last_update = ioloop_time;
+	DLLIST2_REMOVE_FULL(&mail_ips_head, &mail_ips_tail, ip,
+			    sorted_prev, sorted_next);
+	DLLIST2_APPEND_FULL(&mail_ips_head, &mail_ips_tail, ip,
+			    sorted_prev, sorted_next);
+}
+
+void mail_ips_free_memory(void)
+{
+	while (mail_ips_head != NULL && mail_ips_head->refcount == 0) {
+		mail_ip_free(mail_ips_head);
+
+		if (global_used_memory < stats_settings->memory_limit)
+			break;
+		if (ioloop_time -
+		    mail_ips_head->last_update < stats_settings->ip_min_time)
+			break;
+	}
+}
+
+void mail_ips_init(void)
+{
+	mail_ips_hash =
+		hash_table_create(default_pool, default_pool, 0,
+				  (hash_callback_t *)net_ip_hash,
+				  (hash_cmp_callback_t *)net_ip_cmp);
+}
+
+void mail_ips_deinit(void)
+{
+	while (mail_ips_head != NULL)
+		mail_ip_free(mail_ips_head);
+	hash_table_destroy(&mail_ips_hash);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/stats/mail-ip.h	Fri Aug 26 05:15:12 2011 +0300
@@ -0,0 +1,17 @@
+#ifndef MAIL_IP_H
+#define MAIL_IP_H
+
+extern struct mail_ip *stable_mail_ips;
+
+struct mail_ip *mail_ip_login(const struct ip_addr *ip_addr);
+struct mail_ip *mail_ip_lookup(const struct ip_addr *ip_addr);
+void mail_ip_refresh(struct mail_ip *ip, const struct mail_stats *diff_stats);
+
+void mail_ip_ref(struct mail_ip *ip);
+void mail_ip_unref(struct mail_ip **ip);
+
+void mail_ips_free_memory(void);
+void mail_ips_init(void);
+void mail_ips_deinit(void);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/stats/mail-server-connection.c	Fri Aug 26 05:15:12 2011 +0300
@@ -0,0 +1,108 @@
+/* Copyright (c) 2011 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "strescape.h"
+#include "istream.h"
+#include "ostream.h"
+#include "master-service.h"
+#include "mail-session.h"
+#include "mail-command.h"
+#include "mail-server-connection.h"
+
+#include <unistd.h>
+
+#define MAX_INBUF_SIZE (PIPE_BUF*2)
+
+struct mail_server_connection {
+	int fd;
+	struct istream *input;
+	struct ostream *output;
+	struct io *io;
+};
+
+static const char *const*
+mail_server_connection_next_line(struct mail_server_connection *conn)
+{
+	const char *line;
+	char **args;
+	unsigned int i;
+
+	line = i_stream_next_line(conn->input);
+	if (line == NULL)
+		return NULL;
+
+	args = p_strsplit(pool_datastack_create(), line, "\t");
+	for (i = 0; args[i] != NULL; i++)
+		args[i] = str_tabunescape(args[i]);
+	return (void *)args;
+}
+
+static int
+mail_server_connection_request(const char *const *args, const char **error_r)
+{
+	const char *cmd = args[0];
+
+	if (cmd == NULL) {
+		*error_r = "Missing command";
+		return -1;
+	}
+	args++;
+
+	if (strcmp(cmd, "CONNECT") == 0)
+		return mail_session_connect_parse(args, error_r);
+	if (strcmp(cmd, "DISCONNECT") == 0)
+		return mail_session_disconnect_parse(args, error_r);
+	if (strcmp(cmd, "UPDATE-SESSION") == 0)
+		return mail_session_update_parse(args, error_r);
+	if (strcmp(cmd, "UPDATE-CMD") == 0)
+		return mail_command_update_parse(args, error_r);
+
+	*error_r = "Unknown command";
+	return -1;
+}
+
+static void mail_server_connection_input(struct mail_server_connection *conn)
+{
+	const char *const *args, *error;
+
+	switch (i_stream_read(conn->input)) {
+	case -2:
+		i_error("BUG: Mail server sent too much data");
+		mail_server_connection_destroy(&conn);
+		return;
+	case -1:
+		mail_server_connection_destroy(&conn);
+		return;
+	}
+
+	while ((args = mail_server_connection_next_line(conn)) != NULL) {
+		if (mail_server_connection_request(args, &error) < 0)
+			i_error("Mail server input error: %s", error);
+	}
+}
+
+struct mail_server_connection *mail_server_connection_create(int fd)
+{
+	struct mail_server_connection *conn;
+
+	conn = i_new(struct mail_server_connection, 1);
+	conn->fd = fd;
+	conn->input = i_stream_create_fd(fd, MAX_INBUF_SIZE, FALSE);
+	conn->output = o_stream_create_fd(fd, (size_t)-1, FALSE);
+	conn->io = io_add(fd, IO_READ, mail_server_connection_input, conn);
+	return conn;
+}
+
+void mail_server_connection_destroy(struct mail_server_connection **_conn)
+{
+	struct mail_server_connection *conn = *_conn;
+
+	*_conn = NULL;
+
+	io_remove(&conn->io);
+	i_stream_destroy(&conn->input);
+	o_stream_destroy(&conn->output);
+	if (close(conn->fd) < 0)
+		i_error("close(conn) failed: %m");
+	i_free(conn);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/stats/mail-server-connection.h	Fri Aug 26 05:15:12 2011 +0300
@@ -0,0 +1,7 @@
+#ifndef MAIL_SERVER_CONNECTION_H
+#define MAIL_SERVER_CONNECTION_H
+
+struct mail_server_connection *mail_server_connection_create(int fd);
+void mail_server_connection_destroy(struct mail_server_connection **conn);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/stats/mail-session.c	Fri Aug 26 05:15:12 2011 +0300
@@ -0,0 +1,242 @@
+/* Copyright (c) 2011 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "hash.h"
+#include "llist.h"
+#include "global-memory.h"
+#include "stats-settings.h"
+#include "mail-stats.h"
+#include "mail-user.h"
+#include "mail-ip.h"
+#include "mail-session.h"
+
+/* If session doesn't receive any updates for this long, assume that the
+   process associated with it has crashed, and forcibly disconnect the
+   session. Must be larger than SESSION_STATS_FORCE_REFRESH_SECS in
+   stats plugin */
+#define MAIL_SESSION_IDLE_TIMEOUT_MSECS (1000*60*10)
+
+static struct hash_table *mail_sessions_hash;
+/* sessions are sorted by their last_update timestamp, oldest first */
+static struct mail_session *mail_sessions_head, *mail_sessions_tail;
+struct mail_session *stable_mail_sessions;
+
+static size_t mail_session_memsize(const struct mail_session *session)
+{
+	return sizeof(*session) + strlen(session->service) + 1;
+}
+
+static void mail_session_disconnect(struct mail_session *session)
+{
+	hash_table_remove(mail_sessions_hash, session->guid);
+	session->disconnected = TRUE;
+	timeout_remove(&session->to_idle);
+	mail_session_unref(&session);
+}
+
+static void mail_session_idle_timeout(struct mail_session *session)
+{
+	i_warning("Session %s appears to be have crashed, disconnecting it",
+		  guid_128_to_string(session->guid));
+	mail_session_disconnect(session);
+}
+
+int mail_session_connect_parse(const char *const *args, const char **error_r)
+{
+	struct mail_session *session;
+	guid_128_t session_guid;
+	struct ip_addr ip;
+	unsigned int i;
+
+	/* <session guid> <username> <service> [key=value ..] */
+	if (str_array_length(args) < 3) {
+		*error_r = "CONNECT: Too few parameters";
+		return -1;
+	}
+	if (guid_128_from_string(args[0], session_guid) < 0) {
+		*error_r = "CONNECT: Invalid GUID";
+		return -1;
+	}
+	session = hash_table_lookup(mail_sessions_hash, session_guid);
+	if (session != NULL) {
+		*error_r = "CONNECT: Duplicate session GUID";
+		return -1;
+	}
+	session = i_new(struct mail_session, 1);
+	session->refcount = 1; /* unrefed at disconnect */
+	session->service = i_strdup(args[2]);
+	memcpy(session->guid, session_guid, sizeof(session->guid));
+	session->last_update = ioloop_time;
+	session->to_idle = timeout_add(MAIL_SESSION_IDLE_TIMEOUT_MSECS,
+				       mail_session_idle_timeout, session);
+
+	session->user = mail_user_login(args[1]);
+	for (i = 3; args[i] != NULL; i++) {
+		if (strncmp(args[i], "rip=", 4) == 0 &&
+		    net_addr2ip(args[i] + 4, &ip)) {
+			session->ip = mail_ip_login(&ip);
+		}
+	}
+
+	hash_table_insert(mail_sessions_hash, session->guid, session);
+	DLLIST_PREPEND_FULL(&stable_mail_sessions, session,
+			    stable_prev, stable_next);
+	DLLIST2_APPEND_FULL(&mail_sessions_head, &mail_sessions_tail, session,
+			    sorted_prev, sorted_next);
+	DLLIST_PREPEND_FULL(&session->user->sessions, session,
+			    user_prev, user_next);
+	mail_user_ref(session->user);
+	if (session->ip != NULL) {
+		DLLIST_PREPEND_FULL(&session->ip->sessions, session,
+				    ip_prev, ip_next);
+		mail_ip_ref(session->ip);
+	}
+	global_memory_alloc(mail_session_memsize(session));
+	return 0;
+}
+
+void mail_session_ref(struct mail_session *session)
+{
+	session->refcount++;
+}
+
+void mail_session_unref(struct mail_session **_session)
+{
+	struct mail_session *session = *_session;
+
+	i_assert(session->refcount > 0);
+	session->refcount--;
+
+	*_session = NULL;
+}
+
+static void mail_session_free(struct mail_session *session)
+{
+	i_assert(session->refcount == 0);
+
+	global_memory_free(mail_session_memsize(session));
+
+	if (session->to_idle != NULL)
+		timeout_remove(&session->to_idle);
+	if (!session->disconnected)
+		hash_table_remove(mail_sessions_hash, session->guid);
+	DLLIST_REMOVE_FULL(&stable_mail_sessions, session,
+			   stable_prev, stable_next);
+	DLLIST2_REMOVE_FULL(&mail_sessions_head, &mail_sessions_tail, session,
+			    sorted_prev, sorted_next);
+	DLLIST_REMOVE_FULL(&session->user->sessions, session,
+			   user_prev, user_next);
+	mail_user_unref(&session->user);
+	if (session->ip != NULL) {
+		DLLIST_REMOVE_FULL(&session->ip->sessions, session,
+				   ip_prev, ip_next);
+		mail_ip_unref(&session->ip);
+	}
+
+	i_free(session->service);
+	i_free(session);
+}
+
+int mail_session_lookup(const char *guid, struct mail_session **session_r,
+			const char **error_r)
+{
+	guid_128_t session_guid;
+
+	if (guid == NULL) {
+		*error_r = "Too few parameters";
+		return -1;
+	}
+	if (guid_128_from_string(guid, session_guid) < 0) {
+		*error_r = "Invalid GUID";
+		return -1;
+	}
+	*session_r = hash_table_lookup(mail_sessions_hash, session_guid);
+	if (*session_r == NULL) {
+		i_warning("mail disconnect couldn't find session GUID: %s",
+			  guid_128_to_string(session_guid));
+		return 0;
+	}
+	return 1;
+}
+
+int mail_session_disconnect_parse(const char *const *args, const char **error_r)
+{
+	struct mail_session *session;
+	int ret;
+
+	/* <session guid> */
+	if ((ret = mail_session_lookup(args[0], &session, error_r)) <= 0)
+		return ret;
+
+	mail_session_disconnect(session);
+	return 0;
+}
+
+void mail_session_refresh(struct mail_session *session,
+			  const struct mail_stats *diff_stats)
+{
+	if (diff_stats != NULL)
+		mail_stats_add(&session->stats, diff_stats);
+	session->last_update = ioloop_time;
+	DLLIST2_REMOVE_FULL(&mail_sessions_head, &mail_sessions_tail, session,
+			    sorted_prev, sorted_next);
+	DLLIST2_APPEND_FULL(&mail_sessions_head, &mail_sessions_tail, session,
+			    sorted_prev, sorted_next);
+
+	mail_user_refresh(session->user, diff_stats);
+	if (session->ip != NULL)
+		mail_ip_refresh(session->ip, diff_stats);
+}
+
+int mail_session_update_parse(const char *const *args, const char **error_r)
+{
+	struct mail_session *session;
+	struct mail_stats stats, diff_stats;
+	int ret;
+
+	/* <session guid> [key=value ..] */
+	if ((ret = mail_session_lookup(args[0], &session, error_r)) <= 0)
+		return ret;
+
+	if (mail_stats_parse(args+1, &stats, error_r) < 0) {
+		*error_r = t_strconcat("UPDATE-SESSION: ", *error_r, NULL);
+		return -1;
+	}
+
+	if (!mail_stats_diff(&session->stats, &stats, &diff_stats)) {
+		*error_r = "UPDATE-SESSION: stats shrank";
+		return -1;
+	}
+	mail_session_refresh(session, &diff_stats);
+	return 0;
+}
+
+void mail_sessions_free_memory(void)
+{
+	while (mail_sessions_head != NULL &&
+	       mail_sessions_head->refcount == 0) {
+		i_assert(mail_sessions_head->disconnected);
+		mail_session_free(mail_sessions_head);
+
+		if (global_used_memory < stats_settings->memory_limit)
+			break;
+		if (ioloop_time -
+		    mail_sessions_head->last_update < stats_settings->session_min_time)
+			break;
+	}
+}
+
+void mail_sessions_init(void)
+{
+	mail_sessions_hash =
+		hash_table_create(default_pool, default_pool, 0,
+				  guid_128_hash, guid_128_cmp);
+}
+
+void mail_sessions_deinit(void)
+{
+	while (mail_sessions_head != NULL)
+		mail_session_free(mail_sessions_head);
+	hash_table_destroy(&mail_sessions_hash);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/stats/mail-session.h	Fri Aug 26 05:15:12 2011 +0300
@@ -0,0 +1,26 @@
+#ifndef MAIL_SESSION_H
+#define MAIL_SESSION_H
+
+struct mail_stats;
+struct mail_session;
+
+extern struct mail_session *stable_mail_sessions;
+
+int mail_session_connect_parse(const char *const *args, const char **error_r);
+int mail_session_disconnect_parse(const char *const *args, const char **error_r);
+int mail_session_update_parse(const char *const *args, const char **error_r);
+int mail_session_cmd_update_parse(const char *const *args, const char **error_r);
+
+void mail_session_ref(struct mail_session *session);
+void mail_session_unref(struct mail_session **session);
+
+int mail_session_lookup(const char *guid, struct mail_session **session_r,
+			const char **error_r);
+void mail_session_refresh(struct mail_session *session,
+			  const struct mail_stats *diff_stats);
+
+void mail_sessions_free_memory(void);
+void mail_sessions_init(void);
+void mail_sessions_deinit(void);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/stats/mail-stats.c	Fri Aug 26 05:15:12 2011 +0300
@@ -0,0 +1,170 @@
+/* Copyright (c) 2011 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "time-util.h"
+#include "mail-stats.h"
+
+struct mail_stats_parse_map {
+	const char *name;
+	unsigned int offset;
+	unsigned int size;
+} parse_map[] = {
+#define E(parsename, name) { parsename, offsetof(struct mail_stats, name), sizeof(((struct mail_stats *)0)->name) }
+	E("diskin", disk_input),
+	E("diskout", disk_output),
+	E("lpath", lookup_path),
+	E("lattr", lookup_attr),
+	E("rcount", read_count),
+	E("rbytes", read_bytes),
+	E("cache", cache_hits)
+};
+
+static int mail_stats_parse_cpu(const char *value, struct mail_stats *stats)
+{
+	const char *p, *secs_str;
+	unsigned long secs, usecs;
+
+	p = strchr(value, '.');
+	if (p == NULL)
+		return -1;
+
+	secs_str = t_strdup_until(value, p++);
+	if (str_to_ulong(secs_str, &secs) < 0 ||
+	    str_to_ulong(p, &usecs) < 0 ||
+	    usecs > 1000000)
+		return -1;
+
+	stats->cpu_secs.tv_sec = secs;
+	stats->cpu_secs.tv_usec = usecs;
+	return 0;
+}
+
+static struct mail_stats_parse_map *
+parse_map_find(const char *name)
+{
+	unsigned int i;
+
+	for (i = 0; i < N_ELEMENTS(parse_map); i++) {
+		if (strcmp(parse_map[i].name, name) == 0)
+			return &parse_map[i];
+	}
+	return NULL;
+}
+
+static int
+mail_stats_parse_one(const char *key, const char *value,
+		     struct mail_stats *stats, const char **error_r)
+{
+	struct mail_stats_parse_map *map;
+	void *dest;
+
+	map = parse_map_find(key);
+	if (map == NULL)
+		return 0;
+
+	dest = PTR_OFFSET(stats, map->offset);
+	switch (map->size) {
+	case sizeof(uint32_t):
+		if (str_to_uint32(value, dest) < 0) {
+			*error_r = "invalid number";
+			return -1;
+		}
+		break;
+	case sizeof(uint64_t):
+		if (str_to_uint64(value, dest) < 0) {
+			*error_r = "invalid number";
+			return -1;
+		}
+		break;
+	default:
+		i_unreached();
+	}
+	return 0;
+}
+
+int mail_stats_parse(const char *const *args, struct mail_stats *stats_r,
+		     const char **error_r)
+{
+	const char *p, *key, *value;
+	unsigned int i;
+
+	memset(stats_r, 0, sizeof(*stats_r));
+	for (i = 0; args[i] != NULL; i++) {
+		p = strchr(args[i], '=');
+		if (p == NULL) {
+			*error_r = "mail stats parameter missing '='";
+			return -1;
+		}
+		key = t_strdup_until(args[i], p);
+		value = p + 1;
+		if (strcmp(key, "cpu") == 0) {
+			if (mail_stats_parse_cpu(value, stats_r) < 0) {
+				*error_r = "invalid cpu parameter";
+				return -1;
+			}
+		} else {
+			if (mail_stats_parse_one(key, value,
+						 stats_r, error_r) < 0)
+				return -1;
+		}
+	}
+	return 0;
+}
+
+bool mail_stats_diff(const struct mail_stats *stats1,
+		     const struct mail_stats *stats2,
+		     struct mail_stats *diff_stats_r)
+{
+	long long diff_usecs;
+
+	memset(diff_stats_r, 0, sizeof(*diff_stats_r));
+
+	diff_usecs = timeval_diff_usecs(&stats2->cpu_secs, &stats1->cpu_secs);
+	if (diff_usecs < 0)
+		return FALSE;
+	diff_stats_r->cpu_secs.tv_sec = diff_usecs / 1000000;
+	diff_stats_r->cpu_secs.tv_usec = diff_usecs % 1000000;
+
+	if (stats1->disk_input > stats2->disk_input)
+		return FALSE;
+	diff_stats_r->disk_input = stats2->disk_input - stats1->disk_input;
+	if (stats1->disk_output > stats2->disk_output)
+		return FALSE;
+	diff_stats_r->disk_output = stats2->disk_output - stats1->disk_output;
+
+	if (stats1->lookup_path > stats2->lookup_path)
+		return FALSE;
+	diff_stats_r->lookup_path = stats2->lookup_path - stats1->lookup_path;
+	if (stats1->lookup_attr > stats2->lookup_attr)
+		return FALSE;
+	diff_stats_r->lookup_attr = stats2->lookup_attr - stats1->lookup_attr;
+	if (stats1->read_count > stats2->read_count)
+		return FALSE;
+	diff_stats_r->read_count = stats2->read_count - stats1->read_count;
+	if (stats1->cache_hits > stats2->cache_hits)
+		return FALSE;
+	diff_stats_r->cache_hits = stats2->cache_hits - stats1->cache_hits;
+	if (stats1->read_bytes > stats2->read_bytes)
+		return FALSE;
+	diff_stats_r->read_bytes = stats2->read_bytes - stats1->read_bytes;
+
+	return TRUE;
+}
+
+void mail_stats_add(struct mail_stats *dest, const struct mail_stats *src)
+{
+	dest->cpu_secs.tv_sec += src->cpu_secs.tv_sec;
+	dest->cpu_secs.tv_usec += src->cpu_secs.tv_usec;
+	if (dest->cpu_secs.tv_usec > 1000000) {
+		dest->cpu_secs.tv_usec -= 1000000;
+		dest->cpu_secs.tv_sec++;
+	}
+	dest->disk_input += src->disk_input;
+	dest->disk_output += src->disk_output;
+
+	dest->lookup_path += src->lookup_path;
+	dest->lookup_attr += src->lookup_attr;
+	dest->read_count += src->read_count;
+	dest->cache_hits += src->cache_hits;
+	dest->read_bytes += src->read_bytes;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/stats/mail-stats.h	Fri Aug 26 05:15:12 2011 +0300
@@ -0,0 +1,110 @@
+#ifndef MAIL_STATS_H
+#define MAIL_STATS_H
+
+#include "network.h"
+#include "guid.h"
+
+struct mail_stats {
+	struct timeval cpu_secs;
+	uint64_t disk_input, disk_output;
+
+	uint32_t lookup_path, lookup_attr, read_count, cache_hits;
+	uint64_t read_bytes;
+};
+
+struct mail_command {
+	struct mail_command *stable_prev, *stable_next;
+	struct mail_command *session_prev, *session_next;
+
+	struct mail_session *session;
+	char *name, *args;
+	/* non-zero id means the command is still running */
+	unsigned int id;
+
+	time_t last_update;
+	struct mail_stats stats;
+
+	int refcount;
+};
+
+struct mail_session {
+	struct mail_session *stable_prev, *stable_next;
+	struct mail_session *sorted_prev, *sorted_next;
+	struct mail_session *user_prev, *user_next;
+	struct mail_session *ip_prev, *ip_next;
+
+	/* if guid is empty, the session no longer exists */
+	guid_128_t guid;
+	struct mail_user *user;
+	char *service;
+	/* ip address may be NULL if there's none */
+	struct mail_ip *ip;
+	struct timeout *to_idle;
+
+	struct mail_stats stats;
+	time_t last_update;
+	unsigned int num_cmds;
+
+	bool disconnected;
+	unsigned int highest_cmd_id;
+	int refcount;
+	struct mail_command *commands;
+};
+
+struct mail_user {
+	struct mail_user *stable_prev, *stable_next;
+	struct mail_user *sorted_prev, *sorted_next;
+	struct mail_user *domain_prev, *domain_next;
+	char *name;
+	struct mail_domain *domain;
+	time_t reset_timestamp;
+
+	time_t last_update;
+	struct mail_stats stats;
+	unsigned int num_logins;
+	unsigned int num_cmds;
+
+	int refcount;
+	struct mail_session *sessions;
+};
+
+struct mail_domain {
+	struct mail_domain *stable_prev, *stable_next;
+	struct mail_domain *sorted_prev, *sorted_next;
+	char *name;
+	time_t reset_timestamp;
+
+	time_t last_update;
+	struct mail_stats stats;
+	unsigned int num_logins;
+	unsigned int num_cmds;
+
+	int refcount;
+	struct mail_user *users;
+};
+
+struct mail_ip {
+	struct mail_ip *stable_prev, *stable_next;
+	struct mail_ip *sorted_prev, *sorted_next;
+	struct ip_addr ip;
+	time_t reset_timestamp;
+
+	time_t last_update;
+	struct mail_stats stats;
+	unsigned int num_logins;
+	unsigned int num_cmds;
+
+	int refcount;
+	struct mail_session *sessions;
+};
+
+int mail_stats_parse(const char *const *args, struct mail_stats *stats_r,
+		     const char **error_r);
+/* diff1 is supposed to have smaller values than diff2. Returns TRUE if this
+   is so, FALSE if not */
+bool mail_stats_diff(const struct mail_stats *stats1,
+		     const struct mail_stats *stats2,
+		     struct mail_stats *diff_stats_r);
+void mail_stats_add(struct mail_stats *dest, const struct mail_stats *src);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/stats/mail-user.c	Fri Aug 26 05:15:12 2011 +0300
@@ -0,0 +1,138 @@
+/* Copyright (c) 2011 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "hash.h"
+#include "llist.h"
+#include "global-memory.h"
+#include "stats-settings.h"
+#include "mail-stats.h"
+#include "mail-domain.h"
+#include "mail-user.h"
+
+static struct hash_table *mail_users_hash;
+/* users are sorted by their last_update timestamp, oldest first */
+static struct mail_user *mail_users_head, *mail_users_tail;
+struct mail_user *stable_mail_users;
+
+static size_t mail_user_memsize(const struct mail_user *user)
+{
+	return sizeof(*user) + strlen(user->name) + 1;
+}
+
+struct mail_user *mail_user_login(const char *username)
+{
+	struct mail_user *user;
+	const char *domain;
+
+	user = hash_table_lookup(mail_users_hash, username);
+	if (user != NULL) {
+		user->num_logins++;
+		user->domain->num_logins++;
+		mail_user_refresh(user, NULL);
+		return user;
+	}
+
+	domain = strchr(username, '@');
+	if (domain != NULL)
+		domain++;
+	else
+		domain = "";
+
+	user = i_new(struct mail_user, 1);
+	user->name = i_strdup(username);
+	user->reset_timestamp = ioloop_time;
+	user->domain = mail_domain_login(domain);
+
+	hash_table_insert(mail_users_hash, user->name, user);
+	DLLIST_PREPEND_FULL(&stable_mail_users, user,
+			    stable_prev, stable_next);
+	DLLIST2_APPEND_FULL(&mail_users_head, &mail_users_tail, user,
+			    sorted_prev, sorted_next);
+	DLLIST_PREPEND_FULL(&user->domain->users, user,
+			    domain_prev, domain_next);
+	mail_domain_ref(user->domain);
+
+	user->num_logins++;
+	user->last_update = ioloop_time;
+	global_memory_alloc(mail_user_memsize(user));
+	return user;
+}
+
+struct mail_user *mail_user_lookup(const char *username)
+{
+	return hash_table_lookup(mail_users_hash, username);
+}
+
+void mail_user_ref(struct mail_user *user)
+{
+	user->refcount++;
+}
+
+void mail_user_unref(struct mail_user **_user)
+{
+	struct mail_user *user = *_user;
+
+	i_assert(user->refcount > 0);
+	user->refcount--;
+
+	*_user = NULL;
+}
+
+static void mail_user_free(struct mail_user *user)
+{
+	i_assert(user->refcount == 0);
+	i_assert(user->sessions == NULL);
+
+	global_memory_free(mail_user_memsize(user));
+	DLLIST_REMOVE_FULL(&stable_mail_users, user,
+			   stable_prev, stable_next);
+	DLLIST2_REMOVE_FULL(&mail_users_head, &mail_users_tail, user,
+			    sorted_prev, sorted_next);
+	DLLIST_REMOVE_FULL(&user->domain->users, user,
+			   domain_prev, domain_next);
+	mail_domain_unref(&user->domain);
+
+	i_free(user->name);
+	i_free(user);
+}
+
+void mail_user_refresh(struct mail_user *user,
+		       const struct mail_stats *diff_stats)
+{
+	if (diff_stats != NULL)
+		mail_stats_add(&user->stats, diff_stats);
+	user->last_update = ioloop_time;
+	DLLIST2_REMOVE_FULL(&mail_users_head, &mail_users_tail, user,
+			    sorted_prev, sorted_next);
+	DLLIST2_APPEND_FULL(&mail_users_head, &mail_users_tail, user,
+			    sorted_prev, sorted_next);
+	mail_domain_refresh(user->domain, diff_stats);
+}
+
+void mail_users_free_memory(void)
+{
+	while (mail_users_head != NULL && mail_users_head->refcount == 0) {
+		mail_user_free(mail_users_head);
+
+		if (global_used_memory < stats_settings->memory_limit)
+			break;
+		if (ioloop_time -
+		    mail_users_head->last_update < stats_settings->user_min_time)
+			break;
+	}
+}
+
+void mail_users_init(void)
+{
+	mail_users_hash =
+		hash_table_create(default_pool, default_pool, 0,
+				  str_hash, (hash_cmp_callback_t *)strcmp);
+}
+
+void mail_users_deinit(void)
+{
+	while (mail_users_head != NULL)
+		mail_user_free(mail_users_head);
+	hash_table_destroy(&mail_users_hash);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/stats/mail-user.h	Fri Aug 26 05:15:12 2011 +0300
@@ -0,0 +1,21 @@
+#ifndef MAIL_USER_H
+#define MAIL_USER_H
+
+struct mail_stats;
+
+extern struct mail_user *stable_mail_users;
+
+struct mail_user *mail_user_login(const char *username);
+struct mail_user *mail_user_lookup(const char *username);
+
+void mail_user_refresh(struct mail_user *user,
+		       const struct mail_stats *diff_stats);
+
+void mail_user_ref(struct mail_user *user);
+void mail_user_unref(struct mail_user **user);
+
+void mail_users_free_memory(void);
+void mail_users_init(void);
+void mail_users_deinit(void);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/stats/main.c	Fri Aug 26 05:15:12 2011 +0300
@@ -0,0 +1,80 @@
+/* Copyright (c) 2011 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "restrict-access.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "global-memory.h"
+#include "stats-settings.h"
+#include "mail-server-connection.h"
+#include "mail-command.h"
+#include "mail-session.h"
+#include "mail-user.h"
+#include "mail-domain.h"
+#include "client.h"
+
+struct stats_settings set;
+static struct mail_server_connection *mail_server_conn = NULL;
+
+static void client_connected(struct master_service_connection *conn)
+{
+	if (conn->fifo) {
+		if (mail_server_conn != NULL) {
+			i_error("Received another mail-server connection");
+			return;
+		}
+		mail_server_conn = mail_server_connection_create(conn->fd);
+	} else {
+		client_create(conn->fd);
+	}
+	master_service_client_connection_accept(conn);
+}
+
+int main(int argc, char *argv[])
+{
+	const struct setting_parser_info *set_roots[] = {
+		&stats_setting_parser_info,
+		NULL
+	};
+	const enum master_service_flags service_flags =
+		MASTER_SERVICE_FLAG_NO_IDLE_DIE |
+		MASTER_SERVICE_FLAG_UPDATE_PROCTITLE;
+	const char *error;
+	void **sets;
+
+	master_service = master_service_init("stats", service_flags,
+					     &argc, &argv, NULL);
+	if (master_getopt(master_service) > 0)
+		return FATAL_DEFAULT;
+	if (master_service_settings_read_simple(master_service, set_roots,
+						&error) < 0)
+		i_fatal("Error reading configuration: %s", error);
+	master_service_init_log(master_service, "stats: ");
+
+	restrict_access_by_env(NULL, FALSE);
+	restrict_access_allow_coredumps(TRUE);
+	master_service_init_finish(master_service);
+
+	sets = master_service_settings_get_others(master_service);
+	stats_settings = sets[0];
+
+	mail_commands_init();
+	mail_sessions_init();
+	mail_users_init();
+	mail_domains_init();
+
+	master_service_run(master_service, client_connected);
+
+	clients_destroy_all();
+	mail_commands_deinit();
+	mail_sessions_deinit();
+	mail_users_deinit();
+	mail_domains_deinit();
+
+	if (mail_server_conn != NULL)
+		mail_server_connection_destroy(&mail_server_conn);
+
+	i_assert(global_used_memory == 0);
+	master_service_deinit(&master_service);
+        return 0;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/stats/stats-settings.c	Fri Aug 26 05:15:12 2011 +0300
@@ -0,0 +1,94 @@
+/* Copyright (c) 2009-2011 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+#include "stats-settings.h"
+
+/* <settings checks> */
+static struct file_listener_settings stats_unix_listeners_array[] = {
+	{ "stats", 0600, "", "" }
+};
+static struct file_listener_settings *stats_unix_listeners[] = {
+	&stats_unix_listeners_array[0]
+};
+static buffer_t stats_unix_listeners_buf = {
+	stats_unix_listeners, sizeof(stats_unix_listeners), { 0, }
+};
+static struct file_listener_settings stats_fifo_listeners_array[] = {
+	{ "stats-mail", 0600, "", "" }
+};
+static struct file_listener_settings *stats_fifo_listeners[] = {
+	&stats_fifo_listeners_array[0]
+};
+static buffer_t stats_fifo_listeners_buf = {
+	stats_fifo_listeners,
+	sizeof(stats_fifo_listeners), { 0, }
+};
+/* </settings checks> */
+
+struct service_settings stats_service_settings = {
+	.name = "stats",
+	.protocol = "",
+	.type = "",
+	.executable = "stats",
+	.user = "$default_internal_user",
+	.group = "",
+	.privileged_group = "",
+	.extra_groups = "",
+	.chroot = "",
+
+	.drop_priv_before_exec = FALSE,
+
+	.process_min_avail = 0,
+	.process_limit = 1,
+	.client_limit = 0,
+	.service_count = 0,
+	.idle_kill = -1U,
+	.vsz_limit = (uoff_t)-1,
+
+	.unix_listeners = { { &stats_unix_listeners_buf,
+			      sizeof(stats_unix_listeners[0]) } },
+	.fifo_listeners = { { &stats_fifo_listeners_buf,
+			      sizeof(stats_fifo_listeners[0]) } },
+	.inet_listeners = ARRAY_INIT
+};
+
+#undef DEF
+#define DEF(type, name) \
+	{ type, #name, offsetof(struct stats_settings, name), NULL }
+
+static const struct setting_define stats_setting_defines[] = {
+	DEF(SET_SIZE, memory_limit),
+	DEF(SET_TIME, command_min_time),
+	DEF(SET_TIME, session_min_time),
+	DEF(SET_TIME, user_min_time),
+	DEF(SET_TIME, domain_min_time),
+	DEF(SET_TIME, ip_min_time),
+
+	SETTING_DEFINE_LIST_END
+};
+
+const struct stats_settings stats_default_settings = {
+	.memory_limit = 1024*1024*16,
+
+	.command_min_time = 60,
+	.session_min_time = 60*15,
+	.user_min_time = 60*60,
+	.domain_min_time = 60*60*12,
+	.ip_min_time = 60*60*12
+};
+
+const struct setting_parser_info stats_setting_parser_info = {
+	.module_name = "stats",
+	.defines = stats_setting_defines,
+	.defaults = &stats_default_settings,
+
+	.type_offset = (size_t)-1,
+	.struct_size = sizeof(struct stats_settings),
+
+	.parent_offset = (size_t)-1
+};
+
+const struct stats_settings *stats_settings;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/stats/stats-settings.h	Fri Aug 26 05:15:12 2011 +0300
@@ -0,0 +1,18 @@
+#ifndef STATS_SETTINGS_H
+#define STATS_SETTINGS_H
+
+struct stats_settings {
+	uoff_t memory_limit;
+
+	unsigned int command_min_time;
+	unsigned int session_min_time;
+	unsigned int user_min_time;
+	unsigned int domain_min_time;
+	unsigned int ip_min_time;
+};
+
+extern const struct setting_parser_info stats_setting_parser_info;
+extern const struct stats_settings *stats_settings;
+
+#endif
+