sup 0.8.1 → 0.9

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 (67) hide show
  1. data/CONTRIBUTORS +13 -6
  2. data/History.txt +19 -0
  3. data/ReleaseNotes +35 -0
  4. data/bin/sup +82 -77
  5. data/bin/sup-add +7 -7
  6. data/bin/sup-config +104 -85
  7. data/bin/sup-dump +4 -5
  8. data/bin/sup-recover-sources +9 -10
  9. data/bin/sup-sync +121 -100
  10. data/bin/sup-sync-back +18 -15
  11. data/bin/sup-tweak-labels +24 -21
  12. data/lib/sup.rb +53 -33
  13. data/lib/sup/account.rb +0 -2
  14. data/lib/sup/buffer.rb +47 -22
  15. data/lib/sup/colormap.rb +6 -6
  16. data/lib/sup/contact.rb +0 -2
  17. data/lib/sup/crypto.rb +34 -23
  18. data/lib/sup/draft.rb +6 -14
  19. data/lib/sup/ferret_index.rb +471 -0
  20. data/lib/sup/hook.rb +30 -43
  21. data/lib/sup/hook.rb.BACKUP.8625.rb +158 -0
  22. data/lib/sup/hook.rb.BACKUP.8681.rb +158 -0
  23. data/lib/sup/hook.rb.BASE.8625.rb +155 -0
  24. data/lib/sup/hook.rb.BASE.8681.rb +155 -0
  25. data/lib/sup/hook.rb.LOCAL.8625.rb +142 -0
  26. data/lib/sup/hook.rb.LOCAL.8681.rb +142 -0
  27. data/lib/sup/hook.rb.REMOTE.8625.rb +145 -0
  28. data/lib/sup/hook.rb.REMOTE.8681.rb +145 -0
  29. data/lib/sup/imap.rb +18 -8
  30. data/lib/sup/index.rb +70 -528
  31. data/lib/sup/interactive-lock.rb +74 -0
  32. data/lib/sup/keymap.rb +26 -26
  33. data/lib/sup/label.rb +2 -4
  34. data/lib/sup/logger.rb +54 -35
  35. data/lib/sup/maildir.rb +41 -6
  36. data/lib/sup/mbox.rb +1 -1
  37. data/lib/sup/mbox/loader.rb +18 -6
  38. data/lib/sup/mbox/ssh-file.rb +1 -7
  39. data/lib/sup/message-chunks.rb +36 -23
  40. data/lib/sup/message.rb +126 -46
  41. data/lib/sup/mode.rb +3 -2
  42. data/lib/sup/modes/console-mode.rb +108 -0
  43. data/lib/sup/modes/edit-message-mode.rb +15 -5
  44. data/lib/sup/modes/inbox-mode.rb +2 -4
  45. data/lib/sup/modes/label-list-mode.rb +1 -1
  46. data/lib/sup/modes/line-cursor-mode.rb +18 -18
  47. data/lib/sup/modes/log-mode.rb +29 -16
  48. data/lib/sup/modes/poll-mode.rb +7 -9
  49. data/lib/sup/modes/reply-mode.rb +5 -3
  50. data/lib/sup/modes/scroll-mode.rb +2 -2
  51. data/lib/sup/modes/search-results-mode.rb +9 -11
  52. data/lib/sup/modes/text-mode.rb +2 -2
  53. data/lib/sup/modes/thread-index-mode.rb +26 -16
  54. data/lib/sup/modes/thread-view-mode.rb +84 -39
  55. data/lib/sup/person.rb +6 -8
  56. data/lib/sup/poll.rb +46 -47
  57. data/lib/sup/rfc2047.rb +1 -5
  58. data/lib/sup/sent.rb +27 -20
  59. data/lib/sup/source.rb +90 -13
  60. data/lib/sup/textfield.rb +4 -4
  61. data/lib/sup/thread.rb +15 -13
  62. data/lib/sup/undo.rb +0 -1
  63. data/lib/sup/update.rb +0 -1
  64. data/lib/sup/util.rb +51 -43
  65. data/lib/sup/xapian_index.rb +566 -0
  66. metadata +57 -46
  67. data/lib/sup/suicide.rb +0 -36
