commit 09b32d703dd0a5924155082fd7743e536fd8231f
Author: Marc Dequenes <duck@Orfeo.duckcorp.org>
Date:   Sun Feb 22 18:39:47 2009 +0100

    [evol] sign outgoing mails, and lot's of fixes and code improvement in mail handling (related to signing, crypting, and preparing/chaining the two)

diff --git a/lib/cyborghood/mail.rb b/lib/cyborghood/mail.rb
index 22e44c2..4a255af 100644
--- a/lib/cyborghood/mail.rb
+++ b/lib/cyborghood/mail.rb
@@ -45,10 +45,130 @@ module CyborgHood
       {:ok => false, :msg => "mail not RFC3156 compliant"}.to_ostruct
     end
 
+    def create_reply
+      tmail_reply = @mail.create_reply
+      tmail_reply.from_addrs = TMail::Address.parse(@config.mail.from_address || self.to.first)
+      reply = self.class.new(tmail_reply.to_s)
+      reply.set_custom_headers
+      reply
+    end
+
+    def set_custom_headers
+      @mail['Organization'] = @config.mail.organization
+    end
+
+    def check_headers
+      @mail.header.keys.each do |h|
+        @mail[h] = quote_address_if_necessary(@mail[h].to_s, "utf-8")
+      end
+    end
+
+    def deliver
+      check_headers
+
+      smtp_server = @config.mail.smtp_server || "localhost"
+      smtp_port = @config.mail.smtp_port || 25
+      smtp_from = @mail.from_addrs.collect{|a| a.address}.join(", ")
+      smtp_to = @mail.to_addrs.collect{|a| a.address}
+      Net::SMTP.start(smtp_server, smtp_port) do |smtp|
+        #p @mail.to_s
+        smtp.send_message(@mail.to_s, smtp_from, smtp_to)
+      end
+    end
+
+    def to_s
+      @mail.to_s
+    end
+
+    def crypt(fingerprint)
+      clear_data = build_intermediate_mail()
+      encrypted_data = clear_data.pgp_crypt(fingerprint)
+
+      # build properly encrypted mail
+      # (modify original mail parts)
+      @mail.set_content_type("multipart", "encrypted", {'boundary' => TMail.new_boundary, "protocol" => "application/pgp-encrypted"})
+      @mail.transfer_encoding = "7bit"
+      @mail['content-disposition'] = nil
+      @mail.body = "This mail is a RFC3156 encrypted message.\n"
+      @mail.parts.clear
+      p_pgp = TMail::Mail.new
+      p_pgp.set_content_type("application", "pgp-encrypted")
+      p_pgp.transfer_encoding = "7bit"
+      p_pgp.content_disposition = "inline"
+      p_pgp.body = "Version: 1\n"
+      @mail.parts << p_pgp
+      p_encrypted = TMail::Mail.new
+      p_encrypted.set_content_type("application", "octet-stream")
+      p_encrypted.transfer_encoding = "7bit"
+      p_encrypted.content_disposition = "inline"
+      p_encrypted.body = encrypted_data
+      @mail.parts << p_encrypted
+    end
+
+    def sign
+      data = build_intermediate_mail()
+      signature = data.pgp_sign([@config.mail.key_id]) do |uid_hint, passphrase_info, prev_was_bad|
+        @config.mail.key_passphrase
+      end
+
+      original_content_type = @mail.content_type
+      original_content_transfer_encoding = @mail.content_transfer_encoding
+      original_content_disposition = @mail.content_disposition
+
+      # build properly signed mail
+      # (modify original mail parts)
+      @mail.set_content_type("multipart", "signed", {'boundary' => TMail.new_boundary, 'protocol' => "application/pgp-signature"})
+      @mail.transfer_encoding = "7bit"
+      @mail['content-disposition'] = nil
+      @mail.body = "This mail is a RFC3156 signed message.\n"
+      @mail.parts.clear
+      p_signed = data
+      @mail.parts << p_signed
+      p_signature = TMail::Mail.new
+      p_signature.set_content_type("application", "pgp-signature")
+      p_signature.transfer_encoding = "7bit"
+      p_signature.content_disposition = "inline"
+      p_signature.body = signature
+      @mail.parts << p_signature
+    end
+
+    def sign_and_crypt(fingerprint)
+      sign()
+      crypt(fingerprint)
+    end
+
+    def quoted_printable_body=(txt)
+      @mail.transfer_encoding = "quoted-printable"
+      @mail.body = [normalize_new_lines(txt)].pack("M*")
+    end
+
+    private
+
+    def build_intermediate_mail
+      # build a fake mail to get the generated content to be crypted/signed
+      fake_mail = TMail::Mail.new
+      fake_mail['content-type'] = @mail['content-type'].to_s
+      fake_mail.transfer_encoding = @mail.transfer_encoding if @mail.transfer_encoding
+      fake_mail.content_disposition = @mail.content_disposition if @mail.content_disposition
+      if @mail.multipart?
+        @mail.each_part {|p| fake_mail.parts << p }
+      else
+        fake_mail.body = @mail.body
+      end
+
+      # store the calculated content, to be able to use the raw() method
+      fake_mail.write_back
+
+      fake_mail
+    end
+
     def parse_signed
       order = {:ok => false, :msg => "mail not formatted correctly"}
 
