home *** CD-ROM | disk | FTP | other *** search
/ OS/2 Shareware BBS: 10 Tools / 10-Tools.zip / pytho152.zip / emx / lib / python1.5 / mhlib.py < prev    next >
Text File  |  2000-08-10  |  33KB  |  1,007 lines

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