Ditched '_find_SET()', since it was a no-value-added wrapper around
[python/dscho.git] / Lib / mhlib.py
blobf7c39fa0eebb50a70dff25049b3d1efb130b1de4
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 Error = 'mhlib.Error'
91 class MH:
92 """Class representing a particular collection of folders.
93 Optional constructor arguments are the pathname for the directory
94 containing the collection, and the MH profile to use.
95 If either is omitted or empty a default is used; the default
96 directory is taken from the MH profile if it is specified there."""
98 def __init__(self, path = None, profile = None):
99 """Constructor."""
100 if not profile: profile = MH_PROFILE
101 self.profile = os.path.expanduser(profile)
102 if not path: path = self.getprofile('Path')
103 if not path: path = PATH
104 if not os.path.isabs(path) and path[0] != '~':
105 path = os.path.join('~', path)
106 path = os.path.expanduser(path)
107 if not os.path.isdir(path): raise Error, 'MH() path not found'
108 self.path = path
110 def __repr__(self):
111 """String representation."""
112 return 'MH(%s, %s)' % (`self.path`, `self.profile`)
114 def error(self, msg, *args):
115 """Routine to print an error. May be overridden by a derived class."""
116 sys.stderr.write('MH error: %s\n' % (msg % args))
118 def getprofile(self, key):
119 """Return a profile entry, None if not found."""
120 return pickline(self.profile, key)
122 def getpath(self):
123 """Return the path (the name of the collection's directory)."""
124 return self.path
126 def getcontext(self):
127 """Return the name of the current folder."""
128 context = pickline(os.path.join(self.getpath(), 'context'),
129 'Current-Folder')
130 if not context: context = 'inbox'
131 return context
133 def setcontext(self, context):
134 """Set the name of the current folder."""
135 fn = os.path.join(self.getpath(), 'context')
136 f = open(fn, "w")
137 f.write("Current-Folder: %s\n" % context)
138 f.close()
140 def listfolders(self):
141 """Return the names of the top-level folders."""
142 folders = []
143 path = self.getpath()
144 for name in os.listdir(path):
145 fullname = os.path.join(path, name)
146 if os.path.isdir(fullname):
147 folders.append(name)
148 folders.sort()
149 return folders
151 def listsubfolders(self, name):
152 """Return the names of the subfolders in a given folder
153 (prefixed with the given folder name)."""
154 fullname = os.path.join(self.path, name)
155 # Get the link count so we can avoid listing folders
156 # that have no subfolders.
157 st = os.stat(fullname)
158 nlinks = st[ST_NLINK]
159 if nlinks <= 2:
160 return []
161 subfolders = []
162 subnames = os.listdir(fullname)
163 for subname in subnames:
164 fullsubname = os.path.join(fullname, subname)
165 if os.path.isdir(fullsubname):
166 name_subname = os.path.join(name, subname)
167 subfolders.append(name_subname)
168 # Stop looking for subfolders when
169 # we've seen them all
170 nlinks = nlinks - 1
171 if nlinks <= 2:
172 break
173 subfolders.sort()
174 return subfolders
176 def listallfolders(self):
177 """Return the names of all folders and subfolders, recursively."""
178 return self.listallsubfolders('')
180 def listallsubfolders(self, name):
181 """Return the names of subfolders in a given folder, recursively."""
182 fullname = os.path.join(self.path, name)
183 # Get the link count so we can avoid listing folders
184 # that have no subfolders.
185 st = os.stat(fullname)
186 nlinks = st[ST_NLINK]
187 if nlinks <= 2:
188 return []
189 subfolders = []
190 subnames = os.listdir(fullname)
191 for subname in subnames:
192 if subname[0] == ',' or isnumeric(subname): continue
193 fullsubname = os.path.join(fullname, subname)
194 if os.path.isdir(fullsubname):
195 name_subname = os.path.join(name, subname)
196 subfolders.append(name_subname)
197 if not os.path.islink(fullsubname):
198 subsubfolders = self.listallsubfolders(
199 name_subname)
200 subfolders = subfolders + subsubfolders
201 # Stop looking for subfolders when
202 # we've seen them all
203 nlinks = nlinks - 1
204 if nlinks <= 2:
205 break
206 subfolders.sort()
207 return subfolders
209 def openfolder(self, name):
210 """Return a new Folder object for the named folder."""
211 return Folder(self, name)
213 def makefolder(self, name):
214 """Create a new folder (or raise os.error if it cannot be created)."""
215 protect = pickline(self.profile, 'Folder-Protect')
216 if protect and isnumeric(protect):
217 mode = string.atoi(protect, 8)
218 else:
219 mode = FOLDER_PROTECT
220 os.mkdir(os.path.join(self.getpath(), name), mode)
222 def deletefolder(self, name):
223 """Delete a folder. This removes files in the folder but not
224 subdirectories. Raise os.error if deleting the folder itself fails."""
225 fullname = os.path.join(self.getpath(), name)
226 for subname in os.listdir(fullname):
227 fullsubname = os.path.join(fullname, subname)
228 try:
229 os.unlink(fullsubname)
230 except os.error:
231 self.error('%s not deleted, continuing...' %
232 fullsubname)
233 os.rmdir(fullname)
236 numericprog = re.compile('^[1-9][0-9]*$')
237 def isnumeric(str):
238 return numericprog.match(str) is not None
240 class Folder:
241 """Class representing a particular folder."""
243 def __init__(self, mh, name):
244 """Constructor."""
245 self.mh = mh
246 self.name = name
247 if not os.path.isdir(self.getfullname()):
248 raise Error, 'no folder %s' % name
250 def __repr__(self):
251 """String representation."""
252 return 'Folder(%s, %s)' % (`self.mh`, `self.name`)
254 def error(self, *args):
255 """Error message handler."""
256 apply(self.mh.error, args)
258 def getfullname(self):
259 """Return the full pathname of the folder."""
260 return os.path.join(self.mh.path, self.name)
262 def getsequencesfilename(self):
263 """Return the full pathname of the folder's sequences file."""
264 return os.path.join(self.getfullname(), MH_SEQUENCES)
266 def getmessagefilename(self, n):
267 """Return the full pathname of a message in the folder."""
268 return os.path.join(self.getfullname(), str(n))
270 def listsubfolders(self):
271 """Return list of direct subfolders."""
272 return self.mh.listsubfolders(self.name)
274 def listallsubfolders(self):
275 """Return list of all subfolders."""
276 return self.mh.listallsubfolders(self.name)
278 def listmessages(self):
279 """Return the list of messages currently present in the folder.
280 As a side effect, set self.last to the last message (or 0)."""
281 messages = []
282 match = numericprog.match
283 append = messages.append
284 for name in os.listdir(self.getfullname()):
285 if match(name):
286 append(name)
287 messages = map(string.atoi, messages)
288 messages.sort()
289 if messages:
290 self.last = messages[-1]
291 else:
292 self.last = 0
293 return messages
295 def getsequences(self):
296 """Return the set of sequences for the folder."""
297 sequences = {}
298 fullname = self.getsequencesfilename()
299 try:
300 f = open(fullname, 'r')
301 except IOError:
302 return sequences
303 while 1:
304 line = f.readline()
305 if not line: break
306 fields = string.splitfields(line, ':')
307 if len(fields) <> 2:
308 self.error('bad sequence in %s: %s' %
309 (fullname, string.strip(line)))
310 key = string.strip(fields[0])
311 value = IntSet(string.strip(fields[1]), ' ').tolist()
312 sequences[key] = value
313 return sequences
315 def putsequences(self, sequences):
316 """Write the set of sequences back to the folder."""
317 fullname = self.getsequencesfilename()
318 f = None
319 for key in sequences.keys():
320 s = IntSet('', ' ')
321 s.fromlist(sequences[key])
322 if not f: f = open(fullname, 'w')
323 f.write('%s: %s\n' % (key, s.tostring()))
324 if not f:
325 try:
326 os.unlink(fullname)
327 except os.error:
328 pass
329 else:
330 f.close()
332 def getcurrent(self):
333 """Return the current message. Raise KeyError when there is none."""
334 seqs = self.getsequences()
335 try:
336 return max(seqs['cur'])
337 except (ValueError, KeyError):
338 raise Error, "no cur message"
340 def setcurrent(self, n):
341 """Set the current message."""
342 updateline(self.getsequencesfilename(), 'cur', str(n), 0)
344 def parsesequence(self, seq):
345 """Parse an MH sequence specification into a message list.
346 Attempt to mimic mh-sequence(5) as close as possible.
347 Also attempt to mimic observed behavior regarding which
348 conditions cause which error messages."""
349 # XXX Still not complete (see mh-format(5)).
350 # Missing are:
351 # - 'prev', 'next' as count
352 # - Sequence-Negation option
353 all = self.listmessages()
354 # Observed behavior: test for empty folder is done first
355 if not all:
356 raise Error, "no messages in %s" % self.name
357 # Common case first: all is frequently the default
358 if seq == 'all':
359 return all
360 # Test for X:Y before X-Y because 'seq:-n' matches both
361 i = string.find(seq, ':')
362 if i >= 0:
363 head, dir, tail = seq[:i], '', seq[i+1:]
364 if tail[:1] in '-+':
365 dir, tail = tail[:1], tail[1:]
366 if not isnumeric(tail):
367 raise Error, "bad message list %s" % seq
368 try:
369 count = string.atoi(tail)
370 except (ValueError, OverflowError):
371 # Can't use sys.maxint because of i+count below
372 count = len(all)
373 try:
374 anchor = self._parseindex(head, all)
375 except Error, msg:
376 seqs = self.getsequences()
377 if not seqs.has_key(head):
378 if not msg:
379 msg = "bad message list %s" % seq
380 raise Error, msg, sys.exc_info()[2]
381 msgs = seqs[head]
382 if not msgs:
383 raise Error, "sequence %s empty" % head
384 if dir == '-':
385 return msgs[-count:]
386 else:
387 return msgs[:count]
388 else:
389 if not dir:
390 if head in ('prev', 'last'):
391 dir = '-'
392 if dir == '-':
393 i = bisect(all, anchor)
394 return all[max(0, i-count):i]
395 else:
396 i = bisect(all, anchor-1)
397 return all[i:i+count]
398 # Test for X-Y next
399 i = string.find(seq, '-')
400 if i >= 0:
401 begin = self._parseindex(seq[:i], all)
402 end = self._parseindex(seq[i+1:], all)
403 i = bisect(all, begin-1)
404 j = bisect(all, end)
405 r = all[i:j]
406 if not r:
407 raise Error, "bad message list %s" % seq
408 return r
409 # Neither X:Y nor X-Y; must be a number or a (pseudo-)sequence
410 try:
411 n = self._parseindex(seq, all)
412 except Error, msg:
413 seqs = self.getsequences()
414 if not seqs.has_key(seq):
415 if not msg:
416 msg = "bad message list %s" % seq
417 raise Error, msg
418 return seqs[seq]
419 else:
420 if n not in all:
421 if isnumeric(seq):
422 raise Error, "message %d doesn't exist" % n
423 else:
424 raise Error, "no %s message" % seq
425 else:
426 return [n]
428 def _parseindex(self, seq, all):
429 """Internal: parse a message number (or cur, first, etc.)."""
430 if isnumeric(seq):
431 try:
432 return string.atoi(seq)
433 except (OverflowError, ValueError):
434 return sys.maxint
435 if seq in ('cur', '.'):
436 return self.getcurrent()
437 if seq == 'first':
438 return all[0]
439 if seq == 'last':
440 return all[-1]
441 if seq == 'next':
442 n = self.getcurrent()
443 i = bisect(all, n)
444 try:
445 return all[i]
446 except IndexError:
447 raise Error, "no next message"
448 if seq == 'prev':
449 n = self.getcurrent()
450 i = bisect(all, n-1)
451 if i == 0:
452 raise Error, "no prev message"
453 try:
454 return all[i-1]
455 except IndexError:
456 raise Error, "no prev message"
457 raise Error, None
459 def openmessage(self, n):
460 """Open a message -- returns a Message object."""
461 return Message(self, n)
463 def removemessages(self, list):
464 """Remove one or more messages -- may raise os.error."""
465 errors = []
466 deleted = []
467 for n in list:
468 path = self.getmessagefilename(n)
469 commapath = self.getmessagefilename(',' + str(n))
470 try:
471 os.unlink(commapath)
472 except os.error:
473 pass
474 try:
475 os.rename(path, commapath)
476 except os.error, msg:
477 errors.append(msg)
478 else:
479 deleted.append(n)
480 if deleted:
481 self.removefromallsequences(deleted)
482 if errors:
483 if len(errors) == 1:
484 raise os.error, errors[0]
485 else:
486 raise os.error, ('multiple errors:', errors)
488 def refilemessages(self, list, tofolder, keepsequences=0):
489 """Refile one or more messages -- may raise os.error.
490 'tofolder' is an open folder object."""
491 errors = []
492 refiled = {}
493 for n in list:
494 ton = tofolder.getlast() + 1
495 path = self.getmessagefilename(n)
496 topath = tofolder.getmessagefilename(ton)
497 try:
498 os.rename(path, topath)
499 except os.error:
500 # Try copying
501 try:
502 shutil.copy2(path, topath)
503 os.unlink(path)
504 except (IOError, os.error), msg:
505 errors.append(msg)
506 try:
507 os.unlink(topath)
508 except os.error:
509 pass
510 continue
511 tofolder.setlast(ton)
512 refiled[n] = ton
513 if refiled:
514 if keepsequences:
515 tofolder._copysequences(self, refiled.items())
516 self.removefromallsequences(refiled.keys())
517 if errors:
518 if len(errors) == 1:
519 raise os.error, errors[0]
520 else:
521 raise os.error, ('multiple errors:', errors)
523 def _copysequences(self, fromfolder, refileditems):
524 """Helper for refilemessages() to copy sequences."""
525 fromsequences = fromfolder.getsequences()
526 tosequences = self.getsequences()
527 changed = 0
528 for name, seq in fromsequences.items():
529 try:
530 toseq = tosequences[name]
531 new = 0
532 except:
533 toseq = []
534 new = 1
535 for fromn, ton in refileditems:
536 if fromn in seq:
537 toseq.append(ton)
538 changed = 1
539 if new and toseq:
540 tosequences[name] = toseq
541 if changed:
542 self.putsequences(tosequences)
544 def movemessage(self, n, tofolder, ton):
545 """Move one message over a specific destination message,
546 which may or may not already exist."""
547 path = self.getmessagefilename(n)
548 # Open it to check that it exists
549 f = open(path)
550 f.close()
551 del f
552 topath = tofolder.getmessagefilename(ton)
553 backuptopath = tofolder.getmessagefilename(',%d' % ton)
554 try:
555 os.rename(topath, backuptopath)
556 except os.error:
557 pass
558 try:
559 os.rename(path, topath)
560 except os.error:
561 # Try copying
562 ok = 0
563 try:
564 tofolder.setlast(None)
565 shutil.copy2(path, topath)
566 ok = 1
567 finally:
568 if not ok:
569 try:
570 os.unlink(topath)
571 except os.error:
572 pass
573 os.unlink(path)
574 self.removefromallsequences([n])
576 def copymessage(self, n, tofolder, ton):
577 """Copy one message over a specific destination message,
578 which may or may not already exist."""
579 path = self.getmessagefilename(n)
580 # Open it to check that it exists
581 f = open(path)
582 f.close()
583 del f
584 topath = tofolder.getmessagefilename(ton)
585 backuptopath = tofolder.getmessagefilename(',%d' % ton)
586 try:
587 os.rename(topath, backuptopath)
588 except os.error:
589 pass
590 ok = 0
591 try:
592 tofolder.setlast(None)
593 shutil.copy2(path, topath)
594 ok = 1
595 finally:
596 if not ok:
597 try:
598 os.unlink(topath)
599 except os.error:
600 pass
602 def createmessage(self, n, txt):
603 """Create a message, with text from the open file txt."""
604 path = self.getmessagefilename(n)
605 backuppath = self.getmessagefilename(',%d' % n)
606 try:
607 os.rename(path, backuppath)
608 except os.error:
609 pass
610 ok = 0
611 BUFSIZE = 16*1024
612 try:
613 f = open(path, "w")
614 while 1:
615 buf = txt.read(BUFSIZE)
616 if not buf:
617 break
618 f.write(buf)
619 f.close()
620 ok = 1
621 finally:
622 if not ok:
623 try:
624 os.unlink(path)
625 except os.error:
626 pass
628 def removefromallsequences(self, list):
629 """Remove one or more messages from all sequeuces (including last)
630 -- but not from 'cur'!!!"""
631 if hasattr(self, 'last') and self.last in list:
632 del self.last
633 sequences = self.getsequences()
634 changed = 0
635 for name, seq in sequences.items():
636 if name == 'cur':
637 continue
638 for n in list:
639 if n in seq:
640 seq.remove(n)
641 changed = 1
642 if not seq:
643 del sequences[name]
644 if changed:
645 self.putsequences(sequences)
647 def getlast(self):
648 """Return the last message number."""
649 if not hasattr(self, 'last'):
650 messages = self.listmessages()
651 return self.last
653 def setlast(self, last):
654 """Set the last message number."""
655 if last is None:
656 if hasattr(self, 'last'):
657 del self.last
658 else:
659 self.last = last
661 class Message(mimetools.Message):
663 def __init__(self, f, n, fp = None):
664 """Constructor."""
665 self.folder = f
666 self.number = n
667 if not fp:
668 path = f.getmessagefilename(n)
669 fp = open(path, 'r')
670 mimetools.Message.__init__(self, fp)
672 def __repr__(self):
673 """String representation."""
674 return 'Message(%s, %s)' % (repr(self.folder), self.number)
676 def getheadertext(self, pred = None):
677 """Return the message's header text as a string. If an
678 argument is specified, it is used as a filter predicate to
679 decide which headers to return (its argument is the header
680 name converted to lower case)."""
681 if not pred:
682 return string.joinfields(self.headers, '')
683 headers = []
684 hit = 0
685 for line in self.headers:
686 if line[0] not in string.whitespace:
687 i = string.find(line, ':')
688 if i > 0:
689 hit = pred(string.lower(line[:i]))
690 if hit: headers.append(line)
691 return string.joinfields(headers, '')
693 def getbodytext(self, decode = 1):
694 """Return the message's body text as string. This undoes a
695 Content-Transfer-Encoding, but does not interpret other MIME
696 features (e.g. multipart messages). To suppress decoding,
697 pass 0 as an argument."""
698 self.fp.seek(self.startofbody)
699 encoding = self.getencoding()
700 if not decode or encoding in ('', '7bit', '8bit', 'binary'):
701 return self.fp.read()
702 from StringIO import StringIO
703 output = StringIO()
704 mimetools.decode(self.fp, output, encoding)
705 return output.getvalue()
707 def getbodyparts(self):
708 """Only for multipart messages: return the message's body as a
709 list of SubMessage objects. Each submessage object behaves
710 (almost) as a Message object."""
711 if self.getmaintype() != 'multipart':
712 raise Error, 'Content-Type is not multipart/*'
713 bdry = self.getparam('boundary')
714 if not bdry:
715 raise Error, 'multipart/* without boundary param'
716 self.fp.seek(self.startofbody)
717 mf = multifile.MultiFile(self.fp)
718 mf.push(bdry)
719 parts = []
720 while mf.next():
721 n = str(self.number) + '.' + `1 + len(parts)`
722 part = SubMessage(self.folder, n, mf)
723 parts.append(part)
724 mf.pop()
725 return parts
727 def getbody(self):
728 """Return body, either a string or a list of messages."""
729 if self.getmaintype() == 'multipart':
730 return self.getbodyparts()
731 else:
732 return self.getbodytext()
735 class SubMessage(Message):
737 def __init__(self, f, n, fp):
738 """Constructor."""
739 Message.__init__(self, f, n, fp)
740 if self.getmaintype() == 'multipart':
741 self.body = Message.getbodyparts(self)
742 else:
743 self.body = Message.getbodytext(self)
744 self.bodyencoded = Message.getbodytext(self, decode=0)
745 # XXX If this is big, should remember file pointers
747 def __repr__(self):
748 """String representation."""
749 f, n, fp = self.folder, self.number, self.fp
750 return 'SubMessage(%s, %s, %s)' % (f, n, fp)
752 def getbodytext(self, decode = 1):
753 if not decode:
754 return self.bodyencoded
755 if type(self.body) == type(''):
756 return self.body
758 def getbodyparts(self):
759 if type(self.body) == type([]):
760 return self.body
762 def getbody(self):
763 return self.body
766 class IntSet:
767 """Class implementing sets of integers.
769 This is an efficient representation for sets consisting of several
770 continuous ranges, e.g. 1-100,200-400,402-1000 is represented
771 internally as a list of three pairs: [(1,100), (200,400),
772 (402,1000)]. The internal representation is always kept normalized.
774 The constructor has up to three arguments:
775 - the string used to initialize the set (default ''),
776 - the separator between ranges (default ',')
777 - the separator between begin and end of a range (default '-')
778 The separators must be strings (not regexprs) and should be different.
780 The tostring() function yields a string that can be passed to another
781 IntSet constructor; __repr__() is a valid IntSet constructor itself.
784 # XXX The default begin/end separator means that negative numbers are
785 # not supported very well.
787 # XXX There are currently no operations to remove set elements.
789 def __init__(self, data = None, sep = ',', rng = '-'):
790 self.pairs = []
791 self.sep = sep
792 self.rng = rng
793 if data: self.fromstring(data)
795 def reset(self):
796 self.pairs = []
798 def __cmp__(self, other):
799 return cmp(self.pairs, other.pairs)
801 def __hash__(self):
802 return hash(self.pairs)
804 def __repr__(self):
805 return 'IntSet(%s, %s, %s)' % (`self.tostring()`,
806 `self.sep`, `self.rng`)
808 def normalize(self):
809 self.pairs.sort()
810 i = 1
811 while i < len(self.pairs):
812 alo, ahi = self.pairs[i-1]
813 blo, bhi = self.pairs[i]
814 if ahi >= blo-1:
815 self.pairs[i-1:i+1] = [(alo, max(ahi, bhi))]
816 else:
817 i = i+1
819 def tostring(self):
820 s = ''
821 for lo, hi in self.pairs:
822 if lo == hi: t = `lo`
823 else: t = `lo` + self.rng + `hi`
824 if s: s = s + (self.sep + t)
825 else: s = t
826 return s
828 def tolist(self):
829 l = []
830 for lo, hi in self.pairs:
831 m = range(lo, hi+1)
832 l = l + m
833 return l
835 def fromlist(self, list):
836 for i in list:
837 self.append(i)
839 def clone(self):
840 new = IntSet()
841 new.pairs = self.pairs[:]
842 return new
844 def min(self):
845 return self.pairs[0][0]
847 def max(self):
848 return self.pairs[-1][-1]
850 def contains(self, x):
851 for lo, hi in self.pairs:
852 if lo <= x <= hi: return 1
853 return 0
855 def append(self, x):
856 for i in range(len(self.pairs)):
857 lo, hi = self.pairs[i]
858 if x < lo: # Need to insert before
859 if x+1 == lo:
860 self.pairs[i] = (x, hi)
861 else:
862 self.pairs.insert(i, (x, x))
863 if i > 0 and x-1 == self.pairs[i-1][1]:
864 # Merge with previous
865 self.pairs[i-1:i+1] = [
866 (self.pairs[i-1][0],
867 self.pairs[i][1])
869 return
870 if x <= hi: # Already in set
871 return
872 i = len(self.pairs) - 1
873 if i >= 0:
874 lo, hi = self.pairs[i]
875 if x-1 == hi:
876 self.pairs[i] = lo, x
877 return
878 self.pairs.append((x, x))
880 def addpair(self, xlo, xhi):
881 if xlo > xhi: return
882 self.pairs.append((xlo, xhi))
883 self.normalize()
885 def fromstring(self, data):
886 import string
887 new = []
888 for part in string.splitfields(data, self.sep):
889 list = []
890 for subp in string.splitfields(part, self.rng):
891 s = string.strip(subp)
892 list.append(string.atoi(s))
893 if len(list) == 1:
894 new.append((list[0], list[0]))
895 elif len(list) == 2 and list[0] <= list[1]:
896 new.append((list[0], list[1]))
897 else:
898 raise ValueError, 'bad data passed to IntSet'
899 self.pairs = self.pairs + new
900 self.normalize()
903 # Subroutines to read/write entries in .mh_profile and .mh_sequences
905 def pickline(file, key, casefold = 1):
906 try:
907 f = open(file, 'r')
908 except IOError:
909 return None
910 pat = re.escape(key) + ':'
911 prog = re.compile(pat, casefold and re.IGNORECASE)
912 while 1:
913 line = f.readline()
914 if not line: break
915 if prog.match(line):
916 text = line[len(key)+1:]
917 while 1:
918 line = f.readline()
919 if not line or line[0] not in string.whitespace:
920 break
921 text = text + line
922 return string.strip(text)
923 return None
925 def updateline(file, key, value, casefold = 1):
926 try:
927 f = open(file, 'r')
928 lines = f.readlines()
929 f.close()
930 except IOError:
931 lines = []
932 pat = re.escape(key) + ':(.*)\n'
933 prog = re.compile(pat, casefold and re.IGNORECASE)
934 if value is None:
935 newline = None
936 else:
937 newline = '%s: %s\n' % (key, value)
938 for i in range(len(lines)):
939 line = lines[i]
940 if prog.match(line):
941 if newline is None:
942 del lines[i]
943 else:
944 lines[i] = newline
945 break
946 else:
947 if newline is not None:
948 lines.append(newline)
949 tempfile = file + "~"
950 f = open(tempfile, 'w')
951 for line in lines:
952 f.write(line)
953 f.close()
954 os.rename(tempfile, file)
957 # Test program
959 def test():
960 global mh, f
961 os.system('rm -rf $HOME/Mail/@test')
962 mh = MH()
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()
978 print seqs
979 f.putsequences(seqs)
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)
986 do('f.getcurrent()')
987 for seq in ['first', 'last', 'cur', '.', 'prev', 'next',
988 'first:3', 'last:3', 'cur:3', 'cur:-3',
989 'prev:3', 'next:3',
990 '1:3', '1:-3', '100:3', '100:-3', '10000:3', '10000:-3',
991 'all']:
992 try:
993 do('f.parsesequence(%s)' % `seq`)
994 except Error, msg:
995 print "Error:", msg
996 stuff = os.popen("pick %s 2>/dev/null" % `seq`).read()
997 list = map(string.atoi, string.split(stuff))
998 print list, "<-- pick"
999 do('f.listmessages()')
1002 if __name__ == '__main__':
1003 test()