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 SMTP Server - Save To Database

02.19.2008
| 10130 views |
  • submit to reddit
        This was something I've searched for in the past but have yet to find. It's a super simple server and I'm sure there's a few bugs left in it, but the idea is simple and in my opinion, necessary. It's easy to send email from websites, but receiving it still seems dubious.

I've simply used Peter's(http://snippets.dzone.com/user/peter) ultra simplistic SMTP server and routed the email to a database table called "Emails" via ActiveRecord. It's running GServer so it should be able to handle a decent load. Like I mentioned earlier, it's probably still got some bugs, but it's a decent start.

require 'gserver'
require 'rubygems'  
require 'active_record'  
require 'yaml'
   
dbconfig = YAML::load_file(File.dirname(__FILE__) + '/config/database.yml')
ActiveRecord::Base.establish_connection(dbconfig['development']) 

class Email < 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
      return true, ""
    elsif (line =~ /^(HELO|EHLO)/)
      return true, "220 and..?\r\n"
    elsif (line =~ /^QUIT/)
      return false, "bye\r\n"
    elsif (line =~ /^MAIL FROM\:/)
      return true, "220 OK\r\n"
    elsif (line =~ /^RCPT TO\:/)
      return true, "220 OK\r\n"
    elsif (line =~ /^DATA/)
      @data_mode = true
      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)
    mail_from = (/^MAIL FROM\:<(.+)>.*$/).match(email)[1]
    rcpt_to = (/^RCPT TO\:<(.+)>.*$/).match(email)[1]
    subject = (/^Subject\: (.+)$/).match(email)[1]
    Email.create(:mail_from => mail_from, :rcpt_to => rcpt_to, :subject => subject, :email => email)
  end
  
end

a = SMTPServer.new(25)
a.start
a.join
    

Comments

Snippets Manager replied on Mon, 2008/08/11 - 1:08pm

A correction on the pop3d.rb server around line 230. 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

Snippets Manager replied on Mon, 2008/08/11 - 1:08pm

I've corrected a few things and changed the database so that it will work with my pop3d.rb 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 And the pop3d server 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 = 1 while msg = retr(msgid) uidls << "#{msgid} #{Digest::MD5.new.update(msg).hexdigest}" end return true, "+OK\r\n"+uidls.join("\r\n")+"\r\n.\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

Snippets Manager replied on Mon, 2012/05/07 - 2:55pm

This is exactly why I wrote that code in the first place, but I never got round to actually doing the final part of the hookup as you have. Nice job! I might even put this on Ruby Inside soon..!