/* 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); }