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