File Coverage

blib/lib/Mail/Box/Maildir.pm
Criterion Covered Total %
statement 114 181 62.9
branch 20 70 28.5
condition 16 62 25.8
subroutine 16 23 69.5
pod 13 14 92.8
total 179 350 51.1


line stmt bran cond sub pod time code
1             # This code is part of Perl distribution Mail-Box version 4.01.
2             # The POD got stripped from this file by OODoc version 3.05.
3             # For contributors see file ChangeLog.
4              
5             # This software is copyright (c) 2001-2025 by Mark Overmeer.
6              
7             # This is free software; you can redistribute it and/or modify it under
8             # the same terms as the Perl 5 programming language system itself.
9             # SPDX-License-Identifier: Artistic-1.0-Perl OR GPL-1.0-or-later
10              
11              
12             package Mail::Box::Maildir;{
13             our $VERSION = '4.01';
14             }
15              
16 6     6   4485 use parent 'Mail::Box::Dir';
  6         14  
  6         48  
17              
18 6     6   466 use strict;
  6         14  
  6         178  
19 6     6   26 use warnings;
  6         15  
  6         457  
20              
21 6     6   41 use Log::Report 'mail-box', import => [ qw/__x error fault info trace/ ];
  6         13  
  6         43  
22              
23 6     6   4375 use Mail::Box::Maildir::Message;
  6         22  
  6         304  
24              
25 6     6   42 use File::Copy qw/move/;
  6         13  
  6         387  
26 6     6   58 use File::Basename qw/basename/;
  6         11  
  6         304  
27 6     6   37 use Sys::Hostname qw/hostname/;
  6         12  
  6         299  
28 6     6   3674 use File::Remove qw/remove/;
  6         15797  
  6         24331  
