Skip to content

Instantly share code, notes, and snippets.

@pocari
Created June 21, 2013 07:20
Show Gist options
  • Save pocari/5829475 to your computer and use it in GitHub Desktop.
Save pocari/5829475 to your computer and use it in GitHub Desktop.
require 'socket'
class SMTPParameters
attr_accessor :helo_name, :from, :rcpt, :data
def initialize
@helo_name = nil
@from = nil
@rcpt = []
@data = nil
end
def to_s
<<EOS
helo_name: #{@helo_name}
from : #{@from}
rcpt : #{@rcpt.join(" ")}
data : #{"\n" + @data}
EOS
end
end
class SMTPSession
MAX_LINE_LENGTH = 1024 - 2 # -2 is length of CRLF
class ProtocolException < StandardError; end
def initialize(sock, hook=nil)
@sock = sock
@hook = hook
init_session_info
end
def exec()
write_with_code(220, "Welcome")
begin
loop do
command_loop_main
break if @session_stat == :quit
end
rescue => e
log([e.message, e.backtrace].join("\n"))
ensure
@sock.flush
@sock.close
end
end
:private
def init_session_info
@session_stat = nil
@params = SMTPParameters.new
end
def read()
@sock.gets
end
def write(msg)
@sock.write(msg + "\r\n")
@sock.flush
end
def write_with_code(code, msg)
write(with_code(code, msg))
end
def with_code(code, msg)
"#{code} #{msg}"
end
class << self
def define_error_method(code, msg, argnum)
arguments_def = argnum.times.map{|i| "arg" + i.to_s}.join(", ")
str = <<-EOS
def raise_error_#{code}(#{arguments_def})
raise ProtocolException, with_code(#{code}, ["#{msg}" #{argnum == 0 ? "" : ", " + arguments_def}].join(""))
end
EOS
#puts str
module_eval str
end
end
code = [
[500, "Syntax error, command unrecognized.[This may include errors such as command line too long]", 0],
[501, "Syntax error in parameters or arguments. ", 1],
[503, "Bad sequence of commands.", 1]
]
code.each do |params|
define_error_method(*params)
end
def raise_protocol_error(code, msg)
raise ProtocolException, with_code(code, msg)
end
def handle_hello(args)
raise_error_501("HELO <SP> <domain> <CRLF>") unless args =~ /\A\s+(.+)\Z/i
@params.helo_name = Regexp.last_match(1)
write_with_code(250, args)
@session_stat = :done_helo
end
#MAIL <SP> FROM:<reverse-path> <CRLF>
def handle_mail(args)
raise_error_503("(HELO first)") unless @session_stat == :done_helo
raise_error_501("MAIL <SP> FROM:<reverse-path> <CRLF>") unless args =~ /\A\s+FROM:(.*)\Z/i
@params.from = Regexp.last_match(1)
@hook.handle_mail(@params) if @hook
@session_stat = :done_mail
write_with_code(250, "OK")
end
def handle_rcpt(args)
raise_error_503("(before RCPT)") unless [:done_mail, :done_rcpt].member? @session_stat
raise_error_501("RCPT <SP> TO:<forward-path> <CRLF>") unless args =~ /\A\s+TO:(.*)\Z/i
@params.rcpt << Regexp.last_match(1)
@hook.handle_rcpt(@params) if @hook
@session_stat = :done_rcpt
write_with_code(250, "OK")
end
def handle_data()
@session_stat = :data
write_with_code(354, "Start mail input; end with <CRLF>.<CRLF>")
tmp_data = []
loop do
line = read.chomp!
break if line == "."
tmp_data << line
end
@params.data = tmp_data.join("\n")
@hook.handle_rcpt(@params) if @hook
write_with_code(250, "OK")
end
def handle_rset
init_session_info
@hook.handle_rset(@params) if @hook
end
def handle_noop
write_with_code(250, "OK")
@hook.handle_noop(@params) if @hook
end
def handle_quit
write_with_code(221, "OK")
@hook.handle_quit(@params) if @hook
@session_stat = :quit
end
def log(msg)
puts(msg); $stdout.flush
end
def command_loop_main
begin
line = read
line.chomp! if line
raise_error_500 if line.length == MAX_LINE_LENGTH
if line =~ /\A(\S+)(.*)\Z/
command = Regexp.last_match(1)
args = Regexp.last_match(2)
case command.upcase
when 'HELO'
handle_hello(args)
when 'EHLO'
handle_hello(args)
when 'MAIL'
handle_mail(args)
when 'RCPT'
handle_rcpt(args)
when 'DATA'
handle_data()
when 'RSET'
handle_rset()
when 'NOOP'
handle_noop()
when 'QUIT'
handle_quit()
else
raise_error_500
end
end
rescue ProtocolException => e
write(e.message)
rescue => e
log([e.message, e.backtrace].join("\n"))
raise e
end
end
end
class SMTPServer
attr_accessor :hook
def initialize(port, host="127.0.0.1")
@port = port
@host = host
@hook = nil
end
def start
@stop = false
server = TCPServer.new(@host, @port)
until @stop
client_socket = server.accept
Thread.new {
begin
SMTPSession.new(client_socket, @hook).exec
rescue => e
puts e.message; $stdout.flush
end
}
end
end
def stop
@stop = true
end
class << self
def start(port, host="127.0.0.1", &b)
server = self.new(port, host)
if block_given?
hook = SMTPHook.new
hook.instance_eval(&b)
server.hook = hook
end
server.start
end
end
end
class SMTPHook
def handle_hello(params)
end
def handle_hello(params)
end
def handle_mail(params)
end
def handle_rcpt(params)
end
def handle_data(params)
end
def handle_rset(params)
end
def handle_noop(params)
end
def handle_quit(params)
end
def handle_quit(params)
end
end
if $0 == __FILE__
require 'tmail'
SMTPServer.start(30000) do
def log(msg)
$stderr.puts(msg)
end
def to_default_external(str, from)
str.force_encoding(from).encode(Encoding.default_external)
end
def handle_quit(param)
email = TMail::Mail.parse(param.data)
log("----------------------------decoded to '#{Encoding.default_external}' from '#{email.charset}'")
log("helo: " + param.helo_name);
log("mail from: " + param.from);
log("rcpt to:: " + param.rcpt.join(" "));
log("")
log("Date: " + email.date.to_s)
log("From: " + email.from.join(" "))
log("To: " + email.to.join(" "))
log("Subject: " + to_default_external(email.subject, email.charset))
(email.each_header_name.to_a - ["date", "from", "to", "subject"]).each do |header|
log("#{header.capitalize}: #{email[header].to_s}")
end
log("")
log(to_default_external(email.body, email.charset))
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment