symbian-qemu-0.9.1-12/python-2.6.1/Lib/mhlib.py
changeset 1 2fb8b9db1c86
equal deleted inserted replaced
0:ffa851df0825 1:2fb8b9db1c86
       
     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 from warnings import warnpy3k
       
    56 warnpy3k("the mhlib module has been removed in Python 3.0; use the mailbox "
       
    57             "module instead", stacklevel=2)
       
    58 del warnpy3k
       
    59 
       
    60 # XXX To do, functionality:
       
    61 # - annotate messages
       
    62 # - send messages
       
    63 #
       
    64 # XXX To do, organization:
       
    65 # - move IntSet to separate file
       
    66 # - move most Message functionality to module mimetools
       
    67 
       
    68 
       
    69 # Customizable defaults
       
    70 
       
    71 MH_PROFILE = '~/.mh_profile'
       
    72 PATH = '~/Mail'
       
    73 MH_SEQUENCES = '.mh_sequences'
       
    74 FOLDER_PROTECT = 0700
       
    75 
       
    76 
       
    77 # Imported modules
       
    78 
       
    79 import os
       
    80 import sys
       
    81 import re
       
    82 import mimetools
       
    83 import multifile
       
    84 import shutil
       
    85 from bisect import bisect
       
    86 
       
    87 __all__ = ["MH","Error","Folder","Message"]
       
    88 
       
    89 # Exported constants
       
    90 
       
    91 class Error(Exception):
       
    92     pass
       
    93 
       
    94 
       
    95 class MH:
       
    96     """Class representing a particular collection of folders.
       
    97     Optional constructor arguments are the pathname for the directory
       
    98     containing the collection, and the MH profile to use.
       
    99     If either is omitted or empty a default is used; the default
       
   100     directory is taken from the MH profile if it is specified there."""
       
   101 
       
   102     def __init__(self, path = None, profile = None):
       
   103         """Constructor."""
       
   104         if profile is None: profile = MH_PROFILE
       
   105         self.profile = os.path.expanduser(profile)
       
   106         if path is None: path = self.getprofile('Path')
       
   107         if not path: path = PATH
       
   108         if not os.path.isabs(path) and path[0] != '~':
       
   109             path = os.path.join('~', path)
       
   110         path = os.path.expanduser(path)
       
   111         if not os.path.isdir(path): raise Error, 'MH() path not found'
       
   112         self.path = path
       
   113 
       
   114     def __repr__(self):
       
   115         """String representation."""
       
   116         return 'MH(%r, %r)' % (self.path, self.profile)
       
   117 
       
   118     def error(self, msg, *args):
       
   119         """Routine to print an error.  May be overridden by a derived class."""
       
   120         sys.stderr.write('MH error: %s\n' % (msg % args))
       
   121 
       
   122     def getprofile(self, key):
       
   123         """Return a profile entry, None if not found."""
       
   124         return pickline(self.profile, key)
       
   125 
       
   126     def getpath(self):
       
   127         """Return the path (the name of the collection's directory)."""
       
   128         return self.path
       
   129 
       
   130     def getcontext(self):
       
   131         """Return the name of the current folder."""
       
   132         context = pickline(os.path.join(self.getpath(), 'context'),
       
   133                   'Current-Folder')
       
   134         if not context: context = 'inbox'
       
   135         return context
       
   136 
       
   137     def setcontext(self, context):
       
   138         """Set the name of the current folder."""
       
   139         fn = os.path.join(self.getpath(), 'context')
       
   140         f = open(fn, "w")
       
   141         f.write("Current-Folder: %s\n" % context)
       
   142         f.close()
       
   143 
       
   144     def listfolders(self):
       
   145         """Return the names of the top-level folders."""
       
   146         folders = []
       
   147         path = self.getpath()
       
   148         for name in os.listdir(path):
       
   149             fullname = os.path.join(path, name)
       
   150             if os.path.isdir(fullname):
       
   151                 folders.append(name)
       
   152         folders.sort()
       
   153         return folders
       
   154 
       
   155     def listsubfolders(self, name):
       
   156         """Return the names of the subfolders in a given folder
       
   157         (prefixed with the given folder name)."""
       
   158         fullname = os.path.join(self.path, name)
       
   159         # Get the link count so we can avoid listing folders
       
   160         # that have no subfolders.
       
   161         nlinks = os.stat(fullname).st_nlink
       
   162         if nlinks <= 2:
       
   163             return []
       
   164         subfolders = []
       
   165         subnames = os.listdir(fullname)
       
   166         for subname in subnames:
       
   167             fullsubname = os.path.join(fullname, subname)
       
   168             if os.path.isdir(fullsubname):
       
   169                 name_subname = os.path.join(name, subname)
       
   170                 subfolders.append(name_subname)
       
   171                 # Stop looking for subfolders when
       
   172                 # we've seen them all
       
   173                 nlinks = nlinks - 1
       
   174                 if nlinks <= 2:
       
   175                     break
       
   176         subfolders.sort()
       
   177         return subfolders
       
   178 
       
   179     def listallfolders(self):
       
   180         """Return the names of all folders and subfolders, recursively."""
       
   181         return self.listallsubfolders('')
       
   182 
       
   183     def listallsubfolders(self, name):
       
   184         """Return the names of subfolders in a given folder, recursively."""
       
   185         fullname = os.path.join(self.path, name)
       
   186         # Get the link count so we can avoid listing folders
       
   187         # that have no subfolders.
       
   188         nlinks = os.stat(fullname).st_nlink
       
   189         if nlinks <= 2:
       
   190             return []
       
   191         subfolders = []
       
   192         subnames = os.listdir(fullname)
       
   193         for subname in subnames:
       
   194             if subname[0] == ',' or isnumeric(subname): continue
       
   195             fullsubname = os.path.join(fullname, subname)
       
   196             if os.path.isdir(fullsubname):
       
   197                 name_subname = os.path.join(name, subname)
       
   198                 subfolders.append(name_subname)
       
   199                 if not os.path.islink(fullsubname):
       
   200                     subsubfolders = self.listallsubfolders(
       
   201                               name_subname)
       
   202                     subfolders = subfolders + subsubfolders
       
   203                 # Stop looking for subfolders when
       
   204                 # we've seen them all
       
   205                 nlinks = nlinks - 1
       
   206                 if nlinks <= 2:
       
   207                     break
       
   208         subfolders.sort()
       
   209         return subfolders
       
   210 
       
   211     def openfolder(self, name):
       
   212         """Return a new Folder object for the named folder."""
       
   213         return Folder(self, name)
       
   214 
       
   215     def makefolder(self, name):
       
   216         """Create a new folder (or raise os.error if it cannot be created)."""
       
   217         protect = pickline(self.profile, 'Folder-Protect')
       
   218         if protect and isnumeric(protect):
       
   219             mode = int(protect, 8)
       
   220         else:
       
   221             mode = FOLDER_PROTECT
       
   222         os.mkdir(os.path.join(self.getpath(), name), mode)
       
   223 
       
   224     def deletefolder(self, name):
       
   225         """Delete a folder.  This removes files in the folder but not
       
   226         subdirectories.  Raise os.error if deleting the folder itself fails."""
       
   227         fullname = os.path.join(self.getpath(), name)
       
   228         for subname in os.listdir(fullname):
       
   229             fullsubname = os.path.join(fullname, subname)
       
   230             try:
       
   231                 os.unlink(fullsubname)
       
   232             except os.error:
       
   233                 self.error('%s not deleted, continuing...' %
       
   234                           fullsubname)
       
   235         os.rmdir(fullname)
       
   236 
       
   237 
       
   238 numericprog = re.compile('^[1-9][0-9]*$')
       
   239 def isnumeric(str):
       
   240     return numericprog.match(str) is not None
       
   241 
       
   242 class Folder:
       
   243     """Class representing a particular folder."""
       
   244 
       
   245     def __init__(self, mh, name):
       
   246         """Constructor."""
       
   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     def __repr__(self):
       
   253         """String representation."""
       
   254         return 'Folder(%r, %r)' % (self.mh, self.name)
       
   255 
       
   256     def error(self, *args):
       
   257         """Error message handler."""
       
   258         self.mh.error(*args)
       
   259 
       
   260     def getfullname(self):
       
   261         """Return the full pathname of the folder."""
       
   262         return os.path.join(self.mh.path, self.name)
       
   263 
       
   264     def getsequencesfilename(self):
       
   265         """Return the full pathname of the folder's sequences file."""
       
   266         return os.path.join(self.getfullname(), MH_SEQUENCES)
       
   267 
       
   268     def getmessagefilename(self, n):
       
   269         """Return the full pathname of a message in the folder."""
       
   270         return os.path.join(self.getfullname(), str(n))
       
   271 
       
   272     def listsubfolders(self):
       
   273         """Return list of direct subfolders."""
       
   274         return self.mh.listsubfolders(self.name)
       
   275 
       
   276     def listallsubfolders(self):
       
   277         """Return list of all subfolders."""
       
   278         return self.mh.listallsubfolders(self.name)
       
   279 
       
   280     def listmessages(self):
       
   281         """Return the list of messages currently present in the folder.
       
   282         As a side effect, set self.last to the last message (or 0)."""
       
   283         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(int, messages)
       
   290         messages.sort()
       
   291         if messages:
       
   292             self.last = messages[-1]
       
   293         else:
       
   294             self.last = 0
       
   295         return messages
       
   296 
       
   297     def getsequences(self):
       
   298         """Return the set of sequences for the folder."""
       
   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 = line.split(':')
       
   309             if len(fields) != 2:
       
   310                 self.error('bad sequence in %s: %s' %
       
   311                           (fullname, line.strip()))
       
   312             key = fields[0].strip()
       
   313             value = IntSet(fields[1].strip(), ' ').tolist()
       
   314             sequences[key] = value
       
   315         return sequences
       
   316 
       
   317     def putsequences(self, sequences):
       
   318         """Write the set of sequences back to the folder."""
       
   319         fullname = self.getsequencesfilename()
       
   320         f = None
       
   321         for key, seq in sequences.iteritems():
       
   322             s = IntSet('', ' ')
       
   323             s.fromlist(seq)
       
   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     def getcurrent(self):
       
   335         """Return the current message.  Raise Error when there is none."""
       
   336         seqs = self.getsequences()
       
   337         try:
       
   338             return max(seqs['cur'])
       
   339         except (ValueError, KeyError):
       
   340             raise Error, "no cur message"
       
   341 
       
   342     def setcurrent(self, n):
       
   343         """Set the current message."""
       
   344         updateline(self.getsequencesfilename(), 'cur', str(n), 0)
       
   345 
       
   346     def parsesequence(self, seq):
       
   347         """Parse an MH sequence specification into a message list.
       
   348         Attempt to mimic mh-sequence(5) as close as possible.
       
   349         Also attempt to mimic observed behavior regarding which
       
   350         conditions cause which error messages."""
       
   351         # 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 = seq.find(':')
       
   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 = int(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 head in seqs:
       
   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 = seq.find('-')
       
   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 seq in seqs:
       
   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     def _parseindex(self, seq, all):
       
   431         """Internal: parse a message number (or cur, first, etc.)."""
       
   432         if isnumeric(seq):
       
   433             try:
       
   434                 return int(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     def openmessage(self, n):
       
   462         """Open a message -- returns a Message object."""
       
   463         return Message(self, n)
       
   464 
       
   465     def removemessages(self, list):
       
   466         """Remove one or more messages -- may raise os.error."""
       
   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     def refilemessages(self, list, tofolder, keepsequences=0):
       
   491         """Refile one or more messages -- may raise os.error.
       
   492         'tofolder' is an open folder object."""
       
   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     def _copysequences(self, fromfolder, refileditems):
       
   526         """Helper for refilemessages() to copy sequences."""
       
   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 KeyError:
       
   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     def movemessage(self, n, tofolder, ton):
       
   547         """Move one message over a specific destination message,
       
   548         which may or may not already exist."""
       
   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     def copymessage(self, n, tofolder, ton):
       
   579         """Copy one message over a specific destination message,
       
   580         which may or may not already exist."""
       
   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     def createmessage(self, n, txt):
       
   605         """Create a message, with text from the open file 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     def removefromallsequences(self, list):
       
   631         """Remove one or more messages from all sequences (including last)
       
   632         -- but not from 'cur'!!!"""
       
   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     def getlast(self):
       
   650         """Return the last message number."""
       
   651         if not hasattr(self, 'last'):
       
   652             self.listmessages() # Set self.last
       
   653         return self.last
       
   654 
       
   655     def setlast(self, last):
       
   656         """Set the last message number."""
       
   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     def __init__(self, f, n, fp = None):
       
   666         """Constructor."""
       
   667         self.folder = f
       
   668         self.number = n
       
   669         if fp is None:
       
   670             path = f.getmessagefilename(n)
       
   671             fp = open(path, 'r')
       
   672         mimetools.Message.__init__(self, fp)
       
   673 
       
   674     def __repr__(self):
       
   675         """String representation."""
       
   676         return 'Message(%s, %s)' % (repr(self.folder), self.number)
       
   677 
       
   678     def getheadertext(self, pred = None):
       
   679         """Return the message's header text as a string.  If an
       
   680         argument is specified, it is used as a filter predicate to
       
   681         decide which headers to return (its argument is the header
       
   682         name converted to lower case)."""
       
   683         if pred is None:
       
   684             return ''.join(self.headers)
       
   685         headers = []
       
   686         hit = 0
       
   687         for line in self.headers:
       
   688             if not line[0].isspace():
       
   689                 i = line.find(':')
       
   690                 if i > 0:
       
   691                     hit = pred(line[:i].lower())
       
   692             if hit: headers.append(line)
       
   693         return ''.join(headers)
       
   694 
       
   695     def getbodytext(self, decode = 1):
       
   696         """Return the message's body text as string.  This undoes a
       
   697         Content-Transfer-Encoding, but does not interpret other MIME
       
   698         features (e.g. multipart messages).  To suppress decoding,
       
   699         pass 0 as an argument."""
       
   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         try:
       
   705             from cStringIO import StringIO
       
   706         except ImportError:
       
   707             from StringIO import StringIO
       
   708         output = StringIO()
       
   709         mimetools.decode(self.fp, output, encoding)
       
   710         return output.getvalue()
       
   711 
       
   712     def getbodyparts(self):
       
   713         """Only for multipart messages: return the message's body as a
       
   714         list of SubMessage objects.  Each submessage object behaves
       
   715         (almost) as a Message object."""
       
   716         if self.getmaintype() != 'multipart':
       
   717             raise Error, 'Content-Type is not multipart/*'
       
   718         bdry = self.getparam('boundary')
       
   719         if not bdry:
       
   720             raise Error, 'multipart/* without boundary param'
       
   721         self.fp.seek(self.startofbody)
       
   722         mf = multifile.MultiFile(self.fp)
       
   723         mf.push(bdry)
       
   724         parts = []
       
   725         while mf.next():
       
   726             n = "%s.%r" % (self.number, 1 + len(parts))
       
   727             part = SubMessage(self.folder, n, mf)
       
   728             parts.append(part)
       
   729         mf.pop()
       
   730         return parts
       
   731 
       
   732     def getbody(self):
       
   733         """Return body, either a string or a list of messages."""
       
   734         if self.getmaintype() == 'multipart':
       
   735             return self.getbodyparts()
       
   736         else:
       
   737             return self.getbodytext()
       
   738 
       
   739 
       
   740 class SubMessage(Message):
       
   741 
       
   742     def __init__(self, f, n, fp):
       
   743         """Constructor."""
       
   744         Message.__init__(self, f, n, fp)
       
   745         if self.getmaintype() == 'multipart':
       
   746             self.body = Message.getbodyparts(self)
       
   747         else:
       
   748             self.body = Message.getbodytext(self)
       
   749         self.bodyencoded = Message.getbodytext(self, decode=0)
       
   750             # XXX If this is big, should remember file pointers
       
   751 
       
   752     def __repr__(self):
       
   753         """String representation."""
       
   754         f, n, fp = self.folder, self.number, self.fp
       
   755         return 'SubMessage(%s, %s, %s)' % (f, n, fp)
       
   756 
       
   757     def getbodytext(self, decode = 1):
       
   758         if not decode:
       
   759             return self.bodyencoded
       
   760         if type(self.body) == type(''):
       
   761             return self.body
       
   762 
       
   763     def getbodyparts(self):
       
   764         if type(self.body) == type([]):
       
   765             return self.body
       
   766 
       
   767     def getbody(self):
       
   768         return self.body
       
   769 
       
   770 
       
   771 class IntSet:
       
   772     """Class implementing sets of integers.
       
   773 
       
   774     This is an efficient representation for sets consisting of several
       
   775     continuous ranges, e.g. 1-100,200-400,402-1000 is represented
       
   776     internally as a list of three pairs: [(1,100), (200,400),
       
   777     (402,1000)].  The internal representation is always kept normalized.
       
   778 
       
   779     The constructor has up to three arguments:
       
   780     - the string used to initialize the set (default ''),
       
   781     - the separator between ranges (default ',')
       
   782     - the separator between begin and end of a range (default '-')
       
   783     The separators must be strings (not regexprs) and should be different.
       
   784 
       
   785     The tostring() function yields a string that can be passed to another
       
   786     IntSet constructor; __repr__() is a valid IntSet constructor itself.
       
   787     """
       
   788 
       
   789     # XXX The default begin/end separator means that negative numbers are
       
   790     #     not supported very well.
       
   791     #
       
   792     # XXX There are currently no operations to remove set elements.
       
   793 
       
   794     def __init__(self, data = None, sep = ',', rng = '-'):
       
   795         self.pairs = []
       
   796         self.sep = sep
       
   797         self.rng = rng
       
   798         if data: self.fromstring(data)
       
   799 
       
   800     def reset(self):
       
   801         self.pairs = []
       
   802 
       
   803     def __cmp__(self, other):
       
   804         return cmp(self.pairs, other.pairs)
       
   805 
       
   806     def __hash__(self):
       
   807         return hash(self.pairs)
       
   808 
       
   809     def __repr__(self):
       
   810         return 'IntSet(%r, %r, %r)' % (self.tostring(), self.sep, self.rng)
       
   811 
       
   812     def normalize(self):
       
   813         self.pairs.sort()
       
   814         i = 1
       
   815         while i < len(self.pairs):
       
   816             alo, ahi = self.pairs[i-1]
       
   817             blo, bhi = self.pairs[i]
       
   818             if ahi >= blo-1:
       
   819                 self.pairs[i-1:i+1] = [(alo, max(ahi, bhi))]
       
   820             else:
       
   821                 i = i+1
       
   822 
       
   823     def tostring(self):
       
   824         s = ''
       
   825         for lo, hi in self.pairs:
       
   826             if lo == hi: t = repr(lo)
       
   827             else: t = repr(lo) + self.rng + repr(hi)
       
   828             if s: s = s + (self.sep + t)
       
   829             else: s = t
       
   830         return s
       
   831 
       
   832     def tolist(self):
       
   833         l = []
       
   834         for lo, hi in self.pairs:
       
   835             m = range(lo, hi+1)
       
   836             l = l + m
       
   837         return l
       
   838 
       
   839     def fromlist(self, list):
       
   840         for i in list:
       
   841             self.append(i)
       
   842 
       
   843     def clone(self):
       
   844         new = IntSet()
       
   845         new.pairs = self.pairs[:]
       
   846         return new
       
   847 
       
   848     def min(self):
       
   849         return self.pairs[0][0]
       
   850 
       
   851     def max(self):
       
   852         return self.pairs[-1][-1]
       
   853 
       
   854     def contains(self, x):
       
   855         for lo, hi in self.pairs:
       
   856             if lo <= x <= hi: return True
       
   857         return False
       
   858 
       
   859     def append(self, x):
       
   860         for i in range(len(self.pairs)):
       
   861             lo, hi = self.pairs[i]
       
   862             if x < lo: # Need to insert before
       
   863                 if x+1 == lo:
       
   864                     self.pairs[i] = (x, hi)
       
   865                 else:
       
   866                     self.pairs.insert(i, (x, x))
       
   867                 if i > 0 and x-1 == self.pairs[i-1][1]:
       
   868                     # Merge with previous
       
   869                     self.pairs[i-1:i+1] = [
       
   870                             (self.pairs[i-1][0],
       
   871                              self.pairs[i][1])
       
   872                           ]
       
   873                 return
       
   874             if x <= hi: # Already in set
       
   875                 return
       
   876         i = len(self.pairs) - 1
       
   877         if i >= 0:
       
   878             lo, hi = self.pairs[i]
       
   879             if x-1 == hi:
       
   880                 self.pairs[i] = lo, x
       
   881                 return
       
   882         self.pairs.append((x, x))
       
   883 
       
   884     def addpair(self, xlo, xhi):
       
   885         if xlo > xhi: return
       
   886         self.pairs.append((xlo, xhi))
       
   887         self.normalize()
       
   888 
       
   889     def fromstring(self, data):
       
   890         new = []
       
   891         for part in data.split(self.sep):
       
   892             list = []
       
   893             for subp in part.split(self.rng):
       
   894                 s = subp.strip()
       
   895                 list.append(int(s))
       
   896             if len(list) == 1:
       
   897                 new.append((list[0], list[0]))
       
   898             elif len(list) == 2 and list[0] <= list[1]:
       
   899                 new.append((list[0], list[1]))
       
   900             else:
       
   901                 raise ValueError, 'bad data passed to IntSet'
       
   902         self.pairs = self.pairs + new
       
   903         self.normalize()
       
   904 
       
   905 
       
   906 # Subroutines to read/write entries in .mh_profile and .mh_sequences
       
   907 
       
   908 def pickline(file, key, casefold = 1):
       
   909     try:
       
   910         f = open(file, 'r')
       
   911     except IOError:
       
   912         return None
       
   913     pat = re.escape(key) + ':'
       
   914     prog = re.compile(pat, casefold and re.IGNORECASE)
       
   915     while 1:
       
   916         line = f.readline()
       
   917         if not line: break
       
   918         if prog.match(line):
       
   919             text = line[len(key)+1:]
       
   920             while 1:
       
   921                 line = f.readline()
       
   922                 if not line or not line[0].isspace():
       
   923                     break
       
   924                 text = text + line
       
   925             return text.strip()
       
   926     return None
       
   927 
       
   928 def updateline(file, key, value, casefold = 1):
       
   929     try:
       
   930         f = open(file, 'r')
       
   931         lines = f.readlines()
       
   932         f.close()
       
   933     except IOError:
       
   934         lines = []
       
   935     pat = re.escape(key) + ':(.*)\n'
       
   936     prog = re.compile(pat, casefold and re.IGNORECASE)
       
   937     if value is None:
       
   938         newline = None
       
   939     else:
       
   940         newline = '%s: %s\n' % (key, value)
       
   941     for i in range(len(lines)):
       
   942         line = lines[i]
       
   943         if prog.match(line):
       
   944             if newline is None:
       
   945                 del lines[i]
       
   946             else:
       
   947                 lines[i] = newline
       
   948             break
       
   949     else:
       
   950         if newline is not None:
       
   951             lines.append(newline)
       
   952     tempfile = file + "~"
       
   953     f = open(tempfile, 'w')
       
   954     for line in lines:
       
   955         f.write(line)
       
   956     f.close()
       
   957     os.rename(tempfile, file)
       
   958 
       
   959 
       
   960 # Test program
       
   961 
       
   962 def test():
       
   963     global mh, f
       
   964     os.system('rm -rf $HOME/Mail/@test')
       
   965     mh = MH()
       
   966     def do(s): print s; print eval(s)
       
   967     do('mh.listfolders()')
       
   968     do('mh.listallfolders()')
       
   969     testfolders = ['@test', '@test/test1', '@test/test2',
       
   970                    '@test/test1/test11', '@test/test1/test12',
       
   971                    '@test/test1/test11/test111']
       
   972     for t in testfolders: do('mh.makefolder(%r)' % (t,))
       
   973     do('mh.listsubfolders(\'@test\')')
       
   974     do('mh.listallsubfolders(\'@test\')')
       
   975     f = mh.openfolder('@test')
       
   976     do('f.listsubfolders()')
       
   977     do('f.listallsubfolders()')
       
   978     do('f.getsequences()')
       
   979     seqs = f.getsequences()
       
   980     seqs['foo'] = IntSet('1-10 12-20', ' ').tolist()
       
   981     print seqs
       
   982     f.putsequences(seqs)
       
   983     do('f.getsequences()')
       
   984     for t in reversed(testfolders): do('mh.deletefolder(%r)' % (t,))
       
   985     do('mh.getcontext()')
       
   986     context = mh.getcontext()
       
   987     f = mh.openfolder(context)
       
   988     do('f.getcurrent()')
       
   989     for seq in ('first', 'last', 'cur', '.', 'prev', 'next',
       
   990                 'first:3', 'last:3', 'cur:3', 'cur:-3',
       
   991                 'prev:3', 'next:3',
       
   992                 '1:3', '1:-3', '100:3', '100:-3', '10000:3', '10000:-3',
       
   993                 'all'):
       
   994         try:
       
   995             do('f.parsesequence(%r)' % (seq,))
       
   996         except Error, msg:
       
   997             print "Error:", msg
       
   998         stuff = os.popen("pick %r 2>/dev/null" % (seq,)).read()
       
   999         list = map(int, stuff.split())
       
  1000         print list, "<-- pick"
       
  1001     do('f.listmessages()')
       
  1002 
       
  1003 
       
  1004 if __name__ == '__main__':
       
  1005     test()