[22828] | 1 | #!/usr/bin/perl -w |
---|
[20054] | 2 | |
---|
[20593] | 3 | # $Id: mitmaildel.pl,v 1.3 2004-07-29 19:11:52 rbasch Exp $ |
---|
[20054] | 4 | |
---|
| 5 | # Delete (or undelete) messages in an IMAP folder. |
---|
| 6 | |
---|
| 7 | use strict; |
---|
| 8 | use warnings FATAL => 'all'; |
---|
| 9 | use Cyrus::IMAP; |
---|
| 10 | use Getopt::Long; |
---|
| 11 | |
---|
| 12 | sub usage(;$); |
---|
| 13 | sub send_command($); |
---|
| 14 | sub fetch_callback(@); |
---|
| 15 | sub number_callback(@); |
---|
| 16 | sub close_and_errorout($); |
---|
| 17 | sub close_connection(); |
---|
| 18 | sub errorout($); |
---|
| 19 | |
---|
| 20 | my $prog = $0; |
---|
| 21 | my $undelete = ($prog =~ m/undel$/o); |
---|
| 22 | |
---|
| 23 | sub usage(;$) { |
---|
| 24 | print STDERR "$prog: $_[0]\n" if ($_[0] && $_[0] ne "help"); |
---|
| 25 | print STDERR <<EOF; |
---|
| 26 | Usage: $prog [<options>] <message-id> ... |
---|
| 27 | Options: |
---|
| 28 | --by-uid specify message UIDs instead of sequence numbers |
---|
| 29 | --debug turn on debugging |
---|
| 30 | --expunge expunge all deleted messages from mailbox |
---|
| 31 | --help print this usage information |
---|
| 32 | --host=<name> query host <name> instead of default POBOX server |
---|
| 33 | --mailbox=<name> examine mailbox <name> instead of INBOX |
---|
| 34 | --silent suppress acknowledgement output |
---|
| 35 | EOF |
---|
| 36 | exit 1; |
---|
| 37 | } |
---|
| 38 | |
---|
| 39 | # Parse the command line arguments. |
---|
| 40 | use vars qw($opt_by_uid $opt_debug $opt_expunge $opt_host |
---|
| 41 | $opt_mailbox $opt_silent); |
---|
| 42 | |
---|
| 43 | GetOptions("by-uid", |
---|
| 44 | "debug", |
---|
| 45 | "expunge", |
---|
| 46 | "help" => \&usage, |
---|
| 47 | "host=s", |
---|
| 48 | "mailbox=s", |
---|
| 49 | "silent") || usage; |
---|
| 50 | |
---|
| 51 | usage "Please specify a message number" if @ARGV == 0; |
---|
| 52 | |
---|
| 53 | # Check the validity of message ID arguments. |
---|
| 54 | # The ID can be a number or '*', and we accept a range specification, |
---|
| 55 | # of the form 'n:m'. |
---|
| 56 | foreach (@ARGV) { |
---|
| 57 | errorout "Invalid message specification $_" |
---|
| 58 | unless (m/^(?:\d+|\*)(?::(?:\d+|\*))?$/o); |
---|
| 59 | } |
---|
| 60 | |
---|
| 61 | $opt_mailbox = 'INBOX' unless $opt_mailbox; |
---|
| 62 | |
---|
[23697] | 63 | my $username = $ENV{'ATHENA_USER'} || $ENV{'USER'} || getlogin || (getpwuid($<))[0] || |
---|
[20054] | 64 | errorout "Cannot determine user name"; |
---|
| 65 | |
---|
| 66 | unless ($opt_host) { |
---|
[24300] | 67 | $opt_host = (gethostbyname("$username.mail.mit.edu"))[0]; |
---|
| 68 | errorout "Cannot find Post Office server for $username" unless $opt_host; |
---|
[20054] | 69 | } |
---|
[23913] | 70 | errorout "Exchange accounts are not supported yet. Try http://owa.mit.edu/." if $opt_host =~ /EXCHANGE/; |
---|
[20054] | 71 | |
---|
| 72 | # Connect to the IMAP server, and authenticate. |
---|
| 73 | my $client = Cyrus::IMAP->new($opt_host) || |
---|
| 74 | errorout "Cannot connect to IMAP server on $opt_host"; |
---|
[24300] | 75 | $client->authenticate(-authz => $username, -maxssf => 0) || |
---|
[20054] | 76 | close_and_errorout "Cannot authenticate to $opt_host"; |
---|
| 77 | |
---|
| 78 | # Select the mailbox (for read-write access). Store the value of the |
---|
| 79 | # EXISTS (i.e. highest existing message sequence number) data item, |
---|
| 80 | # and, if returned, the UIDNEXT (next UID to be assigned) value. |
---|
| 81 | my $cb_numbered = Cyrus::IMAP::CALLBACK_NUMBERED; |
---|
| 82 | my $maxseq = 0; |
---|
| 83 | my $uidnext = 0; |
---|
| 84 | $client->addcallback({-trigger => 'EXISTS', -flags => $cb_numbered, |
---|
| 85 | -callback => \&number_callback, |
---|
| 86 | -rock => \$maxseq}); |
---|
| 87 | $client->addcallback({-trigger => 'OK', |
---|
| 88 | -callback => sub { |
---|
| 89 | my %cb = @_; |
---|
| 90 | print "In OK callback: text $cb{-text}\n" |
---|
| 91 | if $opt_debug; |
---|
| 92 | return unless ($cb{-text} =~ m/UIDNEXT\s+(\d+)/io); |
---|
| 93 | $uidnext = $1; |
---|
| 94 | }}); |
---|
[20505] | 95 | send_command "SELECT \"$opt_mailbox\""; |
---|
[20054] | 96 | |
---|
| 97 | # If we're operating on UIDs, and did not get the UIDNEXT value above, |
---|
| 98 | # use the STATUS command to get it explicitly. |
---|
| 99 | if ($opt_by_uid && !$uidnext) { |
---|
| 100 | $client->addcallback({-trigger => 'STATUS', |
---|
| 101 | -callback => sub { |
---|
| 102 | my %cb = @_; |
---|
| 103 | print "In STATUS callback: text $cb{-text}\n" |
---|
| 104 | if $opt_debug; |
---|
| 105 | return |
---|
| 106 | unless ($cb{-text} =~ m/UIDNEXT\s+(\d+)/io); |
---|
| 107 | $uidnext = $1; |
---|
| 108 | }}); |
---|
[20505] | 109 | send_command "STATUS \"$opt_mailbox\" (UIDNEXT)"; |
---|
[20054] | 110 | } |
---|
| 111 | |
---|
| 112 | # Note that the STORE command returns success even when the given |
---|
| 113 | # message ID does not exist. So the most feasible way to determine |
---|
| 114 | # whether the (un)delete was successful for a message is to see if the |
---|
| 115 | # FETCH callback (invoked during the server response to the STORE |
---|
| 116 | # command) was invoked for the message, and whether the \Deleted flag |
---|
| 117 | # was returned. We thus initialize a hash whose keys are the |
---|
| 118 | # individual message IDs given; the FETCH callback will remove the key |
---|
| 119 | # from the hash when it detects that the message flags have been set |
---|
| 120 | # as desired. Any keys remaining in the hash upon completion will |
---|
| 121 | # indicate IDs whose flags could not be modified (presumably because |
---|
| 122 | # the message does not exist). |
---|
| 123 | my %unchanged = (); |
---|
| 124 | my $expect = ($undelete ? "undeleted" : "deleted"); |
---|
| 125 | my $exitcode = 0; |
---|
| 126 | my $store_cmd = ($opt_by_uid ? 'UID STORE' : 'STORE'); |
---|
| 127 | my $store_item = ($undelete ? '-FLAGS' : '+FLAGS'); |
---|
| 128 | $client->addcallback({-trigger => 'FETCH', -flags => $cb_numbered, |
---|
| 129 | -callback => \&fetch_callback, |
---|
| 130 | -rock => \%unchanged}); |
---|
| 131 | foreach (@ARGV) { |
---|
| 132 | if ($opt_by_uid) { |
---|
| 133 | # When operating on UIDs, the message numbers in a range are |
---|
| 134 | # not necessarily sequential, so we don't detect unchanged |
---|
| 135 | # messages in the range. We can handle '*', though, as that |
---|
| 136 | # is simply one less than the next UID to be assigned. |
---|
| 137 | $_ = ($uidnext - 1) if ($_ eq '*' && $uidnext); |
---|
| 138 | %unchanged = ($_ => 1) if (/^\d+$/); |
---|
| 139 | } else { |
---|
| 140 | s/\*/$maxseq/o; |
---|
| 141 | m/^(\d+)(?::(\d+))?$/o; |
---|
| 142 | if ($2) { |
---|
| 143 | %unchanged = map { $_ => 1 } ($1 < $2 ? $1 .. $2 : $2 .. $1); |
---|
| 144 | } else { |
---|
| 145 | %unchanged = ($1 => 1); |
---|
| 146 | } |
---|
| 147 | } |
---|
| 148 | send_command "$store_cmd $_ $store_item (\\Deleted)"; |
---|
| 149 | foreach my $msg (sort { $a <=> $b } keys %unchanged) { |
---|
| 150 | print STDERR "$prog: Could not " . |
---|
| 151 | ($undelete ? "un" : "") . "delete $msg\n"; |
---|
| 152 | $exitcode = 1; |
---|
| 153 | } |
---|
| 154 | } |
---|
| 155 | |
---|
| 156 | # Expunge the mailbox if so desired, unless there was an error marking |
---|
| 157 | # any message. |
---|
| 158 | send_command "CLOSE" if ($opt_expunge && ($exitcode == 0)); |
---|
| 159 | |
---|
| 160 | # We are done talking to the IMAP server, close down the connection. |
---|
| 161 | close_connection(); |
---|
| 162 | |
---|
| 163 | exit $exitcode; |
---|
| 164 | |
---|
| 165 | # Subroutine to send a command to the IMAP server, and wait for the |
---|
| 166 | # response; any defined callbacks for the response are invoked. |
---|
| 167 | # If the server response indicates failure, we error out. |
---|
| 168 | sub send_command($) { |
---|
| 169 | print "Sending: $_[0]\n" if $opt_debug; |
---|
| 170 | my ($status, $text) = $client->send('', '', $_[0]); |
---|
| 171 | print "Response: status $status, text $text\n" if $opt_debug; |
---|
| 172 | errorout "Premature end-of-file on IMAP connection to $opt_host" |
---|
| 173 | if $status eq 'EOF'; |
---|
| 174 | close_and_errorout "IMAP error from $opt_host: $text" |
---|
| 175 | if $status ne 'OK'; |
---|
| 176 | } |
---|
| 177 | |
---|
| 178 | # Callback subroutine to parse the FETCH response from a STORE command. |
---|
| 179 | # This callback will be invoked for each message. The "-text" hash |
---|
| 180 | # element contains the text returned by the server. The "-rock" |
---|
| 181 | # element is a reference to the hash containing the message IDs of |
---|
| 182 | # interest; we delete the appropriate key (sequence number or UID) |
---|
| 183 | # from this hash. |
---|
| 184 | sub fetch_callback(@) { |
---|
| 185 | my %cb = @_; |
---|
| 186 | my ($number, $flags); |
---|
| 187 | print "In FETCH callback: msgno $cb{-msgno} text $cb{-text}\n" |
---|
| 188 | if $opt_debug; |
---|
| 189 | $number = $cb{-msgno}; |
---|
| 190 | foreach (split /\r\n/, $cb{-text}) { |
---|
| 191 | $number = $1 if /UID\s+(\d+)/io; |
---|
| 192 | $flags = $1 if /FLAGS\s+\(([^\)]*)\)/io; |
---|
| 193 | } |
---|
| 194 | delete ${$cb{-rock}}{$number}; |
---|
| 195 | my $state = ($flags =~ /\\Deleted\b/io ? "deleted" : "undeleted"); |
---|
| 196 | # Warn if the returned state is not what was expected. |
---|
| 197 | if ($state ne $expect) { |
---|
| 198 | print STDERR "$prog: Warning: Message $number $state\n"; |
---|
| 199 | } else { |
---|
| 200 | # Display an acknowledgement of success if so desired. |
---|
| 201 | print(($opt_by_uid ? "UID" : "Message") . " $number $state\n") |
---|
| 202 | unless $opt_silent; |
---|
| 203 | } |
---|
| 204 | } |
---|
| 205 | |
---|
| 206 | # Callback subroutine to parse a numeric value. The "-rock" hash |
---|
| 207 | # element is a reference to the scalar in which to store the number. |
---|
| 208 | sub number_callback(@) { |
---|
| 209 | my %cb = @_; |
---|
| 210 | print "In number callback: keyword $cb{-keyword}, number $cb{-msgno}\n" |
---|
| 211 | if $opt_debug; |
---|
| 212 | ${$cb{-rock}} = $cb{-msgno}; |
---|
| 213 | } |
---|
| 214 | |
---|
| 215 | # Close the connection to the IMAP server, and error out. |
---|
| 216 | sub close_and_errorout($) { |
---|
| 217 | close_connection(); |
---|
| 218 | errorout $_[0]; |
---|
| 219 | } |
---|
| 220 | |
---|
| 221 | # Logout from the IMAP server, and close the connection. |
---|
| 222 | sub close_connection() { |
---|
| 223 | $client->send('', '', "LOGOUT"); |
---|
[20593] | 224 | # Set the client reference to undef, so that perl invokes the |
---|
| 225 | # destructor, which closes the connection. Note that if we invoke |
---|
| 226 | # the destructor explicitly here, then perl will still invoke it |
---|
| 227 | # again when the program exits, thus touching memory which has |
---|
| 228 | # already been freed. |
---|
| 229 | $client = undef; |
---|
[20054] | 230 | } |
---|
| 231 | |
---|
| 232 | sub errorout($) { |
---|
| 233 | print STDERR "$prog: $_[0]\n"; |
---|
| 234 | exit 1; |
---|
| 235 | } |
---|