/* This file is part of Mailfromd. Copyright (C) 2003, 2004, 2007, 2008, 2009 Sergey Poznyakoff. Mailfromd 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 of the License, or (at your option) any later version. Mailfromd 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 Mailfromd. If not, see . */ #ifdef HAVE_CONFIG_H # include #endif #include #include #ifdef HAVE_GETOPT_H # include #endif #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_READLINE_READLINE_H # include # include #endif #include #include #include #include #include #include #include #define obstack_chunk_alloc malloc #define obstack_chunk_free free #include #include #include #include #include #include "libmf.h" #if defined(USE_GNUTLS) && defined(HAVE_GNUTLS_GNUTLS_H) # include # define HAVE_TLS #endif /* USE_GNUTLS and HAVE_GNUTLS_GNUTLS_H */ FILE *trace = NULL; /* diagnostic output */ int port = 0; /* Port number (for smtp mode) */ int verbose; char *user; /* When started as root, switch to this user privileges */ mu_list_t grouplist; /* List of additional groups to be retained */ #ifdef HAVE_TLS char *tls_cert; /* TLS sertificate */ char *tls_key; /* TLS key */ char *tls_cafile; #define DH_BITS 768 #define enable_tls() \ (tls_cafile != NULL || (tls_cert != NULL && tls_key != NULL)) void tls_init (void); gnutls_dh_params dh_params; static gnutls_certificate_server_credentials x509_cred; #endif /* HAVE_TLS */ int interactive; char *prompt = "(mtasim) "; int mta_daemon (void); int mta_stdio (void); void error (const char *, ...); void smtp_reply (int, char *, ...); void reset_capa (char *); void shell_help (void); void shell (int argc, char **argv); int define_macro (char *arg); #define R_CONT 0x8000 #define R_CODEMASK 0xfff /* Milter-related options */ char *milter_port; size_t max_body_chunk = 65535; unsigned long milter_version_option = 0; unsigned long milter_proto_option = 0; unsigned long milter_acts_option = 0; int gacopyz_log_mask; struct timeval milter_timeouts[GACOPYZ_TO_COUNT] = { { GACOPYZ_WRITE_TIMEOUT, 0 }, { GACOPYZ_READ_TIMEOUT, 0 }, { GACOPYZ_EOM_TIMEOUT, 0 }, { GACOPYZ_CONNECT_TIMEOUT, 0 } }; static int gid_comp (const void *item, const void *data) { return (gid_t) item != (gid_t) data; } static void add_group (const char *gname) { struct group *group = getgrnam(gname); if (group) { if (!grouplist) { int rc = mu_list_create (&grouplist); if (rc) { mu_error(_("Cannot create list: %s"), mu_strerror(rc)); exit (EX_SOFTWARE); } mu_list_set_comparator (grouplist, gid_comp); } mu_list_append (grouplist, (void*)group->gr_gid); } else { mu_error(_("Unknown group: %s"), gname); exit (EX_DATAERR); } } static void priv_setup () { if (getuid() == 0 && user) { struct passwd *pw = getpwnam (user); if (!pw) { mu_error(_("No such user: %s"), user); exit (EX_SOFTWARE); } if (pw && switch_to_privs (pw->pw_uid, pw->pw_gid, grouplist)) exit (EX_SOFTWARE); } } gacopyz_srv_t gsrv; void update_nrcpts (char *name, unsigned n) { char buf[128]; snprintf (buf, sizeof buf, "%u", n); gacopyz_srv_define_macro (gsrv, name, buf); } static mu_list_t defnlist; int defer_define_macro (char *arg) { char *p; p = strchr (arg, '='); if (!p) return 1; *p++ = 0; if (!defnlist) mu_list_create (&defnlist); mu_list_append (defnlist, arg); return 0; } static int do_define (void *item, void *data) { char *name = item; char *value = name + strlen (name) + 1; gacopyz_srv_t srv = data; gacopyz_srv_define_macro (srv, name, value); return 0; } void flush_deferred_defns (gacopyz_srv_t srv) { if (srv) mu_list_do (defnlist, do_define, srv); mu_list_destroy (&defnlist); } /* Expect stuff */ static char expected_code[4]; void free_expected_code () { expected_code[0] = 0; } int set_expected_code (const char *str) { size_t len = strlen (str); if (len == 0 || !strchr ("12345", str[0])) return 1; if (len > 1) { if (!c_isdigit (str[1]) || (len > 2 && !c_isdigit (str[2]))) return 1; } if (len > 3) len = 3; memcpy (expected_code, str, len); expected_code[len] = 0; return 0; } void check_expected_code (char *str) { int i; for (i = 0; i < 3 && expected_code[i]; i++) if (str[i] != expected_code[i]) { /* fill in the expected code */ for (i = strlen(expected_code); i < 3; i++) expected_code[i] = '.'; mu_error (_("Expected %s, but got %3.3s"), expected_code, str); if (gsrv) { gacopyz_srv_abort (gsrv); gacopyz_srv_quit (gsrv); gacopyz_srv_close (gsrv); gacopyz_srv_destroy (&gsrv); } exit (1); } expected_code[0] = 0; } const char *argp_program_version = "mtasim (" PACKAGE_STRING ")"; const char *argp_program_bug_address = "<" PACKAGE_BUGREPORT ">"; static char doc[] = N_("mtasim -- MTA simulator for mailfromd"); enum { OPTION_STDIO = 128, OPTION_GACOPYZ_LOG, OPTION_DAEMON, OPTION_TLS_CERT, OPTION_TLS_CA, OPTION_TLS_KEY, OPTION_TRACE_FILE, OPTION_BODY_CHUNK, OPTION_MILTER_VERSION, OPTION_MILTER_PROTO, OPTION_MILTER_ACTS, OPTION_NO_INTERACTIVE, OPTION_PROMPT, OPTION_STATEDIR }; #ifndef WITH_READLINE # define OPT_INTERACTIVE OPTION_HIDDEN #else # define OPT_INTERACTIVE 0 #endif static struct argp_option options[] = { #define GRP 1 { NULL, 0, NULL, 0, N_("Mode selection"), GRP }, { NULL, 'b', N_("MODE"), 0, N_("Set MTA mode (-bd or -bs)"), GRP+1 }, { "stdio", OPTION_STDIO, NULL, 0, N_("Use the SMTP protocol on standard input and output (same as -bs)"), GRP+1 }, { "daemon", OPTION_DAEMON, NULL, 0, N_("Run as daemon (same as -bd)"), GRP+1 }, { "user", 'u', N_("NAME"), 0, N_("Run with this user privileges"), GRP+1 }, { "group", 'g', N_("NAME"), 0, N_("Retain the supplementary group NAME when switching to user " "privileges"), GRP+1 }, #undef GRP #define GRP 10 { NULL, 0, NULL, 0, N_("Operation modifiers"), GRP }, { "define", 'D', N_("MACRO=VALUE"), 0, N_("Define Sendmail macro"), GRP+1 }, { "port", 'X', N_("PORT"), 0, N_("Communicate with Milter PORT"), GRP+1 }, { "statedir", OPTION_STATEDIR, NULL, 0, N_("Pass temporary directory to mailfromd as its state dir (with -Xauto)"), GRP+1 }, { "body-chunk", OPTION_BODY_CHUNK, N_("NUMBER"), 0, N_("Set the body chunk for xxfi_body calls"), GRP+1 }, { "milter-version", OPTION_MILTER_VERSION, N_("VER"), 0, N_("Force using the given Milter protocol version number"), GRP+1 }, { "milter-actions", OPTION_MILTER_ACTS, N_("BITMASK"), 0, N_("Force the given Milter actions"), GRP+1 }, { "milter-proto", OPTION_MILTER_PROTO, N_("BITMASK"), 0, N_("Set Milter protocol capabilities"), GRP+1 }, #undef GRP #define GRP 20 { "no-interactive", OPTION_NO_INTERACTIVE, NULL, OPT_INTERACTIVE, N_("Not-interactive mode (disable readline)"), GRP+1 }, { "prompt", OPTION_PROMPT, N_("STRING"), OPT_INTERACTIVE, N_("Set readline prompt"), GRP+1 }, #undef GRP #define GRP 30 { NULL, 0, NULL, 0, N_("Debugging and tracing"), GRP }, { "append", 'a', NULL, 0, N_("Append to the trace file"), GRP+1 }, { "trace-file", OPTION_TRACE_FILE, N_("FILE"), 0, N_("Set name of the trace file"), GRP+1 }, { "verbose", 'v', NULL, 0, N_("Increase verbosity level"), GRP+1 }, { "gacopyz-log", OPTION_GACOPYZ_LOG, N_("LEVEL"), 0, N_("Set Gacopyz log level"), GRP+1 }, #undef GRP #define GRP 40 #ifdef HAVE_TLS { NULL, 0, NULL, 0, N_("TLS options"), GRP }, { "tls-cert", OPTION_TLS_CERT, N_("FILE"), 0, N_("Set name of the TLS certificate file"), GRP+1 }, { "tls-ca", OPTION_TLS_CA, N_("FILE"), 0, N_("Set name of the TLS CA file"), GRP+1 }, { "tls-key", OPTION_TLS_KEY, N_("FILE"), 0, N_("Set name of the TLS key file"), GRP+1 }, #endif #undef GRP { NULL } }; int (*mta_mode) (void) = mta_stdio; char *trace_name = NULL; int append; int statedir_option; static unsigned long parse_version (char *arg, struct argp_state *state) { char *p; unsigned long maj, min, pat; maj = strtoul (arg, &p, 0); if (*p == 0) return maj; else if (*p == '.') { min = strtoul (p + 1, &p, 0); if (*p == '.') { pat = strtoul (p + 1, &p, 0); if (*p == 0) return GACOPYZ_SM_MKVER (maj, min, pat); } else if (*p == 0) return GACOPYZ_SM_MKVER (maj, min, 0); } argp_error (state, _("invalid version syntax (near %s)"), p); return 0; } static error_t parse_opt (int key, char *arg, struct argp_state *state) { char *p; switch (key) { case 'a': append = 1; break; case 'b': switch (arg[0]) { case 'd': mta_mode = mta_daemon; break; case 's': mta_mode = mta_stdio; break; default: argp_error (state, _("unsupported mode")); } break; case OPTION_GACOPYZ_LOG: { int lev = gacopyz_string_to_log_level(arg); if (lev == -1) argp_error(state, _("%s: invalid log level"), arg); gacopyz_log_mask = SMI_LOG_FROM (lev); } break; case OPTION_STDIO: mta_mode = mta_stdio; break; case OPTION_DAEMON: mta_mode = mta_daemon; break; #ifdef HAVE_TLS case OPTION_TLS_CERT: tls_cert = arg; break; case OPTION_TLS_CA: tls_cafile = arg; break; case OPTION_TLS_KEY: tls_key = arg; break; #endif case 'D': if (defer_define_macro (arg)) mu_error (_("wrong assignment format: %s"), arg); break; case 'g': add_group (arg); break; case OPTION_TRACE_FILE: trace_name = arg; break; case 'u': user = arg; break; case 'X': milter_port = arg; break; case 'v': verbose++; break; case OPTION_BODY_CHUNK: max_body_chunk = strtoul (arg, &p, 0); if (*p) argp_error (state, _("invalid number: %s"), arg); break; case OPTION_MILTER_VERSION: milter_version_option = parse_version (arg, state); break; case OPTION_MILTER_PROTO: milter_proto_option = strtoul (arg, &p, 0); if (*p) argp_error (state, _("invalid number: %s"), arg); break; case OPTION_MILTER_ACTS: milter_acts_option = strtoul (arg, &p, 0); if (*p) argp_error (state, _("invalid number: %s"), arg); break; #ifdef WITH_READLINE case OPTION_NO_INTERACTIVE: interactive = 0; break; case OPTION_PROMPT: prompt = arg; break; #endif case OPTION_STATEDIR: statedir_option = 1; break; default: return ARGP_ERR_UNKNOWN; } return 0; } static struct argp argp = { options, parse_opt, NULL, doc, NULL, NULL, NULL }; void xalloc_die () { mu_error (_("not enough memory")); abort (); } static int portspec_p (const char *str) { size_t len = strlen (str); static char *proto[] = { "/", "unix:", "local:", "inet:", "inet6:", NULL }; char **p; for (p = proto; *p; p++) { size_t n = strlen (*p); if (len > n && memcmp (str, *p, n) == 0) return 1; } return 0; } pid_t child_pid; static char *tmpdir; static int got_sigchld; static RETSIGTYPE sig_child (int sig) { int status; got_sigchld = 1; waitpid((pid_t)-1, &status, 0); } static int rmdir_r (const char *name); static int recursive_rmdir (const char *name) { int rc; DIR *dir; struct dirent *ent; if (chdir (name)) { mu_error (_("cannot change to directory %s: %s"), name, mu_strerror (errno)); return 1; } dir = opendir ("."); if (!dir) { mu_error (_("cannot open directory %s: %s"), name, mu_strerror (errno)); return 1; } for (rc = 0; rc == 0 && (ent = readdir (dir));) { struct stat st; if (strcmp (ent->d_name, ".") == 0 || strcmp (ent->d_name, "..") == 0) continue; if (stat (ent->d_name, &st) && errno != ENOENT) { mu_error (_("cannot stat file `%s': %s"), name, mu_strerror (errno)); rc = 1; } else if (S_ISDIR (st.st_mode)) rc = rmdir_r (ent->d_name); else if ((rc = unlink (ent->d_name)) != 0 && errno != ENOENT) mu_error (_("cannot unlink %s: %s"), ent->d_name, mu_strerror (errno)); } closedir (dir); return rc; } static int rmdir_r (const char *name) { int rc; struct saved_cwd cwd; if (save_cwd (&cwd)) { mu_error (_("cannot save current directory: %s"), mu_strerror (errno)); return 1; } rc = recursive_rmdir (name); if (restore_cwd (&cwd)) { mu_error (_("cannot restore current directory: %s"), mu_strerror (errno)); rc = 1; } if (rc == 0 && rmdir (name)) { mu_error (_("cannot remove directory %s: %s"), name, mu_strerror (errno)); return 1; } return rc; } void stop_mailfromd (void) { if (child_pid > 0) { int status; pid_t pid; signal (SIGCHLD, SIG_DFL); kill (child_pid, SIGTERM); pid = waitpid (child_pid, &status, 0); if (pid == (pid_t) -1) mu_error ("waitpid: %s", mu_strerror (errno)); else if (WIFEXITED (status)) { status = WEXITSTATUS (status); if (status != 0) mu_error (_("mailfromd exited with status %d"), status); } else if (WIFSIGNALED (status)) mu_error (_("mailfromd terminated on signal %d"), WTERMSIG (status)); else mu_error (_("mailfromd terminated with unrecognized status")); } rmdir_r (tmpdir); } void start_mailfromd (int argc, char **argv) { tmpdir = xstrdup ("/tmp/mtasim-XXXXXX"); if (!mkdtemp (tmpdir)) { mu_error (_("cannot create temprorary directory (%s): %s"), tmpdir, mu_strerror (errno)); exit (EX_OSERR); } atexit (stop_mailfromd); milter_port = xstrdup ("unix:/tmp/mtasim-XXXXXX/socket"); memcpy (milter_port + 5, tmpdir, strlen (tmpdir)); signal (SIGCHLD, sig_child); child_pid = fork (); if (child_pid == -1) { mu_error (_("cannot fork: %s"), mu_strerror (errno)); exit (EX_OSERR); } if (child_pid == 0) { int xargc = argc + 5 + (statedir_option ? 2 : 0); char **xargv = xmalloc ((xargc + 1) * sizeof xargv[0]); int i; xargv[0] = "mailfromd"; for (i = 1; i <= argc; i++) xargv[i] = argv[i-1]; xargv[i++] = "--mtasim"; xargv[i++] = "--remove"; xargv[i++] = "--port"; xargv[i++] = milter_port; if (statedir_option) { xargv[i++] = "--state-directory"; xargv[i++] = tmpdir; } xargv[i] = 0; if (verbose) { fprintf (stderr, "executing "); for (i = 0; i < xargc; i++) fprintf (stderr, "%s ", xargv[i]); fprintf (stderr, "\n"); } execvp (xargv[0], xargv); mu_error (_("cannot execute mailfromd: %s"), mu_strerror (errno)); exit (127); } while (access (milter_port + 5, F_OK)) { struct timeval tv; if (got_sigchld) { mu_error (_("child process exited unexpectedly")); exit (EX_UNAVAILABLE); } tv.tv_sec = 0; tv.tv_usec = 5; select (0, NULL, NULL, NULL, &tv); } } #ifdef WITH_READLINE static char **mta_command_completion (char *cmd, int start, int end); static char *get_history_file_name (void); #endif static void version (FILE *stream, struct argp_state *state) { mailfromd_version("mtasim", stream); } int main (int argc, char **argv) { int status, index; mf_init_nls (); interactive = isatty(0); if (!program_invocation_short_name) program_invocation_short_name = argv[0]; argp_program_version_hook = version; if (argp_parse (&argp, argc, argv, 0, &index, NULL)) exit (EX_USAGE); priv_setup (); #ifdef WITH_READLINE if (interactive) { rl_readline_name = program_invocation_short_name; rl_attempted_completion_function = (CPPFunction*) mta_command_completion; read_history (get_history_file_name ()); } #endif if (trace_name) { char *mode = append ? "a" : "w"; trace = fopen (trace_name, mode); if (!trace) { mu_error (_("cannot open trace output: %s"), trace_name); return 1; } } argc -= index; argv += index; if (!mta_mode) { mu_error (_("use either --stdio or --daemon")); exit (EX_USAGE); } if (milter_port) { int rc; if (gacopyz_log_mask == 0) { gacopyz_log_mask = SMI_DEFAULT_LOG_MASK; if (verbose) gacopyz_log_mask |= SMI_LOG_MASK (SMI_LOG_DEBUG); } gacopyz_set_logger (gacopyz_stderr_log_printer); if (strcmp (milter_port, "auto") == 0) start_mailfromd (argc, argv); if (portspec_p (milter_port)) rc = gacopyz_srv_create (&gsrv, "Test", milter_port, gacopyz_log_mask); else rc = gacopyz_srv_create_X (&gsrv, milter_port, gacopyz_log_mask); if (rc != MI_SUCCESS) { mu_error (_("cannot create gacopyz server")); exit (EX_UNAVAILABLE); } if (milter_version_option) { unsigned long acts = SMFI_DEFAULT_ACTS, proto = SMFI_DEFAULT_PROT; if (milter_version_option == 2 || milter_version_option == 3) { acts = SMFI_V2_ACTS; proto = SMFI_V2_PROT; } gacopyz_srv_set_version (gsrv, milter_version_option); gacopyz_srv_set_protocol (gsrv, proto); gacopyz_srv_set_actions (gsrv, acts); } if (milter_proto_option) gacopyz_srv_set_protocol (gsrv, milter_proto_option); if (milter_acts_option) gacopyz_srv_set_actions (gsrv, milter_acts_option); gacopyz_srv_set_all_timeouts (gsrv, milter_timeouts); if (gacopyz_srv_connect (gsrv) != MI_SUCCESS) { mu_error (_("cannot connect to the milter using %s"), milter_port); exit (EX_UNAVAILABLE); } gacopyz_srv_negotiate (gsrv); } flush_deferred_defns (gsrv); #ifdef HAVE_TLS tls_init (); #endif status = mta_mode (); if (trace) fclose (trace); #ifdef WITH_READLINE if (interactive) write_history (get_history_file_name ()); #endif return status; } static void *in, *out; static const char * _def_strerror (int rc) { return rc == -1 ? _("end of file reached") : strerror (rc); } static int _def_write (void *sd, char *data, size_t size, size_t * nbytes) { int n = write ((int) sd, data, size); if (n != size) return errno; if (nbytes) *nbytes = n; return 0; } static int _def_read (void *sd, char *data, size_t size, size_t * nbytes) { int n = read ((int) sd, data, size); if (n && n != size) return errno; if (nbytes) *nbytes = n; return 0; } static int _def_close (void *sd) { return close ((int) sd); } int (*_mta_read) (void *, char *, size_t, size_t *) = _def_read; int (*_mta_write) (void *, char *, size_t, size_t *) = _def_write; int (*_mta_close) (void *) = _def_close; const char *(*_mta_strerror) (int) = _def_strerror; #ifdef HAVE_TLS static void _tls_cleanup_x509 (void) { if (x509_cred) gnutls_certificate_free_credentials (x509_cred); } static void generate_dh_params (void) { gnutls_dh_params_init (&dh_params); gnutls_dh_params_generate2 (dh_params, DH_BITS); } void tls_init (void) { if (!enable_tls ()) return; gnutls_global_init (); atexit (gnutls_global_deinit); gnutls_certificate_allocate_credentials (&x509_cred); atexit (_tls_cleanup_x509); if (tls_cafile) { int rc = gnutls_certificate_set_x509_trust_file (x509_cred, tls_cafile, GNUTLS_X509_FMT_PEM); if (rc < 0) { gnutls_perror (rc); return; } } if (tls_cert && tls_key) gnutls_certificate_set_x509_key_file (x509_cred, tls_cert, tls_key, GNUTLS_X509_FMT_PEM); generate_dh_params (); gnutls_certificate_set_dh_params (x509_cred, dh_params); } static ssize_t _tls_fd_pull (gnutls_transport_ptr fd, void *buf, size_t size) { int rc; do { rc = read ((int) fd, buf, size); } while (rc == -1 && errno == EAGAIN); return rc; } static ssize_t _tls_fd_push (gnutls_transport_ptr fd, const void *buf, size_t size) { int rc; do { rc = write ((int) fd, buf, size); } while (rc == -1 && errno == EAGAIN); return rc; } static const char * _tls_strerror (int rc) { return gnutls_strerror (rc); } static int _tls_write (void *sd, char *data, size_t size, size_t * nbytes) { int rc; do rc = gnutls_record_send (sd, data, size); while (rc == GNUTLS_E_INTERRUPTED || rc == GNUTLS_E_AGAIN); if (rc >= 0) { if (nbytes) *nbytes = rc; return 0; } return rc; } static int _tls_read (void *sd, char *data, size_t size, size_t * nbytes) { int rc = gnutls_record_recv (sd, data, size); if (rc >= 0) { if (nbytes) *nbytes = rc; return 0; } return rc; } static int _tls_close (void *sd) { if (sd) { gnutls_bye (sd, GNUTLS_SHUT_RDWR); gnutls_deinit (sd); } return 0; } static gnutls_session tls_session_init (void) { gnutls_session session = 0; int rc; gnutls_init (&session, GNUTLS_SERVER); gnutls_set_default_priority (session); gnutls_credentials_set (session, GNUTLS_CRD_CERTIFICATE, x509_cred); gnutls_certificate_server_set_request (session, GNUTLS_CERT_REQUEST); gnutls_dh_set_prime_bits (session, DH_BITS); gnutls_transport_set_pull_function (session, _tls_fd_pull); gnutls_transport_set_push_function (session, _tls_fd_push); gnutls_transport_set_ptr2 (session, (gnutls_transport_ptr) in, (gnutls_transport_ptr) out); rc = gnutls_handshake (session); if (rc < 0) { gnutls_deinit (session); gnutls_perror (rc); return 0; } return (gnutls_session) session; } void smtp_starttls (void) { gnutls_session session; smtp_reply (220, "Ready to start TLS"); session = tls_session_init (); if (session) { in = out = session; _mta_read = _tls_read; _mta_write = _tls_write; _mta_close = _tls_close; _mta_strerror = _tls_strerror; reset_capa ("STARTTLS"); } else smtp_reply (530, "TLS negotiation failed"); } #endif /* HAVE_TLS */ void smtp_send_string (char *str, size_t len) { int rc; if (!len) len = strlen (str); if (trace) fprintf (trace, "%s\n", str); rc = _mta_write (out, str, len, NULL); if (rc == 0 && str[len-1] != '\n') rc = _mta_write (out, "\r\n", 2, NULL); if (rc) { mu_error (_("Write failed: %s"), _mta_strerror (rc)); abort (); } check_expected_code (str); } void smtp_reply (int code, char *fmt, ...) { va_list ap; int cont = code & R_CONT ? '-' : ' '; static char obuf[512]; int n, rc; va_start (ap, fmt); n = snprintf (obuf, sizeof obuf, "%d%c", code & R_CODEMASK, cont); n += vsnprintf (obuf + n, sizeof obuf - n, fmt, ap); va_end (ap); if (trace) fprintf (trace, "%s\n", obuf); n += snprintf (obuf + n, sizeof obuf - n, "\r\n"); rc = _mta_write (out, obuf, n, NULL); if (rc) { mu_error (_("Write failed: %s"), _mta_strerror (rc)); abort (); } check_expected_code (obuf); } int get_input_line (char *buf, size_t bufsize) { int i, rc; #ifdef WITH_READLINE if (interactive) { char *p = readline (prompt); if (!p) return -1; strncpy (buf, p, bufsize); buf[bufsize - 1] = 0; strcat(buf, "\n"); free (p); add_history (buf); return strlen (buf); } #endif for (i = 0; i < bufsize - 1; i++) { size_t n; rc = _mta_read (in, buf + i, 1, &n); if (rc) { mu_error (_("Read failed: %s"), _mta_strerror (rc)); exit (EX_IOERR); } if (n == 0) break; if (buf[i] == '\n') { if (buf[i-1] == '\r') buf[i-1] = '\n'; else i++; break; } } buf[i] = 0; return i; } #define STATE_INIT 0 #define STATE_EHLO 1 #define STATE_MAIL 2 #define STATE_RCPT 3 #define STATE_HEADERS 4 #define STATE_DATA 5 #define STATE_QUIT 6 #define STATE_DOT 7 #define KW_EHLO 0 #define KW_HELO 1 #define KW_MAIL 2 #define KW_RCPT 3 #define KW_DATA 4 #define KW_HELP 5 #define KW_QUIT 6 #define KW_STARTTLS 7 #define KW_RSET 8 struct keyword { char *name; int code; }; static struct keyword kw[] = { { "EHLO", KW_EHLO }, { "HELO", KW_HELO }, { "MAIL", KW_MAIL }, { "RCPT", KW_RCPT }, { "DATA", KW_DATA }, { "HELP", KW_HELP }, { "QUIT", KW_QUIT }, { "HELP", KW_HELP }, #ifdef HAVE_TLS { "STARTTLS", KW_STARTTLS }, #endif { "RSET", KW_RSET }, { NULL }, }; int smtp_kw (const char *name) { int i; for (i = 0; kw[i].name != NULL; i++) if (strcasecmp (name, kw[i].name) == 0) return kw[i].code; return -1; } #ifdef WITH_READLINE #define HISTFILE_SUFFIX "_history" static char * get_history_file_name () { static char *filename = NULL; if (!filename) { size_t size; char *home = getenv ("HOME"); if (!home) { struct passwd *pw = getpwuid (getuid ()); if (!pw) return NULL; home = pw->pw_dir; } size = strlen (home) + 2 + strlen (rl_readline_name) + sizeof HISTFILE_SUFFIX; filename = xmalloc (size); strcpy (filename, home); strcat (filename, "/."); strcat (filename, rl_readline_name); strcat (filename, HISTFILE_SUFFIX); } return filename; } static char * mta_command_generator (const char *text, int state) { static int i, len; const char *name; if (!state) { i = 0; len = strlen (text); } while ((name = kw[i].name)) { i++; if (strncasecmp (name, text, len) == 0) return strdup (name); } return NULL; } char ** mta_command_completion (char *cmd, int start, int end) { if (start == 0) return rl_completion_matches (cmd, mta_command_generator); return NULL; } #endif char * skipws (char *str) { while (*str && c_isspace (*(u_char *) str)) str++; return str; } char * skipword (char *str) { while (*str && !c_isspace (*(u_char *) str)) str++; return str; } int argcv_split (char *buf, int *pargc, char ***pargv) { char *t; int i, argc = 0; char **argv; t = buf; do { argc++; t = skipws (t); } while (*t && (t = skipword (t))); argv = calloc (argc, sizeof (*argv)); for (i = 0, t = strtok (buf, " \t"); t; i++, t = strtok (NULL, " \t")) argv[i] = strdup (t); argv[i] = NULL; *pargc = argc - 1; *pargv = argv; return 0; } int argcv_free (int argc, char **argv) { while (--argc >= 0) if (argv[argc]) free (argv[argc]); free (argv); return 1; } char *mta_capa[] = { #ifdef HAVE_TLS "STARTTLS", #endif NULL }; void reset_capa (char *name) { int i; for (i = 0; mta_capa[i]; i++) if (strcmp (mta_capa[i], name) == 0) { mta_capa[i] = NULL; break; } } static int tempfail; static int discard; static char *nullmailer; unsigned nrcpt = 0; unsigned nbadrcpts = 0; void smtp_ehlo (int extended, char *domain) { int i; if (gsrv) { gacopyz_srv_define_macro (gsrv, "s", domain); switch (gacopyz_srv_helo (gsrv, domain)) { case SMFIR_REPLYCODE: { char *msg; size_t size; gacopyz_srv_reply (gsrv, &msg, &size); nullmailer = strdup (msg); break; } case SMFIR_REJECT: nullmailer = strdup ("Command rejected"); break; case SMFIR_TEMPFAIL: tempfail = 1; break; case SMFIR_DISCARD: case SMFIR_SHUTDOWN: break; } } if (!extended) { smtp_reply (250, "pleased to meet you"); return; } smtp_reply (R_CONT | 250, "pleased to meet you"); for (i = 0; mta_capa[i]; i++) smtp_reply (R_CONT | 250, "%s", mta_capa[i]); smtp_reply (250, "HELP"); } void smtp_help (void) { int i; smtp_reply (250|R_CONT, "mtasim (%s); supported SMTP commands:", PACKAGE_STRING); for (i = 0; kw[i].name; i++) smtp_reply (250|R_CONT, " %s", kw[i].name); if (milter_port) { smtp_reply (250|R_CONT, "Supported administrative commands:"); shell_help (); } smtp_reply (250, "End of help output"); } #define MSG_TEMPFAIL "451 4.3.2 Please try again later" #define MSG_REJECT "550 5.7.1 Command rejected" #define MSG_SHUTDOWN "421 4.7.0 bitbucket closing connection" int parse_email_addr(char *arg, char **psender, char **addr, char **host) { size_t len; char *p = arg, *q; if (*p == '<') { len = strlen (p); if (p[len-1] != '>') return 1; p++; *psender = malloc (len - 1); if (*psender) { memcpy (*psender, p, len - 2); (*psender)[len - 2] = 0; } } else *psender = strdup (arg); if (!*psender) return 1; p = *psender; q = strchr (p, '@'); if (q) len = q - p; else len = strlen (p); *addr = malloc (len + 1); if (!*addr) { free (*psender); return 1; } memcpy (*addr, p, len); (*addr)[len] = 0; if (q) q++; else q = "localhost"; *host = strdup (q); if (!*host) { free (*psender); free (*addr); return 1; } return 0; } static int process_gacopyz_reply (char *sname, char *arg, int rc, int *state) { switch (rc) { case SMFIR_REPLYCODE: { char *msg; size_t size; gacopyz_srv_reply (gsrv, &msg, &size); if (verbose) fprintf (stderr, "Gacopyz: %s=%s, reply=%s", sname, arg, msg); smtp_send_string (msg, 0); return 1; } case SMFIR_REJECT: if (verbose) fprintf (stderr, "Gacopyz: %s=%s, reply=%s", sname, arg, MSG_REJECT); smtp_send_string (MSG_REJECT, 0); return 1; case SMFIR_DISCARD: if (verbose) fprintf (stderr, "Gacopyz: %s=%s, discard", sname, arg); discard = 1; return 1; case SMFIR_TEMPFAIL: if (verbose) fprintf (stderr, "Gacopyz: %s=%s, reply=%s", sname, arg, MSG_TEMPFAIL); smtp_send_string (MSG_TEMPFAIL, 0); return 1; case SMFIR_SHUTDOWN: if (verbose) fprintf (stderr, "Gacopyz: %s=%s, reply=%s", sname, arg, MSG_SHUTDOWN); smtp_send_string (MSG_SHUTDOWN, 0); *state = STATE_QUIT; return 1; } return 0; } static int process_data_reply (char *sname, char *arg, int rc, int *state, char **reply) { switch (rc) { case SMFIR_REPLYCODE: { char *msg; size_t size; gacopyz_srv_reply (gsrv, &msg, &size); if (verbose) fprintf (stderr, "Gacopyz: %s=%s, reply=%s", sname, arg, msg); *reply = strdup (msg); return 1; } case SMFIR_REJECT: if (verbose) fprintf (stderr, "Gacopyz: %s=%s, reply=%s", sname, arg, MSG_REJECT); *reply = strdup (MSG_REJECT); return 1; case SMFIR_DISCARD: if (verbose) fprintf (stderr, "Gacopyz: %s=%s, discard", sname, arg); discard = 1; return 1; case SMFIR_TEMPFAIL: if (verbose) fprintf (stderr, "Gacopyz: %s=%s, reply=%s", sname, arg, MSG_TEMPFAIL); *reply = strdup (MSG_TEMPFAIL); return 1; case SMFIR_SHUTDOWN: if (verbose) fprintf (stderr, "Gacopyz: %s=%s, reply=%s", sname, arg, MSG_SHUTDOWN); smtp_send_string (MSG_SHUTDOWN, 0); *state = STATE_QUIT; return 1; } return 0; } /* Check if (*PARGV)[1] begins with the string PFX, followed by ':'. Return 0 if so, 1 otherwise. If any characters follow the semicolon, reformat *PARGV so that [1] contains only PFX:, [2] contains the characters in question, and the rest of entries after [2] is properly reindexed. */ int check_address_command(const char *pfx, int *pargc, char ***pargv) { int argc = *pargc; char **argv = *pargv; int pfxlen = strlen (pfx); int arglen = strlen (argv[1]); if (argc >= 2 && arglen > pfxlen && strncasecmp (argv[1], pfx, pfxlen) == 0 && argv[1][pfxlen] == ':') { if (arglen > pfxlen + 1) { argc++; argv = xrealloc (argv, (argc + 1) * sizeof (argv[0])); memmove (&argv[2], &argv[1], (argc - 1) * sizeof argv[1]); argv[2] = xstrdup (argv[1] + pfxlen + 1); argv[1][pfxlen + 1] = 0; *pargc = argc; *pargv = argv; } return 0; } return 1; } static void smtp_rcpt (int *pargc, char ***pargv, int *state) { nrcpt++; if (check_address_command("to", pargc, pargv) == 0) { char **argv = *pargv; int rc; char *sender; char *addr; char *host; if (parse_email_addr (argv[2], &sender, &addr, &host)) { smtp_reply (553, "5.0.0 recipient address syntactically incorrect"); nbadrcpts++; return; } if (gsrv) { update_nrcpts ("nrcpts", nrcpt); update_nrcpts ("nbadrcpts", nbadrcpts); gacopyz_srv_define_macro (gsrv, "rcpt_host", host); gacopyz_srv_define_macro (gsrv, "rcpt_addr", addr); rc = gacopyz_srv_envrcpt (gsrv, argv + 2); free (sender); free (addr); free (host); if (process_gacopyz_reply ("to", argv[2], rc, state)) return; } else { free (sender); free (addr); free (host); } smtp_reply (250, "Recipient OK"); *state = STATE_RCPT; } else smtp_reply (501, "Syntax error"); } int process_header (struct obstack *stk, size_t header_size, int *state, char **reply) { int status = 0; char *hn, *hv; obstack_1grow (stk, 0); hn = obstack_finish (stk); hv = strchr (hn, ':'); if (hv) { int rc, len; *hv++ = 0; while (*hv && c_isspace (*hv)) hv++; len = strlen (hv); if (len > 0 && hv[len - 1] == '\n') { if (--len > 0 && hv[len - 1] == '\r') len--; hv[len] = 0; } rc = gacopyz_srv_header(gsrv, hn, hv); status = process_data_reply ("cmd", "header", rc, state, reply); } obstack_free (stk, hn); return status; } struct body_buf { char *bufptr; size_t level; }; int send_body (int state, char *ptr, size_t len, struct body_buf *buf) { while (len > 0) { size_t s = max_body_chunk - buf->level; if (s > len) s = len; memcpy (buf->bufptr + buf->level, ptr, s); ptr += s; len -= s; buf->level += s; if (buf->level == max_body_chunk) { char *datareply = NULL; int rc = gacopyz_srv_body (gsrv, buf->bufptr, buf->level); buf->level = 0; if (process_data_reply ("cmd", "data", rc, &state, &datareply)) { if (datareply) { smtp_send_string (datareply, 0); free (datareply); datareply = 0; } } } } return state; } void smtp (void) { int state; char buf[128]; struct obstack stk; int stk_init = 0; size_t header_size = 0; char *datareply = NULL; struct body_buf body_buf; body_buf.bufptr = xmalloc (max_body_chunk); body_buf.level = 0; if (milter_port) { smtp_reply (220|R_CONT, "mtasim (%s) ready", PACKAGE_STRING); smtp_reply (220, "Connected to milter %s", milter_port); } else smtp_reply (220, "mtasim (%s) ready", PACKAGE_STRING); for (state = STATE_INIT; state != STATE_QUIT;) { int argc; char **argv; int kw, len; if (get_input_line (buf, sizeof buf) <= 0) { state = STATE_QUIT; continue; } len = strlen (buf); if (trace) fprintf (trace, "%s", buf); if (state != STATE_HEADERS && state != STATE_DATA) { while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) len--; buf[len] = 0; argcv_split (buf, &argc, &argv); if (argc == 0) continue; if (argv[0][0] == '\\' && milter_port) { shell (argc, argv); argcv_free (argc, argv); continue; } kw = smtp_kw (argv[0]); switch (kw) { case KW_QUIT: state = STATE_QUIT; smtp_reply (221, "Done"); argcv_free (argc, argv); continue; case KW_HELP: smtp_help (); continue; case KW_RSET: state = STATE_INIT; free (nullmailer); nullmailer = NULL; tempfail = 0; nrcpt = 0; nbadrcpts = 0; discard = 0; header_size = 0; free (datareply); datareply = NULL; if (stk_init) obstack_free (&stk, NULL); stk_init = 0; smtp_reply(250, "2.0.0 Reset state"); continue; } } else if (len > 0 && buf[len - 1] == '\n' && buf[len - 2] == '\r') { buf[len - 2] = '\n'; len--; } buf[len] = 0; if (state != STATE_INIT) { if (nullmailer) { smtp_send_string (nullmailer, 0); continue; } if (tempfail) { smtp_send_string (MSG_TEMPFAIL, 0); continue; } } switch (state) { case STATE_INIT: switch (kw) { case KW_EHLO: case KW_HELO: if (argc == 2) { smtp_ehlo (kw == KW_EHLO, argv[1]); state = STATE_EHLO; } else smtp_reply (501, "%s requires domain address", argv[0]); break; default: smtp_reply (503, "Polite people say HELO first"); break; } break; case STATE_EHLO: switch (kw) { case KW_EHLO: if (argc == 2) { smtp_ehlo (1, argv[1]); } else smtp_reply (501, "%s requires domain address", argv[0]); break; case KW_MAIL: if (check_address_command("from", &argc, &argv) == 0) { int rc; char *sender; char *addr; char *host; if (parse_email_addr (argv[2], &sender, &addr, &host)) { smtp_reply (553, "5.0.0 sender address syntactically incorrect"); break; } if (gsrv) { gacopyz_srv_define_macro (gsrv, "f", sender); gacopyz_srv_define_macro (gsrv, "r", "SMTP"); /* FIXME: gacopyz_srv_define_macro (gsrv, "s", host); */ gacopyz_srv_define_macro (gsrv, "ntries", "0"); gacopyz_srv_define_macro (gsrv, "nrcpts", "0"); gacopyz_srv_define_macro (gsrv, "nbadrcpts", "0"); /* FIXME */ gacopyz_srv_define_macro (gsrv, "mail_mailer", "local"); gacopyz_srv_define_macro (gsrv, "mail_host", host); gacopyz_srv_define_macro (gsrv, "mail_addr", addr); gacopyz_srv_define_macro (gsrv, "mail_from", sender); rc = gacopyz_srv_envfrom (gsrv, argv + 2); free (sender); free (addr); free (host); if (process_gacopyz_reply ("from", argv[2], rc, &state)) continue; } else { free (sender); free (addr); free (host); } smtp_reply (250, "Sender OK"); state = STATE_MAIL; } else smtp_reply (501, "Syntax error"); break; #ifdef HAVE_TLS case KW_STARTTLS: smtp_starttls (); break; #endif default: smtp_reply (503, "Need MAIL command"); } break; case STATE_MAIL: switch (kw) { case KW_MAIL: smtp_reply (503, "5.5.0 Sender already specified"); break; case KW_RCPT: smtp_rcpt (&argc, &argv, &state); break; default: smtp_reply (503, "Need RCPT command"); } break; case STATE_RCPT: switch (kw) { case KW_RCPT: smtp_rcpt (&argc, &argv, &state); break; case KW_DATA: if (gsrv) { int rc = gacopyz_srv_data (gsrv); if (process_gacopyz_reply ("cmd", "data", rc, &state)) continue; } smtp_reply (354, "Enter mail, end with \".\" on a line by itself"); if (!stk_init) { obstack_init (&stk); stk_init = 1; header_size = 0; } state = STATE_HEADERS; break; default: smtp_reply (501, "Syntax error"); } break; case STATE_HEADERS: if (strcmp (buf, "\n") == 0) { if (gsrv) { int rc; if (header_size) { rc = process_header (&stk, header_size, &state, &datareply); header_size = 0; if (rc) continue; } rc = gacopyz_srv_eoh (gsrv); if (process_data_reply ("cmd", "eoh", rc, &state, &datareply)) continue; } body_buf.level = 0; state = STATE_DATA; } else if (buf[0] == ' ' || buf[0] == '\t') { if (gsrv) { obstack_grow (&stk, buf, len - 1); obstack_grow (&stk, "\r\n", 2); header_size += len + 1; } } else if (strcmp (buf, ".") == 0) { if (gsrv) { int rc = gacopyz_srv_eom (gsrv, NULL, 0); process_data_reply ("cmd", "eom", rc, &state, &datareply); /* FIXME: Clear macro table, except for the entries from command line */ gacopyz_srv_clear_macros (gsrv); } if (datareply) { smtp_send_string (datareply, 0); free (datareply); datareply = 0; } else smtp_reply (250, "Mail accepted for delivery"); state = STATE_EHLO; } else if (gsrv) { if (header_size) { int rc = process_header (&stk, header_size, &state, &datareply); header_size = 0; if (rc) continue; } obstack_grow (&stk, buf, len - 1); obstack_grow (&stk, "\r\n", 2); header_size += len + 1; } break; case STATE_DATA: if (strcmp (buf, ".\n") == 0) { if (gsrv) { int rc; rc = gacopyz_srv_eom (gsrv, body_buf.bufptr, body_buf.level); process_data_reply ("cmd", "eom", rc, &state, &datareply); /* FIXME: Clear macro table, except for the entries from the command line */ gacopyz_srv_clear_macros (gsrv); } if (datareply) { smtp_send_string (datareply, 0); free (datareply); datareply = 0; } else smtp_reply (250, "Mail accepted for delivery"); state = STATE_EHLO; } else if (gsrv) { state = send_body (state, buf, len - 1, &body_buf); if (state == STATE_DATA) state = send_body (state, "\r\n", 2, &body_buf); } break; } } if (gsrv) { gacopyz_srv_quit (gsrv); gacopyz_srv_close (gsrv); gacopyz_srv_destroy (&gsrv); } free (body_buf.bufptr); } int mta_daemon () { int on = 1; struct sockaddr_in address; int fd; fd = socket (PF_INET, SOCK_STREAM, 0); if (fd < 0) { perror ("socket"); return 1; } setsockopt (fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof (on)); memset (&address, 0, sizeof (address)); address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; if (port) address.sin_port = htons (port); else address.sin_port = 0; if (bind (fd, (struct sockaddr *) &address, sizeof (address)) < 0) { close (fd); perror ("bind"); return 1; } if (!port) { int len = sizeof (address); int rc = getsockname (fd, (struct sockaddr *) &address, &len); if (rc) { close (fd); mu_error ("getsockname: %s", mu_strerror (errno)); return 1; } port = ntohs (address.sin_port); printf ("%d\n", port); fclose (stdout); } listen (fd, 5); while (1) { fd_set rfds; struct sockaddr_in his_addr; int sfd, len, status; FD_ZERO (&rfds); FD_SET (fd, &rfds); status = select (fd + 1, &rfds, NULL, NULL, NULL); if (status == -1) { if (errno == EINTR) continue; perror ("select"); return 1; } len = sizeof (his_addr); if ((sfd = accept (fd, (struct sockaddr *) &his_addr, &len)) < 0) { perror ("accept"); return 1; } in = out = (void *) fd; smtp (); break; } return 0; } int mta_stdio () { in = (void *) fileno (stdin); out = (void *) fileno (stdout); smtp (); return 0; } int define_macro (char *arg) { char *p; p = strchr (arg, '='); if (!p) return 1; *p++ = 0; gacopyz_srv_define_macro (gsrv, arg, p); return 0; } void undefine_macro (char *arg) { gacopyz_srv_del_macro (gsrv, arg); } void undefine_all_macros () { gacopyz_srv_clear_macros (gsrv); } void list_macro (char *name) { const char *val; if (gacopyz_srv_find_macro (gsrv, name, &val) == MI_SUCCESS) smtp_reply (220, "%s=%s", name, val); else smtp_reply (220, "%s undefined", name); } struct macro_itr { size_t num; size_t count; }; int macprint (const char *name, const char *value, void *data) { struct macro_itr *itr = data; int code = 220; itr->num++; if (itr->num < itr->count) code |= R_CONT; smtp_reply (code, "%s=%s", name, value); return 0; } void list_all_macros () { struct macro_itr itr; itr.num = itr.count = 0; gacopyz_srv_count_macros (gsrv, &itr.count); if (itr.count == 0) smtp_reply (220, "No macros defined"); else gacopyz_srv_iterate_macros (gsrv, macprint, &itr); } void shell_help () { static char *hstr[] = { " \\Dname=value [name=value...] Define Sendmail macros", " \\Ecode Expect given SMTP reply code", " \\L[name] [name...] List macros", " \\Uname [name...] Undefine Sendmail macros", }; #define hcount (sizeof (hstr) / sizeof (hstr[0])) int i; for (i = 0; i < hcount; i++) smtp_reply (((i < hcount-1) ? R_CONT : 0) | 250, "%s", hstr[i]); } void shell (int argc, char **argv) { switch (argv[0][1]) { case 'd': case 'D': { int rc = 0; if (argv[0][2]) rc = define_macro (&argv[0][2]); while (rc == 0 && --argc) rc = define_macro (*++argv); if (rc) smtp_reply (502, "Malformed administrative command"); } break; case 'u': case 'U': if (argv[0][2]) { undefine_macro (&argv[0][2]); while (--argc) undefine_macro (*++argv); } else if (argc > 1) while (--argc) undefine_macro (*++argv); else undefine_all_macros (); break; case 'l': case 'L': if (argv[0][2]) { list_macro (&argv[0][2]); while (--argc) list_macro (*++argv); } else if (argc > 1) while (--argc) list_macro (*++argv); else list_all_macros (); break; case 'e': case 'E': if (set_expected_code (argv[0] + 2)) smtp_reply (502, "Invalid SMPT code: %s", argv[0] + 2); break; case '?': shell_help (); break; default: smtp_reply (502, "Unknown administrative command"); break; } } /* Local Variables: c-file-style: "gnu" End: */ /* EOF */