X-Git-Url: https://git.cworth.org/git?a=blobdiff_plain;ds=sidebyside;f=lib%2Fsup%2Fmodes%2Fedit-message-mode.rb;h=353e76ac137644d9a1389705d997630501a2a6ab;hb=f7daa71b08043a5cfad38ddcc259f6ff80445129;hp=67af505c9b9d8be4001bd297a0bc6cd8f2a985dc;hpb=ec676e80318752143dfed16c725ab87f56b71f94;p=sup diff --git a/lib/sup/modes/edit-message-mode.rb b/lib/sup/modes/edit-message-mode.rb index 67af505..353e76a 100644 --- a/lib/sup/modes/edit-message-mode.rb +++ b/lib/sup/modes/edit-message-mode.rb @@ -1,46 +1,128 @@ require 'tempfile' require 'socket' # just for gethostname! +require 'pathname' +require 'rmail' module Redwood +class SendmailCommandFailed < StandardError; end + class EditMessageMode < LineCursorMode + DECORATION_LINES = 1 + FORCE_HEADERS = %w(From To Cc Bcc Subject) MULTI_HEADERS = %w(To Cc Bcc) NON_EDITABLE_HEADERS = %w(Message-Id Date) + HookManager.register "signature", < @header, :body => @body + super opts - update + regen_text end - def lines; @text.length end - def [] i; @text[i] end + def lines; @text.length + (@selectors.empty? ? 0 : (@selectors.length + DECORATION_LINES)) end + + def [] i + if @selectors.empty? + @text[i] + elsif i < @selectors.length + @selectors[i].line @selector_label_width + elsif i == @selectors.length + "" + else + @text[i - @selectors.length - DECORATION_LINES] + end + end - ## a hook + ## hook for subclasses. i hate this style of programming. def handle_new_text header, body; end - def edit + def edit_message_or_field + lines = DECORATION_LINES + @selectors.size + if lines > curpos + return + elsif (curpos - lines) >= @header_lines.length + edit_message + else + edit_field @header_lines[curpos - lines] + end + end + + def edit_to; edit_field "To" end + def edit_cc; edit_field "Cc" end + def edit_subject; edit_field "Subject" end + + def edit_message @file = Tempfile.new "sup.#{self.class.name.gsub(/.*::/, '').camel_to_hyphy}" - @file.puts header_lines(@header - NON_EDITABLE_HEADERS) + @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" @@ -49,52 +131,117 @@ class EditMessageMode < LineCursorMode BufferManager.shell_out "#{editor} #{@file.path}" @edited = true if File.mtime(@file.path) > mtime + return @edited unless @edited + header, @body = parse_file @file.path @header = header - NON_EDITABLE_HEADERS handle_new_text @header, @body update + + @edited end def killable? !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 + begin + @attachments << RMail::Message.make_file_attachment(fn) + @attachment_names << fn + update + rescue SystemCallError => e + BufferManager.flash "Can't read #{fn}: #{e.message}" + end + end + + def delete_attachment + i = curpos - @attachment_lines_offset - DECORATION_LINES - 1 + if i >= 0 && i < @attachments.size && BufferManager.ask_yes_or_no("Delete attachment #{@attachment_names[i]}?") + @attachments.delete_at i + @attachment_names.delete_at i + update + end + end + protected + def move_cursor_left + if curpos < @selectors.length + @selectors[curpos].roll_left + buffer.mark_dirty + else + col_left + end + end + + def move_cursor_right + if curpos < @selectors.length + @selectors[curpos].roll_right + buffer.mark_dirty + else + col_right + end + end + + def add_selector s + @selectors << s + @selector_label_width = [@selector_label_width, s.label.length].max + end + def update regen_text buffer.mark_dirty if buffer end def regen_text - @text = header_lines(@header - NON_EDITABLE_HEADERS) + [""] + @body + header, @header_lines = format_headers(@header - NON_EDITABLE_HEADERS) + [""] + @text = header + [""] + @body @text += sig_lines unless $config[:edit_signature] + + @attachment_lines_offset = 0 + + unless @attachments.empty? + @text += [""] + @attachment_lines_offset = @text.length + @text += (0 ... @attachments.size).map { |i| [[:attachment_color, "+ Attachment: #{@attachment_names[i]} (#{@attachments[i].body.size.to_human_size})"]] } + end end def parse_file fn File.open(fn) do |f| header = MBox::read_header f - body = f.readlines + body = f.readlines.map { |l| l.chomp } header.delete_if { |k, v| NON_EDITABLE_HEADERS.member? k } - header.each do |k, v| - next unless MULTI_HEADERS.include?(k) && !v.empty? - header[k] = v.split_on_commas.map do |name| - (p = ContactManager.person_with(name)) && p.full_address || name - end - end + header.each { |k, v| header[k] = parse_header k, v } [header, body] end end - def header_lines header - force_headers = FORCE_HEADERS.map { |h| make_lines "#{h}:", header[h] } - other_headers = (header.keys - FORCE_HEADERS).map do |h| - make_lines "#{h}:", header[h] + def parse_header k, v + if MULTI_HEADERS.include?(k) + v.split_on_commas.map do |name| + (p = ContactManager.contact_for(name)) && p.full_address || name + end + else + v end + end - (force_headers + other_headers).flatten.compact + def format_headers header + header_lines = [] + headers = (FORCE_HEADERS + (header.keys - FORCE_HEADERS)).map do |h| + lines = make_lines "#{h}:", header[h] + lines.length.times { header_lines << h } + lines + end.flatten.compact + [headers, header_lines] end def make_lines header, things @@ -120,9 +267,10 @@ protected end def send_message - return unless edited? || BufferManager.ask_yes_or_no("Message unedited. Really send?") + return false if !edited? && !BufferManager.ask_yes_or_no("Message unedited. Really send?") + 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"] =~ /?$/ $1 @@ -134,16 +282,18 @@ protected BufferManager.flash "Sending..." begin - IO.popen(acct.sendmail, "w") { |p| write_message p, true, date } - rescue SystemCallError - end - if $? == 0 - SentManager.write_sent_message(date, from_email) { |f| write_message f, true, date } + 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| f.puts sanitize_body(m.to_s) } BufferManager.kill_buffer buffer BufferManager.flash "Message sent!" - else - Redwood::log "Non-zero return value in running sendmail command for #{acct.longname}: #{acct.sendmail.inspect}" - BufferManager.flash "Problem sending mail. See log for details." + true + rescue SystemCallError, SendmailCommandFailed, CryptoManager::Error => e + Redwood::log "Problem sending mail: #{e.message}" + BufferManager.flash "Problem sending mail: #{e.message}" + false end end @@ -153,11 +303,56 @@ protected BufferManager.flash "Saved for later editing." end + def build_message date + m = RMail::Message.new + m.header["Content-Type"] = "text/plain; charset=#{$encoding}" + 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? + 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 = 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 + + ## finally, set the top-level headers + @header.each do |k, v| + next if v.nil? || v.empty? + m.header[k] = + case v + when String + v + when Array + v.join ", " + end + end + m.header["Date"] = date.rfc2822 + m.header["Message-Id"] = @message_id + m.header["User-Agent"] = "Sup/#{Redwood::VERSION}" + m + end + + ## TODO: remove this. redundant with write_full_message_to. + ## ## this is going to change soon: draft messages (currently written ## with full=false) will be output as yaml. def write_message f, full=true, date=Time.now raise ArgumentError, "no pre-defined date: header allowed" if @header["Date"] - f.puts header_lines(@header) + f.puts format_headers(@header).first f.puts <#{l}" : l } + f.puts sanitize_body(@body.join("\n")) f.puts sig_lines if full unless $config[:edit_signature] end +protected + + def edit_field field + case field + when "Subject" + text = BufferManager.ask :subject, "Subject: ", @header[field] + if text + @header[field] = parse_header field, text + update + end + else + default = case field + when *MULTI_HEADERS + @header[field] ||= [] + @header[field].join(", ") + else + @header[field] + end + + contacts = BufferManager.ask_for_contacts :people, "#{field}: ", default + if contacts + text = contacts.map { |s| s.longname }.join(", ") + @header[field] = parse_header field, text + update + end + end + end + private + def sanitize_body body + body.gsub(/^From /, ">From ") + end + + def mentions_attachments? + @body.any? { |l| l =~ /^[^>]/ && l =~ /\battach(ment|ed|ing|)\b/i } + end + + def top_posting? + @body.join("\n") =~ /(\S+)\s*Excerpts from.*\n(>.*\n)+\s*\Z/ + end + def sig_lines - p = PersonManager.person_for @header["From"] - sigfn = (AccountManager.account_for(p.email) || + 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 + return [] unless from_email + sigfn = (AccountManager.account_for(from_email) || AccountManager.default_account).signature if sigfn && File.exists?(sigfn)