Maildir::Lite - A very simple implementation of Maildir


Maildir-Lite documentation Contained in the Maildir-Lite distribution.

Index


Code Index:

NAME

Top

Maildir::Lite - A very simple implementation of Maildir

SYNOPSIS

Top

Write to a file handle:

   my $mdir=Maildir::Lite->new(dir=>'/home/d/.maildir');
   ...
   # write messages
   my ($fh,$status)=$mdir->creat_message();
   die "creat_message failed" if $status;

   print $fh "Content-Type: text/plain\n"
             ."Date: $date\n"
             ."From: $from\n"
             ."To: $to\n"
             ."Subject: $subject\n\n"
             ."$message";

   die "delivery failed!\n" if $mdir->deliver_message($fh);

Write string and deliver message directly:

   my $status=$mdir->creat_message($email_content);
   die "creat_message failed" if $status;

Read new messages given a file handle:

   my ($fh,$status)=$mdir->get_next_message("new");
   unless($status) {
      while(<$fh>) { # read message
         ...
      }
   }
   $mdir->act($fh,'S'); # flag message as seen and move to cur

Read new messages into an array and flag message as seen while moving it to cur:

   my ($fh,$status)=$mdir->get_next_message("new",\@lines,'S');

DESCRIPTION

Top

This is a simple and very light implementation of Maildir as specified by D. J. Bernstein at http://cr.yp.to/proto/maildir.html

This module provide the user with a simple interface to reading and writing email messages to maildir folders. Some additional useful features are also supported (e.g. support for additional subdirecties and user defined actions for the maildir flags).

Methods

new

   my $maildir = Maildir::Lite->new();

   my $maildir = Maildir::Lite->new(create=>1,
      dir=>'.maildir/', mode=>0750, sort=>'asc');




* create - if set to 0, the directory and the subdirectories will not be created and are assumed to exist.
* dir - the maildir directory; it defaults to ~/.maildir (if $ENV{HOME} exits).
* mode - the (default 0750) directory permissions of dir and sub-directories.
* uniq - set unique integer which will be otherwise randomly generated for filennames; it is important that uniq is actually unique.
* sort - the read messege sorting method. See sort.

add_action($folder,$flag,$action)

Add a specific $action (function or 'close') to $folder for the $flag flag.

For example, if you wish to move files from new to trash when given the flag 'T' (or 'trash'):

   $mdir->add_action('new','trash',\&new_to_trash);

Specifiying 'close' closes the file, without appending the info or moving the file.

The default action for folder new is to move it to cur and append the flag 'S' flag. Reading messages from cur or tmp by default only closes the file.

Returns 0 upon success, -1 otherwise.

Example of action function:

   sub new_to_trash {
      my ($path, $filename,$action)=@_;
      my $flag=uc(substr($action,0,1));

      if($flag eq 'T') {
         if(-d "$path/trash/") { 
            my $old="$path/new/$filename";
            my $new="$path/trash/$filename:2,$flag";

            if(rename($old,$new)) {
               return 0;
            } else {
               die("failed to rename \'$old\' to \'$new\'");
            }
         } else {
            die("\'$path/trash\' directory does not exist");
         }
      }
      return -1;
   }

dir

Set the maildir path:

   $maildir->dir('/tmp/.maildir/');

Get the maildir path:

   $maildir->dir();

mode

Set the mode for creating the directory and subdirectories tmp, cur and new:

   $maildir->mode(0754);

Get the mode:

   $maildir->mode();

mkdir

Create the directory and subdirectories tmp, cur and new if they do not already exist:

   $maildir->mkdir();

As above, but create the additional directories trash, sent:

   $maildir->mkdir("trash","sent");

This subroutine does not need to be explicitly called before creating new messages (unless you want to create folders other than tmp, new, and cur).

This subroutine returns 0 if the directories were created (or exist), otherwise it returns -1 and a warning with carp.

creat_message

Get a file handle $fh to a unique file in the tmp subdirectory:

   my ($fh,$status) = $maildir->creat_message();

Write message to unique file in tmp subdirectory which is then delivered to new:

   my $status=$maildir->creat_message($message);

Return: $status is 0 if success, -1 otherwise. $fh is the filehandle (undef if you pass create_message an argument).

deliver_message

Given file handle $fh, deliver message and close handle:

   $maildir->deliver_message($fh);

Returns 0 upon success, -1 otherwise.

deliver_all_messages

Deliver all messages and close all handles:

   $maildir->deliver_all_messages();

