class EventMachine::Protocols::SmtpServer

This is a protocol handler for the server side of SMTP. It's NOT a complete SMTP server obeying all the semantics of servers conforming to RFC2821. Rather, it uses overridable method stubs to communicate protocol states and data to user code. User code is responsible for doing the right things with the data in order to get complete and correct SMTP server behavior.

Simple SMTP server example:

class EmailServer < EM::P::SmtpServer
  def receive_plain_auth(user, pass)
    true
  end

  def get_server_domain
    "mock.smtp.server.local"
  end

  def get_server_greeting
    "mock smtp server greets you with impunity"
  end

  def receive_sender(sender)
    current.sender = sender
    true
  end

  def receive_recipient(recipient)
    current.recipient = recipient
    true
  end

  def receive_message
    current.received = true
    current.completed_at = Time.now

    p [:received_email, current]
    @current = OpenStruct.new
    true
  end

  def receive_ehlo_domain(domain)
    @ehlo_domain = domain
    true
  end

  def receive_data_command
    current.data = ""
    true
  end

  def receive_data_chunk(data)
    current.data << data.join("\n")
    true
  end

  def receive_transaction
    if @ehlo_domain
      current.ehlo_domain = @ehlo_domain
      @ehlo_domain = nil
    end
    true
  end

  def current
    @current ||= OpenStruct.new
  end

  def self.start(host = 'localhost', port = 1025)
    require 'ostruct'
    @server = EM.start_server host, port, self
  end

  def self.stop
    if @server
      EM.stop_server @server
      @server = nil
    end
  end

  def self.running?
    !!@server
  end
end

EM.run{ EmailServer.start }

Constants

AuthRegex
DataRegex
EhloRegex
ExpnRegex
HeloRegex
HelpRegex
MailFromRegex
NoopRegex
QuitRegex
RcptToRegex
RsetRegex
StarttlsRegex
VrfyRegex

Public Class Methods

new(*args) click to toggle source
Calls superclass method EventMachine::Connection.new
# File lib/em/protocols/smtpserver.rb, line 162
def initialize *args
  super
  @parms = @@parms
  init_protocol_state
end
parms=(parms={}) click to toggle source
# File lib/em/protocols/smtpserver.rb, line 156
def self.parms= parms={}
  @@parms.merge!(parms)
end

Public Instance Methods

connection_ended() click to toggle source

Sent when the remote peer has ended the connection.

# File lib/em/protocols/smtpserver.rb, line 606
def connection_ended
end
get_server_domain() click to toggle source

The domain name returned in the first line of the response to a successful EHLO or HELO command.

# File lib/em/protocols/smtpserver.rb, line 566
def get_server_domain
  "Ok EventMachine SMTP Server"
end
get_server_greeting() click to toggle source

The greeting returned in the initial connection message to the client.

# File lib/em/protocols/smtpserver.rb, line 561
def get_server_greeting
  "EventMachine SMTP Server"
end
init_protocol_state() click to toggle source
# File lib/em/protocols/smtpserver.rb, line 270
def init_protocol_state
  @state ||= []
end
parms=(parms={}) click to toggle source
# File lib/em/protocols/smtpserver.rb, line 168
def parms= parms={}
  @parms.merge!(parms)
end
post_init() click to toggle source

In SMTP, the server talks first. But by a (perhaps flawed) axiom in EM, post_init will execute BEFORE the block passed to start_server, for any given accepted connection. Since in this class we'll probably be getting a lot of initialization parameters, we want the guts of #post_init to run AFTER the application has initialized the connection object. So we use a spawn to schedule the #post_init to run later. It's a little weird, I admit. A reasonable alternative would be to set parameters as a class variable and to do that before accepting any connections.

OBSOLETE, now we have @@parms. But the spawn is nice to keep as an illustration.

