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.

Files changed (53) hide show
  1. data/History.txt +5 -0
  2. data/LICENSE +280 -0
  3. data/Manifest.txt +52 -0
  4. data/README.txt +119 -0
  5. data/Rakefile +45 -0
  6. data/bin/sup +229 -0
  7. data/bin/sup-import +162 -0
  8. data/doc/FAQ.txt +38 -0
  9. data/doc/Philosophy.txt +59 -0
  10. data/doc/TODO +31 -0
  11. data/lib/sup.rb +141 -0
  12. data/lib/sup/account.rb +53 -0
  13. data/lib/sup/buffer.rb +391 -0
  14. data/lib/sup/colormap.rb +118 -0
  15. data/lib/sup/contact.rb +40 -0
  16. data/lib/sup/draft.rb +105 -0
  17. data/lib/sup/index.rb +353 -0
  18. data/lib/sup/keymap.rb +89 -0
  19. data/lib/sup/label.rb +41 -0
  20. data/lib/sup/logger.rb +42 -0
  21. data/lib/sup/mbox.rb +51 -0
  22. data/lib/sup/mbox/loader.rb +116 -0
  23. data/lib/sup/message.rb +302 -0
  24. data/lib/sup/mode.rb +79 -0
  25. data/lib/sup/modes/buffer-list-mode.rb +37 -0
  26. data/lib/sup/modes/compose-mode.rb +33 -0
  27. data/lib/sup/modes/contact-list-mode.rb +121 -0
  28. data/lib/sup/modes/edit-message-mode.rb +162 -0
  29. data/lib/sup/modes/forward-mode.rb +38 -0
  30. data/lib/sup/modes/help-mode.rb +19 -0
  31. data/lib/sup/modes/inbox-mode.rb +45 -0
  32. data/lib/sup/modes/label-list-mode.rb +89 -0
  33. data/lib/sup/modes/label-search-results-mode.rb +29 -0
  34. data/lib/sup/modes/line-cursor-mode.rb +133 -0
  35. data/lib/sup/modes/log-mode.rb +44 -0
  36. data/lib/sup/modes/person-search-results-mode.rb +29 -0
  37. data/lib/sup/modes/poll-mode.rb +24 -0
  38. data/lib/sup/modes/reply-mode.rb +136 -0
  39. data/lib/sup/modes/resume-mode.rb +18 -0
  40. data/lib/sup/modes/scroll-mode.rb +106 -0
  41. data/lib/sup/modes/search-results-mode.rb +31 -0
  42. data/lib/sup/modes/text-mode.rb +51 -0
  43. data/lib/sup/modes/thread-index-mode.rb +389 -0
  44. data/lib/sup/modes/thread-view-mode.rb +338 -0
  45. data/lib/sup/person.rb +120 -0
  46. data/lib/sup/poll.rb +80 -0
  47. data/lib/sup/sent.rb +46 -0
  48. data/lib/sup/tagger.rb +40 -0
  49. data/lib/sup/textfield.rb +83 -0
  50. data/lib/sup/thread.rb +358 -0
  51. data/lib/sup/update.rb +21 -0
  52. data/lib/sup/util.rb +260 -0
  53. metadata +123 -0
@@ -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
@@ -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
@@ -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
@@ -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