diff options
Diffstat (limited to 'slime-0.11/slime_abstract.py')
-rw-r--r-- | slime-0.11/slime_abstract.py | 541 |
1 files changed, 541 insertions, 0 deletions
diff --git a/slime-0.11/slime_abstract.py b/slime-0.11/slime_abstract.py new file mode 100644 index 0000000..4de3b82 --- /dev/null +++ b/slime-0.11/slime_abstract.py @@ -0,0 +1,541 @@ +"""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 <liw@iki.fi>""" + + +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 |