Class: Groat::SMTPD::SMTPSyntax

Inherits:
Base
  • Object
show all
Defined in:
lib/groat/smtpd/smtpsyntax.rb

Direct Known Subclasses

SMTP

Constant Summary collapse

VERB =

RFC 5321 § 2.2.2: “verbs […] are bound by the same rules as EHLO i keywords”; § 4.1.1.1 defines it as /A([A-Za-z0-9-]*)Z/ This splits the verb off and then finds the correct method to call

/\A[A-Za-z0-9]([A-Za-z0-9-]*)\Z/
R_Let_dig =

Path handling functions From RFC5321 section 4.1.2

'[0-9a-z]'
R_Ldh_str =
"[0-9a-z-]*#{R_Let_dig}"
R_sub_domain =
"#{R_Let_dig}(#{R_Ldh_str})?"
R_Domain =
"#{R_sub_domain}(\\.#{R_sub_domain})*"
R_RHS_Domain =

The RHS domain syntax is explicitly from RFC2821; see www.imc.org/ietf-smtp/mail-archive/msg05431.html

"#{R_sub_domain}(\\.#{R_sub_domain})+"
R_At_domain =
"@#{R_Domain}"
R_A_d_l =
"#{R_At_domain}(,#{R_At_domain})*"
R_atext =
"[a-z0-9!\#$%&'*+\\/=?^_`{|}~-]"
R_Atom =
"#{R_atext}+"
R_Dot_string =
"#{R_Atom}(\\.#{R_Atom})*"
R_qtextSMTP =
"[\\040-\\041\\043-\\133\\135-\\176]"
R_quoted_pairSMTP =
"\\134[\\040-\\176]"
R_Quoted_string =
"\"(#{R_qtextSMTP}|#{R_quoted_pairSMTP})*\""
R_Local_part =
"(#{R_Dot_string}|#{R_Quoted_string})"
R_Snum =

This should really be 0-255 with no leading zeros

"(0|[1-9][0-9]{0,2})"
R_IPv4_address_literal =
"#{R_Snum}(\.#{R_Snum}){3}"
R_IPv6_hex =
"[0-9a-f]{1,4}"
R_IPv6_full =
"#{R_IPv6_hex}(:#{R_IPv6_hex}){7}"
R_IPv6_comp =
"(#{R_IPv6_hex}(:#{R_IPv6_hex}){0,5})?::(#{R_IPv6_hex}(:#{R_IPv6_hex}){0,5})?"
R_IPv6v4_full =
"#{R_IPv6_hex}(:#{R_IPv6_hex}){3}:#{R_IPv4_address_literal}"
R_IPv6v4_comp =
"(#{R_IPv6_hex}(:#{R_IPv6_hex}){0,3})?::(#{R_IPv6_hex}(:#{R_IPv6_hex}){0,3})?:#{R_IPv4_address_literal}"
R_IPv6_address_literal =
"IPv6:(#{R_IPv6_full}|#{R_IPv6_comp}|#{R_IPv6v4_full}|#{R_IPv6v4_comp})"
R_address_literal =

RFC 5321 § 4.1.3 “Standardized-tag MUST be specified in a i Standards-Track RFC and registered with IANA At this point, only ”IPv6“ has been register, which already handled. Therefore we are using a slightly simpler regex R_dcontent = ”[\041-\132\136-\176]“ R_General_address_literal = ”#R_Ldh_str:(#R_dcontent+)“ R_address_literal = ”\[(#R_IPv4_address_literal|#R_IPv6_address_literal|#R_General_address_literal)\]“

"\\[(#{R_IPv4_address_literal}|#{R_IPv6_address_literal})\\]"
R_Mailbox =
"#{R_Local_part}@(#{R_RHS_Domain}|#{R_address_literal})"
DOMAIN_OR_LITERAL =

For example, the EHLO/HELO parameter

