sup 0.0.8 → 0.1

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 (57) hide show
  1. data/HACKING +6 -36
  2. data/History.txt +11 -0
  3. data/Manifest.txt +5 -0
  4. data/README.txt +13 -31
  5. data/Rakefile +3 -3
  6. data/bin/sup +167 -89
  7. data/bin/sup-add +39 -29
  8. data/bin/sup-config +57 -31
  9. data/bin/sup-sync +60 -54
  10. data/bin/sup-sync-back +143 -0
  11. data/doc/FAQ.txt +56 -19
  12. data/doc/Philosophy.txt +34 -33
  13. data/doc/TODO +76 -46
  14. data/doc/UserGuide.txt +142 -122
  15. data/lib/sup.rb +76 -36
  16. data/lib/sup/account.rb +27 -19
  17. data/lib/sup/buffer.rb +130 -44
  18. data/lib/sup/contact.rb +1 -1
  19. data/lib/sup/draft.rb +1 -2
  20. data/lib/sup/imap.rb +64 -19
  21. data/lib/sup/index.rb +95 -16
  22. data/lib/sup/keymap.rb +1 -1
  23. data/lib/sup/label.rb +31 -5
  24. data/lib/sup/maildir.rb +7 -5
  25. data/lib/sup/mbox.rb +34 -15
  26. data/lib/sup/mbox/loader.rb +30 -12
  27. data/lib/sup/mbox/ssh-loader.rb +7 -5
  28. data/lib/sup/message.rb +93 -44
  29. data/lib/sup/modes/buffer-list-mode.rb +1 -1
  30. data/lib/sup/modes/completion-mode.rb +55 -0
  31. data/lib/sup/modes/compose-mode.rb +6 -25
  32. data/lib/sup/modes/contact-list-mode.rb +1 -1
  33. data/lib/sup/modes/edit-message-mode.rb +119 -29
  34. data/lib/sup/modes/file-browser-mode.rb +108 -0
  35. data/lib/sup/modes/forward-mode.rb +3 -20
  36. data/lib/sup/modes/inbox-mode.rb +9 -12
  37. data/lib/sup/modes/label-list-mode.rb +28 -46
  38. data/lib/sup/modes/label-search-results-mode.rb +1 -16
  39. data/lib/sup/modes/line-cursor-mode.rb +44 -5
  40. data/lib/sup/modes/person-search-results-mode.rb +1 -16
  41. data/lib/sup/modes/reply-mode.rb +18 -31
  42. data/lib/sup/modes/resume-mode.rb +6 -6
  43. data/lib/sup/modes/scroll-mode.rb +6 -5
  44. data/lib/sup/modes/search-results-mode.rb +6 -17
  45. data/lib/sup/modes/thread-index-mode.rb +70 -28
  46. data/lib/sup/modes/thread-view-mode.rb +65 -29
  47. data/lib/sup/person.rb +71 -30
  48. data/lib/sup/poll.rb +13 -4
  49. data/lib/sup/rfc2047.rb +61 -0
  50. data/lib/sup/sent.rb +7 -5
  51. data/lib/sup/source.rb +12 -9
  52. data/lib/sup/suicide.rb +36 -0
  53. data/lib/sup/tagger.rb +6 -6
  54. data/lib/sup/textfield.rb +76 -14
  55. data/lib/sup/thread.rb +97 -123
  56. data/lib/sup/util.rb +167 -1
  57. metadata +30 -5
data/lib/sup/thread.rb CHANGED
@@ -1,24 +1,28 @@
1
- ## Herein is all the code responsible for threading messages. I use an
2
- ## online version of the JWZ threading algorithm:
1
+ ## Herein lies all the code responsible for threading messages. It's
2
+ ## basically an online version of the JWZ threading algorithm:
3
3
  ## http://www.jwz.org/doc/threading.html
4
4
  ##
