home *** CD-ROM | disk | FTP | other *** search
/ OS/2 Shareware BBS: 10 Tools / 10-Tools.zip / pyos2bin.zip / Lib / mhlib.py < prev    next >
Text File  |  1997-12-24  |  27KB  |  1,003 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.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.  
  65. MH_PROFILE = '~/.mh_profile'
  66. PATH = '~/Mail'
  67. MH_SEQUENCES = '.mh_sequences'
  68. FOLDER_PROTECT = 0700
  69.  
  70.  
  71. # Imported modules
  72.  
  73. import os
  74. import sys
  75. from stat import ST_NLINK
  76. import re
  77. import string
  78. import mimetools
  79. import multifile
  80. import shutil
  81. from bisect import bisect
  82.  
  83.  
  84. # Exported constants
  85.  
  86. Error = '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.  
  95. class 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.  
  239. numericprog = re.compile('^[1-9][0-9]*$')
  240. def isnumeric(str):
  241.     return numericprog.match(str) is not None
  242.  
  243. class 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) >= 0:
  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.  
  663. class 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.  
  737. class 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.  
  786. class 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.  
  904. def 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.  
  924. def 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.  
  958. def 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.  
  1001. if __name__ == '__main__':
  1002.     test()
  1003.