/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
#include "lib.h"
#include "buffer.h"
#include "base64.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"
#define MAIL_COMMAND_TIMEOUT_SECS (60*15)
/* commands are sorted by their last_update timestamp, oldest first */
struct mail_command *stable_mail_commands_head;
struct mail_command *stable_mail_commands_tail;
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_malloc(MALLOC_ADD(sizeof(struct mail_command), stats_alloc_size()));
cmd->stats = (void *)(cmd + 1);
cmd->refcount = 1; /* unrefed at "done" */
cmd->session = session;
cmd->name = i_strdup(name);
cmd->args = i_strdup(args);
cmd->last_update = ioloop_timeval;
DLLIST2_APPEND_FULL(&stable_mail_commands_head,
&stable_mail_commands_tail, 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));
DLLIST2_REMOVE_FULL(&stable_mail_commands_head,
&stable_mail_commands_tail, 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 stats *new_stats, *diff_stats;
buffer_t *buf;
const char *error;
unsigned int i, cmd_id;
bool done = FALSE, continued = FALSE;
/* <session guid> <cmd id> [d] <name> <args> <stats>
<session guid> <cmd id> c[d] <stats> */
if (str_array_length(args) < 3) {
*error_r = "UPDATE-CMD: Too few parameters";
return -1;
}
if (mail_session_get(args[0], &session, error_r) < 0)
return -1;
if (str_to_uint(args[1], &cmd_id) < 0 || cmd_id == 0) {
*error_r = "UPDATE-CMD: Invalid command id";
return -1;
}
for (i = 0; args[2][i] != '\0'; i++) {
switch (args[2][i]) {
case 'd':
done = TRUE;
break;
case 'c':
continued = TRUE;
break;
default:
*error_r = "UPDATE-CMD: Invalid flags parameter";
return -1;
}
}
cmd = mail_command_find(session, cmd_id);
if (!continued) {
/* new command */
if (cmd != NULL) {
*error_r = "UPDATE-CMD: Duplicate new command id";
return -1;
}
if (str_array_length(args) < 5) {
*error_r = "UPDATE-CMD: Too few parameters";
return -1;
}
cmd = mail_command_add(session, args[3], args[4]);
cmd->id = cmd_id;
session->highest_cmd_id =
I_MAX(session->highest_cmd_id, cmd_id);
session->num_cmds++;
session->user->num_cmds++;
session->user->domain->num_cmds++;
if (session->ip != NULL)
session->ip->num_cmds++;
mail_global_stats.num_cmds++;
args += 5;
} else {
if (cmd == NULL) {
/* already expired command, ignore */
i_warning("UPDATE-CMD: Already expired");
return 0;
}
args += 3;
cmd->last_update = ioloop_timeval;
}
buf = t_buffer_create(256);
if (args[0] == NULL ||
base64_decode(args[0], strlen(args[0]), NULL, buf) < 0) {
*error_r = t_strdup_printf("UPDATE-CMD: Invalid base64 input");
return -1;
}
new_stats = stats_alloc(pool_datastack_create());
diff_stats = stats_alloc(pool_datastack_create());
if (!stats_import(buf->data, buf->used, cmd->stats, new_stats, &error)) {
*error_r = t_strdup_printf("UPDATE-CMD: %s", error);
return -1;
}
if (!stats_diff(cmd->stats, new_stats, diff_stats, &error)) {
*error_r = t_strdup_printf("UPDATE-CMD: stats shrank: %s", error);
return -1;
}
stats_add(cmd->stats, diff_stats);
if (done) {
cmd->id = 0;
mail_command_unref(&cmd);
}
mail_session_refresh(session, NULL);
return 0;
}
static bool mail_command_is_timed_out(struct mail_command *cmd)
{
/* some commands like IDLE can run forever */
return ioloop_time - cmd->last_update.tv_sec >
MAIL_COMMAND_TIMEOUT_SECS;
}
void mail_commands_free_memory(void)
{
unsigned int diff;
while (stable_mail_commands_head != NULL) {
struct mail_command *cmd = stable_mail_commands_head;
if (cmd->refcount == 0)
i_assert(cmd->id == 0);
else if (cmd->refcount == 1 &&
(cmd->session->disconnected ||
mail_command_is_timed_out(cmd))) {
/* session was probably lost */
mail_command_unref(&cmd);
} else {
break;
}
mail_command_free(stable_mail_commands_head);
if (global_used_memory < stats_settings->memory_limit ||
stable_mail_commands_head == NULL)
break;
diff = ioloop_time - stable_mail_commands_head->last_update.tv_sec;
if (diff < stats_settings->command_min_time)
break;
}
}
void mail_commands_init(void)
{
}
void mail_commands_deinit(void)
{
while (stable_mail_commands_head != NULL) {
struct mail_command *cmd = stable_mail_commands_head;
if (cmd->id != 0)
mail_command_unref(&cmd);
mail_command_free(stable_mail_commands_head);
}
}