/\A(#{R_Domain}|#{R_address_literal})\Z/i
R_Path =
"<(#{R_A_d_l}:)?#{R_Mailbox}>"
DOT_STRING =

For example, an unquoted local part of a mailbox

/\A#{R_Dot_string}\Z/i
MAILBOX =

MatchData is the local part and [4] is the domain or address literal

/\A#{R_Mailbox}\Z/i
PATH_PART =

If a string begins with a path (allows for characters after the path) MatchData is the Source Route, [9] is the local part, and

12

is the domain or address literal

/\A#{R_Path}/i
PATH =

Only has path (vs. starts with path) Same MatchData as PATH_PART

/\A#{R_Path}\Z/i
EXCESSIVE_QUOTE =
/\134([^\041\134])/
R_xchar_list =

Defined in RFC 3461 § 4, referenced in RFC 5321 § 4.1.2

"\\041-\\052\\054\\074\\076-\\176"
R_xtext_hexchar =
"\\053[0-9A-F]{2}"
XTEXT =
/\A([#{R_xchar_list}]|#{R_xtext_hexchar})*\Z/
XTEXT_HEXSEQ =
/#{R_xtext_hexchar}/
XTEXT_NOT_XCHAR =
/[^#{R_xchar_list}]/

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Base

#clientdata?, #fromclient, #getdata, #getline, #log_line, #reply, #reset_connection, #run, #secure?, #send_greeting, #serve, #service_shutdown, #set_socket, #sockop_timeout, #toclient

Constructor Details

#initialize(*args) ⇒ SMTPSyntax



56
57
58
59
# File 'lib/groat/smtpd/smtpsyntax.rb', line 56

def initialize(*args)
  super(*args)
  @response_class = SMTPResponse
end

Class Method Details

.after_verb(name, method = nil, &block) ⇒ Object



131
132
133
134
135
136
137
# File 'lib/groat/smtpd/smtpsyntax.rb', line 131

def self.after_verb(name, method = nil, &block)
  sym = name.to_s.upcase.intern
  smtp_verbs[sym] = {} unless smtp_verbs.has_key? sym
  smtp_verbs[sym][:after] = [] unless smtp_verbs[sym].has_key? :after
  callback = block_given? ? block : method
  smtp_verbs[sym][:after] << callback
end

.before_verb(name, method = nil, &block) ⇒ Object



123
124
125
126
127
128
129
# File 'lib/groat/smtpd/smtpsyntax.rb', line 123

def self.before_verb(name, method = nil, &block)
  sym = name.to_s.upcase.intern
  smtp_verbs[sym] = {} unless smtp_verbs.has_key? sym
  smtp_verbs[sym][:before] = [] unless smtp_verbs[sym].has_key? :before
  callback = block_given? ? block : method
  smtp_verbs[sym][:before] << callback
end

.ehlo_keyword(keyword, params = [], condition = nil) ⇒ Object



78
79
80
81
# File 'lib/groat/smtpd/smtpsyntax.rb', line 78

def self.ehlo_keyword(keyword, params = [], condition = nil)
  sym = keyword.to_s.upcase.intern
  ehlo_keywords[sym] = {:params => params, :condition => condition}
end

.ehlo_keyword_known?(kw) ⇒ Boolean



83
84
85
86
# File 'lib/groat/smtpd/smtpsyntax.rb', line 83

def self.ehlo_keyword_known?(kw)
  sym = kw.to_s.upcase.intern
  ehlo_keywords.has_key? sym
end

.mail_param(name, method) ⇒ Object



153
154
155
156
# File 'lib/groat/smtpd/smtpsyntax.rb', line 153

def self.mail_param(name, method)
  sym = name.to_s.upcase.intern
  mail_parameters[sym] = method
end

.run_verb_hook_for(hook, verb, scope, *args) ⇒ Object



111
112
113
114
115
116
117
118
119
120
121
# File 'lib/groat/smtpd/smtpsyntax.rb', line 111

def self.run_verb_hook_for(hook, verb, scope, *args)
  if not smtp_verbs[verb].nil? and not smtp_verbs[verb][hook].nil?
    smtp_verbs[verb][hook].each do |callback|
      if callback.kind_of? Symbol
        scope.send(callback, *args)
      else
        callback.call(*args)
      end
    end
  end
end

.validate_verb(name, method = nil, &block) ⇒ Object



139
140
141
142
143
144
145
# File 'lib/groat/smtpd/smtpsyntax.rb', line 139

def self.validate_verb(name, method = nil, &block)
  sym = name.to_s.upcase.intern
  smtp_verbs[sym] = {} unless smtp_verbs.has_key? sym
  smtp_verbs[sym][:valid] = [] unless smtp_verbs[sym].has_key? :valid
  callback = block_given? ? block : method
  smtp_verbs[sym][:valid] << callback
end

.verb(name, method) ⇒ Object



147
148
149
150
151
# File 'lib/groat/smtpd/smtpsyntax.rb', line 147

def self.verb(name, method)
  sym = name.to_s.upcase.intern
  smtp_verbs[sym] = {} unless smtp_verbs.has_key? sym
  smtp_verbs[sym][:method] = method
end

Instance Method Details

#authenticated?Boolean

Did the client successfully authenticate?



210
211
212
# File 'lib/groat/smtpd/smtpsyntax.rb', line 210

def authenticated?
  false
end

#do_garbage(garbage) ⇒ Object

Lines which do not have a valid verb



184
185
186
# File 'lib/groat/smtpd/smtpsyntax.rb', line 184

def do_garbage(garbage)
  response_syntax_error :message=>"syntax error - invalid character"
end

#do_verb(verb, args) ⇒ Object



173
174
175
176
177
178
179
180
181
# File 'lib/groat/smtpd/smtpsyntax.rb', line 173

def do_verb(verb, args)
  args = args.to_s.strip
  run_verb_hook :validate, verb, args
  if smtp_verb(verb).nil?
    verb_missing verb, args
  else
    send smtp_verb(verb), args
  end
end

#ehlo_keywordsObject



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/groat/smtpd/smtpsyntax.rb', line 88

def ehlo_keywords
  list = {}
  self.class.ehlo_keywords.each do |k, v|
    valid = false

    if v[:condition].nil?
      valid = true
    else
      valid = send v[:condition]
    end

    if valid
      if v[:params].kind_of? Symbol
        params = send v[:params]
      else
        params = v[:params]
      end
      list[k] = list[k].to_a|params.to_a
    end
  end
  list
end

#esmtp?Boolean

Does the client support SMTP extensions?



205
206
207
# File 'lib/groat/smtpd/smtpsyntax.rb', line 205

def esmtp?
  false
end

#from_xtext(str) ⇒ Object



366
367
368
369
370
# File 'lib/groat/smtpd/smtpsyntax.rb', line 366

def from_xtext(str)
  if str =~ XTEXT
    str.gsub!(XTEXT_HEXSEQ) {|s| s[1..2].hex.chr }
  end
end

#known_verbsObject



162
163
164
# File 'lib/groat/smtpd/smtpsyntax.rb', line 162

def known_verbs
  self.class.smtp_verbs.map{|k, v| k if v.has_key?(:method)}.compact
end

#mail_params_valid(params) ⇒ Object



191
192
193
194
195
196
# File 'lib/groat/smtpd/smtpsyntax.rb', line 191

def mail_params_valid(params)
  params.each do |name, value|
    return false unless self.class.mail_parameters.has_key? name
  end
  true
end

#normalize_local_part(local) ⇒ Object



338
339
340
341
342
343
344
# File 'lib/groat/smtpd/smtpsyntax.rb', line 338

def normalize_local_part(local)
  if local.start_with? '"'
    local.gsub!(EXCESSIVE_QUOTE, '\1')
    local = local[1..-2] if local[1..-2] =~ DOT_STRING
  end
  local
end

#normalize_mailbox(addr) ⇒ Object



354
355
356
357
# File 'lib/groat/smtpd/smtpsyntax.rb', line 354

def normalize_mailbox(addr)
  addr =~ MAILBOX
  normalize_local_part($~[1]) + "@" + $~[4].downcase
end

#normalize_path(path) ⇒ Object

Remove the leading ‘<’, trailing ‘>’, switch domains lower case and remove unnecessary quoting in the localpart



348
349
350
351
352
# File 'lib/groat/smtpd/smtpsyntax.rb', line 348

def normalize_path(path)
  return '' if path.eql? '<>'
  path =~ PATH
  $~[1].to_s.downcase + normalize_local_part($~[9]) + "@" + $~[12].downcase
end

#parse_params(param_str) ⇒ Object



241
242
243
244
245
246
247
248
249
# File 'lib/groat/smtpd/smtpsyntax.rb', line 241

def parse_params(param_str)
  params = {}
  param_str.split(' ').each do |p|
    k, v = p.split('=', 2)
    k = k.intern
    params[k] = v
  end
  params
end

#process_line(line) ⇒ Object



227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/groat/smtpd/smtpsyntax.rb', line 227

def process_line(line)
  k, v = line.chomp.split(' ', 2)
  if k.to_s !~ VERB
      run :do_garbage, line
  end
  k = k.to_s.upcase.tr('-', '_').intern
  run_hook :before_all_verbs, k
  run_verb_hook :before, k
  res = run :do_verb, k, v.to_s.strip
  run_verb_hook :after, k 
  run_hook :after_all_verbs, k
  res
end

#process_mail_params(params) ⇒ Object



198
199
200
201
202
# File 'lib/groat/smtpd/smtpsyntax.rb', line 198

def process_mail_params(params)
  params.each do |name, value|
    send self.class.mail_parameters[name], value
  end
end

#protocolObject

Return the protocol name for use with “WITH” in the Received: header



215
216
217
218
219
220
# File 'lib/groat/smtpd/smtpsyntax.rb', line 215

def protocol
  # This could return "SMTPS" which is non-standard is two cases:
  #   - Client sends EHLO -> STARTTLS -> HELO sequence
  #   - If using implicit TLS (i.e. non-standard port 465)
  (esmtp? ? "E" : "") + "SMTP" + (secure? ? "S" : "") + (authenticated? ? "A" : "")
end

#run_verb_hook(hook, verb, *args) ⇒ Object



158
159
160
# File 'lib/groat/smtpd/smtpsyntax.rb', line 158

def run_verb_hook(hook, verb, *args)
  self.class.run_verb_hook_for(hook, verb, self, *args)
end

#smtp_verb(verb) ⇒ Object



166
167
168
169
170
171
# File 'lib/groat/smtpd/smtpsyntax.rb', line 166

def smtp_verb(verb)
  hooks = self.class.smtp_verbs[verb]
  unless hooks.nil?
    hooks[:method]
  end
end

#split_path(args) ⇒ Object



325
326
327
328
329
330
331
332
333
334
335
336
# File 'lib/groat/smtpd/smtpsyntax.rb', line 325

def split_path(args)
  m = args =~ PATH_PART
  if m.nil?
    [nil, args]
  else
    response = [$~.to_s, $'.strip]
    if $~[12].start_with? '['
      return [nil, args] unless valid_address_literal $~[12]
    end
    response
  end
end

#to_xtext(str) ⇒ Object



372
373
374
# File 'lib/groat/smtpd/smtpsyntax.rb', line 372

def to_xtext(str)
  str.gsub!(XTEXT_NOT_XCHAR) {|s| '+' + s[0].to_s(16).upcase } 
end

#valid_address_literal(literal) ⇒ Object



314
315
316
317
318
319
320
321
322
323
# File 'lib/groat/smtpd/smtpsyntax.rb', line 314

def valid_address_literal(literal)
  return false unless literal.start_with? '['
  return false unless literal.end_with? ']'
  begin
    IPAddr.new(literal[1..-2])
  rescue ::ArgumentError
    return false
  end
  true
end

#verb_missing(verb, parameters) ⇒ Object



188
189
# File 'lib/groat/smtpd/smtpsyntax.rb', line 188

def verb_missing(verb, parameters)
end