1 /*
2 NAME
3 mbox2dir - convert mbox to maildir or MH format
4
5 SYNOPSIS
6 mbox2dir [-i FILE] [-h NAME] [-v VALUE] [-npu] PATH MBOX [NAMES...]
7 mbox2dir -m PATH MBOX [NAMES...]
8
9 DESCRIPTION
10 Creates a maildir mailbox in PATH and populates it with the
11 messages from the UNIX mbox file MBOX.
12
13 If NAMES arguments are given, each successive message from MBOX
14 is stored in the file named with the corresponding NAME.
15
16 Otherwise, if the -i option is given, message names are read from
17 FILE, which must list single message name on each line.
18
19 In both cases, each name must begin with "new/" or "cur/" directory
20 prefix.
21
22 If nether -i, nor NAMES are given, message names will be generated
23 automatically in the "cur" subdirectory in PATH. Name generation
24 is governed by three options. The -n option instructs mbox2dir to
25 store messages in "new", instead of "cur". The -u option stores
26 the UID of each message in the message file name. Obviously, -n
27 and -u cannot be used together. The -h option supplies the hostname
28 to use in names (default is "localhost").
29
30 The -m option instructs the program to create MH mailbox.
31
32 OPTIONS
33
34 -h NAME
35 Hostname for use in maildir message names.
36
37 -i FILE
38 Read message file names from FILE (one name per line). Convert
39 as many messages as there are names in FILE.
40
41 -m
42 Create mailbox in MH format.
43
44 -n
45 Store messages in the "new" subdirectory.
46
47 -p
48 Create the ".mu-prop" file. Unless -v is given (see below),
49 the uidvalidity value will be set to the number of seconds since
50 Epoch.
51
52 -u
53 Add UID to the message names.
54
55 -v VALUE
56 Set uidvalidity to VALUE. Implies -p.
57
58 AUTHOR
59 Sergey Poznyakoff <gray@gnu.org>
60
61 BUGS
62 The program is not intended for production use, therefore these
63 are rather features, and intentional ones, too. Anyway:
64
65 1. PATH may not exit.
66 2. Buffer space for message name generation is of fixed size.
67 3. Default hostname is "localhost", instead of the actual machine
68 name.
69 4. Random number generator is not initialized.
70
71 LICENSE
72 This program is part of GNU Mailutils testsuite.
73 Copyright (C) 2020-2021 Free Software Foundation, Inc.
74
75 Mbox2dir is free software; you can redistribute it and/or modify
76 it under the terms of the GNU General Public License as published by
77 the Free Software Foundation; either version 3, or (at your option)
78 any later version.
79
80 Mbox2dir is distributed in the hope that it will be useful,
81 but WITHOUT ANY WARRANTY; without even the implied warranty of
82 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
83 GNU General Public License for more details.
84
85 You should have received a copy of the GNU General Public License
86 along with GNU Mailutils. If not, see <http://www.gnu.org/licenses/>.
87 */
88 #include <config.h>
89 #include <stdlib.h>
90 #include <stdio.h>
91 #include <string.h>
92 #include <errno.h>
93 #include <unistd.h>
94 #include <sys/stat.h>
95 #include <sys/types.h>
96 #include <fcntl.h>
97 #include <sysexits.h>
98 #include <assert.h>
99 #include <sys/time.h>
100
101 static char *progname;
102
103 enum
104 {
105 F_MAILDIR,
106 F_MH
107 };
108
109 int format = F_MAILDIR;
110 int new_option;
111 int uid_option;
112 int prop_option;
113 char *hostname = "localhost";
114 char propfile[] = ".mu-prop";
115 unsigned long uidvalidity = 0;
116
117
118 enum
119 {
120 INPUT_UNDEF = -1,
121 INPUT_GENERATE,
122 INPUT_ARGV,
123 INPUT_FILE
124 };
125
126 typedef struct name_input
127 {
128 int type;
129 union
130 {
131 struct /* Structure for INPUT_FILE */
132 {
133 FILE *file; /* Input file */
134 char *bufptr; /* Input buffer */
135 size_t bufsize; /* Buffer size */
136 };
137 struct /* Structure for INPUT_ARGV */
138 {
139 int argc; /* Number of arguments */
140 char **argv; /* Argument vector */
141 int idx; /* Index of the next argument in argv */
142 };
143 unsigned long uid; /* Last assigned UID - for INPUT_GENERATE */
144 };
145 } NAME_INPUT;
146
147
148 struct format_info
149 {
150 void (*init_dir) (int, char const *);
151 char *(*generate_name) (NAME_INPUT *);
152 void (*validate_name) (char const *);
153 };
154
155 static char *mh_generate_name (NAME_INPUT *);
156 static char *maildir_generate_name (NAME_INPUT *);
157 static void maildir_init_dir (int, char const *);
158 static void mh_validate_name (char const *name);
159 static void maildir_validate_name (char const *name);
160
161 static struct format_info format_info[] = {
162 { maildir_init_dir, maildir_generate_name, maildir_validate_name },
163 { NULL, mh_generate_name, mh_validate_name },
164 };
165
166 static void
mh_validate_name(char const * name)167 mh_validate_name (char const *name)
168 {
169 if (name [strspn (name, "0123456789")])
170 {
171 fprintf (stderr, "%s: name must be numeric: %s\n",
172 progname, name);
173 exit (EX_DATAERR);
174 }
175 }
176
177 static char *
mh_generate_name(NAME_INPUT * input)178 mh_generate_name (NAME_INPUT *input)
179 {
180 unsigned long n = input->uid;
181 char buf[11];
182 // char dig[] = "0123456789";
183 char *p = buf + sizeof (buf);
184 *--p = 0;
185 do
186 {
187 if (p == buf)
188 abort ();
189 *--p = n % 10 + '0';
190 }
191 while ((n /= 10) != 0);
192 return strdup (p);
193 }
194
195 static char *subdirs[] = { "cur", "new", "tmp" };
196
197 static int
starts_with_subdir(char const * name)198 starts_with_subdir (char const *name)
199 {
200 int i;
201 size_t n = strlen (name);
202
203 for (i = 0; i < sizeof (subdirs) / sizeof (subdirs[0]); i++)
204 {
205 size_t l = strlen (subdirs[i]);
206 if (n > l + 1 && memcmp (name, subdirs[i], l) == 0 && name[l] == '/')
207 return 1;
208 }
209 return 0;
210 }
211
212 static void
maildir_init_dir(int fd,char const * name)213 maildir_init_dir (int fd, char const *name)
214 {
215 int i;
216
217 for (i = 0; i < sizeof (subdirs) / sizeof (subdirs[0]); i++)
218 {
219 if (mkdirat (fd, subdirs[i], 0755))
220 {
221 fprintf (stderr, "%s: can't create %s/%s: %s\n",
222 progname, name, subdirs[i], strerror (errno));
223 exit (EX_CANTCREAT);
224 }
225 }
226 }
227
228 static char *
maildir_generate_name(NAME_INPUT * input)229 maildir_generate_name (NAME_INPUT *input)
230 {
231 char buf[4096];
232 size_t i = 0;
233 struct timeval tv;
234
235 i += snprintf (buf + i, sizeof (buf) - i, "%s/", new_option ? "new" : "cur");
236 assert (i < sizeof (buf));
237
238 gettimeofday (&tv, NULL);
239 i += snprintf (buf + i, sizeof (buf) - i, "%lu", (unsigned long) tv.tv_sec);
240 assert (i < sizeof (buf));
241
242 i += snprintf (buf + i, sizeof (buf) - i, ".R%lu", random ());
243 assert (i < sizeof (buf));
244
245 i += snprintf (buf + i, sizeof (buf) - i, "M%lu", (unsigned long) tv.tv_usec);
246 assert (i < sizeof (buf));
247
248 i += snprintf (buf + i, sizeof (buf) - i, "P%lu", (unsigned long) getpid ());
249 assert (i < sizeof (buf));
250
251 i += snprintf (buf + i, sizeof (buf) - i, "Q%lu", input->uid-1);
252 assert (i < sizeof (buf));
253
254 i += snprintf (buf + i, sizeof (buf) - i, ".%s", hostname);
255 assert (i < sizeof (buf));
256
257 if (uid_option)
258 {
259 i += snprintf (buf + i, sizeof (buf) - i, ",u=%lu", input->uid);
260 assert (i < sizeof (buf));
261 }
262 if (!new_option)
263 {
264 i += snprintf (buf + i, sizeof (buf) - i, ":2,");
265 assert (i < sizeof (buf));
266 }
267 return strdup (buf);
268 }
269
270 static void
maildir_validate_name(char const * name)271 maildir_validate_name (char const *name)
272 {
273 if (!starts_with_subdir (name))
274 {
275 fprintf (stderr, "%s: name must start with a subdir: %s\n",
276 progname, name);
277 exit (EX_DATAERR);
278 }
279 }
280
281 static char *
next_name_from_argv(NAME_INPUT * input)282 next_name_from_argv (NAME_INPUT *input)
283 {
284 if (input->idx == input->argc)
285 return NULL;
286 return input->argv[input->idx++];
287 }
288
289 static char *
next_name_from_file(NAME_INPUT * input)290 next_name_from_file (NAME_INPUT *input)
291 {
292 ssize_t off = 0;
293 do
294 {
295 if (off + 1 >= input->bufsize)
296 {
297 size_t size;
298 char *buf;
299 if (input->bufsize == 0)
300 {
301 size = 64;
302 }
303 else
304 {
305 if ((size_t) -1 / 3 * 2 <= size)
306 {
307 fprintf (stderr, "%s: out of memory\n", progname);
308 exit (EX_OSERR);
309 }
310 size += (size + 1) / 2;
311 }
312 buf = realloc (input->bufptr, size);
313 if (!buf)
314 {
315 fprintf (stderr, "%s: out of memory\n", progname);
316 exit (EX_OSERR);
317 }
318 input->bufptr = buf;
319 input->bufsize = size;
320 }
321 if (!fgets(input->bufptr + off, input->bufsize - off, input->file))
322 {
323 if (feof (input->file))
324 {
325 if (off == 0)
326 return NULL;
327 break;
328 }
329 else
330 {
331 fprintf (stderr, "%s: read error: %s\n",
332 progname, strerror (errno));
333 exit (EX_OSERR);
334 }
335 }
336 off += strlen(input->bufptr + off);
337 }
338 while (input->bufptr[off - 1] != '\n');
339 input->bufptr[--off] = 0;
340 return input->bufptr;
341 }
342
343 static char *
next_name_generate(NAME_INPUT * input)344 next_name_generate (NAME_INPUT *input)
345 {
346 ++input->uid;
347 return format_info[format].generate_name(input);
348 }
349
350 static char *
next_name(NAME_INPUT * input)351 next_name (NAME_INPUT *input)
352 {
353 static char *(*nextfn[]) (NAME_INPUT *) = {
354 next_name_generate,
355 next_name_from_argv,
356 next_name_from_file
357 };
358 return nextfn[input->type] (input);
359 }
360
361 static void
mkdir_r(char * name)362 mkdir_r (char *name)
363 {
364 size_t namelen = strlen (name);
365 size_t i;
366 int rc = 0;
367
368 while (mkdir (name, 0755))
369 {
370 if (errno == ENOENT)
371 {
372 char *p = strrchr (name, '/');
373 if (!p)
374 abort ();
375 *p = 0;
376 }
377 else
378 {
379 fprintf (stderr, "%s: can't create %s: %s\n",
380 progname, name, strerror (errno));
381 rc = 1;
382 break;
383 }
384 }
385
386 while ((i = strlen (name)) < namelen)
387 {
388 name[i] = '/';
389 if (rc)
390 continue;
391 if (mkdir (name, 0755))
392 {
393 fprintf (stderr, "%s: can't create %s: %s\n",
394 progname, name, strerror (errno));
395 rc = 1;
396 }
397 }
398
399 if (rc)
400 {
401 fprintf (stderr, "%s: while attempting to create %s\n",
402 progname, name);
403 exit (EX_CANTCREAT);
404 }
405 }
406
407 static void
validate_name(char const * name)408 validate_name (char const *name)
409 {
410 return format_info[format].validate_name (name);
411 }
412
413
414 static int
mkhier(char * name)415 mkhier (char *name)
416 {
417 int fd;
418
419 mkdir_r (name);
420 fd = open (name, O_RDONLY | O_NONBLOCK | O_DIRECTORY);
421 if (fd == -1)
422 {
423 fprintf (stderr, "%s: can't open directory %s: %s\n",
424 progname, name, strerror (errno));
425 exit (EX_UNAVAILABLE);
426 }
427
428 if (format_info[format].init_dir)
429 format_info[format].init_dir (fd, name);
430
431 return fd;
432 }
433 /*
434 * Alphabet:
435 * 0 '\n'
436 * 1 'F'
437 * 2 'r'
438 * 3 'o'
439 * 4 'm'
440 * 5 ' '
441 * 6 '>'
442 * 7 ANY
443 *
444 * Transitions:
445 * \ input
446 * state
447 *
448 * \ 0 1 2 3 4 5 6 7
449 * -+ ------------------
450 * 0| 1 0 0 0 0 0 0 0 \n
451 * 1| 1 2 0 0 0 0 6 0 F
452 * 2| 1 0 3 0 0 0 0 0 r
453 * 3| 1 0 0 4 0 0 0 0 o
454 * 4| 1 0 0 0 5 0 0 0 m
455 * 5| 1 0 0 0 0 13 0 0
456 * 6| 1 7 0 0 0 0 0 0 > (X)
457 * 7| 1 0 8 0 0 0 0 0 F
458 * 8| 1 0 0 9 0 0 0 0 r
459 * 9| 1 0 0 0 10 0 0 0 o
460 * 10| 1 0 0 0 0 11 0 0 m
461 * 11| 1 0 0 0 0 0 12 0
462 * 12 <stop> unescape
463 * 13 <stop> mail start
464 *
465 */
466
467 static char alphabet[] = "\nFrom >";
468 #define ASZ (sizeof (alphabet))
469 static int transition[][ASZ] = {
470 { 1, 0, 0, 0, 0, 0, 0, 0 },
471 { 1, 2, 0, 0, 0, 0, 6, 0 },
472 { 1, 0, 3, 0, 0, 0, 0, 0 },
473 { 1, 0, 0, 4, 0, 0, 0, 0 },
474 { 1, 0, 0, 0, 5, 0, 0, 0 },
475 { 1, 0, 0, 0, 0, 13, 0, 0 },
476 { 1, 7, 0, 0, 0, 0, 0, 0 },
477 { 1, 0, 8, 0, 0, 0, 0, 0 },
478 { 1, 0, 0, 9, 0, 0, 0, 0 },
479 { 1, 0, 0, 0, 10, 0, 0, 0 },
480 { 1, 0, 0, 0, 0, 11, 0, 0 },
481 { 1, 0, 0, 0, 0, 0, 12, 0 }
482 };
483
484 static int
getalpha(int input)485 getalpha (int input)
486 {
487 char *p = strchr (alphabet, input);
488 if (p)
489 return p - alphabet;
490 return ASZ-1;
491 }
492
493 int
copy_till(FILE * in,FILE * out,int state)494 copy_till (FILE *in, FILE *out, int state)
495 {
496 int c;
497 char buf[ASZ];
498 int i = 0;
499 #define FLUSH() \
500 do \
501 { \
502 if (i && out) fwrite (buf, 1, i, out); \
503 i = 0; \
504 } \
505 while(0)
506
507 while ((c = fgetc (in)) != EOF)
508 {
509 state = transition[state][getalpha (c)];
510 if (state == 0)
511 {
512 FLUSH ();
513 if (out)
514 fputc (c, out);
515 }
516 else if (state == 12)
517 {
518 FLUSH ();
519 }
520 else if (state == 13)
521 {
522 fseek (in, -5, SEEK_SET);
523 return 0;
524 }
525 else if (state == 6)
526 /* skip */;
527 else if (state == 1)
528 {
529 FLUSH ();
530 buf[i++] = c;
531 }
532 else
533 buf[i++] = c;
534 }
535 FLUSH ();
536 return -1;
537 }
538
539 static void
skip_line(FILE * fp)540 skip_line (FILE *fp)
541 {
542 int c;
543 while ((c = fgetc (fp)) != EOF && c != '\n')
544 ;
545 }
546
547 static int
store_file(int dirfd,char const * name,FILE * input)548 store_file (int dirfd, char const *name, FILE *input)
549 {
550 FILE *fp;
551 int rc;
552 int fd;
553
554 fd = openat (dirfd, name, O_CREAT | O_TRUNC | O_WRONLY, 0600);
555 if (fd == -1)
556 {
557 fprintf (stderr, "%s: can't create file %s: %s\n",
558 progname, name, strerror (errno));
559 exit (EX_CANTCREAT);
560 }
561
562 fp = fdopen (fd, "w");
563 if (!fp)
564 abort ();
565
566 rc = copy_till (input, fp, 0);
567 fclose (fp);
568 return rc;
569 }
570
571 void
create_prop_file(int dirfd)572 create_prop_file (int dirfd)
573 {
574 int fd;
575 FILE *fp;
576
577 fd = openat (dirfd, propfile, O_CREAT | O_TRUNC | O_WRONLY, 0600);
578 if (fd == -1)
579 {
580 fprintf (stderr, "%s: can't create file %s: %s\n",
581 progname, propfile, strerror (errno));
582 exit (EX_CANTCREAT);
583 }
584 fp = fdopen (fd, "w");
585 if (!fp)
586 abort ();
587 if (uidvalidity == 0)
588 {
589 struct timeval tv;
590 gettimeofday (&tv, NULL);
591 uidvalidity = tv.tv_sec;
592 }
593 fprintf (fp, "version: %s\n", PACKAGE_VERSION);
594 fprintf (fp, "uid-validity: %lu\n", uidvalidity);
595 fclose (fp);
596 }
597
598 static void
usage(FILE * fp)599 usage (FILE *fp)
600 {
601 fprintf (fp, "usage: %s [-i FILE] [-h NAME] [-v UIDVALIDITY] [-nmpu] PATH FILE [NAMES...]\n",
602 progname);
603 fprintf (fp, "converts UNIX mbox file FILE to maildir or MH format.\n");
604 }
605
606 int
main(int argc,char ** argv)607 main (int argc, char **argv)
608 {
609 NAME_INPUT input = { INPUT_UNDEF };
610 char *name;
611 FILE *fp;
612 int fd;
613 int c;
614
615 progname = argv[0];
616
617 while ((c = getopt (argc, argv, "i:h:nmpuv:")) != EOF)
618 {
619 switch (c)
620 {
621 case 'h':
622 hostname = optarg;
623 break;
624
625 case 'm':
626 format = F_MH;
627 break;
628
629 case 'n':
630 new_option = 1;
631 break;
632
633 case 'u':
634 uid_option = 1;
635 break;
636
637 case 'p':
638 prop_option = 1;
639 break;
640
641 case 'v':
642 uidvalidity = strtoul (optarg, NULL, 10);
643 prop_option = 1;
644 break;
645
646 case 'i':
647 if (strcmp (optarg, "-") == 0)
648 {
649 input.file = stdin;
650 }
651 else if ((input.file = fopen (optarg, "r")) == NULL)
652 {
653 fprintf (stderr, "%s: can't open input file %s: %s\n",
654 progname, optarg, strerror (errno));
655 exit (EX_OSERR);
656 }
657 input.type = INPUT_FILE;
658 input.bufptr = NULL;
659 input.bufsize = 0;
660 break;
661
662 default:
663 usage (stderr);
664 exit (EX_USAGE);
665 }
666 }
667 if (new_option)
668 uid_option = 0;
669
670 argc -= optind;
671 argv += optind;
672
673 switch (argc)
674 {
675 case 0:
676 case 1:
677 usage (stderr);
678 exit (EX_USAGE);
679
680 case 2:
681 if (input.type == INPUT_UNDEF)
682 {
683 input.type = INPUT_GENERATE;
684 input.uid = 0;
685 }
686 break;
687
688 /* fall through */
689 default:
690 if (input.type == INPUT_FILE)
691 {
692 fprintf (stderr, "%s: NAMES can't be given together with -i\n",
693 progname);
694 exit (EX_USAGE);
695 }
696 input.type = INPUT_ARGV;
697 input.argc = argc - 2;
698 input.argv = argv + 2;
699 input.idx = 0;
700 break;
701 }
702
703 fd = mkhier (argv[0]);
704
705 fp = fopen (argv[1], "r");
706 if (!fp)
707 {
708 fprintf (stderr, "%s: can't open %s: %s\n",
709 progname, argv[1], strerror (errno));
710 exit (EX_OSERR);
711 }
712
713 if (copy_till (fp, NULL, 1))
714 {
715 fprintf (stderr, "%s: can't find any messages in %s\n",
716 progname, argv[1]);
717 exit (EX_NOINPUT);
718 }
719
720 while ((name = next_name (&input)) != NULL)
721 {
722 validate_name (name);
723 skip_line (fp);
724 if (store_file (fd, name, fp))
725 break;
726 }
727 if (input.type != INPUT_GENERATE && (name = next_name (&input)) != NULL)
728 {
729 fprintf (stderr, "%s: extra names ignored (started at %s)\n",
730 progname, name);
731 free (name);
732 }
733
734 if (prop_option)
735 create_prop_file (fd);
736
737 return 0;
738 }
739