"""MH interface -- purely object-oriented (well, almost)
mh = mhlib.MH() # use default mailbox directory and profile
mh = mhlib.MH(mailbox) # override mailbox location (default from profile)
mh = mhlib.MH(mailbox, profile) # override mailbox and profile
mh.error(format, ...) # print error message -- can be overridden
s = mh.getprofile(key) # profile entry (None if not set)
path = mh.getpath() # mailbox pathname
name = mh.getcontext() # name of current folder
mh.setcontext(name) # set name of current folder
list = mh.listfolders() # names of top-level folders
list = mh.listallfolders() # names of all folders, including subfolders
list = mh.listsubfolders(name) # direct subfolders of given folder
list = mh.listallsubfolders(name) # all subfolders of given folder
mh.makefolder(name) # create new folder
mh.deletefolder(name) # delete folder -- must have no subfolders
f = mh.openfolder(name) # new open folder object
f.error(format, ...) # same as mh.error(format, ...)
path = f.getfullname() # folder's full pathname
path = f.getsequencesfilename() # full pathname of folder's sequences file
path = f.getmessagefilename(n) # full pathname of message n in folder
list = f.listmessages() # list of messages in folder (as numbers)
n = f.getcurrent() # get current message
f.setcurrent(n) # set current message
list = f.parsesequence(seq) # parse msgs syntax into list of messages
n = f.getlast() # get last message (0 if no messagse)
f.setlast(n) # set last message (internal use only)
dict = f.getsequences() # dictionary of sequences in folder {name: list}
f.putsequences(dict) # write sequences back to folder
f.createmessage(n, fp) # add message from file f as number n
f.removemessages(list) # remove messages in list from folder
f.refilemessages(list, tofolder) # move messages in list to other folder
f.movemessage(n, tofolder, ton) # move one message to a given destination
f.copymessage(n, tofolder, ton) # copy one message to a given destination
m = f.openmessage(n) # new open message object (costs a file descriptor)
m is a derived class of mimetools.Message(rfc822.Message), with:
s = m.getheadertext() # text of message's headers
s = m.getheadertext(pred) # text of message's headers, filtered by pred
s = m.getbodytext() # text of message's body, decoded
s = m.getbodytext(0) # text of message's body, not decoded
from warnings import warnpy3k
warnpy3k("the mhlib module has been removed in Python 3.0; use the mailbox "
"module instead", stacklevel=2)
# XXX To do, functionality:
# XXX To do, organization:
# - move IntSet to separate file
# - move most Message functionality to module mimetools
MH_PROFILE = '~/.mh_profile'
MH_SEQUENCES = '.mh_sequences'
from bisect import bisect
__all__ = ["MH","Error","Folder","Message"]
"""Class representing a particular collection of folders.
Optional constructor arguments are the pathname for the directory
containing the collection, and the MH profile to use.
If either is omitted or empty a default is used; the default
directory is taken from the MH profile if it is specified there."""
def __init__(self, path = None, profile = None):
if profile is None: profile = MH_PROFILE
self.profile = os.path.expanduser(profile)
if path is None: path = self.getprofile('Path')
if not os.path.isabs(path) and path[0] != '~':
path = os.path.join('~', path)
path = os.path.expanduser(path)
if not os.path.isdir(path): raise Error, 'MH() path not found'
"""String representation."""
return 'MH(%r, %r)' % (self.path, self.profile)
def error(self, msg, *args):
"""Routine to print an error. May be overridden by a derived class."""
sys.stderr.write('MH error: %s\n' % (msg % args))
def getprofile(self, key):
"""Return a profile entry, None if not found."""
return pickline(self.profile, key)
"""Return the path (the name of the collection's directory)."""
"""Return the name of the current folder."""
context = pickline(os.path.join(self.getpath(), 'context'),
if not context: context = 'inbox'
def setcontext(self, context):
"""Set the name of the current folder."""
fn = os.path.join(self.getpath(), 'context')
f.write("Current-Folder: %s\n" % context)
"""Return the names of the top-level folders."""
for name in os.listdir(path):
fullname = os.path.join(path, name)
if os.path.isdir(fullname):
def listsubfolders(self, name):
"""Return the names of the subfolders in a given folder
(prefixed with the given folder name)."""
fullname = os.path.join(self.path, name)
# Get the link count so we can avoid listing folders
# that have no subfolders.
nlinks = os.stat(fullname).st_nlink
subnames = os.listdir(fullname)
fullsubname = os.path.join(fullname, subname)
if os.path.isdir(fullsubname):
name_subname = os.path.join(name, subname)
subfolders.append(name_subname)
# Stop looking for subfolders when
def listallfolders(self):
"""Return the names of all folders and subfolders, recursively."""
return self.listallsubfolders('')
def listallsubfolders(self, name):
"""Return the names of subfolders in a given folder, recursively."""
fullname = os.path.join(self.path, name)
# Get the link count so we can avoid listing folders
# that have no subfolders.
nlinks = os.stat(fullname).st_nlink
subnames = os.listdir(fullname)
if subname[0] == ',' or isnumeric(subname): continue
fullsubname = os.path.join(fullname, subname)
if os.path.isdir(fullsubname):
name_subname = os.path.join(name, subname)
subfolders.append(name_subname)
if not os.path.islink(fullsubname):
subsubfolders = self.listallsubfolders(
subfolders = subfolders + subsubfolders
# Stop looking for subfolders when
def openfolder(self, name):
"""Return a new Folder object for the named folder."""
return Folder(self, name)
def makefolder(self, name):
"""Create a new folder (or raise os.error if it cannot be created)."""
protect = pickline(self.profile, 'Folder-Protect')
if protect and isnumeric(protect):
os.mkdir(os.path.join(self.getpath(), name), mode)
def deletefolder(self, name):
"""Delete a folder. This removes files in the folder but not
subdirectories. Raise os.error if deleting the folder itself fails."""
fullname = os.path.join(self.getpath(), name)
for subname in os.listdir(fullname):
fullsubname = os.path.join(fullname, subname)
self.error('%s not deleted, continuing...' %
numericprog = re.compile('^[1-9][0-9]*$')
return numericprog.match(str) is not None
"""Class representing a particular folder."""
def __init__(self, mh, name):
if not os.path.isdir(self.getfullname()):
raise Error, 'no folder %s' % name
"""String representation."""
return 'Folder(%r, %r)' % (self.mh, self.name)
"""Error message handler."""
"""Return the full pathname of the folder."""
return os.path.join(self.mh.path, self.name)
def getsequencesfilename(self):
"""Return the full pathname of the folder's sequences file."""
return os.path.join(self.getfullname(), MH_SEQUENCES)
def getmessagefilename(self, n):
"""Return the full pathname of a message in the folder."""
return os.path.join(self.getfullname(), str(n))
def listsubfolders(self):
"""Return list of direct subfolders."""
return self.mh.listsubfolders(self.name)
def listallsubfolders(self):
"""Return list of all subfolders."""
return self.mh.listallsubfolders(self.name)
"""Return the list of messages currently present in the folder.
As a side effect, set self.last to the last message (or 0)."""
match = numericprog.match
for name in os.listdir(self.getfullname()):
messages = map(int, messages)
"""Return the set of sequences for the folder."""
fullname = self.getsequencesfilename()
self.error('bad sequence in %s: %s' %
(fullname, line.strip()))
value = IntSet(fields[1].strip(), ' ').tolist()
def putsequences(self, sequences):
"""Write the set of sequences back to the folder."""
fullname = self.getsequencesfilename()
for key, seq in sequences.iteritems():
if not f: f = open(fullname, 'w')
f.write('%s: %s\n' % (key, s.tostring()))
"""Return the current message. Raise Error when there is none."""
seqs = self.getsequences()
except (ValueError, KeyError):
raise Error, "no cur message"
"""Set the current message."""
updateline(self.getsequencesfilename(), 'cur', str(n), 0)
def parsesequence(self, seq):
"""Parse an MH sequence specification into a message list.
Attempt to mimic mh-sequence(5) as close as possible.
Also attempt to mimic observed behavior regarding which
conditions cause which error messages."""
# XXX Still not complete (see mh-format(5)).
# - 'prev', 'next' as count
# - Sequence-Negation option
all = self.listmessages()
# Observed behavior: test for empty folder is done first
raise Error, "no messages in %s" % self.name
# Common case first: all is frequently the default
# Test for X:Y before X-Y because 'seq:-n' matches both
head, dir, tail = seq[:i], '', seq[i+1:]
dir, tail = tail[:1], tail[1:]
raise Error, "bad message list %s" % seq
except (ValueError, OverflowError):
# Can't use sys.maxint because of i+count below
anchor = self._parseindex(head, all)
seqs = self.getsequences()
msg = "bad message list %s" % seq
raise Error, msg, sys.exc_info()[2]
raise Error, "sequence %s empty" % head
if head in ('prev', 'last'):
return all[max(0, i-count):i]
i = bisect(all, anchor-1)
begin = self._parseindex(seq[:i], all)
end = self._parseindex(seq[i+1:], all)
raise Error, "bad message list %s" % seq
# Neither X:Y nor X-Y; must be a number or a (pseudo-)sequence
n = self._parseindex(seq, all)
seqs = self.getsequences()
msg = "bad message list %s" % seq
raise Error, "message %d doesn't exist" % n
raise Error, "no %s message" % seq
def _parseindex(self, seq, all):
"""Internal: parse a message number (or cur, first, etc.)."""
except (OverflowError, ValueError):
raise Error, "no next message"
raise Error, "no prev message"
raise Error, "no prev message"
def openmessage(self, n):
"""Open a message -- returns a Message object."""
def removemessages(self, list):
"""Remove one or more messages -- may raise os.error."""
path = self.getmessagefilename(n)
commapath = self.getmessagefilename(',' + str(n))
os.rename(path, commapath)
self.removefromallsequences(deleted)
raise os.error, errors[0]
raise os.error, ('multiple errors:', errors)
def refilemessages(self, list, tofolder, keepsequences=0):
"""Refile one or more messages -- may raise os.error.
'tofolder' is an open folder object."""
ton = tofolder.getlast() + 1
path = self.getmessagefilename(n)
topath = tofolder.getmessagefilename(ton)