diff options
author | David Bremner <david@tethera.net> | 2017-07-31 17:47:09 -0400 |
---|---|---|
committer | David Bremner <david@tethera.net> | 2017-07-31 17:47:09 -0400 |
commit | 4dc65cebbaa3436490ede470b1cbdb58cda83e06 (patch) | |
tree | d7f78cb95a9fa1868e608f02653d33f2075c889d /src | |
parent | 1547050d23739476ebd1c88d824c72432f06d4dd (diff) | |
parent | 6567de3d2f5936692fd2e272688386efa95f11fb (diff) |
Merge tag '2.0' into upstream
Version 2.0
Diffstat (limited to 'src')
-rw-r--r-- | src/Makefile.am | 12 | ||||
-rw-r--r-- | src/address-main.cc | 21 | ||||
-rw-r--r-- | src/dsn.cc | 223 | ||||
-rw-r--r-- | src/inject.cc | 139 | ||||
-rw-r--r-- | src/mailq.cc | 16 | ||||
-rw-r--r-- | src/queue.cc | 49 | ||||
-rw-r--r-- | src/send.cc | 377 | ||||
-rw-r--r-- | src/sendmail.cc | 13 | ||||
-rw-r--r-- | src/smtpd.cc | 69 |
9 files changed, 615 insertions, 304 deletions
diff --git a/src/Makefile.am b/src/Makefile.am index 5f7898a..4a742c2 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1,5 +1,6 @@ bin_PROGRAMS = \ mailq \ + nullmailer-dsn \ nullmailer-inject \ nullmailer-smtpd sbin_PROGRAMS = \ @@ -9,19 +10,22 @@ sbin_PROGRAMS = \ #noinst_PROGRAMS = address -INCLUDES = -I../lib -I../lib/cli++ +AM_CPPFLAGS = -I$(top_srcdir)/lib mailq_SOURCES = mailq.cc mailq_LDADD = ../lib/libnullmailer.a +nullmailer_dsn_SOURCES = dsn.cc +nullmailer_dsn_LDADD = ../lib/libnullmailer.a ../lib/cli++/libcli++.a + nullmailer_inject_SOURCES = inject.cc -nullmailer_inject_LDADD = ../lib/cli++/libcli++.a ../lib/libnullmailer.a +nullmailer_inject_LDADD = ../lib/libnullmailer.a ../lib/cli++/libcli++.a nullmailer_queue_SOURCES = queue.cc -nullmailer_queue_LDADD = ../lib/libnullmailer.a +nullmailer_queue_LDADD = ../lib/libnullmailer.a ../lib/cli++/libcli++.a nullmailer_send_SOURCES = send.cc -nullmailer_send_LDADD = ../lib/libnullmailer.a +nullmailer_send_LDADD = ../lib/libnullmailer.a ../lib/cli++/libcli++.a nullmailer_smtpd_SOURCES = smtpd.cc nullmailer_smtpd_LDADD = ../lib/libnullmailer.a diff --git a/src/address-main.cc b/src/address-main.cc new file mode 100644 index 0000000..11f29f7 --- /dev/null +++ b/src/address-main.cc @@ -0,0 +1,21 @@ +#include "config.h" +#include "mystring.h" +#include "fdbuf.h" +#include "address.h" + +int main(int argc, char* argv[]) +{ + for(int i = 1; i < argc; i++) { + mystring s(argv[i]); + mystring l; + if(!parse_addresses(s, l)) { + fout.writeln("Parsing failed."); + } + else { + fout.write(l); + fout.write("To: "); + fout.writeln(s); + } + } + return 0; +} diff --git a/src/dsn.cc b/src/dsn.cc new file mode 100644 index 0000000..dfcaa1c --- /dev/null +++ b/src/dsn.cc @@ -0,0 +1,223 @@ +// nullmailer -- a simple relay-only MTA +// Copyright (C) 2016 Bruce Guenter <bruce@untroubled.org> +// +// 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 2 of the License, 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, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// You can contact me at <bruce@untroubled.org>. There is also a mailing list +// available to discuss this package. To subscribe, send an email to +// <nullmailer-subscribe@lists.untroubled.org>. + +#include "config.h" +#include <sys/types.h> +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <time.h> +#include <unistd.h> +#include "cli++/cli++.h" +#include "itoa.h" +#include "defines.h" +#include "list.h" +#include "mystring/mystring.h" +#include "fdbuf/fdbuf.h" +#include "canonicalize.h" +#include "configio.h" +#include "hostname.h" +#include "makefield.h" + +typedef list<mystring> slist; + +static time_t opt_timestamp = 0; +static time_t opt_last_attempt = 0; +static time_t opt_retry_until = 0; +static const char* opt_envelope_id = 0; +static const char* opt_status = 0; +static const char* opt_remote = 0; +static const char* opt_diagnostic_code = 0; +static int opt_lines = -1; +static bool opt_ddn = false; + +const char* cli_program = "nullmailer-dsn"; +const char* cli_help_prefix = +"Reformat a queued message into a delivery status notification (DSN)\n"; +const char* cli_help_suffix = +"\n" +"The status code must be in the form 4.#.# or 5.#.#. If the status\n" +"code starts with 4, a delivery delay notification is generated.\n"; +const char* cli_args_usage = "status-code < message"; +const int cli_args_min = 1; +const int cli_args_max = 1; +cli_option cli_options[] = { + { 0, "diagnostic-code", cli_option::string, 0, &opt_diagnostic_code, + "Diagnostic code message", 0 }, + { 0, "envelope-id", cli_option::string, 0, &opt_envelope_id, + "Original envelope ID", 0 }, + { 0, "last-attempt", cli_option::uinteger, 0, &opt_last_attempt, + "UNIX timestamp of the last attempt", + "access time on the input message" }, + { 0, "orig-timestamp", cli_option::uinteger, 0, &opt_timestamp, + "UNIX timestamp on the original message", + "ctime on the input message" }, + { 0, "remote", cli_option::string, 0, &opt_remote, + "Name of remote server", 0 }, + { 0, "retry-until", cli_option::uinteger, 0, &opt_retry_until, + "UNIX timestamp of the (future) final attempt", 0 }, + { 0, "max-lines", cli_option::integer, 0, &opt_lines, + "Maximum number of lines of the original message to copy", + "the whole message" }, + {0, 0, cli_option::flag, 0, 0, 0, 0} +}; + +#define die1sys(MSG) do{ fout << "nullmailer-dsn: " << MSG << strerror(errno) << endl; exit(111); }while(0) +#define die1(MSG) do{ fout << "nullmailer-dsn: " << MSG << endl; exit(111); }while(0) + +static mystring sender; +static mystring bounceto; +static mystring doublebounceto; +static mystring line; +static slist recipients; + +static mystring idhost; +static const mystring boundary = make_boundary(); + +int cli_main(int, char* argv[]) +{ + struct stat msgstat; + if (fstat(0, &msgstat) < 0) + die1sys("Could not stat the source message"); + if (opt_timestamp == 0) + opt_timestamp = msgstat.st_ctime; + if (opt_last_attempt == 0) + opt_last_attempt = msgstat.st_atime; + opt_status = argv[0]; + if ((opt_status[0] != '4' && opt_status[0] != '5') + || opt_status[1] != '.' + || !isdigit(opt_status[2]) + || opt_status[3] != '.' + || !isdigit(opt_status[4]) + || opt_status[5] != '\0') + die1("Status must be in the format 4.#.# or 5.#.#"); + opt_ddn = opt_status[0] == '4'; + if (opt_lines < 0) + config_readint("bouncelines", opt_lines); + + if (!config_read("doublebounceto", doublebounceto) + || !doublebounceto) + config_read("adminaddr", doublebounceto); + read_hostnames(); + if (!config_read("idhost", idhost)) + idhost = me; + else + canonicalize(idhost); + config_read("bounceto", bounceto); + + if (!fin.getline(sender)) + die1sys("Could not read sender address from message: "); + if (!sender && !doublebounceto) + die1("Nowhere to send double bounce"); + while (fin.getline(line)) { + if (!line) + break; + recipients.append(line); + } + if (recipients.count() == 0) + die1("No recipients were read from message"); + + if (!!sender) + // Bounces either go to the sender or bounceto, if configured + fout << '\n' << (!!bounceto ? bounceto : sender); + else + fout << "#@[]\n" << doublebounceto; + + fout << "\n" + "\n" + "From: Message Delivery Subsystem <MAILER-DAEMON@" << me << ">\n" + "To: <" << sender << ">\n" + "Subject: Returned mail: Could not send message\n" + "Date: " << make_date() << "\n" + "Message-Id: " << make_messageid(idhost) << "\n" + "MIME-Version: 1.0\n" + "Content-Type: multipart/report; report-type=delivery-status;\n" + "\tboundary=\"" << boundary << "\"\n"; + + /* Human readable text portion */ + fout << "\n" + "--" << boundary << "\n" + "Content-Type: text/plain; charset=us-ascii\n" + "\n" + "This is the nullmailer delivery system. The message attached below\n" + << (opt_ddn + ? "has not been" + : "could not be") + << " delivered to one or more of the intended recipients:\n" + "\n"; + for (slist::const_iter recipient(recipients); recipient; recipient++) + fout << "\t<" << (*recipient) << ">\n"; + if (opt_ddn) { + if (opt_retry_until > 0) + fout << "\nDelivery will continue to be attempted until " + << make_date(opt_retry_until) << '\n'; + fout << "\n" + "A final delivery status notification will be generated if delivery\n" + "proves to be impossible within the configured time limit.\n"; + } + + /* delivery-status portion */ + fout << "\n" + "--" << boundary << "\n" + "Content-Type: message/delivery-status\n" + "\n" + "Reporting-MTA: x-local-hostname; " << me << "\n" + "Arrival-Date: " << make_date(opt_timestamp) << "\n"; + if (opt_envelope_id != 0) + fout << "Original-Envelope-Id: " << opt_envelope_id << '\n'; + + for (slist::const_iter recipient(recipients); recipient; recipient++) { + fout << "\n" + "Final-Recipient: rfc822; " << (*recipient) << "\n" + "Action: " << (opt_ddn ? "delayed": "failed") << "\n" + "Status: " << opt_status << "\n" + "Last-Attempt-Date: " << make_date(opt_last_attempt) << '\n'; + if (opt_remote != 0) + fout << "Remote-MTA: dns; " << opt_remote << '\n'; + if (opt_diagnostic_code != 0) + fout << "Diagnostic-Code: " << opt_diagnostic_code << '\n'; + if (opt_ddn and opt_retry_until > 0) + fout << "Will-Retry-Until: " << make_date(opt_retry_until) << '\n'; + } + + // Copy the message + fout << "\n" + "--" << boundary << "\n" + "Content-Type: message/rfc822\n" + "\n"; + // Copy the header + while (fin.getline(line) && !!line) + fout << line << '\n'; + // Optionally copy the body + if (opt_lines) { + fout << '\n'; + for (int i = 0; (opt_lines < 0 || i < opt_lines) && fin.getline(line); i++) + fout << line << '\n'; + } + + fout << "\n" + "--" << boundary << "--\n"; + + return 0; +} diff --git a/src/inject.cc b/src/inject.cc index 6bd5c80..21bf729 100644 --- a/src/inject.cc +++ b/src/inject.cc @@ -1,5 +1,5 @@ // nullmailer -- a simple relay-only MTA -// Copyright (C) 2012 Bruce Guenter <bruce@untroubled.org> +// Copyright (C) 2016 Bruce Guenter <bruce@untroubled.org> // // 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 @@ -38,6 +38,7 @@ #include "configio.h" #include "cli++/cli++.h" #include "makefield.h" +#include "forkexec.h" enum { use_args, use_both, use_either, use_header @@ -71,15 +72,14 @@ cli_option cli_options[] = { {0, 0, cli_option::flag, 0, 0, 0, 0} }; -#define fail(MSG) do{ fout << "nullmailer-inject: " << MSG << endl; return false; }while(0) -#define fail_sys(MSG) do{ fout << "nullmailer-inject: " << MSG << ": " << strerror(errno) << endl; return false; }while(0) -#define bad_hdr(LINE,MSG) do{ header_has_errors = true; fout << "nullmailer-inject: Invalid header line:\n " << LINE << "\n " MSG << endl; }while(0) +#define fail(MSG) do{ ferr << "nullmailer-inject: " << MSG << endl; return false; }while(0) +#define fail_sys(MSG) do{ ferr << "nullmailer-inject: " << MSG << ": " << strerror(errno) << endl; return false; }while(0) +#define bad_hdr(LINE,MSG) do{ header_has_errors = true; ferr << "nullmailer-inject: Invalid header line:\n " << LINE << "\n " MSG << endl; }while(0) typedef list<mystring> slist; // static bool do_debug = false; static mystring cur_line; -static mystring nqueue; /////////////////////////////////////////////////////////////////////////////// // Configuration @@ -90,19 +90,12 @@ extern void canonicalize(mystring& domain); void read_config() { - const char* env; mystring tmp; read_hostnames(); if(!config_read("idhost", idhost)) idhost = me; else canonicalize(idhost); - if ((env = getenv("NULLMAILER_QUEUE")) != 0) - nqueue = env; - else { - nqueue = SBIN_DIR; - nqueue += "/nullmailer-queue"; - } } /////////////////////////////////////////////////////////////////////////////// @@ -111,6 +104,7 @@ void read_config() static slist recipients; static mystring sender; static bool use_header_recips = true; +static bool use_header_sender = true; void parse_recips(const mystring& list) { @@ -175,7 +169,8 @@ struct header_field return true; if(is_resent) { if(!header_is_resent) { - sender = ""; + if(use_header_sender) + sender = ""; if(use_header_recips) recipients.empty(); } @@ -197,7 +192,7 @@ struct header_field parse_recips(list); } else if(is_sender) { - if(is_resent == header_is_resent && !sender) + if(is_resent == header_is_resent && use_header_sender) parse_sender(list); } } @@ -296,7 +291,7 @@ void setup_from() if(!shost) shost = host; canonicalize(shost); - if(!sender) + if(use_header_sender && !sender) sender = suser + "@" + shost; } @@ -425,101 +420,62 @@ bool fix_header() /////////////////////////////////////////////////////////////////////////////// // Message sending /////////////////////////////////////////////////////////////////////////////// -static fdobuf* nqpipe = 0; -static pid_t pid = 0; - -void exec_queue() -{ - execl(nqueue.c_str(), nqueue.c_str(), NULL); - fout << "nullmailer-inject: Could not exec " << nqueue << ": " - << strerror(errno) << endl; - exit(1); -} - -bool start_queue() -{ - int pipe1[2]; - if(pipe(pipe1) == -1) - fail_sys("Could not create pipe to nullmailer-queue"); - fout.flush(); - pid = fork(); - if(pid == -1) - fail_sys("Could not fork"); - if(pid == 0) { - close(pipe1[1]); - close(0); - dup2(pipe1[0], 0); - exec_queue(); - } - else { - close(pipe1[0]); - nqpipe = new fdobuf(pipe1[1], true); - } - return true; -} - -bool send_env() +bool send_env(fdobuf& out) { - if(!(*nqpipe << sender << "\n")) + if(!(out << sender << "\n")) fail("Error sending sender to nullmailer-queue."); for(slist::iter iter(recipients); iter; iter++) - if(!(*nqpipe << *iter << "\n")) + if(!(out << *iter << "\n")) fail("Error sending recipients to nullmailer-queue."); - if(!(*nqpipe << endl)) + if(!(out << endl)) fail("Error sending recipients to nullmailer-queue."); return true; } -bool send_header() +bool send_header(fdobuf& out) { for(slist::iter iter(headers); iter; iter++) - if(!(*nqpipe << *iter << "\n")) + if(!(out << *iter << "\n")) fail("Error sending header to nullmailer-queue."); - if(!(*nqpipe << endl)) + if(!(out << endl)) fail("Error sending header to nullmailer-queue."); return true; } -bool send_body() +bool send_body(fdobuf& out) { - if(!(*nqpipe << cur_line) || - !fdbuf_copy(fin, *nqpipe)) + if(!(out << cur_line) || + !fdbuf_copy(fin, out)) fail("Error sending message body to nullmailer-queue."); return true; } -bool wait_queue() +bool send_message_stdout() { - if(!nqpipe->close()) - fail("Error closing pipe to nullmailer-queue."); - int status; - if(waitpid(pid, &status, 0) == -1) - fail("Error catching the return value from nullmailer-queue."); - if(WIFEXITED(status)) { - status = WEXITSTATUS(status); - if(status) - fail("nullmailer-queue failed."); - else - return true; - } - else - fail("nullmailer-queue crashed or was killed."); + if(show_envelope) + send_env(fout); + send_header(fout); + send_body(fout); + return true; +} + +bool send_message_nqueue() +{ + queue_pipe nq; + autoclose wfd = nq.start(); + if (wfd < 0) + return false; + fdobuf nqout(wfd); + if (!send_env(nqout) || !send_header(nqout) || !send_body(nqout)) + return false; + nqout.flush(); + wfd.close(); + return nq.wait(); } bool send_message() { - if(show_message) { - nqpipe = &fout; - if(show_envelope) - send_env(); - send_header(); - send_body(); - return true; - } - else - return start_queue() && - send_env() && send_header() && send_body() && - wait_queue(); + return show_message ? send_message_stdout() : send_message_nqueue(); } /////////////////////////////////////////////////////////////////////////////// @@ -550,11 +506,14 @@ bool parse_args(int argc, char* argv[]) if(o_from) { mystring list; mystring tmp(o_from); - if(!parse_addresses(tmp, list) || - !parse_sender(list)) { - fout << "nullmailer-inject: Invalid sender address: " << o_from << endl; + if(tmp == "" || tmp == "<>") + sender = ""; + else if(!parse_addresses(tmp, list) || + !parse_sender(list)) { + ferr << "nullmailer-inject: Invalid sender address: " << o_from << endl; return false; } + use_header_sender = false; } use_header_recips = (use_recips != use_args); if(use_recips == use_header) @@ -564,7 +523,7 @@ bool parse_args(int argc, char* argv[]) bool result = true; for(int i = 0; i < argc; i++) { if(!parse_recip_arg(argv[i])) { - fout << "Invalid recipient: " << argv[i] << endl; + ferr << "Invalid recipient: " << argv[i] << endl; result = false; } } @@ -579,7 +538,7 @@ int cli_main(int argc, char* argv[]) !fix_header()) return 1; if(recipients.count() == 0) { - fout << "No recipients were listed." << endl; + ferr << "No recipients were listed." << endl; return 1; } if(!send_message()) diff --git a/src/mailq.cc b/src/mailq.cc index 92421c0..d3edf13 100644 --- a/src/mailq.cc +++ b/src/mailq.cc @@ -1,5 +1,5 @@ // nullmailer -- a simple relay-only MTA -// Copyright (C) 2012 Bruce Guenter <bruce@untroubled.org> +// Copyright (C) 2016 Bruce Guenter <bruce@untroubled.org> // // 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 @@ -20,15 +20,16 @@ // <nullmailer-subscribe@lists.untroubled.org>. #include "config.h" -#include "defines.h" -#include "fdbuf/fdbuf.h" -#include "itoa.h" -#include "mystring/mystring.h" #include <dirent.h> #include <stdlib.h> #include <sys/stat.h> #include <time.h> #include <unistd.h> +#include "configio.h" +#include "defines.h" +#include "fdbuf/fdbuf.h" +#include "itoa.h" +#include "mystring/mystring.h" #define fail(X) do{ fout << X << endl; return 1; }while(0) @@ -36,9 +37,10 @@ int main(int, char*[]) { mystring line; - if(chdir(QUEUE_MSG_DIR)) + mystring msg_dir = CONFIG_PATH(QUEUE, NULL, "queue"); + if(chdir(msg_dir.c_str())) fail("Cannot change directory to queue."); - DIR* dir = opendir(QUEUE_MSG_DIR); + DIR* dir = opendir("."); if(!dir) fail("Cannot open queue directory."); struct dirent* entry; diff --git a/src/queue.cc b/src/queue.cc index 8db9c22..9868a13 100644 --- a/src/queue.cc +++ b/src/queue.cc @@ -1,5 +1,5 @@ // nullmailer -- a simple relay-only MTA -// Copyright (C) 2012 Bruce Guenter <bruce@untroubled.org> +// Copyright (C) 2016 Bruce Guenter <bruce@untroubled.org> // // 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 @@ -26,6 +26,8 @@ #include <sys/stat.h> #include <time.h> #include <unistd.h> +#include "autoclose.h" +#include "configio.h" #include "itoa.h" #include "defines.h" #include "mystring/mystring.h" @@ -33,13 +35,19 @@ #include "configio.h" #include "hostname.h" +const char* cli_program = "nullmailer-queue"; + #define fail(MSG) do{ fout << "nullmailer-queue: " << MSG << endl; return false; }while(0) pid_t pid = getpid(); uid_t uid = getuid(); time_t timesecs = time(0); mystring adminaddr; -bool remapadmin = false; +mystring allmailfrom; + +static mystring trigger_path; +static mystring msg_dir; +static mystring tmp_dir; bool is_dir(const char* path) { @@ -55,36 +63,34 @@ bool is_exist(const char* path) int fsyncdir(const char* path) { - int fd = open(path, O_RDONLY); + autoclose fd = open(path, O_RDONLY); if(fd == -1) return 0; int result = fsync(fd); if(result == -1 && errno != EIO) result = 0; - close(fd); return result; } void trigger() { - int fd = open(QUEUE_TRIGGER, O_WRONLY|O_NONBLOCK, 0666); + autoclose fd = open(trigger_path.c_str(), O_WRONLY|O_NONBLOCK, 0666); if(fd == -1) return; char x = 0; write(fd, &x, 1); - close(fd); } -bool validate_addr(mystring& addr, bool doremap) +bool validate_addr(mystring& addr, bool recipient) { int i = addr.find_last('@'); - if(i < 0) + if(i <= 0) return false; mystring hostname = addr.right(i+1); - if(doremap && remapadmin) { - if(hostname == me || hostname == "localhost") - addr = adminaddr; - } + if (recipient && !!adminaddr && (hostname == me || hostname == "localhost")) + addr = adminaddr; + else if (!recipient && !!allmailfrom) + addr = allmailfrom; else if(hostname.find_first('.') < 0) return false; return true; @@ -93,9 +99,9 @@ bool validate_addr(mystring& addr, bool doremap) bool copyenv(fdobuf& out) { mystring str; - if(!fin.getline(str) || !str) + if(!fin.getline(str)) fail("Could not read envelope sender."); - if(!validate_addr(str, false)) + if(!!str && !validate_addr(str, false)) fail("Envelope sender address is invalid."); if(!(out << str << endl)) fail("Could not write envelope sender."); @@ -148,7 +154,7 @@ bool dump(int fd) bool deliver() { - if(!is_dir(QUEUE_MSG_DIR) || !is_dir(QUEUE_TMP_DIR)) + if(!is_dir(msg_dir.c_str()) || !is_dir(tmp_dir.c_str())) fail("Installation error: queue directory is invalid."); // Notes: @@ -159,9 +165,8 @@ bool deliver() // the previous nullmailer-queue process crashed, and it can be // safely overwritten const mystring pidstr = itoa(pid); - const mystring timestr = itoa(timesecs); - const mystring tmpfile = QUEUE_TMP_DIR + pidstr; - const mystring newfile = QUEUE_MSG_DIR + timestr + "." + pidstr; + const mystring tmpfile = tmp_dir + pidstr; + const mystring newfile = msg_dir + itoa(timesecs) + "." + pidstr; int out = open(tmpfile.c_str(), O_CREAT|O_WRONLY|O_TRUNC, 0600); if(out < 0) @@ -172,7 +177,7 @@ bool deliver() } if(link(tmpfile.c_str(), newfile.c_str())) fail("Error linking the temp file to the new file."); - if(fsyncdir(QUEUE_MSG_DIR)) + if(fsyncdir(msg_dir.c_str())) fail("Error syncing the new directory."); if(unlink(tmpfile.c_str())) fail("Error unlinking the temp file."); @@ -181,12 +186,16 @@ bool deliver() int main(int, char*[]) { + trigger_path = CONFIG_PATH(QUEUE, NULL, "trigger"); + msg_dir = CONFIG_PATH(QUEUE, "queue", ""); + tmp_dir = CONFIG_PATH(QUEUE, "tmp", ""); + umask(077); if(config_read("adminaddr", adminaddr) && !!adminaddr) { adminaddr = adminaddr.subst(',', '\n'); - remapadmin = true; read_hostnames(); } + config_read("allmailfrom", allmailfrom); if(!deliver()) return 1; diff --git a/src/send.cc b/src/send.cc index 1b854fc..647ffc7 100644 --- a/src/send.cc +++ b/src/send.cc @@ -1,5 +1,5 @@ // nullmailer -- a simple relay-only MTA -// Copyright (C) 2012 Bruce Guenter <bruce@untroubled.org> +// Copyright (C) 2016 Bruce Guenter <bruce@untroubled.org> // // 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 @@ -24,6 +24,7 @@ #include <dirent.h> #include <errno.h> #include <signal.h> +#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> @@ -31,23 +32,44 @@ #include <sys/wait.h> #include <unistd.h> #include "ac/time.h" +#include "argparse.h" +#include "autoclose.h" #include "configio.h" #include "defines.h" #include "errcodes.h" #include "fdbuf/fdbuf.h" +#include "forkexec.h" #include "hostname.h" #include "itoa.h" #include "list.h" #include "selfpipe.h" #include "setenv.h" +const char* cli_program = "nullmailer-send"; + selfpipe selfpipe; +typedef enum { tempfail=-1, permfail=0, success=1 } tristate; + +struct message +{ + time_t timestamp; + mystring filename; +}; + typedef list<mystring> slist; +typedef list<struct message> msglist; -#define fail(MSG) do { fout << MSG << endl; return false; } while(0) -#define fail2(MSG1,MSG2) do{ fout << MSG1 << MSG2 << endl; return false; }while(0) -#define fail_sys(MSG) do{ fout << MSG << strerror(errno) << endl; return false; }while(0) +#define msg1(MSG) do{ fout << MSG << endl; }while(0) +#define msg2(MSG1,MSG2) do{ fout << MSG1 << MSG2 << endl; }while(0) +#define msg1sys(MSG) do{ fout << MSG << strerror(errno) << endl; }while(0) +#define fail(MSG) do { msg1(MSG); return false; } while(0) +#define fail2(MSG1,MSG2) do{ msg2(MSG1,MSG2); return false; }while(0) +#define fail1sys(MSG) do{ msg1sys(MSG); return false; }while(0) +#define tempfail1sys(MSG) do{ msg1sys(MSG); return tempfail; }while(0) + +static mystring trigger_path; +static mystring msg_dir; struct remote { @@ -55,7 +77,8 @@ struct remote mystring host; mystring proto; - slist options; + mystring program; + mystring options; remote(const slist& list); ~remote(); }; @@ -66,76 +89,62 @@ remote::remote(const slist& lst) { slist::const_iter iter = lst; host = *iter; + options = "host=" + host + "\n"; ++iter; if(!iter) proto = default_proto; else { proto = *iter; - for(++iter; iter; ++iter) - options.append(*iter); + for(++iter; iter; ++iter) { + mystring option = *iter; + // Strip prefix "--" + if (option[0] == '-' && option[1] == '-') + option = option.right(2); + options += option; + options += '\n'; + } } + options += '\n'; + program = CONFIG_PATH(PROTOCOLS, NULL, proto.c_str()); } remote::~remote() { } typedef list<remote> rlist; -unsigned ws_split(const mystring& str, slist& lst) -{ - lst.empty(); - const char* ptr = str.c_str(); - const char* end = ptr + str.length(); - unsigned count = 0; - for(;;) { - while(ptr < end && isspace(*ptr)) - ++ptr; - const char* start = ptr; - while(ptr < end && !isspace(*ptr)) - ++ptr; - if(ptr == start) - break; - lst.append(mystring(start, ptr-start)); - ++count; - } - return count; -} - static rlist remotes; static int minpause = 60; static int pausetime = minpause; static int maxpause = 24*60*60; static int sendtimeout = 60*60; +static int queuelifetime = 7*24*60*60; bool load_remotes() { slist rtmp; - if(!config_readlist("remotes", rtmp) || - rtmp.count() == 0) - return false; + config_readlist("remotes", rtmp); remotes.empty(); for(slist::const_iter r(rtmp); r; r++) { if((*r)[0] == '#') continue; - slist parts; - if(!ws_split(*r, parts)) + arglist parts; + if (!parse_args(parts, *r)) continue; remotes.append(remote(parts)); } - return remotes.count() > 0; + if (remotes.count() == 0) + fail("No remote hosts listed for delivery"); + return true; } bool load_config() { mystring hh; - bool result = true; if (!config_read("helohost", hh)) hh = me; setenv("HELOHOST", hh.c_str(), 1); - if(!load_remotes()) - result = false; - int oldminpause = minpause; if(!config_readint("pausetime", minpause)) minpause = 60; @@ -143,69 +152,61 @@ bool load_config() maxpause = 24*60*60; if(!config_readint("sendtimeout", sendtimeout)) sendtimeout = 60*60; + if(!config_readint("queuelifetime", queuelifetime)) + queuelifetime = 7*24*60*60; if (minpause != oldminpause) pausetime = minpause; - return result; + return load_remotes(); } -static slist files; -static bool reload_files = false; +static msglist messages; +static bool reload_messages = false; void catch_alrm(int) { signal(SIGALRM, catch_alrm); - reload_files = true; + reload_messages = true; } -bool load_files() +bool load_messages() { - reload_files = false; + reload_messages = false; fout << "Rescanning queue." << endl; DIR* dir = opendir("."); if(!dir) - fail_sys("Cannot open queue directory: "); - files.empty(); + fail1sys("Cannot open queue directory: "); + messages.empty(); struct dirent* entry; while((entry = readdir(dir)) != 0) { const char* name = entry->d_name; - if(name[0] == '.') + if (name[0] == '.') + continue; + struct stat st; + if (stat(name, &st) < 0) { + fout << "Could not stat " << name << ", skipping." << endl; continue; - files.append(name); + } + struct message m = { st.st_mtime, name }; + messages.append(m); } closedir(dir); return true; } -void exec_protocol(int fd, remote& remote) +tristate catchsender(fork_exec& fp) { - if(close(0) == -1 || dup2(fd, 0) == -1 || close(fd) == -1) - return; - mystring program = PROTOCOL_DIR + remote.proto; - const char* args[3+remote.options.count()]; - unsigned i = 0; - args[i++] = program.c_str(); - for(slist::const_iter opt(remote.options); opt; opt++) - args[i++] = strdup((*opt).c_str()); - args[i++] = remote.host.c_str(); - args[i++] = 0; - execv(args[0], (char**)args); -} - -bool catchsender(pid_t pid) -{ - int status; - for (;;) { switch (selfpipe.waitsig(sendtimeout)) { case 0: // timeout - kill(pid, SIGTERM); - waitpid(pid, &status, 0); + fout << "Sending timed out, killing protocol" << endl; + fp.kill(SIGTERM); selfpipe.waitsig(); // catch the signal from killing the child - fail("Sending timed out, killing protocol"); + return tempfail; case -1: - fail_sys("Error waiting for the child signal: "); + msg1sys("Error waiting for the child signal: "); + return tempfail; case SIGCHLD: break; default: @@ -214,72 +215,201 @@ bool catchsender(pid_t pid) break; } - if(waitpid(pid, &status, 0) == -1) - fail_sys("Error catching the child process return value: "); + int status = fp.wait_status(); + if(status < 0) { + fout << "Error catching the child process return value: " + << strerror(errno) << endl; + return tempfail; + } else { if(WIFEXITED(status)) { status = WEXITSTATUS(status); - if(status) - fail2("Sending failed: ", errorstr(status)); + if(status) { + fout << "Sending failed: " << errorstr(status) << endl; + return (status & ERR_PERMANENT_FLAG) ? permfail : tempfail; + } else { fout << "Sent file." << endl; - return true; + return success; } } - else - fail("Sending process crashed or was killed."); + else { + fout << "Sending process crashed or was killed." << endl; + return tempfail; + } } } -bool send_one(mystring filename, remote& remote) +bool log_msg(mystring& filename, remote& remote, int fd) { - int fd = open(filename.c_str(), O_RDONLY); - if(fd == -1) { + fout << "Starting delivery:" + << " host: " << remote.host + << " protocol: " << remote.proto + << " file: " << filename << endl; + fdibuf in(fd); + mystring line; + mystring msg; + if (in.getline(line, '\n')) { + msg = "From: <"; + msg += line; + msg += '>'; + bool has_to = false; + while (in.getline(line, '\n')) { + if (!line) + break; + msg += has_to ? ", " : " to: "; + has_to = true; + msg += '<'; + msg += line; + msg += '>'; + } + fout << msg << endl; + while (in.getline(line, '\n')) { + if (!line) + break; + if (line.left(11).lower() == "message-id:") + fout << line << endl; + } + lseek(fd, 0, SEEK_SET); + return true; + } + fout << endl << "Can't read message" << endl; + return false; +} + +static bool copy_output(int fd, mystring& output) +{ + output = ""; + char buf[256]; + ssize_t rd; + while ((rd = read(fd, buf, sizeof buf)) > 0) + output += mystring(buf, rd); + return rd == 0; +} + +tristate send_one(mystring filename, remote& remote, mystring& output) +{ + autoclose fd = open(filename.c_str(), O_RDONLY); + if(fd < 0) { fout << "Can't open file '" << filename << "'" << endl; + return tempfail; + } + log_msg(filename, remote, fd); + + fork_exec fp(remote.proto.c_str()); + int redirs[] = { REDIRECT_PIPE_TO, REDIRECT_PIPE_FROM, REDIRECT_NONE, fd }; + if (!fp.start(remote.program.c_str(), 4, redirs)) + return tempfail; + + if (write(redirs[0], remote.options.c_str(), remote.options.length()) != (ssize_t)remote.options.length()) + fout << "Warning: Writing options to protocol failed" << endl; + close(redirs[0]); + + tristate result = catchsender(fp); + if (!copy_output(redirs[1], output)) + fout << "Warning: Could not read output from protocol" << endl; + close(redirs[1]); + return result; +} + +static void parse_output(const mystring& output, const remote& remote, mystring& status, mystring& diag) +{ + diag = remote.proto.upper(); + diag += "; "; + diag += output.strip(); + diag.subst('\n', '/'); + status = "5.0.0"; + for (unsigned i = 0; i < output.length()-5; i++) + if (isdigit(output[i]) + && output[i+1] == '.' + && isdigit(output[i+2]) + && output[i+3] == '.' + && isdigit(output[i+4])) { + status = output.sub(i, 5); + break; + } +} + +bool bounce_msg(const message& msg, const remote& remote, const mystring& output) +{ + mystring failed = "../failed/"; + failed += msg.filename; + fout << "Moving message " << msg.filename << " into failed" << endl; + if (rename(msg.filename.c_str(), failed.c_str()) == -1) { + fout << "Can't rename file: " << strerror(errno) << endl; return false; } - fout << "Starting delivery: protocol: " << remote.proto - << " host: " << remote.host - << " file: " << filename << endl; - pid_t pid = fork(); - switch(pid) { - case -1: - fail_sys("Fork failed: "); - case 0: - exec_protocol(fd, remote); - exit(ERR_EXEC_FAILED); - default: - close(fd); - if(!catchsender(pid)) - return false; - if(unlink(filename.c_str()) == -1) - fail_sys("Can't unlink file: "); + autoclose fd = open(failed.c_str(), O_RDONLY); + if (fd < 0) + fout << "Can't open file '" << failed << "' to create bounce message" << endl; + else { + fout << "Generating bounce for '" << msg.filename << "'" << endl; + queue_pipe qp; + autoclose pfd = qp.start(); + if (pfd > 0) { + mystring program = program_path("nullmailer-dsn"); + fork_exec dsn("nullmailer-dsn"); + int redirs[] = { fd, pfd }; + mystring status_code, diag_code; + parse_output(output, remote, status_code, diag_code); + const char* args[] = { program.c_str(), + "--last-attempt", itoa(time(NULL)), + "--remote", remote.host.c_str(), + "--diagnostic-code", diag_code.c_str(), + status_code.c_str(), NULL }; + dsn.start(args, 2, redirs); + // Everything else cleans up itself + } } return true; } -bool send_all() +void send_all() { - if(!load_config()) - fail("Could not load the config"); - if(remotes.count() <= 0) - fail("No remote hosts listed for delivery"); - if(files.count() == 0) - return true; + if(!load_config()) { + fout << "Could not load the config" << endl; + return; + } + if(remotes.count() <= 0) { + fout << "No remote hosts listed for delivery"; + return; + } + if(messages.count() == 0) + return; fout << "Starting delivery, " - << itoa(files.count()) << " message(s) in queue." << endl; + << itoa(messages.count()) << " message(s) in queue." << endl; + mystring output; for(rlist::iter remote(remotes); remote; remote++) { - slist::iter file(files); - while(file) { - if(send_one(*file, *remote)) - files.remove(file); - else - file++; + msglist::iter msg(messages); + while(msg) { + switch (send_one((*msg).filename, *remote, output)) { + case tempfail: + if (time(0) - (*msg).timestamp > queuelifetime) { + if (bounce_msg(*msg, *remote, output)) { + messages.remove(msg); + continue; + } + } + msg++; + break; + case permfail: + if (bounce_msg(*msg, *remote, output)) + messages.remove(msg); + else + msg++; + break; + default: + if(unlink((*msg).filename.c_str()) == -1) { + fout << "Can't unlink file: " << strerror(errno) << endl; + msg++; + } + else + messages.remove(msg); + } } } fout << "Delivery complete, " - << itoa(files.count()) << " message(s) remain." << endl; - return true; + << itoa(messages.count()) << " message(s) remain." << endl; } static int trigger; @@ -289,12 +419,12 @@ static int trigger2; bool open_trigger() { - trigger = open(QUEUE_TRIGGER, O_RDONLY|O_NONBLOCK); + trigger = open(trigger_path.c_str(), O_RDONLY|O_NONBLOCK); #ifdef NAMEDPIPEBUG - trigger2 = open(QUEUE_TRIGGER, O_WRONLY|O_NONBLOCK); + trigger2 = open(trigger_path.c_str(), O_WRONLY|O_NONBLOCK); #endif if(trigger == -1) - fail_sys("Could not open trigger file: "); + fail1sys("Could not open trigger file: "); return true; } @@ -318,7 +448,7 @@ bool do_select() FD_SET(trigger, &readfds); struct timeval timeout; - if (files.count() == 0) + if (messages.count() == 0) pausetime = maxpause; timeout.tv_sec = pausetime; timeout.tv_usec = 0; @@ -331,20 +461,23 @@ bool do_select() if(s == 1) { fout << "Trigger pulled." << endl; read_trigger(); - reload_files = true; + reload_messages = true; pausetime = minpause; } else if(s == -1 && errno != EINTR) - fail_sys("Internal error in select: "); + fail1sys("Internal error in select: "); else if(s == 0) - reload_files = true; - if(reload_files) - load_files(); + reload_messages = true; + if(reload_messages) + load_messages(); return true; } int main(int, char*[]) { + trigger_path = CONFIG_PATH(QUEUE, NULL, "trigger"); + msg_dir = CONFIG_PATH(QUEUE, NULL, "queue"); + read_hostnames(); if(!selfpipe) { @@ -355,7 +488,7 @@ int main(int, char*[]) if(!open_trigger()) return 1; - if(chdir(QUEUE_MSG_DIR) == -1) { + if(chdir(msg_dir.c_str()) == -1) { fout << "Could not chdir to queue message directory." << endl; return 1; } @@ -363,7 +496,7 @@ int main(int, char*[]) signal(SIGALRM, catch_alrm); signal(SIGHUP, SIG_IGN); load_config(); - load_files(); + load_messages(); for(;;) { send_all(); if (minpause == 0) break; diff --git a/src/sendmail.cc b/src/sendmail.cc index 7713d3f..db0620c 100644 --- a/src/sendmail.cc +++ b/src/sendmail.cc @@ -1,5 +1,5 @@ // nullmailer -- a simple relay-only MTA -// Copyright (C) 2012 Bruce Guenter <bruce@untroubled.org> +// Copyright (C) 2016 Bruce Guenter <bruce@untroubled.org> // // 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 @@ -19,12 +19,14 @@ // available to discuss this package. To subscribe, send an email to // <nullmailer-subscribe@lists.untroubled.org>. -#include <config.h> +#include "config.h" #include <stdlib.h> #include <string.h> #include <unistd.h> +#include "configio.h" #include "fdbuf/fdbuf.h" #include "defines.h" +#include "forkexec.h" #include "setenv.h" #include "cli++/cli++.h" @@ -100,13 +102,10 @@ bool setenvelope(char* str) int do_exec(const char* program, const char* xarg1, int argc, char* argv[]) { - if(chdir(BIN_DIR) == -1) { - ferr << "sendmail: Could not change directory to " << BIN_DIR << endl; - return 1; - } + mystring path = program_path(program); const char* newargv[argc+3]; - newargv[0] = program; + newargv[0] = path.c_str(); int j = 1; if (xarg1) newargv[j++] = xarg1; diff --git a/src/smtpd.cc b/src/smtpd.cc index 7e39775..8df62a0 100644 --- a/src/smtpd.cc +++ b/src/smtpd.cc @@ -1,5 +1,5 @@ // nullmailer -- a simple relay-only MTA -// Copyright (C) 2012 Bruce Guenter <bruce@untroubled.org> +// Copyright (C) 2016 Bruce Guenter <bruce@untroubled.org> // // 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 @@ -23,9 +23,11 @@ #include <sys/wait.h> #include <stdlib.h> #include <unistd.h> -#include "lib/defines.h" +#include "autoclose.h" +#include "defines.h" #include "fdbuf/fdbuf.h" #include "mystring/mystring.h" +#include "forkexec.h" static const char resp_data_ok[] = "354 End your message with a period on a line by itself"; static const char resp_goodbye[] = "221 2.0.0 Good bye"; @@ -50,6 +52,8 @@ static mystring line; static mystring sender; static mystring recipients; +extern const char cli_program[] = "nullmailer-smtpd"; + static int readline() { if (!fin.getline(line)) @@ -107,46 +111,6 @@ static bool respond(const char* msg) return fout; } -static pid_t qpid; - -static int start_queue() -{ - int pfd[2]; - int fdnull; - const char* args[2] = { 0 }; - mystring nqueue = getenv("NULLMAILER_QUEUE"); - if (!nqueue) { - nqueue = SBIN_DIR; - nqueue += "/nullmailer-queue"; - } - args[0] = nqueue.c_str(); - - if ((fdnull = open("/dev/null", O_WRONLY)) >= 0) { - if (pipe(pfd) == 0) { - if ((qpid = fork()) >= 0) { - - if (qpid == 0) { - dup2(pfd[0], 0); - dup2(fdnull, 1); - dup2(fdnull, 2); - close(pfd[0]); - close(pfd[1]); - close(fdnull); - execv(args[0], (char**)args); - _exit(111); - } - - close(pfd[0]); - return pfd[1]; - } - close(pfd[0]); - close(pfd[1]); - } - close(fdnull); - } - return -1; -} - static bool qwrite(int qfd, const char* data, size_t len) { ssize_t wr; @@ -169,12 +133,13 @@ static bool DATA(mystring& param) if (!recipients) return respond(resp_no_rcpt); - int qfd; - if ((qfd = start_queue()) < 0) + queue_pipe nq; + autoclose wfd = nq.start(); + if (wfd < 0) return respond(resp_no_queue); - if (!qwrite(qfd, sender.c_str(), sender.length()) - || !qwrite(qfd, recipients.c_str(), recipients.length()) - || !qwrite(qfd, "\n", 1)) + if (!qwrite(wfd, sender.c_str(), sender.length()) + || !qwrite(wfd, recipients.c_str(), recipients.length()) + || !qwrite(wfd, "\n", 1)) return respond(resp_qwrite_err); if (!respond(resp_data_ok)) @@ -186,16 +151,12 @@ static bool DATA(mystring& param) if (line.length() > 1 && line[0] == '.') line = line.sub(1, line.length() - 1); line += '\n'; - if (!qwrite(qfd, line.c_str(), line.length())) + if (!qwrite(wfd, line.c_str(), line.length())) return respond(resp_qwrite_err); } - close(qfd); - - int status; - if (waitpid(qpid, &status, 0) != qpid) - return respond(resp_queue_waiterr); + wfd.close(); - return respond(status ? resp_queue_exiterr : resp_queue_ok); + return respond(nq.wait() ? resp_queue_ok : resp_queue_exiterr); } static bool HELO(mystring& param) |