commit 3ef5e2e01079b557432ad57cabadee36a02b9cf4
parent 46d86b4694dd20d79dc8b18a867b3059beb3cbc7
Author: wmorgan <wmorgan@5c8cc53c-5e98-4d25-b20a-d8db53a31250>
Date: Thu, 13 Dec 2007 23:06:39 +0000
finally! gpg sign, encrypt, and both support on outgoing email
git-svn-id: svn://rubyforge.org/var/svn/sup/trunk@768 5c8cc53c-5e98-4d25-b20a-d8db53a31250
Diffstat:
6 files changed, 134 insertions(+), 69 deletions(-)
diff --git a/lib/sup.rb b/lib/sup.rb
@@ -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
diff --git a/lib/sup/crypto.rb b/lib/sup/crypto.rb
@@ -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
diff --git a/lib/sup/index.rb b/lib/sup/index.rb
@@ -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(" "),
diff --git a/lib/sup/message.rb b/lib/sup/message.rb
@@ -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
diff --git a/lib/sup/modes/edit-message-mode.rb b/lib/sup/modes/edit-message-mode.rb
@@ -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.
diff --git a/lib/sup/util.rb b/lib/sup/util.rb
@@ -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