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