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