]> git.cworth.org Git - sup/blob - lib/sup/message-chunks.rb
bugfix: attachment tempfile deletion
[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 #' stupid ruby-mode
57
58     ## raw_content is the post-MIME-decode content. this is used for
59     ## saving the attachment to disk.
60     attr_reader :content_type, :filename, :lines, :raw_content
61     bool_reader :quotable
62
63     def initialize content_type, filename, encoded_content, sibling_types
64       @content_type = content_type
65       @filename = filename
66       @quotable = false # changed to true if we can parse it through the
67                         # mime-decode hook, or if it's plain text
68       @raw_content =
69         if encoded_content.body
70           encoded_content.decode
71         else
72           "For some bizarre reason, RubyMail was unable to parse this attachment.\n"
73         end
74
75       @lines =
76         case @content_type
77         when /^text\/plain\b/
78           @quotable = true
79           Message.convert_from(@raw_content, encoded_content.charset).split("\n")
80         else
81           text = HookManager.run "mime-decode", :content_type => content_type,
82                                  :filename => lambda { write_to_disk },
83                                  :sibling_types => sibling_types
84           if text
85             @quotable = true
86             text.split("\n")
87           end
88         end
89     end
90
91     def color; :none end
92     def patina_color; :attachment_color end
93     def patina_text
94       if expandable?
95         "Attachment: #{filename} (#{lines.length} lines)"
96       else
97         "Attachment: #{filename} (#{content_type})"
98       end
99     end
100
101     ## an attachment is exapndable if we've managed to decode it into
102     ## something we can display inline. otherwise, it's viewable.
103     def inlineable?; false end
104     def expandable?; !viewable? end
105     def initial_state; :open end
106     def viewable?; @lines.nil? end
107     def view!
108       path = write_to_disk
109       system "/usr/bin/run-mailcap --action=view #{@content_type}:#{path} > /dev/null 2> /dev/null"
110       $? == 0
111     end
112
113     def write_to_disk
114       file = Tempfile.new(@filename || "sup-attachment")
115       file.print @raw_content
116       file.close
117       file.path
118     end
119
120     ## used when viewing the attachment as text
121     def to_s
122       @lines || @raw_content
123     end
124   end
125
126   class Text
127     WRAP_LEN = 80 # wrap at this width
128
129     attr_reader :lines
130     def initialize lines
131       @lines = lines.map { |l| l.chomp.wrap WRAP_LEN }.flatten # wrap
132
133       ## trim off all empty lines except one
134       @lines.pop while @lines.length > 1 && @lines[-1] =~ /^\s*$/ && @lines[-2] =~ /^\s*$/
135     end
136
137     def inlineable?; true end
138     def quotable?; true end
139     def expandable?; false end
140     def viewable?; false end
141     def color; :none end
142   end
143
144   class Quote
145     attr_reader :lines
146     def initialize lines
147       @lines = lines
148     end
149     
150     def inlineable?; @lines.length == 1 end
151     def quotable?; true end
152     def expandable?; !inlineable? end
153     def viewable?; false end
154
155     def patina_color; :quote_patina_color end
156     def patina_text; "(#{lines.length} quoted lines)" end
157     def color; :quote_color end
158   end
159
160   class Signature
161     attr_reader :lines
162     def initialize lines
163       @lines = lines
164     end
165
166     def inlineable?; @lines.length == 1 end
167     def quotable?; false end
168     def expandable?; !inlineable? end
169     def viewable?; false end
170
171     def patina_color; :sig_patina_color end
172     def patina_text; "(#{lines.length}-line signature)" end
173     def color; :sig_color end
174   end
175
176   class EnclosedMessage
177     attr_reader :lines
178     def initialize from, body
179       @from = from
180       @lines = body.split "\n"
181     end
182
183     def from
184       @from ? @from.longname : "unknown sender"
185     end
186
187     def inlineable?; false end
188     def quotable?; false end
189     def expandable?; true end
190     def initial_state; :open end
191     def viewable?; false end
192
193     def patina_color; :generic_notice_patina_color end
194     def patina_text; "Begin enclosed message from #{from} (#{@lines.length} lines)" end
195
196     def color; :quote_color end
197   end
198
199   class CryptoNotice
200     attr_reader :lines, :status, :patina_text
201
202     def initialize status, description, lines=[]
203       @status = status
204       @patina_text = description
205       @lines = lines
206     end
207
208     def patina_color
209       case status
210       when :valid: :cryptosig_valid_color
211       when :invalid: :cryptosig_invalid_color
212       else :cryptosig_unknown_color
213       end
214     end
215     def color; patina_color end
216
217     def inlineable?; false end
218     def quotable?; false end
219     def expandable?; !@lines.empty? end
220     def viewable?; false end
221   end
222 end
223 end