Now deals properly with multi-lines header lines. Does not deal with multipart mails.
[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 VERSION "0.1"
48
49 #define BUFFER_SIZE 16384
50
51 struct parsable_field {
52   char *name;
53   char *regexp_string;
54   regex_t regexp;
55 };
56
57 char *db_filename;
58 char *search_pattern;
59
60 int paranoid;
61 int action_index;
62
63 char *segment_next_field(char *current) {
64   while(*current && *current != ' ') current++;
65   *current = '\0'; current++;
66   while(*current && *current == ' ') current++;
67   return current;
68 }
69
70 void remove_eof(char *c) {
71   while(*c && *c != '\n' && *c != '\r') c++;
72   *c = '\0';
73 }
74
75 /********************************************************************/
76
77 /* malloc with error checking.  */
78
79 void *safe_malloc(size_t n) {
80   void *p = malloc(n);
81   if(!p && n != 0) {
82     fprintf(stderr,
83             "mymail: can not allocate memory: %s\n", strerror(errno));
84     exit(EXIT_FAILURE);
85   }
86   return p;
87 }
88
89 /*********************************************************************/
90
91 void usage(FILE *out) {
92   fprintf(out, "mymail version %s (%s)\n", VERSION, UNAME);
93   fprintf(out, "Written by Francois Fleuret <francois@fleuret.org>.\n");
94   fprintf(out, "\n");
95   fprintf(out, "Usage: mymail [options] [<filename1> [<filename2> ...]]\n");
96   fprintf(out, "\n");
97 }
98
99 /*********************************************************************/
100
101 void search_in_db(const char *search_name, const char *search_regexp_string,
102                   FILE *db_file) {
103   char raw_line[BUFFER_SIZE];
104   char current_mail_filename[BUFFER_SIZE];
105   unsigned long int current_position_in_mail;
106   char *name, *value;
107   regex_t regexp;
108   int already_written;
109
110   if(regcomp(&regexp,
111              search_regexp_string,
112              REG_ICASE)) {
113     fprintf(stderr,
114             "mymail: Syntax error in regexp \"%s\" for field \"%s\".\n",
115             search_regexp_string,
116             search_name);
117     exit(EXIT_FAILURE);
118   }
119
120   current_position_in_mail = 0;
121   already_written = 0;
122
123   while(fgets(raw_line, BUFFER_SIZE, db_file)) {
124     name = raw_line;
125     value = segment_next_field(raw_line);
126
127     if(strcmp("mail", name) == 0) {
128       char *position_in_file_string = value;
129       char *mail_filename = segment_next_field(value);
130       current_position_in_mail = atol(position_in_file_string);
131       strcpy(current_mail_filename, mail_filename);
132       remove_eof(current_mail_filename);
133       already_written = 0;
134     }
135
136     else if(!already_written) {
137       if(strcmp(search_name, name) == 0 && regexec(&regexp, value, 0, 0, 0) == 0) {
138         FILE *mail_file;
139         mail_file = fopen(current_mail_filename, "r");
140         if(!mail_file) {
141           fprintf(stderr, "mymail: Can not open `%s'.\n", current_mail_filename);
142           exit(EXIT_FAILURE);
143         }
144         fseek(mail_file, current_position_in_mail, SEEK_SET);
145         if(fgets(raw_line, BUFFER_SIZE, mail_file)) {
146           printf("%s", raw_line);
147           while(fgets(raw_line, BUFFER_SIZE, mail_file) &&
148                 strncmp(raw_line, "From ", 5)) {
149             printf("%s", raw_line);
150           }
151         }
152         fclose(mail_file);
153         already_written = 1;
154       }
155     }
156   }
157
158   regfree(&regexp);
159 }
160
161 /*********************************************************************/
162
163 void index_one_mbox_line(int nb_fields_to_parse, struct parsable_field *fields_to_parse,
164                          char *raw_line, FILE *db_file) {
165   regmatch_t matches;
166   int f;
167   for(f = 0; f < nb_fields_to_parse; f++) {
168     if(regexec(&fields_to_parse[f].regexp, raw_line, 1, &matches, 0) == 0) {
169       fprintf(db_file, "%s %s\n",
170               fields_to_parse[f].name,
171               raw_line + matches.rm_eo);
172     }
173   }
174 }
175
176 void index_mbox(const char *input_filename,
177                 int nb_fields_to_parse, struct parsable_field *fields_to_parse,
178                 FILE *db_file) {
179   char raw_line[BUFFER_SIZE], full_line[BUFFER_SIZE];
180   char *end_of_full_line;
181   FILE *file;
182   int in_header, new_header;
183   unsigned long int position_in_file;
184
185   file = fopen(input_filename, "r");
186
187   if(!file) {
188     fprintf(stderr, "mymail: Can not open `%s'.\n", input_filename);
189     if(paranoid) { exit(EXIT_FAILURE); }
190     return;
191   }
192
193   in_header = 0;
194   new_header = 0;
195
196   position_in_file = 0;
197
198   while(fgets(raw_line, BUFFER_SIZE, file)) {
199     if(strncmp(raw_line, "From ", 5) == 0) {
200       if(in_header) {
201         fprintf(stderr,
202                 "Got a ^\"From \" in the header in %s:%lu.\n",
203                 input_filename, position_in_file);
204         fprintf(stderr, "%s", raw_line);
205         if(paranoid) { exit(EXIT_FAILURE); }
206       }
207       in_header = 1;
208       new_header = 1;
209     } else if(strncmp(raw_line, "\n", 1) == 0) {
210       if(in_header) { in_header = 0; }
211     }
212
213     /* printf("PARSE %d %s", in_header, raw_line); */
214
215     if(in_header) {
216       if(new_header) {
217         fprintf(db_file, "mail %lu %s\n", position_in_file, input_filename);
218         new_header = 0;
219       }
220
221       if(raw_line[0] == ' ' || raw_line[0] == '\t') {
222         char *start = raw_line;
223         while(*start == ' ' || *start == '\t') start++;
224         *(end_of_full_line++) = ' ';
225         strcpy(end_of_full_line, start);
226         while(*end_of_full_line && *end_of_full_line != '\n') {
227           end_of_full_line++;
228         }
229         *end_of_full_line = '\0';
230       }
231
232       else {
233         /* if(!((raw_line[0] >= 'a' && raw_line[0] <= 'z') || */
234              /* (raw_line[0] >= 'A' && raw_line[0] <= 'Z'))) { */
235           /* fprintf(stderr, */
236                   /* "Header line syntax error %s:%lu.\n", */
237                   /* input_filename, position_in_file); */
238           /* fprintf(stderr, "%s", raw_line); */
239         /* } */
240
241         if(full_line[0]) {
242           index_one_mbox_line(nb_fields_to_parse, fields_to_parse, full_line, db_file);
243         }
244
245         end_of_full_line = full_line;
246         strcpy(end_of_full_line, raw_line);
247         while(*end_of_full_line && *end_of_full_line != '\n') {
248           end_of_full_line++;
249         }
250         *end_of_full_line = '\0';
251       }
252
253     }
254
255     position_in_file += strlen(raw_line);
256   }
257
258   fclose(file);
259 }
260
261 int ignore_entry(const char *name) {
262   return
263     /* strcmp(name, ".") == 0 || */
264     /* strcmp(name, "..") == 0 || */
265     (name[0] == '.' && name[1] != '/');
266 }
267
268 void process_entry(const char *dir_name,
269                    int nb_fields_to_parse, struct parsable_field *fields_to_parse,
270                    FILE *db_file) {
271   DIR *dir;
272   struct dirent *dir_e;
273   struct stat sb;
274   char subname[PATH_MAX + 1];
275
276   if(lstat(dir_name, &sb) != 0) {
277     fprintf(stderr,
278             "mymail: Can not stat \"%s\": %s\n",
279             dir_name,
280             strerror(errno));
281     exit(EXIT_FAILURE);
282   }
283
284   dir = opendir(dir_name);
285
286   if(dir) {
287     printf("Processing directory '%s'.\n", dir_name);
288     while((dir_e = readdir(dir))) {
289       if(!ignore_entry(dir_e->d_name)) {
290         snprintf(subname, PATH_MAX, "%s/%s", dir_name, dir_e->d_name);
291         process_entry(subname, nb_fields_to_parse, fields_to_parse, db_file);
292       }
293     }
294     closedir(dir);
295   } else {
296     index_mbox(dir_name, nb_fields_to_parse, fields_to_parse, db_file);
297   }
298 }
299
300 /*********************************************************************/
301
302 /* For long options that have no equivalent short option, use a
303    non-character as a pseudo short option, starting with CHAR_MAX + 1.  */
304 enum {
305   OPT_BASH_MODE = CHAR_MAX + 1
306 };
307
308 static struct option long_options[] = {
309   { "help", no_argument, 0, 'h' },
310   { "db-prefix", 1, 0, 'p' },
311   { "search-pattern", 1, 0, 's' },
312   { "index", 0, 0, 'i' },
313   { 0, 0, 0, 0 }
314 };
315
316 static struct parsable_field fields_to_parse[] = {
317   {
318     "from",
319     "^\\([Ff][Rr][Oo][Mm]:\\|From\\) *",
320     { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
321   },
322
323   {
324     "dest",
325     "^\\([Tt][Oo]\\|[Cc][Cc]\\|[Bb][Cc][Cc]\\): *",
326     { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
327   },
328 };
329
330 int main(int argc, char **argv) {
331   int error = 0, show_help = 0;
332   const int nb_fields_to_parse = sizeof(fields_to_parse) / sizeof(struct parsable_field);
333   char c;
334   int f;
335
336   paranoid = 0;
337   action_index = 0;
338   search_pattern = 0;
339
340   setlocale(LC_ALL, "");
341
342   while ((c = getopt_long(argc, argv, "hip:s:",
343                           long_options, NULL)) != -1) {
344
345     switch(c) {
346
347     case 'h':
348       show_help = 1;
349       break;
350
351     case 'i':
352       action_index = 1;
353       break;
354
355     case 'p':
356       db_filename = strdup(optarg);
357       break;
358
359     case 's':
360       if(search_pattern) {
361         fprintf(stderr, "mymail: Search pattern already defined.\n");
362         exit(EXIT_FAILURE);
363       }
364       search_pattern = strdup(optarg);
365       break;
366
367     default:
368       error = 1;
369       break;
370     }
371   }
372
373   if(!db_filename) {
374     db_filename = strdup("/tmp/mymail");
375   }
376
377   if(error) {
378     usage(stderr);
379     exit(EXIT_FAILURE);
380   }
381
382   if(show_help) {
383     usage(stdout);
384     exit(EXIT_SUCCESS);
385   }
386
387   if(action_index) {
388     FILE *db_file = fopen(db_filename, "w");
389     if(!db_file) {
390       fprintf(stderr,
391               "mymail: Can not open \"%s\" for writing: %s\n",
392               db_filename,
393               strerror(errno));
394       exit(EXIT_FAILURE);
395     }
396
397     for(f = 0; f < nb_fields_to_parse; f++) {
398       if(regcomp(&fields_to_parse[f].regexp,
399                  fields_to_parse[f].regexp_string,
400                  REG_ICASE)) {
401         fprintf(stderr,
402                 "mymail: Syntax error in regexp \"%s\" for field \"%s\".\n",
403                 fields_to_parse[f].regexp_string,
404                 fields_to_parse[f].name);
405         exit(EXIT_FAILURE);
406       }
407     }
408
409     while(optind < argc) {
410       process_entry(argv[optind],
411                     nb_fields_to_parse, fields_to_parse, db_file);
412       optind++;
413     }
414
415     fclose(db_file);
416
417     for(f = 0; f < nb_fields_to_parse; f++) {
418       regfree(&fields_to_parse[f].regexp);
419     }
420   }
421
422   else {
423     if(search_pattern) {
424       FILE *db_file;
425       char *search_name;
426       char *search_regexp_string;
427       search_name = search_pattern;
428       search_regexp_string = segment_next_field(search_pattern);
429       if(!*search_regexp_string) {
430         fprintf(stderr,
431                 "Syntax error in the search pattern.\n");
432         exit(EXIT_FAILURE);
433       }
434
435       /* printf("Starting search in %s for field \"%s\" matching \"%s\".\n", */
436       /* db_filename, */
437       /* search_name, */
438       /* search_regexp_string); */
439
440       db_file = fopen(db_filename, "r");
441
442       if(!db_file) {
443         fprintf(stderr,
444                 "mymail: Can not open \"%s\" for reading: %s\n",
445                 db_filename,
446                 strerror(errno));
447         exit(EXIT_FAILURE);
448       }
449
450       search_in_db(search_name, search_regexp_string, db_file);
451
452       fclose(db_file);
453       free(search_pattern);
454     }
455   }
456
457   free(db_filename);
458
459   exit(EXIT_SUCCESS);
460 }