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