1 /*************************************************
2 * Exim - an Internet mail transport agent *
3 *************************************************/
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 */
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:
13 http://www.inter7.com/courierimap/README.maildirquota.html
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. */
20 #include "appendfile.h"
21 #include "tf_maildir.h"
23 #define MAX_FILE_SIZE 5120
27 /*************************************************
28 * Ensure maildir directories exist *
29 *************************************************/
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.
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
43 Returns: TRUE on success; FALSE on failure
46 BOOL maildir_ensure_directories(uschar *path, address_item *addr,
47 BOOL create_directory, int dirmode, uschar *maildirfolder_create_regex)
51 const char *subdirs[] = { "/tmp", "/new", "/cur" };
54 debug_printf("ensuring maildir directories exist in %s\n", path);
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. */
62 for (i = 0; i < 4; i++)
65 const uschar *dir, *mdir;
74 mdir = CUS subdirs[i-1];
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,
83 for (j = 0; j < 10; j++)
85 if (Ustat(dir, &statbuf) == 0)
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,
90 addr->basic_errno = ERRNO_NOTDIRECTORY;
94 /* Try to make if non-existent and configured to do so */
96 if (errno == ENOENT && create_directory)
98 if (!directory_make(NULL, dir, dirmode, FALSE))
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;
106 debug_printf("created directory %s%s\n", path, mdir);
107 break; /* out of the race loop */
110 /* stat() error other than ENOENT, or ENOENT and not creatable */
112 addr->message = string_sprintf("stat() error for %s%s: %s", path, mdir,
114 addr->basic_errno = errno;
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
124 addr->message = string_sprintf("existence of %s%s unclear\n", path,
126 addr->basic_errno = errno;
127 addr->special_action = SPECIAL_FREEZE;
131 /* First time through the directories loop, cd to the main directory */
133 if (i == 0 && Uchdir(path) != 0)
135 addr->message = string_sprintf ("cannot chdir to %s", path);
136 addr->basic_errno = errno;
141 /* If the basic path matches maildirfolder_create_regex, we are dealing with
142 a subfolder, and should ensure that a maildirfolder file exists. */
144 if (maildirfolder_create_regex)
146 const pcre2_code * re;
148 DEBUG(D_transport) debug_printf("checking for maildirfolder requirement\n");
150 if (!(re = regex_compile(maildirfolder_create_regex,
151 MCS_NOFLAGS, &addr->message, pcre_gen_cmp_ctx)))
154 if (regex_match(re, path, -1, NULL))
156 uschar *fname = string_sprintf("%s/maildirfolder", path);
157 if (Ustat(fname, &statbuf) == 0)
159 DEBUG(D_transport) debug_printf("maildirfolder already exists\n");
163 int fd = Uopen(fname, O_WRONLY|O_APPEND|O_CREAT, 0600);
166 addr->message = string_sprintf("appendfile: failed to create "
167 "maildirfolder file in %s directory: %s", path, strerror(errno));
171 DEBUG(D_transport) debug_printf("created maildirfolder file\n");
176 DEBUG(D_transport) debug_printf("maildirfolder file not required\n");
180 return TRUE; /* Everything exists that should exist */
186 /*************************************************
187 * Update maildirsizefile for new file *
188 *************************************************/
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...
194 fd the open file descriptor
195 size the size of the message
201 maildir_record_length(int fd, int size)
205 sprintf(CS buffer, "%d 1\n", size);
206 len = Ustrlen(buffer);
207 if (lseek(fd, 0, SEEK_END) >= 0)
209 len = write(fd, buffer, len);
211 debug_printf("added '%.*s' to maildirsize file\n", len-1, buffer);
217 /*************************************************
218 * Find the size of a maildir *
219 *************************************************/
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.
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.
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
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
242 Returns: the sum of the sizes of the messages
246 maildir_compute_size(uschar *path, int *filecount, time_t *latest,
247 const pcre2_code *regex, const pcre2_code *dir_regex, BOOL timestamp_only)
252 if (!(dir = exim_opendir(path)))
255 for (struct dirent *ent; ent = readdir(dir); )
257 uschar * s, * name = US ent->d_name;
260 if (Ustrcmp(name, ".") == 0 || Ustrcmp(name, "..") == 0) continue;
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. */
266 if (dir_regex && !regex_match(dir_regex, name, -1, NULL))
269 debug_printf("skipping %s/%s: dir_regex does not match\n", path, name);
273 /* The name is OK; stat it. */
275 s = string_sprintf("%s/%s", path, name);
276 if (Ustat(s, &statbuf) < 0)
279 debug_printf("maildir_compute_size: stat error %d for %s: %s\n", errno,
284 if ((statbuf.st_mode & S_IFMT) != S_IFDIR)
287 debug_printf("skipping %s/%s: not a directory\n", s, name);
291 /* Keep the latest timestamp encountered */
293 if (statbuf.st_mtime > *latest) *latest = statbuf.st_mtime;
295 /* If this is a maildir folder, call this function recursively. */
298 sum += maildir_compute_size(s, filecount, latest, regex, dir_regex,
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. */
304 else if (!timestamp_only)
305 sum += check_dir_size(s, filecount, regex);
312 debug_printf("maildir_compute_size (timestamp_only): %ld\n",
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);
324 /*************************************************
325 * Create or update maildirsizefile *
326 *************************************************/
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.
332 The logic in this function follows the rules that are described in
334 http://www.inter7.com/courierimap/README.maildirquota.html
336 Or, at least, it is supposed to!
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
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
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)
358 off_t cached_quota = 0;
359 int cached_quota_filecount = 0;
364 uschar buffer[MAX_FILE_SIZE];
365 uschar *ptr = buffer;
368 /* Try a few times to open or create the file, in case another process is doing
371 filename = string_sprintf("%s/maildirsize", path);
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)
376 if (errno != ENOENT) return -1;
378 debug_printf("%s does not exist: recalculating\n", filename);
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. */
386 if ((count = read(fd, buffer, sizeof(buffer))) >= sizeof(buffer))
389 debug_printf("maildirsize file too big (%d): recalculating\n", count);
392 buffer[count] = 0; /* Ensure string terminated */
394 /* Read the quota parameters from the first line of the data. */
397 debug_printf("reading quota parameters from maildirsize data\n");
401 off_t n = (off_t)Ustrtod(ptr, &endptr);
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. */
407 if (*endptr == 'S') cached_quota = n;
408 else if (*endptr == 'C') cached_quota_filecount = (int)n;
409 if (!isalpha(*endptr++))
412 debug_printf("quota parameter number not followed by letter in "
413 "\"%.*s\": recalculating maildirsize\n", (int)(endptr - buffer),
417 if (*endptr == '\n' || *endptr == 0) break;
418 if (*endptr++ != ',')
421 debug_printf("quota parameter not followed by comma in "
422 "\"%.*s\": recalculating maildirsize\n", (int)(endptr - buffer),
429 /* Check the cached values against the current settings */
431 if (cached_quota != ob->quota_value ||
432 cached_quota_filecount != ob->quota_filecount_value)
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);
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'. */
446 debug_printf("computing maildir size from maildirsize data\n");
448 while (*endptr++ == '\n')
450 if (*endptr == 0) break;
453 size += (off_t)Ustrtod(ptr, &endptr);
454 if (*endptr != ' ') break;
456 filecount += Ustrtol(ptr, &endptr, 10);
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. */
468 if (size < 0 || filecount < 0)
470 DEBUG(D_transport) debug_printf("negative value in maildirsize "
471 "(size=" OFF_T_FMT " count=%d): recalculating\n", size, filecount);
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)
485 DEBUG(D_transport) debug_printf("over quota and maildirsize has "
486 "more than 1 entry: recalculating\n");
490 if (fstat(fd, &statbuf) < 0) goto RECALCULATE; /* Should never occur */
492 if (time(NULL) - statbuf.st_mtime > 15*60)
494 DEBUG(D_transport) debug_printf("over quota and maildirsize is older "
495 "than 15 minutes: recalculating\n");
502 /* If *endptr is not zero, there was a syntax error in the file. */
507 time_t old_latest, new_latest;
514 while (p > buffer && p[-1] != '\n') p--;
517 debug_printf("error in maildirsizefile: unexpected character %d in "
518 "line %d (starting '%s'): recalculating\n",
519 *endptr, linecount + 1, string_printing(p));
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. */
529 if (fd >= 0) (void)close(fd);
532 size = maildir_compute_size(path, &filecount, &old_latest, regex, dir_regex,
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);
539 fd = Uopen(tempname, O_RDWR|O_CREAT|O_EXCL, ob->mode ? ob->mode : 0600);
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)
552 /* If any of the directories have been modified since the last timestamp we
553 saw, we have to junk this maildirsize file. */
555 DEBUG(D_transport) debug_printf("checking subdirectory timestamps\n");
557 (void)maildir_compute_size(path, NULL, &new_latest , NULL, dir_regex, TRUE);
558 if (new_latest > old_latest)
560 DEBUG(D_transport) debug_printf("abandoning maildirsize because of "
561 "a later subdirectory modification\n");
562 (void)Uunlink(filename);
568 /* Return the sizes and the file descriptor, if any */
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;
577 /* End of tf_maildir.c */