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

Snippets has posted 5883 posts at DZone. View Full User Profile

Ruby POP3 Server With Database Support

08.12.2008
| 4421 views |
  • submit to reddit
        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