Skip to content

Instantly share code, notes, and snippets.

@yaplik
Created December 15, 2012 23:33
Show Gist options
  • Save yaplik/4300907 to your computer and use it in GitHub Desktop.
Save yaplik/4300907 to your computer and use it in GitHub Desktop.
Project for PV249 - Example SMTP Server with forwarding via smtp
source :rubygems
gem "eventmachine"
#!/usr/bin/env ruby
# Example SMTP Server in Ruby for course FI:PV249
#
# @author Jiri Zapletal <yaplik@gmail.com>
# @license BSD
require 'bundler/setup'
Bundler.require(:default)
require 'resolv'
def resolv(email)
if m = /@(.*)/i.match(email) then
domain = m[1]
else
return nil
end
# get mx exchanges by priority
mx = Resolv::DNS.open do
|dns| dns.getresources(domain, Resolv::DNS::Resource::IN::MX)
end.sort do
|a,b| a.preference <=> b.preference
end.map do
|x| x.exchange.to_s
end
if mx.any? then
return mx
end
# fallback
begin
tmp = Resolv.getaddresses(domain)
return tmp
rescue Resolv::ResolvError
return []
end
end
module SmtpProtocol
include EventMachine::Protocols::LineText2
def post_init
reset_state
send_line("220 mail.example.net SMTP Ruby Server")
end
def reset_state
@mail = nil
@rcpt = []
@data = ""
@clients = []
@queue_ids = []
end
def send_line(data)
puts "[S] " + data
send_data(data + "\r\n")
end
def receive_line(line)
puts "[C] " + line
case
when m = /^HELO (.*)$/i.match(line)
send_line "250 Hello"
when m = /^EHLO (.*)$/i.match(line)
send_line "250-mail.example.net"
send_line "250-STARTTLS"
send_line "250 DSN"
when m = /^STARTTLS$/i.match(line)
send_line "220 2.0.0 Ready to start TLS"
start_tls
when m = /^MAIL FROM:(.*)$/i.match(line)
if @from != nil then
send_line "503 5.5.1 Error: nested MAIL command"
return
end
@from = m[1]
send_line "250 2.1.0 Ok"
when m = /^RCPT TO:(.*)/i.match(line)
if @from == nil then
send_line "503 5.5.1 Error: need MAIL command"
return
end
@rcpt << m[1]
send_line "250 2.1.5 Ok"
when m = /^RSET$/i.match(line)
reset_state
send_line "250 2.0.0 Ok"
when m = /^DATA$/i.match(line)
if @rcpt.count == 0 then
send_line "503 5.5.1 Error: need RCPT command"
return
end
send_line "354 End data with <CR><LF>.<CR><LF>"
set_text_mode
when m = /^QUIT$/i.match(line)
send_line "221 Bye"
close_connection_after_writing
else
puts "[error]", line
send_line "502 5.5.2 Error: command not recognized"
end
end
def receive_binary_data(data)
if /\r\n\.\r\n$/.match(data) then
@data += data[0..-5]
forward_mail(@from, @rcpt, @data)
set_line_mode
else
@data += data
end
end
def unbind
puts "[C Disconnected]"
reset_state
end
def forward_mail(from, to, data)
puts "Email from #{from} to #{to.join(",")}"
@data = "Received: by Ruby Example SMTP forwarder\r\n" + @data
@rcpt.each do |to|
exchanges = resolv(to.tr("<>", ""))
next if exchanges.empty?
puts "Connecting to #{exchanges[0]}:25"
client = EventMachine.connect(exchanges[0], 25, SmtpClientProtocol)
client.send_mail(@from, to, @data)
client.notify_server = self
@clients << client
end
end
def client_done(client, sent, queue_id = nil)
@clients.delete(client)
@queue_ids << queue_id
if not sent then
send_line "421 Failed to forward message"
reset_state
return
end
if @clients.empty? then
send_line "250 2.0.0 Ok: message has been forwarded successfully as " + @queue_ids.join(",")
reset_state
end
end
end
module SmtpClientProtocol
include EventMachine::Protocols::LineText2
def post_init
@state = :from
@from = nil
@rcpt = nil
@data = ""
@notify_server = nil
@queue_id = nil
@sent = false
end
def send_mail(from, rcpt, data)
@from = from
@rcpt = rcpt
@data = data
end
def send_line(data)
puts "[C][C] " + data
send_data(data + "\r\n")
end
def receive_line(line)
puts "[C][S] " + line
case
when m = /^220 /i.match(line)
send_line "HELO localhost"
when m = /^250 (.*)$/i.match(line)
case @state
when :from then
send_line "QUIT" and return if @from == nil
send_line "MAIL FROM: " + @from
@state = :rcpt
when :rcpt then
send_line "QUIT" and return if @rcpt == nil
send_line "RCPT TO: " + @rcpt
@state = :data
when :data then
send_line "DATA"
@state = :quit
when :quit
m = /Queued as (.*)/i.match(line)
@queue_id = m[1] if m
@sent = true
send_line "QUIT"
close_connection_after_writing
end
when m = /^354 (.*)$/i.match(line)
send_line @data
send_line "."
when m = /^221 /i.match(line) #221 2.0.0 Bye; after QUIT
close_connection
when m = /^[45]/i.match(line) #quit on errors
puts "[error]:" + line
send_line "QUIT"
close_connection_after_writing
else
puts "[error]" + line
send_line "QUIT"
close_connection_after_writing
end
end
def notify_server=(obj)
@notify_server = obj
end
def unbind
puts "[C][S Disconnected]"
@notify_server.send(:client_done, self, @sent, @queue_id) unless @notify_server == nil
end
end
EventMachine.run do
Signal.trap("INT") { EventMachine.stop }
Signal.trap("TERM") { EventMachine.stop }
EventMachine.start_server("0.0.0.0", 8025, SmtpProtocol)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment