]> git.cworth.org Git - sup/commitdiff
finally! gpg sign, encrypt, and both support on outgoing email
authorwmorgan <wmorgan@5c8cc53c-5e98-4d25-b20a-d8db53a31250>
Thu, 13 Dec 2007 23:06:39 +0000 (23:06 +0000)
committerwmorgan <wmorgan@5c8cc53c-5e98-4d25-b20a-d8db53a31250>
Thu, 13 Dec 2007 23:06:39 +0000 (23:06 +0000)
git-svn-id: svn://rubyforge.org/var/svn/sup/trunk@768 5c8cc53c-5e98-4d25-b20a-d8db53a31250

lib/sup.rb
lib/sup/crypto.rb
lib/sup/index.rb
lib/sup/message.rb
lib/sup/modes/edit-message-mode.rb
lib/sup/util.rb

index 60679ee882757c517eaa1223ae1bc824f1b70e80..25809ddbe760bfb5ca2648713251bea77c528748 100644 (file)
@@ -204,6 +204,7 @@ else
     :ask_for_subject => true,
     :confirm_no_attachments => true,
     :confirm_top_posting => true,
+    :discard_snippets_from_encrypted_messages => false,
   }
   begin
     FileUtils.mkdir_p Redwood::BASE_DIR
index 5db576b900915edccdd3e552d30e448b65c4fe2f..0f9cdfd40f60a3507bee9cb01af4913f35fe31bd 100644 (file)
@@ -3,6 +3,8 @@ module Redwood
 class CryptoManager
   include Singleton
 
