Class: MailDiode::Engine

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

Overview

State machine that handles incoming SMTP commands

Constant Summary collapse

MAX_ALLOWED_REJECTS =
1

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(hostname, max_recipients) ⇒ Engine

Returns a new instance of Engine.



90
91
92
93
94
95
96
97
98
# File 'lib/engine.rb', line 90

def initialize(hostname, max_recipients)
  @hostname = hostname
  @greeting = "220 #{hostname} ESMTP"
  @helo_response = "250 #{hostname}"
  @filters = []
  @max_recipients = max_recipients
  @rejected_count = 0
  @max_data_lines = 1000000
end

Instance Attribute Details

#envelopeObject (readonly)

Returns the value of attribute envelope.



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

def envelope
  @envelope
end

#max_data_linesObject

Returns the value of attribute max_data_lines.



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

def max_data_lines
  @max_data_lines
end

Instance Method Details

#add_filter(filter) ⇒ Object



104
105
106
# File 'lib/engine.rb', line 104

def add_filter(filter)
  @filters << filter
end

#apply_filters(sender, recipient) ⇒ Object



235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/engine.rb', line 235

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
  filterable_data.original_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



222
223
224
225
226
227
228
229
# File 'lib/engine.rb', line 222

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



164
165
166
167
168
169
170
171
172
173
# File 'lib/engine.rb', line 164

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



186
187
188
189
190
191
192
193
194
195
# File 'lib/engine.rb', line 186

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



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/engine.rb', line 250

def do_message_text(line)
  if line == '.'
    return terminate_message
  end
  
  if line[0..0] == '.'
    line = line[1..-1]
  end
  
  if @message_text.size >= max_data_lines
    raise_smtp_error(SMTPError::MESSAGE_TOO_LONG)
  end
  @message_text << line
  return nil
end

#do_noop(args) ⇒ Object



153
154
155
156
# File 'lib/engine.rb', line 153

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

#do_quit(args) ⇒ Object



158
159
160
161
162
# File 'lib/engine.rb', line 158

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

#do_rcpt(args) ⇒ Object



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/engine.rb', line 197

def do_rcpt(args)
  if(! @envelope)
    raise_smtp_error(SMTPError::NEED_MAIL_BEFORE_RCPT)
  end
  if(@envelope.recipients.size >= @max_recipients)
    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)
    if @rejected_count >= MAX_ALLOWED_REJECTS
      raise_smtp_error(SMTPError::TOO_MANY_REJECTS)
    end
    @rejected_count += 1
    raise_smtp_error(SMTPError::UNKNOWN_RECIPIENT + ": " + recipient)
  end
  @envelope.add_recipient(recipient)
  return RESULT_OK
end

#do_rset(args) ⇒ Object



180
181
182
183
184
# File 'lib/engine.rb', line 180

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

#do_vrfy(args) ⇒ Object



175
176
177
178
# File 'lib/engine.rb', line 175

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

#enforce_address_arg(args, keyword, error_text) ⇒ Object



305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
# File 'lib/engine.rb', line 305

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



298
299
300
301
302
303
# File 'lib/engine.rb', line 298

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



292
293
294
295
296
# File 'lib/engine.rb', line 292

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

#extract_args(line) ⇒ Object



321
322
323
324
325
326
327
328
329
330
# File 'lib/engine.rb', line 321

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



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/engine.rb', line 137

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



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/engine.rb', line 113

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)
		if(error.text.index(SMTPError::TOO_MANY_REJECTS) == 0)
		  @should_terminate = true
		end
    return error.text
  end

end

#process_message(sender, recipient, text) ⇒ Object



280
281
282
283
284
285
286
287
288
289
290
# File 'lib/engine.rb', line 280

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(recipient, filterable_data.recipient, full_text)
  return id
end

#raise_smtp_error(text) ⇒ Object

Raises:



332
333
334
# File 'lib/engine.rb', line 332

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

#set_mail_handler(handler) ⇒ Object



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

def set_mail_handler(handler)
  @mail_handler = handler
end

#start(sender_ip) ⇒ Object



108
109
110
111
# File 'lib/engine.rb', line 108

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

#terminate?Boolean

Returns:

  • (Boolean)


133
134
135
# File 'lib/engine.rb', line 133

def terminate?
  return @should_terminate
end

#terminate_messageObject



266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/engine.rb', line 266

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