DZone Snippets is a public source code repository. Easily build up your personal collection of code snippets, categorize them with tags / keywords, and share them with the world
Ruby POP3 Server With Database Support
I needed to set up a simple SMTP + POP3 combination for testing. I used markpercival's SMTP + DB implementation [2], a derivative of peter's Simple SMTP [3], as a base to build a POP3 server. I tried to implement the functions found in the POP3 RFC [1], although I don't claim any level of compliance to it. :) It's enough to work with net/pop3. Please list any bugs and fixes in the comments.
[1] http://www.ietf.org/rfc/rfc1939.txt
[2] http://snippets.dzone.com/posts/show/5152 (markpercival)
[3] http://snippets.dzone.com/posts/show/3932 (peter)
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)
a.hostname = "localhost"
a.start
a.join