5
- ## I certainly didn't implement it for efficiency, but thanks to our
6
- ## search engine backend, it's typically not applied to very many
7
- ## messages at once.
8
-
9
- ## At the top level, we have a ThreadSet. A ThreadSet represents a set
10
- ## of threads, e.g. a message folder or an inbox. Each ThreadSet
11
- ## contains zero or more Threads. A Thread represents all the message
12
- ## related to a particular subject. Each Thread has one or more
13
- ## Containers. A Container is a recursive structure that holds the
14
- ## tree structure as determined by the references: and in-reply-to:
15
- ## headers. A Thread with multiple Containers occurs if they have the
16
- ## same subject, but (most likely due to someone using a primitive
17
- ## MUA) we don't have evidence from in-reply-to: or references:
18
- ## headers, only subject: (and thus our tree is probably broken). A
19
- ## Container holds zero or one message. In the case of no message, it
20
- ## means we've seen a reference to the message but haven't seen the
21
- ## message itself (yet).
5
+ ## I didn't implement it for efficiency, but thanks to our search
6
+ ## engine backend, it's typically not applied to very many messages at
7
+ ## once.
8
+ ##
9
+ ## At the top level, we have a ThreadSet, which represents a set of
10
+ ## threads, e.g. a message folder or an inbox. Each ThreadSet contains
11
+ ## zero or more Threads. A Thread represents all the message related
12
+ ## to a particular subject. Each Thread has one or more Containers. A
13
+ ## Container is a recursive structure that holds the message tree as
14
+ ## determined by the references: and in-reply-to: headers. EAch
15
+ ## Container holds zero or one messages. In the case of zero messages,
16
+ ## it means we've seen a reference to the message but haven't (yet)
17
+ ## seen the message itself.
18
+ ##
19
+ ## A Thread can have multiple top-level Containers if we decide to
20
+ ## group them together independent of tree structure, typically if
21
+ ## (e.g. due to someone using a primitive MUA) the messages have the
22
+ ## same subject but we don't have evidence from in-reply-to: or
23
+ ## references: headers. In this case Thread#each can optionally yield
24
+ ## a faked root object tying them all together into one tree
25
+ ## structure.
22
26
 
23
27
  module Redwood
24
28
 
@@ -38,20 +42,20 @@ class Thread
38
42
  end
39
43
 
40
44
  def empty?; @containers.empty?; end
41
-
42
- def drop c
43
- raise "bad drop" unless @containers.member? c
44
- @containers.delete c
45
+ def empty!; @containers.clear; end
46
+ def drop c; @containers.delete(c) or raise "bad drop"; end
47
+
48
+ ## unused
49
+ def dump f=$stdout
50
+ f.puts "=== start thread #{self} with #{@containers.length} trees ==="
51
+ @containers.each { |c| c.dump_recursive f }
52
+ f.puts "=== end thread ==="
45
53
  end
46
54
 
47
- def dump
48
- puts "=== start thread #{self} with #{@containers.length} trees ==="
49
- @containers.each { |c| c.dump_recursive }
50
- puts "=== end thread ==="
51
- end
52
-
53
- ## yields each message, its depth, and its parent. note that the
54
- ## message can be a Message object, or :fake_root, or nil.
55
+ ## yields each message, its depth, and its parent. the message yield
56
+ ## parameter can be a Message object, or :fake_root, or nil (no
57
+ ## message found but the presence of one induced from other
58
+ ## messages).
55
59
  def each fake_root=false
56
60
  adj = 0
57
61
  root = @containers.find_all { |c| !Message.subj_is_reply?(c) }.argmin { |c| c.date || 0 }
@@ -201,11 +205,11 @@ class Container
201
205
  ].compact.join(" ") + ">"
202
206
  end
203
207
 
204
- def dump_recursive indent=0, root=true, parent=nil
208
+ def dump_recursive f=$stdout, indent=0, root=true, parent=nil
205
209
  raise "inconsistency" unless parent.nil? || parent.children.include?(self)
206
210
  unless root
207
- print " " * indent
208
- print "+->"
211
+ f.print " " * indent
212
+ f.print "+->"
209
213
  end
210
214
  line = #"[#{useful? ? 'U' : ' '}] " +
211
215
  if @message
@@ -214,22 +218,31 @@ class Container
214
218
  "<no message>"
215
219
  end
216
220
 
