123456789_123456789_123456789_123456789_123456789_

Class: EventMachine::Protocols::SmtpServer

Relationships & Source Files
Super Chains via Extension / Inclusion / Inheritance
Class Chain:
self, Connection
Instance Chain:
Inherits: EventMachine::Connection
Defined in: lib/em/protocols/smtpserver.rb

Overview

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 }

Constant Summary

LineText2 - Included

MaxBinaryLength

Class Method Summary

Connection - Inherited

.new

Override .new so subclasses don't have to call super and can ignore connection-specific arguments.

Instance Attribute Summary

Connection - Inherited

#comm_inactivity_timeout

comm_inactivity_timeout returns the current value (float in seconds) of the inactivity-timeout property of network-connection and datagram-socket objects.

#comm_inactivity_timeout=

Allows you to set the inactivity-timeout property for a network connection or datagram socket.

#error?

Returns true if the connection is in an error state, false otherwise.

#notify_readable=

Watches connection for readability.

#notify_readable?,
#notify_writable=

Watches connection for writeability.

#notify_writable?

Returns true if the connection is being watched for writability.

#paused?,
#pending_connect_timeout

The duration after which a TCP connection in the connecting state will fail.

#pending_connect_timeout=

Sets the duration after which a TCP connection in a connecting state will fail.

#signature, #watch_only?

Instance Method Summary

LineText2 - Included

#receive_binary_data

Stub.

#receive_data,
#receive_end_of_binary_data

Stub.

#receive_line

Stub.

#set_binary_mode

Alias for #set_text_mode, added for back-compatibility with LineAndTextProtocol.

#set_delimiter

The line delimiter may be a regular expression or a string.

#set_line_mode

Called internally but also exposed to user code, for the case in which processing of binary data creates a need to transition back to line mode.

#set_text_mode,
#unbind

In case of a dropped connection, we'll send a partial buffer to user code when in sized text mode.

Connection - Inherited

#associate_callback_target

conn_associate_callback_target.

#close_connection

EventMachine::Connection#close_connection is called only by user code, and never by the event loop.

#close_connection_after_writing
#connection_completed

Called by the event loop when a remote TCP connection attempt completes successfully.

#detach

Removes given connection from the event loop.

#disable_keepalive

t_disable_keepalive.

#enable_keepalive

t_enable_keepalive.

#get_cipher_bits, #get_cipher_name, #get_cipher_protocol,
#get_idle_time

The number of seconds since the last send/receive activity on this connection.

#get_outbound_data_size

conn_get_outbound_data_size.

#get_peer_cert

If TLS is active on the connection, returns the remote X509 certificate as a string, in the popular PEM format.

#get_peername

This method is used with stream-connections to obtain the identity of the remotely-connected peer.

#get_pid

Returns the PID (kernel process identifier) of a subprocess associated with this Connection object.

#get_proxied_bytes

The number of bytes proxied to another connection.

#get_sni_hostname, #get_sock_opt,
#get_sockname

Used with stream-connections to obtain the identity of the local side of the connection.

#get_status

Returns a subprocess exit status.

#initialize

Stubbed initialize so legacy superclasses can safely call super.

#original_method,
#pause

Pause a connection so that EventMachine#send_data and #receive_data events are not fired until #resume is called.

#post_init

Called by the event loop immediately after the network connection has been established, and before resumption of the network loop.

#proxy_completed

called when the reactor finished proxying all of the requested bytes.

#proxy_incoming_to

EventMachine::Connection#proxy_incoming_to is called only by user code.

#proxy_target_unbound

Called by the reactor after attempting to relay incoming data to a descriptor (set as a proxy target descriptor with EventMachine.enable_proxy) that has already been closed.

#receive_data

Called by the event loop whenever data has been received by the network connection.

#reconnect

Reconnect to a given host/port with the current instance.

#resume

Resume a connection's EventMachine#send_data and #receive_data events.

#send_data

Call this method to send data to the remote end of the network connection.

#send_datagram

Sends UDP messages.

#send_file_data

Like Connection#send_data, this sends data to the remote end of the network connection.

#set_sock_opt,
#ssl_handshake_completed

Called by ::EventMachine when the SSL/TLS handshake has been completed, as a result of calling #start_tls to initiate SSL/TLS on the connection.

#ssl_verify_peer

Called by ::EventMachine when :verify_peer => true has been passed to EventMachine#start_tls.

#start_tls

Call EventMachine#start_tls at any point to initiate TLS encryption on connected streams.

#stop_proxying

A helper method for EventMachine.disable_proxy

#stream_file_data

Open a file on the filesystem and send it to the remote peer.

#unbind

called by the framework whenever a connection (either a server or client connection) is closed.

