File Coverage

blib/lib/Mail/MsgStore.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             =head1 NAME
2            
3             Mail::MsgStore - Complete mail client back end.
4            
5             =head1 SYNOPSIS
6            
7             use Mail::MsgStore;
8            
9             # set mailroot
10             Mail::MsgStore::mailroot($ENV{MAILROOT});
11             # get new messages from server
12             $count= Mail::MsgStore::getmail(\&prompt);
13             # send a Mail::Internet message
14             Mail::MsgStore::send($msg);
15            
16             # add an account
17             Mail::MsgStore::acct_set('Joe User (work)',$password);
18             # delete an account
19             Mail::MsgStore::acct_del('Joe User (work)');
20             # change mailroot
21             Mail::MsgStore::mailroot('c:/mail');
22             # change from address
23             Mail::MsgStore::from('Brian Lalonde ');
24             # get SMTP server address
25             $smtp= Mail::MsgStore::smtp;
26            
27             # add message
28             $MsgStore{'/'}= $msg; # auto-filter
29             $MsgStore{'path/to/folder/'}= $msg; # add to specific folder
30             # delete message
31             delete $MsgStore{'path/to/folder/msgid'};
32             # delete folder
33             delete $MsgStore{'path/to/folder/'};
34            
35             # get message
36             $msg= $MsgStore{'path/to/folder/msgid'};
37             # mark message as read, unmark 'general' flag
38             $MsgStore{'path/to/folder/msgid'}= 'read, -general';
39             # get folder's message id list
40             @msgids= $MsgStore{'path/to/folder/'};
41             # get list of folders
42             @folders= keys %MsgStore;
43            
44             # move message
45             $MsgStore{'newfolder/'}= delete $MsgStore{'path/to/folder/msgid'};
46             # copy message
47             $MsgStore{'path/to/newfolder/'}= $MsgStore{'path/to/folder/msgid'};
48            
49            
50             =head1 DESCRIPTION
51            
52             The primary goal of this module is ease of use.
53             The Mail::Folder module, on top of not quite being complete yet, is a
54             pretty low-level API. I was very impressed with how Win32::TieRegistry
55             simplified an otherwise complex task, and decided to adopt a similar
56             interface for handling a mail store.
57            
58             Another, equally important, reason for creating this module was
59             user-configurability.
60             I was unhappy with existing mail clients' filtering capabilities--
61             I wanted to pass every new message through some arbitrary Perl
62             code that was smart enough to forward, reply, send pages, activate
63             emergency-type alerts, etc. based on properties of the message.
64             What I didn't want was more bloatware--Exchange, Outlook and
65             Groupwise have already been written, and despite being huge,
66             still don't do enough.
67            
68             =head2 Storage Format
69            
70             MsgStore uses a modified form of qmail's maildir format.
71             Here's how it works: new messages are downloaded into a
72             file guaranteed to have a unique, but incomplete, name.
73             The filename is completed once the entire message has
74             been successfully downloaded (the finishing of the filename
75             replaces maildir's state subdirectories).
76            
77             The unique filename is generated as a dot-separated list of (uppercase)
78             hexadecimal numbers: seconds past epoch (12 digits), IP address
79             (8 digits), process id (4 digits), and download number (2 digits).
80             The IP should guarantee uniqueness to a machine, the time and pid narrows
81             it down to a specific process, and a simple incremental number ensures
82             that 256 messages can be downloaded per second and still retain
83             uniqueness. The filename also begins and ends with 'mail',
84             also separated by dots.
85            
86             Message flags are part of the message id (although requesting a
87             message by an id with the wrong flags still works).
88             The flags are five characters delimited by parens.
89             Each position is either a dash (off) or a letter (on).
90             Order is significant, but since the letters spell the word
91             FLAGS, that shouldn't be a problem.
92             Here are what the letters stand for:
93            
94             F flame
95             L list/group
96             A answered/replied
97             G general/flag
98             S seen/opened/read
99            
100             =head2 Warning
101            
102             The storage format used for this module quickly becomes unusable for
103             large message stores; hundreds or thousands of tiny files are rarely
104             stored efficiently on the disk.
105            
106             Although the module is completely usable, I hope it will inspire better
107             storage formats to use the same simple tied-hash interface.
108            
109             =head1 EVENTS
110            
111             The message store allows definition of the following subroutines
112             in the F file located in the B directory:
113            
114             =over 4
115            
116             =item C
117            
118             Accepts the Mail::Internet message object.
119             The message's recipient account is available as
120             C in the message header.
121            
122             Returns the name of the folder that the Mail::Internet $msg belongs in.
123             Returning undef implies the C.
124             Also, all message flags should be stored in the C header,
125             either as the native C<(FLOR!)> format of the message ID, or the english
126             equivalents: C.
127            
128             =item C
129            
130             Accepts the Mail::Internet message object.
131             The message's recipient account is available as
132             C in the message header.
133            
134             Returns a boolean value that determines whether the message should
135             be kept on the server.
136            
137             =item C
138            
139             Signs a message before it is sent.
140            
141             =back
142            
143            
144             =head1 FUNCTIONS
145            
146             =head2 Sending and Receiving
147            
148             =over 4
149            
150             =item C
151            
152             Logs on to each mail account, checking for new messages, which are
153             downloaded, passed to C and added.
154            
155             Returns number of messages downloaded.
156             Requires a callback that will be used if there is a problem logging in:
157            
158             =over 4
159            
160             =item C
161            
162             Parameters: C<$acct> ISA Mail::Address: user is the POP3 username,
163             host is the POP3 server.
164            
165             The function must return a password, or undef to cancel.
166             The password will be updated if it was initially set, or
167             left blank otherwise.
168            
169             =item C
170            
171             Parameters: C<$status_message> is a string describing what is going on
172             suitable for GUI statusbars, etc.
173             C<$percent_done> is an integer between 0 and 100 (when included, else C)
174             suitable for feeding to progress bars, etc.
175            
176             =back
177            
178             =item C
179            
180             Signs a Mail::Internet message, using the C function from the
181             user-defined F.
182            
183             =item C
184            
185             Sends a Mail::Internet message, and stores a copy in C.
186            
187             =back
188            
189            
190             =head2 Settings
191            
192             =over 4
193            
194             =item C
195            
196             Gets/sets the root directory of the mailstore.
197             The user's login is appended to this directory.
198             If the directory doesn't exist, it is created.
199             If the directory doesn't contain an F
200             file, one (fully commented) is created.
201            
202             Defaults to C<$ENV{MAILROOT}> or current dir unless set.
203            
204             =item C
205            
206             Reloads the F file.
207             Useful if you provide an editing facility for that file,
208             or otherwise know that it has changed.
209            
210             =item C
211            
212             Gets/sets the address of the outgoing mail server.
213            
214             =item C
215            
216             Gets/sets the email C address.
217            
218             =item C
219            
220             Returns a list of account strings.
221            
222             =item C
223            
224             Adds/sets an POP3 account to the list handled by C.
225             Parameters: account and optional password.
226            
227             Accounts strings are parsed by Mail::Address; the server portion is
228             used to connect, and the user portion is used to log in.
229             Everything else is mnemonic.
230            
231             =item C
232            
233             Deletes an account.
234            
235             =back
236            
237            
238             =head2 The Address Book
239            
240             =over 4
241            
242             =item C
243            
244             Returns a list of (references to) hashes for the entire address book.
245            
246             =item C $value, ... )>
247            
248             Add an entry to the address book.
249             The key for the new entry is returned.
250             The full list of fields is available in C<@addr_field>, pretty names
251             for the fields are in C<%addr_field> (neither exported by default).
252            
253             Some fields of note:
254            
255             =over 4
256            
257             =item key
258            
259             A guaranteed unique identifier for the address entry.
260             Auto-generated on insert.
261            
262             =item notes
263            
264             The I field allowed to contain tabs and newlines.
265            
266             =item firstname, lastname, nickname, email
267            
268             Standard mail-client stuff.
269            
270             =item tons more...
271            
272             (and in no guaranteed order)
273            
274             =back
275            
276             =item C
277            
278             Retrive the hash for an address.
279            
280             =item C $value, ...)>
281            
282             Update fields on an existing address.
283             Boolean success is returned.
284            
285             =item C 1 )>
286            
287             Delete an entry from the address book.
288            
289             =item C
290            
291             Gets/sets a comma or space-delimited list of LDAP servers.
292            
293             =item C
294            
295             Searches the address book fields specified by fields, looking for
296             records that match the regex, the C and C fields
297             by default.
298             (Actually, matches with C<"@addr{@fields}"=~ /regex/>.)
299             The special field C is also checked to match.
300             A list of (references to) hashes of matching records are returned,
301             plus a C field in each hash that contains the value of
302             either C<$field[0]> or C, depending on which field matched.
303            
304             The result set is sorted by matching field.
305            
306             This function is probably unneccessarily complex for most mail clients.
307            
308             =item C $namestart,
309             [ -number =E $hitnum, ] [ -fields =E \@fields, ] )>
310            
311             This is a simpler version of L<"whosearch"> that just returns address strings
312             (rather than entire hashrefs for each record).
313             (Actually, matches with C<"@addr{@fields}"=~ /regex/>.)
314             By default, the C and C fields are used, just
315             as in L<"whosearch">.
316             The special field C is also checked to match.
317             In list context, the list of matching address strings is returned,
318             but in a scalar context, the C<$hitnum>-th element is returned
319             (this allows passing of a kind of "Nope, next one." request).
320            
321             Each address is formatted this way:
322             C C ECE
323             unless the match was via C, in which case the nickname and
324             a tab character are prepended to the address string.
325            
326             =item C
327            
328             Searches the server(s) specified by C for an entry
329             that starts with C<$startswith>, and returns a list similar to
330             L<"addrsearch">. Ignores queries shorter than 3 letters.
331            
332             This function is called by L<"addrsearch">, and probably needn't be
333             called directly.
334            
335             =back
336            
337            
338             =head2 Utility
339            
340             =over 4
341            
342             =item C
343            
344             Searches messages in C<$folder> (and all subfolders) for messages
345             that produce a true value when passed to C<&match>.
346             Returns a list of fully-qualified message IDs.
347            
348             =item C
349            
350             Returns a text-only body of C<$msg>.
351             If the actual C<$msg> is a C or C,
352             for example, this just gives you the text portion of the message for
353             display purposes.
354            
355             =item C
356            
357             Given a fully-qualified messsage ID (one that begins with the folder path),
358             breaks the string into folder path and message ID.
359             (Similar in spirit to the L module.)
360            
361             =item C
362            
363             Given a message ID whose flags may have changed (the message ID contains
364             the message flags), returns the new message ID.
365            
366             =item C
367            
368             Returns a valid flagstring for the Mail::MsgStore message ID,
369             given either a msgid or english string (C<'+read -list !flame'>)
370             to parse.
371             Mostly for internal use.
372            
373             =back
374            
375             =head1 AUTHOR
376            
377             v, Ev@rant.scriptmania.comE
378            
379             =head1 SEE ALSO
380            
381             perl(1),
382             Sys::UniqueId,
383             Mail::Internet,
384             Mail::Folder,
385             Win32::TieRegistry,
386             Net::LDAP,
387             Net::POP3,
388             Time::ParseDate
389            
390             =cut
391            
392             package Mail::MsgStore;
393             require Exporter;
394 1     1   876 use strict;
  1         2  
  1         30  