Returns 0 upon success, -1 otherwise.

sort

Get the current method for sorting messages:

   my $sort=$maildir->sort();

Set the sorting function of method:

   $maildir->sort('non'); # no specific sorting

   $maildir->sort('asc'); # sort based on mtime in increasing order

   $maildir->sort('des'); # sort based on mtime in decreasing order

   $maildir->sort(\&func); # sort based on user defined function

Example of sorting function which sorts according to a line in the message beggining with "sort:" followed by possible spaces and then a digit:

   sub func {
      my ($path,@messages)=@_;
      my %files; my @newmessages;

      foreach my $file (@messages) {
         my $f;
         open($f,"<$path/$file") or return @messages; #don't sort
         while(my $line=<$f>) {
            if($line=~m/sort:\s*(\d)+$/) { # string where sort info is
               $files{$file}=$1;
               close($f);
               last;
            }
         }
      }

      @newmessages= sort { $files{$a} <=> $files{$b}} keys %files;

      return @newmessages;
   }

get_next_message

Get the next message (as file handle) from directory new:

   my ($fh,$status)=$maildir->get_next_message("new");

NOTE: It is important to properly close file handle once finished with close_message or act.

Read lines of next message in array @lines then, close message and execute the action specified for flag 'P' (default for new: move to cur and append ':2,P'):

   my $status=$maildir->get_next_message("new",\@lines,'passed');

Return: $status is 0 if success, -1 otherwise. $fh is the filehandle (undef if you pass get_next_message a second argument).

force_readdir

Force a readdir during the next get_next_message. This is useful if you are reading messages from new and then from cur as some of the messages will be moved there.

   $mdir->force_readdir();

close_message

Given file handle $fh, close handle:

   $maildir->close_message($fh);

Returns 0 upon success, -1 otherwise.

act

Given file handle $fh, and flag ('P','R','S','T','D','F') close message, append the info and execute the specified action for the flag:

   $maildir->act($fh,'T');

Returns 0 upon success, -1 otherwise.

EXAMPLES

Top

Writing messages

The example shows the use of this module with MIME::Entity to write messages.

   #!/usr/bin/perl
   use strict;
   use warnings;
   use MIME::Entity;
   use Maildir::Lite;

   my $mdir=Maildir::Lite->new(dir=>'/tmp/.your_mdir');

   # print message to file handle
   sub print_message {
      my ($from,$to,$subj,$message,$fh)=@_;
      my $date=localtime;
      my $msg = MIME::Entity->build(
            Type        => 'text/plain',
            Date        => $date,
            From        => $from,
            To          => $to,
            Subject     => $subj,
            Data        => $message);

      $msg->print($fh);
   }

   # write messages to maildir folder
   sub  write_message {
      my ($from,$to,$subj,$message)=@_;
      my ($fh,$stat0)=$mdir->creat_message();

      die "creat_message failed" if $stat0;

      print_message($from,$to,$subj,$message,$fh);

      die "delivery failed!\n" if $mdir->deliver_message($fh);
   }

   write_message('me@foo.org', 'you@bar.com','Hi!','One line message');
   write_message('me@foo.org', 'bar@foo.com','Bye!','Who are you?');
   write_message('me2@food.org', 'bar@beer.org','Hello!','You again?');




Reading messages

The example shows the use of this module with MIME::Parser to read messages.

   #!/usr/bin/perl
   use strict;
   use warnings;
   use MIME::Parser;
   use Maildir::Lite;




   my $mdir=Maildir::Lite->new(dir=>'/tmp/.your_mdir');
   # move file from new to trash with changed filename




   sub read_from {
      my $folder=shift;
      my $i=0;

      $mdir->force_readdir();

      print "$folder:\n|".("-"x20)."\n";

      while(1) {
         my $parser = new MIME::Parser;
         $parser->output_under("/tmp");

         my ($fh,$status)=$mdir->get_next_message($folder);
         last if $status;

         my $entity=$parser->parse($fh);

         print "Message $i:\n".$entity->stringify."\n";
         $i++;

         if($mdir->act($fh,'S')) { warn("act failed!\n"); }
      }

      print "|".("-"x20)."\n\n";
   }

   read_from("cur");
   read_from("new");

   read_from("cur"); # to see the force_readdir in action
   read_from("new");

SEE ALSO

Top

There is already an implementation of Maildir, Mail::Box::Maildir, which is great, but more bulky and complicated.

Maildir specifications at http://cr.yp.to/proto/maildir.html

VERSION

