0001 """MH interface -- purely object-oriented (well, almost) 0002 0003 Executive summary: 0004 0005 import mhlib 0006 0007 mh = mhlib.MH() # use default mailbox directory and profile 0008 mh = mhlib.MH(mailbox) # override mailbox location (default from profile) 0009 mh = mhlib.MH(mailbox, profile) # override mailbox and profile 0010 0011 mh.error(format, ...) # print error message -- can be overridden 0012 s = mh.getprofile(key) # profile entry (None if not set) 0013 path = mh.getpath() # mailbox pathname 0014 name = mh.getcontext() # name of current folder 0015 mh.setcontext(name) # set name of current folder 0016 0017 list = mh.listfolders() # names of top-level folders 0018 list = mh.listallfolders() # names of all folders, including subfolders 0019 list = mh.listsubfolders(name) # direct subfolders of given folder 0020 list = mh.listallsubfolders(name) # all subfolders of given folder 0021 0022 mh.makefolder(name) # create new folder 0023 mh.deletefolder(name) # delete folder -- must have no subfolders 0024 0025 f = mh.openfolder(name) # new open folder object 0026 0027 f.error(format, ...) # same as mh.error(format, ...) 0028 path = f.getfullname() # folder's full pathname 0029 path = f.getsequencesfilename() # full pathname of folder's sequences file 0030 path = f.getmessagefilename(n) # full pathname of message n in folder 0031 0032 list = f.listmessages() # list of messages in folder (as numbers) 0033 n = f.getcurrent() # get current message 0034 f.setcurrent(n) # set current message 0035 list = f.parsesequence(seq) # parse msgs syntax into list of messages 0036 n = f.getlast() # get last message (0 if no messagse) 0037 f.setlast(n) # set last message (internal use only) 0038 0039 dict = f.getsequences() # dictionary of sequences in folder {name: list} 0040 f.putsequences(dict) # write sequences back to folder 0041 0042 f.createmessage(n, fp) # add message from file f as number n 0043 f.removemessages(list) # remove messages in list from folder 0044 f.refilemessages(list, tofolder) # move messages in list to other folder 0045 f.movemessage(n, tofolder, ton) # move one message to a given destination 0046 f.copymessage(n, tofolder, ton) # copy one message to a given destination 0047 0048 m = f.openmessage(n) # new open message object (costs a file descriptor) 0049 m is a derived class of mimetools.Message(rfc822.Message), with: 0050 s = m.getheadertext() # text of message's headers 0051 s = m.getheadertext(pred) # text of message's headers, filtered by pred 0052 s = m.getbodytext() # text of message's body, decoded 0053 s = m.getbodytext(0) # text of message's body, not decoded 0054 """ 0055 0056 # XXX To do, functionality: 0057 # - annotate messages 0058 # - send messages 0059 # 0060 # XXX To do, organization: 0061 # - move IntSet to separate file 0062 # - move most Message functionality to module mimetools 0063 0064 0065 # Customizable defaults 0066 0067 MH_PROFILE = '~/.mh_profile' 0068 PATH = '~/Mail' 0069 MH_SEQUENCES = '.mh_sequences' 0070 FOLDER_PROTECT = 0700 0071 0072 0073 # Imported modules 0074 0075 import os 0076 import sys 0077 import re 0078 import mimetools 0079 import multifile 0080 import shutil 0081 from bisect import bisect 0082 0083 __all__ = ["MH","Error","Folder","Message"] 0084 0085 # Exported constants 0086 0087 class Error(Exception): 0088 pass 0089 0090 0091 class MH: 0092 """Class representing a particular collection of folders. 0093 Optional constructor arguments are the pathname for the directory 0094 containing the collection, and the MH profile to use. 0095 If either is omitted or empty a default is used; the default 0096 directory is taken from the MH profile if it is specified there.""" 0097 0098 def __init__(self, path = None, profile = None): 0099 """Constructor.""" 0100 if profile is None: profile = MH_PROFILE 0101 self.profile = os.path.expanduser(profile) 0102 if path is None: path = self.getprofile('Path') 0103 if not path: path = PATH 0104 if not os.path.isabs(path) and path[0] != '~': 0105 path = os.path.join('~', path) 0106 path = os.path.expanduser(path) 0107 if not os.path.isdir(path): raise Error, 'MH() path not found' 0108 self.path = path 0109 0110 def __repr__(self): 0111 """String representation.""" 0112 return 'MH(%r, %r)' % (self.path, self.profile) 0113 0114 def error(self, msg, *args): 0115 """Routine to print an error. May be overridden by a derived class.""" 0116 sys.stderr.write('MH error: %s\n' % (msg % args)) 0117 0118 def getprofile(self, key): 0119 """Return a profile entry, None if not found.""" 0120 return pickline(self.profile, key) 0121 0122 def getpath(self): 0123 """Return the path (the name of the collection's directory).""" 0124 return self.path 0125 0126 def getcontext(self): 0127 """Return the name of the current folder.""" 0128 context = pickline(os.path.join(self.getpath(), 'context'), 0129 'Current-Folder') 0130 if not context: context = 'inbox' 0131 return context 0132 0133 def setcontext(self, context): 0134 """Set the name of the current folder.""" 0135 fn = os.path.join(self.getpath(), 'context') 0136 f = open(fn, "w") 0137 f.write("Current-Folder: %s\n" % context) 0138 f.close() 0139 0140 def listfolders(self): 0141 """Return the names of the top-level folders.""" 0142 folders = [] 0143 path = self.getpath() 0144 for name in os.listdir(path): 0145 fullname = os.path.join(path, name) 0146 if os.path.isdir(fullname): 0147 folders.append(name) 0148 folders.sort() 0149 return folders 0150 0151 def listsubfolders(self, name): 0152 """Return the names of the subfolders in a given folder 0153 (prefixed with the given folder name).""" 0154 fullname = os.path.join(self.path, name) 0155 # Get the link count so we can avoid listing folders 0156 # that have no subfolders. 0157 nlinks = os.stat(fullname).st_nlink 0158 if nlinks <= 2: 0159 return [] 0160 subfolders = [] 0161 subnames = os.listdir(fullname) 0162 for subname in subnames: 0163 fullsubname = os.path.join(fullname, subname) 0164 if os.path.isdir(fullsubname): 0165 name_subname = os.path.join(name, subname) 0166 subfolders.append(name_subname) 0167 # Stop looking for subfolders when 0168 # we've seen them all 0169 nlinks = nlinks - 1 0170 if nlinks <= 2: 0171 break 0172 subfolders.sort() 0173 return subfolders 0174 0175 def listallfolders(self): 0176 """Return the names of all folders and subfolders, recursively.""" 0177 return self.listallsubfolders('') 0178 0179 def listallsubfolders(self, name): 0180 """Return the names of subfolders in a given folder, recursively.""" 0181 fullname = os.path.join(self.path, name) 0182 # Get the link count so we can avoid listing folders 0183 # that have no subfolders. 0184 nlinks = os.stat(fullname).st_nlink 0185 if nlinks <= 2: 0186 return [] 0187 subfolders = [] 0188 subnames = os.listdir(fullname) 0189 for subname in subnames: 0190 if subname[0] == ',' or isnumeric(subname): continue 0191 fullsubname = os.path.join(fullname, subname) 0192 if os.path.isdir(fullsubname): 0193 name_subname = os.path.join(name, subname) 0194 subfolders.append(name_subname) 0195 if not os.path.islink(fullsubname): 0196 subsubfolders = self.listallsubfolders( 0197 name_subname) 0198 subfolders = subfolders + subsubfolders 0199 # Stop looking for subfolders when 0200 # we've seen them all 0201 nlinks = nlinks - 1 0202 if nlinks <= 2: 0203 break 0204 subfolders.sort() 0205 return subfolders 0206 0207 def openfolder(self, name): 0208 """Return a new Folder object for the named folder.""" 0209 return Folder(self, name) 0210 0211 def makefolder(self, name): 0212 """Create a new folder (or raise os.error if it cannot be created).""" 0213 protect = pickline(self.profile, 'Folder-Protect') 0214 if protect and isnumeric(protect): 0215 mode = int(protect, 8) 0216 else: 0217 mode = FOLDER_PROTECT 0218 os.mkdir(os.path.join(self.getpath(), name), mode) 0219 0220 def deletefolder(self, name): 0221 """Delete a folder. This removes files in the folder but not 0222 subdirectories. Raise os.error if deleting the folder itself fails.""" 0223 fullname = os.path.join(self.getpath(), name) 0224 for subname in os.listdir(fullname): 0225 fullsubname = os.path.join(fullname, subname) 0226 try: 0227 os.unlink(fullsubname) 0228 except os.error: 0229 self.error('%s not deleted, continuing...' % 0230 fullsubname) 0231 os.rmdir(fullname) 0232 0233 0234 numericprog = re.compile('^[1-9][0-9]*$') 0235 def isnumeric(str): 0236 return numericprog.match(str) is not None 0237 0238 class Folder: 0239 """Class representing a particular folder.""" 0240 0241 def __init__(self, mh, name): 0242 """Constructor.""" 0243 self.mh = mh 0244 self.name = name 0245 if not os.path.isdir(self.getfullname()): 0246 raise Error, 'no folder %s' % name 0247 0248 def __repr__(self): 0249 """String representation.""" 0250 return 'Folder(%r, %r)' % (self.mh, self.name) 0251 0252 def error(self, *args): 0253 """Error message handler.""" 0254 self.mh.error(*args) 0255 0256 def getfullname(self): 0257 """Return the full pathname of the folder.""" 0258 return os.path.join(self.mh.path, self.name) 0259 0260 def getsequencesfilename(self): 0261 """Return the full pathname of the folder's sequences file.""" 0262 return os.path.join(self.getfullname(), MH_SEQUENCES) 0263 0264 def getmessagefilename(self, n): 0265 """Return the full pathname of a message in the folder.""" 0266 return os.path.join(self.getfullname(), str(n)) 0267 0268 def listsubfolders(self): 0269 """Return list of direct subfolders.""" 0270 return self.mh.listsubfolders(self.name) 0271 0272 def listallsubfolders(self): 0273 """Return list of all subfolders.""" 0274 return self.mh.listallsubfolders(self.name) 0275 0276 def listmessages(self): 0277 """Return the list of messages currently present in the folder. 0278 As a side effect, set self.last to the last message (or 0).""" 0279 messages = [] 0280 match = numericprog.match 0281 append = messages.append 0282 for name in os.listdir(self.getfullname()): 0283 if match(name): 0284 append(name) 0285 messages = map(int, messages) 0286 messages.sort() 0287 if messages: 0288 self.last = messages[-1] 0289 else: 0290 self.last = 0 0291 return messages 0292 0293 def getsequences(self): 0294 """Return the set of sequences for the folder.""" 0295 sequences = {} 0296 fullname = self.getsequencesfilename() 0297 try: 0298 f = open(fullname, 'r') 0299 except IOError: 0300 return sequences 0301 while 1: 0302 line = f.readline() 0303 if not line: break 0304 fields = line.split(':') 0305 if len(fields) != 2: 0306 self.error('bad sequence in %s: %s' % 0307 (fullname, line.strip())) 0308 key = fields[0].strip() 0309 value = IntSet(fields[1].strip(), ' ').tolist() 0310 sequences[key] = value 0311 return sequences 0312 0313 def putsequences(self, sequences): 0314 """Write the set of sequences back to the folder.""" 0315 fullname = self.getsequencesfilename() 0316 f = None 0317 for key, seq in sequences.iteritems(): 0318 s = IntSet('', ' ') 0319 s.fromlist(seq) 0320 if not f: f = open(fullname, 'w') 0321 f.write('%s: %s\n' % (key, s.tostring())) 0322 if not f: 0323 try: 0324 os.unlink(fullname) 0325 except os.error: 0326 pass 0327 else: 0328 f.close() 0329 0330 def getcurrent(self): 0331 """Return the current message. Raise Error when there is none.""" 0332 seqs = self.getsequences() 0333 try: 0334 return max(seqs['cur']) 0335 except (ValueError, KeyError): 0336 raise Error, "no cur message" 0337 0338 def setcurrent(self, n): 0339 """Set the current message.""" 0340 updateline(self.getsequencesfilename(), 'cur', str(n), 0) 0341 0342 def parsesequence(self, seq): 0343 """Parse an MH sequence specification into a message list. 0344 Attempt to mimic mh-sequence(5) as close as possible. 0345 Also attempt to mimic observed behavior regarding which 0346 conditions cause which error messages.""" 0347 # XXX Still not complete (see mh-format(5)). 0348 # Missing are: 0349 # - 'prev', 'next' as count 0350 # - Sequence-Negation option 0351 all = self.listmessages() 0352 # Observed behavior: test for empty folder is done first 0353 if not all: 0354 raise Error, "no messages in %s" % self.name 0355 # Common case first: all is frequently the default 0356 if seq == 'all': 0357 return all 0358 # Test for X:Y before X-Y because 'seq:-n' matches both 0359 i = seq.find(':') 0360 if i >= 0: 0361 head, dir, tail = seq[:i], '', seq[i+1:] 0362 if tail[:1] in '-+': 0363 dir, tail = tail[:1], tail[1:] 0364 if not isnumeric(tail): 0365 raise Error, "bad message list %s" % seq 0366 try: 0367 count = int(tail) 0368 except (ValueError, OverflowError): 0369 # Can't use sys.maxint because of i+count below 0370 count = len(all) 0371 try: 0372 anchor = self._parseindex(head, all) 0373 except Error, msg: 0374 seqs = self.getsequences() 0375 if not head in seqs: 0376 if not msg: 0377 msg = "bad message list %s" % seq 0378 raise Error, msg, sys.exc_info()[2] 0379 msgs = seqs[head] 0380 if not msgs: 0381 raise Error, "sequence %s empty" % head 0382 if dir == '-': 0383 return msgs[-count:] 0384 else: 0385 return msgs[:count] 0386 else: 0387 if not dir: 0388 if head in ('prev', 'last'): 0389 dir = '-' 0390 if dir == '-': 0391 i = bisect(all, anchor) 0392 return all[max(0, i-count):i] 0393 else: 0394 i = bisect(all, anchor-1) 0395 return all[i:i+count] 0396 # Test for X-Y next 0397 i = seq.find('-') 0398 if i >= 0: 0399 begin = self._parseindex(seq[:i], all) 0400 end = self._parseindex(seq[i+1:], all) 0401 i = bisect(all, begin-1) 0402 j = bisect(all, end) 0403 r = all[i:j] 0404 if not r: 0405 raise Error, "bad message list %s" % seq 0406 return r 0407 # Neither X:Y nor X-Y; must be a number or a (pseudo-)sequence 0408 try: 0409 n = self._parseindex(seq, all) 0410 except Error, msg: 0411 seqs = self.getsequences() 0412 if not seq in seqs: 0413 if not msg: 0414 msg = "bad message list %s" % seq 0415 raise Error, msg 0416 return seqs[seq] 0417 else: 0418 if n not in all: 0419 if isnumeric(seq): 0420 raise Error, "message %d doesn't exist" % n 0421 else: 0422 raise Error, "no %s message" % seq 0423 else: 0424 return [n] 0425 0426 def _parseindex(self, seq, all): 0427 """Internal: parse a message number (or cur, first, etc.).""" 0428 if isnumeric(seq): 0429 try: 0430 return int(seq) 0431 except (OverflowError, ValueError): 0432 return sys.maxint 0433 if seq in ('cur', '.'): 0434 return self.getcurrent() 0435 if seq == 'first': 0436 return all[0] 0437 if seq == 'last': 0438 return all[-1] 0439 if seq == 'next': 0440 n = self.getcurrent() 0441 i = bisect(all, n) 0442 try: 0443 return all[i] 0444 except IndexError: 0445 raise Error, "no next message" 0446 if seq == 'prev': 0447 n = self.getcurrent() 0448 i = bisect(all, n-1) 0449 if i == 0: 0450 raise Error, "no prev message" 0451 try: 0452 return all[i-1] 0453 except IndexError: 0454 raise Error, "no prev message" 0455 raise Error, None 0456 0457 def openmessage(self, n): 0458 """Open a message -- returns a Message object.""" 0459 return Message(self, n) 0460 0461 def removemessages(self, list): 0462 """Remove one or more messages -- may raise os.error.""" 0463 errors = [] 0464 deleted = [] 0465 for n in list: 0466 path = self.getmessagefilename(n) 0467 commapath = self.getmessagefilename(',' + str(n)) 0468 try: 0469 os.unlink(commapath) 0470 except os.error: 0471 pass 0472 try: 0473 os.rename(path, commapath) 0474 except os.error, msg: 0475 errors.append(msg) 0476 else: 0477 deleted.append(n) 0478 if deleted: 0479 self.removefromallsequences(deleted) 0480 if errors: 0481 if len(errors) == 1: 0482 raise os.error, errors[0] 0483 else: 0484 raise os.error, ('multiple errors:', errors) 0485 0486 def refilemessages(self, list, tofolder, keepsequences=0): 0487 """Refile one or more messages -- may raise os.error. 0488 'tofolder' is an open folder object.""" 0489 errors = [] 0490 refiled = {} 0491 for n in list: 0492 ton = tofolder.getlast() + 1 0493 path = self.getmessagefilename(n) 0494 topath = tofolder.getmessagefilename(ton) 0495 try: 0496 os.rename(path, topath) 0497 except os.error: 0498 # Try copying 0499 try: 0500 shutil.copy2(path, topath) 0501 os.unlink(path) 0502 except (IOError, os.error), msg: 0503 errors.append(msg) 0504 try: 0505 os.unlink(topath) 0506 except os.error: 0507 pass 0508 continue 0509 tofolder.setlast(ton) 0510 refiled[n] = ton 0511 if refiled: 0512 if keepsequences: 0513 tofolder._copysequences(self, refiled.items()) 0514 self.removefromallsequences(refiled.keys()) 0515 if errors: 0516 if len(errors) == 1: 0517 raise os.error, errors[0] 0518 else: 0519 raise os.error, ('multiple errors:', errors) 0520 0521 def _copysequences(self, fromfolder, refileditems): 0522 """Helper for refilemessages() to copy sequences.""" 0523 fromsequences = fromfolder.getsequences() 0524 tosequences = self.getsequences() 0525 changed = 0 0526 for name, seq in fromsequences.items(): 0527 try: 0528 toseq = tosequences[name] 0529 new = 0 0530 except KeyError: 0531 toseq = [] 0532 new = 1 0533 for fromn, ton in refileditems: 0534 if fromn in seq: 0535 toseq.append(ton) 0536 changed = 1 0537 if new and toseq: 0538 tosequences[name] = toseq 0539 if changed: 0540 self.putsequences(tosequences) 0541 0542 def movemessage(self, n, tofolder, ton): 0543 """Move one message over a specific destination message, 0544 which may or may not already exist.""" 0545 path = self.getmessagefilename(n) 0546 # Open it to check that it exists 0547 f = open(path) 0548 f.close() 0549 del f 0550 topath = tofolder.getmessagefilename(ton) 0551 backuptopath = tofolder.getmessagefilename(',%d' % ton) 0552 try: 0553 os.rename(topath, backuptopath) 0554 except os.error: 0555 pass 0556 try: 0557 os.rename(path, topath) 0558 except os.error: 0559 # Try copying 0560 ok = 0 0561 try: 0562 tofolder.setlast(None) 0563 shutil.copy2(path, topath) 0564 ok = 1 0565 finally: 0566 if not ok: 0567 try: 0568 os.unlink(topath) 0569 except os.error: 0570 pass 0571 os.unlink(path) 0572 self.removefromallsequences([n]) 0573 0574 def copymessage(self, n, tofolder, ton): 0575 """Copy one message over a specific destination message, 0576 which may or may not already exist.""" 0577 path = self.getmessagefilename(n) 0578 # Open it to check that it exists 0579 f = open(path) 0580 f.close() 0581 del f 0582 topath = tofolder.getmessagefilename(ton) 0583 backuptopath = tofolder.getmessagefilename(',%d' % ton) 0584 try: 0585 os.rename(topath, backuptopath) 0586 except os.error: 0587 pass 0588 ok = 0 0589 try: 0590 tofolder.setlast(None) 0591 shutil.copy2(path, topath) 0592 ok = 1 0593 finally: 0594 if not ok: 0595 try: 0596 os.unlink(topath) 0597 except os.error: 0598 pass 0599 0600 def createmessage(self, n, txt): 0601 """Create a message, with text from the open file txt.""" 0602 path = self.getmessagefilename(n) 0603 backuppath = self.getmessagefilename(',%d' % n) 0604 try: 0605 os.rename(path, backuppath) 0606 except os.error: 0607 pass 0608 ok = 0 0609 BUFSIZE = 16*1024 0610 try: 0611 f = open(path, "w") 0612 while 1: 0613 buf = txt.read(BUFSIZE) 0614 if not buf: 0615 break 0616 f.write(buf) 0617 f.close() 0618 ok = 1 0619 finally: 0620 if not ok: 0621 try: 0622 os.unlink(path) 0623 except os.error: 0624 pass 0625 0626 def removefromallsequences(self, list): 0627 """Remove one or more messages from all sequences (including last) 0628 -- but not from 'cur'!!!""" 0629 if hasattr(self, 'last') and self.last in list: 0630 del self.last 0631 sequences = self.getsequences() 0632 changed = 0 0633 for name, seq in sequences.items(): 0634 if name == 'cur': 0635 continue 0636 for n in list: 0637 if n in seq: 0638 seq.remove(n) 0639 changed = 1 0640 if not seq: 0641 del sequences[name] 0642 if changed: 0643 self.putsequences(sequences) 0644 0645 def getlast(self): 0646 """Return the last message number.""" 0647 if not hasattr(self, 'last'): 0648 self.listmessages() # Set self.last 0649 return self.last 0650 0651 def setlast(self, last): 0652 """Set the last message number.""" 0653 if last is None: 0654 if hasattr(self, 'last'): 0655 del self.last 0656 else: 0657 self.last = last 0658 0659 class Message(mimetools.Message): 0660 0661 def __init__(self, f, n, fp = None): 0662 """Constructor.""" 0663 self.folder = f 0664 self.number = n 0665 if fp is None: 0666 path = f.getmessagefilename(n) 0667 fp = open(path, 'r') 0668 mimetools.Message.__init__(self, fp) 0669 0670 def __repr__(self): 0671 """String representation.""" 0672 return 'Message(%s, %s)' % (repr(self.folder), self.number) 0673 0674 def getheadertext(self, pred = None): 0675 """Return the message's header text as a string. If an 0676 argument is specified, it is used as a filter predicate to 0677 decide which headers to return (its argument is the header 0678 name converted to lower case).""" 0679 if pred is None: 0680 return ''.join(self.headers) 0681 headers = [] 0682 hit = 0 0683 for line in self.headers: 0684 if not line[0].isspace(): 0685 i = line.find(':') 0686 if i > 0: 0687 hit = pred(line[:i].lower()) 0688 if hit: headers.append(line) 0689 return ''.join(headers) 0690 0691 def getbodytext(self, decode = 1): 0692 """Return the message's body text as string. This undoes a 0693 Content-Transfer-Encoding, but does not interpret other MIME 0694 features (e.g. multipart messages). To suppress decoding, 0695 pass 0 as an argument.""" 0696 self.fp.seek(self.startofbody) 0697 encoding = self.getencoding() 0698 if not decode or encoding in ('', '7bit', '8bit', 'binary'): 0699 return self.fp.read() 0700 from StringIO import StringIO 0701 output = StringIO() 0702 mimetools.decode(self.fp, output, encoding) 0703 return output.getvalue() 0704 0705 def getbodyparts(self): 0706 """Only for multipart messages: return the message's body as a 0707 list of SubMessage objects. Each submessage object behaves 0708 (almost) as a Message object.""" 0709 if self.getmaintype() != 'multipart': 0710 raise Error, 'Content-Type is not multipart/*' 0711 bdry = self.getparam('boundary') 0712 if not bdry: 0713 raise Error, 'multipart/* without boundary param' 0714 self.fp.seek(self.startofbody) 0715 mf = multifile.MultiFile(self.fp) 0716 mf.push(bdry) 0717 parts = [] 0718 while mf.next(): 0719 n = "%s.%r" % (self.number, 1 + len(parts)) 0720 part = SubMessage(self.folder, n, mf) 0721 parts.append(part) 0722 mf.pop() 0723 return parts 0724 0725 def getbody(self): 0726 """Return body, either a string or a list of messages.""" 0727 if self.getmaintype() == 'multipart': 0728 return self.getbodyparts() 0729 else: 0730 return self.getbodytext() 0731 0732 0733 class SubMessage(Message): 0734 0735 def __init__(self, f, n, fp): 0736 """Constructor.""" 0737 Message.__init__(self, f, n, fp) 0738 if self.getmaintype() == 'multipart': 0739 self.body = Message.getbodyparts(self) 0740 else: 0741 self.body = Message.getbodytext(self) 0742 self.bodyencoded = Message.getbodytext(self, decode=0) 0743 # XXX If this is big, should remember file pointers 0744 0745 def __repr__(self): 0746 """String representation.""" 0747 f, n, fp = self.folder, self.number, self.fp 0748 return 'SubMessage(%s, %s, %s)' % (f, n, fp) 0749 0750 def getbodytext(self, decode = 1): 0751 if not decode: 0752 return self.bodyencoded 0753 if type(self.body) == type(''): 0754 return self.body 0755 0756 def getbodyparts(self): 0757 if type(self.body) == type([]): 0758 return self.body 0759 0760 def getbody(self): 0761 return self.body 0762 0763 0764 class IntSet: 0765 """Class implementing sets of integers. 0766 0767 This is an efficient representation for sets consisting of several 0768 continuous ranges, e.g. 1-100,200-400,402-1000 is represented 0769 internally as a list of three pairs: [(1,100), (200,400), 0770 (402,1000)]. The internal representation is always kept normalized. 0771 0772 The constructor has up to three arguments: 0773 - the string used to initialize the set (default ''), 0774 - the separator between ranges (default ',') 0775 - the separator between begin and end of a range (default '-') 0776 The separators must be strings (not regexprs) and should be different. 0777 0778 The tostring() function yields a string that can be passed to another 0779 IntSet constructor; __repr__() is a valid IntSet constructor itself. 0780 """ 0781 0782 # XXX The default begin/end separator means that negative numbers are 0783 # not supported very well. 0784 # 0785 # XXX There are currently no operations to remove set elements. 0786 0787 def __init__(self, data = None, sep = ',', rng = '-'): 0788 self.pairs = [] 0789 self.sep = sep 0790 self.rng = rng 0791 if data: self.fromstring(data) 0792 0793 def reset(self): 0794 self.pairs = [] 0795 0796 def __cmp__(self, other): 0797 return cmp(self.pairs, other.pairs) 0798 0799 def __hash__(self): 0800 return hash(self.pairs) 0801 0802 def __repr__(self): 0803 return 'IntSet(%r, %r, %r)' % (self.tostring(), self.sep, self.rng) 0804 0805 def normalize(self): 0806 self.pairs.sort() 0807 i = 1 0808 while i < len(self.pairs): 0809 alo, ahi = self.pairs[i-1] 0810 blo, bhi = self.pairs[i] 0811 if ahi >= blo-1: 0812 self.pairs[i-1:i+1] = [(alo, max(ahi, bhi))] 0813 else: 0814 i = i+1 0815 0816 def tostring(self): 0817 s = '' 0818 for lo, hi in self.pairs: 0819 if lo == hi: t = repr(lo) 0820 else: t = repr(lo) + self.rng + repr(hi) 0821 if s: s = s + (self.sep + t) 0822 else: s = t 0823 return s 0824 0825 def tolist(self): 0826 l = [] 0827 for lo, hi in self.pairs: 0828 m = range(lo, hi+1) 0829 l = l + m 0830 return l 0831 0832 def fromlist(self, list): 0833 for i in list: 0834 self.append(i) 0835 0836 def clone(self): 0837 new = IntSet() 0838 new.pairs = self.pairs[:] 0839 return new 0840 0841 def min(self): 0842 return self.pairs[0][0] 0843 0844 def max(self): 0845 return self.pairs[-1][-1] 0846 0847 def contains(self, x): 0848 for lo, hi in self.pairs: 0849 if lo <= x <= hi: return True 0850 return False 0851 0852 def append(self, x): 0853 for i in range(len(self.pairs)): 0854 lo, hi = self.pairs[i] 0855 if x < lo: # Need to insert before 0856 if x+1 == lo: 0857 self.pairs[i] = (x, hi) 0858 else: 0859 self.pairs.insert(i, (x, x)) 0860 if i > 0 and x-1 == self.pairs[i-1][1]: 0861 # Merge with previous 0862 self.pairs[i-1:i+1] = [ 0863 (self.pairs[i-1][0], 0864 self.pairs[i][1]) 0865 ] 0866 return 0867 if x <= hi: # Already in set 0868 return 0869 i = len(self.pairs) - 1 0870 if i >= 0: 0871 lo, hi = self.pairs[i] 0872 if x-1 == hi: 0873 self.pairs[i] = lo, x 0874 return 0875 self.pairs.append((x, x)) 0876 0877 def addpair(self, xlo, xhi): 0878 if xlo > xhi: return 0879 self.pairs.append((xlo, xhi)) 0880 self.normalize() 0881 0882 def fromstring(self, data): 0883 new = [] 0884 for part in data.split(self.sep): 0885 list = [] 0886 for subp in part.split(self.rng): 0887 s = subp.strip() 0888 list.append(int(s)) 0889 if len(list) == 1: 0890 new.append((list[0], list[0])) 0891 elif len(list) == 2 and list[0] <= list[1]: 0892 new.append((list[0], list[1])) 0893 else: 0894 raise ValueError, 'bad data passed to IntSet' 0895 self.pairs = self.pairs + new 0896 self.normalize() 0897 0898 0899 # Subroutines to read/write entries in .mh_profile and .mh_sequences 0900 0901 def pickline(file, key, casefold = 1): 0902 try: 0903 f = open(file, 'r') 0904 except IOError: 0905 return None 0906 pat = re.escape(key) + ':' 0907 prog = re.compile(pat, casefold and re.IGNORECASE) 0908 while 1: 0909 line = f.readline() 0910 if not line: break 0911 if prog.match(line): 0912 text = line[len(key)+1:] 0913 while 1: 0914 line = f.readline() 0915 if not line or not line[0].isspace(): 0916 break 0917 text = text + line 0918 return text.strip() 0919 return None 0920 0921 def updateline(file, key, value, casefold = 1): 0922 try: 0923 f = open(file, 'r') 0924 lines = f.readlines() 0925 f.close() 0926 except IOError: 0927 lines = [] 0928 pat = re.escape(key) + ':(.*)\n' 0929 prog = re.compile(pat, casefold and re.IGNORECASE) 0930 if value is None: 0931 newline = None 0932 else: 0933 newline = '%s: %s\n' % (key, value) 0934 for i in range(len(lines)): 0935 line = lines[i] 0936 if prog.match(line): 0937 if newline is None: 0938 del lines[i] 0939 else: 0940 lines[i] = newline 0941 break 0942 else: 0943 if newline is not None: 0944 lines.append(newline) 0945 tempfile = file + "~" 0946 f = open(tempfile, 'w') 0947 for line in lines: 0948 f.write(line) 0949 f.close() 0950 os.rename(tempfile, file) 0951 0952 0953 # Test program 0954 0955 def test(): 0956 global mh, f 0957 os.system('rm -rf $HOME/Mail/@test') 0958 mh = MH() 0959 def do(s): print s; print eval(s) 0960 do('mh.listfolders()') 0961 do('mh.listallfolders()') 0962 testfolders = ['@test', '@test/test1', '@test/test2', 0963 '@test/test1/test11', '@test/test1/test12', 0964 '@test/test1/test11/test111'] 0965 for t in testfolders: do('mh.makefolder(%r)' % (t,)) 0966 do('mh.listsubfolders(\'@test\')') 0967 do('mh.listallsubfolders(\'@test\')') 0968 f = mh.openfolder('@test') 0969 do('f.listsubfolders()') 0970 do('f.listallsubfolders()') 0971 do('f.getsequences()') 0972 seqs = f.getsequences() 0973 seqs['foo'] = IntSet('1-10 12-20', ' ').tolist() 0974 print seqs 0975 f.putsequences(seqs) 0976 do('f.getsequences()') 0977 for t in reversed(testfolders): do('mh.deletefolder(%r)' % (t,)) 0978 do('mh.getcontext()') 0979 context = mh.getcontext() 0980 f = mh.openfolder(context) 0981 do('f.getcurrent()') 0982 for seq in ['first', 'last', 'cur', '.', 'prev', 'next', 0983 'first:3', 'last:3', 'cur:3', 'cur:-3', 0984 'prev:3', 'next:3', 0985 '1:3', '1:-3', '100:3', '100:-3', '10000:3', '10000:-3', 0986 'all']: 0987 try: 0988 do('f.parsesequence(%r)' % (seq,)) 0989 except Error, msg: 0990 print "Error:", msg 0991 stuff = os.popen("pick %r 2>/dev/null" % (seq,)).read() 0992 list = map(int, stuff.split()) 0993 print list, "<-- pick" 0994 do('f.listmessages()') 0995 0996 0997 if __name__ == '__main__': 0998 test() 0999
Generated by PyXR 0.9.4