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.

@@ -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
@@ -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
@@ -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