source: trunk/athena/bin/from/from.pl @ 22822

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