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