require 'stringio'
require 'time'
require 'rmail'
+require 'cgi'
## fucking imap fucking sucks. what the FUCK kind of committee of
## dunces designed this shit.
@username = username
@password = password
@imap = nil
- @imap_ids = {}
+ @imap_state = {}
@ids = []
@last_scan = nil
- @labels = (labels || []).freeze
+ @labels = ((labels || []) - LabelManager::RESERVED_LABELS).uniq.freeze
@say_id = nil
@mutex = Mutex.new
end
def self.suggest_labels_for path
- if path =~ /inbox/i
- [path.intern]
- else
- []
- end
+ path =~ /([^\/]*inbox[^\/]*)/i ? [$1.downcase.intern] : []
end
def host; @parsed_uri.host; end
def port; @parsed_uri.port || (ssl? ? 993 : 143); end
def mailbox
x = @parsed_uri.path[1..-1]
- x.nil? || x.empty? ? 'INBOX' : x
+ (x.nil? || x.empty?) ? 'INBOX' : CGI.unescape(x)
end
def ssl?; @parsed_uri.scheme == 'imaps' end
- def check
- return unless start_offset
-
- ids =
- @mutex.synchronize do
- unsynchronized_scan_mailbox
- @ids
- end
-
- start = ids.index(cur_offset || start_offset) or raise OutOfSyncSourceError, "Unknown message id #{cur_offset || start_offset}."
- end
+ def check; end # do nothing because anything we do will be too slow,
+ # and we'll catch the errors later.
## is this necessary? TODO: remove maybe
def == o; o.is_a?(IMAP) && o.uri == self.uri && o.username == self.username; end
def load_message id
RMail::Parser.read raw_message(id)
end
+
+ def each_raw_message_line id
+ StringIO.new(raw_message(id)).each { |l| yield l }
+ end
def raw_header id
unsynchronized_scan_mailbox
- header, flags = get_imap_fields id, 'RFC822.HEADER', 'FLAGS'
- ## very bad. this is very very bad. very bad bad bad.
- header = header + "Status: RO\n" if flags.include? :Seen # fake an mbox-style read header # TODO: improve source-marked-as-read reporting system
+ header, flags = get_imap_fields id, 'RFC822.HEADER'
header.gsub(/\r\n/, "\n")
end
synchronized :raw_header
range = (@ids.length + 1) .. last_id
Redwood::log "fetching IMAP headers #{range}"
- fetch(range, ['RFC822.SIZE', 'INTERNALDATE']).each do |v|
+ fetch(range, ['RFC822.SIZE', 'INTERNALDATE', 'FLAGS']).each do |v|
id = make_id v
@ids << id
- @imap_ids[id] = v.seqno
+ @imap_state[id] = { :id => v.seqno, :flags => v.attr["FLAGS"] }
end
+ Redwood::log "done fetching IMAP headers"
end
synchronized :scan_mailbox
start = ids.index(cur_offset || start_offset) or raise OutOfSyncSourceError, "Unknown message id #{cur_offset || start_offset}."
- start.upto(ids.length - 1) do |i|
+ start.upto(ids.length - 1) do |i|
id = ids[i]
- self.cur_offset = id
- yield id, @labels
+ state = @mutex.synchronize { @imap_state[id] } or next
+ self.cur_offset = id
+ labels = { :Flagged => :starred,
+ :Deleted => :deleted
+ }.inject(@labels) do |cur, (imap, sup)|
+ cur + (state[:flags].include?(imap) ? [sup] : [])
+ end
+
+ labels += [:unread] unless state[:flags].include?(:Seen)
+
+ yield id, labels
end
end
## fails with a NO response, the client may try another", in
## practice it seems like they can also send a BAD response.
begin
+ raise Net::IMAP::NoResponseError unless @imap.capability().member? "AUTH=CRAM-MD5"
@imap.authenticate 'CRAM-MD5', @username, @password
rescue Net::IMAP::BadResponseError, Net::IMAP::NoResponseError => e
Redwood::log "CRAM-MD5 authentication failed: #{e.class}. Trying LOGIN auth..."
begin
+ raise Net::IMAP::NoResponseError unless @imap.capability().member? "AUTH=LOGIN"
@imap.authenticate 'LOGIN', @username, @password
rescue Net::IMAP::BadResponseError, Net::IMAP::NoResponseError => e
Redwood::log "LOGIN authentication failed: #{e.class}. Trying plain-text LOGIN..."
end
def get_imap_fields id, *fields
- imap_id = @imap_ids[id] or raise OutOfSyncSourceError, "Unknown message id #{id}"
+ raise OutOfSyncSourceError, "Unknown message id #{id}" unless @imap_state[id]
- retried = false
+ imap_id = @imap_state[id][:id]
result = fetch(imap_id, (fields + ['RFC822.SIZE', 'INTERNALDATE']).uniq).first
got_id = make_id result
- raise OutOfSyncSourceError, "IMAP message mismatch: requested #{id}, got #{got_id}." unless got_id == id
+
+ ## I've turned off the following sanity check because Microsoft Exchange fails it.
+ ## Exchange actually reports two different INTERNALDATEs for the exact same message
+ ## when queried at different points in time.
+ ##
+ ##
+ ## RFC2060 defines the semantics of INTERNALDATE for messages that arrive
+ ## via SMTP for via various IMAP commands, but states that "All other
+ ## cases are implementation defined.". Great, thanks guys, yet another
+ ## useless field.
+ ##
+ ## Of course no OTHER imap server I've encountered returns DIFFERENT values for
+ ## the SAME message. But it's Microsoft; what do you expect? If their programmers
+ ## were any good they'd be working at Google.
+
+ # raise OutOfSyncSourceError, "IMAP message mismatch: requested #{id}, got #{got_id}." unless got_id == id
fields.map { |f| result.attr[f] or raise FatalSourceError, "empty response from IMAP server: #{f}" }
end