sup

A curses threads-with-tags style email client

sup.git

git clone https://supmua.dev/git/sup/

lib/sup/modes/edit_message_mode.rb (21900B) - raw

      1 require 'tempfile'
      2 require 'socket' # just for gethostname!
      3 require 'pathname'
      4 
      5 module Redwood
      6 
      7 class SendmailCommandFailed < StandardError; end
      8 
      9 class EditMessageMode < LineCursorMode
     10   DECORATION_LINES = 1
     11 
     12   FORCE_HEADERS = %w(From To Cc Bcc Subject)
     13   MULTI_HEADERS = %w(To Cc Bcc)
     14   NON_EDITABLE_HEADERS = %w(Message-id Date)
     15 
     16   HookManager.register "signature", <<EOS
     17 Generates a message signature.
     18 Variables:
     19       header: an object that supports string-to-string hashtable-style access
     20               to the raw headers for the message. E.g., header["From"],
     21               header["To"], etc.
     22   from_email: the email part of the From: line, or nil if empty
     23   message_id: the unique message id of the message
     24 Return value:
     25   A string (multi-line ok) containing the text of the signature, or nil to
     26   use the default signature, or :none for no signature.
     27 EOS
     28 
     29   HookManager.register "check-attachment", <<EOS
     30 Do checks on the attachment filename
     31 Variables:
     32 	filename: the name of the attachment
     33 Return value:
     34     A String (single line) containing a message why this attachment is not optimal
     35     to be attached.
     36     If it is ok just return an empty string or nil
     37 EOS
     38 
     39   HookManager.register "before-edit", <<EOS
     40 Modifies message body and headers before editing a new message. Variables
     41 should be modified in place.
     42 Variables:
     43 	header: a hash of headers. See 'signature' hook for documentation.
     44 	body: an array of lines of body text.
     45 Return value:
     46 	none
     47 EOS
     48 
     49   HookManager.register "mentions-attachments", <<EOS
     50 Detects if given message mentions attachments the way it is probable
     51 that there should be files attached to the message.
     52 Variables:
     53 	header: a hash of headers. See 'signature' hook for documentation.
     54 	body: an array of lines of body text.
     55 Return value:
     56 	True if attachments are mentioned.
     57 EOS
     58 
     59   HookManager.register "crypto-mode", <<EOS
     60 Modifies cryptography settings based on header and message content, before
     61 editing a new message. This can be used to set, for example, default cryptography
     62 settings.
     63 Variables:
     64     header: a hash of headers. See 'signature' hook for documentation.
     65     body: an array of lines of body text.
     66     crypto_selector: the UI element that controls the current cryptography setting.
     67 Return value:
     68      none
     69 EOS
     70 
     71   HookManager.register "sendmail", <<EOS
     72 Sends the given mail. If this hook doesn't exist, the sendmail command
     73 configured for the account is used.
     74 The message will be saved after this hook is run, so any modification to it
     75 will be recorded.
     76 Variables:
     77     message: RMail::Message instance of the mail to send
     78     account: Account instance matching the From address
     79 Return value:
     80      True if mail has been sent successfully, false otherwise.
     81 EOS
     82 
     83   attr_reader :status
     84   attr_accessor :body, :header
     85   bool_reader :edited
     86 
     87   register_keymap do |k|
     88     k.add :send_message, "Send message", 'y'
     89     k.add :edit_message_or_field, "Edit selected field", 'e'
     90     k.add :edit_to, "Edit To:", 't'
     91     k.add :edit_cc, "Edit Cc:", 'c'
     92     k.add :edit_subject, "Edit Subject", 's'
     93     k.add :default_edit_message, "Edit message (default)", :enter
     94     k.add :alternate_edit_message, "Edit message (alternate, asynchronously)", 'E'
     95     k.add :save_as_draft, "Save as draft", 'P'
     96     k.add :attach_file, "Attach a file", 'a'
     97     k.add :delete_attachment, "Delete an attachment", 'd'
     98     k.add :move_cursor_right, "Move selector to the right", :right, 'l'
     99     k.add :move_cursor_left, "Move selector to the left", :left, 'h'
    100   end
    101 
    102   def initialize opts={}
    103     @header = opts.delete(:header) || {}
    104     @header_lines = []
    105 
    106     @body = opts.delete(:body) || []
    107 
    108     if opts[:attachments]
    109       @attachments = opts[:attachments].values
    110       @attachment_names = opts[:attachments].keys
    111     else
    112       @attachments = []
    113       @attachment_names = []
    114     end
    115 
    116     begin
    117       hostname = File.open("/etc/mailname", "r").gets.chomp
    118     rescue
    119       nil
    120     end
    121     hostname = Socket.gethostname if hostname.nil? or hostname.empty?
    122 
    123     @message_id = "<#{Time.now.to_i}-sup-#{rand 10000}@#{hostname}>"
    124     @edited = false
    125     @sig_edited = false
    126     @selectors = []
    127     @selector_label_width = 0
    128     @async_mode = nil
    129 
    130     HookManager.run "before-edit", :header => @header, :body => @body
    131 
    132     @account_selector = nil
    133     # only show account selector if there is more than one email address
    134     if $config[:account_selector] && AccountManager.user_emails.length > 1
    135       ## Duplicate e-mail strings to prevent a "can't modify frozen
    136       ## object" crash triggered by the String::display_length()
    137       ## method in util.rb
    138       user_emails_copy = []
    139       AccountManager.user_emails.each { |e| user_emails_copy.push e.dup }
    140 
    141       @account_selector =
    142         HorizontalSelector.new "Account:", AccountManager.user_emails + [nil], user_emails_copy + ["Customized"]
    143 
    144       if @header["From"] =~ /<?(\S+@(\S+?))>?$/
    145         # TODO: this is ugly. might implement an AccountSelector and handle
    146         # special cases more transparently.
    147         account_from = @account_selector.can_set_to?($1) ? $1 : nil
    148         @account_selector.set_to account_from
    149       else
    150         @account_selector.set_to nil
    151       end
    152 
    153       # A single source of truth might better than duplicating this in both
    154       # @account_user and @account_selector.
    155       @account_user = @header["From"]
    156 
    157       add_selector @account_selector
    158     end
    159 
    160     @crypto_selector =
    161       if CryptoManager.have_crypto?
    162         HorizontalSelector.new "Crypto:", [:none] + CryptoManager::OUTGOING_MESSAGE_OPERATIONS.keys, ["None"] + CryptoManager::OUTGOING_MESSAGE_OPERATIONS.values
    163       end
    164     add_selector @crypto_selector if @crypto_selector
    165 
    166     if @crypto_selector
    167       HookManager.run "crypto-mode", :header => @header, :body => @body, :crypto_selector => @crypto_selector
    168     end
    169 
    170     super opts
    171     regen_text
    172   end
    173 
    174   def lines; @text.length + (@selectors.empty? ? 0 : (@selectors.length + DECORATION_LINES)) end
    175 
    176   def [] i
    177     if @selectors.empty?
    178       @text[i]
    179     elsif i < @selectors.length
    180       @selectors[i].line @selector_label_width
    181     elsif i == @selectors.length
    182       ""
    183     else
    184       @text[i - @selectors.length - DECORATION_LINES]
    185     end
    186   end
    187 
    188   ## hook for subclasses. i hate this style of programming.
    189   def handle_new_text header, body; end
    190 
    191   def edit_message_or_field
    192     lines = (@selectors.empty? ? 0 : DECORATION_LINES) + @selectors.size
    193     if lines > curpos
    194       return
    195     elsif (curpos - lines) >= @header_lines.length
    196       default_edit_message
    197     else
    198       edit_field @header_lines[curpos - lines]
    199     end
    200   end
    201 
    202   def edit_to; edit_field "To" end
    203   def edit_cc; edit_field "Cc" end
    204   def edit_subject; edit_field "Subject" end
    205 
    206   def save_message_to_file
    207     sig = sig_lines.join("\n")
    208     @file = Tempfile.new ["sup.#{self.class.name.gsub(/.*::/, '').camel_to_hyphy}", ".eml"]
    209     @file.puts format_headers(@header - NON_EDITABLE_HEADERS).first
    210     @file.puts
    211 
    212     begin
    213       text = @body.join("\n")
    214     rescue Encoding::CompatibilityError
    215       text = @body.map { |x| x.fix_encoding! }.join("\n")
    216       debug "encoding problem while writing message, trying to rescue, but expect errors: #{text}"
    217     end
    218 
    219     @file.puts text
    220     @file.puts sig if ($config[:edit_signature] and !@sig_edited)
    221     @file.close
    222   end
    223 
    224   def set_sig_edit_flag
    225     sig = sig_lines.join("\n")
    226     if $config[:edit_signature]
    227       pbody = @body.map { |x| x.fix_encoding! }.join("\n").fix_encoding!
    228       blen = pbody.length
    229       slen = sig.length
    230 
    231       if blen > slen and pbody[blen-slen..blen] == sig
    232         @sig_edited = false
    233         @body = pbody[0..blen-slen].fix_encoding!.split("\n")
    234       else
    235         @sig_edited = true
    236       end
    237     end
    238   end
    239 
    240   def default_edit_message
    241     if $config[:always_edit_async]
    242       return edit_message_async
    243     else
    244       return edit_message
    245     end
    246   end
    247 
    248   def alternate_edit_message
    249     if $config[:always_edit_async]
    250       return edit_message
    251     else
    252       return edit_message_async
    253     end
    254   end
    255 
    256   def edit_message
    257     old_from = @header["From"] if @account_selector
    258 
    259     begin
    260       save_message_to_file
    261     rescue SystemCallError => e
    262       BufferManager.flash "Can't save message to file: #{e.message}"
    263       return
    264     end
    265 
    266     editor = $config[:editor] || ENV['EDITOR'] || "/usr/bin/vi"
    267 
    268     mtime = File.mtime @file.path
    269     BufferManager.shell_out "#{editor} #{@file.path}"
    270     @edited = true if File.mtime(@file.path) > mtime
    271 
    272     return @edited unless @edited
    273 
    274     header, @body = parse_file @file.path
    275     @header = header - NON_EDITABLE_HEADERS
    276     set_sig_edit_flag
    277 
    278     if @account_selector and @header["From"] != old_from
    279       @account_user = @header["From"]
    280       @account_selector.set_to nil
    281     end
    282 
    283     handle_new_text @header, @body
    284     rerun_crypto_selector_hook
    285     update
    286 
    287     @edited
    288   end
    289 
    290   def edit_message_async
    291     begin
    292       save_message_to_file
    293     rescue SystemCallError => e
    294       BufferManager.flash "Can't save message to file: #{e.message}"
    295       return
    296     end
    297 
    298     @mtime = File.mtime @file.path
    299 
    300     # put up buffer saying you can now edit the message in another
    301     # terminal or app, and continue to use sup in the meantime.
    302     subject = @header["Subject"] || ""
    303     @async_mode = EditMessageAsyncMode.new self, @file.path, subject
    304     BufferManager.spawn "Waiting for message \"#{subject}\" to be finished", @async_mode
    305 
    306     # hide ourselves, and wait for signal to resume from async mode ...
    307     buffer.hidden = true
    308   end
    309 
    310   def edit_message_async_resume being_killed=false
    311     buffer.hidden = false
    312     @async_mode = nil
    313     BufferManager.raise_to_front buffer if !being_killed
    314 
    315     @edited = true if File.mtime(@file.path) > @mtime
    316 
    317     header, @body = parse_file @file.path
    318     @header = header - NON_EDITABLE_HEADERS
    319     set_sig_edit_flag
    320     handle_new_text @header, @body
    321     update
    322 
    323     true
    324   end
    325 
    326   def killable?
    327     if !@async_mode.nil?
    328       return false if !@async_mode.killable?
    329       if File.mtime(@file.path) > @mtime
    330         @edited = true
    331         header, @body = parse_file @file.path
    332         @header = header - NON_EDITABLE_HEADERS
    333         handle_new_text @header, @body
    334         update
    335       end
    336     end
    337     !edited? || BufferManager.ask_yes_or_no("Discard message?")
    338   end
    339 
    340   def unsaved?; edited? end
    341 
    342   def attach_file
    343     fn = BufferManager.ask_for_filename :attachment, "File name (enter for browser): "
    344     return unless fn
    345     if HookManager.enabled? "check-attachment"
    346         reason = HookManager.run("check-attachment", :filename => fn)
    347         if reason
    348             return unless BufferManager.ask_yes_or_no("#{reason} Attach anyway?")
    349         end
    350     end
    351     begin
    352       Dir[fn].each do |f|
    353         @attachments << RMail::Message.make_file_attachment(f)
    354         @attachment_names << f
    355       end
    356       update
    357     rescue SystemCallError => e
    358       BufferManager.flash "Can't read #{fn}: #{e.message}"
    359     end
    360   end
    361 
    362   def delete_attachment
    363     i = curpos - @attachment_lines_offset - (@selectors.empty? ? 0 : DECORATION_LINES) - @selectors.size
    364     if i >= 0 && i < @attachments.size && BufferManager.ask_yes_or_no("Delete attachment #{@attachment_names[i]}?")
    365       @attachments.delete_at i
    366       @attachment_names.delete_at i
    367       update
    368     end
    369   end
    370 
    371 protected
    372 
    373   def rerun_crypto_selector_hook
    374     if @crypto_selector && !@crypto_selector.changed_by_user
    375       HookManager.run "crypto-mode", :header => @header, :body => @body, :crypto_selector => @crypto_selector
    376     end
    377   end
    378 
    379   def mime_encode string
    380     string = [string].pack('M') # basic quoted-printable
    381     string.gsub!(/=\n/,'')      # .. remove trailing newline
    382     string.gsub!(/_/,'=5F')     # .. encode underscores
    383     string.gsub!(/\?/,'=3F')    # .. encode question marks
    384     string.gsub!(/ /,'_')       # .. translate space to underscores
    385     "=?utf-8?q?#{string}?="
    386   end
    387 
    388   def mime_encode_subject string
    389     return string if string.ascii_only?
    390     mime_encode string
    391   end
    392 
    393   RE_ADDRESS = /(.+)( <.*@.*>)/
    394 
    395   # Encode "bælammet mitt <user@example.com>" into
    396   # "=?utf-8?q?b=C3=A6lammet_mitt?= <user@example.com>
    397   def mime_encode_address string
    398     return string if string.ascii_only?
    399     string.sub(RE_ADDRESS) { |match| mime_encode($1) + $2 }
    400   end
    401 
    402   def move_cursor_left
    403     if curpos < @selectors.length
    404       @selectors[curpos].roll_left
    405       buffer.mark_dirty
    406       update if @account_selector
    407     else
    408       col_left
    409     end
    410   end
    411 
    412   def move_cursor_right
    413     if curpos < @selectors.length
    414       @selectors[curpos].roll_right
    415       buffer.mark_dirty
    416       update if @account_selector
    417     else
    418       col_right
    419     end
    420   end
    421 
    422   def add_selector s
    423     @selectors << s
    424     @selector_label_width = [@selector_label_width, s.label.length].max
    425   end
    426 
    427   def update
    428     if @account_selector
    429       if @account_selector.val.nil?
    430         @header["From"] = @account_user
    431       else
    432         @header["From"] = AccountManager.full_address_for @account_selector.val
    433       end
    434     end
    435 
    436     regen_text
    437     buffer.mark_dirty if buffer
    438   end
    439 
    440   def regen_text
    441     header, @header_lines = format_headers(@header - NON_EDITABLE_HEADERS) + [""]
    442     @text = header + [""] + @body
    443     @text += sig_lines unless @sig_edited
    444 
    445     @attachment_lines_offset = 0
    446 
    447     unless @attachments.empty?
    448       @text += [""]
    449       @attachment_lines_offset = @text.length
    450       @text += (0 ... @attachments.size).map { |i| [[:attachment_color, "+ Attachment: #{@attachment_names[i]} (#{@attachments[i].body.size.to_human_size})"]] }
    451     end
    452   end
    453 
    454   def parse_file fn
    455     File.open(fn) do |f|
    456       header = Source.parse_raw_email_header(f).inject({}) { |h, (k, v)| h[k.capitalize] = v; h } # lousy HACK
    457       body = f.readlines.map { |l| l.chomp }
    458 
    459       header.delete_if { |k, v| NON_EDITABLE_HEADERS.member? k }
    460       header.each { |k, v| header[k] = parse_header k, v }
    461 
    462       [header, body]
    463     end
    464   end
    465 
    466   def parse_header k, v
    467     if MULTI_HEADERS.include?(k)
    468       v.split_on_commas.map do |name|
    469         (p = ContactManager.contact_for(name)) && p.full_address || name
    470       end
    471     else
    472       v
    473     end
    474   end
    475 
    476   def format_headers header
    477     header_lines = []
    478     headers = (FORCE_HEADERS + (header.keys - FORCE_HEADERS)).map do |h|
    479       lines = make_lines "#{h}:", header[h]
    480       lines.length.times { header_lines << h }
    481       lines
    482     end.flatten.compact
    483     [headers, header_lines]
    484   end
    485 
    486   def make_lines header, things
    487     case things
    488     when nil, []
    489       [header + " "]
    490     when String
    491       [header + " " + things]
    492     else
    493       if things.empty?
    494         [header]
    495       else
    496         things.map_with_index do |name, i|
    497           raise "an array: #{name.inspect} (things #{things.inspect})" if Array === name
    498           if i == 0
    499             header + " " + name
    500           else
    501             (" " * (header.display_length + 1)) + name
    502           end + (i == things.length - 1 ? "" : ",")
    503         end
    504       end
    505     end
    506   end
    507 
    508   def send_message
    509     return false if !edited? && !BufferManager.ask_yes_or_no("Message unedited. Really send?")
    510     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
    511     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
    512 
    513     from_email =
    514       if @header["From"] =~ /<?(\S+@(\S+?))>?$/
    515         $1
    516       else
    517         AccountManager.default_account.email
    518       end
    519 
    520     acct = AccountManager.account_for(from_email) || AccountManager.default_account
    521     BufferManager.flash "Sending..."
    522 
    523     begin
    524       date = Time.now
    525       m = build_message date
    526 
    527       if HookManager.enabled? "sendmail"
    528         if not HookManager.run "sendmail", :message => m, :account => acct
    529               warn "Sendmail hook was not successful"
    530               return false
    531         end
    532       else
    533         IO.popen(acct.sendmail, "w:UTF-8") { |p| p.puts m }
    534         raise SendmailCommandFailed, "Couldn't execute #{acct.sendmail}" unless $? == 0
    535       end
    536 
    537       SentManager.write_sent_message(date, from_email) { |f| f.puts sanitize_body(m.to_s) }
    538       BufferManager.kill_buffer buffer
    539       BufferManager.flash "Message sent!"
    540       true
    541     rescue SystemCallError, SendmailCommandFailed, CryptoManager::Error, TypeError => e
    542       warn "Problem sending mail: #{e.message}"
    543       BufferManager.flash "Problem sending mail: #{e.message}"
    544       false
    545     end
    546   end
    547 
    548   def save_as_draft
    549     DraftManager.write_draft { |f| write_message f, false }
    550     BufferManager.kill_buffer buffer
    551     BufferManager.flash "Saved for later editing."
    552   end
    553 
    554   def build_message date
    555     m = RMail::Message.new
    556     m.header["Content-Type"] = "text/plain; charset=#{$encoding}"
    557     m.body = @body.join("\n")
    558     m.body += "\n" + sig_lines.join("\n") unless @sig_edited
    559     ## body must end in a newline or GPG signatures will be WRONG!
    560     m.body += "\n" unless m.body =~ /\n\Z/
    561     m.body = m.body.fix_encoding!
    562 
    563     ## there are attachments, so wrap body in an attachment of its own
    564     unless @attachments.empty?
    565       body_m = m
    566       body_m.header["Content-Disposition"] = +"inline"
    567       m = RMail::Message.new
    568 
    569       m.add_part body_m
    570       @attachments.each do |a|
    571         a.body = a.body.fix_encoding! if a.body.kind_of? String
    572         m.add_part a
    573       end
    574     end
    575 
    576     ## do whatever crypto transformation is necessary
    577     if @crypto_selector && @crypto_selector.val != :none
    578       from_email = Person.from_address(@header["From"]).email
    579       to_email = [@header["To"], @header["Cc"], @header["Bcc"]].flatten.compact.map { |p| Person.from_address(p).email }
    580       if m.multipart?
    581         m.each_part {|p| p = transfer_encode p}
    582       else
    583         m = transfer_encode m
    584       end
    585 
    586       m = CryptoManager.send @crypto_selector.val, from_email, to_email, m
    587     end
    588 
    589     ## finally, set the top-level headers
    590     @header.each do |k, v|
    591       next if v.nil? || v.empty?
    592       m.header[k] =
    593         case v
    594         when String
    595           (k.match(/subject/i) ? mime_encode_subject(v).dup.fix_encoding! : mime_encode_address(v)).dup.fix_encoding!
    596         when Array
    597           (v.map { |v| mime_encode_address v }.join ", ").dup.fix_encoding!
    598         end
    599     end
    600 
    601     m.header["Date"] = date.rfc2822
    602     m.header["Message-Id"] = @message_id
    603     m.header["User-Agent"] = "Sup/#{Redwood::VERSION}"
    604     m.header["Content-Transfer-Encoding"] ||= +"8bit"
    605     m.header["MIME-Version"] = +"1.0" if m.multipart?
    606     m
    607   end
    608 
    609   ## TODO: remove this. redundant with write_full_message_to.
    610   ##
    611   ## this is going to change soon: draft messages (currently written
    612   ## with full=false) will be output as yaml.
    613   def write_message f, full=true, date=Time.now
    614     raise ArgumentError, "no pre-defined date: header allowed" if @header["Date"]
    615     f.puts format_headers(@header).first
    616     f.puts <<EOS
    617 Date: #{date.rfc2822}
    618 Message-Id: #{@message_id}
    619 EOS
    620     if full
    621       f.puts <<EOS
    622 Mime-Version: 1.0
    623 Content-Type: text/plain; charset=us-ascii
    624 Content-Disposition: inline
    625 User-Agent: Redwood/#{Redwood::VERSION}
    626 EOS
    627     end
    628 
    629     f.puts
    630     f.puts sanitize_body(@body.join("\n"))
    631     f.puts sig_lines if full unless $config[:edit_signature]
    632   end
    633 
    634 protected
    635 
    636   def edit_field field
    637     case field
    638     when "Subject"
    639       text = BufferManager.ask :subject, "Subject: ", @header[field]
    640        if text
    641          @header[field] = parse_header field, text
    642          update
    643        end
    644     else
    645       default = case field
    646         when *MULTI_HEADERS
    647           @header[field] ||= []
    648           @header[field].join(", ")
    649         else
    650           @header[field]
    651         end
    652 
    653       contacts = BufferManager.ask_for_contacts :people, "#{field}: ", default
    654       if contacts
    655         text = contacts.map { |s| s.full_address }.join(", ")
    656         @header[field] = parse_header field, text
    657 
    658         if @account_selector and field == "From"
    659           @account_user = @header["From"]
    660           @account_selector.set_to nil
    661         end
    662 
    663         rerun_crypto_selector_hook
    664         update
    665       end
    666     end
    667   end
    668 
    669 private
    670 
    671   def sanitize_body body
    672     body.gsub(/^From /, ">From ")
    673   end
    674 
    675   def mentions_attachments?
    676     if HookManager.enabled? "mentions-attachments"
    677       HookManager.run "mentions-attachments", :header => @header, :body => @body
    678     else
    679       @body.any? {  |l| l.fix_encoding! =~ /^[^>]/ && l.fix_encoding! =~ /\battach(ment|ed|ing|)\b/i }
    680     end
    681   end
    682 
    683   def top_posting?
    684     @body.map { |x| x.fix_encoding! }.join("\n").fix_encoding! =~ /(\S+)\s*Excerpts from.*\n(>.*\n)+\s*\Z/
    685   end
    686 
    687   def sig_lines
    688     p = Person.from_address(@header["From"])
    689     from_email = p && p.email
    690 
    691     ## first run the hook
    692     hook_sig = HookManager.run "signature", :header => @header, :from_email => from_email, :message_id => @message_id
    693 
    694     return [] if hook_sig == :none
    695     return ["", "-- "] + hook_sig.split("\n") if hook_sig
    696 
    697     ## no hook, do default signature generation based on config.yaml
    698     return [] unless from_email
    699     sigfn = (AccountManager.account_for(from_email) ||
    700              AccountManager.default_account).signature
    701 
    702     if sigfn && File.exist?(sigfn)
    703       ["", "-- "] + File.readlines(sigfn).map { |l| l.chomp }
    704     else
    705       []
    706     end
    707   end
    708 
    709   def transfer_encode msg_part
    710     ## return the message unchanged if it's already encoded
    711     if (msg_part.header["Content-Transfer-Encoding"] == "base64" ||
    712         msg_part.header["Content-Transfer-Encoding"] == "quoted-printable")
    713       return msg_part
    714     end
    715 
    716     ## encode to quoted-printable for all text/* MIME types,
    717     ## use base64 otherwise
    718     if msg_part.header["Content-Type"] =~ /text\/.*/
    719       msg_part.header.set "Content-Transfer-Encoding", +"quoted-printable"
    720       msg_part.body = [msg_part.body].pack('M')
    721     else
    722       msg_part.header.set "Content-Transfer-Encoding", +"base64"
    723       msg_part.body = [msg_part.body].pack('m')
    724     end
    725     msg_part
    726   end
    727 end
    728 
    729 end