python-2.5.2/win32/Lib/mailbox.py
changeset 0 ae805ac0140d
equal deleted inserted replaced
-1:000000000000 0:ae805ac0140d
       
     1 #! /usr/bin/env python
       
     2 
       
     3 """Read/write support for Maildir, mbox, MH, Babyl, and MMDF mailboxes."""
       
     4 
       
     5 # Notes for authors of new mailbox subclasses:
       
     6 #
       
     7 # Remember to fsync() changes to disk before closing a modified file
       
     8 # or returning from a flush() method.  See functions _sync_flush() and
       
     9 # _sync_close().
       
    10 
       
    11 import sys
       
    12 import os
       
    13 import time
       
    14 import calendar
       
    15 import socket
       
    16 import errno
       
    17 import copy
       
    18 import email
       
    19 import email.Message
       
    20 import email.Generator
       
    21 import rfc822
       
    22 import StringIO
       
    23 try:
       
    24     if sys.platform == 'os2emx':
       
    25         # OS/2 EMX fcntl() not adequate
       
    26         raise ImportError
       
    27     import fcntl
       
    28 except ImportError:
       
    29     fcntl = None
       
    30 
       
    31 __all__ = [ 'Mailbox', 'Maildir', 'mbox', 'MH', 'Babyl', 'MMDF',
       
    32             'Message', 'MaildirMessage', 'mboxMessage', 'MHMessage',
       
    33             'BabylMessage', 'MMDFMessage', 'UnixMailbox',
       
    34             'PortableUnixMailbox', 'MmdfMailbox', 'MHMailbox', 'BabylMailbox' ]
       
    35 
       
    36 class Mailbox:
       
    37     """A group of messages in a particular place."""
       
    38 
       
    39     def __init__(self, path, factory=None, create=True):
       
    40         """Initialize a Mailbox instance."""
       
    41         self._path = os.path.abspath(os.path.expanduser(path))
       
    42         self._factory = factory
       
    43 
       
    44     def add(self, message):
       
    45         """Add message and return assigned key."""
       
    46         raise NotImplementedError('Method must be implemented by subclass')
       
    47 
       
    48     def remove(self, key):
       
    49         """Remove the keyed message; raise KeyError if it doesn't exist."""
       
    50         raise NotImplementedError('Method must be implemented by subclass')
       
    51 
       
    52     def __delitem__(self, key):
       
    53         self.remove(key)
       
    54 
       
    55     def discard(self, key):
       
    56         """If the keyed message exists, remove it."""
       
    57         try:
       
    58             self.remove(key)
       
    59         except KeyError:
       
    60             pass
       
    61 
       
    62     def __setitem__(self, key, message):
       
    63         """Replace the keyed message; raise KeyError if it doesn't exist."""
       
    64         raise NotImplementedError('Method must be implemented by subclass')
       
    65 
       
    66     def get(self, key, default=None):
       
    67         """Return the keyed message, or default if it doesn't exist."""
       
    68         try:
       
    69             return self.__getitem__(key)
       
    70         except KeyError:
       
    71             return default
       
    72 
       
    73     def __getitem__(self, key):
       
    74         """Return the keyed message; raise KeyError if it doesn't exist."""
       
    75         if not self._factory:
       
    76             return self.get_message(key)
       
    77         else:
       
    78             return self._factory(self.get_file(key))
       
    79 
       
    80     def get_message(self, key):
       
    81         """Return a Message representation or raise a KeyError."""
       
    82         raise NotImplementedError('Method must be implemented by subclass')
       
    83 
       
    84     def get_string(self, key):
       
    85         """Return a string representation or raise a KeyError."""
       
    86         raise NotImplementedError('Method must be implemented by subclass')
       
    87 
       
    88     def get_file(self, key):
       
    89         """Return a file-like representation or raise a KeyError."""
       
    90         raise NotImplementedError('Method must be implemented by subclass')
       
    91 
       
    92     def iterkeys(self):
       
    93         """Return an iterator over keys."""
       
    94         raise NotImplementedError('Method must be implemented by subclass')
       
    95 
       
    96     def keys(self):
       
    97         """Return a list of keys."""
       
    98         return list(self.iterkeys())
       
    99 
       
   100     def itervalues(self):
       
   101         """Return an iterator over all messages."""
       
   102         for key in self.iterkeys():
       
   103             try:
       
   104                 value = self[key]
       
   105             except KeyError:
       
   106                 continue
       
   107             yield value
       
   108 
       
   109     def __iter__(self):
       
   110         return self.itervalues()
       
   111 
       
   112     def values(self):
       
   113         """Return a list of messages. Memory intensive."""
       
   114         return list(self.itervalues())
       
   115 
       
   116     def iteritems(self):
       
   117         """Return an iterator over (key, message) tuples."""
       
   118         for key in self.iterkeys():
       
   119             try:
       
   120                 value = self[key]
       
   121             except KeyError:
       
   122                 continue
       
   123             yield (key, value)
       
   124 
       
   125     def items(self):
       
   126         """Return a list of (key, message) tuples. Memory intensive."""
       
   127         return list(self.iteritems())
       
   128 
       
   129     def has_key(self, key):
       
   130         """Return True if the keyed message exists, False otherwise."""
       
   131         raise NotImplementedError('Method must be implemented by subclass')
       
   132 
       
   133     def __contains__(self, key):
       
   134         return self.has_key(key)
       
   135 
       
   136     def __len__(self):
       
   137         """Return a count of messages in the mailbox."""
       
   138         raise NotImplementedError('Method must be implemented by subclass')
       
   139 
       
   140     def clear(self):
       
   141         """Delete all messages."""
       
   142         for key in self.iterkeys():
       
   143             self.discard(key)
       
   144 
       
   145     def pop(self, key, default=None):
       
   146         """Delete the keyed message and return it, or default."""
       
   147         try:
       
   148             result = self[key]
       
   149         except KeyError:
       
   150             return default
       
   151         self.discard(key)
       
   152         return result
       
   153 
       
   154     def popitem(self):
       
   155         """Delete an arbitrary (key, message) pair and return it."""
       
   156         for key in self.iterkeys():
       
   157             return (key, self.pop(key))     # This is only run once.
       
   158         else:
       
   159             raise KeyError('No messages in mailbox')
       
   160 
       
   161     def update(self, arg=None):
       
   162         """Change the messages that correspond to certain keys."""
       
   163         if hasattr(arg, 'iteritems'):
       
   164             source = arg.iteritems()
       
   165         elif hasattr(arg, 'items'):
       
   166             source = arg.items()
       
   167         else:
       
   168             source = arg
       
   169         bad_key = False
       
   170         for key, message in source:
       
   171             try:
       
   172                 self[key] = message
       
   173             except KeyError:
       
   174                 bad_key = True
       
   175         if bad_key:
       
   176             raise KeyError('No message with key(s)')
       
   177 
       
   178     def flush(self):
       
   179         """Write any pending changes to the disk."""
       
   180         raise NotImplementedError('Method must be implemented by subclass')
       
   181 
       
   182     def lock(self):
       
   183         """Lock the mailbox."""
       
   184         raise NotImplementedError('Method must be implemented by subclass')
       
   185 
       
   186     def unlock(self):
       
   187         """Unlock the mailbox if it is locked."""
       
   188         raise NotImplementedError('Method must be implemented by subclass')
       
   189 
       
   190     def close(self):
       
   191         """Flush and close the mailbox."""
       
   192         raise NotImplementedError('Method must be implemented by subclass')
       
   193 
       
   194     def _dump_message(self, message, target, mangle_from_=False):
       
   195         # Most files are opened in binary mode to allow predictable seeking.
       
   196         # To get native line endings on disk, the user-friendly \n line endings
       
   197         # used in strings and by email.Message are translated here.
       
   198         """Dump message contents to target file."""
       
   199         if isinstance(message, email.Message.Message):
       
   200             buffer = StringIO.StringIO()
       
   201             gen = email.Generator.Generator(buffer, mangle_from_, 0)
       
   202             gen.flatten(message)
       
   203             buffer.seek(0)
       
   204             target.write(buffer.read().replace('\n', os.linesep))
       
   205         elif isinstance(message, str):
       
   206             if mangle_from_:
       
   207                 message = message.replace('\nFrom ', '\n>From ')
       
   208             message = message.replace('\n', os.linesep)
       
   209             target.write(message)
       
   210         elif hasattr(message, 'read'):
       
   211             while True:
       
   212                 line = message.readline()
       
   213                 if line == '':
       
   214                     break
       
   215                 if mangle_from_ and line.startswith('From '):
       
   216                     line = '>From ' + line[5:]
       
   217                 line = line.replace('\n', os.linesep)
       
   218                 target.write(line)
       
   219         else:
       
   220             raise TypeError('Invalid message type: %s' % type(message))
       
   221 
       
   222 
       
   223 class Maildir(Mailbox):
       
   224     """A qmail-style Maildir mailbox."""
       
   225 
       
   226     colon = ':'
       
   227 
       
   228     def __init__(self, dirname, factory=rfc822.Message, create=True):
       
   229         """Initialize a Maildir instance."""
       
   230         Mailbox.__init__(self, dirname, factory, create)
       
   231         if not os.path.exists(self._path):
       
   232             if create:
       
   233                 os.mkdir(self._path, 0700)
       
   234                 os.mkdir(os.path.join(self._path, 'tmp'), 0700)
       
   235                 os.mkdir(os.path.join(self._path, 'new'), 0700)
       
   236                 os.mkdir(os.path.join(self._path, 'cur'), 0700)
       
   237             else:
       
   238                 raise NoSuchMailboxError(self._path)
       
   239         self._toc = {}
       
   240 
       
   241     def add(self, message):
       
   242         """Add message and return assigned key."""
       
   243         tmp_file = self._create_tmp()
       
   244         try:
       
   245             self._dump_message(message, tmp_file)
       
   246         finally:
       
   247             _sync_close(tmp_file)
       
   248         if isinstance(message, MaildirMessage):
       
   249             subdir = message.get_subdir()
       
   250             suffix = self.colon + message.get_info()
       
   251             if suffix == self.colon:
       
   252                 suffix = ''
       
   253         else:
       
   254             subdir = 'new'
       
   255             suffix = ''
       
   256         uniq = os.path.basename(tmp_file.name).split(self.colon)[0]
       
   257         dest = os.path.join(self._path, subdir, uniq + suffix)
       
   258         try:
       
   259             if hasattr(os, 'link'):
       
   260                 os.link(tmp_file.name, dest)
       
   261                 os.remove(tmp_file.name)
       
   262             else:
       
   263                 os.rename(tmp_file.name, dest)
       
   264         except OSError, e:
       
   265             os.remove(tmp_file.name)
       
   266             if e.errno == errno.EEXIST:
       
   267                 raise ExternalClashError('Name clash with existing message: %s'
       
   268                                          % dest)
       
   269             else:
       
   270                 raise
       
   271         if isinstance(message, MaildirMessage):
       
   272             os.utime(dest, (os.path.getatime(dest), message.get_date()))
       
   273         return uniq
       
   274 
       
   275     def remove(self, key):
       
   276         """Remove the keyed message; raise KeyError if it doesn't exist."""
       
   277         os.remove(os.path.join(self._path, self._lookup(key)))
       
   278 
       
   279     def discard(self, key):
       
   280         """If the keyed message exists, remove it."""
       
   281         # This overrides an inapplicable implementation in the superclass.
       
   282         try:
       
   283             self.remove(key)
       
   284         except KeyError:
       
   285             pass
       
   286         except OSError, e:
       
   287             if e.errno != errno.ENOENT:
       
   288                 raise
       
   289 
       
   290     def __setitem__(self, key, message):
       
   291         """Replace the keyed message; raise KeyError if it doesn't exist."""
       
   292         old_subpath = self._lookup(key)
       
   293         temp_key = self.add(message)
       
   294         temp_subpath = self._lookup(temp_key)
       
   295         if isinstance(message, MaildirMessage):
       
   296             # temp's subdir and suffix were specified by message.
       
   297             dominant_subpath = temp_subpath
       
   298         else:
       
   299             # temp's subdir and suffix were defaults from add().
       
   300             dominant_subpath = old_subpath
       
   301         subdir = os.path.dirname(dominant_subpath)
       
   302         if self.colon in dominant_subpath:
       
   303             suffix = self.colon + dominant_subpath.split(self.colon)[-1]
       
   304         else:
       
   305             suffix = ''
       
   306         self.discard(key)
       
   307         new_path = os.path.join(self._path, subdir, key + suffix)
       
   308         os.rename(os.path.join(self._path, temp_subpath), new_path)
       
   309         if isinstance(message, MaildirMessage):
       
   310             os.utime(new_path, (os.path.getatime(new_path),
       
   311                                 message.get_date()))
       
   312 
       
   313     def get_message(self, key):
       
   314         """Return a Message representation or raise a KeyError."""
       
   315         subpath = self._lookup(key)
       
   316         f = open(os.path.join(self._path, subpath), 'r')
       
   317         try:
       
   318             if self._factory:
       
   319                 msg = self._factory(f)
       
   320             else:
       
   321                 msg = MaildirMessage(f)
       
   322         finally:
       
   323             f.close()
       
   324         subdir, name = os.path.split(subpath)
       
   325         msg.set_subdir(subdir)
       
   326         if self.colon in name:
       
   327             msg.set_info(name.split(self.colon)[-1])
       
   328         msg.set_date(os.path.getmtime(os.path.join(self._path, subpath)))
       
   329         return msg
       
   330 
       
   331     def get_string(self, key):
       
   332         """Return a string representation or raise a KeyError."""
       
   333         f = open(os.path.join(self._path, self._lookup(key)), 'r')
       
   334         try:
       
   335             return f.read()
       
   336         finally:
       
   337             f.close()
       
   338 
       
   339     def get_file(self, key):
       
   340         """Return a file-like representation or raise a KeyError."""
       
   341         f = open(os.path.join(self._path, self._lookup(key)), 'rb')
       
   342         return _ProxyFile(f)
       
   343 
       
   344     def iterkeys(self):
       
   345         """Return an iterator over keys."""
       
   346         self._refresh()
       
   347         for key in self._toc:
       
   348             try:
       
   349                 self._lookup(key)
       
   350             except KeyError:
       
   351                 continue
       
   352             yield key
       
   353 
       
   354     def has_key(self, key):
       
   355         """Return True if the keyed message exists, False otherwise."""
       
   356         self._refresh()
       
   357         return key in self._toc
       
   358 
       
   359     def __len__(self):
       
   360         """Return a count of messages in the mailbox."""
       
   361         self._refresh()
       
   362         return len(self._toc)
       
   363 
       
   364     def flush(self):
       
   365         """Write any pending changes to disk."""
       
   366         return  # Maildir changes are always written immediately.
       
   367 
       
   368     def lock(self):
       
   369         """Lock the mailbox."""
       
   370         return
       
   371 
       
   372     def unlock(self):
       
   373         """Unlock the mailbox if it is locked."""
       
   374         return
       
   375 
       
   376     def close(self):
       
   377         """Flush and close the mailbox."""
       
   378         return
       
   379 
       
   380     def list_folders(self):
       
   381         """Return a list of folder names."""
       
   382         result = []
       
   383         for entry in os.listdir(self._path):
       
   384             if len(entry) > 1 and entry[0] == '.' and \
       
   385                os.path.isdir(os.path.join(self._path, entry)):
       
   386                 result.append(entry[1:])
       
   387         return result
       
   388 
       
   389     def get_folder(self, folder):
       
   390         """Return a Maildir instance for the named folder."""
       
   391         return Maildir(os.path.join(self._path, '.' + folder),
       
   392                        factory=self._factory,
       
   393                        create=False)
       
   394 
       
   395     def add_folder(self, folder):
       
   396         """Create a folder and return a Maildir instance representing it."""
       
   397         path = os.path.join(self._path, '.' + folder)
       
   398         result = Maildir(path, factory=self._factory)
       
   399         maildirfolder_path = os.path.join(path, 'maildirfolder')
       
   400         if not os.path.exists(maildirfolder_path):
       
   401             os.close(os.open(maildirfolder_path, os.O_CREAT | os.O_WRONLY))
       
   402         return result
       
   403 
       
   404     def remove_folder(self, folder):
       
   405         """Delete the named folder, which must be empty."""
       
   406         path = os.path.join(self._path, '.' + folder)
       
   407         for entry in os.listdir(os.path.join(path, 'new')) + \
       
   408                      os.listdir(os.path.join(path, 'cur')):
       
   409             if len(entry) < 1 or entry[0] != '.':
       
   410                 raise NotEmptyError('Folder contains message(s): %s' % folder)
       
   411         for entry in os.listdir(path):
       
   412             if entry != 'new' and entry != 'cur' and entry != 'tmp' and \
       
   413                os.path.isdir(os.path.join(path, entry)):
       
   414                 raise NotEmptyError("Folder contains subdirectory '%s': %s" %
       
   415                                     (folder, entry))
       
   416         for root, dirs, files in os.walk(path, topdown=False):
       
   417             for entry in files:
       
   418                 os.remove(os.path.join(root, entry))
       
   419             for entry in dirs:
       
   420                 os.rmdir(os.path.join(root, entry))
       
   421         os.rmdir(path)
       
   422 
       
   423     def clean(self):
       
   424         """Delete old files in "tmp"."""
       
   425         now = time.time()
       
   426         for entry in os.listdir(os.path.join(self._path, 'tmp')):
       
   427             path = os.path.join(self._path, 'tmp', entry)
       
   428             if now - os.path.getatime(path) > 129600:   # 60 * 60 * 36
       
   429                 os.remove(path)
       
   430 
       
   431     _count = 1  # This is used to generate unique file names.
       
   432 
       
   433     def _create_tmp(self):
       
   434         """Create a file in the tmp subdirectory and open and return it."""
       
   435         now = time.time()
       
   436         hostname = socket.gethostname()
       
   437         if '/' in hostname:
       
   438             hostname = hostname.replace('/', r'\057')
       
   439         if ':' in hostname:
       
   440             hostname = hostname.replace(':', r'\072')
       
   441         uniq = "%s.M%sP%sQ%s.%s" % (int(now), int(now % 1 * 1e6), os.getpid(),
       
   442                                     Maildir._count, hostname)
       
   443         path = os.path.join(self._path, 'tmp', uniq)
       
   444         try:
       
   445             os.stat(path)
       
   446         except OSError, e:
       
   447             if e.errno == errno.ENOENT:
       
   448                 Maildir._count += 1
       
   449                 try:
       
   450                     return _create_carefully(path)
       
   451                 except OSError, e:
       
   452                     if e.errno != errno.EEXIST:
       
   453                         raise
       
   454             else:
       
   455                 raise
       
   456 
       
   457         # Fall through to here if stat succeeded or open raised EEXIST.
       
   458         raise ExternalClashError('Name clash prevented file creation: %s' %
       
   459                                  path)
       
   460 
       
   461     def _refresh(self):
       
   462         """Update table of contents mapping."""
       
   463         self._toc = {}
       
   464         for subdir in ('new', 'cur'):
       
   465             subdir_path = os.path.join(self._path, subdir)
       
   466             for entry in os.listdir(subdir_path):
       
   467                 p = os.path.join(subdir_path, entry)
       
   468                 if os.path.isdir(p):
       
   469                     continue
       
   470                 uniq = entry.split(self.colon)[0]
       
   471                 self._toc[uniq] = os.path.join(subdir, entry)
       
   472 
       
   473     def _lookup(self, key):
       
   474         """Use TOC to return subpath for given key, or raise a KeyError."""
       
   475         try:
       
   476             if os.path.exists(os.path.join(self._path, self._toc[key])):
       
   477                 return self._toc[key]
       
   478         except KeyError:
       
   479             pass
       
   480         self._refresh()
       
   481         try:
       
   482             return self._toc[key]
       
   483         except KeyError:
       
   484             raise KeyError('No message with key: %s' % key)
       
   485 
       
   486     # This method is for backward compatibility only.
       
   487     def next(self):
       
   488         """Return the next message in a one-time iteration."""
       
   489         if not hasattr(self, '_onetime_keys'):
       
   490             self._onetime_keys = self.iterkeys()
       
   491         while True:
       
   492             try:
       
   493                 return self[self._onetime_keys.next()]
       
   494             except StopIteration:
       
   495                 return None
       
   496             except KeyError:
       
   497                 continue
       
   498 
       
   499 
       
   500 class _singlefileMailbox(Mailbox):
       
   501     """A single-file mailbox."""
       
   502 
       
   503     def __init__(self, path, factory=None, create=True):
       
   504         """Initialize a single-file mailbox."""
       
   505         Mailbox.__init__(self, path, factory, create)
       
   506         try:
       
   507             f = open(self._path, 'rb+')
       
   508         except IOError, e:
       
   509             if e.errno == errno.ENOENT:
       
   510                 if create:
       
   511                     f = open(self._path, 'wb+')
       
   512                 else:
       
   513                     raise NoSuchMailboxError(self._path)
       
   514             elif e.errno == errno.EACCES:
       
   515                 f = open(self._path, 'rb')
       
   516             else:
       
   517                 raise
       
   518         self._file = f
       
   519         self._toc = None
       
   520         self._next_key = 0
       
   521         self._pending = False   # No changes require rewriting the file.
       
   522         self._locked = False
       
   523 
       
   524     def add(self, message):
       
   525         """Add message and return assigned key."""
       
   526         self._lookup()
       
   527         self._toc[self._next_key] = self._append_message(message)
       
   528         self._next_key += 1
       
   529         self._pending = True
       
   530         return self._next_key - 1
       
   531 
       
   532     def remove(self, key):
       
   533         """Remove the keyed message; raise KeyError if it doesn't exist."""
       
   534         self._lookup(key)
       
   535         del self._toc[key]
       
   536         self._pending = True
       
   537 
       
   538     def __setitem__(self, key, message):
       
   539         """Replace the keyed message; raise KeyError if it doesn't exist."""
       
   540         self._lookup(key)
       
   541         self._toc[key] = self._append_message(message)
       
   542         self._pending = True
       
   543 
       
   544     def iterkeys(self):
       
   545         """Return an iterator over keys."""
       
   546         self._lookup()
       
   547         for key in self._toc.keys():
       
   548             yield key
       
   549 
       
   550     def has_key(self, key):
       
   551         """Return True if the keyed message exists, False otherwise."""
       
   552         self._lookup()
       
   553         return key in self._toc
       
   554 
       
   555     def __len__(self):
       
   556         """Return a count of messages in the mailbox."""
       
   557         self._lookup()
       
   558         return len(self._toc)
       
   559 
       
   560     def lock(self):
       
   561         """Lock the mailbox."""
       
   562         if not self._locked:
       
   563             _lock_file(self._file)
       
   564             self._locked = True
       
   565 
       
   566     def unlock(self):
       
   567         """Unlock the mailbox if it is locked."""
       
   568         if self._locked:
       
   569             _unlock_file(self._file)
       
   570             self._locked = False
       
   571 
       
   572     def flush(self):
       
   573         """Write any pending changes to disk."""
       
   574         if not self._pending:
       
   575             return
       
   576         self._lookup()
       
   577         new_file = _create_temporary(self._path)
       
   578         try:
       
   579             new_toc = {}
       
   580             self._pre_mailbox_hook(new_file)
       
   581             for key in sorted(self._toc.keys()):
       
   582                 start, stop = self._toc[key]
       
   583                 self._file.seek(start)
       
   584                 self._pre_message_hook(new_file)
       
   585                 new_start = new_file.tell()
       
   586                 while True:
       
   587                     buffer = self._file.read(min(4096,
       
   588                                                  stop - self._file.tell()))
       
   589                     if buffer == '':
       
   590                         break
       
   591                     new_file.write(buffer)
       
   592                 new_toc[key] = (new_start, new_file.tell())
       
   593                 self._post_message_hook(new_file)
       
   594         except:
       
   595             new_file.close()
       
   596             os.remove(new_file.name)
       
   597             raise
       
   598         _sync_close(new_file)
       
   599         # self._file is about to get replaced, so no need to sync.
       
   600         self._file.close()
       
   601         try:
       
   602             os.rename(new_file.name, self._path)
       
   603         except OSError, e:
       
   604             if e.errno == errno.EEXIST or \
       
   605               (os.name == 'os2' and e.errno == errno.EACCES):
       
   606                 os.remove(self._path)
       
   607                 os.rename(new_file.name, self._path)
       
   608             else:
       
   609                 raise
       
   610         self._file = open(self._path, 'rb+')
       
   611         self._toc = new_toc
       
   612         self._pending = False
       
   613         if self._locked:
       
   614             _lock_file(self._file, dotlock=False)
       
   615 
       
   616     def _pre_mailbox_hook(self, f):
       
   617         """Called before writing the mailbox to file f."""
       
   618         return
       
   619 
       
   620     def _pre_message_hook(self, f):
       
   621         """Called before writing each message to file f."""
       
   622         return
       
   623 
       
   624     def _post_message_hook(self, f):
       
   625         """Called after writing each message to file f."""
       
   626         return
       
   627 
       
   628     def close(self):
       
   629         """Flush and close the mailbox."""
       
   630         self.flush()
       
   631         if self._locked:
       
   632             self.unlock()
       
   633         self._file.close()  # Sync has been done by self.flush() above.
       
   634 
       
   635     def _lookup(self, key=None):
       
   636         """Return (start, stop) or raise KeyError."""
       
   637         if self._toc is None:
       
   638             self._generate_toc()
       
   639         if key is not None:
       
   640             try:
       
   641                 return self._toc[key]
       
   642             except KeyError:
       
   643                 raise KeyError('No message with key: %s' % key)
       
   644 
       
   645     def _append_message(self, message):
       
   646         """Append message to mailbox and return (start, stop) offsets."""
       
   647         self._file.seek(0, 2)
       
   648         self._pre_message_hook(self._file)
       
   649         offsets = self._install_message(message)
       
   650         self._post_message_hook(self._file)
       
   651         self._file.flush()
       
   652         return offsets
       
   653 
       
   654 
       
   655 
       
   656 class _mboxMMDF(_singlefileMailbox):
       
   657     """An mbox or MMDF mailbox."""
       
   658 
       
   659     _mangle_from_ = True
       
   660 
       
   661     def get_message(self, key):
       
   662         """Return a Message representation or raise a KeyError."""
       
   663         start, stop = self._lookup(key)
       
   664         self._file.seek(start)
       
   665         from_line = self._file.readline().replace(os.linesep, '')
       
   666         string = self._file.read(stop - self._file.tell())
       
   667         msg = self._message_factory(string.replace(os.linesep, '\n'))
       
   668         msg.set_from(from_line[5:])
       
   669         return msg
       
   670 
       
   671     def get_string(self, key, from_=False):
       
   672         """Return a string representation or raise a KeyError."""
       
   673         start, stop = self._lookup(key)
       
   674         self._file.seek(start)
       
   675         if not from_:
       
   676             self._file.readline()
       
   677         string = self._file.read(stop - self._file.tell())
       
   678         return string.replace(os.linesep, '\n')
       
   679 
       
   680     def get_file(self, key, from_=False):
       
   681         """Return a file-like representation or raise a KeyError."""
       
   682         start, stop = self._lookup(key)
       
   683         self._file.seek(start)
       
   684         if not from_:
       
   685             self._file.readline()
       
   686         return _PartialFile(self._file, self._file.tell(), stop)
       
   687 
       
   688     def _install_message(self, message):
       
   689         """Format a message and blindly write to self._file."""
       
   690         from_line = None
       
   691         if isinstance(message, str) and message.startswith('From '):
       
   692             newline = message.find('\n')
       
   693             if newline != -1:
       
   694                 from_line = message[:newline]
       
   695                 message = message[newline + 1:]
       
   696             else:
       
   697                 from_line = message
       
   698                 message = ''
       
   699         elif isinstance(message, _mboxMMDFMessage):
       
   700             from_line = 'From ' + message.get_from()
       
   701         elif isinstance(message, email.Message.Message):
       
   702             from_line = message.get_unixfrom()  # May be None.
       
   703         if from_line is None:
       
   704             from_line = 'From MAILER-DAEMON %s' % time.asctime(time.gmtime())
       
   705         start = self._file.tell()
       
   706         self._file.write(from_line + os.linesep)
       
   707         self._dump_message(message, self._file, self._mangle_from_)
       
   708         stop = self._file.tell()
       
   709         return (start, stop)
       
   710 
       
   711 
       
   712 class mbox(_mboxMMDF):
       
   713     """A classic mbox mailbox."""
       
   714 
       
   715     _mangle_from_ = True
       
   716 
       
   717     def __init__(self, path, factory=None, create=True):
       
   718         """Initialize an mbox mailbox."""
       
   719         self._message_factory = mboxMessage
       
   720         _mboxMMDF.__init__(self, path, factory, create)
       
   721 
       
   722     def _pre_message_hook(self, f):
       
   723         """Called before writing each message to file f."""
       
   724         if f.tell() != 0:
       
   725             f.write(os.linesep)
       
   726 
       
   727     def _generate_toc(self):
       
   728         """Generate key-to-(start, stop) table of contents."""
       
   729         starts, stops = [], []
       
   730         self._file.seek(0)
       
   731         while True:
       
   732             line_pos = self._file.tell()
       
   733             line = self._file.readline()
       
   734             if line.startswith('From '):
       
   735                 if len(stops) < len(starts):
       
   736                     stops.append(line_pos - len(os.linesep))
       
   737                 starts.append(line_pos)
       
   738             elif line == '':
       
   739                 stops.append(line_pos)
       
   740                 break
       
   741         self._toc = dict(enumerate(zip(starts, stops)))
       
   742         self._next_key = len(self._toc)
       
   743 
       
   744 
       
   745 class MMDF(_mboxMMDF):
       
   746     """An MMDF mailbox."""
       
   747 
       
   748     def __init__(self, path, factory=None, create=True):
       
   749         """Initialize an MMDF mailbox."""
       
   750         self._message_factory = MMDFMessage
       
   751         _mboxMMDF.__init__(self, path, factory, create)
       
   752 
       
   753     def _pre_message_hook(self, f):
       
   754         """Called before writing each message to file f."""
       
   755         f.write('\001\001\001\001' + os.linesep)
       
   756 
       
   757     def _post_message_hook(self, f):
       
   758         """Called after writing each message to file f."""
       
   759         f.write(os.linesep + '\001\001\001\001' + os.linesep)
       
   760 
       
   761     def _generate_toc(self):
       
   762         """Generate key-to-(start, stop) table of contents."""
       
   763         starts, stops = [], []
       
   764         self._file.seek(0)
       
   765         next_pos = 0
       
   766         while True:
       
   767             line_pos = next_pos
       
   768             line = self._file.readline()
       
   769             next_pos = self._file.tell()
       
   770             if line.startswith('\001\001\001\001' + os.linesep):
       
   771                 starts.append(next_pos)
       
   772                 while True:
       
   773                     line_pos = next_pos
       
   774                     line = self._file.readline()
       
   775                     next_pos = self._file.tell()
       
   776                     if line == '\001\001\001\001' + os.linesep:
       
   777                         stops.append(line_pos - len(os.linesep))
       
   778                         break
       
   779                     elif line == '':
       
   780                         stops.append(line_pos)
       
   781                         break
       
   782             elif line == '':
       
   783                 break
       
   784         self._toc = dict(enumerate(zip(starts, stops)))
       
   785         self._next_key = len(self._toc)
       
   786 
       
   787 
       
   788 class MH(Mailbox):
       
   789     """An MH mailbox."""
       
   790 
       
   791     def __init__(self, path, factory=None, create=True):
       
   792         """Initialize an MH instance."""
       
   793         Mailbox.__init__(self, path, factory, create)
       
   794         if not os.path.exists(self._path):
       
   795             if create:
       
   796                 os.mkdir(self._path, 0700)
       
   797                 os.close(os.open(os.path.join(self._path, '.mh_sequences'),
       
   798                                  os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0600))
       
   799             else:
       
   800                 raise NoSuchMailboxError(self._path)
       
   801         self._locked = False
       
   802 
       
   803     def add(self, message):
       
   804         """Add message and return assigned key."""
       
   805         keys = self.keys()
       
   806         if len(keys) == 0:
       
   807             new_key = 1
       
   808         else:
       
   809             new_key = max(keys) + 1
       
   810         new_path = os.path.join(self._path, str(new_key))
       
   811         f = _create_carefully(new_path)
       
   812         try:
       
   813             if self._locked:
       
   814                 _lock_file(f)
       
   815             try:
       
   816                 self._dump_message(message, f)
       
   817                 if isinstance(message, MHMessage):
       
   818                     self._dump_sequences(message, new_key)
       
   819             finally:
       
   820                 if self._locked:
       
   821                     _unlock_file(f)
       
   822         finally:
       
   823             _sync_close(f)
       
   824         return new_key
       
   825 
       
   826     def remove(self, key):
       
   827         """Remove the keyed message; raise KeyError if it doesn't exist."""
       
   828         path = os.path.join(self._path, str(key))
       
   829         try:
       
   830             f = open(path, 'rb+')
       
   831         except IOError, e:
       
   832             if e.errno == errno.ENOENT:
       
   833                 raise KeyError('No message with key: %s' % key)
       
   834             else:
       
   835                 raise
       
   836         try:
       
   837             if self._locked:
       
   838                 _lock_file(f)
       
   839             try:
       
   840                 f.close()
       
   841                 os.remove(os.path.join(self._path, str(key)))
       
   842             finally:
       
   843                 if self._locked:
       
   844                     _unlock_file(f)
       
   845         finally:
       
   846             f.close()
       
   847 
       
   848     def __setitem__(self, key, message):
       
   849         """Replace the keyed message; raise KeyError if it doesn't exist."""
       
   850         path = os.path.join(self._path, str(key))
       
   851         try:
       
   852             f = open(path, 'rb+')
       
   853         except IOError, e:
       
   854             if e.errno == errno.ENOENT:
       
   855                 raise KeyError('No message with key: %s' % key)
       
   856             else:
       
   857                 raise
       
   858         try:
       
   859             if self._locked:
       
   860                 _lock_file(f)
       
   861             try:
       
   862                 os.close(os.open(path, os.O_WRONLY | os.O_TRUNC))
       
   863                 self._dump_message(message, f)
       
   864                 if isinstance(message, MHMessage):
       
   865                     self._dump_sequences(message, key)
       
   866             finally:
       
   867                 if self._locked:
       
   868                     _unlock_file(f)
       
   869         finally:
       
   870             _sync_close(f)
       
   871 
       
   872     def get_message(self, key):
       
   873         """Return a Message representation or raise a KeyError."""
       
   874         try:
       
   875             if self._locked:
       
   876                 f = open(os.path.join(self._path, str(key)), 'r+')
       
   877             else:
       
   878                 f = open(os.path.join(self._path, str(key)), 'r')
       
   879         except IOError, e:
       
   880             if e.errno == errno.ENOENT:
       
   881                 raise KeyError('No message with key: %s' % key)
       
   882             else:
       
   883                 raise
       
   884         try:
       
   885             if self._locked:
       
   886                 _lock_file(f)
       
   887             try:
       
   888                 msg = MHMessage(f)
       
   889             finally:
       
   890                 if self._locked:
       
   891                     _unlock_file(f)
       
   892         finally:
       
   893             f.close()
       
   894         for name, key_list in self.get_sequences():
       
   895             if key in key_list:
       
   896                 msg.add_sequence(name)
       
   897         return msg
       
   898 
       
   899     def get_string(self, key):
       
   900         """Return a string representation or raise a KeyError."""
       
   901         try:
       
   902             if self._locked:
       
   903                 f = open(os.path.join(self._path, str(key)), 'r+')
       
   904             else:
       
   905                 f = open(os.path.join(self._path, str(key)), 'r')
       
   906         except IOError, e:
       
   907             if e.errno == errno.ENOENT:
       
   908                 raise KeyError('No message with key: %s' % key)
       
   909             else:
       
   910                 raise
       
   911         try:
       
   912             if self._locked:
       
   913                 _lock_file(f)
       
   914             try:
       
   915                 return f.read()
       
   916             finally:
       
   917                 if self._locked:
       
   918                     _unlock_file(f)
       
   919         finally:
       
   920             f.close()
       
   921 
       
   922     def get_file(self, key):
       
   923         """Return a file-like representation or raise a KeyError."""
       
   924         try:
       
   925             f = open(os.path.join(self._path, str(key)), 'rb')
       
   926         except IOError, e:
       
   927             if e.errno == errno.ENOENT:
       
   928                 raise KeyError('No message with key: %s' % key)
       
   929             else:
       
   930                 raise
       
   931         return _ProxyFile(f)
       
   932 
       
   933     def iterkeys(self):
       
   934         """Return an iterator over keys."""
       
   935         return iter(sorted(int(entry) for entry in os.listdir(self._path)
       
   936                                       if entry.isdigit()))
       
   937 
       
   938     def has_key(self, key):
       
   939         """Return True if the keyed message exists, False otherwise."""
       
   940         return os.path.exists(os.path.join(self._path, str(key)))
       
   941 
       
   942     def __len__(self):
       
   943         """Return a count of messages in the mailbox."""
       
   944         return len(list(self.iterkeys()))
       
   945 
       
   946     def lock(self):
       
   947         """Lock the mailbox."""
       
   948         if not self._locked:
       
   949             self._file = open(os.path.join(self._path, '.mh_sequences'), 'rb+')
       
   950             _lock_file(self._file)
       
   951             self._locked = True
       
   952 
       
   953     def unlock(self):
       
   954         """Unlock the mailbox if it is locked."""
       
   955         if self._locked:
       
   956             _unlock_file(self._file)
       
   957             _sync_close(self._file)
       
   958             del self._file
       
   959             self._locked = False
       
   960 
       
   961     def flush(self):
       
   962         """Write any pending changes to the disk."""
       
   963         return
       
   964 
       
   965     def close(self):
       
   966         """Flush and close the mailbox."""
       
   967         if self._locked:
       
   968             self.unlock()
       
   969 
       
   970     def list_folders(self):
       
   971         """Return a list of folder names."""
       
   972         result = []
       
   973         for entry in os.listdir(self._path):
       
   974             if os.path.isdir(os.path.join(self._path, entry)):
       
   975                 result.append(entry)
       
   976         return result
       
   977 
       
   978     def get_folder(self, folder):
       
   979         """Return an MH instance for the named folder."""
       
   980         return MH(os.path.join(self._path, folder),
       
   981                   factory=self._factory, create=False)
       
   982 
       
   983     def add_folder(self, folder):
       
   984         """Create a folder and return an MH instance representing it."""
       
   985         return MH(os.path.join(self._path, folder),
       
   986                   factory=self._factory)
       
   987 
       
   988     def remove_folder(self, folder):
       
   989         """Delete the named folder, which must be empty."""
       
   990         path = os.path.join(self._path, folder)
       
   991         entries = os.listdir(path)
       
   992         if entries == ['.mh_sequences']:
       
   993             os.remove(os.path.join(path, '.mh_sequences'))
       
   994         elif entries == []:
       
   995             pass
       
   996         else:
       
   997             raise NotEmptyError('Folder not empty: %s' % self._path)
       
   998         os.rmdir(path)
       
   999 
       
  1000     def get_sequences(self):
       
  1001         """Return a name-to-key-list dictionary to define each sequence."""
       
  1002         results = {}
       
  1003         f = open(os.path.join(self._path, '.mh_sequences'), 'r')
       
  1004         try:
       
  1005             all_keys = set(self.keys())
       
  1006             for line in f:
       
  1007                 try:
       
  1008                     name, contents = line.split(':')
       
  1009                     keys = set()
       
  1010                     for spec in contents.split():
       
  1011                         if spec.isdigit():
       
  1012                             keys.add(int(spec))
       
  1013                         else:
       
  1014                             start, stop = (int(x) for x in spec.split('-'))
       
  1015                             keys.update(range(start, stop + 1))
       
  1016                     results[name] = [key for key in sorted(keys) \
       
  1017                                          if key in all_keys]
       
  1018                     if len(results[name]) == 0:
       
  1019                         del results[name]
       
  1020                 except ValueError:
       
  1021                     raise FormatError('Invalid sequence specification: %s' %
       
  1022                                       line.rstrip())
       
  1023         finally:
       
  1024             f.close()
       
  1025         return results
       
  1026 
       
  1027     def set_sequences(self, sequences):
       
  1028         """Set sequences using the given name-to-key-list dictionary."""
       
  1029         f = open(os.path.join(self._path, '.mh_sequences'), 'r+')
       
  1030         try:
       
  1031             os.close(os.open(f.name, os.O_WRONLY | os.O_TRUNC))
       
  1032             for name, keys in sequences.iteritems():
       
  1033                 if len(keys) == 0:
       
  1034                     continue
       
  1035                 f.write('%s:' % name)
       
  1036                 prev = None
       
  1037                 completing = False
       
  1038                 for key in sorted(set(keys)):
       
  1039                     if key - 1 == prev:
       
  1040                         if not completing:
       
  1041                             completing = True
       
  1042                             f.write('-')
       
  1043                     elif completing:
       
  1044                         completing = False
       
  1045                         f.write('%s %s' % (prev, key))
       
  1046                     else:
       
  1047                         f.write(' %s' % key)
       
  1048                     prev = key
       
  1049                 if completing:
       
  1050                     f.write(str(prev) + '\n')
       
  1051                 else:
       
  1052                     f.write('\n')
       
  1053         finally:
       
  1054             _sync_close(f)
       
  1055 
       
  1056     def pack(self):
       
  1057         """Re-name messages to eliminate numbering gaps. Invalidates keys."""
       
  1058         sequences = self.get_sequences()
       
  1059         prev = 0
       
  1060         changes = []
       
  1061         for key in self.iterkeys():
       
  1062             if key - 1 != prev:
       
  1063                 changes.append((key, prev + 1))
       
  1064                 if hasattr(os, 'link'):
       
  1065                     os.link(os.path.join(self._path, str(key)),
       
  1066                             os.path.join(self._path, str(prev + 1)))
       
  1067                     os.unlink(os.path.join(self._path, str(key)))
       
  1068                 else:
       
  1069                     os.rename(os.path.join(self._path, str(key)),
       
  1070                               os.path.join(self._path, str(prev + 1)))
       
  1071             prev += 1
       
  1072         self._next_key = prev + 1
       
  1073         if len(changes) == 0:
       
  1074             return
       
  1075         for name, key_list in sequences.items():
       
  1076             for old, new in changes:
       
  1077                 if old in key_list:
       
  1078                     key_list[key_list.index(old)] = new
       
  1079         self.set_sequences(sequences)
       
  1080 
       
  1081     def _dump_sequences(self, message, key):
       
  1082         """Inspect a new MHMessage and update sequences appropriately."""
       
  1083         pending_sequences = message.get_sequences()
       
  1084         all_sequences = self.get_sequences()
       
  1085         for name, key_list in all_sequences.iteritems():
       
  1086             if name in pending_sequences:
       
  1087                 key_list.append(key)
       
  1088             elif key in key_list:
       
  1089                 del key_list[key_list.index(key)]
       
  1090         for sequence in pending_sequences:
       
  1091             if sequence not in all_sequences:
       
  1092                 all_sequences[sequence] = [key]
       
  1093         self.set_sequences(all_sequences)
       
  1094 
       
  1095 
       
  1096 class Babyl(_singlefileMailbox):
       
  1097     """An Rmail-style Babyl mailbox."""
       
  1098 
       
  1099     _special_labels = frozenset(('unseen', 'deleted', 'filed', 'answered',
       
  1100                                  'forwarded', 'edited', 'resent'))
       
  1101 
       
  1102     def __init__(self, path, factory=None, create=True):
       
  1103         """Initialize a Babyl mailbox."""
       
  1104         _singlefileMailbox.__init__(self, path, factory, create)
       
  1105         self._labels = {}
       
  1106 
       
  1107     def add(self, message):
       
  1108         """Add message and return assigned key."""
       
  1109         key = _singlefileMailbox.add(self, message)
       
  1110         if isinstance(message, BabylMessage):
       
  1111             self._labels[key] = message.get_labels()
       
  1112         return key
       
  1113 
       
  1114     def remove(self, key):
       
  1115         """Remove the keyed message; raise KeyError if it doesn't exist."""
       
  1116         _singlefileMailbox.remove(self, key)
       
  1117         if key in self._labels:
       
  1118             del self._labels[key]
       
  1119 
       
  1120     def __setitem__(self, key, message):
       
  1121         """Replace the keyed message; raise KeyError if it doesn't exist."""
       
  1122         _singlefileMailbox.__setitem__(self, key, message)
       
  1123         if isinstance(message, BabylMessage):
       
  1124             self._labels[key] = message.get_labels()
       
  1125 
       
  1126     def get_message(self, key):
       
  1127         """Return a Message representation or raise a KeyError."""
       
  1128         start, stop = self._lookup(key)
       
  1129         self._file.seek(start)
       
  1130         self._file.readline()   # Skip '1,' line specifying labels.
       
  1131         original_headers = StringIO.StringIO()
       
  1132         while True:
       
  1133             line = self._file.readline()
       
  1134             if line == '*** EOOH ***' + os.linesep or line == '':
       
  1135                 break
       
  1136             original_headers.write(line.replace(os.linesep, '\n'))
       
  1137         visible_headers = StringIO.StringIO()
       
  1138         while True:
       
  1139             line = self._file.readline()
       
  1140             if line == os.linesep or line == '':
       
  1141                 break
       
  1142             visible_headers.write(line.replace(os.linesep, '\n'))
       
  1143         body = self._file.read(stop - self._file.tell()).replace(os.linesep,
       
  1144                                                                  '\n')
       
  1145         msg = BabylMessage(original_headers.getvalue() + body)
       
  1146         msg.set_visible(visible_headers.getvalue())
       
  1147         if key in self._labels:
       
  1148             msg.set_labels(self._labels[key])
       
  1149         return msg
       
  1150 
       
  1151     def get_string(self, key):
       
  1152         """Return a string representation or raise a KeyError."""
       
  1153         start, stop = self._lookup(key)
       
  1154         self._file.seek(start)
       
  1155         self._file.readline()   # Skip '1,' line specifying labels.
       
  1156         original_headers = StringIO.StringIO()
       
  1157         while True:
       
  1158             line = self._file.readline()
       
  1159             if line == '*** EOOH ***' + os.linesep or line == '':
       
  1160                 break
       
  1161             original_headers.write(line.replace(os.linesep, '\n'))
       
  1162         while True:
       
  1163             line = self._file.readline()
       
  1164             if line == os.linesep or line == '':
       
  1165                 break
       
  1166         return original_headers.getvalue() + \
       
  1167                self._file.read(stop - self._file.tell()).replace(os.linesep,
       
  1168                                                                  '\n')
       
  1169 
       
  1170     def get_file(self, key):
       
  1171         """Return a file-like representation or raise a KeyError."""
       
  1172         return StringIO.StringIO(self.get_string(key).replace('\n',
       
  1173                                                               os.linesep))
       
  1174 
       
  1175     def get_labels(self):
       
  1176         """Return a list of user-defined labels in the mailbox."""
       
  1177         self._lookup()
       
  1178         labels = set()
       
  1179         for label_list in self._labels.values():
       
  1180             labels.update(label_list)
       
  1181         labels.difference_update(self._special_labels)
       
  1182         return list(labels)
       
  1183 
       
  1184     def _generate_toc(self):
       
  1185         """Generate key-to-(start, stop) table of contents."""
       
  1186         starts, stops = [], []
       
  1187         self._file.seek(0)
       
  1188         next_pos = 0
       
  1189         label_lists = []
       
  1190         while True:
       
  1191             line_pos = next_pos
       
  1192             line = self._file.readline()
       
  1193             next_pos = self._file.tell()
       
  1194             if line == '\037\014' + os.linesep:
       
  1195                 if len(stops) < len(starts):
       
  1196                     stops.append(line_pos - len(os.linesep))
       
  1197                 starts.append(next_pos)
       
  1198                 labels = [label.strip() for label
       
  1199                                         in self._file.readline()[1:].split(',')
       
  1200                                         if label.strip() != '']
       
  1201                 label_lists.append(labels)
       
  1202             elif line == '\037' or line == '\037' + os.linesep:
       
  1203                 if len(stops) < len(starts):
       
  1204                     stops.append(line_pos - len(os.linesep))
       
  1205             elif line == '':
       
  1206                 stops.append(line_pos - len(os.linesep))
       
  1207                 break
       
  1208         self._toc = dict(enumerate(zip(starts, stops)))
       
  1209         self._labels = dict(enumerate(label_lists))
       
  1210         self._next_key = len(self._toc)
       
  1211 
       
  1212     def _pre_mailbox_hook(self, f):
       
  1213         """Called before writing the mailbox to file f."""
       
  1214         f.write('BABYL OPTIONS:%sVersion: 5%sLabels:%s%s\037' %
       
  1215                 (os.linesep, os.linesep, ','.join(self.get_labels()),
       
  1216                  os.linesep))
       
  1217 
       
  1218     def _pre_message_hook(self, f):
       
  1219         """Called before writing each message to file f."""
       
  1220         f.write('\014' + os.linesep)
       
  1221 
       
  1222     def _post_message_hook(self, f):
       
  1223         """Called after writing each message to file f."""
       
  1224         f.write(os.linesep + '\037')
       
  1225 
       
  1226     def _install_message(self, message):
       
  1227         """Write message contents and return (start, stop)."""
       
  1228         start = self._file.tell()
       
  1229         if isinstance(message, BabylMessage):
       
  1230             special_labels = []
       
  1231             labels = []
       
  1232             for label in message.get_labels():
       
  1233                 if label in self._special_labels:
       
  1234                     special_labels.append(label)
       
  1235                 else:
       
  1236                     labels.append(label)
       
  1237             self._file.write('1')
       
  1238             for label in special_labels:
       
  1239                 self._file.write(', ' + label)
       
  1240             self._file.write(',,')
       
  1241             for label in labels:
       
  1242                 self._file.write(' ' + label + ',')
       
  1243             self._file.write(os.linesep)
       
  1244         else:
       
  1245             self._file.write('1,,' + os.linesep)
       
  1246         if isinstance(message, email.Message.Message):
       
  1247             orig_buffer = StringIO.StringIO()
       
  1248             orig_generator = email.Generator.Generator(orig_buffer, False, 0)
       
  1249             orig_generator.flatten(message)
       
  1250             orig_buffer.seek(0)
       
  1251             while True:
       
  1252                 line = orig_buffer.readline()
       
  1253                 self._file.write(line.replace('\n', os.linesep))
       
  1254                 if line == '\n' or line == '':
       
  1255                     break
       
  1256             self._file.write('*** EOOH ***' + os.linesep)
       
  1257             if isinstance(message, BabylMessage):
       
  1258                 vis_buffer = StringIO.StringIO()
       
  1259                 vis_generator = email.Generator.Generator(vis_buffer, False, 0)
       
  1260                 vis_generator.flatten(message.get_visible())
       
  1261                 while True:
       
  1262                     line = vis_buffer.readline()
       
  1263                     self._file.write(line.replace('\n', os.linesep))
       
  1264                     if line == '\n' or line == '':
       
  1265                         break
       
  1266             else:
       
  1267                 orig_buffer.seek(0)
       
  1268                 while True:
       
  1269                     line = orig_buffer.readline()
       
  1270                     self._file.write(line.replace('\n', os.linesep))
       
  1271                     if line == '\n' or line == '':
       
  1272                         break
       
  1273             while True:
       
  1274                 buffer = orig_buffer.read(4096) # Buffer size is arbitrary.
       
  1275                 if buffer == '':
       
  1276                     break
       
  1277                 self._file.write(buffer.replace('\n', os.linesep))
       
  1278         elif isinstance(message, str):
       
  1279             body_start = message.find('\n\n') + 2
       
  1280             if body_start - 2 != -1:
       
  1281                 self._file.write(message[:body_start].replace('\n',
       
  1282                                                               os.linesep))
       
  1283                 self._file.write('*** EOOH ***' + os.linesep)
       
  1284                 self._file.write(message[:body_start].replace('\n',
       
  1285                                                               os.linesep))
       
  1286                 self._file.write(message[body_start:].replace('\n',
       
  1287                                                               os.linesep))
       
  1288             else:
       
  1289                 self._file.write('*** EOOH ***' + os.linesep + os.linesep)
       
  1290                 self._file.write(message.replace('\n', os.linesep))
       
  1291         elif hasattr(message, 'readline'):
       
  1292             original_pos = message.tell()
       
  1293             first_pass = True
       
  1294             while True:
       
  1295                 line = message.readline()
       
  1296                 self._file.write(line.replace('\n', os.linesep))
       
  1297                 if line == '\n' or line == '':
       
  1298                     self._file.write('*** EOOH ***' + os.linesep)
       
  1299                     if first_pass:
       
  1300                         first_pass = False
       
  1301                         message.seek(original_pos)
       
  1302                     else:
       
  1303                         break
       
  1304             while True:
       
  1305                 buffer = message.read(4096)     # Buffer size is arbitrary.
       
  1306                 if buffer == '':
       
  1307                     break
       
  1308                 self._file.write(buffer.replace('\n', os.linesep))
       
  1309         else:
       
  1310             raise TypeError('Invalid message type: %s' % type(message))
       
  1311         stop = self._file.tell()
       
  1312         return (start, stop)
       
  1313 
       
  1314 
       
  1315 class Message(email.Message.Message):
       
  1316     """Message with mailbox-format-specific properties."""
       
  1317 
       
  1318     def __init__(self, message=None):
       
  1319         """Initialize a Message instance."""
       
  1320         if isinstance(message, email.Message.Message):
       
  1321             self._become_message(copy.deepcopy(message))
       
  1322             if isinstance(message, Message):
       
  1323                 message._explain_to(self)
       
  1324         elif isinstance(message, str):
       
  1325             self._become_message(email.message_from_string(message))
       
  1326         elif hasattr(message, "read"):
       
  1327             self._become_message(email.message_from_file(message))
       
  1328         elif message is None:
       
  1329             email.Message.Message.__init__(self)
       
  1330         else:
       
  1331             raise TypeError('Invalid message type: %s' % type(message))
       
  1332 
       
  1333     def _become_message(self, message):
       
  1334         """Assume the non-format-specific state of message."""
       
  1335         for name in ('_headers', '_unixfrom', '_payload', '_charset',
       
  1336                      'preamble', 'epilogue', 'defects', '_default_type'):
       
  1337             self.__dict__[name] = message.__dict__[name]
       
  1338 
       
  1339     def _explain_to(self, message):
       
  1340         """Copy format-specific state to message insofar as possible."""
       
  1341         if isinstance(message, Message):
       
  1342             return  # There's nothing format-specific to explain.
       
  1343         else:
       
  1344             raise TypeError('Cannot convert to specified type')
       
  1345 
       
  1346 
       
  1347 class MaildirMessage(Message):
       
  1348     """Message with Maildir-specific properties."""
       
  1349 
       
  1350     def __init__(self, message=None):
       
  1351         """Initialize a MaildirMessage instance."""
       
  1352         self._subdir = 'new'
       
  1353         self._info = ''
       
  1354         self._date = time.time()
       
  1355         Message.__init__(self, message)
       
  1356 
       
  1357     def get_subdir(self):
       
  1358         """Return 'new' or 'cur'."""
       
  1359         return self._subdir
       
  1360 
       
  1361     def set_subdir(self, subdir):
       
  1362         """Set subdir to 'new' or 'cur'."""
       
  1363         if subdir == 'new' or subdir == 'cur':
       
  1364             self._subdir = subdir
       
  1365         else:
       
  1366             raise ValueError("subdir must be 'new' or 'cur': %s" % subdir)
       
  1367 
       
  1368     def get_flags(self):
       
  1369         """Return as a string the flags that are set."""
       
  1370         if self._info.startswith('2,'):
       
  1371             return self._info[2:]
       
  1372         else:
       
  1373             return ''
       
  1374 
       
  1375     def set_flags(self, flags):
       
  1376         """Set the given flags and unset all others."""
       
  1377         self._info = '2,' + ''.join(sorted(flags))
       
  1378 
       
  1379     def add_flag(self, flag):
       
  1380         """Set the given flag(s) without changing others."""
       
  1381         self.set_flags(''.join(set(self.get_flags()) | set(flag)))
       
  1382 
       
  1383     def remove_flag(self, flag):
       
  1384         """Unset the given string flag(s) without changing others."""
       
  1385         if self.get_flags() != '':
       
  1386             self.set_flags(''.join(set(self.get_flags()) - set(flag)))
       
  1387 
       
  1388     def get_date(self):
       
  1389         """Return delivery date of message, in seconds since the epoch."""
       
  1390         return self._date
       
  1391 
       
  1392     def set_date(self, date):
       
  1393         """Set delivery date of message, in seconds since the epoch."""
       
  1394         try:
       
  1395             self._date = float(date)
       
  1396         except ValueError:
       
  1397             raise TypeError("can't convert to float: %s" % date)
       
  1398 
       
  1399     def get_info(self):
       
  1400         """Get the message's "info" as a string."""
       
  1401         return self._info
       
  1402 
       
  1403     def set_info(self, info):
       
  1404         """Set the message's "info" string."""
       
  1405         if isinstance(info, str):
       
  1406             self._info = info
       
  1407         else:
       
  1408             raise TypeError('info must be a string: %s' % type(info))
       
  1409 
       
  1410     def _explain_to(self, message):
       
  1411         """Copy Maildir-specific state to message insofar as possible."""
       
  1412         if isinstance(message, MaildirMessage):
       
  1413             message.set_flags(self.get_flags())
       
  1414             message.set_subdir(self.get_subdir())
       
  1415             message.set_date(self.get_date())
       
  1416         elif isinstance(message, _mboxMMDFMessage):
       
  1417             flags = set(self.get_flags())
       
  1418             if 'S' in flags:
       
  1419                 message.add_flag('R')
       
  1420             if self.get_subdir() == 'cur':
       
  1421                 message.add_flag('O')
       
  1422             if 'T' in flags:
       
  1423                 message.add_flag('D')
       
  1424             if 'F' in flags:
       
  1425                 message.add_flag('F')
       
  1426             if 'R' in flags:
       
  1427                 message.add_flag('A')
       
  1428             message.set_from('MAILER-DAEMON', time.gmtime(self.get_date()))
       
  1429         elif isinstance(message, MHMessage):
       
  1430             flags = set(self.get_flags())
       
  1431             if 'S' not in flags:
       
  1432                 message.add_sequence('unseen')
       
  1433             if 'R' in flags:
       
  1434                 message.add_sequence('replied')
       
  1435             if 'F' in flags:
       
  1436                 message.add_sequence('flagged')
       
  1437         elif isinstance(message, BabylMessage):
       
  1438             flags = set(self.get_flags())
       
  1439             if 'S' not in flags:
       
  1440                 message.add_label('unseen')
       
  1441             if 'T' in flags:
       
  1442                 message.add_label('deleted')
       
  1443             if 'R' in flags:
       
  1444                 message.add_label('answered')
       
  1445             if 'P' in flags:
       
  1446                 message.add_label('forwarded')
       
  1447         elif isinstance(message, Message):
       
  1448             pass
       
  1449         else:
       
  1450             raise TypeError('Cannot convert to specified type: %s' %
       
  1451                             type(message))
       
  1452 
       
  1453 
       
  1454 class _mboxMMDFMessage(Message):
       
  1455     """Message with mbox- or MMDF-specific properties."""
       
  1456 
       
  1457     def __init__(self, message=None):
       
  1458         """Initialize an mboxMMDFMessage instance."""
       
  1459         self.set_from('MAILER-DAEMON', True)
       
  1460         if isinstance(message, email.Message.Message):
       
  1461             unixfrom = message.get_unixfrom()
       
  1462             if unixfrom is not None and unixfrom.startswith('From '):
       
  1463                 self.set_from(unixfrom[5:])
       
  1464         Message.__init__(self, message)
       
  1465 
       
  1466     def get_from(self):
       
  1467         """Return contents of "From " line."""
       
  1468         return self._from
       
  1469 
       
  1470     def set_from(self, from_, time_=None):
       
  1471         """Set "From " line, formatting and appending time_ if specified."""
       
  1472         if time_ is not None:
       
  1473             if time_ is True:
       
  1474                 time_ = time.gmtime()
       
  1475             from_ += ' ' + time.asctime(time_)
       
  1476         self._from = from_
       
  1477 
       
  1478     def get_flags(self):
       
  1479         """Return as a string the flags that are set."""
       
  1480         return self.get('Status', '') + self.get('X-Status', '')
       
  1481 
       
  1482     def set_flags(self, flags):
       
  1483         """Set the given flags and unset all others."""
       
  1484         flags = set(flags)
       
  1485         status_flags, xstatus_flags = '', ''
       
  1486         for flag in ('R', 'O'):
       
  1487             if flag in flags:
       
  1488                 status_flags += flag
       
  1489                 flags.remove(flag)
       
  1490         for flag in ('D', 'F', 'A'):
       
  1491             if flag in flags:
       
  1492                 xstatus_flags += flag
       
  1493                 flags.remove(flag)
       
  1494         xstatus_flags += ''.join(sorted(flags))
       
  1495         try:
       
  1496             self.replace_header('Status', status_flags)
       
  1497         except KeyError:
       
  1498             self.add_header('Status', status_flags)
       
  1499         try:
       
  1500             self.replace_header('X-Status', xstatus_flags)
       
  1501         except KeyError:
       
  1502             self.add_header('X-Status', xstatus_flags)
       
  1503 
       
  1504     def add_flag(self, flag):
       
  1505         """Set the given flag(s) without changing others."""
       
  1506         self.set_flags(''.join(set(self.get_flags()) | set(flag)))
       
  1507 
       
  1508     def remove_flag(self, flag):
       
  1509         """Unset the given string flag(s) without changing others."""
       
  1510         if 'Status' in self or 'X-Status' in self:
       
  1511             self.set_flags(''.join(set(self.get_flags()) - set(flag)))
       
  1512 
       
  1513     def _explain_to(self, message):
       
  1514         """Copy mbox- or MMDF-specific state to message insofar as possible."""
       
  1515         if isinstance(message, MaildirMessage):
       
  1516             flags = set(self.get_flags())
       
  1517             if 'O' in flags:
       
  1518                 message.set_subdir('cur')
       
  1519             if 'F' in flags:
       
  1520                 message.add_flag('F')
       
  1521             if 'A' in flags:
       
  1522                 message.add_flag('R')
       
  1523             if 'R' in flags:
       
  1524                 message.add_flag('S')
       
  1525             if 'D' in flags:
       
  1526                 message.add_flag('T')
       
  1527             del message['status']
       
  1528             del message['x-status']
       
  1529             maybe_date = ' '.join(self.get_from().split()[-5:])
       
  1530             try:
       
  1531                 message.set_date(calendar.timegm(time.strptime(maybe_date,
       
  1532                                                       '%a %b %d %H:%M:%S %Y')))
       
  1533             except (ValueError, OverflowError):
       
  1534                 pass
       
  1535         elif isinstance(message, _mboxMMDFMessage):
       
  1536             message.set_flags(self.get_flags())
       
  1537             message.set_from(self.get_from())
       
  1538         elif isinstance(message, MHMessage):
       
  1539             flags = set(self.get_flags())
       
  1540             if 'R' not in flags:
       
  1541                 message.add_sequence('unseen')
       
  1542             if 'A' in flags:
       
  1543                 message.add_sequence('replied')
       
  1544             if 'F' in flags:
       
  1545                 message.add_sequence('flagged')
       
  1546             del message['status']
       
  1547             del message['x-status']
       
  1548         elif isinstance(message, BabylMessage):
       
  1549             flags = set(self.get_flags())
       
  1550             if 'R' not in flags:
       
  1551                 message.add_label('unseen')
       
  1552             if 'D' in flags:
       
  1553                 message.add_label('deleted')
       
  1554             if 'A' in flags:
       
  1555                 message.add_label('answered')
       
  1556             del message['status']
       
  1557             del message['x-status']
       
  1558         elif isinstance(message, Message):
       
  1559             pass
       
  1560         else:
       
  1561             raise TypeError('Cannot convert to specified type: %s' %
       
  1562                             type(message))
       
  1563 
       
  1564 
       
  1565 class mboxMessage(_mboxMMDFMessage):
       
  1566     """Message with mbox-specific properties."""
       
  1567 
       
  1568 
       
  1569 class MHMessage(Message):
       
  1570     """Message with MH-specific properties."""
       
  1571 
       
  1572     def __init__(self, message=None):
       
  1573         """Initialize an MHMessage instance."""
       
  1574         self._sequences = []
       
  1575         Message.__init__(self, message)
       
  1576 
       
  1577     def get_sequences(self):
       
  1578         """Return a list of sequences that include the message."""
       
  1579         return self._sequences[:]
       
  1580 
       
  1581     def set_sequences(self, sequences):
       
  1582         """Set the list of sequences that include the message."""
       
  1583         self._sequences = list(sequences)
       
  1584 
       
  1585     def add_sequence(self, sequence):
       
  1586         """Add sequence to list of sequences including the message."""
       
  1587         if isinstance(sequence, str):
       
  1588             if not sequence in self._sequences:
       
  1589                 self._sequences.append(sequence)
       
  1590         else:
       
  1591             raise TypeError('sequence must be a string: %s' % type(sequence))
       
  1592 
       
  1593     def remove_sequence(self, sequence):
       
  1594         """Remove sequence from the list of sequences including the message."""
       
  1595         try:
       
  1596             self._sequences.remove(sequence)
       
  1597         except ValueError:
       
  1598             pass
       
  1599 
       
  1600     def _explain_to(self, message):
       
  1601         """Copy MH-specific state to message insofar as possible."""
       
  1602         if isinstance(message, MaildirMessage):
       
  1603             sequences = set(self.get_sequences())
       
  1604             if 'unseen' in sequences:
       
  1605                 message.set_subdir('cur')
       
  1606             else:
       
  1607                 message.set_subdir('cur')
       
  1608                 message.add_flag('S')
       
  1609             if 'flagged' in sequences:
       
  1610                 message.add_flag('F')
       
  1611             if 'replied' in sequences:
       
  1612                 message.add_flag('R')
       
  1613         elif isinstance(message, _mboxMMDFMessage):
       
  1614             sequences = set(self.get_sequences())
       
  1615             if 'unseen' not in sequences:
       
  1616                 message.add_flag('RO')
       
  1617             else:
       
  1618                 message.add_flag('O')
       
  1619             if 'flagged' in sequences:
       
  1620                 message.add_flag('F')
       
  1621             if 'replied' in sequences:
       
  1622                 message.add_flag('A')
       
  1623         elif isinstance(message, MHMessage):
       
  1624             for sequence in self.get_sequences():
       
  1625                 message.add_sequence(sequence)
       
  1626         elif isinstance(message, BabylMessage):
       
  1627             sequences = set(self.get_sequences())
       
  1628             if 'unseen' in sequences:
       
  1629                 message.add_label('unseen')
       
  1630             if 'replied' in sequences:
       
  1631                 message.add_label('answered')
       
  1632         elif isinstance(message, Message):
       
  1633             pass
       
  1634         else:
       
  1635             raise TypeError('Cannot convert to specified type: %s' %
       
  1636                             type(message))
       
  1637 
       
  1638 
       
  1639 class BabylMessage(Message):
       
  1640     """Message with Babyl-specific properties."""
       
  1641 
       
  1642     def __init__(self, message=None):
       
  1643         """Initialize an BabylMessage instance."""
       
  1644         self._labels = []
       
  1645         self._visible = Message()
       
  1646         Message.__init__(self, message)
       
  1647 
       
  1648     def get_labels(self):
       
  1649         """Return a list of labels on the message."""
       
  1650         return self._labels[:]
       
  1651 
       
  1652     def set_labels(self, labels):
       
  1653         """Set the list of labels on the message."""
       
  1654         self._labels = list(labels)
       
  1655 
       
  1656     def add_label(self, label):
       
  1657         """Add label to list of labels on the message."""
       
  1658         if isinstance(label, str):
       
  1659             if label not in self._labels:
       
  1660                 self._labels.append(label)
       
  1661         else:
       
  1662             raise TypeError('label must be a string: %s' % type(label))
       
  1663 
       
  1664     def remove_label(self, label):
       
  1665         """Remove label from the list of labels on the message."""
       
  1666         try:
       
  1667             self._labels.remove(label)
       
  1668         except ValueError:
       
  1669             pass
       
  1670 
       
  1671     def get_visible(self):
       
  1672         """Return a Message representation of visible headers."""
       
  1673         return Message(self._visible)
       
  1674 
       
  1675     def set_visible(self, visible):
       
  1676         """Set the Message representation of visible headers."""
       
  1677         self._visible = Message(visible)
       
  1678 
       
  1679     def update_visible(self):
       
  1680         """Update and/or sensibly generate a set of visible headers."""
       
  1681         for header in self._visible.keys():
       
  1682             if header in self:
       
  1683                 self._visible.replace_header(header, self[header])
       
  1684             else:
       
  1685                 del self._visible[header]
       
  1686         for header in ('Date', 'From', 'Reply-To', 'To', 'CC', 'Subject'):
       
  1687             if header in self and header not in self._visible:
       
  1688                 self._visible[header] = self[header]
       
  1689 
       
  1690     def _explain_to(self, message):
       
  1691         """Copy Babyl-specific state to message insofar as possible."""
       
  1692         if isinstance(message, MaildirMessage):
       
  1693             labels = set(self.get_labels())
       
  1694             if 'unseen' in labels:
       
  1695                 message.set_subdir('cur')
       
  1696             else:
       
  1697                 message.set_subdir('cur')
       
  1698                 message.add_flag('S')
       
  1699             if 'forwarded' in labels or 'resent' in labels:
       
  1700                 message.add_flag('P')
       
  1701             if 'answered' in labels:
       
  1702                 message.add_flag('R')
       
  1703             if 'deleted' in labels:
       
  1704                 message.add_flag('T')
       
  1705         elif isinstance(message, _mboxMMDFMessage):
       
  1706             labels = set(self.get_labels())
       
  1707             if 'unseen' not in labels:
       
  1708                 message.add_flag('RO')
       
  1709             else:
       
  1710                 message.add_flag('O')
       
  1711             if 'deleted' in labels:
       
  1712                 message.add_flag('D')
       
  1713             if 'answered' in labels:
       
  1714                 message.add_flag('A')
       
  1715         elif isinstance(message, MHMessage):
       
  1716             labels = set(self.get_labels())
       
  1717             if 'unseen' in labels:
       
  1718                 message.add_sequence('unseen')
       
  1719             if 'answered' in labels:
       
  1720                 message.add_sequence('replied')
       
  1721         elif isinstance(message, BabylMessage):
       
  1722             message.set_visible(self.get_visible())
       
  1723             for label in self.get_labels():
       
  1724                 message.add_label(label)
       
  1725         elif isinstance(message, Message):
       
  1726             pass
       
  1727         else:
       
  1728             raise TypeError('Cannot convert to specified type: %s' %
       
  1729                             type(message))
       
  1730 
       
  1731 
       
  1732 class MMDFMessage(_mboxMMDFMessage):
       
  1733     """Message with MMDF-specific properties."""
       
  1734 
       
  1735 
       
  1736 class _ProxyFile:
       
  1737     """A read-only wrapper of a file."""
       
  1738 
       
  1739     def __init__(self, f, pos=None):
       
  1740         """Initialize a _ProxyFile."""
       
  1741         self._file = f
       
  1742         if pos is None:
       
  1743             self._pos = f.tell()
       
  1744         else:
       
  1745             self._pos = pos
       
  1746 
       
  1747     def read(self, size=None):
       
  1748         """Read bytes."""
       
  1749         return self._read(size, self._file.read)
       
  1750 
       
  1751     def readline(self, size=None):
       
  1752         """Read a line."""
       
  1753         return self._read(size, self._file.readline)
       
  1754 
       
  1755     def readlines(self, sizehint=None):
       
  1756         """Read multiple lines."""
       
  1757         result = []
       
  1758         for line in self:
       
  1759             result.append(line)
       
  1760             if sizehint is not None:
       
  1761                 sizehint -= len(line)
       
  1762                 if sizehint <= 0:
       
  1763                     break
       
  1764         return result
       
  1765 
       
  1766     def __iter__(self):
       
  1767         """Iterate over lines."""
       
  1768         return iter(self.readline, "")
       
  1769 
       
  1770     def tell(self):
       
  1771         """Return the position."""
       
  1772         return self._pos
       
  1773 
       
  1774     def seek(self, offset, whence=0):
       
  1775         """Change position."""
       
  1776         if whence == 1:
       
  1777             self._file.seek(self._pos)
       
  1778         self._file.seek(offset, whence)
       
  1779         self._pos = self._file.tell()
       
  1780 
       
  1781     def close(self):
       
  1782         """Close the file."""
       
  1783         del self._file
       
  1784 
       
  1785     def _read(self, size, read_method):
       
  1786         """Read size bytes using read_method."""
       
  1787         if size is None:
       
  1788             size = -1
       
  1789         self._file.seek(self._pos)
       
  1790         result = read_method(size)
       
  1791         self._pos = self._file.tell()
       
  1792         return result
       
  1793 
       
  1794 
       
  1795 class _PartialFile(_ProxyFile):
       
  1796     """A read-only wrapper of part of a file."""
       
  1797 
       
  1798     def __init__(self, f, start=None, stop=None):
       
  1799         """Initialize a _PartialFile."""
       
  1800         _ProxyFile.__init__(self, f, start)
       
  1801         self._start = start
       
  1802         self._stop = stop
       
  1803 
       
  1804     def tell(self):
       
  1805         """Return the position with respect to start."""
       
  1806         return _ProxyFile.tell(self) - self._start
       
  1807 
       
  1808     def seek(self, offset, whence=0):
       
  1809         """Change position, possibly with respect to start or stop."""
       
  1810         if whence == 0:
       
  1811             self._pos = self._start
       
  1812             whence = 1
       
  1813         elif whence == 2:
       
  1814             self._pos = self._stop
       
  1815             whence = 1
       
  1816         _ProxyFile.seek(self, offset, whence)
       
  1817 
       
  1818     def _read(self, size, read_method):
       
  1819         """Read size bytes using read_method, honoring start and stop."""
       
  1820         remaining = self._stop - self._pos
       
  1821         if remaining <= 0:
       
  1822             return ''
       
  1823         if size is None or size < 0 or size > remaining:
       
  1824             size = remaining
       
  1825         return _ProxyFile._read(self, size, read_method)
       
  1826 
       
  1827 
       
  1828 def _lock_file(f, dotlock=True):
       
  1829     """Lock file f using lockf and dot locking."""
       
  1830     dotlock_done = False
       
  1831     try:
       
  1832         if fcntl:
       
  1833             try:
       
  1834                 fcntl.lockf(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
       
  1835             except IOError, e:
       
  1836                 if e.errno in (errno.EAGAIN, errno.EACCES):
       
  1837                     raise ExternalClashError('lockf: lock unavailable: %s' %
       
  1838                                              f.name)
       
  1839                 else:
       
  1840                     raise
       
  1841         if dotlock:
       
  1842             try:
       
  1843                 pre_lock = _create_temporary(f.name + '.lock')
       
  1844                 pre_lock.close()
       
  1845             except IOError, e:
       
  1846                 if e.errno == errno.EACCES:
       
  1847                     return  # Without write access, just skip dotlocking.
       
  1848                 else:
       
  1849                     raise
       
  1850             try:
       
  1851                 if hasattr(os, 'link'):
       
  1852                     os.link(pre_lock.name, f.name + '.lock')
       
  1853                     dotlock_done = True
       
  1854                     os.unlink(pre_lock.name)
       
  1855                 else:
       
  1856                     os.rename(pre_lock.name, f.name + '.lock')
       
  1857                     dotlock_done = True
       
  1858             except OSError, e:
       
  1859                 if e.errno == errno.EEXIST or \
       
  1860                   (os.name == 'os2' and e.errno == errno.EACCES):
       
  1861                     os.remove(pre_lock.name)
       
  1862                     raise ExternalClashError('dot lock unavailable: %s' %
       
  1863                                              f.name)
       
  1864                 else:
       
  1865                     raise
       
  1866     except:
       
  1867         if fcntl:
       
  1868             fcntl.lockf(f, fcntl.LOCK_UN)
       
  1869         if dotlock_done:
       
  1870             os.remove(f.name + '.lock')
       
  1871         raise
       
  1872 
       
  1873 def _unlock_file(f):
       
  1874     """Unlock file f using lockf and dot locking."""
       
  1875     if fcntl:
       
  1876         fcntl.lockf(f, fcntl.LOCK_UN)
       
  1877     if os.path.exists(f.name + '.lock'):
       
  1878         os.remove(f.name + '.lock')
       
  1879 
       
  1880 def _create_carefully(path):
       
  1881     """Create a file if it doesn't exist and open for reading and writing."""
       
  1882     fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDWR)
       
  1883     try:
       
  1884         return open(path, 'rb+')
       
  1885     finally:
       
  1886         os.close(fd)
       
  1887 
       
  1888 def _create_temporary(path):
       
  1889     """Create a temp file based on path and open for reading and writing."""
       
  1890     return _create_carefully('%s.%s.%s.%s' % (path, int(time.time()),
       
  1891                                               socket.gethostname(),
       
  1892                                               os.getpid()))
       
  1893 
       
  1894 def _sync_flush(f):
       
  1895     """Ensure changes to file f are physically on disk."""
       
  1896     f.flush()
       
  1897     if hasattr(os, 'fsync'):
       
  1898         os.fsync(f.fileno())
       
  1899 
       
  1900 def _sync_close(f):
       
  1901     """Close file f, ensuring all changes are physically on disk."""
       
  1902     _sync_flush(f)
       
  1903     f.close()
       
  1904 
       
  1905 ## Start: classes from the original module (for backward compatibility).
       
  1906 
       
  1907 # Note that the Maildir class, whose name is unchanged, itself offers a next()
       
  1908 # method for backward compatibility.
       
  1909 
       
  1910 class _Mailbox:
       
  1911 
       
  1912     def __init__(self, fp, factory=rfc822.Message):
       
  1913         self.fp = fp
       
  1914         self.seekp = 0
       
  1915         self.factory = factory
       
  1916 
       
  1917     def __iter__(self):
       
  1918         return iter(self.next, None)
       
  1919 
       
  1920     def next(self):
       
  1921         while 1:
       
  1922             self.fp.seek(self.seekp)
       
  1923             try:
       
  1924                 self._search_start()
       
  1925             except EOFError:
       
  1926                 self.seekp = self.fp.tell()
       
  1927                 return None
       
  1928             start = self.fp.tell()
       
  1929             self._search_end()
       
  1930             self.seekp = stop = self.fp.tell()
       
  1931             if start != stop:
       
  1932                 break
       
  1933         return self.factory(_PartialFile(self.fp, start, stop))
       
  1934 
       
  1935 # Recommended to use PortableUnixMailbox instead!
       
  1936 class UnixMailbox(_Mailbox):
       
  1937 
       
  1938     def _search_start(self):
       
  1939         while 1:
       
  1940             pos = self.fp.tell()
       
  1941             line = self.fp.readline()
       
  1942             if not line:
       
  1943                 raise EOFError
       
  1944             if line[:5] == 'From ' and self._isrealfromline(line):
       
  1945                 self.fp.seek(pos)
       
  1946                 return
       
  1947 
       
  1948     def _search_end(self):
       
  1949         self.fp.readline()      # Throw away header line
       
  1950         while 1:
       
  1951             pos = self.fp.tell()
       
  1952             line = self.fp.readline()
       
  1953             if not line:
       
  1954                 return
       
  1955             if line[:5] == 'From ' and self._isrealfromline(line):
       
  1956                 self.fp.seek(pos)
       
  1957                 return
       
  1958 
       
  1959     # An overridable mechanism to test for From-line-ness.  You can either
       
  1960     # specify a different regular expression or define a whole new
       
  1961     # _isrealfromline() method.  Note that this only gets called for lines
       
  1962     # starting with the 5 characters "From ".
       
  1963     #
       
  1964     # BAW: According to
       
  1965     #http://home.netscape.com/eng/mozilla/2.0/relnotes/demo/content-length.html
       
  1966     # the only portable, reliable way to find message delimiters in a BSD (i.e
       
  1967     # Unix mailbox) style folder is to search for "\n\nFrom .*\n", or at the
       
  1968     # beginning of the file, "^From .*\n".  While _fromlinepattern below seems
       
  1969     # like a good idea, in practice, there are too many variations for more
       
  1970     # strict parsing of the line to be completely accurate.
       
  1971     #
       
  1972     # _strict_isrealfromline() is the old version which tries to do stricter
       
  1973     # parsing of the From_ line.  _portable_isrealfromline() simply returns
       
  1974     # true, since it's never called if the line doesn't already start with
       
  1975     # "From ".
       
  1976     #
       
  1977     # This algorithm, and the way it interacts with _search_start() and
       
  1978     # _search_end() may not be completely correct, because it doesn't check
       
  1979     # that the two characters preceding "From " are \n\n or the beginning of
       
  1980     # the file.  Fixing this would require a more extensive rewrite than is
       
  1981     # necessary.  For convenience, we've added a PortableUnixMailbox class
       
  1982     # which does no checking of the format of the 'From' line.
       
  1983 
       
  1984     _fromlinepattern = (r"From \s*[^\s]+\s+\w\w\w\s+\w\w\w\s+\d?\d\s+"
       
  1985                         r"\d?\d:\d\d(:\d\d)?(\s+[^\s]+)?\s+\d\d\d\d\s*"
       
  1986                         r"[^\s]*\s*"
       
  1987                         "$")
       
  1988     _regexp = None
       
  1989 
       
  1990     def _strict_isrealfromline(self, line):
       
  1991         if not self._regexp:
       
  1992             import re
       
  1993             self._regexp = re.compile(self._fromlinepattern)
       
  1994         return self._regexp.match(line)
       
  1995 
       
  1996     def _portable_isrealfromline(self, line):
       
  1997         return True
       
  1998 
       
  1999     _isrealfromline = _strict_isrealfromline
       
  2000 
       
  2001 
       
  2002 class PortableUnixMailbox(UnixMailbox):
       
  2003     _isrealfromline = UnixMailbox._portable_isrealfromline
       
  2004 
       
  2005 
       
  2006 class MmdfMailbox(_Mailbox):
       
  2007 
       
  2008     def _search_start(self):
       
  2009         while 1:
       
  2010             line = self.fp.readline()
       
  2011             if not line:
       
  2012                 raise EOFError
       
  2013             if line[:5] == '\001\001\001\001\n':
       
  2014                 return
       
  2015 
       
  2016     def _search_end(self):
       
  2017         while 1:
       
  2018             pos = self.fp.tell()
       
  2019             line = self.fp.readline()
       
  2020             if not line:
       
  2021                 return
       
  2022             if line == '\001\001\001\001\n':
       
  2023                 self.fp.seek(pos)
       
  2024                 return
       
  2025 
       
  2026 
       
  2027 class MHMailbox:
       
  2028 
       
  2029     def __init__(self, dirname, factory=rfc822.Message):
       
  2030         import re
       
  2031         pat = re.compile('^[1-9][0-9]*$')
       
  2032         self.dirname = dirname
       
  2033         # the three following lines could be combined into:
       
  2034         # list = map(long, filter(pat.match, os.listdir(self.dirname)))
       
  2035         list = os.listdir(self.dirname)
       
  2036         list = filter(pat.match, list)
       
  2037         list = map(long, list)
       
  2038         list.sort()
       
  2039         # This only works in Python 1.6 or later;
       
  2040         # before that str() added 'L':
       
  2041         self.boxes = map(str, list)
       
  2042         self.boxes.reverse()
       
  2043         self.factory = factory
       
  2044 
       
  2045     def __iter__(self):
       
  2046         return iter(self.next, None)
       
  2047 
       
  2048     def next(self):
       
  2049         if not self.boxes:
       
  2050             return None
       
  2051         fn = self.boxes.pop()
       
  2052         fp = open(os.path.join(self.dirname, fn))
       
  2053         msg = self.factory(fp)
       
  2054         try:
       
  2055             msg._mh_msgno = fn
       
  2056         except (AttributeError, TypeError):
       
  2057             pass
       
  2058         return msg
       
  2059 
       
  2060 
       
  2061 class BabylMailbox(_Mailbox):
       
  2062 
       
  2063     def _search_start(self):
       
  2064         while 1:
       
  2065             line = self.fp.readline()
       
  2066             if not line:
       
  2067                 raise EOFError
       
  2068             if line == '*** EOOH ***\n':
       
  2069                 return
       
  2070 
       
  2071     def _search_end(self):
       
  2072         while 1:
       
  2073             pos = self.fp.tell()
       
  2074             line = self.fp.readline()
       
  2075             if not line:
       
  2076                 return
       
  2077             if line == '\037\014\n' or line == '\037':
       
  2078                 self.fp.seek(pos)
       
  2079                 return
       
  2080 
       
  2081 ## End: classes from the original module (for backward compatibility).
       
  2082 
       
  2083 
       
  2084 class Error(Exception):
       
  2085     """Raised for module-specific errors."""
       
  2086 
       
  2087 class NoSuchMailboxError(Error):
       
  2088     """The specified mailbox does not exist and won't be created."""
       
  2089 
       
  2090 class NotEmptyError(Error):
       
  2091     """The specified mailbox is not empty and deletion was requested."""
       
  2092 
       
  2093 class ExternalClashError(Error):
       
  2094     """Another process caused an action to fail."""
       
  2095 
       
  2096 class FormatError(Error):
       
  2097     """A file appears to have an invalid format."""