395 1     1   4 use Carp;
  1         2  
  1         91  
396 1     1   5 use File::Find;
  1         12  
  1         59  
397 1     1   5 use File::Path;
  1         2  
  1         208  
398 1     1   1271 use Mail::Address;
  0            
  0            
399             use Mail::Internet;
400             use MIME::Entity;
401             use Net::LDAP;
402             use Net::POP3 2.20;
403             use Time::ParseDate;
404             use Sys::UniqueID;
405             use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS &isa);
406             use vars qw($MsgStore %MsgStore $mailroot @folder);
407             use vars qw(@addr_field %addr_field %mime_ext);
408             use vars qw($_default_script %_folder_sort $_noflock);
409            
410             $VERSION= '1.51';
411             @ISA= qw(Exporter);
412             @EXPORT= qw(%MsgStore);
413             @EXPORT_OK= qw(accounts acct_set acct_del
414             getmail mailroot msgsearch simplifymsg
415             smtp from ldaps signmsg sendmsg %mime_ext
416             address addresses whosearch addrsearch ldapsearch
417             flags msgid msgpath load_events);
418             %EXPORT_TAGS=
419             (
420             ALL => [ @EXPORT, @EXPORT_OK ],
421             ACCT => [ @EXPORT, qw(accounts acct_set acct_del) ],
422             READ => [ @EXPORT, qw(getmail mailroot msgsearch simplifymsg) ],
423             SEND => [ @EXPORT, qw(smtp from ldaps signmsg sendmsg %mime_ext) ],
424             ADDR => [ @EXPORT, qw(address addresses whosearch addrsearch ldapsearch) ],
425             UTIL => [ @EXPORT, qw(flags msgid msgpath load_events) ],
426             );
427             *isa = \&UNIVERSAL::isa;
428             $_noflock= 1 if $^O eq 'MSWin32' and Win32::IsWin95;
429             END { unlink $_noflock if -f $_noflock }
430            
431             ###################################
432             #
433             # Methods
434             #
435            
436             sub mailroot
437             {
438             if(@_)
439             { # change mailroot
440             unlink $_noflock if $_noflock and -f $_noflock;
441             local $_= shift;
442             y,\\,/,; s,/$,,;
443             $_.= '/'.getlogin;
444             unless(-d $_)
445             {
446             mkpath $_;
447             # TODO: if(uname) chmod/Win32::FilePerms
448             # to secure mail dir
449             }
450             $mailroot= $_;
451             load_events();
452             }
453             if($_noflock)
454             {
455             croak "Only one MsgStore application at a time, please!\n".
456             "(Your system can't lock files.)\n"
457             if -f "$mailroot/MsgStore.lck";
458             $_noflock= "$mailroot/MsgStore.lck";
459             open LOCK, ">$_noflock"
460             or croak "Unable to create lock file: $!\n";
461             close LOCK;
462             }
463             return $mailroot;
464             }
465            
466             sub _getkept(\%)
467             {
468             my $kept= shift;
469             if(open KEPT, "<$mailroot/kept")
470             {
471             local $_;
472             while()
473             {
474             chomp;
475             my($key,$val)= split /\t/;
476             $$kept{$key}= $val;
477             }
478             close KEPT;
479             }
480             }
481            
482             sub _savekept(\%)
483             {
484             my $kept= shift;
485             if(open KEPT, ">$mailroot/kept")
486             {
487             for(keys %$kept)
488             { print KEPT $_, "\t", $$kept{$_}, "\n"; }
489             close KEPT;
490             }
491             }
492            
493             sub getmail(&;&)
494             {
495             my($prompt,$status)= @_;
496             $status= sub{} unless $status;
497             my $started= time;
498             my %kept; _getkept(%kept);
499             dbmopen my %acct, $mailroot.'/accounts', 0600
500             or croak "Unable to open accounts database: $!\n";
501             my($NewMsg,$index,@acct)= (0,0,keys %acct);
502             my $grain= 10_000/@acct;
503             ACCT: for(@acct)
504             {
505             my $progress= $index*$grain/100;
506             &$status("Checking $_...",$progress);
507             my($acct)= Mail::Address->parse($_);
508            
509             # Connect and log in to POP3 server
510             carp("Unable to connect to server ".$acct->host().": $!\n"), next ACCT
511             unless my $conn= new Net::POP3($acct->host());
512             my $count= $conn->apop($acct->user(),($acct{$_} ^ getlogin)) if $conn;
513             unless(defined $count)
514             { # APOP didn't work, try basic auth
515             &$status("Connecting to $_...",$progress);
516             $conn->quit() if $conn; # reset connection (some servers get stuck)
517             $conn= new Net::POP3($acct->host());
518             $count= $conn->login($acct->user(),($acct{$_} ^ getlogin)) if $conn;
519             }
520             until(defined $count)
521             {
522             &$status("Login failed for $_...",$progress);
523             my $pass= &$prompt($acct);
524             next ACCT unless defined $pass;
525             unless(defined($count= $conn->apop($acct->user(),$pass)))
526             { # APOP didn't work, try basic auth
527             $conn->quit() if $conn; # reset connection (some servers get stuck)
528             $conn= new Net::POP3($acct->host());
529             $count= $conn->login($acct->user(),$pass) if $conn;
530             }
531             $acct{$_}= $pass ^ getlogin if $acct{$_};
532             }
533            
534             # Get messages
535             &$status("Connected to $_...",$progress);
536             &$status("No new messages for $_...",$progress), next unless int $count;
537             load_events();
538             my($newmsg,$msggrain)= (0, $grain/$count );
539             for my $msgnum (1..$count)
540             {
541             &$status("$_: $msgnum of $count",
542             ($msgnum-1)*$msggrain/100 + $progress);
543             my $uidl= $conn->uidl($msgnum);
544             unless($uidl)
545             { # not all servers support UIDL, here's a substitute
546             my $head= new Mail::Header($conn->top($msgnum));
547             $uidl= join($;,$head->get('Message-Id'),$conn->list($msgnum));
548             $uidl=~ y/\n//d;
549             }
550             if($kept{$_,$uidl})
551             { $kept{$_,$uidl}= time; next }
552             # NULL-value headers really confuse Mail::Internet
553             my @msgdata= grep { !/./..1 or /^(\s|\S+:\s*\S)/ } @{$conn->get($msgnum)};
554             my $msg= new Mail::Internet(\@msgdata);
555             $msg->head->add('X-Recipient-Account',$_);
556             $MsgStore{'/'}= $msg; # filter into message store
557             next unless $msg->get('Received'); # messages disappearing >:(
558             if(Mail::MsgStore::Event::keep($msg))
559             { # keep message (remember uidl)
560             $kept{$_,$uidl}= time;
561             }
562             else
563             { # delete from server
564             $conn->delete($msgnum);
565             }
566             $newmsg++;$NewMsg++;
567             }
568             $conn->quit();
569             $newmsg= 'no' unless $newmsg;
570             &$status("$_: $newmsg new messages.",++$index*$grain/100);
571             }
572             dbmclose %acct;
573             for(keys %kept) { delete $kept{$_} unless $kept{$_} > $started; }
574             _savekept(%kept);
575             $NewMsg= 'No' unless $NewMsg;
576             &$status("$NewMsg New Messages.",100);
577             return $NewMsg;
578             }
579            
580             sub from
581             {
582             my($value)= @_;
583             dbmopen my %settings, $mailroot.'/settings', 0600
584             or croak "Unable to open settings database: $!\n";
585             $settings{from}= $value if $value;
586             $value= $settings{from};
587             dbmclose %settings;
588             return $value;
589             }
590            
591             sub smtp
592             {
593             my($value)= @_;
594             dbmopen my %settings, $mailroot.'/settings', 0600
595             or croak "Unable to open settings database: $!\n";
596             $settings{smtp}= $value if $value;
597             $value= $settings{smtp};
598             dbmclose %settings;
599             return $value;
600             }
601            
602             sub ldaps
603             {
604             my($value)= @_;
605             dbmopen my %settings, $mailroot.'/settings', 0600
606             or croak "Unable to open settings database: $!\n";
607             $settings{ldap}= $value if $value;
608             $value= $settings{ldap};
609             dbmclose %settings;
610             return $value;
611             }
612            
613             sub load_events
614             { # event script default/init
615             my $script= $mailroot.'/events.pl';
616             unless(-f $script)
617             {
618             open SCRIPT, ">$script"
619             or croak "Unable to create default event script file: $!\n";
620             print SCRIPT $_default_script;
621             close SCRIPT;
622             }
623             { package Mail::MsgStore::Event;
624             do $script;
625             }
626             croak "Error(s) in user script: $script.\n$@\n" if $@;
627             }
628            
629             sub flags
630             {
631             return '(-----)' unless local $_= shift;
632             return $_ if s/^([F\-][L\-][A\-][G\-][S\-])$/\(\U($1)\)/i;
633             return uc$1 if m/(\([F\-][L\-][A\-][G\-][S\-]\))/i;
634             shift=~ /\(?([F\-][L\-][A\-][G\-][S\-])\)?/i;
635             my @flag= split //, ($1 or '-----');
636             for(split /[^!\+\-\w]+/)
637             {
638             $flag[0]= ( /\-/ ? '-' : ( /!/ ? ( $flag[0] eq '-' ? 'F' : '-' ) : 'F' ) )
639             and next if /\b(flame|troll)\b/i;
640             $flag[1]= ( /\-/ ? '-' : ( /!/ ? ( $flag[1] eq '-' ? 'L' : '-' ) : 'L' ) )
641             and next if /\b(list|group|sig)\b/i;
642             $flag[2]= ( /\-/ ? '-' : ( /!/ ? ( $flag[2] eq '-' ? 'A' : '-' ) : 'A' ) )
643             and next if /\b(answer(ed)?|repl(y|ied))\b/i;
644             $flag[4]= ( /\-/ ? '-' : ( /!/ ? ( $flag[4] eq '-' ? 'S' : '-' ) : 'S' ) )
645             and next if /\b(seen|open(ed)?|read)\b/i;
646             $flag[3]= ( /\-/ ? '-' : ( /!/ ? ( $flag[3] eq '-' ? 'G' : '-' ) : 'G' ) );
647             }
648             local $";
649             return "(@flag[0..4])";
650             }
651            
652             sub sendmsg($)
653             {
654             my $msg= shift;
655             return unless isa($msg,'Mail::Internet');
656             $msg->head->add('X-Mailer','Mail::MsgStore');
657             $msg->head->combine('X-Mailer',' and ');
658             return unless $msg->smtpsend( Host => smtp() );
659             return 1;
660             }
661            
662             sub signmsg($)
663             {
664             my $msg= shift;
665             die "[signmsg] No message to sign!" unless $msg;
666             $msg->remove_sig; # may want to re-sign (random quotes, ...)
667             return Mail::MsgStore::Event::sign($msg);
668             #return $msg;
669             }
670            
671             sub msgpath
672             {
673             local $_= shift;
674             return '/' if m<^[@*/!?\\]$>; # convenience root
675             return if /^[<|>].*[<|>]$/; # not a path
676             sg; # clean path
677             return $_ if -d "$mailroot/$_"
678             or s<> or not m<^\W?(.*)/(mail[^/]+mail)$>i;
679             return($1,$2);
680             }
681            
682             sub msgid
683             {
684             my($folder,$msgid)= @_;
685             return unless $msgid;
686             return $msgid if -f "$mailroot/$folder/$msgid";
687             $msgid=~ s/\./\\./g;
688             $msgid=~ s/\(.....\)/\\(.....\\)/; # flag-independant msgid search
689             opendir FOLDER, "$mailroot/$folder/"
690             or croak "Unable to open mail folder at '$mailroot/$folder/'.\n";
691             $msgid= ( grep { /^$msgid$/i } readdir FOLDER )[0];
692             closedir FOLDER;
693             return unless $msgid;
694             return $msgid;
695             }
696            
697             sub accounts()
698             { # list accounts
699             dbmopen my %acct, $mailroot.'/accounts', 0600
700             or croak "Unable to open accounts database: $!\n";
701             my @acct= keys %acct;
702             dbmclose %acct;
703             return @acct;
704             }
705            
706             sub acct_set($;$)
707             { # add account: name@server, password
708             my($acct,$pass)= @_;
709             dbmopen my %acct, $mailroot.'/accounts', 0600
710             or croak "Unable to open accounts database: $!\n";
711             $acct{$acct}= ($pass ^ getlogin);
712             dbmclose %acct;
713             return 1;
714             }
715            
716             sub acct_del($)
717             { # remove account
718             my($acct)= @_;
719             dbmopen my %acct, $mailroot.'/accounts', 0600
720             or croak "Unable to open accounts database: $!\n";
721             delete $acct{$acct};
722             dbmclose %acct;
723             return 1;
724             }
725            
726             sub msgsearch
727             {
728             my $folder= msgid(shift);
729             my $match= shift;
730             my @match;
731             my $wanted= sub
732             {
733             return unless /^mail.*mail$/i;
734             (my $folder= $File::Find::dir.'/')=~ s<^$mailroot/><>;
735             push @match, $folder.$_ if &$match($MsgStore{"$folder$_"});
736             };
737             finddepth( $wanted, "$mailroot/$folder" );
738             return @match;
739             }
740            
741             @addr_field=
742             qw(
743             key
744             firstname
745             lastname
746             nickname
747             email
748             url
749             chat
750             title
751             organization
752             department
753             birthdate
754             workphone
755             homephone
756             cellphone
757             pager
758             fax
759             modem
760             street
761             city
762             state
763             zip
764             country
765             notes
766             );
767             @addr_field{@addr_field}=
768             (
769             '',
770             'First Name',
771             'Last Name',
772             'Nickname',
773             'email',
774             'URL',
775             'ICQ/AIM/IRC',
776             'Title',
777             'Organization',
778             'Department',
779             'Birthdate',
780             'Work Phone',
781             'Home Phone',
782             'Cell Phone',
783             'Pager',
784             'Fax',
785             'Modem',
786             'Street Address',
787             'City',
788             'State',
789             'ZIP',
790             'Country',
791             'Notes',
792             );
793            
794             sub address
795             {
796             local $_;
797             my $key;
798             $key= shift if @_&1;
799             my %addr= @_;
800             $key= $addr{key} unless $key;
801             if($key and !@_)
802             { # retrieve address
803             open ADDR, "<$mailroot/address.tsv" or return;
804             while() { last if /^$key\t/; }
805             close ADDR;
806             return unless /^$key\t/;
807             chomp;
808             @addr{@addr_field}= split /\t/;
809             if($addr{notes} and $addr{notes}=~ /\\/)
810             { # unescape
811             $addr{notes}=~ s/\\\\/\\/g;
812             $addr{notes}=~ s/\\n/\n/g;
813             $addr{notes}=~ s/\\t/\t/g;
814             }
815             return %addr;
816             }
817             else
818             {
819             if($addr{notes})
820             { # escape
821             $addr{notes}=~ s/\\/\\\\/g;
822             $addr{notes}=~ s/\n/\\n/g;
823             $addr{notes}=~ s/\t/\\t/g;
824             }
825             if($key)
826             { # update/delete key
827             my $tempaddr= 'addr.'.&uniqueid.'.addr';
828             open NADDR, ">$mailroot/$tempaddr" or return;
829             open ADDR, "<$mailroot/address.tsv" or return;
830             flock(ADDR,1) unless $_noflock;
831             if($addr{Delete})
832             { # delete entry
833             while() { print NADDR unless /^$key\t/; }
834             }
835             else
836             { # update entry
837             my %prev;
838             while() { last if /^$key\t/; print NADDR; }
839             chomp;
840             @prev{@addr_field}= split /\t/;
841             for(keys %addr) { $prev{$_}= $addr{$_}; }
842             print NADDR join("\t",@prev{@addr_field}),"\n";
843             print NADDR while();
844             }
845             close NADDR;
846             close ADDR;
847             unlink "$mailroot/address.tsv";
848             rename "$mailroot/$tempaddr", "$mailroot/address.tsv";
849             return 1;
850             }
851             else
852             { # new: insert (append)
853             $addr{key}= &uniqueid;
854             open ADDR, ">>$mailroot/address.tsv" or return;
855             flock(ADDR,2) unless $_noflock;
856             print ADDR join("\t",@addr{@addr_field}),"\n";
857             close ADDR;
858             return $addr{key};
859             }
860             }
861             return;
862             }
863            
864             sub addresses
865             {
866             local $_;
867             my $query= shift;
868             my $field= (shift or 'firstname');
869             my(%addr,@match);
870             open ADDR, "<$mailroot/address.tsv" or return;
871             while()
872             {
873             chomp;
874             @addr{@addr_field}= split /\t/;
875             if($addr{notes})
876             {
877             $addr{notes}=~ s/\\t/\t/g;
878             $addr{notes}=~ s/\\n/\n/g;
879             $addr{notes}=~ s/\\\\/\\/g;
880             }
881             push @match, { %addr };
882             }
883             close ADDR;
884             return unless @match;
885             return sort { $$a{$$a{MATCHED}} cmp $$b{$$b{MATCHED}} } @match;
886             }
887            
888             sub whosearch
889             { # more comprehensive: find entire records
890             local $_;
891             my $query= shift;
892             my @field= (@_ or qw);
893             my(%addr,@match);
894             open ADDR, "<$mailroot/address.tsv" or return;
895             while()
896             {
897             chomp;
898             @addr{@addr_field}= split /\t/;
899             if($addr{notes})
900             {
901             $addr{notes}=~ s/\\t/\t/g;
902             $addr{notes}=~ s/\\n/\n/g;
903             $addr{notes}=~ s/\\\\/\\/g;
904             }
905             if("@addr{@field}"=~ /$query/)
906             { push @match, { %addr, MATCHED => $field[0] }; }
907             elsif($addr{nickname}=~ /$query/)
908             { push @match, { %addr, MATCHED => 'nickname' }; }
909             }
910             close ADDR;
911             return unless @match;
912             @match= sort { $$a{$$a{MATCHED}} cmp $$b{$$b{MATCHED}} } @match;
913             return( wantarray ? @match : ${$match[0]}{key} );
914             }
915            
916             sub addrsearch
917             { # less ambitious: just find addresses
918             local $_;
919             my %param= @_;
920             my $query= $param{-starts};
921             my $number= $param{-number};
922             my @field= ( $param{-fields} ? @{$param{-fields}} : qw );
923             my(%addr,@match);
924             open ADDR, "<$mailroot/address.tsv" or return;
925             while()
926             {
927             chomp;
928             @addr{@addr_field}= split /\t/;
929             if($addr{notes})
930             {
931             $addr{notes}=~ s/\\t/\t/g;
932             $addr{notes}=~ s/\\n/\n/g;
933             $addr{notes}=~ s/\\\\/\\/g;
934             }
935             if("@addr{@field}"=~ /^$query/i)
936             { push @match, "$addr{firstname} $addr{lastname} <$addr{email}>"; }
937             elsif($addr{nickname}=~ /^$query/i)
938             { push @match,
939             "$addr{nickname}\t$addr{firstname} $addr{lastname} <$addr{email}>"; }
940             }
941             close ADDR;
942             @match= ( @match ? ( sort { lc$a cmp lc$b } @match ) : &ldapsearch($query) );
943             return unless @match;
944             return( wantarray ? @match : $match[$number] );
945             }
946            
947             sub ldapsearch
948             { # EXTREMELY simple LDAP search
949             my @found;
950             my $query= shift;
951             return unless length($query) > 2;
952             my $filter;
953             if($query=~ /\s/)
954             {
955             my($first,$last)= split /\s+/, $query, 2;
956             $filter= "(&(cn=$first*)(sn=$last*))";
957             }
958             else
959             { $filter= "(cn=$query*)"; }
960             for my $server (split /,?\s+|,/, &ldaps())
961             {
962             my $ldap= new Net::LDAP($server, timeout => 3 )
963             or die "Unable to use LDAP: $! $@\n";
964             $ldap->bind; # anonymous logon
965             my $result= $ldap->search ( filter => $filter, timelimit => 3 );
966             carp("LDAP error. ".$result->error()), next if $result->code();
967             push @found, map {$_->get('cn')->[0].' <'.$_->get('mail')->[0].'>'}
968             $result->all_entries;
969             $ldap->unbind; # take down session
970             }
971             return sort { lc$a cmp lc$b } @found;
972             }
973            
974             sub simplifymsg
975             {
976             return unless my $msg= shift;
977             chomp(my $mtype= lc $msg->get('Content-Type'));
978             if($mtype=~ m<^(text/plain|message/rfc822)\b> or not $mtype)
979             { # message body
980             return join('',@{$msg->body})."\n";
981             }
982             elsif($mtype=~ m<^multipart/alternative\b>)
983             { # attachments
984             my $body;
985             my $Brown= new MIME::Parser( output_dir => ( $ENV{TEMP} or $ENV{TMP} ) );
986             my $mime= $Brown->parse_data([@{$msg->header}, "\n", @{$msg->body}]);
987             for my $mimeitem ($mime->parts)
988             { # look for the simplest alternative
989             return "\n\n".$mimeitem->stringify_body()."\n\n"
990             if($mimeitem->head->get('Content-Type')=~ mi);
991             }
992             return "\n\n".$mime->parts(0)->stringify_body()."\n\n";
993             }
994             else
995             { # alternative types
996             my $Brown= new MIME::Parser( output_dir => ( $ENV{TEMP} or $ENV{TMP} ) );
997             my $mime= $Brown->parse_data([ split /^/m, $msg->as_string ]);
998             my $body;
999             for my $mimeitem ($mime->parts)
1000             {
1001             if(my $filename= $mimeitem->head->recommended_filename)
1002             {
1003             $body.= '['.$mimeitem->head->recommended_filename.'] ';
1004             }
1005             else #if($msg->get('Content-Type')=~ m<^(text/plain|message/rfc822)\b>)
1006             {
1007             $body.= $mimeitem->stringify_body;
1008             }
1009             }
1010             return $body;
1011             }
1012             }
1013            
1014             sub _folder_sort
1015             {
1016             $_folder_sort{$a} ?
1017             ( $_folder_sort{$b} ?
1018             ( $_folder_sort{$a} <=> $_folder_sort{$b} ) : -1 ) :
1019             ( $_folder_sort{$b} ?
1020             1 : ( $a cmp $b ) );
1021             }
1022            
1023            
1024             ###################################
1025             #
1026             # Hash Tie Handlers
1027             #
1028            
1029             sub TIEHASH { bless {}, $_[0] }
1030             sub CLEAR { %{$_[0]} = () }
1031            
1032             sub STORE
1033             {
1034             my($this,$key,$val)= @_;
1035             my($folder,$msgid)= msgpath $key;
1036             if($msgid)
1037             { # modify message flag(s)
1038             $msgid= msgid($folder,$msgid);
1039             local $_= $msgid;
1040             s/(\(.....\))/flags($val,$1)/e;
1041             rename "$mailroot/$folder/$msgid", "$mailroot/$folder/$_";
1042             return "$folder/$_";
1043             }
1044             elsif($folder eq '/')
1045             { # use filter() to sort message
1046             my @msg= ( isa($val,'ARRAY') ? @$val : ($val) );
1047             for my $msg (@msg)
1048             {
1049             #print "[STORE:/] Got:\n"; $msg->print; # DEBUG
1050             STORE($this,(Mail::MsgStore::Event::filter($msg) or 'Inbox'),$msg);
1051             }
1052             return scalar @msg;
1053             }
1054             elsif($folder)
1055             { # add message(s) to folder
1056             $folder= "$mailroot/$folder";
1057             # create folder unless exists
1058             mkpath $folder unless -d $folder;
1059             croak "Unable to create folder $folder: $!\n" unless -d $folder;
1060             my @msg= ( isa($val,'ARRAY') ? @$val : ($val) );
1061             for my $msg (@msg)
1062             { # add message to folder
1063             next unless isa($msg,'Mail::Internet');
1064             # build msgid: mail.000238C42D34.69FD09C3.00003082.001A.(FLAGS).mail
1065             $msgid= 'mail.'.&uniqueid;
1066             local $_= "$folder/$msgid";
1067             open MESSAGE, ">$_" or croak "Unable to create $_: $!";
1068             { local $_; $msg->print(\*MESSAGE); } # MIME::Entity isn't friendly to $_
1069             close MESSAGE;
1070             my $time= parsedate($msg->get('Date'));
1071             utime $time, $time, $_;
1072             # message fully saved, complete the msgid (filename)
1073             $msg->head->combine('X-Msg-Flags');
1074             chomp(my $inflags= $msg->get('X-Msg-Flags'));
1075             rename $_, $_.flags($inflags).'.mail';
1076             }
1077             return scalar @msg;
1078             }
1079             else
1080             { # save an instance value
1081             return $$this{$key}= $val;
1082             }
1083             return;
1084             }
1085            
1086             sub EXISTS
1087             {
1088             my($this,$key)= @_;
1089             my($folder,$msgid)= msgpath $key;
1090             if($msgid)
1091             { # message
1092             return "$folder/$msgid" if -f "$mailroot/$folder/$msgid";
1093             return $folder.'/'.msgid($folder,$msgid); # maybe different flags
1094             }
1095             elsif($folder)
1096             { # folder
1097             if(opendir FOLDER, "$mailroot/$folder")
1098             { # check to see if the folder is empty
1099             while($_= readdir FOLDER)
1100             {
1101             next unless /^mail\..*\.mail$/;
1102             close FOLDER;
1103             return 1;
1104             }
1105             close FOLDER;
1106             }
1107             return 0;
1108             }
1109             else
1110             {
1111             return exists $$this{$key};
1112             }
1113             return 0;
1114             }
1115            
1116             sub FETCH
1117             {
1118             my($this,$key)= @_;
1119             my($folder,$msgid)= msgpath $key;
1120             if($msgid)
1121             { # message
1122             $msgid= msgid($folder,$msgid);
1123             return unless open MESSAGE, "<$mailroot/$folder/$msgid";
1124             my $msg= new Mail::Internet(\*MESSAGE);
1125             close MESSAGE;
1126             { local $_; # head->replace unfriendly to $_
1127             # save current flags internally (will be used if re-saved)
1128             $msgid=~ m<(\(.....\))>;
1129             my $curflags= $1;
1130             $msg->head->replace('X-Msg-Flags',$curflags);
1131             }
1132             return $msg;
1133             }
1134             elsif($folder eq '/')
1135             { # convenience root: get new, flagged messages
1136             my @new;
1137             my $wanted= sub
1138             {
1139             return unless /\(--..-\)/i;
1140             (my $folder= $File::Find::dir.'/')=~ s<^$mailroot/><>;
1141             push @new, $folder.$_;
1142             };
1143             finddepth( $wanted, $mailroot );
1144             return \@new;
1145             }
1146             elsif($folder)
1147             { # folder
1148             my @msgid;
1149             if(opendir FOLDER, "$mailroot/$folder")
1150             {
1151             @msgid= sort { (stat "$mailroot/$folder/$b")[9] <=>
1152             (stat "$mailroot/$folder/$a")[9] }
1153             grep /^mail\..*\.mail$/, readdir FOLDER;
1154             close FOLDER;
1155             }
1156             return \@msgid;
1157             }
1158             else
1159             {
1160             return $$this{$key};
1161             }
1162             return;
1163             }
1164            
1165             sub DELETE
1166             {
1167             my($this,$key)= @_;
1168             my($folder,$msgid)= msgpath $key;
1169             return if $folder eq '/';
1170             if($msgid)
1171             { # Trash, delete & return message
1172             $msgid= msgid($folder,$msgid);
1173             my $msg;
1174             return
1175             unless open MSG, "<$mailroot/$folder/$msgid"
1176             and $msg= new Mail::Internet(\*MSG);
1177             close MSG;
1178             { local $_; # head->replace unfriendly to $_
1179             # save current flags internally (will be used if re-saved)
1180             $msgid=~ m<(\(.....\))>;
1181             my $curflags= $1;
1182             $msg->head->replace('X-Msg-Flags',$curflags);
1183             }
1184             return $msg if unlink "$mailroot/$folder/$msgid";
1185             }
1186             elsif($folder)
1187             { # folder
1188             my @msg;
1189             my $wanted= sub
1190             {
1191             return unless /^mail\..*\.mail$/;
1192             (my $folder= $File::Find::dir.'/')=~ s<^$mailroot/><>;
1193             my $msg= $MsgStore{$folder.$_};
1194             push @msg, $msg;
1195             };
1196             finddepth( $wanted, "$mailroot/$folder" );
1197             rmtree "$mailroot/$folder";
1198             return \@msg;
1199             }
1200             else
1201             {
1202             return delete $$this{$key};
1203             }
1204             return;
1205             }
1206            
1207             sub FIRSTKEY
1208             {
1209             undef @folder;
1210             my $wanted= sub
1211             {
1212             (my $folder= $File::Find::dir.'/')=~ s,^$mailroot/,,;
1213             push @folder, $folder.$_ if -d and $_ ne '.';
1214             };
1215             finddepth( $wanted, $mailroot );
1216             @folder= sort {&_folder_sort} @folder;
1217             return shift @folder;
1218             }
1219            
1220             sub NEXTKEY
1221             {
1222             return shift @folder;
1223             }
1224            
1225            
1226             %_folder_sort=
1227             (
1228             Inbox => 1,
1229             Outbox => 2,
1230             Draft => 4,
1231             Sent => 3,
1232             Trash => 5,
1233             );
1234            
1235             %mime_ext=
1236             (
1237             aif => 'audio/x-aiff',
1238             aifc => 'audio/x-aiff',
1239             aiff => 'audio/x-aiff',
1240             asc => 'text/plain',
1241             asp => 'application/x-asp',
1242             au => 'audio/ulaw',
1243             avi => 'video/x-msvideo',
1244             bat => 'application/x-batchfile',
1245             bin => 'application/octet-stream',
1246             bmp => 'image/bitmap',
1247             cgi => 'application/x-perl',
1248             cmd => 'application/x-nt-command-script',
1249             eps => 'application/postscript',
1250             exe => 'application/octet-stream',
1251             gif => 'image/gif',
1252             gtar => 'application/x-gtar',
1253             gz => 'application/x-gunzip',
1254             htm => 'text/html',
1255             html => 'text/html',
1256             ief => 'image/ief',
1257             jpe => 'image/jpeg',
1258             jpeg => 'image/jpeg',
1259             jpg => 'image/jpeg',
1260             latex => 'application/x-latex',
1261             mid => 'audio/midi',
1262             midi => 'audio/midi',
1263             mov => 'video/quicktime',
1264             movie => 'video/x-sgi-movie',
1265             mp2 => 'video/mpeg',
1266             mp3 => 'audio/mpeg-layer3',
1267             mpe => 'video/mpeg',
1268             mpeg => 'video/mpeg',
1269             mpg => 'video/mpeg',
1270             pbm => 'image/x-portable-bitmap',
1271             pdf => 'application/pdf',
1272             pgm => 'image/x-portable-graymap',
1273             pgp => 'application/pgp',
1274             pl => 'application/x-perl',
1275             pm => 'application/x-perl',
1276             png => 'image/png',
1277             pnm => 'image/x-portable-anymap',
1278             ps => 'application/postscript',
1279             qt => 'video/quicktime',
1280             ra => 'audio/x-pn-realaudio',
1281             ram => 'audio/x-pn-realaudio',
1282             ras => 'image/x-cmu-raster',
1283             rgb => 'image/x-rgb',
1284             rm => 'audio/x-pn-realaudio',
1285             rmi => 'audio/midi',
1286             rtf => 'text/richtext',
1287             rtx => 'text/richtext',
1288             shtml => 'text/html',
1289             snd => 'audio/basic',
1290             stm => 'text/html',
1291             tar => 'application/x-tar',
1292             tif => 'image/tiff',
1293             tiff => 'image/tiff',
1294             tsv => 'text/tab-separated-values',
1295             txt => 'text/plain',
1296             wav => 'audio/x-wav',
1297             xbm => 'image/x-bitmap',
1298             xpm => 'image/x-pixmap',
1299             zip => 'application/zip',
1300             );
1301            
1302             $_default_script= <<'SCRIPT_END';
1303             ##############################################################
1304             #
1305             # events.pl - customized mail filtering and more
1306             #
1307            
1308             ##############################################################
1309             #
1310             # filter()
1311             #
1312             # parameter: Mail::Internet object
1313             # returns: name of folder to store message in
1314             # (undef implies 'Inbox')
1315             #
1316             # Message flags can be stored in the 'X-Msg-Flags' message
1317             # header, and can be either native '(FLAGS)' format, or
1318             # the more readable english 'list, flag, answered' format.
1319             #
1320             # Flag English
1321             # F flame
1322             # L list/group
1323             # A answered/replied
1324             # G green/general/flag (general purpose flag)
1325             # S seen/read/opened
1326             #
1327             sub filter($)
1328             {
1329             }
1330            
1331             ##############################################################
1332             #
1333             # keep()
1334             #
1335             # parameter: Mail::Internet object
1336             # returns: boolean - keep message on server?
1337             #
1338             # The source account is stored in the 'X-Recipient-Account'
1339             # message header.
1340             #
1341             sub keep($)
1342             {
1343             return; # delete by default (no return value = false)
1344             }
1345            
1346             ##############################################################
1347             #
1348             # sign()
1349             #
1350             # parameter: Mail::Internet object
1351             # returns: the modified Mail::Internet object
1352             #
1353             # Add a signature to a message.
1354             # $msg->sign( Signature => 'Your Signature Message' );
1355             #
1356             sub sign($)
1357             {
1358             my $msg= shift;
1359             $msg->sign( Signature => 'Your Signature Message' );
1360             return $msg;
1361             }
1362            
1363             local $_;1
1364             SCRIPT_END
1365            
1366            
1367             ###################################
1368             #
1369             # Initialization
1370             #
1371            
1372             tie %MsgStore, __PACKAGE__;
1373             if($ENV{MAILROOT})
1374             { mailroot($ENV{MAILROOT}); }
1375             else
1376             {
1377             $mailroot= '.';
1378             { package Mail::MsgStore::Event;
1379             sub filter($) { }
1380             sub keep($) { 1 }
1381             sub sign($) { }
1382             }
1383             }
1384            
1385             1