File Coverage

lib/Qmail/Deliverable.pm
Criterion Covered Total %
statement 53 183 28.9
branch 19 118 16.1
condition 1 5 20.0
subroutine 10 19 52.6
pod 5 7 71.4
total 88 332 26.5


line stmt bran cond sub pod time code
1             package Qmail::Deliverable;
2              
3 2     2   1329687 use strict;
  2         6  
  2         78  
4 2     2   60 use 5.006;
  2         15  
5 2     2   14 use Carp qw(carp);
  2         6  
  2         170  
6 2     2   12 use base 'Exporter';
  2         6  
  2         7735  
7              
8             our $VERSION = '1.09';
9             our @EXPORT_OK = qw/reread_config qmail_local dot_qmail deliverable qmail_user/;
10             our %EXPORT_TAGS = (all => \@EXPORT_OK);
11             our $VPOPMAIL_EXT = 0;
12             our $qmail_dir = '/var/qmail';
13              
14             # rfc2822's "atext"
15             my $atext = "[A-Za-z0-9!#\$%&\'*+\/=?^_\`{|}~-]";
16             my $valid = qr/^(?!.*\@.*\@)($atext+(?:[\@.]$atext+)*)\.?\z/;
17              
18             # disallow control characters and non-ascii
19             my $ascii = qr/^([\x20-\x7e]*)\z/;
20              
21             # parse shell line
22             my $shell_sq = qr/'[^']+'/; # no escaping in single quotes!
23             my $shell_dq = qr/(?:(?:\")(?:[^\\\"]*(?:\\.[^\\\"]*)*)(?:\"))/; # from Regexp::Common
24             my $shell_bare = qr/[^"'\\\s]+/; # no sq, dq, backslash, or space
25             my $shell_token = qr/(\\.|$shell_sq|$shell_dq|$shell_bare)+/;
26              
27             sub _readpipe {
28 0     0   0 my ($command, @args) = @_;
29 0         0 local %ENV = ();
30 0 0       0 open my $fh, '-|', $command, @args or die "open: @_: $!";
31 0         0 my @data = readline $fh;
32 0         0 close $fh; # Without explicit close, $? is unreliable later
33 0 0       0 return wantarray ? @data : join("", @data);
34             }
35              
36             sub _slurp {
37 9     9   20 my ($fn) = @_;
38 9 100       277 open my $fh, '<', $fn or return;
39 3 50       138 return wantarray ? readline $fh : join("", readline $fh);
40             }
41              
42             sub _first (&@) {
43 4     4   7 my $sub = shift;
44 4   50     17 $sub->($_) and return $_ for @_;
45 4         28 return;
46             }
47              
48             sub _findinpath {
49 4     4   9 my ($command) = @_;
50             # Attempt to retain some level of security against PATH attacks
51             my @path = grep defined, map /($ascii)/, # untaint
52 4         176 grep m{^/[^<>|]+$}, split /:/, $ENV{PATH};
53 4     72   57 return _first { -x $_ } map "$_/$command", @path;
  72         851  
54             }
55              
56             # Resolve only once to further reduce risk of PATH attacks
57             my $valias_exec = _findinpath 'valias';
58             my $vuser_exec = _findinpath 'vuserinfo';
59              
60             my %locals;
61             my %virtualdomains;
62             my %users_exact;
63             my %users_wild;
64              
65             sub reread_config {
66 3     3 1 233715 %locals = ();
67 3         7 %virtualdomains = ();
68 3         6 %users_exact = ();
69 3         3 %users_wild = ();
70 3 100       90 my $locals_fn = -e "$qmail_dir/control/locals"
71             ? "$qmail_dir/control/locals"
72             : "$qmail_dir/control/me";
73 3         36 for (_slurp $locals_fn) {
74 1         17 chomp;
75 1 50       16 ($_) = lc =~ /$ascii/ or do { warn "Invalid character"; next; };
  0         0  
  0         0  
76 1         4 $locals{$_} = 1;
77             }
78 3         10 for (_slurp "$qmail_dir/control/virtualdomains") {
79 1         4 chomp;
80 1 50       13 ($_) = lc =~ /$ascii/ or do { warn "Invalid character"; next; };
  0         0  
  0         0  
81 1         5 my ($domain, $prepend) = split /:/, $_, 2;
82 1         5 $virtualdomains{$domain} = $prepend;
83             }
84 3         19 for (_slurp "$qmail_dir/users/assign") {
85 4         7 chomp;
86 4 50       28 ($_) = /$ascii/ or do { warn "Invalid character"; next; };
  0         0  
  0         0  
87 4 100       28 if (/^#/) { # comment
    50          
    100          
    50          
88 1         3 next;
89             } elsif (s/^=([^:]+)://) {
90 0         0 $users_exact{lc $1} = $_;
91             } elsif (s/^\+([^:]+)://) {
92 2         10 $users_wild{lc $1} = $_;
93             } elsif (/^\.$/) {
94 1         5 last;
95             } else {
96 0         0 warn "Invalid line in users/assign: '$_'\n";
97             }
98             }
99             }
100              
101             sub _qmail_getpw {
102 0     0   0 my ($local) = @_;
103 0         0 local $/ = "\0";
104 0         0 my @a = _readpipe "/var/qmail/bin/qmail-getpw", $local;
105 0         0 chomp @a;
106 0         0 for (@a) {
107 0 0       0 ($_) = /$ascii/ or do { warn "Invalid character"; return ""; }
  0         0  
  0         0  
108             }
109 0         0 return @a;
110             }
111              
112             sub _prepend {
113 0     0   0 my ($domain) = @_;
114              
115 0 0       0 return $virtualdomains{$domain} if exists $virtualdomains{$domain};
116              
117 0         0 my @parts = split /\./, $domain;
118 0         0 for (reverse 1 .. @parts) {
119 0         0 my $wildcard = join "", map ".$_", @parts[-$_ .. -1];
120 0 0       0 return $virtualdomains{$wildcard} if exists $virtualdomains{$wildcard};
121             }
122              
123 0 0       0 return $virtualdomains{''} if exists $virtualdomains{''};
124 0         0 return undef;
125             }
126              
127             sub _potential_exts {
128 0     0   0 my ($ext) = @_;
129 0         0 my @exts;
130              
131             # Exact match has highest precedence
132 0         0 push @exts, $ext; # user, or user-ext
133              
134             # Then user-default
135 0         0 my @parts = split /(-)/, $ext;
136 0         0 for (reverse 1 .. $#parts) {
137 0 0       0 next unless $parts[$_] eq '-';
138 0         0 push @exts, join("", @parts[0..$_]) . "default";
139             }
140              
141 0         0 return @exts;
142             }
143              
144              
145             sub qmail_user {
146 3     3 1 2247 my ($in) = @_;
147             my ($local) = lc($in) =~ /$valid/
148 3 50       49 or do { carp "Invalid address: $in"; return; };
  0         0  
  0         0  
149              
150 3 50       11 if (exists $users_exact{$local}) {
151 0         0 return split /:/, $users_exact{$local}, 7; # colon terminated
152             } else {
153 3         12 for (reverse 1 .. length $local) {
154 52         81 my $try = substr $local, 0, $_;
155 52 100       127 if (exists $users_wild{$try}) {
156 1         7 my @assign = split /:/, $users_wild{$try}, 7;
157 1         5 $assign[5] .= substr($local, $_);
158 1         10 return @assign;
159             }
160             }
161             }
162              
163 2 50       9 if ($qmail_dir eq "t/fixtures") {
164 2         9 return $local;
165             }
166 0           return _qmail_getpw $local;
167             }
168              
169             sub qmail_local {
170 0     0 1   my ($in) = @_;
171             my ($address) = lc($in) =~ /$valid/ or
172 0 0         do { carp "Invalid address: $in"; return; };
  0            
  0            
173              
174 0 0         return $address if $address !~ /\@/;
175 0           my ($local, $domain) = split /\@/, $address;
176              
177 0 0         return $local if exists $locals{$domain};
178              
179 0           my $prepend = _prepend $domain;
180 0 0         return "$prepend-$local" if defined $prepend;
181              
182 0           return undef;
183             }
184              
185             sub dot_qmail {
186 0     0 1   my ($user, $uid, $gid, $homedir, $dash, $ext) = @_;
187 0 0         if (@_ == 1) {
188 0           my ($in) = @_;
189             my ($address) = lc($in) =~ /$valid/
190 0 0         or do { carp "Invalid address: $in"; return; };
  0            
  0            
191              
192 0           my $local = qmail_local $address;
193 0 0         return undef if not defined $local;
194              
195 0           ($user, $uid, $gid, $homedir, $dash, $ext) = qmail_user $local;
196             }
197              
198 0           $ext =~ s/\./:/g;
199              
200 0           my $dashext = $dash . $ext;
201              
202 0 0         if (not length $dashext) {
203 0 0         return "$homedir/.qmail" if -e "$homedir/.qmail";
204 0           return ""; # defaultdelivery
205             }
206              
207 0           for ( _potential_exts($ext), 'default' ) {
208 0 0         return "$homedir/.qmail-$_" if -e "$homedir/.qmail-$_";
209             }
210              
211 0           return undef;
212             }
213              
214             sub valias {
215 0     0 0   my ($address) = @_;
216 0 0         if (not $valias_exec) {
217 0           warn "Cannot check valias; valias executable not found";
218 0           return 0;
219             }
220 0           my ($local, $domain) = split /\@/, $address;
221 0           for ( _potential_exts($local) ) {
222 0           eval { _readpipe( $valias_exec, "$_\@$domain") };
  0            
223 0 0         return 1 if $? == 0;
224             };
225 0           return 0;
226             }
227              
228             sub vuser {
229 0     0 0   my ($address) = @_;
230 0 0         if (not $vuser_exec) {
231 0           warn "Cannot check vuser; vuser executable not found";
232 0           return 0;
233             }
234 0 0         eval { _readpipe $vuser_exec, $address } or return 0;
  0            
235 0 0         return 1 if $? == 0;
236 0           return 0;
237             }
238              
239             sub deliverable {
240 0     0 1   my ($in) = @_;
241             my ($address) = lc($in) =~ /$valid/
242 0 0         or do { carp "Invalid address: $in"; return; };
  0            
  0            
243              
244 0           my $local = qmail_local $address;
245 0 0         return 0xff if not defined $local;
246              
247 0           my ($user, $uid, $gid, $homedir, $dash, $ext) = qmail_user $local;
248              
249 0 0 0       return 0x11 if not -r $homedir or not -x _;
250 0 0         return 0x21 if (stat _)[2] & 0020; # group writable
251 0 0         return 0x21 if (stat _)[2] & 0002; # world writable
252 0 0         return 0x22 if -T _;
253              
254 0           my $dot_qmail = dot_qmail $user, $uid, $gid, $homedir, $dash, $ext;
255              
256 0 0         return 0x00 if not defined $dot_qmail;
257 0 0         return 0xf1 if not length $dot_qmail; # no .qmail => defaultdelivery
258              
259 0 0         return 0x00 if not -e $dot_qmail;
260 0 0         return 0x11 if not -r $dot_qmail;
261 0 0         return 0xf1 if not -s _; # empty => defaultdelivery
262              
263 0           my @dot_qmail = _slurp $dot_qmail;
264              
265 0 0         if ($dot_qmail[0] =~ /^\|\s*\S*vdelivermail/) {
266 0 0         if ($address !~ /\@/) {
267 0           carp "vpopmail support not available if no domain given";
268 0           return 0xfe;
269             }
270 0           my $origlocal = (split /\@/, $address)[0];
271              
272             # Update domain with user field in assign file, just like vpopmail
273             # does. This way we support alias domains. See vpopmail.c,
274             # vget_assign().
275 0           $address = $origlocal . '@' . $user;
276              
277 0 0         if ($dot_qmail[0] =~ /bounce-no-mailbox/) {
278 0 0         return 0xf2 if -d "$homedir/$origlocal";
279 0 0         return 0xf3 if valias $address;
280 0 0         return 0xf5 if vuser $address;
281 0 0         if ( $VPOPMAIL_EXT ) {
282 0           my ($local, $domain) = split /@/, $address;
283 0           my @chunks = split /\-/, $local; # vpopmails qmail-ext option
284 0           for ( 0 .. $#chunks ) {
285 0 0         return 0xf6 if vuser $chunks[$_] .'@'.$domain;
286             };
287             };
288 0           return 0x00;
289             }
290 0           return 0xf4;
291             }
292 0 0         if ($dot_qmail[0] =~ /^\|bouncesaying\s+(.*)/) {
293 0           my @args = $1 =~ /$shell_token/g;
294 0 0         return 0x13 if @args > 1;
295 0           return 0x00;
296             }
297              
298 0 0         return 0x14 if grep /ezmlm/, @dot_qmail;
299 0 0         return 0x12 if grep /^\|/, @dot_qmail;
300              
301 0           return 0xf1;
302             }
303              
304             reread_config;
305              
306             # use Memoize;
307             # use Memoize::Expire;
308             # tie my %deliverable_cache, 'Memoize::Expire', LIFETIME => 60;
309             # memoize 'deliverable';
310              
311             1;
312              
313             __END__