Class: EventMachine::Protocols::SmtpServer

### Home Classes Methods

### In Files

- lib/em/protocols/smtpserver.rb

### Parent

EventMachine::Connection

### Methods

- ::new
- ::parms=
- #connection_ended
- #get_server_domain
- #get_server_greeting
- #init_protocol_state
- #parms=
- #post_init
- #process_auth
- #process_auth_line
- #process_data
- #process_data_line
- #process_ehlo
- #process_expn
- #process_helo
- #process_help
- #process_mail_from
- #process_noop
- #process_quit
- #process_rcpt_to
- #process_rset
- #process_starttls
- #process_unknown
- #process_vrfy
- #receive_data_chunk
- #receive_data_command
- #receive_ehlo_domain
- #receive_line
- #receive_message
- #receive_plain_auth
- #receive_recipient
- #receive_reset
- #receive_sender
- #receive_transaction
- #reset_protocol_state
- #send_server_greeting
- #unbind

### Included Modules

- EventMachine::Protocols::LineText2

### Files

- ChangeLog
- DEFERRABLES
- EPOLL
- INSTALL
- KEYBOARD
- LEGAL
- LIGHTWEIGHT_CONCURRENCY
- PURE_RUBY
- RELEASE_NOTES
- SMTP
- SPAWNED_PROCESSES
- TODO

### Class/Module Index

Quicksearch

- EventMachine
- EventMachine
- EventMachine::Channel
- EventMachine::Completion
- EventMachine::Connection
- EventMachine::DNS
- EventMachine::DNS::Request
- EventMachine::DNS::RequestIdAlreadyUsed
- EventMachine::DNS::Resolver
- EventMachine::DNS::Socket
- EventMachine::DatagramObject
- EventMachine::DefaultDeferrable
- EventMachine::Deferrable
- EventMachine::DeferrableChildProcess
- EventMachine::Error
- EventMachine::EvmaKeyboard
- EventMachine::EvmaTCPClient
- EventMachine::EvmaTCPServer
- EventMachine::EvmaUDPSocket
- EventMachine::EvmaUNIXClient
- EventMachine::EvmaUNIXServer
- EventMachine::FileNotFoundException
- EventMachine::FileStreamer
- EventMachine::FileWatch
- EventMachine::Iterator
- EventMachine::LoopbreakReader
- EventMachine::PeriodicTimer
- EventMachine::Pool
- EventMachine::ProcessWatch
- EventMachine::Protocols
- EventMachine::Protocols
- EventMachine::Protocols::HeaderAndContentProtocol
- EventMachine::Protocols::HttpClient
- EventMachine::Protocols::HttpClient2
- EventMachine::Protocols::HttpClient2::Request
- EventMachine::Protocols::LineAndTextProtocol
- EventMachine::Protocols::LineProtocol
- EventMachine::Protocols::LineText2
- EventMachine::Protocols::Memcache
- EventMachine::Protocols::Memcache::ParserError
- EventMachine::Protocols::ObjectProtocol
- EventMachine::Protocols::Postgres3
- EventMachine::Protocols::SASLauth
- EventMachine::Protocols::SASLauthclient
- EventMachine::Protocols::SmtpClient
- EventMachine::Protocols::SmtpServer
- EventMachine::Protocols::Socks4
- EventMachine::Protocols::Stomp
- EventMachine::Protocols::Stomp::Message
- EventMachine::Protocols::TcpConnectTester
- EventMachine::Queue
- EventMachine::Reactor
- EventMachine::Selectable
- EventMachine::SpawnedProcess
- EventMachine::StreamObject
- EventMachine::SystemCmd
- EventMachine::ThreadedResource
- EventMachine::TickLoop
- EventMachine::Timer
- EventMachine::UuidGenerator
- EventMachine::YieldBlockFromSpawnedProcess
- Object
- Object::EM
- Object::P
- BufferedTokenizer
- IO
- StringIO
- TestConnection

No matching classes.

# 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

    # 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 629
    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 589
    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 584
    def get_server_greeting
      "EventMachine SMTP Server"
    end

init_protocol_state() click to toggle source

    # File lib/em/protocols/smtpserver.rb, line 278
    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 348
    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 366
    def process_auth_line(line)
      plain = line.unpack("m").first
      _,user,psw = plain.split("\0000")

      succeeded = proc {
        send_data "235 authentication ok\r\n"
        @state << :auth
      }
      failed = proc {
        send_data "535 invalid authentication\r\n"
      }
      auth = receive_plain_auth user,psw

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

      @state.delete :auth_incomplete
    end

process_data() click to toggle source

    # File lib/em/protocols/smtpserver.rb, line 394
    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 542
    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 301
    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 248
    def process_expn
      send_data "502 Command not implemented\r\n"
    end

process_helo(domain) click to toggle source

    # File lib/em/protocols/smtpserver.rb, line 320
    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 232
    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 464
    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 335
    def process_noop
      send_data "250 Ok\r\n"
    end

process_quit() click to toggle source

    # File lib/em/protocols/smtpserver.rb, line 330
    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 493
          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 418
    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 437
    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(@@parms[:starttls_options] || {})
          @state << :starttls
        end
      else
        process_unknown
      end
    end

process_unknown() click to toggle source

    # File lib/em/protocols/smtpserver.rb, line 339
    def process_unknown
      send_data "500 Unknown command\r\n"
    end

process_vrfy() click to toggle source

RFC2821, 3.5.3 Meaning of VRFY or EXPN Success Response:

    A server MUST NOT return a 250 code in response to a VRFY or EXPN
    command unless it has actually verified the address.  In particular,
    a server MUST NOT return 250 if all it has done is to verify that the
    syntax given is valid.  In that case, 502 (Command not implemented)
    or 500 (Syntax error, command unrecognized) SHOULD be returned.

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

    # File lib/em/protocols/smtpserver.rb, line 244
    def process_vrfy
      send_data "502 Command not implemented\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 646
    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 637
    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 596
    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 655
    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 601
    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 616
    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 624
    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 609
    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 662
    def receive_transaction
    end

reset_protocol_state() click to toggle source

    # File lib/em/protocols/smtpserver.rb, line 271
    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 424
    def unbind
      connection_ended
    end

[Validate]

Generated with the Darkfish Rdoc Generator 2.