summaryrefslogtreecommitdiff
path: root/slime-0.11/slime_abstract.py
diff options
context:
space:
mode:
Diffstat (limited to 'slime-0.11/slime_abstract.py')
-rw-r--r--slime-0.11/slime_abstract.py541
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