# File lib/em/protocols/smtpserver.rb, line 183
def post_init
  #send_data "220 #{get_server_greeting}\r\n" (ORIGINAL)
  #(EM.spawn {|x| x.send_data "220 #{x.get_server_greeting}\r\n"}).notify(self)
  (EM.spawn {|x| x.send_server_greeting}).notify(self)
end
process_auth(str) click to toggle source
# File lib/em/protocols/smtpserver.rb, line 340
def process_auth str
  if @state.include?(:auth)
    send_data "503 auth already issued\r\n"
  elsif str =~ /\APLAIN\s?/
    if $'.length == 0
      # we got a partial response, so let the client know to send the rest
      @state << :auth_incomplete
      send_data("334 \r\n")
    else
      # we got the initial response, so go ahead & process it
      process_auth_line($')
    end
    #elsif str =~ /\ALOGIN\s+/i
  else
    send_data "504 auth mechanism not available\r\n"
  end
end
process_auth_line(line) click to toggle source
# File lib/em/protocols/smtpserver.rb, line 358
def process_auth_line(line)
  plain = line.unpack("m").first
  _,user,psw = plain.split("\0000")
  if receive_plain_auth user,psw
    send_data "235 authentication ok\r\n"
    @state << :auth
  else
    send_data "535 invalid authentication\r\n"
  end
  @state.delete :auth_incomplete
end
process_data() click to toggle source
# File lib/em/protocols/smtpserver.rb, line 375
def process_data
  unless @state.include?(:rcpt)
    send_data "503 Operation sequence error\r\n"
  else
    succeeded = proc {
      send_data "354 Send it\r\n"
      @state << :data
      @databuffer = []
    }
    failed = proc {
      send_data "550 Operation failed\r\n"
    }

    d = receive_data_command

    if d.respond_to?(:callback)
      d.callback(&succeeded)
      d.errback(&failed)
    else
      (d ? succeeded : failed).call
    end
  end
end
process_data_line(ln) click to toggle source

Send the incoming data to the application one chunk at a time, rather than one line at a time. That lets the application be a little more flexible about storing to disk, etc. Since we clear the chunk array every time we submit it, the caller needs to be aware to do things like dup it if he wants to keep it around across calls.

Resets the transaction upon disposition of the incoming message. RFC5321 says this about the MAIL FROM command:

"This command tells the SMTP-receiver that a new mail transaction is
 starting and to reset all its state tables and buffers, including any
 recipients or mail data."

Equivalent behaviour is implemented by resetting after a completed transaction.

User-written code can return a Deferrable as a response from receive_message.

# File lib/em/protocols/smtpserver.rb, line 519
def process_data_line ln
  if ln == "."
    if @databuffer.length > 0
      receive_data_chunk @databuffer
      @databuffer.clear
    end


    succeeded = proc {
      send_data "250 Message accepted\r\n"
      reset_protocol_state
    }
    failed = proc {
      send_data "550 Message rejected\r\n"
      reset_protocol_state
    }
    d = receive_message

    if d.respond_to?(:set_deferred_status)
      d.callback(&succeeded)
      d.errback(&failed)
    else
      (d ? succeeded : failed).call
    end

    @state.delete :data
  else
    # slice off leading . if any
    ln.slice!(0...1) if ln[0] == .
    @databuffer << ln
    if @databuffer.length > @@parms[:chunksize]
      receive_data_chunk @databuffer
      @databuffer.clear
    end
  end
end
process_ehlo(domain) click to toggle source
# File lib/em/protocols/smtpserver.rb, line 293
def process_ehlo domain
  if receive_ehlo_domain domain
    send_data "250-#{get_server_domain}\r\n"
    if @@parms[:starttls]
      send_data "250-STARTTLS\r\n"
    end
    if @@parms[:auth]
      send_data "250-AUTH PLAIN\r\n"
    end
    send_data "250-NO-SOLICITING\r\n"
    # TODO, size needs to be configurable.
    send_data "250 SIZE 20000000\r\n"
    reset_protocol_state
    @state << :ehlo
  else
    send_data "550 Requested action not taken\r\n"
  end
end
process_expn() click to toggle source

TODO - implement this properly, the implementation is a stub!

# File lib/em/protocols/smtpserver.rb, line 240
def process_expn
  send_data "250 Ok, but unimplemented\r\n"
end
process_helo(domain) click to toggle source
# File lib/em/protocols/smtpserver.rb, line 312
def process_helo domain
  if receive_ehlo_domain domain.dup
    send_data "250 #{get_server_domain}\r\n"
    reset_protocol_state
    @state << :ehlo
  else
    send_data "550 Requested action not taken\r\n"
  end
end
process_help() click to toggle source

TODO - implement this properly, the implementation is a stub!

# File lib/em/protocols/smtpserver.rb, line 236
def process_help
  send_data "250 Ok, but unimplemented\r\n"
end
process_mail_from(sender) click to toggle source
# File lib/em/protocols/smtpserver.rb, line 441
def process_mail_from sender
  if (@@parms[:starttls]==:required and !@state.include?(:starttls))
    send_data "550 This server requires STARTTLS before MAIL FROM\r\n"
  elsif (@@parms[:auth]==:required and !@state.include?(:auth))
    send_data "550 This server requires authentication before MAIL FROM\r\n"
  elsif @state.include?(:mail_from)
    send_data "503 MAIL already given\r\n"
  else
    unless receive_sender sender
      send_data "550 sender is unacceptable\r\n"
    else
      send_data "250 Ok\r\n"
      @state << :mail_from
    end
  end
end
process_noop() click to toggle source
# File lib/em/protocols/smtpserver.rb, line 327
def process_noop
  send_data "250 Ok\r\n"
end
process_quit() click to toggle source
# File lib/em/protocols/smtpserver.rb, line 322
def process_quit
  send_data "221 Ok\r\n"
  close_connection_after_writing
end
process_rcpt_to(rcpt) click to toggle source
# File lib/em/protocols/smtpserver.rb, line 470
      def process_rcpt_to rcpt
        unless @state.include?(:mail_from)
          send_data "503 MAIL is required before RCPT\r\n"
        else
          succeeded = proc {
            send_data "250 Ok\r\n"
            @state << :rcpt unless @state.include?(:rcpt)
          }
          failed = proc {
            send_data "550 recipient is unacceptable\r\n"
          }

          d = receive_recipient rcpt

          if d.respond_to?(:set_deferred_status)
            d.callback(&succeeded)
            d.errback(&failed)
          else
            (d ? succeeded : failed).call
          end

        unless receive_recipient rcpt
          send_data "550 recipient is unacceptable\r\n"
        else
          send_data "250 Ok\r\n"
          @state << :rcpt unless @state.include?(:rcpt)
        end
        end
      end
process_rset() click to toggle source
# File lib/em/protocols/smtpserver.rb, line 399
def process_rset
  reset_protocol_state
  receive_reset
  send_data "250 Ok\r\n"
end
process_starttls() click to toggle source
# File lib/em/protocols/smtpserver.rb, line 414
def process_starttls
  if @@parms[:starttls]
    if @state.include?(:starttls)
      send_data "503 TLS Already negotiated\r\n"
    elsif ! @state.include?(:ehlo)
      send_data "503 EHLO required before STARTTLS\r\n"
    else
      send_data "220 Start TLS negotiation\r\n"
      start_tls
      @state << :starttls
    end
  else
    process_unknown
  end
end
process_unknown() click to toggle source
# File lib/em/protocols/smtpserver.rb, line 331
def process_unknown
  send_data "500 Unknown command\r\n"
end
process_vrfy() click to toggle source

TODO - implement this properly, the implementation is a stub!

# File lib/em/protocols/smtpserver.rb, line 232
def process_vrfy
  send_data "250 Ok, but unimplemented\r\n"
end
receive_data_chunk(data) click to toggle source

Sent when data from the remote peer is available. The size can be controlled by setting the :chunksize parameter. This call can be made multiple times. The goal is to strike a balance between sending the data to the application one line at a time, and holding all of a very large message in memory.

# File lib/em/protocols/smtpserver.rb, line 623
def receive_data_chunk data
  @smtps_msg_size ||= 0
  @smtps_msg_size += data.join.length
  STDERR.write "<#{@smtps_msg_size}>"
end
receive_data_command() click to toggle source

Called when the remote peer sends the DATA command. Returning false will cause us to send a 550 error to the peer. This can be useful for dealing with problems that arise from processing the whole set of sender and recipients.

# File lib/em/protocols/smtpserver.rb, line 614
def receive_data_command
  true
end
receive_ehlo_domain(domain) click to toggle source

A false response from this user-overridable method will cause a 550 error to be returned to the remote client.

# File lib/em/protocols/smtpserver.rb, line 573
def receive_ehlo_domain domain
  true
end
receive_line(ln) click to toggle source
# File lib/em/protocols/smtpserver.rb, line 193
def receive_line ln
  @@parms[:verbose] and $>.puts ">>> #{ln}"

  return process_data_line(ln) if @state.include?(:data)
  return process_auth_line(ln) if @state.include?(:auth_incomplete)

  case ln
  when EhloRegex
    process_ehlo $'.dup
  when HeloRegex
    process_helo $'.dup
  when MailFromRegex
    process_mail_from $'.dup
  when RcptToRegex
    process_rcpt_to $'.dup
  when DataRegex
    process_data
  when RsetRegex
    process_rset
  when VrfyRegex
    process_vrfy
  when ExpnRegex
    process_expn
  when HelpRegex
    process_help
  when NoopRegex
    process_noop
  when QuitRegex
    process_quit
  when StarttlsRegex
    process_starttls
  when AuthRegex
    process_auth $'.dup
  else
    process_unknown
  end
end
receive_message() click to toggle source

Sent after a message has been completely received. User code must return true or false to indicate whether the message has been accepted for delivery.

# File lib/em/protocols/smtpserver.rb, line 632
def receive_message
  @@parms[:verbose] and $>.puts "Received complete message"
  true
end
receive_plain_auth(user, password) click to toggle source

Return true or false to indicate that the authentication is acceptable.

# File lib/em/protocols/smtpserver.rb, line 578
def receive_plain_auth user, password
  true
end
receive_recipient(rcpt) click to toggle source

Receives the argument of a RCPT TO command. Can be given multiple times per transaction. Return false to reject the recipient.

# File lib/em/protocols/smtpserver.rb, line 593
def receive_recipient rcpt
  true
end
receive_reset() click to toggle source

Sent when the remote peer issues the RSET command. Since RSET is not allowed to fail (according to the protocol), we ignore any return value from user overrides of this method.

# File lib/em/protocols/smtpserver.rb, line 601
def receive_reset
end
receive_sender(sender) click to toggle source

Receives the argument of the MAIL FROM command. Return false to indicate to the remote client that the sender is not accepted. This can only be successfully called once per transaction.

# File lib/em/protocols/smtpserver.rb, line 586
def receive_sender sender
  true
end
receive_transaction() click to toggle source

This is called when the protocol state is reset. It happens when the remote client calls EHLO/HELO or RSET.

# File lib/em/protocols/smtpserver.rb, line 639
def receive_transaction
end
reset_protocol_state() click to toggle source
# File lib/em/protocols/smtpserver.rb, line 263
def reset_protocol_state
  init_protocol_state
  s,@state = @state,[]
  @state << :starttls if s.include?(:starttls)
  @state << :ehlo if s.include?(:ehlo)
  receive_transaction
end
send_server_greeting() click to toggle source
# File lib/em/protocols/smtpserver.rb, line 189
def send_server_greeting
  send_data "220 #{get_server_greeting}\r\n"
end
unbind() click to toggle source
# File lib/em/protocols/smtpserver.rb, line 405
def unbind
  connection_ended
end