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