]> git.cworth.org Git - sup/blob - lib/sup/message-chunks.rb
Merge branch 'master' into next
[sup] / lib / sup / message-chunks.rb
1 require 'tempfile'
2
3 ## Here we define all the "chunks" that a message is parsed
4 ## into. Chunks are used by ThreadViewMode to render a message. Chunks
5 ## are used for both MIME stuff like attachments, for Sup's parsing of
6 ## the message body into text, quote, and signature regions, and for
7 ## notices like "this message was decrypted" or "this message contains
8 ## a valid signature"---basically, anything we want to differentiate
9 ## at display time.
10 ##
11 ## A chunk can be inlineable, expandable, or viewable. If it's
12 ## inlineable, #color and #lines are called and the output is treated
13 ## as part of the message text. This is how Text and one-line Quotes
14 ## and Signatures work.
15 ##
16 ## If it's not inlineable but is expandable, #patina_color and
17 ## #patina_text are called to generate a "patina" (a one-line widget,
18 ## basically), and the user can press enter to toggle the display of
19 ## the chunk content, which is generated from #color and #lines as
20 ## above. This is how Quote, Signature, and most widgets
21 ## work. Exandable chunks can additionally define #initial_state to be
22 ## :open if they want to start expanded (default is to start collapsed).
23 ##
24 ## If it's not expandable but is viewable, a patina is displayed using
25 ## #patina_color and #patina_text, but no toggling is allowed. Instead,
26 ## if #view! is defined, pressing enter on the widget calls view! and
27 ## (if that returns false) #to_s. Otherwise, enter does nothing. This
28 ##  is how non-inlineable attachments work.
29 ##
30 ## Independent of all that, a chunk can be quotable, in which case it's
31 ## included as quoted text during a reply. Text, Quotes, and mime-parsed
32 ## attachments are quotable; Signatures are not.
33
34 ## monkey-patch time: make temp files have the right extension
35 class Tempfile
36   def make_tmpname basename, n
37     sprintf '%d-%d-%s', $$, n, basename
38   end
39 end
40
41
42 module Redwood
43 module Chunk
44   class Attachment
45     HookManager.register "mime-decode", <<EOS
46 Executes when decoding a MIME attachment.
47 Variables:
48    content_type: the content-type of the message
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     HookManager.register "mime-view", <<EOS
58 Executes when viewing a MIME attachment, i.e., launching a separate
59 viewer program.
60 Variables:
61    content_type: the content-type of the attachment
62        filename: the filename of the attachment as saved to disk
63 Return value:
64   True if the viewing was successful, false otherwise.
65 EOS
66 #' stupid ruby-mode
67
68     ## raw_content is the post-MIME-decode content. this is used for
69     ## saving the attachment to disk.
70     attr_reader :content_type, :filename, :lines, :raw_content
71     bool_reader :quotable
72
73     def initialize content_type, filename, encoded_content, sibling_types
74       @content_type = content_type
75       @filename = filename
76       @quotable = false # changed to true if we can parse it through the
77                         # mime-decode hook, or if it's plain text
78       @raw_content =
79         if encoded_content.body
80           encoded_content.decode
81         else
82           "For some bizarre reason, RubyMail was unable to parse this attachment.\n"
83         end
84
85       text =
86         case @content_type
87         when /^text\/plain\b/
88           Message.convert_from @raw_content, encoded_content.charset
89         else
90           HookManager.run "mime-decode", :content_type => content_type,
91                           :filename => lambda { write_to_disk },
92                           :sibling_types => sibling_types
93         end
94
95       @lines = nil
96       if text
97         @lines = text.gsub("\r\n", "\n").gsub(/\t/, "        ").gsub(/\r/, "").split("\n")
98         @quotable = true
99       end
100     end
101
102     def color; :none end
103     def patina_color; :attachment_color end
104     def patina_text
105       if expandable?
106         "Attachment: #{filename} (#{lines.length} lines)"
107       else
108         "Attachment: #{filename} (#{content_type})"
109       end
110     end
111
112     ## an attachment is exapndable if we've managed to decode it into
113     ## something we can display inline. otherwise, it's viewable.
114     def inlineable?; false end
115     def expandable?; !viewable? end
116     def initial_state; :open end
117     def viewable?; @lines.nil? end
118     def view_default! path
119       cmd = "/usr/bin/run-mailcap --action=view '#{@content_type}:#{path}' 2>/dev/null"
120       Redwood::log "running: #{cmd.inspect}"
121       system cmd
122       $? == 0
123     end
124
125     def view!
126       path = write_to_disk
127       ret = HookManager.run "mime-view", :content_type => @content_type,
128                                          :filename => path
129       ret || view_default!(path)
130     end
131
132     def write_to_disk
133       file = Tempfile.new(@filename || "sup-attachment")
134       file.print @raw_content
135       file.close
136       file.path
137     end
138
139     ## used when viewing the attachment as text
140     def to_s
141       @lines || @raw_content
142     end
143   end
144
145   class Text
146     WRAP_LEN = 80 # wrap at this width
147
148     attr_reader :lines
149     def initialize lines
150       @lines = lines.map { |l| l.chomp.wrap WRAP_LEN }.flatten # wrap
151
152       ## trim off all empty lines except one
153       @lines.pop while @lines.length > 1 && @lines[-1] =~ /^\s*$/ && @lines[-2] =~ /^\s*$/
154     end
155
156     def inlineable?; true end
157     def quotable?; true end
158     def expandable?; false end
159     def viewable?; false end
160     def color; :none end
161   end
162
163   class Quote
164     attr_reader :lines
165     def initialize lines
166       @lines = lines
167     end
168     
169     def inlineable?; @lines.length == 1 end
170     def quotable?; true end
171     def expandable?; !inlineable? end
172     def viewable?; false end
173
174     def patina_color; :quote_patina_color end
175     def patina_text; "(#{lines.length} quoted lines)" end
176     def color; :quote_color end
177   end
178
179   class Signature
180     attr_reader :lines
181     def initialize lines
182       @lines = lines
183     end
184
185     def inlineable?; @lines.length == 1 end
186     def quotable?; false end
187     def expandable?; !inlineable? end
188     def viewable?; false end
189
190     def patina_color; :sig_patina_color end
191     def patina_text; "(#{lines.length}-line signature)" end
192     def color; :sig_color end
193   end
194
195   class EnclosedMessage
196     attr_reader :lines
197     def initialize from, body
198       @from = from
199       @lines = body.split "\n"
200     end
201
202     def from
203       @from ? @from.longname : "unknown sender"
204     end
205
206     def inlineable?; false end
207     def quotable?; false end
208     def expandable?; true end
209     def initial_state; :closed end
210     def viewable?; false end
211
212     def patina_color; :generic_notice_patina_color end
213     def patina_text; "Begin enclosed message from #{from} (#{@lines.length} lines)" end
214
215     def color; :quote_color end
216   end
217
218   class CryptoNotice
219     attr_reader :lines, :status, :patina_text
220
221     def initialize status, description, lines=[]
222       @status = status
223       @patina_text = description
224       @lines = lines
225     end
226
227     def patina_color
228       case status
229       when :valid: :cryptosig_valid_color
230       when :invalid: :cryptosig_invalid_color
231       else :cryptosig_unknown_color
232       end
233     end
234     def color; patina_color end
235
236     def inlineable?; false end
237     def quotable?; false end
238     def expandable?; !@lines.empty? end
239     def viewable?; false end
240   end
241 end
242 end