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