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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 57dd413a29c66bd22d3180358883ec568e373c28
4
- data.tar.gz: 4bb6ee9f711267d8b21c541c6436a04729c3c2d9
3
+ metadata.gz: ef9bf25b367b217a2f1f8a3da082c3edefcf8621
4
+ data.tar.gz: 507779eff98d8a1c03d980824c37e8bd8e54416a
5
5
  SHA512:
6
- metadata.gz: 3bb7ccee9c50eb0d09e55eca0a4f7c6f9fa5a45d975c0d3393d48a62b5dc622a7d77cba6c5ad9d3625c9e1d347f1634986588c2c075319b565ab62e67b3e80e3
7
- data.tar.gz: 0bc5705a43c46d1200130385e787a448b10af74431ed38767f30339b999c960a94de3cf4a8ec23aedb3f42597be91798e1d0a161f245af9d05eb97c514ff1f67
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.
@@ -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 parse_headers
37
- self[:data][:headers] = {}
38
- header = ""
39
- self[:data][:text].each do |line|
40
- case
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 Quit < Exception; end
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
- #--- Send text to the client ---------------------------#
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
- begin
100
- case
101
- when text.nil?
102
- # do nothing
103
- when text.class==Array
104
- text.each { |line| write_text(line, echo) }
105
- when text.class==String
106
- write_text(text, echo)
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
- #--- Receive text from the client ----------------------#
119
- #-------------------------------------------------------#
96
+ #=======================================================================
97
+ # receive text from the client
98
+
120
99
  def recv_text(echo=true)
121
- begin
122
- Timeout.timeout(ReceiverTimeout) do
123
- begin
124
- temp = @connection.gets
125
- if temp.nil?
126
- LOG.warn("%06d"%Process::pid) {"The client abruptly closed the connection"}
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
- #--- Parse the email address and investigate it --------#
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
- #--- Receive the connection ----------------------------#
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 Quit => e
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"... "for when sh*t happens"
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
- break if text.nil? # the client closed the channel abruptly
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
- # DKIM
442
- # SPF
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
- all_rcptto_accepted = true
454
- @mail[:rcptto].each { |p| all_rcptto_accepted = false if !p[:accepted] } if @mail.has_key?(:rcptto)
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
- all_rcptto_accepted &&
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
@@ -1,4 +1,4 @@
1
1
  module Net
2
- BUILD_VERSION = "1.1.0"
3
- BUILD_DATE = "2016-09-27"
2
+ BUILD_VERSION = "1.2.0"
3
+ BUILD_DATE = "2016-09-30"
4
4
  end
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.1.0
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-27 00:00:00.000000000 Z
11
+ date: 2016-09-30 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Ruby Net Receiver.
14
14
  email: