#! /usr/local/bin/ruby require 'time' require 'mailscanner' require 'smtpforward' TRANS = { :_begin => [ :_begin, :helo, :ehlo, :quit ], :ehlo => [ :xforward, :mail_from, :quit ], :xforward => [ :mail_from, :quit ], :helo => [ :mail_from, :quit ], :mail_from => [ :rcpt_to, :quit ], :rcpt_to => [ :rcpt_to, :data, :quit ], :data => [ :_end ], :quit => [ ], :_end => [ :ehlo ] } class SMTPFilter attr_accessor :client, :pstate, :state, :line, :continue, :saybye, :body, :xf attr_accessor :from, :to, :helo, :esmtp attr_accessor :proxy, :scanner def initialize(client) @client = client @pstate = :_begin @state = :_begin @esmtp = false @line = '' @continue = true @saybye = [ 221, "2.0.0 #{$apphost} #{$appname} Goodbye." ] @body = [] @from = '' @to = [] @xf = {} @helo = '' while(@continue) puts "State #{@pstate.to_s} => #{@state.to_s}" if(TRANS[@pstate].include? @state) then send(@state) @pstate = @state else error() end waitinput() if(@continue) end respond(*@saybye) if(@saybye.length > 0) puts "xforward: " + @xf.keys.collect { |k| k + "=>" + @xf[k] }.join(',') puts @from + " => [ " + @to.join(',') + " ] (" + @body.length.to_s + " lines)" puts @body.join("\n") if($DEBUG) end def respond(code, lines) c = code.to_s if(lines.respond_to? :to_ary) then l = lines.to_ary else l = [ lines.to_s ] end begin while(l.length > 1) p = c + '-' + l.shift puts '>>> ' + p if($ldebug) @client.print p + "\r\n" end p = c + ' ' + l.shift puts '>>> ' + p if($ldebug) @client.print p + "\r\n" rescue Exception => e puts "ERROR : client write error #{e}" @line = nil @continue = false @saybye = '' end end def error if(TRANS.has_key? @state) then respond(503, '5.5.1 Command out of sequence') else respond(502, '5.5.2 Command not recognized') end end def waitinput while(true) begin @line = client.gets rescue Exception => e puts "ERROR : client read error #{e}" @line = nil @continue = false @saybye = '' end if(@line.nil?) then return end @line.chomp! puts '<<< ' + @line if($ldebug) if(@line.length > 1024) then respond(501, '5.1.7 Line too long') else if(@state == :data) then if(@line == '.') then @state = :_end break end @line.gsub!(/^\.\./, '.') @body << @line else s = @line.sub(/ /, '_').downcase @state = TRANS.keys.find { |k| s =~ /^#{k.to_s}(\b|_)/ } @state = :nil if(@state.nil?) break end end end end def _begin() respond(220, "#{$apphost} ESMTP #{$appname} Hello !") end def helo @line.scan(/^helo +(.*)$/i) { |v| @helo = v } @esmtp = false respond(250, "#{$apphost}") end def ehlo @line.scan(/^ehlo +(.*)$/i) { |v| @helo = v } @esmtp = true respond(250, [ "#{$apphost}", 'PIPELINING', 'ENHANCEDSTATUSCODES', '8BITMIME', 'XFORWARD NAME ADDR PROTO HELO' ]) # 'SIZE', end def xforward # XFORWARD NAME=talisker.lacave.net ADDR=217.145.39.3 HELO=talisker.lacave.net PROTO=SMTP @line.scan(/([A-Z]+)=(\S+)/i) { |k, v| @xf[k] = v } respond(250, [ '2.5.0 Ok XFORWARD' ]) end def mail_from @line.scan(/^mail from\s*:?\s*?\s*(?:\s+SIZE\s*=\s*\d+)?$/i) { |t| @from = $1 } if(@from) then respond(250, "2.1.0 Sender #{@from} OK") else respond(550, '5.1.1 Invalid syntax') @state = @pstate end end def rcpt_to to = '' @line.scan(/^rcpt to\s*:?\s*?\s*$/i) { |v| to = v } if(to != '') then @scanner = MailScanner.new(self) if(@scanner.nil?) if((r = @scanner.validate_recipient(to)) == '') then if(@proxy.nil?) then @proxy = SMTPForward.new(self) @proxy.begin end r = @proxy.rcpt_to(to) if(r[0] / 100 == 2) then @to.push(to) respond(250, "2.1.5 Recipient #{to} OK") else respond(r[0], r[1]) end else respond(550, "5.1.1 #{to} rejected your address : #{r}") end else respond(550, '5.1.1 Invalid syntax') @state = @pstate end end def data addr = @client.peeraddr #puts "Client connecting from #{addr[2]} <#{addr[3]}> on port #{addr[1]}" respond(354, 'End data with .') @body << "Received: from #{addr[2]}" + (addr[3].downcase != @helo ? " (HELO #{@helo})" : "") + " [#{addr[3]}]" @body << "\tby #{$apphost} (#{$appid}) with #{(@esmtp?'ESMTP':'SMTP')};" @body << "\t" + Time.now.rfc2822 puts "||| " + @body.join("\n||| ") + "\n" if($ldebug) end def quit @continue = false end def _end @scanner.contenttag respond(*@proxy.data) #respond(250, '2.6.0 Ok, id=123456-01') @state = :ehlo end end