lmode.on_kill { Logger.clear! }
Logger.add_sink lmode
Logger.force_message "Welcome to Sup! Log level is set to #{Logger.level}."
+ if Logger::LEVELS.index(Logger.level) > 0
+ Logger.force_message "For more verbose logging, restart with SUP_LOG_LEVEL=#{Logger::LEVELS[Logger::LEVELS.index(Logger.level)-1]}."
+ end
debug "initializing inbox buffer"
imode = InboxMode.new
Options:
EOS
-end #' stupid ruby-mode
+end
def axe q, default=nil
- ans =
- if default && !default.empty?
- ask "#{q} (enter for \"#{default}\"): "
- else
- ask "#{q}: "
- end
+ ans = if default && !default.empty?
+ ask "#{q} (enter for \"#{default}\"): "
+ else
+ ask "#{q}: "
+ end
ans.empty? ? default : ans
end
while true do
say "Ok, now for the details."
- default_labels, components =
- case type
- when :mbox
- $last_fn ||= ENV["MAIL"]
- fn = axe "What's the full path to the mbox file?", $last_fn #"srm
- return if fn.nil? || fn.empty?
-
- $last_fn = fn
- [Redwood::MBox::Loader.suggest_labels_for(fn),
- { :scheme => "mbox", :path => fn }]
- when :maildir
- $last_fn ||= ENV["MAIL"]
- fn = axe "What's the full path to the maildir directory?", $last_fn #"srm
- return if fn.nil? || fn.empty?
-
- $last_fn = fn
- [Redwood::Maildir.suggest_labels_for(fn),
- { :scheme => "maildir", :path => fn }]
- when :mboxssh
- $last_server ||= "localhost"
- srv = axe "What machine is the mbox file located on?", $last_server
- return if srv.nil? || srv.empty?
- $last_server = srv
-
- fn = axe "What's the path to the mbox file?", $last_fn #" stupid ruby-mode
- return if fn.nil? || fn.empty?
- $last_fn = fn
- fn = "/#{fn}" # lame
- [Redwood::MBox::SSHLoader.suggest_labels_for(fn),
- { :scheme => "mbox+ssh", :host => srv, :path => fn }]
- when :imap, :imaps
- $last_server ||= "localhost"
- srv = axe "What is the IMAP server (host, or host:port notation)?", $last_server
- return if srv.nil? || srv.empty?
- $last_server = srv
-
- $last_folder ||= "INBOX"
- fn = axe "What's the folder path?", $last_folder #"srm
- return if fn.nil? || fn.empty?
- $last_folder = fn
-
- fn = "/#{fn}" # lame
- if srv =~ /^(\S+):(\d+)$/
- host, port = $1, $2.to_i
- else
- host, port = srv, nil
- end
- [Redwood::IMAP.suggest_labels_for(fn),
- { :scheme => type.to_s, :host => host, :port => port, :path => fn }]
- end
-
- uri =
- begin
- URI::Generic.build components
- rescue URI::Error => e
- say "Whoopsie! I couldn't build a URI from that: #{e.message}"
- if axe_yes("Try again?") then next else return end
+ default_labels, components = case type
+ when :mbox
+ $last_fn ||= ENV["MAIL"]
+ fn = axe "What's the full path to the mbox file?", $last_fn
+ return if fn.nil? || fn.empty?
+
+ $last_fn = fn
+ [Redwood::MBox::Loader.suggest_labels_for(fn),
+ { :scheme => "mbox", :path => fn }]
+ when :maildir
+ $last_fn ||= ENV["MAIL"]
+ fn = axe "What's the full path to the maildir directory?", $last_fn
+ return if fn.nil? || fn.empty?
+
+ $last_fn = fn
+ [Redwood::Maildir.suggest_labels_for(fn),
+ { :scheme => "maildir", :path => fn }]
+ when :mboxssh
+ $last_server ||= "localhost"
+ srv = axe "What machine is the mbox file located on?", $last_server
+ return if srv.nil? || srv.empty?
+ $last_server = srv
+
+ fn = axe "What's the path to the mbox file?", $last_fn
+ return if fn.nil? || fn.empty?
+ $last_fn = fn
+ fn = "/#{fn}" # lame
+ [Redwood::MBox::SSHLoader.suggest_labels_for(fn),
+ { :scheme => "mbox+ssh", :host => srv, :path => fn }]
+ when :imap, :imaps
+ $last_server ||= "localhost"
+ srv = axe "What is the IMAP server (host, or host:port notation)?", $last_server
+ return if srv.nil? || srv.empty?
+ $last_server = srv
+
+ $last_folder ||= "INBOX"
+ fn = axe "What's the folder path?", $last_folder
+ return if fn.nil? || fn.empty?
+ $last_folder = fn
+
+ fn = "/#{fn}"
+ if srv =~ /^(\S+):(\d+)$/
+ host, port = $1, $2.to_i
+ else
+ host, port = srv, nil
end
+ [Redwood::IMAP.suggest_labels_for(fn),
+ { :scheme => type.to_s, :host => host, :port => port, :path => fn }]
+ end
+
+ uri = begin
+ URI::Generic.build components
+ rescue URI::Error => e
+ say "Whoopsie! I couldn't build a URI from that: #{e.message}"
+ if axe_yes("Try again?") then next else return end
+ end
say "I'm going to add this source: #{uri}"
unless axe("Does that look right?", "y") =~ /^y|yes$/i
labels_str = axe("Enter any labels to be automatically added to all messages from this source, separated by spaces (or 'none')", default_labels.join(","))
- labels =
- if labels_str =~ /^\s*none\s*$/i
- nil
- else
- labels_str.split(/\s+/)
- end
-
+ labels = if labels_str =~ /^\s*none\s*$/i
+ nil
+ else
+ labels_str.split(/\s+/)
+ end
+
cmd = build_cmd "sup-add"
cmd += " --unusual" unless usual
cmd += " --archive" if archive
Just answer these simple questions and you'll be on your way.
EOS
-#' stupid ruby-mode
account = $config[:accounts][:default]
name = axe "What's your name?", account[:name]
-email = axe "What's your (primary) email address?", account[:email] #'srm
+email = axe "What's your (primary) email address?", account[:email]
-say "Ok, your header will look like this:"
+say "Ok, your from header will look like this:"
say " From: #{name} <#{email}>"
say "\nDo you have any alternate email addresses that also receive email?"
$config[:accounts][:default][:signature] = sigfn
$config[:editor] = editor
-
done = false
until done
say "\nNow, we'll tell Sup where to find all your email."
choose do |menu|
menu.prompt = "Store my sent mail in? "
+ menu.choice('Default (an mbox in ~/.sup, aka sup://sent)') { $config[:sent_source] = 'sup://sent'} unless have_sup_sent
+
valid_sents = Redwood::SourceManager.sources.each do |s|
have_sup_sent = true if s.to_s.eql?('sup://sent')
-
menu.choice(s.to_s) { $config[:sent_source] = s.to_s } if s.respond_to? :store_message
end
-
- menu.choice('Default (sup://sent)') { $config[:sent_source] = 'sup://sent'} unless have_sup_sent
-
end
end
say <<EOS
-Ok. The final step is to import all your messages into the Sup index.
+The final step is to import all your messages into the Sup index.
Depending on how many messages are in the sources, this could take
quite a while.
EOS
-#'
+
if axe_yes "Run sup-sync to import all messages now?"
while true
cmd = build_cmd("sup-sync") + " --all-sources"
## decide what to do based on message labels and the operation we're performing
dothis, new_labels = case
when (op == :restore) && restored_state[m.id] && old_m && (old_m.labels != restored_state[m.id])
+ num_restored += 1
[:update_message_state, restored_state[m.id]]
+ when (op == :restore) && restored_state[m.id] && !old_m
+ num_restored += 1
+ m.labels = restored_state[m.id]
+ :add_message
when op == :discard
if old_m && (old_m.labels != m.labels)
[:update_message_state, m.labels]
def save_yaml_obj o, fn, safe=false
o = if o.is_a?(Array)
o.map { |x| (x.respond_to?(:before_marshal) && x.before_marshal) || x }
+ elsif o.respond_to? :before_marshal
+ o.before_marshal
else
- o.respond_to?(:before_marshal) && o.before_marshal
+ o
end
if safe
## set up default configuration file
if File.exists? Redwood::CONFIG_FN
$config = Redwood::load_yaml_obj Redwood::CONFIG_FN
+ abort "#{Redwood::CONFIG_FN} is not a valid configuration file (it's a #{$config.class}, not a hash)" unless $config.is_a?(Hash)
else
require 'etc'
require 'socket'
end
notice = Chunk::CryptoNotice.new :valid, "This message has been decrypted for display"
- [RMail::Parser.read(decrypted_payload), sig, notice]
+ [notice, sig, RMail::Parser.read(decrypted_payload)]
else
- notice = Chunk::CryptoNotice.new :invalid, "This message could not be decrypted", output.split("\n")
- [nil, nil, notice]
+ Chunk::CryptoNotice.new :invalid, "This message could not be decrypted", output.split("\n")
end
end
module Redwood
class HookManager
- ## there's probably a better way to do this, but to evaluate a hook
- ## with a bunch of pre-set "local variables" i define a function
- ## per variable and then instance_evaluate the code.
- ##
- ## how does rails do it, when you pass :locals into a partial?
- ##
- ## i don't bother providing setters, since i'm pretty sure the
- ## charade will fall apart pretty quickly with respect to scoping.
- ## "fail-fast", we'll call it.
class HookContext
def initialize name
@__say_id = nil
@__name = name
- @__locals = {}
- end
-
- attr_writer :__locals
-
- def method_missing m, *a
- case @__locals[m]
- when Proc
- @__locals[m] = @__locals[m].call(*a) # only call the proc once
- when nil
- super
- else
- @__locals[m]
- end
+ @__cache = {}
end
def say s
HookManager.tags[tag] = value
end
- def __binding
- binding
- end
-
- def __cleanup
+ def __run __hook, __filename, __locals
+ __binding = binding
+ __lprocs, __lvars = __locals.partition { |k, v| v.is_a?(Proc) }
+ eval __lvars.map { |k, v| "#{k} = __locals[#{k.inspect}];" }.join, __binding
+ ## we also support closures for delays evaluation. unfortunately
+ ## we have to do this via method calls, so you don't get all the
+ ## semantics of a regular variable. not ideal.
+ __lprocs.each do |k, v|
+ self.class.instance_eval do
+ define_method k do
+ @__cache[k] ||= v.call
+ end
+ end
+ end
+ ret = eval __hook, __binding, __filename
BufferManager.clear @__say_id if @__say_id
+ @__cache = {}
+ ret
end
end
def run name, locals={}
hook = hook_for(name) or return
context = @contexts[hook] ||= HookContext.new(name)
- context.__locals = locals
result = nil
begin
- result = context.instance_eval @hooks[name], fn_for(name)
+ result = context.__run hook, fn_for(name), locals
rescue Exception => e
log "error running hook: #{e.message}"
log e.backtrace.join("\n")
@hooks[name] = nil # disable it
BufferManager.flash "Error running hook: #{e.message}" if BufferManager.instantiated?
end
- context.__cleanup
result
end
def hook_for name
unless @hooks.member? name
- @hooks[name] =
- begin
- returning IO.read(fn_for(name)) do
- log "read '#{name}' from #{fn_for(name)}"
- end
- rescue SystemCallError => e
- #log "disabled hook for '#{name}': #{e.message}"
- nil
+ @hooks[name] = begin
+ returning IO.read(fn_for(name)) do
+ debug "read '#{name}' from #{fn_for(name)}"
end
+ rescue SystemCallError => e
+ #debug "disabled hook for '#{name}': #{e.message}"
+ nil
+ end
end
@hooks[name]
include SerializeLabelsNicely
yaml_properties :uri, :cur_offset, :usual, :archived, :id, :labels
+ attr_reader :labels
+
## uri_or_fp is horrific. need to refactor.
def initialize uri_or_fp, start_offset=0, usual=true, archived=false, id=nil, labels=nil
@mutex = Mutex.new
end
self.cur_offset = next_offset
- [returned_offset, (@labels + [:unread])]
+ [returned_offset, (labels + [:unread])]
end
end
@list_unsubscribe = header["list-unsubscribe"]
end
+ ## Expected index entry format:
+ ## :message_id, :subject => String
+ ## :date => Time
+ ## :refs, :replytos => Array of String
+ ## :from => Person
+ ## :to, :cc, :bcc => Array of Person
+ def load_from_index! entry
+ @id = entry[:message_id]
+ @from = entry[:from]
+ @date = entry[:date]
+ @subj = entry[:subject]
+ @to = entry[:to]
+ @cc = entry[:cc]
+ @bcc = entry[:bcc]
+ @refs = (@refs + entry[:refs]).uniq
+ @replytos = entry[:replytos]
+
+ @replyto = nil
+ @list_address = nil
+ @recipient_email = nil
+ @source_marked_read = false
+ @list_subscribe = nil
+ @list_unsubscribe = nil
+ end
+
def add_ref ref
@refs << ref
@dirty = true
def has_label? t; @labels.member? t; end
def add_label l
+ l = l.to_sym
return if @labels.member? l
@labels << l
@dirty = true
end
def remove_label l
+ l = l.to_sym
return unless @labels.member? l
@labels.delete l
@dirty = true
def labels= l
raise ArgumentError, "not a set" unless l.is_a?(Set)
+ raise ArgumentError, "not a set of labels" unless l.all? { |ll| ll.is_a?(Symbol) }
return if @labels == l
@labels = l
@dirty = true
return
end
- decryptedm, sig, notice = CryptoManager.decrypt payload
- children = message_to_chunks(decryptedm, true) if decryptedm
- [notice, sig, children].flatten.compact
+ notice, sig, decryptedm = CryptoManager.decrypt payload
+ if decryptedm # managed to decrypt
+ children = message_to_chunks(decryptedm, true)
+ [notice, sig, children]
+ else
+ [notice]
+ end
end
## takes a RMail::Message, breaks it into Chunk:: classes.
chunks
elsif m.header.content_type == "message/rfc822"
- payload = RMail::Parser.read(m.body)
- from = payload.header.from.first
- from_person = from ? Person.from_address(from.format) : nil
- [Chunk::EnclosedMessage.new(from_person, payload.to_s)] +
- message_to_chunks(payload, encrypted)
+ if m.body
+ payload = RMail::Parser.read(m.body)
+ from = payload.header.from.first
+ from_person = from ? Person.from_address(from.format) : nil
+ [Chunk::EnclosedMessage.new(from_person, payload.to_s)] +
+ message_to_chunks(payload, encrypted)
+ else
+ [Chunk::EnclosedMessage.new(nil, "")]
+ end
else
filename =
## first, paw through the headers looking for a filename
PollManager.each_message_from(@source) do |m|
m.remove_label :unread
+ m.add_label :sent
PollManager.add_new_message m
end
end
## nasty multibyte hack for ruby 1.8. if it's utf-8, split into chars using
## the utf8 regex and count those. otherwise, use the byte length.
def display_length
- if $encoding == "UTF-8"
+ if $encoding == "UTF-8" || $encoding == "utf8"
scan(/./u).size
else
size
require 'xapian'
-require 'gdbm'
require 'set'
module Redwood
# for searching due to precomputing thread membership.
class XapianIndex < BaseIndex
STEM_LANGUAGE = "english"
+ INDEX_VERSION = '1'
## dates are converted to integers for xapian, and are used for document ids,
## so we must ensure they're reasonably valid. this typically only affect
end
def load_index
- @entries = MarshalledGDBM.new File.join(@dir, "entries.db")
- @docids = MarshalledGDBM.new File.join(@dir, "docids.db")
- @thread_members = MarshalledGDBM.new File.join(@dir, "thread_members.db")
- @thread_ids = MarshalledGDBM.new File.join(@dir, "thread_ids.db")
- @assigned_docids = GDBM.new File.join(@dir, "assigned_docids.db")
-
- @xapian = Xapian::WritableDatabase.new(File.join(@dir, "xapian"), Xapian::DB_CREATE_OR_OPEN)
+ path = File.join(@dir, 'xapian')
+ if File.exists? path
+ @xapian = Xapian::WritableDatabase.new(path, Xapian::DB_OPEN)
+ db_version = @xapian.get_metadata 'version'
+ db_version = '0' if db_version.empty?
+ if db_version != INDEX_VERSION
+ fail "This Sup version expects a v#{INDEX_VERSION} index, but you have an existing v#{db_version} index. Please downgrade to your previous version and dump your labels before upgrading to this version (then run sup-sync --restore)."
+ end
+ else
+ @xapian = Xapian::WritableDatabase.new(path, Xapian::DB_CREATE)
+ @xapian.set_metadata 'version', INDEX_VERSION
+ end
@term_generator = Xapian::TermGenerator.new()
@term_generator.stemmer = Xapian::Stem.new(STEM_LANGUAGE)
@enquire = Xapian::Enquire.new @xapian
end
def contains_id? id
- synchronize { @entries.member? id }
+ synchronize { find_docid(id) && true }
end
def source_for_id id
- synchronize { @entries[id][:source_id] }
+ synchronize { get_entry(id)[:source_id] }
end
def delete id
- synchronize { @xapian.delete_document @docids[id] }
+ synchronize { @xapian.delete_document mkterm(:msgid, id) }
end
def build_message id
- entry = synchronize { @entries[id] }
+ entry = synchronize { get_entry id }
return unless entry
source = SourceManager[entry[:source_id]]
raise "invalid source #{entry[:source_id]}" unless source
- mk_addrs = lambda { |l| l.map { |e,n| "#{n} <#{e}>" } * ', ' }
- mk_refs = lambda { |l| l.map { |r| "<#{r}>" } * ' ' }
- fake_header = {
- 'message-id' => entry[:message_id],
- 'date' => Time.at(entry[:date]),
- 'subject' => entry[:subject],
- 'from' => mk_addrs[[entry[:from]]],
- 'to' => mk_addrs[entry[:to]],
- 'cc' => mk_addrs[entry[:cc]],
- 'bcc' => mk_addrs[entry[:bcc]],
- 'reply-tos' => mk_refs[entry[:replytos]],
- 'references' => mk_refs[entry[:refs]],
- }
-
- m = Message.new :source => source, :source_info => entry[:source_info],
- :labels => entry[:labels],
- :snippet => entry[:snippet]
- m.parse_header fake_header
- m
+ m = Message.new :source => source, :source_info => entry[:source_info],
+ :labels => entry[:labels], :snippet => entry[:snippet]
+
+ mk_person = lambda { |x| Person.new(*x.reverse!) }
+ entry[:from] = mk_person[entry[:from]]
+ entry[:to].map!(&mk_person)
+ entry[:cc].map!(&mk_person)
+ entry[:bcc].map!(&mk_person)
+
+ m.load_from_index! entry
+ m
end
def add_message m; sync_message m end
def update_message_state m; sync_message m end
def sync_message m, opts={}
- entry = synchronize { @entries[m.id] }
+ entry = synchronize { get_entry m.id }
snippet = m.snippet
entry ||= {}
labels = m.labels
labels.each { |l| LabelManager << l }
synchronize do
- index_message m, opts
- union_threads([m.id] + m.refs + m.replytos)
- @entries[m.id] = d
+ index_message m, d, opts
end
true
end
def each_message_in_thread_for m, opts={}
# TODO thread by subject
# TODO handle killed threads
- ids = synchronize { @thread_members[@thread_ids[m.id]] } || []
- ids.select { |id| contains_id? id }.each { |id| yield id, lambda { build_message id } }
+ return unless doc = find_doc(m.id)
+ queue = doc.value(THREAD_VALUENO).split(',')
+ msgids = [m.id]
+ seen_threads = Set.new
+ seen_messages = Set.new [m.id]
+ while not queue.empty?
+ thread_id = queue.pop
+ next if seen_threads.member? thread_id
+ return false if thread_killed? thread_id
+ seen_threads << thread_id
+ docs = term_docids(mkterm(:thread, thread_id)).map { |x| @xapian.document x }
+ docs.each do |doc|
+ msgid = doc.value MSGID_VALUENO
+ next if seen_messages.member? msgid
+ msgids << msgid
+ seen_messages << msgid
+ queue.concat doc.value(THREAD_VALUENO).split(',')
+ end
+ end
+ msgids.each { |id| yield id, lambda { build_message id } }
true
end
'label' => 'L',
'source_id' => 'I',
'attachment_extension' => 'O',
+ 'msgid' => 'Q',
+ 'thread' => 'H',
+ 'ref' => 'R',
}
PREFIX = NORMAL_PREFIX.merge BOOLEAN_PREFIX
- DATE_VALUENO = 0
+ MSGID_VALUENO = 0
+ THREAD_VALUENO = 1
+ DATE_VALUENO = 2
MAX_TERM_LENGTH = 245
def assign_docid m, truncated_date
t = (truncated_date.to_i - MIDDLE_DATE.to_i).to_f
docid = (DOCID_SCALE - DOCID_SCALE/(Math::E**(-(t/TIME_SCALE)) + 1)).to_i
+ while docid > 0 and docid_exists? docid
+ docid -= 1
+ end
+ docid > 0 ? docid : nil
+ end
+
+ # XXX is there a better way?
+ def docid_exists? docid
begin
- while @assigned_docids.member? [docid].pack("N")
- docid -= 1
- end
- rescue
+ @xapian.doclength docid
+ true
+ rescue RuntimeError #Xapian::DocNotFoundError
+ raise unless $!.message =~ /DocNotFoundError/
+ false
end
- @assigned_docids[[docid].pack("N")] = ''
- docid
+ end
+
+ def term_docids term
+ @xapian.postlist(term).map { |x| x.docid }
+ end
+
+ def find_docid id
+ docids = term_docids(mkterm(:msgid,id))
+ fail unless docids.size <= 1
+ docids.first
+ end
+
+ def find_doc id
+ return unless docid = find_docid(id)
+ @xapian.document docid
+ end
+
+ def get_id docid
+ return unless doc = @xapian.document(docid)
+ doc.value MSGID_VALUENO
+ end
+
+ def get_entry id
+ return unless doc = find_doc(id)
+ Marshal.load doc.data
+ end
+
+ def thread_killed? thread_id
+ not run_query(Q.new(Q::OP_AND, mkterm(:thread, thread_id), mkterm(:label, :Killed)), 0, 1).empty?
end
def synchronize &b
def run_query_ids xapian_query, offset, limit
matchset = run_query xapian_query, offset, limit
- matchset.matches.map { |r| r.document.data }
+ matchset.matches.map { |r| r.document.value MSGID_VALUENO }
end
Q = Xapian::Query
end
end
- def index_message m, opts
+ def index_message m, entry, opts
terms = []
text = []
terms << mkterm(:date,m.date) if m.date
m.labels.each { |t| terms << mkterm(:label,t) }
terms << mkterm(:type, 'mail')
+ terms << mkterm(:msgid, m.id)
terms << mkterm(:source_id, m.source.id)
m.attachments.each do |a|
a =~ /\.(\w+)$/ or next
terms << t
end
+ ## Thread membership
+ children = term_docids(mkterm(:ref, m.id)).map { |docid| @xapian.document docid }
+ parent_ids = m.refs + m.replytos
+ parents = parent_ids.map { |id| find_doc id }.compact
+ thread_members = SavingHash.new { [] }
+ (children + parents).each do |doc2|
+ thread_ids = doc2.value(THREAD_VALUENO).split ','
+ thread_ids.each { |thread_id| thread_members[thread_id] << doc2 }
+ end
+
+ thread_ids = thread_members.empty? ? [m.id] : thread_members.keys
+
+ thread_ids.each { |thread_id| terms << mkterm(:thread, thread_id) }
+ parent_ids.each do |ref|
+ terms << mkterm(:ref, ref)
+ end
+
# Full text search content
text << [subject_text, PREFIX['subject']]
text << [subject_text, PREFIX['body']]
Xapian.sortable_serialise 0
end
- doc = Xapian::Document.new
- docid = @docids[m.id] || assign_docid(m, truncated_date)
+ docid = nil
+ unless doc = find_doc(m.id)
+ doc = Xapian::Document.new
+ if not docid = assign_docid(m, truncated_date)
+ # Could be triggered by spam
+ Redwood::log "warning: docid underflow, dropping #{m.id.inspect}"
+ return
+ end
+ else
+ doc.clear_terms
+ doc.clear_values
+ docid = doc.docid
+ end
@term_generator.document = doc
text.each { |text,prefix| @term_generator.index_text text, 1, prefix }
terms.each { |term| doc.add_term term if term.length <= MAX_TERM_LENGTH }
+ doc.add_value MSGID_VALUENO, m.id
+ doc.add_value THREAD_VALUENO, (thread_ids * ',')
doc.add_value DATE_VALUENO, date_value
- doc.data = m.id
+ doc.data = Marshal.dump entry
@xapian.replace_document docid, doc
- @docids[m.id] = docid
end
# Construct a Xapian term
PREFIX['source_id'] + args[0].to_s.downcase
when :attachment_extension
PREFIX['attachment_extension'] + args[0].to_s.downcase
+ when :msgid, :ref, :thread
+ PREFIX[type.to_s] + args[0][0...(MAX_TERM_LENGTH-1)]
else
raise "Invalid term type #{type}"
end
end
- # Join all the given message-ids into a single thread
- def union_threads ids
- seen_threads = Set.new
- related = Set.new
-
- # Get all the ids that will be in the new thread
- ids.each do |id|
- related << id
- thread_id = @thread_ids[id]
- if thread_id && !seen_threads.member?(thread_id)
- thread_members = @thread_members[thread_id]
- related.merge thread_members
- seen_threads << thread_id
- end
- end
-
- # Pick a leader and move all the others to its thread
- a = related.to_a
- best, *rest = a.sort_by { |x| x.hash }
- @thread_members[best] = a
- @thread_ids[best] = best
- rest.each do |x|
- @thread_members.delete x
- @thread_ids[x] = best
- end
- end
end
end
-
-class MarshalledGDBM < GDBM
- def []= k, v
- super k, Marshal.dump(v)
- end
-
- def [] k
- v = super k
- v ? Marshal.load(v) : nil
- end
-end