]> mj.ucw.cz Git - maildups.git/blob - mdup.c
Teach mparse to handle maildirs
[maildups.git] / mdup.c
1 /*
2  *      Duplicate Mail Checker
3  *
4  *      (c) 2006 Martin Mares <mj@ucw.cz>
5  */
6
7 #include <stdio.h>
8 #include <string.h>
9 #include <stdlib.h>
10 #include <getopt.h>
11 #include <fcntl.h>
12 #include <unistd.h>
13 #include <pwd.h>
14 #include <time.h>
15 #include <sys/file.h>
16 #include <sys/stat.h>
17 #include <sys/mman.h>
18
19 #include "util.h"
20
21 const char progname[] = "mdup";
22
23 static char *db_name;
24 static uns max_age = 86400;
25 static uns now;
26 static char *local_user;
27 static char *local_domain;
28
29 struct item {
30   unsigned char digest[20];
31   uns timestamp;
32 };
33
34 static int db_fd;
35 static uns db_items;
36 static struct item *db_map;
37 static uns db_map_size;
38
39 static void
40 db_open(void)
41 {
42   if (!db_name)
43     {
44       struct passwd *pw = getpwuid(getuid());
45       if (!pw)
46         die("Sorry, but you don't exist!");
47       db_name = xmalloc(strlen(pw->pw_dir) + 10);
48       sprintf(db_name, "%s/.mdup.db", pw->pw_dir);
49     }
50   verb(2, "Opening database %s", db_name);
51   db_fd = open(db_name, O_RDWR | O_CREAT, 0600);
52   if (db_fd < 0)
53     die("Cannot open database %s: %m", db_name);
54   if (flock(db_fd, LOCK_EX) < 0)
55     die("Cannot flock %s: %m", db_name);
56
57   struct stat st;
58   if (fstat(db_fd, &st) < 0)
59     die("Cannot stat %s: %m", db_name);
60   if (st.st_size % sizeof(struct item))
61     die("Database %s is inconsistent: Size is not an integer number of records");
62   db_items = st.st_size / sizeof(struct item);
63   verb(2, "Mapping %d items", db_items);
64
65   db_map_size = sizeof(struct item) * (db_items+16);
66   db_map = mmap(NULL, db_map_size, PROT_READ | PROT_WRITE, MAP_SHARED, db_fd, 0);
67   if (db_map == MAP_FAILED)
68     die("Mmap on %s failed: %m", db_name);
69 }
70
71 static void
72 db_close(void)
73 {
74   munmap(db_map, db_map_size);
75   flock(db_fd, LOCK_UN);
76   close(db_fd);
77 }
78
79 static int
80 db_lookup(struct item *new)
81 {
82   struct item *free = NULL;
83
84   for (uns i=0; i<db_items; i++)
85     {
86       struct item *t = &db_map[i];
87       if (t->timestamp <= now && now - t->timestamp > max_age)
88         {
89           if (!free)
90             free = t;
91         }
92       else if (!memcmp(t->digest, new->digest, 20))
93         {
94           verb(2, "Found at item %d, age %d sec", i, now - t->timestamp);
95           t->timestamp = now;
96           return 1;
97         }
98     }
99
100   if (!free)
101     {
102       if (sizeof(struct item) * db_items >= db_map_size)
103         die("Internal error: map window too small");
104       free = &db_map[db_items++];
105       if (ftruncate(db_fd, sizeof(struct item) * db_items) < 0)
106         die("Cannot enlarge %s: %m", db_name);
107     }
108   verb(2, "Creating new entry");
109   *free = *new;
110   free->timestamp = now;
111   return 0;
112 }
113
114 #define MAX_HDR_LEN 1024
115
116 static char *
117 skip_cfws(char *c)
118 {
119   int nest = 0;
120
121   for (;;)
122     {
123       if (!*c)
124         return c;
125       else if (*c == ' ')
126         ;
127       else if (*c == '(')
128         nest++;
129       else if (!nest)
130         return c;
131       else if (*c == ')')
132         nest--;
133       else if (*c == '\\' && c[1])
134         c++;
135       c++;
136     }
137 }
138
139 static void
140 parse_header_line(char *line, uns cnt, struct item *item)
141 {
142   if (!cnt)
143     return;
144   if (cnt >= MAX_HDR_LEN)
145     {
146       verb(2, "HDR: <too long>");
147       return;
148     }
149
150   line[cnt] = 0;
151   verb(2, "HDR: %s", line);
152   if (strncasecmp(line, "Message-ID: ", 12))
153     return;
154   char *c = skip_cfws(line+12);
155   if (*c++ != '<')
156     return;
157
158   char lhs[MAX_HDR_LEN], *l = lhs;
159   if (*c == '\"')               // LHS is no-fold-quote
160     {
161       c++;
162       while (*c != '\"')
163         {
164           if (!*c)
165             return;
166           else if (*c == '\\' && c[1])
167             {
168               *l++ = c[1];
169               c += 2;
170             }
171           else
172             *l++ = *c++;
173         }
174       c++;
175     }
176   else                          // LHS is dot-atom-text
177     {
178       while (*c && *c != '@')
179         *l++ = *c++;
180     }
181   *l++ = 0;
182
183   if (*c++ != '@')              // "@" is mandatory
184     return;
185
186   char rhs[MAX_HDR_LEN], *r = rhs;
187   if (*c == '[')                // RHS is no-fold-literal
188     {
189       while (*c != ']')
190         {
191           if (!*c)
192             return;
193           else if (*c == '\\' && c[1])
194             {
195               *r++ = c[1];
196               c += 2;
197             }
198           else
199             *r++ = *c++;
200         }
201       c++;
202     }
203   else                          // RHS is dot-atom-text
204     {
205       while (*c && *c != '>')
206         *r++ = *c++;
207     }
208   *r++ = 0;
209
210   if (*c != '>')
211     return;
212
213   *c = 0;
214   verb(1, "Parsed Message-ID <%s@%s>", lhs, rhs);
215
216   if (local_domain && local_user)
217     {
218       uns lul = strlen(local_user);
219       if (!strcasecmp(rhs, local_domain) &&
220           !strncasecmp(lhs, local_user, lul) &&
221           !strncasecmp(lhs+lul, "+md-", 4))
222         {
223           verb(1, "Detected local Message-ID");
224           item->timestamp = 2;
225           return;
226         }
227     }
228
229   struct sha1_ctx ctx;
230   sha1_init(&ctx);
231   sha1_update(&ctx, (unsigned char *) lhs, l-lhs);
232   sha1_update(&ctx, (unsigned char *) rhs, r-rhs);
233   sha1_final(&ctx, item->digest);
234   item->timestamp = 1;
235   if (verbose >= 1)
236     {
237       fprintf(stderr, "Digest: ");
238       for (uns i=0; i<20; i++)
239         fprintf(stderr, "%02x", item->digest[i]);
240       fprintf(stderr, "\n");
241     }
242 }
243
244 static int
245 parse_header(struct item *item)
246 {
247   char buf[MAX_HDR_LEN+1];
248   uns cnt = 0;
249   uns last_nl = 0;
250
251   item->timestamp = 0;
252   for (;;)
253     {
254       int c = getchar();
255       if (c < 0)
256         {
257           verb(1, "Incomplete header");
258           return -1;
259         }
260       if (c == '\r')
261         ;
262       else if (c == '\n')
263         {
264           if (cnt == last_nl)   // End of header
265             {
266               parse_header_line(buf, cnt, item);
267               return item->timestamp;
268             }
269           last_nl = cnt;
270         }
271       else if (c == ' ' || c == '\t' || !c)
272         {
273           if (!cnt)
274             {
275               verb(1, "Misplaced whitespace at the beginning of header");
276               return -1;
277             }
278           if (cnt < MAX_HDR_LEN)
279             buf[cnt++] = ' ';
280         }
281       else
282         {
283           if (cnt == last_nl)
284             {
285               parse_header_line(buf, cnt, item);
286               cnt = last_nl = 0;
287             }
288           if (cnt < MAX_HDR_LEN)
289             buf[cnt++] = c;
290         }
291     }
292 }
293
294 static void NONRET
295 usage(void)
296 {
297   fprintf(stderr, "Usage: mdup [<options>]\n\
298 \n\
299 Options:\n\
300 -a <age>\t\tRecords older than <age> hours are ignored (default: 24)\n\
301 -d <db>\t\t\tUse <db> as a Message-ID database (default: ~/.mdup.db; beware of NFS)\n\
302 -l <user>@<domain>\tDetect looped back messages by their Message-ID\n\
303 -v\t\t\tIncrease verbosity\n\
304 \n\
305 MailDups " STR(VERSION) ", (c) " STR(YEAR) " Martin Mares <mj@ucw.cz>\n\
306 It can be freely distributed and used according to the GNU GPL v2.\n\
307 ");
308   exit(1);
309 }
310
311 int
312 main(int argc, char **argv)
313 {
314   int c;
315   while ((c = getopt(argc, argv, "a:d:l:v")) >= 0)
316     switch (c)
317       {
318       case 'a':
319         max_age = atol(optarg) * 3600;
320         break;
321       case 'd':
322         db_name = optarg;
323         break;
324       case 'l':
325         {
326           char *c = strchr(optarg, '@');
327           if (!c)
328             usage();
329           *c++ = 0;
330           local_user = optarg;
331           local_domain = c;
332           break;
333         }
334       case 'v':
335         verbose++;
336         break;
337       default:
338         usage();
339       }
340   if (optind < argc)
341     usage();
342
343   now = time(NULL);
344
345   struct item msg;
346   int ok = parse_header(&msg);
347   switch (ok)
348     {
349     case 0:
350       puts("NO ID");
351       break;
352     case 1:
353       db_open();
354       int ret = db_lookup(&msg);
355       db_close();
356       puts(ret ? "DUP" : "OK");
357       break;
358     case 2:
359       puts("LOCAL");
360       break;
361     default:
362       puts("ERROR");
363     }
364
365   return 0;
366 }