@@ -16,12 +16,6 @@ class SSHFileError < StandardError; end
16
16
  ## all of the methods here can throw SSHFileErrors, SocketErrors,
17
17
  ## Net::SSH::Exceptions and Errno::ENOENTs.
18
18
 
19
- ## debugging TODO: remove me
20
- def debug s
21
- Redwood::log s
22
- end
23
- module_function :debug
24
-
25
19
  ## a simple buffer of contiguous data
26
20
  class Buffer
27
21
  def initialize
@@ -154,7 +148,7 @@ private
154
148
  ## TODO: share this code with imap
155
149
  def say s
156
150
  @say_id = BufferManager.say s, @say_id if BufferManager.instantiated?
157
- Redwood::log s
151
+ info s
158
152
  end
159
153
 
160
154
  def shutup
@@ -50,7 +50,8 @@ directly in Sup. For attachments that you wish to use a separate program
50
50
  to view (e.g. images), you should use the mime-view hook instead.
51
51
 
52
52
  Variables:
53
- content_type: the content-type of the message
53
+ content_type: the content-type of the attachment
54
+ charset: the charset of the attachment, if applicable
54
55
  filename: the filename of the attachment as saved to disk
55
56
  sibling_types: if this attachment is part of a multipart MIME attachment,
56
57
  an array of content-types for all attachments. Otherwise,
@@ -85,7 +86,7 @@ EOS
85
86
  bool_reader :quotable
86
87
 
87
88
  def initialize content_type, filename, encoded_content, sibling_types
88
- @content_type = content_type
89
+ @content_type = content_type.downcase
89
90
  @filename = filename
90
91
  @quotable = false # changed to true if we can parse it through the
91
92
  # mime-decode hook, or if it's plain text
@@ -96,15 +97,15 @@ EOS
96
97
  "For some bizarre reason, RubyMail was unable to parse this attachment.\n"
97
98
  end
98
99
 
99
- text =
100
- case @content_type
101
- when /^text\/plain\b/
102
- Iconv.easy_decode $encoding, encoded_content.charset || $encoding, @raw_content
103
- else
104
- HookManager.run "mime-decode", :content_type => content_type,
105
- :filename => lambda { write_to_disk },
106
- :sibling_types => sibling_types
107
- end
100
+ text = case @content_type
101
+ when /^text\/plain\b/
102
+ Iconv.easy_decode $encoding, encoded_content.charset || $encoding, @raw_content
103
+ else
104
+ HookManager.run "mime-decode", :content_type => content_type,
105
+ :filename => lambda { write_to_disk },
106
+ :charset => encoded_content.charset,
107
+ :sibling_types => sibling_types
108
+ end
108
109
 
109
110
  @lines = nil
110
111
  if text
@@ -131,9 +132,9 @@ EOS
131
132
  def initial_state; :open end
132
133
  def viewable?; @lines.nil? end
133
134
  def view_default! path
134
- cmd = "/usr/bin/run-mailcap --action=view '#{@content_type}:#{path}' 2>/dev/null"
135
- Redwood::log "running: #{cmd.inspect}"
136
- system cmd
135
+ cmd = "/usr/bin/run-mailcap --action=view '#{@content_type}:#{path}'"
136
+ debug "running: #{cmd.inspect}"
137
+ BufferManager.shell_out(cmd)
137
138
  $? == 0
138
139
  end
139
140
 
@@ -208,13 +209,25 @@ EOS
208
209
 
209
210
  class EnclosedMessage
210
211
  attr_reader :lines
211
- def initialize from, body
212
- @from = from
213
- @lines = body.split "\n"
214
- end
212
+ def initialize from, to, cc, date, subj
213
+ @from = from ? "unknown sender" : from.full_adress
214
+ @to = to ? "" : to.map { |p| p.full_address }.join(", ")
215
+ @cc = cc ? "" : cc.map { |p| p.full_address }.join(", ")
216
+ if date
217
+ @date = date.rfc822
218
+ else
219
+ @date = ""
220
+ end
215
221
 
216
- def from
217
- @from ? @from.longname : "unknown sender"
222
+ @subj = subj
223
+
224
+ @lines = "\nFrom: #{from}\n"
225
+ @lines += "To: #{to}\n"
226
+ if !cc.empty?
227
+ @lines += "Cc: #{cc}\n"
228
+ end
229
+ @lines += "Date: #{date}\n"
230
+ @lines += "Subject: #{subj}\n\n"
218
231
  end