217
- puts "#{id} #{line}"#[0 .. (105 - indent)]
221
+ f.puts "#{id} #{line}"#[0 .. (105 - indent)]
218
222
  indent += 3
219
- @children.each { |c| c.dump_recursive indent, false, self }
223
+ @children.each { |c| c.dump_recursive f, indent, false, self }
220
224
  end
221
225
  end
222
226
 
223
- ## a set of threads (so a forest). builds the thread structures by
224
- ## reading messages from an index.
227
+ ## A set of threads (so a forest). Builds thread structures by reading
228
+ ## messages from an index.
229
+ ##
230
+ ## If 'thread_by_subj' is true, puts messages with the same subject in
231
+ ## one thread, even if they don't reference each other. This is
232
+ ## helpful for crappy MUAs that don't set In-reply-to: or References:
233
+ ## headers, but means that messages may be threaded unnecessarily.
225
234
  class ThreadSet
226
235
  attr_reader :num_messages
236
+ bool_reader :thread_by_subj
227
237
 
228
- def initialize index
238
+ def initialize index, thread_by_subj=true
229
239
  @index = index
230
240
  @num_messages = 0
231
- @messages = {} ## map from message ids to container objects
232
- @subj_thread = {} ## map from subject strings to thread objects
241
+ ## map from message ids to container objects
242
+ @messages = SavingHash.new { |id| Container.new id }
243
+ ## map from subject strings or (or root message ids) to thread objects
244
+ @threads = SavingHash.new { Thread.new }
245
+ @thread_by_subj = thread_by_subj
233
246
  end
234
247
 
235
248
  def contains_id? id; @messages.member?(id) && !@messages[id].empty?; end
@@ -238,19 +251,20 @@ class ThreadSet
238
251
  end
239
252
 
240
253
  def delete_cruft
241
- @subj_thread.each { |k, v| @subj_thread.delete(k) if v.empty? || v.subj != k }
254
+ @threads.each { |k, v| @threads.delete(k) if v.empty? }
242
255
  end
243
256
  private :delete_cruft
244
257
 
245
- def threads; delete_cruft; @subj_thread.values; end
246
- def size; delete_cruft; @subj_thread.size; end
258
+ def threads; delete_cruft; @threads.values; end
259
+ def size; delete_cruft; @threads.size; end
247
260
 
248
- def dump
249
- @subj_thread.each do |s, t|
250
- puts "**********************"
251
- puts "** for subject #{s} **"
252
- puts "**********************"
253
- t.dump
261
+ ## unused
262
+ def dump f
263
+ @threads.each do |s, t|
264
+ f.puts "**********************"
265
+ f.puts "** for subject #{s} **"
266
+ f.puts "**********************"
267
+ t.dump f
254
268
  end
255
269
  end
256
270
 
@@ -291,7 +305,7 @@ class ThreadSet
291
305
  m = builder.call
292
306
  add_message m
293
307
  load_thread_for_message m, :load_killed => opts[:load_killed]
294
- yield @subj_thread.size if block_given?
308
+ yield size if block_given?
295
309
  end
296
310
  end
297
311
 
@@ -305,19 +319,18 @@ class ThreadSet
305
319
 
306
320
  ## merges in a pre-loaded thread
307
321
  def add_thread t
308
- raise "duplicate" if @subj_thread.values.member? t
322
+ raise "duplicate" if @threads.values.member? t
309
323
  t.each { |m, *o| add_message m }
310
324
  end
311
325
 
312
326
  def is_relevant? m
313
- m.refs.any? { |ref_id| @messages[ref_id] }
327
+ m.refs.any? { |ref_id| @messages.member? ref_id }
314
328
  end
315
329
 
316
- ## an "online" version of the jwz threading algorithm.
330
+ ## the heart of the threading code
317
331
  def add_message message
318
- id = message.id
319
- el = (@messages[id] ||= Container.new id)
320
- return if @messages[id].message # we've seen it before
332
+ el = @messages[message.id]
333
+ return if el.message # we've seen it before
321
334
 
322
335
  el.message = message
323
336
  oldroot = el.root
@@ -325,8 +338,7 @@ class ThreadSet
325
338
  ## link via references:
