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.
- data/History.txt +5 -0
- data/LICENSE +280 -0
- data/Manifest.txt +52 -0
- data/README.txt +119 -0
- data/Rakefile +45 -0
- data/bin/sup +229 -0
- data/bin/sup-import +162 -0
- data/doc/FAQ.txt +38 -0
- data/doc/Philosophy.txt +59 -0
- data/doc/TODO +31 -0
- data/lib/sup.rb +141 -0
- data/lib/sup/account.rb +53 -0
- data/lib/sup/buffer.rb +391 -0
- data/lib/sup/colormap.rb +118 -0
- data/lib/sup/contact.rb +40 -0
- data/lib/sup/draft.rb +105 -0
- data/lib/sup/index.rb +353 -0
- data/lib/sup/keymap.rb +89 -0
- data/lib/sup/label.rb +41 -0
- data/lib/sup/logger.rb +42 -0
- data/lib/sup/mbox.rb +51 -0
- data/lib/sup/mbox/loader.rb +116 -0
- data/lib/sup/message.rb +302 -0
- data/lib/sup/mode.rb +79 -0
- data/lib/sup/modes/buffer-list-mode.rb +37 -0
- data/lib/sup/modes/compose-mode.rb +33 -0
- data/lib/sup/modes/contact-list-mode.rb +121 -0
- data/lib/sup/modes/edit-message-mode.rb +162 -0
- data/lib/sup/modes/forward-mode.rb +38 -0
- data/lib/sup/modes/help-mode.rb +19 -0
- data/lib/sup/modes/inbox-mode.rb +45 -0
- data/lib/sup/modes/label-list-mode.rb +89 -0
- data/lib/sup/modes/label-search-results-mode.rb +29 -0
- data/lib/sup/modes/line-cursor-mode.rb +133 -0
- data/lib/sup/modes/log-mode.rb +44 -0
- data/lib/sup/modes/person-search-results-mode.rb +29 -0
- data/lib/sup/modes/poll-mode.rb +24 -0
- data/lib/sup/modes/reply-mode.rb +136 -0
- data/lib/sup/modes/resume-mode.rb +18 -0
- data/lib/sup/modes/scroll-mode.rb +106 -0
- data/lib/sup/modes/search-results-mode.rb +31 -0
- data/lib/sup/modes/text-mode.rb +51 -0
- data/lib/sup/modes/thread-index-mode.rb +389 -0
- data/lib/sup/modes/thread-view-mode.rb +338 -0
- data/lib/sup/person.rb +120 -0
- data/lib/sup/poll.rb +80 -0
- data/lib/sup/sent.rb +46 -0
- data/lib/sup/tagger.rb +40 -0
- data/lib/sup/textfield.rb +83 -0
- data/lib/sup/thread.rb +358 -0
- data/lib/sup/update.rb +21 -0
- data/lib/sup/util.rb +260 -0
- metadata +123 -0
data/lib/sup/poll.rb
ADDED
@@ -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
|
data/lib/sup/sent.rb
ADDED
@@ -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
|
data/lib/sup/tagger.rb
ADDED
@@ -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
|
data/lib/sup/thread.rb
ADDED
@@ -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
|