sup 0.0.6 → 0.0.7

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.

data/lib/sup/person.rb CHANGED
@@ -14,7 +14,7 @@ class PersonManager
14
14
  def register email, name
15
15
  return unless name
16
16
 
17
- name = name.gsub(/^\s+|\s+$/, "").gsub(/\s+/, " ")
17
+ name = name.gsub(/^\s+|\s+$/, "").gsub(/\s+/, " ").gsub(/^['"]|['"]$/, "")
18
18
 
19
19
  ## all else being equal, prefer longer names, unless the prior name
20
20
  ## doesn't contain any capitalization
data/lib/sup/poll.rb CHANGED
@@ -24,7 +24,7 @@ class PollManager
24
24
  if num > 0
25
25
  BufferManager.flash "Loaded #{num} new messages, #{numi} to inbox."
26
26
  else
27
- BufferManager.flash "No new messages."
27
+ BufferManager.flash "No new messages."
28
28
  end
29
29
  [num, numi]
30
30
  end
@@ -38,59 +38,27 @@ class PollManager
38
38
  end
39
39
  end
40
40
 
41
- ## TODO: merge this with sup-import
42
41
  def do_poll
43
42
  total_num = total_numi = 0
44
43
  @mutex.synchronize do
45
- found = {}
46
44
  Index.usual_sources.each do |source|
47
- next if source.broken? || source.done?
48
-
49
- yield "Loading from #{source}... "
50
- start_offset = nil
45
+ # yield "source #{source} is done? #{source.done?} (cur_offset #{source.cur_offset} >= #{source.end_offset})"
46
+ yield "Loading from #{source}... " unless source.done? || source.broken?
51
47
  num = 0
52
- num_inbox = 0
53
-
54
- source.each do |offset, labels|
55
- break if source.broken?
56
- start_offset ||= offset
57
- yield "Found message at #{offset} with labels #{labels * ', '}"
58
-
59
- begin
60
- begin
61
- m = Redwood::Message.new :source => source, :source_info => offset, :labels => labels
62
- rescue MessageFormatError => e
63
- yield "Non-fatal error loading message #{source}##{offset}: #{e.message}"
64
- next
65
- end
66
-
67
- if found[m.id]
68
- yield "Skipping duplicate message #{m.id}"
69
- next
70
- end
71
- found[m.id] = true
72
-
73
- if Index.add_message m
74
- UpdateManager.relay :add, m
75
- num += 1
76
- total_num += 1
77
- total_numi += 1 if m.labels.include? :inbox
78
- end
79
-
80
- if num % 1000 == 0 && num > 0
81
- elapsed = Time.now - start
82
- pctdone = source.pct_done
83
- remaining = (100.0 - pctdone) * (elapsed.to_f / pctdone)
84
- yield "## #{num} (#{pctdone}% done) read; #{elapsed.to_time_s} elapsed; est. #{remaining.to_time_s} remaining"
85
- end
86
- rescue SourceError => e
87
- msg = "Fatal error loading from #{source}: #{e.message}"
88
- Redwood::log msg
89
- yield msg
90
- break
48
+ numi = 0
49
+ add_new_messages_from source do |m, offset, entry|
50
+ ## always preserve the labels on disk.
51
+ m.labels = entry[:label].split(/\s+/).map { |x| x.intern } if entry
52
+ yield "Found message at #{offset} with labels {#{m.labels * ', '}}"
53
+ unless entry
54
+ num += 1
55
+ numi += 1 if m.labels.include? :inbox
91
56
  end
57
+ m
92
58
  end
93
- yield "Found #{num} messages" unless num == 0
59
+ yield "Found #{num} messages, #{numi} to inbox" unless num == 0
60
+ total_num += num
61
+ total_numi += numi
94
62
  end
95
63
 
96
64
  yield "Done polling; loaded #{total_num} new messages total"
@@ -99,6 +67,51 @@ class PollManager
99
67
  end
100
68
  [total_num, total_numi]
101
69
  end
70
+
71
+ ## this is the main mechanism for adding new messages to the
72
+ ## index. it's called both by sup-import and by PollMode.
73
+ ##
74
+ ## for each new message in the source, this yields the message, the
75
+ ## source offset, and the index entry on disk (if any). it expects
76
+ ## the yield to return the message (possibly altered in some way),
77
+ ## and then adds it (if new) or updates it (if previously seen).
78
+ ##
79
+ ## the labels of the yielded message are the source labels. it is
80
+ ## likely that callers will want to replace these with the index
81
+ ## labels, if they exist, so that state is not lost when e.g. a new
82
+ ## version of a message from a mailing list comes in.
83
+ def add_new_messages_from source
84
+ return if source.done? || source.broken?
85
+
86
+ source.each do |offset, labels|
87
+ if source.broken?
88
+ Redwood::log "error loading messages from #{source}: #{source.broken_msg}"
89
+ return
90
+ end
91
+
92
+ labels.each { |l| LabelManager << l }
93
+
94
+ begin
95
+ m = Message.new :source => source, :source_info => offset, :labels => labels
96
+ if m.source_marked_read?
97
+ m.remove_label :unread
98
+ labels.delete :unread
99
+ end
100
+
101
+ docid, entry = Index.load_entry_for_id m.id
102
+ m = yield m, offset, entry
103
+ next unless m
104
+ if entry
105
+ Index.update_message m, docid, entry
106
+ else
107
+ Index.add_message m
108
+ UpdateManager.relay self, :add, m
109
+ end
110
+ rescue MessageFormatError, SourceError => e
111
+ Redwood::log "ignoring erroneous message at #{source}##{offset}: #{e.message}"
112
+ end
113
+ end
114
+ end
102
115
  end
103
116
 
104
117
  end
data/lib/sup/sent.rb CHANGED
@@ -10,7 +10,7 @@ class SentManager
10
10
  self.class.i_am_the_instance self
11
11
  end
12
12
 
13
- def self.source_name; "sent://"; end
13
+ def self.source_name; "sup://sent"; end
14
14
  def self.source_id; 9998; end
15
15
  def new_source; @source = SentLoader.new; end
16
16
 
@@ -24,7 +24,7 @@ class SentManager
24
24
  @source.each do |offset, labels|
25
25
  m = Message.new :source => @source, :source_info => offset, :labels => labels
26
26
  Index.add_message m
27
- UpdateManager.relay :add, m
27
+ UpdateManager.relay self, :add, m
28
28
  end
29
29
  end
30
30
  end
@@ -36,6 +36,7 @@ class SentLoader < MBox::Loader
36
36
  super "mbox://" + filename, cur_offset, true, true
37
37
  end
38
38
 
39
+ def uri; SentManager.source_name; end
39
40
  def to_s; SentManager.source_name; end
40
41
  def id; SentManager.source_id; end
41
42
  def labels; [:sent, :inbox]; end
data/lib/sup/source.rb CHANGED
@@ -10,8 +10,8 @@ class Source
10
10
  ## 3. (optional) see whether the source has marked it read or not
11
11
  ##
12
12
  ## In particular, Sup doesn't need to move messages, mark them as
13
- ## read, delete them, or anything else. (Well, maybe delete at some
14
- ## point.)
13
+ ## read, delete them, or anything else. (Well, at some point it will
14
+ ## need to delete them, but that will be an optional capability.)
15
15
  ##
16
16
  ## On the other hand, Sup assumes that you can assign each message a
17
17
  ## unique integer id, such that newer messages have higher ids than
@@ -22,16 +22,19 @@ class Source
22
22
  ## capability, e.g. IMAP, then you have to do a little more work to
23
23
  ## simulate it.
24
24
  ##
25
- ## To write a new source, subclass this class, and should implement:
25
+ ## To write a new source, subclass this class, and implement:
26
26
  ##
27
27
  ## - start_offset
28
- ## - end_offset
28
+ ## - end_offset (exclusive!)
29
29
  ## - load_header offset
30
30
  ## - load_message offset
31
31
  ## - raw_header offset
32
- ## - raw_full_message offset-
32
+ ## - raw_full_message offset
33
33
  ## - next (or each, if you prefer)
34
34
  ##
35
+ ## ... where "offset" really means unique id. (You can tell I
36
+ ## started with mbox.)
37
+ ##
35
38
  ## You can throw SourceErrors from any of those, but we don't catch
36
39
  ## anything else, so make sure you catch *all* errors and reraise
37
40
  ## them as SourceErrors, and set broken_msg to something if the
@@ -39,6 +42,11 @@ class Source
39
42
  ##
40
43
  ## Also, be sure to make the source thread-safe, since it WILL be
41
44
  ## pummeled from multiple threads at once.
45
+ ##
46
+ ## Two examples for you to look at, though sadly neither of them is
47
+ ## as simple as I'd like: mbox/loader.rb and imap.rb
48
+
49
+
42
50
 
43
51
  ## dirty? described whether cur_offset has changed, which means the
44
52
  ## source info needs to be re-saved to sources.yaml.
@@ -78,7 +86,7 @@ class Source
78
86
  true
79
87
  end
80
88
  end
81
- def is_source_for? s; to_s == s; end
89
+ def is_source_for? uri; URI(self.uri) == URI(uri); end
82
90
 
83
91
  def each
84
92
  return if broken?
data/lib/sup/textfield.rb CHANGED
@@ -29,6 +29,7 @@ class TextField
29
29
  @w.mvaddstr @y, 0, @question
30
30
  Ncurses.curs_set 1
31
31
  Ncurses::Form.form_driver @form, Ncurses::Form::REQ_END_FIELD
32
+ Ncurses::Form.form_driver @form, Ncurses::Form::REQ_NEXT_CHAR if @history[@i] =~ / $/ # fucking RETARDED!!!!
32
33
  end
33
34
 
34
35
  def deactivate
data/lib/sup/thread.rb CHANGED
@@ -1,4 +1,24 @@
1
- require 'date'
1
+ ## Herein is all the code responsible for threading messages. I use an
2
+ ## online version of the JWZ threading algorithm:
3
+ ## http://www.jwz.org/doc/threading.html
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).
2
22
 
3
23
  module Redwood
4
24
 
@@ -30,9 +50,8 @@ class Thread
30
50
  puts "=== end thread ==="
31
51
  end
32
52
 
33
- ## yields each message, its depth, and its parent
34
- ## note that the message can be a Message object, or :fake_root,
35
- ## or nil.
53
+ ## yields each message, its depth, and its parent. note that the
54
+ ## message can be a Message object, or :fake_root, or nil.
36
55
  def each fake_root=false
37
56
  adj = 0
38
57
  root = @containers.find_all { |c| !Message.subj_is_reply?(c) }.argmin { |c| c.date }
@@ -59,15 +78,14 @@ class Thread
59
78
  end
60
79
  end
61
80
 
81
+ def first; each { |m, *o| return m if m }; nil; end
62
82
  def dirty?; any? { |m, *o| m && m.dirty? }; end
63
83
  def date; map { |m, *o| m.date if m }.compact.max; end
64
84
  def snippet; argfind { |m, *o| m && m.snippet }; end
65
85
  def authors; map { |m, *o| m.from if m }.compact.uniq; end
66
86
 
67
87
  def apply_label t; each { |m, *o| m && m.add_label(t) }; end
68
- def remove_label t
69
- each { |m, *o| m && m.remove_label(t) }
70
- end
88
+ def remove_label t; each { |m, *o| m && m.remove_label(t) }; end
71
89
 
72
90
  def toggle_label label
73
91
  if has_label? label
@@ -82,7 +100,6 @@ class Thread
82
100
  def set_labels l; each { |m, *o| m && m.labels = l }; end
83
101
 
84
102
  def has_label? t; any? { |m, *o| m && m.has_label?(t) }; end
85
- def dirty?; any? { |m, *o| m && m.dirty? }; end
86
103
  def save index; each { |m, *o| m && m.save(index) }; end
87
104
 
88
105
  def direct_participants
@@ -220,13 +237,13 @@ class ThreadSet
220
237
  (c = @messages[m.id]) && c.root.thread
221
238
  end
222
239
 
223
- def delete_empties
224
- @subj_thread.each { |k, v| @subj_thread.delete(k) if v.empty? }
240
+ def delete_cruft
241
+ @subj_thread.each { |k, v| @subj_thread.delete(k) if v.empty? || v.subj != k }
225
242
  end
226
- private :delete_empties
243
+ private :delete_cruft
227
244
 
228
- def threads; delete_empties; @subj_thread.values; end
229
- def size; delete_empties; @subj_thread.size; end
245
+ def threads; delete_cruft; @subj_thread.values; end
246
+ def size; delete_cruft; @subj_thread.size; end
230
247
 
231
248
  def dump
232
249
  @subj_thread.each do |s, t|
@@ -273,19 +290,25 @@ class ThreadSet
273
290
 
274
291
  m = builder.call
275
292
  add_message m
276
- load_thread_for_message m
293
+ load_thread_for_message m, :load_killed => opts[:load_killed]
277
294
  yield @subj_thread.size if block_given?
278
295
  end
279
296
  end
280
297
 
281
298
  ## loads in all messages needed to thread m
282
- def load_thread_for_message m
283
- @index.each_message_in_thread_for m, :limit => 100 do |mid, builder|
299
+ def load_thread_for_message m, opts={}
300
+ @index.each_message_in_thread_for m, opts.merge({:limit => 100}) do |mid, builder|
284
301
  next if contains_id? mid
285
302
  add_message builder.call
286
303
  end
287
304
  end
288
305
 
306
+ ## merges in a pre-loaded thread
307
+ def add_thread t
308
+ raise "duplicate" if @subj_thread.values.member? t
309
+ t.each { |m, *o| add_message m }
310
+ end
311
+
289
312
  def is_relevant? m
290
313
  m.refs.any? { |ref_id| @messages[ref_id] }
291
314
  end
@@ -323,38 +346,68 @@ class ThreadSet
323
346
 
324
347
  if root == oldroot
325
348
  if oldroot.thread
326
- # puts "*** root (#{root.subj}) == oldroot (#{oldroot.subj}); ignoring"
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
+
327
365
  else
328
366
  ## to disable subject grouping, use the next line instead
329
367
  ## (and the same for below)
330
- #Redwood::log "[1] normalized subject for #{id} is #{Message.normalize_subj(root.subj)}"
368
+ #Redwood::log "[1] for #{root}, subject #{Message.normalize_subj(root.subj)} has #{@subj_thread[Message.normalize_subj(root.subj)] ? 'a' : 'no'} thread"
331
369
  thread = (@subj_thread[Message.normalize_subj(root.subj)] ||= Thread.new)
332
370
  #thread = (@subj_thread[root.id] ||= Thread.new)
333
371
 
334
372
  thread << root
335
373
  root.thread = thread
336
- # puts "# (1) added #{root} to #{thread}"
374
+ # Redwood::log "[1] added #{root} to #{thread}"
337
375
  end
338
376
  else
339
377
  if oldroot.thread
340
378
  ## new root. need to drop old one and put this one in its place
341
- # puts "*** DROPPING #{oldroot} from #{oldroot.thread}"
342
379
  oldroot.thread.drop oldroot
343
380
  oldroot.thread = nil
344
381
  end
345
382
 
346
383
  if root.thread
347
- # puts "*** IGNORING cuz root already has a 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
396
+ end
397
+
348
398
  else
349
399
  ## to disable subject grouping, use the next line instead
350
400
  ## (and the same above)
351
- #Redwood::log "[2] normalized subject for #{id} is #{Message.normalize_subj(root.subj)}"
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
+
352
405
  thread = (@subj_thread[Message.normalize_subj(root.subj)] ||= Thread.new)
353
406
  #thread = (@subj_thread[root.id] ||= Thread.new)
354
407
 
355
408
  thread << root
356
409
  root.thread = thread
357
- # puts "# (2) added #{root} to #{thread}"
410
+ # Redwood::log "[2] added #{root} to #{thread}"
358
411
  end
359
412
  end
360
413
 
data/lib/sup/update.rb CHANGED
@@ -11,10 +11,9 @@ class UpdateManager
11
11
  def register o; @targets[o] = true; end
12
12
  def unregister o; @targets.delete o; end
13
13
 
14
- def relay type, *args
14
+ def relay sender, type, *args
15
15
  meth = "handle_#{type}_update".intern
16
- @targets.keys.each { |o| o.send meth, *args if o.respond_to? meth }
17
- BufferManager.draw_screen ## TODO: think about this
16
+ @targets.keys.each { |o| o.send meth, sender, *args unless o == sender if o.respond_to? meth }
18
17
  end
19
18
  end
20
19
 
data/lib/sup/util.rb CHANGED
@@ -43,6 +43,19 @@ class Object
43
43
  ## i'm sure there's pithy comment i could make here about the
44
44
  ## superiority of lisp, but fuck lisp.
45
45
  def returning x; yield x; x; end
46
+
47
+ ## clone of java-style whole-method synchronization
48
+ ## assumes a @mutex variable
49
+ def synchronized *meth
50
+ meth.each do
51
+ class_eval <<-EOF
52
+ alias unsynchronized_#{meth} #{meth}
53
+ def #{meth}(*a, &b)
54
+ @mutex.synchronize { unsynchronized_#{meth}(*a, &b) }
55
+ end
56
+ EOF
57
+ end
58
+ end
46
59
  end
47
60
 
48
61
  class String
@@ -176,19 +189,6 @@ class Array
176
189
  def rest; self[1..-1]; end
177
190
 
178
191
  def to_boolean_h; Hash[*map { |x| [x, true] }.flatten]; end
179
-
180
- ## apparently uniq doesn't use ==. wtf.
181
- def remove_successive_dupes
182
- ret = []
183
- last = nil
184
- each do |e|
185
- unless e == last
186
- ret << e
187
- last = e
188
- end
189
- end
190
- ret
191
- end
192
192
  end
193
193
 
194
194
  class Time