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