219
232
 
220
233
  def inlineable?; false end
@@ -224,7 +237,7 @@ EOS
224
237
  def viewable?; false end
225
238
 
226
239
  def patina_color; :generic_notice_patina_color end
227
- def patina_text; "Begin enclosed message from #{from} (#{@lines.length} lines)" end
240
+ def patina_text; "Begin enclosed message sent on #{@date}" end
228
241
 
229
242
  def color; :quote_color end
230
243
  end
@@ -240,8 +253,8 @@ EOS
240
253
 
241
254
  def patina_color
242
255
  case status
243
- when :valid: :cryptosig_valid_color
244
- when :invalid: :cryptosig_invalid_color
256
+ when :valid then :cryptosig_valid_color
257
+ when :invalid then :cryptosig_invalid_color
245
258
  else :cryptosig_unknown_color
246
259
  end
247
260
  end
@@ -46,7 +46,7 @@ class Message
46
46
  @snippet = opts[:snippet]
47
47
  @snippet_contains_encrypted_content = false
48
48
  @have_snippet = !(opts[:snippet].nil? || opts[:snippet].empty?)
49
- @labels = (opts[:labels] || []).to_set_of_symbols
49
+ @labels = Set.new(opts[:labels] || [])
50
50
  @dirty = false
51
51
  @encrypted = false
52
52
  @chunks = nil
@@ -73,7 +73,7 @@ class Message
73
73
  else
74
74
  id = "sup-faked-" + Digest::MD5.hexdigest(raw_header)
75
75
  from = header["from"]
76
- #Redwood::log "faking non-existent message-id for message from #{from}: #{id}"
76
+ #debug "faking non-existent message-id for message from #{from}: #{id}"
77
77
  id
78
78
  end
79
79
 
@@ -81,7 +81,7 @@ class Message
81
81
  header["from"]
82
82
  else
83
83
  name = "Sup Auto-generated Fake Sender <sup@fake.sender.example.com>"
84
- #Redwood::log "faking non-existent sender for message #@id: #{name}"
84
+ #debug "faking non-existent sender for message #@id: #{name}"
85
85
  name
86
86
  end)
87
87
 
@@ -92,11 +92,11 @@ class Message
92
92
  begin
93
93
  Time.parse date
94
94
  rescue ArgumentError => e
95
- #Redwood::log "faking mangled date header for #{@id} (orig #{header['date'].inspect} gave error: #{e.message})"
95
+ #debug "faking mangled date header for #{@id} (orig #{header['date'].inspect} gave error: #{e.message})"
96
96
  Time.now
97
97
  end
98
98
  else
99
- #Redwood::log "faking non-existent date header for #{@id}"
99
+ #debug "faking non-existent date header for #{@id}"
100
100
  Time.now
101
101
  end
102
102
 
@@ -127,6 +127,31 @@ class Message
127
127
  @list_unsubscribe = header["list-unsubscribe"]
128
128
  end
129
129
 
130
+ ## Expected index entry format:
131
+ ## :message_id, :subject => String
132
+ ## :date => Time
133
+ ## :refs, :replytos => Array of String
134
+ ## :from => Person
135
+ ## :to, :cc, :bcc => Array of Person
136
+ def load_from_index! entry
137
+ @id = entry[:message_id]
138
+ @from = entry[:from]
139
+ @date = entry[:date]
140
+ @subj = entry[:subject]
141
+ @to = entry[:to]
142
+ @cc = entry[:cc]
143
+ @bcc = entry[:bcc]
144
+ @refs = (@refs + entry[:refs]).uniq
145
+ @replytos = entry[:replytos]
146
+
147
+ @replyto = nil
148
+ @list_address = nil
149
+ @recipient_email = nil
150
+ @source_marked_read = false
151
+ @list_subscribe = nil
152
+ @list_unsubscribe = nil
153
+ end
154
+
130
155
  def add_ref ref
131
156
  @refs << ref
132
157
  @dirty = true
@@ -136,7 +161,7 @@ class Message
136
161
  @dirty = true if @refs.delete ref
