sup 0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sup might be problematic. Click here for more details.

Files changed (48) hide show
  1. data/History.txt +9 -0
  2. data/Manifest.txt +5 -1
  3. data/Rakefile +2 -1
  4. data/bin/sup +27 -10
  5. data/bin/sup-add +2 -1
  6. data/bin/sup-sync-back +51 -23
  7. data/doc/FAQ.txt +29 -37
  8. data/doc/Hooks.txt +38 -0
  9. data/doc/{UserGuide.txt → NewUserGuide.txt} +27 -21
  10. data/doc/TODO +91 -57
  11. data/lib/sup.rb +17 -1
  12. data/lib/sup/buffer.rb +80 -16
  13. data/lib/sup/colormap.rb +0 -2
  14. data/lib/sup/contact.rb +3 -2
  15. data/lib/sup/crypto.rb +110 -0
  16. data/lib/sup/draft.rb +2 -6
  17. data/lib/sup/hook.rb +131 -0
  18. data/lib/sup/imap.rb +27 -16
  19. data/lib/sup/index.rb +38 -14
  20. data/lib/sup/keymap.rb +0 -2
  21. data/lib/sup/label.rb +30 -9
  22. data/lib/sup/logger.rb +12 -1
  23. data/lib/sup/maildir.rb +48 -3
  24. data/lib/sup/mbox.rb +1 -1
  25. data/lib/sup/mbox/loader.rb +22 -12
  26. data/lib/sup/mbox/ssh-loader.rb +1 -1
  27. data/lib/sup/message-chunks.rb +198 -0
  28. data/lib/sup/message.rb +154 -115
  29. data/lib/sup/modes/compose-mode.rb +18 -0
  30. data/lib/sup/modes/contact-list-mode.rb +1 -1
  31. data/lib/sup/modes/edit-message-mode.rb +112 -31
  32. data/lib/sup/modes/file-browser-mode.rb +1 -1
  33. data/lib/sup/modes/inbox-mode.rb +1 -1
  34. data/lib/sup/modes/label-list-mode.rb +8 -6
  35. data/lib/sup/modes/label-search-results-mode.rb +4 -1
  36. data/lib/sup/modes/log-mode.rb +1 -1
  37. data/lib/sup/modes/reply-mode.rb +18 -16
  38. data/lib/sup/modes/search-results-mode.rb +1 -1
  39. data/lib/sup/modes/thread-index-mode.rb +61 -33
  40. data/lib/sup/modes/thread-view-mode.rb +111 -102
  41. data/lib/sup/person.rb +5 -1
  42. data/lib/sup/poll.rb +36 -7
  43. data/lib/sup/sent.rb +1 -0
  44. data/lib/sup/source.rb +7 -3
  45. data/lib/sup/textfield.rb +48 -34
  46. data/lib/sup/thread.rb +9 -5
  47. data/lib/sup/util.rb +16 -22
  48. metadata +7 -3
@@ -15,9 +15,8 @@ class MessageFormatError < StandardError; end
15
15
  ## sequences in the text of an email. (how sweet would that be?)
16
16
  class Message
17
17
  SNIPPET_LEN = 80
18
- WRAP_LEN = 80 # wrap at this width
19
18
  RE_PATTERN = /^((re|re[\[\(]\d[\]\)]):\s*)+/i
20
-
19
+
21
20
  ## some utility methods
22
21
  class << self
23
22
  def normalize_subj s; s.gsub(RE_PATTERN, ""); end
@@ -25,63 +24,13 @@ class Message
25
24
  def reify_subj s; subj_is_reply?(s) ? s : "Re: " + s; end
26
25
  end
27
26
 
28
- class Attachment
29
- attr_reader :content_type, :filename, :content, :lines
30
- def initialize content_type, filename, content
31
- @content_type = content_type
32
- @filename = filename
33
- @content = content
34
-
35
- if inlineable?
36
- @lines = to_s.split("\n")
37
- end
38
- end
39
-
40
- def view!
41
- file = Tempfile.new "redwood.attachment"
42
- file.print raw_content
43
- file.close
44
-
45
- system "/usr/bin/run-mailcap --action=view #{@content_type}:#{file.path} >& /dev/null"
46
- $? == 0
47
- end
48
-
49
- def to_s; Message.decode_and_convert @content; end
50
- def raw_content; @content.decode end
51
-
52
- def inlineable?; @content_type =~ /^text\/plain/ end
53
- end
54
-
55
- class Text
56
- attr_reader :lines
57
- def initialize lines
58
- ## do some wrapping
59
- @lines = lines.map { |l| l.chomp.wrap WRAP_LEN }.flatten
60
- end
61
- end
62
-
63
- class Quote
64
- attr_reader :lines
65
- def initialize lines
66
- @lines = lines
67
- end
68
- end
69
-
70
- class Signature
71
- attr_reader :lines
72
- def initialize lines
73
- @lines = lines
74
- end
75
- end
76
-
77
-
78
27
  QUOTE_PATTERN = /^\s{0,4}[>|\}]/
