/* * Copyright 1998-2002 Ben Smithurst * All rights reserved. * * 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. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. */ /* * Miscellaneous routines shared by multiple programs. */ static const char rcsid[] = "$BCPS: src/mailutils/misc.c,v 1.75 2003/01/19 19:18:25 ben Exp $"; #include "misc.h" int quiet = -1; volatile sig_atomic_t sig_count = 0; char *curfile = NULL; static int maildir_seq; void signal_handler(int sig) { if (++sig_count > 2) exit(2); } char * print_mailbox(const char *name) { int maildir = is_maildir(name); int mlen = strlen(mailpath()); int hlen = strlen(homedir()); static char buf[1024]; const char *pre, *suf; if (strncmp(name, mailpath(), mlen) == 0 && name[mlen] == '/') { pre = "="; name += mlen + 1; } else if (strncmp(name, homedir(), hlen) == 0 && name[hlen] == '/') { pre = "~"; name += hlen; } else pre = ""; if (maildir) suf = "/"; else suf = ""; snprintf(buf, sizeof buf, "%s%s%s", pre, name, suf); return (buf); } static char * hostname(void) { static char host[MAXHOSTNAMELEN]; if (host[0] == '\0') { if (gethostname(host, sizeof host - 1) < 0) return (NULL); host[sizeof host - 1] = '\0'; } return (host); } #define GLT_MAX 10 #define GLT_BUF 1024 gl_getline_t glts[GLT_MAX]; int glcount = 0; char * gl_getline(FILE *fp) { gl_getline_t *glp; int i, n; for (i = 0; i < glcount; i++) if (glts[i].fp == fp || glts[i].fp == NULL) break; if (i == glcount || glts[i].fp == NULL) { if (glcount == GLT_MAX) { warnx("glcount==GLT_MAX"); errno = ENOMEM; return (NULL); } glts[i].fp = fp; if (i == glcount) { if ((glts[i].buf = malloc(GLT_BUF)) == NULL) { warnx("can't malloc %d bytes for glt.buf", GLT_BUF); return (NULL); } glts[i].size = GLT_BUF; glts[i].eof = 0; glcount++; } } glp = &glts[i]; if (glp->eof) return (NULL); n = 0; for (;;) { char *p; if (glp->size - n < GLT_BUF) { glp->size += GLT_BUF; if ((p = realloc(glp->buf, glp->size)) == NULL) { warnx("can't realloc glp->buf to %d", glp->size); gl_destroy(glp->fp); return (NULL); } else glp->buf = p; } p = fgets(glp->buf + n, glp->size - n, glp->fp); if (p == NULL) { glp->eof = 1; if (n == 0) return (NULL); glp->buf[n] = '\n'; glp->buf[n+1] = '\0'; return (glp->buf); } n += strlen(p); if (glp->buf[n-1] == '\n') return (glp->buf); } } void gl_destroy(FILE *fp) { int i; for (i = 0; i < glcount; i++) if (glts[i].fp == fp) { glts[i].fp = NULL; return; } } /* * This function would be much easier to write by just using a regex, but * that would probably require linking with the PCRE library. It's not hard * to do manually... honest. * * XXX this will fail on addresses with a space in them (e.g., "foo * bar"@domain.example.com). */ int is_from(char *line, int strict) { /* this function attempts to check whether the specified line matches * this regular expression: * * "^From\\s+(\\S+)\\s+(?:[a-zA-Z]{3},?\\s+)?" / Common start * "(?:" / Non-extracting bracket * "[a-zA-Z]{3}\\s+\\d?\\d|" / First form * "\\d?\\d\\s+[a-zA-Z]{3}\\s+\\d\\d(?:\\d\\d)?" / Second form * ")" / End alternation * "\\s+\\d\\d?:\\d\\d?"; / Start of time */ /* Check for From\s */ if (strncmp(line, "From", 4) != 0 || !isspace(line[4])) return (0); else if (!strict) return (1); /* Now skip spaces, non-spaces (address), and more spaces. */ line += 5; while (isspace(*line)) line++; while (*line != '\0' && !isspace(*line)) line++; while (isspace(*line)) line++; if (*line == '\0') return (0); /* Check for three letters. This must be followed by either a comma * or a space, and could either be the month name or the weekday * name. */ if (isalpha(line[0]) && isalpha(line[1]) && isalpha(line[2])) { if (line[3] == ',') line += 4; else if (!isspace(line[3])) return (0); else line += 3; while (isspace(*line)) line++; if (*line == '\0') return (0); /* we have matched three letters. They are either a weekday * name or a month name. If the next character is another * letter, it was a weekday name, and is followed by a * month. (first form.) Otherwise, we have to do a bit more * work. There should be a digit next, if not a letter, * followed by some spaces. After the spaces, the next * character will determine which of the two alternatives to * take. */ if (isalpha(*line)) { if (!isalpha(line[1]) || !isalpha(line[2])) return (0); line += 3; while (isspace(*line)) line++; if (!isdigit(*line++)) return (0); if (isdigit(*line)) line++; } else if (!isdigit(*line++)) return (0); else { if (isdigit(*line)) line++; if (!isspace(*line++)) return (0); while (isspace(*line)) line++; if (*line == '\0') return (0); if (isalpha(*line)) goto second_form; else if (isdigit(*line++)) /* was first form all along */ goto near_end; else return (0); } } else { /* the second form */ if (!isdigit(*line++)) return (0); if (isdigit(*line)) line++; while (isspace(*line)) line++; second_form: if (!isalpha(line[0]) || !isalpha(line[1]) || !isalpha(line[2])) return (0); line += 3; while (isspace(*line)) return (0); if (!isdigit(line[0]) || !isdigit(line[1])) return (0); line += 2; if (isdigit(line[0]) && isdigit(line[1])) line += 2; } while (isspace(*line)) line++; if (!isdigit(*line++)) return (0); near_end: if (isdigit(*line)) line++; if (*line != ':') return (0); line++; if (!isdigit(*line)) return (0); return (1); } static char * maildir_nextfile(char *dir) { static int done_new; static DIR *dp; struct dirent *dep; static const char *subdir; static char file[MAXPATHLEN]; if (dir == NULL) { done_new = 0; if (dp != NULL) { closedir(dp); dp = NULL; } return (NULL); } if (dp == NULL) { subdir = done_new ? "cur" : "new"; if ((dp = opendir(subdir)) == NULL) { warn("opendir %s/%s", dir, subdir); return (NULL); } } while ((dep = readdir(dp)) != NULL) if (dep->d_name[0] != '.') { snprintf(file, sizeof file, "%s/%s", subdir, dep->d_name); return (file); } if (done_new) return (NULL); closedir(dp); dp = NULL; done_new = 1; return (maildir_nextfile(dir)); } static int process_maildir(char *file, int (*func)(int, char *, FILE *)) { int ok = 1, fd, reset = 1, ret; FILE *fp; char *fn; if ((fd = open(".", O_RDONLY)) < 0) { warn("open ."); return (0); } if (chdir(file) != 0) { warn("chdir %s", file); return (0); } maildir_nextfile(NULL); while (ok && (fn = maildir_nextfile(file)) != NULL) { if (sig_count > 0) errx(1, "exiting because of signal"); if ((fp = fopen(fn, "r")) == NULL) { warn("%s/%s", file, fn); ok = 0; break; } switch (ret = func(reset, fn, fp)) { case RET_ERROR: warn("maildir %s file %s", file, fn); /* FALLTHROUGH */ case RET_LOCALERROR: ok = 0; break; case RET_NOCHANGE: /* nothing to do */ break; case RET_DELETE: if (unlink(fn) != 0) { warn("unlink %s/%s", file, fn); ok = 0; } break; default: warnx("unknown return %d for %s/%s", ret, file, fn); ok = 0; break; } fclose(fp); reset = 0; } maildir_nextfile(NULL); if (fchdir(fd) != 0) { warn("fchdir"); return (0); } return (1); } int process(char **argv, int (*func)(FILE *, FILE *), int (*maildir_func)(int, char *, FILE *)) { int ok, i, fd, tmp; FILE *in_fp, *out_fp; char tmpname[MAXPATHLEN]; struct stat sb; struct timeval times[2]; ok = 1; times[0].tv_usec = times[1].tv_usec = 0; /* process from stdin to stdout if no arguments */ if (argv[0] == NULL) { curfile = "stdin"; switch (func(stdin, stdout)) { case RET_ERROR: warnx("%s", strerror(errno)); /* FALLTHROUGH */ case RET_LOCALERROR: return (0); default: return (1); } } for (i = 0; argv[i] != NULL; i++) { curfile = argv[i]; if (is_maildir(curfile)) { if (maildir_func != NULL) { if (!process_maildir(curfile, maildir_func)) ok = 0; } else { warnx("%s: is a Maildir (not implemented yet)", curfile); ok = 0; } continue; } if ((in_fp = fopen(argv[i], "r+")) == NULL) { warn("%s", argv[i]); ok = 0; continue; } fd = fileno(in_fp); /* some things need raw fd */ snprintf(tmpname, sizeof tmpname, "%s.tmp.XXXXXXXX", argv[i]); if ((tmp = mkstemp(tmpname)) < 0) { warn("mkstemp: %s", tmpname); fclose(in_fp); ok = 0; continue; } /* attach temp file to FILE* */ if ((out_fp = fdopen(tmp, "w")) == NULL) { warn("fdopen: %d (%s)", tmp, tmpname); fclose(in_fp); close(tmp); unlink(tmpname); ok = 0; continue; } if (!mailboxlock(argv[i], 0, fd, LF_GET)) { warn("mailboxlock: %s", argv[i]); fclose(in_fp); fclose(out_fp); unlink(tmpname); ok = 0; continue; } if (sig_count > 0) { unlink(tmpname); mailboxlock(argv[i], 0, -1, LF_REL); errx(1, "exiting because of signal"); } /* * get the current atime/mtime, to put back if we change the * file. */ if (fstat(fd, &sb) < 0) { warn("fstat: %d (%s)", fd, argv[i]); fclose(in_fp); fclose(out_fp); unlink(tmpname); mailboxlock(argv[i], 0, -1, LF_REL); ok = 0; continue; } switch (func(in_fp, out_fp)) { case RET_ERROR: warn("%s", argv[i]); /* FALLTHROUGH */ case RET_LOCALERROR: ok = 0; /* FALLTHROUGH */ case RET_NOCHANGE: fclose(in_fp); fclose(out_fp); mailboxlock(argv[i], 0, -1, LF_REL); unlink(tmpname); break; case RET_CHANGE: if (fclose(out_fp) != 0) { warn("fclose %s", tmpname); ok = 0; } fclose(in_fp); /* * at this stage, the fcntl lock is no * longer in effect, but the .lock file is, * or should be. */ if (rename(tmpname, argv[i]) < 0) { warn("rename: %s %s", tmpname, argv[i]); ok = 0; } times[0].tv_sec = sb.st_atime; times[1].tv_sec = sb.st_mtime; if (utimes(argv[i], times) < 0) warn("utimes: %s", argv[i]); mailboxlock(argv[i], 0, -1, LF_REL); break; } if (sig_count > 0) errx(1, "exiting because of signal"); } return (ok); } char * inbox_check(char *file) { if (strcmp(file, "INBOX") != 0) return (file); else return (user_inbox()); } /* * Get user's inbox. */ char * user_inbox(void) { char *e; struct passwd *pw; static char buf[MAXPATHLEN]; if ((e = getenv("MAIL")) != NULL) return (e); if ((pw = getpwuid(getuid())) == NULL) return (NULL); if (snprintf(buf, sizeof buf, "%s/%s", _PATH_MAILDIR, pw->pw_name) >= sizeof buf) return (NULL); else return (buf); } /* * Get the user's home directory. */ char * homedir(void) { struct passwd *pw; char *home; static char buf[MAXPATHLEN]; if (buf[0] == '\0') { if ((home = getenv("HOME")) != NULL) strlcpy(buf, home, sizeof buf); else if ((pw = getpwuid(getuid())) != NULL) strlcpy(buf, pw->pw_dir, sizeof buf); else errx(1, "couldn't find your home directory"); } return (buf); } /* * Get user's mail directory: checks for ~/mail and ~/Mail. */ char * mailpath(void) { char *mail; char *env; int len, i; struct stat sb; const char *home = homedir(); static char buf[MAXPATHLEN]; if (buf[0] != '\0') return (buf); if ((env = getenv("MAILUTILS_DIR")) != NULL) { strlcpy(buf, env, sizeof buf); return (buf); } len = strlen(home); mail = malloc(len + sizeof "/mail"); if (mail == NULL) errx(1, "malloc for mailpath failed"); for (i = 0; i < 2; i++) { /* build the path, space will be replaced with 'm' or 'M' */ strcpy(mail, home); strcat(mail, "/ ail"); if (i == 0) mail[len + 1] = 'm'; else mail[len + 1] = 'M'; if (stat(mail, &sb) == 0 && S_ISDIR(sb.st_mode)) { strlcpy(buf, mail, sizeof buf); return (buf); } } errx(1, "couldn't find your mail directory"); } unsigned int str_to_int(char *p) { unsigned int ret; char *end; ret = (unsigned int)strtol(p, &end, 0); /* Cope with the G, M and K suffixes. */ switch (*end) { case 'g': case 'G': if (ret > UINT_MAX / 1024) return (UINT_MAX); ret *= 1024; /* FALLTHROUGH */ case 'm': case 'M': if (ret > UINT_MAX / 1024) return (UINT_MAX); ret *= 1024; /* FALLTHROUGH */ case 'k': case 'K': if (ret > UINT_MAX / 1024) return (UINT_MAX); ret *= 1024; end++; break; case '\0': break; default: warnx("str_to_int: ignoring tailing garbage `%s'", end); } return (ret); } void free_tree(TREE *p) { if (p == NULL) return; free_tree(p->left); free_tree(p->right); free(p->file); free(p); } void check_tree(TREE *tp, void (*check_file)(char *)) { if (sig_count > 0 || tp == NULL) return; check_tree(tp->left, check_file); if (sig_count > 0) return; check_file(tp->file); check_tree(tp->right, check_file); } /* add string to a tree */ void add_to_tree(TREE **tp, char *str) { if (tp == NULL) errx(1, "tp == NULL in add_to_tree()"); if (*tp == NULL) { MALLOC(*tp, sizeof **tp); MALLOC((*tp)->file, strlen(str) + 1); strcpy((*tp)->file, str); (*tp)->left = (*tp)->right = NULL; return; } if (strcmp((*tp)->file, str) < 0) add_to_tree(&(*tp)->right, str); else add_to_tree(&(*tp)->left, str); return; } /* * convert an array (e.g. from argv) to the binary tree * format needed. */ void array_to_tree(TREE **tp, char **array) { while (*array) { add_to_tree(tp, *array); array++; } } /* * walk through the directory tree starting at dir, adding all * regular files to the specified tree. */ void files_to_tree(TREE **tp, const char *dir) { DIR *dirp; struct dirent *dp; char buf[MAXPATHLEN]; int len; if (sig_count > 0) return; if ((dirp = opendir(dir)) == NULL) err(1, "%s", dir); while ((dp = readdir(dirp)) != NULL) { struct stat sb; /* skip files starting with a dot */ if (dp->d_name[0] == '.') continue; /* skip *.gz, *.lock, *.lock.* */ len = strlen(dp->d_name); if ((len >= 3 && strcmp(dp->d_name + len - 3, ".gz") == 0) || (len >= 5 && strcmp(dp->d_name + len - 5, ".lock") == 0) || strstr(dp->d_name, ".lock.") != NULL) continue; snprintf(buf, sizeof buf, "%s/%s", dir, dp->d_name); if (stat(buf, &sb) != 0) { warn("stat of %s failed", buf); continue; } if (is_maildir(buf) || S_ISREG(sb.st_mode)) add_to_tree(tp, buf); else if (S_ISDIR(sb.st_mode)) { if (strcmp(dp->d_name, "archive") == 0) continue; files_to_tree(tp, buf); } else warnx("%s not regular or directory (%#o), skipping", dp->d_name, sb.st_mode); } closedir(dirp); } int getlock(int fd, int type) { struct flock fl; fl.l_start = 0; fl.l_whence = SEEK_SET; fl.l_len = 0; fl.l_pid = getpid(); fl.l_type = type; return (fcntl(fd, F_SETLK, &fl)); } int mailboxlock(char *name, int maildir, int fd, int op) { int tries; int delay; int tmpfd; char hitchpost[MAXPATHLEN], lockfile[MAXPATHLEN]; char newfile[MAXPATHLEN], *s; struct stat sb; time_t now; int gotlock = 0; int soft = 0; if (maildir) { /* kludge, so that releasing a lock on a /tmp/ file in a maildir * will move it to /new/. This is used in conjunction with * open_mailbox really. */ if (fd != -1 || op != LF_REL) { errno = EINVAL; warnx("mailboxlock(%s, %d, %d, %d) called", name, maildir, fd, op); return (0); } /* check for "/tmp/" */ if (strlen(name) >= sizeof newfile || (s = strrchr(name, '/')) == NULL || s < name + 4 || s[-4] != '/' || s[-3] != 't' || s[-2] != 'm' || s[-1] != 'p') { errno = EINVAL; warnx("mailboxlock: %s: bad filename", name); return (0); } /* copy, changing "tmp" to "new" */ strcpy(newfile, name); memcpy(newfile + (s - name) - 3, "new", 3); return (rename(name, newfile) == 0); } if (op == LF_GET_SOFT) { op = LF_GET; soft = 1; } if (name) { snprintf(hitchpost, sizeof hitchpost, "%s.lock.%s.%lx.%lx.XXXXXXXX", name, hostname(), (unsigned long)getpid(), (unsigned long)time(NULL)); snprintf(lockfile, sizeof lockfile, "%s.lock", name); } if (op == LF_REL) { if (fd >= 0) getlock(fd, F_UNLCK); if (name) unlink(lockfile); return (1); } if (fd < 0) { errno = EBADF; return (0); } delay = 200000; for (tries = 0; tries < 10; tries++) { if (getlock(fd, F_WRLCK) != -1) { gotlock = 1; break; } if (errno != EAGAIN) break; if (sig_count > 0) return (0); if (tries == 4) delay = 2000000; usleep(delay); } if (name) { /* create a hitching post */ if ((tmpfd = mkstemp(hitchpost)) < 0) return (gotlock); close(tmpfd); /* get current time */ if (time(&now) < 0) { unlink(hitchpost); return (gotlock); } /* check for stale lock -- race condition here? XXX */ if (stat(lockfile, &sb) == 0 && now - sb.st_ctime > 1800) unlink(lockfile); /* attempt a link to the real lockfile */ tries = 0; delay = 200000; while (link(hitchpost, lockfile) < 0) { if (tries == 4) delay = 2000000; if (sig_count > 0 || ++tries == 10 || errno != EEXIST) { unlink(hitchpost); return (gotlock); } /* * check hitchpost link count, the link() may have * succeeded after all, if the link count == 2. */ if (stat(hitchpost, &sb) < 0) { /* shouldn't happen */ unlink(hitchpost); return (gotlock); } if (sb.st_nlink == 2) break; usleep(delay); } /* remove hitching post */ unlink(hitchpost); } /* success */ return (gotlock); } FILE * open_mailbox(char *file, char **ret, int *maildir) { int fd, sverrno; FILE *fp; char buf[MAXPATHLEN]; file = inbox_check(file); if (file == NULL) return (NULL); *maildir = (is_maildir(file)); if (*maildir) { snprintf(buf, sizeof buf, "%s/tmp/%lu.%d_%d.%s", file, (unsigned long)time(NULL), (int)getpid(), maildir_seq++, hostname()); if ((fd = open(buf, O_CREAT|O_EXCL|O_WRONLY, 0600)) < 0) return (NULL); if ((*ret = strdup(buf)) == NULL) { sverrno = errno; close(fd); unlink(buf); errno = sverrno; return (NULL); } if ((fp = fdopen(fd, "w")) == NULL) { sverrno = errno; close(fd); unlink(buf); free(*ret); errno = sverrno; return (NULL); } return (fp); } fd = open(file, O_WRONLY|O_CREAT, 0600); if (fd < 0) return (NULL); if (!mailboxlock(file, 0, fd, LF_GET)) { sverrno = errno; close(fd); errno = sverrno; return (NULL); } *ret = malloc(strlen(file) + 1); if (*ret == NULL) { sverrno = errno; close(fd); errno = sverrno; return (NULL); } else strcpy(*ret, file); fp = fdopen(fd, "w"); if (fp == NULL) { sverrno = errno; free(*ret); close(fd); errno = sverrno; return (NULL); } if (fseek(fp, 0, SEEK_END) < 0) { sverrno = errno; free(*ret); fclose(fp); errno = sverrno; return (NULL); } return (fp); } static int _checkdir(const char *d, const char *e) { char buf[MAXPATHLEN]; struct stat sb; snprintf(buf, sizeof buf, "%s/%s", d, e); return (stat(buf, &sb) == 0 && S_ISDIR(sb.st_mode)); } int is_maildir(const char *d) { return (_checkdir(d, "new") && _checkdir(d, "tmp") && _checkdir(d, "cur")); } static int _dirsize(const char *d, const char *e) { char dirbuf[MAXPATHLEN], filebuf[MAXPATHLEN]; DIR *dp; struct dirent *dep; struct stat sb; int total = 0; snprintf(dirbuf, sizeof dirbuf, "%s/%s", d, e); if ((dp = opendir(dirbuf)) == NULL) return (-1); while ((dep = readdir(dp)) != NULL) { snprintf(filebuf, sizeof filebuf, "%s/%s", dirbuf, dep->d_name); if (stat(filebuf, &sb) != 0) { closedir(dp); return (-1); } else if (S_ISREG(sb.st_mode)) total += (int)sb.st_size; } closedir(dp); return (total); } int mailbox_size(const char *d, int *num) { struct stat sb; int n, c; if (!is_maildir(d)) { if (stat(d, &sb) != 0) return (-1); else return ((int)sb.st_size); } if ((n = _dirsize(d, "new")) < 0) return (-1); if ((c = _dirsize(d, "cur")) < 0) return (-1); return (n + c); } void xerr(int code, char *fmt, ...) { va_list ap; if (quiet) exit(EX_TEMPFAIL); va_start(ap, fmt); verr(code, fmt, ap); } void xerrx(int code, char *fmt, ...) { va_list ap; if (quiet) exit(EX_TEMPFAIL); va_start(ap, fmt); verrx(code, fmt, ap); } void xfprintf(FILE *fp, char *fmt, ...) { va_list ap; va_start(ap, fmt); if (vfprintf(fp, fmt, ap) < 0) xerr(1, "vfprintf"); va_end(ap); }