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