# IMAP folder support # Copyright (C) 2002-2004 John Goerzen # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from Base import BaseFolder from offlineimap import imaputil, imaplib from offlineimap.ui import UIBase from offlineimap.version import versionstr import rfc822, time, string, random, binascii, re from StringIO import StringIO from copy import copy class IMAPFolder(BaseFolder): def __init__(self, imapserver, name, visiblename, accountname, repository): self.config = imapserver.config self.expunge = repository.getexpunge() self.name = imaputil.dequote(name) self.root = None # imapserver.root self.sep = imapserver.delim self.imapserver = imapserver self.messagelist = None self.visiblename = visiblename self.accountname = accountname self.repository = repository self.randomgenerator = random.Random() BaseFolder.__init__(self) def getaccountname(self): return self.accountname def suggeststhreads(self): return 1 def waitforthread(self): self.imapserver.connectionwait() def getcopyinstancelimit(self): return 'MSGCOPY_' + self.repository.getname() def getvisiblename(self): return self.visiblename def getuidvalidity(self): imapobj = self.imapserver.acquireconnection() try: # Primes untagged_responses imapobj.select(self.getfullname(), readonly = 1) return long(imapobj.untagged_responses['UIDVALIDITY'][0]) finally: self.imapserver.releaseconnection(imapobj) def cachemessagelist(self): imapobj = self.imapserver.acquireconnection() self.messagelist = {} try: # Primes untagged_responses imapobj.select(self.getfullname(), readonly = 1, force = 1) try: # Some mail servers do not return an EXISTS response if # the folder is empty. maxmsgid = long(imapobj.untagged_responses['EXISTS'][0]) except KeyError: return if maxmsgid < 1: # No messages; return return # Now, get the flags and UIDs for these. # We could conceivably get rid of maxmsgid and just say # '1:*' here. response = imapobj.fetch('1:%d' % maxmsgid, '(FLAGS UID)')[1] finally: self.imapserver.releaseconnection(imapobj) for messagestr in response: # Discard the message number. messagestr = string.split(messagestr, maxsplit = 1)[1] options = imaputil.flags2hash(messagestr) if not options.has_key('UID'): UIBase.getglobalui().warn('No UID in message with options %s' %\ str(options), minor = 1) else: uid = long(options['UID']) flags = imaputil.flagsimap2maildir(options['FLAGS']) self.messagelist[uid] = {'uid': uid, 'flags': flags} def getmessagelist(self): return self.messagelist def getmessage(self, uid): ui = UIBase.getglobalui() imapobj = self.imapserver.acquireconnection() try: imapobj.select(self.getfullname(), readonly = 1) initialresult = imapobj.uid('fetch', '%d' % uid, '(BODY.PEEK[])') ui.debug('imap', 'Returned object from fetching %d: %s' % \ (uid, str(initialresult))) return initialresult[1][0][1].replace("\r\n", "\n") finally: self.imapserver.releaseconnection(imapobj) def getmessageflags(self, uid): return self.messagelist[uid]['flags'] def savemessage_getnewheader(self, content): headername = 'X-OfflineIMAP-%s-' % str(binascii.crc32(content)).replace('-', 'x') headername += binascii.hexlify(self.repository.getname()) + '-' headername += binascii.hexlify(self.getname()) headervalue= '%d-' % long(time.time()) headervalue += str(self.randomgenerator.random()).replace('.', '') headervalue += '-v' + versionstr return (headername, headervalue) def savemessage_addheader(self, content, headername, headervalue): ui = UIBase.getglobalui() ui.debug('imap', 'savemessage_addheader: called to add %s: %s' % (headername, headervalue)) insertionpoint = content.find("\r\n") ui.debug('imap', 'savemessage_addheader: insertionpoint = %d' % insertionpoint) leader = content[0:insertionpoint] ui.debug('imap', 'savemessage_addheader: leader = %s' % repr(leader)) if insertionpoint == 0 or insertionpoint == -1: newline = '' insertionpoint = 0 else: newline = "\r\n" newline += "%s: %s" % (headername, headervalue) ui.debug('imap', 'savemessage_addheader: newline = ' + repr(newline)) trailer = content[insertionpoint:] ui.debug('imap', 'savemessage_addheader: trailer = ' + repr(trailer)) return leader + newline + trailer def savemessage_searchforheader(self, imapobj, headername, headervalue): if imapobj.untagged_responses.has_key('APPENDUID'): return long(imapobj.untagged_responses['APPENDUID'][-1].split(' ')[1]) ui = UIBase.getglobalui() ui.debug('imap', 'savemessage_searchforheader called for %s: %s' % \ (headername, headervalue)) # Now find the UID it got. headervalue = imapobj._quote(headervalue) try: matchinguids = imapobj.uid('search', None, '(HEADER %s %s)' % (headername, headervalue))[1][0] except imapobj.error: # IMAP server doesn't implement search or had a problem. return 0 ui.debug('imap', 'savemessage_searchforheader got initial matchinguids: ' + repr(matchinguids)) matchinguids = matchinguids.split(' ') ui.debug('imap', 'savemessage_searchforheader: matchinguids now ' + \ repr(matchinguids)) if len(matchinguids) != 1 or matchinguids[0] == None: raise ValueError, "While attempting to find UID for message with header %s, got wrong-sized matchinguids of %s" % (headername, str(matchinguids)) matchinguids.sort() return long(matchinguids[0]) def savemessage(self, uid, content, flags): imapobj = self.imapserver.acquireconnection() ui = UIBase.getglobalui() ui.debug('imap', 'savemessage: called') try: try: imapobj.select(self.getfullname()) # Needed for search except imapobj.readonly: ui.msgtoreadonly(self, uid, content, flags) # Return indicating message taken, but no UID assigned. # Fudge it. return 0 # This backend always assigns a new uid, so the uid arg is ignored. # In order to get the new uid, we need to save off the message ID. message = rfc822.Message(StringIO(content)) datetuple = rfc822.parsedate(message.getheader('Date')) # Will be None if missing or not in a valid format. if datetuple == None: datetuple = time.localtime() try: if datetuple[0] < 1981: raise ValueError # This could raise a value error if it's not a valid format. date = imaplib.Time2Internaldate(datetuple) except ValueError: # Argh, sometimes it's a valid format but year is 0102 # or something. Argh. It seems that Time2Internaldate # will rause a ValueError if the year is 0102 but not 1902, # but some IMAP servers nonetheless choke on 1902. date = imaplib.Time2Internaldate(time.localtime()) ui.debug('imap', 'savemessage: using date ' + str(date)) content = re.sub("(?