Home | History | Annotate | Download | only in Lib
      1 #! /usr/bin/env python

      2 
      3 """Read/write support for Maildir, mbox, MH, Babyl, and MMDF mailboxes."""
      4 
      5 # Notes for authors of new mailbox subclasses:

      6 #

      7 # Remember to fsync() changes to disk before closing a modified file

      8 # or returning from a flush() method.  See functions _sync_flush() and

      9 # _sync_close().

     10 
     11 import sys
     12 import os
     13 import time
     14 import calendar
     15 import socket
     16 import errno
     17 import copy
     18 import email
     19 import email.message
     20 import email.generator
     21 import StringIO
     22 try:
     23     if sys.platform == 'os2emx':
     24         # OS/2 EMX fcntl() not adequate

     25         raise ImportError
     26     import fcntl
     27 except ImportError:
     28     fcntl = None
     29 
     30 import warnings
     31 with warnings.catch_warnings():
     32     if sys.py3kwarning:
     33         warnings.filterwarnings("ignore", ".*rfc822 has been removed",
     34                                 DeprecationWarning)
     35     import rfc822
     36 
     37 __all__ = [ 'Mailbox', 'Maildir', 'mbox', 'MH', 'Babyl', 'MMDF',
     38             'Message', 'MaildirMessage', 'mboxMessage', 'MHMessage',
     39             'BabylMessage', 'MMDFMessage', 'UnixMailbox',
     40             'PortableUnixMailbox', 'MmdfMailbox', 'MHMailbox', 'BabylMailbox' ]
     41 
     42 class Mailbox:
     43     """A group of messages in a particular place."""
     44 
     45     def __init__(self, path, factory=None, create=True):
     46         """Initialize a Mailbox instance."""
     47         self._path = os.path.abspath(os.path.expanduser(path))
     48         self._factory = factory
     49 
     50     def add(self, message):
     51         """Add message and return assigned key."""
     52         raise NotImplementedError('Method must be implemented by subclass')
     53 
     54     def remove(self, key):
     55         """Remove the keyed message; raise KeyError if it doesn't exist."""
     56         raise NotImplementedError('Method must be implemented by subclass')
     57 
     58     def __delitem__(self, key):
     59         self.remove(key)
     60 
     61     def discard(self, key):
     62         """If the keyed message exists, remove it."""
     63         try:
     64             self.remove(key)
     65         except KeyError:
     66             pass
     67 
     68     def __setitem__(self, key, message):
     69         """Replace the keyed message; raise KeyError if it doesn't exist."""
     70         raise NotImplementedError('Method must be implemented by subclass')
     71 
     72     def get(self, key, default=None):
     73         """Return the keyed message, or default if it doesn't exist."""
     74         try:
     75             return self.__getitem__(key)
     76         except KeyError:
     77             return default
     78 
     79     def __getitem__(self, key):
     80         """Return the keyed message; raise KeyError if it doesn't exist."""
     81         if not self._factory:
     82             return self.get_message(key)
     83         else:
     84             return self._factory(self.get_file(key))
     85 
     86     def get_message(self, key):
     87         """Return a Message representation or raise a KeyError."""
     88         raise NotImplementedError('Method must be implemented by subclass')
     89 
     90     def get_string(self, key):
     91         """Return a string representation or raise a KeyError."""
     92         raise NotImplementedError('Method must be implemented by subclass')
     93 
     94     def get_file(self, key):
     95         """Return a file-like representation or raise a KeyError."""
     96         raise NotImplementedError('Method must be implemented by subclass')
     97 
     98     def iterkeys(self):
     99         """Return an iterator over keys."""
    100         raise NotImplementedError('Method must be implemented by subclass')
    101 
    102     def keys(self):
    103         """Return a list of keys."""
    104         return list(self.iterkeys())
    105 
    106     def itervalues(self):
    107         """Return an iterator over all messages."""
    108         for key in self.iterkeys():
    109             try:
    110                 value = self[key]
    111             except KeyError:
    112                 continue
    113             yield value
    114 
    115     def __iter__(self):
    116         return self.itervalues()
    117 
    118     def values(self):
    119         """Return a list of messages. Memory intensive."""
    120         return list(self.itervalues())
    121 
    122     def iteritems(self):
    123         """Return an iterator over (key, message) tuples."""
    124         for key in self.iterkeys():
    125             try:
    126                 value = self[key]
    127             except KeyError:
    128                 continue
    129             yield (key, value)
    130 
    131     def items(self):
    132         """Return a list of (key, message) tuples. Memory intensive."""
    133         return list(self.iteritems())
    134 
    135     def has_key(self, key):
    136         """Return True if the keyed message exists, False otherwise."""
    137         raise NotImplementedError('Method must be implemented by subclass')
    138 
    139     def __contains__(self, key):
    140         return self.has_key(key)
    141 
    142     def __len__(self):
    143         """Return a count of messages in the mailbox."""
    144         raise NotImplementedError('Method must be implemented by subclass')
    145 
    146     def clear(self):
    147         """Delete all messages."""
    148         for key in self.iterkeys():
    149             self.discard(key)
    150 
    151     def pop(self, key, default=None):
    152         """Delete the keyed message and return it, or default."""
    153         try:
    154             result = self[key]
    155         except KeyError:
    156             return default
    157         self.discard(key)
    158         return result
    159 
    160     def popitem(self):
    161         """Delete an arbitrary (key, message) pair and return it."""
    162         for key in self.iterkeys():
    163             return (key, self.pop(key))     # This is only run once.

    164         else:
    165             raise KeyError('No messages in mailbox')
    166 
    167     def update(self, arg=None):
    168         """Change the messages that correspond to certain keys."""
    169         if hasattr(arg, 'iteritems'):
    170             source = arg.iteritems()
    171         elif hasattr(arg, 'items'):
    172             source = arg.items()
    173         else:
    174             source = arg
    175         bad_key = False
    176         for key, message in source:
    177             try:
    178                 self[key] = message
    179             except KeyError:
    180                 bad_key = True
    181         if bad_key:
    182             raise KeyError('No message with key(s)')
    183 
    184     def flush(self):
    185         """Write any pending changes to the disk."""
    186         raise NotImplementedError('Method must be implemented by subclass')
    187 
    188     def lock(self):
    189         """Lock the mailbox."""
    190         raise NotImplementedError('Method must be implemented by subclass')
    191 
    192     def unlock(self):
    193         """Unlock the mailbox if it is locked."""
    194         raise NotImplementedError('Method must be implemented by subclass')
    195 
    196     def close(self):
    197         """Flush and close the mailbox."""
    198         raise NotImplementedError('Method must be implemented by subclass')
    199 
    200     def _dump_message(self, message, target, mangle_from_=False):
    201         # Most files are opened in binary mode to allow predictable seeking.

    202         # To get native line endings on disk, the user-friendly \n line endings

    203         # used in strings and by email.Message are translated here.

    204         """Dump message contents to target file."""
    205         if isinstance(message, email.message.Message):
    206             buffer = StringIO.StringIO()
    207             gen = email.generator.Generator(buffer, mangle_from_, 0)
    208             gen.flatten(message)
    209             buffer.seek(0)
    210             target.write(buffer.read().replace('\n', os.linesep))
    211         elif isinstance(message, str):
    212             if mangle_from_:
    213                 message = message.replace('\nFrom ', '\n>From ')
    214             message = message.replace('\n', os.linesep)
    215             target.write(message)
    216         elif hasattr(message, 'read'):
    217             while True:
    218                 line = message.readline()
    219                 if line == '':
    220                     break
    221                 if mangle_from_ and line.startswith('From '):
    222                     line = '>From ' + line[5:]
    223                 line = line.replace('\n', os.linesep)
    224                 target.write(line)
    225         else:
    226             raise TypeError('Invalid message type: %s' % type(message))
    227 
    228 
    229 class Maildir(Mailbox):
    230     """A qmail-style Maildir mailbox."""
    231 
    232     colon = ':'
    233 
    234     def __init__(self, dirname, factory=rfc822.Message, create=True):
    235         """Initialize a Maildir instance."""
    236         Mailbox.__init__(self, dirname, factory, create)
    237         self._paths = {
    238             'tmp': os.path.join(self._path, 'tmp'),
    239             'new': os.path.join(self._path, 'new'),
    240             'cur': os.path.join(self._path, 'cur'),
    241             }
    242         if not os.path.exists(self._path):
    243             if create:
    244                 os.mkdir(self._path, 0700)
    245                 for path in self._paths.values():
    246                     os.mkdir(path, 0o700)
    247             else:
    248                 raise NoSuchMailboxError(self._path)
    249         self._toc = {}
    250         self._toc_mtimes = {}
    251         for subdir in ('cur', 'new'):
    252             self._toc_mtimes[subdir] = os.path.getmtime(self._paths[subdir])
    253         self._last_read = time.time()  # Records last time we read cur/new

    254         self._skewfactor = 0.1         # Adjust if os/fs clocks are skewing

    255 
    256     def add(self, message):
    257         """Add message and return assigned key."""
    258         tmp_file = self._create_tmp()
    259         try:
    260             self._dump_message(message, tmp_file)
    261         except BaseException:
    262             tmp_file.close()
    263             os.remove(tmp_file.name)
    264             raise
    265         _sync_close(tmp_file)
    266         if isinstance(message, MaildirMessage):
    267             subdir = message.get_subdir()
    268             suffix = self.colon + message.get_info()
    269             if suffix == self.colon:
    270                 suffix = ''
    271         else:
    272             subdir = 'new'
    273             suffix = ''
    274         uniq = os.path.basename(tmp_file.name).split(self.colon)[0]
    275         dest = os.path.join(self._path, subdir, uniq + suffix)
    276         try:
    277             if hasattr(os, 'link'):
    278                 os.link(tmp_file.name, dest)
    279                 os.remove(tmp_file.name)
    280             else:
    281                 os.rename(tmp_file.name, dest)
    282         except OSError, e:
    283             os.remove(tmp_file.name)
    284             if e.errno == errno.EEXIST:
    285                 raise ExternalClashError('Name clash with existing message: %s'
    286                                          % dest)
    287             else:
    288                 raise
    289         if isinstance(message, MaildirMessage):
    290             os.utime(dest, (os.path.getatime(dest), message.get_date()))
    291         return uniq
    292 
    293     def remove(self, key):
    294         """Remove the keyed message; raise KeyError if it doesn't exist."""
    295         os.remove(os.path.join(self._path, self._lookup(key)))
    296 
    297     def discard(self, key):
    298         """If the keyed message exists, remove it."""
    299         # This overrides an inapplicable implementation in the superclass.

    300         try:
    301             self.remove(key)
    302         except KeyError:
    303             pass
    304         except OSError, e:
    305             if e.errno != errno.ENOENT:
    306                 raise
    307 
    308     def __setitem__(self, key, message):
    309         """Replace the keyed message; raise KeyError if it doesn't exist."""
    310         old_subpath = self._lookup(key)
    311         temp_key = self.add(message)
    312         temp_subpath = self._lookup(temp_key)
    313         if isinstance(message, MaildirMessage):
    314             # temp's subdir and suffix were specified by message.

    315             dominant_subpath = temp_subpath
    316         else:
    317             # temp's subdir and suffix were defaults from add().

    318             dominant_subpath = old_subpath
    319         subdir = os.path.dirname(dominant_subpath)
    320         if self.colon in dominant_subpath:
    321             suffix = self.colon + dominant_subpath.split(self.colon)[-1]
    322         else:
    323             suffix = ''
    324         self.discard(key)
    325         new_path = os.path.join(self._path, subdir, key + suffix)
    326         os.rename(os.path.join(self._path, temp_subpath), new_path)
    327         if isinstance(message, MaildirMessage):
    328             os.utime(new_path, (os.path.getatime(new_path),
    329                                 message.get_date()))
    330 
    331     def get_message(self, key):
    332         """Return a Message representation or raise a KeyError."""
    333         subpath = self._lookup(key)
    334         f = open(os.path.join(self._path, subpath), 'r')
    335         try:
    336             if self._factory:
    337                 msg = self._factory(f)
    338             else:
    339                 msg = MaildirMessage(f)
    340         finally:
    341             f.close()
    342         subdir, name = os.path.split(subpath)
    343         msg.set_subdir(subdir)
    344         if self.colon in name:
    345             msg.set_info(name.split(self.colon)[-1])
    346         msg.set_date(os.path.getmtime(os.path.join(self._path, subpath)))
    347         return msg
    348 
    349     def get_string(self, key):
    350         """Return a string representation or raise a KeyError."""
    351         f = open(os.path.join(self._path, self._lookup(key)), 'r')
    352         try:
    353             return f.read()
    354         finally:
    355             f.close()
    356 
    357     def get_file(self, key):
    358         """Return a file-like representation or raise a KeyError."""
    359         f = open(os.path.join(self._path, self._lookup(key)), 'rb')
    360         return _ProxyFile(f)
    361 
    362     def iterkeys(self):
    363         """Return an iterator over keys."""
    364         self._refresh()
    365         for key in self._toc:
    366             try:
    367                 self._lookup(key)
    368             except KeyError:
    369                 continue
    370             yield key
    371 
    372     def has_key(self, key):
    373         """Return True if the keyed message exists, False otherwise."""
    374         self._refresh()
    375         return key in self._toc
    376 
    377     def __len__(self):
    378         """Return a count of messages in the mailbox."""
    379         self._refresh()
    380         return len(self._toc)
    381 
    382     def flush(self):
    383         """Write any pending changes to disk."""
    384         # Maildir changes are always written immediately, so there's nothing

    385         # to do.

    386         pass
    387 
    388     def lock(self):
    389         """Lock the mailbox."""
    390         return
    391 
    392     def unlock(self):
    393         """Unlock the mailbox if it is locked."""
    394         return
    395 
    396     def close(self):
    397         """Flush and close the mailbox."""
    398         return
    399 
    400     def list_folders(self):
    401         """Return a list of folder names."""
    402         result = []
    403         for entry in os.listdir(self._path):
    404             if len(entry) > 1 and entry[0] == '.' and \
    405                os.path.isdir(os.path.join(self._path, entry)):
    406                 result.append(entry[1:])
    407         return result
    408 
    409     def get_folder(self, folder):
    410         """Return a Maildir instance for the named folder."""
    411         return Maildir(os.path.join(self._path, '.' + folder),
    412                        factory=self._factory,
    413                        create=False)
    414 
    415     def add_folder(self, folder):
    416         """Create a folder and return a Maildir instance representing it."""
    417         path = os.path.join(self._path, '.' + folder)
    418         result = Maildir(path, factory=self._factory)
    419         maildirfolder_path = os.path.join(path, 'maildirfolder')
    420         if not os.path.exists(maildirfolder_path):
    421             os.close(os.open(maildirfolder_path, os.O_CREAT | os.O_WRONLY,
    422                 0666))
    423         return result
    424 
    425     def remove_folder(self, folder):
    426         """Delete the named folder, which must be empty."""
    427         path = os.path.join(self._path, '.' + folder)
    428         for entry in os.listdir(os.path.join(path, 'new')) + \
    429                      os.listdir(os.path.join(path, 'cur')):
    430             if len(entry) < 1 or entry[0] != '.':
    431                 raise NotEmptyError('Folder contains message(s): %s' % folder)
    432         for entry in os.listdir(path):
    433             if entry != 'new' and entry != 'cur' and entry != 'tmp' and \
    434                os.path.isdir(os.path.join(path, entry)):
    435                 raise NotEmptyError("Folder contains subdirectory '%s': %s" %
    436                                     (folder, entry))
    437         for root, dirs, files in os.walk(path, topdown=False):
    438             for entry in files:
    439                 os.remove(os.path.join(root, entry))
    440             for entry in dirs:
    441                 os.rmdir(os.path.join(root, entry))
    442         os.rmdir(path)
    443 
    444     def clean(self):
    445         """Delete old files in "tmp"."""
    446         now = time.time()
    447         for entry in os.listdir(os.path.join(self._path, 'tmp')):
    448             path = os.path.join(self._path, 'tmp', entry)
    449             if now - os.path.getatime(path) > 129600:   # 60 * 60 * 36

    450                 os.remove(path)
    451 
    452     _count = 1  # This is used to generate unique file names.

    453 
    454     def _create_tmp(self):
    455         """Create a file in the tmp subdirectory and open and return it."""
    456         now = time.time()
    457         hostname = socket.gethostname()
    458         if '/' in hostname:
    459             hostname = hostname.replace('/', r'\057')
    460         if ':' in hostname:
    461             hostname = hostname.replace(':', r'\072')
    462         uniq = "%s.M%sP%sQ%s.%s" % (int(now), int(now % 1 * 1e6), os.getpid(),
    463                                     Maildir._count, hostname)
    464         path = os.path.join(self._path, 'tmp', uniq)
    465         try:
    466             os.stat(path)
    467         except OSError, e:
    468             if e.errno == errno.ENOENT:
    469                 Maildir._count += 1
    470                 try:
    471                     return _create_carefully(path)
    472                 except OSError, e:
    473                     if e.errno != errno.EEXIST:
    474                         raise
    475             else:
    476                 raise
    477 
    478         # Fall through to here if stat succeeded or open raised EEXIST.

    479         raise ExternalClashError('Name clash prevented file creation: %s' %
    480                                  path)
    481 
    482     def _refresh(self):
    483         """Update table of contents mapping."""
    484         # If it has been less than two seconds since the last _refresh() call,

    485         # we have to unconditionally re-read the mailbox just in case it has

    486         # been modified, because os.path.mtime() has a 2 sec resolution in the

    487         # most common worst case (FAT) and a 1 sec resolution typically.  This

    488         # results in a few unnecessary re-reads when _refresh() is called

    489         # multiple times in that interval, but once the clock ticks over, we

    490         # will only re-read as needed.  Because the filesystem might be being

    491         # served by an independent system with its own clock, we record and

    492         # compare with the mtimes from the filesystem.  Because the other

    493         # system's clock might be skewing relative to our clock, we add an

    494         # extra delta to our wait.  The default is one tenth second, but is an

    495         # instance variable and so can be adjusted if dealing with a

    496         # particularly skewed or irregular system.

    497         if time.time() - self._last_read > 2 + self._skewfactor:
    498             refresh = False
    499             for subdir in self._toc_mtimes:
    500                 mtime = os.path.getmtime(self._paths[subdir])
    501                 if mtime > self._toc_mtimes[subdir]:
    502                     refresh = True
    503                 self._toc_mtimes[subdir] = mtime
    504             if not refresh:
    505                 return
    506         # Refresh toc

    507         self._toc = {}
    508         for subdir in self._toc_mtimes:
    509             path = self._paths[subdir]
    510             for entry in os.listdir(path):
    511                 p = os.path.join(path, entry)
    512                 if os.path.isdir(p):
    513                     continue
    514                 uniq = entry.split(self.colon)[0]
    515                 self._toc[uniq] = os.path.join(subdir, entry)
    516         self._last_read = time.time()
    517 
    518     def _lookup(self, key):
    519         """Use TOC to return subpath for given key, or raise a KeyError."""
    520         try:
    521             if os.path.exists(os.path.join(self._path, self._toc[key])):
    522                 return self._toc[key]
    523         except KeyError:
    524             pass
    525         self._refresh()
    526         try:
    527             return self._toc[key]
    528         except KeyError:
    529             raise KeyError('No message with key: %s' % key)
    530 
    531     # This method is for backward compatibility only.

    532     def next(self):
    533         """Return the next message in a one-time iteration."""
    534         if not hasattr(self, '_onetime_keys'):
    535             self._onetime_keys = self.iterkeys()
    536         while True:
    537             try:
    538                 return self[self._onetime_keys.next()]
    539             except StopIteration:
    540                 return None
    541             except KeyError:
    542                 continue
    543 
    544 
    545 class _singlefileMailbox(Mailbox):
    546     """A single-file mailbox."""
    547 
    548     def __init__(self, path, factory=None, create=True):
    549         """Initialize a single-file mailbox."""
    550         Mailbox.__init__(self, path, factory, create)
    551         try:
    552             f = open(self._path, 'rb+')
    553         except IOError, e:
    554             if e.errno == errno.ENOENT:
    555                 if create:
    556                     f = open(self._path, 'wb+')
    557                 else:
    558                     raise NoSuchMailboxError(self._path)
    559             elif e.errno in (errno.EACCES, errno.EROFS):
    560                 f = open(self._path, 'rb')
    561             else:
    562                 raise
    563         self._file = f
    564         self._toc = None
    565         self._next_key = 0
    566         self._pending = False   # No changes require rewriting the file.

    567         self._locked = False
    568         self._file_length = None        # Used to record mailbox size

    569 
    570     def add(self, message):
    571         """Add message and return assigned key."""
    572         self._lookup()
    573         self._toc[self._next_key] = self._append_message(message)
    574         self._next_key += 1
    575         self._pending = True
    576         return self._next_key - 1
    577 
    578     def remove(self, key):
    579         """Remove the keyed message; raise KeyError if it doesn't exist."""
    580         self._lookup(key)
    581         del self._toc[key]
    582         self._pending = True
    583 
    584     def __setitem__(self, key, message):
    585         """Replace the keyed message; raise KeyError if it doesn't exist."""
    586         self._lookup(key)
    587         self._toc[key] = self._append_message(message)
    588         self._pending = True
    589 
    590     def iterkeys(self):
    591         """Return an iterator over keys."""
    592         self._lookup()
    593         for key in self._toc.keys():
    594             yield key
    595 
    596     def has_key(self, key):
    597         """Return True if the keyed message exists, False otherwise."""
    598         self._lookup()
    599         return key in self._toc
    600 
    601     def __len__(self):
    602         """Return a count of messages in the mailbox."""
    603         self._lookup()
    604         return len(self._toc)
    605 
    606     def lock(self):
    607         """Lock the mailbox."""
    608         if not self._locked:
    609             _lock_file(self._file)
    610             self._locked = True
    611 
    612     def unlock(self):
    613         """Unlock the mailbox if it is locked."""
    614         if self._locked:
    615             _unlock_file(self._file)
    616             self._locked = False
    617 
    618     def flush(self):
    619         """Write any pending changes to disk."""
    620         if not self._pending:
    621             return
    622 
    623         # In order to be writing anything out at all, self._toc must

    624         # already have been generated (and presumably has been modified

    625         # by adding or deleting an item).

    626         assert self._toc is not None
    627 
    628         # Check length of self._file; if it's changed, some other process

    629         # has modified the mailbox since we scanned it.

    630         self._file.seek(0, 2)
    631         cur_len = self._file.tell()
    632         if cur_len != self._file_length:
    633             raise ExternalClashError('Size of mailbox file changed '
    634                                      '(expected %i, found %i)' %
    635                                      (self._file_length, cur_len))
    636 
    637         new_file = _create_temporary(self._path)
    638         try:
    639             new_toc = {}
    640             self._pre_mailbox_hook(new_file)
    641             for key in sorted(self._toc.keys()):
    642                 start, stop = self._toc[key]
    643                 self._file.seek(start)
    644                 self._pre_message_hook(new_file)
    645                 new_start = new_file.tell()
    646                 while True:
    647                     buffer = self._file.read(min(4096,
    648                                                  stop - self._file.tell()))
    649                     if buffer == '':
    650                         break
    651                     new_file.write(buffer)
    652                 new_toc[key] = (new_start, new_file.tell())
    653                 self._post_message_hook(new_file)
    654         except:
    655             new_file.close()
    656             os.remove(new_file.name)
    657             raise
    658         _sync_close(new_file)
    659         # self._file is about to get replaced, so no need to sync.

    660         self._file.close()
    661         try:
    662             os.rename(new_file.name, self._path)
    663         except OSError, e:
    664             if e.errno == errno.EEXIST or \
    665               (os.name == 'os2' and e.errno == errno.EACCES):
    666                 os.remove(self._path)
    667                 os.rename(new_file.name, self._path)
    668             else:
    669                 raise
    670         self._file = open(self._path, 'rb+')
    671         self._toc = new_toc
    672         self._pending = False
    673         if self._locked:
    674             _lock_file(self._file, dotlock=False)
    675 
    676     def _pre_mailbox_hook(self, f):
    677         """Called before writing the mailbox to file f."""
    678         return
    679 
    680     def _pre_message_hook(self, f):
    681         """Called before writing each message to file f."""
    682         return
    683 
    684     def _post_message_hook(self, f):
    685         """Called after writing each message to file f."""
    686         return
    687 
    688     def close(self):
    689         """Flush and close the mailbox."""
    690         self.flush()
    691         if self._locked:
    692             self.unlock()
    693         self._file.close()  # Sync has been done by self.flush() above.

    694 
    695     def _lookup(self, key=None):
    696         """Return (start, stop) or raise KeyError."""
    697         if self._toc is None:
    698             self._generate_toc()
    699         if key is not None:
    700             try:
    701                 return self._toc[key]
    702             except KeyError:
    703                 raise KeyError('No message with key: %s' % key)
    704 
    705     def _append_message(self, message):
    706         """Append message to mailbox and return (start, stop) offsets."""
    707         self._file.seek(0, 2)
    708         before = self._file.tell()
    709         try:
    710             self._pre_message_hook(self._file)
    711             offsets = self._install_message(message)
    712             self._post_message_hook(self._file)
    713         except BaseException:
    714             self._file.truncate(before)
    715             raise
    716         self._file.flush()
    717         self._file_length = self._file.tell()  # Record current length of mailbox

    718         return offsets
    719 
    720 
    721 
    722 class _mboxMMDF(_singlefileMailbox):
    723     """An mbox or MMDF mailbox."""
    724 
    725     _mangle_from_ = True
    726 
    727     def get_message(self, key):
    728         """Return a Message representation or raise a KeyError."""
    729         start, stop = self._lookup(key)
    730         self._file.seek(start)
    731         from_line = self._file.readline().replace(os.linesep, '')
    732         string = self._file.read(stop - self._file.tell())
    733         msg = self._message_factory(string.replace(os.linesep, '\n'))
    734         msg.set_from(from_line[5:])
    735         return msg
    736 
    737     def get_string(self, key, from_=False):
    738         """Return a string representation or raise a KeyError."""
    739         start, stop = self._lookup(key)
    740         self._file.seek(start)
    741         if not from_:
    742             self._file.readline()
    743         string = self._file.read(stop - self._file.tell())
    744         return string.replace(os.linesep, '\n')
    745 
    746     def get_file(self, key, from_=False):
    747         """Return a file-like representation or raise a KeyError."""
    748         start, stop = self._lookup(key)
    749         self._file.seek(start)
    750         if not from_:
    751             self._file.readline()
    752         return _PartialFile(self._file, self._file.tell(), stop)
    753 
    754     def _install_message(self, message):
    755         """Format a message and blindly write to self._file."""
    756         from_line = None
    757         if isinstance(message, str) and message.startswith('From '):
    758             newline = message.find('\n')
    759             if newline != -1:
    760                 from_line = message[:newline]
    761                 message = message[newline + 1:]
    762             else:
    763                 from_line = message
    764                 message = ''
    765         elif isinstance(message, _mboxMMDFMessage):
    766             from_line = 'From ' + message.get_from()
    767         elif isinstance(message, email.message.Message):
    768             from_line = message.get_unixfrom()  # May be None.

    769         if from_line is None:
    770             from_line = 'From MAILER-DAEMON %s' % time.asctime(time.gmtime())
    771         start = self._file.tell()
    772         self._file.write(from_line + os.linesep)
    773         self._dump_message(message, self._file, self._mangle_from_)
    774         stop = self._file.tell()
    775         return (start, stop)
    776 
    777 
    778 class mbox(_mboxMMDF):
    779     """A classic mbox mailbox."""
    780 
    781     _mangle_from_ = True
    782 
    783     def __init__(self, path, factory=None, create=True):
    784         """Initialize an mbox mailbox."""
    785         self._message_factory = mboxMessage
    786         _mboxMMDF.__init__(self, path, factory, create)
    787 
    788     def _pre_message_hook(self, f):
    789         """Called before writing each message to file f."""
    790         if f.tell() != 0:
    791             f.write(os.linesep)
    792 
    793     def _generate_toc(self):
    794         """Generate key-to-(start, stop) table of contents."""
    795         starts, stops = [], []
    796         self._file.seek(0)
    797         while True:
    798             line_pos = self._file.tell()
    799             line = self._file.readline()
    800             if line.startswith('From '):
    801                 if len(stops) < len(starts):
    802                     stops.append(line_pos - len(os.linesep))
    803                 starts.append(line_pos)
    804             elif line == '':
    805                 stops.append(line_pos)
    806                 break
    807         self._toc = dict(enumerate(zip(starts, stops)))
    808         self._next_key = len(self._toc)
    809         self._file_length = self._file.tell()
    810 
    811 
    812 class MMDF(_mboxMMDF):
    813     """An MMDF mailbox."""
    814 
    815     def __init__(self, path, factory=None, create=True):
    816         """Initialize an MMDF mailbox."""
    817         self._message_factory = MMDFMessage
    818         _mboxMMDF.__init__(self, path, factory, create)
    819 
    820     def _pre_message_hook(self, f):
    821         """Called before writing each message to file f."""
    822         f.write('\001\001\001\001' + os.linesep)
    823 
    824     def _post_message_hook(self, f):
    825         """Called after writing each message to file f."""
    826         f.write(os.linesep + '\001\001\001\001' + os.linesep)
    827 
    828     def _generate_toc(self):
    829         """Generate key-to-(start, stop) table of contents."""
    830         starts, stops = [], []
    831         self._file.seek(0)
    832         next_pos = 0
    833         while True:
    834             line_pos = next_pos
    835             line = self._file.readline()
    836             next_pos = self._file.tell()
    837             if line.startswith('\001\001\001\001' + os.linesep):
    838                 starts.append(next_pos)
    839                 while True:
    840                     line_pos = next_pos
    841                     line = self._file.readline()
    842                     next_pos = self._file.tell()
    843                     if line == '\001\001\001\001' + os.linesep:
    844                         stops.append(line_pos - len(os.linesep))
    845                         break
    846                     elif line == '':
    847                         stops.append(line_pos)
    848                         break
    849             elif line == '':
    850                 break
    851         self._toc = dict(enumerate(zip(starts, stops)))
    852         self._next_key = len(self._toc)
    853         self._file.seek(0, 2)
    854         self._file_length = self._file.tell()
    855 
    856 
    857 class MH(Mailbox):
    858     """An MH mailbox."""
    859 
    860     def __init__(self, path, factory=None, create=True):
    861         """Initialize an MH instance."""
    862         Mailbox.__init__(self, path, factory, create)
    863         if not os.path.exists(self._path):
    864             if create:
    865                 os.mkdir(self._path, 0700)
    866                 os.close(os.open(os.path.join(self._path, '.mh_sequences'),
    867                                  os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0600))
    868             else:
    869                 raise NoSuchMailboxError(self._path)
    870         self._locked = False
    871 
    872     def add(self, message):
    873         """Add message and return assigned key."""
    874         keys = self.keys()
    875         if len(keys) == 0:
    876             new_key = 1
    877         else:
    878             new_key = max(keys) + 1
    879         new_path = os.path.join(self._path, str(new_key))
    880         f = _create_carefully(new_path)
    881         closed = False
    882         try:
    883             if self._locked:
    884                 _lock_file(f)
    885             try:
    886                 try:
    887                     self._dump_message(message, f)
    888                 except BaseException:
    889                     # Unlock and close so it can be deleted on Windows

    890                     if self._locked:
    891                         _unlock_file(f)
    892                     _sync_close(f)
    893                     closed = True
    894                     os.remove(new_path)
    895                     raise
    896                 if isinstance(message, MHMessage):
    897                     self._dump_sequences(message, new_key)
    898             finally:
    899                 if self._locked:
    900                     _unlock_file(f)
    901         finally:
    902             if not closed:
    903                 _sync_close(f)
    904         return new_key
    905 
    906     def remove(self, key):
    907         """Remove the keyed message; raise KeyError if it doesn't exist."""
    908         path = os.path.join(self._path, str(key))
    909         try:
    910             f = open(path, 'rb+')
    911         except IOError, e:
    912             if e.errno == errno.ENOENT:
    913                 raise KeyError('No message with key: %s' % key)
    914             else:
    915                 raise
    916         else:
    917             f.close()
    918             os.remove(path)
    919 
    920     def __setitem__(self, key, message):
    921         """Replace the keyed message; raise KeyError if it doesn't exist."""
    922         path = os.path.join(self._path, str(key))
    923         try:
    924             f = open(path, 'rb+')
    925         except IOError, e:
    926             if e.errno == errno.ENOENT:
    927                 raise KeyError('No message with key: %s' % key)
    928             else:
    929                 raise
    930         try:
    931             if self._locked:
    932                 _lock_file(f)
    933             try:
    934                 os.close(os.open(path, os.O_WRONLY | os.O_TRUNC))
    935                 self._dump_message(message, f)
    936                 if isinstance(message, MHMessage):
    937                     self._dump_sequences(message, key)
    938             finally:
    939                 if self._locked:
    940                     _unlock_file(f)
    941         finally:
    942             _sync_close(f)
    943 
    944     def get_message(self, key):
    945         """Return a Message representation or raise a KeyError."""
    946         try:
    947             if self._locked:
    948                 f = open(os.path.join(self._path, str(key)), 'r+')
    949             else:
    950                 f = open(os.path.join(self._path, str(key)), 'r')
    951         except IOError, e:
    952             if e.errno == errno.ENOENT:
    953                 raise KeyError('No message with key: %s' % key)
    954             else:
    955                 raise
    956         try:
    957             if self._locked:
    958                 _lock_file(f)
    959             try:
    960                 msg = MHMessage(f)
    961             finally:
    962                 if self._locked:
    963                     _unlock_file(f)
    964         finally:
    965             f.close()
    966         for name, key_list in self.get_sequences().iteritems():
    967             if key in key_list:
    968                 msg.add_sequence(name)
    969         return msg
    970 
    971     def get_string(self, key):
    972         """Return a string representation or raise a KeyError."""
    973         try:
    974             if self._locked:
    975                 f = open(os.path.join(self._path, str(key)), 'r+')
    976             else:
    977                 f = open(os.path.join(self._path, str(key)), 'r')
    978         except IOError, e:
    979             if e.errno == errno.ENOENT:
    980                 raise KeyError('No message with key: %s' % key)
    981             else:
    982                 raise
    983         try:
    984             if self._locked:
    985                 _lock_file(f)
    986             try:
    987                 return f.read()
    988             finally:
    989                 if self._locked:
    990                     _unlock_file(f)
    991         finally:
    992             f.close()
    993 
    994     def get_file(self, key):
    995         """Return a file-like representation or raise a KeyError."""
    996         try:
    997             f = open(os.path.join(self._path, str(key)), 'rb')
    998         except IOError, e:
    999             if e.errno == errno.ENOENT:
   1000                 raise KeyError('No message with key: %s' % key)
   1001             else:
   1002                 raise
   1003         return _ProxyFile(f)
   1004 
   1005     def iterkeys(self):
   1006         """Return an iterator over keys."""
   1007         return iter(sorted(int(entry) for entry in os.listdir(self._path)
   1008                                       if entry.isdigit()))
   1009 
   1010     def has_key(self, key):
   1011         """Return True if the keyed message exists, False otherwise."""
   1012         return os.path.exists(os.path.join(self._path, str(key)))
   1013 
   1014     def __len__(self):
   1015         """Return a count of messages in the mailbox."""
   1016         return len(list(self.iterkeys()))
   1017 
   1018     def lock(self):
   1019         """Lock the mailbox."""
   1020         if not self._locked:
   1021             self._file = open(os.path.join(self._path, '.mh_sequences'), 'rb+')
   1022             _lock_file(self._file)
   1023             self._locked = True
   1024 
   1025     def unlock(self):
   1026         """Unlock the mailbox if it is locked."""
   1027         if self._locked:
   1028             _unlock_file(self._file)
   1029             _sync_close(self._file)
   1030             del self._file
   1031             self._locked = False
   1032 
   1033     def flush(self):
   1034         """Write any pending changes to the disk."""
   1035         return
   1036 
   1037     def close(self):
   1038         """Flush and close the mailbox."""
   1039         if self._locked:
   1040             self.unlock()
   1041 
   1042     def list_folders(self):
   1043         """Return a list of folder names."""
   1044         result = []
   1045         for entry in os.listdir(self._path):
   1046             if os.path.isdir(os.path.join(self._path, entry)):
   1047                 result.append(entry)
   1048         return result
   1049 
   1050     def get_folder(self, folder):
   1051         """Return an MH instance for the named folder."""
   1052         return MH(os.path.join(self._path, folder),
   1053                   factory=self._factory, create=False)
   1054 
   1055     def add_folder(self, folder):
   1056         """Create a folder and return an MH instance representing it."""
   1057         return MH(os.path.join(self._path, folder),
   1058                   factory=self._factory)
   1059 
   1060     def remove_folder(self, folder):
   1061         """Delete the named folder, which must be empty."""
   1062         path = os.path.join(self._path, folder)
   1063         entries = os.listdir(path)
   1064         if entries == ['.mh_sequences']:
   1065             os.remove(os.path.join(path, '.mh_sequences'))
   1066         elif entries == []:
   1067             pass
   1068         else:
   1069             raise NotEmptyError('Folder not empty: %s' % self._path)
   1070         os.rmdir(path)
   1071 
   1072     def get_sequences(self):
   1073         """Return a name-to-key-list dictionary to define each sequence."""
   1074         results = {}
   1075         f = open(os.path.join(self._path, '.mh_sequences'), 'r')
   1076         try:
   1077             all_keys = set(self.keys())
   1078             for line in f:
   1079                 try:
   1080                     name, contents = line.split(':')
   1081                     keys = set()
   1082                     for spec in contents.split():
   1083                         if spec.isdigit():
   1084                             keys.add(int(spec))
   1085                         else:
   1086                             start, stop = (int(x) for x in spec.split('-'))
   1087                             keys.update(range(start, stop + 1))
   1088                     results[name] = [key for key in sorted(keys) \
   1089                                          if key in all_keys]
   1090                     if len(results[name]) == 0:
   1091                         del results[name]
   1092                 except ValueError:
   1093                     raise FormatError('Invalid sequence specification: %s' %
   1094                                       line.rstrip())
   1095         finally:
   1096             f.close()
   1097         return results
   1098 
   1099     def set_sequences(self, sequences):
   1100         """Set sequences using the given name-to-key-list dictionary."""
   1101         f = open(os.path.join(self._path, '.mh_sequences'), 'r+')
   1102         try:
   1103             os.close(os.open(f.name, os.O_WRONLY | os.O_TRUNC))
   1104             for name, keys in sequences.iteritems():
   1105                 if len(keys) == 0:
   1106                     continue
   1107                 f.write('%s:' % name)
   1108                 prev = None
   1109                 completing = False
   1110                 for key in sorted(set(keys)):
   1111                     if key - 1 == prev:
   1112                         if not completing:
   1113                             completing = True
   1114                             f.write('-')
   1115                     elif completing:
   1116                         completing = False
   1117                         f.write('%s %s' % (prev, key))
   1118                     else:
   1119                         f.write(' %s' % key)
   1120                     prev = key
   1121                 if completing:
   1122                     f.write(str(prev) + '\n')
   1123                 else:
   1124                     f.write('\n')
   1125         finally:
   1126             _sync_close(f)
   1127 
   1128     def pack(self):
   1129         """Re-name messages to eliminate numbering gaps. Invalidates keys."""
   1130         sequences = self.get_sequences()
   1131         prev = 0
   1132         changes = []
   1133         for key in self.iterkeys():
   1134             if key - 1 != prev:
   1135                 changes.append((key, prev + 1))
   1136                 if hasattr(os, 'link'):
   1137                     os.link(os.path.join(self._path, str(key)),
   1138                             os.path.join(self._path, str(prev + 1)))
   1139                     os.unlink(os.path.join(self._path, str(key)))
   1140                 else:
   1141                     os.rename(os.path.join(self._path, str(key)),
   1142                               os.path.join(self._path, str(prev + 1)))
   1143             prev += 1
   1144         self._next_key = prev + 1
   1145         if len(changes) == 0:
   1146             return
   1147         for name, key_list in sequences.items():
   1148             for old, new in changes:
   1149                 if old in key_list:
   1150                     key_list[key_list.index(old)] = new
   1151         self.set_sequences(sequences)
   1152 
   1153     def _dump_sequences(self, message, key):
   1154         """Inspect a new MHMessage and update sequences appropriately."""
   1155         pending_sequences = message.get_sequences()
   1156         all_sequences = self.get_sequences()
   1157         for name, key_list in all_sequences.iteritems():
   1158             if name in pending_sequences:
   1159                 key_list.append(key)
   1160             elif key in key_list:
   1161                 del key_list[key_list.index(key)]
   1162         for sequence in pending_sequences:
   1163             if sequence not in all_sequences:
   1164                 all_sequences[sequence] = [key]
   1165         self.set_sequences(all_sequences)
   1166 
   1167 
   1168 class Babyl(_singlefileMailbox):
   1169     """An Rmail-style Babyl mailbox."""
   1170 
   1171     _special_labels = frozenset(('unseen', 'deleted', 'filed', 'answered',
   1172                                  'forwarded', 'edited', 'resent'))
   1173 
   1174     def __init__(self, path, factory=None, create=True):
   1175         """Initialize a Babyl mailbox."""
   1176         _singlefileMailbox.__init__(self, path, factory, create)
   1177         self._labels = {}
   1178 
   1179     def add(self, message):
   1180         """Add message and return assigned key."""
   1181         key = _singlefileMailbox.add(self, message)
   1182         if isinstance(message, BabylMessage):
   1183             self._labels[key] = message.get_labels()
   1184         return key
   1185 
   1186     def remove(self, key):
   1187         """Remove the keyed message; raise KeyError if it doesn't exist."""
   1188         _singlefileMailbox.remove(self, key)
   1189         if key in self._labels:
   1190             del self._labels[key]
   1191 
   1192     def __setitem__(self, key, message):
   1193         """Replace the keyed message; raise KeyError if it doesn't exist."""
   1194         _singlefileMailbox.__setitem__(self, key, message)
   1195         if isinstance(message, BabylMessage):
   1196             self._labels[key] = message.get_labels()
   1197 
   1198     def get_message(self, key):
   1199         """Return a Message representation or raise a KeyError."""
   1200         start, stop = self._lookup(key)
   1201         self._file.seek(start)
   1202         self._file.readline()   # Skip '1,' line specifying labels.

   1203         original_headers = StringIO.StringIO()
   1204         while True:
   1205             line = self._file.readline()
   1206             if line == '*** EOOH ***' + os.linesep or line == '':
   1207                 break
   1208             original_headers.write(line.replace(os.linesep, '\n'))
   1209         visible_headers = StringIO.StringIO()
   1210         while True:
   1211             line = self._file.readline()
   1212             if line == os.linesep or line == '':
   1213                 break
   1214             visible_headers.write(line.replace(os.linesep, '\n'))
   1215         body = self._file.read(stop - self._file.tell()).replace(os.linesep,
   1216                                                                  '\n')
   1217         msg = BabylMessage(original_headers.getvalue() + body)
   1218         msg.set_visible(visible_headers.getvalue())
   1219         if key in self._labels:
   1220             msg.set_labels(self._labels[key])
   1221         return msg
   1222 
   1223     def get_string(self, key):
   1224         """Return a string representation or raise a KeyError."""
   1225         start, stop = self._lookup(key)
   1226         self._file.seek(start)
   1227         self._file.readline()   # Skip '1,' line specifying labels.

   1228         original_headers = StringIO.StringIO()
   1229         while True:
   1230             line = self._file.readline()
   1231             if line == '*** EOOH ***' + os.linesep or line == '':
   1232                 break
   1233             original_headers.write(line.replace(os.linesep, '\n'))
   1234         while True:
   1235             line = self._file.readline()
   1236             if line == os.linesep or line == '':
   1237                 break
   1238         return original_headers.getvalue() + \
   1239                self._file.read(stop - self._file.tell()).replace(os.linesep,
   1240                                                                  '\n')
   1241 
   1242     def get_file(self, key):
   1243         """Return a file-like representation or raise a KeyError."""
   1244         return StringIO.StringIO(self.get_string(key).replace('\n',
   1245                                                               os.linesep))
   1246 
   1247     def get_labels(self):
   1248         """Return a list of user-defined labels in the mailbox."""
   1249         self._lookup()
   1250         labels = set()
   1251         for label_list in self._labels.values():
   1252             labels.update(label_list)
   1253         labels.difference_update(self._special_labels)
   1254         return list(labels)
   1255 
   1256     def _generate_toc(self):
   1257         """Generate key-to-(start, stop) table of contents."""
   1258         starts, stops = [], []
   1259         self._file.seek(0)
   1260         next_pos = 0
   1261         label_lists = []
   1262         while True:
   1263             line_pos = next_pos
   1264             line = self._file.readline()
   1265             next_pos = self._file.tell()
   1266             if line == '\037\014' + os.linesep:
   1267                 if len(stops) < len(starts):
   1268                     stops.append(line_pos - len(os.linesep))
   1269                 starts.append(next_pos)
   1270                 labels = [label.strip() for label
   1271                                         in self._file.readline()[1:].split(',')
   1272                                         if label.strip() != '']
   1273                 label_lists.append(labels)
   1274             elif line == '\037' or line == '\037' + os.linesep:
   1275                 if len(stops) < len(starts):
   1276                     stops.append(line_pos - len(os.linesep))
   1277             elif line == '':
   1278                 stops.append(line_pos - len(os.linesep))
   1279                 break
   1280         self._toc = dict(enumerate(zip(starts, stops)))
   1281         self._labels = dict(enumerate(label_lists))
   1282         self._next_key = len(self._toc)
   1283         self._file.seek(0, 2)
   1284         self._file_length = self._file.tell()
   1285 
   1286     def _pre_mailbox_hook(self, f):
   1287         """Called before writing the mailbox to file f."""
   1288         f.write('BABYL OPTIONS:%sVersion: 5%sLabels:%s%s\037' %
   1289                 (os.linesep, os.linesep, ','.join(self.get_labels()),
   1290                  os.linesep))
   1291 
   1292     def _pre_message_hook(self, f):
   1293         """Called before writing each message to file f."""
   1294         f.write('\014' + os.linesep)
   1295 
   1296     def _post_message_hook(self, f):
   1297         """Called after writing each message to file f."""
   1298         f.write(os.linesep + '\037')
   1299 
   1300     def _install_message(self, message):
   1301         """Write message contents and return (start, stop)."""
   1302         start = self._file.tell()
   1303         if isinstance(message, BabylMessage):
   1304             special_labels = []
   1305             labels = []
   1306             for label in message.get_labels():
   1307                 if label in self._special_labels:
   1308                     special_labels.append(label)
   1309                 else:
   1310                     labels.append(label)
   1311             self._file.write('1')
   1312             for label in special_labels:
   1313                 self._file.write(', ' + label)
   1314             self._file.write(',,')
   1315             for label in labels:
   1316                 self._file.write(' ' + label + ',')
   1317             self._file.write(os.linesep)
   1318         else:
   1319             self._file.write('1,,' + os.linesep)
   1320         if isinstance(message, email.message.Message):
   1321             orig_buffer = StringIO.StringIO()
   1322             orig_generator = email.generator.Generator(orig_buffer, False, 0)
   1323             orig_generator.flatten(message)
   1324             orig_buffer.seek(0)
   1325             while True:
   1326                 line = orig_buffer.readline()
   1327                 self._file.write(line.replace('\n', os.linesep))
   1328                 if line == '\n' or line == '':
   1329                     break
   1330             self._file.write('*** EOOH ***' + os.linesep)
   1331             if isinstance(message, BabylMessage):
   1332                 vis_buffer = StringIO.StringIO()
   1333                 vis_generator = email.generator.Generator(vis_buffer, False, 0)
   1334                 vis_generator.flatten(message.get_visible())
   1335                 while True:
   1336                     line = vis_buffer.readline()
   1337                     self._file.write(line.replace('\n', os.linesep))
   1338                     if line == '\n' or line == '':
   1339                         break
   1340             else:
   1341                 orig_buffer.seek(0)
   1342                 while True:
   1343                     line = orig_buffer.readline()
   1344                     self._file.write(line.replace('\n', os.linesep))
   1345                     if line == '\n' or line == '':
   1346                         break
   1347             while True:
   1348                 buffer = orig_buffer.read(4096) # Buffer size is arbitrary.

   1349                 if buffer == '':
   1350                     break
   1351                 self._file.write(buffer.replace('\n', os.linesep))
   1352         elif isinstance(message, str):
   1353             body_start = message.find('\n\n') + 2
   1354             if body_start - 2 != -1:
   1355                 self._file.write(message[:body_start].replace('\n',
   1356                                                               os.linesep))
   1357                 self._file.write('*** EOOH ***' + os.linesep)
   1358                 self._file.write(message[:body_start].replace('\n',
   1359                                                               os.linesep))
   1360                 self._file.write(message[body_start:].replace('\n',
   1361                                                               os.linesep))
   1362             else:
   1363                 self._file.write('*** EOOH ***' + os.linesep + os.linesep)
   1364                 self._file.write(message.replace('\n', os.linesep))
   1365         elif hasattr(message, 'readline'):
   1366             original_pos = message.tell()
   1367             first_pass = True
   1368             while True:
   1369                 line = message.readline()
   1370                 self._file.write(line.replace('\n', os.linesep))
   1371                 if line == '\n' or line == '':
   1372                     self._file.write('*** EOOH ***' + os.linesep)
   1373                     if first_pass:
   1374                         first_pass = False
   1375                         message.seek(original_pos)
   1376                     else:
   1377                         break
   1378             while True:
   1379                 buffer = message.read(4096)     # Buffer size is arbitrary.

   1380                 if buffer == '':
   1381                     break
   1382                 self._file.write(buffer.replace('\n', os.linesep))
   1383         else:
   1384             raise TypeError('Invalid message type: %s' % type(message))
   1385         stop = self._file.tell()
   1386         return (start, stop)
   1387 
   1388 
   1389 class Message(email.message.Message):
   1390     """Message with mailbox-format-specific properties."""
   1391 
   1392     def __init__(self, message=None):
   1393         """Initialize a Message instance."""
   1394         if isinstance(message, email.message.Message):
   1395             self._become_message(copy.deepcopy(message))
   1396             if isinstance(message, Message):
   1397                 message._explain_to(self)
   1398         elif isinstance(message, str):
   1399             self._become_message(email.message_from_string(message))
   1400         elif hasattr(message, "read"):
   1401             self._become_message(email.message_from_file(message))
   1402         elif message is None:
   1403             email.message.Message.__init__(self)
   1404         else:
   1405             raise TypeError('Invalid message type: %s' % type(message))
   1406 
   1407     def _become_message(self, message):
   1408         """Assume the non-format-specific state of message."""
   1409         for name in ('_headers', '_unixfrom', '_payload', '_charset',
   1410                      'preamble', 'epilogue', 'defects', '_default_type'):
   1411             self.__dict__[name] = message.__dict__[name]
   1412 
   1413     def _explain_to(self, message):
   1414         """Copy format-specific state to message insofar as possible."""
   1415         if isinstance(message, Message):
   1416             return  # There's nothing format-specific to explain.

   1417         else:
   1418             raise TypeError('Cannot convert to specified type')
   1419 
   1420 
   1421 class MaildirMessage(Message):
   1422     """Message with Maildir-specific properties."""
   1423 
   1424     def __init__(self, message=None):
   1425         """Initialize a MaildirMessage instance."""
   1426         self._subdir = 'new'
   1427         self._info = ''
   1428         self._date = time.time()
   1429         Message.__init__(self, message)
   1430 
   1431     def get_subdir(self):
   1432         """Return 'new' or 'cur'."""
   1433         return self._subdir
   1434 
   1435     def set_subdir(self, subdir):
   1436         """Set subdir to 'new' or 'cur'."""
   1437         if subdir == 'new' or subdir == 'cur':
   1438             self._subdir = subdir
   1439         else:
   1440             raise ValueError("subdir must be 'new' or 'cur': %s" % subdir)
   1441 
   1442     def get_flags(self):
   1443         """Return as a string the flags that are set."""
   1444         if self._info.startswith('2,'):
   1445             return self._info[2:]
   1446         else:
   1447             return ''
   1448 
   1449     def set_flags(self, flags):
   1450         """Set the given flags and unset all others."""
   1451         self._info = '2,' + ''.join(sorted(flags))
   1452 
   1453     def add_flag(self, flag):
   1454         """Set the given flag(s) without changing others."""
   1455         self.set_flags(''.join(set(self.get_flags()) | set(flag)))
   1456 
   1457     def remove_flag(self, flag):
   1458         """Unset the given string flag(s) without changing others."""
   1459         if self.get_flags() != '':
   1460             self.set_flags(''.join(set(self.get_flags()) - set(flag)))
   1461 
   1462     def get_date(self):
   1463         """Return delivery date of message, in seconds since the epoch."""
   1464         return self._date
   1465 
   1466     def set_date(self, date):
   1467         """Set delivery date of message, in seconds since the epoch."""
   1468         try:
   1469             self._date = float(date)
   1470         except ValueError:
   1471             raise TypeError("can't convert to float: %s" % date)
   1472 
   1473     def get_info(self):
   1474         """Get the message's "info" as a string."""
   1475         return self._info
   1476 
   1477     def set_info(self, info):
   1478         """Set the message's "info" string."""
   1479         if isinstance(info, str):
   1480             self._info = info
   1481         else:
   1482             raise TypeError('info must be a string: %s' % type(info))
   1483 
   1484     def _explain_to(self, message):
   1485         """Copy Maildir-specific state to message insofar as possible."""
   1486         if isinstance(message, MaildirMessage):
   1487             message.set_flags(self.get_flags())
   1488             message.set_subdir(self.get_subdir())
   1489             message.set_date(self.get_date())
   1490         elif isinstance(message, _mboxMMDFMessage):
   1491             flags = set(self.get_flags())
   1492             if 'S' in flags:
   1493                 message.add_flag('R')
   1494             if self.get_subdir() == 'cur':
   1495                 message.add_flag('O')
   1496             if 'T' in flags:
   1497                 message.add_flag('D')
   1498             if 'F' in flags:
   1499                 message.add_flag('F')
   1500             if 'R' in flags:
   1501                 message.add_flag('A')
   1502             message.set_from('MAILER-DAEMON', time.gmtime(self.get_date()))
   1503         elif isinstance(message, MHMessage):
   1504             flags = set(self.get_flags())
   1505             if 'S' not in flags:
   1506                 message.add_sequence('unseen')
   1507             if 'R' in flags:
   1508                 message.add_sequence('replied')
   1509             if 'F' in flags:
   1510                 message.add_sequence('flagged')
   1511         elif isinstance(message, BabylMessage):
   1512             flags = set(self.get_flags())
   1513             if 'S' not in flags:
   1514                 message.add_label('unseen')
   1515             if 'T' in flags:
   1516                 message.add_label('deleted')
   1517             if 'R' in flags:
   1518                 message.add_label('answered')
   1519             if 'P' in flags:
   1520                 message.add_label('forwarded')
   1521         elif isinstance(message, Message):
   1522             pass
   1523         else:
   1524             raise TypeError('Cannot convert to specified type: %s' %
   1525                             type(message))
   1526 
   1527 
   1528 class _mboxMMDFMessage(Message):
   1529     """Message with mbox- or MMDF-specific properties."""
   1530 
   1531     def __init__(self, message=None):
   1532         """Initialize an mboxMMDFMessage instance."""
   1533         self.set_from('MAILER-DAEMON', True)
   1534         if isinstance(message, email.message.Message):
   1535             unixfrom = message.get_unixfrom()
   1536             if unixfrom is not None and unixfrom.startswith('From '):
   1537                 self.set_from(unixfrom[5:])
   1538         Message.__init__(self, message)
   1539 
   1540     def get_from(self):
   1541         """Return contents of "From " line."""
   1542         return self._from
   1543 
   1544     def set_from(self, from_, time_=None):
   1545         """Set "From " line, formatting and appending time_ if specified."""
   1546         if time_ is not None:
   1547             if time_ is True:
   1548                 time_ = time.gmtime()
   1549             from_ += ' ' + time.asctime(time_)
   1550         self._from = from_
   1551 
   1552     def get_flags(self):
   1553         """Return as a string the flags that are set."""
   1554         return self.get('Status', '') + self.get('X-Status', '')
   1555 
   1556     def set_flags(self, flags):
   1557         """Set the given flags and unset all others."""
   1558         flags = set(flags)
   1559         status_flags, xstatus_flags = '', ''
   1560         for flag in ('R', 'O'):
   1561             if flag in flags:
   1562                 status_flags += flag
   1563                 flags.remove(flag)
   1564         for flag in ('D', 'F', 'A'):
   1565             if flag in flags:
   1566                 xstatus_flags += flag
   1567                 flags.remove(flag)
   1568         xstatus_flags += ''.join(sorted(flags))
   1569         try:
   1570             self.replace_header('Status', status_flags)
   1571         except KeyError:
   1572             self.add_header('Status', status_flags)
   1573         try:
   1574             self.replace_header('X-Status', xstatus_flags)
   1575         except KeyError:
   1576             self.add_header('X-Status', xstatus_flags)
   1577 
   1578     def add_flag(self, flag):
   1579         """Set the given flag(s) without changing others."""
   1580         self.set_flags(''.join(set(self.get_flags()) | set(flag)))
   1581 
   1582     def remove_flag(self, flag):
   1583         """Unset the given string flag(s) without changing others."""
   1584         if 'Status' in self or 'X-Status' in self:
   1585             self.set_flags(''.join(set(self.get_flags()) - set(flag)))
   1586 
   1587     def _explain_to(self, message):
   1588         """Copy mbox- or MMDF-specific state to message insofar as possible."""
   1589         if isinstance(message, MaildirMessage):
   1590             flags = set(self.get_flags())
   1591             if 'O' in flags:
   1592                 message.set_subdir('cur')
   1593             if 'F' in flags:
   1594                 message.add_flag('F')
   1595             if 'A' in flags:
   1596                 message.add_flag('R')
   1597             if 'R' in flags:
   1598                 message.add_flag('S')
   1599             if 'D' in flags:
   1600                 message.add_flag('T')
   1601             del message['status']
   1602             del message['x-status']
   1603             maybe_date = ' '.join(self.get_from().split()[-5:])
   1604             try:
   1605                 message.set_date(calendar.timegm(time.strptime(maybe_date,
   1606                                                       '%a %b %d %H:%M:%S %Y')))
   1607             except (ValueError, OverflowError):
   1608                 pass
   1609         elif isinstance(message, _mboxMMDFMessage):
   1610             message.set_flags(self.get_flags())
   1611             message.set_from(self.get_from())
   1612         elif isinstance(message, MHMessage):
   1613             flags = set(self.get_flags())
   1614             if 'R' not in flags:
   1615                 message.add_sequence('unseen')
   1616             if 'A' in flags:
   1617                 message.add_sequence('replied')
   1618             if 'F' in flags:
   1619                 message.add_sequence('flagged')
   1620             del message['status']
   1621             del message['x-status']
   1622         elif isinstance(message, BabylMessage):
   1623             flags = set(self.get_flags())
   1624             if 'R' not in flags:
   1625                 message.add_label('unseen')
   1626             if 'D' in flags:
   1627                 message.add_label('deleted')
   1628             if 'A' in flags:
   1629                 message.add_label('answered')
   1630             del message['status']
   1631             del message['x-status']
   1632         elif isinstance(message, Message):
   1633             pass
   1634         else:
   1635             raise TypeError('Cannot convert to specified type: %s' %
   1636                             type(message))
   1637 
   1638 
   1639 class mboxMessage(_mboxMMDFMessage):
   1640     """Message with mbox-specific properties."""
   1641 
   1642 
   1643 class MHMessage(Message):
   1644     """Message with MH-specific properties."""
   1645 
   1646     def __init__(self, message=None):
   1647         """Initialize an MHMessage instance."""
   1648         self._sequences = []
   1649         Message.__init__(self, message)
   1650 
   1651     def get_sequences(self):
   1652         """Return a list of sequences that include the message."""
   1653         return self._sequences[:]
   1654 
   1655     def set_sequences(self, sequences):
   1656         """Set the list of sequences that include the message."""
   1657         self._sequences = list(sequences)
   1658 
   1659     def add_sequence(self, sequence):
   1660         """Add sequence to list of sequences including the message."""
   1661         if isinstance(sequence, str):
   1662             if not sequence in self._sequences:
   1663                 self._sequences.append(sequence)
   1664         else:
   1665             raise TypeError('sequence must be a string: %s' % type(sequence))
   1666 
   1667     def remove_sequence(self, sequence):
   1668         """Remove sequence from the list of sequences including the message."""
   1669         try:
   1670             self._sequences.remove(sequence)
   1671         except ValueError:
   1672             pass
   1673 
   1674     def _explain_to(self, message):
   1675         """Copy MH-specific state to message insofar as possible."""
   1676         if isinstance(message, MaildirMessage):
   1677             sequences = set(self.get_sequences())
   1678             if 'unseen' in sequences:
   1679                 message.set_subdir('cur')
   1680             else:
   1681                 message.set_subdir('cur')
   1682                 message.add_flag('S')
   1683             if 'flagged' in sequences:
   1684                 message.add_flag('F')
   1685             if 'replied' in sequences:
   1686                 message.add_flag('R')
   1687         elif isinstance(message, _mboxMMDFMessage):
   1688             sequences = set(self.get_sequences())
   1689             if 'unseen' not in sequences:
   1690                 message.add_flag('RO')
   1691             else:
   1692                 message.add_flag('O')
   1693             if 'flagged' in sequences:
   1694                 message.add_flag('F')
   1695             if 'replied' in sequences:
   1696                 message.add_flag('A')
   1697         elif isinstance(message, MHMessage):
   1698             for sequence in self.get_sequences():
   1699                 message.add_sequence(sequence)
   1700         elif isinstance(message, BabylMessage):
   1701             sequences = set(self.get_sequences())
   1702             if 'unseen' in sequences:
   1703                 message.add_label('unseen')
   1704             if 'replied' in sequences:
   1705                 message.add_label('answered')
   1706         elif isinstance(message, Message):
   1707             pass
   1708         else:
   1709             raise TypeError('Cannot convert to specified type: %s' %
   1710                             type(message))
   1711 
   1712 
   1713 class BabylMessage(Message):
   1714     """Message with Babyl-specific properties."""
   1715 
   1716     def __init__(self, message=None):
   1717         """Initialize an BabylMessage instance."""
   1718         self._labels = []
   1719         self._visible = Message()
   1720         Message.__init__(self, message)
   1721 
   1722     def get_labels(self):
   1723         """Return a list of labels on the message."""
   1724         return self._labels[:]
   1725 
   1726     def set_labels(self, labels):
   1727         """Set the list of labels on the message."""
   1728         self._labels = list(labels)
   1729 
   1730     def add_label(self, label):
   1731         """Add label to list of labels on the message."""
   1732         if isinstance(label, str):
   1733             if label not in self._labels:
   1734                 self._labels.append(label)
   1735         else:
   1736             raise TypeError('label must be a string: %s' % type(label))
   1737 
   1738     def remove_label(self, label):
   1739         """Remove label from the list of labels on the message."""
   1740         try:
   1741             self._labels.remove(label)
   1742         except ValueError:
   1743             pass
   1744 
   1745     def get_visible(self):
   1746         """Return a Message representation of visible headers."""
   1747         return Message(self._visible)
   1748 
   1749     def set_visible(self, visible):
   1750         """Set the Message representation of visible headers."""
   1751         self._visible = Message(visible)
   1752 
   1753     def update_visible(self):
   1754         """Update and/or sensibly generate a set of visible headers."""
   1755         for header in self._visible.keys():
   1756             if header in self:
   1757                 self._visible.replace_header(header, self[header])
   1758             else:
   1759                 del self._visible[header]
   1760         for header in ('Date', 'From', 'Reply-To', 'To', 'CC', 'Subject'):
   1761             if header in self and header not in self._visible:
   1762                 self._visible[header] = self[header]
   1763 
   1764     def _explain_to(self, message):
   1765         """Copy Babyl-specific state to message insofar as possible."""
   1766         if isinstance(message, MaildirMessage):
   1767             labels = set(self.get_labels())
   1768             if 'unseen' in labels:
   1769                 message.set_subdir('cur')
   1770             else:
   1771                 message.set_subdir('cur')
   1772                 message.add_flag('S')
   1773             if 'forwarded' in labels or 'resent' in labels:
   1774                 message.add_flag('P')
   1775             if 'answered' in labels:
   1776                 message.add_flag('R')
   1777             if 'deleted' in labels:
   1778                 message.add_flag('T')
   1779         elif isinstance(message, _mboxMMDFMessage):
   1780             labels = set(self.get_labels())
   1781             if 'unseen' not in labels:
   1782                 message.add_flag('RO')
   1783             else:
   1784                 message.add_flag('O')
   1785             if 'deleted' in labels:
   1786                 message.add_flag('D')
   1787             if 'answered' in labels:
   1788                 message.add_flag('A')
   1789         elif isinstance(message, MHMessage):
   1790             labels = set(self.get_labels())
   1791             if 'unseen' in labels:
   1792                 message.add_sequence('unseen')
   1793             if 'answered' in labels:
   1794                 message.add_sequence('replied')
   1795         elif isinstance(message, BabylMessage):
   1796             message.set_visible(self.get_visible())
   1797             for label in self.get_labels():
   1798                 message.add_label(label)
   1799         elif isinstance(message, Message):
   1800             pass
   1801         else:
   1802             raise TypeError('Cannot convert to specified type: %s' %
   1803                             type(message))
   1804 
   1805 
   1806 class MMDFMessage(_mboxMMDFMessage):
   1807     """Message with MMDF-specific properties."""
   1808 
   1809 
   1810 class _ProxyFile:
   1811     """A read-only wrapper of a file."""
   1812 
   1813     def __init__(self, f, pos=None):
   1814         """Initialize a _ProxyFile."""
   1815         self._file = f
   1816         if pos is None:
   1817             self._pos = f.tell()
   1818         else:
   1819             self._pos = pos
   1820 
   1821     def read(self, size=None):
   1822         """Read bytes."""
   1823         return self._read(size, self._file.read)
   1824 
   1825     def readline(self, size=None):
   1826         """Read a line."""
   1827         return self._read(size, self._file.readline)
   1828 
   1829     def readlines(self, sizehint=None):
   1830         """Read multiple lines."""
   1831         result = []
   1832         for line in self:
   1833             result.append(line)
   1834             if sizehint is not None:
   1835                 sizehint -= len(line)
   1836                 if sizehint <= 0:
   1837                     break
   1838         return result
   1839 
   1840     def __iter__(self):
   1841         """Iterate over lines."""
   1842         return iter(self.readline, "")
   1843 
   1844     def tell(self):
   1845         """Return the position."""
   1846         return self._pos
   1847 
   1848     def seek(self, offset, whence=0):
   1849         """Change position."""
   1850         if whence == 1:
   1851             self._file.seek(self._pos)
   1852         self._file.seek(offset, whence)
   1853         self._pos = self._file.tell()
   1854 
   1855     def close(self):
   1856         """Close the file."""
   1857         del self._file
   1858 
   1859     def _read(self, size, read_method):
   1860         """Read size bytes using read_method."""
   1861         if size is None:
   1862             size = -1
   1863         self._file.seek(self._pos)
   1864         result = read_method(size)
   1865         self._pos = self._file.tell()
   1866         return result
   1867 
   1868 
   1869 class _PartialFile(_ProxyFile):
   1870     """A read-only wrapper of part of a file."""
   1871 
   1872     def __init__(self, f, start=None, stop=None):
   1873         """Initialize a _PartialFile."""
   1874         _ProxyFile.__init__(self, f, start)
   1875         self._start = start
   1876         self._stop = stop
   1877 
   1878     def tell(self):
   1879         """Return the position with respect to start."""
   1880         return _ProxyFile.tell(self) - self._start
   1881 
   1882     def seek(self, offset, whence=0):
   1883         """Change position, possibly with respect to start or stop."""
   1884         if whence == 0:
   1885             self._pos = self._start
   1886             whence = 1
   1887         elif whence == 2:
   1888             self._pos = self._stop
   1889             whence = 1
   1890         _ProxyFile.seek(self, offset, whence)
   1891 
   1892     def _read(self, size, read_method):
   1893         """Read size bytes using read_method, honoring start and stop."""
   1894         remaining = self._stop - self._pos
   1895         if remaining <= 0:
   1896             return ''
   1897         if size is None or size < 0 or size > remaining:
   1898             size = remaining
   1899         return _ProxyFile._read(self, size, read_method)
   1900 
   1901 
   1902 def _lock_file(f, dotlock=True):
   1903     """Lock file f using lockf and dot locking."""
   1904     dotlock_done = False
   1905     try:
   1906         if fcntl:
   1907             try:
   1908                 fcntl.lockf(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
   1909             except IOError, e:
   1910                 if e.errno in (errno.EAGAIN, errno.EACCES, errno.EROFS):
   1911                     raise ExternalClashError('lockf: lock unavailable: %s' %
   1912                                              f.name)
   1913                 else:
   1914                     raise
   1915         if dotlock:
   1916             try:
   1917                 pre_lock = _create_temporary(f.name + '.lock')
   1918                 pre_lock.close()
   1919             except IOError, e:
   1920                 if e.errno in (errno.EACCES, errno.EROFS):
   1921                     return  # Without write access, just skip dotlocking.

   1922                 else:
   1923                     raise
   1924             try:
   1925                 if hasattr(os, 'link'):
   1926                     os.link(pre_lock.name, f.name + '.lock')
   1927                     dotlock_done = True
   1928                     os.unlink(pre_lock.name)
   1929                 else:
   1930                     os.rename(pre_lock.name, f.name + '.lock')
   1931                     dotlock_done = True
   1932             except OSError, e:
   1933                 if e.errno == errno.EEXIST or \
   1934                   (os.name == 'os2' and e.errno == errno.EACCES):
   1935                     os.remove(pre_lock.name)
   1936                     raise ExternalClashError('dot lock unavailable: %s' %
   1937                                              f.name)
   1938                 else:
   1939                     raise
   1940     except:
   1941         if fcntl:
   1942             fcntl.lockf(f, fcntl.LOCK_UN)
   1943         if dotlock_done:
   1944             os.remove(f.name + '.lock')
   1945         raise
   1946 
   1947 def _unlock_file(f):
   1948     """Unlock file f using lockf and dot locking."""
   1949     if fcntl:
   1950         fcntl.lockf(f, fcntl.LOCK_UN)
   1951     if os.path.exists(f.name + '.lock'):
   1952         os.remove(f.name + '.lock')
   1953 
   1954 def _create_carefully(path):
   1955     """Create a file if it doesn't exist and open for reading and writing."""
   1956     fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDWR, 0666)
   1957     try:
   1958         return open(path, 'rb+')
   1959     finally:
   1960         os.close(fd)
   1961 
   1962 def _create_temporary(path):
   1963     """Create a temp file based on path and open for reading and writing."""
   1964     return _create_carefully('%s.%s.%s.%s' % (path, int(time.time()),
   1965                                               socket.gethostname(),
   1966                                               os.getpid()))
   1967 
   1968 def _sync_flush(f):
   1969     """Ensure changes to file f are physically on disk."""
   1970     f.flush()
   1971     if hasattr(os, 'fsync'):
   1972         os.fsync(f.fileno())
   1973 
   1974 def _sync_close(f):
   1975     """Close file f, ensuring all changes are physically on disk."""
   1976     _sync_flush(f)
   1977     f.close()
   1978 
   1979 ## Start: classes from the original module (for backward compatibility).

   1980 
   1981 # Note that the Maildir class, whose name is unchanged, itself offers a next()

   1982 # method for backward compatibility.

   1983 
   1984 class _Mailbox:
   1985 
   1986     def __init__(self, fp, factory=rfc822.Message):
   1987         self.fp = fp
   1988         self.seekp = 0
   1989         self.factory = factory
   1990 
   1991     def __iter__(self):
   1992         return iter(self.next, None)
   1993 
   1994     def next(self):
   1995         while 1:
   1996             self.fp.seek(self.seekp)
   1997             try:
   1998                 self._search_start()
   1999             except EOFError:
   2000                 self.seekp = self.fp.tell()
   2001                 return None
   2002             start = self.fp.tell()
   2003             self._search_end()
   2004             self.seekp = stop = self.fp.tell()
   2005             if start != stop:
   2006                 break
   2007         return self.factory(_PartialFile(self.fp, start, stop))
   2008 
   2009 # Recommended to use PortableUnixMailbox instead!

   2010 class UnixMailbox(_Mailbox):
   2011 
   2012     def _search_start(self):
   2013         while 1:
   2014             pos = self.fp.tell()
   2015             line = self.fp.readline()
   2016             if not line:
   2017                 raise EOFError
   2018             if line[:5] == 'From ' and self._isrealfromline(line):
   2019                 self.fp.seek(pos)
   2020                 return
   2021 
   2022     def _search_end(self):
   2023         self.fp.readline()      # Throw away header line

   2024         while 1:
   2025             pos = self.fp.tell()
   2026             line = self.fp.readline()
   2027             if not line:
   2028                 return
   2029             if line[:5] == 'From ' and self._isrealfromline(line):
   2030                 self.fp.seek(pos)
   2031                 return
   2032 
   2033     # An overridable mechanism to test for From-line-ness.  You can either

   2034     # specify a different regular expression or define a whole new

   2035     # _isrealfromline() method.  Note that this only gets called for lines

   2036     # starting with the 5 characters "From ".

   2037     #

   2038     # BAW: According to

   2039     #http://home.netscape.com/eng/mozilla/2.0/relnotes/demo/content-length.html

   2040     # the only portable, reliable way to find message delimiters in a BSD (i.e

   2041     # Unix mailbox) style folder is to search for "\n\nFrom .*\n", or at the

   2042     # beginning of the file, "^From .*\n".  While _fromlinepattern below seems

   2043     # like a good idea, in practice, there are too many variations for more

   2044     # strict parsing of the line to be completely accurate.

   2045     #

   2046     # _strict_isrealfromline() is the old version which tries to do stricter

   2047     # parsing of the From_ line.  _portable_isrealfromline() simply returns

   2048     # true, since it's never called if the line doesn't already start with

   2049     # "From ".

   2050     #

   2051     # This algorithm, and the way it interacts with _search_start() and

   2052     # _search_end() may not be completely correct, because it doesn't check

   2053     # that the two characters preceding "From " are \n\n or the beginning of

   2054     # the file.  Fixing this would require a more extensive rewrite than is

   2055     # necessary.  For convenience, we've added a PortableUnixMailbox class

   2056     # which does no checking of the format of the 'From' line.

   2057 
   2058     _fromlinepattern = (r"From \s*[^\s]+\s+\w\w\w\s+\w\w\w\s+\d?\d\s+"
   2059                         r"\d?\d:\d\d(:\d\d)?(\s+[^\s]+)?\s+\d\d\d\d\s*"
   2060                         r"[^\s]*\s*"
   2061                         "$")
   2062     _regexp = None
   2063 
   2064     def _strict_isrealfromline(self, line):
   2065         if not self._regexp:
   2066             import re
   2067             self._regexp = re.compile(self._fromlinepattern)
   2068         return self._regexp.match(line)
   2069 
   2070     def _portable_isrealfromline(self, line):
   2071         return True
   2072 
   2073     _isrealfromline = _strict_isrealfromline
   2074 
   2075 
   2076 class PortableUnixMailbox(UnixMailbox):
   2077     _isrealfromline = UnixMailbox._portable_isrealfromline
   2078 
   2079 
   2080 class MmdfMailbox(_Mailbox):
   2081 
   2082     def _search_start(self):
   2083         while 1:
   2084             line = self.fp.readline()
   2085             if not line:
   2086                 raise EOFError
   2087             if line[:5] == '\001\001\001\001\n':
   2088                 return
   2089 
   2090     def _search_end(self):
   2091         while 1:
   2092             pos = self.fp.tell()
   2093             line = self.fp.readline()
   2094             if not line:
   2095                 return
   2096             if line == '\001\001\001\001\n':
   2097                 self.fp.seek(pos)
   2098                 return
   2099 
   2100 
   2101 class MHMailbox:
   2102 
   2103     def __init__(self, dirname, factory=rfc822.Message):
   2104         import re
   2105         pat = re.compile('^[1-9][0-9]*$')
   2106         self.dirname = dirname
   2107         # the three following lines could be combined into:

   2108         # list = map(long, filter(pat.match, os.listdir(self.dirname)))

   2109         list = os.listdir(self.dirname)
   2110         list = filter(pat.match, list)
   2111         list = map(long, list)
   2112         list.sort()
   2113         # This only works in Python 1.6 or later;

   2114         # before that str() added 'L':

   2115         self.boxes = map(str, list)
   2116         self.boxes.reverse()
   2117         self.factory = factory
   2118 
   2119     def __iter__(self):
   2120         return iter(self.next, None)
   2121 
   2122     def next(self):
   2123         if not self.boxes:
   2124             return None
   2125         fn = self.boxes.pop()
   2126         fp = open(os.path.join(self.dirname, fn))
   2127         msg = self.factory(fp)
   2128         try:
   2129             msg._mh_msgno = fn
   2130         except (AttributeError, TypeError):
   2131             pass
   2132         return msg
   2133 
   2134 
   2135 class BabylMailbox(_Mailbox):
   2136 
   2137     def _search_start(self):
   2138         while 1:
   2139             line = self.fp.readline()
   2140             if not line:
   2141                 raise EOFError
   2142             if line == '*** EOOH ***\n':
   2143                 return
   2144 
   2145     def _search_end(self):
   2146         while 1:
   2147             pos = self.fp.tell()
   2148             line = self.fp.readline()
   2149             if not line:
   2150                 return
   2151             if line == '\037\014\n' or line == '\037':
   2152                 self.fp.seek(pos)
   2153                 return
   2154 
   2155 ## End: classes from the original module (for backward compatibility).

   2156 
   2157 
   2158 class Error(Exception):
   2159     """Raised for module-specific errors."""
   2160 
   2161 class NoSuchMailboxError(Error):
   2162     """The specified mailbox does not exist and won't be created."""
   2163 
   2164 class NotEmptyError(Error):
   2165     """The specified mailbox is not empty and deletion was requested."""
   2166 
   2167 class ExternalClashError(Error):
   2168     """Another process caused an action to fail."""
   2169 
   2170 class FormatError(Error):
   2171     """A file appears to have an invalid format."""
   2172