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