require 'pathname'
require 'rmail'
+# from jcode.rb, not included in ruby 1.9
+PATTERN_UTF8 = '[\xc0-\xdf][\x80-\xbf]|[\xe0-\xef][\x80-\xbf][\x80-\xbf]'
+RE_UTF8 = Regexp.new(PATTERN_UTF8, 0, 'n')
+
module Redwood
class SendmailCommandFailed < StandardError; end
FORCE_HEADERS = %w(From To Cc Bcc Subject)
MULTI_HEADERS = %w(To Cc Bcc)
- NON_EDITABLE_HEADERS = %w(Message-Id Date)
+ NON_EDITABLE_HEADERS = %w(Message-id Date)
HookManager.register "signature", <<EOS
Generates a message signature.
from_email: the email part of the From: line, or nil if empty
Return value:
A string (multi-line ok) containing the text of the signature, or nil to
- use the default signature.
+ use the default signature, or :none for no signature.
EOS
HookManager.register "before-edit", <<EOS
@file = Tempfile.new "sup.#{self.class.name.gsub(/.*::/, '').camel_to_hyphy}"
@file.puts format_headers(@header - NON_EDITABLE_HEADERS).first
@file.puts
- @file.puts @body
+ @file.puts @body.join("\n")
@file.close
editor = $config[:editor] || ENV['EDITOR'] || "/usr/bin/vi"
!edited? || BufferManager.ask_yes_or_no("Discard message?")
end
+ def unsaved?; edited? end
+
def attach_file
fn = BufferManager.ask_for_filename :attachment, "File name (enter for browser): "
return unless fn
protected
+ def mime_encode string
+ string = [string].pack('M') # basic quoted-printable
+ string.gsub!(/=\n/,'') # .. remove trailing newline
+ string.gsub!(/_/,'=96') # .. encode underscores
+ string.gsub!(/\?/,'=3F') # .. encode question marks
+ string.gsub!(/ /,'_') # .. translate space to underscores
+ "=?utf-8?q?#{string}?="
+ end
+
+ def mime_encode_subject string
+ return string unless string.match(String::RE_UTF8)
+ mime_encode string
+ end
+
+ RE_ADDRESS = /(.+)( <.*@.*>)/
+
+ # Encode "bælammet mitt <user@example.com>" into
+ # "=?utf-8?q?b=C3=A6lammet_mitt?= <user@example.com>
+ def mime_encode_address string
+ return string unless string.match(String::RE_UTF8)
+ string.sub(RE_ADDRESS) { |match| mime_encode($1) + $2 }
+ end
+
def move_cursor_left
if curpos < @selectors.length
@selectors[curpos].roll_left
def parse_file fn
File.open(fn) do |f|
- header = MBox::read_header f
- body = f.readlines
+ header = Source.parse_raw_email_header(f).inject({}) { |h, (k, v)| h[k.capitalize] = v; h } # lousy HACK
+ body = f.readlines.map { |l| l.chomp }
header.delete_if { |k, v| NON_EDITABLE_HEADERS.member? k }
header.each { |k, v| header[k] = parse_header k, v }
if i == 0
header + " " + name
else
- (" " * (header.length + 1)) + name
+ (" " * (header.display_length + 1)) + name
end + (i == things.length - 1 ? "" : ",")
end
end
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 = @body.join("\n")
m.body += sig_lines.join("\n") unless $config[:edit_signature]
+ ## body must end in a newline or GPG signatures will be WRONG!
+ m.body += "\n" unless m.body =~ /\n\Z/
## there are attachments, so wrap body in an attachment of its own
unless @attachments.empty?
## 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 }
+ from_email = Person.from_address(@header["From"]).email
+ to_email = [@header["To"], @header["Cc"], @header["Bcc"]].flatten.compact.map { |p| Person.from_address(p).email }
m = CryptoManager.send @crypto_selector.val, from_email, to_email, m
end
m.header[k] =
case v
when String
- v
+ k.match(/subject/i) ? mime_encode_subject(v) : mime_encode_address(v)
when Array
- v.join ", "
+ v.map { |v| mime_encode_address v }.join ", "
end
end
+
m.header["Date"] = date.rfc2822
m.header["Message-Id"] = @message_id
m.header["User-Agent"] = "Sup/#{Redwood::VERSION}"
+ m.header["Content-Transfer-Encoding"] = '8bit'
m
end
end
f.puts
- f.puts sanitize_body(@body.join)
+ f.puts sanitize_body(@body.join("\n"))
f.puts sig_lines if full unless $config[:edit_signature]
end
if text
@header[field] = parse_header field, text
update
- field
end
else
- default =
- case field
+ default = case field
when *MULTI_HEADERS
+ @header[field] ||= []
@header[field].join(", ")
else
@header[field]
contacts = BufferManager.ask_for_contacts :people, "#{field}: ", default
if contacts
- text = contacts.map { |s| s.longname }.join(", ")
+ text = contacts.map { |s| s.full_address }.join(", ")
@header[field] = parse_header field, text
update
- field
end
end
end
end
def top_posting?
- @body.join =~ /(\S+)\s*Excerpts from.*\n(>.*\n)+\s*\Z/
+ @body.join("\n") =~ /(\S+)\s*Excerpts from.*\n(>.*\n)+\s*\Z/
end
def sig_lines
- p = PersonManager.person_for(@header["From"])
+ p = Person.from_address(@header["From"])
from_email = p && p.email
## first run the hook
hook_sig = HookManager.run "signature", :header => @header, :from_email => from_email
+
+ return [] if hook_sig == :none
return ["", "-- "] + hook_sig.split("\n") if hook_sig
## no hook, do default signature generation based on config.yaml