File Coverage

blib/lib/Mail/ExpandAliases.pm
Criterion Covered Total %
statement 104 131 79.3
branch 27 48 56.2
condition 11 19 57.8
subroutine 16 19 84.2
pod 0 8 0.0
total 158 225 70.2


line stmt bran cond sub pod time code
1             package Mail::ExpandAliases;
2              
3             # -------------------------------------------------------------------
4             # Mail::ExpandAliases - Expand aliases from /etc/aliases files
5             # Copyright (C) 2002 darren chamberlain
6             #
7             # This program is free software; you can redistribute it and/or
8             # modify it under the terms of the GNU General Public License as
9             # published by the Free Software Foundation; version 2.
10             #
11             # This program is distributed in the hope that it will be useful, but
12             # WITHOUT ANY WARRANTY; without even the implied warranty of
13             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14             # General Public License for more details.
15             #
16             # You should have received a copy of the GNU General Public License
17             # along with this program; if not, write to the Free Software
18             # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
19             # 02111-1307 USA
20             # -------------------------------------------------------------------
21             # Design of this class:
22             #
23             # - Read aliases file
24             #
25             # - Parse aliases file
26             #
27             # o Read file, normalize
28             #
29             # + Skip malformed lines
30             #
31             # + Join multi-line entries
32             #
33             # + Discard comments
34             #
35             # o Create internal structure
36             #
37             # - On call to expand
38             #
39             # o Start with first alias, and expand
40             #
41             # o Expand each alias, unless:
42             #
43             # + It is non-local
44             #
45             # + It has already been seen
46             #
47             # - Return list of responses
48             # -------------------------------------------------------------------
49              
50 2     2   5324 use strict;
  2         5  
  2         76  
51 2     2   10 use vars qw($VERSION $DEBUG @POSSIBLE_ALIAS_FILES);
  2         3  
  2         210  
52              
53             $VERSION = 0.49;
54             $DEBUG = 0 unless defined $DEBUG;
55             @POSSIBLE_ALIAS_FILES = qw(/etc/aliases
56             /etc/mail/aliases
57             /etc/postfix/aliases
58             /etc/exim/aliases);
59              
60 2     2   12 use constant PARSED => 0; # Parsed aliases
  2         16  
  2         159  
61 2     2   9 use constant CACHED => 1; # Caches lookups
  2         4  
  2         98  
62 2     2   9 use constant FILE => 2; # "Main" aliases file
  2         4  
  2         3208  
