sup 0.13.2.1 → 0.14.0
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/CONTRIBUTORS +11 -7
- data/History.txt +9 -3
- data/ReleaseNotes +13 -16
- data/bin/sup +10 -7
- data/bin/sup-psych-ify-config-files +16 -0
- data/lib/sup.rb +32 -18
- data/lib/sup/account.rb +2 -1
- data/lib/sup/buffer.rb +13 -13
- data/lib/sup/contact.rb +3 -1
- data/lib/sup/crypto.rb +44 -18
- data/lib/sup/draft.rb +5 -2
- data/lib/sup/index.rb +54 -34
- data/lib/sup/label.rb +3 -1
- data/lib/sup/message.rb +10 -4
- data/lib/sup/message_chunks.rb +13 -47
- data/lib/sup/modes/compose_mode.rb +2 -0
- data/lib/sup/modes/edit_message_mode.rb +4 -4
- data/lib/sup/modes/inbox_mode.rb +1 -1
- data/lib/sup/modes/thread_index_mode.rb +4 -4
- data/lib/sup/modes/thread_view_mode.rb +4 -0
- data/lib/sup/poll.rb +1 -1
- data/lib/sup/rfc2047.rb +1 -3
- data/lib/sup/sent.rb +7 -2
- data/lib/sup/source.rb +12 -20
- data/lib/sup/textfield.rb +10 -0
- data/lib/sup/util.rb +85 -43
- data/lib/sup/util/query.rb +14 -0
- data/lib/sup/version.rb +1 -1
- metadata +57 -141
data/lib/sup/draft.rb
CHANGED
@@ -32,14 +32,17 @@ class DraftLoader < Source
|
|
32
32
|
attr_accessor :dir
|
33
33
|
yaml_properties
|
34
34
|
|
35
|
-
def initialize
|
36
|
-
dir = Redwood::DRAFT_DIR
|
35
|
+
def initialize dir=Redwood::DRAFT_DIR
|
37
36
|
Dir.mkdir dir unless File.exists? dir
|
38
37
|
super DraftManager.source_name, true, false
|
39
38
|
@dir = dir
|
40
39
|
@cur_offset = 0
|
41
40
|
end
|
42
41
|
|
42
|
+
def properly_initialized?
|
43
|
+
!!(@dir && @cur_offset)
|
44
|
+
end
|
45
|
+
|
43
46
|
def id; DraftManager.source_id; end
|
44
47
|
def to_s; DraftManager.source_name; end
|
45
48
|
def uri; DraftManager.source_name; end
|
data/lib/sup/index.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
ENV["XAPIAN_FLUSH_THRESHOLD"] = "1000"
|
2
|
+
ENV["XAPIAN_CJK_NGRAM"] = "1"
|
2
3
|
|
3
4
|
require 'xapian'
|
4
5
|
require 'set'
|
@@ -6,13 +7,21 @@ require 'fileutils'
|
|
6
7
|
require 'monitor'
|
7
8
|
require 'chronic'
|
8
9
|
|
10
|
+
require "sup/util/query"
|
9
11
|
require "sup/interactive_lock"
|
10
12
|
require "sup/hook"
|
11
13
|
require "sup/logger/singleton"
|
12
14
|
|
13
15
|
|
14
|
-
if ([Xapian.major_version, Xapian.minor_version, Xapian.revision] <=> [1,2,
|
15
|
-
|
16
|
+
if ([Xapian.major_version, Xapian.minor_version, Xapian.revision] <=> [1,2,15]) < 0
|
17
|
+
fail <<-EOF
|
18
|
+
\n
|
19
|
+
Xapian version 1.2.15 or higher required.
|
20
|
+
If you have xapian-full-alaveteli installed,
|
21
|
+
Please remove it by running `gem uninstall xapian-full-alaveteli`
|
22
|
+
since it's been replaced by the xapian-ruby gem.
|
23
|
+
|
24
|
+
EOF
|
16
25
|
end
|
17
26
|
|
18
27
|
module Redwood
|
@@ -312,6 +321,48 @@ EOS
|
|
312
321
|
|
313
322
|
class ParseError < StandardError; end
|
314
323
|
|
324
|
+
# Stemmed
|
325
|
+
NORMAL_PREFIX = {
|
326
|
+
'subject' => {:prefix => 'S', :exclusive => false},
|
327
|
+
'body' => {:prefix => 'B', :exclusive => false},
|
328
|
+
'from_name' => {:prefix => 'FN', :exclusive => false},
|
329
|
+
'to_name' => {:prefix => 'TN', :exclusive => false},
|
330
|
+
'name' => {:prefix => %w(FN TN), :exclusive => false},
|
331
|
+
'attachment' => {:prefix => 'A', :exclusive => false},
|
332
|
+
'email_text' => {:prefix => 'E', :exclusive => false},
|
333
|
+
'' => {:prefix => %w(S B FN TN A E), :exclusive => false},
|
334
|
+
}
|
335
|
+
|
336
|
+
# Unstemmed
|
337
|
+
BOOLEAN_PREFIX = {
|
338
|
+
'type' => {:prefix => 'K', :exclusive => true},
|
339
|
+
'from_email' => {:prefix => 'FE', :exclusive => false},
|
340
|
+
'to_email' => {:prefix => 'TE', :exclusive => false},
|
341
|
+
'email' => {:prefix => %w(FE TE), :exclusive => false},
|
342
|
+
'date' => {:prefix => 'D', :exclusive => true},
|
343
|
+
'label' => {:prefix => 'L', :exclusive => false},
|
344
|
+
'source_id' => {:prefix => 'I', :exclusive => true},
|
345
|
+
'attachment_extension' => {:prefix => 'O', :exclusive => false},
|
346
|
+
'msgid' => {:prefix => 'Q', :exclusive => true},
|
347
|
+
'id' => {:prefix => 'Q', :exclusive => true},
|
348
|
+
'thread' => {:prefix => 'H', :exclusive => false},
|
349
|
+
'ref' => {:prefix => 'R', :exclusive => false},
|
350
|
+
'location' => {:prefix => 'J', :exclusive => false},
|
351
|
+
}
|
352
|
+
|
353
|
+
PREFIX = NORMAL_PREFIX.merge BOOLEAN_PREFIX
|
354
|
+
|
355
|
+
COMPL_OPERATORS = %w[AND OR NOT]
|
356
|
+
COMPL_PREFIXES = (
|
357
|
+
%w[
|
358
|
+
from to
|
359
|
+
is has label
|
360
|
+
filename filetypem
|
361
|
+
before on in during after
|
362
|
+
limit
|
363
|
+
] + NORMAL_PREFIX.keys + BOOLEAN_PREFIX.keys
|
364
|
+
).map{|p|"#{p}:"} + COMPL_OPERATORS
|
365
|
+
|
315
366
|
## parse a query string from the user. returns a query object
|
316
367
|
## that can be passed to any index method with a 'query'
|
317
368
|
## argument.
|
@@ -441,7 +492,7 @@ EOS
|
|
441
492
|
raise ParseError, "xapian query parser error: #{e}"
|
442
493
|
end
|
443
494
|
|
444
|
-
debug "parsed xapian query: #{xapian_query
|
495
|
+
debug "parsed xapian query: #{Util::Query.describe(xapian_query)}"
|
445
496
|
|
446
497
|
raise ParseError if xapian_query.nil? or xapian_query.empty?
|
447
498
|
query[:qobj] = xapian_query
|
@@ -482,37 +533,6 @@ EOS
|
|
482
533
|
|
483
534
|
private
|
484
535
|
|
485
|
-
# Stemmed
|
486
|
-
NORMAL_PREFIX = {
|
487
|
-
'subject' => {:prefix => 'S', :exclusive => false},
|
488
|
-
'body' => {:prefix => 'B', :exclusive => false},
|
489
|
-
'from_name' => {:prefix => 'FN', :exclusive => false},
|
490
|
-
'to_name' => {:prefix => 'TN', :exclusive => false},
|
491
|
-
'name' => {:prefix => %w(FN TN), :exclusive => false},
|
492
|
-
'attachment' => {:prefix => 'A', :exclusive => false},
|
493
|
-
'email_text' => {:prefix => 'E', :exclusive => false},
|
494
|
-
'' => {:prefix => %w(S B FN TN A E), :exclusive => false},
|
495
|
-
}
|
496
|
-
|
497
|
-
# Unstemmed
|
498
|
-
BOOLEAN_PREFIX = {
|
499
|
-
'type' => {:prefix => 'K', :exclusive => true},
|
500
|
-
'from_email' => {:prefix => 'FE', :exclusive => false},
|
501
|
-
'to_email' => {:prefix => 'TE', :exclusive => false},
|
502
|
-
'email' => {:prefix => %w(FE TE), :exclusive => false},
|
503
|
-
'date' => {:prefix => 'D', :exclusive => true},
|
504
|
-
'label' => {:prefix => 'L', :exclusive => false},
|
505
|
-
'source_id' => {:prefix => 'I', :exclusive => true},
|
506
|
-
'attachment_extension' => {:prefix => 'O', :exclusive => false},
|
507
|
-
'msgid' => {:prefix => 'Q', :exclusive => true},
|
508
|
-
'id' => {:prefix => 'Q', :exclusive => true},
|
509
|
-
'thread' => {:prefix => 'H', :exclusive => false},
|
510
|
-
'ref' => {:prefix => 'R', :exclusive => false},
|
511
|
-
'location' => {:prefix => 'J', :exclusive => false},
|
512
|
-
}
|
513
|
-
|
514
|
-
PREFIX = NORMAL_PREFIX.merge BOOLEAN_PREFIX
|
515
|
-
|
516
536
|
MSGID_VALUENO = 0
|
517
537
|
THREAD_VALUENO = 1
|
518
538
|
DATE_VALUENO = 2
|
data/lib/sup/label.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
1
3
|
module Redwood
|
2
4
|
|
3
5
|
class LabelManager
|
@@ -77,7 +79,7 @@ class LabelManager
|
|
77
79
|
|
78
80
|
def save
|
79
81
|
return unless @modified
|
80
|
-
File.open(@fn, "w") { |f| f.puts @labels.keys.sort_by { |l| l.to_s } }
|
82
|
+
File.open(@fn, "w:UTF-8") { |f| f.puts @labels.keys.sort_by { |l| l.to_s } }
|
81
83
|
@new_labels = {}
|
82
84
|
end
|
83
85
|
end
|
data/lib/sup/message.rb
CHANGED
@@ -69,7 +69,9 @@ class Message
|
|
69
69
|
return unless v
|
70
70
|
return v unless v.is_a? String
|
71
71
|
return unless v.size < MAX_HEADER_VALUE_SIZE # avoid regex blowup on spam
|
72
|
-
|
72
|
+
d = v.dup
|
73
|
+
d = d.transcode($encoding, 'ASCII')
|
74
|
+
Rfc2047.decode_to $encoding, d
|
73
75
|
end
|
74
76
|
|
75
77
|
def parse_header encoded_header
|
@@ -109,7 +111,9 @@ class Message
|
|
109
111
|
Time.now
|
110
112
|
end
|
111
113
|
|
112
|
-
|
114
|
+
subj = header["subject"]
|
115
|
+
subj = subj ? subj.fix_encoding : nil
|
116
|
+
@subj = subj ? subj.gsub(/\s+/, " ").gsub(/\s+$/, "") : DEFAULT_SUBJECT
|
113
117
|
@to = Person.from_address_list header["to"]
|
114
118
|
@cc = Person.from_address_list header["cc"]
|
115
119
|
@bcc = Person.from_address_list header["bcc"]
|
@@ -260,6 +264,8 @@ class Message
|
|
260
264
|
rescue SourceError, SocketError, RMail::EncodingUnsupportedError => e
|
261
265
|
warn "problem reading message #{id}"
|
262
266
|
[Chunk::Text.new(error_message.split("\n"))]
|
267
|
+
|
268
|
+
debug "could not load message: #{location.inspect}, exception: #{e.inspect}"
|
263
269
|
end
|
264
270
|
end
|
265
271
|
|
@@ -524,7 +530,7 @@ private
|
|
524
530
|
## if there's no charset, use the current encoding as the charset.
|
525
531
|
## this ensures that the body is normalized to avoid non-displayable
|
526
532
|
## characters
|
527
|
-
body =
|
533
|
+
body = m.decode.transcode($encoding, m.charset)
|
528
534
|
else
|
529
535
|
body = ""
|
530
536
|
end
|
@@ -546,7 +552,7 @@ private
|
|
546
552
|
msg = RMail::Message.new
|
547
553
|
msg.body = gpg.join("\n")
|
548
554
|
|
549
|
-
body =
|
555
|
+
body = body.transcode(encoding_to, encoding_from)
|
550
556
|
lines = body.split("\n")
|
551
557
|
sig = lines.between(GPG_SIGNED_START, GPG_SIG_START)
|
552
558
|
startidx = lines.index(GPG_SIGNED_START)
|
data/lib/sup/message_chunks.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
require 'tempfile'
|
2
2
|
require 'rbconfig'
|
3
|
-
require 'shellwords'
|
4
3
|
|
5
4
|
## Here we define all the "chunks" that a message is parsed
|
6
5
|
## into. Chunks are used by ThreadViewMode to render a message. Chunks
|
@@ -60,8 +59,6 @@ end
|
|
60
59
|
module Redwood
|
61
60
|
module Chunk
|
62
61
|
class Attachment
|
63
|
-
## please see note in write_to_disk on important usage
|
64
|
-
## of quotes to avoid remote command injection.
|
65
62
|
HookManager.register "mime-decode", <<EOS
|
66
63
|
Decodes a MIME attachment into text form. The text will be displayed
|
67
64
|
directly in Sup. For attachments that you wish to use a separate program
|
@@ -78,9 +75,6 @@ Return value:
|
|
78
75
|
The decoded text of the attachment, or nil if not decoded.
|
79
76
|
EOS
|
80
77
|
|
81
|
-
|
82
|
-
## please see note in write_to_disk on important usage
|
83
|
-
## of quotes to avoid remote command injection.
|
84
78
|
HookManager.register "mime-view", <<EOS
|
85
79
|
Views a non-text MIME attachment. This hook allows you to run
|
86
80
|
third-party programs for attachments that require such a thing (e.g.
|
@@ -106,18 +100,8 @@ EOS
|
|
106
100
|
attr_reader :content_type, :filename, :lines, :raw_content
|
107
101
|
bool_reader :quotable
|
108
102
|
|
109
|
-
## store tempfile objects as class variables so that they
|
110
|
-
## are not removed when the viewing process returns. they
|
111
|
-
## should be garbage collected when the class variable is removed.
|
112
|
-
@@view_tempfiles = []
|
113
|
-
|
114
103
|
def initialize content_type, filename, encoded_content, sibling_types
|
115
104
|
@content_type = content_type.downcase
|
116
|
-
if Shellwords.escape(@content_type) != @content_type
|
117
|
-
warn "content_type #{@content_type} is not safe, changed to application/octet-stream"
|
118
|
-
@content_type = 'application/octet-stream'
|
119
|
-
end
|
120
|
-
|
121
105
|
@filename = filename
|
122
106
|
@quotable = false # changed to true if we can parse it through the
|
123
107
|
# mime-decode hook, or if it's plain text
|
@@ -132,9 +116,7 @@ EOS
|
|
132
116
|
when /^text\/plain\b/
|
133
117
|
@raw_content
|
134
118
|
else
|
135
|
-
|
136
|
-
## of quotes to avoid remote command injection.
|
137
|
-
HookManager.run "mime-decode", :content_type => @content_type,
|
119
|
+
HookManager.run "mime-decode", :content_type => content_type,
|
138
120
|
:filename => lambda { write_to_disk },
|
139
121
|
:charset => encoded_content.charset,
|
140
122
|
:sibling_types => sibling_types
|
@@ -142,7 +124,7 @@ EOS
|
|
142
124
|
|
143
125
|
@lines = nil
|
144
126
|
if text
|
145
|
-
text = text.transcode(encoded_content.charset || $encoding)
|
127
|
+
text = text.transcode(encoded_content.charset || $encoding, text.encoding)
|
146
128
|
@lines = text.gsub("\r\n", "\n").gsub(/\t/, " ").gsub(/\r/, "").split("\n")
|
147
129
|
@quotable = true
|
148
130
|
end
|
@@ -165,13 +147,11 @@ EOS
|
|
165
147
|
def initial_state; :open end
|
166
148
|
def viewable?; @lines.nil? end
|
167
149
|
def view_default! path
|
168
|
-
## please see note in write_to_disk on important usage
|
169
|
-
## of quotes to avoid remote command injection.
|
170
150
|
case RbConfig::CONFIG['arch']
|
171
151
|
when /darwin/
|
172
|
-
cmd = "open #{path}"
|
152
|
+
cmd = "open '#{path}'"
|
173
153
|
else
|
174
|
-
cmd = "/usr/bin/run-mailcap --action=view #{@content_type}:#{path}"
|
154
|
+
cmd = "/usr/bin/run-mailcap --action=view '#{@content_type}:#{path}'"
|
175
155
|
end
|
176
156
|
debug "running: #{cmd.inspect}"
|
177
157
|
BufferManager.shell_out(cmd)
|
@@ -179,31 +159,17 @@ EOS
|
|
179
159
|
end
|
180
160
|
|
181
161
|
def view!
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
@@view_tempfiles.push file # make sure the tempfile is not garbage collected before sup stops
|
187
|
-
|
188
|
-
ret = HookManager.run "mime-view", :content_type => @content_type,
|
189
|
-
:filename => file.path
|
190
|
-
ret || view_default!(file.path)
|
191
|
-
end
|
162
|
+
path = write_to_disk
|
163
|
+
ret = HookManager.run "mime-view", :content_type => @content_type,
|
164
|
+
:filename => path
|
165
|
+
ret || view_default!(path)
|
192
166
|
end
|
193
167
|
|
194
|
-
## note that the path returned from write_to_disk is
|
195
|
-
## Shellwords.escaped and is intended to be used without single
|
196
|
-
## or double quotes. the use of either opens sup up for remote
|
197
|
-
## code injection through the file name.
|
198
168
|
def write_to_disk
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
return file.path
|
204
|
-
ensure
|
205
|
-
file.close
|
206
|
-
end
|
169
|
+
file = Tempfile.new(["sup", @filename.gsub("/", "_") || "sup-attachment"])
|
170
|
+
file.print @raw_content
|
171
|
+
file.close
|
172
|
+
file.path
|
207
173
|
end
|
208
174
|
|
209
175
|
## used when viewing the attachment as text
|
@@ -263,7 +229,7 @@ EOS
|
|
263
229
|
class EnclosedMessage
|
264
230
|
attr_reader :lines
|
265
231
|
def initialize from, to, cc, date, subj
|
266
|
-
@from = from ? "unknown sender" : from.
|
232
|
+
@from = from ? "unknown sender" : from.full_adress
|
267
233
|
@to = to ? "" : to.map { |p| p.full_address }.join(", ")
|
268
234
|
@cc = cc ? "" : cc.map { |p| p.full_address }.join(", ")
|
269
235
|
if date
|
@@ -484,10 +484,10 @@ protected
|
|
484
484
|
m = build_message date
|
485
485
|
|
486
486
|
if HookManager.enabled? "sendmail"
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
487
|
+
if not HookManager.run "sendmail", :message => m, :account => acct
|
488
|
+
warn "Sendmail hook was not successful"
|
489
|
+
return false
|
490
|
+
end
|
491
491
|
else
|
492
492
|
IO.popen(acct.sendmail, "w") { |p| p.puts m }
|
493
493
|
raise SendmailCommandFailed, "Couldn't execute #{acct.sendmail}" unless $? == 0
|
data/lib/sup/modes/inbox_mode.rb
CHANGED
@@ -856,14 +856,14 @@ protected
|
|
856
856
|
|
857
857
|
abbrev =
|
858
858
|
if cur_width + name.display_length > from_width
|
859
|
-
name
|
859
|
+
name.slice_by_display_length(from_width - cur_width - 1) + "."
|
860
860
|
elsif cur_width + name.display_length == from_width
|
861
|
-
name
|
861
|
+
name.slice_by_display_length(from_width - cur_width)
|
862
862
|
else
|
863
863
|
if last
|
864
|
-
name
|
864
|
+
name.slice_by_display_length(from_width - cur_width)
|
865
865
|
else
|
866
|
-
name
|
866
|
+
name.slice_by_display_length(from_width - cur_width - 1) + ","
|
867
867
|
end
|
868
868
|
end
|
869
869
|
|
@@ -846,6 +846,10 @@ private
|
|
846
846
|
else
|
847
847
|
width = buffer.content_width
|
848
848
|
end
|
849
|
+
# lines can apparently be both String and Array, convert to Array for map.
|
850
|
+
if lines.kind_of? String
|
851
|
+
lines = lines.lines.to_a
|
852
|
+
end
|
849
853
|
lines = lines.map { |l| l.chomp.wrap width if l }.flatten
|
850
854
|
end
|
851
855
|
return lines
|
data/lib/sup/poll.rb
CHANGED
@@ -171,7 +171,7 @@ EOS
|
|
171
171
|
## labels and locations set correctly. The Messages are saved to or removed
|
172
172
|
## from the index after being yielded.
|
173
173
|
def poll_from source, opts={}
|
174
|
-
debug "trying to
|
174
|
+
debug "trying to acquire poll lock for: #{source}.."
|
175
175
|
if source.poll_lock.try_lock
|
176
176
|
debug "lock acquired for: #{source}."
|
177
177
|
begin
|
data/lib/sup/rfc2047.rb
CHANGED
@@ -16,8 +16,6 @@
|
|
16
16
|
#
|
17
17
|
# This file is distributed under the same terms as Ruby.
|
18
18
|
|
19
|
-
require 'iconv'
|
20
|
-
|
21
19
|
module Rfc2047
|
22
20
|
WORD = %r{=\?([!\#$%&'*+-/0-9A-Z\\^\`a-z{|}~]+)\?([BbQq])\?([!->@-~]+)\?=} # :nodoc: 'stupid ruby-mode
|
23
21
|
WORDSEQ = %r{(#{WORD.source})\s+(?=#{WORD.source})}
|
@@ -52,7 +50,7 @@ module Rfc2047
|
|
52
50
|
# WORD.
|
53
51
|
end
|
54
52
|
|
55
|
-
|
53
|
+
text.transcode(target, charset)
|
56
54
|
end
|
57
55
|
end
|
58
56
|
end
|
data/lib/sup/sent.rb
CHANGED
@@ -25,8 +25,13 @@ class SentManager
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def write_sent_message date, from_email, &block
|
28
|
-
|
29
|
-
|
28
|
+
::Thread.new do
|
29
|
+
debug "store the sent message (locking sent source..)"
|
30
|
+
@source.poll_lock.synchronize do
|
31
|
+
@source.store_message date, from_email, &block
|
32
|
+
end
|
33
|
+
PollManager.poll_from @source
|
34
|
+
end
|
30
35
|
end
|
31
36
|
end
|
32
37
|
|
data/lib/sup/source.rb
CHANGED
@@ -22,30 +22,23 @@ class Source
|
|
22
22
|
## read, delete them, or anything else. (Well, it's nice to be able
|
23
23
|
## to delete them, but that is optional.)
|
24
24
|
##
|
25
|
-
##
|
26
|
-
## unique
|
27
|
-
##
|
28
|
-
##
|
29
|
-
##
|
30
|
-
##
|
31
|
-
## capability, e.g. IMAP, then you have to do a little more work to
|
32
|
-
## simulate it.
|
25
|
+
## Messages are identified internally based on the message id, and stored
|
26
|
+
## with an unique document id. Along with the message, source information
|
27
|
+
## that can contain arbitrary fields (set up by the source) is stored. This
|
28
|
+
## information will be passed back to the source when a message in the
|
29
|
+
## index (Sup database) needs to be identified to its source, e.g. when
|
30
|
+
## re-reading or modifying a unique message.
|
33
31
|
##
|
34
32
|
## To write a new source, subclass this class, and implement:
|
35
33
|
##
|
36
|
-
## -
|
37
|
-
## - end_offset (exclusive!) (or, #done?)
|
34
|
+
## - initialize
|
38
35
|
## - load_header offset
|
39
36
|
## - load_message offset
|
40
37
|
## - raw_header offset
|
41
38
|
## - raw_message offset
|
42
|
-
## -
|
39
|
+
## - store_message (optional)
|
40
|
+
## - poll (loads new messages)
|
43
41
|
## - go_idle (optional)
|
44
|
-
## - next (or each, if you prefer): should return a message and an
|
45
|
-
## array of labels.
|
46
|
-
##
|
47
|
-
## ... where "offset" really means unique id. (You can tell I
|
48
|
-
## started with mbox.)
|
49
42
|
##
|
50
43
|
## All exceptions relating to accessing the source must be caught
|
51
44
|
## and rethrown as FatalSourceErrors or OutOfSyncSourceErrors.
|
@@ -57,8 +50,7 @@ class Source
|
|
57
50
|
## Finally, be sure the source is thread-safe, since it WILL be
|
58
51
|
## pummelled from multiple threads at once.
|
59
52
|
##
|
60
|
-
## Examples for you to look at: mbox
|
61
|
-
## maildir.rb.
|
53
|
+
## Examples for you to look at: mbox.rb and maildir.rb.
|
62
54
|
|
63
55
|
bool_accessor :usual, :archived
|
64
56
|
attr_reader :uri
|
@@ -211,9 +203,9 @@ class SourceManager
|
|
211
203
|
end
|
212
204
|
end
|
213
205
|
|
214
|
-
def save_sources fn=Redwood::SOURCE_FN
|
206
|
+
def save_sources fn=Redwood::SOURCE_FN, force=false
|
215
207
|
@source_mutex.synchronize do
|
216
|
-
if @sources_dirty
|
208
|
+
if @sources_dirty || force
|
217
209
|
Redwood::save_yaml_obj sources, fn, false, true
|
218
210
|
end
|
219
211
|
@sources_dirty = false
|