| 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; |