Avoid unnecessary scan of maildir mailbox when mailbox_size is set but
[users/jgh/exim.git] / src / src / transports / tf_maildir.c
1 /* $Cambridge: exim/src/src/transports/tf_maildir.c,v 1.8 2006/02/07 11:19:03 ph10 Exp $ */
2
3 /*************************************************
4 *     Exim - an Internet mail transport agent    *
5 *************************************************/
6
7 /* Copyright (c) University of Cambridge 1995 - 2006 */
8 /* See the file NOTICE for conditions of use and distribution. */
9
10 /* Functions in support of the use of maildirsize files for handling quotas in
11 maildir directories. Some of the rules are a bit baroque:
12
13 http://www.inter7.com/courierimap/README.maildirquota.html
14
15 We try to follow most of that, except that the directories to skip for quota
16 calculations are not hard wired in, but are supplied as a regex. */
17
18
19 #include "../exim.h"
20 #include "appendfile.h"
21 #include "tf_maildir.h"
22
23 #define MAX_FILE_SIZE  5120
24
25
26
27 /*************************************************
28 *      Ensure maildir directories exist          *
29 *************************************************/
30
31 /* This function is called at the start of a maildir delivery, to ensure that
32 all the relevant directories exist.
33
34 Argument:
35   path              the base directory name
36   addr              the address item (for setting an error message)
37   create_directory  true if we are allowed to create missing directories
38   dirmode           the mode for created directories
39
40 Returns:            TRUE on success; FALSE on failure
41 */
42
43 BOOL maildir_ensure_directories(uschar *path, address_item *addr,
44   BOOL create_directory, int dirmode)
45 {
46 int i;
47 struct stat statbuf;
48 char *subdirs[] = { "/tmp", "/new", "/cur" };
49
50 DEBUG(D_transport)
51   debug_printf("ensuring maildir directories exist in %s\n", path);
52
53 /* First ensure that the path we have is a directory; if it does not exist,
54 create it. Then make sure the tmp, new & cur subdirs of the maildir are
55 there. If not, fail which aborts the delivery (even though the cur subdir is
56 not actually needed for delivery). Handle all 4 directory tests/creates in a
57 loop so that code can be shared. */
58
59 for (i = 0; i < 4; i++)
60   {
61   int j;
62   uschar *dir, *mdir;
63
64   if (i == 0)
65     {
66     mdir = US"";
67     dir = path;
68     }
69   else
70     {
71     mdir = US subdirs[i-1];
72     dir = mdir + 1;
73     }
74
75   /* Check an existing path is a directory. This is inside a loop because
76   there is a potential race condition when creating the directory - some
77   other process may get there first. Give up after trying several times,
78   though. */
79
80   for (j = 0; j < 10; j++)
81     {
82     if (Ustat(dir, &statbuf) == 0)
83       {
84       if (S_ISDIR(statbuf.st_mode)) break;   /* out of the race loop */
85       addr->message = string_sprintf("%s%s is not a directory", path,
86         mdir);
87       addr->basic_errno = ERRNO_NOTDIRECTORY;
88       return FALSE;
89       }
90
91     /* Try to make if non-existent and configured to do so */
92
93     if (errno == ENOENT && create_directory)
94       {
95       if (!directory_make(NULL, dir, dirmode, FALSE))
96         {
97         if (errno == EEXIST) continue;     /* repeat the race loop */
98         addr->message = string_sprintf("cannot create %s%s", path, mdir);
99         addr->basic_errno = errno;
100         return FALSE;
101         }
102       DEBUG(D_transport)
103         debug_printf("created directory %s%s\n", path, mdir);
104       break;   /* out of the race loop */
105       }
106
107     /* stat() error other than ENOENT, or ENOENT and not creatable */
108
109     addr->message = string_sprintf("stat() error for %s%s: %s", path, mdir,
110       strerror(errno));
111     addr->basic_errno = errno;
112     return FALSE;
113     }
114
115   /* If we went round the loop 10 times, the directory was flickering in
116   and out of existence like someone in a malfunctioning Star Trek
117   transporter. */
118
119   if (j >= 10)
120     {
121     addr->message = string_sprintf("existence of %s%s unclear\n", path,
122       mdir);
123     addr->basic_errno = errno;
124     addr->special_action = SPECIAL_FREEZE;
125     return FALSE;
126     }
127
128   /* First time through the directories loop, cd to the main directory */
129
130   if (i == 0 && Uchdir(path) != 0)
131     {
132     addr->message = string_sprintf ("cannot chdir to %s", path);
133     addr->basic_errno = errno;
134     return FALSE;
135     }
136   }
137
138 return TRUE;   /* All directories exist */
139 }
140
141
142
143
144 /*************************************************
145 *       Update maildirsizefile for new file      *
146 *************************************************/
147
148 /* This function is called to add a new line to the file, recording the length
149 of the newly added message. There isn't much we can do on failure...
150
151 Arguments:
152   fd           the open file descriptor
153   size         the size of the message
154
155 Returns:       nothing
156 */
157
158 void
159 maildir_record_length(int fd, int size)
160 {
161 int len;
162 uschar buffer[256];
163 sprintf(CS buffer, "%d 1\n", size);
164 len = Ustrlen(buffer);
165 (void)lseek(fd, 0, SEEK_END);
166 (void)write(fd, buffer, len);
167 DEBUG(D_transport)
168   debug_printf("added '%.*s' to maildirsize file\n", len-1, buffer);
169 }
170
171
172
173 /*************************************************
174 *          Find the size of a maildir            *
175 *************************************************/
176
177 /* This function is called when we have to recalculate the size of a maildir by
178 scanning all the files and directories therein. There are rules and conventions
179 about which files or directories are included. We support this by the use of a
180 regex to match directories that are to be included.
181
182 Maildirs can only be one level deep. However, this function recurses, so it
183 might cope with deeper nestings. We use the existing check_dir_size() function
184 to add up the sizes of the files in a directory that contains messages.
185
186 The function returns the most recent timestamp encountered. It can also be run
187 in a dummy mode in which it does not scan for sizes, but just returns the
188 timestamp.
189
190 Arguments:
191   path            the path to the maildir
192   filecount       where to store the count of messages
193   latest          where to store the latest timestamp encountered
194   regex           a regex for getting files sizes from file names
195   dir_regex       a regex for matching directories to be included
196   timestamp_only  don't actually compute any sizes
197
198 Returns:      the sum of the sizes of the messages
199 */
200
201 off_t
202 maildir_compute_size(uschar *path, int *filecount, time_t *latest,
203   const pcre *regex, const pcre *dir_regex, BOOL timestamp_only)
204 {
205 DIR *dir;
206 off_t sum = 0;
207 struct dirent *ent;
208 struct stat statbuf;
209
210 dir = opendir(CS path);
211 if (dir == NULL) return 0;
212
213 while ((ent = readdir(dir)) != NULL)
214   {
215   uschar *name = US ent->d_name;
216   uschar buffer[1024];
217
218   if (Ustrcmp(name, ".") == 0 || Ustrcmp(name, "..") == 0) continue;
219
220   /* We are normally supplied with a regex for choosing which directories to
221   scan. We do the regex match first, because that avoids a stat() for names
222   we aren't interested in. */
223
224   if (dir_regex != NULL &&
225       pcre_exec(dir_regex, NULL, CS name, Ustrlen(name), 0, 0, NULL, 0) < 0)
226     {
227     DEBUG(D_transport)
228       debug_printf("skipping %s/%s: dir_regex does not match\n", path, name);
229     continue;
230     }
231
232   /* The name is OK; stat it. */
233
234   if (!string_format(buffer, sizeof(buffer), "%s/%s", path, name))
235     {
236     DEBUG(D_transport)
237       debug_printf("maildir_compute_size: name too long: dir=%s name=%s\n",
238         path, name);
239     continue;
240     }
241
242   if (Ustat(buffer, &statbuf) < 0)
243     {
244     DEBUG(D_transport)
245       debug_printf("maildir_compute_size: stat error %d for %s: %s\n", errno,
246         buffer, strerror(errno));
247     continue;
248     }
249
250   if ((statbuf.st_mode & S_IFMT) != S_IFDIR)
251     {
252     DEBUG(D_transport)
253       debug_printf("skipping %s/%s: not a directory\n", path, name);
254     continue;
255     }
256
257   /* Keep the latest timestamp encountered */
258
259   if (statbuf.st_mtime > *latest) *latest = statbuf.st_mtime;
260
261   /* If this is a maildir folder, call this function recursively. */
262
263   if (name[0] == '.')
264     {
265     sum += maildir_compute_size(buffer, filecount, latest, regex, dir_regex,
266       timestamp_only);
267     }
268
269   /* Otherwise it must be a folder that contains messages (e.g. new or cur), so
270   we need to get its size, unless all we are interested in is the timestamp. */
271
272   else if (!timestamp_only)
273     {
274     sum += check_dir_size(buffer, filecount, regex);
275     }
276   }
277
278 closedir(dir);
279 DEBUG(D_transport)
280   {
281   if (timestamp_only)
282     debug_printf("maildir_compute_size (timestamp_only): %ld\n",
283     (long int) *latest);
284   else
285     debug_printf("maildir_compute_size: path=%s\n  sum=" OFF_T_FMT
286       " filecount=%d timestamp=%ld\n",
287       path, sum, *filecount, (long int) *latest);
288   }
289 return sum;
290 }
291
292
293
294 /*************************************************
295 *        Create or update maildirsizefile        *
296 *************************************************/
297
298 /* This function is called before a delivery if the option to use
299 maildirsizefile is enabled. Its function is to create the file if it does not
300 exist, or to update it if that is necessary.
301
302 The logic in this function follows the rules that are described in
303
304   http://www.inter7.com/courierimap/README.maildirquota.html
305
306 Or, at least, it is supposed to!
307
308 Arguments:
309   path             the path to the maildir directory; this is already backed-up
310                      to the parent if the delivery diretory is a maildirfolder
311   ob               the appendfile options block
312   regex            a compiled regex for getting a file's size from its name
313   dir_regex        a compiled regex for selecting maildir directories
314   returned_size    where to return the current size of the maildir, even if
315                      the maildirsizefile is removed because of a race
316
317 Returns:           >=0  a file descriptor for an open maildirsize file
318                    -1   there was an error opening or accessing the file
319                    -2   the file was removed because of a race
320 */
321
322 int
323 maildir_ensure_sizefile(uschar *path, appendfile_transport_options_block *ob,
324   const pcre *regex, const pcre *dir_regex, off_t *returned_size,
325   int *returned_filecount)
326 {
327 int count, fd;
328 off_t cached_quota = 0;
329 int cached_quota_filecount = 0;
330 int filecount = 0;
331 int linecount = 0;
332 off_t size = 0;
333 uschar *filename;
334 uschar buffer[MAX_FILE_SIZE];
335 uschar *ptr = buffer;
336 uschar *endptr;
337
338 /* Try a few times to open or create the file, in case another process is doing
339 the same thing. */
340
341 filename = string_sprintf("%s/maildirsize", path);
342
343 DEBUG(D_transport) debug_printf("looking for maildirsize in %s\n", path);
344 fd = Uopen(filename, O_RDWR|O_APPEND, 0);
345 if (fd < 0)
346   {
347   if (errno != ENOENT) return -1;
348   DEBUG(D_transport)
349     debug_printf("%s does not exist: recalculating\n", filename);
350   goto RECALCULATE;
351   }
352
353 /* The file has been successfully opened. Check that the cached quota value is
354 still correct, and that the size of the file is still small enough. If so,
355 compute the maildir size from the file. */
356
357 count = read(fd, buffer, sizeof(buffer));
358 if (count >= sizeof(buffer))
359   {
360   DEBUG(D_transport)
361     debug_printf("maildirsize file too big (%d): recalculating\n", count);
362   goto RECALCULATE;
363   }
364 buffer[count] = 0;   /* Ensure string terminated */
365
366 /* Read the quota parameters from the first line of the data. */
367
368 DEBUG(D_transport)
369   debug_printf("reading quota parameters from maildirsize data\n");
370
371 for (;;)
372   {
373   off_t n = (off_t)Ustrtod(ptr, &endptr);
374
375   /* Only two data items are currently defined; ignore any others that
376   may be present. The spec is for a number followed by a letter. Anything
377   else we reject and recalculate. */
378
379   if (*endptr == 'S') cached_quota = n;
380     else if (*endptr == 'C') cached_quota_filecount = (int)n;
381   if (!isalpha(*endptr++))
382     {
383     DEBUG(D_transport)
384       debug_printf("quota parameter number not followed by letter in "
385         "\"%.*s\": recalculating maildirsize\n", (int)(endptr - buffer),
386         buffer);
387     goto RECALCULATE;
388     }
389   if (*endptr == '\n' || *endptr == 0) break;
390   if (*endptr++ != ',')
391     {
392     DEBUG(D_transport)
393       debug_printf("quota parameter not followed by comma in "
394         "\"%.*s\": recalculating maildirsize\n", (int)(endptr - buffer),
395         buffer);
396     goto RECALCULATE;
397     }
398   ptr = endptr;
399   }
400
401 /* Check the cached values against the current settings */
402
403 if (cached_quota != ob->quota_value ||
404     cached_quota_filecount != ob->quota_filecount_value)
405   {
406   DEBUG(D_transport)
407     debug_printf("cached quota is out of date: recalculating\n"
408       "  quota=" OFF_T_FMT " cached_quota=" OFF_T_FMT " filecount_quota=%d "
409       "cached_quota_filecount=%d\n", ob->quota_value,
410       cached_quota, ob->quota_filecount_value, cached_quota_filecount);
411   goto RECALCULATE;
412   }
413
414 /* Quota values agree; parse the rest of the data to get the sizes. At this
415 stage, *endptr points either to 0 or to '\n'.  */
416
417 DEBUG(D_transport)
418   debug_printf("computing maildir size from maildirsize data\n");
419
420 while (*endptr++ == '\n')
421   {
422   if (*endptr == 0) break;
423   linecount++;
424   ptr = endptr;
425   size += (off_t)Ustrtod(ptr, &endptr);
426   if (*endptr != ' ') break;
427   ptr = endptr + 1;
428   filecount += Ustrtol(ptr, &endptr, 10);
429   }
430
431 /* If *endptr is zero, we have successfully parsed the file, and we now have
432 the size of the mailbox as cached in the file. The "rules" say that if this
433 value indicates that the mailbox is over quota, we must recalculate if there is
434 more than one entry in the file, or if the file is older than 15 minutes. Also,
435 just in case there are weird values in the file, recalculate if either of the
436 values is negative. */
437
438 if (*endptr == 0)
439   {
440   if (size < 0 || filecount < 0)
441     {
442     DEBUG(D_transport) debug_printf("negative value in maildirsize "
443       "(size=" OFF_T_FMT " count=%d): recalculating\n", size, filecount);
444     goto RECALCULATE;
445     }
446
447   if (ob->quota_value > 0 &&
448       (size + (ob->quota_is_inclusive? message_size : 0) > ob->quota_value ||
449         (ob->quota_filecount_value > 0 &&
450           filecount + (ob->quota_is_inclusive ? 1:0) >
451             ob->quota_filecount_value)
452       ))
453     {
454     struct stat statbuf;
455     if (linecount > 1)
456       {
457       DEBUG(D_transport) debug_printf("over quota and maildirsize has "
458         "more than 1 entry: recalculating\n");
459       goto RECALCULATE;
460       }
461
462     if (fstat(fd, &statbuf) < 0) goto RECALCULATE;  /* Should never occur */
463
464     if (time(NULL) - statbuf.st_mtime > 15*60)
465       {
466       DEBUG(D_transport) debug_printf("over quota and maildirsize is older "
467         "than 15 minutes: recalculating\n");
468       goto RECALCULATE;
469       }
470     }
471   }
472
473
474 /* If *endptr is not zero, there was a syntax error in the file. */
475
476 else
477   {
478   int len;
479   time_t old_latest, new_latest;
480   uschar *tempname;
481   struct timeval tv;
482
483   DEBUG(D_transport)
484     {
485     uschar *p = endptr;
486     while (p > buffer && p[-1] != '\n') p--;
487     endptr[1] = 0;
488
489     debug_printf("error in maildirsizefile: unexpected character %d in "
490       "line %d (starting '%s'): recalculating\n",
491       *endptr, linecount + 1, string_printing(p));
492     }
493
494   /* Either there is no file, or the quota value has changed, or the file has
495   got too big, or there was some format error in the file. Recalculate the size
496   and write new contents to a temporary file; then rename it. After any
497   error, just return -1 as the file descriptor. */
498
499   RECALCULATE:
500
501   if (fd >= 0) (void)close(fd);
502   old_latest = 0;
503   filecount = 0;
504   size = maildir_compute_size(path, &filecount, &old_latest, regex, dir_regex,
505     FALSE);
506
507   (void)gettimeofday(&tv, NULL);
508   tempname = string_sprintf("%s/tmp/%lu.H%luP%lu.%s", path, tv.tv_sec,
509     tv.tv_usec, getpid(), primary_hostname);
510
511   fd = Uopen(tempname, O_RDWR|O_CREAT|O_EXCL, 0600);
512   if (fd >= 0)
513     {
514     (void)sprintf(CS buffer, OFF_T_FMT "S,%dC\n" OFF_T_FMT " %d\n",
515       ob->quota_value, ob->quota_filecount_value, size, filecount);
516     len = Ustrlen(buffer);
517     if (write(fd, buffer, len) != len || Urename(tempname, filename) < 0)
518       {
519       (void)close(fd);
520       fd = -1;
521       }
522     }
523
524   /* If any of the directories have been modified since the last timestamp we
525   saw, we have to junk this maildirsize file. */
526
527   DEBUG(D_transport) debug_printf("checking subdirectory timestamps\n");
528   new_latest = 0;
529   (void)maildir_compute_size(path, NULL, &new_latest , NULL, dir_regex, TRUE);
530   if (new_latest > old_latest)
531     {
532     DEBUG(D_transport) debug_printf("abandoning maildirsize because of "
533       "a later subdirectory modification\n");
534     (void)Uunlink(filename);
535     (void)close(fd);
536     fd = -1;
537     }
538   }
539
540 /* Return the sizes and the file descriptor, if any */
541
542 DEBUG(D_transport) debug_printf("returning maildir size=" OFF_T_FMT
543   " filecount=%d\n", size, filecount);
544 *returned_size = size;
545 *returned_filecount = filecount;
546 return fd;
547 }
548
549 /* End of tf_maildir.c */