Now handle negative search constraints with a leading '!'.
[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
47 #define MYMAIL_DB_MAGIC_TOKEN "mymail_index_file"
48 #define VERSION "0.1"
49
50 #define MAX_NB_SEARCH_PATTERNS 10
51
52 #define BUFFER_SIZE 65536
53
54 enum {
55   ID_MAIL,
56   ID_FROM,
57   ID_DEST,
58   ID_SUBJECT,
59   ID_PARTICIPANT,
60   MAX_ID
61 };
62
63 static char *field_names[] = {
64   "mail",
65   "from",
66   "dest",
67   "subject",
68   "part"
69 };
70
71 struct search_request {
72   int field_id;
73   int negation;
74   regex_t regexp;
75 };
76
77 struct parsable_field {
78   int id;
79   char *regexp_string;
80   regex_t regexp;
81 };
82
83 char *db_filename;
84 char *db_root_path;
85
86 int paranoid;
87 int action_index;
88
89 char *segment_next_field(char *current) {
90   while(*current && *current != ' ') current++;
91   *current = '\0'; current++;
92   while(*current && *current == ' ') current++;
93   return current;
94 }
95
96 void remove_eof(char *c) {
97   while(*c && *c != '\n' && *c != '\r') c++;
98   *c = '\0';
99 }
100
101 /********************************************************************/
102
103 /* malloc with error checking.  */
104
105 void *safe_malloc(size_t n) {
106   void *p = malloc(n);
107   if(!p && n != 0) {
108     fprintf(stderr,
109             "mymail: cannot allocate memory: %s\n", strerror(errno));
110     exit(EXIT_FAILURE);
111   }
112   return p;
113 }
114
115 /*********************************************************************/
116
117 void print_version(FILE *out) {
118   fprintf(out, "mymail version %s (%s)\n", VERSION, UNAME);
119 }
120
121 void print_usage(FILE *out) {
122   print_version(out);
123   fprintf(out, "Written by Francois Fleuret <francois@fleuret.org>.\n");
124   fprintf(out, "\n");
125   fprintf(out, "Usage: mymail [options] [<mbox dir1> [<mbox dir2> ...]]\n");
126   fprintf(out, "\n");
127   fprintf(out, " -h, --help\n");
128   fprintf(out, "         show this help\n");
129   fprintf(out, " -v, --version\n");
130   fprintf(out, "         print the version number\n");
131   fprintf(out, " -i, --index\n");
132   fprintf(out, "         index mails\n");
133   fprintf(out, " -s <search pattern>, --search <search pattern>\n");
134   fprintf(out, "         search for matching mails in the data-base file\n");
135   fprintf(out, " -d <db filename>, --db-file <db filename>\n");
136   fprintf(out, "         set the data-base file\n");
137   fprintf(out, " -r <db root path>, --db-root <db root path>\n");
138   fprintf(out, "         set the data-base root path for recursive search\n");
139 }
140
141 /*********************************************************************/
142
143 int ignore_entry(const char *name) {
144   return
145     /* strcmp(name, ".") == 0 || */
146     /* strcmp(name, "..") == 0 || */
147     (name[0] == '.' && name[1] != '/');
148 }
149
150 int mbox_line_match_search(struct search_request *request,
151                            int mbox_id, char *mbox_value) {
152   return
153     (request->field_id == mbox_id ||
154      (request->field_id == ID_PARTICIPANT && (mbox_id == ID_FROM || mbox_id == ID_DEST)))
155     &&
156     regexec(&request->regexp, mbox_value, 0, 0, 0) == 0;
157 }
158
159 void search_in_db(int nb_search_patterns,
160                   struct search_request *search_requests,
161                   FILE *db_file) {
162   int hits[MAX_NB_SEARCH_PATTERNS];
163   char raw_db_line[BUFFER_SIZE];
164   char raw_mbox_line[BUFFER_SIZE];
165   char current_mail_filename[PATH_MAX + 1];
166   unsigned long int current_position_in_mail;
167   char *mbox_name, *mbox_value;
168   int mbox_id;
169   int already_written, m, n;
170
171   current_position_in_mail = 0;
172   already_written = 0;
173
174   for(n = 0; n < nb_search_patterns; n++) { hits[n] = 0; }
175
176   while(fgets(raw_db_line, BUFFER_SIZE, db_file)) {
177     mbox_name = raw_db_line;
178     mbox_value = segment_next_field(raw_db_line);
179
180     if(strcmp("mail", mbox_name) == 0) {
181       char *position_in_file_string;
182       char *mail_filename;
183
184       for(n = 0; n < nb_search_patterns &&
185             ((hits[n] && !search_requests[n].negation) ||
186              (!hits[n] && search_requests[n].negation)); n++);
187
188       if(n == nb_search_patterns) {
189         FILE *mail_file;
190
191         mail_file = fopen(current_mail_filename, "r");
192
193         if(!mail_file) {
194           fprintf(stderr, "mymail: Cannot open mbox '%s'.\n", current_mail_filename);
195           exit(EXIT_FAILURE);
196         }
197
198         fseek(mail_file, current_position_in_mail, SEEK_SET);
199
200         if(fgets(raw_mbox_line, BUFFER_SIZE, mail_file)) {
201           printf("%s", raw_mbox_line);
202           while(fgets(raw_mbox_line, BUFFER_SIZE, mail_file) &&
203                 strncmp(raw_mbox_line, "From ", 5)) {
204             printf("%s", raw_mbox_line);
205           }
206         }
207
208         fclose(mail_file);
209       }
210
211       for(n = 0; n < nb_search_patterns; n++) { hits[n] = 0; }
212
213       position_in_file_string = mbox_value;
214       mail_filename = segment_next_field(mbox_value);
215       current_position_in_mail = atol(position_in_file_string);
216       strcpy(current_mail_filename, mail_filename);
217
218       remove_eof(current_mail_filename);
219       already_written = 0;
220     }
221
222     else {
223       mbox_id = -1;
224       for(m = 0; (m < MAX_ID) && mbox_id == -1; m++) {
225         if(strncmp(field_names[m], mbox_name, strlen(mbox_name)) == 0) {
226           mbox_id = m;
227         }
228       }
229       for(n = 0; n < nb_search_patterns; n++) {
230         hits[n] |= mbox_line_match_search(&search_requests[n],
231                                           mbox_id, mbox_value);
232       }
233     }
234   }
235 }
236
237 void recursive_search_in_db(const char *entry_name,
238                             int nb_search_patterns,
239                             struct search_request *search_requests) {
240   DIR *dir;
241   struct dirent *dir_e;
242   struct stat sb;
243   char raw_db_line[BUFFER_SIZE];
244   char subname[PATH_MAX + 1];
245
246   if(lstat(entry_name, &sb) != 0) {
247     fprintf(stderr,
248             "mymail: Cannot stat \"%s\": %s\n",
249             entry_name,
250             strerror(errno));
251     exit(EXIT_FAILURE);
252   }
253
254   dir = opendir(entry_name);
255
256   if(dir) {
257     while((dir_e = readdir(dir))) {
258       if(!ignore_entry(dir_e->d_name)) {
259         snprintf(subname, PATH_MAX, "%s/%s", entry_name, dir_e->d_name);
260         recursive_search_in_db(subname,
261                                nb_search_patterns,
262                                search_requests);
263       }
264     }
265     closedir(dir);
266   } else {
267     const char *s = entry_name, *filename = entry_name;
268     while(*s) { if(*s == '/') { filename = s+1; } s++; }
269
270     if(strcmp(filename, db_filename) == 0) {
271       FILE *db_file = fopen(entry_name, "r");
272
273       if(!db_file) {
274         fprintf(stderr,
275                 "mymail: Cannot open \"%s\" for reading: %s\n",
276                 db_filename,
277                 strerror(errno));
278         exit(EXIT_FAILURE);
279       }
280
281       if(fgets(raw_db_line, BUFFER_SIZE, db_file)) {
282         if(strncmp(raw_db_line, MYMAIL_DB_MAGIC_TOKEN, strlen(MYMAIL_DB_MAGIC_TOKEN))) {
283           fprintf(stderr,
284                   "mymail: Header line in '%s' does not match the mymail db format.\n",
285                   entry_name);
286           exit(EXIT_FAILURE);
287         }
288       } else {
289         fprintf(stderr,
290                 "mymail: Cannot read the header line in '%s'.\n",
291                 entry_name);
292         exit(EXIT_FAILURE);
293       }
294
295       search_in_db(nb_search_patterns, search_requests, db_file);
296
297       fclose(db_file);
298     }
299   }
300 }
301
302 /*********************************************************************/
303
304 void index_one_mbox_line(int nb_fields_to_parse, struct parsable_field *fields_to_parse,
305                          char *raw_mbox_line, FILE *db_file) {
306   regmatch_t matches;
307   int f;
308   for(f = 0; f < nb_fields_to_parse; f++) {
309     if(regexec(&fields_to_parse[f].regexp, raw_mbox_line, 1, &matches, 0) == 0) {
310       fprintf(db_file, "%s %s\n",
311               field_names[fields_to_parse[f].id],
312               raw_mbox_line + matches.rm_eo);
313     }
314   }
315 }
316
317 void index_mbox(const char *mbox_filename,
318                 int nb_fields_to_parse, struct parsable_field *fields_to_parse,
319                 FILE *db_file) {
320   char raw_mbox_line[BUFFER_SIZE], full_line[BUFFER_SIZE];
321   char *end_of_full_line;
322   FILE *file;
323   int in_header, new_header;
324   unsigned long int position_in_file;
325
326   file = fopen(mbox_filename, "r");
327
328   if(!file) {
329     fprintf(stderr, "mymail: Cannot open '%s'.\n", mbox_filename);
330     if(paranoid) { exit(EXIT_FAILURE); }
331     return;
332   }
333
334   in_header = 0;
335   new_header = 0;
336
337   position_in_file = 0;
338   end_of_full_line = 0;
339   full_line[0] = '\0';
340
341   while(fgets(raw_mbox_line, BUFFER_SIZE, file)) {
342     if(strncmp(raw_mbox_line, "From ", 5) == 0) {
343       if(in_header) {
344         fprintf(stderr,
345                 "Got a ^\"From \" in the header in %s:%lu.\n",
346                 mbox_filename, position_in_file);
347         fprintf(stderr, "%s", raw_mbox_line);
348         if(paranoid) { exit(EXIT_FAILURE); }
349       }
350       in_header = 1;
351       new_header = 1;
352     } else if(strncmp(raw_mbox_line, "\n", 1) == 0) {
353       if(in_header) { in_header = 0; }
354     }
355
356     if(in_header) {
357       if(new_header) {
358         fprintf(db_file, "mail %lu %s\n", position_in_file, mbox_filename);
359         new_header = 0;
360       }
361
362       if(raw_mbox_line[0] == ' ' || raw_mbox_line[0] == '\t') {
363         char *start = raw_mbox_line;
364         while(*start == ' ' || *start == '\t') start++;
365         *(end_of_full_line++) = ' ';
366         strcpy(end_of_full_line, start);
367         while(*end_of_full_line && *end_of_full_line != '\n') {
368           end_of_full_line++;
369         }
370         *end_of_full_line = '\0';
371       }
372
373       else {
374         /*
375           if(!((raw_mbox_line[0] >= 'a' && raw_mbox_line[0] <= 'z') ||
376           (raw_mbox_line[0] >= 'A' && raw_mbox_line[0] <= 'Z'))) {
377           fprintf(stderr,
378           "Header line syntax error %s:%lu.\n",
379           mbox_filename, position_in_file);
380           fprintf(stderr, "%s", raw_mbox_line);
381           }
382         */
383
384         if(full_line[0]) {
385           index_one_mbox_line(nb_fields_to_parse, fields_to_parse, full_line, db_file);
386         }
387
388         end_of_full_line = full_line;
389         strcpy(end_of_full_line, raw_mbox_line);
390         while(*end_of_full_line && *end_of_full_line != '\n') {
391           end_of_full_line++;
392         }
393         *end_of_full_line = '\0';
394       }
395
396     }
397
398     position_in_file += strlen(raw_mbox_line);
399   }
400
401   fclose(file);
402 }
403
404 void recursive_index_mbox(FILE *db_file,
405                           const char *entry_name,
406                           int nb_fields_to_parse, struct parsable_field *fields_to_parse) {
407   DIR *dir;
408   struct dirent *dir_e;
409   struct stat sb;
410   char subname[PATH_MAX + 1];
411
412   if(lstat(entry_name, &sb) != 0) {
413     fprintf(stderr,
414             "mymail: Cannot stat \"%s\": %s\n",
415             entry_name,
416             strerror(errno));
417     exit(EXIT_FAILURE);
418   }
419
420   dir = opendir(entry_name);
421
422   if(dir) {
423     while((dir_e = readdir(dir))) {
424       if(!ignore_entry(dir_e->d_name)) {
425         snprintf(subname, PATH_MAX, "%s/%s", entry_name, dir_e->d_name);
426         recursive_index_mbox(db_file, subname, nb_fields_to_parse, fields_to_parse);
427       }
428     }
429     closedir(dir);
430   } else {
431     index_mbox(entry_name, nb_fields_to_parse, fields_to_parse, db_file);
432   }
433 }
434
435 /*********************************************************************/
436
437 /* For long options that have no equivalent short option, use a
438    non-character as a pseudo short option, starting with CHAR_MAX + 1.  */
439 enum {
440   OPT_BASH_MODE = CHAR_MAX + 1
441 };
442
443 static struct option long_options[] = {
444   { "help", no_argument, 0, 'h' },
445   { "version", no_argument, 0, 'v' },
446   { "db-file", 1, 0, 'd' },
447   { "db-root", 1, 0, 'r' },
448   { "search", 1, 0, 's' },
449   { "index", 0, 0, 'i' },
450   { 0, 0, 0, 0 }
451 };
452
453 static struct parsable_field fields_to_parse[] = {
454   {
455     ID_FROM,
456     "^\\([Ff][Rr][Oo][Mm]:\\|From\\) *",
457     { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
458   },
459
460   {
461     ID_DEST,
462     "^\\([Tt][Oo]\\|[Cc][Cc]\\|[Bb][Cc][Cc]\\): *",
463     { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
464   },
465
466   {
467     ID_SUBJECT,
468     "^[Ss][Uu][Bb][Jj][Ee][Cc][Tt]: *",
469     { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
470   },
471
472 };
473
474 /*********************************************************************/
475
476 int main(int argc, char **argv) {
477   int error = 0, show_help = 0;
478   const int nb_fields_to_parse = sizeof(fields_to_parse) / sizeof(struct parsable_field);
479   char c;
480   int f;
481   int nb_search_patterns;
482   char *search_pattern[MAX_NB_SEARCH_PATTERNS];
483
484   /* for(f = 0; f < argc; f++) { */
485   /* printf("arg %d \"%s\"\n", f, argv[f]); */
486   /* } */
487
488   paranoid = 0;
489   action_index = 0;
490   db_filename = 0;
491   db_root_path = 0;
492
493   setlocale(LC_ALL, "");
494
495   nb_search_patterns = 0;
496
497   while ((c = getopt_long(argc, argv, "hvip:s:d:r:",
498                           long_options, NULL)) != -1) {
499
500     switch(c) {
501
502     case 'h':
503       show_help = 1;
504       break;
505
506     case 'v':
507       print_version(stdout);
508       break;
509
510     case 'i':
511       action_index = 1;
512       break;
513
514     case 'd':
515       db_filename = strdup(optarg);
516       break;
517
518     case 'r':
519       db_root_path = strdup(optarg);
520       break;
521
522     case 's':
523       if(nb_search_patterns == MAX_NB_SEARCH_PATTERNS) {
524         fprintf(stderr, "mymail: Too many search patterns.\n");
525         exit(EXIT_FAILURE);
526       }
527       search_pattern[nb_search_patterns++] = strdup(optarg);
528       break;
529
530     default:
531       error = 1;
532       break;
533     }
534   }
535
536   if(!db_filename) {
537     char *default_db_filename = getenv("MYMAIL_DB_FILE");
538
539     if(!default_db_filename) {
540       default_db_filename = "mymail.db";
541     }
542
543     db_filename = strdup(default_db_filename);
544   }
545
546   if(!db_root_path) {
547     char *default_db_root_path = getenv("MYMAIL_DB_ROOT");
548
549     if(default_db_root_path) {
550       db_root_path = strdup(default_db_root_path);
551     }
552   }
553
554   if(!db_root_path) {
555     fprintf(stderr,
556             "mymail: db root path is not set\n");
557     exit(EXIT_FAILURE);
558   }
559
560
561   if(error) {
562     print_usage(stderr);
563     exit(EXIT_FAILURE);
564   }
565
566   if(show_help) {
567     print_usage(stdout);
568     exit(EXIT_SUCCESS);
569   }
570
571   if(action_index) {
572     FILE *db_file;
573
574     db_file = fopen(db_filename, "w");
575
576     if(!db_file) {
577       fprintf(stderr,
578               "mymail: Cannot open \"%s\" for writing: %s\n",
579               db_filename,
580               strerror(errno));
581       exit(EXIT_FAILURE);
582     }
583
584     for(f = 0; f < nb_fields_to_parse; f++) {
585       if(regcomp(&fields_to_parse[f].regexp,
586                  fields_to_parse[f].regexp_string,
587                  REG_ICASE)) {
588         fprintf(stderr,
589                 "mymail: Syntax error in regexp \"%s\" for field \"%s\".\n",
590                 fields_to_parse[f].regexp_string,
591                 field_names[fields_to_parse[f].id]);
592         exit(EXIT_FAILURE);
593       }
594     }
595
596     fprintf(db_file, "%s version_%s raw version\n", MYMAIL_DB_MAGIC_TOKEN, VERSION);
597
598     while(optind < argc) {
599       recursive_index_mbox(db_file,
600                            argv[optind],
601                            nb_fields_to_parse, fields_to_parse);
602       optind++;
603     }
604
605     fclose(db_file);
606
607     for(f = 0; f < nb_fields_to_parse; f++) {
608       regfree(&fields_to_parse[f].regexp);
609     }
610   }
611
612   else {
613
614     if(nb_search_patterns > 0) {
615       struct search_request search_requests[MAX_NB_SEARCH_PATTERNS];
616       char *search_regexp_string;
617       int m, n;
618
619       for(n = 0; n < nb_search_patterns; n++) {
620         search_regexp_string = segment_next_field(search_pattern[n]);
621
622         if(search_pattern[n][0] == '!') {
623           search_pattern[n]++;
624           search_requests[n].negation = 1;
625         } else {
626           search_requests[n].negation = 0;
627         }
628
629         search_requests[n].field_id = -1;
630         for(m = 0; (m < MAX_ID) && search_requests[n].field_id == -1; m++) {
631           if(strncmp(field_names[m], search_pattern[n], strlen(search_pattern[n])) == 0) {
632             search_requests[n].field_id = m;
633           }
634         }
635
636         if(regcomp(&search_requests[n].regexp,
637                    search_regexp_string,
638                    REG_ICASE)) {
639           fprintf(stderr,
640                   "mymail: Syntax error in regexp \"%s\" for field \"%s\".\n",
641                   search_regexp_string,
642                   field_names[search_requests[n].field_id]);
643           exit(EXIT_FAILURE);
644         }
645       }
646
647       recursive_search_in_db(db_root_path,
648                              nb_search_patterns, search_requests);
649
650       for(n = 0; n < nb_search_patterns; n++) {
651         free(search_pattern[n]);
652       }
653     }
654   }
655
656   free(db_filename);
657   free(db_root_path);
658
659   exit(EXIT_SUCCESS);
660 }