#!/usr/bin/env python # TODO readonly flag import imaplib import getpass import lisp from cStringIO import StringIO import codecs import sys import errno import re import exceptions import rfc822 import time import gmtime import timeout import imapcodec def from_internaldate(date): try: #print >>sys.stderr, "ndate", date date = rfc822.parsedate_tz(date) #print >>sys.stderr, "xdate", date, type(date) date = gmtime.mkgmtime(date) #print >>sys.stderr, "okdate", date except: date = 0 # None # (1970, 1, 1, 18, 16, 22, 0, 1, 0) return date def pair(items): """ >>> pair([1,2,3,4]) [(1,2),(3,4)] can still be optimized a LOT. """ result = [] while len(items) > 0: key = items[0] #print "key", key value = items[1] items = items[2:] result.append((key, value)) return result def lisp_escaped_quoted_string(s): return "\"%s\"" % s.replace("\"", "\\\"") def parse_attributes(text): stream = StringIO() stream.write(text) stream.seek(0) premonition_stream_1 = lisp.premonition_stream(stream) items = lisp.parse(premonition_stream_1) return items def parse_flags(text): # text = '1848 (FLAGS (\Deleted) INTERNALDATE \"25-May-2007 17:08:58 +0200\" RFC822 )' attributes = parse_attributes(text) #>>> attributes[1] #[\FLAGS, [\Deleted], \INTERNALDATE, '25-May-2007 17:08:58 +0200', \RFC822] #>>> attributes[2] IndexError: list index out of range items = dict(pair(attributes[1])) return items def parse_items(all_items, just_one_item_p = False): #re_items = re.compile(r"^([0-9][0-9]*) (.*)$") #match = re_items.match(items) #assert(match) #print >>sys.stderr,"group(1)", match.group(1), uid, type(uid) #assert(int(match.group(1)) == int(uid)) #items = match.group(2) stream = StringIO() # all_items arbitrary item, literal, ... parts for items in all_items: if isinstance(items, str) and items.startswith("Some messages in the mailbox had previously been expunged and could not be returned"): # Some messages in the mailbox had previously been expunged and could not be returned. <type 'str'> pass #raise ParseError() if items is None: # ??? # ('46 (ENVELOPE ("Wed, 11 Apr 2007 13:08:38 +0200" {35}', 'Neues "Local-Index-Service" Projekt') <type 'tuple'> print >>sys.stderr, "error: one item is None: all_items = ", all_items raise ParseError() if isinstance(items, tuple): # has literal parts #print >>sys.stderr, "tuple", items coalesced_result = "" is_literal = False for tuple_item in items: if not is_literal: i = tuple_item.rfind("{") if i > -1: tuple_item = tuple_item[ : i] is_literal = True else: # is_literal is_literal = False tuple_item = lisp_escaped_quoted_string(tuple_item) stream.write(tuple_item) else: stream.write(items) stream.seek(0) #import pickle #pickle.dump(stream.getvalue(), open('/tmp/save.p','w')) # FIXME #print >>sys.stderr, ">>>", stream.getvalue(), "<<<" premonition_stream_1 = lisp.premonition_stream(stream) #assert(premonition_stream_1.peek() == "(") #premonition_stream_1.consume() items = lisp.parse(premonition_stream_1) # items = [1, [\UID, 0807, \FLAGS, [\Seen], ...], 2, [\UID, 0808, \FLAGS, [\Seen], ...]] items = pair(items) # items = [(1, [\UID, 0807, \FLAGS, [\Seen], ...]), (2, [\UID, 0808, \FLAGS, [\Seen], ...], ...)] """ (ENVELOPE ("Wed, 10 Jan 2007 18:16:22 +0200" "subject" ( ("Long, Name" NIL "Name.Long" "fabalabs.org") ) ( ("Long, Name" NIL "Name.Long" "fabalabs.org") ) ( ("Long, Name" NIL "Name.Long" "fabalabs.org") ) ( ("Milosavljevic, Danny" NIL "Danny.Milosavljevic" "fabasoft.com") ("Pesendorfer, Klaus" NIL "Klaus.Pesendorfer" "fabasoft.com") ("Lechner, Jakob" NIL "Jakob.Lechner" "fabasoft.com") ) NIL NIL NIL "<C2DF4DAA34CD0644A089964F6B55BBAD05387129@EVS-FABASOFT.fabagl.fabasoft.com>")) """ #items [\UID, \10525, \ENVELOPE, ['Wed, 10 Jan 2007 18:16:22 +0200', 'subject' .. #print >>sys.stderr, "items", items # [0] #assert(items[0][0] in ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]) #items.next() if not just_one_item_p: items = items[1 : ] for number, item in items: #print >>sys.stderr, "item", item #print >>sys.stderr, "yield", dict(pair(item)) # FIXME yield dict(pair(item)) def unescape_imap_name(text): return text.decode("imap4-utf-7") def escape_imap_name(text): if not isinstance(text, unicode): text = text.decode("utf-8") return text.encode("imap4-utf-7") def unescape_folder(folder): assert(folder.startswith("(")) parts = folder.split(")", 1) # TODO can be nested. # "NIL" assert(len(parts) == 2) flags = parts[0][1:] rest = parts[1] """ input: (\Marked \HasNoChildren) "/" "Public Folders/Fabasoft Consulting" <type 'str'> (\Marked \HasChildren) "/" "Public Folders/Fabasoft Besprechungen" <type 'str'> (\HasNoChildren) "/" "Public Folders/Fabasoft Training" <type 'str'> (\Marked \HasNoChildren) "/" "Public Folders/Fabasoft Linux Schedule" <type 'str'> (\HasNoChildren) "/" "Public Folders/Firmenautos Wien" <type 'str'> (\Marked \HasNoChildren) "/" "Public Folders/Linux" <type 'str'> result: yield (flags, separator, name) """ def decode_folders(folders): for folder_text in folders: #print "folder_text", folder_text stream = StringIO() stream.write(folder_text) # (\HasNoChildren) "/" "Gel&APY-schte Objekte" <type 'str'> stream.seek(0) premonition_stream_1 = lisp.premonition_stream(stream) assert(premonition_stream_1.peek() == "(") premonition_stream_1.consume() flags = lisp.parse(premonition_stream_1) assert(premonition_stream_1.peek() == " ") premonition_stream_1.consume() separator = lisp.parse_string_literal(premonition_stream_1) assert(separator == "/") # limitation assert(premonition_stream_1.peek() == " ") premonition_stream_1.consume() if premonition_stream_1.peek() == "\"": name = lisp.parse_string_literal(premonition_stream_1) else: # weird. name = premonition_stream_1.consume_rest() name = "".join(unescape_imap_name(name)) #flags, separator, name = data yield flags, separator, name #else: # print >>sys.stderr, "warning", "ignored line", data class ParseError(exceptions.Exception): pass class nice_IMAP(object): def __init__(self, use_SSL, *args, **kwargs): if use_SSL: self.connection = imaplib.IMAP4_SSL(*args, **kwargs) else: self.connection = imaplib.IMAP4(*args, **kwargs) self.current_directory = None self.current_directory_readonly = True self.current_uid_validity = None def flat_list(self, base): state, folders = self.connection.list(base) assert(state == "OK") if folders == [ None ]: # error? folders = [] #print "F", folders folders = [x for x in decode_folders(folders)] for flags, separator, name in folders: if name.startswith(base): rest_name = name[len(base):] if rest_name != separator and base != "": continue if rest_name[1:].find("/") > -1: continue yield name, flags # , separator def flat_list_simple(self, base): for name, flags in self.flat_list(base): yield name def get_message_uids(self): if self.current_directory_contents is not None: oldest_items_cachetime = min([items_cachetime for items_cachetime, items in self.current_directory_contents.values()]) if time.time() - oldest_items_cachetime > timeout.timeout: # items too old. self.current_directory_contents = None #self.current_directory_contents = None # instead if self.current_directory_contents is not None: #print >>sys.stderr, "from cache", len(self.current_directory_contents) return self.current_directory_contents.keys() #[x[lisp.symbol("UID")] for x in self.current_directory_contents] else: print >>sys.stderr, "get_message_uids: live update" status, message_set = self.connection.uid("SEARCH", "ALL") # charset, criteria assert(status == "OK") message_set = message_set[0].split(" ") self.cache_directory_attributes() # so that it doesn't fetch live all the time if len(message_set) == 1 and message_set[0] == "": return [] return message_set def get_mail_body(self, uid): import errno import exceptions status, items = self.connection.uid("FETCH", uid, '(FLAGS INTERNALDATE RFC822)') #print >>sys.stderr, "items", items text = items[0] number_flags_date_syntax, body = text #print >>sys.stderr, ">>>%s<<<" % number_flags_date_syntax if number_flags_date_syntax.find(r"\Deleted") > -1: # speedup # "1848 (FLAGS (\Seen \Deleted) INTERNALDATE "25-May-2007 17:08:58 +0200" RFC822 {4118669}" flags_text = number_flags_date_syntax[ : number_flags_date_syntax.rfind("{")] + " \"\")" # replace body by something small print >>sys.stderr, "flags_text", flags_text items = parse_flags(flags_text) flags = items[lisp.symbol("FLAGS")] if lisp.symbol("Deleted") in flags: raise exceptions.IOError(errno.ENOENT, "No such file or directory 'uid#%s'" % uid) # ('47 (FLAGS (\\Seen) INTERNALDATE "11-Apr-2007 13:11:23 +0200" RFC822 {12482}', #'X-MimeOLE: Produced By Microsoft Exchange V6.5\r\nReceived: by FABAMAIL.fabagl.fabasoft.com \r\n\tid <01C77C2A.23ACB406@FABAMAIL.fabagl.fabasoft.com>; #print "---" #for item in items: return body def login(self, *args, **kwargs): self.connection.login(*args, **kwargs) def get_mail_envelope(self, uid): # SLOOOW #print >>sys.stderr, "get_mail_envelope UID", uid items = None if self.current_directory_contents is not None and uid in self.current_directory_contents: items_cachetime, items = self.current_directory_contents[uid] # [x for x in self.current_directory_contents if x[lisp.symbol("UID")] == uid] if time.time() - items_cachetime >= timeout.timeout: # too old items = None #items = None # FIXME for i in range(3): if items is None or items == []: # FIXME the latter is weird... and not necessary anymore status, items = self.connection.uid("FETCH", uid, '(UID ENVELOPE RFC822.SIZE FLAGS)') #f = file("/tmp/uid-%s" % uid, "w") #import pickle #pickle.dump(items, f) # FIXME FIXME #f.close() items = [x for x in parse_items(items, True)] if items == []: # weird, give it some time time.sleep(1) else: items = items[0] break if items == []: # not found raise exceptions.LookupError("message with UID %s not found in mailbox \"%s\"" % (uid, self.current_directory)) #print >>sys.stderr, "items ENV", items uid = items[lisp.symbol("UID")] size = items[lisp.symbol("RFC822.SIZE")] envelope = items[lisp.symbol("ENVELOPE")] flags = items[lisp.symbol("FLAGS")] """[ 'Wed, 10 Jan 2007 18:16:22 +0200', 'Some subject', [['Long, Name', [], 'Silvio.Ziegelwanger', 'fabalabs.org']], [['Long, Name', [], 'Long.Name', 'fabalabs.org']], [['Long, Name', [], 'Long.Name', 'fabalabs.org']], [['Milosavljevic, Danny', [], 'Danny.Milosavljevic', 'fabasoft.com'], ['Pesendorfer, Klaus', [], 'Klaus.Pesendorfer', 'fabasoft.com']], [], [], [], '<C2DF4DAA34CD0644A089964F6B55BBAD05387129@EVS-FABASOFT.fabagl.fabasoft.com>' ]""" date, subject, from_, sender, reply_to, to_, cc, bcc, in_reply_to, message_id = envelope[:10] date = from_internaldate(date) return uid, flags, size, date, subject, from_, sender, reply_to, to_, cc, bcc, in_reply_to, message_id def select(self, mailbox = u"INBOX", readonly = None): if self.current_directory != mailbox or self.current_directory_readonly != readonly: #print >>sys.stderr, "really selecting", mailbox if mailbox != "": mailbox = escape_imap_name(mailbox) status, foo = self.connection.select(mailbox, readonly = readonly) else: status, foo = "OK", "0" # imaplib.IMAP4.select(self, readonly = readonly) if status == "OK": self.current_directory = mailbox self.current_directory_readonly = readonly try: uidvalidity_raw = self.connection.response("UIDVALIDITY") self.current_uid_validity = int(uidvalidity_raw[1][0]) except: self.current_uid_validity = None #print >>sys.stderr, "uidvalidity_raw weird", uidvalidity_raw # ("UIDVALIDITY", [None]) try: self.cache_directory_attributes() except exceptions.Exception, e: print >>sys.stderr, "niceimap.cache_directory_attributes() failed because", e raise return status, foo else: return "OK", "TODO" def cache_directory_attributes(self): self.current_directory_contents = {} # uid -> [...] status, items = self.connection.uid("FETCH", "1:1000000000", "(UID FLAGS INTERNALDATE ENVELOPE RFC822.SIZE)") if status == "OK": if items == [None]: items = [] # ??? #print >>sys.stderr, "cache_directory_attributes len items", len(items) self.current_directory_contents = {} for item in parse_items(items): uid = item[lisp.symbol("UID")] items_cachetime = time.time() self.current_directory_contents[uid] = items_cachetime, item else: self.current_directory_contents = None def chdir(self, path): status, foo = self.connection.select(path, readonly = True) if status != "OK": print exceptions.OSError(2, "No such file or directory: '%s'" % "a") # TODO more def unlink_message(self, uid): status, foo = self.connection.uid("STORE", uid, "+FLAGS", r"(\Deleted)") if status != "OK": raise exceptions.OSError(2, "No such file or directory: '%s'" % "a") # TODO more if __name__ == "__main__": assert(parse_flags("1848 (FLAGS (\Deleted) INTERNALDATE \"25-May-2007 17:08:58 +0200\" RFC822 \"\")") == { lisp.symbol("FLAGS"): [lisp.symbol("Deleted")], lisp.symbol("INTERNALDATE"): "25-May-2007 17:08:58 +0200", lisp.symbol("RFC822"): "" })