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