Comment cosmetics.
[mymail.git] / mymail.c
1
2 /*
3  *  Copyright (c) 2013 Francois Fleuret
4  *  Written by Francois Fleuret <francois@fleuret.org>
5  *
6  *  This file is part of mymail.
7  *
8  *  mymail is free software: you can redistribute it and/or modify
9  *  it under the terms of the GNU General Public License version 3 as
10  *  published by the Free Software Foundation.
11  *
12  *  mymail is distributed in the hope that it will be useful, but
13  *  WITHOUT ANY WARRANTY; without even the implied warranty of
14  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  *  General Public License for more details.
16  *
17  *  You should have received a copy of the GNU General Public License
18  *  along with mymail.  If not, see <http://www.gnu.org/licenses/>.
19  *
20  */
21
22 /*
23
24   mymail is a simple mail indexer. It can:
25
26   (1) scan mbox files, and create a db file containing for each mail a
27       list of fields computed from its header.
28
29   (2) read such a db file, gets all the mails matching regexp-defined
30       conditions on the fields, and generates a resulting mbox file.
31
32   It is low-tech, simple, light and fast.
33
34 */
35
36 #define _GNU_SOURCE
37
38 #include <stdio.h>
39 #include <stdlib.h>
40 #include <string.h>
41 #include <errno.h>
42 #include <fcntl.h>
43 #include <locale.h>
44 #include <getopt.h>
45 #include <limits.h>
46 #include <dirent.h>
47 #include <regex.h>
48 #include <time.h>
49
50 #define MYMAIL_DB_MAGIC_TOKEN "mymail_index_file"
51 #define MYMAIL_VERSION "0.9.9"
52
53 #define MYMAIL_DB_FORMAT_VERSION 1
54
55 #define MAX_NB_SEARCH_CONDITIONS 32
56
57 #define BUFFER_SIZE 65536
58 #define TOKEN_BUFFER_SIZE 1024
59
60 #define LEADING_FROM_LINE_REGEXP_STRING "^From .*\\(Mon\\|Tue\\|Wed\\|Thu\\|Fri\\|Sat\\|Sun\\) \\(Jan\\|Feb\\|Mar\\|Apr\\|May\\|Jun\\|Jul\\|Aug\\|Sep\\|Oct\\|Nov\\|Dec\\) [ 0123][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9] [0-9][0-9][0-9][0-9]\n$"
61
62 /********************************************************************/
63
64 struct alias_node {
65   char *alias, *value;
66   struct alias_node *next;
67 };
68
69 /* Global variables! */
70
71 int global_quiet;
72 int global_use_leading_time;
73 int global_nb_mails_max;
74 regex_t global_leading_from_line_regexp;
75 struct alias_node *global_alias_list;
76
77 /********************************************************************/
78
79 enum {
80   ID_MAIL = 0,
81   ID_LEADING_LINE,
82   ID_FROM,
83   ID_TO,
84   ID_SUBJECT,
85   ID_DATE,
86   ID_PARTICIPANT,
87   ID_BODY,
88   ID_TIME_INTERVAL,
89   ID_MAIL_ID,
90   ID_REFERENCE_ID,
91   MAX_ID
92 };
93
94 static char *field_keys[] = {
95   "mail",
96   "lead",
97   "from",
98   "to",
99   "subject",
100   "date",
101   "part",
102   "body",
103   "interval",
104   "mailid",
105   "references"
106 };
107
108 /********************************************************************/
109
110 struct search_condition {
111   int db_key;
112   regex_t db_value_regexp;
113   int negation;
114   time_t time_start, time_stop;
115 };
116
117 /********************************************************************/
118
119 struct parsable_field {
120   int id;
121   int cflags;
122   char *regexp_string;
123   regex_t regexp;
124 };
125
126 static struct parsable_field fields_to_parse[] = {
127   {
128     ID_LEADING_LINE,
129     0,
130     "^From ",
131     { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
132   },
133
134   {
135     ID_FROM,
136     REG_ICASE,
137     "^\\(from\\|reply-to\\|sender\\|return-path\\): ",
138     { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
139   },
140
141   {
142     ID_TO,
143     REG_ICASE,
144     "^\\(to\\|cc\\|bcc\\): ",
145     { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
146   },
147
148   {
149     ID_SUBJECT,
150     REG_ICASE,
151     "^subject: ",
152     { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
153   },
154
155   {
156     ID_DATE,
157     REG_ICASE,
158     "^date: ",
159     { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
160   },
161
162   {
163     ID_MAIL_ID,
164     REG_ICASE,
165     "^message-id: ",
166     { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
167   },
168
169   {
170     ID_REFERENCE_ID,
171     REG_ICASE,
172     "^\\(in-reply-to\\|references\\): ",
173     { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
174   },
175
176 };
177
178 /********************************************************************/
179
180 int xor(int a, int b) {
181   return (a && !b) || (!a && b);
182 }
183
184 const char *parse_token(char *token_buffer, size_t token_buffer_size,
185                         char separator, const char *string) {
186   char *u = token_buffer;
187
188   while(*string == separator) { string++; }
189
190   while(u < token_buffer + token_buffer_size - 1 && *string && *string != separator) {
191     *(u++) = *(string++);
192   }
193
194   while(*string == separator) { string++; }
195
196   *u = '\0';
197   return string;
198 }
199
200 char *default_value(char *current_value,
201                     const char *env_variable,
202                     const char *hard_default_value) {
203   if(current_value) {
204     return current_value;
205   } else {
206     char *env_value = getenv(env_variable);
207     if(env_value) {
208       return strdup(env_value);
209     } else if(hard_default_value) {
210       return strdup(hard_default_value);
211     } else {
212       return 0;
213     }
214   }
215 }
216
217 /********************************************************************/
218
219 void *safe_malloc(size_t n) {
220   void *p = malloc(n);
221   if(!p && n != 0) {
222     fprintf(stderr,
223             "mymail: cannot allocate memory: %s\n", strerror(errno));
224     exit(EXIT_FAILURE);
225   }
226   return p;
227 }
228
229 FILE *safe_fopen(const char *path, const char *mode, const char *comment) {
230   FILE *result = fopen(path, mode);
231   if(result) {
232     return result;
233   } else {
234     fprintf(stderr,
235             "mymail: Cannot open file '%s' (%s) with mode \"%s\": %s\n",
236             path, comment, mode,
237             strerror(errno));
238     exit(EXIT_FAILURE);
239   }
240 }
241
242 /*********************************************************************/
243
244 void print_version(FILE *out) {
245   fprintf(out, "mymail version %s (%s)\n", MYMAIL_VERSION, UNAME);
246 }
247
248 void print_usage(FILE *out) {
249   print_version(out);
250   fprintf(out, "Written by Francois Fleuret <francois@fleuret.org>.\n");
251   fprintf(out, "\n");
252   fprintf(out, "Usage: mymail [options] [<mbox dir1> [<mbox dir2> ...]|<db file1> [<db file2> ...]]\n");
253   fprintf(out, "\n");
254   fprintf(out, " -h, --help\n");
255   fprintf(out, "         show this help\n");
256   fprintf(out, " -v, --version\n");
257   fprintf(out, "         print the version number\n");
258   fprintf(out, " -q, --quiet\n");
259   fprintf(out, "         do not print information during search\n");
260   fprintf(out, " -t, --use-leading-time\n");
261   fprintf(out, "         use the time stamp from the leading line of each mail and not the Date:\n");
262   fprintf(out, "         field\n");
263   fprintf(out, " -p <db filename pattern>, --db-pattern <db filename pattern>\n");
264   fprintf(out, "         set the db filename pattern for recursive search\n");
265   fprintf(out, " -r <db root path>, --db-root <db root path>\n");
266   fprintf(out, "         set the db root path for recursive search\n");
267   fprintf(out, " -l <db filename list>, --db-list <db filename list>\n");
268   fprintf(out, "         set the semicolon-separated list of db files for search\n");
269   fprintf(out, " -m <mbox filename pattern>, --mbox-pattern <mbox filename pattern>\n");
270   fprintf(out, "         set the mbox filename pattern for recursive search\n");
271   fprintf(out, " -s <search pattern>, --search <search pattern>\n");
272   fprintf(out, "         search for matching mails in the db file\n");
273   fprintf(out, " -d <db filename>, --db-file-output <db filename>\n");
274   fprintf(out, "         set the db filename for indexing\n");
275   fprintf(out, " -i, --index\n");
276   fprintf(out, "         index mails\n");
277   fprintf(out, " -o <output filename>, --output <output filename>\n");
278   fprintf(out, "         set the result file, use stdout if unset\n");
279   fprintf(out, " -n <max number of mails>, --nb-mails-max <max number of mails>\n");
280   fprintf(out, "         set the maximum number of mails to extract\n");
281   fprintf(out, " -a <search field>, --default-search <search field>\n");
282   fprintf(out, "         set the default search field\n");
283 }
284
285 /*********************************************************************/
286
287 int ignore_entry(const char *name) {
288   return
289     strcmp(name, ".") == 0 ||
290     strcmp(name, "..") == 0 ||
291     (name[0] == '.' && name[1] != '/');
292 }
293
294 int is_a_leading_from_line(char *mbox_line) {
295   return
296     strncmp(mbox_line, "From ", 5) == 0 &&
297     regexec(&global_leading_from_line_regexp, mbox_line, 0, 0, 0) == 0;
298 }
299
300 int db_line_match_search(struct search_condition *condition,
301                          int db_key, const char *db_value) {
302
303   return
304     (
305      (condition->db_key == db_key)
306
307      ||
308
309      (condition->db_key == ID_PARTICIPANT && (db_key == ID_LEADING_LINE ||
310                                               db_key == ID_FROM ||
311                                               db_key == ID_TO))
312      ||
313
314      (condition->db_key == ID_FROM && db_key == ID_LEADING_LINE)
315
316      )
317
318     &&
319
320     regexec(&condition->db_value_regexp, db_value, 0, 0, 0) == 0;
321 }
322
323 void update_body_hits(char *mail_filename, int position_in_mail,
324                       int nb_search_conditions, struct search_condition *search_conditions,
325                       int nb_body_conditions,
326                       int *hits) {
327   FILE *mail_file;
328   int header, n;
329   char raw_mbox_line[BUFFER_SIZE];
330   int nb_body_hits;
331
332   nb_body_hits = 0;
333
334   header = 1;
335   mail_file = safe_fopen(mail_filename, "r", "mbox for body scan");
336
337   fseek(mail_file, position_in_mail, SEEK_SET);
338
339   if(fgets(raw_mbox_line, BUFFER_SIZE, mail_file)) {
340     while(nb_body_hits < nb_body_conditions) {
341       if(raw_mbox_line[0] == '\n') { header = 0; }
342
343       if(!header) {
344         for(n = 0; n < nb_search_conditions; n++) {
345           if(search_conditions[n].db_key == ID_BODY && !hits[n]) {
346             hits[n] =
347               (regexec(&search_conditions[n].db_value_regexp, raw_mbox_line, 0, 0, 0) == 0);
348             if(hits[n]) {
349               nb_body_hits++;
350             }
351           }
352         }
353       }
354
355       if(!fgets(raw_mbox_line, BUFFER_SIZE, mail_file) ||
356          (is_a_leading_from_line(raw_mbox_line)))
357         break;
358     }
359   }
360
361   fclose(mail_file);
362 }
363
364 void extract_mail(const char *mail_filename, unsigned long int position_in_mail,
365                   FILE *output_file) {
366   char raw_mbox_line[BUFFER_SIZE];
367   FILE *mail_file;
368
369   /* printf("Extract\n"); */
370
371   mail_file = safe_fopen(mail_filename, "r", "mbox for mail extraction");
372   fseek(mail_file, position_in_mail, SEEK_SET);
373
374   if(fgets(raw_mbox_line, BUFFER_SIZE, mail_file)) {
375     fprintf(output_file, "%s", raw_mbox_line);
376     while(1) {
377       if(!fgets(raw_mbox_line, BUFFER_SIZE, mail_file) ||
378          is_a_leading_from_line(raw_mbox_line))
379         break;
380       fprintf(output_file, "%s", raw_mbox_line);
381     }
382   }
383
384   fclose(mail_file);
385 }
386
387 int check_full_mail_match(char *current_mail_filename,
388                           time_t mail_time,
389                           int nb_search_conditions,
390                           struct search_condition *search_conditions,
391                           int nb_body_conditions,
392                           int *hits,
393                           int current_position_in_mail) {
394   int n, nb_fulfilled_body_conditions;
395
396   for(n = 0; n < nb_search_conditions; n++) {
397     if(search_conditions[n].db_key == ID_TIME_INTERVAL) {
398       hits[n] = (mail_time >= search_conditions[n].time_start &&
399                  (search_conditions[n].time_stop == 0 ||
400                   mail_time <= search_conditions[n].time_stop));
401     }
402   }
403
404   /* We first check all conditions but the body ones */
405
406   for(n = 0; n < nb_search_conditions &&
407         ((search_conditions[n].db_key == ID_BODY) ||
408          xor(hits[n], search_conditions[n].negation)); n++);
409
410   if(n == nb_search_conditions) {
411
412     /* Now check the body ones */
413
414     nb_fulfilled_body_conditions = 0;
415
416     if(nb_body_conditions > 0) {
417       update_body_hits(current_mail_filename, current_position_in_mail,
418                        nb_search_conditions, search_conditions,
419                        nb_body_conditions,
420                        hits);
421
422       for(n = 0; n < nb_search_conditions; n++) {
423         if(search_conditions[n].db_key == ID_BODY &&
424            xor(hits[n], search_conditions[n].negation)) {
425           nb_fulfilled_body_conditions++;
426         }
427       }
428     }
429     return nb_body_conditions == nb_fulfilled_body_conditions;
430   } else {
431     return 0;
432   }
433 }
434
435 /* We use the mail leading line time by default, and if we should and
436    can, we update with the Date: field */
437
438 void update_time(int db_key, const char *db_value, time_t *t) {
439   const char *c;
440   struct tm tm;
441
442   memset(&tm, 0, sizeof(struct tm));
443
444   if(db_key == ID_LEADING_LINE) {
445     c = db_value;
446     while(*c && *c != ' ') c++; while(*c && *c == ' ') c++;
447     /* printf("From %s", db_value); */
448     strptime(c, "%a %b %e %k:%M:%S %Y", &tm);
449     *t = mktime(&tm);
450   } else {
451     if(!global_use_leading_time) {
452       if(db_key == ID_DATE) {
453         if(strptime(db_value, "%a, %d %b %Y %k:%M:%S", &tm) ||
454            strptime(db_value, "%d %b %Y %k:%M:%S", &tm)) {
455           /* printf("Date: %s", db_value); */
456           *t = mktime(&tm);
457         }
458       }
459     }
460   }
461 }
462
463 int search_in_db(const char *db_filename,
464                  int nb_extracted_mails,
465                  int nb_search_conditions,
466                  struct search_condition *search_conditions,
467                  FILE *output_file) {
468
469   FILE *db_file;
470   char raw_db_line[BUFFER_SIZE];
471   char current_mail_filename[PATH_MAX + 1];
472   char db_key_string[TOKEN_BUFFER_SIZE];
473   char position_in_file_string[TOKEN_BUFFER_SIZE];
474   unsigned long int current_position_in_mail;
475   const char *db_value;
476   int db_key;
477   int hits[MAX_NB_SEARCH_CONDITIONS];
478   int nb_body_conditions, need_time;
479   time_t mail_time;
480
481   int m, n;
482
483   if(!global_quiet) {
484     printf("Searching in '%s' ... ", db_filename);
485     fflush(stdout);
486   }
487
488   db_file = safe_fopen(db_filename, "r", "index file for search");
489
490   /* First, check the db file leading line integrity */
491
492   if(fgets(raw_db_line, BUFFER_SIZE, db_file)) {
493     if(strncmp(raw_db_line, MYMAIL_DB_MAGIC_TOKEN, strlen(MYMAIL_DB_MAGIC_TOKEN))) {
494       fprintf(stderr,
495               "mymail: Header line in '%s' does not match the mymail db format.\n",
496               db_filename);
497       exit(EXIT_FAILURE);
498     }
499   } else {
500     fprintf(stderr,
501             "mymail: Cannot read the header line in '%s'.\n",
502             db_filename);
503     exit(EXIT_FAILURE);
504   }
505
506   /* Then parse the said db file */
507
508   current_position_in_mail = 0;
509
510   for(n = 0; n < nb_search_conditions; n++) { hits[n] = 0; }
511
512   nb_body_conditions = 0;
513   need_time = 0;
514   mail_time = 0;
515
516   for(n = 0; n < nb_search_conditions; n++) {
517     if(search_conditions[n].db_key == ID_BODY) {
518       nb_body_conditions++;
519     }
520     else if(search_conditions[n].db_key == ID_TIME_INTERVAL) {
521       need_time = 1;
522     }
523   }
524
525   strcpy(current_mail_filename, "");
526
527   while(nb_extracted_mails < global_nb_mails_max &&
528         fgets(raw_db_line, BUFFER_SIZE, db_file)) {
529
530     /* Removes the CR */
531     char *s = raw_db_line;
532     while(*s && *s != '\n') { s++; }
533     *s = '\0';
534
535     db_value = parse_token(db_key_string, TOKEN_BUFFER_SIZE, ' ', raw_db_line);
536
537     if(strcmp("mail", db_key_string) == 0) {
538       if(current_mail_filename[0]) {
539         if(check_full_mail_match(current_mail_filename,
540                                  mail_time,
541                                  nb_search_conditions, search_conditions,
542                                  nb_body_conditions, hits, current_position_in_mail)) {
543           extract_mail(current_mail_filename, current_position_in_mail, output_file);
544           nb_extracted_mails++;
545         }
546       }
547
548       for(n = 0; n < nb_search_conditions; n++) { hits[n] = 0; }
549       db_value = parse_token(position_in_file_string, TOKEN_BUFFER_SIZE, ' ', db_value);
550       strncpy(current_mail_filename, db_value, PATH_MAX + 1);
551       current_position_in_mail = atol(position_in_file_string);
552     }
553
554     else {
555       db_key = -1;
556       for(m = 0; (m < MAX_ID) && db_key == -1; m++) {
557         if(strncmp(field_keys[m], db_key_string, strlen(db_key_string)) == 0) {
558           db_key = m;
559         }
560       }
561
562       for(n = 0; n < nb_search_conditions; n++) {
563         hits[n] |= db_line_match_search(&search_conditions[n],
564                                         db_key, db_value);
565       }
566
567       if(need_time) {
568         update_time(db_key, db_value, &mail_time);
569       }
570     }
571   }
572
573   if(nb_extracted_mails < global_nb_mails_max &&
574      current_mail_filename[0] &&
575      check_full_mail_match(current_mail_filename,
576                            mail_time,
577                            nb_search_conditions, search_conditions,
578                            nb_body_conditions, hits, current_position_in_mail)) {
579     extract_mail(current_mail_filename, current_position_in_mail, output_file);
580     nb_extracted_mails++;
581   }
582
583   fclose(db_file);
584
585   if(!global_quiet) {
586     printf("done.\n");
587     fflush(stdout);
588   }
589
590   return nb_extracted_mails;
591 }
592
593 int recursive_search_in_db(const char *entry_name, regex_t *db_filename_regexp,
594                            int nb_extracted_mails,
595                            int nb_search_conditions,
596                            struct search_condition *search_conditions,
597                            FILE *output_file) {
598   DIR *dir;
599   struct dirent *dir_e;
600   struct stat sb;
601   char subname[PATH_MAX + 1];
602
603   if(lstat(entry_name, &sb) != 0) {
604     fprintf(stderr,
605             "mymail: Cannot stat \"%s\": %s\n",
606             entry_name,
607             strerror(errno));
608     exit(EXIT_FAILURE);
609   }
610
611   /* printf("recursive_search_in_db %s\n", entry_name); */
612
613   dir = opendir(entry_name);
614
615   if(dir) {
616     while((dir_e = readdir(dir)) &&
617           nb_extracted_mails < global_nb_mails_max) {
618       if(!ignore_entry(dir_e->d_name)) {
619         snprintf(subname, PATH_MAX, "%s/%s", entry_name, dir_e->d_name);
620         nb_extracted_mails = recursive_search_in_db(subname, db_filename_regexp,
621                                                     nb_extracted_mails,
622                                                     nb_search_conditions, search_conditions,
623                                                     output_file);
624       }
625     }
626     closedir(dir);
627   }
628
629   else {
630     const char *s = entry_name, *filename = entry_name;
631     while(*s) { if(*s == '/') { filename = s+1; } s++; }
632
633     if(regexec(db_filename_regexp, filename, 0, 0, 0) == 0) {
634       nb_extracted_mails =
635         search_in_db(entry_name,
636                      nb_extracted_mails,
637                      nb_search_conditions, search_conditions, output_file);
638     }
639   }
640
641   return nb_extracted_mails;
642 }
643
644 /*********************************************************************/
645
646 void index_one_mbox_line(unsigned int nb_fields_to_parse,
647                          struct parsable_field *fields_to_parse,
648                          char *raw_mbox_line, FILE *db_file) {
649   regmatch_t matches;
650   unsigned int f;
651   for(f = 0; f < nb_fields_to_parse; f++) {
652     if(regexec(&fields_to_parse[f].regexp, raw_mbox_line, 1, &matches, 0) == 0) {
653       fprintf(db_file, "%s %s\n",
654               field_keys[fields_to_parse[f].id],
655               raw_mbox_line + matches.rm_eo);
656     }
657   }
658 }
659
660 void index_mbox(const char *mbox_filename,
661                 int nb_fields_to_parse, struct parsable_field *fields_to_parse,
662                 FILE *db_file) {
663   char raw_mbox_line[BUFFER_SIZE], full_line[BUFFER_SIZE];
664   char *end_of_full_line;
665   FILE *file;
666   int in_header, new_header;
667   unsigned long int position_in_file;
668
669   file = safe_fopen(mbox_filename, "r", "mbox for indexing");
670
671   in_header = 0;
672   new_header = 0;
673
674   position_in_file = 0;
675   end_of_full_line = 0;
676   full_line[0] = '\0';
677
678   while(fgets(raw_mbox_line, BUFFER_SIZE, file)) {
679     if(is_a_leading_from_line(raw_mbox_line)) {
680       /* This starts a new mail */
681       if(in_header) {
682         fprintf(stderr,
683                 "Got a ^\"From \" in the header in %s:%lu.\n",
684                 mbox_filename, position_in_file);
685         fprintf(stderr, "%s", raw_mbox_line);
686       }
687
688       /* printf("LEADING_LINE %s", raw_mbox_line); */
689
690       in_header = 1;
691       new_header = 1;
692     } else if(raw_mbox_line[0] == '\n') {
693       if(in_header) {
694         in_header = 0;
695         /* We leave the header, index the current line */
696         if(full_line[0]) {
697           /* printf("INDEX %s\n", full_line); */
698           index_one_mbox_line(nb_fields_to_parse, fields_to_parse, full_line, db_file);
699         }
700         end_of_full_line = full_line;
701         *end_of_full_line = '\0';
702       }
703     }
704
705     if(in_header) {
706       if(new_header) {
707         fprintf(db_file, "mail %lu %s\n", position_in_file, mbox_filename);
708         new_header = 0;
709       }
710
711       if(raw_mbox_line[0] == ' ' || raw_mbox_line[0] == '\t') {
712         /* Continuation of a line */
713         char *start = raw_mbox_line;
714         while(*start == ' ' || *start == '\t') start++;
715         *(end_of_full_line++) = ' ';
716         strcpy(end_of_full_line, start);
717         while(*end_of_full_line && *end_of_full_line != '\n') {
718           end_of_full_line++;
719         }
720         *end_of_full_line = '\0';
721       }
722
723       else {
724         /* Start a new header line, not a continuation */
725
726         if(full_line[0]) {
727           /* printf("INDEX %s\n", full_line); */
728           index_one_mbox_line(nb_fields_to_parse, fields_to_parse, full_line, db_file);
729         }
730
731         end_of_full_line = full_line;
732         strcpy(end_of_full_line, raw_mbox_line);
733         while(*end_of_full_line && *end_of_full_line != '\n') {
734           end_of_full_line++;
735         }
736         *end_of_full_line = '\0';
737       }
738
739     }
740
741     position_in_file += strlen(raw_mbox_line);
742   }
743
744   fclose(file);
745 }
746
747 void recursive_index_mbox(FILE *db_file,
748                           const char *entry_name, regex_t *mbox_filename_regexp,
749                           int nb_fields_to_parse, struct parsable_field *fields_to_parse) {
750   DIR *dir;
751   struct dirent *dir_e;
752   struct stat sb;
753   char subname[PATH_MAX + 1];
754
755   if(lstat(entry_name, &sb) != 0) {
756     fprintf(stderr,
757             "mymail: Cannot stat \"%s\": %s\n",
758             entry_name,
759             strerror(errno));
760     exit(EXIT_FAILURE);
761   }
762
763   dir = opendir(entry_name);
764
765   if(dir) {
766     while((dir_e = readdir(dir))) {
767       if(!ignore_entry(dir_e->d_name)) {
768         snprintf(subname, PATH_MAX, "%s/%s", entry_name, dir_e->d_name);
769         recursive_index_mbox(db_file, subname, mbox_filename_regexp,
770                              nb_fields_to_parse, fields_to_parse);
771       }
772     }
773     closedir(dir);
774   } else {
775     const char *s = entry_name, *filename = s;
776     while(*s) { if(*s == '/') { filename = s+1; }; s++; }
777     if(!mbox_filename_regexp || regexec(mbox_filename_regexp, filename, 0, 0, 0) == 0) {
778       index_mbox(entry_name, nb_fields_to_parse, fields_to_parse, db_file);
779     }
780   }
781 }
782
783 /*********************************************************************/
784
785 /* For long options that have no equivalent short option, use a
786    non-character as a pseudo short option, starting with CHAR_MAX + 1.  */
787 enum {
788   OPT_BASH_MODE = CHAR_MAX + 1
789 };
790
791 static struct option long_options[] = {
792   { "help", no_argument, 0, 'h' },
793   { "version", no_argument, 0, 'v' },
794   { "quiet", no_argument, 0, 'q' },
795   { "use-leading-time", no_argument, 0, 't' },
796   { "db-file-output", 1, 0, 'd' },
797   { "db-pattern", 1, 0, 'p' },
798   { "db-root", 1, 0, 'r' },
799   { "db-list", 1, 0, 'l' },
800   { "mbox-pattern", 1, 0, 'm' },
801   { "search", 1, 0, 's' },
802   { "index", 0, 0, 'i' },
803   { "output", 1, 0, 'o' },
804   { "default-search", 1, 0, 'a' },
805   { "nb-mails-max", 1, 0, 'n' },
806   { 0, 0, 0, 0 }
807 };
808
809 struct time_criterion {
810   char *label;
811   int day_criterion;
812   int start_hour, end_hour;
813   int past_week_day;
814 };
815
816 /*********************************************************************/
817
818 static struct time_criterion time_criteria[] = {
819
820   { "8h",        0,  8,       -1, -1 },
821   { "24h",       0, 24,       -1, -1 },
822   { "48h",       0, 48,       -1, -1 },
823   { "week",      0, 24 *   7, -1, -1 },
824   { "month",     0, 24 *  31, -1, -1 },
825   { "trimester", 0, 24 *  92, -1, -1 },
826   { "year",      0, 24 * 365, -1, -1 },
827
828   { "yesterday", 1, -1,       -1, -1 },
829   { "today",     1, -1,       -1,  0 },
830
831   { "monday",    1, -1,       -1,  1 },
832   { "tuesday",   1, -1,       -1,  2 },
833   { "wednesday", 1, -1,       -1,  3 },
834   { "thursday",  1, -1,       -1,  4 },
835   { "friday",    1, -1,       -1,  5 },
836   { "saturday",  1, -1,       -1,  6 },
837   { "sunday",    1, -1,       -1,  7 },
838
839 };
840
841 /*********************************************************************/
842
843 time_t time_for_past_day(int day) {
844   time_t t;
845   struct tm *tm;
846   int delta_day;
847   t = time(0);
848   tm = localtime(&t);
849   if(day > 0) {
850     delta_day = (7 + tm->tm_wday - day) % 7;
851   } else {
852     delta_day = - day;
853   }
854   return t - (delta_day * 3600 * 24 + tm->tm_sec + 60 * tm->tm_min + 3600 * tm->tm_hour);
855 }
856
857 void init_condition(struct search_condition *condition, const char *full_string,
858                     const char *default_search_field) {
859   char full_search_field[TOKEN_BUFFER_SIZE], *search_field;
860   unsigned int k, m;
861   const char *string;
862   struct alias_node *a;
863
864   for(a = global_alias_list; a; a = a->next) {
865     if(strcmp(full_string, a->alias) == 0) {
866       full_string = a->value;
867       break;
868     }
869   }
870
871   string = parse_token(full_search_field, TOKEN_BUFFER_SIZE, ' ', full_string);
872   search_field = full_search_field;
873
874   if(search_field[0] == '!') {
875     search_field++;
876     condition->negation = 1;
877   } else {
878     condition->negation = 0;
879   }
880
881   condition->db_key = -1;
882
883   /* Time condition */
884
885   for(k = 0; k < sizeof(time_criteria) / sizeof(struct time_criterion); k++) {
886     if(strcmp(time_criteria[k].label, search_field) == 0) {
887       condition->db_key = ID_TIME_INTERVAL;
888       if(time_criteria[k].day_criterion) {
889         condition->time_start = time_for_past_day(time_criteria[k].past_week_day);
890         condition->time_stop = condition->time_start + 3600 * 24;
891       } else {
892         condition->time_start = time(0) - 3600 * time_criteria[k].start_hour;
893         if(time_criteria[k].end_hour >= 0) {
894           condition->time_stop = time(0) - 3600 * time_criteria[k].end_hour;
895         } else {
896           condition->time_stop = 0;
897         }
898       }
899
900       break;
901     }
902   }
903
904   if(condition->db_key == -1) {
905
906     /* No time condition matched, look for the search fields */
907
908     for(m = 0; (m < MAX_ID) && condition->db_key == -1; m++) {
909       if(strncmp(field_keys[m], search_field, strlen(search_field)) == 0) {
910         condition->db_key = m;
911       }
912     }
913
914     /* None match, if there is a default search field, re-run the search with it */
915
916     if(condition->db_key == -1) {
917       if(default_search_field) {
918         for(m = 0; (m < MAX_ID) && condition->db_key == -1; m++) {
919           if(strncmp(field_keys[m],
920                      default_search_field, strlen(default_search_field)) == 0) {
921             condition->db_key = m;
922           }
923         }
924         string = full_string;
925         if(string[0] == '!') { string++; }
926       }
927     }
928
929     if(condition->db_key == -1) {
930       fprintf(stderr,
931               "mymail: Syntax error in field key \"%s\".\n",
932               search_field);
933       exit(EXIT_FAILURE);
934     }
935
936     if(regcomp(&condition->db_value_regexp,
937                string,
938                REG_ICASE)) {
939       fprintf(stderr,
940               "mymail: Syntax error in regexp \"%s\" for field \"%s\".\n",
941               string,
942               field_keys[condition->db_key]);
943       exit(EXIT_FAILURE);
944     }
945   }
946 }
947
948 void free_condition(struct search_condition *condition) {
949   if(condition->db_key != ID_TIME_INTERVAL) {
950     regfree(&condition->db_value_regexp);
951   }
952 }
953
954 const char *eat_space(const char *s) {
955   while(*s == ' ' || *s == '\t') { s++; }
956   return s;
957 }
958
959 void read_rc_file(const char *rc_filename) {
960   char raw_line[BUFFER_SIZE];
961   char command[TOKEN_BUFFER_SIZE], tmp_token[TOKEN_BUFFER_SIZE];
962
963   FILE *rc_file;
964   int line_number;
965   const char *s;
966   char *t;
967
968   rc_file = fopen(rc_filename, "r");
969
970   if(rc_file) {
971     line_number = 1;
972     while(fgets(raw_line, BUFFER_SIZE, rc_file)) {
973       t = raw_line;
974       while(*t) { if(*t == '\n') { *t = '\0'; }; t++; }
975
976       s = raw_line;
977       s = eat_space(s);
978
979       if(*s && *s != '#') {
980         s = parse_token(command, TOKEN_BUFFER_SIZE, ' ', s);
981
982         if(strcmp(command, "alias") == 0) {
983           struct alias_node *a = safe_malloc(sizeof(struct alias_node));
984           a->next = global_alias_list;
985           global_alias_list = a;
986           if(s) {
987             s = eat_space(s);
988             s = parse_token(tmp_token, TOKEN_BUFFER_SIZE, '=', s);
989             a->alias = strdup(tmp_token);
990             if(s) {
991               s = eat_space(s);
992               a->value = strdup(s);
993             } else {
994               fprintf(stderr, "%s:%d syntax error, missing alias value.\n",
995                       rc_filename,
996                       line_number);
997               exit(EXIT_FAILURE);
998             }
999           } else {
1000             fprintf(stderr, "%s:%d syntax error, missing alias key.\n",
1001                     rc_filename,
1002                     line_number);
1003             exit(EXIT_FAILURE);
1004           }
1005         } else {
1006           fprintf(stderr, "%s:%d syntax error, unknown command '%s'.\n",
1007                   rc_filename,
1008                   line_number,
1009                   command);
1010           exit(EXIT_FAILURE);
1011         }
1012       }
1013
1014       line_number++;
1015     }
1016     fclose(rc_file);
1017   }
1018 }
1019
1020 /*********************************************************************/
1021 /*********************************************************************/
1022 /*********************************************************************/
1023
1024 int main(int argc, char **argv) {
1025   char *db_filename = 0;
1026   char *db_filename_regexp_string = 0;
1027   char *db_root_path = 0;
1028   char *db_filename_list = 0;
1029   char *mbox_filename_regexp_string = 0;
1030   char *default_search_field;
1031   char output_filename[PATH_MAX + 1];
1032   char rc_filename[PATH_MAX + 1];
1033   int action_index = 0;
1034   int error = 0, show_help = 0;
1035   const unsigned int nb_fields_to_parse =
1036     sizeof(fields_to_parse) / sizeof(struct parsable_field);
1037   char c;
1038   unsigned int f, n;
1039   unsigned int nb_search_conditions;
1040   struct search_condition search_conditions[MAX_NB_SEARCH_CONDITIONS];
1041   struct alias_node *a, *b;
1042
1043   if(regcomp(&global_leading_from_line_regexp, LEADING_FROM_LINE_REGEXP_STRING, 0)) {
1044     fprintf(stderr,
1045             "mymail: Cannot compile leading \"from\" line regexp. That is strange.\n");
1046     exit(EXIT_FAILURE);
1047   }
1048
1049   if(getenv("MYMAILRC")) {
1050     sprintf(rc_filename, "%s", getenv("MYMAILRC"));
1051   } else if(getenv("HOME")) {
1052     sprintf(rc_filename, "%s/.mymailrc", getenv("HOME"));
1053   } else {
1054     rc_filename[0] = '\0';
1055   }
1056
1057   global_alias_list = 0;
1058   global_quiet = 0;
1059   global_use_leading_time = 0;
1060   global_nb_mails_max = 250;
1061
1062   default_search_field = 0;
1063   strncpy(output_filename, "", PATH_MAX);
1064
1065   if(rc_filename[0]) {
1066     read_rc_file(rc_filename);
1067   }
1068
1069   /*
1070     {
1071     #warning Test code added on 2013 May 02 11:17:01
1072     struct alias_node *a;
1073     for(a = global_alias_list; a; a = a->next) {
1074     printf ("ALIAS [%s] [%s]\n", a->alias, a->value);
1075     }
1076     }
1077   */
1078
1079   setlocale(LC_ALL, "");
1080
1081   nb_search_conditions = 0;
1082
1083   while ((c = getopt_long(argc, argv, "hvqip:s:d:r:l:o:a:m:",
1084                           long_options, NULL)) != -1) {
1085
1086     switch(c) {
1087
1088     case 'h':
1089       show_help = 1;
1090       break;
1091
1092     case 'v':
1093       print_version(stdout);
1094       break;
1095
1096     case 'q':
1097       global_quiet = 1;
1098       break;
1099
1100     case 't':
1101       global_use_leading_time = 1;
1102       break;
1103
1104     case 'i':
1105       action_index = 1;
1106       break;
1107
1108     case 'd':
1109       if(db_filename) {
1110         fprintf(stderr, "mymail: Can not set the db filename twice.\n");
1111         exit(EXIT_FAILURE);
1112       }
1113       db_filename = strdup(optarg);
1114       break;
1115
1116     case 'p':
1117       if(db_filename_regexp_string) {
1118         fprintf(stderr, "mymail: Can not set the db filename pattern twice.\n");
1119         exit(EXIT_FAILURE);
1120       }
1121       db_filename_regexp_string = strdup(optarg);
1122       break;
1123
1124     case 'm':
1125       if(mbox_filename_regexp_string) {
1126         fprintf(stderr, "mymail: Can not set the mbox filename pattern twice.\n");
1127         exit(EXIT_FAILURE);
1128       }
1129       mbox_filename_regexp_string = strdup(optarg);
1130       break;
1131
1132     case 'o':
1133       strncpy(output_filename, optarg, PATH_MAX);
1134       break;
1135
1136     case 'r':
1137       if(db_root_path) {
1138         fprintf(stderr, "mymail: Can not set the db root path twice.\n");
1139         exit(EXIT_FAILURE);
1140       }
1141       db_root_path = strdup(optarg);
1142       break;
1143
1144     case 'l':
1145       if(db_filename_list) {
1146         fprintf(stderr, "mymail: Can not set the db filename list twice.\n");
1147         exit(EXIT_FAILURE);
1148       }
1149       db_filename_list = strdup(optarg);
1150       break;
1151
1152     case 's':
1153       if(nb_search_conditions == MAX_NB_SEARCH_CONDITIONS) {
1154         fprintf(stderr, "mymail: Too many search patterns.\n");
1155         exit(EXIT_FAILURE);
1156       }
1157       init_condition(&search_conditions[nb_search_conditions], optarg, default_search_field);
1158       nb_search_conditions++;
1159       break;
1160
1161     case 'a':
1162       default_search_field = optarg;
1163       break;
1164
1165     case 'n':
1166       global_nb_mails_max = atoi(optarg);
1167       break;
1168
1169     default:
1170       error = 1;
1171       break;
1172     }
1173   }
1174
1175   if(error) {
1176     print_usage(stderr);
1177     exit(EXIT_FAILURE);
1178   }
1179
1180   if(show_help) {
1181     print_usage(stdout);
1182     exit(EXIT_SUCCESS);
1183   }
1184
1185   /* Set all the values that may defined in the arguments, through
1186      environment variables, or hard-coded */
1187
1188   db_filename = default_value(db_filename,
1189                               "MYMAIL_DB_FILE",
1190                               "mymail.db");
1191
1192   db_filename_regexp_string = default_value(db_filename_regexp_string,
1193                                             "MYMAIL_DB_FILE",
1194                                             "\\.db$");
1195
1196   db_root_path = default_value(db_root_path,
1197                                "MYMAIL_DB_ROOT",
1198                                0);
1199
1200   db_filename_list = default_value(db_filename_list,
1201                                    "MYMAIL_DB_LIST",
1202                                    0);
1203
1204   mbox_filename_regexp_string = default_value(mbox_filename_regexp_string,
1205                                               "MYMAIL_MBOX_PATTERN",
1206                                               0);
1207
1208   /* mbox indexing */
1209
1210   if(action_index) {
1211     FILE *db_file;
1212     regex_t mbox_filename_regexp_static;
1213     regex_t *mbox_filename_regexp;
1214
1215     if(mbox_filename_regexp_string) {
1216       if(regcomp(&mbox_filename_regexp_static,
1217                  mbox_filename_regexp_string,
1218                  0)) {
1219         fprintf(stderr,
1220                 "mymail: Syntax error in regexp \"%s\".\n",
1221                 mbox_filename_regexp_string);
1222         exit(EXIT_FAILURE);
1223       }
1224       mbox_filename_regexp = &mbox_filename_regexp_static;
1225     } else {
1226       mbox_filename_regexp = 0;
1227     }
1228
1229     db_file = safe_fopen(db_filename, "w", "index file for indexing");
1230
1231     for(f = 0; f < nb_fields_to_parse; f++) {
1232       if(regcomp(&fields_to_parse[f].regexp,
1233                  fields_to_parse[f].regexp_string,
1234                  fields_to_parse[f].cflags)) {
1235         fprintf(stderr,
1236                 "mymail: Syntax error in regexp \"%s\" for field \"%s\".\n",
1237                 fields_to_parse[f].regexp_string,
1238                 field_keys[fields_to_parse[f].id]);
1239         exit(EXIT_FAILURE);
1240       }
1241     }
1242
1243     fprintf(db_file,
1244             "%s version_%s format_%d raw\n",
1245             MYMAIL_DB_MAGIC_TOKEN,
1246             MYMAIL_VERSION,
1247             MYMAIL_DB_FORMAT_VERSION);
1248
1249     while(optind < argc) {
1250       recursive_index_mbox(db_file,
1251                            argv[optind], mbox_filename_regexp,
1252                            nb_fields_to_parse, fields_to_parse);
1253       optind++;
1254     }
1255
1256     fflush(db_file);
1257     fclose(db_file);
1258
1259     if(mbox_filename_regexp) {
1260       regfree(mbox_filename_regexp);
1261     }
1262
1263     for(f = 0; f < nb_fields_to_parse; f++) {
1264       regfree(&fields_to_parse[f].regexp);
1265     }
1266   }
1267
1268   /* Mail search */
1269
1270   else {
1271
1272     FILE *output_file;
1273     int nb_extracted_mails = 0;
1274
1275     if(output_filename[0]) {
1276       output_file = safe_fopen(output_filename, "w", "result mbox");
1277     } else {
1278       output_file = stdout;
1279       global_quiet = 1;
1280     }
1281
1282     if(nb_search_conditions > 0) {
1283
1284       /* Recursive search if db_root_path is set */
1285
1286       if(db_root_path) {
1287         regex_t db_filename_regexp;
1288         if(regcomp(&db_filename_regexp,
1289                    db_filename_regexp_string,
1290                    0)) {
1291           fprintf(stderr,
1292                   "mymail: Syntax error in regexp \"%s\".\n",
1293                   db_filename_regexp_string);
1294           exit(EXIT_FAILURE);
1295         }
1296
1297         nb_extracted_mails = recursive_search_in_db(db_root_path, &db_filename_regexp,
1298                                                     nb_extracted_mails,
1299                                                     nb_search_conditions, search_conditions,
1300                                                     output_file);
1301
1302         regfree(&db_filename_regexp);
1303       }
1304
1305       /* Search in all db files listed in db_filename_list */
1306
1307       if(db_filename_list) {
1308         char db_filename[PATH_MAX + 1];
1309         const char *s;
1310
1311         s = db_filename_list;
1312
1313         while(*s) {
1314           s = parse_token(db_filename, PATH_MAX + 1, ';', s);
1315
1316           if(db_filename[0]) {
1317             nb_extracted_mails =
1318               search_in_db(db_filename,
1319                            nb_extracted_mails,
1320                            nb_search_conditions, search_conditions, output_file);
1321           }
1322         }
1323       }
1324
1325       /* Search in all db files listed in the command arguments */
1326
1327       while(optind < argc) {
1328         nb_extracted_mails =
1329           search_in_db(argv[optind],
1330                        nb_extracted_mails,
1331                        nb_search_conditions, search_conditions, output_file);
1332         optind++;
1333       }
1334     }
1335
1336     if(!global_quiet) {
1337       if(nb_extracted_mails > 0) {
1338         printf("Found %d matching mails.\n", nb_extracted_mails);
1339       } else {
1340         printf("No matching mail found.\n");
1341       }
1342     }
1343
1344     fflush(output_file);
1345
1346     if(output_file != stdout) {
1347       fclose(output_file);
1348     }
1349   }
1350
1351   for(n = 0; n < nb_search_conditions; n++) {
1352     free_condition(&search_conditions[n]);
1353   }
1354
1355   a = global_alias_list;
1356   while(a) {
1357     b = a->next;
1358     free(a->alias);
1359     free(a->value);
1360     free(a);
1361     a = b;
1362   }
1363
1364   free(db_filename);
1365   free(db_filename_regexp_string);
1366   free(db_root_path);
1367   free(db_filename_list);
1368   free(mbox_filename_regexp_string);
1369
1370   regfree(&global_leading_from_line_regexp);
1371
1372   exit(EXIT_SUCCESS);
1373 }