: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
class CryptoManager
include Singleton
+ class Error < StandardError; end
+
OUTGOING_MESSAGE_OPERATIONS = OrderedHash.new(
[:sign, "Sign"],
[:sign_and_encrypt, "Sign and encrypt"],
self.class.i_am_the_instance self
bin = `which gpg`.chomp
- bin = `which pgp`.chomp unless bin =~ /\S/
@cmd =
case bin
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
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 =
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
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
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(" "),
: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))
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
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
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
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
## 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
## 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 = []
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
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
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
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] =
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.
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