groat-smtpd 0.1.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.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/LICENSE +202 -0
- data/Rakefile +2 -0
- data/groat-smtpd.gemspec +30 -0
- data/lib/groat/smtpd.rb +22 -0
- data/lib/groat/smtpd/LICENSE +202 -0
- data/lib/groat/smtpd/base.rb +159 -0
- data/lib/groat/smtpd/extensions/authentication.rb +164 -0
- data/lib/groat/smtpd/extensions/binarymime.rb +48 -0
- data/lib/groat/smtpd/extensions/chunking.rb +57 -0
- data/lib/groat/smtpd/extensions/eightbitmime.rb +47 -0
- data/lib/groat/smtpd/extensions/help.rb +42 -0
- data/lib/groat/smtpd/extensions/mechanism-login.rb +74 -0
- data/lib/groat/smtpd/extensions/no-soliciting.rb +41 -0
- data/lib/groat/smtpd/extensions/onex.rb +37 -0
- data/lib/groat/smtpd/extensions/pipelining.rb +59 -0
- data/lib/groat/smtpd/extensions/size.rb +46 -0
- data/lib/groat/smtpd/extensions/starttls.rb +69 -0
- data/lib/groat/smtpd/extensions/verb.rb +37 -0
- data/lib/groat/smtpd/server.rb +39 -0
- data/lib/groat/smtpd/smtp.rb +277 -0
- data/lib/groat/smtpd/smtpsyntax.rb +377 -0
- data/lib/groat/smtpd/version.rb +25 -0
- metadata +112 -0
@@ -0,0 +1,277 @@
|
|
1
|
+
# vim: set sw=2 sts=2 ts=2 et syntax=ruby: #
|
2
|
+
=begin license
|
3
|
+
Copyright 2011 Novell, Inc.
|
4
|
+
|
5
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
you may not use this file except in compliance with the License.
|
7
|
+
You may obtain a copy of the License at
|
8
|
+
|
9
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
|
11
|
+
Unless required by applicable law or agreed to in writing, software
|
12
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
See the License for the specific language governing permissions and
|
15
|
+
limitations under the License.
|
16
|
+
|
17
|
+
Author(s):
|
18
|
+
Peter Bowen <pzbowen@gmail.com> Ottawa, Ontario, Canada
|
19
|
+
=end
|
20
|
+
|
21
|
+
require 'groat/smtpd/smtpsyntax'
|
22
|
+
|
23
|
+
module Groat
|
24
|
+
module SMTPD
|
25
|
+
class SMTP < SMTPSyntax
|
26
|
+
ehlo_keyword "x-groat"
|
27
|
+
verb :mail, :smtp_verb_mail
|
28
|
+
verb :rcpt, :smtp_verb_rcpt
|
29
|
+
verb :data, :smtp_verb_data
|
30
|
+
verb :helo, :smtp_verb_helo
|
31
|
+
verb :ehlo, :smtp_verb_ehlo
|
32
|
+
verb :quit, :smtp_verb_quit
|
33
|
+
verb :rset, :smtp_verb_rset
|
34
|
+
verb :noop, :smtp_verb_noop
|
35
|
+
|
36
|
+
def initialize(*args)
|
37
|
+
@hostname = "groat.example" if @hostname.nil?
|
38
|
+
super(*args)
|
39
|
+
end
|
40
|
+
|
41
|
+
def deliver!
|
42
|
+
end
|
43
|
+
|
44
|
+
# Reply methods
|
45
|
+
|
46
|
+
def response_ok(args = {})
|
47
|
+
defaults = {:code => 250, :message => "OK"}
|
48
|
+
reply defaults.merge(args)
|
49
|
+
end
|
50
|
+
|
51
|
+
def response_bad_command(args = {})
|
52
|
+
defaults = {:code => 500, :message => "Bad command"}
|
53
|
+
reply defaults.merge(args)
|
54
|
+
end
|
55
|
+
|
56
|
+
def response_syntax_error(args = {})
|
57
|
+
defaults = {:code => 501, :message => "Syntax error"}
|
58
|
+
reply defaults.merge(args)
|
59
|
+
end
|
60
|
+
|
61
|
+
def response_bad_sequence(args = {})
|
62
|
+
defaults = {:code => 503, :message => "Bad sequence of commands",
|
63
|
+
:terminate => true }
|
64
|
+
reply defaults.merge(args)
|
65
|
+
end
|
66
|
+
|
67
|
+
def response_bad_command_parameter(args = {})
|
68
|
+
defaults = {:code => 504, :message => "Invalid parameter"}
|
69
|
+
reply defaults.merge(args)
|
70
|
+
end
|
71
|
+
|
72
|
+
def response_no_valid_rcpt(args = {})
|
73
|
+
defaults = {:code => 554, :message => "No valid recipients"}
|
74
|
+
reply defaults.merge(args)
|
75
|
+
end
|
76
|
+
|
77
|
+
def response_bad_parameter(args = {})
|
78
|
+
defaults = {:code => 555, :message => "Parameter not recognized"}
|
79
|
+
reply defaults.merge(args)
|
80
|
+
end
|
81
|
+
|
82
|
+
def response_service_shutdown(args = {})
|
83
|
+
defaults = {:code => 421, :message => "Server closing connection",
|
84
|
+
:terminate => true}
|
85
|
+
reply defaults.merge(args)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Groat framework methods
|
89
|
+
|
90
|
+
def send_greeting
|
91
|
+
reply :code => 220, :message => "#{@hostname} ESMTP Ready"
|
92
|
+
end
|
93
|
+
|
94
|
+
def service_shutdown
|
95
|
+
response_service_shutdown
|
96
|
+
end
|
97
|
+
|
98
|
+
def verb_missing(verb, parameters)
|
99
|
+
response_bad_command :message => "Unknown command #{verb}"
|
100
|
+
end
|
101
|
+
|
102
|
+
# Utility functions
|
103
|
+
|
104
|
+
# No pipelining allowed (not in RFC5321)
|
105
|
+
def check_command_group
|
106
|
+
if clientdata?
|
107
|
+
response_bad_sequence
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def reset_connection
|
112
|
+
@hello = nil
|
113
|
+
@hello_extra = nil
|
114
|
+
@esmtp = false
|
115
|
+
reset_buffers
|
116
|
+
super
|
117
|
+
end
|
118
|
+
|
119
|
+
def reset_buffers
|
120
|
+
@mailfrom = nil
|
121
|
+
@rcptto = []
|
122
|
+
@message = ""
|
123
|
+
end
|
124
|
+
|
125
|
+
def in_mail_transaction?
|
126
|
+
not @mailfrom.nil?
|
127
|
+
end
|
128
|
+
|
129
|
+
def esmtp?
|
130
|
+
@esmtp
|
131
|
+
end
|
132
|
+
|
133
|
+
# Generic handler for hello action
|
134
|
+
# Keyword determines Mail Service Type
|
135
|
+
# See: http://www.iana.org/assignments/mail-parameters
|
136
|
+
def handle_hello(args, keyword)
|
137
|
+
keyword = keyword.to_s.upcase.intern
|
138
|
+
check_command_group
|
139
|
+
response_syntax_error if args.empty?
|
140
|
+
hello, hello_extra = args.split(" ", 2)
|
141
|
+
hello =~ DOMAIN_OR_LITERAL
|
142
|
+
if $~.nil?
|
143
|
+
respond_syntax_error :message=>"Syntax Error: expected hostname or IP literal"
|
144
|
+
elsif hello.start_with? '[' and not valid_address_literal(hello)
|
145
|
+
respond_syntax_error :message=>"Syntax Error: invalid IP literal"
|
146
|
+
else
|
147
|
+
@hello = hello
|
148
|
+
@hello_extra = hello_extra
|
149
|
+
end
|
150
|
+
reset_buffers
|
151
|
+
response_text = ["#{@hostname} at your service"]
|
152
|
+
if (keyword == :EHLO)
|
153
|
+
@esmtp = true
|
154
|
+
ehlo_keywords.each do |kw, params|
|
155
|
+
param_str = params.to_a.join(' ')
|
156
|
+
if param_str.empty?
|
157
|
+
response_text << "#{kw}"
|
158
|
+
else
|
159
|
+
response_text << "#{kw} #{param_str}"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
reply :code => 250, :message => response_text
|
164
|
+
end
|
165
|
+
|
166
|
+
# Verb handlers
|
167
|
+
def smtp_verb_helo(args)
|
168
|
+
handle_hello(args, :HELO)
|
169
|
+
end
|
170
|
+
|
171
|
+
def smtp_verb_ehlo(args)
|
172
|
+
handle_hello(args, :EHLO)
|
173
|
+
end
|
174
|
+
|
175
|
+
define_hook :validate_mailfrom
|
176
|
+
def smtp_verb_mail(args)
|
177
|
+
check_command_group
|
178
|
+
response_bad_sequence if @hello.nil?
|
179
|
+
# This should be start_with? 'FROM:<', but Outlook puts a space
|
180
|
+
# between the ':' and the '<'
|
181
|
+
response_syntax_error unless args.upcase.start_with? 'FROM:'
|
182
|
+
# Remove leading "FROM:" and following spaces
|
183
|
+
args = args[5..-1].lstrip
|
184
|
+
if args[0..2].rstrip.eql? '<>'
|
185
|
+
path = '<>'
|
186
|
+
param_str = args[3..-1].to_s
|
187
|
+
else
|
188
|
+
path, param_str = split_path(args)
|
189
|
+
response_syntax_error :message => 'Path error' if path.nil?
|
190
|
+
end
|
191
|
+
unless param_str.strip.empty?
|
192
|
+
response_syntax_error unless esmtp?
|
193
|
+
end
|
194
|
+
params = parse_params(param_str)
|
195
|
+
response_bad_parameter unless mail_params_valid(params)
|
196
|
+
# Validation complete
|
197
|
+
# RFC5321 § 4.1.1.2
|
198
|
+
# "This command clears the reverse-path buffer, the forward-path
|
199
|
+
# buffer, and the mail data buffer, and it inserts the reverse-path
|
200
|
+
# information from its argument clause into the reverse-path buffer."
|
201
|
+
reset_buffers
|
202
|
+
process_mail_params(params)
|
203
|
+
mailfrom = normalize_path(path)
|
204
|
+
run_hook :validate_mailfrom, mailfrom
|
205
|
+
@mailfrom = mailfrom
|
206
|
+
response_ok
|
207
|
+
end
|
208
|
+
|
209
|
+
define_hook :validate_rcptto
|
210
|
+
def smtp_verb_rcpt(args)
|
211
|
+
check_command_group
|
212
|
+
response_bad_sequence if @mailfrom.nil?
|
213
|
+
# This should be start_with? 'TO:<', but Outlook puts a space
|
214
|
+
# between the ':' and the '<'
|
215
|
+
response_syntax_error unless args.upcase.start_with? 'TO:'
|
216
|
+
# Remove leading "TO:" and the following spaces
|
217
|
+
args = args[3..-1].lstrip
|
218
|
+
path, param_str = split_path(args)
|
219
|
+
response_syntax error :message => 'Path error' if path.nil?
|
220
|
+
unless param_str.strip.empty?
|
221
|
+
response_syntax_error unless esmtp?
|
222
|
+
end
|
223
|
+
params = parse_params(param_str)
|
224
|
+
rcptto = normalize_path(path)
|
225
|
+
run_hook :validate_rcptto, rcptto
|
226
|
+
@rcptto << rcptto
|
227
|
+
response_ok
|
228
|
+
end
|
229
|
+
|
230
|
+
def smtp_verb_data(args)
|
231
|
+
check_command_group
|
232
|
+
response_syntax_error unless args.empty?
|
233
|
+
return response_no_valid_rcpt if @rcptto.count < 1
|
234
|
+
toclient "354 Enter message, ending with \".\" on a line by itself.\r\n"
|
235
|
+
loop do
|
236
|
+
line = fromclient
|
237
|
+
# RFC 5321 § 4.1.1.4
|
238
|
+
# "The custom of accepting lines ending only in <LF>, as a concession to
|
239
|
+
# non-conforming behavior on the part of some UNIX systems, has proven
|
240
|
+
# to cause more interoperability problems than it solves, and SMTP
|
241
|
+
# server systems MUST NOT do this, even in the name of improved
|
242
|
+
# robustness."
|
243
|
+
break if line.chomp("\r\n").eql?('.')
|
244
|
+
# RFC5321 sect 4.5.2, remove leading '.' if found
|
245
|
+
line.slice!(0) if line.start_with? '.'
|
246
|
+
@message << line
|
247
|
+
end
|
248
|
+
message = deliver!
|
249
|
+
reset_buffers
|
250
|
+
response_ok :message => message
|
251
|
+
end
|
252
|
+
|
253
|
+
def smtp_verb_rset(args)
|
254
|
+
check_command_group
|
255
|
+
response_syntax_error unless args.empty?
|
256
|
+
reset_buffers
|
257
|
+
response_ok
|
258
|
+
end
|
259
|
+
|
260
|
+
def smtp_verb_quit(args)
|
261
|
+
check_command_group
|
262
|
+
response_syntax_error unless args.empty?
|
263
|
+
reset_buffers
|
264
|
+
reply :code=>221,
|
265
|
+
:message=>"#{@hostname} Service closing transmission channel",
|
266
|
+
:terminate=>true
|
267
|
+
end
|
268
|
+
|
269
|
+
# RFC 5321 § 4.1.1.9
|
270
|
+
# "If a parameter string is specified, servers SHOULD ignore it."
|
271
|
+
def smtp_verb_noop(args)
|
272
|
+
check_command_group
|
273
|
+
response_ok
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
@@ -0,0 +1,377 @@
|
|
1
|
+
# vim: set sw=2 sts=2 ts=2 et syntax=ruby: #
|
2
|
+
=begin license
|
3
|
+
Copyright 2011 Novell, Inc.
|
4
|
+
|
5
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
you may not use this file except in compliance with the License.
|
7
|
+
You may obtain a copy of the License at
|
8
|
+
|
9
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
|
11
|
+
Unless required by applicable law or agreed to in writing, software
|
12
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
See the License for the specific language governing permissions and
|
15
|
+
limitations under the License.
|
16
|
+
|
17
|
+
Author(s):
|
18
|
+
Peter Bowen <pzbowen@gmail.com> Ottawa, Ontario, Canada
|
19
|
+
=end
|
20
|
+
|
21
|
+
require 'groat/smtpd/base'
|
22
|
+
require 'ipaddr'
|
23
|
+
|
24
|
+
## SMTP Syntax implements the basic functions
|
25
|
+
## that allow implementing RFC 5321 and RFC 3848
|
26
|
+
## It does not define any verbs
|
27
|
+
|
28
|
+
module Groat
|
29
|
+
module SMTPD
|
30
|
+
class SMTPExtensionError < Exception
|
31
|
+
end
|
32
|
+
|
33
|
+
class SMTPResponse < Response
|
34
|
+
def initialize(args = {})
|
35
|
+
@code = args[:code] || 500
|
36
|
+
super(args)
|
37
|
+
end
|
38
|
+
|
39
|
+
def reply_text
|
40
|
+
text = ""
|
41
|
+
if @message.is_a? Array
|
42
|
+
last = @message.pop
|
43
|
+
if @message.count > 0
|
44
|
+
@message.each do |line|
|
45
|
+
text << @code.to_s + "-#{line}\r\n"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
text << @code.to_s + " #{last}\r\n"
|
49
|
+
else
|
50
|
+
text << @code.to_s + " #{@message}\r\n"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class SMTPSyntax < Base
|
56
|
+
def initialize(*args)
|
57
|
+
super(*args)
|
58
|
+
@response_class = SMTPResponse
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
inheritable_attr(:ehlo_keywords)
|
63
|
+
self.ehlo_keywords = {}
|
64
|
+
inheritable_attr(:smtp_verbs)
|
65
|
+
self.smtp_verbs = {}
|
66
|
+
inheritable_attr(:mail_parameters)
|
67
|
+
self.mail_parameters = {}
|
68
|
+
inheritable_attr(:rcpt_parameters)
|
69
|
+
self.rcpt_parameters = {}
|
70
|
+
inheritable_attr(:mail_maxlen)
|
71
|
+
self.mail_maxlen = 512
|
72
|
+
inheritable_attr(:rcpt_maxlen)
|
73
|
+
self.rcpt_maxlen = 512
|
74
|
+
|
75
|
+
define_hook :before_all_verbs
|
76
|
+
define_hook :after_all_verbs
|
77
|
+
|
78
|
+
def self.ehlo_keyword(keyword, params = [], condition = nil)
|
79
|
+
sym = keyword.to_s.upcase.intern
|
80
|
+
ehlo_keywords[sym] = {:params => params, :condition => condition}
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.ehlo_keyword_known?(kw)
|
84
|
+
sym = kw.to_s.upcase.intern
|
85
|
+
ehlo_keywords.has_key? sym
|
86
|
+
end
|
87
|
+
|
88
|
+
def ehlo_keywords
|
89
|
+
list = {}
|
90
|
+
self.class.ehlo_keywords.each do |k, v|
|
91
|
+
valid = false
|
92
|
+
|
93
|
+
if v[:condition].nil?
|
94
|
+
valid = true
|
95
|
+
else
|
96
|
+
valid = send v[:condition]
|
97
|
+
end
|
98
|
+
|
99
|
+
if valid
|
100
|
+
if v[:params].kind_of? Symbol
|
101
|
+
params = send v[:params]
|
102
|
+
else
|
103
|
+
params = v[:params]
|
104
|
+
end
|
105
|
+
list[k] = list[k].to_a|params.to_a
|
106
|
+
end
|
107
|
+
end
|
108
|
+
list
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.run_verb_hook_for(hook, verb, scope, *args)
|
112
|
+
if not smtp_verbs[verb].nil? and not smtp_verbs[verb][hook].nil?
|
113
|
+
smtp_verbs[verb][hook].each do |callback|
|
114
|
+
if callback.kind_of? Symbol
|
115
|
+
scope.send(callback, *args)
|
116
|
+
else
|
117
|
+
callback.call(*args)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.before_verb(name, method = nil, &block)
|
124
|
+
sym = name.to_s.upcase.intern
|
125
|
+
smtp_verbs[sym] = {} unless smtp_verbs.has_key? sym
|
126
|
+
smtp_verbs[sym][:before] = [] unless smtp_verbs[sym].has_key? :before
|
127
|
+
callback = block_given? ? block : method
|
128
|
+
smtp_verbs[sym][:before] << callback
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.after_verb(name, method = nil, &block)
|
132
|
+
sym = name.to_s.upcase.intern
|
133
|
+
smtp_verbs[sym] = {} unless smtp_verbs.has_key? sym
|
134
|
+
smtp_verbs[sym][:after] = [] unless smtp_verbs[sym].has_key? :after
|
135
|
+
callback = block_given? ? block : method
|
136
|
+
smtp_verbs[sym][:after] << callback
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.validate_verb(name, method = nil, &block)
|
140
|
+
sym = name.to_s.upcase.intern
|
141
|
+
smtp_verbs[sym] = {} unless smtp_verbs.has_key? sym
|
142
|
+
smtp_verbs[sym][:valid] = [] unless smtp_verbs[sym].has_key? :valid
|
143
|
+
callback = block_given? ? block : method
|
144
|
+
smtp_verbs[sym][:valid] << callback
|
145
|
+
end
|
146
|
+
|
147
|
+
def self.verb(name, method)
|
148
|
+
sym = name.to_s.upcase.intern
|
149
|
+
smtp_verbs[sym] = {} unless smtp_verbs.has_key? sym
|
150
|
+
smtp_verbs[sym][:method] = method
|
151
|
+
end
|
152
|
+
|
153
|
+
def self.mail_param(name, method)
|
154
|
+
sym = name.to_s.upcase.intern
|
155
|
+
mail_parameters[sym] = method
|
156
|
+
end
|
157
|
+
|
158
|
+
def run_verb_hook(hook, verb, *args)
|
159
|
+
self.class.run_verb_hook_for(hook, verb, self, *args)
|
160
|
+
end
|
161
|
+
|
162
|
+
def known_verbs
|
163
|
+
self.class.smtp_verbs.map{|k, v| k if v.has_key?(:method)}.compact
|
164
|
+
end
|
165
|
+
|
166
|
+
def smtp_verb(verb)
|
167
|
+
hooks = self.class.smtp_verbs[verb]
|
168
|
+
unless hooks.nil?
|
169
|
+
hooks[:method]
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def do_verb(verb, args)
|
174
|
+
args = args.to_s.strip
|
175
|
+
run_verb_hook :validate, verb, args
|
176
|
+
if smtp_verb(verb).nil?
|
177
|
+
verb_missing verb, args
|
178
|
+
else
|
179
|
+
send smtp_verb(verb), args
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Lines which do not have a valid verb
|
184
|
+
def do_garbage(garbage)
|
185
|
+
response_syntax_error :message=>"syntax error - invalid character"
|
186
|
+
end
|
187
|
+
|
188
|
+
def verb_missing(verb, parameters)
|
189
|
+
end
|
190
|
+
|
191
|
+
def mail_params_valid(params)
|
192
|
+
params.each do |name, value|
|
193
|
+
return false unless self.class.mail_parameters.has_key? name
|
194
|
+
end
|
195
|
+
true
|
196
|
+
end
|
197
|
+
|
198
|
+
def process_mail_params(params)
|
199
|
+
params.each do |name, value|
|
200
|
+
send self.class.mail_parameters[name], value
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
# Does the client support SMTP extensions?
|
205
|
+
def esmtp?
|
206
|
+
false
|
207
|
+
end
|
208
|
+
|
209
|
+
# Did the client successfully authenticate?
|
210
|
+
def authenticated?
|
211
|
+
false
|
212
|
+
end
|
213
|
+
|
214
|
+
# Return the protocol name for use with "WITH" in the Received: header
|
215
|
+
def protocol
|
216
|
+
# This could return "SMTPS" which is non-standard is two cases:
|
217
|
+
# - Client sends EHLO -> STARTTLS -> HELO sequence
|
218
|
+
# - If using implicit TLS (i.e. non-standard port 465)
|
219
|
+
(esmtp? ? "E" : "") + "SMTP" + (secure? ? "S" : "") + (authenticated? ? "A" : "")
|
220
|
+
end
|
221
|
+
|
222
|
+
|
223
|
+
# RFC 5321 § 2.2.2: "verbs [...] are bound by the same rules as EHLO i
|
224
|
+
# keywords"; § 4.1.1.1 defines it as /\A[A-Za-z0-9]([A-Za-z0-9-]*)\Z/
|
225
|
+
# This splits the verb off and then finds the correct method to call
|
226
|
+
VERB = /\A[A-Za-z0-9]([A-Za-z0-9-]*)\Z/
|
227
|
+
def process_line(line)
|
228
|
+
k, v = line.chomp.split(' ', 2)
|
229
|
+
if k.to_s !~ VERB
|
230
|
+
run :do_garbage, line
|
231
|
+
end
|
232
|
+
k = k.to_s.upcase.tr('-', '_').intern
|
233
|
+
run_hook :before_all_verbs, k
|
234
|
+
run_verb_hook :before, k
|
235
|
+
res = run :do_verb, k, v.to_s.strip
|
236
|
+
run_verb_hook :after, k
|
237
|
+
run_hook :after_all_verbs, k
|
238
|
+
res
|
239
|
+
end
|
240
|
+
|
241
|
+
def parse_params(param_str)
|
242
|
+
params = {}
|
243
|
+
param_str.split(' ').each do |p|
|
244
|
+
k, v = p.split('=', 2)
|
245
|
+
k = k.intern
|
246
|
+
params[k] = v
|
247
|
+
end
|
248
|
+
params
|
249
|
+
end
|
250
|
+
|
251
|
+
## Path handling functions
|
252
|
+
# From RFC5321 section 4.1.2
|
253
|
+
R_Let_dig = '[0-9a-z]'
|
254
|
+
R_Ldh_str = "[0-9a-z-]*#{R_Let_dig}"
|
255
|
+
R_sub_domain = "#{R_Let_dig}(#{R_Ldh_str})?"
|
256
|
+
R_Domain = "#{R_sub_domain}(\\.#{R_sub_domain})*"
|
257
|
+
# The RHS domain syntax is explicitly from RFC2821; see
|
258
|
+
# http://www.imc.org/ietf-smtp/mail-archive/msg05431.html
|
259
|
+
R_RHS_Domain = "#{R_sub_domain}(\\.#{R_sub_domain})+"
|
260
|
+
R_At_domain = "@#{R_Domain}"
|
261
|
+
R_A_d_l = "#{R_At_domain}(,#{R_At_domain})*"
|
262
|
+
|
263
|
+
R_atext = "[a-z0-9!\#$%&'*+\\/=?^_`{|}~-]"
|
264
|
+
R_Atom = "#{R_atext}+"
|
265
|
+
R_Dot_string = "#{R_Atom}(\\.#{R_Atom})*"
|
266
|
+
R_qtextSMTP = "[\\040-\\041\\043-\\133\\135-\\176]"
|
267
|
+
R_quoted_pairSMTP = "\\134[\\040-\\176]"
|
268
|
+
R_Quoted_string = "\"(#{R_qtextSMTP}|#{R_quoted_pairSMTP})*\""
|
269
|
+
|
270
|
+
R_Local_part = "(#{R_Dot_string}|#{R_Quoted_string})"
|
271
|
+
|
272
|
+
# This should really be 0-255 with no leading zeros
|
273
|
+
R_Snum = "(0|[1-9][0-9]{0,2})"
|
274
|
+
R_IPv4_address_literal = "#{R_Snum}(\.#{R_Snum}){3}"
|
275
|
+
R_IPv6_hex = "[0-9a-f]{1,4}"
|
276
|
+
R_IPv6_full = "#{R_IPv6_hex}(:#{R_IPv6_hex}){7}"
|
277
|
+
R_IPv6_comp = "(#{R_IPv6_hex}(:#{R_IPv6_hex}){0,5})?::(#{R_IPv6_hex}(:#{R_IPv6_hex}){0,5})?"
|
278
|
+
R_IPv6v4_full = "#{R_IPv6_hex}(:#{R_IPv6_hex}){3}:#{R_IPv4_address_literal}"
|
279
|
+
R_IPv6v4_comp = "(#{R_IPv6_hex}(:#{R_IPv6_hex}){0,3})?::(#{R_IPv6_hex}(:#{R_IPv6_hex}){0,3})?:#{R_IPv4_address_literal}"
|
280
|
+
R_IPv6_address_literal = "IPv6:(#{R_IPv6_full}|#{R_IPv6_comp}|#{R_IPv6v4_full}|#{R_IPv6v4_comp})"
|
281
|
+
# RFC 5321 § 4.1.3 "Standardized-tag MUST be specified in a i
|
282
|
+
# Standards-Track RFC and registered with IANA
|
283
|
+
# At this point, only "IPv6" has been register, which
|
284
|
+
# already handled. Therefore we are using a slightly simpler regex
|
285
|
+
#R_dcontent = "[\\041-\\132\\136-\\176]"
|
286
|
+
#R_General_address_literal = "#{R_Ldh_str}:(#{R_dcontent}+)"
|
287
|
+
#R_address_literal = "\\[(#{R_IPv4_address_literal}|#{R_IPv6_address_literal}|#{R_General_address_literal})\\]"
|
288
|
+
R_address_literal = "\\[(#{R_IPv4_address_literal}|#{R_IPv6_address_literal})\\]"
|
289
|
+
|
290
|
+
R_Mailbox = "#{R_Local_part}@(#{R_RHS_Domain}|#{R_address_literal})"
|
291
|
+
|
292
|
+
# For example, the EHLO/HELO parameter
|
293
|
+
DOMAIN_OR_LITERAL = /\A(#{R_Domain}|#{R_address_literal})\Z/i
|
294
|
+
|
295
|
+
R_Path = "<(#{R_A_d_l}:)?#{R_Mailbox}>"
|
296
|
+
|
297
|
+
# For example, an unquoted local part of a mailbox
|
298
|
+
DOT_STRING = /\A#{R_Dot_string}\Z/i
|
299
|
+
|
300
|
+
# MatchData[1] is the local part and [4] is the domain or address literal
|
301
|
+
MAILBOX = /\A#{R_Mailbox}\Z/i
|
302
|
+
|
303
|
+
# If a string begins with a path (allows for characters after the path)
|
304
|
+
# MatchData[1] is the Source Route, [9] is the local part, and
|
305
|
+
# [12] is the domain or address literal
|
306
|
+
PATH_PART = /\A#{R_Path}/i
|
307
|
+
|
308
|
+
# Only has path (vs. starts with path)
|
309
|
+
# Same MatchData as PATH_PART
|
310
|
+
PATH= /\A#{R_Path}\Z/i
|
311
|
+
|
312
|
+
EXCESSIVE_QUOTE = /\134([^\041\134])/
|
313
|
+
|
314
|
+
def valid_address_literal(literal)
|
315
|
+
return false unless literal.start_with? '['
|
316
|
+
return false unless literal.end_with? ']'
|
317
|
+
begin
|
318
|
+
IPAddr.new(literal[1..-2])
|
319
|
+
rescue ::ArgumentError
|
320
|
+
return false
|
321
|
+
end
|
322
|
+
true
|
323
|
+
end
|
324
|
+
|
325
|
+
def split_path(args)
|
326
|
+
m = args =~ PATH_PART
|
327
|
+
if m.nil?
|
328
|
+
[nil, args]
|
329
|
+
else
|
330
|
+
response = [$~.to_s, $'.strip]
|
331
|
+
if $~[12].start_with? '['
|
332
|
+
return [nil, args] unless valid_address_literal $~[12]
|
333
|
+
end
|
334
|
+
response
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
def normalize_local_part(local)
|
339
|
+
if local.start_with? '"'
|
340
|
+
local.gsub!(EXCESSIVE_QUOTE, '\1')
|
341
|
+
local = local[1..-2] if local[1..-2] =~ DOT_STRING
|
342
|
+
end
|
343
|
+
local
|
344
|
+
end
|
345
|
+
|
346
|
+
# Remove the leading '<', trailing '>', switch domains lower case and
|
347
|
+
# remove unnecessary quoting in the localpart
|
348
|
+
def normalize_path(path)
|
349
|
+
return '' if path.eql? '<>'
|
350
|
+
path =~ PATH
|
351
|
+
$~[1].to_s.downcase + normalize_local_part($~[9]) + "@" + $~[12].downcase
|
352
|
+
end
|
353
|
+
|
354
|
+
def normalize_mailbox(addr)
|
355
|
+
addr =~ MAILBOX
|
356
|
+
normalize_local_part($~[1]) + "@" + $~[4].downcase
|
357
|
+
end
|
358
|
+
|
359
|
+
# Defined in RFC 3461 § 4, referenced in RFC 5321 § 4.1.2
|
360
|
+
R_xchar_list = "\\041-\\052\\054\\074\\076-\\176"
|
361
|
+
R_xtext_hexchar = "\\053[0-9A-F]{2}"
|
362
|
+
XTEXT = /\A([#{R_xchar_list}]|#{R_xtext_hexchar})*\Z/
|
363
|
+
XTEXT_HEXSEQ = /#{R_xtext_hexchar}/
|
364
|
+
XTEXT_NOT_XCHAR = /[^#{R_xchar_list}]/
|
365
|
+
|
366
|
+
def from_xtext(str)
|
367
|
+
if str =~ XTEXT
|
368
|
+
str.gsub!(XTEXT_HEXSEQ) {|s| s[1..2].hex.chr }
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
def to_xtext(str)
|
373
|
+
str.gsub!(XTEXT_NOT_XCHAR) {|s| '+' + s[0].to_s(16).upcase }
|
374
|
+
end
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|