8 attr_reader :containers
10 ## ah, the joys of a multithreaded application with a class called
11 ## "Thread". i keep instantiating the wrong one...
12 raise "wrong Thread class, buddy!" if block_given?
20 def empty?; @containers.empty?; end
23 raise "bad drop" unless @containers.member? c
28 puts "=== start thread #{self} with #{@containers.length} trees ==="
29 @containers.each { |c| c.dump_recursive }
30 puts "=== end thread ==="
33 ## yields each message, its depth, and its parent
34 ## note that the message can be a Message object, or :fake_root,
36 def each fake_root=false
38 root = @containers.find_all { |c| !Message.subj_is_reply?(c) }.argmin { |c| c.date }
42 root.first_useful_descendant.each_with_stuff do |c, d, par|
43 yield c.message, d, (par ? par.message : nil)
45 elsif @containers.length > 1 && fake_root
47 yield :fake_root, 0, nil
50 @containers.each do |cont|
52 fud = cont.first_useful_descendant
53 fud.each_with_stuff do |c, d, par|
54 ## special case here: if we're an empty root that's already
55 ## been joined by a fake root, don't emit
56 yield c.message, d + adj, (par ? par.message : nil) unless
57 fake_root && c.message.nil? && root.nil? && c == fud
62 def dirty?; any? { |m, *o| m && m.dirty? }; end
63 def date; map { |m, *o| m.date if m }.compact.max; end
64 def snippet; argfind { |m, *o| m && m.snippet }; end
65 def authors; map { |m, *o| m.from if m }.compact.uniq; end
67 def apply_label t; each { |m, *o| m && m.add_label(t) }; end
69 each { |m, *o| m && m.remove_label(t) }
72 def toggle_label label
82 def set_labels l; each { |m, *o| m && m.labels = l }; end
84 def has_label? t; any? { |m, *o| m && m.has_label?(t) }; end
85 def dirty?; any? { |m, *o| m && m.dirty? }; end
86 def save index; each { |m, *o| m && m.save(index) }; end
88 def direct_participants
89 map { |m, *o| [m.from] + m.to if m }.flatten.compact.uniq
93 map { |m, *o| [m.from] + m.to + m.cc + m.bcc if m }.flatten.compact.uniq
96 def size; map { |m, *o| m ? 1 : 0 }.sum; end
97 def subj; argfind { |m, *o| m && m.subj }; end
99 map { |m, *o| m && m.labels }.flatten.compact.uniq.sort_by { |t| t.to_s }
102 each { |m, *o| m && m.labels = l.clone }
106 inject(nil) do |a, b|
113 b.date > a.date ? b : a
119 "<thread containing: #{@containers.join ', '}>"
123 ## recursive structure used internally to represent message trees as
124 ## described by reply-to: and references: headers.
126 ## the 'id' field is the same as the message id. but the message might
127 ## be empty, in the case that we represent a message that was referenced
128 ## by another message (as an ancestor) but never received.
130 attr_accessor :message, :parent, :children, :id, :thread
133 raise "non-String #{id.inspect}" unless id.is_a? String
135 @message, @parent, @thread = nil, nil, nil
139 def each_with_stuff parent=nil
140 yield self, 0, parent
141 @children.each do |c|
142 c.each_with_stuff(self) { |cc, d, par| yield cc, d + 1, par }
150 @parent && @parent.descendant_of?(o)
154 def == o; Container === o && id == o.id; end
156 def empty?; @message.nil?; end
157 def root?; @parent.nil?; end
158 def root; root? ? self : @parent.root; end
160 def first_useful_descendant
161 if empty? && @children.size == 1
162 @children.first.first_useful_descendant
170 @children.argfind { |c| c.find_attr attr }
175 def subj; find_attr :subj; end
176 def date; find_attr :date; end
178 def is_reply?; subj && Message.subject_is_reply?(subj); end
182 (@parent.nil? ? nil : "parent=#{@parent.id}"),
183 (@children.empty? ? nil : "children=#{@children.map { |c| c.id }.inspect}"),
184 ].compact.join(" ") + ">"
187 def dump_recursive indent=0, root=true, parent=nil
188 raise "inconsistency" unless parent.nil? || parent.children.include?(self)
193 line = #"[#{useful? ? 'U' : ' '}] " +
195 "[#{thread}] #{@message.subj} " ##{@message.refs.inspect} / #{@message.replytos.inspect}"
200 puts "#{id} #{line}"#[0 .. (105 - indent)]
202 @children.each { |c| c.dump_recursive indent, false, self }
206 ## a set of threads (so a forest). builds the thread structures by
207 ## reading messages from an index.
209 attr_reader :num_messages
214 @messages = {} ## map from message ids to container objects
215 @subj_thread = {} ## map from subject strings to thread objects
218 def contains_id? id; @messages.member?(id) && !@messages[id].empty?; end
220 (c = @messages[m.id]) && c.root.thread
224 @subj_thread.each { |k, v| @subj_thread.delete(k) if v.empty? }
226 private :delete_empties
228 def threads; delete_empties; @subj_thread.values; end
229 def size; delete_empties; @subj_thread.size; end
232 @subj_thread.each do |s, t|
233 puts "**********************"
234 puts "** for subject #{s} **"
235 puts "**********************"
240 def link p, c, overwrite=false
241 if p == c || p.descendant_of?(c) || c.descendant_of?(p) # would create a loop
242 # puts "*** linking parent #{p} and child #{c} would create a loop"
246 if c.parent.nil? || overwrite
247 c.parent.children.delete c if overwrite && c.parent
259 return unless(c = @messages[mid])
261 c.parent.children.delete c if c.parent
268 ## load in (at most) num number of threads from the index
269 def load_n_threads num, opts={}
270 @index.each_id_by_date opts do |mid, builder|
272 next if contains_id? mid
276 load_thread_for_message m
277 yield @subj_thread.size if block_given?
281 ## 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|
284 next if contains_id? mid
285 add_message builder.call
290 m.refs.any? { |ref_id| @messages[ref_id] }
293 ## an "online" version of the jwz threading algorithm.
294 def add_message message
296 el = (@messages[id] ||= Container.new id)
297 return if @messages[id].message # we've seen it before
302 ## link via references:
304 message.refs.each do |ref_id|
305 raise "non-String ref id #{ref_id.inspect} (full: #{message.refs.inspect})" unless ref_id.is_a?(String)
306 ref = (@messages[ref_id] ||= Container.new ref_id)
307 link prev, ref if prev
310 link prev, el, true if prev
312 ## link via in-reply-to:
313 message.replytos.each do |ref_id|
314 ref = (@messages[ref_id] ||= Container.new ref_id)
316 break # only do the first one
319 ## update subject grouping
321 # puts "> have #{el}, root #{root}, oldroot #{oldroot}"
326 # puts "*** root (#{root.subj}) == oldroot (#{oldroot.subj}); ignoring"
328 ## to disable subject grouping, use the next line instead
329 ## (and the same for below)
330 #Redwood::log "[1] normalized subject for #{id} is #{Message.normalize_subj(root.subj)}"
331 thread = (@subj_thread[Message.normalize_subj(root.subj)] ||= Thread.new)
332 #thread = (@subj_thread[root.id] ||= Thread.new)
336 # puts "# (1) added #{root} to #{thread}"
340 ## new root. need to drop old one and put this one in its place
341 # puts "*** DROPPING #{oldroot} from #{oldroot.thread}"
342 oldroot.thread.drop oldroot
347 # puts "*** IGNORING cuz root already has a thread"
349 ## to disable subject grouping, use the next line instead
350 ## (and the same above)
351 #Redwood::log "[2] normalized subject for #{id} is #{Message.normalize_subj(root.subj)}"
352 thread = (@subj_thread[Message.normalize_subj(root.subj)] ||= Thread.new)
353 #thread = (@subj_thread[root.id] ||= Thread.new)
357 # puts "# (2) added #{root} to #{thread}"