Now uses the environment variable MYMAIL_DB_FILE.
[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 65536
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   end_of_full_line = 0;
198
199   while(fgets(raw_line, BUFFER_SIZE, file)) {
200     if(strncmp(raw_line, "From ", 5) == 0) {
201       if(in_header) {
202         fprintf(stderr,
203                 "Got a ^\"From \" in the header in %s:%lu.\n",
204                 input_filename, position_in_file);
205         fprintf(stderr, "%s", raw_line);
206         if(paranoid) { exit(EXIT_FAILURE); }
207       }
208       in_header = 1;
209       new_header = 1;
210     } else if(strncmp(raw_line, "\n", 1) == 0) {
211       if(in_header) { in_header = 0; }
212     }
213
214     /* printf("PARSE %d %s", in_header, raw_line); */
215
216     if(in_header) {
217       if(new_header) {
218         fprintf(db_file, "mail %lu %s\n", position_in_file, input_filename);
219         new_header = 0;
220       }
221
222       if(raw_line[0] == ' ' || raw_line[0] == '\t') {
223         char *start = raw_line;
224         while(*start == ' ' || *start == '\t') start++;
225         *(end_of_full_line++) = ' ';
226         strcpy(end_of_full_line, start);
227         while(*end_of_full_line && *end_of_full_line != '\n') {
228           end_of_full_line++;
229         }
230         *end_of_full_line = '\0';
231       }
232
233       else {
234         /* if(!((raw_line[0] >= 'a' && raw_line[0] <= 'z') || */
235              /* (raw_line[0] >= 'A' && raw_line[0] <= 'Z'))) { */
236           /* fprintf(stderr, */
237                   /* "Header line syntax error %s:%lu.\n", */
238                   /* input_filename, position_in_file); */
239           /* fprintf(stderr, "%s", raw_line); */
240         /* } */
241
242         if(full_line[0]) {
243           index_one_mbox_line(nb_fields_to_parse, fields_to_parse, full_line, db_file);
244         }
245
246         end_of_full_line = full_line;
247         strcpy(end_of_full_line, raw_line);
248         while(*end_of_full_line && *end_of_full_line != '\n') {
249           end_of_full_line++;
250         }
251         *end_of_full_line = '\0';
252       }
253
254     }
255
256     position_in_file += strlen(raw_line);
257   }
258
259   fclose(file);
260 }
261
262 int ignore_entry(const char *name) {
263   return
264     /* strcmp(name, ".") == 0 || */
265     /* strcmp(name, "..") == 0 || */
266     (name[0] == '.' && name[1] != '/');
267 }
268
269 void process_entry(const char *dir_name,
270                    int nb_fields_to_parse, struct parsable_field *fields_to_parse,
271                    FILE *db_file) {
272   DIR *dir;
273   struct dirent *dir_e;
274   struct stat sb;
275   char subname[PATH_MAX + 1];
276
277   if(lstat(dir_name, &sb) != 0) {
278     fprintf(stderr,
279             "mymail: Can not stat \"%s\": %s\n",
280             dir_name,
281             strerror(errno));
282     exit(EXIT_FAILURE);
283   }
284
285   dir = opendir(dir_name);
286
287   if(dir) {
288     printf("Processing directory '%s'.\n", dir_name);
289     while((dir_e = readdir(dir))) {
290       if(!ignore_entry(dir_e->d_name)) {
291         snprintf(subname, PATH_MAX, "%s/%s", dir_name, dir_e->d_name);
292         process_entry(subname, nb_fields_to_parse, fields_to_parse, db_file);
293       }
294     }
295     closedir(dir);
296   } else {
297     index_mbox(dir_name, nb_fields_to_parse, fields_to_parse, db_file);
298   }
299 }
300
301 /*********************************************************************/
302
303 /* For long options that have no equivalent short option, use a
304    non-character as a pseudo short option, starting with CHAR_MAX + 1.  */
305 enum {
306   OPT_BASH_MODE = CHAR_MAX + 1
307 };
308
309 static struct option long_options[] = {
310   { "help", no_argument, 0, 'h' },
311   { "db-file", 1, 0, 'p' },
312   { "search-pattern", 1, 0, 's' },
313   { "index", 0, 0, 'i' },
314   { 0, 0, 0, 0 }
315 };
316
317 static struct parsable_field fields_to_parse[] = {
318   {
319     "from",
320     "^\\([Ff][Rr][Oo][Mm]:\\|From\\) *",
321     { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
322   },
323
324   {
325     "dest",
326     "^\\([Tt][Oo]\\|[Cc][Cc]\\|[Bb][Cc][Cc]\\): *",
327     { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
328   },
329 };
330
331 int main(int argc, char **argv) {
332   int error = 0, show_help = 0;
333   const int nb_fields_to_parse = sizeof(fields_to_parse) / sizeof(struct parsable_field);
334   char c;
335   int f;
336
337   paranoid = 0;
338   action_index = 0;
339   search_pattern = 0;
340
341   setlocale(LC_ALL, "");
342
343   while ((c = getopt_long(argc, argv, "hip:s:",
344                           long_options, NULL)) != -1) {
345
346     switch(c) {
347
348     case 'h':
349       show_help = 1;
350       break;
351
352     case 'i':
353       action_index = 1;
354       break;
355
356     case 'p':
357       db_filename = strdup(optarg);
358       printf("db_filename=\"%s\"\n", db_filename);
359       break;
360
361     case 's':
362       if(search_pattern) {
363         fprintf(stderr, "mymail: Search pattern already defined.\n");
364         exit(EXIT_FAILURE);
365       }
366       search_pattern = strdup(optarg);
367       break;
368
369     default:
370       error = 1;
371       break;
372     }
373   }
374
375   if(!db_filename) {
376     char *default_db_filename = getenv("MYMAIL_DB_FILE");
377     if(!default_db_filename) { default_db_filename = "/tmp/mymail.db"; }
378     db_filename = strdup(default_db_filename);
379   }
380
381   if(error) {
382     usage(stderr);
383     exit(EXIT_FAILURE);
384   }
385
386   if(show_help) {
387     usage(stdout);
388     exit(EXIT_SUCCESS);
389   }
390
391   if(action_index) {
392     FILE *db_file = fopen(db_filename, "w");
393     if(!db_file) {
394       fprintf(stderr,
395               "mymail: Can not open \"%s\" for writing: %s\n",
396               db_filename,
397               strerror(errno));
398       exit(EXIT_FAILURE);
399     }
400
401     for(f = 0; f < nb_fields_to_parse; f++) {
402       if(regcomp(&fields_to_parse[f].regexp,
403                  fields_to_parse[f].regexp_string,
404                  REG_ICASE)) {
405         fprintf(stderr,
406                 "mymail: Syntax error in regexp \"%s\" for field \"%s\".\n",
407                 fields_to_parse[f].regexp_string,
408                 fields_to_parse[f].name);
409         exit(EXIT_FAILURE);
410       }
411     }
412
413     while(optind < argc) {
414       process_entry(argv[optind],
415                     nb_fields_to_parse, fields_to_parse, db_file);
416       optind++;
417     }
418
419     fclose(db_file);
420
421     for(f = 0; f < nb_fields_to_parse; f++) {
422       regfree(&fields_to_parse[f].regexp);
423     }
424   }
425
426   else {
427     if(search_pattern) {
428       FILE *db_file;
429       char *search_name;
430       char *search_regexp_string;
431       search_name = search_pattern;
432       search_regexp_string = segment_next_field(search_pattern);
433       if(!*search_regexp_string) {
434         fprintf(stderr,
435                 "Syntax error in the search pattern.\n");
436         exit(EXIT_FAILURE);
437       }
438
439       /* printf("Starting search in %s for field \"%s\" matching \"%s\".\n", */
440       /* db_filename, */
441       /* search_name, */
442       /* search_regexp_string); */
443
444       db_file = fopen(db_filename, "r");
445
446       if(!db_file) {
447         fprintf(stderr,
448                 "mymail: Can not open \"%s\" for reading: %s\n",
449                 db_filename,
450                 strerror(errno));
451         exit(EXIT_FAILURE);
452       }
453
454       search_in_db(search_name, search_regexp_string, db_file);
455
456       fclose(db_file);
457       free(search_pattern);
458     }
459   }
460
461   free(db_filename);
462
463   exit(EXIT_SUCCESS);
464 }