net-receiver 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -2
- data/lib/net/item_of_mail.rb +6 -39
- data/lib/net/receiver.rb +77 -113
- data/lib/net/version.rb +2 -2
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ef9bf25b367b217a2f1f8a3da082c3edefcf8621
|
4
|
+
data.tar.gz: 507779eff98d8a1c03d980824c37e8bd8e54416a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c293edb6d7d16915411e33838781290137ab4b3fd3ac7ee14e48b8ac9dff01e174dc5baa412bdb046cac9c02554f5e54e89864c9e39b53df5f39b179bbc5b12e
|
7
|
+
data.tar.gz: 89be49e4cfecfed76a66ec46ed80394b13f1b2db4887ac37a30888548ec594544cadfd13c0a10535662a7c4a73602e4cf8e69104ddc32c11c55a589b36d75e05
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
# v1.2.0
|
2
|
+
Simplified the send_text and recv_text by moving the error handling to the end of Receiver#receive, and removing redundent statements. Removed the code which handled those errors (SLAM and TIMEOUT) from the Patterns list, and removed the associated handler code.
|
3
|
+
|
4
|
+
Fixed the accept logic to accept the email if one or more RCPT TO commands were valid (as opposed to requiring all to be valid).
|
5
|
+
|
6
|
+
Added an SPF check for the MAIL FROM address. To learn more about SPF, go to `http://www.openspf.org/`. The SPF check result goes to `@mail[:mailfrom][:spf]`.
|
7
|
+
|
1
8
|
# v1.1.0
|
2
9
|
Changed the defaults for :sender_mx_check and recipient_mx_check to true because the cost is low and we need to know if the sender has a mail server (remote-->local), or if the recipient has a mail server (local-->remote). You can still turn off these checks, if you don't want them.
|
3
10
|
|
@@ -11,7 +18,5 @@ In `psych_value`, added a test for "MAIL FROM: <>" (a bounce message). In the ca
|
|
11
18
|
},
|
12
19
|
````
|
13
20
|
|
14
|
-
|
15
|
-
|
16
21
|
# v1.0.0
|
17
22
|
This is the initial load of the gem. If you find a problem, report it to me at mjwelchphd@gmail.com, or FORK the library, fix the bug, then add a Pull Request.
|
data/lib/net/item_of_mail.rb
CHANGED
@@ -14,6 +14,7 @@
|
|
14
14
|
# See the README.md for documentation.
|
15
15
|
|
16
16
|
require 'sequel'
|
17
|
+
require 'spf'
|
17
18
|
|
18
19
|
module Net
|
19
20
|
class ItemOfMail < Hash
|
@@ -33,45 +34,11 @@ module Net
|
|
33
34
|
self[:time] = Time.now.strftime("%Y-%m-%d %H:%M:%S %z")
|
34
35
|
end
|
35
36
|
|
36
|
-
def
|
37
|
-
|
38
|
-
|
39
|
-
self[:data][:text]
|
40
|
-
|
41
|
-
when line.nil?
|
42
|
-
break
|
43
|
-
when line =~ /^[ \t]/
|
44
|
-
header << String::new(line)
|
45
|
-
when line.empty?
|
46
|
-
break
|
47
|
-
when !header.empty?
|
48
|
-
keyword, value = header.split(":", 2)
|
49
|
-
self[:data][:headers][keyword.downcase.gsub("-","_").to_sym] = value.strip
|
50
|
-
header = String::new(line)
|
51
|
-
else
|
52
|
-
header = String::new(line)
|
53
|
-
end
|
54
|
-
end
|
55
|
-
if !header.empty?
|
56
|
-
keyword, value = header.split(":", 2)
|
57
|
-
self[:data][:headers][keyword.downcase.gsub("-","_").to_sym] = if !value.nil? then value.strip else "" end
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
def spf_check(scope,identity,ip,ehlo)
|
62
|
-
spf_server = SPF::Server.new
|
63
|
-
begin
|
64
|
-
request = SPF::Request.new(
|
65
|
-
versions: [1, 2],
|
66
|
-
scope: scope,
|
67
|
-
identity: identity,
|
68
|
-
ip_address: ip,
|
69
|
-
helo_identity: ehlo)
|
70
|
-
spf_server.process(request).code
|
71
|
-
rescue SPF::OptionRequiredError => e
|
72
|
-
@log.info("%06d"%Process::pid) {"SPF check failed: #{e.to_s}"}
|
73
|
-
:fail
|
74
|
-
end
|
37
|
+
def reconstituted_email
|
38
|
+
text = []
|
39
|
+
self[:data][:headers].each { |k,v| text << "#{k}:#{v}" }
|
40
|
+
text.concat(self[:data][:text])
|
41
|
+
text.join(CRLF)+CRLF
|
75
42
|
end
|
76
43
|
end
|
77
44
|
end
|
data/lib/net/receiver.rb
CHANGED
@@ -17,8 +17,9 @@ require 'net/server'
|
|
17
17
|
require 'net/item_of_mail'
|
18
18
|
require 'net/extended_classes'
|
19
19
|
require 'pdkim'
|
20
|
+
require 'spf'
|
20
21
|
|
21
|
-
class
|
22
|
+
class Slam < Exception; end
|
22
23
|
|
23
24
|
module Net
|
24
25
|
|
@@ -27,8 +28,6 @@ module Net
|
|
27
28
|
CRLF = "\r\n"
|
28
29
|
Patterns = [
|
29
30
|
[0, "[ /t]*QUIT[ /t]*", :do_quit],
|
30
|
-
[0, "[ /t]*SLAM[ /t]*", :do_slam],
|
31
|
-
[0, "[ /t]*TIMEOUT[ /t]*", :do_timeout],
|
32
31
|
[1, "[ /t]*AUTH[ /t]*(.+)", :do_auth],
|
33
32
|
[1, "[ /t]*EHLO(.*)", :do_ehlo],
|
34
33
|
[1, "[ /t]*EXPN[ /t]*", :do_expn],
|
@@ -36,7 +35,6 @@ module Net
|
|
36
35
|
[1, "[ /t]*HELP[ /t]*", :do_help],
|
37
36
|
[1, "[ /t]*NOOP[ /t]*", :do_noop],
|
38
37
|
[1, "[ /t]*RSET[ /t]*", :do_rset],
|
39
|
-
[1, "[ /t]*TIMEOUT[ /t]*", :do_timeout],
|
40
38
|
[1, "[ /t]*VFRY[ /t]*", :do_vfry],
|
41
39
|
[2, "[ /t]*STARTTLS[ /t]*", :do_starttls],
|
42
40
|
[2, "[ /t]*MAIL FROM[ /t]*:[ \t]*(.+)", :do_mail_from],
|
@@ -67,7 +65,8 @@ module Net
|
|
67
65
|
@connection = connection
|
68
66
|
@option_list = [[:ehlo_validation_check, false], [:sender_character_check, true],
|
69
67
|
[:recipient_character_check, true], [:sender_mx_check, true],
|
70
|
-
[:recipient_mx_check, true],[:max_failed_msgs_per_period,3]
|
68
|
+
[:recipient_mx_check, true],[:max_failed_msgs_per_period,3],
|
69
|
+
[:copy_to_sysout, false]]
|
71
70
|
@options = options
|
72
71
|
@option_list.each do |key,value|
|
73
72
|
@options[key] = value if !options.has_key?(key)
|
@@ -75,80 +74,41 @@ module Net
|
|
75
74
|
@enc_ind = '-'
|
76
75
|
end
|
77
76
|
|
78
|
-
|
79
|
-
|
80
|
-
#-------------------------------------------------------#
|
81
|
-
def log_msg_if_level_5(msg)
|
82
|
-
if msg[0]=='5'
|
83
|
-
m = msg.match(/^([0-9]{3} [0-9]\.[0-9]\.[0-9] )/)
|
84
|
-
start = if !m then 0 else m[1].size end
|
85
|
-
LOG.error("%06d"%Process::pid) {msg[start..-1]}
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
def write_text(text, echo)
|
90
|
-
puts "<#{@enc_ind} #{text.inspect}" # DEBUG!
|
91
|
-
@connection.write(text)
|
92
|
-
@connection.write(CRLF)
|
93
|
-
@has_level_5_warnings = true if text[0]=='5'
|
94
|
-
LOG.info("%06d"%Process::pid) {"<#{@enc_ind} #{text}"} if echo && LogConversation
|
95
|
-
log_msg_if_level_5(text)
|
96
|
-
end
|
77
|
+
#=======================================================================
|
78
|
+
# send text to the client
|
97
79
|
|
98
80
|
def send_text(text,echo=true)
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
#
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
81
|
+
if !text.nil?
|
82
|
+
text = [ text ] if text.class==String
|
83
|
+
text.each do |line|
|
84
|
+
puts "<#{@enc_ind} #{text.inspect}" if @options[:copy_to_sysout]
|
85
|
+
@connection.write(line)
|
86
|
+
@connection.write(CRLF)
|
87
|
+
@has_level_5_warnings = true if line[0]=='5'
|
88
|
+
LOG.info("%06d"%Process::pid) {"<#{@enc_ind} #{text}"} if echo && LogConversation
|
89
|
+
m = line.match(/^5[0-9]{2} [0-9]\.[0-9]\.[0-9] (.*)$/)
|
90
|
+
LOG.error("%06d"%Process::pid) {m[1]} if m
|
107
91
|
end
|
108
|
-
rescue Errno::EPIPE => e
|
109
|
-
LOG.error("%06d"%Process::pid) {"#{e.to_s}#{Unexpectedly}"}
|
110
|
-
raise Quit
|
111
|
-
rescue Errno::EIO => e
|
112
|
-
LOG.error("%06d"%Process::pid) {"#{e.to_s}#{Unexpectedly}"}
|
113
|
-
raise Quit
|
114
92
|
end
|
93
|
+
return nil
|
115
94
|
end
|
116
95
|
|
117
|
-
|
118
|
-
|
119
|
-
|
96
|
+
#=======================================================================
|
97
|
+
# receive text from the client
|
98
|
+
|
120
99
|
def recv_text(echo=true)
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
text = "QUIT"
|
128
|
-
else
|
129
|
-
text = temp.chomp
|
130
|
-
end
|
131
|
-
rescue Errno::ECONNRESET => e
|
132
|
-
LOG.warn("%06d"%Process::pid) {"The client slammed the connection shut"}
|
133
|
-
text = "SLAM"
|
134
|
-
end
|
135
|
-
LOG.info("%06d"%Process::pid) {" #{@enc_ind}> #{text}"} if echo && LogConversation
|
136
|
-
puts " #{@enc_ind}> #{text.inspect}" # DEBUG!
|
137
|
-
return text
|
138
|
-
end
|
139
|
-
rescue Errno::EIO => e
|
140
|
-
LOG.error("%06d"%Process::pid) {"#{e.to_s}"}
|
141
|
-
raise Quit
|
142
|
-
rescue Timeout::Error => e
|
143
|
-
puts " #{@enc_ind}> \"TIMEOUT\"" # DEBUG!
|
144
|
-
return "TIMEOUT"
|
100
|
+
Timeout.timeout(ReceiverTimeout) do
|
101
|
+
raise Slam if (temp = @connection.gets).nil?
|
102
|
+
text = temp.chomp
|
103
|
+
LOG.info("%06d"%Process::pid) {" #{@enc_ind}> #{text}"} if echo && LogConversation
|
104
|
+
puts " #{@enc_ind}> #{text.inspect}" if @options[:copy_to_sysout]
|
105
|
+
return text
|
145
106
|
end
|
146
|
-
puts " #{@enc_ind}> *669* Investigate why this got here" # DEBUG!
|
147
107
|
end
|
148
108
|
|
149
|
-
|
150
|
-
|
151
|
-
|
109
|
+
#=======================================================================
|
110
|
+
# parse the email address and investigate it
|
111
|
+
|
152
112
|
def psych_value(kind, part, value)
|
153
113
|
# the value gets set in both MAIL FROM and RCPT TO
|
154
114
|
part[:value] = value
|
@@ -206,9 +166,9 @@ puts " #{@enc_ind}> *669* Investigate why this got here" # DEBUG!
|
|
206
166
|
return nil
|
207
167
|
end
|
208
168
|
|
209
|
-
|
210
|
-
|
211
|
-
|
169
|
+
#=======================================================================
|
170
|
+
# receive the connection
|
171
|
+
|
212
172
|
def receive(local_port, local_hostname, remote_port, remote_hostname, remote_ip)
|
213
173
|
# Start a hash to collect the information gathered from the receive process
|
214
174
|
@mail = Net::ItemOfMail::new(local_port, local_hostname, remote_port, remote_hostname, remote_ip)
|
@@ -234,8 +194,6 @@ puts " #{@enc_ind}> *669* Investigate why this got here" # DEBUG!
|
|
234
194
|
case
|
235
195
|
when pattern[2]==:do_quit
|
236
196
|
send_text(do_quit(m[1]))
|
237
|
-
when pattern[2]==:do_slam
|
238
|
-
send_text(do_slam(m[1]))
|
239
197
|
when @mail[:prohibited]
|
240
198
|
send_text("450 4.7.1 Sender IP #{@mail[:remote_ip]} is temporarily prohibited from sending")
|
241
199
|
when pattern[0]>@level
|
@@ -251,18 +209,18 @@ puts " #{@enc_ind}> *669* Investigate why this got here" # DEBUG!
|
|
251
209
|
response = "500 5.5.1 Unrecognized command #{text.inspect}, incorrectly formatted command, or command out of sequence"
|
252
210
|
send_text(response)
|
253
211
|
end
|
254
|
-
rescue OpenSSL::SSL::SSLError => e
|
255
|
-
LOG.error("%06d"%Process::pid) {"SSL error: #{e.inspect}"}
|
256
|
-
e.backtrace.each { |line| LOG.error("%06d"%Process::pid) {line} }
|
257
|
-
@done = true
|
258
212
|
end until @done
|
259
213
|
|
260
|
-
rescue
|
214
|
+
rescue OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::EIO, Errno::EPIPE, Timeout::Error => e
|
215
|
+
LOG.error("%06d"%Process::pid) {e}
|
216
|
+
@mail[:accepted] = false
|
217
|
+
|
218
|
+
rescue Slam
|
219
|
+
LOG.info("%06d"%Process::pid) {"Sender slammed the connection shut IP=#{@mail[:remote_ip]}"}
|
261
220
|
@mail[:accepted] = false
|
262
|
-
# nothing to do but exit
|
263
221
|
|
264
222
|
rescue => e
|
265
|
-
# this is the "rescue of last resort"...
|
223
|
+
# this is the "rescue of last resort"... for "when sh*t happens"
|
266
224
|
LOG.fatal("%06d"%Process::pid) {e.inspect}
|
267
225
|
e.backtrace.each { |line| LOG.fatal("%06d"%Process::pid) {line} }
|
268
226
|
@mail[:accepted] = false
|
@@ -280,7 +238,8 @@ puts " #{@enc_ind}> *669* Investigate why this got here" # DEBUG!
|
|
280
238
|
end
|
281
239
|
|
282
240
|
#=======================================================================
|
283
|
-
# these methods provide all the basic processing
|
241
|
+
# these methods provide all the basic processing that needs to be done
|
242
|
+
# regardless of any additional checks that you make want to make
|
284
243
|
|
285
244
|
def ok?(msg)
|
286
245
|
msg[0]!='4' && msg[0]!='5'
|
@@ -318,19 +277,6 @@ puts " #{@enc_ind}> *669* Investigate why this got here" # DEBUG!
|
|
318
277
|
return msg
|
319
278
|
end
|
320
279
|
|
321
|
-
def do_slam(value)
|
322
|
-
LOG.info("%06d"%Process::pid) {"Sender slammed the connection shut IP=#{@mail[:remote_ip]}"}
|
323
|
-
@done = true
|
324
|
-
@mail[:accepted] = false
|
325
|
-
return nil
|
326
|
-
end
|
327
|
-
|
328
|
-
def do_timeout(value)
|
329
|
-
@done = true
|
330
|
-
@mail[:accepted] = false
|
331
|
-
return ("501 5.4.7 Closing connection due to inactivity--#{@mail[:id]} was NOT saved")
|
332
|
-
end
|
333
|
-
|
334
280
|
def do_auth(value)
|
335
281
|
auth_type, auth_encoded = value.split
|
336
282
|
# auth_encoded contains both username and password
|
@@ -387,7 +333,6 @@ puts " #{@enc_ind}> *669* Investigate why this got here" # DEBUG!
|
|
387
333
|
def do_mail_from(value)
|
388
334
|
@mail[:mailfrom] = p = {:accepted=>false}
|
389
335
|
@mail[:rcptto] = []
|
390
|
-
# TODO! A special case is the NULL envelope sender address (i.e. MAIL FROM: <>)
|
391
336
|
msg = psych_value(:mailfrom, p, value)
|
392
337
|
return (msg) if msg
|
393
338
|
|
@@ -413,33 +358,36 @@ puts " #{@enc_ind}> *669* Investigate why this got here" # DEBUG!
|
|
413
358
|
end
|
414
359
|
|
415
360
|
def do_data(value)
|
416
|
-
# http://www.tldp.org/HOWTO/Spam-Filtering-for-MX/datachecks.html
|
417
361
|
@mail[:data] = body = {}
|
418
362
|
body[:accepted] = false
|
419
363
|
# receive the body of the mail
|
420
364
|
body[:value] = value # this should be nil -- no argument on the DATA command
|
365
|
+
body[:headers] = headers = {}
|
421
366
|
body[:text] = lines = []
|
422
367
|
send_text("354 3.0.0 Enter message, ending with \".\" on a line by itself", false)
|
423
368
|
LOG.info("%06d"%Process::pid) {" -> (email message)"} if LogConversation
|
369
|
+
|
370
|
+
# get the headers into a hash
|
424
371
|
while true
|
425
372
|
text = recv_text(false)
|
426
|
-
|
427
|
-
lines << text
|
428
|
-
if text=="."
|
373
|
+
if text.strip.empty?
|
429
374
|
body[:accepted] = true
|
430
375
|
break
|
431
376
|
end
|
377
|
+
m = text.match(/^(.+?):(.+)$/)
|
378
|
+
return "501 5.5.2 Malformed header" if m.nil?
|
379
|
+
headers[m[1]] = m[2]
|
432
380
|
end
|
433
|
-
@mail.parse_headers
|
434
|
-
# should contain:
|
435
|
-
# To: ...
|
436
|
-
# Date: ...
|
437
|
-
# From: ...
|
438
|
-
# Subject: ...
|
439
|
-
# Message-ID: ...
|
440
381
|
|
441
|
-
#
|
442
|
-
|
382
|
+
# get the body into an array of strings
|
383
|
+
while true
|
384
|
+
text = recv_text(false)
|
385
|
+
if text=="."
|
386
|
+
body[:accepted] = true
|
387
|
+
break
|
388
|
+
end
|
389
|
+
lines << text
|
390
|
+
end
|
443
391
|
|
444
392
|
# check the DKIM headers, if any
|
445
393
|
ok, signatures = pdkim_verify_an_email(PDKIM_INPUT_NORMAL, @mail[:data][:text])
|
@@ -449,13 +397,28 @@ puts " #{@enc_ind}> *669* Investigate why this got here" # DEBUG!
|
|
449
397
|
@mail[:signatures] << [signature[:domain], signature[:verify_status], DkimOutcomes[signature[:verify_status]]]
|
450
398
|
end if ok==PDKIM_OK
|
451
399
|
|
400
|
+
# check the SPF, if any
|
401
|
+
begin
|
402
|
+
spf_server = SPF::Server.new
|
403
|
+
request = SPF::Request.new(
|
404
|
+
versions: [1, 2],
|
405
|
+
scope: 'mfrom',
|
406
|
+
identity: @mail[:mailfrom][:url],
|
407
|
+
ip_address: @mail[:remote_ip],
|
408
|
+
helo_identity: @mail[:ehlo][:domain])
|
409
|
+
@mail[:mailfrom][:spf] = spf_server.process(request).code
|
410
|
+
rescue SPF::OptionRequiredError => e
|
411
|
+
@log.info("%06d"%Process::pid) {"SPF check failed: #{e.to_s}"}
|
412
|
+
@mail[:mailfrom][:spf] = :fail
|
413
|
+
end
|
414
|
+
|
452
415
|
# test all the RCPT TOs
|
453
|
-
|
454
|
-
@mail[:rcptto].each { |p|
|
416
|
+
any_rcptto_accepted = false
|
417
|
+
@mail[:rcptto].each { |p| any_rcptto_accepted = true if p[:accepted] } if @mail.has_key?(:rcptto)
|
455
418
|
# passed thru the guantlet with no failures
|
456
419
|
@mail[:accepted] = true \
|
457
420
|
if @mail[:mailfrom][:accepted] &&
|
458
|
-
|
421
|
+
any_rcptto_accepted &&
|
459
422
|
@mail[:data][:accepted] &&
|
460
423
|
@has_level_5_warnings==false
|
461
424
|
|
@@ -465,7 +428,8 @@ puts " #{@enc_ind}> *669* Investigate why this got here" # DEBUG!
|
|
465
428
|
end
|
466
429
|
|
467
430
|
#=======================================================================
|
468
|
-
# these are the defaults, in case the user doesn't override
|
431
|
+
# these are the defaults, in case the user doesn't override--you can
|
432
|
+
# override these in your Receiver class in order to add tests
|
469
433
|
|
470
434
|
def connect(remote_ip)
|
471
435
|
return "220 2.0.0 ESMTP RubyMTA 0.01 #{Time.new.strftime("%^a, %d %^b %Y %H:%M:%S %z")}"
|
data/lib/net/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: net-receiver
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael J. Welch, Ph.D.
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-09-
|
11
|
+
date: 2016-09-30 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Ruby Net Receiver.
|
14
14
|
email:
|