/* This file is part of Mailfromd.
Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 Sergey
Poznyakoff
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3, or (at your option)
any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see . */
#ifdef HAVE_CONFIG_H
# include
#endif
#include
#include
#include
#include
#include "xalloc.h"
#include "inttostr.h"
#include "libmf.h"
#include "filenames.h"
#include "callout.h"
#include "srvman.h"
#include "gacopyz.h"
#include "srvcfg.h"
char *mailfromd_state_dir;
int server_flags = 0;
char *log_stream = DEFAULT_LOG_STREAM;
char *pidfile;
int smtp_transcript;
static int transcript_option;
unsigned long source_address = INADDR_ANY; /* Source address for TCP
connections */
size_t max_callout_mx = MAXMXCOUNT;
/* Timeouts */
time_t smtp_timeout_soft[SMTP_NUM_TIMEOUT] = {
10,
30,
0,
0,
0,
0,
0,
};
/* Hard timeouts comply to RFC 2821 */
time_t smtp_timeout_hard[SMTP_NUM_TIMEOUT] = {
5*60, /* smtp_timeout_connect */
5*60, /* smtp_timeout_initial */
5*60, /* smtp_timeout_helo */
10*60, /* smtp_timeout_mail */
5*60, /* smtp_timeout_rcpt */
5*60, /* smtp_timeout_rset */
2*60, /* smtp_timeout_quit */
};
/* I/O timeout. Overrides unset smtp_timeouts */
time_t io_timeout = 3;
time_t negative_expire_interval = DEFAULT_EXPIRE_INTERVAL/2;
/* Expire negative cache entries after this
number of seconds */
static const char *
next_server_id()
{
static size_t count;
static char nbuf[INT_BUFSIZE_BOUND(uintmax_t)];
count++;
return umaxtostr(count, nbuf);
}
static mu_url_t
_parse_url(const char *str)
{
mu_url_t url;
int rc;
const char *s;
rc = mu_url_create(&url, str);
if (rc) {
mu_error(_("cannot create URL from `%s': %s"),
str, mu_strerror(rc));
return NULL;
}
if (mu_url_sget_scheme(url, &s) == 0 && strcmp(s, "file") == 0)
mu_url_set_scheme(url, "unix");
return url;
}
mu_url_t
parse_milter_url(const char *str)
{
mu_url_t url;
char *tmp;
char *proto, *port, *path;
if (gacopyz_parse_connection(str,
&proto,
&port, &path) != MI_SUCCESS) {
mu_error(_("%s: error parsing URL"), str);
}
if (port)
asprintf(&tmp, "%s://%s:%s",
proto ? proto : "unix", path, port);
else
asprintf(&tmp, "%s://%s",
proto ? proto : "unix", path);
free(proto);
free(path);
free(port);
url = _parse_url(tmp);
free(tmp);
return url;
}
static void
set_debug(void *value)
{
mu_debug_parse_spec(value);
}
void
set_source_info(void *value)
{
mu_debug_line_info = (int) value;
}
static void
set_user(void *value)
{
mf_server_user = value;
}
static int
gid_comp(const void *item, const void *data)
{
return (gid_t) item != (gid_t) data;
}
static int
mf_option_group(const char *arg)
{
struct group *group = getgrnam(arg);
if (group) {
if (!mf_server_retain_groups) {
int rc = mu_list_create(&mf_server_retain_groups);
if (rc) {
mu_error(_("Cannot create list: %s"),
strerror(rc));
return 1;
}
mu_list_set_comparator(mf_server_retain_groups,
gid_comp);
}
mu_list_append(mf_server_retain_groups, (void*)group->gr_gid);
} else {
mu_error(_("unknown group: %s"), arg);
return 1;
}
return 0;
}
static int
option_group(char *opt, void **pval, char *newval)
{
return mf_option_group(newval);
}
static int
option_pidfile(char *opt, void **pval, char *newval)
{
if (newval[0] != '/') {
mu_error(_("invalid pidfile name: must be absolute"));
return 1;
}
return mf_option_string(opt, pval, newval);
}
static void
set_pidfile(void *value)
{
pidfile = value;
}
static void
set_io_timeout(void *value)
{
io_timeout = *(time_t*) value;
free(value);
}
static void
set_logger_option(void *value)
{
log_stream = value;
}
static int
mf_option_state_directory(const char *arg)
{
struct stat st;
if (stat(arg, &st)) {
mu_error(_("cannot stat file `%s': %s"),
arg,
mu_strerror(errno));
return 1;
}
if (!S_ISDIR(st.st_mode)) {
mu_error(_("`%s' is not a directory"), arg);
return 1;
}
if (arg[0] != '/') {
mu_error(_("state directory `%s' is not an absolute "
"file name"), arg);
return 1;
}
mailfromd_state_dir = xstrdup(arg);
return 0;
}
void
set_state_directory(void *value)
{
/* nothing */
}
int
option_state_directory(char *opt, void **pval, char *newval)
{
return mf_option_state_directory(newval);
}
void
mf_srvcfg_add(const char *type, const char *urlstr)
{
struct mf_srvcfg cfg;
memset(&cfg, 0, sizeof(cfg));
cfg.id = next_server_id();
cfg.url = parse_milter_url(urlstr);
if (cfg.url) {
mfd_server_t srv;
if (mf_server_function(type, &cfg)) {
mu_error(_("INTERNAL ERROR: no such server type: %s"),
type);
exit(EX_SOFTWARE);
} else if (!cfg.server) {
mu_error(_("INTERNAL ERROR at %s:%d: "
"server function not defined"),
__FILE__, __LINE__);
exit(EX_SOFTWARE);
}
srv = mfd_server_new(cfg.id, cfg.url, cfg.server, 0);
if (srv)
mfd_srvman_attach_server(srv);
else
abort();
}
}
static void
set_port(void *value)
{
mf_srvcfg_add("default", value);
}
static int
mf_option_source_ip(const char *arg, unsigned long *pval)
{
unsigned long address = inet_addr(arg);
if (address == INADDR_NONE) {
struct hostent *phe = gethostbyname(arg);
if (!phe) {
mu_error(_("cannot resolve `%s'"), arg);
return 1;
}
address = *(((unsigned long **) phe->h_addr_list)[0]);
}
*pval = address;
return 0;
}
static int
option_source_ip(char *opt, void **pval, char *newval)
{
unsigned long address;
if (mf_option_source_ip(newval, &address))
return 1;
if (*pval == 0)
*pval = xmalloc (sizeof address);
*(unsigned long*)*pval = address;
return 0;
}
static void
set_source_ip(void *value)
{
source_address = *(unsigned long*)value;
}
static struct mf_option_cache srv_option_cache[] = {
{ "debug", NULL, mf_option_string, set_debug },
{ "source-info", NULL, mf_option_boolean, set_source_info },
{ "user", NULL, mf_option_string, set_user },
{ "group", NULL, option_group, NULL },
{ "pidfile", NULL, option_pidfile, set_pidfile },
{ "source-ip", NULL, option_source_ip, set_source_ip },
{ "io-timeout", NULL, mf_option_time, set_io_timeout },
{ "logger", NULL, mf_option_string, set_logger_option },
{ "state-directory", NULL, option_state_directory,
set_state_directory },
{ "port", NULL, mf_option_string, set_port },
{ NULL }
};
static int
cb_debug(void *data, mu_config_value_t *arg)
{
if (mu_cfg_assert_value_type(arg, MU_CFG_STRING))
return 1;
mu_debug_parse_spec(arg->v.string);
return 0;
}
/* See also option_group. */
static int
cb_group(void *data, mu_config_value_t *arg)
{
if (mu_cfg_assert_value_type(arg, MU_CFG_STRING))
return 1;
return mf_option_group(arg->v.string);
}
static int
cb_source_ip(void *data, mu_config_value_t *arg)
{
if (mu_cfg_assert_value_type(arg, MU_CFG_STRING))
return 1;
return mf_option_source_ip(arg->v.string, &source_address);
}
static struct mf_srvcfg server_config_stmt;
static int
cb_server_stmt_listen(void *data, mu_config_value_t *arg)
{
if (mu_cfg_assert_value_type(arg, MU_CFG_STRING))
return 1;
*(mu_url_t*)data = parse_milter_url(arg->v.string);
return 0;
}
struct mu_cfg_param server_section_param[] = {
{ "id", mu_cfg_string,
&server_config_stmt.id, 0,
NULL,
N_("Server ID.") },
{ "listen", mu_cfg_callback,
&server_config_stmt.url, 0,
cb_server_stmt_listen,
N_("Listen on this URL."),
N_("url") },
{ "max-instances", mu_cfg_size,
&server_config_stmt.max_children, 0,
NULL,
N_("Maximum number of instances allowed for this server.") },
{ "single-process", mu_cfg_bool,
&server_config_stmt.single_process, 0, NULL,
N_("Single-process mode.") },
{ "reuseaddr", mu_cfg_bool,
&server_config_stmt.reuseaddr, 0, NULL,
N_("Reuse existing socket (default).") },
{ "acl", mu_cfg_section,
&server_config_stmt.acl },
{ "option", MU_CFG_LIST_OF(mu_cfg_string),
&server_config_stmt.options, 0, NULL,
N_("Server-dependent options") },
{ "default", mu_cfg_bool,
&server_config_stmt.defopt, 0, NULL,
N_("Deprecated. It is equivalemt to `option \"default\"', "
"i.e. it marks this callout server as the default one.") },
{ NULL }
};
static int
server_section_parser(enum mu_cfg_section_stage stage,
const mu_cfg_node_t *node,
const char *section_label, void **section_data,
void *call_data,
mu_cfg_tree_t *tree)
{
switch (stage) {
case mu_cfg_section_start:
memset(&server_config_stmt, 0, sizeof(server_config_stmt));
server_config_stmt.reuseaddr = 1;
if (node->label &&
mu_cfg_assert_value_type(node->label, MU_CFG_STRING))
return 1;
break;
case mu_cfg_section_end:
if (server_config_stmt.options)
mu_list_set_comparator(server_config_stmt.options,
mf_list_compare_string);
if (mf_server_function(node->label ?
node->label->v.string : NULL,
&server_config_stmt)) {
mu_error(_("unknown server type"));
return 1;
} else if (!server_config_stmt.server) {
mu_error(_("INTERNAL ERROR at %s:%d: "
"server function not defined"),
__FILE__, __LINE__);
return 1;
}
if (!server_config_stmt.id)
server_config_stmt.id = next_server_id();
if (server_config_stmt.url && server_config_stmt.server) {
int flags = 0;
mfd_server_t srv;
if (server_config_stmt.single_process)
flags |= SRV_SINGLE_PROCESS;
if (!server_config_stmt.reuseaddr)
flags |= SRV_KEEP_EXISTING;
srv = mfd_server_new(server_config_stmt.id,
server_config_stmt.url,
server_config_stmt.server,
flags);
if (srv) {
mfd_server_set_max_children(srv,
server_config_stmt.max_children);
mfd_server_set_acl(srv,
server_config_stmt.acl);
mfd_srvman_attach_server(srv);
}
}
mu_list_destroy(&server_config_stmt.options);
break;
}
return 0;
}
static void
server_stmt_init(const char *label)
{
struct mu_cfg_section *section;
if (mu_create_canned_section ("server", §ion) == 0) {
section->parser = server_section_parser;
section->docstring = N_("Configure server.");
section->label = (char*) (label ? label : _("label"));
mu_cfg_section_add_params(section, server_section_param);
}
}
static int
smtp_timeout_section_parser (enum mu_cfg_section_stage stage,
const mu_cfg_node_t *node,
const char *section_label, void **section_data,
void *call_data,
mu_cfg_tree_t *tree)
{
switch (stage) {
case mu_cfg_section_start:
if (!node->label)
*section_data = smtp_timeout_soft;
else {
if (mu_cfg_assert_value_type(node->label,
MU_CFG_STRING))
return 0;
if (strcmp(node->label->v.string, "soft") == 0)
*section_data = smtp_timeout_soft;
else if (strcmp(node->label->v.string, "hard") == 0)
*section_data = smtp_timeout_hard;
else
mu_error (_("unknown timeout class: %s"),
node->label->v.string);
}
break;
case mu_cfg_section_end:
break;
}
return 0;
}
static int
cb_timeout(void *data, mu_config_value_t *arg)
{
struct timeval tv;
int rc = config_cb_timeout (&tv, arg);
if (rc == 0)
*(time_t*) data = tv.tv_sec;
return rc;
}
struct mu_cfg_param smtp_timeout_section_param[] = {
{ "connection", mu_cfg_callback,
NULL, smtp_timeout_connect * sizeof(time_t), cb_timeout,
N_("Initial SMTP connection timeout."),
N_("time") },
{ "initial-response",
mu_cfg_callback,
NULL, smtp_timeout_initial * sizeof(time_t), cb_timeout,
N_("Timeout for initial SMTP response."),
N_("time") },
{ "helo",
mu_cfg_callback,
NULL, smtp_timeout_helo * sizeof(time_t), cb_timeout,
N_("Timeout for HELO resonse."),
N_("time") },
{ "mail",
mu_cfg_callback,
NULL, smtp_timeout_mail * sizeof(time_t), cb_timeout,
N_("Timeout for MAIL response."),
N_("time") },
{ "rcpt",
mu_cfg_callback,
NULL, smtp_timeout_rcpt * sizeof(time_t),
cb_timeout,
N_("Timeout for RCPT response."),
N_("time") },
{ "rset",
mu_cfg_callback,
NULL, smtp_timeout_rset * sizeof(time_t),
cb_timeout,
N_("Timeout for RSET response."),
N_("time") },
{ "quit",
mu_cfg_callback,
NULL, smtp_timeout_quit * sizeof(time_t),
cb_timeout,
N_("Timeout for QUIT response."),
N_("time") },
{ NULL }
};
static void
smtp_timeout_cfg_init()
{
struct mu_cfg_section *section;
if (mu_create_canned_section ("smtp-timeout", §ion) == 0) {
section->parser = smtp_timeout_section_parser;
section->docstring = N_("Set SMTP timeouts.");
/* TRANSLATORS: soft and hard are keywords, do not translate
them */
section->label = N_("[soft | hard]");
mu_cfg_section_add_params (section, smtp_timeout_section_param);
}
}
static int
cb_state_directory(void *data, mu_config_value_t *arg)
{
if (mu_cfg_assert_value_type(arg, MU_CFG_STRING))
return 1;
return mf_option_state_directory(arg->v.string);
}
/* FIXME: Umask not configurable */
static struct mu_cfg_param srv_cfg_param[] = {
{ "debug", mu_cfg_callback, NULL, 0, cb_debug,
N_("Set Mailfromd debug verbosity level. Argument is a comma-"
"separated list of debug specifications, each of which has the "
"following form:\n"
" [=]\n"
"where is the name of a Mailfromd module, and "
"is the desired verbosity level for that module."),
N_("spec: list") },
{ "transcript", mu_cfg_bool, &smtp_transcript, 0, NULL,
N_("Enable transcript of call-out SMTP sessions.") },
{ "smtp-timeout", mu_cfg_section, },
{ "io-timeout", mu_cfg_callback, &io_timeout, 0, cb_timeout,
N_("Timeout for all SMTP I/O operations."),
N_("time") },
/* FIXME: Could have used mu_cfg_ipv4 here, but... */
{ "source-ip", mu_cfg_callback, NULL, 0, cb_source_ip,
N_("Set source address for TCP connections."),
N_("ip: ipaddr") },
{ "group", mu_cfg_callback, NULL, 0, cb_group,
N_("Retain the supplementary group when switching to user "
"privileges") },
{ "user", mu_cfg_string, &mf_server_user, 0, NULL,
N_("Switch to this user privileges after startup.") },
{ "pidfile", mu_cfg_string, &pidfile, 0, NULL,
N_("Set file to store PID value in."),
N_("file") },
{ "server", mu_cfg_section },
{ "acl", mu_cfg_section, &srvman_param.acl },
{ "logger", mu_cfg_string, &log_stream, 0, NULL,
N_("Set logger stream.") },
{ "max-callout-mx", mu_cfg_size, &max_callout_mx, 0, NULL,
N_("Maximum number of MXs to be polled during "
"callout verification.") },
{ "state-directory", mu_cfg_callback, NULL, 0, cb_state_directory,
N_("Set program state directory."),
N_("dir") },
{ "database", mu_cfg_section, NULL, 0, NULL, NULL },
{ "ehlo-domain", mu_cfg_string, &ehlo_domain, 0, NULL,
N_("Set the domain name for EHLO command.") },
{ "mail-from-address", mu_cfg_string, &mailfrom_address, 0, NULL,
N_("Set email address for use in SMTP `MAIL FROM' command. "
"Argument is an email address or a comma-separated list of "
"addresses. Use <> for null address. Other addresses can "
"be given without angle brackets."),
N_("addr") },
{ "enable-vrfy", mu_cfg_bool, &enable_vrfy, 0, NULL,
N_("Use the SMTP VRFY command, when available.") },
{ NULL }
};
enum {
OPTION_FOREGROUND=256,
OPTION_LOGGER,
OPTION_LOG_TAG,
OPTION_PIDFILE,
OPTION_SINGLE_PROCESS,
OPTION_SOURCE_INFO,
OPTION_STATE_DIRECTORY,
OPTION_STDERR,
OPTION_SYSLOG
};
static struct argp_option srv_options[] = {
#define GRP 0
{ NULL, 0, NULL, 0,
N_("Server configuration modifiers"), GRP },
{ "foreground", OPTION_FOREGROUND, NULL, 0,
N_("stay in foreground"), GRP+1 },
{ "single-process", OPTION_SINGLE_PROCESS, NULL, 0,
N_("run in single-process mode"), GRP+1 },
{ "pidfile", OPTION_PIDFILE, N_("FILE"), 0,
N_("set pidfile name"), GRP+1 },
{ "user", 'u', N_("NAME"), 0,
N_("switch to this user privileges after startup"), GRP+1 },
{ "group", 'g', N_("NAME"), 0,
N_("retain the supplementary group NAME when switching to user "
"privileges"),
GRP+1 },
{ "source-ip", 'S', N_("ADDRESS"), 0,
N_("set source address for TCP connections"), GRP+1 },
/* FIXME: For backward compatibility: */
{ "remove", 'r', NULL, OPTION_HIDDEN, NULL, GRP+1 },
{ "state-directory", OPTION_STATE_DIRECTORY, N_("DIR"), 0,
N_("set new program state directory"), GRP+1 },
#undef GRP
#define GRP 10
{ NULL, 0, NULL, 0,
N_("Logging and debugging options"), GRP },
{ "transcript", 'X', NULL, 0,
N_("enable transcript of SMTP sessions"), GRP+1 },
{ "debug", 'd', N_("LEVEL"), 0,
N_("set debugging level"), GRP+1 },
{ "stderr", OPTION_STDERR, NULL, 0,
N_("log to stderr"), GRP+1 },
{ "syslog", OPTION_SYSLOG, NULL, 0,
N_("log to syslog (default)"), GRP+1 },
{ "logger", OPTION_LOGGER, N_("STREAM"), 0,
N_("select logger stream"), GRP+1 },
{ "log-tag", OPTION_LOG_TAG, N_("STRING"), 0,
N_("set the identifier used in syslog messages to STRING"), GRP+1 },
{ "source-info", OPTION_SOURCE_INFO, NULL, 0,
N_("debug messages include source information"), GRP+1 },
{ NULL }
};
static error_t
srv_parse_opt(int key, char *arg, struct argp_state *state)
{
switch (key) {
case 'd':
mf_optcache_set_option("debug", arg);
break;
case OPTION_LOG_TAG:
mu_log_tag = arg;
break;
case 'r':
/* Option ignored for backward compatibility */
break;
case 'S':
mf_optcache_set_option("source-ip", arg);
break;
case 'u':
mf_optcache_set_option("user", arg);
break;
case 'g':
mf_optcache_set_option("group", arg);
break;
case 'X':
transcript_option = 1;
break;
case OPTION_FOREGROUND:
server_flags |= MF_SERVER_FOREGROUND;
break;
case OPTION_LOGGER:
mf_optcache_set_option("logger", arg);
break;
case OPTION_PIDFILE:
mf_optcache_set_option("pidfile", arg);
break;
case OPTION_SINGLE_PROCESS:
srvman_param.flags |= SRV_SINGLE_PROCESS;
break;
case OPTION_SOURCE_INFO:
mf_optcache_set_option("source-info", "yes");
break;
case OPTION_STATE_DIRECTORY:
mf_optcache_set_option("state-directory", arg);
break;
case OPTION_SYSLOG:
mf_optcache_set_option("logger", "syslog");
break;
case OPTION_STDERR:
mf_optcache_set_option("logger", "stderr");
break;
default:
return ARGP_ERR_UNKNOWN;
}
return 0;
}
static struct argp _srv_argp = {
srv_options,
srv_parse_opt
};
static struct argp_child _srv_argp_child = {
&_srv_argp,
0,
NULL,
0
};
static int
srvcfg_gocs_init(enum mu_gocs_op op, void *data)
{
return 0;
}
#define PIDSUF ".pid"
static void
setdefpidfilename(const char *progname)
{
size_t len;
const char *base = strrchr(progname, '/');
if (base)
base++;
else
base = progname;
len = strlen(base);
pidfile = xmalloc(len + sizeof(PIDSUF));
memcpy(pidfile, base, len);
strcpy(pidfile + len, PIDSUF);
}
void
mf_srvcfg_init(const char *progname, const char *label)
{
struct mu_cfg_section *section;
smtp_timeout_cfg_init();
server_stmt_init(label);
mailfromd_state_dir = DEFAULT_STATE_DIR;
if (!pidfile)
setdefpidfilename(progname);
mf_optcache_add(srv_option_cache, 0, MF_OCF_NULL|MF_OCF_STATIC);
mu_gocs_register(".mfd:server", srvcfg_gocs_init);
if (mu_register_argp_capa(".mfd:server", &_srv_argp_child)) {
mu_error(_("INTERNAL ERROR: "
"cannot register argp capability `%s'"),
"mfd:server");
abort ();
}
if (mu_create_canned_section (".mfd:server", §ion) == 0) {
mu_cfg_section_add_params (section, srv_cfg_param);
}
}
static void
init_ehlo_domain()
{
char *smtp_hostname;
char *smtp_domain;
mu_get_host_name(&smtp_hostname);
smtp_domain = strchr(smtp_hostname, '.');
if (smtp_domain)
smtp_domain++;
else
smtp_domain = smtp_hostname;
ehlo_domain = xstrdup(smtp_domain);
}
void
mf_srvcfg_flush()
{
int i;
mf_optcache_flush();
if (transcript_option)
smtp_transcript = transcript_option;
/* Fix unspecified timeouts */
for (i = 0; i < SMTP_NUM_TIMEOUT; i++) {
if (smtp_timeout_soft[i] == 0)
smtp_timeout_soft[i] = io_timeout;
if (smtp_timeout_hard[i] == 0)
smtp_timeout_hard[i] = io_timeout;
}
if (!ehlo_domain)
init_ehlo_domain();
}
void
mf_srvcfg_log_setup(char *stream)
{
if (logger_select(stream)) {
mu_error(_("unsupported logger stream: %s"), stream);
exit(EX_USAGE);
}
logger_open();
FD_ZERO(&srvman_param.keepfds);
logger_fdset(&srvman_param.keepfds);
if (logger_flags(LOGF_STDERR))
/* Keep also stdout.
FIXME: Is it still needed? */
FD_SET(1, &srvman_param.keepfds);
}
void
mf_server_log_setup(void)
{
mf_srvcfg_log_setup(log_stream);
}