# Copyrights 2001-2020 by [Mark Overmeer]. # For other contributors see ChangeLog. # See the manual pages for details on the licensing terms. # Pod stripped from pm file by OODoc 2.02. # This code is part of distribution Mail-Box. Meta-POD processed with # OODoc into POD and HTML manual-pages. See README.md # Copyright Mark Overmeer. Licensed under the same terms as Perl itself. package Mail::Box::Maildir; use vars '$VERSION'; $VERSION = '3.009'; use base 'Mail::Box::Dir'; use strict; use warnings; use filetest 'access'; use Mail::Box::Maildir::Message; use Carp; use File::Copy 'move'; use File::Basename 'basename'; use Sys::Hostname 'hostname'; use File::Remove 'remove'; # Maildir is only supported on UNIX, because the filenames probably # do not work on other platforms. Since MailBox 2.052, the use of # File::Spec to create filenames has been removed: benchmarks showed # that catfile() consumed 20% of the time of a folder open(). And # '/' file separators work on Windows too! my $default_folder_dir = exists $ENV{HOME} ? "$ENV{HOME}/.maildir" : '.'; sub init($) { my ($self, $args) = @_; croak "No locking possible for maildir folders." if exists $args->{locker} || (defined $args->{lock_type} && $args->{lock_type} ne 'NONE'); $args->{lock_type} = 'NONE'; $args->{folderdir} ||= $default_folder_dir; return undef unless $self->SUPER::init($args); $self->acceptMessages if $args->{accept_new}; $self; } sub create($@) { my ($thingy, $name, %args) = @_; my $class = ref $thingy || $thingy; my $folderdir = $args{folderdir} || $default_folder_dir; my $directory = $class->folderToDirectory($name, $folderdir); if($class->createDirs($directory)) { $class->log(PROGRESS => "Created folder Maildir $name."); return $class; } else { $class->log(ERROR => "Cannot create Maildir folder $name."); return undef; } } sub foundIn($@) { my $class = shift; my $name = @_ % 2 ? shift : undef; my %args = @_; my $folderdir = $args{folderdir} || $default_folder_dir; my $directory = $class->folderToDirectory($name, $folderdir); -d "$directory/cur"; } sub type() {'maildir'} sub listSubFolders(@) { my ($class, %args) = @_; my $dir; if(ref $class) { $dir = $class->directory; $class = ref $class; } else { my $folder = $args{folder} || '='; my $folderdir = $args{folderdir} || $default_folder_dir; $dir = $class->folderToDirectory($folder, $folderdir); } $args{skip_empty} ||= 0; $args{check} ||= 0; # Read the directories from the directory, to find all folders # stored here. Some directories have to be removed because they # are created by all kinds of programs, but are no folders. return () unless -d $dir && opendir DIR, $dir; my @dirs; while(my $d = readdir DIR) { next if $d =~ m/^(new|tmp|cur|\.\.?)$/; my $dir = "$dir/$d"; push @dirs, $d if -d $dir && -r _; } closedir DIR; # Skip empty folders. @dirs = grep {!$class->folderIsEmpty("$dir/$_")} @dirs if $args{skip_empty}; # Check if the files we want to return are really folders. @dirs = map { m/(.*)/ && $1 } @dirs; # untaint return @dirs unless $args{check}; grep { $class->foundIn("$dir/$_") } @dirs; } sub openSubFolder($@) { my ($self, $name) = (shift, shift); $self->createDirs($self->nameOfSubFolder($name)); $self->SUPER::openSubFolder($name, @_); } sub topFolderWithMessages() { 1 } my $uniq = rand 1000; sub coerce($) { my ($self, $message) = (shift, shift); my $is_native = $message->isa('Mail::Box::Maildir::Message'); my $coerced = $self->SUPER::coerce($message, @_); my $basename = $is_native ? basename($message->filename) : ($message->timestamp || time) .'.'. hostname .'.'. $uniq++; my $dir = $self->directory; my $tmp = "$dir/tmp/$basename"; my $new = "$dir/new/$basename"; if($coerced->create($tmp) && $coerced->create($new)) {$self->log(PROGRESS => "Added Maildir message in $new") } else {$self->log(ERROR => "Cannot create Maildir message file $new.") } $coerced->labelsToFilename unless $is_native; $coerced; } #------------------------------------------- sub createDirs($) { my ($thing, $dir) = @_; $thing->log(ERROR => "Cannot create Maildir folder directory $dir: $!"), return unless -d $dir || mkdir $dir; my $tmp = "$dir/tmp"; $thing->log(ERROR => "Cannot create Maildir folder subdir $tmp: $!"), return unless -d $tmp || mkdir $tmp; my $new = "$dir/new"; $thing->log(ERROR => "Cannot create Maildir folder subdir $new: $!"), return unless -d $new || mkdir $new; my $cur = "$dir/cur"; $thing->log(ERROR => "Cannot create Maildir folder subdir $cur: $!"), return unless -d $cur || mkdir $cur; $thing; } sub folderIsEmpty($) { my ($self, $dir) = @_; return 1 unless -d $dir; foreach (qw/tmp new cur/) { my $subdir = "$dir/$_"; next unless -d $subdir; opendir DIR, $subdir or return 0; my $first = readdir DIR; closedir DIR; return 0 if defined $first; } opendir DIR, $dir or return 1; while(my $entry = readdir DIR) { next if $entry =~ m/^(?:tmp|cur|new|bulletin(?:time|lock)|seriallock|\..?)$/; closedir DIR; return 0; } closedir DIR; 1; } sub delete(@) { my $self = shift; # Subfolders are not nested in the directory structure remove \1, $self->directory; } sub readMessageFilenames { my ($self, $dirname) = @_; opendir DIR, $dirname or return (); my @files; if(${^TAINT}) { # unsorted list of untainted filenames. @files = map { m/^([0-9][\w.:,=\-]+)$/ && -f "$dirname/$1" ? $1 : () } readdir DIR; } else { # not running tainted @files = grep /^([0-9][\w.:,=\-]+)$/ && -f "$dirname/$1", readdir DIR; } closedir DIR; # Sort the names. Solve the Y2K (actually the 1 billion seconds # since 1970 bug) which hunts Maildir. The timestamp, which is # the start of the filename will have some 0's in front, so each # timestamp has the same length. my %unified; m/^(\d+)/ and $unified{ ('0' x (10-length($1))).$_ } = $_ for @files; map "$dirname/$unified{$_}", sort keys %unified; } sub readMessages(@) { my ($self, %args) = @_; my $directory = $self->directory; return unless -d $directory; # # Read all messages # my $curdir = "$directory/cur"; my @cur = map +[$_, 1], $self->readMessageFilenames($curdir); my $newdir = "$directory/new"; my @new = map +[$_, 0], $self->readMessageFilenames($newdir); my @log = $self->logSettings; foreach (@cur, @new) { my ($filename, $accepted) = @$_; my $message = $args{message_type}->new ( head => $args{head_delayed_type}->new(@log) , filename => $filename , folder => $self , fix_header=> $self->{MB_fix_headers} , labels => [ accepted => $accepted ] ); my $body = $args{body_delayed_type}->new(@log, message => $message); $message->storeBody($body) if $body; $self->storeMessage($message); } $self; } sub acceptMessages($) { my ($self, %args) = @_; my @accept = $self->messages('!accepted'); $_->accept foreach @accept; @accept; } sub writeMessages($) { my ($self, $args) = @_; # Write each message. Two things complicate life: # 1 - we may have a huge folder, which should not be on disk twice # 2 - we may have to replace a message, but it is unacceptable # to remove the original before we are sure that the new version # is on disk. my $writer = 0; my $directory = $self->directory; my @messages = @{$args->{messages}}; my $tmpdir = "$directory/tmp"; die "Cannot create directory $tmpdir: $!" unless -d $tmpdir || mkdir $tmpdir; foreach my $message (@messages) { next unless $message->isModified; my $filename = $message->filename; my $basename = basename $filename; my $newtmp = "$directory/tmp/$basename"; open my $new, '>', $newtmp or croak "Cannot create file $newtmp: $!"; $message->write($new); close $new; unlink $filename; move $newtmp, $filename or warn "Cannot move $newtmp to $filename: $!\n"; } # Remove an empty folder. This is done last, because the code before # in this method will have cleared the contents of the directory. if(!@messages && $self->{MB_remove_empty}) { # If something is still in the directory, this will fail, but I # don't mind. rmdir "$directory/cur"; rmdir "$directory/tmp"; rmdir "$directory/new"; rmdir $directory; } $self; } sub appendMessages(@) { my $class = shift; my %args = @_; my @messages = exists $args{message} ? $args{message} : exists $args{messages} ? @{$args{messages}} : return (); my $self = $class->new(@_, access => 'a'); my $directory= $self->directory; return unless -d $directory; my $tmpdir = "$directory/tmp"; croak "Cannot create directory $tmpdir: $!", return unless -d $tmpdir || mkdir $tmpdir; my $msgtype = $args{message_type} || 'Mail::Box::Maildir::Message'; foreach my $message (@messages) { my $is_native = $message->isa($msgtype); my ($basename, $coerced); if($is_native) { $coerced = $message; $basename = basename $message->filename; } else { $coerced = $self->SUPER::coerce($message); $basename = ($message->timestamp||time).'.'. hostname.'.'.$uniq++; } my $dir = $self->directory; my $tmp = "$dir/tmp/$basename"; my $new = "$dir/new/$basename"; if($coerced->create($tmp) && $coerced->create($new)) {$self->log(PROGRESS => "Appended Maildir message in $new") } else {$self->log(ERROR => "Cannot append Maildir message in $new to folder $self.") } } $self->close; @messages; } #------------------------------------------- 1;