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/colormap.rb
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
require "curses"
|
2
|
+
|
3
|
+
module Redwood
|
4
|
+
|
5
|
+
class Colormap
|
6
|
+
@@instance = nil
|
7
|
+
|
8
|
+
CURSES_COLORS = [Curses::COLOR_BLACK, Curses::COLOR_RED, Curses::COLOR_GREEN,
|
9
|
+
Curses::COLOR_YELLOW, Curses::COLOR_BLUE,
|
10
|
+
Curses::COLOR_MAGENTA, Curses::COLOR_CYAN,
|
11
|
+
Curses::COLOR_WHITE]
|
12
|
+
NUM_COLORS = 15
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
raise "only one instance can be created" if @@instance
|
16
|
+
@@instance = self
|
17
|
+
@entries = {}
|
18
|
+
@color_pairs = {[Curses::COLOR_WHITE, Curses::COLOR_BLACK] => 0}
|
19
|
+
@users = []
|
20
|
+
@next_id = 0
|
21
|
+
yield self if block_given?
|
22
|
+
@entries[highlight_sym(:none)] = highlight_for(Curses::COLOR_WHITE,
|
23
|
+
Curses::COLOR_BLACK,
|
24
|
+
[]) + [nil]
|
25
|
+
end
|
26
|
+
|
27
|
+
def add sym, fg, bg, *attrs
|
28
|
+
raise ArgumentError, "color for #{sym} already defined" if
|
29
|
+
@entries.member? sym
|
30
|
+
raise ArgumentError, "color '#{fg}' unknown" unless CURSES_COLORS.include? fg
|
31
|
+
raise ArgumentError, "color '#{bg}' unknown" unless CURSES_COLORS.include? bg
|
32
|
+
|
33
|
+
@entries[sym] = [fg, bg, attrs, nil]
|
34
|
+
@entries[highlight_sym(sym)] = highlight_for(fg, bg, attrs) + [nil]
|
35
|
+
end
|
36
|
+
|
37
|
+
def highlight_sym sym
|
38
|
+
"#{sym}_highlight".intern
|
39
|
+
end
|
40
|
+
|
41
|
+
def highlight_for fg, bg, attrs
|
42
|
+
hfg =
|
43
|
+
case fg
|
44
|
+
when Curses::COLOR_BLUE
|
45
|
+
Curses::COLOR_WHITE
|
46
|
+
when Curses::COLOR_YELLOW, Curses::COLOR_GREEN
|
47
|
+
fg
|
48
|
+
else
|
49
|
+
Curses::COLOR_BLACK
|
50
|
+
end
|
51
|
+
|
52
|
+
hbg =
|
53
|
+
case bg
|
54
|
+
when Curses::COLOR_CYAN
|
55
|
+
Curses::COLOR_YELLOW
|
56
|
+
else
|
57
|
+
Curses::COLOR_CYAN
|
58
|
+
end
|
59
|
+
|
60
|
+
attrs =
|
61
|
+
if fg == Curses::COLOR_WHITE && attrs.include?(Curses::A_BOLD)
|
62
|
+
[Curses::A_BOLD]
|
63
|
+
else
|
64
|
+
case hfg
|
65
|
+
when Curses::COLOR_BLACK
|
66
|
+
[]
|
67
|
+
else
|
68
|
+
[Curses::A_BOLD]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
[hfg, hbg, attrs]
|
72
|
+
end
|
73
|
+
|
74
|
+
def color_for sym, highlight=false
|
75
|
+
sym = highlight_sym(sym) if highlight
|
76
|
+
return Curses::COLOR_BLACK if sym == :none
|
77
|
+
raise ArgumentError, "undefined color #{sym}" unless @entries.member? sym
|
78
|
+
|
79
|
+
## if this color is cached, return it
|
80
|
+
fg, bg, attrs, color = @entries[sym]
|
81
|
+
return color if color
|
82
|
+
|
83
|
+
if(cp = @color_pairs[[fg, bg]])
|
84
|
+
## nothing
|
85
|
+
else ## need to get a new colorpair
|
86
|
+
@next_id = (@next_id + 1) % NUM_COLORS
|
87
|
+
@next_id += 1 if @next_id == 0 # 0 is always white on black
|
88
|
+
id = @next_id
|
89
|
+
Redwood::log "colormap: for color #{sym}, using id #{id} -> #{fg}, #{bg}"
|
90
|
+
Curses.init_pair id, fg, bg or raise ArgumentError,
|
91
|
+
"couldn't initialize curses color pair #{fg}, #{bg} (key #{id})"
|
92
|
+
|
93
|
+
cp = @color_pairs[[fg, bg]] = Curses.color_pair(id)
|
94
|
+
## delete the old mapping, if it exists
|
95
|
+
if @users[cp]
|
96
|
+
@users[cp].each do |usym|
|
97
|
+
Redwood::log "dropping color #{usym} (#{id})"
|
98
|
+
@entries[usym][3] = nil
|
99
|
+
end
|
100
|
+
@users[cp] = []
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
## by now we have a color pair
|
105
|
+
color = attrs.inject(cp) { |color, attr| color | attr }
|
106
|
+
@entries[sym][3] = color # fill the cache
|
107
|
+
(@users[cp] ||= []) << sym # record entry as a user of that color pair
|
108
|
+
color
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.instance; @@instance; end
|
112
|
+
def self.method_missing meth, *a
|
113
|
+
Colorcolors.new unless @@instance
|
114
|
+
@@instance.send meth, *a
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
data/lib/sup/contact.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
module Redwood
|
2
|
+
|
3
|
+
class ContactManager
|
4
|
+
include Singleton
|
5
|
+
|
6
|
+
def initialize fn
|
7
|
+
@fn = fn
|
8
|
+
@people = {}
|
9
|
+
|
10
|
+
if File.exists? fn
|
11
|
+
IO.foreach(fn) do |l|
|
12
|
+
l =~ /^(\S+): (.*)$/ or raise "can't parse #{fn} line #{l.inspect}"
|
13
|
+
aalias, addr = $1, $2
|
14
|
+
@people[aalias] = Person.for addr
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
self.class.i_am_the_instance self
|
19
|
+
end
|
20
|
+
|
21
|
+
def contacts; @people; end
|
22
|
+
def set_contact person, aalias
|
23
|
+
oldentry = @people.find { |a, p| p == person }
|
24
|
+
@people.delete oldentry.first if oldentry
|
25
|
+
@people[aalias] = person
|
26
|
+
end
|
27
|
+
def drop_contact person; @people.delete person; end
|
28
|
+
def delete t; @people.delete t; end
|
29
|
+
def resolve aalias; @people[aalias]; end
|
30
|
+
|
31
|
+
def save
|
32
|
+
File.open(@fn, "w") do |f|
|
33
|
+
@people.keys.sort.each do |aalias|
|
34
|
+
f.puts "#{aalias}: #{@people[aalias].full_address}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
data/lib/sup/draft.rb
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
module Redwood
|
2
|
+
|
3
|
+
class DraftManager
|
4
|
+
include Singleton
|
5
|
+
|
6
|
+
attr_accessor :source
|
7
|
+
def initialize dir
|
8
|
+
@dir = dir
|
9
|
+
@source = nil
|
10
|
+
self.class.i_am_the_instance self
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.source_name; "drafts"; end
|
14
|
+
def self.source_id; 9999; end
|
15
|
+
def new_source; @source = DraftLoader.new @dir; end
|
16
|
+
|
17
|
+
def write_draft
|
18
|
+
offset = @source.gen_offset
|
19
|
+
fn = @source.fn_for_offset offset
|
20
|
+
File.open(fn, "w") { |f| yield f }
|
21
|
+
|
22
|
+
@source.each do |offset, labels|
|
23
|
+
m = Message.new @source, offset, labels
|
24
|
+
Index.add_message m
|
25
|
+
UpdateManager.relay :add, m
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def discard mid
|
30
|
+
docid, entry = Index.load_entry_for_id mid
|
31
|
+
raise ArgumentError, "can't find entry for draft: #{mid.inspect}" unless entry
|
32
|
+
raise ArgumentError, "not a draft: source id #{entry[:source_id].inspect}, should be #{DraftManager.source_id.inspect} for #{mid.inspect} / docno #{docid}" unless entry[:source_id].to_i == DraftManager.source_id
|
33
|
+
Index.drop_entry docid
|
34
|
+
File.delete @source.fn_for_offset(entry[:source_info])
|
35
|
+
UpdateManager.relay :delete, mid
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class DraftLoader
|
40
|
+
attr_accessor :dir, :end_offset
|
41
|
+
bool_reader :dirty
|
42
|
+
|
43
|
+
def initialize dir, end_offset=0
|
44
|
+
Dir.mkdir dir unless File.exists? dir
|
45
|
+
@dir = dir
|
46
|
+
@end_offset = end_offset
|
47
|
+
@dirty = false
|
48
|
+
end
|
49
|
+
|
50
|
+
def done?; !File.exists? fn_for_offset(@end_offset); end
|
51
|
+
def usual?; true; end
|
52
|
+
def id; DraftManager.source_id; end
|
53
|
+
def to_s; DraftManager.source_name; end
|
54
|
+
def is_source_for? x; x == DraftManager.source_name; end
|
55
|
+
|
56
|
+
def gen_offset
|
57
|
+
i = @end_offset
|
58
|
+
while File.exists? fn_for_offset(i)
|
59
|
+
i += 1
|
60
|
+
end
|
61
|
+
i
|
62
|
+
end
|
63
|
+
|
64
|
+
def fn_for_offset o; File.join(@dir, o.to_s); end
|
65
|
+
|
66
|
+
def load_header offset
|
67
|
+
File.open fn_for_offset(offset) do |f|
|
68
|
+
return MBox::read_header(f)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def load_message offset
|
73
|
+
File.open fn_for_offset(offset) do |f|
|
74
|
+
RMail::Mailbox::MBoxReader.new(f).each_message do |input|
|
75
|
+
return RMail::Parser.read(input)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
## load the full header text
|
81
|
+
def load_header_text offset
|
82
|
+
ret = ""
|
83
|
+
File.open fn_for_offset(offset) do |f|
|
84
|
+
until f.eof? || (l = f.gets) =~ /^$/
|
85
|
+
ret += l
|
86
|
+
end
|
87
|
+
end
|
88
|
+
ret
|
89
|
+
end
|
90
|
+
|
91
|
+
def each
|
92
|
+
while File.exists?(fn = File.join(@dir, @end_offset.to_s))
|
93
|
+
yield @end_offset, [:draft, :inbox]
|
94
|
+
@end_offset += 1
|
95
|
+
@dirty = true
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def total; Dir[File.join(@dir, "*")].sort.last.to_i; end
|
100
|
+
def reset!; @end_offset = 0; @dirty = true; end
|
101
|
+
end
|
102
|
+
|
103
|
+
Redwood::register_yaml(DraftLoader, %w(dir end_offset))
|
104
|
+
|
105
|
+
end
|
data/lib/sup/index.rb
ADDED
@@ -0,0 +1,353 @@
|
|
1
|
+
## the index structure for redwood. interacts with ferret.
|
2
|
+
|
3
|
+
require 'thread'
|
4
|
+
require 'fileutils'
|
5
|
+
require_gem 'ferret', ">= 0.10.13"
|
6
|
+
|
7
|
+
module Redwood
|
8
|
+
|
9
|
+
class IndexError < StandardError
|
10
|
+
attr_reader :source
|
11
|
+
|
12
|
+
def initialize source, s
|
13
|
+
super s
|
14
|
+
@source = source
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class Index
|
19
|
+
include Singleton
|
20
|
+
|
21
|
+
LOAD_THREAD_PETIT_DELAY = 0.1
|
22
|
+
LOAD_THREAD_GRAND_DELAY = 5
|
23
|
+
|
24
|
+
MESSAGES_AT_A_TIME = 10
|
25
|
+
|
26
|
+
attr_reader :index # debugging only
|
27
|
+
|
28
|
+
def initialize dir=BASE_DIR
|
29
|
+
@dir = dir
|
30
|
+
@mutex = Mutex.new
|
31
|
+
@load_thread = nil # loads new messages
|
32
|
+
@sources = {}
|
33
|
+
@sources_dirty = false
|
34
|
+
|
35
|
+
self.class.i_am_the_instance self
|
36
|
+
end
|
37
|
+
|
38
|
+
def load
|
39
|
+
load_sources
|
40
|
+
load_index
|
41
|
+
end
|
42
|
+
|
43
|
+
def save
|
44
|
+
FileUtils.mkdir_p @dir unless File.exists? @dir
|
45
|
+
save_sources
|
46
|
+
save_index
|
47
|
+
end
|
48
|
+
|
49
|
+
def add_source source
|
50
|
+
raise "duplicate source!" if @sources.include? source
|
51
|
+
@sources_dirty = true
|
52
|
+
source.id ||= @sources.size
|
53
|
+
source.id += 1 while @sources.member? source.id
|
54
|
+
@sources[source.id] = source
|
55
|
+
end
|
56
|
+
|
57
|
+
def source_for name; @sources.values.find { |s| s.is_source_for? name }; end
|
58
|
+
def usual_sources; @sources.values.find_all { |s| s.usual? }; end
|
59
|
+
|
60
|
+
def load_index dir=File.join(@dir, "ferret")
|
61
|
+
wsa = Ferret::Analysis::WhiteSpaceAnalyzer.new false
|
62
|
+
sa = Ferret::Analysis::StandardAnalyzer.new
|
63
|
+
analyzer = Ferret::Analysis::PerFieldAnalyzer.new wsa
|
64
|
+
analyzer[:body] = sa
|
65
|
+
|
66
|
+
if File.exists? dir
|
67
|
+
Redwood::log "loading index"
|
68
|
+
@index = Ferret::Index::Index.new(:path => dir, :analyzer => analyzer)
|
69
|
+
else
|
70
|
+
Redwood::log "creating index"
|
71
|
+
field_infos = Ferret::Index::FieldInfos.new :store => :yes
|
72
|
+
field_infos.add_field :message_id
|
73
|
+
field_infos.add_field :source_id
|
74
|
+
field_infos.add_field :source_info, :index => :no, :term_vector => :no
|
75
|
+
field_infos.add_field :date, :index => :untokenized
|
76
|
+
field_infos.add_field :body, :store => :no
|
77
|
+
field_infos.add_field :label
|
78
|
+
field_infos.add_field :subject
|
79
|
+
field_infos.add_field :from
|
80
|
+
field_infos.add_field :to
|
81
|
+
field_infos.add_field :refs
|
82
|
+
field_infos.add_field :snippet, :index => :no, :term_vector => :no
|
83
|
+
field_infos.create_index dir
|
84
|
+
@index = Ferret::Index::Index.new(:path => dir, :analyzer => analyzer)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
## update the message by deleting and re-adding
|
89
|
+
def update_message m, source=nil, source_info=nil
|
90
|
+
docid, entry = load_entry_for_id m.id
|
91
|
+
if entry
|
92
|
+
source ||= entry[:source_id].to_i
|
93
|
+
source_info ||= entry[:source_info].to_i
|
94
|
+
end
|
95
|
+
raise "no entry and no source info for message #{m.id}" unless source && source_info
|
96
|
+
|
97
|
+
raise "deleting non-corresponding entry #{docid}" unless @index[docid][:message_id] == m.id
|
98
|
+
@index.delete docid
|
99
|
+
add_message m
|
100
|
+
end
|
101
|
+
|
102
|
+
def save_index fn=File.join(@dir, "ferret")
|
103
|
+
# don't have to do anything apparently
|
104
|
+
end
|
105
|
+
|
106
|
+
def contains_id? id
|
107
|
+
@index.search(Ferret::Search::TermQuery.new(:message_id, id)).total_hits > 0
|
108
|
+
end
|
109
|
+
def contains? m; contains_id? m.id; end
|
110
|
+
def size; @index.size; end
|
111
|
+
|
112
|
+
## you should probably not call this on a block that doesn't break
|
113
|
+
## rather quickly because the results will probably be, as we say
|
114
|
+
## in scotland, frikkin' huuuge.
|
115
|
+
EACH_BY_DATE_NUM = 100
|
116
|
+
def each_id_by_date opts={}
|
117
|
+
return if @index.size == 0 # otherwise ferret barfs
|
118
|
+
query = build_query opts
|
119
|
+
offset = 0
|
120
|
+
while true
|
121
|
+
results = @index.search(query, :sort => "date DESC", :limit => EACH_BY_DATE_NUM, :offset => offset)
|
122
|
+
Redwood::log "got #{results.total_hits} results for query (offset #{offset}) #{query.inspect}"
|
123
|
+
results.hits.each { |hit| yield @index[hit.doc][:message_id], lambda { build_message hit.doc } }
|
124
|
+
break if offset >= results.total_hits - EACH_BY_DATE_NUM
|
125
|
+
offset += EACH_BY_DATE_NUM
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def num_results_for opts={}
|
130
|
+
query = build_query opts
|
131
|
+
x = @index.search(query).total_hits
|
132
|
+
Redwood::log "num_results_for: have #{x} for query #{query}"
|
133
|
+
x
|
134
|
+
end
|
135
|
+
|
136
|
+
SAME_SUBJECT_DATE_LIMIT = 7
|
137
|
+
def each_message_in_thread_for m, opts={}
|
138
|
+
messages = {}
|
139
|
+
searched = {}
|
140
|
+
num_queries = 0
|
141
|
+
|
142
|
+
## temporarily disabling subject searching because it's a
|
143
|
+
## significant slowdown.
|
144
|
+
##
|
145
|
+
## TODO: make this configurable, i guess
|
146
|
+
if false
|
147
|
+
date_min = m.date - (SAME_SUBJECT_DATE_LIMIT * 12 * 3600)
|
148
|
+
date_max = m.date + (SAME_SUBJECT_DATE_LIMIT * 12 * 3600)
|
149
|
+
|
150
|
+
q = Ferret::Search::BooleanQuery.new true
|
151
|
+
sq = Ferret::Search::PhraseQuery.new(:subject)
|
152
|
+
wrap_subj(Message.normalize_subj(m.subj)).split(/\s+/).each do |t|
|
153
|
+
sq.add_term t
|
154
|
+
end
|
155
|
+
q.add_query sq, :must
|
156
|
+
q.add_query Ferret::Search::RangeQuery.new(:date, :>= => date_min.to_indexable_s, :<= => date_max.to_indexable_s), :must
|
157
|
+
|
158
|
+
pending = @index.search(q).hits.map { |hit| @index[hit.doc][:message_id] }
|
159
|
+
Redwood::log "found #{pending.size} results for subject query #{q}"
|
160
|
+
else
|
161
|
+
pending = [m.id]
|
162
|
+
end
|
163
|
+
|
164
|
+
until pending.empty? || (opts[:limit] && messages.size >= opts[:limit])
|
165
|
+
id = pending.pop
|
166
|
+
next if searched.member? id
|
167
|
+
searched[id] = true
|
168
|
+
q = Ferret::Search::BooleanQuery.new true
|
169
|
+
q.add_query Ferret::Search::TermQuery.new(:message_id, id), :should
|
170
|
+
q.add_query Ferret::Search::TermQuery.new(:refs, id), :should
|
171
|
+
|
172
|
+
num_queries += 1
|
173
|
+
@index.search_each(q, :limit => :all) do |docid, score|
|
174
|
+
break if opts[:limit] && messages.size >= opts[:limit]
|
175
|
+
mid = @index[docid][:message_id]
|
176
|
+
unless messages.member? mid
|
177
|
+
messages[mid] ||= lambda { build_message docid }
|
178
|
+
refs = @index[docid][:refs].split(" ")
|
179
|
+
pending += refs
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
Redwood::log "ran #{num_queries} queries to build thread of #{messages.size} messages for #{m.id}"
|
184
|
+
messages.each { |mid, builder| yield mid, builder }
|
185
|
+
end
|
186
|
+
|
187
|
+
## builds a message object from a ferret result
|
188
|
+
def build_message docid
|
189
|
+
doc = @index[docid]
|
190
|
+
source = @sources[doc[:source_id].to_i]
|
191
|
+
#puts "building message #{doc[:message_id]} (#{source}##{doc[:source_info]})"
|
192
|
+
raise "invalid source #{doc[:source_id]}" unless source
|
193
|
+
begin
|
194
|
+
raise "no snippet" unless doc[:snippet]
|
195
|
+
Message.new source, doc[:source_info].to_i,
|
196
|
+
doc[:label].split(" ").map { |s| s.intern },
|
197
|
+
doc[:snippet]
|
198
|
+
rescue MessageFormatError => e
|
199
|
+
raise IndexError.new(source, "error building message #{doc[:message_id]} at #{source}/#{doc[:source_info]}: #{e.message}")
|
200
|
+
nil
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def start_load_thread
|
205
|
+
return if @load_thread
|
206
|
+
@load_thread = true
|
207
|
+
@load_thread = ::Thread.new do
|
208
|
+
while @load_thread
|
209
|
+
load_some_entries ENTRIES_AT_A_TIME, LOAD_THREAD_PETIT_DELAY, LOAD_THREAD_GRAND_DELAY
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def end_load_thread; @load_thread = nil; end
|
215
|
+
def fresh_thread_id; @next_thread_id += 1; end
|
216
|
+
|
217
|
+
def wrap_subj subj; "__START_SUBJECT__ #{subj} __END_SUBJECT__"; end
|
218
|
+
|
219
|
+
def add_message m
|
220
|
+
return false if contains? m
|
221
|
+
|
222
|
+
source_id =
|
223
|
+
if m.source.is_a? Integer
|
224
|
+
m.source
|
225
|
+
else
|
226
|
+
m.source.id or raise "unregistered source #{m.source}"
|
227
|
+
end
|
228
|
+
|
229
|
+
to = (m.to + m.cc + m.bcc).map { |x| x.email }.join(" ")
|
230
|
+
d = {
|
231
|
+
:message_id => m.id,
|
232
|
+
:source_id => source_id,
|
233
|
+
:source_info => m.source_info,
|
234
|
+
:date => m.date.to_indexable_s,
|
235
|
+
:body => m.content,
|
236
|
+
:snippet => m.snippet,
|
237
|
+
:label => m.labels.join(" "),
|
238
|
+
:from => m.from ? m.from.email : "",
|
239
|
+
:to => (m.to + m.cc + m.bcc).map { |x| x.email }.join(" "),
|
240
|
+
:subject => wrap_subj(Message.normalize_subj(m.subj)),
|
241
|
+
:refs => (m.refs + m.replytos).join(" "),
|
242
|
+
}
|
243
|
+
|
244
|
+
@index.add_document d
|
245
|
+
|
246
|
+
## TODO: figure out why this is sometimes triggered
|
247
|
+
#docid, entry = load_entry_for_id m.id
|
248
|
+
#raise "just added message #{m.id} but couldn't find it in a search" unless docid
|
249
|
+
true
|
250
|
+
end
|
251
|
+
|
252
|
+
def drop_entry docno; @index.delete docno; end
|
253
|
+
|
254
|
+
def load_entry_for_id mid
|
255
|
+
results = @index.search(Ferret::Search::TermQuery.new(:message_id, mid))
|
256
|
+
return if results.total_hits == 0
|
257
|
+
docid = results.hits[0].doc
|
258
|
+
[docid, @index[docid]]
|
259
|
+
end
|
260
|
+
|
261
|
+
def load_contacts emails, h={}
|
262
|
+
q = Ferret::Search::BooleanQuery.new true
|
263
|
+
emails.each do |e|
|
264
|
+
qq = Ferret::Search::BooleanQuery.new true
|
265
|
+
qq.add_query Ferret::Search::TermQuery.new(:from, e), :should
|
266
|
+
qq.add_query Ferret::Search::TermQuery.new(:to, e), :should
|
267
|
+
q.add_query qq
|
268
|
+
end
|
269
|
+
q.add_query Ferret::Search::TermQuery.new(:label, "spam"), :must_not
|
270
|
+
|
271
|
+
Redwood::log "contact search: #{q}"
|
272
|
+
contacts = {}
|
273
|
+
num = h[:num] || 20
|
274
|
+
@index.search_each(q, :sort => "date DESC", :limit => :all) do |docid, score|
|
275
|
+
break if contacts.size >= num
|
276
|
+
#Redwood::log "got message with to: #{@index[docid][:to].inspect} and from: #{@index[docid][:from].inspect}"
|
277
|
+
f = @index[docid][:from]
|
278
|
+
t = @index[docid][:to]
|
279
|
+
|
280
|
+
if AccountManager.is_account_email? f
|
281
|
+
t.split(" ").each { |e| #Redwood::log "adding #{e} because there's a message to him from account email #{f}";
|
282
|
+
contacts[Person.for(e)] = true }
|
283
|
+
else
|
284
|
+
#Redwood::log "adding from #{f} because there's a message from him to #{t}"
|
285
|
+
contacts[Person.for(f)] = true
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
contacts.keys.compact
|
290
|
+
end
|
291
|
+
|
292
|
+
protected
|
293
|
+
|
294
|
+
## TODO: convert this to query objects rather than strings
|
295
|
+
def build_query opts
|
296
|
+
query = ""
|
297
|
+
query += opts[:labels].map { |t| "+label:#{t}" }.join(" ") if opts[:labels]
|
298
|
+
query += " +label:#{opts[:label]}" if opts[:label]
|
299
|
+
query += " #{opts[:content]}" if opts[:content]
|
300
|
+
if opts[:participants]
|
301
|
+
query += "+(" +
|
302
|
+
opts[:participants].map { |p| "from:#{p.email} OR to:#{p.email}" }.join(" OR ") + ")"
|
303
|
+
end
|
304
|
+
|
305
|
+
query += " -label:spam" unless opts[:load_spam] || opts[:labels] == :spam ||
|
306
|
+
(opts[:labels] && opts[:labels].include?(:spam))
|
307
|
+
query += " -label:killed" unless opts[:load_killed] || opts[:labels] == :killed ||
|
308
|
+
(opts[:labels] && opts[:labels].include?(:killed))
|
309
|
+
query
|
310
|
+
end
|
311
|
+
|
312
|
+
def load_sources fn=Redwood::SOURCE_FN
|
313
|
+
@sources = Hash[*(Redwood::load_yaml_obj(fn) || []).map { |s| [s.id, s] }.flatten]
|
314
|
+
@sources_dirty = false
|
315
|
+
end
|
316
|
+
|
317
|
+
def save_sources fn=Redwood::SOURCE_FN
|
318
|
+
if @sources_dirty || @sources.any? { |id, s| s.dirty? }
|
319
|
+
FileUtils.mv fn, fn + ".bak", :force => true if File.exists? fn
|
320
|
+
Redwood::save_yaml_obj @sources.values, fn
|
321
|
+
end
|
322
|
+
@sources_dirty = false
|
323
|
+
end
|
324
|
+
|
325
|
+
def load_some_entries max=ENTRIES_AT_A_TIME, delay1=nil, delay2=nil
|
326
|
+
num = 0
|
327
|
+
begin
|
328
|
+
@sources.each_with_index do |source, source_id|
|
329
|
+
next if source.done? || num >= max
|
330
|
+
source.each do |source_info, label|
|
331
|
+
begin
|
332
|
+
m = Message.new(source, source_info, label + [:inbox])
|
333
|
+
add_message m unless contains_id? m.id
|
334
|
+
puts m.content.inspect
|
335
|
+
num += 1
|
336
|
+
rescue MessageFormatError => e
|
337
|
+
$stderr.puts "ignoring erroneous message at #{source}##{source_info}: #{e.message}"
|
338
|
+
end
|
339
|
+
break if num >= max
|
340
|
+
sleep delay1 if delay1
|
341
|
+
end
|
342
|
+
Redwood::log "loaded #{num} entries from #{source}"
|
343
|
+
sleep delay2 if delay2
|
344
|
+
end
|
345
|
+
ensure
|
346
|
+
save_sources
|
347
|
+
save_index
|
348
|
+
end
|
349
|
+
num
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
end
|