Cosmetics.
[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 paranoid;
64 int action_index;
65
66 char *segment_next_field(char *current) {
67   while(*current && *current != ' ') current++;
68   *current = '\0'; current++;
69   while(*current && *current == ' ') current++;
70   return current;
71 }
72
73 void remove_eof(char *c) {
74   while(*c && *c != '\n' && *c != '\r') c++;
75   *c = '\0';
76 }
77
78 /********************************************************************/
79
80 /* malloc with error checking.  */
81
82 void *safe_malloc(size_t n) {
83   void *p = malloc(n);
84   if(!p && n != 0) {
85     fprintf(stderr,
86             "mymail: cannot allocate memory: %s\n", strerror(errno));
87     exit(EXIT_FAILURE);
88   }
89   return p;
90 }
91
92 /*********************************************************************/
93
94 void print_version(FILE *out) {
95   fprintf(out, "mymail version %s (%s)\n", VERSION, UNAME);
96 }
97
98 void print_usage(FILE *out) {
99   print_version(out);
100   fprintf(out, "Written by Francois Fleuret <francois@fleuret.org>.\n");
101   fprintf(out, "\n");
102   fprintf(out, "Usage: mymail [options] [<mbox dir1> [<mbox dir2> ...]]\n");
103   fprintf(out, "\n");
104   fprintf(out, " -h, --help\n");
105   fprintf(out, "         show this help\n");
106   fprintf(out, " -v, --version\n");
107   fprintf(out, "         print the version number\n");
108   fprintf(out, " -i, --index\n");
109   fprintf(out, "         index mails\n");
110   fprintf(out, " -s <search pattern>, --search <search pattern>\n");
111   fprintf(out, "         search for matching mails in the data-base file\n");
112   fprintf(out, " -d <db filename>, --db-file <db filename>\n");
113   fprintf(out, "         set the data-base file\n");
114   fprintf(out, " -r <db root path>, --db-root <db root path>\n");
115   fprintf(out, "         set the data-base root path for recursive search\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           (strncmp(search_name[n], name, strlen(search_name[n])) == 0 &&
199            regexec(&regexp[n], value, 0, 0, 0) == 0);
200       }
201     }
202   }
203
204   for(n = 0; n < nb_search_patterns; n++) {
205     regfree(&regexp[n]);
206   }
207 }
208
209 void recursive_search_in_db(const char *entry_name,
210                             int nb_search_patterns,
211                             char **search_name, char **search_regexp_string) {
212   DIR *dir;
213   struct dirent *dir_e;
214   struct stat sb;
215   char raw_db_line[BUFFER_SIZE];
216   char subname[PATH_MAX + 1];
217
218   if(lstat(entry_name, &sb) != 0) {
219     fprintf(stderr,
220             "mymail: Cannot stat \"%s\": %s\n",
221             entry_name,
222             strerror(errno));
223     exit(EXIT_FAILURE);
224   }
225
226   dir = opendir(entry_name);
227
228   if(dir) {
229     while((dir_e = readdir(dir))) {
230       if(!ignore_entry(dir_e->d_name)) {
231         snprintf(subname, PATH_MAX, "%s/%s", entry_name, dir_e->d_name);
232         recursive_search_in_db(subname,
233                                nb_search_patterns,
234                                search_name, search_regexp_string);
235       }
236     }
237     closedir(dir);
238   } else {
239     const char *s = entry_name, *filename = entry_name;
240     while(*s) { if(*s == '/') { filename = s+1; } s++; }
241
242     if(strcmp(filename, db_filename) == 0) {
243       FILE *db_file = fopen(entry_name, "r");
244
245       if(!db_file) {
246         fprintf(stderr,
247                 "mymail: Cannot open \"%s\" for reading: %s\n",
248                 db_filename,
249                 strerror(errno));
250         exit(EXIT_FAILURE);
251       }
252
253       if(fgets(raw_db_line, BUFFER_SIZE, db_file)) {
254         if(strncmp(raw_db_line, MYMAIL_DB_MAGIC_TOKEN, strlen(MYMAIL_DB_MAGIC_TOKEN))) {
255           fprintf(stderr,
256                   "mymail: Header line in '%s' does not match the mymail db format.\n",
257                   entry_name);
258           exit(EXIT_FAILURE);
259         }
260       } else {
261         fprintf(stderr,
262                 "mymail: Cannot read the header line in '%s'.\n",
263                 entry_name);
264         exit(EXIT_FAILURE);
265       }
266
267       search_in_db(nb_search_patterns, search_name, search_regexp_string,
268                    db_file);
269
270       fclose(db_file);
271     }
272   }
273 }
274
275 /*********************************************************************/
276
277 void index_one_mbox_line(int nb_fields_to_parse, struct parsable_field *fields_to_parse,
278                          char *raw_mbox_line, FILE *db_file) {
279   regmatch_t matches;
280   int f;
281   for(f = 0; f < nb_fields_to_parse; f++) {
282     if(regexec(&fields_to_parse[f].regexp, raw_mbox_line, 1, &matches, 0) == 0) {
283       fprintf(db_file, "%s %s\n",
284               fields_to_parse[f].name,
285               raw_mbox_line + matches.rm_eo);
286     }
287   }
288 }
289
290 void index_mbox(const char *mbox_filename,
291                 int nb_fields_to_parse, struct parsable_field *fields_to_parse,
292                 FILE *db_file) {
293   char raw_mbox_line[BUFFER_SIZE], full_line[BUFFER_SIZE];
294   char *end_of_full_line;
295   FILE *file;
296   int in_header, new_header;
297   unsigned long int position_in_file;
298
299   file = fopen(mbox_filename, "r");
300
301   if(!file) {
302     fprintf(stderr, "mymail: Cannot open '%s'.\n", mbox_filename);
303     if(paranoid) { exit(EXIT_FAILURE); }
304     return;
305   }
306
307   in_header = 0;
308   new_header = 0;
309
310   position_in_file = 0;
311   end_of_full_line = 0;
312   full_line[0] = '\0';
313
314   while(fgets(raw_mbox_line, BUFFER_SIZE, file)) {
315     if(strncmp(raw_mbox_line, "From ", 5) == 0) {
316       if(in_header) {
317         fprintf(stderr,
318                 "Got a ^\"From \" in the header in %s:%lu.\n",
319                 mbox_filename, position_in_file);
320         fprintf(stderr, "%s", raw_mbox_line);
321         if(paranoid) { exit(EXIT_FAILURE); }
322       }
323       in_header = 1;
324       new_header = 1;
325     } else if(strncmp(raw_mbox_line, "\n", 1) == 0) {
326       if(in_header) { in_header = 0; }
327     }
328
329     if(in_header) {
330       if(new_header) {
331         fprintf(db_file, "mail %lu %s\n", position_in_file, mbox_filename);
332         new_header = 0;
333       }
334
335       if(raw_mbox_line[0] == ' ' || raw_mbox_line[0] == '\t') {
336         char *start = raw_mbox_line;
337         while(*start == ' ' || *start == '\t') start++;
338         *(end_of_full_line++) = ' ';
339         strcpy(end_of_full_line, start);
340         while(*end_of_full_line && *end_of_full_line != '\n') {
341           end_of_full_line++;
342         }
343         *end_of_full_line = '\0';
344       }
345
346       else {
347         /* if(!((raw_mbox_line[0] >= 'a' && raw_mbox_line[0] <= 'z') || */
348              /* (raw_mbox_line[0] >= 'A' && raw_mbox_line[0] <= 'Z'))) { */
349           /* fprintf(stderr, */
350                   /* "Header line syntax error %s:%lu.\n", */
351                   /* mbox_filename, position_in_file); */
352           /* fprintf(stderr, "%s", raw_mbox_line); */
353         /* } */
354
355         if(full_line[0]) {
356           index_one_mbox_line(nb_fields_to_parse, fields_to_parse, full_line, db_file);
357         }
358
359         end_of_full_line = full_line;
360         strcpy(end_of_full_line, raw_mbox_line);
361         while(*end_of_full_line && *end_of_full_line != '\n') {
362           end_of_full_line++;
363         }
364         *end_of_full_line = '\0';
365       }
366
367     }
368
369     position_in_file += strlen(raw_mbox_line);
370   }
371
372   fclose(file);
373 }
374
375 void recursive_index_mbox(FILE *db_file,
376                           const char *entry_name,
377                           int nb_fields_to_parse, struct parsable_field *fields_to_parse) {
378   DIR *dir;
379   struct dirent *dir_e;
380   struct stat sb;
381   char subname[PATH_MAX + 1];
382
383   if(lstat(entry_name, &sb) != 0) {
384     fprintf(stderr,
385             "mymail: Cannot stat \"%s\": %s\n",
386             entry_name,
387             strerror(errno));
388     exit(EXIT_FAILURE);
389   }
390
391   dir = opendir(entry_name);
392
393   if(dir) {
394     while((dir_e = readdir(dir))) {
395       if(!ignore_entry(dir_e->d_name)) {
396         snprintf(subname, PATH_MAX, "%s/%s", entry_name, dir_e->d_name);
397         recursive_index_mbox(db_file, subname, nb_fields_to_parse, fields_to_parse);
398       }
399     }
400     closedir(dir);
401   } else {
402     index_mbox(entry_name, nb_fields_to_parse, fields_to_parse, db_file);
403   }
404 }
405
406 /*********************************************************************/
407
408 /* For long options that have no equivalent short option, use a
409    non-character as a pseudo short option, starting with CHAR_MAX + 1.  */
410 enum {
411   OPT_BASH_MODE = CHAR_MAX + 1
412 };
413
414 static struct option long_options[] = {
415   { "help", no_argument, 0, 'h' },
416   { "version", no_argument, 0, 'v' },
417   { "db-file", 1, 0, 'd' },
418   { "db-root", 1, 0, 'r' },
419   { "search", 1, 0, 's' },
420   { "index", 0, 0, 'i' },
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   /* for(f = 0; f < argc; f++) { */
456     /* printf("arg %d \"%s\"\n", f, argv[f]); */
457   /* } */
458
459   paranoid = 0;
460   action_index = 0;
461   db_filename = 0;
462   db_root_path = 0;
463
464   setlocale(LC_ALL, "");
465
466   nb_search_patterns = 0;
467
468   while ((c = getopt_long(argc, argv, "hvip:s:d:r:",
469                           long_options, NULL)) != -1) {
470
471     switch(c) {
472
473     case 'h':
474       show_help = 1;
475       break;
476
477     case 'v':
478       print_version(stdout);
479       break;
480
481     case 'i':
482       action_index = 1;
483       break;
484
485     case 'd':
486       db_filename = strdup(optarg);
487       break;
488
489     case 'r':
490       db_root_path = strdup(optarg);
491       break;
492
493     case 's':
494       if(nb_search_patterns == MAX_NB_SEARCH_PATTERNS) {
495         fprintf(stderr, "mymail: Too many search patterns.\n");
496         exit(EXIT_FAILURE);
497       }
498       search_pattern[nb_search_patterns++] = strdup(optarg);
499       break;
500
501     default:
502       error = 1;
503       break;
504     }
505   }
506
507   if(!db_filename) {
508     char *default_db_filename = getenv("MYMAIL_DB_FILE");
509
510     if(!default_db_filename) {
511       default_db_filename = "mymail.db";
512     }
513
514     db_filename = strdup(default_db_filename);
515   }
516
517   if(!db_root_path) {
518     char *default_db_root_path = getenv("MYMAIL_DB_ROOT");
519
520     if(default_db_root_path) {
521       db_root_path = strdup(default_db_root_path);
522     }
523   }
524
525   if(!db_root_path) {
526     fprintf(stderr,
527             "mymail: db root path is not set\n");
528     exit(EXIT_FAILURE);
529   }
530
531
532   if(error) {
533     print_usage(stderr);
534     exit(EXIT_FAILURE);
535   }
536
537   if(show_help) {
538     print_usage(stdout);
539     exit(EXIT_SUCCESS);
540   }
541
542   if(action_index) {
543     FILE *db_file;
544
545     db_file = fopen(db_filename, "w");
546
547     if(!db_file) {
548       fprintf(stderr,
549               "mymail: Cannot open \"%s\" for writing: %s\n",
550               db_filename,
551               strerror(errno));
552       exit(EXIT_FAILURE);
553     }
554
555     for(f = 0; f < nb_fields_to_parse; f++) {
556       if(regcomp(&fields_to_parse[f].regexp,
557                  fields_to_parse[f].regexp_string,
558                  REG_ICASE)) {
559         fprintf(stderr,
560                 "mymail: Syntax error in regexp \"%s\" for field \"%s\".\n",
561                 fields_to_parse[f].regexp_string,
562                 fields_to_parse[f].name);
563         exit(EXIT_FAILURE);
564       }
565     }
566
567     fprintf(db_file, "%s version_%s raw version\n", MYMAIL_DB_MAGIC_TOKEN, VERSION);
568
569     while(optind < argc) {
570       recursive_index_mbox(db_file,
571                            argv[optind],
572                            nb_fields_to_parse, fields_to_parse);
573       optind++;
574     }
575
576     fclose(db_file);
577
578     for(f = 0; f < nb_fields_to_parse; f++) {
579       regfree(&fields_to_parse[f].regexp);
580     }
581   }
582
583   else {
584
585     if(nb_search_patterns > 0) {
586       char *search_name[MAX_NB_SEARCH_PATTERNS];
587       char *search_regexp_string[MAX_NB_SEARCH_PATTERNS];
588       int n;
589
590       for(n = 0; n < nb_search_patterns; n++) {
591         search_name[n] = search_pattern[n];
592         search_regexp_string[n] = segment_next_field(search_pattern[n]);
593       }
594
595       if(!*search_regexp_string) {
596         fprintf(stderr,
597                 "Syntax error in the search pattern.\n");
598         exit(EXIT_FAILURE);
599       }
600
601       recursive_search_in_db(db_root_path,
602                              nb_search_patterns, search_name, search_regexp_string);
603
604       for(n = 0; n < nb_search_patterns; n++) {
605         free(search_pattern[n]);
606       }
607     }
608   }
609
610   free(db_filename);
611   free(db_root_path);
612
613   exit(EXIT_SUCCESS);
614 }