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