63              
64             # ----------------------------------------------------------------------
65             # import(@files)
66             #
67             # Allow for compile-time additions to @POSSIBLE_ALIAS_FILES
68             # ----------------------------------------------------------------------
69             sub import {
70 4     4   409 my $class = shift;
71              
72 4         100 for my $x (@_) {
73 0 0       0 if ($x =~ /^debug$/i) {
    0          
74 0         0 $DEBUG = 1;
75             }
76             elsif (-f "$x") {
77 0         0 unshift @POSSIBLE_ALIAS_FILES, $x;
78             }
79             }
80             }
81              
82             sub new {
83 2     2 0 1220 my ($class, $file) = @_;
84 2         11 my $self = bless [ { }, { }, "" ] => $class;
85              
86 2 100       9 $self->[ FILE ] = (grep { -e $_ && -r _ }
  10         222  
87             ($file, @POSSIBLE_ALIAS_FILES))[0];
88 2         16 $self->debug("Using alias file " . $self->[ FILE ]);
89 2         9 $self->init();
90              
91 2         12 return $self;
92             }
93              
94             sub debug {
95 64     64 0 117 my $class = shift;
96 64   33     140 $class = ref $class || $class;
97 64 50       139 if ($DEBUG) {
98             warn "[ $class ] $_\n"
99 0         0 for (@_);
100             }
101             }
102              
103             # ----------------------------------------------------------------------
104             # init($file)
105             #
106             # Parse file, extracting aliases. Note that this is a (more or less)
107             # literal representation of the file; expansion of aliases happens at
108             # run time, as aliases are requested.
109             # # ----------------------------------------------------------------------
110             sub init {
111 2     2 0 5 my $self = shift;
112 2   33     13 my $file = shift || $self->[ FILE ];
113 2 50       9 return $self unless defined $file;
114              
115             # Chapter 24 of the sendmail book
116             # (www.oreilly.com/catalog/sendmail/) describes the process of
117             # looking for aliases thusly:
118             #
119             # "The aliases(5) file is composed of lines of text. Any line that
120             # begins with a # is a comment and is ignored. Empty lines (those
121             # that contain only a newline character) are also ignored. Any
122             # lines that begins with a space or tab is joined (appended) to the
123             # line above it. All other lines are text are viewed as alias
124             # lines. The format for an alias line is:
125             #
126             # local: alias
127             #
128             # "The local must begin a line. It is an address in the form of a
129             # local recipient address... The colon follows the local on
130             # the same line and may be preceded with spaces or tabs. If the
131             # colon is missing, sendmail prints and syslog(3)'s the following
132             # error message and skips that alias line:
133             #
134             # missing colon
135             #
136             # "The alias (to the right of the colon) is one or more addresses on
137             # the same line. Indented continuation lines are permitted. Each
138             # address should be separated from the next by a comma and optional
139             # space characters. A typical alias looks like this:
140             #
141             # root: jim, sysadmin@server, gunther ^ | indenting whitespace
142             #
143             # "Here, root is hte local address to be aliases. When mail is to
144             # be locally delivered to root, it is looked up in the aliases(5)
145             # file. If found, root is replaced with the three addresses show
146             # earlier, and mail is instead delivered to those other three
147             # addresses.
148             #
149             # "This process of looking up and possibly aliases local recipients
150             # is repeated for each recipient until no more aliases are found in
151             # the aliases(5) file. That is, for example, if one of the aliases
152             # for root is jim, and if jim also exists to the left of a colon in
153             # the aliases file, he too is replaced with his alias:
154             #
155             # jim: jim@otherhost
156             #
157             # "The list of addresses to the right of the colon may be mail
158             # addresses (such as gunther or jim@otherhost), the name of a
159             # program to run (such as /etc/relocated), the name of a file onto
160             # which to append (such as /usr/share/archive), or the name of a
161             # file to read for additional addresses (using :include:)."
162              
163 2         13 $self->debug("Opening alias file '$file'");
164 2 50       19 my $fh = File::Aliases->new($file)
165             or die "Can't open $file: $!";
166              
167 2         11 while (my $line = $fh->next) {
168 50         59 chomp($line);
169 50 100       143 next if $line =~ /^#/;
170 30 50       87 next if $line =~ /^\s*$/;
171              
172 30         116 $line =~ s/\s+/ /g;
173 30         37 my ($orig, $alias, @expandos);
174              
175 30         35 $orig = $line;
176 30 50       122 if ($line =~ s/^([^:]+)\s*:\s*//) {
177 30         64 $alias = lc $1;
178 30         120 $self->debug("$. => '$alias'");
179             }
180             else {
181 0         0 local $DEBUG = 1;
182 0         0 $self->debug("$file line $.: missing colon");
183 0         0 next;
184             }
185              
186 44         109 @expandos =
187             #grep !/^$alias$/,
188 30         71 map { s/^\s*//; s/\s*$//; $_ }
  44         174  
  44         112  
189             split /,/, $line;
190              
191 30         109 $self->debug($alias, map "\t$_", @expandos);
192 30         127 $self->[ PARSED ]->{ $alias } = \@expandos;
193             }
194              
195 2         46 return $self;
196             }
197              
198             # ----------------------------------------------------------------------
199             # expand($name)
200             #
201             # Expands $name to @addresses. If @addresses is empty, return $name.
202             # In list context, returns a list of the matching expansions; in
203             # scalar context, returns a reference to an array of expansions.
204             # ----------------------------------------------------------------------
205             sub expand {
206 26     26 0 5442 my ($self, $name, $original, $lcname, %answers, @answers, @names, $n);
207 26         30 $self = shift;
208 26   50     53 $name = shift || return $name;
209 26         28 $original = shift;
210 26         37 $lcname = lc $name;
211              
212 26 100 100     85 return $name if (defined $original && $name eq $original);
213              
214 24 100       58 return @{ $self->[ CACHED ]->{ $lcname } }
  4         43  
215             if (defined $self->[ CACHED ]->{ $lcname });
216              
217 20 100       22 if (@names = @{ $self->[ PARSED ]->{ $lcname } || [ ] }) {
  20 100       99  
218 13         27 my $c = $self->[ CACHED ]->{ $lcname } = [ ];
219              
220 13         24 for $n (@names) {
221 18         71 $n =~ s/^[\s'"]*//g;
222 18         100 $n =~ s/['"\s]*$//g;
223 18         33 my $type = substr $n, 0, 1;
224              
225 18 100 100     100 if ($type eq '|' or $type eq '/') {
    50          
    100          
226             # |/path/to/program
227             # /path/to/mbox
228 4         9 $answers{ $n }++;
229 4         16 push @$c, $n;
230             }
231              
232             elsif ($type eq ':') {
233             # :include:
234             #$n =~ s/:include:\s*//ig;
235             #$self->parse($n);
236 0         0 warn "Skipping include file $n\n";
237             }
238              
239             elsif ($type eq '\\') {
240             # \foo
241             # literal, non-escaped value, useful for
242             # aliases like:
243             # foo: \foo, bar
244             # where mail to foo, a local user, should also
245             # go to bar.
246 1         4 $n =~ s/^\\//;
247 1         2 $answers{ $n }++;
248 1         4 push @$c, $n;
249             }
250              
251             else {
252 13   66     57 for ($self->expand($n, $original || $name)) {
253 13         52 $answers{ $_ }++
254             }
255             }
256             }
257              
258             # Add to the cache
259 13         45 @answers = sort keys %answers;
260 13         28 $self->[ CACHED ]->{ $lcname } = \@answers;
261 13 50       67 return wantarray ? @answers : \@answers;
262             }
263              
264 7         23 return $name;
265             }
266              
267             # ----------------------------------------------------------------------
268             # reload()
269             #
270             # Reset the instance. Clears out parsed aliases and empties the cache.
271             # ----------------------------------------------------------------------
272             sub reload {
273 0     0 0 0 my ($self, $file) = @_;
274              
275 0         0 %{ $self->[ PARSED ] } = ();
  0         0  
276 0         0 %{ $self->[ CACHED ] } = ();
  0         0  
277 0 0       0 $self->[ FILE ] = $file if defined $file;
278              
279 0         0 $self->parse;
280              
281 0         0 return $self;
282             }
283              
284             # ----------------------------------------------------------------------
285             # aliases()
286             #
287             # Lists the aliases.
288             # In list context, returns an array;
289             # in scalar context, returns a reference to an array.
290             #
291             # From a patch submitted by Thomas Kishel
292             # ----------------------------------------------------------------------
293             sub aliases {
294 0     0 0 0 my ($self, @answers);
295 0         0 $self = shift;
296 0         0 @answers = sort keys %{ $self->[ PARSED ] };
  0         0  
297 0 0       0 return wantarray ? @answers : \@answers;
298             }
299              
300             # ----------------------------------------------------------------------
301             # exists($alias)
302             #
303             # Determine if an alias exists not not
304             # ----------------------------------------------------------------------
305             sub exists {
306 2     2 0 4 my ($self, $alias) = @_;
307 2         14 return CORE::exists($self->[ PARSED ]->{ $alias });
308             }
309              
310             # ----------------------------------------------------------------------
311             # check($alias)
312             #
313             # Returns the unexpanded form an an alias. I.e., exactly what is in the
314             # file, without expansion.
315             #
316             # Unlike expand, if $alias does not exist in the file, check() returns
317             # the empty array. Otherwise, $alias returns an array (in list context)
318             # or a reference to an array (in scalar context) to the items in the
319             # aliases file.
320             #
321             # You can emulate expand() by calling check recusrively.
322             # ----------------------------------------------------------------------
323             sub check {
324 0     0 0 0 my $self = shift;
325 0         0 my $ret;
326              
327 0 0       0 if (my $name = shift) {
328 0         0 $ret = $self->[ PARSED ]->{ $name }
329             }
330              
331 0   0     0 $ret ||= [];
332              
333 0 0       0 return wantarray ? @$ret : [ @$ret ];
334             }
335              
336             package File::Aliases;
337 2     2   13 use constant FH => 0;
  2         3  
  2         100  
338 2     2   11 use constant BUFFER => 1;
  2         3  
  2         102  
339              
340 2     2   1788 use IO::File;
  2         22590  
  2         691  
341              
342             # This package ensures that each read (i.e., calls to next() --
343             # I'm too lazy to implement this as a tied file handle so it can
344             # be used in <>) returns a single alias entry, which may span
345             # multiple lines.
346             #
347             # XXX I suppose I could simply subclass IO::File, and rename next
348             # to readline.
349              
350             sub new {
351 2     2   5 my $class = shift;
352 2         4 my $file = shift;
353 2         20 my $fh = IO::File->new($file);
354              
355 2         231 my $self = bless [ $fh, '' ] => $class;
356 2 50       75 $self->[ BUFFER ] = <$fh>
357             if $fh;
358              
359 2         18 return $self;
360             }
361              
362             sub next {
363 52     52   56 my $self = shift;
364 52         82 my $buffer = $self->[ BUFFER ];
365 52         57 my $fh = $self->[ FH ];
366              
367 52 50       95 return ""
368             unless defined $fh;
369              
370 52         63 $self->[ BUFFER ] = "";
371 52         178 while (<$fh>) {
372 66 100       167 if (/^\S/) {
373 48         93 $self->[ BUFFER ] = $_;
374 48         55 last;
375             } else {
376 18         49 $buffer .= $_;
377             }
378             }
379              
380 52         157 return $buffer;
381             }
382              
383             1;
384              
385             __END__