29              
30             # Maildir is only supported on UNIX, because the filenames probably
31             # do not work on other platforms. Since MailBox 2.052, the use of
32             # File::Spec to create filenames has been removed: benchmarks showed
33             # that catfile() consumed 20% of the time of a folder open(). And
34             # '/' file separators work on Windows too!
35              
36             #--------------------
37              
38             my $default_folder_dir = exists $ENV{HOME} ? "$ENV{HOME}/.maildir" : '.';
39              
40             sub init($)
41 7     7 0 103 { my ($self, $args) = @_;
42              
43 7 50 66     73 ! exists $args->{locker} && (! defined $args->{lock_type} || $args->{lock_type} eq 'NONE')
      66        
44             or error __x"no locking possible for maildir folders.";
45              
46 7         26 $args->{lock_type} = 'NONE';
47 7   66     53 $args->{folderdir} ||= $default_folder_dir;
48              
49 7         73 $self->SUPER::init($args);
50              
51 7 50       26 $self->acceptMessages if $args->{accept_new};
52 7         32 $self;
53             }
54              
55              
56             sub create($@)
57 0     0 1 0 { my ($thingy, $name, %args) = @_;
58 0   0     0 my $class = ref $thingy || $thingy;
59 0   0     0 my $folderdir = $args{folderdir} || $default_folder_dir;
60 0         0 my $directory = $class->folderToDirectory($name, $folderdir);
61              
62 0         0 $class->createDirs($directory);
63 0         0 trace "created folder Maildir $name.";
64 0         0 $directory;
65             }
66              
67             sub foundIn($@)
68 5     5 1 128 { my $class = shift;
69 5 50       65 my $name = @_ % 2 ? shift : undef;
70 5         33 my %args = @_;
71 5   66     41 my $folderdir = $args{folderdir} || $default_folder_dir;
72 5         68 my $directory = $class->folderToDirectory($name, $folderdir);
73              
74 5         162 -d "$directory/cur";
75             }
76              
77             sub type() { 'maildir' }
78              
79             sub listSubFolders(@)
80 0     0 1 0 { my ($class, %args) = @_;
81 0         0 my $dir;
82              
83 0 0       0 if(ref $class)
84 0         0 { $dir = $class->directory;
85 0         0 $class = ref $class;
86             }
87             else
88 0   0     0 { my $folder = $args{folder} || '=';
89 0   0     0 my $folderdir = $args{folderdir} || $default_folder_dir;
90 0         0 $dir = $class->folderToDirectory($folder, $folderdir);
91             }
92              
93 0   0     0 $args{skip_empty} ||= 0;
94 0   0     0 $args{check} ||= 0;
95              
96             # Read the directories from the directory, to find all folders
97             # stored here. Some directories have to be removed because they
98             # are created by all kinds of programs, but are no folders.
99              
100 0 0 0     0 -d $dir && opendir my $dh, $dir
101             or return ();
102              
103 0         0 my @dirs;
104 0         0 while(my $d = readdir $dh)
105 0 0       0 { next if $d =~ m/^(new|tmp|cur|\.\.?)$/;
106              
107 0         0 my $dir = "$dir/$d";
108 0 0       0 push @dirs, $d if -d $dir;
109             }
110              
111 0         0 closedir $dh;
112              
113             # Skip empty folders.
114              
115             @dirs = grep !$class->folderIsEmpty("$dir/$_"), @dirs
116 0 0       0 if $args{skip_empty};
117              
118             # Check if the files we want to return are really folders.
119              
120 0 0       0 @dirs = map { m/(.*)/ && $1 } @dirs; # untaint
  0         0  
121 0 0       0 $args{check} or return @dirs;
122              
123 0         0 grep $class->foundIn("$dir/$_"), @dirs;
124             }
125              
126             sub openSubFolder($@)
127 0     0 1 0 { my ($self, $name) = (shift, shift);
128 0         0 $self->createDirs($self->nameOfSubFolder($name));
129 0         0 $self->SUPER::openSubFolder($name, @_);
130             }
131              
132             sub topFolderWithMessages() { 1 }
133              
134             my $uniq = rand 1000;
135              
136              
137             sub coerce($)
138 2     2 1 6 { my ($self, $message) = (shift, shift);
139              
140 2         21 my $is_native = $message->isa('Mail::Box::Maildir::Message');
141 2         15 my $coerced = $self->SUPER::coerce($message, @_);
142              
143 2 50 33     115 my $basename = $is_native ? basename($message->filename)
144             : ($message->timestamp || time) .'.'. hostname .'.'. $uniq++;
145              
146 2         2030 my $dir = $self->directory;
147 2         24 $coerced->create("$dir/tmp/$basename");
148 2         12 my $new = $coerced->create("$dir/new/$basename");
149              
150 2         20 trace "Added Maildir message in $new";
151 2 50       90 $coerced->labelsToFilename unless $is_native;
152 2         8 $coerced;
153             }
154              
155             #--------------------
156              
157             sub createDirs($)
158 0     0 1 0 { my ($thing, $dir) = @_;
159              
160 0 0 0     0 -d $dir || mkdir $dir
161             or fault __x"cannot create Maildir folder directory {dir}", dir => $dir;
162              
163 0         0 foreach my $sub (qw/tmp new cur/)
164 0         0 { my $subdir = "$dir/$sub";
165 0 0 0     0 -d $subdir || mkdir $subdir
166             or fault __x"cannot create Maildir folder subdir {dir}", dir => $subdir;
167             }
168              
169 0         0 $thing;
170             }
171              
172              
173             sub folderIsEmpty($)
174 0     0 1 0 { my ($self, $dir) = @_;
175 0 0       0 return 1 unless -d $dir;
176              
177 0         0 foreach my $sub (qw/tmp new cur/)
178 0         0 { my $subdir = "$dir/$sub";
179 0 0       0 -d $subdir or next;
180              
181 0 0       0 opendir my $dh, $subdir or return 0;
182 0         0 my $first = readdir $dh;
183 0         0 closedir $dh;
184              
185 0 0       0 return 0 if defined $first;
186             }
187              
188 0 0       0 opendir my $dh, $dir or return 1;
189 0         0 while(my $entry = readdir $dh)
190 0 0       0 { next if $entry =~ m/^(?:tmp|cur|new|bulletin(?:time|lock)|seriallock|\..?)$/;
191 0         0 closedir $dh;
192 0         0 return 0;
193             }
194              
195 0         0 closedir $dh;
196 0         0 1;
197             }
198              
199             sub delete(@)
200 0     0 1 0 { my $self = shift;
201              
202             # Subfolders are not nested in the directory structure
203 0         0 remove \1, $self->directory;
204             }
205              
206             sub readMessageFilenames
207 14     14 1 50 { my ($self, $dirname) = @_;
208              
209 14 50       827 opendir my $dh, $dirname or return ();
210              
211 14         61 my @files;
212 14 50       66 if(${^TAINT})
213             { # unsorted list of untainted filenames.
214 0 0 0     0 @files = map +(m/^([0-9][\w.:,=\-]+)$/ && -f "$dirname/$1" ? $1 : ()), readdir $dh;
215             }
216             else
217             { # not running tainted
218 14   66     4503 @files = grep m/^([0-9][\w.:,=\-]+)$/ && -f "$dirname/$1", readdir $dh;
219             }
220 14         241 closedir $dh;
221              
222             # Sort the names. Solve the Y2K (actually the 1 billion seconds
223             # since 1970 bug) which hunts Maildir. The timestamp, which is
224             # the start of the filename will have some 0's in front, so each
225             # timestamp has the same length.
226              
227 14         31 my %unified;
228             m/^(\d+)/ and $unified{ ('0' x (10-length($1))).$_ } = $_
229 14   33     993 for @files;
230              
231 14         751 map "$dirname/$unified{$_}", sort keys %unified;
232             }
233              
234             sub readMessages(@)
235 7     7 1 61 { my ($self, %args) = @_;
236              
237 7         30 my $directory = $self->directory;
238 7 50       173 -d $directory or return;
239              
240             #
241             # Read all messages
242             #
243              
244 7         21 my $curdir = "$directory/cur";
245 7         35 my @cur = map +[$_, 1], $self->readMessageFilenames($curdir);
246              
247 7         64 my $newdir = "$directory/new";
248 7         31 my @new = map +[$_, 0], $self->readMessageFilenames($newdir);
249              
250 7         30 foreach (@cur, @new)
251 303         927 { my ($filename, $accepted) = @$_;
252             my $message = $args{message_type}->new(
253             head => $args{head_delayed_type}->new,
254 303         1346 filename => $filename,
255             folder => $self,
256             fix_header=> $self->fixHeaders,
257             labels => [ accepted => $accepted ],
258             );
259              
260 303         1991 my $body = $args{body_delayed_type}->new(message => $message);
261 303 50       972 $message->storeBody($body) if $body;
262 303         1435 $self->storeMessage($message);
263             }
264              
265 7         164 $self;
266             }
267              
268              
269             sub acceptMessages($)
270 0     0 1 0 { my ($self, %args) = @_;
271 0         0 my @accept = $self->messages('!accepted');
272 0         0 $_->accept foreach @accept;
273 0         0 @accept;
274             }
275              
276              
277             sub writeMessages($)
278 5     5 1 15 { my ($self, $args) = @_;
279              
280             # Write each message. Two things complicate life:
281             # 1 - we may have a huge folder, which should not be on disk twice
282             # 2 - we may have to replace a message, but it is unacceptable
283             # to remove the original before we are sure that the new version
284             # is on disk.
285              
286 5         13 my $writer = 0;
287              
288 5         34 my $directory = $self->directory;
289 5         11 my @messages = @{$args->{messages}};
  5         32  
290              
291 5         16 my $tmpdir = "$directory/tmp";
292 5 50 33     151 -d $tmpdir || mkdir $tmpdir
293             or fault __x"cannot create directory {dir}", dir => $tmpdir;
294              
295 5         19 foreach my $message (@messages)
296 192 100       366 { $message->isModified or next;
297              
298 1         10 my $filename = $message->filename;
299 1         74 my $basename = basename $filename;
300              
301 1         4 my $newtmp = "$directory/tmp/$basename";
302 1 50       163 open my $new, '>:raw', $newtmp
303             or fault __x"cannot create file {file}", file => $newtmp;
304              
305 1         8 $message->write($new);
306 1         425 close $new;
307              
308 1         117 unlink $filename;
309 1 50       7 move $newtmp, $filename
310             or fault __x"cannot rename {from} to {to}", from => $newtmp, to => $filename;
311             }
312              
313             # Remove an empty folder. This is done last, because the code before
314             # in this method will have cleared the contents of the directory.
315              
316 5 50 33     236 if(!@messages && $self->removeEmpty)
317             { # If something is still in the directory, this will fail, but I
318             # don't mind.
319 0         0 rmdir "$directory/cur";
320 0         0 rmdir "$directory/tmp";
321 0         0 rmdir "$directory/new";
322 0         0 rmdir $directory;
323             }
324              
325 5         25 $self;
326             }
327              
328              
329             sub appendMessages(@)
330 1     1 1 2 { my $class = shift;
331 1         7 my %args = @_;
332              
333             my @messages
334             = exists $args{message} ? $args{message}
335 1 50       59 : exists $args{messages} ? @{$args{messages}}
  1 50       5  
336             : return ();
337              
338 1         9 my $self = $class->new(@_, access => 'a');
339 1         5 my $directory= $self->directory;
340 1 50       30 -d $directory or return;
341              
342 1         4 my $tmpdir = "$directory/tmp";
343 1 50 33     13 -d $tmpdir || mkdir $tmpdir
344             or fault __x"cannot create directory {dir}", dir => $tmpdir;
345              
346 1   50     9 my $msgtype = $args{message_type} || 'Mail::Box::Maildir::Message';
347              
348 1         15 foreach my $message (@messages)
349 1         58 { my $is_native = $message->isa($msgtype);
350 1         4 my ($basename, $coerced);
351              
352 1 50       5 if($is_native)
353 1         2 { $coerced = $message;
354 1         15 $basename = basename $message->filename;
355             }
356             else
357 0         0 { $coerced = $self->SUPER::coerce($message);
358 0   0     0 $basename = ($message->timestamp||time).'.'. hostname.'.'.$uniq++;
359             }
360              
361 1         7 my $dir = $self->directory;
362 1         7 $coerced->create("$dir/tmp/$basename");
363 1         6 my $new = $coerced->create("$dir/new/$basename");
364 1         11 trace "Appended Maildir message in $new";
365             }
366              
367 1         54 $self->close;
368 1         16 @messages;
369             }
370              
371             #--------------------
372              
373             1;