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