sup 0.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 (53) hide show
  1. data/History.txt +5 -0
  2. data/LICENSE +280 -0
  3. data/Manifest.txt +52 -0
  4. data/README.txt +119 -0
  5. data/Rakefile +45 -0
  6. data/bin/sup +229 -0
  7. data/bin/sup-import +162 -0
  8. data/doc/FAQ.txt +38 -0
  9. data/doc/Philosophy.txt +59 -0
  10. data/doc/TODO +31 -0
  11. data/lib/sup.rb +141 -0
  12. data/lib/sup/account.rb +53 -0
  13. data/lib/sup/buffer.rb +391 -0
  14. data/lib/sup/colormap.rb +118 -0
  15. data/lib/sup/contact.rb +40 -0
  16. data/lib/sup/draft.rb +105 -0
  17. data/lib/sup/index.rb +353 -0
  18. data/lib/sup/keymap.rb +89 -0
  19. data/lib/sup/label.rb +41 -0
  20. data/lib/sup/logger.rb +42 -0
  21. data/lib/sup/mbox.rb +51 -0
  22. data/lib/sup/mbox/loader.rb +116 -0
  23. data/lib/sup/message.rb +302 -0
  24. data/lib/sup/mode.rb +79 -0
  25. data/lib/sup/modes/buffer-list-mode.rb +37 -0
  26. data/lib/sup/modes/compose-mode.rb +33 -0
  27. data/lib/sup/modes/contact-list-mode.rb +121 -0
  28. data/lib/sup/modes/edit-message-mode.rb +162 -0
  29. data/lib/sup/modes/forward-mode.rb +38 -0
  30. data/lib/sup/modes/help-mode.rb +19 -0
  31. data/lib/sup/modes/inbox-mode.rb +45 -0
  32. data/lib/sup/modes/label-list-mode.rb +89 -0
  33. data/lib/sup/modes/label-search-results-mode.rb +29 -0
  34. data/lib/sup/modes/line-cursor-mode.rb +133 -0
  35. data/lib/sup/modes/log-mode.rb +44 -0
  36. data/lib/sup/modes/person-search-results-mode.rb +29 -0
  37. data/lib/sup/modes/poll-mode.rb +24 -0
  38. data/lib/sup/modes/reply-mode.rb +136 -0
  39. data/lib/sup/modes/resume-mode.rb +18 -0
  40. data/lib/sup/modes/scroll-mode.rb +106 -0
  41. data/lib/sup/modes/search-results-mode.rb +31 -0
  42. data/lib/sup/modes/text-mode.rb +51 -0
  43. data/lib/sup/modes/thread-index-mode.rb +389 -0
  44. data/lib/sup/modes/thread-view-mode.rb +338 -0
  45. data/lib/sup/person.rb +120 -0
  46. data/lib/sup/poll.rb +80 -0
  47. data/lib/sup/sent.rb +46 -0
  48. data/lib/sup/tagger.rb +40 -0
  49. data/lib/sup/textfield.rb +83 -0
  50. data/lib/sup/thread.rb +358 -0
  51. data/lib/sup/update.rb +21 -0
  52. data/lib/sup/util.rb +260 -0
  53. metadata +123 -0
