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