/* * Copyright (c) 2008-2014, Simon Schubert <2@0x2c.org>. * Copyright (c) 2008 The DragonFly Project. All rights reserved. * * This code is derived from software contributed to The DragonFly Project * by Simon Schubert <2@0x2c.org>. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * 3. Neither the name of The DragonFly Project nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific, prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ #include "dfcompat.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "dma.h" extern int yyparse(void); extern FILE *yyin; static void deliver(struct qitem *); struct aliases aliases = LIST_HEAD_INITIALIZER(aliases); struct strlist tmpfs = SLIST_HEAD_INITIALIZER(tmpfs); struct authusers authusers = LIST_HEAD_INITIALIZER(authusers); char username[USERNAME_SIZE]; uid_t useruid; const char *logident_base; char errmsg[ERRMSG_SIZE]; static int daemonize = 1; static int doqueue = 0; struct config config = { .smarthost = NULL, .port = 25, .aliases = "/etc/aliases", .spooldir = "/var/spool/dma", .authpath = NULL, .certfile = NULL, .features = 0, .mailname = NULL, .masquerade_host = NULL, .masquerade_user = NULL, .fingerprint = NULL, }; static void sighup_handler(int signo) { (void)signo; /* so that gcc doesn't complain */ } static char * set_from(struct queue *queue, const char *osender) { const char *addr; char *sender; if (config.masquerade_user) { addr = config.masquerade_user; } else if (osender) { addr = osender; } else if (getenv("EMAIL") != NULL) { addr = getenv("EMAIL"); } else { addr = username; } if (!strchr(addr, '@')) { const char *from_host = hostname(); if (config.masquerade_host) from_host = config.masquerade_host; if (asprintf(&sender, "%s@%s", addr, from_host) <= 0) return (NULL); } else { sender = strdup(addr); if (sender == NULL) return (NULL); } if (strchr(sender, '\n') != NULL) { errno = EINVAL; return (NULL); } queue->sender = sender; return (sender); } static int read_aliases(void) { yyin = fopen(config.aliases, "r"); if (yyin == NULL) { /* * Non-existing aliases file is not a fatal error */ if (errno == ENOENT) return (0); /* Other problems are. */ return (-1); } if (yyparse()) return (-1); /* fatal error, probably malloc() */ fclose(yyin); return (0); } static int do_alias(struct queue *queue, const char *addr) { struct alias *al; struct stritem *sit; int aliased = 0; LIST_FOREACH(al, &aliases, next) { if (strcmp(al->alias, addr) != 0) continue; SLIST_FOREACH(sit, &al->dests, next) { if (add_recp(queue, sit->str, EXPAND_ADDR) != 0) return (-1); } aliased = 1; } return (aliased); } int add_recp(struct queue *queue, const char *str, int expand) { struct qitem *it, *tit; struct passwd *pw; char *host; int aliased = 0; it = calloc(1, sizeof(*it)); if (it == NULL) return (-1); it->addr = strdup(str); if (it->addr == NULL) return (-1); it->sender = queue->sender; host = strrchr(it->addr, '@'); if (host != NULL && (strcmp(host + 1, hostname()) == 0 || strcmp(host + 1, "localhost") == 0)) { *host = 0; } LIST_FOREACH(tit, &queue->queue, next) { /* weed out duplicate dests */ if (strcmp(tit->addr, it->addr) == 0) { free(it->addr); free(it); return (0); } } LIST_INSERT_HEAD(&queue->queue, it, next); /** * Do local delivery if there is no @. * Do not do local delivery when NULLCLIENT is set. */ if (strrchr(it->addr, '@') == NULL && (config.features & NULLCLIENT) == 0) { it->remote = 0; if (expand) { aliased = do_alias(queue, it->addr); if (!aliased && expand == EXPAND_WILDCARD) aliased = do_alias(queue, "*"); if (aliased < 0) return (-1); if (aliased) { LIST_REMOVE(it, next); } else { /* Local destination, check */ pw = getpwnam(it->addr); if (pw == NULL) goto out; /* XXX read .forward */ endpwent(); } } } else { it->remote = 1; } return (0); out: free(it->addr); free(it); return (-1); } static struct qitem * go_background(struct queue *queue) { struct sigaction sa; struct qitem *it; pid_t pid; if (daemonize && daemon(0, 0) != 0) { syslog(LOG_ERR, "can not daemonize: %m"); exit(EX_OSERR); } daemonize = 0; bzero(&sa, sizeof(sa)); sa.sa_handler = SIG_IGN; sigaction(SIGCHLD, &sa, NULL); LIST_FOREACH(it, &queue->queue, next) { /* No need to fork for the last dest */ if (LIST_NEXT(it, next) == NULL) goto retit; pid = fork(); switch (pid) { case -1: syslog(LOG_ERR, "can not fork: %m"); exit(EX_OSERR); break; case 0: /* * Child: * * return and deliver mail */ retit: /* * If necessary, acquire the queue and * mail files. * If this fails, we probably were raced by another * process. It is okay to be raced if we're supposed * to flush the queue. */ setlogident("%s", it->queueid); switch (acquirespool(it)) { case 0: break; case 1: if (doqueue) exit(EX_OK); syslog(LOG_WARNING, "could not lock queue file"); exit(EX_SOFTWARE); default: exit(EX_SOFTWARE); } dropspool(queue, it); return (it); default: /* * Parent: * * fork next child */ break; } } syslog(LOG_CRIT, "reached dead code"); exit(EX_SOFTWARE); } static void deliver(struct qitem *it) { int error; unsigned int backoff = MIN_RETRY, slept; struct timeval now; struct stat st; snprintf(errmsg, sizeof(errmsg), "unknown bounce reason"); retry: syslog(LOG_INFO, "<%s> trying delivery", it->addr); if (it->remote) error = deliver_remote(it); else error = deliver_local(it); switch (error) { case 0: syslog(LOG_INFO, "<%s> delivery successful", it->addr); delqueue(it); exit(EX_OK); case 1: if (stat(it->queuefn, &st) != 0) { syslog(LOG_ERR, "lost queue file `%s'", it->queuefn); exit(EX_SOFTWARE); } if (gettimeofday(&now, NULL) == 0 && (now.tv_sec - st.st_mtim.tv_sec > MAX_TIMEOUT)) { snprintf(errmsg, sizeof(errmsg), "Could not deliver for the last %d seconds. Giving up.", MAX_TIMEOUT); goto bounce; } for (slept = 0; slept < backoff;) { slept += SLEEP_TIMEOUT - sleep(SLEEP_TIMEOUT); if (flushqueue_since(slept)) { backoff = MIN_RETRY; goto retry; } } if (slept >= backoff) { /* pick the next backoff between [1.5, 2.5) times backoff */ backoff = backoff + backoff / 2 + random() % backoff; if (backoff > MAX_RETRY) backoff = MAX_RETRY; } goto retry; case -1: default: break; } bounce: bounce(it, errmsg); /* NOTREACHED */ } void run_queue(struct queue *queue) { struct qitem *it; if (LIST_EMPTY(&queue->queue)) return; it = go_background(queue); deliver(it); /* NOTREACHED */ } static void show_queue(struct queue *queue) { struct qitem *it; int locked = 0; /* XXX */ if (LIST_EMPTY(&queue->queue)) { printf("Mail queue is empty\n"); return; } LIST_FOREACH(it, &queue->queue, next) { printf("ID\t: %s%s\n" "From\t: %s\n" "To\t: %s\n", it->queueid, locked ? "*" : "", it->sender, it->addr); if (LIST_NEXT(it, next) != NULL) printf("--\n"); } } /* * TODO: * * - alias processing * - use group permissions * - proper sysexit codes */ int main(int argc, char **argv) { struct sigaction act; char *sender = NULL; char *own_name = NULL; struct queue queue; int i, ch; int nodot = 0, showq = 0, queue_only = 0, newaliases = 0; int recp_from_header = 0; set_username(); /* * We never run as root. If called by root, drop permissions * to the mail user. */ if (geteuid() == 0 || getuid() == 0) { struct passwd *pw; errno = 0; pw = getpwnam(DMA_ROOT_USER); if (pw == NULL) { if (errno == 0) errx(EX_CONFIG, "user '%s' not found", DMA_ROOT_USER); else err(EX_OSERR, "cannot drop root privileges"); } if (setuid(pw->pw_uid) != 0) err(EX_OSERR, "cannot drop root privileges"); if (geteuid() == 0 || getuid() == 0) errx(EX_OSERR, "cannot drop root privileges"); } atexit(deltmp); init_random(); bzero(&queue, sizeof(queue)); LIST_INIT(&queue.queue); own_name = basename(argv[0]); if (strcmp(own_name, "mailq") == 0) { argv++; argc--; showq = 1; if (argc != 0 && strcmp(argv[0], "-Ac") != 0) errx(EX_USAGE, "invalid arguments"); goto skipopts; } else if (strcmp(own_name, "newaliases") == 0) { newaliases = 1; goto skipopts; } else if (strcmp(own_name, "hoststat") == 0 || strcmp(own_name, "purgestat") == 0) { exit(EX_OK); } opterr = 0; while ((ch = getopt(argc, argv, ":A:b:B:C:d:Df:F:h:iL:N:no:O:q:r:R:tUV:vX:")) != -1) { switch (ch) { case 'A': /* -AX is being ignored, except for -A{c,m} */ if (optarg[0] == 'c' || optarg[0] == 'm') { break; } /* Else FALLTHROUGH */ case 'b': /* -bX is being ignored, except for -bp */ if (optarg[0] == 'p') { showq = 1; break; } else if (optarg[0] == 'q') { queue_only = 1; break; } /* Else FALLTHROUGH */ case 'D': daemonize = 0; break; case 'L': logident_base = optarg; break; case 'f': case 'r': sender = optarg; break; case 't': recp_from_header = 1; break; case 'o': /* -oX is being ignored, except for -oi */ if (optarg[0] != 'i') break; /* Else FALLTHROUGH */ case 'i': nodot = 1; break; case 'q': /* Don't let getopt slup up other arguments */ if (optarg && *optarg == '-') optind--; doqueue = 1; break; /* Ignored options */ case 'B': case 'C': case 'd': case 'F': case 'h': case 'N': case 'n': case 'O': case 'R': case 'U': case 'V': case 'v': case 'X': break; case ':': if (optopt == 'q') { doqueue = 1; break; } /* Else FALLTHROUGH */ default: fprintf(stderr, "invalid argument: `-%c'\n", optopt); exit(EX_USAGE); } } argc -= optind; argv += optind; opterr = 1; if (argc != 0 && (showq || doqueue)) errx(EX_USAGE, "sending mail and queue operations are mutually exclusive"); if (showq + doqueue > 1) errx(EX_USAGE, "conflicting queue operations"); skipopts: if (logident_base == NULL) logident_base = "dma"; setlogident(NULL); act.sa_handler = sighup_handler; act.sa_flags = 0; sigemptyset(&act.sa_mask); if (sigaction(SIGHUP, &act, NULL) != 0) syslog(LOG_WARNING, "can not set signal handler: %m"); parse_conf(CONF_PATH "/dma.conf"); if (config.authpath != NULL) parse_authfile(config.authpath); if (showq) { if (load_queue(&queue) < 0) errlog(EX_NOINPUT, "can not load queue"); show_queue(&queue); return (0); } if (doqueue) { flushqueue_signal(); if (load_queue(&queue) < 0) errlog(EX_NOINPUT, "can not load queue"); run_queue(&queue); return (0); } if (read_aliases() != 0) errlog(EX_SOFTWARE, "could not parse aliases file `%s'", config.aliases); if (newaliases) return(0); if ((sender = set_from(&queue, sender)) == NULL) errlog(EX_SOFTWARE, NULL); if (newspoolf(&queue) != 0) errlog(EX_CANTCREAT, "can not create temp file in `%s'", config.spooldir); setlogident("%s", queue.id); for (i = 0; i < argc; i++) { if (add_recp(&queue, argv[i], EXPAND_WILDCARD) != 0) errlogx(EX_DATAERR, "invalid recipient `%s'", argv[i]); } if (LIST_EMPTY(&queue.queue) && !recp_from_header) errlogx(EX_NOINPUT, "no recipients"); if (readmail(&queue, nodot, recp_from_header) != 0) errlog(EX_NOINPUT, "can not read mail"); if (LIST_EMPTY(&queue.queue)) errlogx(EX_NOINPUT, "no recipients"); if (linkspool(&queue) != 0) errlog(EX_CANTCREAT, "can not create spools"); /* From here on the mail is safe. */ if (config.features & DEFER || queue_only) return (0); run_queue(&queue); /* NOTREACHED */ return (0); }