1 /*************************************************
2 * Exim - an Internet mail transport agent *
3 *************************************************/
5 /* Copyright (c) University of Cambridge 1995 - 2018 */
6 /* Copyright (c) The Exim Maintainers 2020 */
7 /* See the file NOTICE for conditions of use and distribution. */
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:
12 http://www.inter7.com/courierimap/README.maildirquota.html
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. */
19 #include "appendfile.h"
20 #include "tf_maildir.h"
22 #define MAX_FILE_SIZE 5120
26 /*************************************************
27 * Ensure maildir directories exist *
28 *************************************************/
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.
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
42 Returns: TRUE on success; FALSE on failure
45 BOOL maildir_ensure_directories(uschar *path, address_item *addr,
46 BOOL create_directory, int dirmode, uschar *maildirfolder_create_regex)
50 const char *subdirs[] = { "/tmp", "/new", "/cur" };
53 debug_printf("ensuring maildir directories exist in %s\n", path);
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. */
61 for (i = 0; i < 4; i++)
64 const uschar *dir, *mdir;
73 mdir = CUS subdirs[i-1];
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,
82 for (j = 0; j < 10; j++)
84 if (Ustat(dir, &statbuf) == 0)
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,
89 addr->basic_errno = ERRNO_NOTDIRECTORY;
93 /* Try to make if non-existent and configured to do so */
95 if (errno == ENOENT && create_directory)
97 if (!directory_make(NULL, dir, dirmode, FALSE))
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;
105 debug_printf("created directory %s%s\n", path, mdir);
106 break; /* out of the race loop */
109 /* stat() error other than ENOENT, or ENOENT and not creatable */
111 addr->message = string_sprintf("stat() error for %s%s: %s", path, mdir,
113 addr->basic_errno = errno;
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
123 addr->message = string_sprintf("existence of %s%s unclear\n", path,
125 addr->basic_errno = errno;
126 addr->special_action = SPECIAL_FREEZE;
130 /* First time through the directories loop, cd to the main directory */
132 if (i == 0 && Uchdir(path) != 0)
134 addr->message = string_sprintf ("cannot chdir to %s", path);
135 addr->basic_errno = errno;
140 /* If the basic path matches maildirfolder_create_regex, we are dealing with
141 a subfolder, and should ensure that a maildirfolder file exists. */
143 if (maildirfolder_create_regex)
147 const pcre2_code * re;
149 DEBUG(D_transport) debug_printf("checking for maildirfolder requirement\n");
151 if (!(re = pcre2_compile((PCRE2_SPTR)maildirfolder_create_regex,
152 PCRE2_ZERO_TERMINATED, PCRE_COPT, &err, &offset, pcre_cmp_ctx)))
155 pcre2_get_error_message(err, errbuf, sizeof(errbuf));
156 addr->message = string_sprintf("appendfile: regular expression "
157 "error: %s at offset %ld while compiling %s", errbuf, (long)offset,
158 maildirfolder_create_regex);
162 if (regex_match(re, path, -1, NULL))
164 uschar *fname = string_sprintf("%s/maildirfolder", path);
165 if (Ustat(fname, &statbuf) == 0)
167 DEBUG(D_transport) debug_printf("maildirfolder already exists\n");
171 int fd = Uopen(fname, O_WRONLY|O_APPEND|O_CREAT, 0600);
174 addr->message = string_sprintf("appendfile: failed to create "
175 "maildirfolder file in %s directory: %s", path, strerror(errno));
179 DEBUG(D_transport) debug_printf("created maildirfolder file\n");
184 DEBUG(D_transport) debug_printf("maildirfolder file not required\n");
188 return TRUE; /* Everything exists that should exist */
194 /*************************************************
195 * Update maildirsizefile for new file *
196 *************************************************/
198 /* This function is called to add a new line to the file, recording the length
199 of the newly added message. There isn't much we can do on failure...
202 fd the open file descriptor
203 size the size of the message
209 maildir_record_length(int fd, int size)
213 sprintf(CS buffer, "%d 1\n", size);
214 len = Ustrlen(buffer);
215 if (lseek(fd, 0, SEEK_END) >= 0)
217 len = write(fd, buffer, len);
219 debug_printf("added '%.*s' to maildirsize file\n", len-1, buffer);
225 /*************************************************
226 * Find the size of a maildir *
227 *************************************************/
229 /* This function is called when we have to recalculate the size of a maildir by
230 scanning all the files and directories therein. There are rules and conventions
231 about which files or directories are included. We support this by the use of a
232 regex to match directories that are to be included.
234 Maildirs can only be one level deep. However, this function recurses, so it
235 might cope with deeper nestings. We use the existing check_dir_size() function
236 to add up the sizes of the files in a directory that contains messages.
238 The function returns the most recent timestamp encountered. It can also be run
239 in a dummy mode in which it does not scan for sizes, but just returns the
243 path the path to the maildir
244 filecount where to store the count of messages
245 latest where to store the latest timestamp encountered
246 regex a regex for getting files sizes from file names
247 dir_regex a regex for matching directories to be included
248 timestamp_only don't actually compute any sizes
250 Returns: the sum of the sizes of the messages
254 maildir_compute_size(uschar *path, int *filecount, time_t *latest,
255 const pcre2_code *regex, const pcre2_code *dir_regex, BOOL timestamp_only)
260 if (!(dir = exim_opendir(path)))
263 for (struct dirent *ent; ent = readdir(dir); )
265 uschar * s, * name = US ent->d_name;
268 if (Ustrcmp(name, ".") == 0 || Ustrcmp(name, "..") == 0) continue;
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. */
274 if (dir_regex && !regex_match(dir_regex, name, -1, NULL))
277 debug_printf("skipping %s/%s: dir_regex does not match\n", path, name);
281 /* The name is OK; stat it. */
283 s = string_sprintf("%s/%s", path, name);
284 if (Ustat(s, &statbuf) < 0)
287 debug_printf("maildir_compute_size: stat error %d for %s: %s\n", errno,
292 if ((statbuf.st_mode & S_IFMT) != S_IFDIR)
295 debug_printf("skipping %s/%s: not a directory\n", s, name);
299 /* Keep the latest timestamp encountered */
301 if (statbuf.st_mtime > *latest) *latest = statbuf.st_mtime;
303 /* If this is a maildir folder, call this function recursively. */
306 sum += maildir_compute_size(s, filecount, latest, regex, dir_regex,
309 /* Otherwise it must be a folder that contains messages (e.g. new or cur), so
310 we need to get its size, unless all we are interested in is the timestamp. */
312 else if (!timestamp_only)
313 sum += check_dir_size(s, filecount, regex);
320 debug_printf("maildir_compute_size (timestamp_only): %ld\n",
323 debug_printf("maildir_compute_size: path=%s\n sum=" OFF_T_FMT
324 " filecount=%d timestamp=%ld\n",
325 path, sum, *filecount, (long int) *latest);
332 /*************************************************
333 * Create or update maildirsizefile *
334 *************************************************/
336 /* This function is called before a delivery if the option to use
337 maildirsizefile is enabled. Its function is to create the file if it does not
338 exist, or to update it if that is necessary.
340 The logic in this function follows the rules that are described in
342 http://www.inter7.com/courierimap/README.maildirquota.html
344 Or, at least, it is supposed to!
347 path the path to the maildir directory; this is already backed-up
348 to the parent if the delivery directory is a maildirfolder
349 ob the appendfile options block
350 regex a compiled regex for getting a file's size from its name
351 dir_regex a compiled regex for selecting maildir directories
352 returned_size where to return the current size of the maildir, even if
353 the maildirsizefile is removed because of a race
355 Returns: >=0 a file descriptor for an open maildirsize file
356 -1 there was an error opening or accessing the file
357 -2 the file was removed because of a race
361 maildir_ensure_sizefile(uschar *path, appendfile_transport_options_block *ob,
362 const pcre2_code *regex, const pcre2_code *dir_regex, off_t *returned_size,
363 int *returned_filecount)
366 off_t cached_quota = 0;
367 int cached_quota_filecount = 0;
372 uschar buffer[MAX_FILE_SIZE];
373 uschar *ptr = buffer;
376 /* Try a few times to open or create the file, in case another process is doing
379 filename = string_sprintf("%s/maildirsize", path);
381 DEBUG(D_transport) debug_printf("looking for maildirsize in %s\n", path);
382 if ((fd = Uopen(filename, O_RDWR|O_APPEND, ob->mode ? ob->mode : 0600)) < 0)
384 if (errno != ENOENT) return -1;
386 debug_printf("%s does not exist: recalculating\n", filename);
390 /* The file has been successfully opened. Check that the cached quota value is
391 still correct, and that the size of the file is still small enough. If so,
392 compute the maildir size from the file. */
394 if ((count = read(fd, buffer, sizeof(buffer))) >= sizeof(buffer))
397 debug_printf("maildirsize file too big (%d): recalculating\n", count);
400 buffer[count] = 0; /* Ensure string terminated */
402 /* Read the quota parameters from the first line of the data. */
405 debug_printf("reading quota parameters from maildirsize data\n");
409 off_t n = (off_t)Ustrtod(ptr, &endptr);
411 /* Only two data items are currently defined; ignore any others that
412 may be present. The spec is for a number followed by a letter. Anything
413 else we reject and recalculate. */
415 if (*endptr == 'S') cached_quota = n;
416 else if (*endptr == 'C') cached_quota_filecount = (int)n;
417 if (!isalpha(*endptr++))
420 debug_printf("quota parameter number not followed by letter in "
421 "\"%.*s\": recalculating maildirsize\n", (int)(endptr - buffer),
425 if (*endptr == '\n' || *endptr == 0) break;
426 if (*endptr++ != ',')
429 debug_printf("quota parameter not followed by comma in "
430 "\"%.*s\": recalculating maildirsize\n", (int)(endptr - buffer),
437 /* Check the cached values against the current settings */
439 if (cached_quota != ob->quota_value ||
440 cached_quota_filecount != ob->quota_filecount_value)
443 debug_printf("cached quota is out of date: recalculating\n"
444 " quota=" OFF_T_FMT " cached_quota=" OFF_T_FMT " filecount_quota=%d "
445 "cached_quota_filecount=%d\n", ob->quota_value,
446 cached_quota, ob->quota_filecount_value, cached_quota_filecount);
450 /* Quota values agree; parse the rest of the data to get the sizes. At this
451 stage, *endptr points either to 0 or to '\n'. */
454 debug_printf("computing maildir size from maildirsize data\n");
456 while (*endptr++ == '\n')
458 if (*endptr == 0) break;
461 size += (off_t)Ustrtod(ptr, &endptr);
462 if (*endptr != ' ') break;
464 filecount += Ustrtol(ptr, &endptr, 10);
467 /* If *endptr is zero, we have successfully parsed the file, and we now have
468 the size of the mailbox as cached in the file. The "rules" say that if this
469 value indicates that the mailbox is over quota, we must recalculate if there is
470 more than one entry in the file, or if the file is older than 15 minutes. Also,
471 just in case there are weird values in the file, recalculate if either of the
472 values is negative. */
476 if (size < 0 || filecount < 0)
478 DEBUG(D_transport) debug_printf("negative value in maildirsize "
479 "(size=" OFF_T_FMT " count=%d): recalculating\n", size, filecount);
483 if (ob->quota_value > 0 &&
484 (size + (ob->quota_is_inclusive? message_size : 0) > ob->quota_value ||
485 (ob->quota_filecount_value > 0 &&
486 filecount + (ob->quota_is_inclusive ? 1:0) >
487 ob->quota_filecount_value)
493 DEBUG(D_transport) debug_printf("over quota and maildirsize has "
494 "more than 1 entry: recalculating\n");
498 if (fstat(fd, &statbuf) < 0) goto RECALCULATE; /* Should never occur */
500 if (time(NULL) - statbuf.st_mtime > 15*60)
502 DEBUG(D_transport) debug_printf("over quota and maildirsize is older "
503 "than 15 minutes: recalculating\n");
510 /* If *endptr is not zero, there was a syntax error in the file. */
515 time_t old_latest, new_latest;
522 while (p > buffer && p[-1] != '\n') p--;
525 debug_printf("error in maildirsizefile: unexpected character %d in "
526 "line %d (starting '%s'): recalculating\n",
527 *endptr, linecount + 1, string_printing(p));
530 /* Either there is no file, or the quota value has changed, or the file has
531 got too big, or there was some format error in the file. Recalculate the size
532 and write new contents to a temporary file; then rename it. After any
533 error, just return -1 as the file descriptor. */
537 if (fd >= 0) (void)close(fd);
540 size = maildir_compute_size(path, &filecount, &old_latest, regex, dir_regex,
543 (void)gettimeofday(&tv, NULL);
544 tempname = string_sprintf("%s/tmp/" TIME_T_FMT ".H%luP%lu.%s",
545 path, tv.tv_sec, tv.tv_usec, (long unsigned) getpid(), primary_hostname);
547 fd = Uopen(tempname, O_RDWR|O_CREAT|O_EXCL, ob->mode ? ob->mode : 0600);
550 (void)sprintf(CS buffer, OFF_T_FMT "S,%dC\n" OFF_T_FMT " %d\n",
551 ob->quota_value, ob->quota_filecount_value, size, filecount);
552 len = Ustrlen(buffer);
553 if (write(fd, buffer, len) != len || Urename(tempname, filename) < 0)
560 /* If any of the directories have been modified since the last timestamp we
561 saw, we have to junk this maildirsize file. */
563 DEBUG(D_transport) debug_printf("checking subdirectory timestamps\n");
565 (void)maildir_compute_size(path, NULL, &new_latest , NULL, dir_regex, TRUE);
566 if (new_latest > old_latest)
568 DEBUG(D_transport) debug_printf("abandoning maildirsize because of "
569 "a later subdirectory modification\n");
570 (void)Uunlink(filename);
576 /* Return the sizes and the file descriptor, if any */
578 DEBUG(D_transport) debug_printf("returning maildir size=" OFF_T_FMT
579 " filecount=%d\n", size, filecount);
580 *returned_size = size;
581 *returned_filecount = filecount;
585 /* End of tf_maildir.c */