79
28
  BLOCK_QUOTE_PATTERN = /^-----\s*Original Message\s*----+$/
80
29
  QUOTE_START_PATTERN = /(^\s*Excerpts from)|(^\s*In message )|(^\s*In article )|(^\s*Quoting )|((wrote|writes|said|says)\s*:\s*$)/
81
- SIG_PATTERN = /(^-- ?$)|(^\s*----------+\s*$)|(^\s*_________+\s*$)|(^\s*--~--~-)/
30
+ SIG_PATTERN = /(^-- ?$)|(^\s*----------+\s*$)|(^\s*_________+\s*$)|(^\s*--~--~-)|(^\s*--\+\+\*\*==)/
82
31
 
83
32
  MAX_SIG_DISTANCE = 15 # lines from the end
84
- DEFAULT_SUBJECT = "(missing subject)"
33
+ DEFAULT_SUBJECT = ""
85
34
  DEFAULT_SENDER = "(missing sender)"
86
35
 
87
36
  attr_reader :id, :date, :from, :subj, :refs, :replytos, :to, :source,
@@ -90,8 +39,8 @@ class Message
90
39
 
91
40
  bool_reader :dirty, :source_marked_read
92
41
 
93
- ## if you specify a :header, will use values from that. otherwise, will try and
94
- ## load the header from the source.
42
+ ## if you specify a :header, will use values from that. otherwise,
43
+ ## will try and load the header from the source.
95
44
  def initialize opts
96
45
  @source = opts[:source] or raise ArgumentError, "source can't be nil"
97
46
  @source_info = opts[:source_info] or raise ArgumentError, "source_info can't be nil"
@@ -101,32 +50,45 @@ class Message
101
50
  @dirty = false
102
51
  @chunks = nil
103
52
 
104
- read_header(opts[:header] || @source.load_header(@source_info))
53
+ parse_header(opts[:header] || @source.load_header(@source_info))
105
54
  end
106
55
 
107
- def read_header header
56
+ def parse_header header
108
57
  header.each { |k, v| header[k.downcase] = v }
58
+
59
+ @from = PersonManager.person_for header["from"]
109
60
 
110
- %w(message-id date).each do |f|
111
- raise MessageFormatError, "no #{f} field in header #{header.inspect} (source #@source offset #@source_info)" unless header.include? f
112
- raise MessageFormatError, "nil #{f} field in header #{header.inspect} (source #@source offset #@source_info)" unless header[f]
113
- end
114
-
115
- begin
116
- date = header["date"]
117
- @date = Time === date ? date : Time.parse(header["date"])
118
- rescue ArgumentError => e
119
- raise MessageFormatError, "unparsable date #{header['date']}: #{e.message}"
120
- end
61
+ @id =
62
+ if header["message-id"]
63
+ sanitize_message_id header["message-id"]
64
+ else
65
+ "sup-faked-" + Digest::MD5.hexdigest(raw_header)
66
+ Redwood::log "faking message-id for message from #@from: #@id"
67
+ end
68
+
69
+ date = header["date"]
70
+ @date =
71
+ case date
72
+ when Time
73
+ date
74
+ when String
75
+ begin
76
+ Time.parse date
77
+ rescue ArgumentError => e
78
+ raise MessageFormatError, "unparsable date #{header['date']}: #{e.message}"
79
+ end
80
+ else
81
+ Redwood::log "faking date header for #{@id}"
82
+ Time.now
83
+ end
121
84
 
122
85
  @subj = header.member?("subject") ? header["subject"].gsub(/\s+/, " ").gsub(/\s+$/, "") : DEFAULT_SUBJECT
123
- @from = PersonManager.person_for header["from"]
124
86
  @to = PersonManager.people_for header["to"]
125
87
  @cc = PersonManager.people_for header["cc"]
126
88
  @bcc = PersonManager.people_for header["bcc"]
127
- @id = header["message-id"]
128
- @refs = (header["references"] || "").gsub(/[<>]/, "").split(/\s+/).flatten
129
- @replytos = (header["in-reply-to"] || "").scan(/<(.*?)>/).flatten
89
+ @refs = (header["references"] || "").scan(/<(.+?)>/).map { |x| sanitize_message_id x.first }
90
+ @replytos = (header["in-reply-to"] || "").scan(/<(.+?)>/).map { |x| sanitize_message_id x.first }
91
+
130
92
  @replyto = PersonManager.person_for header["reply-to"]