-      sig_check = verify_pgp_signature()
+      sigs_check = verify_pgp_signature()
+      return order.to_ostruct if sigs_check.nil? or sigs_check.size != 1
+
+      sig_check = sigs_check.first
       if sig_check.status == 0
         logger.info "Mail content was properly signed by key #{sig_check.fingerprint}"
         user = Person.find_by_fingerprint(sig_check.fingerprint)
@@ -101,7 +221,7 @@ module CyborgHood
       catch :notforme do
         begin
           # block is not passed to delegate (limitation ?)
-          clear_message = @mail.decrypt do |uid_hint, passphrase_info, prev_was_bad|
+          clear_message = @mail.pgp_decrypt do |uid_hint, passphrase_info, prev_was_bad|
             logger.info "Mail crypted for #{uid_hint}"
 
             # check if requesting passphrase for the expected key
@@ -122,81 +242,5 @@ module CyborgHood
 
       order.to_ostruct
     end
-
-    def create_reply
-      tmail_reply = @mail.create_reply
-      tmail_reply.from_addrs = TMail::Address.parse(@config.mail.from_address || self.to.first)
-      reply = self.class.new(tmail_reply.to_s)
-      reply.set_custom_headers
-      reply
-    end
-
-    def set_custom_headers
-      @mail['Organization'] = @config.mail.organization
-    end
-
-    def check_headers
-      @mail.header.keys.each do |h|
-        @mail[h] = quote_address_if_necessary(@mail[h].to_s, "utf-8")
-      end
-    end
-
-    def deliver
-      check_headers
-
-      smtp_server = @config.mail.smtp_server || "localhost"
-      smtp_port = @config.mail.smtp_port || 25
-      smtp_from = @mail.from_addrs.collect{|a| a.address}.join(", ")
-      smtp_to = @mail.to_addrs.collect{|a| a.address}
-      Net::SMTP.start(smtp_server, smtp_port) do |smtp|
-        #p @mail.to_s
-        smtp.send_message(@mail.to_s, smtp_from, smtp_to)
-      end
-    end
-
-    def to_s
-      @mail.to_s
-    end
-
-    def crypt(fingerprint)
-      # build a fake mail to get the generated content to be crypted
-      if @mail.multipart?
-        fake_mail = TMail::Mail.new
-        fake_mail.set_content_type("multipart", "mixed", {'boundary' => TMail.new_boundary})
-        @mail.each_part {|p| fake_mail.parts << p }
-        clear_data = fake_mail.to_s
-      else
-        fake_mail = TMail::Mail.new
-        fake_mail.content_type = @mail.content_type
-        fake_mail.body = @mail.body
-        fake_mail.transfer_encoding = @mail.transfer_encoding if @mail.transfer_encoding
-        clear_data = fake_mail.to_s
-      end
-
-      # retrieve key and encrypt
-      gpg = GPGME::Ctx.new
-      key = gpg.get_key(fingerprint)
-      encrypted_data = GPGME.encrypt([key], clear_data, {:armor => true, :always_trust => true})
-
-      # build properly encrypted mail
-      # (modify original mail parts)
-      @mail.set_content_type("multipart", "encrypted", {'boundary' => TMail.new_boundary, "protocol" => "application/pgp-encrypted"})
-      @mail.transfer_encoding = nil
-      @mail.body = "This mail is a RFC3156 encrypted message."
-      @mail.parts.clear
-      p_pgp = TMail::Mail.new
-      p_pgp.set_content_type("application", "pgp-encrypted")
-      p_pgp.body = "Version: 1"
-      @mail.parts << p_pgp
-      p_encrypted = TMail::Mail.new
-      p_encrypted.set_content_type("application", "octet-stream")
-      p_encrypted.body = encrypted_data
-      @mail.parts << p_encrypted
-    end
-
-    def quoted_printable_body=(txt)
-      @mail.transfer_encoding = "quoted-printable"
-      @mail.body = [normalize_new_lines(txt)].pack("M*")
-    end
   end
 end
diff --git a/lib/tmail_extra.rb b/lib/tmail_extra.rb
index 92eb935..5e886bd 100644
--- a/lib/tmail_extra.rb
+++ b/lib/tmail_extra.rb
@@ -19,17 +19,16 @@ module TMail
     def verify_pgp_signature
       return nil unless is_pgp_signed?
 
-      # using RAW part, without any decoding
-      # remove last EOL due to MIME protocol and properly convert all EOL to CRLF
-      content = parts[0].raw.chomp.gsub(/\r?\n/, "\r\n")
+      content = parts[0].to_rfc3156
       sig = pgp_signature()
 
-      sig_check = nil
+      sigs_check = nil
       GPGME.verify(sig, content) do |signature|
-        sig_check = signature
+        sigs_check ||= []
+        sigs_check << signature
       end
 
-      sig_check
+      sigs_check
     end
 
     def is_pgp_encrypted?
@@ -49,23 +48,46 @@ module TMail
       parts[1].body
     end
 
