struct.pack has become picky about h (short) and H (unsigned short).
[python/dscho.git] / Lib / mhlib.py
blobdf538f274e8a096de626d455ecbb297449eceae0
1 """MH interface -- purely object-oriented (well, almost)
3 Executive summary:
5 import mhlib
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
54 """
56 # XXX To do, functionality:
57 # - annotate messages
58 # - send messages
60 # XXX To do, organization:
61 # - move IntSet to separate file
62 # - move most Message functionality to module mimetools
65 # Customizable defaults
67 MH_PROFILE = '~/.mh_profile'
68 PATH = '~/Mail'
69 MH_SEQUENCES = '.mh_sequences'
70 FOLDER_PROTECT = 0700
73 # Imported modules
75 import os
76 import sys
77 from stat import ST_NLINK
78 import re
79 import string
80 import mimetools
81 import multifile
82 import shutil
83 from bisect import bisect
86 # Exported constants
88 class Error(Exception):
89 pass
92 class MH:
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):
100 """Constructor."""
101 if not profile: profile = MH_PROFILE
102 self.profile = os.path.expanduser(profile)
103 if not path: path = self.getprofile('Path')
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'
109 self.path = path
111 def __repr__(self):
112 """String representation."""
113 return 'MH(%s, %s)' % (`self.path`, `self.profile`)
115 def error(self, msg, *args):
116 """Routine to print an error. May be overridden by a derived class."""
117 sys.stderr.write('MH error: %s\n' % (msg % args))
119 def getprofile(self, key):
120 """Return a profile entry, None if not found."""
121 return pickline(self.profile, key)
123 def getpath(self):
124 """Return the path (the name of the collection's directory)."""
125 return self.path
127 def getcontext(self):
128 """Return the name of the current folder."""
129 context = pickline(os.path.join(self.getpath(), 'context'),
130 'Current-Folder')
131 if not context: context = 'inbox'
132 return context
134 def setcontext(self, context):
135 """Set the name of the current folder."""
136 fn = os.path.join(self.getpath(), 'context')
137 f = open(fn, "w")
138 f.write("Current-Folder: %s\n" % context)
139 f.close()
141 def listfolders(self):
142 """Return the names of the top-level folders."""
143 folders = []
144 path = self.getpath()
145 for name in os.listdir(path):
146 fullname = os.path.join(path, name)
147 if os.path.isdir(fullname):
148 folders.append(name)
149 folders.sort()
150 return folders
152 def listsubfolders(self, name):
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)
156 # Get the link count so we can avoid listing folders
157 # that have no subfolders.
158 st = os.stat(fullname)
159 nlinks = st[ST_NLINK]
160 if nlinks <= 2:
161 return []
162 subfolders = []
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)
169 # Stop looking for subfolders when
170 # we've seen them all
171 nlinks = nlinks - 1
172 if nlinks <= 2:
173 break
174 subfolders.sort()
175 return subfolders
177 def listallfolders(self):
178 """Return the names of all folders and subfolders, recursively."""
179 return self.listallsubfolders('')
181 def listallsubfolders(self, name):
182 """Return the names of subfolders in a given folder, recursively."""
183 fullname = os.path.join(self.path, name)
184 # Get the link count so we can avoid listing folders
185 # that have no subfolders.
186 st = os.stat(fullname)
187 nlinks = st[ST_NLINK]
188 if nlinks <= 2:
189 return []
190 subfolders = []
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):
199 subsubfolders = self.listallsubfolders(
200 name_subname)
201 subfolders = subfolders + subsubfolders
202 # Stop looking for subfolders when
203 # we've seen them all
204 nlinks = nlinks - 1
205 if nlinks <= 2:
206 break
207 subfolders.sort()
208 return subfolders
210 def openfolder(self, name):
211 """Return a new Folder object for the named folder."""
212 return Folder(self, name)
214 def makefolder(self, name):
215 """Create a new folder (or raise os.error if it cannot be created)."""
216 protect = pickline(self.profile, 'Folder-Protect')
217 if protect and isnumeric(protect):
218 mode = string.atoi(protect, 8)
219 else:
220 mode = FOLDER_PROTECT
221 os.mkdir(os.path.join(self.getpath(), name), mode)
223 def deletefolder(self, name):
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)
229 try:
230 os.unlink(fullsubname)
231 except os.error:
232 self.error('%s not deleted, continuing...' %
233 fullsubname)
234 os.rmdir(fullname)
237 numericprog = re.compile('^[1-9][0-9]*$')
238 def isnumeric(str):
239 return numericprog.match(str) is not None
241 class Folder:
242 """Class representing a particular folder."""
244 def __init__(self, mh, name):
245 """Constructor."""
246 self.mh = mh
247 self.name = name
248 if not os.path.isdir(self.getfullname()):
249 raise Error, 'no folder %s' % name
251 def __repr__(self):
252 """String representation."""
253 return 'Folder(%s, %s)' % (`self.mh`, `self.name`)
255 def error(self, *args):
256 """Error message handler."""
257 apply(self.mh.error, args)
259 def getfullname(self):
260 """Return the full pathname of the folder."""
261 return os.path.join(self.mh.path, self.name)
263 def getsequencesfilename(self):
264 """Return the full pathname of the folder's sequences file."""
265 return os.path.join(self.getfullname(), MH_SEQUENCES)
267 def getmessagefilename(self, n):
268 """Return the full pathname of a message in the folder."""
269 return os.path.join(self.getfullname(), str(n))
271 def listsubfolders(self):
272 """Return list of direct subfolders."""
273 return self.mh.listsubfolders(self.name)
275 def listallsubfolders(self):
276 """Return list of all subfolders."""
277 return self.mh.listallsubfolders(self.name)
279 def listmessages(self):
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)."""
282 messages = []
283 match = numericprog.match
284 append = messages.append
285 for name in os.listdir(self.getfullname()):
286 if match(name):
287 append(name)
288 messages = map(string.atoi, messages)
289 messages.sort()
290 if messages:
291 self.last = messages[-1]
292 else:
293 self.last = 0
294 return messages
296 def getsequences(self):
297 """Return the set of sequences for the folder."""
298 sequences = {}
299 fullname = self.getsequencesfilename()
300 try:
301 f = open(fullname, 'r')
302 except IOError:
303 return sequences
304 while 1:
305 line = f.readline()
306 if not line: break
307 fields = string.splitfields(line, ':')
308 if len(fields) <> 2:
309 self.error('bad sequence in %s: %s' %
310 (fullname, string.strip(line)))
311 key = string.strip(fields[0])
312 value = IntSet(string.strip(fields[1]), ' ').tolist()
313 sequences[key] = value
314 return sequences
316 def putsequences(self, sequences):
317 """Write the set of sequences back to the folder."""
318 fullname = self.getsequencesfilename()
319 f = None
320 for key in sequences.keys():
321 s = IntSet('', ' ')
322 s.fromlist(sequences[key])
323 if not f: f = open(fullname, 'w')
324 f.write('%s: %s\n' % (key, s.tostring()))
325 if not f:
326 try:
327 os.unlink(fullname)
328 except os.error:
329 pass
330 else:
331 f.close()
333 def getcurrent(self):
334 """Return the current message. Raise Error when there is none."""
335 seqs = self.getsequences()
336 try:
337 return max(seqs['cur'])
338 except (ValueError, KeyError):
339 raise Error, "no cur message"
341 def setcurrent(self, n):
342 """Set the current message."""
343 updateline(self.getsequencesfilename(), 'cur', str(n), 0)
345 def parsesequence(self, seq):
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."""
350 # XXX Still not complete (see mh-format(5)).
351 # Missing are:
352 # - 'prev', 'next' as count
353 # - Sequence-Negation option
354 all = self.listmessages()
355 # Observed behavior: test for empty folder is done first
356 if not all:
357 raise Error, "no messages in %s" % self.name
358 # Common case first: all is frequently the default
359 if seq == 'all':
360 return all
361 # Test for X:Y before X-Y because 'seq:-n' matches both
362 i = string.find(seq, ':')
363 if i >= 0:
364 head, dir, tail = seq[:i], '', seq[i+1:]
365 if tail[:1] in '-+':
366 dir, tail = tail[:1], tail[1:]
367 if not isnumeric(tail):
368 raise Error, "bad message list %s" % seq
369 try:
370 count = string.atoi(tail)
371 except (ValueError, OverflowError):
372 # Can't use sys.maxint because of i+count below
373 count = len(all)
374 try:
375 anchor = self._parseindex(head, all)
376 except Error, msg:
377 seqs = self.getsequences()
378 if not seqs.has_key(head):
379 if not msg:
380 msg = "bad message list %s" % seq
381 raise Error, msg, sys.exc_info()[2]
382 msgs = seqs[head]
383 if not msgs:
384 raise Error, "sequence %s empty" % head
385 if dir == '-':
386 return msgs[-count:]
387 else:
388 return msgs[:count]
389 else:
390 if not dir:
391 if head in ('prev', 'last'):
392 dir = '-'
393 if dir == '-':
394 i = bisect(all, anchor)
395 return all[max(0, i-count):i]
396 else:
397 i = bisect(all, anchor-1)
398 return all[i:i+count]
399 # Test for X-Y next
400 i = string.find(seq, '-')
401 if i >= 0:
402 begin = self._parseindex(seq[:i], all)
403 end = self._parseindex(seq[i+1:], all)
404 i = bisect(all, begin-1)
405 j = bisect(all, end)
406 r = all[i:j]
407 if not r:
408 raise Error, "bad message list %s" % seq
409 return r
410 # Neither X:Y nor X-Y; must be a number or a (pseudo-)sequence
411 try:
412 n = self._parseindex(seq, all)
413 except Error, msg:
414 seqs = self.getsequences()
415 if not seqs.has_key(seq):
416 if not msg:
417 msg = "bad message list %s" % seq
418 raise Error, msg
419 return seqs[seq]
420 else:
421 if n not in all:
422 if isnumeric(seq):
423 raise Error, "message %d doesn't exist" % n
424 else:
425 raise Error, "no %s message" % seq
426 else:
427 return [n]
429 def _parseindex(self, seq, all):
430 """Internal: parse a message number (or cur, first, etc.)."""
431 if isnumeric(seq):
432 try:
433 return string.atoi(seq)
434 except (OverflowError, ValueError):
435 return sys.maxint
436 if seq in ('cur', '.'):
437 return self.getcurrent()
438 if seq == 'first':
439 return all[0]
440 if seq == 'last':
441 return all[-1]
442 if seq == 'next':
443 n = self.getcurrent()
444 i = bisect(all, n)
445 try:
446 return all[i]
447 except IndexError:
448 raise Error, "no next message"
449 if seq == 'prev':
450 n = self.getcurrent()
451 i = bisect(all, n-1)
452 if i == 0:
453 raise Error, "no prev message"
454 try:
455 return all[i-1]
456 except IndexError:
457 raise Error, "no prev message"
458 raise Error, None
460 def openmessage(self, n):
461 """Open a message -- returns a Message object."""
462 return Message(self, n)
464 def removemessages(self, list):
465 """Remove one or more messages -- may raise os.error."""
466 errors = []
467 deleted = []
468 for n in list:
469 path = self.getmessagefilename(n)
470 commapath = self.getmessagefilename(',' + str(n))
471 try:
472 os.unlink(commapath)
473 except os.error:
474 pass
475 try:
476 os.rename(path, commapath)
477 except os.error, msg:
478 errors.append(msg)
479 else:
480 deleted.append(n)
481 if deleted:
482 self.removefromallsequences(deleted)
483 if errors:
484 if len(errors) == 1:
485 raise os.error, errors[0]
486 else:
487 raise os.error, ('multiple errors:', errors)
489 def refilemessages(self, list, tofolder, keepsequences=0):
490 """Refile one or more messages -- may raise os.error.
491 'tofolder' is an open folder object."""
492 errors = []
493 refiled = {}
494 for n in list:
495 ton = tofolder.getlast() + 1
496 path = self.getmessagefilename(n)
497 topath = tofolder.getmessagefilename(ton)
498 try:
499 os.rename(path, topath)
500 except os.error:
501 # Try copying
502 try:
503 shutil.copy2(path, topath)
504 os.unlink(path)
505 except (IOError, os.error), msg:
506 errors.append(msg)
507 try:
508 os.unlink(topath)
509 except os.error:
510 pass
511 continue
512 tofolder.setlast(ton)
513 refiled[n] = ton
514 if refiled:
515 if keepsequences:
516 tofolder._copysequences(self, refiled.items())
517 self.removefromallsequences(refiled.keys())
518 if errors:
519 if len(errors) == 1:
520 raise os.error, errors[0]
521 else:
522 raise os.error, ('multiple errors:', errors)
524 def _copysequences(self, fromfolder, refileditems):
525 """Helper for refilemessages() to copy sequences."""
526 fromsequences = fromfolder.getsequences()
527 tosequences = self.getsequences()
528 changed = 0
529 for name, seq in fromsequences.items():
530 try:
531 toseq = tosequences[name]
532 new = 0
533 except:
534 toseq = []
535 new = 1
536 for fromn, ton in refileditems:
537 if fromn in seq:
538 toseq.append(ton)
539 changed = 1
540 if new and toseq:
541 tosequences[name] = toseq
542 if changed:
543 self.putsequences(tosequences)
545 def movemessage(self, n, tofolder, ton):
546 """Move one message over a specific destination message,
547 which may or may not already exist."""
548 path = self.getmessagefilename(n)
549 # Open it to check that it exists
550 f = open(path)
551 f.close()
552 del f
553 topath = tofolder.getmessagefilename(ton)
554 backuptopath = tofolder.getmessagefilename(',%d' % ton)
555 try:
556 os.rename(topath, backuptopath)
557 except os.error:
558 pass
559 try:
560 os.rename(path, topath)
561 except os.error:
562 # Try copying
563 ok = 0
564 try:
565 tofolder.setlast(None)
566 shutil.copy2(path, topath)
567 ok = 1
568 finally:
569 if not ok:
570 try:
571 os.unlink(topath)
572 except os.error:
573 pass
574 os.unlink(path)
575 self.removefromallsequences([n])
577 def copymessage(self, n, tofolder, ton):
578 """Copy one message over a specific destination message,
579 which may or may not already exist."""
580 path = self.getmessagefilename(n)
581 # Open it to check that it exists
582 f = open(path)
583 f.close()
584 del f
585 topath = tofolder.getmessagefilename(ton)
586 backuptopath = tofolder.getmessagefilename(',%d' % ton)
587 try:
588 os.rename(topath, backuptopath)
589 except os.error:
590 pass
591 ok = 0
592 try:
593 tofolder.setlast(None)
594 shutil.copy2(path, topath)
595 ok = 1
596 finally:
597 if not ok:
598 try:
599 os.unlink(topath)
600 except os.error:
601 pass
603 def createmessage(self, n, txt):
604 """Create a message, with text from the open file txt."""
605 path = self.getmessagefilename(n)
606 backuppath = self.getmessagefilename(',%d' % n)
607 try:
608 os.rename(path, backuppath)
609 except os.error:
610 pass
611 ok = 0
612 BUFSIZE = 16*1024
613 try:
614 f = open(path, "w")
615 while 1:
616 buf = txt.read(BUFSIZE)
617 if not buf:
618 break
619 f.write(buf)
620 f.close()
621 ok = 1
622 finally:
623 if not ok:
624 try:
625 os.unlink(path)
626 except os.error:
627 pass
629 def removefromallsequences(self, list):
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:
633 del self.last
634 sequences = self.getsequences()
635 changed = 0
636 for name, seq in sequences.items():
637 if name == 'cur':
638 continue
639 for n in list:
640 if n in seq:
641 seq.remove(n)
642 changed = 1
643 if not seq:
644 del sequences[name]
645 if changed:
646 self.putsequences(sequences)
648 def getlast(self):
649 """Return the last message number."""
650 if not hasattr(self, 'last'):
651 messages = self.listmessages()
652 return self.last
654 def setlast(self, last):
655 """Set the last message number."""
656 if last is None:
657 if hasattr(self, 'last'):
658 del self.last
659 else:
660 self.last = last
662 class Message(mimetools.Message):
664 def __init__(self, f, n, fp = None):
665 """Constructor."""
666 self.folder = f
667 self.number = n
668 if not fp:
669 path = f.getmessagefilename(n)
670 fp = open(path, 'r')
671 mimetools.Message.__init__(self, fp)
673 def __repr__(self):
674 """String representation."""
675 return 'Message(%s, %s)' % (repr(self.folder), self.number)
677 def getheadertext(self, pred = None):
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)."""
682 if not pred:
683 return string.joinfields(self.headers, '')
684 headers = []
685 hit = 0
686 for line in self.headers:
687 if line[0] not in string.whitespace:
688 i = string.find(line, ':')
689 if i > 0:
690 hit = pred(string.lower(line[:i]))
691 if hit: headers.append(line)
692 return string.joinfields(headers, '')
694 def getbodytext(self, decode = 1):
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."""
699 self.fp.seek(self.startofbody)
700 encoding = self.getencoding()
701 if not decode or encoding in ('', '7bit', '8bit', 'binary'):
702 return self.fp.read()
703 from StringIO import StringIO
704 output = StringIO()
705 mimetools.decode(self.fp, output, encoding)
706 return output.getvalue()
708 def getbodyparts(self):
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."""
712 if self.getmaintype() != 'multipart':
713 raise Error, 'Content-Type is not multipart/*'
714 bdry = self.getparam('boundary')
715 if not bdry:
716 raise Error, 'multipart/* without boundary param'
717 self.fp.seek(self.startofbody)
718 mf = multifile.MultiFile(self.fp)
719 mf.push(bdry)
720 parts = []
721 while mf.next():
722 n = str(self.number) + '.' + `1 + len(parts)`
723 part = SubMessage(self.folder, n, mf)
724 parts.append(part)
725 mf.pop()
726 return parts
728 def getbody(self):
729 """Return body, either a string or a list of messages."""
730 if self.getmaintype() == 'multipart':
731 return self.getbodyparts()
732 else:
733 return self.getbodytext()
736 class SubMessage(Message):
738 def __init__(self, f, n, fp):
739 """Constructor."""
740 Message.__init__(self, f, n, fp)
741 if self.getmaintype() == 'multipart':
742 self.body = Message.getbodyparts(self)
743 else:
744 self.body = Message.getbodytext(self)
745 self.bodyencoded = Message.getbodytext(self, decode=0)
746 # XXX If this is big, should remember file pointers
748 def __repr__(self):
749 """String representation."""
750 f, n, fp = self.folder, self.number, self.fp
751 return 'SubMessage(%s, %s, %s)' % (f, n, fp)
753 def getbodytext(self, decode = 1):
754 if not decode:
755 return self.bodyencoded
756 if type(self.body) == type(''):
757 return self.body
759 def getbodyparts(self):
760 if type(self.body) == type([]):
761 return self.body
763 def getbody(self):
764 return self.body
767 class IntSet:
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.
785 # XXX The default begin/end separator means that negative numbers are
786 # not supported very well.
788 # XXX There are currently no operations to remove set elements.
790 def __init__(self, data = None, sep = ',', rng = '-'):
791 self.pairs = []
792 self.sep = sep
793 self.rng = rng
794 if data: self.fromstring(data)
796 def reset(self):
797 self.pairs = []
799 def __cmp__(self, other):
800 return cmp(self.pairs, other.pairs)
802 def __hash__(self):
803 return hash(self.pairs)
805 def __repr__(self):
806 return 'IntSet(%s, %s, %s)' % (`self.tostring()`,
807 `self.sep`, `self.rng`)
809 def normalize(self):
810 self.pairs.sort()
811 i = 1
812 while i < len(self.pairs):
813 alo, ahi = self.pairs[i-1]
814 blo, bhi = self.pairs[i]
815 if ahi >= blo-1:
816 self.pairs[i-1:i+1] = [(alo, max(ahi, bhi))]
817 else:
818 i = i+1
820 def tostring(self):
821 s = ''
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)
826 else: s = t
827 return s
829 def tolist(self):
830 l = []
831 for lo, hi in self.pairs:
832 m = range(lo, hi+1)
833 l = l + m
834 return l
836 def fromlist(self, list):
837 for i in list:
838 self.append(i)
840 def clone(self):
841 new = IntSet()
842 new.pairs = self.pairs[:]
843 return new
845 def min(self):
846 return self.pairs[0][0]
848 def max(self):
849 return self.pairs[-1][-1]
851 def contains(self, x):
852 for lo, hi in self.pairs:
853 if lo <= x <= hi: return 1
854 return 0
856 def append(self, x):
857 for i in range(len(self.pairs)):
858 lo, hi = self.pairs[i]
859 if x < lo: # Need to insert before
860 if x+1 == lo:
861 self.pairs[i] = (x, hi)
862 else:
863 self.pairs.insert(i, (x, x))
864 if i > 0 and x-1 == self.pairs[i-1][1]:
865 # Merge with previous
866 self.pairs[i-1:i+1] = [
867 (self.pairs[i-1][0],
868 self.pairs[i][1])
870 return
871 if x <= hi: # Already in set
872 return
873 i = len(self.pairs) - 1
874 if i >= 0:
875 lo, hi = self.pairs[i]
876 if x-1 == hi:
877 self.pairs[i] = lo, x
878 return
879 self.pairs.append((x, x))
881 def addpair(self, xlo, xhi):
882 if xlo > xhi: return
883 self.pairs.append((xlo, xhi))
884 self.normalize()
886 def fromstring(self, data):
887 import string
888 new = []
889 for part in string.splitfields(data, self.sep):
890 list = []
891 for subp in string.splitfields(part, self.rng):
892 s = string.strip(subp)
893 list.append(string.atoi(s))
894 if len(list) == 1:
895 new.append((list[0], list[0]))
896 elif len(list) == 2 and list[0] <= list[1]:
897 new.append((list[0], list[1]))
898 else:
899 raise ValueError, 'bad data passed to IntSet'
900 self.pairs = self.pairs + new
901 self.normalize()
904 # Subroutines to read/write entries in .mh_profile and .mh_sequences
906 def pickline(file, key, casefold = 1):
907 try:
908 f = open(file, 'r')
909 except IOError:
910 return None
911 pat = re.escape(key) + ':'
912 prog = re.compile(pat, casefold and re.IGNORECASE)
913 while 1:
914 line = f.readline()
915 if not line: break
916 if prog.match(line):
917 text = line[len(key)+1:]
918 while 1:
919 line = f.readline()
920 if not line or line[0] not in string.whitespace:
921 break
922 text = text + line
923 return string.strip(text)
924 return None
926 def updateline(file, key, value, casefold = 1):
927 try:
928 f = open(file, 'r')
929 lines = f.readlines()
930 f.close()
931 except IOError:
932 lines = []
933 pat = re.escape(key) + ':(.*)\n'
934 prog = re.compile(pat, casefold and re.IGNORECASE)
935 if value is None:
936 newline = None
937 else:
938 newline = '%s: %s\n' % (key, value)
939 for i in range(len(lines)):
940 line = lines[i]
941 if prog.match(line):
942 if newline is None:
943 del lines[i]
944 else:
945 lines[i] = newline
946 break
947 else:
948 if newline is not None:
949 lines.append(newline)
950 tempfile = file + "~"
951 f = open(tempfile, 'w')
952 for line in lines:
953 f.write(line)
954 f.close()
955 os.rename(tempfile, file)
958 # Test program
960 def test():
961 global mh, f
962 os.system('rm -rf $HOME/Mail/@test')
963 mh = MH()
964 def do(s): print s; print eval(s)
965 do('mh.listfolders()')
966 do('mh.listallfolders()')
967 testfolders = ['@test', '@test/test1', '@test/test2',
968 '@test/test1/test11', '@test/test1/test12',
969 '@test/test1/test11/test111']
970 for t in testfolders: do('mh.makefolder(%s)' % `t`)
971 do('mh.listsubfolders(\'@test\')')
972 do('mh.listallsubfolders(\'@test\')')
973 f = mh.openfolder('@test')
974 do('f.listsubfolders()')
975 do('f.listallsubfolders()')
976 do('f.getsequences()')
977 seqs = f.getsequences()
978 seqs['foo'] = IntSet('1-10 12-20', ' ').tolist()
979 print seqs
980 f.putsequences(seqs)
981 do('f.getsequences()')
982 testfolders.reverse()
983 for t in testfolders: do('mh.deletefolder(%s)' % `t`)
984 do('mh.getcontext()')
985 context = mh.getcontext()
986 f = mh.openfolder(context)
987 do('f.getcurrent()')
988 for seq in ['first', 'last', 'cur', '.', 'prev', 'next',
989 'first:3', 'last:3', 'cur:3', 'cur:-3',
990 'prev:3', 'next:3',
991 '1:3', '1:-3', '100:3', '100:-3', '10000:3', '10000:-3',
992 'all']:
993 try:
994 do('f.parsesequence(%s)' % `seq`)
995 except Error, msg:
996 print "Error:", msg
997 stuff = os.popen("pick %s 2>/dev/null" % `seq`).read()
998 list = map(string.atoi, string.split(stuff))
999 print list, "<-- pick"
1000 do('f.listmessages()')
1003 if __name__ == '__main__':
1004 test()