667 lines
22 KiB
Ruby
667 lines
22 KiB
Ruby
#--
|
|
#
|
|
# 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
|