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