137
162
  end
138
163
 
139
- def snippet; @snippet || (chunks && @snippet); end
164
+ attr_reader :snippet
140
165
  def is_list_message?; !@list_address.nil?; end
141
166
  def is_draft?; @source.is_a? DraftLoader; end
142
167
  def draft_filename
@@ -157,22 +182,24 @@ class Message
157
182
  ## don't tempt me.
158
183
  def sanitize_message_id mid; mid.gsub(/(\s|[^\000-\177])+/, "")[0..254] end
159
184
 
160
- def save index
185
+ def save_state index
161
186
  return unless @dirty
162
- index.sync_message self
187
+ index.update_message_state self
163
188
  @dirty = false
164
189
  true
165
190
  end
166
191
 
167
192
  def has_label? t; @labels.member? t; end
168
- def add_label t
169
- return if @labels.member? t
170
- @labels = (@labels + [t]).to_set_of_symbols
193
+ def add_label l
194
+ l = l.to_sym
195
+ return if @labels.member? l
196
+ @labels << l
171
197
  @dirty = true
172
198
  end
173
- def remove_label t
174
- return unless @labels.member? t
175
- @labels.delete t
199
+ def remove_label l
200
+ l = l.to_sym
201
+ return unless @labels.member? l
202
+ @labels.delete l
176
203
  @dirty = true
177
204
  end
178
205
 
@@ -181,7 +208,10 @@ class Message
181
208
  end
182
209
 
183
210
  def labels= l
184
- @labels = l.to_set_of_symbols
211
+ raise ArgumentError, "not a set" unless l.is_a?(Set)
212
+ raise ArgumentError, "not a set of labels" unless l.all? { |ll| ll.is_a?(Symbol) }
213
+ return if @labels == l
214
+ @labels = l
185
215
  @dirty = true
186
216
  end
187
217
 
@@ -208,7 +238,7 @@ class Message
208
238
  parse_header @source.load_header(@source_info)
209
239
  message_to_chunks @source.load_message(@source_info)
210
240
  rescue SourceError, SocketError => e
211
- Redwood::log "problem getting messages from #{@source}: #{e.message}"
241
+ warn "problem getting messages from #{@source}: #{e.message}"
212
242
  ## we need force_to_top here otherwise this window will cover
213
243
  ## up the error message one
214
244
  @source.error ||= e
@@ -242,7 +272,7 @@ EOS
242
272
  begin
243
273
  yield
244
274
  rescue SourceError => e
245
- Redwood::log "problem getting messages from #{@source}: #{e.message}"
275
+ warn "problem getting messages from #{@source}: #{e.message}"
246
276
  @source.error ||= e
247
277
  Redwood::report_broken_sources :force_to_top => true
248
278
  error_message e.message
@@ -270,11 +300,23 @@ EOS
270
300
  to.map { |p| p.indexable_content },
271
301
  cc.map { |p| p.indexable_content },
272
302
  bcc.map { |p| p.indexable_content },
273
- chunks.select { |c| c.is_a? Chunk::Text }.map { |c| c.lines },
274
- Message.normalize_subj(subj),
303
+ indexable_chunks.map { |c| c.lines },
304
+ indexable_subject,
275
305
  ].flatten.compact.join " "
276
306
  end
277
307
 
308
+ def indexable_body
309
+ indexable_chunks.map { |c| c.lines }.flatten.compact.join " "
310
+ end
311
+
312
+ def indexable_chunks
313
+ chunks.select { |c| c.is_a? Chunk::Text }
314
+ end
315
+
316
+ def indexable_subject
317
+ Message.normalize_subj(subj)
318
+ end
319
+
278
320
  def quotable_body_lines
279
321
  chunks.find_all { |c| c.quotable? }.map { |c| c.lines }.flatten
280
322
  end
@@ -288,6 +330,12 @@ EOS
288
330
  "Subject: #{@subj}"]
289
331
  end
290
332
 
333
+ def self.build_from_source source, source_info
334
+ m = Message.new :source => source, :source_info => source_info
335
+ m.load_from_source!
336
+ m
337
+ end
338
+
291
339
  private
292
340
 
293
341
  ## here's where we handle decoding mime attachments. unfortunately
