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