python-2.5.2/win32/Lib/mhlib.py
changeset 0 ae805ac0140d
equal deleted inserted replaced
-1:000000000000 0:ae805ac0140d
       
     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 import re
       
    78 import mimetools
       
    79 import multifile
       
    80 import shutil
       
    81 from bisect import bisect
       
    82 
       
    83 __all__ = ["MH","Error","Folder","Message"]
       
    84 
       
    85 # Exported constants
       
    86 
       
    87 class Error(Exception):
       
    88     pass
       
    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 profile is None: profile = MH_PROFILE
       
   101         self.profile = os.path.expanduser(profile)
       
   102         if path is None: 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(%r, %r)' % (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         nlinks = os.stat(fullname).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     def listallfolders(self):
       
   176         """Return the names of all folders and subfolders, recursively."""
       
   177         return self.listallsubfolders('')
       
   178 
       
   179     def listallsubfolders(self, name):
       
   180         """Return the names of subfolders in a given folder, recursively."""
       
   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         nlinks = os.stat(fullname).st_nlink
       
   185         if nlinks <= 2:
       
   186             return []
       
   187         subfolders = []
       
   188         subnames = os.listdir(fullname)
       
   189         for subname in subnames:
       
   190             if subname[0] == ',' or isnumeric(subname): continue
       
   191             fullsubname = os.path.join(fullname, subname)
       
   192             if os.path.isdir(fullsubname):
       
   193                 name_subname = os.path.join(name, subname)
       
   194                 subfolders.append(name_subname)
       
   195                 if not os.path.islink(fullsubname):
       
   196                     subsubfolders = self.listallsubfolders(
       
   197                               name_subname)
       
   198                     subfolders = subfolders + subsubfolders
       
   199                 # Stop looking for subfolders when
       
   200                 # we've seen them all
       
   201                 nlinks = nlinks - 1
       
   202                 if nlinks <= 2:
       
   203                     break
       
   204         subfolders.sort()
       
   205         return subfolders
       
   206 
       
   207     def openfolder(self, name):
       
   208         """Return a new Folder object for the named folder."""
       
   209         return Folder(self, name)
       
   210 
       
   211     def makefolder(self, name):
       
   212         """Create a new folder (or raise os.error if it cannot be created)."""
       
   213         protect = pickline(self.profile, 'Folder-Protect')
       
   214         if protect and isnumeric(protect):
       
   215             mode = int(protect, 8)
       
   216         else:
       
   217             mode = FOLDER_PROTECT
       
   218         os.mkdir(os.path.join(self.getpath(), name), mode)
       
   219 
       
   220     def deletefolder(self, name):
       
   221         """Delete a folder.  This removes files in the folder but not
       
   222         subdirectories.  Raise os.error if deleting the folder itself fails."""
       
   223         fullname = os.path.join(self.getpath(), name)
       
   224         for subname in os.listdir(fullname):
       
   225             fullsubname = os.path.join(fullname, subname)
       
   226             try:
       
   227                 os.unlink(fullsubname)
       
   228             except os.error:
       
   229                 self.error('%s not deleted, continuing...' %
       
   230                           fullsubname)
       
   231         os.rmdir(fullname)
       
   232 
       
   233 
       
   234 numericprog = re.compile('^[1-9][0-9]*$')
       
   235 def isnumeric(str):
       
   236     return numericprog.match(str) is not None
       
   237 
       
   238 class Folder:
       
   239     """Class representing a particular folder."""
       
   240 
       
   241     def __init__(self, mh, name):
       
   242         """Constructor."""
       
   243         self.mh = mh
       
   244         self.name = name
       
   245         if not os.path.isdir(self.getfullname()):
       
   246             raise Error, 'no folder %s' % name
       
   247 
       
   248     def __repr__(self):
       
   249         """String representation."""
       
   250         return 'Folder(%r, %r)' % (self.mh, self.name)
       
   251 
       
   252     def error(self, *args):
       
   253         """Error message handler."""
       
   254         self.mh.error(*args)
       
   255 
       
   256     def getfullname(self):
       
   257         """Return the full pathname of the folder."""
       
   258         return os.path.join(self.mh.path, self.name)
       
   259 
       
   260     def getsequencesfilename(self):
       
   261         """Return the full pathname of the folder's sequences file."""
       
   262         return os.path.join(self.getfullname(), MH_SEQUENCES)
       
   263 
       
   264     def getmessagefilename(self, n):
       
   265         """Return the full pathname of a message in the folder."""
       
   266         return os.path.join(self.getfullname(), str(n))
       
   267 
       
   268     def listsubfolders(self):
       
   269         """Return list of direct subfolders."""
       
   270         return self.mh.listsubfolders(self.name)
       
   271 
       
   272     def listallsubfolders(self):
       
   273         """Return list of all subfolders."""
       
   274         return self.mh.listallsubfolders(self.name)
       
   275 
       
   276     def listmessages(self):
       
   277         """Return the list of messages currently present in the folder.
       
   278         As a side effect, set self.last to the last message (or 0)."""
       
   279         messages = []
       
   280         match = numericprog.match
       
   281         append = messages.append
       
   282         for name in os.listdir(self.getfullname()):
       
   283             if match(name):
       
   284                 append(name)
       
   285         messages = map(int, messages)
       
   286         messages.sort()
       
   287         if messages:
       
   288             self.last = messages[-1]
       
   289         else:
       
   290             self.last = 0
       
   291         return messages
       
   292 
       
   293     def getsequences(self):
       
   294         """Return the set of sequences for the folder."""
       
   295         sequences = {}
       
   296         fullname = self.getsequencesfilename()
       
   297         try:
       
   298             f = open(fullname, 'r')
       
   299         except IOError:
       
   300             return sequences
       
   301         while 1:
       
   302             line = f.readline()
       
   303             if not line: break
       
   304             fields = line.split(':')
       
   305             if len(fields) != 2:
       
   306                 self.error('bad sequence in %s: %s' %
       
   307                           (fullname, line.strip()))
       
   308             key = fields[0].strip()
       
   309             value = IntSet(fields[1].strip(), ' ').tolist()
       
   310             sequences[key] = value
       
   311         return sequences
       
   312 
       
   313     def putsequences(self, sequences):
       
   314         """Write the set of sequences back to the folder."""
       
   315         fullname = self.getsequencesfilename()
       
   316         f = None
       
   317         for key, seq in sequences.iteritems():
       
   318             s = IntSet('', ' ')
       
   319             s.fromlist(seq)
       
   320             if not f: f = open(fullname, 'w')
       
   321             f.write('%s: %s\n' % (key, s.tostring()))
       
   322         if not f:
       
   323             try:
       
   324                 os.unlink(fullname)
       
   325             except os.error:
       
   326                 pass
       
   327         else:
       
   328             f.close()
       
   329 
       
   330     def getcurrent(self):
       
   331         """Return the current message.  Raise Error when there is none."""
       
   332         seqs = self.getsequences()
       
   333         try:
       
   334             return max(seqs['cur'])
       
   335         except (ValueError, KeyError):
       
   336             raise Error, "no cur message"
       
   337 
       
   338     def setcurrent(self, n):
       
   339         """Set the current message."""
       
   340         updateline(self.getsequencesfilename(), 'cur', str(n), 0)
       
   341 
       
   342     def parsesequence(self, seq):
       
   343         """Parse an MH sequence specification into a message list.
       
   344         Attempt to mimic mh-sequence(5) as close as possible.
       
   345         Also attempt to mimic observed behavior regarding which
       
   346         conditions cause which error messages."""
       
   347         # XXX Still not complete (see mh-format(5)).
       
   348         # Missing are:
       
   349         # - 'prev', 'next' as count
       
   350         # - Sequence-Negation option
       
   351         all = self.listmessages()
       
   352         # Observed behavior: test for empty folder is done first
       
   353         if not all:
       
   354             raise Error, "no messages in %s" % self.name
       
   355         # Common case first: all is frequently the default
       
   356         if seq == 'all':
       
   357             return all
       
   358         # Test for X:Y before X-Y because 'seq:-n' matches both
       
   359         i = seq.find(':')
       
   360         if i >= 0:
       
   361             head, dir, tail = seq[:i], '', seq[i+1:]
       
   362             if tail[:1] in '-+':
       
   363                 dir, tail = tail[:1], tail[1:]
       
   364             if not isnumeric(tail):
       
   365                 raise Error, "bad message list %s" % seq
       
   366             try:
       
   367                 count = int(tail)
       
   368             except (ValueError, OverflowError):
       
   369                 # Can't use sys.maxint because of i+count below
       
   370                 count = len(all)
       
   371             try:
       
   372                 anchor = self._parseindex(head, all)
       
   373             except Error, msg:
       
   374                 seqs = self.getsequences()
       
   375                 if not head in seqs:
       
   376                     if not msg:
       
   377                         msg = "bad message list %s" % seq
       
   378                     raise Error, msg, sys.exc_info()[2]
       
   379                 msgs = seqs[head]
       
   380                 if not msgs:
       
   381                     raise Error, "sequence %s empty" % head
       
   382                 if dir == '-':
       
   383                     return msgs[-count:]
       
   384                 else:
       
   385                     return msgs[:count]
       
   386             else:
       
   387                 if not dir:
       
   388                     if head in ('prev', 'last'):
       
   389                         dir = '-'
       
   390                 if dir == '-':
       
   391                     i = bisect(all, anchor)
       
   392                     return all[max(0, i-count):i]
       
   393                 else:
       
   394                     i = bisect(all, anchor-1)
       
   395                     return all[i:i+count]
       
   396         # Test for X-Y next
       
   397         i = seq.find('-')
       
   398         if i >= 0:
       
   399             begin = self._parseindex(seq[:i], all)
       
   400             end = self._parseindex(seq[i+1:], all)
       
   401             i = bisect(all, begin-1)
       
   402             j = bisect(all, end)
       
   403             r = all[i:j]
       
   404             if not r:
       
   405                 raise Error, "bad message list %s" % seq
       
   406             return r
       
   407         # Neither X:Y nor X-Y; must be a number or a (pseudo-)sequence
       
   408         try:
       
   409             n = self._parseindex(seq, all)
       
   410         except Error, msg:
       
   411             seqs = self.getsequences()
       
   412             if not seq in seqs:
       
   413                 if not msg:
       
   414                     msg = "bad message list %s" % seq
       
   415                 raise Error, msg
       
   416             return seqs[seq]
       
   417         else:
       
   418             if n not in all:
       
   419                 if isnumeric(seq):
       
   420                     raise Error, "message %d doesn't exist" % n
       
   421                 else:
       
   422                     raise Error, "no %s message" % seq
       
   423             else:
       
   424                 return [n]
       
   425 
       
   426     def _parseindex(self, seq, all):
       
   427         """Internal: parse a message number (or cur, first, etc.)."""
       
   428         if isnumeric(seq):
       
   429             try:
       
   430                 return int(seq)
       
   431             except (OverflowError, ValueError):
       
   432                 return sys.maxint
       
   433         if seq in ('cur', '.'):
       
   434             return self.getcurrent()
       
   435         if seq == 'first':
       
   436             return all[0]
       
   437         if seq == 'last':
       
   438             return all[-1]
       
   439         if seq == 'next':
       
   440             n = self.getcurrent()
       
   441             i = bisect(all, n)
       
   442             try:
       
   443                 return all[i]
       
   444             except IndexError:
       
   445                 raise Error, "no next message"
       
   446         if seq == 'prev':
       
   447             n = self.getcurrent()
       
   448             i = bisect(all, n-1)
       
   449             if i == 0:
       
   450                 raise Error, "no prev message"
       
   451             try:
       
   452                 return all[i-1]
       
   453             except IndexError:
       
   454                 raise Error, "no prev message"
       
   455         raise Error, None
       
   456 
       
   457     def openmessage(self, n):
       
   458         """Open a message -- returns a Message object."""
       
   459         return Message(self, n)
       
   460 
       
   461     def removemessages(self, list):
       
   462         """Remove one or more messages -- may raise os.error."""
       
   463         errors = []
       
   464         deleted = []
       
   465         for n in list:
       
   466             path = self.getmessagefilename(n)
       
   467             commapath = self.getmessagefilename(',' + str(n))
       
   468             try:
       
   469                 os.unlink(commapath)
       
   470             except os.error:
       
   471                 pass
       
   472             try:
       
   473                 os.rename(path, commapath)
       
   474             except os.error, msg:
       
   475                 errors.append(msg)
       
   476             else:
       
   477                 deleted.append(n)
       
   478         if deleted:
       
   479             self.removefromallsequences(deleted)
       
   480         if errors:
       
   481             if len(errors) == 1:
       
   482                 raise os.error, errors[0]
       
   483             else:
       
   484                 raise os.error, ('multiple errors:', errors)
       
   485 
       
   486     def refilemessages(self, list, tofolder, keepsequences=0):
       
   487         """Refile one or more messages -- may raise os.error.
       
   488         'tofolder' is an open folder object."""
       
   489         errors = []
       
   490         refiled = {}
       
   491         for n in list:
       
   492             ton = tofolder.getlast() + 1
       
   493             path = self.getmessagefilename(n)
       
   494             topath = tofolder.getmessagefilename(ton)
       
   495             try:
       
   496                 os.rename(path, topath)
       
   497             except os.error:
       
   498                 # Try copying
       
   499                 try:
       
   500                     shutil.copy2(path, topath)
       
   501                     os.unlink(path)
       
   502                 except (IOError, os.error), msg:
       
   503                     errors.append(msg)
       
   504                     try:
       
   505                         os.unlink(topath)
       
   506                     except os.error:
       
   507                         pass
       
   508                     continue
       
   509             tofolder.setlast(ton)
       
   510             refiled[n] = ton
       
   511         if refiled:
       
   512             if keepsequences:
       
   513                 tofolder._copysequences(self, refiled.items())
       
   514             self.removefromallsequences(refiled.keys())
       
   515         if errors:
       
   516             if len(errors) == 1:
       
   517                 raise os.error, errors[0]
       
   518             else:
       
   519                 raise os.error, ('multiple errors:', errors)
       
   520 
       
   521     def _copysequences(self, fromfolder, refileditems):
       
   522         """Helper for refilemessages() to copy sequences."""
       
   523         fromsequences = fromfolder.getsequences()
       
   524         tosequences = self.getsequences()
       
   525         changed = 0
       
   526         for name, seq in fromsequences.items():
       
   527             try:
       
   528                 toseq = tosequences[name]
       
   529                 new = 0
       
   530             except KeyError:
       
   531                 toseq = []
       
   532                 new = 1
       
   533             for fromn, ton in refileditems:
       
   534                 if fromn in seq:
       
   535                     toseq.append(ton)
       
   536                     changed = 1
       
   537             if new and toseq:
       
   538                 tosequences[name] = toseq
       
   539         if changed:
       
   540             self.putsequences(tosequences)
       
   541 
       
   542     def movemessage(self, n, tofolder, ton):
       
   543         """Move one message over a specific destination message,
       
   544         which may or may not already exist."""
       
   545         path = self.getmessagefilename(n)
       
   546         # Open it to check that it exists
       
   547         f = open(path)
       
   548         f.close()
       
   549         del f
       
   550         topath = tofolder.getmessagefilename(ton)
       
   551         backuptopath = tofolder.getmessagefilename(',%d' % ton)
       
   552         try:
       
   553             os.rename(topath, backuptopath)
       
   554         except os.error:
       
   555             pass
       
   556         try:
       
   557             os.rename(path, topath)
       
   558         except os.error:
       
   559             # Try copying
       
   560             ok = 0
       
   561             try:
       
   562                 tofolder.setlast(None)
       
   563                 shutil.copy2(path, topath)
       
   564                 ok = 1
       
   565             finally:
       
   566                 if not ok:
       
   567                     try:
       
   568                         os.unlink(topath)
       
   569                     except os.error:
       
   570                         pass
       
   571             os.unlink(path)
       
   572         self.removefromallsequences([n])
       
   573 
       
   574     def copymessage(self, n, tofolder, ton):
       
   575         """Copy one message over a specific destination message,
       
   576         which may or may not already exist."""
       
   577         path = self.getmessagefilename(n)
       
   578         # Open it to check that it exists
       
   579         f = open(path)
       
   580         f.close()
       
   581         del f
       
   582         topath = tofolder.getmessagefilename(ton)
       
   583         backuptopath = tofolder.getmessagefilename(',%d' % ton)
       
   584         try:
       
   585             os.rename(topath, backuptopath)
       
   586         except os.error:
       
   587             pass
       
   588         ok = 0
       
   589         try:
       
   590             tofolder.setlast(None)
       
   591             shutil.copy2(path, topath)
       
   592             ok = 1
       
   593         finally:
       
   594             if not ok:
       
   595                 try:
       
   596                     os.unlink(topath)
       
   597                 except os.error:
       
   598                     pass
       
   599 
       
   600     def createmessage(self, n, txt):
       
   601         """Create a message, with text from the open file txt."""
       
   602         path = self.getmessagefilename(n)
       
   603         backuppath = self.getmessagefilename(',%d' % n)
       
   604         try:
       
   605             os.rename(path, backuppath)
       
   606         except os.error:
       
   607             pass
       
   608         ok = 0
       
   609         BUFSIZE = 16*1024
       
   610         try:
       
   611             f = open(path, "w")
       
   612             while 1:
       
   613                 buf = txt.read(BUFSIZE)
       
   614                 if not buf:
       
   615                     break
       
   616                 f.write(buf)
       
   617             f.close()
       
   618             ok = 1
       
   619         finally:
       
   620             if not ok:
       
   621                 try:
       
   622                     os.unlink(path)
       
   623                 except os.error:
       
   624                     pass
       
   625 
       
   626     def removefromallsequences(self, list):
       
   627         """Remove one or more messages from all sequences (including last)
       
   628         -- but not from 'cur'!!!"""
       
   629         if hasattr(self, 'last') and self.last in list:
       
   630             del self.last
       
   631         sequences = self.getsequences()
       
   632         changed = 0
       
   633         for name, seq in sequences.items():
       
   634             if name == 'cur':
       
   635                 continue
       
   636             for n in list:
       
   637                 if n in seq:
       
   638                     seq.remove(n)
       
   639                     changed = 1
       
   640                     if not seq:
       
   641                         del sequences[name]
       
   642         if changed:
       
   643             self.putsequences(sequences)
       
   644 
       
   645     def getlast(self):
       
   646         """Return the last message number."""
       
   647         if not hasattr(self, 'last'):
       
   648             self.listmessages() # Set self.last
       
   649         return self.last
       
   650 
       
   651     def setlast(self, last):
       
   652         """Set the last message number."""
       
   653         if last is None:
       
   654             if hasattr(self, 'last'):
       
   655                 del self.last
       
   656         else:
       
   657             self.last = last
       
   658 
       
   659 class Message(mimetools.Message):
       
   660 
       
   661     def __init__(self, f, n, fp = None):
       
   662         """Constructor."""
       
   663         self.folder = f
       
   664         self.number = n
       
   665         if fp is None:
       
   666             path = f.getmessagefilename(n)
       
   667             fp = open(path, 'r')
       
   668         mimetools.Message.__init__(self, fp)
       
   669 
       
   670     def __repr__(self):
       
   671         """String representation."""
       
   672         return 'Message(%s, %s)' % (repr(self.folder), self.number)
       
   673 
       
   674     def getheadertext(self, pred = None):
       
   675         """Return the message's header text as a string.  If an
       
   676         argument is specified, it is used as a filter predicate to
       
   677         decide which headers to return (its argument is the header
       
   678         name converted to lower case)."""
       
   679         if pred is None:
       
   680             return ''.join(self.headers)
       
   681         headers = []
       
   682         hit = 0
       
   683         for line in self.headers:
       
   684             if not line[0].isspace():
       
   685                 i = line.find(':')
       
   686                 if i > 0:
       
   687                     hit = pred(line[:i].lower())
       
   688             if hit: headers.append(line)
       
   689         return ''.join(headers)
       
   690 
       
   691     def getbodytext(self, decode = 1):
       
   692         """Return the message's body text as string.  This undoes a
       
   693         Content-Transfer-Encoding, but does not interpret other MIME
       
   694         features (e.g. multipart messages).  To suppress decoding,
       
   695         pass 0 as an argument."""
       
   696         self.fp.seek(self.startofbody)
       
   697         encoding = self.getencoding()
       
   698         if not decode or encoding in ('', '7bit', '8bit', 'binary'):
       
   699             return self.fp.read()
       
   700         try:
       
   701             from cStringIO import StringIO
       
   702         except ImportError:
       
   703             from StringIO import StringIO
       
   704         output = StringIO()
       
   705         mimetools.decode(self.fp, output, encoding)
       
   706         return output.getvalue()
       
   707 
       
   708     def getbodyparts(self):
       
   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         if self.getmaintype() != 'multipart':
       
   713             raise Error, 'Content-Type is not multipart/*'
       
   714         bdry = self.getparam('boundary')
       
   715         if not bdry:
       
   716             raise Error, 'multipart/* without boundary param'
       
   717         self.fp.seek(self.startofbody)
       
   718         mf = multifile.MultiFile(self.fp)
       
   719         mf.push(bdry)
       
   720         parts = []
       
   721         while mf.next():
       
   722             n = "%s.%r" % (self.number, 1 + len(parts))
       
   723             part = SubMessage(self.folder, n, mf)
       
   724             parts.append(part)
       
   725         mf.pop()
       
   726         return parts
       
   727 
       
   728     def getbody(self):
       
   729         """Return body, either a string or a list of messages."""
       
   730         if self.getmaintype() == 'multipart':
       
   731             return self.getbodyparts()
       
   732         else:
       
   733             return self.getbodytext()
       
   734 
       
   735 
       
   736 class SubMessage(Message):
       
   737 
       
   738     def __init__(self, f, n, fp):
       
   739         """Constructor."""
       
   740         Message.__init__(self, f, n, fp)
       
   741         if self.getmaintype() == 'multipart':
       
   742             self.body = Message.getbodyparts(self)
       
   743         else:
       
   744             self.body = Message.getbodytext(self)
       
   745         self.bodyencoded = Message.getbodytext(self, decode=0)
       
   746             # XXX If this is big, should remember file pointers
       
   747 
       
   748     def __repr__(self):
       
   749         """String representation."""
       
   750         f, n, fp = self.folder, self.number, self.fp
       
   751         return 'SubMessage(%s, %s, %s)' % (f, n, fp)
       
   752 
       
   753     def getbodytext(self, decode = 1):
       
   754         if not decode:
       
   755             return self.bodyencoded
       
   756         if type(self.body) == type(''):
       
   757             return self.body
       
   758 
       
   759     def getbodyparts(self):
       
   760         if type(self.body) == type([]):
       
   761             return self.body
       
   762 
       
   763     def getbody(self):
       
   764         return self.body
       
   765 
       
   766 
       
   767 class IntSet:
       
   768     """Class implementing sets of integers.
       
   769 
       
   770     This is an efficient representation for sets consisting of several
       
   771     continuous ranges, e.g. 1-100,200-400,402-1000 is represented
       
   772     internally as a list of three pairs: [(1,100), (200,400),
       
   773     (402,1000)].  The internal representation is always kept normalized.
       
   774 
       
   775     The constructor has up to three arguments:
       
   776     - the string used to initialize the set (default ''),
       
   777     - the separator between ranges (default ',')
       
   778     - the separator between begin and end of a range (default '-')
       
   779     The separators must be strings (not regexprs) and should be different.
       
   780 
       
   781     The tostring() function yields a string that can be passed to another
       
   782     IntSet constructor; __repr__() is a valid IntSet constructor itself.
       
   783     """
       
   784 
       
   785     # XXX The default begin/end separator means that negative numbers are
       
   786     #     not supported very well.
       
   787     #
       
   788     # XXX There are currently no operations to remove set elements.
       
   789 
       
   790     def __init__(self, data = None, sep = ',', rng = '-'):
       
   791         self.pairs = []
       
   792         self.sep = sep
       
   793         self.rng = rng
       
   794         if data: self.fromstring(data)
       
   795 
       
   796     def reset(self):
       
   797         self.pairs = []
       
   798 
       
   799     def __cmp__(self, other):
       
   800         return cmp(self.pairs, other.pairs)
       
   801 
       
   802     def __hash__(self):
       
   803         return hash(self.pairs)
       
   804 
       
   805     def __repr__(self):
       
   806         return 'IntSet(%r, %r, %r)' % (self.tostring(), 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 = repr(lo)
       
   823             else: t = repr(lo) + self.rng + repr(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 True
       
   853         return False
       
   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         new = []
       
   887         for part in data.split(self.sep):
       
   888             list = []
       
   889             for subp in part.split(self.rng):
       
   890                 s = subp.strip()
       
   891                 list.append(int(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 not line[0].isspace():
       
   919                     break
       
   920                 text = text + line
       
   921             return text.strip()
       
   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(%r)' % (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     for t in reversed(testfolders): do('mh.deletefolder(%r)' % (t,))
       
   981     do('mh.getcontext()')
       
   982     context = mh.getcontext()
       
   983     f = mh.openfolder(context)
       
   984     do('f.getcurrent()')
       
   985     for seq in ('first', 'last', 'cur', '.', 'prev', 'next',
       
   986                 'first:3', 'last:3', 'cur:3', 'cur:-3',
       
   987                 'prev:3', 'next:3',
       
   988                 '1:3', '1:-3', '100:3', '100:-3', '10000:3', '10000:-3',
       
   989                 'all'):
       
   990         try:
       
   991             do('f.parsesequence(%r)' % (seq,))
       
   992         except Error, msg:
       
   993             print "Error:", msg
       
   994         stuff = os.popen("pick %r 2>/dev/null" % (seq,)).read()
       
   995         list = map(int, stuff.split())
       
   996         print list, "<-- pick"
       
   997     do('f.listmessages()')
       
   998 
       
   999 
       
  1000 if __name__ == '__main__':
       
  1001     test()