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