File Coverage

blib/lib/Mail/MtPolicyd/Plugin/SMTPVerify.pm
Criterion Covered Total %
statement 55 70 78.5
branch 15 34 44.1
condition n/a
subroutine 8 8 100.0
pod 1 2 50.0
total 79 114 69.3


line stmt bran cond sub pod time code
1             package Mail::MtPolicyd::Plugin::SMTPVerify;
2              
3 2     2   2339 use Moose;
  2         2  
  2         10  
4 2     2   8113 use namespace::autoclean;
  2         3  
  2         12  
5              
6             our $VERSION = '2.01'; # VERSION
7             # ABSTRACT: mtpolicyd plugin for remote SMTP address checks
8              
9             extends 'Mail::MtPolicyd::Plugin';
10              
11             with 'Mail::MtPolicyd::Plugin::Role::Scoring';
12             with 'Mail::MtPolicyd::Plugin::Role::UserConfig' => {
13             'uc_attributes' => [ 'enabled' ],
14             };
15              
16 2     2   173 use Mail::MtPolicyd::Plugin::Result;
  2         2  
  2         34  
17              
18 2     2   899 use Net::SMTP::Verify;
  2         590547  
  2         1453  
19              
20              
21             has 'enabled' => ( is => 'rw', isa => 'Str', default => 'on' );
22              
23             has 'host' => ( is => 'ro', isa => 'Maybe[Str]' );
24             has 'port' => ( is => 'ro', isa => 'Maybe[Int]' );
25              
26             has 'check_tlsa' => ( is => 'ro', isa => 'Str', default => 'off' );
27             has 'check_openpgp' => ( is => 'ro', isa => 'Str', default => 'off' );
28              
29             with 'Mail::MtPolicyd::Plugin::Role::ConfigurableFields' => {
30             'fields' => {
31             'size' => {
32             isa => 'Str',
33             default => 'size',
34             value_isa => 'Int',
35             },
36             'sender' => {
37             isa => 'Str',
38             default => 'recipient',
39             value_isa => 'Str',
40             },
41             'recipient' => {
42             isa => 'Str',
43             default => 'sender',
44             value_isa => 'Str',
45             },
46             },
47             };
48              
49             has 'temp_fail_action' => ( is => 'rw', isa => 'Maybe[Str]' );
50             has 'temp_fail_score' => ( is => 'rw', isa => 'Maybe[Num]' );
51             has 'perm_fail_action' => ( is => 'rw', isa => 'Maybe[Str]' );
52             has 'perm_fail_score' => ( is => 'rw', isa => 'Maybe[Num]' );
53              
54             has 'has_starttls_score' => ( is => 'rw', isa => 'Maybe[Num]' );
55             has 'no_starttls_score' => ( is => 'rw', isa => 'Maybe[Num]' );
56              
57             has 'has_tlsa_score' => ( is => 'rw', isa => 'Maybe[Num]' );
58             has 'no_tlsa_score' => ( is => 'rw', isa => 'Maybe[Num]' );
59              
60             has 'has_openpgp_score' => ( is => 'rw', isa => 'Maybe[Num]' );
61             has 'no_openpgp_score' => ( is => 'rw', isa => 'Maybe[Num]' );
62              
63             has 'sender' => ( is => 'ro', isa => 'Maybe[Str]' );
64              
65             # store current request for logging_callback
66             has '_current_request' => (
67             is => 'rw', isa => 'Maybe[Mail::MtPolicyd::Request]'
68             );
69              
70             has '_verify' => ( is => 'ro', isa => 'Net::SMTP::Verify', lazy => 1,
71             default => sub {
72             my $self = shift;
73             return Net::SMTP::Verify->new(
74             defined $self->host ? ( host => $self->host ) : (),
75             defined $self->port ? ( port => $self->port ) : (),
76             $self->check_tlsa eq 'on' ? ( tlsa => 1 ) : (),
77             $self->check_openpgp eq 'on' ? ( openpgp => 1 ) : (),
78             logging_callback => sub {
79             my $msg = shift;
80             my $r = $self->_current_request;
81             if( defined $r ) {
82             $self->log( $r, $msg );
83             }
84             return;
85             },
86             );
87             },
88             );
89              
90             sub get_sender {
91 2     2 0 3 my ( $self, $r ) = @_;
92 2 50       58 if( defined $self->sender ) {
93 0         0 return( $self->sender );
94             }
95 2         10 return $self->get_sender_value( $r );
96             }
97              
98             sub run {
99 2     2 1 429 my ( $self, $r ) = @_;
100 2         65 $self->_current_request( $r );
101 2         56 my $session = $r->session;
102              
103 2 50       23 if( $self->get_uc( $session, 'enabled') eq 'off' ) {
104 0         0 return;
105             }
106 2         17 my $size = $self->get_size_value( $r );
107 2         13 my $sender = $self->get_sender( $r );
108 2         7 my $recipient = $self->get_recipient_value( $r );
109              
110 2 50       24 if( $r->is_already_done('verify-'.$recipient) ) {
111 0         0 return;
112             }
113              
114 2         59 my $result = $self->_verify->check(
115             $size, $sender, $recipient
116             );
117 2 50       2274 if( ! $result->count ) {
118 0         0 die('Net::SMTP::Verify returned empty resultset!'); # should not happen
119             }
120 2         86 my ( $rcpt ) = $result->entries;
121              
122 2         22 $self->_apply_score( $r, $rcpt, 'starttls' );
123              
124 2 50       57 if( $self->check_tlsa eq 'on' ) {
125 0         0 $self->_apply_score( $r, $rcpt, 'tlsa' );
126             }
127 2 50       56 if( $self->check_openpgp eq 'on' ) {
128 0         0 $self->_apply_score( $r, $rcpt, 'openpgp' );
129             }
130              
131 2 100       13 if( $rcpt->is_error ) {
132 1         88 return $self->_handle_rcpt_error( $r, $rcpt );
133             }
134              
135 1         117 $self->_current_request( undef );
136 1         20 return;
137             }
138              
139             sub _apply_score {
140 2     2   4 my ( $self, $r, $rcpt, $name ) = @_;
141 2         5 my $field = 'has_'.$name;
142 2         49 my $value = $rcpt->$field;
143 2 50       13 if( ! defined $value ) {
144 0         0 return;
145             }
146              
147 2         2 my $score_field;
148 2 50       15 if( $value ) {
149 2         4 $score_field = 'has_'.$name.'_score';
150             } else {
151 0         0 $score_field = 'no_'.$name.'_score';
152             }
153 2         66 my $score = $self->$score_field;
154 2 50       5 if( ! defined $score ) {
155 0         0 return;
156             }
157              
158 2         59 $self->add_score($r,
159             $self->name.'-'.$rcpt->address.'-'.$name => $score );
160              
161 2         4 return;
162             }
163              
164             sub _handle_rcpt_error {
165 1     1   6 my ( $self, $r, $rcpt ) = @_;
166 1         1 my $action;
167              
168 1 50       4 if( $rcpt->is_perm_error ) {
    0          
169 1 50       91 if( defined $self->perm_fail_action ) {
170 1         27 $action = $self->perm_fail_action;
171             }
172 1 50       29 if( defined $self->perm_fail_score ) {
173 1         23 $self->add_score($r,
174             $self->name.'-'.$rcpt->address => $self->perm_fail_score);
175             }
176             } elsif( $rcpt->is_temp_error ) {
177 0 0       0 if( defined $self->temp_fail_action ) {
178 0         0 $action = $self->temp_fail_action;
179             }
180 0 0       0 if( defined $self->temp_fail_score ) {
181 0         0 $self->add_score($r,
182             $self->name.'-'.$rcpt->address => $self->temp_fail_score );
183             }
184             } else {
185 0         0 return;
186             }
187              
188 1 50       4 if( ! defined $action ) {
189 0         0 return;
190             }
191            
192 1         23 my $msg = $rcpt->smtp_message;
193 1         11 $action =~ s/%MSG%/$msg/;
194              
195 1         41 return Mail::MtPolicyd::Plugin::Result->new(
196             action => $action,
197             abort => 1,
198             );
199             }
200              
201             __PACKAGE__->meta->make_immutable;
202              
203             1;
204              
205             __END__
206              
207             =pod
208              
209             =encoding UTF-8
210              
211             =head1 NAME
212              
213             Mail::MtPolicyd::Plugin::SMTPVerify - mtpolicyd plugin for remote SMTP address checks
214              
215             =head1 VERSION
216              
217             version 2.01
218              
219             =head1 DESCRIPTION
220              
221             This plugin can be used to do remote SMTP verification of addresses.
222              
223             =head1 Example
224              
225             To check if the recipient exists on a internal relay and mailbox is able
226             to recieve a message of this size:
227              
228             <Plugin smtp-rcpt-check>
229             module = "SMTPVerify"
230            
231             host = "mail.company.internal"
232             sender_field = "sender"
233             recipient_field = "recipient"
234             # send SIZE to check quota
235             size_field = "size"
236              
237             temp_fail_action = "defer %MSG%"
238             perm_fail_action = "reject %MSG%"
239             </Plugin>
240              
241             Do some very strict checks on sender address:
242              
243             <Plugin sender-sender-check>
244             module = "SMTPVerify"
245              
246             # use a verifiable address in MAIL FROM:
247             sender = "horst@mydomain.tld"
248             recipient_field = "sender"
249             no_starttls_action = "reject sender address does not support STARTTLS"
250             temp_fail_action = "defer sender address failed verification: %MSG%"
251             perm_fail_action = "reject sender address does not accept mail: %MSG%"
252             </Plugin>
253              
254             Or do advanced checking of sender address and apply a score:
255              
256             <Plugin sender-sender-check>
257             module = "SMTPVerify"
258              
259             # use a verifiable address in MAIL FROM:
260             sender = "horst@mydomain.tld"
261             recipient_field = "sender"
262             check_tlsa = "on"
263             check_openpgp = "on"
264              
265             temp_fail_score = "1"
266             perm_fail_score = "3"
267              
268             has_starttls_score = "-1"
269             no_starttls_score = "5"
270             has_tlsa_score = "-3"
271             has_openpgp_score = "-3"
272             </Plugin>
273              
274             Based on the score you can later apply greylisting or other actions.
275              
276             =head1 Configuration
277              
278             =head2 Parameters
279              
280             The module takes the following parameters:
281              
282             =over
283              
284             =item (uc_)enabled (default: on)
285              
286             Enable/disable this check.
287              
288             =item host (default: empty)
289              
290             If defined this host will be used for checks instead of a MX.
291              
292             =item port (default: 25)
293              
294             Port to use for connection.
295              
296             =item check_tlsa (default: off)
297              
298             Set to 'on' to enable check if an TLSA record for the MX exists.
299              
300             This requires that your DNS resolver returnes the AD flag for DNSSEC
301             secured records.
302              
303             =item check_openpgp (default: off)
304              
305             Set to 'on' to enable check if an OPENPGPKEY records for the
306             recipients exists.
307              
308             =item sender_field (default: recipient)
309              
310             Field to take the MAIL FROM address from.
311              
312             =item sender (default: empty)
313              
314             If set use this fixed sender in MAIL FROM instead of sender_field.
315              
316             =item recipient_field (default: sender)
317              
318             Field to take the RCPT TO address from.
319              
320             =item size_field (default: size)
321              
322             Field to take the message SIZE from.
323              
324             =item perm_fail_action (default: empty)
325              
326             Action to return if the remote server returned an permanent error
327             for this recipient.
328              
329             The string "%MSG%" will be replaced by the smtp message:
330              
331             perm_fail_action = "reject %MSG%"
332              
333             =item temp_fail_action (default: empty)
334              
335             Like perm_fail_action but this message is returned when an temporary
336             error is returned by the remote smtp server.
337              
338             temp_fail_action = "defer %MSG%"
339              
340             =item perm_fail_score (default: empty)
341              
342             Score to apply when a permanent error is returned for this recipient.
343              
344             =item temp_fail_score (default: empty)
345              
346             Score to apply when a temporary error is returned for this recipient.
347              
348             =item has_starttls_score (default: emtpy)
349              
350             =item no_starttls_score (default: emtpy)
351              
352             Score to apply when the smtp server of the recipient
353             announces support for STARTTLS extension.
354              
355             =item has_tlsa_score (default: empty)
356              
357             =item no_tlsa_score (default: empty)
358              
359             Score to apply when there is a TLSA or no TLSA record
360             for the remote SMTP server.
361              
362             =item has_openpgp_score (default: empty)
363              
364             =item no_openpgp_score (default: empty)
365              
366             Score to apply when a OPENPGPKEY record for the recpient
367             exists or not exists.
368              
369             =back
370              
371             =head1 AUTHOR
372              
373             Markus Benning <ich@markusbenning.de>
374              
375             =head1 COPYRIGHT AND LICENSE
376              
377             This software is Copyright (c) 2014 by Markus Benning <ich@markusbenning.de>.
378              
379             This is free software, licensed under:
380              
381             The GNU General Public License, Version 2, June 1991
382              
383             =cut