Constructor Details

.new(*args) ⇒ SmtpServer

[ GitHub ]

  
# File 'lib/em/protocols/smtpserver.rb', line 162

def initialize *args
  super
  @parms = @@parms
  init_protocol_state
end

Class Method Details

.parms=(parms = {})

[ GitHub ]

  
# File 'lib/em/protocols/smtpserver.rb', line 156

def self.parms= parms={}
  @@parms.merge!(parms)
end

Instance Method Details

#connection_ended

Sent when the remote peer has ended the connection.

[ GitHub ]

  
# File 'lib/em/protocols/smtpserver.rb', line 629

def connection_ended
end

#get_server_domain

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

[ GitHub ]

  
# File 'lib/em/protocols/smtpserver.rb', line 589

def get_server_domain
  "Ok EventMachine SMTP Server"
end

#get_server_greeting

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

[ GitHub ]

  
# File 'lib/em/protocols/smtpserver.rb', line 584

def get_server_greeting
  "EventMachine SMTP Server"
end

#init_protocol_state

[ GitHub ]

  
# File 'lib/em/protocols/smtpserver.rb', line 278

def init_protocol_state
  @state ||= []
end

#parms=(parms = {})

[ GitHub ]

  
# File 'lib/em/protocols/smtpserver.rb', line 168

def parms= parms={}
  @parms.merge!(parms)
end

#post_init

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.

[ GitHub ]

  
# 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)

[ GitHub ]

  
# 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?/i
    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)

[ GitHub ]

  
# File 'lib/em/protocols/smtpserver.rb', line 366

def process_auth_line(line)
  plain = line.unpack("m").first
  _,user,psw = plain.split("\000")
  
  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

[ GitHub ]

  
# 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)

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.

[ GitHub ]

  
# 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 -= [:data, :mail_from, :rcpt]
  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)

[ GitHub ]

  
# 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

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

[ GitHub ]

  
# File 'lib/em/protocols/smtpserver.rb', line 248

def process_expn
  send_data "502 Command not implemented\r\n"
end

#process_helo(domain)

[ GitHub ]

  
# 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

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

[ GitHub ]

  
# File 'lib/em/protocols/smtpserver.rb', line 232

def process_help
  send_data "250 Ok, but unimplemented\r\n"
end

#process_mail_from(sender)

[ GitHub ]

  
# 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

[ GitHub ]

  
# File 'lib/em/protocols/smtpserver.rb', line 335

def process_noop
  send_data "250 Ok\r\n"
end

#process_quit

[ GitHub ]

  
# 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)

[ GitHub ]

  
# 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

=begin
  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
end

#process_rset

[ GitHub ]

  
# 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

[ GitHub ]

  
# 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

[ GitHub ]

  
# File 'lib/em/protocols/smtpserver.rb', line 339

def process_unknown
  send_data "500 Unknown command\r\n"
end

#process_vrfy

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!

[ GitHub ]

  
# File 'lib/em/protocols/smtpserver.rb', line 244

def process_vrfy
  send_data "502 Command not implemented\r\n"
end

#receive_data_chunk(data)

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.

[ GitHub ]

  
# 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

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.

[ GitHub ]

  
# File 'lib/em/protocols/smtpserver.rb', line 637

def receive_data_command
  true
end

#receive_ehlo_domain(domain)

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

[ GitHub ]

  
# File 'lib/em/protocols/smtpserver.rb', line 596

def receive_ehlo_domain domain
  true
end

#receive_line(ln)

[ GitHub ]

  
# 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

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.

[ GitHub ]

  
# 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)

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

[ GitHub ]

  
# File 'lib/em/protocols/smtpserver.rb', line 601

def receive_plain_auth user, password
  true
end

#receive_recipient(rcpt)

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

[ GitHub ]

  
# File 'lib/em/protocols/smtpserver.rb', line 616

def receive_recipient rcpt
  true
end

#receive_reset

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.

[ GitHub ]

  
# File 'lib/em/protocols/smtpserver.rb', line 624

def receive_reset
end

#receive_sender(sender)

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.

[ GitHub ]

  
# File 'lib/em/protocols/smtpserver.rb', line 609

def receive_sender sender
  true
end

#receive_transaction

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

[ GitHub ]

  
# File 'lib/em/protocols/smtpserver.rb', line 662

def receive_transaction
end

#reset_protocol_state

[ GitHub ]

  
# 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

[ GitHub ]

  
# File 'lib/em/protocols/smtpserver.rb', line 189

def send_server_greeting
  send_data "220 #{get_server_greeting}\r\n"
end

#unbind

[ GitHub ]

  
# File 'lib/em/protocols/smtpserver.rb', line 424

def unbind
  connection_ended
end