home *** CD-ROM | disk | FTP | other *** search
/ SGI Freeware 1999 August / SGI Freeware 1999 August.iso / dist / fw_python.idb / usr / freeware / lib / python1.5 / mhlib.py.z / mhlib.py
Encoding:
Python Source  |  1999-04-16  |  32.1 KB  |  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):
  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.