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