net-receiver 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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:
|