Skip to content

Instantly share code, notes, and snippets.

@epitron
Created January 4, 2009 03:29
Show Gist options
  • Save epitron/42996 to your computer and use it in GitHub Desktop.
Save epitron/42996 to your computer and use it in GitHub Desktop.
A 77 line SMTP server, and a 253 line POP3 server.
require 'gserver'
require 'rubygems'
require 'active_record'
dbconfig = YAML::load_file(File.dirname(__FILE__) + '/config/database.yml')
ActiveRecord::Base.establish_connection(dbconfig['development'])
# CREATE TABLE emails (id integer primary key autoincrement, mail_from, rcpt_to, subject, email, user_id integer);
# CREATE TABLE users (id integer primary key autoincrement, username, password, email);
class Email < ActiveRecord::Base
end
class User < ActiveRecord::Base
end
class SMTPServer < GServer
def serve(io)
@data_mode = false
@email_message = ""
puts "Connected"
io.print "220 hello\r\n"
loop do
if IO.select([io], nil, nil, 0.1)
data = io.readpartial(4096)
puts ">>" + data
@email_message << data
ok, op = process_line(data)
break unless ok
puts "<<" + op
io.print op
end
break if io.closed?
end
db_insert(@email_message)
io.print "221 bye\r\n"
io.close
end
def process_line(line)
if (@data_mode) && (line.chomp =~ /^\.$/)
@data_mode = false
return true, "220 OK\r\n"
elsif @data_mode
@email_body += line
return true, ""
elsif (line =~ /^(HELO|EHLO)/)
return true, "220 and..?\r\n"
elsif (line =~ /^QUIT/)
return false, "bye\r\n"
elsif (line =~ /^MAIL FROM\:/)
@mail_from = (/^MAIL FROM\:<(.+)>.*$/).match(line)[1]
return true, "220 OK\r\n"
elsif (line =~ /^RCPT TO\:/)
@rcpt_to = (/^RCPT TO\:<(.+)>.*$/).match(line)[1]
return true, "220 OK\r\n"
elsif (line =~ /^DATA/)
@data_mode = true
@email_body = ''
return true, "354 Enter message, ending with \".\" on a line by itself\r\n"
else
return true, "500 ERROR\r\n"
end
end
def db_insert(email)
subject = (/^Subject\: (.+)$/).match(@email_body)[1]
u = User.find(:first, :conditions => { :email => @rcpt_to })
if u and @mail_from and @rcpt_to
Email.create(:mail_from => @mail_from, :rcpt_to => @rcpt_to, :subject => subject, :email => @email_body, :user_id => u.id)
end
end
end
a = SMTPServer.new(2225)
a.start
a.join
require 'gserver'
require 'digest/md5'
require 'rubygems'
require 'active_record'
dbconfig = YAML::load_file(File.dirname(__FILE__) + '/config/database.yml')
ActiveRecord::Base.establish_connection(dbconfig['development'])
# CREATE TABLE emails (id integer primary key autoincrement, mail_from, rcpt_to, subject, email, user_id integer);
# CREATE TABLE users (id integer primary key autoincrement, username, password, email);
class Email < ActiveRecord::Base
def initialize(*kvars)
@deleted = false
super(kvars)
end
def deleted?
@deleted
end
def deleted=(deleted)
@deleted = deleted
end
end
class User < ActiveRecord::Base
end
class POP3Server < GServer
attr_writer :hostname
def serve(io)
@state = 'auth'
@failed = 0
@apop_challenge = "<#{rand(10**4 - 1)}.#{rand(10**9 - 1)}@#{@hostname}>"
io.print("+OK POP3 server ready #{@apop_challenge}\r\n")
loop do
if IO.select([io], nil, nil, 0.1)
begin
data = io.readpartial(4096)
puts ">> #{data.chomp}"
ok, op = process_line(data)
break unless ok
puts "<< #{op.chomp}"
io.print op
rescue Exception
end
end
break if io.closed?
end
io.close unless io.closed?
end
def user(username)
@user = User.find(:first, :conditions => { :username => username })
end
def pass(password)
return false unless @user
return false unless @user.password == password
true
end
def emails
@emails = [ Email.find_all_by_user_id(@user.id) ].flatten
end
def stat
msgs = bytes = 0
@emails.each do |e|
next if e.deleted?
msgs += 1
bytes += e.email.length
end
return msgs, bytes
end
def list(msgid = nil)
msgid = msgid.to_i if msgid
if msgid
return false if msgid > @emails.length or @emails[msgid-1].deleted?
return [ [msgid, @emails[msgid].email.length] ]
else
msgs = []
@emails.each_with_index do |e,i|
msgs << [ i + 1, e.email.length ]
end
msgs
end
end
def retr(msgid)
msgid = msgid.to_i
return false if msgid > @emails.length or @emails[msgid-1].deleted?
@emails[msgid-1].email
end
def dele(msgid)
msgid = msgid.to_i
return false if msgid > @emails.length
@emails[msgid-1].deleted = true
end
def rset
@emails.each do |e|
e.deleted = false
end
end
def quit
@emails.each do |e|
if e.deleted?
Email.delete(e.id)
end
end
end
def apop(username, hash)
user(username)
return false unless @user
if Digest::MD5.new.update("#{@apop_challenge}#{@user.password}").hexdigest == hash
return true
end
false
end
def process_line(line)
line.chomp!
case @state
when 'auth'
case line
when /^QUIT$/
return false, "+OK dewey POP3 server signing off\r\n"
when /^USER (.+)$/
user($1)
if @user
return true, "+OK #{@user.username} is most welcome here\r\n"
else
@failed += 1
if @failed > 2
return false, "-ERR you're out!\r\n"
end
return true, "-ERR sorry, no mailbox for #{$1} here\r\n"
end
when /^PASS (.+)$/
if pass($1)
@state = 'trans'
emails
msgs, bytes = stat
return true, "+OK #{@user.username}'s maildrop has #{msgs} messages (#{bytes} octets)\r\n"
else
@failed += 1
if @failed > 2
return false, "-ERR you're out!\r\n"
end
return true, "-ERR no dope.\r\n"
end
when /^APOP ([^\s]+) (.+)$/
if apop($1,$2)
@state = 'trans'
emails
return true, "+OK #{@user.username} is most welcome here\r\n"
else
@failed += 1
if @failed > 2
return false, "-ERR you're out!\r\n"
end
return true, "-ERR sorry, no mailbox for #{$1} here\r\n"
end
end
when 'trans'
case line
when /^NOOP$/
return true, "+OK\r\n"
when /^STAT$/
msgs, bytes = stat
return true, "+OK #{msgs} #{bytes}\r\n"
when /^LIST$/
msgs, bytes = stat
msg = "+OK #{msgs} messages (#{bytes} octets)\r\n"
list.each do |num, bytes|
msg += "#{num} #{bytes}\r\n"
end
msg += ".\r\n"
return true, msg
when /^LIST (\d+)$/
msgs, bytes = stat
num, bytes = list($1)
if num
return true, "+OK #{num} #{bytes}\r\n"
else
return true, "-ERR no such message, only #{msgs} messages in maildrop\r\n"
end
when /^RETR (\d+)$/
msg = retr($1)
if msg
msg = "+OK #{msg.length} octets\r\n" + msg
msg += "\r\n.\r\n"
else
msg = "-ERR no such message\r\n"
end
return true, msg
when /^DELE (\d+)$/
if dele($1)
return true, "+OK message #{$1} deleted\r\n"
else
return true, "-ERR message #{$1} already deleted\r\n"
end
when /^RSET$/
rset
msgs, bytes = stat
return true, "+OK maildrop has #{msgs} messages (#{bytes} octets)\r\n"
when /^QUIT$/
@state = 'update'
quit
msgs, bytes = stat
if msgs > 0
return true, "+OK dewey POP3 server signing off (#{msgs} messages left)\r\n"
else
return true, "+OK dewey POP3 server signing off (maildrop empty)\r\n"
end
when /^TOP (\d+) (\d+)$/
lines = $2
msg = retr($1)
unless msg
return true, "-ERR no such message\r\n"
end
cnt = nil
final = ""
msg.split(/\n/).each do |l|
final += l+"\n"
if cnt
cnt += 1
break if cnt > lines
end
if l !~ /\w/
cnt = 0
end
end
return true, "+OK\r\n"+final+".\r\n"
when /^UIDL$/
msgid = 0
msg = ''
@email.each do |e|
msgid += 1
next if e.deleted?
msg += "#{msgid} #{Digest::MD5.new.update(msg).hexdigest}\r\n"
end
return true, "+OK\r\n#{msg}.\r\n";
end
when 'update'
case line
when /^QUIT$/
return true, "+OK dewey POP3 server signing off\r\n"
end
end
return true, "-ERR unknown command\r\n"
end
end
a = POP3Server.new(2226,'',4,$stderr,true,true)
a.hostname = "localhost"
a.start
a.join
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment