From c4c0a743a968d0ec08b916cdd45005d5089de6d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=20Kl=C3=A4rner?= Date: Wed, 9 Oct 2024 20:28:11 +0200 Subject: [PATCH] Maildir: avoid race conditions when email are deleted while reading As the list of email is generated before all emails can be iterated, there is always a chance that another program accesses the Maildir and decides to delete a file. In my use case we iterate over a directory scanning for the most recently received mail, and have a daily cleanup job remove older mails. Expectedly whenever the cleanup job runs our script fails to complete, since the script needs to scan and parse 30k mails, and the cleanup job only needs to delete files (so it always overtakes the scanning script). The solution is to ignore only "No such file or directory" errors when opening a mail, and if this error occurs directly go to the next message. All other errors are reported back to the user as before. I've considered the following alternatives: - read all emails into memory directly, before even parsing them => possibly too much mail to sensibly keep in memory - only one call of readdir() per next_message() call => already POSIX.1-2024 suggests in the description of readdir and opendir that there are no guarantees if files created since the opendir or last rewinddir are returned at all, or files skipped that have been deleted. Conversely they make the remark, that applications conventionally have requested buffers with more than one directory entry, so no matter what we do in perl code, the libraries will already have a cache filled with possibly outdated files. Hence I deemed both alternatives non-feasible. --- lib/Email/Folder/Maildir.pm | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/Email/Folder/Maildir.pm b/lib/Email/Folder/Maildir.pm index 887203a..8523cca 100644 --- a/lib/Email/Folder/Maildir.pm +++ b/lib/Email/Folder/Maildir.pm @@ -50,8 +50,15 @@ sub next_message { my $what = $self->{_messages} || $self->_what_is_there; my $file = shift @$what or return; + local *FILE; - open FILE, $file or croak "couldn't open '$file' for reading"; + $! = 0; # reset ERRNO to ensure we only exactly test what we intend to below + unless( open FILE, $file ){ + # if the file no longer exists, hand over to the next file + return $self->next_message() if $!{ENOENT}; + # else complain to the user + croak "couldn't open '$file' for reading: $!"; + } join '', ; }