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

Revision 24300, 10.6 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>.
  • Property svn:executable set to *
Line 
1#!/usr/bin/perl -w
2
3# $Id: from.pl,v 1.6 2004-07-29 19:11:51 rbasch Exp $
4
5# This is an implementation of the Athena "from" utility using the
6# Perl interface to the Cyrus imclient IMAP library.
7
8use strict;
9use warnings FATAL => 'all';
10use Cyrus::IMAP;
11use Getopt::Std;
12
13sub usage(;$);
14sub send_command($);
15sub search_callback(@);
16sub fetch_callback(@);
17sub number_callback(@);
18sub make_msgspecs(@);
19sub close_connection();
20sub get_localmail();
21sub get_terminal_width();
22sub errorout($);
23
24sub usage(;$) {
25    print STDERR "from: $_[0]\n" if $_[0];
26    print STDERR <<EOF;
27Usage: from [<options>] [<user>]
28  Options:
29    -N            check only NEW messages in IMAP mailbox (default is UNSEEN)
30    -A            check all messages in IMAP mailbox
31    -m <mailbox>  check <mailbox> (default is INBOX)
32    -h <host>     query <host> instead of default post office server
33    -s <sender>   show mail from <sender> only
34    -n            be silent when there is no mail
35    -r            include Subject header
36    -v            include To, Date, and Subject headers
37    -t            display message totals only
38    -p            check post office server only
39    -u            check local mail only
40    -d            turn on debugging
41EOF
42    exit 1;
43}
44
45# By default, we search for UNSEEN messages.  If the user specifies -N,
46# we search for NEW messages (NEW is equivalent to "UNSEEN RECENT").
47# If -A is given, we check ALL messages.
48my $search_key = "unseen";
49
50# Parse the command line arguments.
51my %opts;
52getopts('Adh:m:Nnprs:tuv', \%opts) || usage;
53my $have_user = 0;
54my $username = shift @ARGV;
55if ($username) {
56    $have_user = 1;
57} else {
58    $username = $ENV{"ATHENA_USER"} || $ENV{'USER'} || getlogin || (getpwuid($<))[0] ||
59        errorout "Cannot determine user name";
60}
61usage "Too many arguments" if @ARGV > 0;
62my $checkall = $opts{'A'} && ($search_key = "all");
63my $debug = $opts{'d'};
64my $host = $opts{'h'} || (gethostbyname("$username.mail.mit.edu"))[0];
65errorout "Cannot find Post Office server for $username" unless $host;
66errorout "Exchange accounts are not supported yet. Try http://owa.mit.edu/." if $host =~ /EXCHANGE/;
67my $mbox = $opts{'m'} || "INBOX";
68my $quiet = $opts{'n'};
69my $checknew = $opts{'N'} && ($search_key = "new");
70my $imaponly = $opts{'p'};
71my $report = $opts{'r'};
72my $sender = $opts{'s'};
73my $totals_only = $opts{'t'};
74my $localonly = $opts{'u'};
75my $verbose = $opts{'v'};
76usage "Cannot specify both -A and -N" if $checkall && $checknew;
77usage "Cannot specify both -p and -u" if $imaponly && $localonly;
78usage "Cannot specify both -r and -t" if $report && $totals_only;
79usage "Cannot specify both -t and -v" if $totals_only && $verbose;
80
81# Check local mail first.
82my $localcount = 0;
83$localcount = get_localmail() unless $imaponly;
84
85exit 0 if $localonly;
86
87# Check mail on the IMAP server.
88# Connect to the server, and authenticate.
89my $client = Cyrus::IMAP->new($host) ||
90    errorout "Cannot connect to IMAP server on $host";
91unless ($client->authenticate(-authz => $username, -maxssf => 0)) {
92    close_connection();
93    errorout "Cannot authenticate to $host";
94}
95
96# Examine the mailbox.  This gives the numbers of existing and recent
97# messages, as well as selecting the mailbox for read-only access.
98my $recentmsgcount = -1;
99my $totalmsgcount = -1;
100my @msgids = ();
101my @pomsgs = ();
102my $cb_numbered = Cyrus::IMAP::CALLBACK_NUMBERED;
103$client->addcallback({-trigger => 'EXISTS', -flags => $cb_numbered,
104                      -callback => \&number_callback,
105                      -rock => \$totalmsgcount});
106$client->addcallback({-trigger => 'RECENT', -flags => $cb_numbered,
107                      -callback => \&number_callback,
108                      -rock => \$recentmsgcount});
109send_command "EXAMINE \"$mbox\"";
110
111if ($totalmsgcount && !($checknew && !$recentmsgcount)) {
112    # Search the mailbox to obtain the message UID's.
113    $client->addcallback({-trigger => 'SEARCH',
114                          -callback => \&search_callback,
115                          -rock => \@msgids});
116    send_command "UID SEARCH $search_key" . ($sender ? " FROM $sender" : "");
117
118    # If there are messages of interest, fetch their size, and any desired
119    # headers.
120    if (@msgids > 0) {
121        my $fetch = "RFC822.SIZE";
122        $fetch .= " BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)]"
123            unless $totals_only;
124        $client->addcallback({-trigger => 'FETCH', -flags => $cb_numbered,
125                              -callback => \&fetch_callback,
126                              -rock => \@pomsgs});
127        foreach (make_msgspecs(@msgids)) {
128            send_command "UID FETCH $_ ($fetch)";
129        }
130    }
131}
132my $msgcount = @pomsgs;
133
134# We are done talking to the IMAP server, close down the connection.
135close_connection();
136
137my $msg;
138
139# Print out the summary line if appropriate.
140if (($verbose || $totals_only) && ($msgcount > 0 || !$quiet)) {
141    my $totalsize = 0;
142    for $msg (@pomsgs) {
143        $totalsize += $msg->{size};
144    }
145
146    print $have_user ? "$username has " : "You have ";
147    if ($msgcount > 0) {
148        print "$msgcount " .
149            ($checkall ? "total" : $search_key) . " message" .
150            ($msgcount > 1 ? 's' : '') .
151            " ($totalsize bytes)" .
152            ($checkall ? "" : ", $totalmsgcount total,");
153    } else {
154        print "no" .
155            ($checkall || $totalmsgcount == 0 ? "" : " $search_key") .
156            " messages";
157    }
158    print " in $mbox on $host" .
159        ($verbose && $msgcount > 0 ? ':' : '.') . "\n";
160}
161
162# Show the desired headers if appropriate.
163if (!$totals_only && $msgcount > 0) {
164    my $subject_width;
165
166    print ucfirst(($checkall ? "" : "$search_key ") .
167                  "mail in IMAP folder $mbox:\n") unless $verbose || $imaponly;
168    if ($report) {
169        my $tty_width = get_terminal_width();
170        $subject_width = ($tty_width > 33 ? $tty_width - 33 : 0);
171    }
172    for $msg (@pomsgs) {
173        if ($report) {
174            printf("%-30.30s ", $msg->{from});
175            print substr($msg->{subject}, 0, $subject_width)
176                if $msg->{subject} && $subject_width;
177            print "\n";
178        } else {
179            if ($verbose) {
180                print "\n";
181                print "To: $msg->{to}\n" if $msg->{to};
182                print "Subject: $msg->{subject}\n" if $msg->{subject};
183                print "Date: $msg->{date}\n" if $msg->{date};
184            }
185            print "From: $msg->{from}\n";
186        }
187    }
188}
189
190# Subroutine to send a command to the IMAP server, and wait for the
191# response; any defined callbacks for the response are invoked.
192# If the server response indicates failure, we error out.
193sub send_command($) {
194    print "Sending: $_[0]\n" if $debug;
195    my ($status, $text) = $client->send('', '', $_[0]);
196    print "Response: status $status, text $text\n" if $debug;
197    errorout "Premature end-of-file on IMAP connection to $host"
198        if $status eq 'EOF';
199    if ($status ne 'OK') {
200        close_connection();
201        errorout "IMAP error for $mbox on $host: $text"
202    }
203}
204
205# Callback subroutine to parse the response from a SEARCH command.
206# The "-text" hash element contains the returned message UIDs,
207# separated by a space.  The "-rock" element is a reference to the
208# array in which to store the UIDs.
209sub search_callback(@) {
210    my %cb = @_;
211    print "In SEARCH callback: text $cb{-text}\n" if $debug;
212    @{$cb{-rock}} = split(/\s/, $cb{-text});
213}
214
215# Callback subroutine to parse the response from a FETCH command.
216# This callback will be invoked for each message.  The "-text" hash
217# element contains the text returned by the server.  The "-rock"
218# element is a reference to the array in which to push a hash of the
219# various message data items.
220sub fetch_callback(@) {
221    my %cb = @_;
222    my ($from, $to, $subject, $date) = '';
223    my $size = 0;
224    print "In FETCH callback: text $cb{-text}\n" if $debug;
225    for (split /\r\n/, $cb{-text}) {
226        $size = $1 if /RFC822.SIZE\s+(\d+)/io;
227        $from = $_ if s/^From:\s*//io;
228        $to = $_ if s/^To:\s*//io;
229        $subject = $_ if s/^Subject:\s*//io;
230        $date = $_ if s/^Date:\s*//io;
231       
232    }
233    push @{$cb{-rock}}, {from => $from, to => $to, subject => $subject,
234                         date => $date, size => $size};
235}
236
237# Callback subroutine to parse a numeric value.  The "-rock" hash
238# element is a reference to the scalar in which to store the number.
239sub number_callback(@) {
240    my %cb = @_;
241    print "In number callback: keyword $cb{-keyword}, number $cb{-msgno}\n"
242        if $debug;
243    ${$cb{-rock}} = $cb{-msgno};
244}
245
246# This subroutine takes a list of IMAP message UID numbers, and constructs
247# single-string representations of the set, collapsing sequences into
248# ranges where possible.  In order to avoid constructing a specification
249# which is too long to be processed, the result is returned as an array
250# of manageably-sized specification strings, currently limited to about
251# 200 characters each.
252sub make_msgspecs(@) {
253    return '' if @_ == 0;
254    my @specs = ();
255    my $first = shift(@_);
256    my $last = $first;
257    my $spec = $first;
258    foreach (@_) {
259        if ($_ != $last + 1) {
260            # This UID is not in sequence with the previous element.
261            # If that marks the end of a range, complete it.
262            $spec .= ":$last" if ($first != $last);
263            # Begin a new sequence.  Create another spec string if the
264            # current one is getting long.
265            if (length($spec) > 200) {
266                push @specs, $spec;
267                $spec = $_;
268            } else {
269                $spec .= ",$_";
270            }
271            $first = $_;
272        }
273        $last = $_;
274    }
275    # Complete the final range if necessary.
276    $spec .= ":$last" if ($first != $last);
277    push @specs, $spec if ($spec);
278    return @specs;
279}   
280
281# Logout from the IMAP server, and close the connection.
282sub close_connection() {
283    $client->send('', '', "LOGOUT");
284    # Set the client reference to undef, so that perl invokes the
285    # destructor, which closes the connection.  Note that if we invoke
286    # the destructor explicitly here, then perl will still invoke it
287    # again when the program exits, thus touching memory which has
288    # already been freed.
289    $client = undef;
290}
291
292# Get mail from the local ("Unix") mail drop.
293# Returns the number of messages found.
294sub get_localmail() {
295    my $maildrop = $ENV{'MAILDROP'} || "/var/spool/mail/$username";
296    # Open the mail drop.
297    unless (open MAIL, $maildrop) {
298        errorout "Cannot open maildrop $maildrop" if $localonly;
299        return 0;
300    }
301    my $count = 0;
302    my $from = '';
303    while (<MAIL>) {
304        chop;
305        if ($_ eq '' && $from) {
306            print "$from\n" unless $totals_only;
307            $count++;
308            $from = '';
309        }
310        elsif (/^From\s+([^\s\t]*)/o) {
311            next if $sender && $1 !~ /$sender/io;
312            print "Local mail:\n"
313                unless ($count > 0 || $totals_only || $localonly);
314            $from = $_;
315        }
316    }
317    if ($from) {
318        print "$from\n" unless $totals_only;
319        $count++;
320    }
321    if ($totals_only && $count) {
322        my $size = -s MAIL;
323        print $have_user ? "$username has" : "You have";
324        print " $count local messages ($size bytes).\n";
325    }
326    close(MAIL);
327    return $count;
328}   
329
330sub get_terminal_width() {
331    my $columns = 80;
332    open STTY, "stty -a |" or return $columns;
333    while (<STTY>) {
334        if (/columns[\s=]+(\d+);/o) {
335            $columns = $1;
336            last;
337        }
338    }
339    close STTY;
340    return $columns;
341}
342
343sub errorout($) {
344    print STDERR "from: $_[0]\n";
345    exit 1;
346}
Note: See TracBrowser for help on using the repository browser.