lib/sup/message_chunks.rb (10816B) - raw
1 # encoding: UTF-8
2
3 require 'tempfile'
4 require 'rbconfig'
5 require 'shellwords'
6
7 ## Here we define all the "chunks" that a message is parsed
8 ## into. Chunks are used by ThreadViewMode to render a message. Chunks
9 ## are used for both MIME stuff like attachments, for Sup's parsing of
10 ## the message body into text, quote, and signature regions, and for
11 ## notices like "this message was decrypted" or "this message contains
12 ## a valid signature"---basically, anything we want to differentiate
13 ## at display time.
14 ##
15 ## A chunk can be inlineable, expandable, or viewable. If it's
16 ## inlineable, #color and #lines are called and the output is treated
17 ## as part of the message text. This is how Text and one-line Quotes
18 ## and Signatures work.
19 ##
20 ## If it's not inlineable but is expandable, #patina_color and
21 ## #patina_text are called to generate a "patina" (a one-line widget,
22 ## basically), and the user can press enter to toggle the display of
23 ## the chunk content, which is generated from #color and #lines as
24 ## above. This is how Quote, Signature, and most widgets
25 ## work. Exandable chunks can additionally define #initial_state to be
26 ## :open if they want to start expanded (default is to start collapsed).
27 ##
28 ## If it's not expandable but is viewable, a patina is displayed using
29 ## #patina_color and #patina_text, but no toggling is allowed. Instead,
30 ## if #view! is defined, pressing enter on the widget calls view! and
31 ## (if that returns false) #to_s. Otherwise, enter does nothing. This
32 ## is how non-inlineable attachments work.
33 ##
34 ## Independent of all that, a chunk can be quotable, in which case it's
35 ## included as quoted text during a reply. Text, Quotes, and mime-parsed
36 ## attachments are quotable; Signatures are not.
37
38 module Redwood
39 module Chunk
40 class Attachment
41 HookManager.register "mime-decode", <<EOS
42 Decodes a MIME attachment into text form. The text will be displayed
43 directly in Sup. For attachments that you wish to use a separate program
44 to view (e.g. images), you should use the mime-view hook instead.
45
46 Variables:
47 content_type: the content-type of the attachment
48 charset: the charset of the attachment, if applicable
49 filename: the filename of the attachment as saved to disk
50 sibling_types: if this attachment is part of a multipart MIME attachment,
51 an array of content-types for all attachments. Otherwise,
52 the empty array.
53 Return value:
54 The decoded text of the attachment, or nil if not decoded.
55 EOS
56
57
58 HookManager.register "mime-view", <<EOS
59 Views a non-text MIME attachment. This hook allows you to run
60 third-party programs for attachments that require such a thing (e.g.
61 images). To instead display a text version of the attachment directly in
62 Sup, use the mime-decode hook instead.
63
64 Note that by default (at least on systems that have a run-mailcap command),
65 Sup uses the default mailcap handler for the attachment's MIME type. If
66 you want a particular behavior to be global, you may wish to change your
67 mailcap instead.
68
69 Variables:
70 content_type: the content-type of the attachment
71 filename: the filename of the attachment as saved to disk
72 Return value:
73 True if the viewing was successful, false otherwise. If false, calling
74 /usr/bin/run-mailcap will be tried.
75 EOS
76 #' stupid ruby-mode
77
78 ## raw_content is the post-MIME-decode content. this is used for
79 ## saving the attachment to disk.
80 attr_reader :content_type, :filename, :lines, :raw_content
81 bool_reader :quotable
82
83 ## store tempfile objects as class variables so that they
84 ## are not removed when the viewing process returns. they
85 ## should be garbage collected when the class variable is removed.
86 @@view_tempfiles = []
87
88 def initialize content_type, filename, encoded_content, sibling_types
89 @content_type = content_type.downcase
90 if Shellwords.escape(@content_type) != @content_type
91 warn "content_type #{@content_type} is not safe, changed to application/octet-stream"
92 @content_type = 'application/octet-stream'
93 end
94
95 @filename = filename
96 @quotable = false # changed to true if we can parse it through the
97 # mime-decode hook, or if it's plain text
98 @raw_content =
99 if encoded_content.body
100 encoded_content.decode
101 else
102 "For some bizarre reason, RubyMail was unable to parse this attachment.\n"
103 end
104
105 text = case @content_type
106 when /^text\/plain\b/
107 if /^UTF-7$/i =~ encoded_content.charset
108 @raw_content.decode_utf7
109 else
110 begin
111 charset = Encoding.find(encoded_content.charset || 'US-ASCII')
112 rescue ArgumentError
113 charset = 'US-ASCII'
114 end
115 @raw_content.force_encoding(charset)
116 end
117 else
118 HookManager.run "mime-decode", :content_type => @content_type,
119 :filename => lambda { write_to_disk },
120 :charset => encoded_content.charset,
121 :sibling_types => sibling_types
122 end
123
124 @lines = nil
125 if text
126 text = text.encode($encoding, :invalid => :replace, :undef => :replace)
127 begin
128 @lines = text.gsub("\r\n", "\n").gsub(/\t/, " ").gsub(/\r/, "").split("\n")
129 rescue Encoding::CompatibilityError
130 @lines = text.fix_encoding!.gsub("\r\n", "\n").gsub(/\t/, " ").gsub(/\r/, "").split("\n")
131 debug "error while decoding message text, falling back to default encoding, expect errors in encoding: #{text.fix_encoding!}"
132 end
133
134 @quotable = true
135 end
136 end
137
138 def color; :text_color end
139 def patina_color; :attachment_color end
140 def patina_text
141 if expandable?
142 "Attachment: #{filename} (#{lines.length} lines)"
143 else
144 "Attachment: #{filename} (#{content_type}; #{@raw_content.size.to_human_size})"
145 end
146 end
147 def safe_filename; Shellwords.escape(@filename).gsub("/", "_") end
148 def filesafe_filename; @filename.gsub("/", "_") end
149
150 ## an attachment is exapndable if we've managed to decode it into
151 ## something we can display inline. otherwise, it's viewable.
152 def inlineable?; false end
153 def expandable?; !viewable? end
154 def indexable?; expandable? end
155 def initial_state; :open end
156 def viewable?; @lines.nil? end
157 def view_default! path
158 case RbConfig::CONFIG['arch']
159 when /darwin/
160 cmd = "open #{path}"
161 else
162 cmd = "/usr/bin/run-mailcap --action=view #{@content_type}:#{path}"
163 end
164 debug "running: #{cmd.inspect}"
165 BufferManager.shell_out(cmd)
166 $? == 0
167 end
168
169 def view!
170 write_to_disk do |path|
171 ret = HookManager.run "mime-view", :content_type => @content_type,
172 :filename => path
173 ret || view_default!(path)
174 end
175 end
176
177 def write_to_disk
178 begin
179 # Add the original extension to the generated tempfile name only if the
180 # extension is "safe" (won't be interpreted by the shell). Since
181 # Tempfile.new always generates safe file names this should prevent
182 # attacking the user with funny attachment file names.
183 tempname = if (File.extname @filename) =~ /^\.[[:alnum:]]+$/ then
184 ["sup-attachment", File.extname(@filename)]
185 else
186 "sup-attachment"
187 end
188
189 file = Tempfile.new(tempname)
190 file.print @raw_content
191 file.flush
192
193 @@view_tempfiles.push file # make sure the tempfile is not garbage collected before sup stops
194
195 yield file.path if block_given?
196 return file.path
197 ensure
198 file.close
199 end
200 end
201
202 ## used when viewing the attachment as text
203 def to_s
204 @lines || @raw_content
205 end
206 end
207
208 class Text
209
210 attr_reader :lines
211 def initialize lines
212 @lines = lines
213 ## trim off all empty lines except one
214 @lines.pop while @lines.length > 1 && @lines[-1] =~ /^\s*$/ && @lines[-2] =~ /^\s*$/
215 end
216
217 def inlineable?; true end
218 def quotable?; true end
219 def expandable?; false end
220 def indexable?; true end
221 def viewable?; false end
222 def color; :text_color end
223 end
224
225 class Quote
226 attr_reader :lines
227 def initialize lines
228 @lines = lines
229 end
230
231 def inlineable?; @lines.length == 1 end
232 def quotable?; true end
233 def expandable?; !inlineable? end
234 def indexable?; expandable? end
235 def viewable?; false end
236
237 def patina_color; :quote_patina_color end
238 def patina_text; "(#{lines.length} quoted lines)" end
239 def color; :quote_color end
240 end
241
242 class Signature
243 attr_reader :lines
244 def initialize lines
245 @lines = lines
246 end
247
248 def inlineable?; @lines.length == 1 end
249 def quotable?; false end
250 def expandable?; !inlineable? end
251 def indexable?; expandable? end
252 def viewable?; false end
253
254 def patina_color; :sig_patina_color end
255 def patina_text; "(#{lines.length}-line signature)" end
256 def color; :sig_color end
257 end
258
259 class EnclosedMessage
260 attr_reader :lines
261 def initialize from, to, cc, date, subj
262 @from = !from ? "unknown sender" : from.full_address
263 @to = !to ? "" : to.map { |p| p.full_address }.join(", ")
264 @cc = !cc ? "" : cc.map { |p| p.full_address }.join(", ")
265 @date = !date ? "" : date.rfc822
266 @subj = subj
267 @lines = [
268 "From: #{@from}",
269 "To: #{@to}",
270 "Cc: #{@cc}",
271 "Date: #{@date}",
272 "Subject: #{@subj}"
273 ]
274 @lines.delete_if{ |line| line == 'Cc: ' }
275 end
276
277 def inlineable?; false end
278 def quotable?; false end
279 def expandable?; true end
280 def indexable?; true end
281 def initial_state; :closed end
282 def viewable?; false end
283
284 def patina_color; :generic_notice_patina_color end
285 def patina_text
286 "Begin enclosed message" + (
287 @date == "" ? "" : " sent on #{@date}"
288 )
289 end
290
291 def color; :quote_color end
292 end
293
294 class CryptoNotice
295 attr_reader :lines, :status, :patina_text, :unknown_fingerprint
296
297 def initialize status, description, lines=[], unknown_fingerprint=nil
298 @status = status
299 @patina_text = description
300 @lines = lines
301 @unknown_fingerprint = unknown_fingerprint
302 end
303
304 def patina_color
305 case status
306 when :valid then :cryptosig_valid_color
307 when :valid_untrusted then :cryptosig_valid_untrusted_color
308 when :invalid then :cryptosig_invalid_color
309 else :cryptosig_unknown_color
310 end
311 end
312 def color; patina_color end
313
314 def inlineable?; false end
315 def quotable?; false end
316 def expandable?; !@lines.empty? end
317 def indexable?; false end
318 def viewable?; false end
319 end
320 end
321 end