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