Created
December 15, 2012 23:33
-
-
Save yaplik/4300907 to your computer and use it in GitHub Desktop.
Project for PV249 - Example SMTP Server with forwarding via smtp
This file contains hidden or 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
source :rubygems | |
gem "eventmachine" |
This file contains hidden or 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
#!/usr/bin/env ruby | |
# Example SMTP Server in Ruby for course FI:PV249 | |
# | |
# @author Jiri Zapletal <[email protected]> | |
# @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