1 /*
2  * Copyright 1998-2003 Ben Smithurst <ben@smithurst.org>
3  * All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  * 1. Redistributions of source code must retain the above copyright
9  *    notice, this list of conditions and the following disclaimer.
10  * 2. Redistributions in binary form must reproduce the above copyright
11  *    notice, this list of conditions and the following disclaimer in the
12  *    documentation and/or other materials provided with the distribution.
13  *
14  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
15  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17  * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
18  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
20  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
21  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
22  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
23  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
24  * SUCH DAMAGE.
25  */
26 
27 /*
28  * compress and rotate mailboxes when they get past a threshold size.
29  */
30 
31 static const char rcsid[] =
32 	"$BCPS: src/mailutils/rotatemail.c,v 1.74 2003/01/21 13:43:55 ben Exp $";
33 
34 #include "misc.h"
35 
36 int maildir_rotate(const char *, int, char *, char *);
37 int mbox_rotate(int, char *, char *);
38 int pmkdir(char *, mode_t);
39 static int wait_for_all(void);
40 void check_file(char *);
41 void usage(void);
42 
43 char *mail;
44 int mlen, slack = 5;
45 long max;
46 #define max_slack_high() (((100 + (slack)) * (max)) / 100)
47 #define max_slack_low()  (((100 - (slack)) * (max)) / 100)
48 int do_nothing;
49 int debug_level;
50 time_t now;
51 char date[16];
52 
53 int
main(int argc,char * argv[])54 main(int argc, char *argv[]) {
55 	TREE *tree;
56 	int ch;
57 	struct tm *tp;
58 	char *home, *path, newpath[1024];
59 
60 	umask(077);
61 
62 	/* catch SIGINT */
63 	signal(SIGINT, signal_handler);
64 	signal(SIGTERM, signal_handler);
65 
66 	/* default size for trimming */
67 	max = 1 << 20;
68 
69 	do_nothing = 0;
70 
71 	/* parse options */
72 	while ((ch = getopt(argc, argv, "dfm:ns")) != -1)
73 		switch (ch) {
74 		case 'd':
75 			debug_level++;
76 			break;
77 		case 'f':
78 			max = 0;
79 			break;
80 		case 'm':
81 			max = str_to_int(optarg);
82 			break;
83 		case 'n':
84 			do_nothing = 1; /* just show what would be done */
85 			break;
86 		case 's':
87 			slack = atoi(optarg);
88 			break;
89 		default:
90 			usage();
91 			/* NOTREACHED */
92 		}
93 
94 	argc -= optind;
95 	argv += optind;
96 
97 	if (time(&now) < 0)
98 		err(1, "time");
99 	if ((tp = localtime(&now)) == NULL)
100 		err(1, "localtime");
101 
102 	strftime(date, sizeof date, "%Y%m", tp);
103 
104 	if ((home = homedir()) == NULL)
105 		err(1, "couldn't get home directory");
106 	if ((mail = mailpath()) == NULL)
107 		err(1, "couldn't find a mail directory");
108 	mlen = strlen(mail);
109 
110 	/* add ~/bin to PATH */
111 	path = getenv("PATH");
112 	if (path == NULL)
113 		snprintf(newpath, sizeof newpath, "%s/bin", home);
114 	else if (path[0] == ':')
115 		snprintf(newpath, sizeof newpath, "%s/bin%s", home, path);
116 	else
117 		snprintf(newpath, sizeof newpath, "%s/bin:%s", home, path);
118 	setenv("PATH", newpath, 1);
119 
120 	tree = NULL;
121 	if (argc == 0)
122 		files_to_tree(&tree, mail);
123 	else
124 		array_to_tree(&tree, argv);
125 
126 	if (chdir(mail) < 0)
127 		err(1, "chdir: %s", mail);
128 
129 	check_tree(tree, check_file);
130 
131 	return (0);
132 }
133 
134 void
check_file(char * file)135 check_file(char *file) {
136 	char *name;
137 	char *p, *tmp;
138 	char argbuf[64];
139 	char dir[MAXPATHLEN];
140 	char tmpname[MAXPATHLEN];
141 	int fd = -1, st, tmp_fd;
142 	int len;
143 	pid_t pid;
144 	struct stat sb;
145 	struct timeval times[2];
146 	int maildir, size;
147 	long max2;
148 
149 	if (strncmp(file, mail, mlen) != 0 || file[mlen] != '/') {
150 		warnx("%s doesn't begin with %s prefix", file, mail);
151 		return;
152 	}
153 
154 	name = file + mlen + 1;
155 	len = strlen(name);
156 
157 	/* ignore filenames starting with "." */
158 	p = strrchr(name, '/');
159 	if (p == NULL)
160 		p = name;
161 	else
162 		p++;
163 	if (*p == '.')
164 		return;
165 
166 	/* /\.gz$/ */
167 	if (len >= 3 && strcmp(name + len - 3, ".gz") == 0)
168 		return;
169 
170 	/* Maildir? */
171 	maildir = is_maildir(file);
172 
173 	/* is this a temporary file, left around? */
174 	tmp = strstr(name, ".tmp.");
175 
176 	/*
177 	 * if not a temp file, see if it ends in all digits. Also
178 	 * ignore these files.
179 	 */
180 	if (tmp == NULL) {
181 		/* /\.\d+$/ */
182 		p = name + len - 1;
183 		while (p > name && isdigit(*p))
184 			p--;
185 		if (p < name + len - 1 && *p == '.')
186 			return;
187 	}
188 
189 	if (!maildir) {
190 		/*
191 		 * lock mailbox before renaming it out of the way.
192 		 * There was a potential race here: newmail didn't do any locking,
193 		 * so it was possible that newmail could have indicated no mail,
194 		 * but a message could have been appended before renaming the
195 		 * mailbox. Apply the lock before calling newmail, and pass the
196 		 * file descriptor to the newmail program, along with the -f option,
197 		 * so newmail doesn't need to do a separate open or lock of the
198 		 * file.
199 		 */
200 		fd = open(name, O_RDWR|O_APPEND);
201 		if (fd < 0) {
202 			warn("%s", name);
203 			return;
204 		}
205 
206 		/* check for signal */
207 		if (sig_count > 0) {
208 			close(fd);
209 			return;
210 		}
211 
212 		/* don't create dotlock for temp files */
213 		if (!mailboxlock(tmp ? NULL : name, 0, fd, LF_GET)) {
214 			warn("mailboxlock: %s", name);
215 			close(fd);
216 			return;
217 		}
218 
219 #define CLOSE_UNLOCK() do {					\
220 	if (!maildir) {						\
221 		mailboxlock(tmp ? NULL : name, 0, fd, LF_REL);	\
222 		close(fd);					\
223 	}							\
224 } while (0)
225 
226 		/* stat the mailbox to check things */
227 		if (fstat(fd, &sb) < 0) {
228 			warn("fstat: %s", name);
229 			CLOSE_UNLOCK();
230 			return;
231 		}
232 
233 		/* set up times[2] for utimes */
234 		times[0].tv_usec = times[1].tv_usec = 0;
235 		times[0].tv_sec = sb.st_atime;
236 		times[1].tv_sec = sb.st_mtime;
237 	}
238 
239 	/* for maildirs, rotate when the size is above max + slack%,
240 	 * and keep removing messages until the size is below
241 	 * max - slack%.  This avoids silly rotations where just one
242 	 * message is archived.
243 	 */
244 	if (maildir)
245 		max2 = max_slack_high();
246 	else
247 		max2 = max;
248 
249 	/* check whether to rotate it or not */
250 	size = mailbox_size(file, NULL);
251 	if (tmp != NULL) {
252 		if (do_nothing) {
253 			printf("%s is a temp file, would rotate\n", name);
254 			CLOSE_UNLOCK();
255 			return;
256 		} else if (debug_level)
257 			fprintf(stderr, "%s is a temp file, rotating\n", name);
258 	} else if (size >= max2) {
259 		if (do_nothing) {
260 			printf("%s size %d >= maximum size %lu, would rotate\n", name, size, max2);
261 			CLOSE_UNLOCK();
262 			return;
263 		} else if (debug_level)
264 			fprintf(stderr, "%s size %d >= maximum size %lu, rotating\n", name, size, max2);
265 	} else {
266 		if (debug_level > 1)
267 			fprintf(stderr, "%s not a temp file and size %d < max %lu, not rotating\n",
268 			  name, size, max2);
269 		CLOSE_UNLOCK();
270 		return;
271 	}
272 
273 	if (!maildir) {
274 		/* construct arguments for newmail */
275 		snprintf(argbuf, sizeof argbuf, "-sf%d", fd);
276 
277 		switch (pid = fork()) {
278 		case -1:
279 			warn("fork");
280 			mailboxlock(name, 0, fd, LF_REL);
281 			exit(1);
282 			/* NOTREACHED */
283 		case 0:
284 			execlp("newmail", "newmail", argbuf, NULL);
285 			_exit(127);
286 		default:
287 			break;
288 		}
289 
290 		if (waitpid(pid, &st, 0) < 0)
291 			err(1, "waitpid");
292 		else if (st != 0 && st != 512) {
293 			warnx("newmail: %d", st);
294 			CLOSE_UNLOCK();
295 			utimes(name, times);
296 			return;
297 		}
298 
299 		/*
300 		 * ignore "unread" mail in sent-mail, since it isn't really
301 		 * unread XXX hack
302 		 */
303 		if (strcmp(name, "sent-mail") != 0 && st == 0) {
304 			CLOSE_UNLOCK();
305 			if (utimes(name, times) < 0)
306 				warn("utimes: %s", name);
307 			if (debug_level)
308 				fprintf(stderr, "%s has unread mail, not rotating\n", name);
309 			return;
310 		}
311 	}
312 
313 	/* chop off ".tmp." onwards before making directory if a temp file */
314 	if (tmp) *tmp = '\0';
315 	snprintf(dir, sizeof dir, "archive/%s", name);
316 	if (tmp) *tmp = '.';
317 
318 	if (pmkdir(dir, 0700) < 0 && errno != EEXIST) {
319 		warn("mkdir: %s", dir);
320 		CLOSE_UNLOCK();
321 		utimes(name, times);
322 		return;
323 	}
324 
325 	/* check for signal */
326 	if (sig_count > 0) {
327 		CLOSE_UNLOCK();
328 		utimes(name, times);
329 		return;
330 	}
331 
332 	if (!maildir) {
333 		/*
334 		 * rename the file to a temporary filename.
335 		 */
336 		if (tmp == NULL) {
337 			snprintf(tmpname, sizeof tmpname, "%s.tmp.XXXXXXXX", name);
338 			tmp_fd = mkstemp(tmpname);
339 			if (tmp_fd < 0) {
340 				warn("mkstemp: %s", tmpname);
341 				CLOSE_UNLOCK();
342 				utimes(name, times);
343 				return;
344 			}
345 
346 			/* close file and rename mailbox on top of it */
347 			close(tmp_fd);
348 			if (rename(name, tmpname) < 0) {
349 				warn("rename: %s %s", name, tmpname);
350 				CLOSE_UNLOCK();
351 				utimes(name, times);
352 				return;
353 			}
354 
355 			/* create a new, empty mailbox */
356 			if ((tmp_fd = open(name, O_CREAT|O_WRONLY, 0600)) < 0)
357 				warn("open: %s", name);
358 			else
359 				close(tmp_fd);
360 
361 			/* remove the dotlock (not created for temp files) */
362 			mailboxlock(name, 0, -1, LF_REL);
363 		} else {
364 			if (strlen(name) >= sizeof tmpname) {
365 				errno = ENAMETOOLONG;
366 				warn("%s", name);
367 				return;
368 			}
369 			strcpy(tmpname, name);
370 		}
371 	}
372 
373 	/*
374 	 * strip un-needed headers, remove duplicate messages,
375 	 * and compress
376 	 */
377 	if (maildir)
378 		maildir_rotate(file, size, name, tmp);
379 	else {
380 		/* rewind the file */
381 		if (lseek(fd, 0, SEEK_SET) < 0)
382 			err(1, "lseek");
383 		if (mbox_rotate(fd, name, tmp) && unlink(tmpname) < 0)
384 			warn("unlink: %s", tmpname);
385 	}
386 
387 	wait_for_all();
388 }
389 
390 static int
wait_for_all(void)391 wait_for_all(void) {
392 	pid_t pid;
393 	int st, ok = 1;
394 
395 	while ((pid = waitpid(-1, &st, 0)) != -1)
396 		if (st != 0) {
397 			warnx("child %d exited with non-zero status %#x",
398 			  pid, st);
399 			ok = 0;
400 		}
401 
402 	/* Check for errors which shouldn't have happened */
403 	if (errno != ECHILD) {
404 		ok = 0;
405 		warn("waitpid");
406 	}
407 
408 	return (ok);
409 }
410 
411 int
mbox_rotate(int fd,char * name,char * tmp)412 mbox_rotate(int fd, char *name, char *tmp) {
413 	int pipe1[2], pipe2[2];
414 	char output[MAXPATHLEN];
415 
416 	if (pipe(pipe1) < 0)
417 		err(1, "pipe");
418 
419 	switch (fork()) {
420 	case -1:
421 		err(1, "fork");
422 	case 0:
423 		/* close un-needed descriptors */
424 		close(pipe1[0]);
425 
426 		/* connect the file to stdin */
427 		if (fd != STDIN_FILENO) {
428 			dup2(fd, STDIN_FILENO);
429 			close(fd);
430 		}
431 
432 		/* connect stdout to the pipe */
433 		if (pipe1[1] != STDOUT_FILENO) {
434 			dup2(pipe1[1], STDOUT_FILENO);
435 			close(pipe1[1]);
436 		}
437 
438 		execlp("mail-strip", "mail-strip", NULL);
439 		_exit(127);
440 	default:
441 		close(fd);	/* mailbox no longer needed in parent */
442 		close(pipe1[1]);
443 		break;
444 	}
445 
446 	if (pipe(pipe2) < 0)
447 		err(1, "pipe");
448 
449 	switch (fork()) {
450 	case -1:
451 		err(1, "fork");
452 	case 0:
453 		/* close un-needed descriptors */
454 		close(pipe2[0]);
455 
456 		/* connect pipes appropriately */
457 		if (pipe1[0] != STDIN_FILENO) {
458 			dup2(pipe1[0], STDIN_FILENO);
459 			close(pipe1[0]);
460 		}
461 
462 		if (pipe2[1] != STDOUT_FILENO) {
463 			dup2(pipe2[1], STDOUT_FILENO);
464 			close(pipe2[1]);
465 		}
466 
467 		execlp("de-dupe", "de-dupe", NULL);
468 		_exit(127);
469 	default:
470 		close(pipe1[0]);
471 		close(pipe2[1]);
472 	}
473 
474 	switch (fork()) {
475 	case -1:
476 		err(1, "fork");
477 	case 0:
478 		if (pipe2[0] != STDIN_FILENO) {
479 			dup2(pipe2[0], STDIN_FILENO);
480 			close(pipe2[0]);
481 		}
482 
483 		if (tmp) *tmp = '\0';
484 		snprintf(output, sizeof output,
485 		  "archive/%s/%s.gz", name, date);
486 
487 		close(STDOUT_FILENO);
488 		fd = open(output, O_CREAT|O_APPEND|O_WRONLY, 0600);
489 		if (fd < 0)
490 			err(1, "%s", output);
491 		else if (fd != STDOUT_FILENO)
492 			errx(1, "open didn't return STDOUT_FILENO");
493 
494 		/* lock the output file */
495 		if (!mailboxlock(NULL, 0, fd, LF_GET))
496 			err(1, "mailboxlock");
497 
498 		execlp("gzip", "gzip", NULL);
499 		_exit(127);
500 	default:
501 		close(pipe2[0]);
502 		break;
503 	}
504 
505 	return (1);
506 
507 }
508 
509 static int
numstrcmp(unsigned char * c1,unsigned char * c2)510 numstrcmp(unsigned char *c1, unsigned char *c2) {
511 	int n1, n2;
512 
513 	for ( ; *c1 && *c2; c1++, c2++) {
514 		if (isdigit(*c1) && isdigit(*c2)) {
515 			n1 = strtol(c1, (char **)&c1, 10);
516 			n2 = strtol(c2, (char **)&c2, 10);
517 			if (n1 - n2 != 0)
518 				return (n1 - n2);
519 			continue;
520 		}
521 		if (*c1 - *c2 != 0)
522 			return (*c1 - *c2);
523 	}
524 	return (*c1 - *c2);
525 }
526 
527 /* this is a kludge so that filenames like 'msg*' foo sort
528  * before correct maildir filenames like '<time>.<pid>..etc'.
529  */
530 static int
qsort_strcmp(const void * v1,const void * v2)531 qsort_strcmp(const void *v1, const void *v2) {
532 #define C(v) (*(unsigned char **)(uintptr_t)(v))
533 	unsigned char *c1 = C(v1), *c2 = C(v2);
534 
535 	/* either both start with number, or neither do */
536 	if (!!isdigit(*c1) == !!isdigit(*c2))
537 		return (numstrcmp(c1, c2));
538 
539 	if (isdigit(*c1))
540 		return (1);
541 	else
542 		return (-1);
543 }
544 
545 int
maildir_rotate(const char * file,int size,char * name,char * tmp)546 maildir_rotate(const char *file, int size, char *name, char *tmp) {
547 	int pfd[2];
548 	pid_t pid;
549 	char dirbuf[MAXPATHLEN], filebuf[MAXPATHLEN], tmpbuf[MAXPATHLEN], buf[1024], *fn;
550 	char **filelist = NULL;
551 	size_t flu = 0, fls = 0, d = 0;
552 	DIR *dp;
553 	struct dirent *dep = NULL;
554 	int fd, n, ok = 1, has_from;
555 	long max2;
556 	FILE *fp;
557 
558 	snprintf(dirbuf, sizeof dirbuf, "%s/cur", file);
559 	if ((dp = opendir(dirbuf)) == NULL) {
560 		warn("%s", dirbuf);
561 		return (0);
562 	}
563 	while ((dep = readdir(dp)) != NULL) {
564 		fn = dep->d_name;
565 
566 		if (fn[0] == '.' && (fn[1] == '\0' ||
567 		  (fn[1] == '.' && fn[2] == '\0')))
568 			continue;
569 
570 		if (flu >= fls) {
571 			if (flu == 0)
572 				fls = 16;
573 			else
574 				fls <<= 1;
575 			filelist = realloc(filelist, fls * sizeof (*filelist));
576 			if (filelist == NULL) { /* XXX */
577 				warn("realloc");
578 				return (0);
579 			}
580 		}
581 		filelist[flu++] = strdup(fn); /* XXX */
582 	}
583 	qsort(filelist, flu, sizeof *filelist, qsort_strcmp);
584 	if (debug_level > 2) {
585 		for (d = 0; d < flu; d++)
586 			fprintf(stderr, "filelist[%u]=%s\n", d, filelist[d]);
587 		return (1);
588 	}
589 	closedir(dp);
590 
591 	if (pipe(pfd) < 0)
592 		err(1, "pipe");
593 
594 	switch (pid = fork()) {
595 	case -1:
596 		err(1, "fork");
597 	case 0:
598 		close(pfd[1]);
599 		mbox_rotate(pfd[0], name, tmp);
600 		_exit(wait_for_all() ? EXIT_SUCCESS : EXIT_FAILURE);
601 	default:
602 		close(pfd[0]);
603 		break;
604 	}
605 
606 	if ((fp = fdopen(pfd[1], "w")) == NULL) {
607 		warn("fdopen on pipe fd");
608 		close(pfd[1]);
609 		return (0);
610 	}
611 
612 	/* see comment earlier */
613 	max2 = max_slack_low();
614 	while ((tmp != NULL || (size > max2)) && d < flu) {
615 		has_from = 0;
616 		fn = filelist[d++];
617 		snprintf(filebuf, sizeof filebuf, "%s/%s", dirbuf, fn);
618 		snprintf(tmpbuf, sizeof tmpbuf, "%s/tmp/%s", file, fn);
619 
620 		if (rename(filebuf, tmpbuf) != 0) { /* XXX */
621 			warn("rename %s -> %s", filebuf, tmpbuf);
622 			ok = 0;
623 			continue;
624 		}
625 
626 		if (debug_level)
627 			fprintf(stderr, "  continuing: size=%u, max2=%lu, archiving %s\n",
628 			  size, max2, fn);
629 
630 		if ((fd = open(tmpbuf, O_RDONLY)) < 0) {
631 			warn("%s", tmpbuf);
632 			ok = 0;
633 			continue;
634 		}
635 
636 #define FAKE_FROM "From root@localhost Tue Nov 26 13:48:38 2002\n"
637 #define STATUS_RO "Status: RO\n"
638 		while ((n = read(fd, buf, sizeof buf)) > 0) {
639 			if (!has_from) {
640 				if (strncmp(buf, "From ", 5) != 0)
641 					fwrite(FAKE_FROM, sizeof FAKE_FROM - 1, 1, fp);
642 				has_from = 1;
643 				fwrite(STATUS_RO, sizeof STATUS_RO - 1, 1, fp);
644 			}
645 			if (fwrite(buf, 1, n, fp) != n)
646 				break;
647 			size -= n;
648 		}
649 		fputc('\n', fp);
650 		if (ferror(fp)) {
651 			warn("error writing to pipe");
652 			fclose(fp);
653 			ok = 0;
654 			break;
655 		} else if (n < 0) {
656 			warn("%s", tmpbuf);
657 			close(fd);
658 			ok = 0;
659 			continue;
660 		} else {
661 			close(fd);
662 			/* XXX unlink(tmpbuf); */
663 		}
664 	}
665 	if (debug_level)
666 		fprintf(stderr, "stopping: size=%u max2=%lu tmp=%p d=%u/%u\n",
667 		  size, max2, tmp, d, flu);
668 	fclose(fp);
669 
670 	return (ok);
671 }
672 
673 /* mkdir, including all parents, like mkdir -p */
674 int
pmkdir(char * name,mode_t mode)675 pmkdir(char *name, mode_t mode) {
676 	char *p;
677 
678 	for (p = name; (p = strchr(p, '/')) != NULL; p++) {
679 		*p = '\0';
680 		if (mkdir(name, mode) < 0 && errno != EEXIST)
681 			return (-1);
682 		*p = '/';
683 	}
684 
685 	return (mkdir(name, mode));
686 }
687 
688 void
usage(void)689 usage(void) {
690 	fprintf(stderr, "usage: rotatemail [-f] [-m max] [-n] [mailbox ...]\n");
691 	exit(EX_USAGE);
692 }
693