+  class Error < StandardError; end
+
   OUTGOING_MESSAGE_OPERATIONS = OrderedHash.new(
     [:sign, "Sign"],
     [:sign_and_encrypt, "Sign and encrypt"],
@@ -14,7 +16,6 @@ class CryptoManager
     self.class.i_am_the_instance self
 
     bin = `which gpg`.chomp
-    bin = `which pgp`.chomp unless bin =~ /\S/
 
     @cmd =
       case bin
@@ -29,25 +30,71 @@ class CryptoManager
 
   def have_crypto?; !@cmd.nil? end
 
+  def sign from, to, payload
+    payload_fn = Tempfile.new "redwood.payload"
+    payload_fn.write format_payload(payload)
+    payload_fn.close
+
+    output = run_gpg "--output - --armor --detach-sign --textmode --local-user '#{from}' #{payload_fn.path}"
+
+    raise Error, (output || "gpg command failed: #{cmd}") unless $?.success?
+
+    envelope = RMail::Message.new
+    envelope.header["Content-Type"] = 'multipart/signed; protocol="application/pgp-signature"; micalg=pgp-sha1'
+
+    envelope.add_part payload
+    signature = RMail::Message.make_attachment output, "application/pgp-signature", nil, "signature.asc"
+    envelope.add_part signature
+    envelope
+  end
+
+  def encrypt from, to, payload, sign=false
+    payload_fn = Tempfile.new "redwood.payload"
+    payload_fn.write format_payload(payload)
+    payload_fn.close
+
+    recipient_opts = to.map { |r| "--recipient '#{r}'" }.join(" ")
+    sign_opts = sign ? "--sign --local-user '#{from}'" : ""
+    gpg_output = run_gpg "--output - --armor --encrypt --textmode #{sign_opts} #{recipient_opts} #{payload_fn.path}"
+    raise Error, (gpg_output || "gpg command failed: #{cmd}") unless $?.success?
+
+    encrypted_payload = RMail::Message.new
+    encrypted_payload.header["Content-Type"] = "application/octet-stream"
+    encrypted_payload.header["Content-Disposition"] = 'inline; filename="msg.asc"'
+    encrypted_payload.body = gpg_output
+
+    control = RMail::Message.new
+    control.header["Content-Type"] = "application/pgp-encrypted"
+    control.header["Content-Disposition"] = "attachment"
+    control.body = "Version: 1\n"
+
+    envelope = RMail::Message.new
+    envelope.header["Content-Type"] = 'multipart/encrypted; protocol="application/pgp-encrypted"'
+
+    envelope.add_part control
+    envelope.add_part encrypted_payload
+    envelope
+  end
+
+  def sign_and_encrypt from, to, payload
+    encrypt from, to, payload, true
+  end
+
   def verify payload, signature # both RubyMail::Message objects
     return unknown_status(cant_find_binary) unless @cmd
 
     payload_fn = Tempfile.new "redwood.payload"
-    payload_fn.write payload.to_s.gsub(/(^|[^\r])\n/, "\\1\r\n").gsub(/^MIME-Version: .*\r\n/, "")
+    payload_fn.write format_payload(payload)
     payload_fn.close
 
     signature_fn = Tempfile.new "redwood.signature"
     signature_fn.write signature.decode
     signature_fn.close
 
-    cmd = "#{@cmd} --verify #{signature_fn.path} #{payload_fn.path} 2> /dev/null"
-
-    #Redwood::log "gpg: running: #{cmd}"
-    gpg_output = `#{cmd}`
-    #Redwood::log "got output: #{gpg_output.inspect}"
-    output_lines = gpg_output.split(/\n/)
+    output = run_gpg "--verify #{signature_fn.path} #{payload_fn.path}"
+    output_lines = output.split(/\n/)
 
-    if gpg_output =~ /^gpg: (.* signature from .*$)/
+    if output =~ /^gpg: (.* signature from .*$)/
       if $? == 0
         Chunk::CryptoNotice.new :valid, $1, output_lines
       else
@@ -58,35 +105,22 @@ class CryptoManager
     end
   end
 
-  # returns decrypted_message, status, desc, lines
-  def decrypt payload # RubyMail::Message objects
+  ## returns decrypted_message, status, desc, lines
+  def decrypt payload # a RubyMail::Message object
     return unknown_status(cant_find_binary) unless @cmd
 
-#    cmd = "#{@cmd} --decrypt 2> /dev/null"
-
-#    Redwood::log "gpg: running: #{cmd}"
-
-#    gpg_output =
-#      IO.popen(cmd, "a+") do |f|
-#        f.puts payload.to_s
-#        f.gets
-#      end
-
     payload_fn = Tempfile.new "redwood.payload"
     payload_fn.write payload.to_s
     payload_fn.close
 
-    cmd = "#{@cmd} --decrypt #{payload_fn.path} 2> /dev/null"
-    Redwood::log "gpg: running: #{cmd}"
-    gpg_output = `#{cmd}`
-    Redwood::log "got output: #{gpg_output.inspect}"
+    output = run_gpg "--decrypt #{payload_fn.path}"
 
-    if $? == 0 # successful decryption
+    if $?.success?
       decrypted_payload, sig_lines =
-        if gpg_output =~ /\A(.*?)((^gpg: .*$)+)\Z/m
+        if output =~ /\A(.*?)((^gpg: .*$)+)\Z/m
           [$1, $2]
         else
-          [gpg_output, nil]
+          [output, nil]
         end
       
       sig = 
@@ -101,7 +135,7 @@ class CryptoManager
       notice = Chunk::CryptoNotice.new :valid, "This message has been decrypted for display"
       [RMail::Parser.read(decrypted_payload), sig, notice]
     else
-      notice = Chunk::CryptoNotice.new :invalid, "This message could not be decrypted", gpg_output.split("\n")
+      notice = Chunk::CryptoNotice.new :invalid, "This message could not be decrypted", output.split("\n")
       [nil, nil, notice]
     end
   end
@@ -113,7 +147,19 @@ private
   end
   
   def cant_find_binary
-    ["Can't find gpg or pgp binary in path"]
+    ["Can't find gpg binary in path."]
+  end
+
+  def format_payload payload
+    payload.to_s.gsub(/(^|[^\r])\n/, "\\1\r\n").gsub(/^MIME-Version: .*\r\n/, "")
+  end
+
+  def run_gpg args
+    cmd = "#{@cmd} #{args} 2> /dev/null"
+    Redwood::log "crypto: running: #{cmd}"
+    output = `#{cmd}`
+    Redwood::log "crypto: output: #{output.inspect}" unless $?.success?
+    output
   end
 end
 end
index 30a42717fd7d075110886638120ffe3737c285c4..fd46495e8706866419559d10e6d6a3a87464e521 100644 (file)
@@ -171,13 +171,20 @@ EOS
       end
 
     to = (m.to + m.cc + m.bcc).map { |x| x.email }.join(" ")
+    snippet = 
+      if m.snippet_contains_encrypted_content? && $config[:discard_snippets_from_encrypted_messages]
+        ""
+      else
+        m.snippet
+      end
+
     d = {
       :message_id => m.id,
       :source_id => source_id,
       :source_info => m.source_info,
       :date => m.date.to_indexable_s,
       :body => m.content,
-      :snippet => m.snippet,
+      :snippet => snippet,
       :label => m.labels.uniq.join(" "),
       :from => m.from ? m.from.email : "",
       :to => (m.to + m.cc + m.bcc).map { |x| x.email }.join(" "),
index fbea1f66a960cde892516d8eb3912dcd5fcc3d3d..4d9e04f7becdb75cbc99c3e0b6cbe5e7b4d33eba 100644 (file)
@@ -41,17 +41,19 @@ class Message
               :cc, :bcc, :labels, :list_address, :recipient_email, :replyto,
               :source_info, :chunks, :list_subscribe, :list_unsubscribe
 
-  bool_reader :dirty, :source_marked_read
+  bool_reader :dirty, :source_marked_read, :snippet_contains_encrypted_content
 
   ## if you specify a :header, will use values from that. otherwise,
   ## will try and load the header from the source.
   def initialize opts
     @source = opts[:source] or raise ArgumentError, "source can't be nil"
     @source_info = opts[:source_info] or raise ArgumentError, "source_info can't be nil"
-    @snippet = opts[:snippet] || ""
-    @have_snippet = !opts[:snippet].nil?
+    @snippet = opts[:snippet]
+    @snippet_contains_encrypted_content = false
+    @have_snippet = !(opts[:snippet].nil? || opts[:snippet].empty?)
     @labels = [] + (opts[:labels] || [])
     @dirty = false
+    @encrypted = false
     @chunks = nil
 
     parse_header(opts[:header] || @source.load_header(@source_info))
@@ -116,7 +118,7 @@ class Message
   end
   private :parse_header
 
-  def snippet; @snippet || chunks && @snippet; end
+  def snippet; @snippet || (chunks && @snippet); end
   def is_list_message?; !@list_address.nil?; end
   def is_draft?; @source.is_a? DraftLoader; end
   def draft_filename
@@ -301,7 +303,6 @@ private
   end
 
   def multipart_encrypted_to_chunks m
-    Redwood::log ">> multipart ENCRYPTED: #{m.header['Content-Type']}: #{m.body.size}"
     if m.body.size != 2
       Redwood::log "warning: multipart/encrypted with #{m.body.size} parts (expecting 2)"
       return
@@ -324,11 +325,11 @@ private
     end
 
     decryptedm, sig, notice = CryptoManager.decrypt payload
-    children = message_to_chunks(decryptedm) if decryptedm
+    children = message_to_chunks(decryptedm, true) if decryptedm
     [notice, sig, children].flatten.compact
   end
 
-  def message_to_chunks m, sibling_types=[]
+  def message_to_chunks m, encrypted=false, sibling_types=[]
     if m.multipart?
       chunks =
         case m.header.content_type
@@ -340,7 +341,7 @@ private
 
       unless chunks
         sibling_types = m.body.map { |p| p.header.content_type }
-        chunks = m.body.map { |p| message_to_chunks p, sibling_types }.flatten.compact
+        chunks = m.body.map { |p| message_to_chunks p, encrypted, sibling_types }.flatten.compact
       end
 
       chunks
@@ -378,7 +379,7 @@ private
       ## otherwise, it's body text
       else
         body = Message.convert_from m.decode, m.charset
-        text_to_chunks((body || "").normalize_whitespace.split("\n"))
+        text_to_chunks (body || "").normalize_whitespace.split("\n"), encrypted
       end
     end
   end
@@ -398,7 +399,7 @@ private
   ## parse the lines of text into chunk objects.  the heuristics here
   ## need tweaking in some nice manner. TODO: move these heuristics
   ## into the classes themselves.
-  def text_to_chunks lines
+  def text_to_chunks lines, encrypted
     state = :text # one of :text, :quote, or :sig
     chunks = []
     chunk_lines = []
@@ -450,11 +451,14 @@ private
       when :block_quote, :sig
         chunk_lines << line
       end
+
       if !@have_snippet && state == :text && (@snippet.nil? || @snippet.length < SNIPPET_LEN) && line !~ /[=\*#_-]{3,}/ && line !~ /^\s*$/
+        @snippet ||= ""
         @snippet += " " unless @snippet.empty?
         @snippet += line.gsub(/^\s+/, "").gsub(/[\r\n]/, "").gsub(/\s+/, " ")
         @snippet = @snippet[0 ... SNIPPET_LEN].chomp
+        @dirty = true unless encrypted && $config[:discard_snippets_from_encrypted_messages]
+        @snippet_contains_encrypted_content = true if encrypted
       end
     end
 
index fa3479022603d2b67ea3a22fda8ed6adaca2fe68..3df3176f924ac0b0ccf525adc41b6ce2acdcd059 100644 (file)
@@ -257,7 +257,6 @@ protected
     return false if $config[:confirm_no_attachments] && mentions_attachments? && @attachments.size == 0 && !BufferManager.ask_yes_or_no("You haven't added any attachments. Really send?")#" stupid ruby-mode
     return false if $config[:confirm_top_posting] && top_posting? && !BufferManager.ask_yes_or_no("You're top-posting. That makes you a bad person. Really send?") #" stupid ruby-mode
 
-    date = Time.now
     from_email = 
       if @header["From"] =~ /<?(\S+@(\S+?))>?$/
         $1
@@ -269,13 +268,15 @@ protected
     BufferManager.flash "Sending..."
 
     begin
-      IO.popen(acct.sendmail, "w") { |p| write_full_message_to p, date, false }
+      date = Time.now
+      m = build_message date
+      IO.popen(acct.sendmail, "w") { |p| p.puts m }
       raise SendmailCommandFailed, "Couldn't execute #{acct.sendmail}" unless $? == 0
-      SentManager.write_sent_message(date, from_email) { |f| write_full_message_to f, date, true }
+      SentManager.write_sent_message(date, from_email) { |f| f.puts sanitize_body(m.to_s) }
       BufferManager.kill_buffer buffer
       BufferManager.flash "Message sent!"
       true
-    rescue SystemCallError, SendmailCommandFailed => e
+    rescue SystemCallError, SendmailCommandFailed, CryptoManager::Error => e
       Redwood::log "Problem sending mail: #{e.message}"
       BufferManager.flash "Problem sending mail: #{e.message}"
       false
@@ -288,8 +289,32 @@ protected
     BufferManager.flash "Saved for later editing."
   end
 
-  def write_full_message_to f, date=Time.now, escape=false
+  def build_message date
     m = RMail::Message.new
+    m.header["Content-Type"] = "text/plain; charset=#{$encoding}"
+    m.body = @body.join
+    m.body = m.body
+    m.body += sig_lines.join("\n") unless $config[:edit_signature]
+
+    ## there are attachments, so wrap body in an attachment of its own
+    unless @attachments.empty?
+      body_m = m
+      body_m.header["Content-Disposition"] = "inline"
+      m = RMail::Message.new
+      
+      m.add_part body_m
+      @attachments.each { |a| m.add_part a }
+    end
+
+    ## do whatever crypto transformation is necessary
+    if @crypto_selector && @crypto_selector.val != :none
+      from_email = PersonManager.person_for(@header["From"]).email
+      to_email = (@header["To"] + @header["Cc"] + @header["Bcc"]).map { |p| PersonManager.person_for(p).email }
+
+      m = CryptoManager.send @crypto_selector.val, from_email, to_email, m
+    end
+
+    ## finally, set the top-level headers
     @header.each do |k, v|
       next if v.nil? || v.empty?
       m.header[k] = 
@@ -300,28 +325,10 @@ protected
           v.join ", "
         end
     end
-
     m.header["Date"] = date.rfc2822
     m.header["Message-Id"] = @message_id
     m.header["User-Agent"] = "Sup/#{Redwood::VERSION}"
-
-    if @attachments.empty?
-      m.header["Content-Type"] = "text/plain; charset=#{$encoding}"
-      m.body = @body.join
-      m.body = sanitize_body m.body if escape
-      m.body += sig_lines.join("\n") unless $config[:edit_signature]
-    else
-      body_m = RMail::Message.new
-      body_m.body = @body.join
-      body_m.body = sanitize_body body_m.body if escape
-      body_m.body += sig_lines.join("\n") unless $config[:edit_signature]
-      body_m.header["Content-Type"] = "text/plain; charset=#{$encoding}"
-      body_m.header["Content-Disposition"] = "inline"
-      
-      m.add_part body_m
-      @attachments.each { |a| m.add_part a }
-    end
-    f.puts m.to_s
+    m
   end
 
   ## TODO: remove this. redundant with write_full_message_to.
index bd150b484da89dfb238efe0e2dd9920846cced62..2164e858c7721daa43e7eadc50a8cc3e9c69320b 100644 (file)
@@ -70,17 +70,17 @@ module RMail
       a = Message.new
       a.header.add "Content-Disposition", "attachment; filename=#{filename.inspect}"
       a.header.add "Content-Type", "#{mime_type}; name=#{filename.inspect}"
-      a.header.add "Content-Transfer-Encoding", encoding
+      a.header.add "Content-Transfer-Encoding", encoding if encoding
       a.body =
         case encoding
         when "base64"
           [payload].pack "m"
         when "quoted-printable"
           [payload].pack "M"
-        when "7bit", "8bit"
+        when "7bit", "8bit", nil
           payload
         else
-          raise EncodingUnsupportedError, t.encoding
+          raise EncodingUnsupportedError, encoding.inspect
         end
       a
     end