@@ -315,25 +363,25 @@ private
315
363
 
316
364
  def multipart_signed_to_chunks m
317
365
  if m.body.size != 2
318
- Redwood::log "warning: multipart/signed with #{m.body.size} parts (expecting 2)"
366
+ warn "multipart/signed with #{m.body.size} parts (expecting 2)"
319
367
  return
320
368
  end
321
369
 
322
370
  payload, signature = m.body
323
371
  if signature.multipart?
324
- Redwood::log "warning: multipart/signed with payload multipart #{payload.multipart?} and signature multipart #{signature.multipart?}"
372
+ warn "multipart/signed with payload multipart #{payload.multipart?} and signature multipart #{signature.multipart?}"
325
373
  return
326
374
  end
327
375
 
328
376
  ## this probably will never happen
329
- if payload.header.content_type == "application/pgp-signature"
330
- Redwood::log "warning: multipart/signed with payload content type #{payload.header.content_type}"
377
+ if payload.header.content_type && payload.header.content_type.downcase == "application/pgp-signature"
378
+ warn "multipart/signed with payload content type #{payload.header.content_type}"
331
379
  return
332
380
  end
333
381
 
334
- if signature.header.content_type != "application/pgp-signature"
382
+ if signature.header.content_type && signature.header.content_type.downcase != "application/pgp-signature"
335
383
  ## unknown signature type; just ignore.
336
- #Redwood::log "warning: multipart/signed with signature content type #{signature.header.content_type}"
384
+ #warn "multipart/signed with signature content type #{signature.header.content_type}"
337
385
  return
338
386
  end
339
387
 
@@ -342,36 +390,40 @@ private
342
390
 
343
391
  def multipart_encrypted_to_chunks m
344
392
  if m.body.size != 2
345
- Redwood::log "warning: multipart/encrypted with #{m.body.size} parts (expecting 2)"
393
+ warn "multipart/encrypted with #{m.body.size} parts (expecting 2)"
346
394
  return
347
395
  end
348
396
 
349
397
  control, payload = m.body
350
398
  if control.multipart?
351
- Redwood::log "warning: multipart/encrypted with control multipart #{control.multipart?} and payload multipart #{payload.multipart?}"
399
+ warn "multipart/encrypted with control multipart #{control.multipart?} and payload multipart #{payload.multipart?}"
352
400
  return
353
401
  end
354
402
 
355
- if payload.header.content_type != "application/octet-stream"
356
- Redwood::log "warning: multipart/encrypted with payload content type #{payload.header.content_type}"
403
+ if payload.header.content_type && payload.header.content_type.downcase != "application/octet-stream"
404
+ warn "multipart/encrypted with payload content type #{payload.header.content_type}"
357
405
  return
358
406
  end
359
407
 
360
- if control.header.content_type != "application/pgp-encrypted"
361
- Redwood::log "warning: multipart/encrypted with control content type #{signature.header.content_type}"
408
+ if control.header.content_type && control.header.content_type.downcase != "application/pgp-encrypted"
409
+ warn "multipart/encrypted with control content type #{signature.header.content_type}"
362
410
  return
363
411
  end
364
412
 
365
- decryptedm, sig, notice = CryptoManager.decrypt payload
366
- children = message_to_chunks(decryptedm, true) if decryptedm
367
- [notice, sig, children].flatten.compact
413
+ notice, sig, decryptedm = CryptoManager.decrypt payload
414
+ if decryptedm # managed to decrypt
415
+ children = message_to_chunks(decryptedm, true)
416
+ [notice, sig].compact + children
417
+ else
418
+ [notice]
419
+ end
368
420
  end
369
421
 
370
422
  ## takes a RMail::Message, breaks it into Chunk:: classes.
371
423
  def message_to_chunks m, encrypted=false, sibling_types=[]
372
424
  if m.multipart?
373
425
  chunks =
374
- case m.header.content_type
426
+ case m.header.content_type.downcase
375
427
  when "multipart/signed"
376
428
  multipart_signed_to_chunks m
377
429
  when "multipart/encrypted"
@@ -384,29 +436,57 @@ private
384
436
  end
385
437
 
386
438
  chunks
