sup 0.11 → 0.12
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 +16 -5
- data/History.txt +9 -0
- data/ReleaseNotes +9 -0
- data/bin/sup +9 -24
- data/bin/sup-add +3 -3
- data/bin/sup-config +1 -1
- data/bin/sup-dump +22 -9
- data/bin/sup-recover-sources +1 -11
- data/bin/sup-sync +65 -130
- data/bin/sup-sync-back +6 -5
- data/bin/sup-tweak-labels +5 -6
- data/lib/sup.rb +89 -71
- data/lib/sup/account.rb +3 -2
- data/lib/sup/buffer.rb +28 -16
- data/lib/sup/client.rb +92 -0
- data/lib/sup/crypto.rb +91 -49
- data/lib/sup/draft.rb +14 -17
- data/lib/sup/hook.rb +10 -5
- data/lib/sup/index.rb +72 -28
- data/lib/sup/logger.rb +1 -1
- data/lib/sup/maildir.rb +55 -112
- data/lib/sup/mbox.rb +151 -6
- data/lib/sup/message-chunks.rb +20 -4
- data/lib/sup/message.rb +183 -76
- data/lib/sup/modes/compose-mode.rb +2 -1
- data/lib/sup/modes/console-mode.rb +4 -1
- data/lib/sup/modes/edit-message-mode.rb +50 -5
- data/lib/sup/modes/line-cursor-mode.rb +1 -0
- data/lib/sup/modes/reply-mode.rb +17 -11
- data/lib/sup/modes/thread-index-mode.rb +10 -9
- data/lib/sup/modes/thread-view-mode.rb +48 -2
- data/lib/sup/poll.rb +56 -60
- data/lib/sup/protocol.rb +161 -0
- data/lib/sup/sent.rb +8 -11
- data/lib/sup/server.rb +116 -0
- data/lib/sup/source.rb +15 -33
- data/lib/sup/thread.rb +6 -0
- data/lib/sup/util.rb +44 -39
- metadata +126 -88
- data/lib/sup/connection.rb +0 -63
- data/lib/sup/imap.rb +0 -349
- data/lib/sup/mbox/loader.rb +0 -180
- data/lib/sup/mbox/ssh-file.rb +0 -254
- data/lib/sup/mbox/ssh-loader.rb +0 -74
data/lib/sup/connection.rb
DELETED
@@ -1,63 +0,0 @@
|
|
1
|
-
module Redwood
|
2
|
-
|
3
|
-
## Hacky implementation of the sup-server API using existing Sup code
|
4
|
-
class Connection
|
5
|
-
def result_from_message m, raw
|
6
|
-
mkperson = lambda { |p| { :email => p.email, :name => p.name } }
|
7
|
-
{
|
8
|
-
'summary' => {
|
9
|
-
'message_id' => m.id,
|
10
|
-
'date' => m.date,
|
11
|
-
'from' => mkperson[m.from],
|
12
|
-
'to' => m.to.map(&mkperson),
|
13
|
-
'cc' => m.cc.map(&mkperson),
|
14
|
-
'bcc' => m.bcc.map(&mkperson),
|
15
|
-
'subject' => m.subj,
|
16
|
-
'refs' => m.refs,
|
17
|
-
'replytos' => m.replytos,
|
18
|
-
'labels' => m.labels.map(&:to_s),
|
19
|
-
},
|
20
|
-
'raw' => raw ? m.raw_message : nil,
|
21
|
-
}
|
22
|
-
end
|
23
|
-
|
24
|
-
def query query, offset, limit, raw
|
25
|
-
c = 0
|
26
|
-
Index.each_message query do |m|
|
27
|
-
next if c < offset
|
28
|
-
break if c >= offset + limit if limit
|
29
|
-
yield result_from_message(m, raw)
|
30
|
-
c += 1
|
31
|
-
end
|
32
|
-
nil
|
33
|
-
end
|
34
|
-
|
35
|
-
def count query
|
36
|
-
Index.num_results_for query
|
37
|
-
end
|
38
|
-
|
39
|
-
def label query, remove_labels, add_labels
|
40
|
-
Index.each_message query do |m|
|
41
|
-
remove_labels.each { |l| m.remove_label l }
|
42
|
-
add_labels.each { |l| m.add_label l }
|
43
|
-
Index.update_message_state m
|
44
|
-
end
|
45
|
-
nil
|
46
|
-
end
|
47
|
-
|
48
|
-
def add raw, labels
|
49
|
-
SentManager.source.store_message Time.now, "test@example.com" do |io|
|
50
|
-
io.write raw
|
51
|
-
end
|
52
|
-
m2 = nil
|
53
|
-
PollManager.each_message_from(SentManager.source) do |m|
|
54
|
-
PollManager.add_new_message m
|
55
|
-
m2 = m
|
56
|
-
end
|
57
|
-
m2.labels = Set.new(labels.map(&:to_sym))
|
58
|
-
Index.update_message_state m2
|
59
|
-
nil
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
end
|
data/lib/sup/imap.rb
DELETED
@@ -1,349 +0,0 @@
|
|
1
|
-
require 'uri'
|
2
|
-
require 'net/imap'
|
3
|
-
require 'stringio'
|
4
|
-
require 'time'
|
5
|
-
require 'rmail'
|
6
|
-
require 'cgi'
|
7
|
-
require 'set'
|
8
|
-
|
9
|
-
## TODO: remove synchronized method protector calls; use a Monitor instead
|
10
|
-
## (ruby's reentrant mutex)
|
11
|
-
|
12
|
-
## fucking imap fucking sucks. what the FUCK kind of committee of dunces
|
13
|
-
## designed this shit.
|
14
|
-
##
|
15
|
-
## imap talks about 'unique ids' for messages, to be used for
|
16
|
-
## cross-session identification. great---just what sup needs! except it
|
17
|
-
## turns out the uids can be invalidated every time the 'uidvalidity'
|
18
|
-
## value changes on the server, and 'uidvalidity' can change without
|
19
|
-
## restriction. it can change any time you log in. it can change EVERY
|
20
|
-
## time you log in. of course the imap spec "strongly recommends" that it
|
21
|
-
## never change, but there's nothing to stop people from just setting it
|
22
|
-
## to the current timestamp, and in fact that's EXACTLY what the one imap
|
23
|
-
## server i have at my disposal does. thus the so-called uids are
|
24
|
-
## absolutely useless and imap provides no cross-session way of uniquely
|
25
|
-
## identifying a message. but thanks for the "strong recommendation",
|
26
|
-
## guys!
|
27
|
-
##
|
28
|
-
## so right now i'm using the 'internal date' and the size of each
|
29
|
-
## message to uniquely identify it, and i scan over the entire mailbox
|
30
|
-
## each time i open it to map those things to message ids. that can be
|
31
|
-
## slow for large mailboxes, and we'll just have to hope that there are
|
32
|
-
## no collisions. ho ho! a perfectly reasonable solution!
|
33
|
-
##
|
34
|
-
## and here's another thing. check out RFC2060 2.2.2 paragraph 5:
|
35
|
-
##
|
36
|
-
## A client MUST be prepared to accept any server response at all
|
37
|
-
## times. This includes server data that was not requested.
|
38
|
-
##
|
39
|
-
## yeah. that totally makes a lot of sense. and once again, the idiocy of
|
40
|
-
## the spec actually happens in practice. you'll request flags for one
|
41
|
-
## message, and get it interspersed with a random bunch of flags for some
|
42
|
-
## other messages, including a different set of flags for the same
|
43
|
-
## message! totally ok by the imap spec. totally retarded by any other
|
44
|
-
## metric.
|
45
|
-
##
|
46
|
-
## fuck you, imap committee. you managed to design something nearly as
|
47
|
-
## shitty as mbox but goddamn THIRTY YEARS LATER.
|
48
|
-
module Redwood
|
49
|
-
|
50
|
-
class IMAP < Source
|
51
|
-
include SerializeLabelsNicely
|
52
|
-
SCAN_INTERVAL = 60 # seconds
|
53
|
-
|
54
|
-
## upon these errors we'll try to rereconnect a few times
|
55
|
-
RECOVERABLE_ERRORS = [ Errno::EPIPE, Errno::ETIMEDOUT, OpenSSL::SSL::SSLError ]
|
56
|
-
|
57
|
-
attr_accessor :username, :password
|
58
|
-
yaml_properties :uri, :username, :password, :cur_offset, :usual,
|
59
|
-
:archived, :id, :labels
|
60
|
-
|
61
|
-
def initialize uri, username, password, last_idate=nil, usual=true, archived=false, id=nil, labels=[]
|
62
|
-
raise ArgumentError, "username and password must be specified" unless username && password
|
63
|
-
raise ArgumentError, "not an imap uri" unless uri =~ %r!imaps?://!
|
64
|
-
|
65
|
-
super uri, last_idate, usual, archived, id
|
66
|
-
|
67
|
-
@parsed_uri = URI(uri)
|
68
|
-
@username = username
|
69
|
-
@password = password
|
70
|
-
@imap = nil
|
71
|
-
@imap_state = {}
|
72
|
-
@ids = []
|
73
|
-
@last_scan = nil
|
74
|
-
@labels = Set.new((labels || []) - LabelManager::RESERVED_LABELS)
|
75
|
-
@say_id = nil
|
76
|
-
@mutex = Mutex.new
|
77
|
-
end
|
78
|
-
|
79
|
-
def self.suggest_labels_for path
|
80
|
-
path =~ /([^\/]*inbox[^\/]*)/i ? [$1.downcase.intern] : []
|
81
|
-
end
|
82
|
-
|
83
|
-
def host; @parsed_uri.host; end
|
84
|
-
def port; @parsed_uri.port || (ssl? ? 993 : 143); end
|
85
|
-
def mailbox
|
86
|
-
x = @parsed_uri.path[1..-1]
|
87
|
-
(x.nil? || x.empty?) ? 'INBOX' : CGI.unescape(x)
|
88
|
-
end
|
89
|
-
def ssl?; @parsed_uri.scheme == 'imaps' end
|
90
|
-
|
91
|
-
def check; end # do nothing because anything we do will be too slow,
|
92
|
-
# and we'll catch the errors later.
|
93
|
-
|
94
|
-
## is this necessary? TODO: remove maybe
|
95
|
-
def == o; o.is_a?(IMAP) && o.uri == self.uri && o.username == self.username; end
|
96
|
-
|
97
|
-
def load_header id
|
98
|
-
parse_raw_email_header StringIO.new(raw_header(id))
|
99
|
-
end
|
100
|
-
|
101
|
-
def load_message id
|
102
|
-
RMail::Parser.read raw_message(id)
|
103
|
-
end
|
104
|
-
|
105
|
-
def each_raw_message_line id
|
106
|
-
StringIO.new(raw_message(id)).each { |l| yield l }
|
107
|
-
end
|
108
|
-
|
109
|
-
def raw_header id
|
110
|
-
unsynchronized_scan_mailbox
|
111
|
-
header, flags = get_imap_fields id, 'RFC822.HEADER'
|
112
|
-
header.gsub(/\r\n/, "\n")
|
113
|
-
end
|
114
|
-
synchronized :raw_header
|
115
|
-
|
116
|
-
def store_message date, from_email, &block
|
117
|
-
message = StringIO.new
|
118
|
-
yield message
|
119
|
-
message.string.gsub! /\n/, "\r\n"
|
120
|
-
|
121
|
-
safely { @imap.append mailbox, message.string, [:Seen], Time.now }
|
122
|
-
end
|
123
|
-
|
124
|
-
def raw_message id
|
125
|
-
unsynchronized_scan_mailbox
|
126
|
-
get_imap_fields(id, 'RFC822').first.gsub(/\r\n/, "\n")
|
127
|
-
end
|
128
|
-
synchronized :raw_message
|
129
|
-
|
130
|
-
def mark_as_deleted ids
|
131
|
-
ids = [ids].flatten # accept single arguments
|
132
|
-
unsynchronized_scan_mailbox
|
133
|
-
imap_ids = ids.map { |i| @imap_state[i] && @imap_state[i][:id] }.compact
|
134
|
-
return if imap_ids.empty?
|
135
|
-
@imap.store imap_ids, "+FLAGS", [:Deleted]
|
136
|
-
end
|
137
|
-
synchronized :mark_as_deleted
|
138
|
-
|
139
|
-
def expunge
|
140
|
-
@imap.expunge
|
141
|
-
unsynchronized_scan_mailbox true
|
142
|
-
true
|
143
|
-
end
|
144
|
-
synchronized :expunge
|
145
|
-
|
146
|
-
def connect
|
147
|
-
return if @imap
|
148
|
-
safely { } # do nothing!
|
149
|
-
end
|
150
|
-
synchronized :connect
|
151
|
-
|
152
|
-
def scan_mailbox force=false
|
153
|
-
return if !force && @last_scan && (Time.now - @last_scan) < SCAN_INTERVAL
|
154
|
-
last_id = safely do
|
155
|
-
@imap.examine mailbox
|
156
|
-
@imap.responses["EXISTS"].last
|
157
|
-
end
|
158
|
-
@last_scan = Time.now
|
159
|
-
|
160
|
-
@ids = [] if force
|
161
|
-
return if last_id == @ids.length
|
162
|
-
|
163
|
-
range = (@ids.length + 1) .. last_id
|
164
|
-
debug "fetching IMAP headers #{range}"
|
165
|
-
fetch(range, ['RFC822.SIZE', 'INTERNALDATE', 'FLAGS']).each do |v|
|
166
|
-
id = make_id v
|
167
|
-
@ids << id
|
168
|
-
@imap_state[id] = { :id => v.seqno, :flags => v.attr["FLAGS"] }
|
169
|
-
end
|
170
|
-
debug "done fetching IMAP headers"
|
171
|
-
end
|
172
|
-
synchronized :scan_mailbox
|
173
|
-
|
174
|
-
def each
|
175
|
-
return unless start_offset
|
176
|
-
|
177
|
-
ids =
|
178
|
-
@mutex.synchronize do
|
179
|
-
unsynchronized_scan_mailbox
|
180
|
-
@ids
|
181
|
-
end
|
182
|
-
|
183
|
-
start = ids.index(cur_offset || start_offset) or raise OutOfSyncSourceError, "Unknown message id #{cur_offset || start_offset}."
|
184
|
-
|
185
|
-
start.upto(ids.length - 1) do |i|
|
186
|
-
id = ids[i]
|
187
|
-
state = @mutex.synchronize { @imap_state[id] } or next
|
188
|
-
self.cur_offset = id
|
189
|
-
labels = { :Flagged => :starred,
|
190
|
-
:Deleted => :deleted
|
191
|
-
}.inject(@labels) do |cur, (imap, sup)|
|
192
|
-
cur + (state[:flags].include?(imap) ? [sup] : [])
|
193
|
-
end
|
194
|
-
|
195
|
-
labels += [:unread] unless state[:flags].include?(:Seen)
|
196
|
-
|
197
|
-
yield id, labels
|
198
|
-
end
|
199
|
-
end
|
200
|
-
|
201
|
-
def start_offset
|
202
|
-
unsynchronized_scan_mailbox
|
203
|
-
@ids.first
|
204
|
-
end
|
205
|
-
synchronized :start_offset
|
206
|
-
|
207
|
-
def end_offset
|
208
|
-
unsynchronized_scan_mailbox
|
209
|
-
@ids.last + 1
|
210
|
-
end
|
211
|
-
synchronized :end_offset
|
212
|
-
|
213
|
-
def pct_done; 100.0 * (@ids.index(cur_offset) || 0).to_f / (@ids.length - 1).to_f; end
|
214
|
-
|
215
|
-
private
|
216
|
-
|
217
|
-
def fetch ids, fields
|
218
|
-
results = safely { @imap.fetch ids, fields }
|
219
|
-
good_results =
|
220
|
-
if ids.respond_to? :member?
|
221
|
-
results.find_all { |r| ids.member?(r.seqno) && fields.all? { |f| r.attr.member?(f) } }
|
222
|
-
else
|
223
|
-
results.find_all { |r| ids == r.seqno && fields.all? { |f| r.attr.member?(f) } }
|
224
|
-
end
|
225
|
-
|
226
|
-
if good_results.empty?
|
227
|
-
raise FatalSourceError, "no IMAP response for #{ids} containing all fields #{fields.join(', ')} (got #{results.size} results)"
|
228
|
-
elsif good_results.size < results.size
|
229
|
-
warn "Your IMAP server sucks. It sent #{results.size} results for a request for #{good_results.size} messages. What are you using, Binc?"
|
230
|
-
end
|
231
|
-
|
232
|
-
good_results
|
233
|
-
end
|
234
|
-
|
235
|
-
def unsafe_connect
|
236
|
-
say "Connecting to IMAP server #{host}:#{port}..."
|
237
|
-
|
238
|
-
## apparently imap.rb does a lot of threaded stuff internally and if
|
239
|
-
## an exception occurs, it will catch it and re-raise it on the
|
240
|
-
## calling thread. but i can't seem to catch that exception, so i've
|
241
|
-
## resorted to initializing it in its own thread. surely there's a
|
242
|
-
## better way.
|
243
|
-
exception = nil
|
244
|
-
::Thread.new do
|
245
|
-
begin
|
246
|
-
#raise Net::IMAP::ByeResponseError, "simulated imap failure"
|
247
|
-
@imap = Net::IMAP.new host, port, ssl?
|
248
|
-
say "Logging in..."
|
249
|
-
|
250
|
-
## although RFC1730 claims that "If an AUTHENTICATE command fails
|
251
|
-
## with a NO response, the client may try another", in practice
|
252
|
-
## it seems like they can also send a BAD response.
|
253
|
-
begin
|
254
|
-
raise Net::IMAP::NoResponseError unless @imap.capability().member? "AUTH=CRAM-MD5"
|
255
|
-
@imap.authenticate 'CRAM-MD5', @username, @password
|
256
|
-
rescue Net::IMAP::BadResponseError, Net::IMAP::NoResponseError => e
|
257
|
-
debug "CRAM-MD5 authentication failed: #{e.class}. Trying LOGIN auth..."
|
258
|
-
begin
|
259
|
-
raise Net::IMAP::NoResponseError unless @imap.capability().member? "AUTH=LOGIN"
|
260
|
-
@imap.authenticate 'LOGIN', @username, @password
|
261
|
-
rescue Net::IMAP::BadResponseError, Net::IMAP::NoResponseError => e
|
262
|
-
debug "LOGIN authentication failed: #{e.class}. Trying plain-text LOGIN..."
|
263
|
-
@imap.login @username, @password
|
264
|
-
end
|
265
|
-
end
|
266
|
-
say "Successfully connected to #{@parsed_uri}."
|
267
|
-
rescue Exception => e
|
268
|
-
exception = e
|
269
|
-
ensure
|
270
|
-
shutup
|
271
|
-
end
|
272
|
-
end.join
|
273
|
-
|
274
|
-
raise exception if exception
|
275
|
-
end
|
276
|
-
|
277
|
-
def say s
|
278
|
-
@say_id = BufferManager.say s, @say_id if BufferManager.instantiated?
|
279
|
-
info s
|
280
|
-
end
|
281
|
-
|
282
|
-
def shutup
|
283
|
-
BufferManager.clear @say_id if BufferManager.instantiated?
|
284
|
-
@say_id = nil
|
285
|
-
end
|
286
|
-
|
287
|
-
def make_id imap_stuff
|
288
|
-
# use 7 digits for the size. why 7? seems nice.
|
289
|
-
%w(RFC822.SIZE INTERNALDATE).each do |w|
|
290
|
-
raise FatalSourceError, "requested data not in IMAP response: #{w}" unless imap_stuff.attr[w]
|
291
|
-
end
|
292
|
-
|
293
|
-
msize, mdate = imap_stuff.attr['RFC822.SIZE'] % 10000000, Time.parse(imap_stuff.attr["INTERNALDATE"])
|
294
|
-
sprintf("%d%07d", mdate.to_i, msize).to_i
|
295
|
-
end
|
296
|
-
|
297
|
-
def get_imap_fields id, *fields
|
298
|
-
raise OutOfSyncSourceError, "Unknown message id #{id}" unless @imap_state[id]
|
299
|
-
|
300
|
-
imap_id = @imap_state[id][:id]
|
301
|
-
result = fetch(imap_id, (fields + ['RFC822.SIZE', 'INTERNALDATE']).uniq).first
|
302
|
-
got_id = make_id result
|
303
|
-
|
304
|
-
## I've turned off the following sanity check because Microsoft
|
305
|
-
## Exchange fails it. Exchange actually reports two different
|
306
|
-
## INTERNALDATEs for the exact same message when queried at different
|
307
|
-
## points in time.
|
308
|
-
##
|
309
|
-
## RFC2060 defines the semantics of INTERNALDATE for messages that
|
310
|
-
## arrive via SMTP for via various IMAP commands, but states that
|
311
|
-
## "All other cases are implementation defined.". Great, thanks guys,
|
312
|
-
## yet another useless field.
|
313
|
-
##
|
314
|
-
## Of course no OTHER imap server I've encountered returns DIFFERENT
|
315
|
-
## values for the SAME message. But it's Microsoft; what do you
|
316
|
-
## expect? If their programmers were any good they'd be working at
|
317
|
-
## Google.
|
318
|
-
|
319
|
-
# raise OutOfSyncSourceError, "IMAP message mismatch: requested #{id}, got #{got_id}." unless got_id == id
|
320
|
-
|
321
|
-
fields.map { |f| result.attr[f] or raise FatalSourceError, "empty response from IMAP server: #{f}" }
|
322
|
-
end
|
323
|
-
|
324
|
-
## execute a block, connected if unconnected, re-connected up to 3
|
325
|
-
## times if a recoverable error occurs, and properly dying if an
|
326
|
-
## unrecoverable error occurs.
|
327
|
-
def safely
|
328
|
-
retries = 0
|
329
|
-
begin
|
330
|
-
begin
|
331
|
-
unsafe_connect unless @imap
|
332
|
-
yield
|
333
|
-
rescue *RECOVERABLE_ERRORS => e
|
334
|
-
if (retries += 1) <= 3
|
335
|
-
@imap = nil
|
336
|
-
warn "got #{e.class.name}: #{e.message.inspect}"
|
337
|
-
sleep 2
|
338
|
-
retry
|
339
|
-
end
|
340
|
-
raise
|
341
|
-
end
|
342
|
-
rescue SocketError, Net::IMAP::Error, SystemCallError, IOError, OpenSSL::SSL::SSLError => e
|
343
|
-
raise FatalSourceError, "While communicating with IMAP server (type #{e.class.name}): #{e.message.inspect}"
|
344
|
-
end
|
345
|
-
end
|
346
|
-
|
347
|
-
end
|
348
|
-
|
349
|
-
end
|
data/lib/sup/mbox/loader.rb
DELETED
@@ -1,180 +0,0 @@
|
|
1
|
-
require 'rmail'
|
2
|
-
require 'uri'
|
3
|
-
require 'set'
|
4
|
-
|
5
|
-
module Redwood
|
6
|
-
module MBox
|
7
|
-
|
8
|
-
class Loader < Source
|
9
|
-
include SerializeLabelsNicely
|
10
|
-
yaml_properties :uri, :cur_offset, :usual, :archived, :id, :labels
|
11
|
-
|
12
|
-
attr_reader :labels
|
13
|
-
|
14
|
-
## uri_or_fp is horrific. need to refactor.
|
15
|
-
def initialize uri_or_fp, start_offset=nil, usual=true, archived=false, id=nil, labels=nil
|
16
|
-
@mutex = Mutex.new
|
17
|
-
@labels = Set.new((labels || []) - LabelManager::RESERVED_LABELS)
|
18
|
-
|
19
|
-
case uri_or_fp
|
20
|
-
when String
|
21
|
-
uri = URI(Source.expand_filesystem_uri(uri_or_fp))
|
22
|
-
raise ArgumentError, "not an mbox uri" unless uri.scheme == "mbox"
|
23
|
-
raise ArgumentError, "mbox URI ('#{uri}') cannot have a host: #{uri.host}" if uri.host
|
24
|
-
raise ArgumentError, "mbox URI must have a path component" unless uri.path
|
25
|
-
@f = File.open uri.path, 'rb'
|
26
|
-
@path = uri.path
|
27
|
-
else
|
28
|
-
@f = uri_or_fp
|
29
|
-
@path = uri_or_fp.path
|
30
|
-
end
|
31
|
-
|
32
|
-
start_offset ||= 0
|
33
|
-
super uri_or_fp, start_offset, usual, archived, id
|
34
|
-
end
|
35
|
-
|
36
|
-
def file_path; @path end
|
37
|
-
def is_source_for? uri; super || (self.uri.is_a?(String) && (URI(Source.expand_filesystem_uri(uri)) == URI(Source.expand_filesystem_uri(self.uri)))) end
|
38
|
-
|
39
|
-
def self.suggest_labels_for path
|
40
|
-
## heuristic: use the filename as a label, unless the file
|
41
|
-
## has a path that probably represents an inbox.
|
42
|
-
if File.dirname(path) =~ /\b(var|usr|spool)\b/
|
43
|
-
[]
|
44
|
-
else
|
45
|
-
[File.basename(path).downcase.intern]
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
def check
|
50
|
-
if (cur_offset ||= start_offset) > end_offset
|
51
|
-
raise OutOfSyncSourceError, "mbox file is smaller than last recorded message offset. Messages have probably been deleted by another client."
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
def start_offset; 0; end
|
56
|
-
def end_offset; File.size @f; end
|
57
|
-
|
58
|
-
def load_header offset
|
59
|
-
header = nil
|
60
|
-
@mutex.synchronize do
|
61
|
-
@f.seek offset
|
62
|
-
l = @f.gets
|
63
|
-
unless MBox::is_break_line? l
|
64
|
-
raise OutOfSyncSourceError, "mismatch in mbox file offset #{offset.inspect}: #{l.inspect}."
|
65
|
-
end
|
66
|
-
header = parse_raw_email_header @f
|
67
|
-
end
|
68
|
-
header
|
69
|
-
end
|
70
|
-
|
71
|
-
def load_message offset
|
72
|
-
@mutex.synchronize do
|
73
|
-
@f.seek offset
|
74
|
-
begin
|
75
|
-
## don't use RMail::Mailbox::MBoxReader because it doesn't properly ignore
|
76
|
-
## "From" at the start of a message body line.
|
77
|
-
string = ""
|
78
|
-
l = @f.gets
|
79
|
-
string << l until @f.eof? || MBox::is_break_line?(l = @f.gets)
|
80
|
-
RMail::Parser.read string
|
81
|
-
rescue RMail::Parser::Error => e
|
82
|
-
raise FatalSourceError, "error parsing mbox file: #{e.message}"
|
83
|
-
end
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
## scan forward until we're at the valid start of a message
|
88
|
-
def correct_offset!
|
89
|
-
@mutex.synchronize do
|
90
|
-
@f.seek cur_offset
|
91
|
-
string = ""
|
92
|
-
until @f.eof? || MBox::is_break_line?(l = @f.gets)
|
93
|
-
string << l
|
94
|
-
end
|
95
|
-
self.cur_offset += string.length
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
def raw_header offset
|
100
|
-
ret = ""
|
101
|
-
@mutex.synchronize do
|
102
|
-
@f.seek offset
|
103
|
-
until @f.eof? || (l = @f.gets) =~ /^\r*$/
|
104
|
-
ret << l
|
105
|
-
end
|
106
|
-
end
|
107
|
-
ret
|
108
|
-
end
|
109
|
-
|
110
|
-
def raw_message offset
|
111
|
-
ret = ""
|
112
|
-
each_raw_message_line(offset) { |l| ret << l }
|
113
|
-
ret
|
114
|
-
end
|
115
|
-
|
116
|
-
def store_message date, from_email, &block
|
117
|
-
need_blank = File.exists?(@filename) && !File.zero?(@filename)
|
118
|
-
File.open(@filename, "ab") do |f|
|
119
|
-
f.puts if need_blank
|
120
|
-
f.puts "From #{from_email} #{date.rfc2822}"
|
121
|
-
yield f
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
|
-
## apparently it's a million times faster to call this directly if
|
126
|
-
## we're just moving messages around on disk, than reading things
|
127
|
-
## into memory with raw_message.
|
128
|
-
##
|
129
|
-
## i hoped never to have to move shit around on disk but
|
130
|
-
## sup-sync-back has to do it.
|
131
|
-
def each_raw_message_line offset
|
132
|
-
@mutex.synchronize do
|
133
|
-
@f.seek offset
|
134
|
-
yield @f.gets
|
135
|
-
until @f.eof? || MBox::is_break_line?(l = @f.gets)
|
136
|
-
yield l
|
137
|
-
end
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
def next
|
142
|
-
returned_offset = nil
|
143
|
-
next_offset = cur_offset
|
144
|
-
|
145
|
-
begin
|
146
|
-
@mutex.synchronize do
|
147
|
-
@f.seek cur_offset
|
148
|
-
|
149
|
-
## cur_offset could be at one of two places here:
|
150
|
-
|
151
|
-
## 1. before a \n and a mbox separator, if it was previously at
|
152
|
-
## EOF and a new message was added; or,
|
153
|
-
## 2. at the beginning of an mbox separator (in all other
|
154
|
-
## cases).
|
155
|
-
|
156
|
-
l = @f.gets or return nil
|
157
|
-
if l =~ /^\s*$/ # case 1
|
158
|
-
returned_offset = @f.tell
|
159
|
-
@f.gets # now we're at a BREAK_RE, so skip past it
|
160
|
-
else # case 2
|
161
|
-
returned_offset = cur_offset
|
162
|
-
## we've already skipped past the BREAK_RE, so just go
|
163
|
-
end
|
164
|
-
|
165
|
-
while(line = @f.gets)
|
166
|
-
break if MBox::is_break_line? line
|
167
|
-
next_offset = @f.tell
|
168
|
-
end
|
169
|
-
end
|
170
|
-
rescue SystemCallError, IOError => e
|
171
|
-
raise FatalSourceError, "Error reading #{@f.path}: #{e.message}"
|
172
|
-
end
|
173
|
-
|
174
|
-
self.cur_offset = next_offset
|
175
|
-
[returned_offset, (labels + [:unread])]
|
176
|
-
end
|
177
|
-
end
|
178
|
-
|
179
|
-
end
|
180
|
-
end
|