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