class Redwood::ThreadSet

A set of threads, so a forest. Is integrated with the index and builds thread structures by reading messages from it.

If 'thread_by_subj' is true, puts messages with the same subject in one thread, even if they don't reference each other. This is helpful for crappy MUAs that don't set In-reply-to: or References: headers, but means that messages may be threaded unnecessarily.

The following invariants are maintained: every Thread has at least one Container tree, and every Container tree has at least one Message.

Attributes

num_messages[R]

Public Class Methods

new(index, thread_by_subj=true) click to toggle source
# File lib/sup/thread.rb, line 263
def initialize index, thread_by_subj=true
  @index = index
  @num_messages = 0
  ## map from message ids to container objects
  @messages = SavingHash.new { |id| Container.new id }
  ## map from subject strings or (or root message ids) to thread objects
  @threads = SavingHash.new { Thread.new }
  @thread_by_subj = thread_by_subj
end

Public Instance Methods

add_message(message) click to toggle source

the heart of the threading code

# File lib/sup/thread.rb, line 402
def add_message message
  el = @messages[message.id]
  return if el.message # we've seen it before

  #puts "adding: #{message.id}, refs #{message.refs.inspect}"

  el.message = message
  oldroot = el.root

  ## link via references:
  (message.refs + [el.id]).inject(nil) do |prev, ref_id|
    ref = @messages[ref_id]
    link prev, ref if prev
    ref
  end

  ## link via in-reply-to:
  message.replytos.each do |ref_id|
    ref = @messages[ref_id]
    link ref, el, true
    break # only do the first one
  end

  root = el.root
  key =
    if thread_by_subj?
      Message.normalize_subj root.subj
    else
      root.id
    end

  ## check to see if the subject is still the same (in the case
  ## that we first added a child message with a different
  ## subject)
  if root.thread
    if @threads.member?(key) && @threads[key] != root.thread
      @threads.delete key
    end
  else
    thread = @threads[key]
    thread << root
    root.thread = thread
  end

  ## last bit
  @num_messages += 1
end
add_thread(t) click to toggle source

merges in a pre-loaded thread

# File lib/sup/thread.rb, line 360
def add_thread t
  raise "duplicate" if @threads.values.member? t
  t.each { |m, *o| add_message m }
end
contains?(m;) click to toggle source
# File lib/sup/thread.rb, line 276
def contains? m; contains_id? m.id end
contains_id?(id;) click to toggle source
# File lib/sup/thread.rb, line 274
def contains_id? id; @messages.member?(id) && !@messages[id].empty? end
delete_message(message) click to toggle source
# File lib/sup/thread.rb, line 395
def delete_message message
  el = @messages[message.id]
  return unless el.message
  el.message = nil
end
dump(f=$stdout) click to toggle source
# File lib/sup/thread.rb, line 281
def dump f=$stdout
  @threads.each do |s, t|
    f.puts "**********************"
    f.puts "** for subject #{s} **"
    f.puts "**********************"
    t.dump f
  end
end
is_relevant?(m) click to toggle source
# File lib/sup/thread.rb, line 391
def is_relevant? m
  m.refs.any? { |ref_id| @messages.member? ref_id }
end
join_threads(threads) click to toggle source

merges two threads together. both must be members of this threadset. does its best, heuristically, to determine which is the parent.

# File lib/sup/thread.rb, line 367
def join_threads threads
  return if threads.size < 2

  containers = threads.map do |t|
    c = @messages.member?(t.first.id) ? @messages[t.first.id] : nil
    raise "not in threadset: #{t.first.id}" unless c && c.message
    c
  end

  ## use subject headers heuristically
  parent = containers.find { |c| !c.is_reply? }

  ## no thread was rooted by a non-reply, so make a fake parent
  parent ||= @messages["joining-ref-" + containers.map { |c| c.id }.join("-")]

  containers.each do |c|
    next if c == parent
    c.message.add_ref parent.id
    link parent, c
  end

  true
end
load_n_threads(num, opts={}) { |size| ... } click to toggle source

load in (at most) num number of threads from the index

# File lib/sup/thread.rb, line 338
def load_n_threads num, opts={}
  @index.each_id_by_date opts do |mid, builder|
    break if size >= num unless num == -1
    next if contains_id? mid

    m = builder.call
    load_thread_for_message m, :skip_killed => opts[:skip_killed], :load_deleted => opts[:load_deleted], :load_spam => opts[:load_spam]
    yield size if block_given?
  end
end
load_thread_for_message(m, opts={}) click to toggle source

loads in all messages needed to thread m may do nothing if m's thread is killed

# File lib/sup/thread.rb, line 351
def load_thread_for_message m, opts={}
  good = @index.each_message_in_thread_for m, opts do |mid, builder|
    next if contains_id? mid
    add_message builder.call
  end
  add_message m if good
end
remove_id(mid) click to toggle source
# File lib/sup/thread.rb, line 323
def remove_id mid
  return unless @messages.member?(mid)
  c = @messages[mid]
  remove_container c
  prune_thread_of c
end
remove_thread_containing_id(mid) click to toggle source
# File lib/sup/thread.rb, line 330
def remove_thread_containing_id mid
  return unless @messages.member?(mid)
  c = @messages[mid]
  t = c.root.thread
  @threads.delete_if { |key, thread| t == thread }
end
size() click to toggle source
# File lib/sup/thread.rb, line 279
def size; @threads.size end
thread_for(m;) click to toggle source
# File lib/sup/thread.rb, line 275
def thread_for m; thread_for_id m.id end
thread_for_id(mid;) click to toggle source
# File lib/sup/thread.rb, line 273
def thread_for_id mid; @messages.member?(mid) && @messages[mid].root.thread end
threads() click to toggle source
# File lib/sup/thread.rb, line 278
def threads; @threads.values end

Private Instance Methods

prune_thread_of(c) click to toggle source
# File lib/sup/thread.rb, line 315
def prune_thread_of c
  return unless c.thread
  c.thread.drop c
  @threads.delete_if { |k, v| v == c.thread } if c.thread.empty?
  c.thread = nil
end
remove_container(c) click to toggle source
# File lib/sup/thread.rb, line 310
def remove_container c
  c.parent.children.delete c if c.parent # remove from tree
end