]> git.cworth.org Git - sup/blob - lib/sup/modes/edit-message-mode.rb
779f1c63d3ef20515d1ab99ef0b2b74220935388
[sup] / lib / sup / modes / edit-message-mode.rb
1 require 'tempfile'
2 require 'socket' # just for gethostname!
3 require 'pathname'
4 require 'rmail'
5
6 module Redwood
7
8 class SendmailCommandFailed < StandardError; end
9
10 class EditMessageMode < LineCursorMode
11   FORCE_HEADERS = %w(From To Cc Bcc Subject)
12   MULTI_HEADERS = %w(To Cc Bcc)
13   NON_EDITABLE_HEADERS = %w(Message-Id Date)
14
15   attr_reader :status
16   attr_accessor :body, :header
17   bool_reader :edited
18
19   register_keymap do |k|
20     k.add :send_message, "Send message", 'y'
21     k.add :edit_field, "Edit field", 'e'
22     k.add :edit_message, "Edit message", :enter
23     k.add :save_as_draft, "Save as draft", 'P'
24     k.add :attach_file, "Attach a file", 'a'
25     k.add :delete_attachment, "Delete an attachment", 'd'
26   end
27
28   def initialize opts={}
29     @header = opts.delete(:header) || {} 
30     @header_lines = []
31
32     @body = opts.delete(:body) || []
33     @body += sig_lines if $config[:edit_signature]
34
35     @attachments = []
36     @message_id = "<#{Time.now.to_i}-sup-#{rand 10000}@#{Socket.gethostname}>"
37     @edited = false
38     @skip_top_rows = opts[:skip_top_rows] || 0
39
40     super opts
41     regen_text
42   end
43
44   def lines; @text.length end
45   def [] i; @text[i] end
46
47   ## a hook
48   def handle_new_text header, body; end
49
50   def edit_field
51     if (curpos - @skip_top_rows) >= @header_lines.length
52       edit_message
53     else
54       case(field = @header_lines[curpos - @skip_top_rows])
55       when "Subject"
56         text = BufferManager.ask :subject, "Subject: ", @header[field]
57         @header[field] = parse_header field, text if text
58       else
59         default =
60           case field
61           when *MULTI_HEADERS
62             @header[field].join(", ")
63           else
64             @header[field]
65           end
66
67         contacts = BufferManager.ask_for_contacts :people, "#{field}: ", default
68         if contacts
69           text = contacts.map { |s| s.longname }.join(", ")
70           @header[field] = parse_header field, text
71         end
72       end
73       update
74     end
75   end
76
77   def edit_message
78     @file = Tempfile.new "sup.#{self.class.name.gsub(/.*::/, '').camel_to_hyphy}"
79     @file.puts format_headers(@header - NON_EDITABLE_HEADERS).first
80     @file.puts
81     @file.puts @body
82     @file.close
83
84     editor = $config[:editor] || ENV['EDITOR'] || "/usr/bin/vi"
85
86     mtime = File.mtime @file.path
87     BufferManager.shell_out "#{editor} #{@file.path}"
88     @edited = true if File.mtime(@file.path) > mtime
89
90     return @edited unless @edited
91
92     header, @body = parse_file @file.path
93     @header = header - NON_EDITABLE_HEADERS
94     handle_new_text @header, @body
95     update
96
97     @edited
98   end
99
100   def killable?
101     !edited? || BufferManager.ask_yes_or_no("Discard message?")
102   end
103
104   def attach_file
105     fn = BufferManager.ask_for_filename :attachment, "File name (enter for browser): "
106     @attachments << Pathname.new(fn)
107     update
108   end
109
110   def delete_attachment
111     i = (curpos - @skip_top_rows) - @attachment_lines_offset
112     if i >= 0 && i < @attachments.size && BufferManager.ask_yes_or_no("Delete attachment #{@attachments[i]}?")
113       @attachments.delete_at i
114       update
115     end
116   end
117
118 protected
119
120   def update
121     regen_text
122     buffer.mark_dirty if buffer
123   end
124
125   def regen_text
126     header, @header_lines = format_headers(@header - NON_EDITABLE_HEADERS) + [""]
127     @text = header + [""] + @body
128     @text += sig_lines unless $config[:edit_signature]
129
130     unless @attachments.empty?
131       @text += [""]
132       @attachment_lines_offset = @text.length
133       @text += @attachments.map { |f| [[:attachment_color, "+ Attachment: #{f} (#{f.human_size})"]] }
134     end
135   end
136
137   def parse_file fn
138     File.open(fn) do |f|
139       header = MBox::read_header f
140       body = f.readlines
141
142       header.delete_if { |k, v| NON_EDITABLE_HEADERS.member? k }
143       header.each { |k, v| header[k] = parse_header k, v }
144
145       [header, body]
146     end
147   end
148
149   def parse_header k, v
150     if MULTI_HEADERS.include?(k)
151       v.split_on_commas.map do |name|
152         (p = ContactManager.contact_for(name)) && p.full_address || name
153       end
154     else
155       v
156     end
157   end
158
159   def format_headers header
160     header_lines = []
161     headers = (FORCE_HEADERS + (header.keys - FORCE_HEADERS)).map do |h|
162       lines = make_lines "#{h}:", header[h]
163       lines.length.times { header_lines << h }
164       lines
165     end.flatten.compact
166     [headers, header_lines]
167   end
168
169   def make_lines header, things
170     case things
171     when nil, []
172       [header + " "]
173     when String
174       [header + " " + things]
175     else
176       if things.empty?
177         [header]
178       else
179         things.map_with_index do |name, i|
180           raise "an array: #{name.inspect} (things #{things.inspect})" if Array === name
181           if i == 0
182             header + " " + name
183           else
184             (" " * (header.length + 1)) + name
185           end + (i == things.length - 1 ? "" : ",")
186         end
187       end
188     end
189   end
190
191   def send_message
192     return if !edited? && !BufferManager.ask_yes_or_no("Message unedited. Really send?")
193     return 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
194     return 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
195
196     date = Time.now
197     from_email = 
198       if @header["From"] =~ /<?(\S+@(\S+?))>?$/
199         $1
200       else
201         AccountManager.default_account.email
202       end
203
204     acct = AccountManager.account_for(from_email) || AccountManager.default_account
205     BufferManager.flash "Sending..."
206
207     begin
208       IO.popen(acct.sendmail, "w") { |p| write_full_message_to p, date }
209       raise SendmailCommandFailed, "Couldn't execute #{acct.sendmail}" unless $? == 0
210       SentManager.write_sent_message(date, from_email) { |f| write_full_message_to f, date }
211       BufferManager.kill_buffer buffer
212       BufferManager.flash "Message sent!"
213     rescue SystemCallError, SendmailCommandFailed => e
214       Redwood::log "Problem sending mail: #{e.message}"
215       BufferManager.flash "Problem sending mail: #{e.message}"
216     end
217   end
218
219   def save_as_draft
220     DraftManager.write_draft { |f| write_message f, false }
221     BufferManager.kill_buffer buffer
222     BufferManager.flash "Saved for later editing."
223   end
224
225   def write_full_message_to f, date=Time.now
226     m = RMail::Message.new
227     @header.each do |k, v|
228       next if v.nil? || v.empty?
229       m.header[k] = 
230         case v
231         when String
232           v
233         when Array
234           v.join ", "
235         end
236     end
237
238     m.header["Date"] = date.rfc2822
239     m.header["Message-Id"] = @message_id
240     m.header["User-Agent"] = "Sup/#{Redwood::VERSION}"
241
242     if @attachments.empty?
243       m.header["Content-Type"] = "text/plain; charset=#{$encoding}"
244       m.body = @body.join
245       m.body += sig_lines.join("\n") unless $config[:edit_signature]
246     else
247       body_m = RMail::Message.new
248       body_m.body = @body.join
249       body_m.body += sig_lines.join("\n") unless $config[:edit_signature]
250       body_m.header["Content-Disposition"] = "inline"
251       
252       m.add_part body_m
253       @attachments.each { |fn| m.add_file_attachment fn.to_s }
254     end
255     f.puts m.to_s
256   end
257
258   ## this is going to change soon: draft messages (currently written
259   ## with full=false) will be output as yaml.
260   def write_message f, full=true, date=Time.now
261     raise ArgumentError, "no pre-defined date: header allowed" if @header["Date"]
262     f.puts format_headers(@header).first
263     f.puts <<EOS
264 Date: #{date.rfc2822}
265 Message-Id: #{@message_id}
266 EOS
267     if full
268       f.puts <<EOS
269 Mime-Version: 1.0
270 Content-Type: text/plain; charset=us-ascii
271 Content-Disposition: inline
272 User-Agent: Redwood/#{Redwood::VERSION}
273 EOS
274     end
275
276     f.puts
277     f.puts @body.map { |l| l =~ /^From / ? ">#{l}" : l }
278     f.puts sig_lines if full unless $config[:edit_signature]
279   end  
280
281 private
282
283   def mentions_attachments?
284     @body.any? { |l| l =~ /^[^>]/ && l =~ /\battach(ment|ed|ing|)\b/i }
285   end
286
287   def top_posting?
288     @body.join =~ /(\S+)\s*Excerpts from /
289   end
290
291   def sig_lines
292     p = PersonManager.person_for(@header["From"]) or return []
293     sigfn = (AccountManager.account_for(p.email) || 
294              AccountManager.default_account).signature
295
296     if sigfn && File.exists?(sigfn)
297       ["", "-- "] + File.readlines(sigfn).map { |l| l.chomp }
298     else
299       []
300     end
301   end
302 end
303
304 end