-    def decrypt(&passphrase_callback)
+    def pgp_decrypt(&passphrase_callback)
       return nil unless is_pgp_encrypted?
       protocol_version = pgp_crypt_info()["Version"].to_i
       raise NotImplementedError, "pgp-encrypted protocol version #{protocol_version} is not implemented" unless protocol_version == 1
 
       encrypted_data = pgp_encrypted_part()
-      passphrase_callback_wrapper = Proc.new do |hook, uid_hint, passphrase_info, prev_was_bad, fd|
-        # sending key
-        io = IO.for_fd(fd, 'w')
-        io.puts hook.call(uid_hint, passphrase_info, prev_was_bad)
-        io.flush
-      end
-      GPGME.decrypt(encrypted_data, {:passphrase_callback => passphrase_callback_wrapper, :passphrase_callback_value => passphrase_callback, :textmode => true})
+      GPGME.decrypt(encrypted_data, {:passphrase_callback => method(:gpg_passphrase_callback_wrapper),
+                      :passphrase_callback_value => passphrase_callback, :textmode => true})
+    end
+
+    def pgp_crypt(fingerprint)
+      GPGME.encrypt([gpg_key(fingerprint)], self.to_s, {:armor => true, :always_trust => true})
+    end
+
+    def pgp_sign(signers_id, &passphrase_callback)
+      signers = signers_id.collect{|key_id| gpg_key(key_id) }
+      GPGME.sign(self.to_rfc3156, {:signers => signers, :passphrase_callback => method(:gpg_passphrase_callback_wrapper),
+                 :passphrase_callback_value => passphrase_callback, :armor => true})
+    end
+
+    def to_rfc3156
+      # using RAW part, without any decoding
+      # remove last EOL due to MIME protocol and properly convert all EOL to CRLF
+      raw.chomp.gsub(/\r?\n/, "\r\n")
     end
 
     protected
 
+    def gpg_key(fingerprint)
+      gpg = GPGME::Ctx.new
+      gpg.get_key(fingerprint)
+    end
+
+    def gpg_passphrase_callback_wrapper(hook, uid_hint, passphrase_info, prev_was_bad, fd)
+      # sending key
+      io = IO.for_fd(fd, 'w')
+      io.puts hook.call(uid_hint, passphrase_info, prev_was_bad)
+      io.flush
+    end
+
     def raw
       @port.read_all
     end
diff --git a/postman b/postman
index 4f59c5b..df4176c 100755
--- a/postman
+++ b/postman
@@ -243,7 +243,10 @@ module CyborgHood
         elsif not order.ok
           logger.info "Sending reply for rejected message (#{order.msg})"
           mail_reply = mail.create_reply
+          mail_reply.set_content_type("text", "plain", {'charset' => "utf-8"})
+          mail_reply.set_disposition("inline")
           mail_reply.quoted_printable_body = "A message (ID: #{mail.message_id}) apparently from you was rejected for the following reason:\n  #{order.msg}"
+          mail_reply.sign
           mail_reply.deliver
           msg.delete
           next
@@ -252,9 +255,8 @@ module CyborgHood
         logger.info "Message accepted, processing orders..."
         result_list = CommandParser.run(order)
 
-        # create reply
-        logger.info "Sending reply"
-        mail_reply = mail.create_reply
+        # create transcript
+        logger.debug "Preparing reply"
         reply_txt = "Hello #{order.user.cn},\n\nFollows the transcript of your commands:\n"
         reply_attachments = []
         result_list.each do |result|
@@ -262,19 +264,18 @@ module CyborgHood
           reply_txt << "#{result.message}\n"
           reply_attachments += result.refs unless result.refs.nil?
         end
+
+        # create mail
+        logger.debug "Preparing mail"
+        mail_reply = mail.create_reply
         if reply_attachments.empty?
-          mail_reply.set_content_type("text", "plain")
-          mail_reply.set_disposition("inline")
-          mail_reply.quoted_printable_body = reply_txt
+          transcript_part = mail_reply
         else
-
           mail_reply.set_content_type("multipart", "mixed", {'boundary' => TMail.new_boundary})
           parts = []
 
           p = CyborgHood::Mail.new
-          p.set_content_type("text", "plain", {'charset' => "utf-8"})
-          p.set_disposition("inline")
-          p.quoted_printable_body = reply_txt
+          transcript_part = p
           mail_reply.parts << p
 
           reply_attachments.each do |attachment|
@@ -285,10 +286,17 @@ module CyborgHood
             mail_reply.parts << p
           end
         end
-        mail_reply.crypt(order.user.keyFingerPrint)
+        # insert transcript
+        transcript_part.set_content_type("text", "plain", {'charset' => 'utf-8', 'format' => 'flowed'})
+        transcript_part.set_disposition("inline")
+        transcript_part.quoted_printable_body = reply_txt
+
+        # send reply
+        logger.debug "Sending mail"
+        mail_reply.sign_and_crypt(order.user.keyFingerPrint)
         mail_reply.deliver
 
-        logger.debug "Message processed completely"
+        logger.info "Message processed completely, deleting"
         msg.delete
       end
     end
