File Coverage

lib/Qmail/Deliverable.pm
Criterion Covered Total %
statement 145 186 77.9
branch 89 118 75.4
condition 2 5 40.0
subroutine 16 20 80.0
pod 5 7 71.4
total 257 336 76.4


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