File Coverage

blib/lib/Mail/Colander/Server.pm
Criterion Covered Total %
statement 47 157 29.9
branch 0 42 0.0
condition 0 12 0.0
subroutine 16 29 55.1
pod 1 1 100.0
total 64 241 26.5


line stmt bran cond sub pod time code
1             package Mail::Colander::Server;
2 1     1   2837 use v5.24;
  1         5  
3 1     1   6 use warnings;
  1         2  
  1         71  
4 1     1   6 use experimental qw< signatures >;
  1         3  
  1         19  
5             { our $VERSION = '0.004' }
6              
7 1     1   248 use constant DEFAULT_CHAIN => 'DEFAULT';
  1         3  
  1         80  
8              
9 1     1   6 use Ouch qw< :trytiny_var >;
  1         29  
  1         7  
10 1     1   121 use Try::Catch;
  1         3  
  1         68  
11 1     1   10 use Scalar::Util qw< blessed >;
  1         2  
  1         51  
12 1     1   693 use Log::Any qw< $log >;
  1         11295  
  1         7  
13 1     1   3818 use Mail::Colander::Session;
  1         3  
  1         38  
14 1     1   568 use Mail::Colander::Server::Util qw< xxd_message >;
  1         3  
  1         63  
15 1     1   6 use Data::Annotation::Overlay;
  1         1  
  1         42  
16 1     1   690 use IO::Handle;
  1         6904  
  1         50  
17 1     1   6 use Module::Runtime qw< require_module >;
  1         1  
  1         7  
18 1     1   654 use Net::Server::Mail::ESMTP;
  1         9764  
  1         36  
19 1     1   6 use JSON::PP ();
  1         1  
  1         18  
20              
21 1     1   3 use Exporter qw< import >;
  1         1  
  1         1606  
