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