131
93
  @list_address =
132
94
  if header["list-post"]
@@ -138,7 +100,7 @@ class Message
138
100
  @recipient_email = header["envelope-to"] || header["x-original-to"] || header["delivered-to"]
139
101
  @source_marked_read = header["status"] == "RO"
140
102
  end
141
- private :read_header
103
+ private :parse_header
142
104
 
143
105
  def snippet; @snippet || chunks && @snippet; end
144
106
  def is_list_message?; !@list_address.nil?; end
@@ -148,6 +110,8 @@ class Message
148
110
  @source.fn_for_offset @source_info
149
111
  end
150
112
 
113
+ def sanitize_message_id mid; mid.gsub(/\s/, "") end
114
+
151
115
  def save index
152
116
  index.sync_message self if @dirty
153
117
  @dirty = false
@@ -178,7 +142,7 @@ class Message
178
142
  def load_from_source!
179
143
  @chunks ||=
180
144
  if @source.has_errors?
181
- [Text.new(error_message(@source.error.message.split("\n")))]
145
+ [Chunk::Text.new(error_message(@source.error.message.split("\n")))]
182
146
  else
183
147
  begin
184
148
  ## we need to re-read the header because it contains information
@@ -189,14 +153,14 @@ class Message
189
153
  ## bloat the index.
190
154
  ## actually, it's also the differentiation between to/cc/bcc,
191
155
  ## so i will keep this.
192
- read_header @source.load_header(@source_info)
156
+ parse_header @source.load_header(@source_info)
193
157
  message_to_chunks @source.load_message(@source_info)
194
158
  rescue SourceError, SocketError, MessageFormatError => e
195
159
  Redwood::log "problem getting messages from #{@source}: #{e.message}"
196
160
  ## we need force_to_top here otherwise this window will cover
197
161
  ## up the error message one
198
162
  Redwood::report_broken_sources :force_to_top => true
199
- [Text.new(error_message(e.message))]
163
+ [Chunk::Text.new(error_message(e.message))]
200
164
  end
201
165
  end
202
166
  end
@@ -220,22 +184,26 @@ The error message was:
220
184
  EOS
221
185
  end
222
186
 
223
- def raw_header
187
+ def with_source_errors_handled
224
188
  begin
225
- @source.raw_header @source_info
189
+ yield
226
190
  rescue SourceError => e
227
191
  Redwood::log "problem getting messages from #{@source}: #{e.message}"
228
192
  error_message e.message
229
193
  end
230
194
  end
231
195
 
232
- def raw_full_message
233
- begin
234
- @source.raw_full_message @source_info
235
- rescue SourceError => e
236
- Redwood::log "problem getting messages from #{@source}: #{e.message}"
237
- error_message(e.message)
238
- end
196
+ def raw_header
197
+ with_source_errors_handled { @source.raw_header @source_info }
198
+ end
199
+
200
+ def raw_message
201
+ with_source_errors_handled { @source.raw_message @source_info }
202
+ end
203
+
204
+ ## much faster than raw_message
205
+ def each_raw_message_line &b
206
+ with_source_errors_handled { @source.each_raw_message_line(@source_info, &b) }
239
207
  end
240
208
 
241
209
  def content
@@ -245,13 +213,13 @@ EOS
245
213
  to.map { |p| "#{p.name} #{p.email}" },
246
214
  cc.map { |p| "#{p.name} #{p.email}" },
247
215
  bcc.map { |p| "#{p.name} #{p.email}" },
248
- chunks.select { |c| c.is_a? Text }.map { |c| c.lines },
216
+ chunks.select { |c| c.is_a? Chunk::Text }.map { |c| c.lines },
249
217
  Message.normalize_subj(subj),
250
218
  ].flatten.compact.join " "
251
219
  end
252
220
 
253
221
  def basic_body_lines
254
- chunks.find_all { |c| c.is_a?(Text) || c.is_a?(Quote) }.map { |c| c.lines }.flatten
222
+ chunks.find_all { |c| c.is_a?(Chunk::Text) || c.is_a?(Chunk::Quote) }.map { |c| c.lines }.flatten
255
223
  end
256
224
 
257
225
  def basic_header_lines
@@ -274,18 +242,95 @@ private
274
242
  ## the general behavior i want is: ignore content-disposition, at
275
243
  ## least in so far as it suggests something being inline vs being an
276
244
  ## attachment. (because really, that should be the recipient's
