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