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