326
339
  prev = nil
327
340
  message.refs.each do |ref_id|
328
- raise "non-String ref id #{ref_id.inspect} (full: #{message.refs.inspect})" unless ref_id.is_a?(String)
329
- ref = (@messages[ref_id] ||= Container.new ref_id)
341
+ ref = @messages[ref_id]
330
342
  link prev, ref if prev
331
343
  prev = ref
332
344
  end
@@ -334,81 +346,43 @@ class ThreadSet
334
346
 
335
347
  ## link via in-reply-to:
336
348
  message.replytos.each do |ref_id|
337
- ref = (@messages[ref_id] ||= Container.new ref_id)
349
+ ref = @messages[ref_id]
338
350
  link ref, el, true
339
351
  break # only do the first one
340
352
  end
341
353
 
342
- ## update subject grouping
343
354
  root = el.root
344
- # puts "> have #{el}, root #{root}, oldroot #{oldroot}"
345
- # el.dump_recursive
346
-
347
- if root == oldroot
348
- if oldroot.thread
349
- ## check to see if the subject is still the same (in the case
350
- ## that we first added a child message with a different
351
- ## subject)
352
-
353
- ## this code is duplicated below. sorry! TODO: refactor
354
- s = Message.normalize_subj(root.subj)
355
- unless @subj_thread[s] == root.thread
356
- ## Redwood::log "[1] moving thread to new subject #{root.subj}"
357
- if @subj_thread[s]
358
- @subj_thread[s] << root
359
- root.thread = @subj_thread[s]
360
- else
361
- @subj_thread[s] = root.thread
362
- end
363
- end
364
355
 
356
+ ## new root. need to drop old one and put this one in its place
357
+ if root != oldroot && oldroot.thread
358
+ oldroot.thread.drop oldroot
359
+ oldroot.thread = nil
360
+ end
361
+
362
+ key =
363
+ if thread_by_subj?
364
+ Message.normalize_subj root.subj
365
365
  else
366
- ## to disable subject grouping, use the next line instead
367
- ## (and the same for below)
368
- #Redwood::log "[1] for #{root}, subject #{Message.normalize_subj(root.subj)} has #{@subj_thread[Message.normalize_subj(root.subj)] ? 'a' : 'no'} thread"
369
- thread = (@subj_thread[Message.normalize_subj(root.subj)] ||= Thread.new)
370
- #thread = (@subj_thread[root.id] ||= Thread.new)
371
-
372
- thread << root
373
- root.thread = thread
374
- # Redwood::log "[1] added #{root} to #{thread}"
375
- end
376
- else
377
- if oldroot.thread
378
- ## new root. need to drop old one and put this one in its place
379
- oldroot.thread.drop oldroot
380
- oldroot.thread = nil
366
+ root.id
381
367
  end
382
368
 
383
- if root.thread
384
- ## check to see if the subject is still the same (in the case
385
- ## that we first added a child message with a different
386
- ## subject)
387
- s = Message.normalize_subj(root.subj)
388
- unless @subj_thread[s] == root.thread
389
- # Redwood::log "[2] moving thread to new subject #{root.subj}"
390
- if @subj_thread[s]
391
- @subj_thread[s] << root
392
- root.thread = @subj_thread[s]
393
- else
394
- @subj_thread[s] = root.thread
395
- end
369
+ ## check to see if the subject is still the same (in the case
370
+ ## that we first added a child message with a different
371
+ ## subject)
372
+ if root.thread
373
+ unless @threads[key] == root.thread
374
+ if @threads[key]
375
+ root.thread.empty!
376
+ @threads[key] << root
377
+ root.thread = @threads[key]
378
+ else
379
+ @threads[key] = root.thread
396
380
  end
397
-
398
- else
399
- ## to disable subject grouping, use the next line instead
400
- ## (and the same above)
401
-
402
- ## this code is duplicated above. sorry! TODO: refactor
403
- # Redwood::log "[2] for #{root}, subject '#{Message.normalize_subj(root.subj)}' has #{@subj_thread[Message.normalize_subj(root.subj)] ? 'a' : 'no'} thread"
404
-
405
- thread = (@subj_thread[Message.normalize_subj(root.subj)] ||= Thread.new)
406
- #thread = (@subj_thread[root.id] ||= Thread.new)
407
-
408
- thread << root
409
- root.thread = thread
410
- # Redwood::log "[2] added #{root} to #{thread}"
411
381
  end
