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