/* mdeliver.c -- Deliver a message to a Maildir. * * $Id$ * * This program is in the public domain. * * mdeliver is a simple, robust MDA. It delivers a single message * from stdin to the user's Maildir and executes (in the background) a * single program with the message filename as the first argument. * Additional arguments may be specified; see main() below for * details. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef EX_TEMPFAIL #ifdef QMAIL #define EX_TEMPFAIL 111 #else #include #endif #endif #if !defined(HOST_NAME_MAX) #if defined MAXHOSTNAMELEN #define HOST_NAME_MAX MAXHOSTNAMELEN #else #define HOST_NAME_MAX 64 #endif #endif /* holds 2^128 - 1 in decimal, plus '\0' */ #define LEN_ULONG 40 /* 4 for "tmp/", 2 for the two dots ('.'), HOST_NAME_MAX for the * hostname, two LEN_ULONGs for time and pid, and 1 for '\0', . */ #define LEN_FILENAME 4 + 2 + HOST_NAME_MAX + LEN_ULONG + LEN_ULONG + 1 #define save_errno(op) { \ int olderrno = errno; \ op; \ errno = olderrno; \ } #define try_unlink(fn) save_errno( unlink(fn) ) static char ident[] = "$Id$"; #define err(...) _err(true, __LINE__, __VA_ARGS__) #define errx(...) _err(false, __LINE__, __VA_ARGS__) static void _err(bool want_strerror, int lineno, char *format, ...) { va_list ap; int olderrno = errno; fprintf(stderr, "mdeliver:%d:", lineno); va_start(ap, format); vfprintf(stderr, format, ap); va_end(ap); if (want_strerror) { fputs(": ", stderr); fputs(strerror(olderrno), stderr); } fputs("\n", stderr); exit(EX_TEMPFAIL); } static void full_sleep(unsigned seconds) { while (seconds > 0) { seconds = sleep(seconds); } } /* Write all of buf, even if write(2) is interrupted. */ static ssize_t full_write(int fd, const char *buf, size_t nbytes) { ssize_t r, w = 0; /* Loop until nbytes of buf have been written. */ while (w < nbytes) { /* Keep trying until write succeeds without interruption. */ do { r = write(fd, buf + w, nbytes - w); } while (r < 0 && errno == EINTR); if (r < 0) { return r; } w += r; } return w; } /* Find a filename for the message according to the algorithm * documented in the maildir(5) manual page from qmail * (http://www.qmail.org/qmail-manual-html/man5/maildir.html). */ static void get_filename(char *fn) { pid_t pid; char host[HOST_NAME_MAX + 1]; int try; time_t now; struct stat st; pid = getpid(); if (gethostname(host, sizeof(host)) != 0) { err("gethostname"); } /* http://www.opengroup.org/onlinepubs/009695399/functions/gethostname.html * "it is unspecified whether the returned name is null-terminated" */ host[sizeof(host) - 1] = '\0'; for (try = 0; ; try++) { now = time(NULL); if (now == -1) { err("time"); } sprintf(fn, "tmp/%lu.%lu.%s", (long unsigned)now, (long unsigned)pid, host); if (stat(fn, &st) != 0) { if (errno == ENOENT) { /* Every failure point in this program relies on errno * describing what went wrong. This is the one * failure we *want*, so it is important to restore * errno to 0 (success) before returning. */ errno = 0; break; } } if (try == 2) { errx("Attempted filename exists."); } full_sleep(2); } } #ifndef QMAIL /* Strip the non-RFC2822-compliant 'From ' line some busted-ass * mailers (such as Postfix) add as the first line of input. This bit * of historical gunk originated with Berkeley mbox files, which use * this line to indicate the beginning of a message. Postfix should * only add such a line when it is acting as an MDA and delivering to * a Berkeley mbox; the data written to an external MDA should be the * raw RFC2822 message. >sigh< */ /* buf is passed only as an (early, evil) space optimization). */ static void strip_from(int fd, char *buf, char *fn) { ssize_t r; /* Yes, read(2) can be interrupted and read fewer bytes than * requested. However, i'm only asking for five bytes here. I * have tested this program extensively and it handles large * volumes of mail correctly every day on my systems. This holds * up my original opinion that expecting to be able to get five * bytes safely is not an unreasonable demand. If i can't get * five measly bytes, something is probably wrong, so bomb. */ r = read(0, buf, 5); if (r != 5) { try_unlink(fn); err("read(0, 5)"); } if (strncmp(buf, "From ", 5) == 0) { while ((r = read(0, buf, 1)) > 0 || (r == -1 && errno == EINTR)) { if (buf[0] == '\n') { break; } } if (r == -1) { try_unlink(fn); err("read(0, 1)"); } } else { if (full_write(fd, buf, r) != r) { try_unlink(fn); err("write(%s)", fn); } } } #endif /* Copy the file from INFD to OUTFD in blocks of BUFSIZE bytes (stored * in BUF). BUF must point to a buffer of at least BUFSIZE bytes; * we're not dealing with null-terminated strings here. * * Note that OUTFD is closed before returning; this is necessary to * ensure the file was fully written on some filesystems, such as NFS. */ static void copy_file(int infd, int outfd, char *buf, size_t bufsize, char *fn) { ssize_t r; while ((r = read(infd, buf, bufsize)) > 0 || (r == -1 && errno == EINTR)) { if (full_write(outfd, buf, r) != r) { try_unlink(fn); err("write(%s)", fn); } } if (r == -1) { try_unlink(fn); err("read(%d, %d)", infd, bufsize); } if (fsync(outfd) < 0) { try_unlink(fn); err("fsync(%s)", fn); } if (close(outfd) < 0) { try_unlink(fn); err("close(%s)", fn); } } static void deliver(char *maildir, char *sub, char *newfn) { char fn[LEN_FILENAME]; int fd; char buf[BUFSIZ]; /* Allocated here only as an (early, * evil) space optimization. */ if (chdir(maildir) != 0) { err("chdir(%s)", maildir); } get_filename(fn); fd = open(fn, O_WRONLY | O_EXCL | O_CREAT, 0600); if (fd < 0) { err("open(%s)", fn); } /* From this point, we have a tmp file, so if someting fails we * must try to unlink it before bombing. */ #ifdef QMAIL { char *buf; size_t len; buf = getenv("RPLINE"); len = strlen(buf); if (full_write(fd, buf, len) != len) { try_unlink(fn); err("write(%s)", fn); } buf = getenv("DTLINE"); len = strlen(buf); if (full_write(fd, buf, len) != len) { try_unlink(fn); err("write(%s)", fn); } } #else strip_from(fd, buf, fn); #endif copy_file(0, fd, buf, BUFSIZ, fn); strcpy(newfn, fn); memcpy(newfn, sub, 3); if (link(fn, newfn) != 0) { try_unlink(fn); err("link(%s, %s)", fn, newfn); } try_unlink(fn); } /* Run a program to process the new message. */ static void run_processor(char *home, char *newfn) { pid_t pid; char command[PATH_MAX]; FILE *errfile; char *fail; int olderrno; int fd; if ((pid = fork()) > 0) { return; } else if (pid == 0) { snprintf(command, sizeof(command), "%s/%s", home, ".mdeliver-processor"); execl(command, command, newfn, NULL); if (errno == ENOENT) { /* Not having a processor is not an error. */ _exit(0); } fail = "exec"; } else { fail = "fork"; } olderrno = errno; /* If we got here fork or exec failed, so try to write an error * message where later mail processors can find it. We really * can't do anything about it if this fails. */ memcpy(newfn, "err", 3); mkdir("err", 0700); fd = open(newfn, O_WRONLY | O_EXCL | O_CREAT, 0644); if (fd >= 0) { errfile = fdopen(fd, "w"); if (errfile) { fprintf(errfile, "mdeliver:mdeliver.c:%d:%s: %s\n", __LINE__, fail, strerror(olderrno)); fflush(errfile); } fsync(fd); close(fd); } if (fail[0] == 'e') { _exit(0); } } int main(int argc, char *argv[]) { char *home; char newfn[LEN_FILENAME]; struct passwd *pwd; struct stat st; if (argv[1] && argv[1][0] == '-' && argv[1][1] == 'v') { puts(ident); return 0; } if ((home = getenv("HOME")) == NULL) { if ((pwd = getpwuid(geteuid())) != NULL) { home = pwd->pw_dir; } else { errx("HOME is not set and getpwuid() failed."); } } if (chdir(home) != 0) { err("chdir(%s)", home); } if (stat(".mdeliver-processor", &st) == 0) { deliver("Maildir", "mdp", newfn); run_processor(home, newfn); } else { deliver("Maildir", "new", newfn); } return 0; }