Class: MailDiode::Engine

Inherits:
Object
  • Object
show all
Defined in:
lib/engine.rb

Overview

State machine that handles incoming SMTP commands

Instance Method Summary collapse

Constructor Details

#initialize(hostname) ⇒ Engine

Returns a new instance of Engine.



84
85
86
87
88
89
# File 'lib/engine.rb', line 84

def initialize(hostname)
  @hostname = hostname
  @greeting = "220 #{hostname} ESMTP"
  @helo_response = "250 #{hostname}"
  @filters = []
end

Instance Method Details

#add_filter(filter) ⇒ Object



95
96
97
# File 'lib/engine.rb', line 95

def add_filter(filter)
  @filters << filter
end

#apply_filters(sender, recipient) ⇒ Object



219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/engine.rb', line 219

def apply_filters(sender, recipient)
  filterable_data = FilterableData.new
  filterable_data.sender_ip = @sender_ip
  filterable_data.helo = @helo
  filterable_data.sender = sender
  filterable_data.recipient = recipient
  @filters.each do | filter |
    if filter.respond_to? :process
      filter.process(filterable_data)
    end
  end
  return filterable_data
end

#do_data(args) ⇒ Object



206
207
208
209
210
211
212
213
# File 'lib/engine.rb', line 206

def do_data(args)
  if(! @envelope || ! @envelope.sender)
    raise_smtp_error(SMTPError::NEED_RCPT_BEFORE_DATA)
  end
  enforce_no_args(args, SMTPError::SYNTAX_DATA)
  @message_text = []
  return RESULT_DATA_OK
end

#do_helo(args) ⇒ Object



152
153
154
155
156
157
158
159
160
161
# File 'lib/engine.rb', line 152

def do_helo(args)
  enforce_host_arg(args, SMTPError::SYNTAX_HELO)
  @helo = args
  @filters.each do | filter |
    if filter.respond_to? :helo
      filter.helo @helo
    end
  end
  return @helo_response
end

#do_mail(args) ⇒ Object



174
175
176
177
178
179
180
181
182
183
# File 'lib/engine.rb', line 174

def do_mail(args)
  from_address = enforce_address_arg(args, 'from', SMTPError::SYNTAX_MAIL)
  @envelope = Envelope.new(from_address)
  @filters.each do | filter |
    if filter.respond_to? :mail
      filter.mail from_address
    end
  end
 return RESULT_OK
end

#do_message_text(line) ⇒ Object



233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/engine.rb', line 233

def do_message_text(line)
  if line == '.'
    return terminate_message
  end
  
  if line[0..0] == '.'
    line = line[1..-1]
  end
  
  @message_text << line
  return nil
end

#do_noop(args) ⇒ Object



141
142
143
144
# File 'lib/engine.rb', line 141

def do_noop(args)
  enforce_no_args(args, SMTPError::SYNTAX_NOOP)
  return RESULT_OK
end

#do_quit(args) ⇒ Object



146
147
148
149
150
# File 'lib/engine.rb', line 146

def do_quit(args)
  enforce_no_args(args, SMTPError::SYNTAX_QUIT)
  @should_terminate = true
  return RESULT_BYE
end

#do_rcpt(args) ⇒ Object



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/engine.rb', line 185

def do_rcpt(args)
  if(! @envelope)
    raise_smtp_error(SMTPError::NEED_MAIL_BEFORE_RCPT)
  end
  if(@envelope.recipients.size >= 100)
    raise_smtp_error(SMTPError::TOO_MANY_RECIPIENTS)
  end
  recipient = enforce_address_arg(args, 'to', SMTPError::SYNTAX_RCPT)
  @filters.each do | filter |
    if filter.respond_to? :rcpt
      filter.rcpt recipient
    end
  end
  filterable_data = apply_filters(@envelope.sender, recipient)
  if !@mail_handler.valid_recipient?(filterable_data.recipient)
    raise_smtp_error(SMTPError::UNKNOWN_RECIPIENT + filterable_data.recipient)
  end
  @envelope.add_recipient(recipient)
  return RESULT_OK
end

#do_rset(args) ⇒ Object



168
169
170
171
172
# File 'lib/engine.rb', line 168

def do_rset(args)
  enforce_no_args(args, SMTPError::SYNTAX_RSET)
  @envelope = nil
  return RESULT_OK