Top

Version 0.01

AUTHOR

Top

Deian Stefan, <stefan at cooper.edu>

http://www.deian.net

BUGS

Top

Please report any bugs or feature requests to bug-maildir-lite at rt.cpan.org, or through the web interface at http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Maildir-Lite. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.

SUPPORT

Top

You can find documentation for this module with the perldoc command.

    perldoc Maildir::Lite




You can also look for information at:

* RT: CPAN's request tracker

http://rt.cpan.org/NoAuth/Bugs.html?Dist=Maildir-Lite

* AnnoCPAN: Annotated CPAN documentation

http://annocpan.org/dist/Maildir-Lite

* CPAN Ratings

http://cpanratings.perl.org/d/Maildir-Lite

* Search CPAN

http://search.cpan.org/dist/Maildir-Lite

COPYRIGHT & LICENSE

Top


Maildir-Lite documentation Contained in the Maildir-Lite distribution.
require 5.008008;
package Maildir::Lite;

use strict;
use Sys::Hostname 'hostname';
use File::Sync 'fsync';
use Carp;

our $VERSION ='0.02';



sub new {
   my($class,%args)=@_;

   my $create=exists $args{create} ? $args{create} : 1;
   my $dir=exists $args{dir} ? $args{dir} : 
               exists $ENV{HOME} ? "$ENV{HOME}/.maildir" : undef;
   my $mode=exists $args{mode} ? $args{mode} : 0750; 
   my $uniq=exists $args{uniq} ? $args{uniq} : int(rand(10000));
   my $sort=exists $args{sort} ? $args{sort} : 'non';

   my $self= {
      __create          =>      $create,
      __dir             =>      $dir,
      __uniq            =>      $uniq,
      __mode            =>      $mode,
      __message_fh      =>      {}, # keep track of fh/fname based on fileno
                                    # for open messages to be written
      __read_messages   =>      {},   # list of messages to be read
      __last_sort       =>      undef, #keep track of last sort method
      __sort            =>      $sort,  #current sort method
      __force_readdir      =>      0,  #force readdir
      __default_act     =>      'seen',
      __folder_actions  =>      {
         new =>  { 'default' => \&new_to_cur },
         tmp =>  { 'default' => 'close' },
         cur =>  { 'default' => 'close' }
      }
   };

   bless($self,$class);
   return $self;
}

# move file from new to current with changed filename
sub new_to_cur {
   my ($path, $filename,$action)=@_;
   if($action ne 'close') {
      my $flag=uc(substr($action,0,1));
      my $old="$path/new/$filename";
      my $new="$path/cur/$filename:2,$flag";

      if(rename($old,$new)) {
         return 0;
      } else {
         carp("new_to_cur: failed to rename \'$old\' to \'$new\': $!");
      }
   }
   return -1;
}


sub add_action {
   my ($self,$dir,$action,$func) = @_;

   
   if(!defined $dir) {
      carp("add_action: No folder specified");
      return -1;
   } elsif(!defined $action) {
      carp("add_action: No action specified");
      return -1;
   } elsif(!defined $func) {
      carp("add_action: No function specified");
      return -1;
   }

   my $path=$self->{__dir}."/$dir";
   my $flag=$action;

   if(!(-d $path)) {
      if(!mkdir($path)) {
         carp("add_action: mkdir failed to create folder \'$path\': $!");
         return -1;
      }
   }

   if($action ne 'default')  { $flag=uc(substr($action,0,1)); }
   $self->{__folder_actions}->{$dir}->{$flag}=$func;

   return 0;

}


sub dir {
   my ($self,$dir) = @_;

   if(defined $dir) { $self->{__dir}=$dir; } 

   return $self->{__dir};
}

sub mode {
   my ($self,$mode) = @_;

   if(defined $mode) { $self->{__mode}=$mode; } 

   return $self->{__mode};
}

sub mkdir {
   my ($self,@additional_dir)=@_;
   my $mode=$self->{__mode};
   my @dirs=("","tmp","cur","new");
   push(@dirs,@additional_dir);

   if(!defined $self->{__dir}) {
      carp("mkdir: No directory name given");
      return -1;
   } 

   if($self->{__create}!=1) {
      carp("mkdir: The create flag is not 1");
      return -1;
   }

   foreach my $path (@dirs) {
      $path=$self->{__dir}."/$path";
      if(!(-e $path)) { 
         if(!mkdir($path)) {
            carp("mkdir: mkdir failed to create \'$path\': $!");
            return -1;
         }
      }

      if(-d $path) { 
         if(chmod($self->{__mode},$path)!=1) {
            carp("mkdir: chmod \'$path\' to ".$self->{__mode}." failed: $!");
         }
      } else {
         carp("mkdir: \'$path\' is not a directory\n");
         return -1;
      }

   }

   return 0;
}