387
- elsif m.header.content_type == "message/rfc822"
388
- payload = RMail::Parser.read(m.body)
389
- from = payload.header.from.first
390
- from_person = from ? Person.from_address(from.format) : nil
391
- [Chunk::EnclosedMessage.new(from_person, payload.to_s)] +
392
- message_to_chunks(payload, encrypted)
439
+ elsif m.header.content_type && m.header.content_type.downcase == "message/rfc822"
440
+ if m.body
441
+ payload = RMail::Parser.read(m.body)
442
+ from = payload.header.from.first ? payload.header.from.first.format : ""
443
+ to = payload.header.to.map { |p| p.format }.join(", ")
444
+ cc = payload.header.cc.map { |p| p.format }.join(", ")
445
+ subj = payload.header.subject
446
+ subj = subj ? Message.normalize_subj(payload.header.subject.gsub(/\s+/, " ").gsub(/\s+$/, "")) : subj
447
+ if Rfc2047.is_encoded? subj
448
+ subj = Rfc2047.decode_to $encoding, subj
449
+ end
450
+ msgdate = payload.header.date
451
+ from_person = from ? Person.from_address(from) : nil
452
+ to_people = to ? Person.from_address_list(to) : nil
453
+ cc_people = cc ? Person.from_address_list(cc) : nil
454
+ [Chunk::EnclosedMessage.new(from_person, to_people, cc_people, msgdate, subj)] + message_to_chunks(payload, encrypted)
455
+ else
456
+ debug "no body for message/rfc822 enclosure; skipping"
457
+ []
458
+ end
459
+ elsif m.header.content_type && m.header.content_type.downcase == "application/pgp" && m.body
460
+ ## apparently some versions of Thunderbird generate encryped email that
461
+ ## does not follow RFC3156, e.g. messages with X-Enigmail-Version: 0.95.0
462
+ ## they have no MIME multipart and just set the body content type to
463
+ ## application/pgp. this handles that.
464
+ ##
465
+ ## TODO: unduplicate code between here and multipart_encrypted_to_chunks
466
+ notice, sig, decryptedm = CryptoManager.decrypt m.body
467
+ if decryptedm # managed to decrypt
468
+ children = message_to_chunks decryptedm, true
469
+ [notice, sig].compact + children
470
+ else
471
+ [notice]
472
+ end
393
473
  else
394
474
  filename =
395
475
  ## first, paw through the headers looking for a filename
396
476
  if m.header["Content-Disposition"] && m.header["Content-Disposition"] =~ /filename="?(.*?[^\\])("|;|$)/
397
477
  $1
398
- elsif m.header["Content-Type"] && m.header["Content-Type"] =~ /name="?(.*?[^\\])("|;|$)/
478
+ elsif m.header["Content-Type"] && m.header["Content-Type"] =~ /name="?(.*?[^\\])("|;|$)/i
399
479
  $1
400
480
 
401
481
  ## haven't found one, but it's a non-text message. fake
402
482
  ## it.
403
483
  ##
404
484
  ## TODO: make this less lame.
405
- elsif m.header["Content-Type"] && m.header["Content-Type"] !~ /^text\/plain/
485
+ elsif m.header["Content-Type"] && m.header["Content-Type"] !~ /^text\/plain/i
406
486
  extension =
407
487
  case m.header["Content-Type"]
408
- when /text\/html/: "html"
409
- when /image\/(.*)/: $1
488
+ when /text\/html/ then "html"
489
+ when /image\/(.*)/ then $1
410
490
  end
411
491
 
412
492
  ["sup-attachment-#{Time.now.to_i}-#{rand 10000}", extension].join(".")
@@ -419,7 +499,7 @@ private
419
499
  # Lowercase the filename because searches are easier that way
420
500
  @attachments.push filename.downcase unless filename =~ /^sup-attachment-/
421
501
  add_label :attachment unless filename =~ /^sup-attachment-/
422
- content_type = m.header.content_type || "application/unknown" # sometimes RubyMail gives us nil
502
+ content_type = m.header.content_type.downcase || "application/unknown" # sometimes RubyMail gives us nil
423
503
  [Chunk::Attachment.new(content_type, filename, m, sibling_types)]
424
504
 
425
505
  ## otherwise, it's body text