277
- ## decision to make.) if a mime part is text/plain, then decode it
278
- ## and display it inline. if it has associated filename, then make
279
- ## it collapsable and individually saveable; otherwise, treat it as
280
- ## regular body text.
245
+ ## decision to make.) if a mime part is text/plain, OR if the user
246
+ ## decoding hook converts it, then decode it and display it
247
+ ## inline. for these decoded attachments, if it has associated
248
+ ## filename, then make it collapsable and individually saveable;
249
+ ## otherwise, treat it as regular body text.
250
+ ##
251
+ ## everything else is just an attachment and is not displayed
252
+ ## inline.
281
253
  ##
282
254
  ## so, in contrast to mutt, the user is not exposed to the workings
283
255
  ## of the gruesome slaughterhouse and sausage factory that is a
284
256
  ## mime-encoded message, but need only see the delicious end
285
257
  ## product.
286
- def message_to_chunks m
258
+
259
+ def multipart_signed_to_chunks m
260
+ # Redwood::log ">> multipart SIGNED: #{m.header['Content-Type']}: #{m.body.size}"
261
+ if m.body.size != 2
262
+ Redwood::log "warning: multipart/signed with #{m.body.size} parts (expecting 2)"
263
+ return
264
+ end
265
+
266
+ payload, signature = m.body
267
+ if signature.multipart?
268
+ Redwood::log "warning: multipart/signed with payload multipart #{payload.multipart?} and signature multipart #{signature.multipart?}"
269
+ return
270
+ end
271
+
272
+ if payload.header.content_type == "application/pgp-signature"
273
+ Redwood::log "warning: multipart/signed with payload content type #{payload.header.content_type}"
274
+ return
275
+ end
276
+
277
+ if signature.header.content_type != "application/pgp-signature"
278
+ Redwood::log "warning: multipart/signed with signature content type #{signature.header.content_type}"
279
+ return
280
+ end
281
+
282
+ [CryptoManager.verify(payload, signature), message_to_chunks(payload)].flatten.compact
283
+ end
284
+
285
+ def multipart_encrypted_to_chunks m
286
+ Redwood::log ">> multipart ENCRYPTED: #{m.header['Content-Type']}: #{m.body.size}"
287
+ if m.body.size != 2
288
+ Redwood::log "warning: multipart/encrypted with #{m.body.size} parts (expecting 2)"
289
+ return
290
+ end
291
+
292
+ control, payload = m.body
293
+ if control.multipart?
294
+ Redwood::log "warning: multipart/encrypted with control multipart #{control.multipart?} and payload multipart #{payload.multipart?}"
295
+ return
296
+ end
297
+
298
+ if payload.header.content_type != "application/octet-stream"
299
+ Redwood::log "warning: multipart/encrypted with payload content type #{payload.header.content_type}"
300
+ return
301
+ end
302
+
303
+ if control.header.content_type != "application/pgp-encrypted"
304
+ Redwood::log "warning: multipart/encrypted with control content type #{signature.header.content_type}"
305
+ return
306
+ end
307
+
308
+ decryptedm, sig, notice = CryptoManager.decrypt payload
309
+ children = message_to_chunks(decryptedm) if decryptedm
310
+ [notice, sig, children].flatten.compact
311
+ end
312
+
313
+ def message_to_chunks m, sibling_types=[]
287
314
  if m.multipart?
288
- m.body.map { |p| message_to_chunks p }.flatten.compact # recurse
315
+ chunks =
316
+ case m.header.content_type
317
+ when "multipart/signed"
318
+ multipart_signed_to_chunks m
319
+ when "multipart/encrypted"
320
+ multipart_encrypted_to_chunks m
321
+ end
322
+
323
+ unless chunks
324
+ sibling_types = m.body.map { |p| p.header.content_type }
325
+ chunks = m.body.map { |p| message_to_chunks p, sibling_types }.flatten.compact
326
+ end
327
+
328
+ chunks
329
+ elsif m.header.content_type == "message/rfc822"
330
+ payload = RMail::Parser.read(m.body)
331
+ from = payload.header.from.first
332
+ from_person = from ? PersonManager.person_for(from.format) : nil
333
+ [Chunk::EnclosedMessage.new(from_person, payload.to_s)]
289
334
  else
290
335
  filename =
291
336
  ## first, paw through the headers looking for a filename
@@ -304,32 +349,26 @@ private
304
349
 
305
350
  ## if there's a filename, we'll treat it as an attachment.
306
351
  if filename
307
- [Attachment.new(m.header.content_type, filename, m)]
352
+ [Chunk::Attachment.new(m.header.content_type, filename, m, sibling_types)]
308
353
 