end

#do_vrfy(args) ⇒ Object



163
164
165
166
# File 'lib/engine.rb', line 163

def do_vrfy(args)
  enforce_host_arg(args, SMTPError::SYNTAX_VRFY)
  return RESULT_UNSURE
end

#enforce_address_arg(args, keyword, error_text) ⇒ Object



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/engine.rb', line 285

def enforce_address_arg(args, keyword, error_text)
  re = /#{keyword}:\s*(.+)/i
  match = re.match(args)
  if !match
    raise_smtp_error(error_text)
  end
  candidate = match[1].strip
  strip_brackets = /<(.*)>/
  match = strip_brackets.match(candidate)
  if(match)
    candidate = match[1]
  end
  
  return candidate
end

#enforce_host_arg(args, error_text) ⇒ Object



278
279
280
281
282
283
# File 'lib/engine.rb', line 278

def enforce_host_arg(args, error_text)
  args = args.split(/\s+/)
  if args.size != 1
    raise_smtp_error(error_text)
  end
end

#enforce_no_args(args, error_text) ⇒ Object



272
273
274
275
276
# File 'lib/engine.rb', line 272

def enforce_no_args(args, error_text)
  if ! args.empty?
    raise_smtp_error(error_text)
  end
end

#extract_args(line) ⇒ Object



301
302
303
304
305
306
307
308
309
310
# File 'lib/engine.rb', line 301

def extract_args(line)
  command, args = line.split(nil, 2)
  if command.nil?
    command = ''
  end
  if args.nil?
    args = ''
  end
  return command, args
end

#process_command(command, args) ⇒ Object



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/engine.rb', line 125

def process_command(command, args)
  case command.upcase
    when NOOP then return do_noop(args)
    when QUIT then return do_quit(args)
    when HELO then return do_helo(args)
    when EHLO then return do_helo(args)
    when VRFY then return do_vrfy(args)
    when RSET then return do_rset(args)
    when MAIL then return do_mail(args)
    when RCPT then return do_rcpt(args)
    when DATA then return do_data(args)
  end
  
  raise_smtp_error(SMTPError::BAD_COMMAND + ': ' + command)
end

#process_line(line) ⇒ Object



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/engine.rb', line 104

def process_line(line)
  begin
    if @message_text
      return do_message_text(line)
    end
    
    command, args = extract_args(line)
        result = process_command(command, args)
        MailDiode::log_success(command, args, result)
        return result
  rescue SMTPError => error
		MailDiode.log_info(error.text)
    return error.text
  end

end

#process_message(sender, recipient, text) ⇒ Object



260
261
262
263
264
265
266
267
268
269
270
# File 'lib/engine.rb', line 260

def process_message(sender, recipient, text)
  received = "Received: from #{@helo} (#{@sender_ip}) " + 
        "by #{@hostname} " +
        "for <#{recipient}> " + 
        "at #{Time.now.to_s}"
  full_text = received + NEWLINE + text.join(NEWLINE)

  filterable_data = apply_filters(sender, recipient)
  id = @mail_handler.process_message(filterable_data.recipient, full_text)
  return id
end

#raise_smtp_error(text) ⇒ Object

Raises:



312
313
314
# File 'lib/engine.rb', line 312

def raise_smtp_error(text)
 raise SMTPError.new(text)
end

#set_mail_handler(handler) ⇒ Object



91
92
93
# File 'lib/engine.rb', line 91

def set_mail_handler(handler)
  @mail_handler = handler
end

#start(sender_ip) ⇒ Object



99
100
101
102
# File 'lib/engine.rb', line 99

def start(sender_ip)
  @sender_ip = sender_ip
  return @greeting
end

#terminate?Boolean

Returns:

  • (Boolean)


121
122
123
# File 'lib/engine.rb', line 121

def terminate?
  return @should_terminate
end

#terminate_messageObject



246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/engine.rb', line 246

def terminate_message  
  sender = @envelope.sender
  recipients = @envelope.recipients
  text = @message_text
  @envelope = nil
  @message_text = nil

  ids = []
  recipients.each do | recipient |
    ids << process_message(sender, recipient, text)
  end
  return "#{RESULT_OK} #{ids.join(';')}"
end