# returns a unique filename
sub fname {
   my $self=shift;

   my $time=time();
   my $hostname=hostname();
#replace / with \057 and : with \072
   $hostname=~s/\//\\057/g; $hostname=~s/:/\\072/g; 

   return $time.'.'.($$."_".$self->{__uniq}++).'.'.$hostname;
}



sub creat_message {
   my ($self,$message)=@_;
   my ($filename,$fh);

   $self->mkdir; #maybe some of the directories were deleted?

# make sure that the file does not exist
   $filename=$self->fname;
   while(-e $self->{__dir}."/tmp/$filename") {
      sleep(2);
      $filename=$self->fname;
   }

   unless(open($fh,">".$self->{__dir}."/tmp/$filename")) {
      carp("creat_message: failed to open file \'"
            .$self->{__dir}."/tmp/$filename\': $!");
      return (undef,-1);
   }

   if(defined $message) {
      print $fh $message;
      unless(fsync($fh)) {
         carp("creat_message: fsync failed: $!");
         return (undef,-1);
      }
      close($fh);

      return (undef,$self->deliver($filename));
   } elsif(defined $self->{__message_fh}->{fileno $fh}) {
      carp("creat_message: file handle \'"
            .(fileno $fh)."\' is already defined in table");
      return (undef,-1);
   } else {
      $self->{__message_fh}->{fileno $fh}->{'fh'}=$fh;
      $self->{__message_fh}->{fileno $fh}->{'filename'}=$filename;
      return ($fh,0);
   }
   
}

sub deliver_message {
   my ($self,$fh)=@_;

   if(defined $self->{__message_fh}->{fileno $fh}) {
      my $rc=-1;
      my $fno=fileno $fh; #need to index the hash __message_fh
      unless(fsync($fh)) {
         carp("deliver_message: fsync failed: $!");
         return (undef,-1);
      }
      close($fh);

      $rc=$self->deliver($self->{__message_fh}->{$fno}->{'filename'});
      delete $self->{__message_fh}->{$fno};
      return $rc;
   }
 
   return -1;
}

sub deliver_all_messages {
   my $self=shift;

   foreach my $fno (keys %{$self->{__message_fh}}) {
      if($self->deliver_message($self->{__message_fh}->{$fno}->{'fh'})==-1) {
         return -1;
      }
   }
   return 0;
}



# copy filename from tmp to new and delte from tmp
sub deliver {
   my ($self,$filename)=@_;

   if(!(-e $self->{__dir}."/tmp/$filename")) {
      carp("deliver: "
            ."file \'$filename\' does not exist in subdirectory \'tmp\'");
      return -1;
   }

   if(-e $self->{__dir}."/new/$filename") {
      carp("deliver: "
            ."file \'$filename\' already exists in subdirectory \'new\'");
      return -1;
   }

   if(!link($self->{__dir}."/tmp/$filename", $self->{__dir}."/new/$filename")) {
      carp("deliver: "
         ."file \'$filename\' could not be linked from \'tmp\' to \'new\': $!");
      return -1;
   }

   if(unlink($self->{__dir}."/tmp/$filename")<1) {
      carp("deliver: "
            ."file \'$filename\' could not be unlinked from \'tmp\': $!");
      return -1;
   }

   return 0;
}


sub sort {
   my ($self,$func)=@_;
   if(defined $func) {
      $self->{__last_sort}=$self->{__sort};
      $self->{__sort}=$func;
   }
   return $self->{__sort};
}

# get all the filenames in directory $dir sorted accorting to $self->{__sort}
sub get_messages {
   my ($self,$dir)=@_;
   my $path;
   my @messages;

   if(defined $self->{__read_messages}->{$dir} 
         and ($self->{__last_sort} eq $self->{__sort}) 
         and !$self->{__force_readdir}) {
      return @{$self->{__read_messages}->{$dir}};
   } else {
      $self->{__force_readdir}=0;
      $self->{__last_sort}=$self->{__sort};
# and sort:
   }

   if(!defined $dir) {
      carp("get_messages: get_messages expects a directory to open");
      return -1;
   }

   $path=$self->{__dir}."/$dir";

   unless(opendir(DIR, $path)) {
      carp("get_messages: failed to open directory \'$path\': $!");
      return -1;
   }

   @messages=map{ /^(\d[\w.:,_]+)$/ && -f "$path/$1"?$1:() } readdir(DIR);

   closedir(DIR);

   @{$self->{__read_messages}->{$dir}}=$self->sort_messages($dir,@messages);
   return @{$self->{__read_messages}->{$dir}};
}