309
354
  ## otherwise, it's body text
310
355
  else
311
- body = Message.decode_and_convert m
356
+ body = Message.convert_from m.decode, m.charset
312
357
  text_to_chunks body.normalize_whitespace.split("\n")
313
358
  end
314
359
  end
315
360
  end
316
361
 
317
- def self.decode_and_convert m
318
- charset =
319
- if m.header.field?("content-type") && m.header.fetch("content-type") =~ /charset=(.*?)(;|$)/
320
- $1
321
- end
322
-
323
- m.body && body = m.decode or raise MessageFormatError, "For some bizarre reason, RubyMail was unable to parse this message."
362
+ def self.convert_from body, charset
363
+ return body unless charset
324
364
 
325
- if charset
326
- begin
327
- body = Iconv.iconv($encoding, charset, body).join
328
- rescue Errno::EINVAL, Iconv::InvalidEncoding, Iconv::IllegalSequence => e
329
- Redwood::log "warning: error decoding message body from #{charset}: #{e.message}"
330
- end
365
+ begin
366
+ Iconv.iconv($encoding, charset, body).join
367
+ rescue Errno::EINVAL, Iconv::InvalidEncoding, Iconv::IllegalSequence => e
368
+ Redwood::log "warning: error (#{e.class.name}) decoding message body from #{charset}: #{e.message}"
369
+ File.open("sup-unable-to-decode.txt", "w") { |f| f.write body }
370
+ body
331
371
  end
332
- body
333
372
  end
334
373
 
335
374
  ## parse the lines of text into chunk objects. the heuristics here
@@ -356,7 +395,7 @@ private
356
395
  end
357
396
 
358
397
  if newstate
359
- chunks << Text.new(chunk_lines) unless chunk_lines.empty?
398
+ chunks << Chunk::Text.new(chunk_lines) unless chunk_lines.empty?
360
399
  chunk_lines = [line]
361
400
  state = newstate
362
401
  else
@@ -378,7 +417,7 @@ private
378
417
  if chunk_lines.empty?
379
418
  # nothing
380
419
  else
381
- chunks << Quote.new(chunk_lines)
420
+ chunks << Chunk::Quote.new(chunk_lines)
382
421
  end
383
422
  chunk_lines = [line]
384
423
  state = newstate
@@ -398,11 +437,11 @@ private
398
437
  ## final object
399
438
  case state
400
439
  when :quote, :block_quote
401
- chunks << Quote.new(chunk_lines) unless chunk_lines.empty?
440
+ chunks << Chunk::Quote.new(chunk_lines) unless chunk_lines.empty?
402
441
  when :text
403
- chunks << Text.new(chunk_lines) unless chunk_lines.empty?
442
+ chunks << Chunk::Text.new(chunk_lines) unless chunk_lines.empty?
404
443
  when :sig
405
- chunks << Signature.new(chunk_lines) unless chunk_lines.empty?
444
+ chunks << Chunk::Signature.new(chunk_lines) unless chunk_lines.empty?
406
445
  end
407
446
  chunks
408
447
  end
@@ -1,5 +1,17 @@
1
1
  module Redwood
2
2
 
3
+ module CanSpawnComposeMode
4
+ def spawn_compose_mode opts={}
5
+ to = opts[:to] || BufferManager.ask_for_contacts(:people, "To: ") or return
6
+ cc = opts[:cc] || BufferManager.ask_for_contacts(:people, "Cc: ") or return if $config[:ask_for_cc]
7
+ bcc = opts[:bcc] || BufferManager.ask_for_contacts(:people, "Bcc: ") or return if $config[:ask_for_bcc]
8
+
9
+ mode = ComposeMode.new :to => to, :cc => cc, :bcc => bcc
10
+ BufferManager.spawn "New Message", mode
11
+ mode.edit_message
12
+ end
13
+ end
14
+
3
15
  class ComposeMode < EditMessageMode
4
16
  def initialize opts={}
5
17
  header = {
@@ -13,6 +25,12 @@ class ComposeMode < EditMessageMode
13
25
 
14
26
  super :header => header, :body => (opts[:body] || [])
15
27
  end
28
+
29
+ def edit_message
30
+ edited = super
31
+ BufferManager.kill_buffer self.buffer unless edited
32
+ edited
33
+ end
16
34
  end
17
35
 
18
36
  end
@@ -67,7 +67,7 @@ class ContactListMode < LineCursorMode
67
67
  when :regular
68
68
  mode = ComposeMode.new :to => people
69
69
  BufferManager.spawn "new message", mode
70
- mode.edit
70
+ mode.edit_message
71
71
  end
72
72
  end
73
73