sup

A curses threads-with-tags style email client

sup.git

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

lib/sup/modes/reply_mode.rb (7468B) - raw

      1 module Redwood
      2 
      3 class ReplyMode < EditMessageMode
      4   REPLY_TYPES = [:sender, :recipient, :list, :all, :user]
      5   TYPE_DESCRIPTIONS = {
      6     :sender => "Sender",
      7     :recipient => "Recipient",
      8     :all => "All",
      9     :list => "Mailing list",
     10     :user => "Customized"
     11   }
     12 
     13   HookManager.register "attribution", <<EOS
     14 Generates an attribution ("Excerpts from Joe Bloggs's message of Fri Jan 11 09:54:32 -0500 2008:").
     15 Variables:
     16   message: a message object representing the message being replied to
     17     (useful values include message.from.name and message.date)
     18 Return value:
     19   A string containing the text of the quote line (can be multi-line)
     20 EOS
     21 
     22   HookManager.register "reply-from", <<EOS
     23 Selects a default address for the From: header of a new reply.
     24 Variables:
     25   message: a message object representing the message being replied to
     26     (useful values include message.recipient_email, message.to, and message.cc)
     27 Return value:
     28   A Person to be used as the default for the From: header, or nil to use the
     29   default behavior.
     30 EOS
     31 
     32   HookManager.register "reply-to", <<EOS
     33 Set the default reply-to mode.
     34 Variables:
     35   modes: array of valid modes to choose from, which will be a subset of
     36              [:#{REPLY_TYPES * ', :'}]
     37          The default behavior is equivalent to
     38              ([:list, :sender, :recipent] & modes)[0]
     39   message: a message object representing the message being replied to
     40     (useful values include message.is_list_message? and message.list_address)
     41 Return value:
     42   The reply mode you desire, or nil to use the default behavior.
     43 EOS
     44 
     45   def initialize message, type_arg=nil
     46     @m = message
     47     @edited = false
     48 
     49     ## it's important to put this early because it forces a read of
     50     ## the full headers (most importantly the list-post header, if
     51     ## any)
     52     body = reply_body_lines message
     53     @body_orig = body
     54 
     55     ## first, determine the address at which we received this email. this will
     56     ## become our From: address in the reply.
     57     hook_reply_from = HookManager.run "reply-from", :message => @m
     58 
     59     ## sanity check that selection is a Person (or we'll fail below)
     60     ## don't check that it's an Account, though; assume they know what they're
     61     ## doing.
     62     if hook_reply_from && !(hook_reply_from.is_a? Person)
     63       info "reply-from returned non-Person, using default from."
     64       hook_reply_from = nil
     65     end
     66 
     67     ## determine the from address of a reply.
     68     ## if we have a value from a hook, use it.
     69     from = if hook_reply_from
     70       hook_reply_from
     71     ## otherwise, try and find an account somewhere in the list of to's
     72     ## and cc's and look up the corresponding name form the list of accounts.
     73     ## if this does not succeed use the recipient_email (=envelope-to) instead.
     74     ## this is for the case where mail is received from a mailing lists (so the
     75     ## To: is the list id itself). if the user subscribes via a particular
     76     ## alias, we want to use that alias in the reply.
     77     elsif(b = (@m.to.collect {|t| t.email} + @m.cc.collect {|c| c.email} + [@m.recipient_email] ).find { |p| AccountManager.is_account_email? p })
     78       a = AccountManager.account_for(b)
     79       Person.new a.name, b
     80     ## if all else fails, use the default
     81     else
     82       AccountManager.default_account
     83     end
     84 
     85     ## now, determine to: and cc: addressess. we ignore reply-to for list
     86     ## messages because it's typically set to the list address, which we
     87     ## explicitly treat with reply type :list
     88     to = @m.is_list_message? ? @m.from : (@m.replyto || @m.from)
     89 
     90     ## next, cc:
     91     cc = (@m.to + @m.cc - [from, to]).uniq
     92 
     93     ## one potential reply type is "reply to recipient". this only happens
     94     ## in certain cases:
     95     ## if there's no cc, then the sender is the person you want to reply
     96     ## to. if it's a list message, then the list address is. otherwise,
     97     ## the cc contains a recipient.
     98     useful_recipient = !(cc.empty? || @m.is_list_message?)
     99 
    100     @headers = {}
    101     @headers[:recipient] = {
    102       "To" => cc.map { |p| p.full_address },
    103       "Cc" => [],
    104     } if useful_recipient
    105 
    106     ## typically we don't want to have a reply-to-sender option if the sender
    107     ## is a user account. however, if the cc is empty, it's a message to
    108     ## ourselves, so for the lack of any other options, we'll add it.
    109     @headers[:sender] = {
    110       "To" => [to.full_address],
    111       "Cc" => [],
    112     } if !AccountManager.is_account?(to) || !useful_recipient
    113 
    114     @headers[:user] = {
    115       "To" => [],
    116       "Cc" => [],
    117     }
    118 
    119     not_me_ccs = cc.select { |p| !AccountManager.is_account?(p) }
    120     @headers[:all] = {
    121       "To" => [to.full_address],
    122       "Cc" => not_me_ccs.map { |p| p.full_address },
    123     } unless not_me_ccs.empty?
    124 
    125     @headers[:list] = {
    126       "To" => [@m.list_address.full_address],
    127       "Cc" => [],
    128     } if @m.is_list_message?
    129 
    130     refs = gen_references
    131 
    132     types = REPLY_TYPES.select { |t| @headers.member?(t) }
    133     @type_selector = HorizontalSelector.new "Reply to:", types, types.map { |x| TYPE_DESCRIPTIONS[x] }
    134 
    135     hook_reply = HookManager.run "reply-to", :modes => types, :message => @m
    136 
    137     @type_selector.set_to(
    138       if types.include? type_arg
    139         type_arg
    140       elsif types.include? hook_reply
    141         hook_reply
    142       elsif @m.is_list_message?
    143         :list
    144       elsif @headers.member? :sender
    145         :sender
    146       else
    147         :recipient
    148       end)
    149 
    150     headers_full = {
    151       "From" => from.full_address,
    152       "Bcc" => [],
    153       "In-reply-to" => "<#{@m.id}>",
    154       "Subject" => Message.reify_subj(@m.subj),
    155       "References" => refs,
    156     }.merge @headers[@type_selector.val]
    157 
    158     HookManager.run "before-edit", :header => headers_full, :body => body
    159 
    160     super :header => headers_full, :body => body, :twiddles => false
    161     add_selector @type_selector
    162   end
    163 
    164 protected
    165 
    166   def move_cursor_right
    167     super
    168     if @headers[@type_selector.val] != self.header
    169       self.header = self.header.merge @headers[@type_selector.val]
    170       rerun_crypto_selector_hook
    171       update
    172     end
    173   end
    174 
    175   def move_cursor_left
    176     super
    177     if @headers[@type_selector.val] != self.header
    178       self.header = self.header.merge @headers[@type_selector.val]
    179       rerun_crypto_selector_hook
    180       update
    181     end
    182   end
    183 
    184   def reply_body_lines m
    185     attribution = HookManager.run("attribution", :message => m) || default_attribution(m)
    186     lines = attribution.split("\n") + m.quotable_body_lines.map { |l| "> #{l}" }
    187     lines.pop while lines.last =~ /^\s*$/
    188     lines
    189   end
    190 
    191   def default_attribution m
    192     "Excerpts from #{@m.from.name}'s message of #{@m.date}:"
    193   end
    194 
    195   def handle_new_text new_header, new_body
    196     if new_body != @body_orig
    197       @body_orig = new_body
    198       @edited = true
    199     end
    200     old_header = @headers[@type_selector.val]
    201     if old_header.any? { |k, v| new_header[k] != v }
    202       @type_selector.set_to :user
    203       self.header["To"] = @headers[:user]["To"] = new_header["To"]
    204       self.header["Cc"] = @headers[:user]["Cc"] = new_header["Cc"]
    205       update
    206     end
    207   end
    208 
    209   def gen_references
    210     (@m.refs + [@m.id]).map { |x| "<#{x}>" }.join(" ")
    211   end
    212 
    213   def edit_field field
    214     edited_field = super
    215     if edited_field and (field == "To" or field == "Cc")
    216       @type_selector.set_to :user
    217       @headers[:user]["To"] = self.header["To"]
    218       @headers[:user]["Cc"] = self.header["Cc"]
    219       update
    220     end
    221   end
    222 
    223   def send_message
    224     return unless super # super returns true if the mail has been sent
    225     @m.add_label :replied
    226     Index.save_message @m
    227   end
    228 end
    229 
    230 end