]> git.cworth.org Git - sup/blobdiff - lib/sup/imap.rb
bugfix: attachment tempfile deletion
[sup] / lib / sup / imap.rb
index 42937bd6e04e06b9abd75fd134313c61f6a3b53c..057027b0ba233b79cf1b217d1a0ab534b858dff9 100644 (file)
@@ -3,6 +3,7 @@ require 'net/imap'
 require 'stringio'
 require 'time'
 require 'rmail'
+require 'cgi'
 
 ## fucking imap fucking sucks. what the FUCK kind of committee of
 ## dunces designed this shit.
@@ -62,41 +63,28 @@ class IMAP < Source
     @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
@@ -108,12 +96,14 @@ class IMAP < Source
   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
@@ -142,11 +132,12 @@ class IMAP < Source
 
     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
 
@@ -161,10 +152,19 @@ class IMAP < Source
 
     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
 
@@ -221,10 +221,12 @@ private
         ## 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..."
@@ -263,12 +265,27 @@ private
   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