382
+ else
383
+ thread = @threads[key]
384
+ thread << root
385
+ root.thread = thread
412
386
  end
413
387
 
414
388
  ## last bit
data/lib/sup/util.rb CHANGED
@@ -1,3 +1,98 @@
1
+ require 'lockfile'
2
+ require 'mime/types'
3
+ require 'pathname'
4
+
5
+ ## time for some monkeypatching!
6
+ class Lockfile
7
+ def gen_lock_id
8
+ Hash[
9
+ 'host' => "#{ Socket.gethostname }",
10
+ 'pid' => "#{ Process.pid }",
11
+ 'ppid' => "#{ Process.ppid }",
12
+ 'time' => timestamp,
13
+ 'pname' => $0,
14
+ 'user' => ENV["USER"]
15
+ ]
16
+ end
17
+
18
+ def dump_lock_id lock_id = @lock_id
19
+ "host: %s\npid: %s\nppid: %s\ntime: %s\nuser: %s\npname: %s\n" %
20
+ lock_id.values_at('host','pid','ppid','time','user', 'pname')
21
+ end
22
+
23
+ def lockinfo_on_disk
24
+ h = load_lock_id IO.read(path)
25
+ h['mtime'] = File.mtime path
26
+ h
27
+ end
28
+
29
+ def touch_yourself; touch path end
30
+ end
31
+
32
+ class Pathname
33
+ def human_size
34
+ s =
35
+ begin
36
+ size
37
+ rescue SystemCallError
38
+ return "?"
39
+ end
40
+
41
+ if s < 1024
42
+ s.to_s + "b"
43
+ elsif s < (1024 * 1024)
44
+ (s / 1024).to_s + "k"
45
+ elsif s < (1024 * 1024 * 1024)
46
+ (s / 1024 / 1024).to_s + "m"
47
+ else
48
+ (s / 1024 / 1024 / 1024).to_s + "g"
49
+ end
50
+ end
51
+
52
+ def human_time
53
+ begin
54
+ ctime.strftime("%Y-%m-%d %H:%M")
55
+ rescue SystemCallError
56
+ "?"
57
+ end
58
+ end
59
+ end
60
+
61
+ ## more monkeypatching!
62
+ module RMail
63
+ class EncodingUnsupportedError < StandardError; end
64
+
65
+ class Message
66
+ def add_attachment fn
67
+ bfn = File.basename fn
68
+ a = Message.new
69
+ t = MIME::Types.type_for(bfn).first || MIME::Types.type_for("exe").first
70
+
71
+ a.header.add "Content-Disposition", "attachment; filename=#{bfn}"
72
+ a.header.add "Content-Type", "#{t.content_type}; name=#{bfn}"
73
+ a.header.add "Content-Transfer-Encoding", t.encoding
74
+ a.body =
75
+ case t.encoding
76
+ when "base64"
77
+ [IO.read(fn)].pack "m"
78
+ when "quoted-printable"
79
+ [IO.read(fn)].pack "M"
80
+ else
81
+ raise EncodingUnsupportedError, t.encoding
82
+ end
83
+
84
+ add_part a
85
+ end
86
+ end
87
+ end
88
+
89
+ class Range
90
+ ## only valid for integer ranges (unless I guess it's exclusive)
91
+ def size
92
+ last - first + (exclude_end? ? 0 : 1)
93
+ end
94
+ end
95
+
1
96
  class Module
2
97
  def bool_reader *args
