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