source: trunk/third/sendmail/contrib/expn.pl @ 19204

Revision 19204, 36.0 KB checked in by zacheiss, 22 years ago (diff)
This commit was generated by cvs2svn to compensate for changes in r19203, which included commits to RCS files with non-trunk default branches.
  • Property svn:executable set to *
Line 
1#!/usr/bin/perl
2'di ';
3'ds 00 \\"';
4'ig 00 ';
5#
6#       THIS PROGRAM IS ITS OWN MANUAL PAGE.  INSTALL IN man & bin.
7#
8
9use 5.001;
10use IO::Socket;
11
12# system requirements:
13#       must have 'nslookup' and 'hostname' programs.
14
15# $OrigHeader: /home/muir/bin/RCS/expn,v 3.11 1997/09/10 08:14:02 muir Exp muir $
16
17# TODO:
18#       less magic should apply to command-line addresses
19#       less magic should apply to local addresses
20#       add magic to deal with cross-domain cnames
21#       disconnect & reconnect after 25 commands to the same sendmail 8.8.* host
22
23# Checklist: (hard addresses)
24#       250 Kimmo Suominen <"|/usr/local/mh/lib/slocal -user kim"@grendel.tac.nyc.ny.us>
25#       harry@hofmann.cs.Berkeley.EDU -> harry@tenet (.berkeley.edu)  [dead]
26#       bks@cs.berkeley.edu -> shiva.CS (.berkeley.edu)               [dead]
27#       dan@tc.cornell.edu -> brown@tiberius (.tc.cornell.edu)
28
29#############################################################################
30#
31#  Copyright (c) 1993 David Muir Sharnoff
32#  All rights reserved.
33#
34#  Redistribution and use in source and binary forms, with or without
35#  modification, are permitted provided that the following conditions
36#  are met:
37#  1. Redistributions of source code must retain the above copyright
38#     notice, this list of conditions and the following disclaimer.
39#  2. Redistributions in binary form must reproduce the above copyright
40#     notice, this list of conditions and the following disclaimer in the
41#     documentation and/or other materials provided with the distribution.
42#  3. All advertising materials mentioning features or use of this software
43#     must display the following acknowledgement:
44#       This product includes software developed by the David Muir Sharnoff.
45#  4. The name of David Sharnoff may not be used to endorse or promote products
46#     derived from this software without specific prior written permission.
47#
48#  THIS SOFTWARE IS PROVIDED BY THE DAVID MUIR SHARNOFF ``AS IS'' AND
49#  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
50#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
51#  ARE DISCLAIMED.  IN NO EVENT SHALL DAVID MUIR SHARNOFF BE LIABLE
52#  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
53#  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
54#  OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
55#  HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
56#  LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
57#  OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
58#  SUCH DAMAGE.
59#
60# This copyright notice derrived from material copyrighted by the Regents
61# of the University of California.
62#
63# Contributions accepted.
64#
65#############################################################################
66
67# overall structure:
68#       in an effort to not trace each address individually, but rather
69#       ask each server in turn a whole bunch of questions, addresses to
70#       be expanded are queued up.
71#
72#       This means that all accounting w.r.t. an address must be stored in
73#       various arrays.  Generally these arrays are indexed by the
74#       string "$addr *** $server" where $addr is the address to be
75#       expanded "foo" or maybe "foo@bar" and $server is the hostname
76#       of the SMTP server to contact.
77#
78
79# important global variables:
80#
81# @hosts : list of servers still to be contacted
82# $server : name of the current we are currently looking at
83# @users = $users{@hosts[0]} : addresses to expand at this server
84# $u = $users[0] : the current address being expanded
85# $names{"$users[0] *** $server"} : the 'name' associated with the address
86# $mxbacktrace{"$users[0] *** $server"} : record of mx expansion
87# $mx_secondary{$server} : other mx relays at the same priority
88# $domainify_fallback{"$users[0] *** $server"} : alternative names to try
89#       instead of $server if $server doesn't work
90# $temporary_redirect{"$users[0] *** $server"} : when trying alternates,
91#       temporarily channel all tries along current path
92# $giveup{$server} : do not bother expanding addresses at $server
93# $verbose : -v
94# $watch : -w
95# $vw : -v or -w
96# $debug : -d
97# $valid : -a
98# $levels : -1
99# $S : the socket connection to $server
100
101$have_nslookup = 1;     # we have the nslookup program
102$port = 'smtp';
103$av0 = $0;
104$ENV{'PATH'} .= ":/usr/etc" unless $ENV{'PATH'} =~ m,/usr/etc,;
105$ENV{'PATH'} .= ":/usr/ucb" unless $ENV{'PATH'} =~ m,/usr/ucb,;
106select(STDERR);
107
108$0 = "$av0 - running hostname";
109chop($name = `hostname || uname -n`);
110
111$0 = "$av0 - lookup host FQDN and IP addr";
112($hostname,$aliases,$type,$len,$thisaddr) = gethostbyname($name);
113
114$0 = "$av0 - parsing args";
115$usage = "Usage: $av0 [-1avwd] user[\@host] [user2[host2] ...]";
116for $a (@ARGV) {
117        die $usage if $a eq "-";
118        while ($a =~ s/^(-.*)([1avwd])/$1/) {
119                eval '$'."flag_$2 += 1";
120        }
121        next if $a eq "-";
122        die $usage if $a =~ /^-/;
123        &expn(&parse($a,$hostname,undef,1));
124}
125$verbose = $flag_v;
126$watch = $flag_w;
127$vw = $flag_v + $flag_w;
128$debug = $flag_d;
129$valid = $flag_a;
130$levels = $flag_1;
131
132die $usage unless @hosts;
133if ($valid) {
134        if ($valid == 1) {
135                $validRequirement = 0.8;
136        } elsif ($valid == 2) {
137                $validRequirement = 1.0;
138        } elsif ($valid == 3) {
139                $validRequirement = 0.9;
140        } else {
141                $validRequirement = (1 - (1/($valid-3)));
142                print "validRequirement = $validRequirement\n" if $debug;
143        }
144}
145
146HOST:
147while (@hosts) {
148        $server = shift(@hosts);
149        @users = split(' ',$users{$server});
150        delete $users{$server};
151
152        # is this server already known to be bad?
153        $0 = "$av0 - looking up $server";
154        if ($giveup{$server}) {
155                &giveup('mx domainify',$giveup{$server});
156                next;
157        }
158
159        # do we already have an mx record for this host?
160        next HOST if &mxredirect($server,*users);
161
162        # look it up, or try for an mx.
163        $0 = "$av0 - gethostbyname($server)";
164
165        ($name,$aliases,$type,$len,$thataddr) = gethostbyname($server);
166        # if we can't get an A record, try for an MX record.
167        unless($thataddr) {
168                &mxlookup(1,$server,"$server: could not resolve name",*users);
169                next HOST;
170        }
171                               
172        # get a connection, or look for an mx
173        $0 = "$av0 - socket to $server";
174
175        $S = new IO::Socket::INET (
176                'PeerAddr' => $server,
177                'PeerPort' => $port,
178                'Proto' => 'tcp');
179
180        if (! $S || ($debug == 10 && $server =~ /relay\d.UU.NET$/i)) {
181                $0 = "$av0 - $server: could not connect: $!\n";
182                $emsg = $!;
183                unless (&mxlookup(0,$server,"$server: could not connect: $!",*users)) {
184                        &giveup('mx',"$server: Could not connect: $emsg");
185                }
186                next HOST;
187        }
188        $S->autoflush(1);
189
190        # read the greeting
191        $0 = "$av0 - talking to $server";
192        &alarm("greeting with $server",'');
193        while(<$S>) {
194                alarm(0);
195                print if $watch;
196                if (/^(\d+)([- ])/) {
197                        if ($1 != 220) {
198                                $0 = "$av0 - bad numeric response from $server";
199                                &alarm("giving up after bad response from $server",'');
200                                &read_response($2,$watch);
201                                alarm(0);
202                                print STDERR "$server: NOT 220 greeting: $_"
203                                        if ($debug || $vw);
204                                if (&mxlookup(0,$server,"$server: did not respond with a 220 greeting",*users)) {
205                                        close($S);
206                                        next HOST;
207                                }
208                        }
209                        last if ($2 eq " ");
210                } else {
211                        $0 = "$av0 - bad response from $server";
212                        print STDERR "$server: NOT 220 greeting: $_"
213                                if ($debug || $vw);
214                        unless (&mxlookup(0,$server,"$server: did not respond with SMTP codes",*users)) {
215                                &giveup('',"$server: did not talk SMTP");
216                        }
217                        close($S);
218                        next HOST;
219                }
220                &alarm("greeting with $server",'');
221        }
222        alarm(0);
223       
224        # if this causes problems, remove it
225        $0 = "$av0 - sending helo to $server";
226        &alarm("sending helo to $server","");
227        &ps("helo $hostname");
228        while(<$S>) {
229                print if $watch;
230                last if /^\d+ /;
231        }
232        alarm(0);
233
234        # try the users, one by one
235        USER:
236        while(@users) {
237                $u = shift(@users);
238                $0 = "$av0 - expanding $u [\@$server]";
239
240                # do we already have a name for this user?
241                $oldname = $names{"$u *** $server"};
242
243                print &compact($u,$server)." ->\n" if ($verbose && ! $valid);
244                if ($valid) {
245                        #
246                        # when running with -a, we delay taking any action
247                        # on the results of our query until we have looked
248                        # at the complete output.  @toFinal stores expansions
249                        # that will be final if we take them.  @toExpn stores
250                        # expnansions that are not final.  @isValid keeps
251                        # track of our ability to send mail to each of the
252                        # expansions.
253                        #
254                        @isValid = ();
255                        @toFinal = ();
256                        @toExpn = ();
257                }
258
259#               ($ecode,@expansion) = &expn_vrfy($u,$server);
260                (@foo) = &expn_vrfy($u,$server);
261                ($ecode,@expansion) = @foo;
262                if ($ecode) {
263                        &giveup('',$ecode,$u);
264                        last USER;
265                }
266
267                for $s (@expansion) {
268                        $s =~ s/[\n\r]//g;
269                        $0 = "$av0 - parsing $server: $s";
270
271                        $skipwatch = $watch;
272
273                        if ($s =~ /^[25]51([- ]).*<(.+)>/) {
274                                print "$s" if $watch;
275                                print "(pretending 250$1<$2>)" if ($debug && $watch);
276                                print "\n" if $watch;
277                                $s = "250$1<$2>";
278                                $skipwatch = 0;
279                        }
280
281                        if ($s =~ /^250([- ])(.+)/) {
282                                print "$s\n" if $skipwatch;
283                                ($done,$addr) = ($1,$2);
284                                ($newhost, $newaddr, $newname) =  &parse($addr,$server,$oldname, $#expansion == 0);
285                                print "($newhost, $newaddr, $newname) = &parse($addr, $server, $oldname)\n" if $debug;
286                                if (! $newhost) {
287                                        # no expansion is possible w/o a new server to call
288                                        if ($valid) {
289                                                push(@isValid, &validAddr($newaddr));
290                                                push(@toFinal,$newaddr,$server,$newname);
291                                        } else {
292                                                &verbose(&final($newaddr,$server,$newname));
293                                        }
294                                } else {
295                                        $newmxhost = &mx($newhost,$newaddr);
296                                        print "$newmxhost = &mx($newhost)\n"
297                                                if ($debug && $newhost ne $newmxhost);
298                                        $0 = "$av0 - parsing $newaddr [@$newmxhost]";
299                                        print "levels = $levels, level{$u *** $server} = ".$level{"$u *** $server"}."\n" if ($debug > 1);
300                                        # If the new server is the current one,
301                                        # it would have expanded things for us
302                                        # if it could have.  Mx records must be
303                                        # followed to compare server names.
304                                        # We are also done if the recursion
305                                        # count has been exceeded.
306                                        if (&trhost($newmxhost) eq &trhost($server) || ($levels && $level{"$u *** $server"} >= $levels)) {
307                                                if ($valid) {
308                                                        push(@isValid, &validAddr($newaddr));
309                                                        push(@toFinal,$newaddr,$newmxhost,$newname);
310                                                } else {
311                                                        &verbose(&final($newaddr,$newmxhost,$newname));
312                                                }
313                                        } else {
314                                                # more work to do...
315                                                if ($valid) {
316                                                        push(@isValid, &validAddr($newaddr));
317                                                        push(@toExpn,$newmxhost,$newaddr,$newname,$level{"$u *** $server"});
318                                                } else {
319                                                        &verbose(&expn($newmxhost,$newaddr,$newname,$level{"$u *** $server"}));
320                                                }
321                                        }
322                                }
323                                last if ($done eq " ");
324                                next;
325                        }
326                        # 550 is a known code...  Should the be
327                        # included in -a output?  Might be a bug
328                        # here.  Does it matter?  Can assume that
329                        # there won't be UNKNOWN USER responses
330                        # mixed with valid users?
331                        if ($s =~ /^(550)([- ])/) {
332                                if ($valid) {
333                                        print STDERR "\@$server:$u ($oldname) USER UNKNOWN\n";
334                                } else {
335                                        &verbose(&final($u,$server,$oldname,"USER UNKNOWN"));
336                                }
337                                last if ($2 eq " ");
338                                next;
339                        }
340                        # 553 is a known code... 
341                        if ($s =~ /^(553)([- ])/) {
342                                if ($valid) {
343                                        print STDERR "\@$server:$u ($oldname) USER AMBIGUOUS\n";
344                                } else {
345                                        &verbose(&final($u,$server,$oldname,"USER AMBIGUOUS"));
346                                }
347                                last if ($2 eq " ");
348                                next;
349                        }
350                        # 252 is a known code... 
351                        if ($s =~ /^(252)([- ])/) {
352                                if ($valid) {
353                                        print STDERR "\@$server:$u ($oldname) REFUSED TO VRFY\n";
354                                } else {
355                                        &verbose(&final($u,$server,$oldname,"REFUSED TO VRFY"));
356                                }
357                                last if ($2 eq " ");
358                                next;
359                        }
360                        &giveup('',"$server: did not grok '$s'",$u);
361                        last USER;
362                }
363
364                if ($valid) {
365                        #
366                        # now we decide if we are going to take these
367                        # expansions or roll them back.
368                        #
369                        $avgValid = &average(@isValid);
370                        print "avgValid = $avgValid\n" if $debug;
371                        if ($avgValid >= $validRequirement) {
372                                print &compact($u,$server)." ->\n" if $verbose;
373                                while (@toExpn) {
374                                        &verbose(&expn(splice(@toExpn,0,4)));
375                                }
376                                while (@toFinal) {
377                                        &verbose(&final(splice(@toFinal,0,3)));
378                                }
379                        } else {
380                                print "Tossing some valid to avoid invalid ".&compact($u,$server)."\n" if ($avgValid > 0.0 && ($vw || $debug));
381                                print &compact($u,$server)." ->\n" if $verbose;
382                                &verbose(&final($u,$server,$newname));
383                        }
384                }
385        }
386
387        &alarm("sending 'quit' to $server",'');
388        $0 = "$av0 - sending 'quit' to $server";
389        &ps("quit");
390        while(<$S>) {
391                print if $watch;
392                last if /^\d+ /;
393        }
394        close($S);
395        alarm(0);
396}
397
398$0 = "$av0 - printing final results";
399print "----------\n" if $vw;
400select(STDOUT);
401for $f (sort @final) {
402        print "$f\n";
403}
404unlink("/tmp/expn$$");
405exit(0);
406
407
408# abandon all attempts deliver to $server
409# register the current addresses as the final ones
410sub giveup
411{
412        local($redirect_okay,$reason,$user) = @_;
413        local($us,@so,$nh,@remaining_users);
414        local($pk,$file,$line);
415        ($pk, $file, $line) = caller;
416
417        $0 = "$av0 - giving up on $server: $reason";
418        #
419        # add back a user if we gave up in the middle
420        #
421        push(@users,$user) if $user;
422        #
423        # don't bother with this system anymore
424        #
425        unless ($giveup{$server}) {
426                $giveup{$server} = $reason;
427                print STDERR "$reason\n";
428        }
429        print "Giveup at $file:$line!!! redirect okay = $redirect_okay; $reason\n" if $debug;
430        #
431        # Wait!
432        # Before giving up, see if there is a chance that
433        # there is another host to redirect to!
434        # (Kids, don't do this at home!  Hacking is a dangerous
435        # crime and you could end up behind bars.)
436        #
437        for $u (@users) {
438                if ($redirect_okay =~ /\bmx\b/) {
439                        next if &try_fallback('mx',$u,*server,
440                                *mx_secondary,
441                                *already_mx_fellback);
442                }
443                if ($redirect_okay =~ /\bdomainify\b/) {
444                        next if &try_fallback('domainify',$u,*server,
445                                *domainify_fallback,
446                                *already_domainify_fellback);
447                }
448                push(@remaining_users,$u);
449        }
450        @users = @remaining_users;
451        for $u (@users) {
452                print &compact($u,$server)." ->\n" if ($verbose && $valid && $u);
453                &verbose(&final($u,$server,$names{"$u *** $server"},$reason));
454        }
455}
456#
457# This routine is used only within &giveup.  It checks to
458# see if we really have to giveup or if there is a second
459# chance because we did something before that can be
460# backtracked.
461#
462# %fallback{"$user *** $host"} tracks what is able to fallback
463# %fellback{"$user *** $host"} tracks what has fallen back
464#
465# If there is a valid backtrack, then queue up the new possibility
466#
467sub try_fallback
468{
469        local($method,$user,*host,*fall_table,*fellback) = @_;
470        local($us,$fallhost,$oldhost,$ft,$i);
471
472        if ($debug > 8) {
473                print "Fallback table $method:\n";
474                for $i (sort keys %fall_table) {
475                        print "\t'$i'\t\t'$fall_table{$i}'\n";
476                }
477                print "Fellback table $method:\n";
478                for $i (sort keys %fellback) {
479                        print "\t'$i'\t\t'$fellback{$i}'\n";
480                }
481                print "U: $user H: $host\n";
482        }
483       
484        $us = "$user *** $host";
485        if (defined $fellback{$us}) {
486                #
487                # Undo a previous fallback so that we can try again
488                # Nested fallbacks are avoided because they could
489                # lead to infinite loops
490                #
491                $fallhost = $fellback{$us};
492                print "Already $method fell back from $us -> \n" if $debug;
493                $us = "$user *** $fallhost";
494                $oldhost = $fallhost;
495        } elsif (($method eq 'mx') && (defined $mxbacktrace{$us}) && (defined $mx_secondary{$mxbacktrace{$us}})) {
496                print "Fallback an MX expansion $us -> \n" if $debug;
497                $oldhost = $mxbacktrace{$us};
498        } else {
499                print "Oldhost($host, $us) = " if $debug;
500                $oldhost = $host;
501        }
502        print "$oldhost\n" if $debug;
503        if (((defined $fall_table{$us}) && ($ft = $us)) || ((defined $fall_table{$oldhost}) && ($ft = $oldhost))) {
504                print "$method Fallback = ".$fall_table{$ft}."\n" if $debug;
505                local(@so,$newhost);
506                @so = split(' ',$fall_table{$ft});
507                $newhost = shift(@so);
508                print "Falling back ($method) $us -> $newhost (from $oldhost)\n" if $debug;
509                if ($method eq 'mx') {
510                        if (! defined ($mxbacktrace{"$user *** $newhost"})) {
511                                if (defined $mxbacktrace{"$user *** $oldhost"}) {
512                                        print "resetting oldhost $oldhost to the original: " if $debug;
513                                        $oldhost = $mxbacktrace{"$user *** $oldhost"};
514                                        print "$oldhost\n" if $debug;
515                                }
516                                $mxbacktrace{"$user *** $newhost"} = $oldhost;
517                                print "mxbacktrace $user *** $newhost -> $oldhost\n" if $debug;
518                        }
519                        $mx{&trhost($oldhost)} = $newhost;
520                } else {
521                        $temporary_redirect{$us} = $newhost;
522                }
523                if (@so) {
524                        print "Can still $method  $us: @so\n" if $debug;
525                        $fall_table{$ft} = join(' ',@so);
526                } else {
527                        print "No more fallbacks for $us\n" if $debug;
528                        delete $fall_table{$ft};
529                }
530                if (defined $create_host_backtrack{$us}) {
531                        $create_host_backtrack{"$user *** $newhost"}
532                                = $create_host_backtrack{$us};
533                }
534                $fellback{"$user *** $newhost"} = $oldhost;
535                &expn($newhost,$user,$names{$us},$level{$us});
536                return 1;
537        }
538        delete $temporary_redirect{$us};
539        $host = $oldhost;
540        return 0;
541}
542# return 1 if you could send mail to the address as is.
543sub validAddr
544{
545        local($addr) = @_;
546        $res = &do_validAddr($addr);
547        print "validAddr($addr) = $res\n" if $debug;
548        $res;
549}
550sub do_validAddr
551{
552        local($addr) = @_;
553        local($urx) = "[-A-Za-z_.0-9+]+";
554
555        # \u
556        return 0 if ($addr =~ /^\\/);
557        # ?@h
558        return 1 if ($addr =~ /.\@$urx$/);
559        # @h:?
560        return 1 if ($addr =~ /^\@$urx\:./);
561        # h!u
562        return 1 if ($addr =~ /^$urx!./);
563        # u
564        return 1 if ($addr =~ /^$urx$/);
565        # ?
566        print "validAddr($addr) = ???\n" if $debug;
567        return 0;
568}
569# Some systems use expn and vrfy interchangeably.  Some only
570# implement one or the other.  Some check expn against mailing
571# lists and vrfy against users.  It doesn't appear to be
572# consistent.
573#
574# So, what do we do?  We try everything!
575#
576#
577# Ranking of result codes: good: 250, 251/551, 252, 550, anything else
578#
579# Ranking of inputs: best: user@host.domain, okay: user
580#
581# Return value: $error_string, @responses_from_server
582sub expn_vrfy
583{
584        local($u,$server) = @_;
585        local(@c) = ('expn', 'vrfy');
586        local(@try_u) = $u;
587        local(@ret,$code);
588
589        if (($u =~ /(.+)@(.+)/) && (&trhost($2) eq &trhost($server))) {
590                push(@try_u,$1);
591        }
592
593        TRY:
594        for $c (@c) {
595                for $try_u (@try_u) {
596                        &alarm("${c}'ing $try_u on $server",'',$u);
597                        &ps("$c $try_u");
598                        alarm(0);
599                        $s = <$S>;
600                        if ($s eq '') {
601                                return "$server: lost connection";
602                        }
603                        if ($s !~ /^(\d+)([- ])/) {
604                                return "$server: garbled reply to '$c $try_u'";
605                        }
606                        if ($1 == 250) {
607                                $code = 250;
608                                @ret = ("",$s);
609                                push(@ret,&read_response($2,$debug));
610                                return (@ret);
611                        }
612                        if ($1 == 551 || $1 == 251) {
613                                $code = $1;
614                                @ret = ("",$s);
615                                push(@ret,&read_response($2,$debug));
616                                next;
617                        }
618                        if ($1 == 252 && ($code == 0 || $code == 550)) {
619                                $code = 252;
620                                @ret = ("",$s);
621                                push(@ret,&read_response($2,$watch));
622                                next;
623                        }
624                        if ($1 == 550 && $code == 0) {
625                                $code = 550;
626                                @ret = ("",$s);
627                                push(@ret,&read_response($2,$watch));
628                                next;
629                        }
630                        &read_response($2,$watch);
631                }
632        }
633        return "$server: expn/vrfy not implemented" unless @ret;
634        return @ret;
635}
636# sometimes the old parse routine (now parse2) didn't
637# reject funky addresses.
638sub parse
639{
640        local($oldaddr,$server,$oldname,$one_to_one) = @_;
641        local($newhost, $newaddr, $newname, $um) =  &parse2($oldaddr,$server,$oldname,$one_to_one);
642        if ($newaddr =~ m,^["/],) {
643                return (undef, $oldaddr, $newname) if $valid;
644                return (undef, $um, $newname);
645        }
646        return ($newhost, $newaddr, $newname);
647}
648
649# returns ($new_smtp_server,$new_address,$new_name)
650# given a response from a SMTP server ($newaddr), the
651# current host ($server), the old "name" and a flag that
652# indicates if it is being called during the initial
653# command line parsing ($parsing_args)
654sub parse2
655{
656        local($newaddr,$context_host,$old_name,$parsing_args) = @_;
657        local(@names) = $old_name;
658        local($urx) = "[-A-Za-z_.0-9+]+";
659        local($unmangle);
660
661        #
662        # first, separate out the address part.
663        #
664
665        #
666        # [NAME] <ADDR [(NAME)]>
667        # [NAME] <[(NAME)] ADDR
668        # ADDR [(NAME)]
669        # (NAME) ADDR
670        # [(NAME)] <ADDR>
671        #
672        if ($newaddr =~ /^\<(.*)\>$/) {
673                print "<A:$1>\n" if $debug;
674                ($newaddr) = &trim($1);
675                print "na = $newaddr\n" if $debug;
676        }
677        if ($newaddr =~ /^([^\<\>]*)\<([^\<\>]*)\>([^\<\>]*)$/) {
678                # address has a < > pair in it.
679                print "N:$1 <A:$2> N:$3\n" if $debug;
680                ($newaddr) = &trim($2);
681                unshift(@names, &trim($3,$1));
682                print "na = $newaddr\n" if $debug;
683        }
684        if ($newaddr =~ /^([^\(\)]*)\(([^\(\)]*)\)([^\(\)]*)$/) {
685                # address has a ( ) pair in it.
686                print "A:$1 (N:$2) A:$3\n" if $debug;
687                unshift(@names,&trim($2));
688                local($f,$l) = (&trim($1),&trim($3));
689                if (($f && $l) || !($f || $l)) {
690                        # address looks like:
691                        # foo (bar) baz  or (bar)
692                        # not allowed!
693                        print STDERR "Could not parse $newaddr\n" if $vw;
694                        return(undef,$newaddr,&firstname(@names));
695                }
696                $newaddr = $f if $f;
697                $newaddr = $l if $l;
698                print "newaddr now = $newaddr\n" if $debug;
699        }
700        #
701        # @foo:bar
702        # j%k@l
703        # a@b
704        # b!a
705        # a
706        #
707        $unmangle = $newaddr;
708        if ($newaddr =~ /^\@($urx)\:(.+)$/) {
709                print "(\@:)" if $debug;
710                # this is a bit of a cheat, but it seems necessary
711                return (&domainify($1,$context_host,$2),$2,&firstname(@names),$unmangle);
712        }
713        if ($newaddr =~ /^(.+)\@($urx)$/) {
714                print "(\@)" if $debug;
715                return (&domainify($2,$context_host,$newaddr),$newaddr,&firstname(@names),$unmangle);
716        }
717        if ($parsing_args) {
718                if ($newaddr =~ /^($urx)\!(.+)$/) {
719                        return (&domainify($1,$context_host,$newaddr),$newaddr,&firstname(@names),$unmangle);
720                }
721                if ($newaddr =~ /^($urx)$/) {
722                        return ($context_host,$newaddr,&firstname(@names),$unmangle);
723                }
724                print STDERR "Could not parse $newaddr\n";
725        }
726        print "(?)" if $debug;
727        return(undef,$newaddr,&firstname(@names),$unmangle);
728}
729# return $u (@$server) unless $u includes reference to $server
730sub compact
731{
732        local($u, $server) = @_;
733        local($se) = $server;
734        local($sp);
735        $se =~ s/(\W)/\\$1/g;
736        $sp = " (\@$server)";
737        if ($u !~ /$se/i) {
738                return "$u$sp";
739        }
740        return $u;
741}
742# remove empty (spaces don't count) members from an array
743sub trim
744{
745        local(@v) = @_;
746        local($v,@r);
747        for $v (@v) {
748                $v =~ s/^\s+//;
749                $v =~ s/\s+$//;
750                push(@r,$v) if ($v =~ /\S/);
751        }
752        return(@r);
753}
754# using the host part of an address, and the server name, add the
755# servers' domain to the address if it doesn't already have a
756# domain.  Since this sometimes fails, save a back reference so
757# it can be unrolled.
758sub domainify
759{
760        local($host,$domain_host,$u) = @_;
761        local($domain,$newhost);
762
763        # cut of trailing dots
764        $host =~ s/\.$//;
765        $domain_host =~ s/\.$//;
766
767        if ($domain_host !~ /\./) {
768                #
769                # domain host isn't, keep $host whatever it is
770                #
771                print "domainify($host,$domain_host) = $host\n" if $debug;
772                return $host;
773        }
774
775        #
776        # There are several weird situtations that need to be
777        # accounted for.  They have to do with domain relay hosts.
778        #
779        # Examples:
780        #       host            server          "right answer"
781        #       
782        #       shiva.cs        cs.berkeley.edu shiva.cs.berkeley.edu
783        #       shiva           cs.berkeley.edu shiva.cs.berekley.edu
784        #       cumulus         reed.edu        @reed.edu:cumulus.uucp
785        #       tiberius        tc.cornell.edu  tiberius.tc.cornell.edu
786        #
787        # The first try must always be to cut the domain part out of
788        # the server and tack it onto the host.
789        #
790        # A reasonable second try is to tack the whole server part onto
791        # the host and for each possible repeated element, eliminate
792        # just that part.
793        #
794        # These extra "guesses" get put into the %domainify_fallback
795        # array.  They will be used to give addresses a second chance
796        # in the &giveup routine
797        #
798
799        local(%fallback);
800
801        local($long);
802        $long = "$host $domain_host";
803        $long =~ tr/A-Z/a-z/;
804        print "long = $long\n" if $debug;
805        if ($long =~ s/^([^ ]+\.)([^ ]+) \2(\.[^ ]+\.[^ ]+)/$1$2$3/) {
806                # matches shiva.cs cs.berkeley.edu and returns shiva.cs.berkeley.edu
807                print "condensed fallback $host $domain_host -> $long\n" if $debug;
808                $fallback{$long} = 9;
809        }
810
811        local($fh);
812        $fh = $domain_host;
813        while ($fh =~ /\./) {
814                print "FALLBACK $host.$fh = 1\n" if $debug > 7;
815                $fallback{"$host.$fh"} = 1;
816                $fh =~ s/^[^\.]+\.//;
817        }
818
819        $fallback{"$host.$domain_host"} = 2;
820
821        ($domain = $domain_host) =~ s/^[^\.]+//;
822        $fallback{"$host$domain"} = 6
823                if ($domain =~ /\./);
824
825        if ($host =~ /\./) {
826                #
827                # Host is already okay, but let's look for multiple
828                # interpretations
829                #
830                print "domainify($host,$domain_host) = $host\n" if $debug;
831                delete $fallback{$host};
832                $domainify_fallback{"$u *** $host"} = join(' ',sort {$fallback{$b} <=> $fallback{$a};} keys %fallback) if %fallback;
833                return $host;
834        }
835
836        $domain = ".$domain_host"
837                if ($domain !~ /\..*\./);
838        $newhost = "$host$domain";
839
840        $create_host_backtrack{"$u *** $newhost"} = $domain_host;
841        print "domainify($host,$domain_host) = $newhost\n" if $debug;
842        delete $fallback{$newhost};
843        $domainify_fallback{"$u *** $newhost"} = join(' ',sort {$fallback{$b} <=> $fallback{$a};} keys %fallback) if %fallback;
844        if ($debug) {
845                print "fallback = ";
846                print $domainify_fallback{"$u *** $newhost"}
847                        if defined($domainify_fallback{"$u *** $newhost"});
848                print "\n";
849        }
850        return $newhost;
851}
852# return the first non-empty element of an array
853sub firstname
854{
855        local(@names) = @_;
856        local($n);
857        while(@names) {
858                $n = shift(@names);
859                return $n if $n =~ /\S/;
860        }
861        return undef;
862}
863# queue up more addresses to expand
864sub expn
865{
866        local($host,$addr,$name,$level) = @_;
867        if ($host) {
868                $host = &trhost($host);
869
870                if (($debug > 3) || (defined $giveup{$host})) {
871                        unshift(@hosts,$host) unless $users{$host};
872                } else {
873                        push(@hosts,$host) unless $users{$host};
874                }
875                $users{$host} .= " $addr";
876                $names{"$addr *** $host"} = $name;
877                $level{"$addr *** $host"} = $level + 1;
878                print "expn($host,$addr,$name)\n" if $debug;
879                return "\t$addr\n";
880        } else {
881                return &final($addr,'NONE',$name);
882        }
883}
884# compute the numerical average value of an array
885sub average
886{
887        local(@e) = @_;
888        return 0 unless @e;
889        local($e,$sum);
890        for $e (@e) {
891                $sum += $e;
892        }
893        $sum / @e;
894}
895# print to the server (also to stdout, if -w)
896sub ps
897{
898        local($p) = @_;
899        print ">>> $p\n" if $watch;
900        print $S "$p\n";
901}
902# return case-adjusted name for a host (for comparison purposes)
903sub trhost
904{
905        # treat foo.bar as an alias for Foo.BAR
906        local($host) = @_;
907        local($trhost) = $host;
908        $trhost =~ tr/A-Z/a-z/;
909        if ($trhost{$trhost}) {
910                $host = $trhost{$trhost};
911        } else {
912                $trhost{$trhost} = $host;
913        }
914        $trhost{$trhost};
915}
916# re-queue users if an mx record dictates a redirect
917# don't allow a user to be redirected more than once
918sub mxredirect
919{
920        local($server,*users) = @_;
921        local($u,$nserver,@still_there);
922
923        $nserver = &mx($server);
924
925        if (&trhost($nserver) ne &trhost($server)) {
926                $0 = "$av0 - mx redirect $server -> $nserver\n";
927                for $u (@users) {
928                        if (defined $mxbacktrace{"$u *** $nserver"}) {
929                                push(@still_there,$u);
930                        } else {
931                                $mxbacktrace{"$u *** $nserver"} = $server;
932                                print "mxbacktrace{$u *** $nserver} = $server\n"
933                                        if ($debug > 1);
934                                &expn($nserver,$u,$names{"$u *** $server"});
935                        }
936                }
937                @users = @still_there;
938                if (! @users) {
939                        return $nserver;
940                } else {
941                        return undef;
942                }
943        }
944        return undef;
945}
946# follow mx records, return a hostname
947# also follow temporary redirections comming from &domainify and
948# &mxlookup
949sub mx
950{
951        local($h,$u) = @_;
952
953        for (;;) {
954                if (defined $mx{&trhost($h)} && $h ne $mx{&trhost($h)}) {
955                        $0 = "$av0 - mx expand $h";
956                        $h = $mx{&trhost($h)};
957                        return $h;
958                }
959                if ($u) {
960                        if (defined $temporary_redirect{"$u *** $h"}) {
961                                $0 = "$av0 - internal redirect $h";
962                                print "Temporary redirect taken $u *** $h -> " if $debug;
963                                $h = $temporary_redirect{"$u *** $h"};
964                                print "$h\n" if $debug;
965                                next;
966                        }
967                        $htr = &trhost($h);
968                        if (defined $temporary_redirect{"$u *** $htr"}) {
969                                $0 = "$av0 - internal redirect $h";
970                                print "temporary redirect taken $u *** $h -> " if $debug;
971                                $h = $temporary_redirect{"$u *** $htr"};
972                                print "$h\n" if $debug;
973                                next;
974                        }
975                }
976                return $h;
977        }
978}
979# look up mx records with the name server.
980# re-queue expansion requests if possible
981# optionally give up on this host.
982sub mxlookup
983{
984        local($lastchance,$server,$giveup,*users) = @_;
985        local(*T);
986        local(*NSLOOKUP);
987        local($nh, $pref,$cpref);
988        local($o0) = $0;
989        local($nserver);
990        local($name,$aliases,$type,$len,$thataddr);
991        local(%fallback);
992
993        return 1 if &mxredirect($server,*users);
994
995        if ((defined $mx{$server}) || (! $have_nslookup)) {
996                return 0 unless $lastchance;
997                &giveup('mx domainify',$giveup);
998                return 0;
999        }
1000
1001        $0 = "$av0 - nslookup of $server";
1002        open(T,">/tmp/expn$$") || die "open > /tmp/expn$$: $!\n";
1003        print T "set querytype=MX\n";
1004        print T "$server\n";
1005        close(T);
1006        $cpref = 1.0E12;
1007        undef $nserver;
1008        open(NSLOOKUP,"nslookup < /tmp/expn$$ 2>&1 |") || die "open nslookup: $!";
1009        while(<NSLOOKUP>) {
1010                print if ($debug > 2);
1011                if (/mail exchanger = ([-A-Za-z_.0-9+]+)/) {
1012                        $nh = $1;
1013                        if (/preference = (\d+)/) {
1014                                $pref = $1;
1015                                if ($pref < $cpref) {
1016                                        $nserver = $nh;
1017                                        $cpref = $pref;
1018                                } elsif ($pref) {
1019                                        $fallback{$pref} .= " $nh";
1020                                }
1021                        }
1022                }
1023                if (/Non-existent domain/) {
1024                        #
1025                        # These addresss are hosed.  Kaput!  Dead!
1026                        # However, if we created the address in the
1027                        # first place then there is a chance of
1028                        # salvation.
1029                        #
1030                        1 while(<NSLOOKUP>);   
1031                        close(NSLOOKUP);
1032                        return 0 unless $lastchance;
1033                        &giveup('domainify',"$server: Non-existent domain",undef,1);
1034                        return 0;       
1035                }
1036                               
1037        }
1038        close(NSLOOKUP);
1039        unlink("/tmp/expn$$");
1040        unless ($nserver) {
1041                $0 = "$o0 - finished mxlookup";
1042                return 0 unless $lastchance;
1043                &giveup('mx domainify',"$server: Could not resolve address");
1044                return 0;
1045        }
1046
1047        # provide fallbacks in case $nserver doesn't work out
1048        if (defined $fallback{$cpref}) {
1049                $mx_secondary{$server} = $fallback{$cpref};
1050        }
1051
1052        $0 = "$av0 - gethostbyname($nserver)";
1053        ($name,$aliases,$type,$len,$thataddr) = gethostbyname($nserver);
1054
1055        unless ($thataddr) {
1056                $0 = $o0;
1057                return 0 unless $lastchance;
1058                &giveup('mx domainify',"$nserver: could not resolve address");
1059                return 0;
1060        }
1061        print "MX($server) = $nserver\n" if $debug;
1062        print "$server -> $nserver\n" if $vw && !$debug;
1063        $mx{&trhost($server)} = $nserver;
1064        # redeploy the users
1065        unless (&mxredirect($server,*users)) {
1066                return 0 unless $lastchance;
1067                &giveup('mx domainify',"$nserver: only one level of mx redirect allowed");
1068                return 0;
1069        }
1070        $0 = "$o0 - finished mxlookup";
1071        return 1;
1072}
1073# if mx expansion did not help to resolve an address
1074# (ie: foo@bar became @baz:foo@bar, then undo the
1075# expansion).
1076# this is only used by &final
1077sub mxunroll
1078{
1079        local(*host,*addr) = @_;
1080        local($r) = 0;
1081        print "looking for mxbacktrace{$addr *** $host}\n"
1082                if ($debug > 1);
1083        while (defined $mxbacktrace{"$addr *** $host"}) {
1084                print "Unrolling MX expnasion: \@$host:$addr -> "
1085                        if ($debug || $verbose);
1086                $host = $mxbacktrace{"$addr *** $host"};
1087                print "\@$host:$addr\n"
1088                        if ($debug || $verbose);
1089                $r = 1;
1090        }
1091        return 1 if $r;
1092        $addr = "\@$host:$addr"
1093                if ($host =~ /\./);
1094        return 0;
1095}
1096# register a completed expnasion.  Make the final address as
1097# simple as possible.
1098sub final
1099{
1100        local($addr,$host,$name,$error) = @_;
1101        local($he);
1102        local($hb,$hr);
1103        local($au,$ah);
1104
1105        if ($error =~ /Non-existent domain/) {
1106                #
1107                # If we created the domain, then let's undo the
1108                # damage...
1109                #
1110                if (defined $create_host_backtrack{"$addr *** $host"}) {
1111                        while (defined $create_host_backtrack{"$addr *** $host"}) {
1112                                print "Un&domainifying($host) = " if $debug;
1113                                $host = $create_host_backtrack{"$addr *** $host"};
1114                                print "$host\n" if $debug;
1115                        }
1116                        $error = "$host: could not locate";
1117                } else {
1118                        #
1119                        # If we only want valid addresses, toss out
1120                        # bad host names.
1121                        #
1122                        if ($valid) {
1123                                print STDERR "\@$host:$addr ($name) Non-existent domain\n";
1124                                return "";
1125                        }
1126                }
1127        }
1128
1129        MXUNWIND: {
1130                $0 = "$av0 - final parsing of \@$host:$addr";
1131                ($he = $host) =~ s/(\W)/\\$1/g;
1132                if ($addr !~ /@/) {
1133                        # addr does not contain any host
1134                        $addr = "$addr@$host";
1135                } elsif ($addr !~ /$he/i) {
1136                        # if host part really something else, use the something
1137                        # else.
1138                        if ($addr =~ m/(.*)\@([^\@]+)$/) {
1139                                ($au,$ah) = ($1,$2);
1140                                print "au = $au ah = $ah\n" if $debug;
1141                                if (defined $temporary_redirect{"$addr *** $ah"}) {
1142                                        $addr = "$au\@".$temporary_redirect{"$addr *** $ah"};
1143                                        print "Rewrite! to $addr\n" if $debug;
1144                                        next MXUNWIND;
1145                                }
1146                        }
1147                        # addr does not contain full host
1148                        if ($valid) {
1149                                if ($host =~ /^([^\.]+)(\..+)$/) {
1150                                        # host part has a . in it - foo.bar
1151                                        ($hb, $hr) = ($1, $2);
1152                                        if ($addr =~ /\@([^\.\@]+)$/ && ($1 eq $hb)) {
1153                                                # addr part has not .
1154                                                # and matches beginning of
1155                                                # host part -- tack on a
1156                                                # domain name.
1157                                                $addr .= $hr;
1158                                        } else {
1159                                                &mxunroll(*host,*addr)
1160                                                        && redo MXUNWIND;
1161                                        }
1162                                } else {
1163                                        &mxunroll(*host,*addr)
1164                                                && redo MXUNWIND;
1165                                }
1166                        } else {
1167                                $addr = "${addr}[\@$host]"
1168                                        if ($host =~ /\./);
1169                        }
1170                }
1171        }
1172        $name = "$name " if $name;
1173        $error = " $error" if $error;
1174        if ($valid) {
1175                push(@final,"$name<$addr>");
1176        } else {
1177                push(@final,"$name<$addr>$error");
1178        }
1179        "\t$name<$addr>$error\n";
1180}
1181
1182sub alarm
1183{
1184        local($alarm_action,$alarm_redirect,$alarm_user) = @_;
1185        alarm(3600);
1186        $SIG{ALRM} = 'handle_alarm';
1187}
1188# this involves one great big ugly hack.
1189# the "next HOST" unwinds the stack!
1190sub handle_alarm
1191{
1192        &giveup($alarm_redirect,"Timed out during $alarm_action",$alarm_user);
1193        next HOST;
1194}
1195
1196# read the rest of the current smtp daemon's response (and toss it away)
1197sub read_response
1198{
1199        local($done,$watch) = @_;
1200        local(@resp);
1201        print $s if $watch;
1202        while(($done eq "-") && ($s = <$S>) && ($s =~ /^\d+([- ])/)) {
1203                print $s if $watch;
1204                $done = $1;
1205                push(@resp,$s);
1206        }
1207        return @resp;
1208}
1209# print args if verbose.  Return them in any case
1210sub verbose
1211{
1212        local(@tp) = @_;
1213        print "@tp" if $verbose;
1214}
1215# to pass perl -w:
1216@tp;
1217$flag_a;
1218$flag_d;
1219$flag_1;
1220%already_domainify_fellback;
1221%already_mx_fellback;
1222&handle_alarm;
1223################### BEGIN PERL/TROFF TRANSITION
1224.00 ;   
1225
1226'di
1227.nr nl 0-1
1228.nr % 0
1229.\\"'; __END__
1230.\" ############## END PERL/TROFF TRANSITION
1231.TH EXPN 1 "March 11, 1993"
1232.AT 3
1233.SH NAME
1234expn \- recursively expand mail aliases
1235.SH SYNOPSIS
1236.B expn
1237.RI [ -a ]
1238.RI [ -v ]
1239.RI [ -w ]
1240.RI [ -d ]
1241.RI [ -1 ]
1242.IR user [@ hostname ]
1243.RI [ user [@ hostname ]]...
1244.SH DESCRIPTION
1245.B expn
1246will use the SMTP
1247.B expn
1248and
1249.B vrfy
1250commands to expand mail aliases. 
1251It will first look up the addresses you provide on the command line.
1252If those expand into addresses on other systems, it will
1253connect to the other systems and expand again.  It will keep
1254doing this until no further expansion is possible.
1255.SH OPTIONS
1256The default output of
1257.B expn
1258can contain many lines which are not valid
1259email addresses.  With the
1260.I -aa
1261flag, only expansions that result in legal addresses
1262are used.  Since many mailing lists have an illegal
1263address or two, the single
1264.IR -a ,
1265address, flag specifies that a few illegal addresses can
1266be mixed into the results.   More
1267.I -a
1268flags vary the ratio.  Read the source to track down
1269the formula.  With the
1270.I -a
1271option, you should be able to construct a new mailing
1272list out of an existing one.
1273.LP
1274If you wish to limit the number of levels deep that
1275.B expn
1276will recurse as it traces addresses, use the
1277.I -1
1278option.  For each
1279.I -1
1280another level will be traversed.  So,
1281.I -111
1282will traverse no more than three levels deep.
1283.LP
1284The normal mode of operation for
1285.B expn
1286is to do all of its work silently.
1287The following options make it more verbose.
1288It is not necessary to make it verbose to see what it is
1289doing because as it works, it changes its
1290.BR argv [0]
1291variable to reflect its current activity.
1292To see how it is expanding things, the
1293.IR -v ,
1294verbose, flag will cause
1295.B expn
1296to show each address before
1297and after translation as it works.
1298The
1299.IR -w ,
1300watch, flag will cause
1301.B expn
1302to show you its conversations with the mail daemons.
1303Finally, the
1304.IR -d ,
1305debug, flag will expose many of the inner workings so that
1306it is possible to eliminate bugs.
1307.SH ENVIRONMENT
1308No environment variables are used.
1309.SH FILES
1310.PD 0
1311.B /tmp/expn$$
1312.B temporary file used as input to
1313.BR nslookup .
1314.SH SEE ALSO
1315.BR aliases (5),
1316.BR sendmail (8),
1317.BR nslookup (8),
1318RFC 823, and RFC 1123.
1319.SH BUGS
1320Not all mail daemons will implement
1321.B expn
1322or
1323.BR vrfy .
1324It is not possible to verify addresses that are served
1325by such daemons.
1326.LP
1327When attempting to connect to a system to verify an address,
1328.B expn
1329only tries one IP address.  Most mail daemons
1330will try harder.
1331.LP
1332It is assumed that you are running domain names and that
1333the
1334.BR nslookup (8)
1335program is available.  If not,
1336.B expn
1337will not be able to verify many addresses.  It will also pause
1338for a long time unless you change the code where it says
1339.I $have_nslookup = 1
1340to read
1341.I $have_nslookup =
1342.IR 0 .
1343.LP
1344Lastly,
1345.B expn
1346does not handle every valid address.  If you have an example,
1347please submit a bug report.
1348.SH CREDITS
1349In 1986 or so, Jon Broome wrote a program of the same name
1350that did about the same thing.  It has since suffered bit rot
1351and Jon Broome has dropped off the face of the earth!
1352(Jon, if you are out there, drop me a line)
1353.SH AVAILABILITY
1354The latest version of
1355.B expn
1356is available through anonymous ftp at
1357.IR ftp://ftp.idiom.com/pub/muir-programs/expn .
1358.SH AUTHOR
1359.I David Muir Sharnoff\ \ \ \ <muir@idiom.com>
Note: See TracBrowser for help on using the repository browser.