22             our @EXPORT_OK = qw<
23             mojo_ioloop_server_callback_factory
24             >;
25             our %EXPORT_TAGS = (all => \@EXPORT_OK);
26              
27 0     0     sub _encode_json_pretty ($data) {
  0            
  0            
28 0           state $encoder = JSON::PP->new->ascii->canonical->pretty;
29 0           return $encoder->encode($data);
30             }
31              
32 0     0     sub _require_class_module ($module) {
  0            
  0            
33 0 0         require_module($module) unless $module->can('new');
34 0           return $module;
35             }
36              
37 0     0     sub _resolve ($in, $class) {
  0            
  0            
  0            
38 0 0         return blessed($in) ? $in : _require_class_module($class)->new($in);
39             }
40              
41 0     0     sub _argslist ($in) {
  0            
  0            
42 0 0         ref($in) eq 'ARRAY' ? $in->@* : defined($in) ? $in->%* : ()
    0          
43             }
44              
45 0     0     sub _sieve_call ($sieve, $command, $overlay) {
  0            
  0            
  0            
  0            
46             my $outcome = try {
47 0     0     my ($out, $data, $call_sequence)
48             = $sieve->policy_for($command, $overlay);
49              
50 0 0 0       if ($log->is_debug && $call_sequence && $call_sequence->@*) {
      0        
51 0           my $calls = join "\n", map { ' ' . $_->{chain} } $call_sequence->@*;
  0            
52             $log->debug(
53             join ' ',
54             "Colander check $command:",
55             (
56             join ' -> ',
57             map {
58 0           my ($chain, $rule) = $_->@{qw< chain rule >};
59 0 0         length($rule) ? "$chain $rule" : $chain;
60             } $call_sequence->@*
61             ),
62             '=> ' . $call_sequence->[-1]{outcome}
63 0           );
64             }
65 0 0         $log->trace(_encode_json_pretty($call_sequence))
66             if $log->is_trace;
67              
68 0           $out eq 'accept';
69             }
70             catch {
71 0     0     my $e = $_;
72 0           $log->error(_encode_json_pretty($e->data));
73 0           undef;
74 0           };
75 0           return $outcome;
76             }
77              
78 0     0 1   sub mojo_ioloop_server_callback_factory (%args) {
  0            
  0            
79 0           _require_class_module(__PACKAGE__ . '::IOWrapper');
80              
81             my @extensions = map {
82 0 0         m{\A /}mxs ? substr($_, 1) : 'Net::Server::Mail::ESMTP::' . $_
83 0   0       } ($args{esmtp_extensions} // [])->@*;
84              
85             my %subargs = (
86             sieve => _resolve($args{sieve}, 'Mail::Colander'),
87             esmtp_args => [ _argslist($args{esmtp_args}) ],
88             esmtp_extensions => \@extensions,
89 0   0       callback_for => ($args{callback_for} // {}),
90             );
91              
92 0     0     return sub ($loop, $stream, $id) {
  0            
  0            
  0            
  0            
93              
94 0           my $sh = $stream->handle;
95 0           my ($ip, $port) = ($sh->peerhost, $sh->peerport);
96 0           $log->debug("$id: connection from $ip:$port");
97              
98             # this is what will handle the SMTP exchange for us
99 0           my ($banner, $reader) = _mios_smtp_factory($stream, %subargs);
100 0 0         if (! defined($banner)) { # connection has not been accepted!
101 0           $stream->close;
102 0           return;
103             }
104              
105             # "connect" the stream to input parsing
106 0           $stream->on(close => sub ($stream) { $log->debug("$id: closed") });
  0            
107 0           $stream->on(error => sub ($stream, $error) { ... });
  0            
108             $stream->on(
109             read => sub ($stream, $bytes) {
110 0 0         if ($log->is_trace) {
111 0           my $n_bytes = length($bytes);
112 0           $log->trace($_) for (
113             "$id: $n_bytes",
114             xxd_message($bytes, max_lines => -1),
115             );
116             }
117 0 0         if ($log->is_debug) {
118 0           my $n_bytes = length($bytes);
119 0           $log->trace($_) for (
120             "$id: $n_bytes",
121             xxd_message($bytes, max_lines => 3),
122             );
123             }
124 0 0         $stream->close if defined($reader->($bytes));
125             }
126 0           );
127 0           $stream->on(timeout => sub ($strm) { $log->info("$id: timed out") });
  0            
128 0   0       $stream->timeout($args{timeout} // 3);
129              
130             # setup complete, kick-start the ESMTP session
131 0           $banner->();
132 0           };
133             }
134              
135 0     0     sub _mios_smtp_factory ($stream, %args) {
  0            
  0            
  0            
136              
137             # this is used to figure out whether something can be admitted or not
138 0           my $sieve = $args{sieve};
139              
140             # collect events inside a $session object that we can eventually
141             # pass down to the $sieve
142 0           my $sh = $stream->handle;
143 0           my $session = Mail::Colander::Session->new(
144             peer_ip => $sh->peerhost,
145             peer_port => $sh->peerport,
146             );
147 0           my $overlay = Data::Annotation::Overlay->new(
148             under => $session,
149             cache_existing => 0,
150             );
151              
152             # first of all collect the peer IP address and figure out whether
153             # it's worth bothering or not
154 0           my $outcome = _sieve_call($sieve, connect => $overlay);
155 0 0         if ($outcome) {
156             $args{callback_for}{connect}->($session)
157 0 0         if $args{callback_for}{connect};
158             }
159             else {
160             $args{callback_for}{reject}->(connect => $session)
161 0 0         if $args{callback_for}{reject};
162 0           return;
163             }
164              
165             # this wraps IO operations to make Net::Server::Mail::ESMTP happy
166             # for interacting with IO::Handles and pass data around.
167 0           my $iowrap = Mail::Colander::Server::IOWrapper->new(stream => $stream);
168              
169             my $smtp_in = Net::Server::Mail::ESMTP->new(
170              
171             # defaults
172             error_sleep_time => 2,
173             idle_timeout => 5,
174              
175             # whatever came in
176             $args{esmtp_args}->@*,
177              
178             # overridden for sure
179 0 0         handle_in => IO::Handle->new, # anything goes
180             handle_out => $iowrap->ofh,
181              
182             ) or ouch 500, "can't start server";
183              
184             # register supported extensions
185 0           $smtp_in->register($_) for $args{esmtp_extensions}->@*;
186              
187 0           my @cmds = qw< HELO EHLO MAIL RCPT DATA-INIT DATA-PART DATA QUIT >;
188 0           for my $command (@cmds) {
189 0 0         my $method = $session->can($command =~ s{\W+}{_}rgmxs)
190             or next; # no support, no party
191              
192             $smtp_in->set_callback(
193             $command,
194             sub {
195 0 0   0     return unless eval { $session->$method(@_) }; # accumulate
  0            
196              
197             # call the $sieve if so instructed
198 0           my $outcome = _sieve_call($sieve, $command, $overlay);
199 0 0         if (! $outcome) {
200             $args{callback_for}{reject}->($command, $session)
201 0 0         if $args{callback_for}{reject};
202 0           $session->reset;
203 0           return;
204             }
205              
206             # if we are here we can hand over to the callbacks, if any,
207             # or just return a true value.
208 0 0         return 1 unless defined($args{callback_for}{$command});
209 0           return $args{callback_for}{$command}->($session);
210             }
211 0           );
212             }
213              
214             # return a pair of callbacks, one for sending out the banner and one
215             # for processing data as they arrive.
216             return (
217             sub {
218 0     0     my $rv = $smtp_in->banner;
219 0           $iowrap->write_output;
220 0           return $rv;
221             },
222             sub {
223 0     0     my $rv = $smtp_in->process_once($iowrap->read_input($_[0]));
224 0           $iowrap->write_output;
225 0           return $rv;
226             },
227 0           );
228              
229             }
230              
231             1;