Created
June 21, 2013 07:20
-
-
Save pocari/5829475 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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