commit 55a687122adc0daae0f5a49cca2f6abdb4e81085
Author: Marc Dequenes <duck@Orfeo.duckcorp.org>
Date:   Mon Feb 16 01:44:20 2009 +0100

    [evol] added postman bot (not fully cyborgified yet)

diff --git a/postman b/postman
new file mode 100755
index 0000000..c33f212
--- /dev/null
+++ b/postman
@@ -0,0 +1,225 @@
+#!/usr/bin/ruby -Ku
+
+# http://www.ruby-doc.org/stdlib/libdoc/net/imap/rdoc/index.html
+# http://tmail.rubyforge.org/reference/index.html
+# http://tools.ietf.org/html/rfc3156
+
+$: << "./lib"
+
+$KCODE = 'UTF8'
+require 'jcode'
+require 'log4r'
+require 'net/imap'
+require 'tmail'
+#require 'socket'
+#require 'fileutils'
+#require 'tempfile'
+require 'gpgme'
+require 'active_ldap'
+require 'shellwords'
+require 'cyborghood/base'
+
+logger = Log4r::Logger.new('test')
+logger.outputters = Log4r::StderrOutputter.new('')
+logger.level = Log4r::WARN
+#logger.level = Log4r::DEBUG
+
+class Person < ActiveLdap::Base
+  ldap_mapping :dn_attribute => 'uid', :prefix => '', :classes => ['person', 'extInetOrgPerson']
+end
+
+class DnsDomain < ActiveLdap::Base
+  ldap_mapping :dn_attribute => 'cn', :prefix => '', :classes => ['genericDomain']
+
+  def managers
+    list = self.manager
+    return [] if list.nil?
+    return list.collect{|dn| dn.to_s } if list.is_a? Array
+    return [list.to_s]
+  end
+end
+
+ldap_config = {
+  :host => 'localhost',
+  :port => 389,
+  :base => 'dc=milkypond,dc=org',
+  :bind_dn => 'cn=srv-auth,dc=milkypond,dc=org',
+  :password => 'teckLetsoaj5',
+  :logger => logger,
+  :allow_anonymous => false,
+  :try_sasl => false,
+  #:method => :tls,
+  :scope => :sub
+}
+
+ActiveLdap::Base.establish_connection(ldap_config)
+
+#Socket.gethostname
+
+# needed to verify _untouched_ signed MIME part
+module TMail
+  class Mail
+    def raw
+      @port.read_all
+    end
+  end
+end
+
+#
+# TODO:
+#   - should be able to handle encrypted messages for user to send sensitive data (postman would need a GPG key too)
+#
+
+class CommandParser
+  def self.run(user, txt)
+    txt.each_line do |line|
+      line.chomp!
+      sline = line.strip
+      # skip empty lines and comments
+      next if sline == "" or sline[0, 1] == "#"
+      # stop processing when detecting message signature
+      break if line == "-- "
+
+      logger.info "### Executing command: #{sline}"
+      execute_cmd(user, sline)
+    end
+  end
+
+  private
+
+  def self.execute_cmd(user, cmdstr)
+    cmdline = Shellwords.shellwords(cmdstr)
+    subsys = cmdline.shift
+
+    ok = true
+    case subsys.upcase
+    when "DNS"
+      case cmdline.shift.upcase
+      when "INFO"
+	if cmdline.empty?
+          list = DnsDomain.find(:all, :attribute => 'manager', :value => user.dn)
+	  logger.info "### User is manager of the following zones: " + list.collect{|z| z.cn }.join(", ")
+	else
+	  ok = false
+	end
+      when "GET"
+        case cmdline.shift.upcase
+        when "ZONE"
+          zone = cmdline.shift.downcase
+          if zone =~ /^[a-z0-9.-]+\.[a-z]{2,4}$/
+	    logger.info "### User requesting zone content for '#{zone}'"
+	    begin
+	      domain = DnsDomain.find(zone)
+	      if domain.managers.include? user.dn
+	        logger.info "### User is manager of the zone"
+	      else
+		logger.info "### User is not allowed to manage the zone"
+	      end
+	    rescue
+	      logger.info "### Zone not managed"
+	    end
+	  else
+            logger.info "### Invalid zone name specified (#{zone})"
+	  end
+	else
+	  ok = false
+	end
+      when "SET"
+      else
+        ok = false
+      end
+    else
+      ok = false
+    end
+
+    if not ok
+      logger.info "### Command not recognized: #{cmdstr}"
+    end
+  end
+end
+
+module CyborgHood
+  # not yet ready to be a real Cyborg
+  class Postman #< Cyborg
+    def initialize
+      # load config
+      Config.load(self.human_name.downcase)
+      @config = Config.instance
+
+      # setup logs
+      unless @config.log.nil?
+        logger.output_level(@config.log.console_level) unless @config.log.console_level.nil?
+        logger.log_to_file(@config.log.file) unless @config.log.file.nil?
+      end
+
+      logger.info "Bot '#{self.human_name}' loaded"
+    end
+
+    def run
+      # using SSL because TLS does not work in the NET::IMAP library
+      #imap = Net::IMAP.new('imap.duckcorp.org', 993, true, "/etc/ssl/certs/duckcorp.crt", true)
+      imap = Net::IMAP.new('localhost')
+      logger.debug "Connected to IMAP server"
+      logger.debug imap.capability()
+      imap.authenticate('LOGIN', @config.imap.login, @config.imap.passwd)
+      logger.debug "Logged into IMAP account"
+      #p imap.getquotaroot("INBOX")
+      imap.select('INBOX')
+      imap.search(["ALL"], "UTF-8").each do |message_id|
+        msg = imap.fetch(message_id, "RFC822")[0].attr["RFC822"]
+        # unquote headers and transform into TMail object
+        mail = TMail::Mail.parse(TMail::Unquoter.unquote_and_convert_to(msg, "UTF-8"))
+
+	logger.set_prefix()
+        logger.debug "######################################"
+	logger.set_prefix("[#{mail.message_id}] ")
+        logger.info "#{mail.from_addrs} -> #{mail.to_addrs}: #{mail.subject}"
+        # ignore mails not signed
+	unless mail.content_type == "multipart/signed" and mail.parts.size == 2 and mail.parts[1].content_type == "application/pgp-signature"
+	  logger.info "Mail not signed or not RFC3156 compliant"
+          next
+	end
+
+        content = mail.parts[0].raw.chomp.gsub(/\r?\n/, "\r\n")
+        decoded_content = mail.parts[0].body
+        sig = mail.parts[1].decoded
+
+        logger.debug "Proper signed content detected"
+        GPGME::verify(sig, content) do |signature|
+          if signature.status == 0
+            logger.info "Mail content was properly signed by key #{signature.fingerprint}"
+      	list = Person.find(:all, :attribute => 'keyFingerPrint', :value => signature.fingerprint)
+      	case list.size
+      	when 0
+      	  logger.info "Mail is from an unknow person"
+      	when 1
+      	  user = list.first
+      	  logger.info "Mail is from user #{user.uid} (#{user.cn})"
+      	  CommandParser.run(user, decoded_content)
+      	else
+      	  logger.warn "Multiple users match in database, so i guess there is a mistake. It is safer to skip..."
+      	end
+          else
+            logger.info "Mail content tampered or badly signed: " + signature.to_s
+          end
+        end
+      end
+      imap.logout
+    end
+
+    def ask_to_stop
+    end
+  end
+end
+
+bot = CyborgHood::Postman.new
+
+trap('INT') do
+  bot.ask_to_stop
+end
+trap('TERM') do
+  bot.ask_to_stop
+end
+
+bot.run
+
