"""Abstract class definition for mail folders and messages. This module defines two classes, SlimeFolder and SlimeMessage, which define an interface to mail folders and messages, respectively. Classes derived from SlimeFolder and SlimeMessage give access to different kinds of folders, e.g., slime_mh.py defines SlimeFolder_MH and SlimeMessage_MH for MH folders. Other parts of Slime use the interface defined by this module to access folders and messags, without having to care about the details of each folder type. The interface is intended to become generic enough for any kind of folder, including all types of local files, and remote ones as defined by POP and IMAP. It will certainly require refinement. Lars Wirzenius """ import string, time, regsub import StringIO # # Exceptions that may be raised by SlimeFolder and SlimeMessage subclasses. # UnimplementedError = "UnimplementedError" Error = "Error" def notdone(): """Raise hell, because subclass didn't implement all methods.""" raise UnimplementedError, "Subclass didn't implement all methods." class SlimeFolder: """Abstract interface to a mail folder.""" def __init__(self): self._is_open = 0 self._name = None self._messages = [] self._last_new_messages = [] self._last_deleted_messages = [] self._subfolders = [] self._last_new_folders = [] self._last_deleted_folders = [] self._threads = [] self._dirty = 0 def __repr__(self): """Return textual name of subfolder.""" return self._name def get_nice_name(self): """Return a user-friendly representation of the name.""" x = string.split(self._name, "/") return x[-1] def is_clean(self): """Is this folder dirty?""" return not self._dirty def make_dirty(self): """Mark the folder as dirty; commit needs to be done.""" self._dirty = 1 def make_clean(self): """Mark the folder as clean; commit doesn't need to done.""" self._dirty = 0 def open(self): """Open the folder. Operations below require an open folder.""" notdone() def close(self): """Close the folder. This should call msg.uncache_headers for every message.""" notdone() def is_open(self): """Is this folder open?""" return self._is_open def _assert_open(self): """Make sure this folder is open.""" if not self._is_open: raise Error, "Using closed folder " + repr(self) def rescan_subfolders(self): """Check for changes to subfolders. This method sets self._subfolders.""" notdone() def rescan_messages(self): """Check for changes to messages in folder. This method sets self._messages.""" notdone() def list_all_messages(self): """Return list of SlimeMessages for all messages here.""" self._assert_open() return self._messages def add_message(self, file): """Add message in file to folder, return new SlimeMessage. This will change the folder immediately (i.e., doesn't wait until commit).""" notdone() def move_message_here(self, msg): """Move a message from another folder to this folder. This will change the target folder immediately (i.e., doesn't wait until commit), but the message won't be deleted from the other folder until commit.""" self._assert_open() newmsg = self.copy_message_here(msg) msg.delete_self() return newmsg def copy_message_here(self, msg): """Copy a message from another folder to this folder. This will change the folder immediately (i.e., doesn't wait until commit).""" self._assert_open() txt = msg.getfulltext() return self.add_message(StringIO.StringIO(txt)) def list_all_threads(self): """Return the thread forest of a folder.""" self._assert_open() if not self._threads: self.rethread_folder() return self._threads def rethread_folder(self): """Build new thread data structure.""" self._assert_open() children = {} ids = {} roots = [] for msg in self._messages: parent = msg.get_parent_id() date = msg.get_linear_time() subj = string.lower(msg["subject"]) subj = regsub.gsub("^re: *", "", subj) if parent: if children.has_key(parent): children[parent].append(date, subj, msg) else: children[parent] = [(date, subj, msg)] else: roots.append(date, subj, msg) id = msg["message-id"] if id: ids[id] = msg for id in children.keys(): children[id].sort() if not ids.has_key(id): for date, subj, msg in children[id]: roots.append(date, subj, msg) subjects = {} for date, subj, msg in roots: subjects[subj] = (date, subj, msg) aux_roots = [] for subj in subjects.keys(): aux_roots.append(subjects[subj]) aux_roots.sort() real_roots = [] for date, subj, msg in aux_roots: list = [] for date2, subj2, msg2 in roots: if subj == subj2: list.append(date2, msg2) list.sort() real_roots = real_roots + list self._threads = [] for date, msg in real_roots: self.append_thread(0, msg, children) def append_thread(self, level, msg, children): """Append msg to self._threads, and then all its children.""" self._threads.append(level, msg) id = msg["message-id"] if children.has_key(id): level = level + 1 for date, subj, child in children[id]: self.append_thread(level, child, children) def get_msg_level(self, msg): """Return indentation level of message in thread display.""" self._assert_open() for i, m in self._threads: if m == msg: return i return 0 def list_all_subfolders(self): """Return list of SlimeFolder objects for subfolders.""" self._assert_open() return self._subfolders def add_subfolder(self, name): """Add or create a new subfolder named `name'.""" notdone() def move_subfolder_here(self, subfolder_object): """Move the subfolder to this folder.""" # xxx placeholder notdone() def delete_self(self): """Remove this folder.""" # xxx place holder notdone() def rename_self(self, new_name): """Rename this folder to have a new name.""" # xxx place holder notdone() def commit_changes(self): """Commit all changes to folder and its messages.""" notdone() _interned_strings = {} def _intern_string(str): """Make sure there is only one instance of a given string.""" try: str = _interned_strings[str] except KeyError: _interned_strings[str] = str return str interesting_headers = ["from", "subject", "to", "cc", "date", "status", "in-reply-to", "message-id", "references"] class SlimeMessage: """Abstract interface to a single e-mail message. Status: Every message has a status, which is a string consisting of single-character flags (graphical flags only): N = new message D = message should be deleted at commit time (still searching for good info on the chars) """ # Note that some operations may require changes to the folder # the message resides in. Changes are commited only after the # folder is commited. def __init__(self, folder): """Initialize the message. Derived classes must call this.""" self._folder = folder self._msg = None self._status = "" self._cached_headers = {} self._cached_date = None self._open_count = 0 def _open(self): """Open the message. Done automatically in background.""" notdone() def _close(self): """Close the message. Done automatically in background.""" notdone() def _assert_open(self): """Assert that the folder this message is in is open.""" self._folder._assert_open() def uncache_headers(self): """Forget about cached headers.""" self._assert_open() self._cached_headers = {} def get_status(self): """Return status of this message (read/unread, etc).""" self._assert_open() return self._status def _clean_status(self, status): cleaned = "" for c in "ND": if c in status: cleaned = cleaned + c return cleaned def set_status(self, new_status): """Change the status of this message, don't mark folder dirty.""" self._assert_open() self._status = self._clean_status(new_status) def has_status(self, statuschar): """Does the message status have statuschar?""" self._assert_open() return string.find(self._status, statuschar) >= 0 def give_status(self, statuschar): """Add statuschar to status of message.""" self._assert_open() cleaned = self._clean_status(statuschar) if statuschar and string.find(self._status, cleaned) == -1: self._folder.make_dirty() self._status = self._status + cleaned def remove_status(self, statuschar): """Remove statuschar from status of message.""" self._assert_open() if statuschar: i = string.find(self._status, statuschar) if i >= 0: self._folder.make_dirty() self._status = self._status[:i] + \ self._status[i+1:] def change_text(self, file): """Change the contents of the message to that in file. Note that not all folder formats support this functionality easily. It is needed for the draft folder only, and that one is always an MH folder, so it gives an error for other folder types.""" raise Error, "change_text is not supported for this folder" def delete_self(self): """Delete this message from the folder it is in.""" self._assert_open() self.give_status("D") self._folder.make_dirty() def undelete_self(self): """Undelete this message from the folder it is in.""" self._assert_open() self.remove_status("D") def _find_ids(self, str): """Return list of Message-ID's in a string.""" str = string.join(string.split(str, "\n"), " ") str = string.join(string.split(str, "\t"), " ") str = regsub.sub("^[^<]*", "", str) str = regsub.gsub(">[^<]*<", "> <", str) str = regsub.sub(">[^<]*$", ">", str) str = regsub.sub(" *", " ", str) return string.split(str) def get_parent_id(self): """Return the Message-ID of the message this is a reply to.""" self._assert_open() irt = self["in-reply-to"] if irt: ids = self._find_ids(irt) if ids: return ids[0] refs = self["references"] if refs: ids = self._find_ids(refs) if ids: return ids[-1] return "" def get_linear_time(self): """Return the Date: header converted to seconds since epoch.""" self._assert_open() date = self.getdate("date") if date is None: return 0 return time.mktime(date) def getfulltext(self): """Return full text of message.""" return self.getheadertext() + "\n" + self.getbodytext(0) # # The following are the interface provided together by # rfc822.Message, mimetools.Message, and mhlib.Message. # def getallmatchingheaders(self, name): self._open() x = self._msg.getallmatchingheaders(name) self._close() return x def getfirstmatchingheader(self, name): self._open() x = self._msg.getfirstmatchingheader(name) self._close() return x def getrawheader(self, name): self._open() x = self._msg.getrawheader(name) self._close() return x def getheader(self, name): self._open() x = self._msg.getheader(name) self._close() return x def getaddr(self, name): self._open() x = self._msg.getaddr(name) self._close() return x def getaddrlist(self, name): self._open() x = self._msg.getaddrlist(name) self._close() return x def getdate(self, name): if string.lower(name) == "date": if not self._cached_date: self._open() self._cache_headers() self._close() return self._cached_date else: self._open() date = self._msg.getdate(name) self._close() return date def __len__(self): self._open() x = self._msg.__len__() self._close() return x def _cache_headers(self): self._assert_open() for h in interesting_headers: try: x = self._msg.__getitem__(h) except KeyError: x = "" x = _intern_string(x) self._cached_headers[h] = x self._cached_date = self._msg.getdate("date") if not self._cached_date: self._cached_date = (1970,1,1,0,0,0,3,1,0) def __getitem__(self, name): # Note: we assume it is benign to return "" for missing header. name = string.lower(name) if self._cached_headers.has_key(name): return self._cached_headers[name] self._open() try: x = self._msg.__getitem__(name) except KeyError: x = "" x = _intern_string(x) self._cached_headers[name] = x self._close() return x def has_key(self, name): self._open() x = self._msg.has_key(name) self._close() return x def keys(self): self._open() x = self._msg.keys() self._close() return x def values(self): self._open() x = self._msg.values() self._close() return x def items(self): self._open() x = self._msg.items() self._close() return x def getplist(self): self._open() x = self._msg.getplist() self._close() return x def getparam(self, name): self._open() x = self._msg.getparam(name) self._close() return x def getparamnames(self): self._open() x = self._msg.getparamnames() self._close() return x def getencoding(self): self._open() x = self._msg.getencoding() self._close() return x def gettype(self): self._open() x = self._msg.gettype() self._close() return x def getmaintype(self): self._open() x = self._msg.getmaintype() self._close() return x def getsubtype(self): self._open() x = self._msg.getsubtype() self._close() return x def getheadertext(self, pred = None): self._open() x = self._msg.getheadertext(pred) self._close() return x def getbodytext(self, decode = 1): self._open() x = self._msg.getbodytext(decode) self._close() return x def getbodyparts(self): self._open() x = self._msg.getbodyparts() self._close() return x def getbody(self): self._open() x = self._msg.getbody() self._close() return x