xref: /haiku/src/bin/mail_utils/mbox2mail.cpp (revision e36a1b58e6daf3efeec46621114691ef499faafc)
1 /******************************************************************************
2  * $Id$
3  *
4  * MboxToBeMail is a utility program that converts Unix mailbox (mbox) files
5  * (the kind that Pine uses) into e-mail files for use with BeOS.  It also
6  * handles news files from rn and trn, which have messages very similar to mail
7  * messages but with a different separator line.  The input files store
8  * multiple mail messages in text format separated by "From ..." lines or
9  * "Article ..." lines.  The output is a bunch of separate files, each one with
10  * one message plus BeOS BFS attributes describing that message.  For
11  * convenience, all the messages that were from one file are put in a specified
12  * directory.
13  *
14  * Command line driven.  Version 1.7 and up (which have support for
15  * international character sets (as requested by "MFC" Zahar from Russia) and
16  * the Thread attribute) require the libmail.so that comes with the Mail Daemon
17  * Replacement (MDR) version 2.2.6 or later (http://www.bebits.com/app/2289 or
18  * http://sourceforge.net/projects/bemaildaemon/).  Use version 1.6 (get it
19  * from the version history pages on BeBits) if you don't want to install MDR.
20  *
21  * $Log: MailboxToBeMail.cpp,v $ (manually updated now that SVN is used)
22  * r13960 | agmsmith | 2005-08-13 22:10:49 -0400 (Sat, 13 Aug 2005) | 3 lines
23  * More file movement for the BeMail utilities...  Content updates later to
24  * avoid confusing svn.
25  *
26  * r13955 | agmsmith | 2005-08-13 19:43:41 -0400 (Sat, 13 Aug 2005) | 5 lines
27  * Half way through adding some more BeMail related utilities - they use
28  * libmail.so which isn't backwards compatibile so they need recompiling for
29  * Haiku - thus better to include them here.  Also want spam levels of 1E-6 to
30  * be visible for genuine messages.
31  *
32  * Revision 1.9  2003/12/31 00:37:46  agmsmith
33  * Use the MDR date parsing routine rather than parsedate, so it
34  * can handle newer date formats.
35  *
36  * Revision 1.8  2003/12/28 19:03:57  agmsmith
37  * Added an option to save or not save the separator text line.
38  *
39  * Revision 1.7  2003/12/28 04:23:21  agmsmith
40  * Now uses the Mail Daemon Replacement library to parse the headers,
41  * which means they now handle foreign character sets (converted
42  * automatically to UTF-8), so Subject and To are correct when they
43  * are used as part of the file name.
44  *
45  * Revision 1.6  2002/01/14 20:23:21  agmsmith
46  * Include the detection line with the message header, just for completeness.
47  *
48  * Revision 1.5  2002/01/14 03:21:43  agmsmith
49  * Added processing of UseNet articles, and allowed an older date format.
50  *
51  * Revision 1.4  2002/01/14 00:06:59  agmsmith
52  * Don't allow slashes in file names.
53  *
54  * Revision 1.3  2002/01/13 23:33:26  agmsmith
55  * First seemingly working version.
56  *
57  * Revision 1.2  2002/01/13 17:57:26  agmsmith
58  * Can now pick out separate messages from the inbox.
59  *
60  * Revision 1.1  2002/01/13 15:25:33  agmsmith
61  * Initial revision
62  */
63 
64 /* BeOS headers. */
65 
66 #include <Application.h>
67 #include <E-mail.h>
68 #include <StorageKit.h>
69 #include <SupportKit.h>
70 
71 /* Posix headers. */
72 
73 #include <string.h>
74 #include <stdio.h>
75 #include <ctype.h>
76 #include <errno.h>
77 
78 /* MDR headers (get the MDR source and add the MDR include/public and
79 include/numail directories to the project path settings).  Or just use
80 the Haiku build process which is already set up to include the right ones. */
81 
82 #include <MailMessage.h>
83 #include <mail_util.h>
84 
85 
86 
87 /******************************************************************************
88  * Globals
89  */
90 
91 char       InputPathName [B_PATH_NAME_LENGTH];
92 FILE      *InputFile;
93 BDirectory OutputDir;
94 
95 
96 typedef enum StandardHeaderEnum
97 {
98   STD_HDR_DATE = 0, /* The Date: field.  First one since it is numeric. */
99   STD_HDR_FROM, /* The whole From: field, including quotes and address. */
100   STD_HDR_TO, /* All the stuff in the To: field. */
101   STD_HDR_CC, /* All the CC: field (originally means carbon copy). */
102   STD_HDR_REPLY, /* Things in the reply-to: field. */
103   STD_HDR_SUBJECT, /* The Subject: field. */
104   STD_HDR_PRIORITY, /* The Priority: and related fields, usually "Normal". */
105   STD_HDR_STATUS, /* The BeOS mail Read / New status text attribute. */
106   STD_HDR_THREAD, /* The subject simplified. */
107   STD_HDR_NAME, /* The From address simplified into a plain name. */
108   STD_HDR_MAX
109 } StandardHeaderCodes;
110 
111 const char *g_StandardAttributeNames [STD_HDR_MAX] =
112 {
113   B_MAIL_ATTR_WHEN,
114   B_MAIL_ATTR_FROM,
115   B_MAIL_ATTR_TO,
116   B_MAIL_ATTR_CC,
117   B_MAIL_ATTR_REPLY,
118   B_MAIL_ATTR_SUBJECT,
119   B_MAIL_ATTR_PRIORITY,
120   B_MAIL_ATTR_STATUS,
121   B_MAIL_ATTR_THREAD,
122   B_MAIL_ATTR_NAME
123 };
124 
125 
126 
127 /******************************************************************************
128  * Global utility function to display an error message and return.  The message
129  * part describes the error, and if ErrorNumber is non-zero, gets the string
130  * ", error code $X (standard description)." appended to it.  If the message
131  * is NULL then it gets defaulted to "Something went wrong".
132  */
133 
134 static void DisplayErrorMessage (
135   const char *MessageString = NULL,
136   int ErrorNumber = 0,
137   const char *TitleString = NULL)
138 {
139   char ErrorBuffer [B_PATH_NAME_LENGTH + 80 /* error message */ + 80];
140 
141   if (TitleString == NULL)
142     TitleString = "Error Message:";
143 
144   if (MessageString == NULL)
145   {
146     if (ErrorNumber == 0)
147       MessageString = "No error, no message, why bother?";
148     else
149       MessageString = "Something went wrong";
150   }
151 
152   if (ErrorNumber != 0)
153   {
154     sprintf (ErrorBuffer, "%s, error code $%X/%d (%s) has occured.",
155       MessageString, ErrorNumber, ErrorNumber, strerror (ErrorNumber));
156     MessageString = ErrorBuffer;
157   }
158 
159   fputs (TitleString, stderr);
160   fputc ('\n', stderr);
161   fputs (MessageString, stderr);
162   fputc ('\n', stderr);
163 }
164 
165 
166 
167 /******************************************************************************
168  * Determine if a line of text is the start of another message.  Pine mailbox
169  * files have messages that start with a line that could say something like
170  * "From agmsmith@achilles.net Fri Oct 31 21:19:36 EST 1997" or maybe something
171  * like "From POPmail Mon Oct 20 21:12:36 1997" or in a more modern format,
172  * "From agmsmith@achilles.net Tue Sep 4 09:04:11 2001 -0400".  I generalise it
173  * to "From blah Day MMM NN XX:XX:XX TZONE1 YYYY TZONE2".  Blah is an e-mail
174  * address you can ignore (just treat it as a word separated by spaces).  Day
175  * is a 3 letter day of the week.  MMM is a 3 letter month name.  NN is the two
176  * digit day of the week, has a leading space if the day is less than 10.
177  * XX:XX:XX is the time, the X's are digits.  TZONE1 is the old style optional
178  * time zone of 3 capital letters.  YYYY is the four digit year.  TZONE2 is the
179  * optional modern time zone info, a plus or minus sign and 4 digits.  Returns
180  * true if the line of text (ended with a NUL byte, no line feed or carriage
181  * returns at the end) is the start of a message.
182  */
183 
184 bool IsStartOfMailMessage (char *LineString)
185 {
186   char        *StringPntr;
187 
188   /* It starts with "From " */
189 
190   if (memcmp ("From ", LineString, 5) != 0)
191     return false;
192   StringPntr = LineString + 4;
193   while (*StringPntr == ' ')
194     StringPntr++;
195 
196   /* Skip over the e-mail address (or stop at the end of string). */
197 
198   while (*StringPntr != ' ' && *StringPntr != 0)
199     StringPntr++;
200   while (*StringPntr == ' ')
201     StringPntr++;
202 
203   /* Look for the 3 letter day of the week. */
204 
205   if (memcmp (StringPntr, "Mon", 3) != 0 &&
206   memcmp (StringPntr, "Tue", 3) != 0 &&
207   memcmp (StringPntr, "Wed", 3) != 0 &&
208   memcmp (StringPntr, "Thu", 3) != 0 &&
209   memcmp (StringPntr, "Fri", 3) != 0 &&
210   memcmp (StringPntr, "Sat", 3) != 0 &&
211   memcmp (StringPntr, "Sun", 3) != 0)
212   {
213     printf ("False alarm, not a valid day of the week in \"%s\".\n",
214       LineString);
215     return false;
216   }
217   StringPntr += 3;
218   while (*StringPntr == ' ')
219     StringPntr++;
220 
221   /* Look for the 3 letter month code. */
222 
223   if (memcmp (StringPntr, "Jan", 3) != 0 &&
224   memcmp (StringPntr, "Feb", 3) != 0 &&
225   memcmp (StringPntr, "Mar", 3) != 0 &&
226   memcmp (StringPntr, "Apr", 3) != 0 &&
227   memcmp (StringPntr, "May", 3) != 0 &&
228   memcmp (StringPntr, "Jun", 3) != 0 &&
229   memcmp (StringPntr, "Jul", 3) != 0 &&
230   memcmp (StringPntr, "Aug", 3) != 0 &&
231   memcmp (StringPntr, "Sep", 3) != 0 &&
232   memcmp (StringPntr, "Oct", 3) != 0 &&
233   memcmp (StringPntr, "Nov", 3) != 0 &&
234   memcmp (StringPntr, "Dec", 3) != 0)
235   {
236     printf ("False alarm, not a valid month name in \"%s\".\n",
237       LineString);
238     return false;
239   }
240   StringPntr += 3;
241   while (*StringPntr == ' ')
242     StringPntr++;
243 
244   /* Skip the day of the month.  Require at least one digit. */
245 
246   if (*StringPntr < '0' || *StringPntr > '9')
247   {
248     printf ("False alarm, not a valid day of the month number in \"%s\".\n",
249       LineString);
250     return false;
251   }
252   while (*StringPntr >= '0' && *StringPntr <= '9')
253     StringPntr++;
254   while (*StringPntr == ' ')
255     StringPntr++;
256 
257   /* Check the time.  Look for the sequence
258   digit-digit-colon-digit-digit-colon-digit-digit. */
259 
260   if (StringPntr[0] < '0' || StringPntr[0] > '9' ||
261   StringPntr[1] < '0' || StringPntr[1] > '9' ||
262   StringPntr[2] != ':' ||
263   StringPntr[3] < '0' || StringPntr[3] > '9' ||
264   StringPntr[4] < '0' || StringPntr[4] > '9' ||
265   StringPntr[5] != ':' ||
266   StringPntr[6] < '0' || StringPntr[6] > '9' ||
267   StringPntr[7] < '0' || StringPntr[7] > '9')
268   {
269     printf ("False alarm, not a valid time value in \"%s\".\n",
270       LineString);
271     return false;
272   }
273   StringPntr += 8;
274   while (*StringPntr == ' ')
275     StringPntr++;
276 
277   /* Look for the optional antique 3 capital letter time zone and skip it. */
278 
279   if (StringPntr[0] >= 'A' && StringPntr[0] <= 'Z' &&
280   StringPntr[1] >= 'A' && StringPntr[1] <= 'Z' &&
281   StringPntr[2] >= 'A' && StringPntr[2] <= 'Z')
282   {
283     StringPntr += 3;
284     while (*StringPntr == ' ')
285       StringPntr++;
286   }
287 
288   /* Look for the 4 digit year. */
289 
290   if (StringPntr[0] < '0' || StringPntr[0] > '9' ||
291   StringPntr[1] < '0' || StringPntr[1] > '9' ||
292   StringPntr[2] < '0' || StringPntr[2] > '9' ||
293   StringPntr[3] < '0' || StringPntr[3] > '9')
294   {
295     printf ("False alarm, not a valid 4 digit year in \"%s\".\n",
296       LineString);
297     return false;
298   }
299   StringPntr += 4;
300   while (*StringPntr == ' ')
301     StringPntr++;
302 
303   /* Look for the optional modern time zone and skip over it if present. */
304 
305   if ((StringPntr[0] == '+' || StringPntr[0] == '-') &&
306   StringPntr[1] >= '0' && StringPntr[1] <= '9' &&
307   StringPntr[2] >= '0' && StringPntr[2] <= '9' &&
308   StringPntr[3] >= '0' && StringPntr[3] <= '9' &&
309   StringPntr[4] >= '0' && StringPntr[4] <= '9')
310   {
311     StringPntr += 5;
312     while (*StringPntr == ' ')
313       StringPntr++;
314   }
315 
316   /* Look for end of string. */
317 
318   if (*StringPntr != 0)
319   {
320     printf ("False alarm, extra stuff after the year/time zone in \"%s\".\n",
321       LineString);
322     return false;
323   }
324 
325   return true;
326 }
327 
328 
329 
330 /******************************************************************************
331  * Determine if a line of text is the start of a news article.  TRN and RN news
332  * article save files have messages that start with a line that looks like
333  * "Article 11721 of rec.games.video.3do:".  Returns true if the line of text
334  * (ended with a NUL byte, no line feed or carriage returns at the end) is the
335  * start of an article.
336  */
337 
338 bool IsStartOfUsenetArticle (char *LineString)
339 {
340   char        *StringPntr;
341 
342   /* It starts with "Article " */
343 
344   if (memcmp ("Article ", LineString, 8) != 0)
345     return false;
346   StringPntr = LineString + 7;
347   while (*StringPntr == ' ')
348     StringPntr++;
349 
350   /* Skip the article number.  Require at least one digit. */
351 
352   if (*StringPntr < '0' || *StringPntr > '9')
353   {
354     printf ("False alarm, not a valid article number in \"%s\".\n",
355       LineString);
356     return false;
357   }
358   while (*StringPntr >= '0' && *StringPntr <= '9')
359     StringPntr++;
360   while (*StringPntr == ' ')
361     StringPntr++;
362 
363   /* Now it should have "of " */
364 
365   if (memcmp ("of ", StringPntr, 3) != 0)
366   {
367     printf ("False alarm, article line \"of\" misssing in \"%s\".\n",
368       LineString);
369     return false;
370   }
371   StringPntr += 2;
372   while (*StringPntr == ' ')
373     StringPntr++;
374 
375   /* Skip over the newsgroup name (no spaces) to the colon. */
376 
377   while (*StringPntr != ':' && *StringPntr != ' ' && *StringPntr != 0)
378     StringPntr++;
379 
380   if (StringPntr[0] != ':' || StringPntr[1] != 0)
381   {
382     printf ("False alarm, article doesn't end with a colon in \"%s\".\n",
383       LineString);
384     return false;
385   }
386 
387   return true;
388 }
389 
390 
391 
392 /******************************************************************************
393  * Saves the message text to a file in the output directory.  The file name is
394  * derived from the message headers.  Returns zero if successful, a negative
395  * error code if an error occured.
396  */
397 
398 status_t SaveMessage (BString &MessageText)
399 {
400   time_t                   DateInSeconds;
401   status_t                 ErrorCode;
402   BString                  FileName;
403   BString                  HeaderValues [STD_HDR_MAX];
404   int                      i;
405   int                      Length;
406   BEmailMessage            MailMessage;
407   BFile                    OutputFile;
408   char                     TempString [80];
409   struct tm                TimeFields;
410   BString                  UniqueFileName;
411   int32                    Uniquer;
412 
413   /* Remove blank lines from the end of the message (a pet peeve of mine), but
414   end the message with a single new line to avoid annoying text editors that
415   like to have it. */
416 
417   i = MessageText.Length ();
418   while (i > 0 && (MessageText[i-1] == '\n' || MessageText[i-1] == '\r'))
419     i--;
420   MessageText.Truncate (i);
421   MessageText.Append ("\r\n");
422 
423   /* Make a pretend file to hold the message, so the MDR library can use it. */
424 
425   BMemoryIO FakeFile (MessageText.String (), MessageText.Length ());
426 
427   /* Hand the message text off to the MDR library, which will parse it, extract
428   the subject, sender's name, and other attributes, taking into account the
429   character set headers. */
430 
431   ErrorCode = MailMessage.SetToRFC822 (&FakeFile,
432     MessageText.Length (), false /* parse_now - decodes message body */);
433   if (ErrorCode != B_OK)
434   {
435     DisplayErrorMessage ("Mail library was unable to process a mail "
436       "message for some reason", ErrorCode);
437     return ErrorCode;
438   }
439 
440   /* Get the values for the standard attributes.  NULL if missing. */
441 
442   HeaderValues [STD_HDR_TO] = MailMessage.To ();
443   HeaderValues [STD_HDR_FROM] = MailMessage.From ();
444   HeaderValues [STD_HDR_CC] = MailMessage.CC ();
445   HeaderValues [STD_HDR_DATE] = MailMessage.Date ();
446   HeaderValues [STD_HDR_REPLY] = MailMessage.ReplyTo ();
447   HeaderValues [STD_HDR_SUBJECT] = MailMessage.Subject ();
448   HeaderValues [STD_HDR_STATUS] = "Read";
449   if (MailMessage.Priority () != 3 /* Normal */)
450   {
451     sprintf (TempString, "%d", MailMessage.Priority ());
452     HeaderValues [STD_HDR_PRIORITY] = TempString;
453   }
454 
455   HeaderValues[STD_HDR_THREAD] = HeaderValues[STD_HDR_SUBJECT];
456   SubjectToThread (HeaderValues[STD_HDR_THREAD]);
457   if (HeaderValues[STD_HDR_THREAD].Length() <= 0)
458     HeaderValues[STD_HDR_THREAD] = "No Subject";
459 
460   HeaderValues[STD_HDR_NAME] = HeaderValues[STD_HDR_FROM];
461   extract_address_name (HeaderValues[STD_HDR_NAME]);
462 
463   // Generate a file name for the incoming message.
464 
465   FileName = HeaderValues [STD_HDR_THREAD];
466   if (FileName[0] == '.')
467     FileName.Prepend ("_"); // Avoid hidden files, starting with a dot.
468 
469   // Convert the date into a year-month-day fixed digit width format, so that
470   // sorting by file name will give all the messages with the same subject in
471   // order of date.
472 
473   DateInSeconds =
474     ParseDateWithTimeZone (HeaderValues[STD_HDR_DATE].String());
475   if (DateInSeconds == -1)
476     DateInSeconds = 0; /* Set it to the earliest time if date isn't known. */
477 
478   localtime_r (&DateInSeconds, &TimeFields);
479   sprintf (TempString, "%04d%02d%02d%02d%02d%02d",
480     TimeFields.tm_year + 1900,
481     TimeFields.tm_mon + 1,
482     TimeFields.tm_mday,
483     TimeFields.tm_hour,
484     TimeFields.tm_min,
485     TimeFields.tm_sec);
486   FileName << " " << TempString << " " << HeaderValues[STD_HDR_NAME];
487   FileName.Truncate (240);  // reserve space for the uniquer
488 
489   // Get rid of annoying characters which are hard to use in the shell.
490   FileName.ReplaceAll('/','_');
491   FileName.ReplaceAll('\'','_');
492   FileName.ReplaceAll('"','_');
493   FileName.ReplaceAll('!','_');
494   FileName.ReplaceAll('<','_');
495   FileName.ReplaceAll('>','_');
496   while (FileName.FindFirst("  ") >= 0) // Remove multiple spaces.
497     FileName.Replace("  " /* Old */, " " /* New */, 1024 /* Count */);
498 
499   Uniquer = 0;
500   UniqueFileName = FileName;
501   while (true)
502   {
503     ErrorCode = OutputFile.SetTo (&OutputDir,
504       const_cast<const char *> (UniqueFileName.String ()),
505       B_READ_WRITE | B_CREATE_FILE | B_FAIL_IF_EXISTS);
506     if (ErrorCode == B_OK)
507       break;
508     if (ErrorCode != B_FILE_EXISTS)
509     {
510       UniqueFileName.Prepend ("Unable to create file \"");
511       UniqueFileName.Append ("\" for writing a message to");
512       DisplayErrorMessage (UniqueFileName.String (), ErrorCode);
513       return ErrorCode;
514     }
515     Uniquer++;
516     UniqueFileName = FileName;
517     UniqueFileName << " " << Uniquer;
518   }
519 
520   /* Write the message contents to the file, use the unchanged original one. */
521 
522   ErrorCode = OutputFile.Write (MessageText.String (), MessageText.Length ());
523   if (ErrorCode < 0)
524   {
525       UniqueFileName.Prepend ("Error while writing file \"");
526       UniqueFileName.Append ("\"");
527       DisplayErrorMessage (UniqueFileName.String (), ErrorCode);
528       return ErrorCode;
529   }
530 
531   /* Attach the attributes to the file.  Save the MIME type first, otherwise
532   the live queries don't pick up the new file.  Theoretically it would be
533   better to do it last so that other programs don't start reading the message
534   before the other attributes are set. */
535 
536   OutputFile.WriteAttr ("BEOS:TYPE", B_MIME_STRING_TYPE, 0,
537     "text/x-email", 13);
538 
539   OutputFile.WriteAttr (g_StandardAttributeNames[STD_HDR_DATE],
540     B_TIME_TYPE, 0, &DateInSeconds, sizeof (DateInSeconds));
541 
542   /* Write out all the string based attributes. */
543 
544   for (i = 1 /* The date was zero */; i < STD_HDR_MAX; i++)
545   {
546     if ((Length = HeaderValues[i].Length()) > 0)
547       OutputFile.WriteAttr (g_StandardAttributeNames[i], B_STRING_TYPE, 0,
548       HeaderValues[i].String(), Length + 1);
549   }
550 
551   return 0;
552 }
553 
554 
555 
556 /******************************************************************************
557  * Finally, the main program which drives it all.
558  */
559 
560 int main (int argc, char** argv)
561 {
562   char         ErrorMessage [B_PATH_NAME_LENGTH + 80];
563   bool         HaveOldMessage = false;
564   int          MessagesDoneCount = 0;
565   BString      MessageText;
566   BApplication MyApp ("application/x-vnd.agmsmith.mboxtobemail");
567   int          NextArgIndex;
568   char         OutputDirectoryPathName [B_PATH_NAME_LENGTH];
569   status_t     ReturnCode = -1;
570   bool         SaveSeparatorLine = false;
571   char        *StringPntr;
572   char         TempString [102400];
573 
574   if (argc <= 1)
575   {
576     printf ("%s is a utility for converting Pine e-mail\n",
577       argv[0]);
578     printf ("files (mbox files) to BeOS e-mail files with attributes.  It\n");
579     printf ("could well work with other Unix style mailbox files, and\n");
580     printf ("saved Usenet article files.  Each message in the input\n");
581     printf ("mailbox is converted into a separate file.  You can\n");
582     printf ("optionally specify a directory (will be created if needed) to\n");
583     printf ("put all the output files in, otherwise it scatters them into\n");
584     printf ("the current directory.  The -s option makes it leave in the\n");
585     printf ("separator text line at the top of each message, the default\n");
586     printf ("is to lose it.\n\n");
587     printf ("Usage:\n\n");
588     printf ("mboxtobemail [-s] InputFile [OutputDirectory]\n\n");
589     printf ("Public domain, by Alexander G. M. Smith.\n");
590     printf ("$Id$\n");
591     printf ("$HeadURL$\n");
592     return -10;
593   }
594 
595   NextArgIndex = 1;
596   if (strcmp (argv[NextArgIndex], "-s") == 0)
597   {
598     SaveSeparatorLine = true;
599     NextArgIndex++;
600   }
601 
602   /* Try to open the input file. */
603 
604   if (NextArgIndex >= argc)
605   {
606     ReturnCode = -20;
607     DisplayErrorMessage ("Missing the input file (mbox file) name argument.");
608     goto ErrorExit;
609   }
610   strncpy (InputPathName, argv[NextArgIndex], sizeof (InputPathName) - 1);
611   NextArgIndex++;
612   InputFile = fopen (InputPathName, "rb");
613   if (InputFile == NULL)
614   {
615     ReturnCode = errno;
616     sprintf (ErrorMessage, "Unable to open file \"%s\" for reading",
617       InputPathName);
618     DisplayErrorMessage (ErrorMessage, ReturnCode);
619     goto ErrorExit;
620   }
621 
622   /* Try to make the output directory.  First get its name. */
623 
624   if (NextArgIndex < argc)
625   {
626     strncpy (OutputDirectoryPathName, argv[NextArgIndex],
627       sizeof (OutputDirectoryPathName) - 2
628       /* Leave space for adding trailing slash and NUL byte */);
629     NextArgIndex++;
630   }
631   else
632     strcpy (OutputDirectoryPathName, ".");
633 
634   /* Remove trailing '/' characters from the output directory path. */
635 
636   StringPntr =
637     OutputDirectoryPathName + (strlen (OutputDirectoryPathName) - 1);
638   while (StringPntr >= OutputDirectoryPathName)
639   {
640     if (*StringPntr != '/')
641       break;
642     StringPntr--;
643   }
644   *(++StringPntr) = 0;
645 
646   if (StringPntr - OutputDirectoryPathName > 0 &&
647   strcmp (OutputDirectoryPathName, ".") != 0)
648   {
649     if (mkdir (OutputDirectoryPathName, 0777))
650     {
651       ReturnCode = errno;
652       if (ReturnCode != B_FILE_EXISTS)
653       {
654         sprintf (ErrorMessage, "Unable to make output directory \"%s\"",
655           OutputDirectoryPathName);
656         DisplayErrorMessage (ErrorMessage, ReturnCode);
657         goto ErrorExit;
658       }
659     }
660   }
661 
662   /* Set the output BDirectory. */
663 
664   ReturnCode = OutputDir.SetTo (OutputDirectoryPathName);
665   if (ReturnCode != B_OK)
666   {
667     sprintf (ErrorMessage, "Unable to set output BDirectory to \"%s\"",
668       OutputDirectoryPathName);
669     DisplayErrorMessage (ErrorMessage, ReturnCode);
670     goto ErrorExit;
671   }
672 
673   printf ("Input file: \"%s\", Output directory: \"%s\", ",
674     InputPathName, OutputDirectoryPathName);
675   printf ("%ssaving separator text line at the top of each message.  Working",
676     SaveSeparatorLine ? "" : "not ");
677 
678   /* Extract a text message from the mail file.  It starts with a line that
679   says "From blah Day MM NN XX:XX:XX YYYY TZONE".  Blah is an e-mail address
680   you can ignore (just treat it as a word separated by spaces).  Day is a 3
681   letter day of the week.  MM is a 3 letter month name.  NN is the two digit
682   day of the week, has a leading space if the day is less than 10.  XX:XX:XX is
683   the time, the X's are digits.  YYYY is the four digit year.  TZONE is the
684   optional time zone info, a plus or minus sign and 4 digits. */
685 
686   while (!feof (InputFile))
687   {
688     /* First read in one line of text. */
689 
690     if (!fgets (TempString, sizeof (TempString), InputFile))
691     {
692       ReturnCode = errno;
693       if (ferror (InputFile))
694       {
695         sprintf (ErrorMessage,
696           "Error while reading from \"%s\"", InputPathName);
697         DisplayErrorMessage (ErrorMessage, ReturnCode);
698         goto ErrorExit;
699       }
700       break; /* No error, just end of file. */
701     }
702 
703     /* Remove any trailing control characters (line feed usually, or CRLF).
704     Might also nuke trailing tabs too.  Doesn't usually matter.  The main thing
705     is to allow input files with both LF and CRLF endings (and even CR endings
706     if you come from the Macintosh world). */
707 
708     StringPntr = TempString + strlen (TempString) - 1;
709     while (StringPntr >= TempString && *StringPntr < 32)
710       StringPntr--;
711     *(++StringPntr) = 0;
712 
713     /* See if this is the start of a new message. */
714 
715     if (IsStartOfUsenetArticle (TempString) ||
716     IsStartOfMailMessage (TempString))
717     {
718       if (HaveOldMessage)
719       {
720         if ((ReturnCode = SaveMessage (MessageText)) != 0)
721           goto ErrorExit;
722         putchar ('.');
723         fflush (stdout);
724         MessagesDoneCount++;
725       }
726       HaveOldMessage = true;
727       MessageText.SetTo (SaveSeparatorLine ? TempString : "");
728     }
729     else
730     {
731       /* Append the line to the current message text. */
732 
733       if (MessageText.Length () > 0)
734         MessageText.Append ("\r\n"); /* Yes, BeMail expects CR/LF line ends. */
735       MessageText.Append (TempString);
736     }
737   }
738 
739   /* Flush out the last message. */
740 
741   if (HaveOldMessage)
742   {
743     if ((ReturnCode = SaveMessage (MessageText)) != 0)
744       goto ErrorExit;
745     putchar ('.');
746     MessagesDoneCount++;
747   }
748   printf ("  Did %d messages.\n", MessagesDoneCount);
749 
750   ReturnCode = 0;
751 
752 ErrorExit:
753   if (InputFile != NULL)
754     fclose (InputFile);
755   OutputDir.Unset ();
756   return ReturnCode;
757 }
758