1 """MH interface -- purely object-oriented (well, almost)
7 mh = mhlib.MH() # use default mailbox directory and profile
8 mh = mhlib.MH(mailbox) # override mailbox location (default from profile)
9 mh = mhlib.MH(mailbox, profile) # override mailbox and profile
11 mh.error(format, ...) # print error message -- can be overridden
12 s = mh.getprofile(key) # profile entry (None if not set)
13 path = mh.getpath() # mailbox pathname
14 name = mh.getcontext() # name of current folder
15 mh.setcontext(name) # set name of current folder
17 list = mh.listfolders() # names of top-level folders
18 list = mh.listallfolders() # names of all folders, including subfolders
19 list = mh.listsubfolders(name) # direct subfolders of given folder
20 list = mh.listallsubfolders(name) # all subfolders of given folder
22 mh.makefolder(name) # create new folder
23 mh.deletefolder(name) # delete folder -- must have no subfolders
25 f = mh.openfolder(name) # new open folder object
27 f.error(format, ...) # same as mh.error(format, ...)
28 path = f.getfullname() # folder's full pathname
29 path = f.getsequencesfilename() # full pathname of folder's sequences file
30 path = f.getmessagefilename(n) # full pathname of message n in folder
32 list = f.listmessages() # list of messages in folder (as numbers)
33 n = f.getcurrent() # get current message
34 f.setcurrent(n) # set current message
35 list = f.parsesequence(seq) # parse msgs syntax into list of messages
36 n = f.getlast() # get last message (0 if no messagse)
37 f.setlast(n) # set last message (internal use only)
39 dict = f.getsequences() # dictionary of sequences in folder {name: list}
40 f.putsequences(dict) # write sequences back to folder
42 f.createmessage(n, fp) # add message from file f as number n
43 f.removemessages(list) # remove messages in list from folder
44 f.refilemessages(list, tofolder) # move messages in list to other folder
45 f.movemessage(n, tofolder, ton) # move one message to a given destination
46 f.copymessage(n, tofolder, ton) # copy one message to a given destination
48 m = f.openmessage(n) # new open message object (costs a file descriptor)
49 m is a derived class of mimetools.Message(rfc822.Message), with:
50 s = m.getheadertext() # text of message's headers
51 s = m.getheadertext(pred) # text of message's headers, filtered by pred
52 s = m.getbodytext() # text of message's body, decoded
53 s = m.getbodytext(0) # text of message's body, not decoded
67 MH_PROFILE =
'~/.mh_profile'
69 MH_SEQUENCES =
'.mh_sequences'
77 from stat
import ST_NLINK
82 from bisect
import bisect
84 __all__ = [
"MH",
"Error",
"Folder",
"Message"]
93 """Class representing a particular collection of folders.
94 Optional constructor arguments are the pathname for the directory
95 containing the collection, and the MH profile to use.
96 If either is omitted or empty a default is used; the default
97 directory is taken from the MH profile if it is specified there."""
99 def __init__(self, path = None, profile = None):
101 if not profile: profile = MH_PROFILE
104 if not path: path = PATH
105 if not os.path.isabs(path)
and path[0] !=
'~':
106 path = os.path.join(
'~', path)
107 path = os.path.expanduser(path)
108 if not os.path.isdir(path):
raise Error,
'MH() path not found'
112 """String representation."""
113 return 'MH(%s, %s)' % (`self.
path`, `self.
profile`)
116 """Routine to print an error. May be overridden by a derived class."""
117 sys.stderr.write(
'MH error: %s\n' % (msg % args))
120 """Return a profile entry, None if not found."""
124 """Return the path (the name of the collection's directory)."""
128 """Return the name of the current folder."""
131 if not context: context =
'inbox'
135 """Set the name of the current folder."""
136 fn = os.path.join(self.
getpath(),
'context')
138 f.write(
"Current-Folder: %s\n" % context)
142 """Return the names of the top-level folders."""
145 for name
in os.listdir(path):
146 fullname = os.path.join(path, name)
147 if os.path.isdir(fullname):
153 """Return the names of the subfolders in a given folder
154 (prefixed with the given folder name)."""
155 fullname = os.path.join(self.
path, name)
158 st = os.stat(fullname)
159 nlinks = st[ST_NLINK]
163 subnames = os.listdir(fullname)
164 for subname
in subnames:
165 fullsubname = os.path.join(fullname, subname)
166 if os.path.isdir(fullsubname):
167 name_subname = os.path.join(name, subname)
168 subfolders.append(name_subname)
178 """Return the names of all folders and subfolders, recursively."""
182 """Return the names of subfolders in a given folder, recursively."""
183 fullname = os.path.join(self.
path, name)
186 st = os.stat(fullname)
187 nlinks = st[ST_NLINK]
191 subnames = os.listdir(fullname)
192 for subname
in subnames:
193 if subname[0] ==
',' or isnumeric(subname):
continue
194 fullsubname = os.path.join(fullname, subname)
195 if os.path.isdir(fullsubname):
196 name_subname = os.path.join(name, subname)
197 subfolders.append(name_subname)
198 if not os.path.islink(fullsubname):
201 subfolders = subfolders + subsubfolders
211 """Return a new Folder object for the named folder."""
215 """Create a new folder (or raise os.error if it cannot be created)."""
218 mode = int(protect, 8)
220 mode = FOLDER_PROTECT
221 os.mkdir(os.path.join(self.
getpath(), name), mode)
224 """Delete a folder. This removes files in the folder but not
225 subdirectories. Raise os.error if deleting the folder itself fails."""
226 fullname = os.path.join(self.
getpath(), name)
227 for subname
in os.listdir(fullname):
228 fullsubname = os.path.join(fullname, subname)
230 os.unlink(fullsubname)
232 self.
error(
'%s not deleted, continuing...' %
237 numericprog = re.compile(
'^[1-9][0-9]*$')
239 return numericprog.match(str)
is not None
242 """Class representing a particular folder."""
249 raise Error,
'no folder %s' % name
252 """String representation."""
253 return 'Folder(%s, %s)' % (`self.
mh`, `self.
name`)
256 """Error message handler."""
257 apply(self.mh.error, args)
260 """Return the full pathname of the folder."""
261 return os.path.join(self.mh.path, self.
name)
264 """Return the full pathname of the folder's sequences file."""
265 return os.path.join(self.
getfullname(), MH_SEQUENCES)
268 """Return the full pathname of a message in the folder."""
272 """Return list of direct subfolders."""
273 return self.mh.listsubfolders(self.
name)
276 """Return list of all subfolders."""
277 return self.mh.listallsubfolders(self.
name)
280 """Return the list of messages currently present in the folder.
281 As a side effect, set self.last to the last message (or 0)."""
283 match = numericprog.match
284 append = messages.append
288 messages = map(int, messages)
297 """Return the set of sequences for the folder."""
301 f =
open(fullname,
'r')
307 fields = line.split(
':')
309 self.
error(
'bad sequence in %s: %s' %
310 (fullname, line.strip()))
311 key = fields[0].
strip()
313 sequences[key] = value
317 """Write the set of sequences back to the folder."""
320 for key
in sequences.keys():
322 s.fromlist(sequences[key])
323 if not f: f =
open(fullname,
'w')
324 f.write(
'%s: %s\n' % (key, s.tostring()))
334 """Return the current message. Raise Error when there is none."""
337 return max(seqs[
'cur'])
338 except (ValueError, KeyError):
339 raise Error,
"no cur message"
342 """Set the current message."""
346 """Parse an MH sequence specification into a message list.
347 Attempt to mimic mh-sequence(5) as close as possible.
348 Also attempt to mimic observed behavior regarding which
349 conditions cause which error messages."""
357 raise Error,
"no messages in %s" % self.
name
364 head, dir, tail = seq[:i],
'', seq[i+1:]
366 dir, tail = tail[:1], tail[1:]
368 raise Error,
"bad message list %s" % seq
371 except (ValueError, OverflowError):
378 if not seqs.has_key(head):
380 msg =
"bad message list %s" % seq
381 raise Error, msg, sys.exc_info()[2]
384 raise Error,
"sequence %s empty" % head
391 if head
in (
'prev',
'last'):
395 return all[
max(0, i-count):i]
398 return all[i:i+count]
408 raise Error,
"bad message list %s" % seq
415 if not seqs.has_key(seq):
417 msg =
"bad message list %s" % seq
423 raise Error,
"message %d doesn't exist" % n
425 raise Error,
"no %s message" % seq
429 def _parseindex(self, seq, all):
430 """Internal: parse a message number (or cur, first, etc.)."""
434 except (OverflowError, ValueError):
436 if seq
in (
'cur',
'.'):
448 raise Error,
"no next message"
453 raise Error,
"no prev message"
457 raise Error,
"no prev message"
461 """Open a message -- returns a Message object."""
465 """Remove one or more messages -- may raise os.error."""
476 os.rename(path, commapath)
477 except os.error, msg:
485 raise os.error, errors[0]
487 raise os.error, (
'multiple errors:', errors)
490 """Refile one or more messages -- may raise os.error.
491 'tofolder' is an open folder object."""
495 ton = tofolder.getlast() + 1
497 topath = tofolder.getmessagefilename(ton)
499 os.rename(path, topath)
505 except (IOError, os.error), msg:
512 tofolder.setlast(ton)
516 tofolder._copysequences(self, refiled.items())
520 raise os.error, errors[0]
522 raise os.error, (
'multiple errors:', errors)
524 def _copysequences(self, fromfolder, refileditems):
525 """Helper for refilemessages() to copy sequences."""
526 fromsequences = fromfolder.getsequences()
529 for name, seq
in fromsequences.items():
531 toseq = tosequences[name]
536 for fromn, ton
in refileditems:
541 tosequences[name] = toseq
546 """Move one message over a specific destination message,
547 which may or may not already exist."""
553 topath = tofolder.getmessagefilename(ton)
554 backuptopath = tofolder.getmessagefilename(
',%d' % ton)
556 os.rename(topath, backuptopath)
560 os.rename(path, topath)
565 tofolder.setlast(
None)
578 """Copy one message over a specific destination message,
579 which may or may not already exist."""
585 topath = tofolder.getmessagefilename(ton)
586 backuptopath = tofolder.getmessagefilename(
',%d' % ton)
588 os.rename(topath, backuptopath)
593 tofolder.setlast(
None)
604 """Create a message, with text from the open file txt."""
608 os.rename(path, backuppath)
616 buf = txt.read(BUFSIZE)
630 """Remove one or more messages from all sequences (including last)
631 -- but not from 'cur'!!!"""
632 if hasattr(self,
'last')
and self.
last in list:
636 for name, seq
in sequences.items():
649 """Return the last message number."""
650 if not hasattr(self,
'last'):
655 """Set the last message number."""
657 if hasattr(self,
'last'):
669 path = f.getmessagefilename(n)
674 """String representation."""
678 """Return the message's header text as a string. If an
679 argument is specified, it is used as a filter predicate to
680 decide which headers to return (its argument is the header
681 name converted to lower case)."""
687 if not line[0].isspace():
690 hit = pred(line[:i].
lower())
691 if hit: headers.append(line)
692 return ''.
join(headers)
695 """Return the message's body text as string. This undoes a
696 Content-Transfer-Encoding, but does not interpret other MIME
697 features (e.g. multipart messages). To suppress decoding,
698 pass 0 as an argument."""
701 if not decode
or encoding
in (
'',
'7bit',
'8bit',
'binary'):
702 return self.fp.read()
703 from StringIO
import StringIO
706 return output.getvalue()
709 """Only for multipart messages: return the message's body as a
710 list of SubMessage objects. Each submessage object behaves
711 (almost) as a Message object."""
713 raise Error,
'Content-Type is not multipart/*'
716 raise Error,
'multipart/* without boundary param'
722 n =
str(self.
number) +
'.' + `1 + len(parts)`
729 """Return body, either a string or a list of messages."""
740 Message.__init__(self, f, n, fp)
742 self.
body = Message.getbodyparts(self)
744 self.
body = Message.getbodytext(self)
749 """String representation."""
751 return 'SubMessage(%s, %s, %s)' % (f, n, fp)
768 """Class implementing sets of integers.
770 This is an efficient representation for sets consisting of several
771 continuous ranges, e.g. 1-100,200-400,402-1000 is represented
772 internally as a list of three pairs: [(1,100), (200,400),
773 (402,1000)]. The internal representation is always kept normalized.
775 The constructor has up to three arguments:
776 - the string used to initialize the set (default ''),
777 - the separator between ranges (default ',')
778 - the separator between begin and end of a range (default '-')
779 The separators must be strings (not regexprs) and should be different.
781 The tostring() function yields a string that can be passed to another
782 IntSet constructor; __repr__() is a valid IntSet constructor itself.
790 def __init__(self, data = None, sep = ',', rng = '-'):
803 return hash(self.
pairs)
806 return 'IntSet(%s, %s, %s)' % (`self.
tostring()`,
812 while i < len(self.
pairs):
813 alo, ahi = self.
pairs[i-1]
814 blo, bhi = self.
pairs[i]
816 self.
pairs[i-1:i+1] = [(alo,
max(ahi, bhi))]
822 for lo, hi
in self.
pairs:
823 if lo == hi: t = `lo`
824 else: t = `lo` + self.
rng + `hi`
825 if s: s = s + (self.
sep + t)
831 for lo, hi
in self.
pairs:
842 new.pairs = self.
pairs[:]
846 return self.
pairs[0][0]
849 return self.
pairs[-1][-1]
852 for lo, hi
in self.
pairs:
853 if lo <= x <= hi:
return 1
857 for i
in range(len(self.
pairs)):
858 lo, hi = self.
pairs[i]
861 self.
pairs[i] = (x, hi)
863 self.pairs.insert(i, (x, x))
864 if i > 0
and x-1 == self.
pairs[i-1][1]:
866 self.
pairs[i-1:i+1] = [
873 i = len(self.
pairs) - 1
875 lo, hi = self.
pairs[i]
877 self.
pairs[i] = lo, x
879 self.pairs.append((x, x))
883 self.pairs.append((xlo, xhi))
888 for part
in data.split(self.
sep):
890 for subp
in part.split(self.
rng):
894 new.append((list[0], list[0]))
895 elif len(list) == 2
and list[0] <= list[1]:
896 new.append((list[0], list[1]))
898 raise ValueError,
'bad data passed to IntSet'
910 pat = re.escape(key) +
':'
911 prog = re.compile(pat, casefold
and re.IGNORECASE)
916 text = line[len(key)+1:]
919 if not line
or not line[0].isspace():
928 lines = f.readlines()
932 pat = re.escape(key) +
':(.*)\n'
933 prog = re.compile(pat, casefold
and re.IGNORECASE)
937 newline =
'%s: %s\n' % (key, value)
938 for i
in range(len(lines)):
947 if newline
is not None:
948 lines.append(newline)
949 tempfile = file +
"~"
950 f =
open(tempfile,
'w')
954 os.rename(tempfile, file)
961 os.system(
'rm -rf $HOME/Mail/@test')
963 def do(s):
print s;
print eval(s)
964 do(
'mh.listfolders()')
965 do(
'mh.listallfolders()')
966 testfolders = [
'@test',
'@test/test1',
'@test/test2',
967 '@test/test1/test11',
'@test/test1/test12',
968 '@test/test1/test11/test111']
969 for t
in testfolders: do(
'mh.makefolder(%s)' % `t`)
970 do(
'mh.listsubfolders(\'@test\')')
971 do(
'mh.listallsubfolders(\'@test\')')
972 f = mh.openfolder(
'@test')
973 do(
'f.listsubfolders()')
974 do(
'f.listallsubfolders()')
975 do(
'f.getsequences()')
976 seqs = f.getsequences()
977 seqs[
'foo'] =
IntSet(
'1-10 12-20',
' ').tolist()
980 do(
'f.getsequences()')
981 testfolders.reverse()
982 for t
in testfolders: do(
'mh.deletefolder(%s)' % `t`)
983 do(
'mh.getcontext()')
984 context = mh.getcontext()
985 f = mh.openfolder(context)
987 for seq
in [
'first',
'last',
'cur',
'.',
'prev',
'next',
988 'first:3',
'last:3',
'cur:3',
'cur:-3',
990 '1:3',
'1:-3',
'100:3',
'100:-3',
'10000:3',
'10000:-3',
993 do(
'f.parsesequence(%s)' % `seq`)
996 stuff = os.popen(
"pick %s 2>/dev/null" % `seq`).read()
997 list = map(int, stuff.split())
998 print list,
"<-- pick"
999 do(
'f.listmessages()')
1002 if __name__ ==
'__main__':