3
98
  args.each { |sym| class_eval %{ def #{sym}?; @#{sym}; end } }
@@ -9,7 +104,10 @@ class Module
9
104
  end
10
105
 
11
106
  def defer_all_other_method_calls_to obj
12
- class_eval %{ def method_missing meth, *a, &b; @#{obj}.send meth, *a, &b; end }
107
+ class_eval %{
108
+ def method_missing meth, *a, &b; @#{obj}.send meth, *a, &b; end
109
+ def respond_to? meth; @#{obj}.respond_to?(meth); end
110
+ }
13
111
  end
14
112
  end
15
113
 
@@ -38,6 +136,8 @@ class Object
38
136
  ##
39
137
  ## i'm sure there's pithy comment i could make here about the
40
138
  ## superiority of lisp, but fuck lisp.
139
+ ##
140
+ ## addendum: apparently this is a "k combinator". whoda thunk it?
41
141
  def returning x; yield x; x; end
42
142
 
43
143
  ## clone of java-style whole-method synchronization
@@ -174,6 +274,24 @@ module Enumerable
174
274
  end
175
275
  best
176
276
  end
277
+
278
+ ## returns the maximum shared prefix of an array of strings
279
+ ## optinally excluding a prefix
280
+ def shared_prefix exclude=""
281
+ return "" if empty?
282
+ prefix = ""
283
+ (0 ... first.length).each do |i|
284
+ c = first[i]
285
+ break unless all? { |s| s[i] == c }
286
+ next if exclude[i] == c
287
+ prefix += c.chr
288
+ end
289
+ prefix
290
+ end
291
+
292
+ def max_of
293
+ map { |e| yield e }.max
294
+ end
177
295
  end
178
296
 
179
297
  class Array
@@ -185,6 +303,8 @@ class Array
185
303
  def rest; self[1..-1]; end
186
304
 
187
305
  def to_boolean_h; Hash[*map { |x| [x, true] }.flatten]; end
306
+
307
+ def last= e; self[-1] = e end
188
308
  end
189
309
 
190
310
  class Time
@@ -271,6 +391,13 @@ module Singleton
271
391
  def deinstantiate!; @instance = nil; end
272
392
  def method_missing meth, *a, &b
273
393
  raise "no instance defined!" unless defined? @instance
394
+
395
+ ## if we've been deinstantiated, just drop all calls. this is
396
+ ## useful because threads that might be active during the
397
+ ## cleanup process (e.g. polling) would otherwise have to
398
+ ## special-case every call to a Singleton object
399
+ return nil if @instance.nil?
400
+
274
401
  @instance.send meth, *a, &b
275
402
  end
276
403
  def i_am_the_instance o
@@ -301,6 +428,9 @@ class Recoverable
301
428
  def id; __pass :id; end
302
429
  def to_s; __pass :to_s; end
303
430
  def to_yaml x; __pass :to_yaml, x; end
431
+ def is_a? c; @o.is_a? c; end
432
+
433
+ def respond_to? m; @o.respond_to? m end
304
434
 
305
435
  def __pass m, *a, &b
306
436
  begin
@@ -311,3 +441,39 @@ class Recoverable
311
441
  end
312
442
  end
313
443
  end
444
+
445
+ ## acts like a hash with an initialization block, but saves any
446
+ ## newly-created value even upon lookup.
447
+ ##
448
+ ## for example:
449
+ ##
450
+ ## class C
451
+ ## attr_accessor :val
452
+ ## def initialize; @val = 0 end
453
+ ## end
454
+ ##
455
+ ## h = Hash.new { C.new }
456
+ ## h[:a].val # => 0
457
+ ## h[:a].val = 1
458
+ ## h[:a].val # => 0
459
+ ##
460
+ ## h2 = SavingHash.new { C.new }
461
+ ## h2[:a].val # => 0
462
+ ## h2[:a].val = 1
463
+ ## h2[:a].val # => 1
464
+ ##
465
+ ## important note: you REALLY want to use #member? to test existence,
466
+ ## because just checking h[anything] will always evaluate to true
467
+ ## (except for degenerate constructor blocks that return nil or false)
468
+ class SavingHash
469
+ def initialize &b
470
+ @constructor = b
471
+ @hash = Hash.new
472
+ end
473
+
474
+ def [] k
475
+ @hash[k] ||= @constructor.call(k)
476
+ end
477
+
478
+ defer_all_other_method_calls_to :hash
479
+ end