# sort default sorting methods (ascending|descending) wased on mtime
sub sort_messages {
   my ($self,$dir,@messages)=@_;
   my %files;
   my @newmessages;

   if($self->{__sort}=~m/asc|des/i) {
      foreach my $m (@messages) {
         $files{$m}=(stat($self->{__dir}."/$dir/$m"))[9];

         if(!(defined $files{$m})) {
            carp("sort_messages: ".
                  "stat failed for file \'".$self->{__dir}."/$dir/$m\': $!");
            return @messages;
         }
      }

      if($self->{__sort}=~m/asc/i) {
         @newmessages= sort { $files{$a} <=> $files{$b}} keys %files;
      } else {
         @newmessages= sort { $files{$b} <=> $files{$a}} keys %files;
      }
   } elsif($self->{__sort}=~/non/i) {
      @newmessages=@messages;
   } else {
      @newmessages=&{$self->{__sort}}($self->{__dir}."/$dir/",@messages);
   }

   return @newmessages;
}

sub get_next_message {
   my ($self,$dir,$lines,$action)=@_;
   my $fh;
   $self->get_messages($dir);
   my $message=shift(@{$self->{__read_messages}->{$dir}});
   if(!defined $action) {
      $action=$self->{__default_act};
   }

   if(!$message) { return (undef,-1); }

   unless(open($fh,"<".$self->{__dir}."/$dir/$message")) {
      carp("get_next_message: "
            ."failed to open file \'".$self->{__dir}."/$dir/$message\': $!");
      return (undef,-1);
   }

   if(defined $self->{__message_fh}->{fileno $fh}) {
      carp("get_next_message: file handle \'$fh\' is already defined in table");
      return (undef,-1);
   } else {
      $self->{__message_fh}->{fileno $fh}->{'fh'}=$fh;
      $self->{__message_fh}->{fileno $fh}->{'filename'}=$message;
      $self->{__message_fh}->{fileno $fh}->{'dir'}=$dir;
      if(defined $lines) {
         @$lines=<$fh>;
         return (undef,$self->act($fh,$action));
      } else {
         return ($fh,0);
      }
   }
}

sub force_readdir {
   my $self=shift;
   $self->{__force_readdir}=1;
}

sub close_message {
   my ($self,$fh)=@_;

   if(defined $self->{__message_fh}->{fileno $fh}) {
      my $fno=fileno $fh; #need to index the hash __message_fh
      unless(fsync($fh)) {
         carp("close_message: fsync failed: $!");
         return (undef,-1);
      }
      close($fh);

      delete $self->{__message_fh}->{$fno};
      return 0;
   }
 
   return -1;
}


sub act {
   my ($self,$fh,$action)=@_;

   if(!defined $fh) {
      carp("act: No file handle specified!\n");
      return -1;
   }
   if(!defined $action) {
      carp("act: No action specified!\n");
      return -1;
   }

   my $filename=$self->{__message_fh}->{fileno $fh}->{'filename'};
   my $dir=$self->{__message_fh}->{fileno $fh}->{'dir'};
   my $flag=uc(substr($action,0,1));

   my $close_rc=$self->close_message($fh);

   return $close_rc if $action eq 'close';

   if(exists $self->{__folder_actions}->{$dir}) {
      if(exists $self->{__folder_actions}->{$dir}->{$flag}) {
         if($self->{__folder_actions}->{$dir}->{$flag} ne 'close') {
            &{$self->{__folder_actions}->{$dir}->{$flag}}($self->{__dir},
                  $filename, $action);
         }
      } elsif(exists $self->{__folder_actions}->{$dir}->{'default'}) {
         if($self->{__folder_actions}->{$dir}->{'default'} ne 'close') {
            &{$self->{__folder_actions}->{$dir}->{'default'}}($self->{__dir},
                  $filename, $action);
         }
      } else {
         carp("act: unknown action \'$action\' for directory \'$dir\',"
               ."closed file");
      }
   } else {
      carp("act: unknown action \'$action\', closed file");
   }

   return $close_rc;
}

1; # End of Maildir::Lite