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