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