source: trunk/athena/bin/mitmailutils/mitmaildel.pl @ 24300

Revision 24300, 8.0 KB checked in by geofft, 14 years ago (diff)
In mitmailutils: * Switch from Hesiod to using *.mail.mit.edu so we can use GSSAPI authentication (Trac: #403). * Because of a Cyrus SASL bug (documented in that ticket) regarding parsing long encrypted responses, set maxssf to zero to send mail in the clear. This isn't as terrible as it sounds because mail travels in the clear on the public Internet anyway, and a bunch of client programs (Pine on Athena 9, for instance) don't use encryption, and the major use of mitmailutils is `from` and `mailquota` anyway. Patch based on one from Jonathan Reed <jdreed@mit.edu>.
Line 
1#!/usr/bin/perl -w
2
3# $Id: mitmaildel.pl,v 1.3 2004-07-29 19:11:52 rbasch Exp $
4
5# Delete (or undelete) messages in an IMAP folder.
6
7use strict;
8use warnings FATAL => 'all';
9use Cyrus::IMAP;
10use Getopt::Long;
11
12sub usage(;$);
13sub send_command($);
14sub fetch_callback(@);
15sub number_callback(@);
16sub close_and_errorout($);
17sub close_connection();
18sub errorout($);
19
20my $prog = $0;
21my $undelete = ($prog =~ m/undel$/o);
22
23sub usage(;$) {
24    print STDERR "$prog: $_[0]\n" if ($_[0] && $_[0] ne "help");
25    print STDERR <<EOF;
26Usage: $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
35EOF
36    exit 1;
37}
38
39# Parse the command line arguments.
40use vars qw($opt_by_uid $opt_debug $opt_expunge $opt_host
41            $opt_mailbox $opt_silent);
42
43GetOptions("by-uid",
44           "debug",
45           "expunge",
46           "help" => \&usage,
47           "host=s",
48           "mailbox=s",
49           "silent") || usage;
50
51usage "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'.
56foreach (@ARGV) {
57    errorout "Invalid message specification $_"
58        unless (m/^(?:\d+|\*)(?::(?:\d+|\*))?$/o);
59}
60
61$opt_mailbox = 'INBOX' unless $opt_mailbox;
62
63my $username = $ENV{'ATHENA_USER'} || $ENV{'USER'} || getlogin || (getpwuid($<))[0] ||
64    errorout "Cannot determine user name";
65
66unless ($opt_host) {
67    $opt_host = (gethostbyname("$username.mail.mit.edu"))[0];
68    errorout "Cannot find Post Office server for $username" unless $opt_host;
69}
70errorout "Exchange accounts are not supported yet. Try http://owa.mit.edu/." if $opt_host =~ /EXCHANGE/;
71
72# Connect to the IMAP server, and authenticate.
73my $client = Cyrus::IMAP->new($opt_host) ||
74    errorout "Cannot connect to IMAP server on $opt_host";
75$client->authenticate(-authz => $username, -maxssf => 0) ||
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.
81my $cb_numbered = Cyrus::IMAP::CALLBACK_NUMBERED;
82my $maxseq = 0;
83my $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                      }});
95send_command "SELECT \"$opt_mailbox\"";
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.
99if ($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                          }});
109    send_command "STATUS \"$opt_mailbox\" (UIDNEXT)";
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).
123my %unchanged = ();
124my $expect = ($undelete ? "undeleted" : "deleted");
125my $exitcode = 0;
126my $store_cmd = ($opt_by_uid ? 'UID STORE' : 'STORE');
127my $store_item = ($undelete ? '-FLAGS' : '+FLAGS');
128$client->addcallback({-trigger => 'FETCH', -flags => $cb_numbered,
129                      -callback => \&fetch_callback,
130                      -rock => \%unchanged});
131foreach (@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.
158send_command "CLOSE" if ($opt_expunge && ($exitcode == 0));
159
160# We are done talking to the IMAP server, close down the connection.
161close_connection();
162
163exit $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.
168sub 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.
184sub 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.
208sub 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.
216sub close_and_errorout($) {
217    close_connection();
218    errorout $_[0];
219}
220
221# Logout from the IMAP server, and close the connection.
222sub close_connection() {
223    $client->send('', '', "LOGOUT");
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;
230}
231
232sub errorout($) {
233    print STDERR "$prog: $_[0]\n";
234    exit 1;
235}
Note: See TracBrowser for help on using the repository browser.