1# -*-perl-*-
2#
3# Copyright (c) 1996-1998 Kevin Johnson <kjj@pobox.com>.
4#
5# All rights reserved. This program is free software; you can
6# redistribute it and/or modify it under the same terms as Perl
7# itself.
8#
9# $Id: Maildir.pm,v 1.4 1998/04/05 17:21:53 kjj Exp $
10
11require 5.00397;
12package Mail::Folder::Maildir;
13use strict;
14use POSIX qw(ENOENT);
15use vars qw($VERSION @ISA);
16
17@ISA = qw(Mail::Folder);
18$VERSION = "0.07";
19
20Mail::Folder->register_type('maildir');
21
22my $counter = 0;
23
24=head1 NAME
25
26Mail::Folder::Maildir - A maildir folder interface for Mail::Folder.
27
28B<WARNING: This code is in alpha release. Expect the interface to
29change.>
30
31=head1 SYNOPSIS
32
33C<use Mail::Folder::Maildir;>
34
35=head1 DESCRIPTION
36
37This module provides an interface to the B<maildir> folder mechanism.
38
39The B<maildir> folder format is the preferred folder mechanism for the
40B<qmail> mail transport agent.  It uses directories as folders and
41files as messages.  It also provides separate directories for new and
42current messages.  One of the most distinguishing features of the
43C<maildir> format is that it accomplishes it's job without the need
44for file locking, so it's better equipped to deal with things like nfs
45mounts and the like.
46
47More information about qmail is available from
48C<http://pobox.com/~djb/qmail.html>.
49
50=cut
51
52use Mail::Folder;
53use Mail::Internet;
54use Mail::Header;
55use Mail::Address;
56use Sys::Hostname;
57use IO::File;
58use DirHandle;
59use File::Sync qw(fsync);
60
61use Carp;
62
63=head1 METHODS
64
65=head2 open($folder_name)
66
67Populates the C<Mail::Folder> object with information about the folder.
68
69=over 2
70
71=item * Call the superclass C<open> method.
72
73=item * Make sure it is a valid maildir folder.
74
75=item * Detect whether the folder is readonly.
76
77=item * Clean the folder C<tmp> directory.
78
79=item * Move message in folder C<new> directory into the C<cur> directory.
80
81=item * Clean up the folder C<tmp> directory.
82
83=item * Moves message file in C<new> directory to the C<cur> directory.
84
85=item * For every message in the folder, add a new message number to
86the list of messages in the object, and remember the association between
87the message number and the message filename.
88
89=item * Set C<current_message> to 1 (ugh).
90
91=back
92
93=cut
94
95sub open {
96  my $self = shift;
97  my $foldername = shift;
98
99  return 0 unless $self->SUPER::open($foldername);
100
101  is_valid_folder_format($foldername)
102    or croak "$foldername isn't an maildir folder";
103
104  if (($< == 0) || ($> == 0)) {
105    $self->set_readonly unless ((stat($foldername))[2] & 0200);
106  } else {
107    $self->set_readonly unless (-w $foldername);
108  }
109
110  $self->_absorb_folder($foldername);
111
112  $self->current_message(1);
113
114  return 1;
115}
116
117=head2 close
118
119Deletes the working copy of the folder and calls the superclass
120C<close> method.
121
122=cut
123
124sub close {
125  my $self = shift;
126
127  delete $self->{MAILDIR_MsgFiles};
128  return $self->SUPER::close;
129}
130
131=head2 sync
132
133=over 2
134
135=item * Call the superclass C<sync> method.
136
137=item * Scan for new messages and absorb them.
138
139=item * If the folder is not readonly, expunge messages marked for
140deletion.
141
142=item * Update the C<:info> portion of each file in the folder.
143
144=item * Return the quantity of new messages found.
145
146=cut
147
148sub sync {
149  my $self = shift;
150
151  my $qty_new_messages = 0;
152  my @deletes = $self->select_label('deleted');
153  my $foldername = $self->foldername;
154
155  return -1 if ($self->SUPER::sync == -1);
156
157  $self->_absorb_folder($foldername);
158
159  unless ($self->is_readonly) {
160    if (@deletes) {
161      # we need to diddle current_message if it's pointing to a deleted msg
162      my $msg = $self->current_message;
163      while ($msg >= $self->first_message) {
164	last if (!$self->label_exists($msg, 'deleted'));
165	$msg = $self->prev_message($msg);
166      }
167      $self->current_message($msg);
168
169      unlink(map { "$foldername/$self->{Messages}{$_}{Filename}" } @deletes);
170      for my $msg (@deletes) {
171	$self->forget_message($msg);
172      }
173      $self->clear_label('deleted');
174    }
175  }
176
177  $self->_maildir_update_info unless ($self->is_readonly ||
178				      $self->get_option('NotMUA'));
179
180  return $qty_new_messages;
181}
182
183=head2 pack
184
185Calls the superclass C<pack> method.  Reassociates the filenames in
186the folders to message numbers, deleting holes in the sequence of
187message numbers.
188
189=cut
190
191sub pack {
192  my $self = shift;
193
194  my $newmsg = 0;
195  my $current_message = $self->current_message;
196
197  return 0 if (!$self->SUPER::pack || $self->is_readonly);
198
199  for my $msg (sort { $a <=> $b } $self->message_list) {
200    $newmsg++;
201    if ($msg > $newmsg) {
202      $self->current_message($newmsg) if ($msg == $current_message);
203      $self->remember_message($newmsg);
204      $self->cache_header($newmsg, $self->{Messages}{$msg}{Header});
205      $self->forget_message($msg);
206    }
207  }
208  return 1;
209}
210
211=head2 get_message($msg_number)
212
213Call the superclass C<get_message> method.
214
215Retrieves the contents of the file pointed to by C<$msg_number> into a
216B<Mail::Internet> object reference, caches the header, marks the
217message as 'C<seen>' and returns the reference.
218
219=cut
220
221sub get_message {
222  my $self = shift;
223  my $key = shift;
224
225  return undef unless $self->SUPER::get_message($key);
226
227  my $filename = $self->foldername . "/$self->{Messages}{$key}{Filename}";
228  my $fh = new IO::File $filename or croak "can't open $filename: $!";
229  my $mref = new Mail::Internet($fh,
230				Modify => 0,
231				MailFrom => 'COERCE');
232  $fh->close;
233
234  my $href = $mref->head;
235  $self->cache_header($key, $href);
236  $self->add_label($key, 'seen');
237
238  return $mref;
239}
240
241=head2 get_message_file($msg_number)
242
243Call the superclass C<get_message_file> method.
244
245Retrieves the given mail message file pointed to by $msg_number
246and returns the name of the file.
247
248=cut
249
250sub get_message_file {
251  my $self = shift;
252  my $key = shift;
253
254  return undef unless $self->SUPER::get_message_file($key);
255
256  return($self->foldername . "/$self->{Messages}{$key}{Filename}");
257}
258
259=head2 get_header($msg_number)
260
261If the particular header has never been retrieved then C<get_header>
262loads the header of the given mail message into a member of
263C<$self-E<gt>{Messages}{$msg_number}> and returns the object reference
264
265If the header for the given mail message has already been retrieved in
266a prior call to C<get_header>, then the cached entry is returned.
267
268=cut
269
270sub get_header {
271  my $self = shift;
272  my $key = shift;
273
274  my $hdr = $self->SUPER::get_header($key);
275  return $hdr if defined($hdr);
276
277  # return undef unless ($self->SUPER::get_header($key));
278
279  # return $self->{Messages}{$key}{Header} if ($self->{Messages}{$key}{Header});
280
281  my $filename = $self->foldername . "/$self->{Messages}{$key}{Filename}";
282
283  my $fh = new IO::File $filename or return undef;
284  my $href = new Mail::Header($fh,
285			      Modify => 0,
286			      MailFrom => 'COERCE');
287  $fh->close;
288
289  $self->cache_header($key, $href);
290
291  return $href;
292}
293
294=head2 append_message($mref)
295
296Calls the superclass C<append_message> method.
297
298Writes a temporary copy of the message in C<$mref> to the
299folder C<tmp> directory, then moves that temporary copy into the
300folder C<cur> directory.
301
302It will delete the C<From_> line in the header if one is present.
303
304=cut
305
306sub append_message {
307  my $self = shift;
308  my $mref = shift;
309
310  my $folder = $self->foldername;
311  my $msg_num = $self->last_message;
312
313  my $dup_mref = $mref->dup;
314
315  return 0 unless $self->SUPER::append_message($dup_mref);
316
317  $msg_num++;
318  $dup_mref->delete('From ');
319
320  my $tmpfile = $self->_get_tmp_file()
321    or croak "timed out trying to create a file in $folder/tmp";
322  my $fh = new IO::File "$folder/tmp/$tmpfile", O_CREAT|O_WRONLY, 0600
323    or croak "can't create $folder/tmp/$tmpfile: $!";
324  $fh->autoflush(1);
325  _coerce_header($dup_mref);
326  $dup_mref->print($fh) or croak "failed writing $folder/tmp/$tmpfile: $!";
327  fsync($fh) or croak "failed fsyncing $folder/tmp/$tmpfile: $!";
328  $fh->close or croak "failed closing $folder/tmp/$tmpfile: $!";
329
330  link("$folder/tmp/$tmpfile", "$folder/cur/$tmpfile")
331    or croak "can't link $folder/tmp/$tmpfile to $folder/cur/$tmpfile for append method: $!";
332  unlink("$folder/tmp/$tmpfile")
333    or croak "can't unlink $folder/tmp/$tmpfile for append method: $!";
334
335  $self->remember_message($msg_num);
336  $self->cache_header($msg_num, $dup_mref->head);
337  $self->{MAILDIR_MsgFiles}{$tmpfile} = $msg_num; # file to msgnum mapping
338  $self->{Messages}{$msg_num}{Filename} = "cur/$tmpfile";
339
340  return 1;
341}
342
343=head2 update_message($msg_number, $mref)
344
345Calls the superclass C<update_message> method.
346
347Writes a temporary copy of the message in C<$mref> to the
348folder C<tmp> directory, then moves that temporary copy into the
349folder C<cur> directory, replacing the message pointed to by
350C<$msg_number>.
351
352It will delete the C<From_> line in the header if one is present.
353
354=cut
355
356sub update_message {
357  my $self = shift;
358  my $key = shift;
359  my $mref = shift;
360
361  my $folder = $self->foldername;
362  my $dup_mref = $mref->dup;
363
364  return 0 unless $self->SUPER::update_message($key, $dup_mref);
365
366  $dup_mref->delete('From ');
367
368  my $tmpfile = $self->_get_tmp_file()
369    or croak "timed out trying to create a tmpfile";
370  my $fh = new IO::File $tmpfile, O_CREAT|O_WRONLY, 0600
371    or croak "can't create $tmpfile: $!";
372  $fh->autoflush(1);
373  _coerce_header($dup_mref);
374  $dup_mref->print($fh) or croak "failed writing $tmpfile: $!";
375  fsync($fh) or croak "failed fsyncing $tmpfile: $!";
376  $fh->close or croak "failed closing $tmpfile: $!";
377
378  rename($tmpfile, "$folder/$self->{Messages}{$key}{Filename}") or
379    croak "can't rename $tmpfile to $folder/$self->{Messages}{$key}{Filename}: $!";
380
381  return 1;
382}
383
384=head2 is_valid_folder_format($foldername)
385
386Returns C<1> if the folder is a directory and contains C<tmp>, C<cur>,
387and C<new> subdirectories otherwise returns C<0>.
388
389=cut
390
391sub is_valid_folder_format {
392  my $foldername = shift;
393
394  return 0 unless (-d $foldername &&
395		   -d "$foldername/tmp" &&
396		   -d "$foldername/cur" &&
397		   -d "$foldername/new");
398  return 1;
399}
400
401=head2 create($foldername)
402
403Creates a new folder named C<$foldername>.  Returns C<0> if the folder
404already exists, otherwise returns C<1>.
405
406=cut
407
408sub create {
409  my $self = shift;
410  my $foldername = shift;
411
412  return 0 if (-e $foldername);
413
414  mkdir($foldername, 0700) or croak "can't create $foldername: $!";
415  mkdir("$foldername/cur", 0700);
416  mkdir("$foldername/new", 0700);
417  mkdir("$foldername/tmp", 0700);
418  return 1;
419}
420###############################################################################
421sub _coerce_header {
422  my $mref = shift;
423  my $from = '';
424
425  if ($mref->head->count('Return-Path') == 0) {
426    if ($from =
427	$mref->get('Reply-To') ||
428	$mref->get('From') ||
429	$mref->get('Sender')) {	# this is dubious
430      my @addrs = Mail::Address->parse($from);
431      $from = $addrs[0]->address();
432      $mref->add('Return-Path', "<$from>", 0);
433    } else {
434      croak "can't synthesize Return-Path";
435    }
436  }
437
438  return $mref;
439}
440
441# this returns the name of a newly create file in the folder tmp
442# directory following the qmail rules for it's creation.
443
444sub _get_tmp_file {
445  my $self = shift;
446  my $folder = $self->foldername;
447  my $filename = '';
448  my $counter = $self->_bump_counter;
449
450  my $hostname = hostname or croak "can't determine hostname: $!";
451  # this loop duration should be configurable, but it's according to spec
452  for my $num (1 .. 30) {
453    my $time = time;
454    $filename = "$time.$$" . "_$counter.$hostname";
455    if (stat("$folder/tmp/$filename") || ($! != ENOENT)) {
456      select(undef, undef, undef, 2.0);
457      next;
458    }
459    my $fh = new IO::File "$folder/tmp/$filename", O_CREAT|O_WRONLY, 0600
460      or croak "can't create $folder/tmp/$filename: $!";
461    $fh->close;
462    return $filename;
463  }
464
465  return undef;
466}
467
468sub _bump_counter {
469  # my $self = shift;
470  return $counter++;
471}
472
473sub _maildir_update_info {
474  my $self = shift;
475
476  my $foldername = $self->foldername;
477
478  for my $msg ($self->message_list) {
479    my $file = $self->{Messages}{$msg}{Filename};
480    my $uniqpart = $file; $uniqpart =~ s/:.*$//;
481    my $oldinfo = '';
482    my $newinfo = '';
483    $newinfo .= 'F' if ($self->label_exists($msg, 'flagged'));
484    $newinfo .= 'R' if ($self->label_exists($msg, 'replied'));
485    $newinfo .= 'S' if ($self->label_exists($msg, 'seen'));
486    next if (($file =~ /:/) && ($file !~ /:2,/));
487    if ($file =~ /:(.*)/) {
488      $oldinfo = $1;
489    }
490    if ($oldinfo ne $newinfo) {
491      my $newfile = "$uniqpart:2,$newinfo";
492      croak "can't rename $foldername/$file to $foldername/$newfile: $!"
493	unless (rename("$foldername/$file", "$foldername/$newfile"));
494      $self->{Messages}{$msg}{Filename} = $newfile;
495      delete $self->{MAILDIR_MsgFiles}{$file};
496      $self->{MAILDIR_MsgFiles}{$newfile} = $msg;
497    }
498  }
499}
500
501sub _maildir_clean {
502  my $foldername = shift;
503
504  my @statary;
505  my $time = time;
506  my $tmpdir = "$foldername/tmp";
507
508  my $dir = new DirHandle $tmpdir or croak "can't open $tmpdir: $!";
509  my @files = $dir->read;
510  $dir->close;
511
512  for my $file (@files) {
513    next if ($file =~ /^\./);	# per djb, skip filenames that start with "."
514    unlink("$tmpdir/$file") if ((@statary = stat("$tmpdir/$file")) &&
515				($statary[9] + 129600) < $time);
516  }
517}
518
519sub _maildir_move_new_to_cur {
520  my $foldername = shift;
521
522  my @newfiles;
523
524  my $dir = new DirHandle "$foldername/new"
525    or croak"can't open $foldername/new: $!";
526  my @files = $dir->read;
527  $dir->close;
528
529  for my $file (@files) {
530    next if ($file =~ /^\./);
531    unlink("$foldername/new/$file")
532      if (link("$foldername/new/$file", "$foldername/cur/$file"));
533    push(@newfiles, $file);
534  }
535  return(@newfiles);
536}
537
538sub _absorb_folder {
539  my $self = shift;
540  my $folder_dir = shift;
541  my $msg_num = $self->last_message;
542
543  _maildir_clean($folder_dir);
544
545  _maildir_move_new_to_cur($folder_dir);
546
547  my $dir = new DirHandle "$folder_dir/cur"
548    or croak "can't open $folder_dir/cur: $!";
549  my @files = sort map { "cur/$_" } grep((!/^\./ &&
550					  !/^RCS$/ &&
551					  -f "$folder_dir/cur/$_"),
552					 $dir->read);
553  $dir->close;
554  if (0) {
555    $dir = new DirHandle "$folder_dir/new"
556      or croak "can't open $folder_dir/new: $!";
557    push @files, sort map { "new/$_" } grep((!/^\./ &&
558					     !/^RCS$/ &&
559					     -f "$folder_dir/new/$_"),
560					    $dir->read);
561    $dir->close;
562  }
563
564  for my $file (@files) {
565    next if defined($self->{MAILDIR_MsgFiles}{$file});
566    $msg_num++;
567    $self->remember_message($msg_num);
568    $self->{MAILDIR_MsgFiles}{$file} = $msg_num; # file-to-msgnum mapping
569    $self->{Messages}{$msg_num}{Filename} = $file;
570
571    next unless ($file =~ /:(.+)$/); # no info field
572
573    my $info = $1;
574    next unless ($info =~ /^2,/); # do we know this info field structure?
575
576    $self->add_label($msg_num, 'flagged') if ($info =~ /F/);
577    $self->add_label($msg_num, 'replied') if ($info =~ /R/);
578    $self->add_label($msg_num, 'seen') if ($info =~ /S/);
579    $self->delete_message($msg_num) if ($info =~ /T/);
580				# Not convinced we should do this...
581  }
582}
583
584###############################################################################
585
586=head1 AUTHOR
587
588Kevin Johnson E<lt>F<kjj@pobox.com>E<gt>
589
590=head1 COPYRIGHT
591
592Copyright (c) 1996-1998 Kevin Johnson <kjj@pobox.com>.
593
594All rights reserved. This program is free software; you can
595redistribute it and/or modify it under the same terms as Perl itself.
596
597=cut
598
5991;
600