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