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