File Coverage

blib/lib/Mail/Dir.pm
Criterion Covered Total %
statement 24 153 15.6
branch 0 90 0.0
condition 0 3 0.0
subroutine 8 23 34.7
pod 8 14 57.1
total 40 283 14.1


line stmt bran cond sub pod time code
1             # Copyright (c) 2016 cPanel, Inc.
2             # All rights reserved.
3             # http://cpanel.net/
4             #
5             # Distributed under the terms of the MIT license. See the LICENSE file for
6             # further details.
7              
8             package Mail::Dir;
9              
10 1     1   27042 use strict;
  1         2  
  1         27  
11 1     1   16 use warnings;
  1         2  
  1         35  
12              
13 1     1   9 use Errno;
  1         5  
  1         36  
14 1     1   5 use IO::Handle;
  1         2  
  1         36  
15              
16 1     1   4 use Cwd ();
  1         2  
  1         14  
17 1     1   4 use Fcntl ();
  1         2  
  1         13  
18 1     1   761 use Sys::Hostname ();
  1         1169  
  1         21  
19              
20 1     1   566 use Mail::Dir::Message ();
  1         3  
  1         2105  
21              
22             our $VERSION = '0.03';
23              
24             my $MAX_BUFFER_LEN = 4096;
25             my $MAX_TMP_LAST_ACCESS = 129600;
26             my $DEFAULT_MAILBOX = 'INBOX';
27              
28             =encoding utf8
29              
30             =head1 NAME
31              
32             Mail::Dir - Compliant Maildir and Maildir++ delivery mechanism
33              
34             =head1 SYNOPSIS
35              
36             use Mail::Dir;
37              
38             my $maildir = Mail::Dir->open("$ENV{'HOME'}/Maildir");
39              
40             $maildir->deliver('somefile.msg');
41              
42             #
43             # Create a new Maildir++ mailbox with sub-mailboxes
44             #
45             my $maildirPP = Mail::Dir->open("$ENV{'HOME'}/newmaildir",
46             'maildir++' => 1,
47             'create' => 1
48             );
49              
50             $maildirPP->create_mailbox('INBOX.foo');
51             $maildirPP->create_mailbox('INBOX.foo.bar');
52             $maildirPP->select_mailbox('INBOX.foo.bar');
53              
54             $maildirPP->deliver(\*STDIN);
55              
56             =head1 DESCRIPTION
57              
58             C provides a straightforward mechanism for delivering mail messages
59             to a Maildir or Maildir++ mailbox.
60              
61             =cut
62              
63             sub dirs {
64 0     0 0   my ($dir) = @_;
65              
66             return (
67 0           'dir' => $dir,
68             'tmp' => "$dir/tmp",
69             'new' => "$dir/new",
70             'cur' => "$dir/cur"
71             );
72             }
73              
74             =head1 OPENING OR CREATING A MAILBOX
75              
76             =over
77              
78             =item Copen(I<$dir>, I<%opts>)>
79              
80             Open or create a mailbox, in a manner dependent on the flags specified in
81             I<%opts>, and returns an object representing the Maildir structure.
82              
83             Recognized option flags are:
84              
85             =over
86              
87             =item * C
88              
89             When specified, create a Maildir inbox at I<$dir> if one does not already
90             exist.
91              
92             =item * C
93              
94             When specified, enable management and usage of Maildir++ sub-mailboxes.
95              
96             =back
97              
98             =back
99              
100             =cut
101              
102             sub open {
103 0     0 1   my ( $class, $dir, %opts ) = @_;
104              
105 0 0         die('No Maildir path specified') unless $dir;
106              
107 0           my %dirs = dirs($dir);
108              
109 0           foreach my $key (qw(dir tmp new cur)) {
110 0           my $dir = $dirs{$key};
111              
112 0 0         if ( $opts{'create'} ) {
113 0 0         unless ( -d $dir ) {
114 0 0         mkdir($dir) or die("Unable to mkdir() $dir: $!");
115             }
116             }
117             else {
118 0 0         die("$dir: Not a directory") unless -d $dir;
119             }
120             }
121              
122 0           my $hostname = Sys::Hostname::hostname();
123              
124             return bless {
125             'dir' => $dir,
126 0 0         'maildir++' => $opts{'maildir++'} ? 1 : 0,
127             'hostname' => $hostname,
128             'mailbox' => $DEFAULT_MAILBOX,
129             'deliveries' => 0
130             }, $class;
131             }
132              
133             sub validate_mailbox_name {
134 0     0 0   my ($mailbox) = @_;
135              
136 0 0         my @components = split( /\./, $mailbox ) or die("Invalid mailbox name $mailbox");
137              
138 0           my $first = $components[0];
139              
140 0 0         if ( $first =~ /^\~/ ) {
141 0           die("Invalid mailbox name $mailbox: Name cannot start with a tilde");
142             }
143              
144 0           foreach my $component (@components) {
145 0 0         die("Invalid mailbox name $mailbox: Name cannot contain '..'") if $component eq '';
146 0 0         die("Invalid mailbox name $mailbox: Name cannot contain '/'") if $component =~ /\//;
147             }
148              
149 0           return;
150             }
151              
152             sub mailbox_dir {
153 0     0 0   my ( $self, $mailbox ) = @_;
154              
155 0   0       $mailbox ||= $self->mailbox;
156              
157 0           validate_mailbox_name($mailbox);
158              
159 0 0         if ( $mailbox eq $DEFAULT_MAILBOX ) {
160 0           return $self->{'dir'};
161             }
162              
163 0           my @components = split /\./, $mailbox;
164              
165 0           my $subdir = join( '.', @components );
166              
167 0           return "$self->{'dir'}/.$subdir";
168             }
169              
170             =head1 MANIPULATING MAILBOXES
171              
172             The following methods require Maildir++ extensions to be enabled.
173              
174             =over
175              
176             =item C<$maildir-Eselect_mailbox(I<$mailbox>)>
177              
178             Change the current mailbox to which mail is delivered, to I<$mailbox>.
179              
180             =cut
181              
182             sub select_mailbox {
183 0     0 1   my ( $self, $mailbox ) = @_;
184              
185 0 0         die('Maildir++ extensions not enabled') unless $self->{'maildir++'};
186              
187 0           validate_mailbox_name($mailbox);
188              
189 0 0         die('Mailbox does not exist') unless -d $self->mailbox_dir($mailbox);
190              
191 0           return $self->{'mailbox'} = $mailbox;
192             }
193              
194             =item C<$maildir-Emailbox()>
195              
196             Returns the name of the currently selected mailbox.
197              
198             =cut
199              
200             sub mailbox {
201 0     0 1   my ($self) = @_;
202              
203 0           return $self->{'mailbox'};
204             }
205              
206             =item C<$maildir-Emailbox_exists(I<$mailbox>)>
207              
208             Returns true if I<$mailbox> exists.
209              
210             =cut
211              
212             sub mailbox_exists {
213 0     0 1   my ( $self, $mailbox ) = @_;
214              
215 0           return -d $self->mailbox_dir($mailbox);
216             }
217              
218             sub parent_mailbox {
219 0     0 0   my ($mailbox) = @_;
220              
221 0           my @components = split /\./, $mailbox;
222 0 0         pop @components if @components;
223              
224 0           return join( '.', @components );
225             }
226              
227             =item C<$maildir-Ecreate_mailbox(I<$mailbox>)>
228              
229             Create the new I<$mailbox> if it does not already exist. Will throw an error
230             if the parent mailbox does not already exist.
231              
232             =back
233              
234             =cut
235              
236             sub create_mailbox {
237 0     0 1   my ( $self, $mailbox ) = @_;
238              
239 0 0         die('Maildir++ extensions not enabled') unless $self->{'maildir++'};
240 0 0         die('Parent mailbox does not exist') unless $self->mailbox_exists( parent_mailbox($mailbox) );
241              
242 0           my %dirs = dirs( $self->mailbox_dir($mailbox) );
243              
244 0           foreach my $key (qw(dir tmp new cur)) {
245 0           my $dir = $dirs{$key};
246              
247 0 0         mkdir($dir) or die("Unable to mkdir() $dir: $!");
248             }
249              
250 0           return 1;
251             }
252              
253             sub name {
254 0     0 0   my ( $self, %args ) = @_;
255              
256 0 0         my $from = $args{'from'} or die('No message file, handle or source subroutine specified');
257 0 0         my $time = $args{'time'} ? $args{'time'} : time();
258              
259 0           my $name = sprintf( "%d.P%dQ%d.%s", $time, $$, $self->{'deliveries'}, $self->{'hostname'} );
260              
261 0 0         if ( $self->{'maildir++'} ) {
262 0           my $size;
263              
264 0 0         if ( defined $args{'size'} ) {
    0          
265 0           $size = $args{'size'};
266             }
267             elsif ( !ref($from) ) {
268 0 0         my @st = stat($from) or die("Unable to stat() $from: $!");
269 0           $size = $st[7];
270             }
271              
272 0 0         if ( defined $size ) {
273 0           $name .= sprintf( ",S=%d", $size );
274             }
275             }
276              
277 0           return $name;
278             }
279              
280             sub spool {
281 0     0 0   my ( $self, %args ) = @_;
282              
283 0           my $size = 0;
284              
285 0           my $from = $args{'from'};
286 0           my $to = $args{'to'};
287              
288 0 0         sysopen( my $fh_to, $to, &Fcntl::O_CREAT | &Fcntl::O_WRONLY ) or die("Unable to open $to for writing: $!");
289              
290 0 0         if ( ref($from) eq 'CODE' ) {
291 0           $from->($fh_to);
292              
293 0           $fh_to->flush;
294 0           $fh_to->sync;
295              
296 0           $size = tell $fh_to;
297             }
298             else {
299 0           my $fh_from;
300              
301 0 0         if ( ref($from) eq 'GLOB' ) {
    0          
302 0           $fh_from = $from;
303             }
304             elsif ( ref($from) eq '' ) {
305 0 0         sysopen( $fh_from, $from, &Fcntl::O_RDONLY ) or die("Unable to open $from for reading: $!");
306             }
307              
308 0           while ( my $len = $fh_from->read( my $buf, $MAX_BUFFER_LEN ) ) {
309 0           $size += syswrite( $fh_to, $buf, $len );
310              
311 0           $fh_to->flush;
312 0           $fh_to->sync;
313             }
314              
315 0 0         close $fh_from unless ref($from) eq 'GLOB';
316             }
317              
318 0           close $fh_to;
319              
320 0           return $size;
321             }
322              
323             =head1 DELIVERING MESSAGES
324              
325             =over
326              
327             =item C<$maildir-Edeliver(I<$from>)>
328              
329             Deliver a piece of mail from the source indicated by I<$from>. The following
330             types of values can be specified in I<$from>:
331              
332             =over
333              
334             =item * A C reference
335              
336             When passed a C reference, the subroutine specified in I<$from> is called,
337             with a file handle passed that the subroutine may write mail data to.
338              
339             =item * A file handle
340              
341             The file handle passed in I<$from> is read until end-of-file condition is
342             reached, and spooled to a new message in the current mailbox.
343              
344             =item * A filename
345              
346             The message at the filename indicated by I<$from> is spooled into the current
347             mailbox.
348              
349             =back
350              
351             =cut
352              
353             sub deliver {
354 0     0 1   my ( $self, $from ) = @_;
355              
356 0 0         die('No message source provided') unless defined $from;
357              
358 0 0         my $oldcwd = Cwd::getcwd() or die("Unable to getcwd(): $!");
359 0           my $dir = $self->mailbox_dir;
360 0           my $time = time();
361              
362 0           my $name = $self->name(
363             'from' => $from,
364             'time' => $time
365             );
366              
367 0 0         chdir($dir) or die("Unable to chdir() to $dir: $!");
368              
369 0           my $file_tmp = "tmp/$name";
370              
371 0 0         return if -e $file_tmp;
372              
373 0           my $size = $self->spool(
374             'from' => $from,
375             'to' => $file_tmp
376             );
377              
378 0           my $name_new = $self->name(
379             'from' => $file_tmp,
380             'time' => $time,
381             'size' => $size
382             );
383              
384 0           my $file_new = "new/$name_new";
385              
386 0 0         unless ( rename( $file_tmp => $file_new ) ) {
387 0           die("Unable to deliver incoming message to $file_new: $!");
388             }
389              
390 0 0         my @st = stat($file_new) or die("Unable to stat() $file_new: $!");
391              
392 0 0         chdir($oldcwd) or die("Unable to chdir() to $oldcwd: $!");
393              
394 0           $self->{'deliveries'}++;
395              
396             return Mail::Dir::Message->from_file(
397             'maildir' => $self,
398 0           'mailbox' => $self->{'mailbox'},
399             'dir' => 'new',
400             'file' => "$dir/$file_new",
401             'name' => $name_new,
402             'st' => \@st
403             );
404             }
405              
406             =back
407              
408             =head1 RETRIEVING MESSAGES
409              
410             =over
411              
412             =item C<$maildir-Emessages(I<%opts>)>
413              
414             Return a list of L references containing mail messages as
415             selected by the criteria specified in I<%opts>. Options include:
416              
417             =over
418              
419             =item * C, C, C
420              
421             When any of these are set to 1, messages in those queues are processed.
422              
423             =item * C
424              
425             A subroutine can be passed via C reference which filters for messages
426             that are desired. Each L object is passed to the
427             subroutine as its sole argument, and is kept if the subroutine returns 1.
428              
429             =back
430              
431             =back
432              
433             =cut
434              
435             sub messages {
436 0     0 1   my ( $self, %opts ) = @_;
437 0           my $dir = $self->mailbox_dir;
438              
439 0           my @ret;
440              
441 0           foreach my $key (qw(tmp new cur)) {
442 0 0         next unless $opts{$key};
443              
444 0           my $path = "$dir/$key";
445              
446 0 0         opendir( my $dh, $path ) or die("Unable to opendir() $path: $!");
447              
448 0           while ( my $item = readdir($dh) ) {
449 0 0         next if $item =~ /^\./;
450              
451 0           my $file = "$path/$item";
452 0 0         my @st = stat($file) or die("Unable to stat() $file: $!");
453              
454             my $message = Mail::Dir::Message->from_file(
455             'maildir' => $self,
456 0           'mailbox' => $self->{'mailbox'},
457             'dir' => $key,
458             'file' => $file,
459             'name' => $item,
460             'st' => \@st
461             );
462              
463 0 0         if ( defined $opts{'filter'} ) {
464 0 0         next unless $opts{'filter'}->($message);
465             }
466              
467 0           push @ret, $message;
468             }
469              
470 0           closedir $dh;
471             }
472              
473 0           return \@ret;
474             }
475              
476             =head1 PURGING EXPIRED MESSAGES
477              
478             =over
479              
480             =item C<$maildir-Epurge()>
481              
482             Purge all messages in the C queue that have not been accessed for the past
483             36 hours.
484              
485             =back
486              
487             =cut
488              
489             sub purge {
490 0     0 1   my ($self) = @_;
491 0           my $time = time();
492              
493             my $messages = $self->messages(
494             'tmp' => 1,
495             'filter' => sub {
496 0     0     my ($message) = @_;
497              
498 0 0         return ( $time - $message->{'atime'} > $MAX_TMP_LAST_ACCESS ) ? 1 : 0;
499             }
500 0           );
501              
502 0           foreach my $message ( @{$messages} ) {
  0            
503 0 0         unlink( $message->{'file'} ) or die("Unable to unlink() $message->{'file'}: $!");
504             }
505              
506 0           return $messages;
507             }
508              
509             1;
510              
511             __END__