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   This command is a dumb mail indexer. It can either (1) scan
25   directories containing mbox files, and create a db file containing
26   for each mail a list of fields computed from the header, or (2)
27   read such a db file and get all the mails matching regexp-defined
28   conditions on the fields.
29
30   It is low-tech, simple, light and fast.
31
32 */
33
34 #define _GNU_SOURCE
35
36 #include <stdio.h>
37 #include <stdlib.h>
38 #include <string.h>
39 #include <errno.h>
40 #include <fcntl.h>
41 #include <locale.h>
42 #include <getopt.h>
43 #include <limits.h>
44 #include <dirent.h>
45 #include <regex.h>
46 #include <time.h>
47
48 #define MYMAIL_DB_MAGIC_TOKEN "mymail_index_file"
49 #define VERSION "0.9.1"
50
51 #define MAX_NB_SEARCH_CONDITIONS 10
52
53 #define BUFFER_SIZE 65536
54 #define TOKEN_BUFFER_SIZE 1024
55
56 regex_t leading_from_line_regexp;
57
58 /* Global variables! */
59
60 int paranoid;
61 int quiet;
62 char *default_search_field;
63
64 /********************************************************************/
65
66 enum {
67   ID_MAIL = 0,
68   ID_LEADING_LINE,
69   ID_FROM,
70   ID_TO,
71   ID_SUBJECT,
72   ID_DATE,
73   ID_PARTICIPANT,
74   ID_BODY,
75   ID_INTERVAL,
76   MAX_ID
77 };
78
79 static char *field_names[] = {
80   "mail",
81   "lead",
82   "from",
83   "to",
84   "subject",
85   "date",
86   "part",
87   "body",
88   "interval"
89 };
90
91 /********************************************************************/
92
93 struct search_condition {
94   int field_id;
95   int negation;
96   regex_t regexp;
97   time_t interval_start, interval_stop;
98 };
99
100 /********************************************************************/
101
102 struct parsable_field {
103   int id;
104   int cflags;
105   char *regexp_string;
106   regex_t regexp;
107 };
108
109 static struct parsable_field fields_to_parse[] = {
110   {
111     ID_LEADING_LINE,
112     0,
113     "^From ",
114     { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
115   },
116
117   {
118     ID_FROM,
119     REG_ICASE,
120     "^\\(from\\|reply-to\\|sender\\|return-path\\): ",
121     { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
122   },
123
124   {
125     ID_TO,
126     REG_ICASE,
127     "^\\(to\\|cc\\|bcc\\): ",
128     { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
129   },
130
131   {
132     ID_SUBJECT,
133     REG_ICASE,
134     "^subject: ",
135     { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
136   },
137
138   {
139     ID_DATE,
140     REG_ICASE,
141     "^date: ",
142     { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
143   },
144
145 };
146
147 /********************************************************************/
148
149 int xor(int a, int b) {
150   return (a && !b) || (!a && b);
151 }
152
153 char *parse_token(char *token_buffer, size_t token_buffer_size,
154                   char separator, char *string) {
155   char *u = token_buffer;
156   while(u < token_buffer + token_buffer_size - 1 && *string &&
157         *string != separator) {
158     *(u++) = *(string++);
159   }
160   while(*string == separator) string++;
161   *u = '\0';
162   return string;
163 }
164
165 /********************************************************************/
166
167 /* malloc with error checking.  */
168
169 void *safe_malloc(size_t n) {
170   void *p = malloc(n);
171   if(!p && n != 0) {
172     fprintf(stderr,
173             "mymail: cannot allocate memory: %s\n", strerror(errno));
174     exit(EXIT_FAILURE);
175   }
176   return p;
177 }
178
179 /*********************************************************************/
180
181 void print_version(FILE *out) {
182   fprintf(out, "mymail version %s (%s)\n", VERSION, UNAME);
183 }
184
185 void print_usage(FILE *out) {
186   print_version(out);
187   fprintf(out, "Written by Francois Fleuret <francois@fleuret.org>.\n");
188   fprintf(out, "\n");
189   fprintf(out, "Usage: mymail [options] [<mbox dir1> [<mbox dir2> ...]|<db file1> [<db file2> ...]]\n");
190   fprintf(out, "\n");
191   fprintf(out, " -h, --help\n");
192   fprintf(out, "         show this help\n");
193   fprintf(out, " -v, --version\n");
194   fprintf(out, "         print the version number\n");
195   fprintf(out, " -q, --quiet\n");
196   fprintf(out, "         do not print information during search\n");
197   fprintf(out, " -p <db filename pattern>, --db-pattern <db filename pattern>\n");
198   fprintf(out, "         set the db filename pattern for recursive search\n");
199   fprintf(out, " -r <db root path>, --db-root <db root path>\n");
200   fprintf(out, "         set the db root path for recursive search\n");
201   fprintf(out, " -l <db filename list>, --db-list <db filename list>\n");
202   fprintf(out, "         set the semicolon-separated list of db files for search\n");
203   fprintf(out, " -s <search pattern>, --search <search pattern>\n");
204   fprintf(out, "         search for matching mails in the db file\n");
205   fprintf(out, " -d <db filename>, --db-file <db filename>\n");
206   fprintf(out, "         set the db filename for indexing\n");
207   fprintf(out, " -i, --index\n");
208   fprintf(out, "         index mails\n");
209   fprintf(out, " -o <output filename>, --output <output filename>\n");
210   fprintf(out, "         set the result file, use stdout if unset\n");
211   fprintf(out, " -a <search field>, --default-search <search field>\n");
212   fprintf(out, "         set the default search field\n");
213 }
214
215 /*********************************************************************/
216
217 time_t time_for_past_day(int day) {
218   time_t t;
219   struct tm *tm;
220   int delta_day;
221   t = time(0);
222   tm = localtime(&t);
223   delta_day = (7 + tm->tm_wday - day) % 7 + 1;
224   return t - delta_day * 3600 * 24 + tm->tm_sec + 60 * tm->tm_min + 3600 * tm->tm_hour;
225 }
226
227 /*********************************************************************/
228
229 int ignore_entry(const char *name) {
230   return
231     /* strcmp(name, ".") == 0 || */
232     /* strcmp(name, "..") == 0 || */
233     (name[0] == '.' && name[1] != '/');
234 }
235
236 int is_a_leading_from_line(int last_mbox_line_was_empty, char *mbox_line) {
237   return
238
239     /*
240
241       The mbox man page in qmail documentation states:
242
243        > The reader should not attempt to take advantage of the fact
244        > that every From_ line (past the beginning of the file) is
245        > preceded by a blank line.
246
247     */
248
249     /* last_mbox_line_was_empty && */
250     strncmp(mbox_line, "From ", 5) == 0 &&
251     regexec(&leading_from_line_regexp, mbox_line, 0, 0, 0) == 0;
252 }
253
254 int mbox_line_match_search(struct search_condition *condition,
255                            int mbox_id, char *mbox_value) {
256
257   if(condition->field_id == ID_INTERVAL) {
258     if(mbox_id == ID_LEADING_LINE) {
259       char *c;
260       time_t t;
261       struct tm tm;
262
263       c = mbox_value;
264       while(*c && *c != ' ') c++; while(*c && *c == ' ') c++;
265       strptime(c, "%a %b %e %k:%M:%S %Y", &tm);
266       t = mktime(&tm);
267
268       return (t >= condition->interval_start &&
269               (condition->interval_stop == 0 ||
270                t <= condition->interval_stop));
271     } else {
272       return 0;
273     }
274   } else {
275     return
276       (
277
278        (condition->field_id == mbox_id)
279
280        ||
281
282        (condition->field_id == ID_PARTICIPANT && (mbox_id == ID_LEADING_LINE ||
283                                                   mbox_id == ID_FROM ||
284                                                   mbox_id == ID_TO))
285        ||
286
287        (condition->field_id == ID_FROM && mbox_id == ID_LEADING_LINE)
288
289        )
290
291       &&
292
293       regexec(&condition->regexp, mbox_value, 0, 0, 0) == 0;
294   }
295 }
296
297 void update_body_hits(char *mail_filename, int position_in_mail,
298                       int nb_search_conditions, struct search_condition *search_conditions,
299                       int nb_body_conditions,
300                       int *hits) {
301   FILE *mail_file;
302   int header, n;
303   int last_mbox_line_was_empty;
304   char raw_mbox_line[BUFFER_SIZE];
305   int nb_body_hits;
306
307   nb_body_hits = 0;
308
309   header = 1;
310   mail_file = fopen(mail_filename, "r");
311
312   if(!mail_file) {
313     fprintf(stderr,
314             "mymail: Cannot open mbox '%s' for body scan.\n",
315             mail_filename);
316     exit(EXIT_FAILURE);
317   }
318
319   fseek(mail_file, position_in_mail, SEEK_SET);
320
321   if(fgets(raw_mbox_line, BUFFER_SIZE, mail_file)) {
322     while(nb_body_hits < nb_body_conditions) {
323       last_mbox_line_was_empty = (raw_mbox_line[0] == '\n');
324
325       if(last_mbox_line_was_empty) { header = 0; }
326
327       if(!header) {
328         for(n = 0; n < nb_search_conditions; n++) {
329           if(search_conditions[n].field_id == ID_BODY && !hits[n]) {
330             hits[n] =
331               (regexec(&search_conditions[n].regexp, raw_mbox_line, 0, 0, 0) == 0);
332             if(hits[n]) {
333               nb_body_hits++;
334             }
335           }
336         }
337       }
338
339       if(!fgets(raw_mbox_line, BUFFER_SIZE, mail_file) ||
340          (is_a_leading_from_line(last_mbox_line_was_empty, raw_mbox_line)))
341         break;
342     }
343   }
344
345   fclose(mail_file);
346 }
347
348 void write_mail(const char *mail_filename, unsigned long int position_in_mail,
349                 FILE *output_file) {
350   char raw_mbox_line[BUFFER_SIZE];
351   int last_mbox_line_was_empty;
352   FILE *mail_file;
353
354   mail_file = fopen(mail_filename, "r");
355
356   if(!mail_file) {
357     fprintf(stderr,
358             "mymail: Cannot open mbox '%s' for mail extraction.\n",
359             mail_filename);
360     exit(EXIT_FAILURE);
361   }
362
363   fseek(mail_file, position_in_mail, SEEK_SET);
364
365   if(fgets(raw_mbox_line, BUFFER_SIZE, mail_file)) {
366     last_mbox_line_was_empty = 0;
367     fprintf(output_file, "%s", raw_mbox_line);
368     while(1) {
369       if(!fgets(raw_mbox_line, BUFFER_SIZE, mail_file) ||
370          (is_a_leading_from_line(last_mbox_line_was_empty, raw_mbox_line))
371          )
372         break;
373       last_mbox_line_was_empty = (raw_mbox_line[0] == '\n');
374       fprintf(output_file, "%s", raw_mbox_line);
375     }
376   }
377
378   fclose(mail_file);
379 }
380
381 void search_in_db(const char *db_filename,
382                   int nb_search_conditions,
383                   struct search_condition *search_conditions,
384                   FILE *output_file) {
385
386   int hits[MAX_NB_SEARCH_CONDITIONS];
387   char raw_db_line[BUFFER_SIZE];
388   char current_mail_filename[PATH_MAX + 1];
389   unsigned long int current_position_in_mail;
390   char mbox_name[TOKEN_BUFFER_SIZE], *mbox_value;
391   int mbox_id;
392   int already_written, m, n;
393   int nb_body_conditions, nb_fulfilled_body_conditions;
394   FILE *db_file;
395
396   if(!quiet) {
397     printf("Searching in '%s' ... ", db_filename);
398     fflush(stdout);
399   }
400
401   db_file = fopen(db_filename, "r");
402
403   if(!db_file) {
404     fprintf(stderr,
405             "mymail: Cannot open \"%s\" for reading: %s\n",
406             db_filename,
407             strerror(errno));
408     exit(EXIT_FAILURE);
409   }
410
411   /* First, check the db file leading line integrity */
412
413   if(fgets(raw_db_line, BUFFER_SIZE, db_file)) {
414     if(strncmp(raw_db_line, MYMAIL_DB_MAGIC_TOKEN, strlen(MYMAIL_DB_MAGIC_TOKEN))) {
415       fprintf(stderr,
416               "mymail: Header line in '%s' does not match the mymail db format.\n",
417               db_filename);
418       exit(EXIT_FAILURE);
419     }
420   } else {
421     fprintf(stderr,
422             "mymail: Cannot read the header line in '%s'.\n",
423             db_filename);
424     exit(EXIT_FAILURE);
425   }
426
427   /* Then parse the said db file */
428
429   current_position_in_mail = 0;
430   already_written = 0;
431
432   for(n = 0; n < nb_search_conditions; n++) { hits[n] = 0; }
433
434   nb_body_conditions = 0;
435   for(n = 0; n < nb_search_conditions; n++) {
436     if(search_conditions[n].field_id == ID_BODY) {
437       nb_body_conditions++;
438     }
439   }
440
441   strcpy(current_mail_filename, "");
442
443   while(fgets(raw_db_line, BUFFER_SIZE, db_file)) {
444     mbox_value = parse_token(mbox_name, TOKEN_BUFFER_SIZE, ' ', raw_db_line);
445
446     if(strcmp("mail", mbox_name) == 0) {
447       char position_in_file_string[TOKEN_BUFFER_SIZE];
448
449       if(current_mail_filename[0]) {
450
451         /* We first check all conditions but the body ones */
452
453         for(n = 0; n < nb_search_conditions &&
454               ((search_conditions[n].field_id == ID_BODY) ||
455                xor(hits[n], search_conditions[n].negation)); n++);
456
457         if(n == nb_search_conditions) {
458
459           /* Now check the body ones */
460
461           if(nb_body_conditions > 0) {
462             update_body_hits(current_mail_filename, current_position_in_mail,
463                              nb_search_conditions, search_conditions,
464                              nb_body_conditions,
465                              hits);
466           }
467
468           nb_fulfilled_body_conditions = 0;
469
470           for(n = 0; n < nb_search_conditions; n++) {
471             if(search_conditions[n].field_id == ID_BODY &&
472                xor(hits[n], search_conditions[n].negation)) {
473               nb_fulfilled_body_conditions++;
474             }
475           }
476
477           if(nb_body_conditions == nb_fulfilled_body_conditions) {
478             write_mail(current_mail_filename, current_position_in_mail, output_file);
479           }
480         }
481       }
482
483       for(n = 0; n < nb_search_conditions; n++) { hits[n] = 0; }
484
485       mbox_value = parse_token(position_in_file_string, TOKEN_BUFFER_SIZE, ' ', mbox_value);
486       mbox_value = parse_token(current_mail_filename, TOKEN_BUFFER_SIZE, '\n', mbox_value);
487       current_position_in_mail = atol(position_in_file_string);
488       already_written = 0;
489     }
490
491     else {
492       mbox_id = -1;
493       for(m = 0; (m < MAX_ID) && mbox_id == -1; m++) {
494         if(strncmp(field_names[m], mbox_name, strlen(mbox_name)) == 0) {
495           mbox_id = m;
496         }
497       }
498       for(n = 0; n < nb_search_conditions; n++) {
499         hits[n] |= mbox_line_match_search(&search_conditions[n],
500                                           mbox_id, mbox_value);
501       }
502     }
503   }
504
505   fclose(db_file);
506
507   if(!quiet) {
508     printf("done.\n");
509     fflush(stdout);
510   }
511 }
512
513 void recursive_search_in_db(const char *entry_name, regex_t *db_filename_regexp,
514                             int nb_search_conditions,
515                             struct search_condition *search_conditions,
516                             FILE *output_file) {
517   DIR *dir;
518   struct dirent *dir_e;
519   struct stat sb;
520   char subname[PATH_MAX + 1];
521
522   if(lstat(entry_name, &sb) != 0) {
523     fprintf(stderr,
524             "mymail: Cannot stat \"%s\": %s\n",
525             entry_name,
526             strerror(errno));
527     exit(EXIT_FAILURE);
528   }
529
530   dir = opendir(entry_name);
531
532   if(dir) {
533     while((dir_e = readdir(dir))) {
534       if(!ignore_entry(dir_e->d_name)) {
535         snprintf(subname, PATH_MAX, "%s/%s", entry_name, dir_e->d_name);
536         recursive_search_in_db(subname, db_filename_regexp,
537                                nb_search_conditions, search_conditions,
538                                output_file);
539       }
540     }
541     closedir(dir);
542   }
543
544   else {
545     const char *s = entry_name, *filename = entry_name;
546     while(*s) { if(*s == '/') { filename = s+1; } s++; }
547
548     if(regexec(db_filename_regexp, filename, 0, 0, 0) == 0) {
549       search_in_db(entry_name, nb_search_conditions, search_conditions, output_file);
550     }
551   }
552 }
553
554 /*********************************************************************/
555
556 void index_one_mbox_line(int nb_fields_to_parse, struct parsable_field *fields_to_parse,
557                          char *raw_mbox_line, FILE *db_file) {
558   regmatch_t matches;
559   int f;
560   for(f = 0; f < nb_fields_to_parse; f++) {
561     if(regexec(&fields_to_parse[f].regexp, raw_mbox_line, 1, &matches, 0) == 0) {
562       fprintf(db_file, "%s %s\n",
563               field_names[fields_to_parse[f].id],
564               raw_mbox_line + matches.rm_eo);
565     }
566   }
567 }
568
569 void index_mbox(const char *mbox_filename,
570                 int nb_fields_to_parse, struct parsable_field *fields_to_parse,
571                 FILE *db_file) {
572   char raw_mbox_line[BUFFER_SIZE], full_line[BUFFER_SIZE];
573   char *end_of_full_line;
574   FILE *file;
575   int in_header, new_header, last_mbox_line_was_empty;
576   unsigned long int position_in_file;
577
578   file = fopen(mbox_filename, "r");
579
580   if(!file) {
581     fprintf(stderr, "mymail: Cannot open '%s'.\n", mbox_filename);
582     if(paranoid) { exit(EXIT_FAILURE); }
583     return;
584   }
585
586   in_header = 0;
587   new_header = 0;
588
589   position_in_file = 0;
590   end_of_full_line = 0;
591   full_line[0] = '\0';
592   last_mbox_line_was_empty = 1;
593
594   while(fgets(raw_mbox_line, BUFFER_SIZE, file)) {
595     if(is_a_leading_from_line(last_mbox_line_was_empty, raw_mbox_line)) {
596       if(in_header) {
597         fprintf(stderr,
598                 "Got a ^\"From \" in the header in %s:%lu.\n",
599                 mbox_filename, position_in_file);
600         fprintf(stderr, "%s", raw_mbox_line);
601         if(paranoid) { exit(EXIT_FAILURE); }
602       }
603       in_header = 1;
604       new_header = 1;
605     } else if(raw_mbox_line[0] == '\n') {
606       if(in_header) { in_header = 0; }
607     }
608
609     last_mbox_line_was_empty = (raw_mbox_line[0] == '\n');
610
611     if(in_header) {
612       if(new_header) {
613         fprintf(db_file, "mail %lu %s\n", position_in_file, mbox_filename);
614         new_header = 0;
615       }
616
617       if(raw_mbox_line[0] == ' ' || raw_mbox_line[0] == '\t') {
618         char *start = raw_mbox_line;
619         while(*start == ' ' || *start == '\t') start++;
620         *(end_of_full_line++) = ' ';
621         strcpy(end_of_full_line, start);
622         while(*end_of_full_line && *end_of_full_line != '\n') {
623           end_of_full_line++;
624         }
625         *end_of_full_line = '\0';
626       }
627
628       else {
629         /*
630           if(!((raw_mbox_line[0] >= 'a' && raw_mbox_line[0] <= 'z') ||
631           (raw_mbox_line[0] >= 'A' && raw_mbox_line[0] <= 'Z'))) {
632           fprintf(stderr,
633           "Header line syntax error %s:%lu.\n",
634           mbox_filename, position_in_file);
635           fprintf(stderr, "%s", raw_mbox_line);
636           }
637         */
638
639         if(full_line[0]) {
640           index_one_mbox_line(nb_fields_to_parse, fields_to_parse, full_line, db_file);
641         }
642
643         end_of_full_line = full_line;
644         strcpy(end_of_full_line, raw_mbox_line);
645         while(*end_of_full_line && *end_of_full_line != '\n') {
646           end_of_full_line++;
647         }
648         *end_of_full_line = '\0';
649       }
650
651     }
652
653     position_in_file += strlen(raw_mbox_line);
654   }
655
656   fclose(file);
657 }
658
659 void recursive_index_mbox(FILE *db_file,
660                           const char *entry_name,
661                           int nb_fields_to_parse, struct parsable_field *fields_to_parse) {
662   DIR *dir;
663   struct dirent *dir_e;
664   struct stat sb;
665   char subname[PATH_MAX + 1];
666
667   if(lstat(entry_name, &sb) != 0) {
668     fprintf(stderr,
669             "mymail: Cannot stat \"%s\": %s\n",
670             entry_name,
671             strerror(errno));
672     exit(EXIT_FAILURE);
673   }
674
675   dir = opendir(entry_name);
676
677   if(dir) {
678     while((dir_e = readdir(dir))) {
679       if(!ignore_entry(dir_e->d_name)) {
680         snprintf(subname, PATH_MAX, "%s/%s", entry_name, dir_e->d_name);
681         recursive_index_mbox(db_file, subname, nb_fields_to_parse, fields_to_parse);
682       }
683     }
684     closedir(dir);
685   } else {
686     index_mbox(entry_name, nb_fields_to_parse, fields_to_parse, db_file);
687   }
688 }
689
690 /*********************************************************************/
691
692 /* For long options that have no equivalent short option, use a
693    non-character as a pseudo short option, starting with CHAR_MAX + 1.  */
694 enum {
695   OPT_BASH_MODE = CHAR_MAX + 1
696 };
697
698 static struct option long_options[] = {
699   { "help", no_argument, 0, 'h' },
700   { "version", no_argument, 0, 'v' },
701   { "quiet", no_argument, 0, 'q' },
702   { "db-file", 1, 0, 'd' },
703   { "db-pattern", 1, 0, 'p' },
704   { "db-root", 1, 0, 'r' },
705   { "db-list", 1, 0, 'l' },
706   { "search", 1, 0, 's' },
707   { "index", 0, 0, 'i' },
708   { "output", 1, 0, 'o' },
709   { "default-search", 1, 0, 'a' },
710   { 0, 0, 0, 0 }
711 };
712
713 /*********************************************************************/
714
715 void init_condition(struct search_condition *condition, char *full_string) {
716   char full_search_field[TOKEN_BUFFER_SIZE], *search_field;
717   int m;
718   char *string;
719
720   string = parse_token(full_search_field, TOKEN_BUFFER_SIZE, ' ', full_string);
721   search_field = full_search_field;
722
723   if(search_field[0] == '!') {
724     search_field++;
725     condition->negation = 1;
726   } else {
727     condition->negation = 0;
728   }
729
730   /* Recently */
731
732   if(strcmp(search_field, "8h") == 0) {
733     condition->field_id = ID_INTERVAL;
734     condition->interval_start = time(0) - 3600 * 8;
735     condition->interval_stop = 0;
736   }
737
738   else if(strcmp(search_field, "24h") == 0 ||
739           strcmp(search_field, "today") == 0) {
740     condition->field_id = ID_INTERVAL;
741     condition->interval_start = time(0) - 3600 * 24;
742     condition->interval_stop = 0;
743   }
744
745   else if(strcmp(search_field, "week") == 0) {
746     condition->field_id = ID_INTERVAL;
747     condition->interval_start = time(0) - 3600 * 24 * 7;
748     condition->interval_stop = 0;
749   }
750
751   else if(strcmp(search_field, "month") == 0) {
752     condition->field_id = ID_INTERVAL;
753     condition->interval_start = time(0) - 3600 * 24 * 31;
754     condition->interval_stop = 0;
755   }
756
757   else if(strcmp(search_field, "year") == 0) {
758     condition->field_id = ID_INTERVAL;
759     condition->interval_start = time(0) - 3600 * 24 * 365;
760     condition->interval_stop = 0;
761   }
762
763   /* Yesterday */
764
765   else if(strcmp(search_field, "yesterday") == 0) {
766     condition->field_id = ID_INTERVAL;
767     condition->interval_start = time(0) - 2 * 3600 * 24;
768     condition->interval_stop = condition->interval_start + 3600 * 24;
769   }
770
771   /* Week days */
772
773   else if(strcmp(search_field, "monday") == 0) {
774     condition->field_id = ID_INTERVAL;
775     condition->interval_start = time_for_past_day(1);
776     condition->interval_stop = condition->interval_start + 3600 * 24;
777   }
778
779   else if(strcmp(search_field, "tuesday") == 0) {
780     condition->field_id = ID_INTERVAL;
781     condition->interval_start = time_for_past_day(2);
782     condition->interval_stop = condition->interval_start + 3600 * 24;
783   }
784
785   else if(strcmp(search_field, "wednesday") == 0) {
786     condition->field_id = ID_INTERVAL;
787     condition->interval_start = time_for_past_day(3);
788     condition->interval_stop = condition->interval_start + 3600 * 24;
789   }
790
791   else if(strcmp(search_field, "thursday") == 0) {
792     condition->field_id = ID_INTERVAL;
793     condition->interval_start = time_for_past_day(4);
794     condition->interval_stop = condition->interval_start + 3600 * 24;
795   }
796
797   else if(strcmp(search_field, "friday") == 0) {
798     condition->field_id = ID_INTERVAL;
799     condition->interval_start = time_for_past_day(5);
800     condition->interval_stop = condition->interval_start + 3600 * 24;
801   }
802
803   else if(strcmp(search_field, "saturday") == 0) {
804     condition->field_id = ID_INTERVAL;
805     condition->interval_start = time_for_past_day(6);
806     condition->interval_stop = condition->interval_start + 3600 * 24;
807   }
808
809   else if(strcmp(search_field, "sunday") == 0) {
810     condition->field_id = ID_INTERVAL;
811     condition->interval_start = time_for_past_day(7);
812     condition->interval_stop = condition->interval_start + 3600 * 24;
813   }
814
815   else {
816
817     /* header-related conditions */
818
819     condition->field_id = -1;
820
821     for(m = 0; (m < MAX_ID) && condition->field_id == -1; m++) {
822       if(strncmp(field_names[m], search_field, strlen(search_field)) == 0) {
823         condition->field_id = m;
824       }
825     }
826
827     if(condition->field_id == -1) {
828       if(default_search_field) {
829         for(m = 0; (m < MAX_ID) && condition->field_id == -1; m++) {
830           if(strncmp(field_names[m],
831                      default_search_field, strlen(default_search_field)) == 0) {
832             condition->field_id = m;
833           }
834         }
835         string = full_string;
836       }
837     }
838
839     if(condition->field_id == -1) {
840       fprintf(stderr,
841               "mymail: Syntax error in field name \"%s\".\n",
842               search_field);
843       exit(EXIT_FAILURE);
844     }
845
846     if(regcomp(&condition->regexp,
847                string,
848                REG_ICASE)) {
849       fprintf(stderr,
850               "mymail: Syntax error in regexp \"%s\" for field \"%s\".\n",
851               string,
852               field_names[condition->field_id]);
853       exit(EXIT_FAILURE);
854     }
855   }
856 }
857
858 void free_condition(struct search_condition *condition) {
859   if(condition->field_id != ID_INTERVAL) {
860     regfree(&condition->regexp);
861   }
862 }
863
864 /*********************************************************************/
865 /*********************************************************************/
866 /*********************************************************************/
867
868 int main(int argc, char **argv) {
869   char *db_filename;
870   char *db_filename_regexp_string;
871   char *db_root_path;
872   char *db_filename_list;
873   char output_filename[PATH_MAX + 1];
874   int action_index;
875   int error = 0, show_help = 0;
876   const int nb_fields_to_parse = sizeof(fields_to_parse) / sizeof(struct parsable_field);
877   char c;
878   int f, n;
879   int nb_search_conditions;
880   FILE *output_file;
881   struct search_condition search_conditions[MAX_NB_SEARCH_CONDITIONS];
882
883   if(regcomp(&leading_from_line_regexp,
884              "^From [^ ]*  \\(Mon\\|Tue\\|Wed\\|Thu\\|Fri\\|Sat\\|Sun\\) \\(Jan\\|Feb\\|Mar\\|Apr\\|May\\|Jun\\|Jul\\|Aug\\|Sep\\|Oct\\|Nov\\|Dec\\) [ 123][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9] [0-9][0-9][0-9][0-9]\n$",
885              0)) {
886     fprintf(stderr,
887             "mymail: Cannot compile leading \"from\" line regexp. That is strange.\n");
888     exit(EXIT_FAILURE);
889   }
890
891   paranoid = 0;
892   action_index = 0;
893   db_filename = 0;
894   db_filename_regexp_string = 0;
895   db_root_path = 0;
896   db_filename_list = 0;
897   quiet = 0;
898   default_search_field = 0;
899
900   setlocale(LC_ALL, "");
901
902   nb_search_conditions = 0;
903
904   while ((c = getopt_long(argc, argv, "hvqip:s:d:r:l:o:a:",
905                           long_options, NULL)) != -1) {
906
907     switch(c) {
908
909     case 'h':
910       show_help = 1;
911       break;
912
913     case 'v':
914       print_version(stdout);
915       break;
916
917     case 'q':
918       quiet = 1;
919       break;
920
921     case 'i':
922       action_index = 1;
923       break;
924
925     case 'd':
926       db_filename = strdup(optarg);
927       break;
928
929     case 'p':
930       db_filename_regexp_string = strdup(optarg);
931       break;
932
933     case 'o':
934       strncpy(output_filename, optarg, PATH_MAX);
935       break;
936
937     case 'r':
938       db_root_path = strdup(optarg);
939       break;
940
941     case 'l':
942       db_filename_list = strdup(optarg);
943       break;
944
945     case 's':
946       if(nb_search_conditions == MAX_NB_SEARCH_CONDITIONS) {
947         fprintf(stderr, "mymail: Too many search patterns.\n");
948         exit(EXIT_FAILURE);
949       }
950       init_condition(&search_conditions[nb_search_conditions], optarg);
951       nb_search_conditions++;
952       break;
953
954     case 'a':
955       default_search_field = optarg;
956       break;
957
958     default:
959       error = 1;
960       break;
961     }
962   }
963
964   if(!db_filename) {
965     char *default_db_filename = getenv("MYMAIL_DB_FILE");
966
967     if(!default_db_filename) {
968       default_db_filename = "mymail.db";
969     }
970
971     db_filename = strdup(default_db_filename);
972   }
973
974   if(!db_filename_regexp_string) {
975     char *default_db_filename_regexp_string = getenv("MYMAIL_DB_PATTERN");
976
977     if(!default_db_filename_regexp_string) {
978       default_db_filename_regexp_string = "^mymail.db$";
979     }
980
981     db_filename_regexp_string = strdup(default_db_filename_regexp_string);
982   }
983
984   if(!db_root_path) {
985     char *default_db_root_path = getenv("MYMAIL_DB_ROOT");
986
987     if(default_db_root_path) {
988       db_root_path = strdup(default_db_root_path);
989     }
990   }
991
992   if(!db_filename_list) {
993     char *default_db_filename_list = getenv("MYMAIL_DB_LIST");
994
995     if(default_db_filename_list) {
996       db_filename_list = strdup(default_db_filename_list);
997     }
998   }
999
1000   if(output_filename[0]) {
1001     output_file = fopen(output_filename, "w");
1002
1003     if(!output_file) {
1004       fprintf(stderr,
1005               "mymail: Cannot open result file \"%s\" for writing: %s\n",
1006               output_filename,
1007               strerror(errno));
1008       exit(EXIT_FAILURE);
1009     }
1010   } else {
1011     output_file = stdout;
1012     quiet = 1;
1013   }
1014
1015   if(error) {
1016     print_usage(stderr);
1017     exit(EXIT_FAILURE);
1018   }
1019
1020   if(show_help) {
1021     print_usage(stdout);
1022     exit(EXIT_SUCCESS);
1023   }
1024
1025   if(action_index) {
1026     FILE *db_file;
1027
1028     db_file = fopen(db_filename, "w");
1029
1030     if(!db_file) {
1031       fprintf(stderr,
1032               "mymail: Cannot open \"%s\" for writing: %s\n",
1033               db_filename,
1034               strerror(errno));
1035       exit(EXIT_FAILURE);
1036     }
1037
1038     for(f = 0; f < nb_fields_to_parse; f++) {
1039       if(regcomp(&fields_to_parse[f].regexp,
1040                  fields_to_parse[f].regexp_string,
1041                  fields_to_parse[f].cflags)) {
1042         fprintf(stderr,
1043                 "mymail: Syntax error in regexp \"%s\" for field \"%s\".\n",
1044                 fields_to_parse[f].regexp_string,
1045                 field_names[fields_to_parse[f].id]);
1046         exit(EXIT_FAILURE);
1047       }
1048     }
1049
1050     fprintf(db_file, "%s version_%s raw\n", MYMAIL_DB_MAGIC_TOKEN, VERSION);
1051
1052     while(optind < argc) {
1053       recursive_index_mbox(db_file,
1054                            argv[optind],
1055                            nb_fields_to_parse, fields_to_parse);
1056       optind++;
1057     }
1058
1059     fflush(db_file);
1060     fclose(db_file);
1061
1062     for(f = 0; f < nb_fields_to_parse; f++) {
1063       regfree(&fields_to_parse[f].regexp);
1064     }
1065   }
1066
1067   else {
1068
1069     if(nb_search_conditions > 0) {
1070
1071       /* Recursive search if db_root_path is set */
1072
1073       if(db_root_path) {
1074         regex_t db_filename_regexp;
1075         if(regcomp(&db_filename_regexp,
1076                    db_filename_regexp_string,
1077                    0)) {
1078           fprintf(stderr,
1079                   "mymail: Syntax error in regexp \"%s\".\n",
1080                   db_filename_regexp_string);
1081           exit(EXIT_FAILURE);
1082         }
1083
1084         recursive_search_in_db(db_root_path, &db_filename_regexp,
1085                                nb_search_conditions, search_conditions,
1086                                output_file);
1087
1088         regfree(&db_filename_regexp);
1089       }
1090
1091       /* Search in all db files listed in db_filename_list */
1092
1093       if(db_filename_list) {
1094         char db_filename[PATH_MAX + 1];
1095         char *s;
1096
1097         s = db_filename_list;
1098
1099         while(*s) {
1100           s = parse_token(db_filename, PATH_MAX + 1, ';', s);
1101
1102           if(db_filename[0]) {
1103             search_in_db(db_filename, nb_search_conditions, search_conditions, output_file);
1104           }
1105         }
1106       }
1107
1108       /* Search in all db files listed in the command arguments */
1109
1110       while(optind < argc) {
1111         search_in_db(argv[optind], nb_search_conditions, search_conditions, output_file);
1112         optind++;
1113       }
1114     }
1115   }
1116
1117   for(n = 0; n < nb_search_conditions; n++) {
1118     free_condition(&search_conditions[n]);
1119   }
1120
1121   if(output_file != stdout) {
1122     fflush(output_file);
1123     fclose(output_file);
1124   }
1125
1126   free(db_filename);
1127   free(db_filename_regexp_string);
1128   free(db_root_path);
1129   free(db_filename_list);
1130
1131   regfree(&leading_from_line_regexp);
1132
1133   exit(EXIT_SUCCESS);
1134 }