/* This file is part of Mailfromd.
Copyright (C) 2005-2020 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
#include "libmf.h"
#include "filenames.h"
#include "callout.h"
#include "srvman.h"
#include "gacopyz.h"
#include "srvcfg.h"
#include "mfdb.h"
char *mailfromd_state_dir;
int server_flags = 0;
char *log_stream = DEFAULT_LOG_STREAM;
char *pidfile;
int smtp_transcript;
static int transcript_option;
struct mu_sockaddr *source_address; /* Source address for TCP connections */
int mtasim_option; /* mtasim compatibility mode */
char *db_type_str = DEFAULT_DB_TYPE;
/* 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;
static const char *
next_server_id()
{
static unsigned long count;
static char nbuf[INT_BUFSIZE_BOUND(unsigned long)];
count++;
snprintf(nbuf, sizeof(nbuf), "%lu", count);
return 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;
/* FIXME: This is awkward. */
if (gacopyz_parse_connection(str,
&proto,
&port, &path) != MI_SUCCESS) {
mu_error(_("%s: error parsing URL"), str);
}
if (port) {
if (!proto)
mu_asprintf(&tmp, "unix://%s:%s", path, port);
else if (strcmp(proto, "inet6") == 0)
mu_asprintf(&tmp, "inet6://[%s]:%s", path, port);
else
mu_asprintf(&tmp, "%s://%s:%s", proto, path, port);
} else
mu_asprintf(&tmp, "%s://%s",
proto ? proto : "unix", path);
free(proto);
free(path);
free(port);
url = _parse_url(tmp);
free(tmp);
return url;
}
static int
mf_option_group(const char *arg)
{
struct group *group = getgrnam(arg);
if (group) {
if (!mf_server_retain_groups)
mf_server_retain_groups = mf_gid_list_alloc();
mf_gid_list_add(mf_server_retain_groups, group->gr_gid);
} else {
mu_error(_("unknown group: %s"), arg);
return 1;
}
return 0;
}
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 = mu_strdup(arg);
return 0;
}
static int
set_source_ip(char const *arg)
{
int rc;
struct mu_sockaddr_hints hints;
memset(&hints, 0, sizeof hints);
hints.family = AF_INET;
hints.socktype = SOCK_STREAM;
hints.protocol = IPPROTO_TCP;
rc = mu_sockaddr_from_node(&source_address, arg, NULL, &hints);
if (rc) {
mu_error(_("cannot convert %s to sockaddr: %s"),
arg, mu_strerror(rc));
return 1;
}
return 0;
}
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);
}
}
/* FIXME: This duplicates the debug.level statement */
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;
set_source_ip(arg->v.string);
return 0;
}
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_c_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: string") },
{ "max-instances", mu_c_size,
&server_config_stmt.max_children, 0,
NULL,
N_("Maximum number of instances allowed for this server.") },
{ "single-process", mu_c_bool,
&server_config_stmt.single_process, 0, NULL,
N_("Single-process mode.") },
{ "reuseaddr", mu_c_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_c_string),
&server_config_stmt.options, 0, NULL,
N_("Server-dependent options") },
{ "default", mu_c_bool,
&server_config_stmt.defopt, 0, NULL,
N_("Deprecated. It is equivalent to `option \"default\"', "
"i.e. it marks this callout server as the default one.") },
{ "backlog", mu_c_int,
&server_config_stmt.backlog, 0, NULL,
N_("Size of queue of pending connections.") },
{ 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 (mtasim_option)
mu_url_destroy(&server_config_stmt.url);
else {
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);
if (server_config_stmt.backlog)
mfd_server_set_backlog(srv, server_config_stmt.backlog);
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: interval") },
{ "initial-response",
mu_cfg_callback,
NULL, smtp_timeout_initial * sizeof(time_t), cb_timeout,
N_("Timeout for initial SMTP response."),
N_("time: interval") },
{ "helo",
mu_cfg_callback,
NULL, smtp_timeout_helo * sizeof(time_t), cb_timeout,
N_("Timeout for HELO response."),
N_("time: interval") },
{ "mail",
mu_cfg_callback,
NULL, smtp_timeout_mail * sizeof(time_t), cb_timeout,
N_("Timeout for MAIL response."),
N_("time: interval") },
{ "rcpt",
mu_cfg_callback,
NULL, smtp_timeout_rcpt * sizeof(time_t),
cb_timeout,
N_("Timeout for RCPT response."),
N_("time: interval") },
{ "rset",
mu_cfg_callback,
NULL, smtp_timeout_rset * sizeof(time_t),
cb_timeout,
N_("Timeout for RSET response."),
N_("time: interval") },
{ "quit",
mu_cfg_callback,
NULL, smtp_timeout_quit * sizeof(time_t),
cb_timeout,
N_("Timeout for QUIT response."),
N_("time: interval") },
{ 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 debugging level. Argument is a semicolon-separated list "
"of debugging specifications. A simplified specification syntax is:\n"
" [!][.,...]\n"
"For details, please see the Mailfromd manual, section 3.18 \"Logging\n"
"and Debugging\", GNU Mailutils manual, section 3.3 \"Debugging\",\n"
"or visit ."),
N_("spec: list") },
{ "transcript", mu_c_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: seconds") },
/* 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"),
N_("group: string") },
{ "user", mu_c_string, &mf_server_user, 0, NULL,
N_("Switch to this user privileges after startup.") },
{ "pidfile", mu_c_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_c_string, &log_stream, 0, NULL,
N_("Set logger stream.") },
{ "state-directory", mu_cfg_callback, NULL, 0, cb_state_directory,
N_("Set program state directory."),
N_("dir: string") },
{ "database-type", mu_c_string, &db_type_str, 0, NULL,
N_("Default database type"),
N_("type") },
{ "database", mu_cfg_section, NULL, 0, NULL, NULL },
{ "database-mode", mu_cfg_callback,
&mf_database_mode, 0, cb_database_mode,
N_("Configure file mode for database files"),
N_("mode: octal") },
{ "ehlo-domain", mu_c_string, &ehlo_domain, 0, NULL,
N_("Set the domain name for EHLO command.") },
{ "mail-from-address", mu_c_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_c_bool, &enable_vrfy, 0, NULL,
N_("Use the SMTP VRFY command, when available.") },
{ NULL }
};
static void
opt_foreground (struct mu_parseopt *po, struct mu_option *op, char const *arg)
{
server_flags |= MF_SERVER_FOREGROUND;
}
static void
opt_single_process (struct mu_parseopt *po, struct mu_option *op,
char const *arg)
{
srvman_param.flags |= SRV_SINGLE_PROCESS;
}
static void
opt_group (struct mu_parseopt *po, struct mu_option *op, char const *arg)
{
mf_option_group(arg);
}
static void
opt_source_ip (struct mu_parseopt *po, struct mu_option *op, char const *arg)
{
if (set_source_ip(arg))
exit (po->po_exit_error);
}
static void
opt_state_directory (struct mu_parseopt *po, struct mu_option *op,
char const *arg)
{
struct stat st;
if (stat(arg, &st)) {
mu_parseopt_error(po, _("cannot stat file `%s': %s"),
arg,
mu_strerror(errno));
exit(po->po_exit_error);
}
if (!S_ISDIR(st.st_mode)) {
mu_parseopt_error(po, _("`%s' is not a directory"), arg);
exit(po->po_exit_error);
}
if (arg[0] != '/') {
mu_parseopt_error(po,
_("state directory `%s' is not an absolute "
"file name"), arg);
exit(po->po_exit_error);
}
mailfromd_state_dir = mu_strdup(arg);
}
static void
opt_debug(struct mu_parseopt *po, struct mu_option *op, char const *arg)
{
mu_debug_parse_spec(arg);
}
static void
opt_logger_stream(struct mu_parseopt *po, struct mu_option *op,
char const *arg)
{
//FIXME: Check arg: either syslog or stderr */
/* This option is handled twice. First, it must take
effect immediately, so that any eventual startup
errors end up being reported to syslog: */
mf_srvcfg_log_setup(arg);
/* Second, it overrides any logging settings read from the
configuration file. */
log_stream = mu_strdup(arg);
}
static void
opt_pidfile(struct mu_parseopt *po, struct mu_option *op, char const *arg)
{
if (arg[0] != '/') {
mu_parseopt_error(po,
_("invalid pidfile name: must be absolute"));
exit(po->po_exit_error);
}
pidfile = mu_strdup(arg);
}
static struct mu_option srv_options[] = {
MU_OPTION_GROUP(N_("Server configuration modifiers")),
{ "foreground", 0, NULL, MU_OPTION_DEFAULT,
N_("stay in foreground"),
mu_c_string, NULL, opt_foreground },
{ "single-process", 0, NULL, MU_OPTION_DEFAULT,
N_("run in single-process mode"),
mu_c_string, NULL, opt_single_process },
{ "pidfile", 0, N_("FILE"), MU_OPTION_DEFAULT,
N_("set pidfile name"),
mu_c_string, &pidfile, opt_pidfile },
{ "user", 'u', N_("NAME"), MU_OPTION_DEFAULT,
N_("switch to this user privileges after startup"),
mu_c_string, &mf_server_user },
{ "group", 'g', N_("NAME"), MU_OPTION_DEFAULT,
N_("retain the supplementary group NAME when switching to user "
"privileges"),
mu_c_string, NULL, opt_group },
{ "source-ip", 'S', N_("ADDRESS"), MU_OPTION_DEFAULT,
N_("set source address for TCP connections"),
mu_c_string, NULL, opt_source_ip },
{ "state-directory", 0, N_("DIR"), MU_OPTION_DEFAULT,
N_("set new program state directory"),
mu_c_string, NULL, opt_state_directory },
MU_OPTION_GROUP(N_("Logging and debugging options")),
{ "transcript", 'X', NULL, MU_OPTION_DEFAULT,
N_("enable transcript of SMTP sessions"),
mu_c_bool, &transcript_option },
{ "debug", 'd', N_("LEVEL"), MU_OPTION_DEFAULT,
N_("set debugging level"),
mu_c_string, NULL, opt_debug },
/* In the contrast to --syslog, this option takes effect
only after the configuration file has been parsed.
This is so because --stderr is the default behavior,
so that any configuration file errors are reported to
stderr anyway (unless it is closed, in which case they
are redirected to syslog). */
{ "stderr", 0, NULL, MU_OPTION_DEFAULT,
N_("log to stderr"),
mu_c_string, NULL, opt_logger_stream, "stderr" },
{ "syslog", 0, NULL, MU_OPTION_DEFAULT,
N_("log to syslog (default)"),
mu_c_string, NULL, opt_logger_stream, "syslog" },
{ "logger", 0, N_("STREAM"), MU_OPTION_DEFAULT,
N_("select logger stream"),
mu_c_string, NULL, opt_logger_stream },
{ "log-tag", 0, N_("STRING"), MU_OPTION_DEFAULT,
N_("set the identifier used in syslog messages to STRING"),
mu_c_string, &mu_log_tag },
{ "source-info", 0, NULL, MU_OPTION_DEFAULT,
N_("debug messages include source information"),
mu_c_bool, &mu_debug_line_info },
MU_OPTION_END
};
#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 = mu_alloc(len + sizeof(PIDSUF));
memcpy(pidfile, base, len);
strcpy(pidfile + len, PIDSUF);
}
static struct mu_cli_capa mfd_capa_server = {
".mfd:server",
srv_options,
};
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);
mu_cli_capa_register (&mfd_capa_server);
if (mu_create_canned_section (".mfd:server", §ion) == 0) {
mu_cfg_section_add_params (section, srv_cfg_param);
}
}
static void
init_ehlo_domain(void)
{
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 = mu_strdup(smtp_domain);
}
void
mf_srvcfg_flush()
{
int i;
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();
if (db_type_str) {
mu_url_t dbhint;
int rc;
static char const *defparam[] = { "linkwrdir", "awrdir" };
if ((rc = mu_url_create_null(&dbhint)) ||
(rc = mu_url_set_scheme(dbhint, db_type_str)) ||
(rc = mu_url_add_param(dbhint,
MU_ARRAY_SIZE(defparam),
defparam))) {
mu_error(_("cannot initialize DBM hint: %s"),
mu_strerror(rc));
exit(EX_SOFTWARE);
}
mu_url_destroy(&mu_dbm_hint);
mu_dbm_hint = dbhint;
}
}
void
mf_srvcfg_log_setup(char const *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);
}