/* Copyright (c) 2011-2016 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "net.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,
	MAIL_EXPORT_LEVEL_GLOBAL
};
static const char *mail_export_level_names[] = {
	"command", "session", "user", "domain", "ip", "global"
};

struct mail_export_filter {
	const char *user, *domain, *session;
	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> | session=<str>
	   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, "session=", 8) == 0)
			filter_r->session = p_strdup(pool, *args + 8);
		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_stats_headers(struct client *client)
{
	unsigned int i, count = stats_field_count();
	string_t *str = t_str_new(128);

	i_assert(count > 0);

	str_append(str, stats_field_name(0));
	for (i = 1; i < count; i++) {
		str_append_c(str, '\t');
		str_append(str, stats_field_name(i));
	}
	str_append_c(str, '\n');
	o_stream_send(client->output, str_data(str), str_len(str));
}

static void
client_export_stats(string_t *str, const struct stats *stats)
{
	unsigned int i, count = stats_field_count();

	i_assert(count > 0);

	stats_field_value(str, stats, 0);
	for (i = 1; i < count; i++) {
		str_append_c(str, '\t');
		stats_field_value(str, stats, i);
	}
}

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.tv_sec)
		return FALSE;
	if (filter->session != NULL &&
	    strcmp(session->id, filter->session) != 0)
		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.tv_sec)
		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.tv_sec)
		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.tv_sec)
		return FALSE;
	if (filter->ip_bits > 0 &&
	    !net_is_in_network(&ip->ip, &filter->ip, filter->ip_bits))
		return FALSE;
	return TRUE;
}

static void client_export_timeval(string_t *str, const struct timeval *tv)
{
	str_printfa(str, "\t%ld.%06u", (long)tv->tv_sec,
		    (unsigned int)tv->tv_usec);
}

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_COMMAND);
	mail_command_unref(&client->mail_cmd_iter);

	if (!cmd->header_sent) {
		o_stream_nsend_str(client->output,
			"cmd\targs\tsession\tuser\tlast_update\t");
		client_export_stats_headers(client);
		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_append_tabescaped(cmd->str, command->name);
		str_append_c(cmd->str, '\t');
		str_append_tabescaped(cmd->str, command->args);
		str_append_c(cmd->str, '\t');
		str_append(cmd->str, command->session->id);
		str_append_c(cmd->str, '\t');
		str_append_tabescaped(cmd->str,
				      command->session->user->name);
		client_export_timeval(cmd->str, &command->last_update);
		str_append_c(cmd->str, '\t');
		client_export_stats(cmd->str, command->stats);
		str_append_c(cmd->str, '\n');
		o_stream_nsend(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_nsend_str(client->output,
			"session\tuser\tip\tservice\tpid\tconnected"
			"\tlast_update\tnum_cmds\t");
		client_export_stats_headers(client);
		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);
		str_append(cmd->str, session->id);
		str_append_c(cmd->str, '\t');
		str_append_tabescaped(cmd->str, session->user->name);
		str_append_c(cmd->str, '\t');
		if (session->ip != NULL) T_BEGIN {
			str_append(cmd->str, net_ip2addr(&session->ip->ip));
		} T_END;
		str_append_c(cmd->str, '\t');
		str_append_tabescaped(cmd->str, session->service);
		str_printfa(cmd->str, "\t%ld", (long)session->pid);
		str_printfa(cmd->str, "\t%d", !session->disconnected);
		client_export_timeval(cmd->str, &session->last_update);
		str_printfa(cmd->str, "\t%u\t", session->num_cmds);
		client_export_stats(cmd->str, session->stats);
		str_append_c(cmd->str, '\n');
		o_stream_nsend(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_nsend_str(client->output,
			"user\treset_timestamp\tlast_update"
			"\tnum_logins\tnum_cmds\t");
		client_export_stats_headers(client);
		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_append_tabescaped(cmd->str, user->name);
		str_printfa(cmd->str, "\t%ld", (long)user->reset_timestamp);
		client_export_timeval(cmd->str, &user->last_update);
		str_printfa(cmd->str, "\t%u\t%u\t",
			    user->num_logins, user->num_cmds);
		client_export_stats(cmd->str, user->stats);
		str_append_c(cmd->str, '\n');
		o_stream_nsend(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_nsend_str(client->output,
			"domain\treset_timestamp\tlast_update"
			"\tnum_logins\tnum_cmds\tnum_connected_sessions\t");
		client_export_stats_headers(client);
		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_append_tabescaped(cmd->str, domain->name);
		str_printfa(cmd->str, "\t%ld", (long)domain->reset_timestamp);
		client_export_timeval(cmd->str, &domain->last_update);
		str_printfa(cmd->str, "\t%u\t%u\t%u\t",
			    domain->num_logins, domain->num_cmds,
			    domain->num_connected_sessions);
		client_export_stats(cmd->str, domain->stats);
		str_append_c(cmd->str, '\n');
		o_stream_nsend(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);

	if (!cmd->header_sent) {
		o_stream_nsend_str(client->output,
			"ip\treset_timestamp\tlast_update"
			"\tnum_logins\tnum_cmds\tnum_connected_sessions\t");
		client_export_stats_headers(client);
		cmd->header_sent = TRUE;
	}

	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", (long)ip->reset_timestamp);
		client_export_timeval(cmd->str, &ip->last_update);
		str_printfa(cmd->str, "\t%u\t%u\t%u\t",
			    ip->num_logins, ip->num_cmds, ip->num_connected_sessions);
		client_export_stats(cmd->str, ip->stats);
		str_append_c(cmd->str, '\n');
		o_stream_nsend(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_iter_global(struct client *client)
{
	struct client_export_cmd *cmd = client->cmd_export;
	struct mail_global *g = &mail_global_stats;

	i_assert(cmd->level == MAIL_EXPORT_LEVEL_GLOBAL);

	if (!cmd->header_sent) {
		o_stream_nsend_str(client->output,
			"reset_timestamp\tlast_update"
			"\tnum_logins\tnum_cmds\tnum_connected_sessions\t");
		client_export_stats_headers(client);
		cmd->header_sent = TRUE;
	}

	str_truncate(cmd->str, 0);
	str_printfa(cmd->str, "%ld", (long)g->reset_timestamp);
	client_export_timeval(cmd->str, &g->last_update);
	str_printfa(cmd->str, "\t%u\t%u\t%u\t",
		    g->num_logins, g->num_cmds, g->num_connected_sessions);
	client_export_stats(cmd->str, g->stats);
	str_append_c(cmd->str, '\n');
	o_stream_nsend(client->output, str_data(cmd->str),
		       str_len(cmd->str));
	return 1;
}

static int client_export_more(struct client *client)
{
	if (client->cmd_export->export_iter(client) == 0)
		return 0;
	o_stream_nsend_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_head;
		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;
	case MAIL_EXPORT_LEVEL_GLOBAL:
		cmd->export_iter = client_export_iter_global;
		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_nsend_str(client->output, "\n");
		return 1;
	}
	client->cmd_more = client_export_more;
	return client_export_more(client);
}