@@ -0,0 +1,80 @@
1
+ require 'thread'
2
+
3
+ module Redwood
4
+
5
+ class PollManager
6
+ include Singleton
7
+
8
+ DELAY = 300
9
+
10
+ def initialize
11
+ @polling = false
12
+ @last_poll = nil
13
+
14
+ self.class.i_am_the_instance self
15
+
16
+ ::Thread.new do
17
+ while true
18
+ sleep DELAY / 2
19
+ if @last_poll.nil? || (Time.now - @last_poll) >= DELAY
20
+ mbid = BufferManager.say "Polling for new messages..."
21
+ num, numi = poll { |s| BufferManager.say s, mbid }
22
+ BufferManager.clear mbid
23
+ BufferManager.flash "Loaded #{num} new messages, #{numi} to inbox." if num > 0
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ def poll
30
+ return [0, 0] if @polling
31
+ @polling = true
32
+ found = {}
33
+ total_num = 0
34
+ total_numi = 0
35
+ Index.usual_sources.each do |source|
36
+ next if source.done?
37
+ yield "Loading from #{source}... "
38
+
39
+ start_offset = nil
40
+ num = 0
41
+ num_inbox = 0
42
+ source.each do |offset, labels|
43
+ start_offset ||= offset
44
+
45
+ begin
46
+ m = Redwood::Message.new source, offset, labels
47
+ if found[m.id]
48
+ yield "Skipping duplicate message #{m.id}"
49
+ next
50
+ else
51
+ found[m.id] = true
52
+ end
53
+
54
+ if Index.add_message m
55
+ UpdateManager.relay :add, m
56
+ num += 1
57
+ total_num += 1
58
+ total_numi += 1 if m.labels.include? :inbox
59
+ end
60
+ rescue Redwood::MessageFormatError => e
61
+ yield "Ignoring erroneous message at #{source}##{offset}: #{e.message}"
62
+ end
63
+
64
+ if num % 1000 == 0 && num > 0
65
+ elapsed = Time.now - start
66
+ pctdone = (offset.to_f - start_offset) / (source.total.to_f - start_offset)
67
+ remaining = (source.total.to_f - offset.to_f) * (elapsed.to_f / (offset.to_f - start_offset))
68
+ yield "## #{num} (#{(pctdone * 100.0)}% done) read; #{elapsed.to_time_s} elapsed; est. #{remaining.to_time_s} remaining"
69
+ end
70
+ end
71
+ yield "Found #{num} messages" unless num == 0
72
+ end
73
+ yield "Done polling; loaded #{total_num} new messages total"
74
+ @last_poll = Time.now
75
+ @polling = false
76
+ [total_num, total_numi]
77
+ end
78
+ end
79
+
80
+ end
@@ -0,0 +1,46 @@
1
+ module Redwood
2
+
3
+ class SentManager
4
+ include Singleton
5
+
6
+ attr_accessor :source
7
+ def initialize fn
8
+ @fn = fn
9
+ @source = nil
10
+ self.class.i_am_the_instance self
11
+ end
12
+
13
+ def self.source_name; "sent"; end
14
+ def self.source_id; 9998; end
15
+ def new_source; @source = SentLoader.new @fn; end
16
+
17
+ def write_sent_message date, from_email
18
+ need_blank = File.exists?(@fn) && !File.zero?(@fn)
19
+ File.open(@fn, "a") do |f|
20
+ f.puts if need_blank
21
+ f.puts "From #{from_email} #{date}"
22
+ yield f
23
+ end
24
+ @source.each do |offset, labels|
25
+ m = Message.new @source, offset, labels
26
+ Index.add_message m
27
+ UpdateManager.relay :add, m
28
+ end
29
+ end
30
+ end
31
+
32
+ class SentLoader < MBox::Loader
33
+ def initialize filename, end_offset=0
34
+ File.open(filename, "w") { } unless File.exists? filename
35
+ super filename, end_offset, true, true
36
+ end
37
+
38
+ def id; SentManager.source_id; end
39
+ def to_s; SentManager.source_name; end
40
+
41
+ def labels; [:sent, :inbox]; end
42
+ end
43
+
44
+ Redwood::register_yaml(SentLoader, %w(filename end_offset))
45
+
46
+ end
@@ -0,0 +1,40 @@
1
+ module Redwood
2
+
3
+ class Tagger
4
+ def initialize mode
5
+ @mode = mode
6
+ @tagged = {}
7
+ end
8
+
9
+ def tagged? o; @tagged[o]; end
10
+ def toggle_tag_for o; @tagged[o] = !@tagged[o]; end
11
+ def drop_all_tags; @tagged.clear; end
12
+ def drop_tag_for o; @tagged.delete o; end
13
+
14
+ def apply_to_tagged
15
+ num_tagged = @tagged.map { |t| t ? 1 : 0 }.sum
16
+ if num_tagged == 0
17
+ BufferManager.flash "No tagged messages!"
18
+ return
19
+ end
20
+
21
+ noun = num_tagged == 1 ? "message" : "messages"
22
+ c = BufferManager.ask_getch "apply to #{num_tagged} tagged #{noun}:"
23
+ return if c.nil? # user cancelled
24
+
25
+ if(action = @mode.resolve_input c)
26
+ tagged_sym = "multi_#{action}".intern
27
+ if @mode.respond_to? tagged_sym
28
+ targets = @tagged.select_by_value
29
+ @mode.send tagged_sym, targets
30
+ else
31
+ BufferManager.flash "That command cannot be applied to multiple messages."
32
+ end
33
+ else
34
+ BufferManager.flash "Unknown command #{c.to_character}."
35
+ end
36
+ end
37
+
38
+ end
39
+
40
+ end
@@ -0,0 +1,83 @@
1
+ require 'curses'
2
+
3
+ module Redwood
4
+
5
+ class TextField
6
+ attr_reader :value
7
+
8
+ def initialize window, y, x, width
9
+ @w, @x, @y = window, x, y
10
+ @width = width
11
+ @i = nil
12
+ @history = []
13
+ end
14
+
15
+ def activate question, default=nil
16
+ @question = question
17
+ @value = nil
18
+ @field = Ncurses::Form.new_field 1, @width - question.length,
19
+ @y, @x + question.length, 0, 0
20
+ @form = Ncurses::Form.new_form [@field]
21
+
22
+ @history[@i = @history.size] = default || ""
23
+ Ncurses::Form.post_form @form
24
+ @field.set_field_buffer 0, @history[@i]
25
+ end
26
+
27
+ def position_cursor
28
+ @w.attrset Colormap.color_for(:none)
29
+ @w.mvaddstr @y, 0, @question
30
+ Ncurses.curs_set 1
31
+ Ncurses::Form.form_driver @form, Ncurses::Form::REQ_END_FIELD
32
+ end
33
+
34
+ def deactivate
35
+ @form.unpost_form
36
+ @form.free_form
37
+ @field.free_field
38
+ Ncurses.curs_set 0
39
+ end
40
+
41
+ def handle_input c
42
+ if c == 10 # Ncurses::KEY_ENTER
43
+ Ncurses::Form.form_driver @form, Ncurses::Form::REQ_VALIDATION
44
+ @value = @history[@i] = @field.field_buffer(0).gsub(/^\s+|\s+$/, "").gsub(/\s+/, " ")
45
+ return false
46
+ elsif c == Ncurses::KEY_CANCEL
47
+ @history.delete_at @i
48
+ @i = @history.empty? ? nil : (@i - 1) % @history.size
49
+ @value = nil
50
+ return false
51
+ end
52
+
53
+ d =
54
+ case c
55
+ when Ncurses::KEY_LEFT
56
+ Ncurses::Form::REQ_PREV_CHAR
57
+ when Ncurses::KEY_RIGHT
58
+ Ncurses::Form::REQ_NEXT_CHAR
59
+ when Ncurses::KEY_BACKSPACE
60
+ Ncurses::Form::REQ_DEL_PREV
61
+ when ?\001
62
+ Ncurses::Form::REQ_BEG_FIELD
63
+ when ?\005
64
+ Ncurses::Form::REQ_END_FIELD
65
+ when Ncurses::KEY_UP
66
+ @history[@i] = @field.field_buffer(0)
67
+ @i = (@i - 1) % @history.size
68
+ @field.set_field_buffer 0, @history[@i]
69
+ when Ncurses::KEY_DOWN
70
+ @history[@i] = @field.field_buffer(0)
71
+ @i = (@i + 1) % @history.size
72
+ @field.set_field_buffer 0, @history[@i]
73
+ else
74
+ c
75
+ end
76
+
77
+ Ncurses::Form.form_driver @form, d
78
+ Ncurses.refresh
79
+
80
+ true
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,358 @@
1
+ require 'date'
2
+
3
+ module Redwood
4
+
5
+ class Thread
6
+ include Enumerable
7
+
8
+ attr_reader :containers
9
+ def initialize
10
+ @containers = []
11
+ end
12
+
13
+ def << c
14
+ @containers << c
15
+ end
16
+
17
+ def empty?; @containers.empty?; end
18
+
19
+ def drop c
20
+ raise "bad drop" unless @containers.member? c
21
+ @containers.delete c
22
+ end
23
+
24
+ def dump
25
+ puts "=== start thread #{self} with #{@containers.length} trees ==="
26
+ @containers.each { |c| c.dump_recursive }
27
+ puts "=== end thread ==="
28
+ end
29
+
30
+ ## yields each message and some stuff
31
+ def each fake_root=false
32
+ adj = 0
33
+ root = @containers.find_all { |c| !Message.subj_is_reply?(c) }.argmin { |c| c.date }
34
+
35
+ if root
36
+ adj = 1
37
+ root.first_useful_descendant.each_with_stuff do |c, d, par|
38
+ yield c.message, d, (par ? par.message : nil)
39
+ end
40
+ elsif @containers.length > 1 && fake_root
41
+ adj = 1
42
+ yield :fake_root, 0, nil
43
+ end
44
+
45
+ @containers.each do |cont|
46
+ next if cont == root
47
+ fud = cont.first_useful_descendant
48
+ fud.each_with_stuff do |c, d, par|
49
+ ## special case here: if we're an empty root that's already
50
+ ## been joined by a fake root, don't emit
51
+ yield c.message, d + adj, (par ? par.message : nil) unless
52
+ fake_root && c.message.nil? && root.nil? && c == fud
53
+ end
54
+ end
55
+ end
56
+
57
+ def dirty?; any? { |m, *o| m && m.dirty? }; end
58
+ def date; map { |m, *o| m.date if m }.compact.max; end
59
+ def snippet; argfind { |m, *o| m && m.snippet }; end
60
+ def authors; map { |m, *o| m.from if m }.compact.uniq; end
61
+
62
+ def apply_label t; each { |m, *o| m && m.add_label(t) }; end
63
+ def remove_label t
64
+ each { |m, *o| m && m.remove_label(t) }
65
+ end
66
+
67
+ def toggle_label label
68
+ if has_label? label
69
+ remove_label label
70
+ return false
71
+ else
72
+ apply_label label
73
+ return true
74
+ end
75
+ end
76
+
77
+ def set_labels l; each { |m, *o| m && m.labels = l }; end
78
+
79
+ def has_label? t; any? { |m, *o| m && m.has_label?(t) }; end
80
+ def save index; each { |m, *o| m && m.save(index) }; end
81
+
82
+ def direct_participants
83
+ map { |m, *o| [m.from] + m.to if m }.flatten.compact.uniq
84
+ end
85
+
86
+ def participants
87
+ map { |m, *o| [m.from] + m.to + m.cc + m.bcc if m }.flatten.compact.uniq
88
+ end
89
+
90
+ def size; map { |m, *o| m ? 1 : 0 }.sum; end
91
+ def subj; argfind { |m, *o| m && m.subj }; end
92
+ def labels
93
+ map { |m, *o| m && m.labels }.flatten.compact.uniq.sort_by { |t| t.to_s }
94
+ end
95
+ def labels= l
96
+ each { |m, *o| m && m.labels = l.clone }
97
+ end
98
+
99
+ def latest_message
100
+ inject(nil) do |a, b|
101
+ b = b.first
102
+ if a.nil?
103
+ b
104
+ elsif b.nil?
105
+ a
106
+ else
107
+ b.date > a.date ? b : a
108
+ end
109
+ end
110
+ end
111
+
112
+ def to_s
113
+ "<thread containing: #{@containers.join ', '}>"
114
+ end
115
+ end
116
+
117
+ ## recursive structure used internally to represent message trees as
118
+ ## described by reply-to: and references: headers.
119
+ ##
120
+ ## the 'id' field is the same as the message id. but the message might
121
+ ## be empty, in the case that we represent a message that was referenced
122
+ ## by another message (as an ancestor) but never received.
123
+ class Container
124
+ attr_accessor :message, :parent, :children, :id, :thread
125
+
126
+ def initialize id
127
+ raise "non-String #{id.inspect}" unless id.is_a? String
128
+ @id = id
129
+ @message, @parent, @thread = nil, nil, nil
130
+ @children = []
131
+ end
132
+
133
+ def each_with_stuff parent=nil
134
+ yield self, 0, parent
135
+ @children.each do |c|
136
+ c.each_with_stuff(self) { |cc, d, par| yield cc, d + 1, par }
137
+ end
138
+ end
139
+
140
+ def descendant_of? o
141
+ if o == self
142
+ true
143
+ else
144
+ @parent && @parent.descendant_of?(o)
145
+ end
146
+ end
147
+
148
+ def == o; Container === o && id == o.id; end
149
+
150
+ def empty?; @message.nil?; end
151
+ def root?; @parent.nil?; end
152
+ def root; root? ? self : @parent.root; end
153
+
154
+ def first_useful_descendant
155
+ if empty? && @children.size == 1
156
+ @children.first.first_useful_descendant
157
+ else
158
+ self
159
+ end
160
+ end
161
+
162
+ def find_attr attr
163
+ if empty?
164
+ @children.argfind { |c| c.find_attr attr }
165
+ else
166
+ @message.send attr
167
+ end
168
+ end
169
+ def subj; find_attr :subj; end
170
+ def date; find_attr :date; end
171
+
172
+ def is_reply?; subj && Message.subject_is_reply?(subj); end
173
+
174
+ def to_s
175
+ [ "<#{id}",
176
+ (@parent.nil? ? nil : "parent=#{@parent.id}"),
177
+ (@children.empty? ? nil : "children=#{@children.map { |c| c.id }.inspect}"),
178
+ ].compact.join(" ") + ">"
179
+ end
180
+
181
+ def dump_recursive indent=0, root=true, parent=nil
182
+ raise "inconsistency" unless parent.nil? || parent.children.include?(self)
183
+ unless root
184
+ print " " * indent
185
+ print "+->"
186
+ end
187
+ line = #"[#{useful? ? 'U' : ' '}] " +
188
+ if @message
189
+ "[#{thread}] #{@message.subj} " ##{@message.refs.inspect} / #{@message.replytos.inspect}"
190
+ else
191
+ "<no message>"
192
+ end
193
+
194
+ puts "#{id} #{line}"#[0 .. (105 - indent)]
195
+ indent += 3
196
+ @children.each { |c| c.dump_recursive indent, false, self }
197
+ end
198
+ end
199
+
200
+ ## a set of threads (so a forest). builds the thread structures by
201
+ ## reading messages from an index.
202
+ class ThreadSet
203
+ attr_reader :num_messages
204
+
205
+ def initialize index
206
+ @index = index
207
+ @num_messages = 0
208
+ @messages = {} ## map from message ids to container objects
209
+ @subj_thread = {} ## map from subject strings to thread objects
210
+ end
211
+
212
+ def contains_id? id; @messages.member?(id) && !@messages[id].empty?; end
213
+ def thread_for m
214
+ (c = @messages[m.id]) && c.root.thread
215
+ end
216
+
217
+ def delete_empties
218
+ @subj_thread.each { |k, v| @subj_thread.delete(k) if v.empty? }
219
+ end
220
+ private :delete_empties
221
+
222
+ def threads; delete_empties; @subj_thread.values; end
223
+ def size; delete_empties; @subj_thread.size; end
224
+
225
+ def dump
226
+ @subj_thread.each do |s, t|
227
+ puts "**********************"
228
+ puts "** for subject #{s} **"
229
+ puts "**********************"
230
+ t.dump
231
+ end
232
+ end
233
+
234
+ def link p, c, overwrite=false
235
+ if p == c || p.descendant_of?(c) || c.descendant_of?(p) # would create a loop
236
+ # puts "*** linking parent #{p} and child #{c} would create a loop"
237
+ return
238
+ end
239
+
240
+ if c.parent.nil? || overwrite
241
+ c.parent.children.delete c if overwrite && c.parent
242
+ if c.thread
243
+ c.thread.drop c
244
+ c.thread = nil
245
+ end
246
+ p.children << c
247
+ c.parent = p
248
+ end
249
+ end
250
+ private :link
251
+
252
+ def remove mid
253
+ return unless(c = @messages[mid])
254
+
255
+ c.parent.children.delete c if c.parent
256
+ if c.thread
257
+ c.thread.drop c
258
+ c.thread = nil
259
+ end
260
+ end
261
+
262
+ ## load in (at most) num number of threads from the index
263
+ def load_n_threads num, opts={}
264
+ @index.each_id_by_date opts do |mid, builder|
265
+ break if size >= num
266
+ next if contains_id? mid
267
+
268
+ m = builder.call
269
+ add_message m
270
+ load_thread_for_message m
271
+ yield @subj_thread.size if block_given?
272
+ end
273
+ end
274
+
275
+ ## loads in all messages needed to thread m
276
+ def load_thread_for_message m
277
+ @index.each_message_in_thread_for m, :limit => 100 do |mid, builder|
278
+ next if contains_id? mid
279
+ add_message builder.call
280
+ end
281
+ end
282
+
283
+ def is_relevant? m
284
+ m.refs.any? { |ref_id| @messages[ref_id] }
285
+ end
286
+
287
+ ## an "online" version of the jwz threading algorithm.
288
+ def add_message message
289
+ id = message.id
290
+ el = (@messages[id] ||= Container.new id)
291
+ return if @messages[id].message # we've seen it before
292
+
293
+ el.message = message
294
+ oldroot = el.root
295
+
296
+ ## link via references:
297
+ prev = nil
298
+ message.refs.each do |ref_id|
299
+ raise "non-String ref id #{ref_id.inspect} (full: #{message.refs.inspect})" unless ref_id.is_a?(String)
300
+ ref = (@messages[ref_id] ||= Container.new ref_id)
301
+ link prev, ref if prev
302
+ prev = ref
303
+ end
304
+ link prev, el, true if prev
305
+
306
+ ## link via in-reply-to:
307
+ message.replytos.each do |ref_id|
308
+ ref = (@messages[ref_id] ||= Container.new ref_id)
309
+ link ref, el, true
310
+ break # only do the first one
311
+ end
312
+
313
+ ## update subject grouping
314
+ root = el.root
315
+ # puts "> have #{el}, root #{root}, oldroot #{oldroot}"
316
+ # el.dump_recursive
317
+
318
+ if root == oldroot
319
+ if oldroot.thread
320
+ # puts "*** root (#{root.subj}) == oldroot (#{oldroot.subj}); ignoring"
321
+ else
322
+ ## to disable subject grouping, use the next line instead
323
+ ## (and the same for below)
324
+ thread = (@subj_thread[Message.normalize_subj(root.subj)] ||= Thread.new)
325
+ #thread = (@subj_thread[root.id] ||= Thread.new)
326
+
327
+ thread << root
328
+ root.thread = thread
329
+ # puts "# (1) added #{root} to #{thread}"
330
+ end
331
+ else
332
+ if oldroot.thread
333
+ ## new root. need to drop old one and put this one in its place
334
+ # puts "*** DROPPING #{oldroot} from #{oldroot.thread}"
335
+ oldroot.thread.drop oldroot
336
+ oldroot.thread = nil
337
+ end
338
+
339
+ if root.thread
340
+ # puts "*** IGNORING cuz root already has a thread"
341
+ else
342
+ ## to disable subject grouping, use the next line instead
343
+ ## (and the same above)
344
+ thread = (@subj_thread[Message.normalize_subj(root.subj)] ||= Thread.new)
345
+ #thread = (@subj_thread[root.id] ||= Thread.new)
346
+
347
+ thread << root
348
+ root.thread = thread
349
+ # puts "# (2) added #{root} to #{thread}"
350
+ end
351
+ end
352
+
353
+ ## last bit
354
+ @num_messages += 1
355
+ end
356
+ end
357
+
358
+ end