#!/usr/bin/perl -w # $Id: mitmailmove.pl,v 1.3 2004-08-31 16:00:16 rbasch Exp $ # Move or copy messages between IMAP folders. use strict; use warnings FATAL => 'all'; use Cyrus::IMAP; use Getopt::Long; sub usage(;$); sub send_command($); sub fetch_callback(@); sub close_and_errorout($); sub close_connection(); sub errorout($); my $prog = $0; my $copy = ($prog =~ m/copy$/o); sub usage(;$) { print STDERR "$prog: $_[0]\n" if ($_[0] && $_[0] ne "help"); print STDERR <] [...] Options: --by-uid specify message UIDs instead of sequence numbers --debug turn on debugging --help print this usage information --host= query host instead of default POBOX server --mailbox= examine mailbox instead of INBOX --no-create do not create the target mailbox automatically EOF exit 1; } # Parse the command line arguments. use vars qw($opt_by_uid $opt_debug $opt_host $opt_mailbox $opt_no_create); GetOptions("by-uid", "debug", "help" => \&usage, "host=s", "mailbox=s", "no-create") || usage; usage "Please specify a message number and target mailbox" if @ARGV < 2; my $target = pop @ARGV; # Check the validity of message ID arguments. # The ID can be a number or '*', and we accept a range specification, # of the form 'n:m'. foreach (@ARGV) { errorout "Invalid message specification $_" unless (m/^(?:\d+|\*)(?::(?:\d+|\*))?$/o); } $opt_mailbox = 'INBOX' unless $opt_mailbox; my $username = $ENV{'ATHENA_USER'} || $ENV{'USER'} || getlogin || (getpwuid($<))[0] || errorout "Cannot determine user name"; unless ($opt_host) { $opt_host = (gethostbyname("$username.mail.mit.edu"))[0]; errorout "Cannot find Post Office server for $username" unless $opt_host; } errorout "Exchange accounts are not supported yet. Try http://owa.mit.edu/." if $opt_host =~ /EXCHANGE/; # Connect to the IMAP server, and authenticate. my $client = Cyrus::IMAP->new($opt_host) || errorout "Cannot connect to IMAP server on $opt_host"; $client->authenticate(-authz => $username, -maxssf => 0) || close_and_errorout "Cannot authenticate to $opt_host"; # Select (or examine, if copying only) the mailbox. my $select_cmd = ($copy ? 'EXAMINE' : 'SELECT'); send_command "$select_cmd \"$opt_mailbox\""; # Use the correct forms of the COPY and STORE commands, depending on # whether we are operating on UIDs, or sequence numbers. my $store_cmd; my $copy_cmd; if ($opt_by_uid) { $copy_cmd = 'UID COPY'; $store_cmd = 'UID STORE'; } else { $copy_cmd = 'COPY'; $store_cmd = 'STORE'; } # If we are doing a "move" operation, we will execute a STORE command # following the copy to set the message's Deleted flag. Add a callback # for the FETCH response generated by the STORE. my $cb_numbered = Cyrus::IMAP::CALLBACK_NUMBERED; unless ($copy) { $client->addcallback({-trigger => 'FETCH', -flags => $cb_numbered, -callback => \&fetch_callback}); } # Copy each given message ID to the target mailbox, and, if doing a # "move", mark the source message(s) for deletion. my $exitcode = 0; foreach (@ARGV) { my ($status, $text) = send_command "$copy_cmd $_ \"$target\""; if ($status ne 'OK') { if ($text =~ m/\bTRYCREATE\b/io) { close_and_errorout "Mailbox $target does not exist" if $opt_no_create; print "Creating $target\n" if $opt_debug; # send_command will error out if either the CREATE fails. send_command "CREATE \"$target\""; ($status, $text) = send_command "$copy_cmd $_ \"$target\""; } if ($status ne 'OK') { print STDERR "IMAP error copying $_ to $target: $text\n"; $exitcode = 2; next; } } send_command "$store_cmd $_ +FLAGS (\\Deleted)" unless $copy; } # We are done talking to the IMAP server, close down the connection. close_connection(); exit $exitcode; # Subroutine to send a command to the IMAP server, and wait for the # response; any defined callbacks for the response are invoked. # If called in list context, we return the server's response, as # status and text values. Otherwise, if the response indicates # failure, we error out. sub send_command($) { print "Sending: $_[0]\n" if $opt_debug; my ($status, $text) = $client->send('', '', $_[0]); print "Response: status $status, text $text\n" if $opt_debug; errorout "Premature end-of-file on IMAP connection to $opt_host" if $status eq 'EOF'; return ($status, $text) if wantarray; close_and_errorout "IMAP error from $opt_host: $text" if $status ne 'OK'; } # Callback subroutine to parse the FETCH response from a STORE command. # This callback will be invoked for each message. The "-text" hash # element contains the text returned by the server. sub fetch_callback(@) { my %cb = @_; my ($number, $flags); print "In FETCH callback: msgno $cb{-msgno} text $cb{-text}\n" if $opt_debug; $number = $cb{-msgno}; foreach (split /\r\n/, $cb{-text}) { $number = $1 if /UID\s+(\d+)/io; $flags = $1 if /FLAGS\s+\(([^\)]*)\)/io; } # Warn if the message's state does not include the Deleted flag # (should never happen). if ($flags !~ m/\\Deleted\b/io) { print STDERR "$prog: Warning: Message $number not deleted"; print STDERR " from $opt_mailbox\n"; } } # Close the connection to the IMAP server, and error out. sub close_and_errorout($) { close_connection(); errorout $_[0]; } # Logout from the IMAP server, and close the connection. sub close_connection() { $client->send('', '', "LOGOUT"); # Set the client reference to undef, so that perl invokes the # destructor, which closes the connection. Note that if we invoke # the destructor explicitly here, then perl will still invoke it # again when the program exits, thus touching memory which has # already been freed. $client = undef; } sub errorout($) { print STDERR "$prog: $_[0]\n"; exit 1; }