/* Local mail delivery Copyright (C) 1996, 1997, 1998, 1999 Free Software Foundation, Inc. Written by Miles Bader This file is part of the GNU Hurd. The GNU Hurd 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, or (at your option) any later version. The GNU Hurd 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, USA. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define OPT_FILE -5 #define OPT_REMOVE -6 const char *argp_program_version = STANDARD_HURD_VERSION (mail.local); static const struct argp_option options[] = { {"from", 'f', "USER", 0, "Record sender as USER"}, {0, 'd', 0, OPTION_ALIAS|OPTION_HIDDEN}, {0, 'r', 0, OPTION_ALIAS|OPTION_HIDDEN}, {"file", OPT_FILE, "FILE", 0, "Deliver FILE instead of standard input"}, {"remove", OPT_REMOVE, 0, 0, "Remove FILE after successful delivery"}, {"mail-dir",'m', "DIR", 0, "Look for mailboxes in DIR"}, {"use-lock-file",'l', 0, OPTION_HIDDEN, "Use a lock file instead of flock for mailboxes"}, {0} }; static const char args_doc[] = "USER..."; static const char doc[] = "Deliver mail to the local mailboxes of USER..."; #define HDR_PFX "From " /* Header, at the beginning of a line, starting each msg in a mailbox. */ #define ESC_PFX ">" /* Used to escape occurrences of HDR_PFX in the msg body. */ #define BMAX (64*1024) /* Chunk size for I/O. */ struct params { char *from; /* Who the mail is from. */ char *mail_dir; /* Mailbox directory. */ }; /* Convert the system error code ERR to an appropriate exit value. This function currently only returns three sorts: success, temporary failure, or error, with exit codes 0, EX_TEMPFAIL, or EX_UNAVAILABLE. The table of temporary failures is from the bsd original of this program. */ static int err_to_ex (error_t err) { switch (err) { case 0: return 0; /* Success */ /* Temporary failures: */ #if defined(EWOULDBLOCK) && (EWOULDBLOCK != EAGAIN) case EWOULDBLOCK: /* Operation would block. */ #endif case EAGAIN: /* Resource temporarily unavailable */ case EDQUOT: /* Disc quota exceeded */ case EBUSY: /* Device busy */ case EPROCLIM: /* Too many processes */ case EUSERS: /* Too many users */ case ECONNABORTED: /* Software caused connection abort */ case ECONNREFUSED: /* Connection refused */ case ECONNRESET: /* Connection reset by peer */ case EDEADLK: /* Resource deadlock avoided */ case EFBIG: /* File too large */ case EHOSTDOWN: /* Host is down */ case EHOSTUNREACH: /* No route to host */ case EMFILE: /* Too many open files */ case ENETDOWN: /* Network is down */ case ENETRESET: /* Network dropped connection on reset */ case ENETUNREACH: /* Network is unreachable */ case ENFILE: /* Too many open files in system */ case ENOBUFS: /* No buffer space available */ case ENOMEM: /* Cannot allocate memory */ case ENOSPC: /* No space left on device */ case EROFS: /* Read-only file system */ case ESTALE: /* Stale NFS file handle */ case ETIMEDOUT: /* Connection timed out */ return EX_TEMPFAIL; default: return EX_UNAVAILABLE; } } /* Print and syslog the given error message, with the system error string for ERRNO appended. Return an appropriate exit code for ERRNO. */ #define SYSERR(fmt, args...) \ ({ syslog (LOG_ERR, fmt ": %m" , ##args); err_to_ex (errno); }) /* Print and syslog the given error message. Return the exit code EX_UNAVAILABLE. */ #define ERR(fmt, args...) \ ({ syslog (LOG_ERR, fmt , ##args); EX_UNAVAILABLE; }) /* Print and syslog the given error message, with the system error string for CODE appended. Return an appropriate exit code for CODE. */ #define SYSERRX(code, fmt, args...) \ ({ error_t _code = (code); \ syslog (LOG_ERR, fmt ": %s" , ##args , strerror (_code)); \ err_to_ex (_code); }) /* Block I/O functions. These are structured to allow the use of a read method that avoids copying the data locally. */ /* Free block allocated by bread. */ static void bfree (char *blk, size_t blk_len) { if (blk_len > 0) munmap (blk, blk_len); } /* Read up to MAX chars from IN into BLK & BLK_LEN, which may be reused or freed. */ static int bread (int in, char *in_name, size_t max, char **blk, size_t *blk_len) { char *orig_blk = *blk; size_t orig_blk_len = *blk_len; error_t err = HURD_DPORT_USE (in, io_read (port, blk, blk_len, -1, max)); if (err) return SYSERRX (err, "%s", in_name); if (*blk != orig_blk) bfree (orig_blk, orig_blk_len); return 0; } /* Write BLK & BLK_LEN to OUT. An exit status is returned. */ static int bwrite (int out, char *out_name, const char *blk, size_t blk_len) { while (blk_len > 0) { ssize_t wr = write (out, blk, blk_len); if (wr < 0) return SYSERR ("%s", out_name); blk += wr; blk_len -= wr; } return 0; } /* Copy from file descriptor IN to OUT. An exit status is returned. */ static int copy (int in, char *in_name, int out, char *out_name) { int ex = 0; char *blk = 0; size_t blk_len = 0; do { ex = bread (in, in_name, BMAX, &blk, &blk_len); if (! ex) ex = bwrite (out, out_name, blk, blk_len); } while (blk_len > 0 && !ex); bfree (blk, blk_len); return ex; } static int write_header (int out, char *out_name, struct params *params) { char *hdr; size_t hdr_len; struct timeval tv; time_t time; int ex = 0; if (gettimeofday (&tv, 0) < 0) return SYSERR ("gettimeofday"); /* Note that the string returned by ctime includes a terminating newline. */ time = tv.tv_sec; hdr_len = asprintf (&hdr, "From %s %s", params->from, ctime (&time)); if (! hdr) return SYSERRX (ENOMEM, "%s", out_name); ex = bwrite (out, out_name, hdr, hdr_len); free (hdr); return ex; } /* Copy from file descriptor IN to OUT, making any changes needed to make the contents a valid mailbox entry. These include: (1) Prepending a `From ...' line, and appending a blank line. (2) Replacing any occurrences of `From ' at the beginning of lines with `>From '. An exit status is returned. */ static int process (int in, char *in_name, int out, char *out_name, struct params *params) { /* The block currently being processed. */ char *blk = 0; size_t blk_len = 0; /* MATCH is the string we're looking for to escape, NL_MATCH is the same string prefixed by a newline to ease searching (MATCH only matches at the beginning of lines). */ const char *const nl_match = "\n" HDR_PFX, *const match = nl_match + 1; /* The portion of MATCH that matched the end of the previous block. 0 means that there was at least a newline, so initializing MATCHED to 0 simulates a newline at the start of IN. */ ssize_t matched = 0; int ex = write_header (out, out_name, params); #define match_len (sizeof HDR_PFX - 1) if (ex) return ex; #define BWRITE(p, p_len) \ ({ size_t _len = (p_len); \ if (_len > 0 && (ex = bwrite (out, out_name, p, _len))) \ break; }) do { char *start, *end; ex = bread (in, in_name, BMAX, &blk, &blk_len); if (matched >= 0) /* The last block ended in a partial match, so see if we can complete it in this block. */ { if (blk_len >= match_len - matched && memcmp (blk, match + matched, match_len - matched) == 0) /* It matched; output the escape prefix before the match. */ BWRITE (ESC_PFX, sizeof ESC_PFX - 1); /* Now we have to output the part of the preceding block that we didn't do before because it was a partial match. */ BWRITE (match, matched); /* Ok, we're all caught up. The portion of the match that's in this block will be output as normal text. */ matched = -1; } /* Scan through the block looking for matches. */ for (start = end = blk; start < blk + blk_len; start = end) { /* Look for our match, prefixed by a newline. */ end = memmem (start, blk + blk_len - start, nl_match, match_len + 1); if (end) /* We found a match, output the escape prefix before it. */ { end++; /* The newline should precede the escape. */ BWRITE (start, end - start); /* part of block before match. */ BWRITE (ESC_PFX, sizeof ESC_PFX - 1); /* escape prefix. */ } else { end = blk + blk_len; break; } } /* Now see if there are any partial matches at the end. */ for (matched = end - start < match_len + 1 ? end - start - 1 : match_len; matched >= 0; matched--) if (memcmp (end - matched - 1, nl_match, matched + 1) == 0) /* There's a partial match MATCHED characters long at the end of this block, so delay outputting it until the next block can be examined; we do output the preceding newline here, though. */ { end -= matched; break; } BWRITE (start, end - start); } while (blk_len > 0); if (! ex) ex = bwrite (out, out_name, "\n", 1); /* Append a blank line. */ bfree (blk, blk_len); return ex; } /* Deliver flags. */ #define D_PROCESS 0x1 /* Deliver */ #define D_REWIND 0x2 /* Rewind MSG before using it. */ /* Deliver the text from the file descriptor MSG to the mailbox of the user RCPT in MAIL_DIR. FLAGS is from the set D_* above. An exit appropriate exit code is returned. */ static int deliver (int msg, char *msg_name, char *rcpt, int flags, struct params *params) { char *mbox; /* Name of mailbox */ int fd; /* Opened mailbox */ struct stat stat; int ex = 0; /* Exit status */ struct passwd *pw = getpwnam (rcpt); /* Details of recipient */ if (! pw) return ERR ("%s: Unknown user", rcpt); asprintf (&mbox, "%s/%s", params->mail_dir, rcpt); if (! mbox) return SYSERRX (ENOMEM, "%s", rcpt); do { /* First try to open an existing mailbox. */ fd = open (mbox, O_WRONLY|O_APPEND|O_NOLINK|O_EXLOCK); if (fd < 0 && errno == ENOENT) /* There is none; try to create it. */ { fd = open (mbox, O_WRONLY|O_APPEND|O_CREAT|O_EXCL|O_NOLINK|O_EXLOCK, S_IRUSR|S_IWUSR); if (fd >= 0) /* Made a new mailbox! Set the owner and group appropiately. */ { if (fchown (fd, pw->pw_uid, pw->pw_gid) < 0) { close (fd); fd = -1; /* Propagate error. */ } } } } /* EEXIST can only be returned someone else created the mailbox between the two opens above, so if we try again, the first open should work. */ while (fd < 0 && errno == EEXIST); if (fd < 0 || fstat (fd, &stat) < 0) ex = SYSERR ("%s", mbox); else if (S_ISLNK (stat.st_mode) || stat.st_nlink != 1) ex = ERR ("%s: Is linked", mbox); else { if (flags & D_REWIND) { if (lseek (msg, 0L, SEEK_SET) < 0) ex = SYSERR ("%s", msg_name); } if (! ex) { if (flags & D_PROCESS) ex = process (msg, msg_name, fd, mbox, params); else ex = copy (msg, msg_name, fd, mbox); } } if (fd >= 0) { if (fsync (fd) < 0 && !ex) ex = SYSERR ("%s", mbox); if (close (fd) < 0 && !ex) ex = SYSERR ("%s", mbox); } free (mbox); return ex; } /* Process from the file descriptor IN into a temporary file, which return a descriptor to in CACHED; once *CACHED is closed, it will go away permanently. The file pointer of *CACHED is at an undefined location. An exit status is returned. */ static int cache (int in, char *in_name, struct params *params, int *cached) { int ex; error_t err; file_t file; /* Hurd port for temp file */ int fd; /* File descriptor for it */ file_t tmp_dir = file_name_lookup (_PATH_TMP, O_RDONLY, 0); if (tmp_dir == MACH_PORT_NULL) return SYSERR ("%s", _PATH_TMP); /* Create FILE without actually putting it into TMP_DIR. */ err = dir_mkfile (tmp_dir, O_RDWR, 0600, &file); if (err) return SYSERRX (err, "%s", _PATH_TMP); fd = _hurd_intern_fd (file, O_RDWR, 1); if (fd < 0) return SYSERR ("%s", _PATH_TMP); ex = process (in, in_name, fd, _PATH_TMP, params); if (! ex) *cached = fd; else close (fd); return ex; } int main (int argc, char **argv) { int rcpt = 0; /* Index in ARGV of next recipient. */ char *file = 0; /* File containing message. */ int remove = 0; /* Remove file after successful delivery. */ int in = 0; /* Raw input file descriptor. */ int ex = 0; /* Exit status. */ struct params params = { from: 0, mail_dir: _PATH_MAILDIR }; error_t parse_opt (int key, char *arg, struct argp_state *state) { switch (key) { case 'd': /* do nothing; compatibility */ break; case 'f': case 'r': params.from = arg; break; case OPT_FILE: file = arg; break; case OPT_REMOVE: remove = 1; break; case 'm': params.mail_dir = arg; break; case 'l': argp_failure (state, EX_USAGE, EINVAL, "-l not supported"); case ARGP_KEY_NO_ARGS: argp_error (state, "No recipients"); default: return ARGP_ERR_UNKNOWN; } return 0; } const struct argp argp = { options, parse_opt, args_doc, doc }; /* Parse arguments. */ argp_parse (&argp, argc, argv, 0, &rcpt, 0); /* Use syslog to log errors. */ openlog ("mail.local", LOG_PERROR, LOG_MAIL); if (! params.from) /* No from address specified, use the current user. */ { struct passwd *pw; int uid = getuid (); if (uid == -1) exit (ERR ("No user id")); pw = getpwuid (uid); if (! pw) exit (ERR ("%d: Unknown uid", uid)); params.from = strdup (pw->pw_name); } if (file) /* Use FILE as the message contents. */ { in = open (file, O_RDONLY); if (in < 0) exit (SYSERR ("%s", file)); } else /* Use standard input. */ in = 0; if (rcpt == argc - 1) /* Only a single recipient. */ ex = deliver (in, file ?: "-", argv[rcpt], D_PROCESS, ¶ms); else /* Multiple recipients. */ { int cached; /* Temporary processed input file. */ ex = cache (in, file ?: "-", ¶ms, &cached); if (! ex) while (rcpt < argc) { /* Deliver to one recipient. */ int rex = deliver (cached, "message cache", argv[rcpt++], D_REWIND, ¶ms); /* Merge the exit code for that recipient. Temporary failures take precedence over hard failures and success, as subsequently delivering duplicates (of the successful messages) is preferable to not delivering the temporary failures. */ if (ex != EX_TEMPFAIL) { if (rex == EX_TEMPFAIL) ex = EX_TEMPFAIL; else if (! ex) ex = rex; } } } if (file && remove && !ex) unlink (file); exit (ex); }