require 'chronic'
$have_chronic = true
rescue LoadError => e
- Redwood::log "'chronic' library not found. run 'gem install chronic' to install."
+ Redwood::log "optional 'chronic' library not found (run 'gem install chronic' to install)"
$have_chronic = false
end
end
def start_lock_update_thread
- @lock_update_thread = Redwood::reporting_thread do
+ @lock_update_thread = Redwood::reporting_thread("lock update") do
while true
sleep 30
@lock.touch_yourself
def add_source source
raise "duplicate source!" if @sources.include? source
@sources_dirty = true
- source.id ||= @sources.size
- ##TODO: why was this necessary?
+ max = @sources.max_of { |id, s| s.is_a?(DraftLoader) || s.is_a?(SentLoader) ? 0 : id }
+ source.id ||= (max || 0) + 1
##source.id += 1 while @sources.member? source.id
@sources[source.id] = source
end
- def source_for uri; @sources.values.find { |s| s.is_source_for? uri }; end
- def usual_sources; @sources.values.find_all { |s| s.usual? }; end
- def sources; @sources.values; end
+ def sources
+ ## favour the inbox by listing non-archived sources first
+ @sources.values.sort_by { |s| s.id }.partition { |s| !s.archived? }.flatten
+ end
+
+ def source_for uri; sources.find { |s| s.is_source_for? uri }; end
+ def usual_sources; sources.find_all { |s| s.usual? }; end
def load_index dir=File.join(@dir, "ferret")
if File.exists? dir
end
to = (m.to + m.cc + m.bcc).map { |x| x.email }.join(" ")
+ snippet =
+ if m.snippet_contains_encrypted_content? && $config[:discard_snippets_from_encrypted_messages]
+ ""
+ else
+ m.snippet
+ end
+
d = {
:message_id => m.id,
:source_id => source_id,
:source_info => m.source_info,
:date => m.date.to_indexable_s,
- :body => m.content,
- :snippet => m.snippet,
+ :body => m.indexable_content,
+ :snippet => snippet,
:label => m.labels.uniq.join(" "),
- :from => m.from ? m.from.email : "",
- :to => (m.to + m.cc + m.bcc).map { |x| x.email }.join(" "),
- :subject => wrap_subj(Message.normalize_subj(m.subj)),
+ :from => m.from ? m.from.indexable_content : "",
+ :to => (m.to + m.cc + m.bcc).map { |x| x.indexable_content }.join(" "),
+ :subject => wrap_subj(m.subj),
:refs => (m.refs + m.replytos).uniq.join(" "),
}
## true, stops loading any thread if a message with a :killed flag
## is found.
SAME_SUBJECT_DATE_LIMIT = 7
+ MAX_CLAUSES = 1000
def each_message_in_thread_for m, opts={}
#Redwood::log "Building thread for #{m.id}: #{m.subj}"
messages = {}
end
until pending.empty? || (opts[:limit] && messages.size >= opts[:limit])
- id = pending.pop
- next if searched.member? id
- searched[id] = true
q = Ferret::Search::BooleanQuery.new true
- q.add_query Ferret::Search::TermQuery.new(:message_id, id), :should
- q.add_query Ferret::Search::TermQuery.new(:refs, id), :should
+ # this disappeared in newer ferrets... wtf.
+ # q.max_clause_count = 2048
+
+ lim = [MAX_CLAUSES / 2, pending.length].min
+ pending[0 ... lim].each do |id|
+ searched[id] = true
+ q.add_query Ferret::Search::TermQuery.new(:message_id, id), :should
+ q.add_query Ferret::Search::TermQuery.new(:refs, id), :should
+ end
+ pending = pending[lim .. -1]
q = build_query :qobj => q
#Redwood::log "got #{mid} as a child of #{id}"
messages[mid] ||= lambda { build_message docid }
refs = @index[docid][:refs].split(" ")
- pending += refs
+ pending += refs.select { |id| !searched[id] }
end
end
end
+
if killed
Redwood::log "thread for #{m.id} is killed, ignoring"
false
"date" => Time.at(doc[:date].to_i),
"subject" => unwrap_subj(doc[:subject]),
"from" => doc[:from],
- "to" => doc[:to],
+ "to" => doc[:to].split(/\s+/).join(", "), # reformat
"message-id" => doc[:message_id],
"references" => doc[:refs].split(/\s+/).map { |x| "<#{x}>" }.join(" "),
}
## do any specialized parsing
## returns nil and flashes error message if parsing failed
- def parse_user_query_string str
- result = str.gsub(/\b(to|from):(\S+)\b/) do
+ def parse_user_query_string s
+ extraopts = {}
+
+ ## this is a little hacky, but it works, at least until ferret changes
+ ## its api. we parse the user query string with ferret twice: the first
+ ## time we just turn the resulting object back into a string, which has
+ ## the next effect of transforming the original string into a nice
+ ## normalized form with + and - instead of AND, OR, etc. then we do some
+ ## string substitutions which depend on this normalized form, re-parse
+ ## the string with Ferret, and return the resulting query object.
+
+ norms = @qparser.parse(s).to_s
+ Redwood::log "normalized #{s.inspect} to #{norms.inspect}" unless s == norms
+
+ subs = norms.gsub(/\b(to|from):(\S+)\b/) do
field, name = $1, $2
if(p = ContactManager.contact_for(name))
[field, p.email]
+ elsif name == "me"
+ [field, "(" + AccountManager.user_emails.join("||") + ")"]
else
[field, name]
end.join(":")
end
-
+
+ ## if we see a label:deleted or a label:spam term anywhere in the query
+ ## string, we set the extra load_spam or load_deleted options to true.
+ ## bizarre? well, because the query allows arbitrary parenthesized boolean
+ ## expressions, without fully parsing the query, we can't tell whether
+ ## the user is explicitly directing us to search spam messages or not.
+ ## e.g. if the string is -(-(-(-(-label:spam)))), does the user want to
+ ## search spam messages or not?
+ ##
+ ## so, we rely on the fact that turning these extra options ON turns OFF
+ ## the adding of "-label:deleted" or "-label:spam" terms at the very
+ ## final stage of query processing. if the user wants to search spam
+ ## messages, not adding that is the right thing; if he doesn't want to
+ ## search spam messages, then not adding it won't have any effect.
+ extraopts[:load_spam] = true if subs =~ /\blabel:spam\b/
+ extraopts[:load_deleted] = true if subs =~ /\blabel:deleted\b/
+
+ ## gmail style "is" operator
+ subs = subs.gsub(/\b(is):(\S+)\b/) do
+ field, label = $1, $2
+ case label
+ when "read"
+ "-label:unread"
+ when "spam"
+ extraopts[:load_spam] = true
+ "label:spam"
+ when "deleted"
+ extraopts[:load_deleted] = true
+ "label:deleted"
+ else
+ "label:#{$2}"
+ end
+ end
+
if $have_chronic
chronic_failure = false
- result = result.gsub(/\b(before|on|in|after):(\((.+?)\)\B|(\S+)\b)/) do
+ subs = subs.gsub(/\b(before|on|in|during|after):(\((.+?)\)\B|(\S+)\b)/) do
break if chronic_failure
field, datestr = $1, ($3 || $4)
- realdate = Chronic.parse(datestr, :guess => false)
+ realdate = Chronic.parse(datestr, :guess => false, :context => :none)
if realdate
case field
when "after"
"date:(<= #{sprintf "%012d", realdate.end.to_i}) date:(>= #{sprintf "%012d", realdate.begin.to_i})"
end
else
- BufferManager.flash "Don't understand date #{datestr.inspect}!"
+ BufferManager.flash "Can't understand date #{datestr.inspect}!"
chronic_failure = true
end
end
- result = nil if chronic_failure
+ subs = nil if chronic_failure
end
- Redwood::log "translated #{str.inspect} to #{result}" unless result == str
- @qparser.parse result if result
+ Redwood::log "translated #{norms.inspect} to #{subs.inspect}" unless subs == norms
+ if subs
+ [@qparser.parse(subs), extraopts]
+ else
+ nil
+ end
end
def build_query opts
File.chmod 0600, fn
FileUtils.mv fn, bakfn, :force => true unless File.exists?(bakfn) && File.size(fn) == 0
end
- Redwood::save_yaml_obj @sources.values.sort_by { |s| s.id.to_i }, fn, true
+ Redwood::save_yaml_obj sources.sort_by { |s| s.id.to_i }, fn, true
File.chmod 0600, fn
end
@sources_dirty = false