#-- # # Author:: Francis Cianfrocca (gmail: blackhedd) # Homepage:: http://rubyeventmachine.com # Date:: 16 July 2006 # # See EventMachine and EventMachine::Connection for documentation and # usage examples. # #---------------------------------------------------------------------------- # # Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved. # Gmail: blackhedd # # This program is free software; you can redistribute it and/or modify # it under the terms of either: 1) the GNU General Public License # as published by the Free Software Foundation; either version 2 of the # License, or (at your option) any later version; or 2) Ruby's License. # # See the file COPYING for complete licensing information. # #--------------------------------------------------------------------------- # # module EventMachine module Protocols # 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 } # #-- # Useful paragraphs in RFC-2821: # 4.3.2: Concise list of command-reply sequences, in essence a text representation # of the command state-machine. # # STARTTLS is defined in RFC2487. # Observe that there are important rules governing whether a publicly-referenced server # (meaning one whose Internet address appears in public MX records) may require the # non-optional use of TLS. # Non-optional TLS does not apply to EHLO, NOOP, QUIT or STARTTLS. class SmtpServer < EventMachine::Connection include Protocols::LineText2 HeloRegex = /\AHELO\s*/i EhloRegex = /\AEHLO\s*/i QuitRegex = /\AQUIT/i MailFromRegex = /\AMAIL FROM:\s*/i RcptToRegex = /\ARCPT TO:\s*/i DataRegex = /\ADATA/i NoopRegex = /\ANOOP/i RsetRegex = /\ARSET/i VrfyRegex = /\AVRFY\s+/i ExpnRegex = /\AEXPN\s+/i HelpRegex = /\AHELP/i StarttlsRegex = /\ASTARTTLS/i AuthRegex = /\AAUTH\s+/i # Class variable containing default parameters that can be overridden # in application code. # Individual objects of this class will make an instance-local copy of # the class variable, so that they can be reconfigured on a per-instance # basis. # # Chunksize is the number of data lines we'll buffer before # sending them to the application. TODO, make this user-configurable. # @@parms = { :chunksize => 4000, :verbose => false } def self.parms= parms={} @@parms.merge!(parms) end def initialize *args super @parms = @@parms init_protocol_state end def parms= parms={} @parms.merge!(parms) end # 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. # 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 def send_server_greeting send_data "220 #{get_server_greeting}\r\n" end 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 # TODO - implement this properly, the implementation is a stub! def process_help send_data "250 Ok, but unimplemented\r\n" end # 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! def process_vrfy send_data "502 Command not implemented\r\n" end # TODO - implement this properly, the implementation is a stub! def process_expn send_data "502 Command not implemented\r\n" end #-- # This is called at several points to restore the protocol state # to a pre-transaction state. In essence, we "forget" having seen # any valid command except EHLO and STARTTLS. # We also have to callback user code, in case they're keeping track # of senders, recipients, and whatnot. # # We try to follow the convention of avoiding the verb "receive" for # internal method names except receive_line (which we inherit), and # using only receive_xxx for user-overridable stubs. # # init_protocol_state is called when we initialize the connection as # well as during reset_protocol_state. It does NOT call the user # override method. This enables us to promise the users that they # won't see the overridable fire except after EHLO and RSET, and # after a message has been received. Although the latter may be wrong. # The standard may allow multiple DATA segments with the same set of # senders and recipients. # 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 def init_protocol_state @state ||= [] end #-- # EHLO/HELO is always legal, per the standard. On success # it always clears buffers and initiates a mail "transaction." # Which means that a MAIL FROM must follow. # # Per the standard, an EHLO/HELO or a RSET "initiates" an email # transaction. Thereafter, MAIL FROM must be received before # RCPT TO, before DATA. Not sure what this specific ordering # achieves semantically, but it does make it easier to # implement. We also support user-specified requirements for # STARTTLS and AUTH. We make it impossible to proceed to MAIL FROM # without fulfilling tls and/or auth, if the user specified either # or both as required. We need to check the extension standard # for auth to see if a credential is discarded after a RSET along # with all the rest of the state. We'll behave as if it is. # Now clearly, we can't discard tls after its been negotiated # without dropping the connection, so that flag doesn't get cleared. # 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 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 def process_quit send_data "221 Ok\r\n" close_connection_after_writing end def process_noop send_data "250 Ok\r\n" end def process_unknown send_data "500 Unknown command\r\n" end #-- # So far, only AUTH PLAIN is supported but we should do at least LOGIN as well. # TODO, support clients that send AUTH PLAIN with no parameter, expecting a 3xx # response and a continuation of the auth conversation. # 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 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 #-- # Unusually, we can deal with a Deferrable returned from the user application. # This was added to deal with a special case in a particular application, but # it would be a nice idea to add it to the other user-code callbacks. # 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 def process_rset reset_protocol_state receive_reset send_data "250 Ok\r\n" end def unbind connection_ended end #-- # STARTTLS may not be issued before EHLO, or unless the user has chosen # to support it. # # If :starttls_options is present and :starttls is set in the parms # pass the options in :starttls_options to start_tls. Do this if you want to use # your own certificate # e.g. {:cert_chain_file => "/etc/ssl/cert.pem", :private_key_file => "/etc/ssl/private/cert.key"} 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 #-- # Requiring TLS is touchy, cf RFC2784. # Requiring AUTH seems to be much more reasonable. # We don't currently support any notion of deriving an authentication from the TLS # negotiation, although that would certainly be reasonable. # We DON'T allow MAIL FROM to be given twice. # We DON'T enforce all the various rules for validating the sender or # the reverse-path (like whether it should be null), and notifying the reverse # path in case of delivery problems. All of that is left to the calling application. # 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 #-- # Since we require :mail_from to have been seen before we process RCPT TO, # we don't need to repeat the tests for TLS and AUTH. # Note that we don't remember or do anything else with the recipients. # All of that is on the user code. # TODO: we should enforce user-definable limits on the total number of # recipients per transaction. # We might want to make sure that a given recipient is only seen once, but # for now we'll let that be the user's problem. # # User-written code can return a deferrable from receive_recipient. # 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 # 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. # 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 #------------------------------------------ # Everything from here on can be overridden in user code. # The greeting returned in the initial connection message to the client. def get_server_greeting "EventMachine SMTP Server" end # The domain name returned in the first line of the response to a # successful EHLO or HELO command. def get_server_domain "Ok EventMachine SMTP Server" end # A false response from this user-overridable method will cause a # 550 error to be returned to the remote client. # def receive_ehlo_domain domain true end # Return true or false to indicate that the authentication is acceptable. def receive_plain_auth user, password true end # 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. # def receive_sender sender true end # Receives the argument of a RCPT TO command. Can be given multiple # times per transaction. Return false to reject the recipient. # def receive_recipient rcpt true end # 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. # def receive_reset end # Sent when the remote peer has ended the connection. # def connection_ended end # 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. # def receive_data_command true end # 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. # def receive_data_chunk data @smtps_msg_size ||= 0 @smtps_msg_size += data.join.length STDERR.write "<#{@smtps_msg_size}>" end # 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. def receive_message @@parms[:verbose] and $>.puts "Received complete message" true end # This is called when the protocol state is reset. It happens # when the remote client calls EHLO/HELO or RSET. def receive_transaction end end end end