commit ad54b91564dc7be18136a794d0edfe62ab600b8d
Author: Marc Dequènes (Duck) <Duck@DuckCorp.org>
Date:   Mon Feb 25 00:59:38 2013 +0100

    [evol] ported the GPG extension from TMail to Mail (CyborgHood::Mail needs to be adapted now)

diff --git a/lib/mail/gpg.rb b/lib/mail/gpg.rb
new file mode 100644
index 0000000..b6c73bd
--- /dev/null
+++ b/lib/mail/gpg.rb
@@ -0,0 +1,296 @@
+#--
+# Copyright (c) 2013 Marc Dequènes (Duck) <Duck@DuckCorp.org>
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+#++
+
+
+# Attempt to handle PGP/GPG features in a RFC3156-compliant way
+#
+# notes: These methods have been designed to be able to sign, crypt,
+#        or sign+crypt with RFC 1847 Encapsulation. There is no
+#        support (yet?) for the combined method described in chapter
+#        6.2 of RFC3156.
+module Mail
+  module GpgExtension
+    def is_pgp_signed?
+      mime_type == "multipart/signed" and
+        content_type_parameters['protocol'] == "application/pgp-signature" and
+        parts.size == 2 and
+        pgp_signature_part
+    end
+
+    def pgp_signature_part
+      parts.detect { |p| p.mime_type == "application/pgp-signature" && !p.attachment? }
+    end
+
+    def pgp_signed_part
+      return nil unless is_pgp_signed?
+
+      p = parts.detect { |p| p.mime_type != "application/pgp-signature" && !p.attachment? }
+    end
+
+    def verify_pgp_signature
+      return nil unless is_pgp_signed?
+
+      # use untouched data, or verification will fail
+      content = pgp_signed_part.raw_source
+      # RFC3156: properly convert all EOL to CRLF
+      content.to_crlf
+      # XXX: ruby-mail strangely adds a CRLF at the begining of the part
+      content.sub!(/^\r\n/m, "")
+
+      sig = pgp_signature_part.decoded
+
+      sigs_check = nil
+      crypto = GPGME::Crypto.new
+      crypto.verify(sig, :signed_text => content) do |signature|
+        sigs_check ||= []
+        sigs_check << signature
+      end
+
+      sigs_check
+    end
+
+    def is_pgp_encrypted?
+      mime_type == "multipart/encrypted" and
+        content_type_parameters['protocol'] == "application/pgp-encrypted" and
+        parts.size == 2 and
+        pgp_crypt_info_part and
+        pgp_encrypted_part
+    end
+
+    def pgp_crypt_info_part
+      parts.detect { |p| p.mime_type == "application/pgp-encrypted" && !p.attachment? }
+    end
+
+    def pgp_crypt_info
+      return nil unless is_pgp_encrypted?
+
+      a = pgp_crypt_info_part.body.decoded.split("\n").collect do |l|
+        l.chomp.split(": ") if l =~ /: /
+      end.compact.flatten
+
+      Hash[*a]
+    end
+
+    def pgp_encrypted_part
+      parts.detect { |p| p.mime_type == "application/octet-stream" && !p.attachment? }
+    end
+
+    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
+
+      clear_data = nil
+      GPGME::Ctx.new(:textmode => true) do |gpg|
+        gpg.set_passphrase_callback(method(:gpg_passphrase_callback_wrapper), passphrase_callback)
+        clear_data = gpg.decrypt(GPGME::Data.new(pgp_encrypted_part.body.decoded))
+      end
+
+      clear_data.seek(0, IO::SEEK_SET)
+      clear_data.read
+    end
+
+    def pgp_crypt(crypters_id)
+      crypters_id = [crypters_id] unless crypters_id.is_a? Array
+      crypters = crypters_id.collect{|key_id| gpg_key(key_id, false) }
+
+      encrypted_data = nil
+      GPGME::Ctx.new(:armor => true) do |gpg|
+        encrypted_data = gpg.encrypt(crypters, GPGME::Data.new(self.encoded))
+      end
+
+      encrypted_data.seek(0, IO::SEEK_SET)
+      encrypted_data.read
+    end
+
+    def pgp_sign(signers_id, &passphrase_callback)
+      signers_id = [signers_id] unless signers_id.is_a? Array
+      signers = signers_id.collect{|key_id| gpg_key(key_id, true) }
+
+      # RFC3156:
+      #   - remove lines containing only whitespaces
+      #   - remove trailing whitespaces
+      #   - properly convert all EOL to CRLF
+      prepared_data = self.encoded.split(/\r?\n/, -1).select do |l|
+        not l =~ /^ +$/
+      end.join("\n").gsub(/ +$/, "").to_crlf
+      raise 'Mail ERROR: data to sign MUST be in 7bit format, using Quoted-Printable or Base64 encoding !' unless
+        prepared_data.ascii_only?
+
+      sig_data = GPGME::Data.new
+      micalg = nil
+
+      # we don't use GPGME::Crypto.sign(), because we need to get operation information to get
+      # the hash_algo and compute the micalg parameter
+      GPGME::Ctx.new(:armor => true) do |gpg|
+        gpg.add_signer(*signers)
+        gpg.set_passphrase_callback(method(:gpg_passphrase_callback_wrapper), passphrase_callback)
+
+        gpg.sign(GPGME::Data.new(prepared_data), sig_data, GPGME::SIG_MODE_DETACH)
+
+        hash_algo = gpg.sign_result.signatures.first.hash_algo
+        micalg = "pgp-" + GPGME.gpgme_hash_algo_name(hash_algo).downcase
+
+        # rewind
+        sig_data.seek(0, IO::SEEK_SET)
+      end
+
+      {:signature => sig_data.read, :micalg => micalg}
+    end
+
+    def create_encrypted(crypters_id)
+      # build properly encrypted mail
+      # (preserving headers from original mail)
+      mail = Mail::Message.new
+      mail.copy_headers_from(self)
+
+      # cryptographic info
+      p_pgp = Mail::Part.new
+      p_pgp.content_type = "application/pgp-encrypted"
+      p_pgp.content_transfer_encoding = "7bit"
+      p_pgp.content_disposition = "inline"
+      p_pgp.body = "Version: 1\n"
+      mail.add_part(p_pgp)
+
+      encrypted_data = self.convert_to_part.pgp_crypt(crypters_id)
+
+      # encrypted message
+      p_encrypted = Mail::Part.new
+      p_encrypted.content_type = "application/octet-stream"
+      p_encrypted.content_transfer_encoding = "7bit"
+      p_encryptedcontent_disposition = "inline"
+      p_encrypted.body = encrypted_data
+      mail.add_part(p_encrypted)
+
+      # XXX: there is no way to have a body while being multipart in ruby-mail
+      #mail.body = "This mail is a RFC3156 signed message.\n"
+      # XXX: setup after mail has switched to multipart or settings are overwritten by ruby-mail
+      # save previous content-type parameters, as they are lost when chaging the content-type
+      prev_params = mail.content_type_parameters
+      mail.content_type = "multipart/encrypted"
+      prev_params.each_pair do |k, v|
+        mail.content_type_parameters[k] = v
+      end
+      mail.content_type_parameters['protocol'] = "application/pgp-encrypted"
+      mail.content_transfer_encoding = "7bit"
+      mail.content_disposition = nil
+
+      mail
+    end
+
+    def create_signed(signers_id)
+      # build properly signed mail
+      # (preserving headers from original mail)
+      mail = Mail::Message.new
+      mail.copy_headers_from(self)
+
+      # signed message
+      p_signed = self.convert_to_part
+      mail.add_part(p_signed)
+
+      signature_data = p_signed.pgp_sign(signers_id) do |uid_hint, passphrase_info, prev_was_bad|
+        yield(uid_hint, passphrase_info, prev_was_bad)
+      end
+
+      # signature
+      p_signature = Mail::Message.new
+      p_signature.content_type = "application/pgp-signature"
+      p_signature.content_transfer_encoding = "7bit"
+      p_signature.content_disposition = "inline"
+      p_signature.body = signature_data[:signature]
+      mail.add_part(p_signature)
+
+      # XXX: there is no way to have a body while being multipart in ruby-mail
+      #mail.body = "This mail is a RFC3156 signed message.\n"
+      # XXX: setup after mail has switched to multipart or settings are overwritten by ruby-mail
+      # save previous content-type parameters, as they are lost when chaging the content-type
+      prev_params = mail.content_type_parameters
+      mail.content_type = "multipart/signed"
+      prev_params.each_pair do |k, v|
+        mail.content_type_parameters[k] = v
+      end
+      mail.content_type_parameters['protocol'] = "application/pgp-signature"
+      mail.content_type_parameters['micalg'] = signature_data[:micalg]
+      mail.content_transfer_encoding = "7bit"
+      mail.content_disposition = nil
+
+      mail
+    end
+
+
+    def create_signed_and_encrypted(signers_id, crypters_id)
+      create_signed(signers_id) do |uid_hint, passphrase_info, prev_was_bad|
+        yield(uid_hint, passphrase_info, prev_was_bad)
+      end.create_encrypted(crypters_id)
+    end
+
+    def copy_headers_from(mail)
+      # deep clone
+      @header = Marshal.load(Marshal.dump(mail.header))
+    end
+
+    def has_content_disposition?
+      header[:content_disposition] && header[:content_disposition].errors.blank?
+    end
+
+    def convert_to_part
+      # build a part from a mail to get the generated content to be crypted/signed
+      p = Mail::Part.new
+      p.content_type = self.content_type
+      p.content_transfer_encoding = self.content_transfer_encoding if has_content_transfer_encoding?
+      p.content_disposition = self.content_disposition if has_content_disposition?
+      if multipart?
+        parts.each {|orig_p| p.add_part(orig_p) }
+      else
+        p.body = self.body.encoded
+      end
+
+      p
+    end
+
+    protected
+
+    def gpg_key(fingerprint, secret = false)
+      GPGME::Ctx.new do |gpg|
+        gpg.get_key(fingerprint, secret)
+      end
+    rescue EOFError
+      raise "Mail ERROR: #{secret ? "secret" : "public"} key not found (#{fingerprint})"
+    end
+
+    def gpg_passphrase_callback_wrapper(hook, uid_hint, passphrase_info, prev_was_bad, fd)
+      io = IO.for_fd(fd, 'w')
+      io.puts hook.call(uid_hint, passphrase_info, prev_was_bad)
+      io.flush
+    end
+  end
+end
+
+begin
+  require 'gpgme'
+  Mail::Message.send(:include, Mail::GpgExtension)
+rescue LoadError
+end
diff --git a/lib/tmail_gpg.rb b/lib/tmail_gpg.rb
deleted file mode 100644
index 63db189..0000000
--- a/lib/tmail_gpg.rb
+++ /dev/null
@@ -1,275 +0,0 @@
-#--
-# Copyright (c) 2009-2010 Marc Dequènes (Duck) <Duck@DuckCorp.org>
-#
-# Permission is hereby granted, free of charge, to any person obtaining
-# a copy of this software and associated documentation files (the
-# "Software"), to deal in the Software without restriction, including
-# without limitation the rights to use, copy, modify, merge, publish,
-# distribute, sublicense, and/or sell copies of the Software, and to
-# permit persons to whom the Software is furnished to do so, subject to
-# the following conditions:
-#
-# The above copyright notice and this permission notice shall be
-# included in all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-#
-#++
-
-class String
-  def is_7bit?
-    each_byte{|c| return false if c >= 128 }
-    true
-  end
-end
-
-# Attempt to handle PGP/GPG features in a RFC3156-compliant way
-#
-# notes: These methods have been designed to be able to sign, crypt,
-#        or sign+crypt with RFC 1847 Encapsulation. There is no
-#        support (yet?) for the combined method described in chapter
-#        6.2 of RFC3156.
-module TMail
-  module GpgExtension
-    def is_pgp_signed?
-      content_type == "multipart/signed" and
-        header['content-type'].params['protocol'] == "application/pgp-signature" and
-        parts.size == 2 and
-        parts[1].content_type == "application/pgp-signature"
-    end
-
-    def pgp_signature_part
-      return nil unless is_pgp_signed?
-      parts[1].decoded
-    end
-
-    def pgp_signature
-      return nil unless is_pgp_signed?
-      parts[1].body
-    end
-
-    # don't forget lines may not be normalized
-    def pgp_signed_part
-      return nil unless is_pgp_signed?
-      # remove final CRLF added to respect OpenPGP convention
-      # (see note in chapter 5 of RFC3156)
-      signed_part = parts[0].raw.chomp
-      return TMail::Mail.parse(signed_part)
-    end
-
-    def verify_pgp_signature
-      return nil unless is_pgp_signed?
-
-      content = parts[0].to_rfc3156(true)
-      sig = pgp_signature()
-
-      sigs_check = nil
-      gpg = GPGME::Ctx.new
-      gpg.verify(GPGME::Data.new(sig), GPGME::Data.new(content)) do |signature|
-        sigs_check ||= []
-        sigs_check << signature
-      end
-      gpg.release
-
-      sigs_check
-    end
-
-    def is_pgp_encrypted?
-      content_type == "multipart/encrypted" and
-        header['content-type'].params['protocol'] == "application/pgp-encrypted" and
-        parts.size == 2 and
-        parts[0].content_type == "application/pgp-encrypted" and
-        parts[1].content_type == "application/octet-stream"
-    end
-
-    def pgp_crypt_info
-      return nil unless is_pgp_encrypted?
-      a = parts[0].body.split("\n").collect{|l| l.chomp.split(": ") if l =~ /: / }.compact.flatten
-      Hash[*a]
-    end
-
-    def pgp_encrypted_part
-      return nil unless is_pgp_encrypted?
-      parts[1].body
-    end
-
-    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()
-      gpg = GPGME::Ctx.new({:passphrase_callback => method(:gpg_passphrase_callback_wrapper),
-                             :passphrase_callback_value => passphrase_callback, :textmode => true})
-      gpg.decrypt(GPGME::Data.new(encrypted_data))
-      gpg.release
-    end
-
-    def pgp_crypt(crypters_id)
-      crypters_id = [crypters_id] unless crypters_id.is_a? Array
-      crypters = crypters_id.collect{|key_id| gpg_key(key_id, false) }
-      gpg = GPGME::Ctx.new({:armor => true})
-      gpg.encrypt(crypters, GPGME::Data.new(self.to_s))
-      gpg.release
-    end
-
-    def pgp_sign(signers_id, &passphrase_callback)
-      signers_id = [signers_id] unless signers_id.is_a? Array
-      signers = signers_id.collect{|key_id| gpg_key(key_id, true) }
-      # we don't use GPGME::Crypto.sign(), because we need to get operation information to get the hash_algo and compute the micalg parameter
-      gpg = GPGME::Ctx.new({:signers => signers, :passphrase_callback => method(:gpg_passphrase_callback_wrapper),
-                             :passphrase_callback_value => passphrase_callback, :armor => true})
-      gpg.add_signer(*signers)
-      sig_data = GPGME::Data.new
-      # adds a final CRLF to respect OpenPGP convention (see note in chapter 5 of RFC3156)
-      prepared_data = self.to_rfc3156 + "\r\n"
-      raise 'TMail ERROR: data to sign MUST be in 7bit format, using Quotes-Printable or Base64 encoding !' unless prepared_data.is_7bit?
-      gpg.sign(GPGME::Data.new(prepared_data), GPGME::Data.new(sig_data), GPGME::SIG_MODE_DETACH)
-      hash_algo = GPGME.gpgme_op_sign_result(gpg).signatures.first.hash_algo
-      micalg = "pgp-" + GPGME.gpgme_hash_algo_name(hash_algo).downcase
-      sig_data.seek(0, IO::SEEK_SET)
-      gpg.release
-
-      {:signature => sig_data.read, :micalg => micalg}
-    end
-
-    def to_rfc3156(untouched = false)
-      # - using RAW part, without any decoding
-      # - remove last EOL due to MIME protocol
-      # - remove lines containing only whitespaces
-      # - remove trailing whitespaces
-      # - properly convert all EOL to CRLF
-      txt = untouched ? self.raw : self.to_s
-      txt.chomp.split(/\r?\n/, -1).select{|l| not l =~ /^ +$/}.join("\n").gsub(/ +$/, "").gsub(/\r?\n/, "\r\n")
-    end
-
-    def create_encrypted(crypters_id)
-      clear_data = build_intermediate_mail()
-      encrypted_data = clear_data.pgp_crypt(crypters_id)
-
-      # build properly encrypted mail
-      # (preserving headers from original mail)
-      mail = TMail::Mail.new
-      mail.copy_headers_from(self)
-      mail.set_content_type("multipart", "encrypted", {'boundary' => TMail.new_boundary, "protocol" => "application/pgp-encrypted"})
-      mail.transfer_encoding = "7bit"
-      mail['content-disposition'] = nil
-      mail.parts.clear
-      mail.quoted_body = "This mail is a RFC3156 encrypted message.\n"
-
-      # cryptographic info
-      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
-
-      # encrypted message
-      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
-
-      # store the calculated content, to be able to use parsing methods
-      mail.write_back
-
-      mail
-    end
-
-    def create_signed(signers_id)
-      data = build_intermediate_mail()
-      sign_data = data.pgp_sign(signers_id) do |uid_hint, passphrase_info, prev_was_bad|
-        yield(uid_hint, passphrase_info, prev_was_bad)
-      end
-
-      # build properly signed mail
-      # (preserving headers from original mail)
-      mail = TMail::Mail.new
-      mail.copy_headers_from(self)
-      mail.set_content_type("multipart", "signed", {'boundary' => TMail.new_boundary, 'protocol' => "application/pgp-signature", 'micalg' => sign_data[:micalg]})
-      mail.transfer_encoding = "7bit"
-      mail['content-disposition'] = nil
-      mail.parts.clear
-      mail.quoted_body = "This mail is a RFC3156 signed message.\n"
-
-      # signed message
-      p_signed = data
-      mail.parts << p_signed
-
-      # signature
-      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 = sign_data[:signature]
-      mail.parts << p_signature
-
-      # store the calculated content, to be able to use parsing methods
-      mail.write_back
-
-      mail
-    end
-
-    def create_signed_and_encrypted(signers_id, crypters_id)
-      create_signed(signers_id) do |uid_hint, passphrase_info, prev_was_bad|
-        yield(uid_hint, passphrase_info, prev_was_bad)
-      end.create_encrypted(crypters_id)
-    end
-
-    def copy_headers_from(mail)
-      # deep clone
-      @header = Marshal.load(Marshal.dump(mail.header))
-    end
-
-    protected
-
-    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'] = self['content-type'].to_s
-      fake_mail.transfer_encoding = self.transfer_encoding if self.transfer_encoding
-      fake_mail.content_disposition = self.content_disposition if self.content_disposition
-      if self.multipart?
-        self.each_part {|p| fake_mail.parts << p }
-      end
-      fake_mail.quoted_body = self.quoted_body
-
-      # store the calculated content, to be able to use parsing methods
-      fake_mail.write_back
-
-      fake_mail
-    end
-
-    def gpg_key(fingerprint, secret = false)
-      gpg = GPGME::Ctx.new
-      gpg.get_key(fingerprint, secret)
-    rescue EOFError
-      raise "TMail ERROR: #{secret ? "secret" : "public"} key not found (#{fingerprint})"
-    end
-
-    def gpg_passphrase_callback_wrapper(hook, uid_hint, passphrase_info, prev_was_bad, fd)
-      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
-  end
-end
-
-begin
-  require 'gpgme'
-  TMail::Mail.send(:include, TMail::GpgExtension)
-rescue LoadError
-end
