Commit 1da4a947 authored by Andrew M. Kuchling's avatar Andrew M. Kuchling

Add Gregory K. Johnson's revised version of mailbox.py (funded by

the 2005 Summer of Code).

The revision adds a number of new mailbox classes that support adding
and removing messages; these classes also support mailbox locking and
default to using email.Message instead of rfc822.Message.

The old mailbox classes are largely left alone for backward compatibility.
The exception is the Maildir class, which was present in the old module
and now inherits from the new classes.  The Maildir class's interface
is pretty simple, though, so I think it'll be compatible with existing
code.

(The change to the NEWS file also adds a missing word to a different
news item, which unfortunately required rewrapping the line.)
parent 81efcf68
\section{\module{mailbox} ---
Read various mailbox formats}
Manipulate mailboxes in various formats}
\declaremodule{standard}{mailbox}
\modulesynopsis{Read various mailbox formats.}
\declaremodule{}{mailbox}
\moduleauthor{Gregory K.~Johnson}{gkj@gregorykjohnson.com}
\sectionauthor{Gregory K.~Johnson}{gkj@gregorykjohnson.com}
\modulesynopsis{Manipulate mailboxes in various formats}
This module defines a number of classes that allow easy and uniform
access to mail messages in a (\UNIX) mailbox.
This module defines two classes, \class{Mailbox} and \class{Message}, for
accessing and manipulating on-disk mailboxes and the messages they contain.
\class{Mailbox} offers a dictionary-like mapping from keys to messages.
\class{Message} extends the \module{email.Message} module's \class{Message}
class with format-specific state and behavior. Supported mailbox formats are
Maildir, mbox, MH, Babyl, and MMDF.
\begin{seealso}
\seemodule{email}{Represent and manipulate messages.}
\end{seealso}
\subsection{\class{Mailbox} objects}
\label{mailbox-objects}
\begin{classdesc*}{Mailbox}
A mailbox, which may be inspected and modified.
\end{classdesc*}
The \class{Mailbox} interface is dictionary-like, with small keys
corresponding to messages. Keys are issued by the \class{Mailbox} instance
with which they will be used and are only meaningful to that \class{Mailbox}
instance. A key continues to identify a message even if the corresponding
message is modified, such as by replacing it with another message. Messages may
be added to a \class{Mailbox} instance using the set-like method
\method{add()} and removed using a \code{del} statement or the set-like methods
\method{remove()} and \method{discard()}.
\class{Mailbox} interface semantics differ from dictionary semantics in some
noteworthy ways. Each time a message is requested, a new representation
(typically a \class{Message} instance) is generated, based upon the current
state of the mailbox. Similarly, when a message is added to a \class{Mailbox}
instance, the provided message representation's contents are copied. In neither
case is a reference to the message representation kept by the \class{Mailbox}
instance.
The default \class{Mailbox} iterator iterates over message representations, not
keys as the default dictionary iterator does. Moreover, modification of a
mailbox during iteration is safe and well-defined. Messages added to the
mailbox after an iterator is created will not be seen by the iterator. Messages
removed from the mailbox before the iterator yields them will be silently
skipped, though using a key from an iterator may result in a
\exception{KeyError} exception if the corresponding message is subsequently
removed.
\class{Mailbox} itself is intended to define an interface and to be inherited
from by format-specific subclasses but is not intended to be instantiated.
Instead, you should instantiate a subclass.
\class{Mailbox} instances have the following methods:
\begin{methoddesc}{add}{message}
Add \var{message} to the mailbox and return the key that has been assigned to
it.
Parameter \var{message} may be a \class{Message} instance, an
\class{email.Message.Message} instance, a string, or a file-like object (which
should be open in text mode). If \var{message} is an instance of the
appropriate format-specific \class{Message} subclass (e.g., if it's an
\class{mboxMessage} instance and this is an \class{mbox} instance), its
format-specific information is used. Otherwise, reasonable defaults for
format-specific information are used.
\end{methoddesc}
\begin{methoddesc}{remove}{key}
\methodline{__delitem__}{key}
\methodline{discard}{key}
Delete the message corresponding to \var{key} from the mailbox.
If no such message exists, a \exception{KeyError} exception is raised if the
method was called as \method{remove()} or \method{__delitem__()} but no
exception is raised if the method was called as \method{discard()}. The
behavior of \method{discard()} may be preferred if the underlying mailbox
format supports concurrent modification by other processes.
\end{methoddesc}
\begin{methoddesc}{__setitem__}{key, message}
Replace the message corresponding to \var{key} with \var{message}. Raise a
\exception{KeyError} exception if no message already corresponds to \var{key}.
As with \method{add()}, parameter \var{message} may be a \class{Message}
instance, an \class{email.Message.Message} instance, a string, or a file-like
object (which should be open in text mode). If \var{message} is an instance of
the appropriate format-specific \class{Message} subclass (e.g., if it's an
\class{mboxMessage} instance and this is an \class{mbox} instance), its
format-specific information is used. Otherwise, the format-specific information
of the message that currently corresponds to \var{key} is left unchanged.
\end{methoddesc}
\begin{methoddesc}{iterkeys}{}
\methodline{keys}{}
Return an iterator over all keys if called as \method{iterkeys()} or return a
list of keys if called as \method{keys()}.
\end{methoddesc}
\begin{methoddesc}{itervalues}{}
\methodline{__iter__}{}
\methodline{values}{}
Return an iterator over representations of all messages if called as
\method{itervalues()} or \method{__iter__()} or return a list of such
representations if called as \method{values()}. The messages are represented as
instances of the appropriate format-specific \class{Message} subclass unless a
custom message factory was specified when the \class{Mailbox} instance was
initialized. \note{The behavior of \method{__iter__()} is unlike that of
dictionaries, which iterate over keys.}
\end{methoddesc}
\begin{methoddesc}{iteritems}{}
\methodline{items}{}
Return an iterator over (\var{key}, \var{message}) pairs, where \var{key} is a
key and \var{message} is a message representation, if called as
\method{iteritems()} or return a list of such pairs if called as
\method{items()}. The messages are represented as instances of the appropriate
format-specific \class{Message} subclass unless a custom message factory was
specified when the \class{Mailbox} instance was initialized.
\end{methoddesc}
\begin{methoddesc}{get}{key\optional{, default=None}}
\methodline{__getitem__}{key}
Return a representation of the message corresponding to \var{key}. If no such
message exists, \var{default} is returned if the method was called as
\method{get()} and a \exception{KeyError} exception is raised if the method was
called as \method{__getitem__()}. The message is represented as an instance of
the appropriate format-specific \class{Message} subclass unless a custom
message factory was specified when the \class{Mailbox} instance was
initialized.
\end{methoddesc}
\begin{methoddesc}{get_message}{key}
Return a representation of the message corresponding to \var{key} as an
instance of the appropriate format-specific \class{Message} subclass, or raise
a \exception{KeyError} exception if no such message exists.
\end{methoddesc}
\begin{methoddesc}{get_string}{key}
Return a string representation of the message corresponding to \var{key}, or
raise a \exception{KeyError} exception if no such message exists.
\end{methoddesc}
\begin{methoddesc}{get_file}{key}
Return a file-like representation of the message corresponding to \var{key},
or raise a \exception{KeyError} exception if no such message exists. The
file-like object behaves as if open in binary mode. This file should be closed
once it is no longer needed.
\note{Unlike other representations of messages, file-like representations are
not necessarily independent of the \class{Mailbox} instance that created them
or of the underlying mailbox. More specific documentation is provided by each
subclass.}
\end{methoddesc}
\begin{methoddesc}{has_key}{key}
\methodline{__contains__}{key}
Return \code{True} if \var{key} corresponds to a message, \code{False}
otherwise.
\end{methoddesc}
\begin{methoddesc}{__len__}{}
Return a count of messages in the mailbox.
\end{methoddesc}
\begin{methoddesc}{clear}{}
Delete all messages from the mailbox.
\end{methoddesc}
\begin{methoddesc}{pop}{key\optional{, default}}
Return a representation of the message corresponding to \var{key} and delete
the message. If no such message exists, return \var{default} if it was supplied
or else raise a \exception{KeyError} exception. The message is represented as
an instance of the appropriate format-specific \class{Message} subclass unless
a custom message factory was specified when the \class{Mailbox} instance was
initialized.
\end{methoddesc}
\begin{methoddesc}{popitem}{}
Return an arbitrary (\var{key}, \var{message}) pair, where \var{key} is a key
and \var{message} is a message representation, and delete the corresponding
message. If the mailbox is empty, raise a \exception{KeyError} exception. The
message is represented as an instance of the appropriate format-specific
\class{Message} subclass unless a custom message factory was specified when the
\class{Mailbox} instance was initialized.
\end{methoddesc}
\begin{methoddesc}{update}{arg}
Parameter \var{arg} should be a \var{key}-to-\var{message} mapping or an
iterable of (\var{key}, \var{message}) pairs. Updates the mailbox so that, for
each given \var{key} and \var{message}, the message corresponding to \var{key}
is set to \var{message} as if by using \method{__setitem__()}. As with
\method{__setitem__()}, each \var{key} must already correspond to a message in
the mailbox or else a \exception{KeyError} exception will be raised, so in
general it is incorrect for \var{arg} to be a \class{Mailbox} instance.
\note{Unlike with dictionaries, keyword arguments are not supported.}
\end{methoddesc}
\begin{methoddesc}{flush}{}
Write any pending changes to the filesystem. For some \class{Mailbox}
subclasses, changes are always written immediately and this method does
nothing.
\end{methoddesc}
\begin{methoddesc}{lock}{}
Acquire an exclusive advisory lock on the mailbox so that other processes know
not to modify it. An \exception{ExternalClashError} is raised if the lock is
not available. The particular locking mechanisms used depend upon the mailbox
format.
\end{methoddesc}
\begin{methoddesc}{unlock}{}
Release the lock on the mailbox, if any.
\end{methoddesc}
\begin{methoddesc}{close}{}
Flush the mailbox, unlock it if necessary, and close any open files. For some
\class{Mailbox} subclasses, this method does nothing.
\end{methoddesc}
\subsubsection{\class{Maildir}}
\label{mailbox-maildir}
\begin{classdesc}{Maildir}{dirname\optional{, factory=rfc822.Message\optional{,
create=True}}}
A subclass of \class{Mailbox} for mailboxes in Maildir format. Parameter
\var{factory} is a callable object that accepts a file-like message
representation (which behaves as if open in binary mode) and returns a custom
representation. If \var{factory} is \code{None}, \class{MaildirMessage} is used
as the default message representation. If \var{create} is \code{True}, the
mailbox is created if it does not exist.
It is for historical reasons that \var{factory} defaults to
\class{rfc822.Message} and that \var{dirname} is named as such rather than
\var{path}. For a \class{Maildir} instance that behaves like instances of other
\class{Mailbox} subclasses, set \var{factory} to \code{None}.
\end{classdesc}
Maildir is a directory-based mailbox format invented for the qmail mail
transfer agent and now widely supported by other programs. Messages in a
Maildir mailbox are stored in separate files within a common directory
structure. This design allows Maildir mailboxes to be accessed and modified by
multiple unrelated programs without data corruption, so file locking is
unnecessary.
Maildir mailboxes contain three subdirectories, namely: \file{tmp}, \file{new},
and \file{cur}. Messages are created momentarily in the \file{tmp} subdirectory
and then moved to the \file{new} subdirectory to finalize delivery. A mail user
agent may subsequently move the message to the \file{cur} subdirectory and
store information about the state of the message in a special "info" section
appended to its file name.
Folders of the style introduced by the Courier mail transfer agent are also
supported. Any subdirectory of the main mailbox is considered a folder if
\character{.} is the first character in its name. Folder names are represented
by \class{Maildir} without the leading \character{.}. Each folder is itself a
Maildir mailbox but should not contain other folders. Instead, a logical
nesting is indicated using \character{.} to delimit levels, e.g.,
"Archived.2005.07".
\begin{notice}
The Maildir specification requires the use of a colon (\character{:}) in
certain message file names. However, some operating systems do not permit this
character in file names, If you wish to use a Maildir-like format on such an
operating system, you should specify another character to use instead. The
exclamation point (\character{!}) is a popular choice. For example:
\begin{verbatim}
import mailbox
mailbox.Maildir.colon = '!'
\end{verbatim}
The \member{colon} attribute may also be set on a per-instance basis.
\end{notice}
\class{Maildir} instances have all of the methods of \class{Mailbox} in
addition to the following:
\begin{methoddesc}{list_folders}{}
Return a list of the names of all folders.
\end{methoddesc}
\begin{methoddesc}{get_folder}{folder}
Return a \class{Maildir} instance representing the folder whose name is
\var{folder}. A \exception{NoSuchMailboxError} exception is raised if the
folder does not exist.
\end{methoddesc}
\begin{methoddesc}{add_folder}{folder}
Create a folder whose name is \var{folder} and return a \class{Maildir}
instance representing it.
\end{methoddesc}
\begin{methoddesc}{remove_folder}{folder}
Delete the folder whose name is \var{folder}. If the folder contains any
messages, a \exception{NotEmptyError} exception will be raised and the folder
will not be deleted.
\end{methoddesc}
\begin{methoddesc}{clean}{}
Delete temporary files from the mailbox that have not been accessed in the
last 36 hours. The Maildir specification says that mail-reading programs
should do this occasionally.
\end{methoddesc}
Some \class{Mailbox} methods implemented by \class{Maildir} deserve special
remarks:
\begin{methoddesc}{add}{message}
\methodline[Maildir]{__setitem__}{key, message}
\methodline[Maildir]{update}{arg}
\warning{These methods generate unique file names based upon the current
process ID. When using multiple threads, undetected name clashes may occur and
cause corruption of the mailbox unless threads are coordinated to avoid using
these methods to manipulate the same mailbox simultaneously.}
\end{methoddesc}
\begin{methoddesc}{flush}{}
All changes to Maildir mailboxes are immediately applied, so this method does
nothing.
\end{methoddesc}
\begin{methoddesc}{lock}{}
\methodline{unlock}{}
Maildir mailboxes do not support (or require) locking, so these methods do
nothing.
\end{methoddesc}
\begin{methoddesc}{close}{}
\class{Maildir} instances do not keep any open files and the underlying
mailboxes do not support locking, so this method does nothing.
\end{methoddesc}
\begin{methoddesc}{get_file}{key}
Depending upon the host platform, it may not be possible to modify or remove
the underlying message while the returned file remains open.
\end{methoddesc}
\begin{seealso}
\seelink{http://www.qmail.org/man/man5/maildir.html}{maildir man page from
qmail}{The original specification of the format.}
\seelink{http://cr.yp.to/proto/maildir.html}{Using maildir format}{Notes
on Maildir by its inventor. Includes an updated name-creation scheme and
details on "info" semantics.}
\seelink{http://www.courier-mta.org/?maildir.html}{maildir man page from
Courier}{Another specification of the format. Describes a common extension
for supporting folders.}
\end{seealso}
\subsubsection{\class{mbox}}
\label{mailbox-mbox}
\begin{classdesc}{mbox}{path\optional{, factory=None\optional{, create=True}}}
A subclass of \class{Mailbox} for mailboxes in mbox format. Parameter
\var{factory} is a callable object that accepts a file-like message
representation (which behaves as if open in binary mode) and returns a custom
representation. If \var{factory} is \code{None}, \class{mboxMessage} is used as
the default message representation. If \var{create} is \code{True}, the mailbox
is created if it does not exist.
\end{classdesc}
The mbox format is the classic format for storing mail on \UNIX{} systems. All
messages in an mbox mailbox are stored in a single file with the beginning of
each message indicated by a line whose first five characters are "From~".
Several variations of the mbox format exist to address perceived shortcomings
in the original. In the interest of compatibility, \class{mbox} implements the
original format, which is sometimes referred to as \dfn{mboxo}. This means that
the \mailheader{Content-Length} header, if present, is ignored and that any
occurrences of "From~" at the beginning of a line in a message body are
transformed to ">From~" when storing the message, although occurences of
">From~" are not transformed to "From~" when reading the message.
Some \class{Mailbox} methods implemented by \class{mbox} deserve special
remarks:
\begin{methoddesc}{get_file}{key}
Using the file after calling \method{flush()} or \method{close()} on the
\class{mbox} instance may yield unpredictable results or raise an exception.
\end{methoddesc}
\begin{methoddesc}{lock}{}
\methodline{unlock}{}
Three locking mechanisms are used---dot locking and, if available, the
\cfunction{flock()} and \cfunction{lockf()} system calls.
\end{methoddesc}
\begin{seealso}
\seelink{http://www.qmail.org/man/man5/mbox.html}{mbox man page from
qmail}{A specification of the format and its variations.}
\seelink{http://www.tin.org/bin/man.cgi?section=5\&topic=mbox}{mbox man
page from tin}{Another specification of the format, with details on
locking.}
\seelink{http://home.netscape.com/eng/mozilla/2.0/relnotes/demo/content-length.html}
{Configuring Netscape Mail on \UNIX{}: Why The Content-Length Format is
Bad}{An argument for using the original mbox format rather than a
variation.}
\seelink{http://homepages.tesco.net./\tilde{}J.deBoynePollard/FGA/mail-mbox-formats.html}
{"mbox" is a family of several mutually incompatible mailbox formats}{A
history of mbox variations.}
\end{seealso}
\subsubsection{\class{MH}}
\label{mailbox-mh}
\begin{classdesc}{MH}{path\optional{, factory=None\optional{, create=True}}}
A subclass of \class{Mailbox} for mailboxes in MH format. Parameter
\var{factory} is a callable object that accepts a file-like message
representation (which behaves as if open in binary mode) and returns a custom
representation. If \var{factory} is \code{None}, \class{MHMessage} is used as
the default message representation. If \var{create} is \code{True}, the mailbox
is created if it does not exist.
\end{classdesc}
MH is a directory-based mailbox format invented for the MH Message Handling
System, a mail user agent. Each message in an MH mailbox resides in its own
file. An MH mailbox may contain other MH mailboxes (called \dfn{folders}) in
addition to messages. Folders may be nested indefinitely. MH mailboxes also
support \dfn{sequences}, which are named lists used to logically group messages
without moving them to sub-folders. Sequences are defined in a file called
\file{.mh_sequences} in each folder.
The \class{MH} class manipulates MH mailboxes, but it does not attempt to
emulate all of \program{mh}'s behaviors. In particular, it does not modify and
is not affected by the \file{context} or \file{.mh_profile} files that are used
by \program{mh} to store its state and configuration.
\class{MH} instances have all of the methods of \class{Mailbox} in addition to
the following:
\begin{methoddesc}{list_folders}{}
Return a list of the names of all folders.
\end{methoddesc}
\begin{methoddesc}{get_folder}{folder}
Return an \class{MH} instance representing the folder whose name is
\var{folder}. A \exception{NoSuchMailboxError} exception is raised if the
folder does not exist.
\end{methoddesc}
\begin{methoddesc}{add_folder}{folder}
Create a folder whose name is \var{folder} and return an \class{MH} instance
representing it.
\end{methoddesc}
\begin{methoddesc}{remove_folder}{folder}
Delete the folder whose name is \var{folder}. If the folder contains any
messages, a \exception{NotEmptyError} exception will be raised and the folder
will not be deleted.
\end{methoddesc}
\begin{methoddesc}{get_sequences}{}
Return a dictionary of sequence names mapped to key lists. If there are no
sequences, the empty dictionary is returned.
\end{methoddesc}
\begin{methoddesc}{set_sequences}{sequences}
Re-define the sequences that exist in the mailbox based upon \var{sequences}, a
dictionary of names mapped to key lists, like returned by
\method{get_sequences()}.
\end{methoddesc}
\begin{methoddesc}{pack}{}
Rename messages in the mailbox as necessary to eliminate gaps in numbering.
Entries in the sequences list are updated correspondingly. \note{Already-issued
keys are invalidated by this operation and should not be subsequently used.}
\end{methoddesc}
Some \class{Mailbox} methods implemented by \class{MH} deserve special remarks:
\begin{methoddesc}{remove}{key}
\methodline{__delitem__}{key}
\methodline{discard}{key}
These methods immediately delete the message. The MH convention of marking a
message for deletion by prepending a comma to its name is not used.
\end{methoddesc}
\begin{methoddesc}{lock}{}
\methodline{unlock}{}
Three locking mechanisms are used---dot locking and, if available, the
\cfunction{flock()} and \cfunction{lockf()} system calls. For MH mailboxes,
locking the mailbox means locking the \file{.mh_sequences} file and, only for
the duration of any operations that affect them, locking individual message
files.
\end{methoddesc}
\begin{methoddesc}{get_file}{key}
Depending upon the host platform, it may not be possible to remove the
underlying message while the returned file remains open.
\end{methoddesc}
\begin{methoddesc}{flush}{}
All changes to MH mailboxes are immediately applied, so this method does
nothing.
\end{methoddesc}
\begin{methoddesc}{close}{}
\class{MH} instances do not keep any open files, so this method is equivelant
to \method{unlock()}.
\end{methoddesc}
\begin{seealso}
\seelink{http://www.nongnu.org/nmh/}{nmh - Message Handling System}{Home page
of \program{nmh}, an updated version of the original \program{mh}.}
\seelink{http://www.ics.uci.edu/\tilde{}mh/book/}{MH \& nmh: Email for Users \&
Programmers}{A GPL-licensed book on \program{mh} and \program{nmh}, with some
information on the mailbox format.}
\end{seealso}
\subsubsection{\class{Babyl}}
\label{mailbox-babyl}
\begin{classdesc}{Babyl}{path\optional{, factory=None\optional{, create=True}}}
A subclass of \class{Mailbox} for mailboxes in Babyl format. Parameter
\var{factory} is a callable object that accepts a file-like message
representation (which behaves as if open in binary mode) and returns a custom
representation. If \var{factory} is \code{None}, \class{BabylMessage} is used
as the default message representation. If \var{create} is \code{True}, the
mailbox is created if it does not exist.
\end{classdesc}
Babyl is a single-file mailbox format used by the Rmail mail user agent
included with Emacs. The beginning of a message is indicated by a line
containing the two characters Control-Underscore
(\character{\textbackslash037}) and Control-L (\character{\textbackslash014}).
The end of a message is indicated by the start of the next message or, in the
case of the last message, a line containing a Control-Underscore
(\character{\textbackslash037}) character.
Messages in a Babyl mailbox have two sets of headers, original headers and
so-called visible headers. Visible headers are typically a subset of the
original headers that have been reformatted or abridged to be more attractive.
Each message in a Babyl mailbox also has an accompanying list of \dfn{labels},
or short strings that record extra information about the message, and a list of
all user-defined labels found in the mailbox is kept in the Babyl options
section.
\class{Babyl} instances have all of the methods of \class{Mailbox} in addition
to the following:
\begin{methoddesc}{get_labels}{}
Return a list of the names of all user-defined labels used in the mailbox.
\note{The actual messages are inspected to determine which labels exist in the
mailbox rather than consulting the list of labels in the Babyl options section,
but the Babyl section is updated whenever the mailbox is modified.}
\end{methoddesc}
Some \class{Mailbox} methods implemented by \class{Babyl} deserve special
remarks:
\begin{methoddesc}{get_file}{key}
In Babyl mailboxes, the headers of a message are not stored contiguously with
the body of the message. To generate a file-like representation, the headers
and body are copied together into a \class{StringIO} instance (from the
\module{StringIO} module), which has an API identical to that of a file. As a
result, the file-like object is truly independent of the underlying mailbox but
does not save memory compared to a string representation.
\end{methoddesc}
\begin{methoddesc}{lock}{}
\methodline{unlock}{}
Three locking mechanisms are used---dot locking and, if available, the
\cfunction{flock()} and \cfunction{lockf()} system calls.
\end{methoddesc}
\begin{seealso}
\seelink{http://quimby.gnus.org/notes/BABYL}{Format of Version 5 Babyl Files}{A
specification of the Babyl format.}
\seelink{http://www.gnu.org/software/emacs/manual/html_node/Rmail.html}{Reading
Mail with Rmail}{The Rmail manual, with some information on Babyl semantics.}
\end{seealso}
\subsubsection{\class{MMDF}}
\label{mailbox-mmdf}
\begin{classdesc}{MMDF}{path\optional{, factory=None\optional{, create=True}}}
A subclass of \class{Mailbox} for mailboxes in MMDF format. Parameter
\var{factory} is a callable object that accepts a file-like message
representation (which behaves as if open in binary mode) and returns a custom
representation. If \var{factory} is \code{None}, \class{MMDFMessage} is used as
the default message representation. If \var{create} is \code{True}, the mailbox
is created if it does not exist.
\end{classdesc}
MMDF is a single-file mailbox format invented for the Multichannel Memorandum
Distribution Facility, a mail transfer agent. Each message is in the same form
as an mbox message but is bracketed before and after by lines containing four
Control-A (\character{\textbackslash001}) characters. As with the mbox format,
the beginning of each message is indicated by a line whose first five
characters are "From~", but additional occurrences of "From~" are not
transformed to ">From~" when storing messages because the extra message
separator lines prevent mistaking such occurrences for the starts of subsequent
messages.
Some \class{Mailbox} methods implemented by \class{MMDF} deserve special
remarks:
\begin{methoddesc}{get_file}{key}
Using the file after calling \method{flush()} or \method{close()} on the
\class{MMDF} instance may yield unpredictable results or raise an exception.
\end{methoddesc}
\begin{methoddesc}{lock}{}
\methodline{unlock}{}
Three locking mechanisms are used---dot locking and, if available, the
\cfunction{flock()} and \cfunction{lockf()} system calls.
\end{methoddesc}
\begin{seealso}
\seelink{http://www.tin.org/bin/man.cgi?section=5\&topic=mmdf}{mmdf man page
from tin}{A specification of MMDF format from the documentation of tin, a
newsreader.}
\seelink{http://en.wikipedia.org/wiki/MMDF}{MMDF}{A Wikipedia article
describing the Multichannel Memorandum Distribution Facility.}
\end{seealso}
\subsection{\class{Message} objects}
\label{mailbox-message-objects}
\begin{classdesc}{Message}{\optional{message}}
A subclass of the \module{email.Message} module's \class{Message}. Subclasses
of \class{mailbox.Message} add mailbox-format-specific state and behavior.
If \var{message} is omitted, the new instance is created in a default, empty
state. If \var{message} is an \class{email.Message.Message} instance, its
contents are copied; furthermore, any format-specific information is converted
insofar as possible if \var{message} is a \class{Message} instance. If
\var{message} is a string or a file, it should contain an \rfc{2822}-compliant
message, which is read and parsed.
\end{classdesc}
The format-specific state and behaviors offered by subclasses vary, but in
general it is only the properties that are not specific to a particular mailbox
that are supported (although presumably the properties are specific to a
particular mailbox format). For example, file offsets for single-file mailbox
formats and file names for directory-based mailbox formats are not retained,
because they are only applicable to the original mailbox. But state such as
whether a message has been read by the user or marked as important is retained,
because it applies to the message itself.
There is no requirement that \class{Message} instances be used to represent
messages retrieved using \class{Mailbox} instances. In some situations, the
time and memory required to generate \class{Message} representations might not
not acceptable. For such situations, \class{Mailbox} instances also offer
string and file-like representations, and a custom message factory may be
specified when a \class{Mailbox} instance is initialized.
\subsubsection{\class{MaildirMessage}}
\label{mailbox-maildirmessage}
\begin{classdesc}{MaildirMessage}{\optional{message}}
A message with Maildir-specific behaviors. Parameter \var{message}
has the same meaning as with the \class{Message} constructor.
\end{classdesc}
Typically, a mail user agent application moves all of the messages in the
\file{new} subdirectory to the \file{cur} subdirectory after the first time the
user opens and closes the mailbox, recording that the messages are old whether
or not they've actually been read. Each message in \file{cur} has an "info"
section added to its file name to store information about its state. (Some mail
readers may also add an "info" section to messages in \file{new}.) The "info"
section may take one of two forms: it may contain "2," followed by a list of
standardized flags (e.g., "2,FR") or it may contain "1," followed by so-called
experimental information. Standard flags for Maildir messages are as follows:
\begin{tableiii}{l|l|l}{textrm}{Flag}{Meaning}{Explanation}
\lineiii{D}{Draft}{Under composition}
\lineiii{F}{Flagged}{Marked as important}
\lineiii{P}{Passed}{Forwarded, resent, or bounced}
\lineiii{R}{Replied}{Replied to}
\lineiii{S}{Seen}{Read}
\lineiii{T}{Trashed}{Marked for subsequent deletion}
\end{tableiii}
\class{MaildirMessage} instances offer the following methods:
\begin{methoddesc}{get_subdir}{}
Return either "new" (if the message should be stored in the \file{new}
subdirectory) or "cur" (if the message should be stored in the \file{cur}
subdirectory). \note{A message is typically moved from \file{new} to \file{cur}
after its mailbox has been accessed, whether or not the message is has been
read. A message \code{msg} has been read if \code{"S" not in msg.get_flags()}
is \code{True}.}
\end{methoddesc}
\begin{methoddesc}{set_subdir}{subdir}
Set the subdirectory the message should be stored in. Parameter \var{subdir}
must be either "new" or "cur".
\end{methoddesc}
\begin{methoddesc}{get_flags}{}
Return a string specifying the flags that are currently set. If the message
complies with the standard Maildir format, the result is the concatenation in
alphabetical order of zero or one occurrence of each of \character{D},
\character{F}, \character{P}, \character{R}, \character{S}, and \character{T}.
The empty string is returned if no flags are set or if "info" contains
experimental semantics.
\end{methoddesc}
\begin{methoddesc}{set_flags}{flags}
Set the flags specified by \var{flags} and unset all others.
\end{methoddesc}
\begin{methoddesc}{add_flag}{flag}
Set the flag(s) specified by \var{flag} without changing other flags. To add
more than one flag at a time, \var{flag} may be a string of more than one
character. The current "info" is overwritten whether or not it contains
experimental information rather than
flags.
\end{methoddesc}
\begin{methoddesc}{remove_flag}{flag}
Unset the flag(s) specified by \var{flag} without changing other flags. To
remove more than one flag at a time, \var{flag} maybe a string of more than one
character. If "info" contains experimental information rather than flags, the
current "info" is not modified.
\end{methoddesc}
\begin{methoddesc}{get_date}{}
Return the delivery date of the message as a floating-point number representing
seconds since the epoch.
\end{methoddesc}
\begin{methoddesc}{set_date}{date}
Set the delivery date of the message to \var{date}, a floating-point number
representing seconds since the epoch.
\end{methoddesc}
\begin{methoddesc}{get_info}{}
Return a string containing the "info" for a message. This is useful for
accessing and modifying "info" that is experimental (i.e., not a list of
flags).
\end{methoddesc}
\begin{methoddesc}{set_info}{info}
Set "info" to \var{info}, which should be a string.
\end{methoddesc}
When a \class{MaildirMessage} instance is created based upon an
\class{mboxMessage} or \class{MMDFMessage} instance, the \mailheader{Status}
and \mailheader{X-Status} headers are omitted and the following conversions
take place:
\begin{tableii}{l|l}{textrm}
{Resulting state}{\class{mboxMessage} or \class{MMDFMessage} state}
\lineii{"cur" subdirectory}{O flag}
\lineii{F flag}{F flag}
\lineii{R flag}{A flag}
\lineii{S flag}{R flag}
\lineii{T flag}{D flag}
\end{tableii}
When a \class{MaildirMessage} instance is created based upon an
\class{MHMessage} instance, the following conversions take place:
\begin{tableii}{l|l}{textrm}
{Resulting state}{\class{MHMessage} state}
\lineii{"cur" subdirectory}{"unseen" sequence}
\lineii{"cur" subdirectory and S flag}{no "unseen" sequence}
\lineii{F flag}{"flagged" sequence}
\lineii{R flag}{"replied" sequence}
\end{tableii}
When a \class{MaildirMessage} instance is created based upon a
\class{BabylMessage} instance, the following conversions take place:
\begin{tableii}{l|l}{textrm}
{Resulting state}{\class{BabylMessage} state}
\lineii{"cur" subdirectory}{"unseen" label}
\lineii{"cur" subdirectory and S flag}{no "unseen" label}
\lineii{P flag}{"forwarded" or "resent" label}
\lineii{R flag}{"answered" label}
\lineii{T flag}{"deleted" label}
\end{tableii}
\subsubsection{\class{mboxMessage}}
\label{mailbox-mboxmessage}
\begin{classdesc}{mboxMessage}{\optional{message}}
A message with mbox-specific behaviors. Parameter \var{message} has the same
meaning as with the \class{Message} constructor.
\end{classdesc}
Messages in an mbox mailbox are stored together in a single file. The sender's
envelope address and the time of delivery are typically stored in a line
beginning with "From~" that is used to indicate the start of a message, though
there is considerable variation in the exact format of this data among mbox
implementations. Flags that indicate the state of the message, such as whether
it has been read or marked as important, are typically stored in
\mailheader{Status} and \mailheader{X-Status} headers.
Conventional flags for mbox messages are as follows:
\begin{tableiii}{l|l|l}{textrm}{Flag}{Meaning}{Explanation}
\lineiii{R}{Read}{Read}
\lineiii{O}{Old}{Previously detected by MUA}
\lineiii{D}{Deleted}{Marked for subsequent deletion}
\lineiii{F}{Flagged}{Marked as important}
\lineiii{A}{Answered}{Replied to}
\end{tableiii}
The "R" and "O" flags are stored in the \mailheader{Status} header, and the
"D", "F", and "A" flags are stored in the \mailheader{X-Status} header. The
flags and headers typically appear in the order mentioned.
\class{mboxMessage} instances offer the following methods:
\begin{methoddesc}{get_from}{}
Return a string representing the "From~" line that marks the start of the
message in an mbox mailbox. The leading "From~" and the trailing newline are
excluded.
\end{methoddesc}
\begin{methoddesc}{set_from}{from_\optional{, time_=None}}
Set the "From~" line to \var{from_}, which should be specified without a
leading "From~" or trailing newline. For convenience, \var{time_} may be
specified and will be formatted appropriately and appended to \var{from_}. If
\var{time_} is specified, it should be a \class{struct_time} instance, a tuple
suitable for passing to \method{time.strftime()}, or \code{True} (to use
\method{time.gmtime()}).
\end{methoddesc}
\begin{methoddesc}{get_flags}{}
Return a string specifying the flags that are currently set. If the message
complies with the conventional format, the result is the concatenation in the
following order of zero or one occurrence of each of \character{R},
\character{O}, \character{D}, \character{F}, and \character{A}.
\end{methoddesc}
\begin{methoddesc}{set_flags}{flags}
Set the flags specified by \var{flags} and unset all others. Parameter
\var{flags} should be the concatenation in any order of zero or more
occurrences of each of \character{R}, \character{O}, \character{D},
\character{F}, and \character{A}.
\end{methoddesc}
\begin{methoddesc}{add_flag}{flag}
Set the flag(s) specified by \var{flag} without changing other flags. To add
more than one flag at a time, \var{flag} may be a string of more than one
character.
\end{methoddesc}
\begin{methoddesc}{remove_flag}{flag}
Unset the flag(s) specified by \var{flag} without changing other flags. To
remove more than one flag at a time, \var{flag} maybe a string of more than one
character.
\end{methoddesc}
When an \class{mboxMessage} instance is created based upon a
\class{MaildirMessage} instance, a "From~" line is generated based upon the
\class{MaildirMessage} instance's delivery date, and the following conversions
take place:
\begin{tableii}{l|l}{textrm}
{Resulting state}{\class{MaildirMessage} state}
\lineii{R flag}{S flag}
\lineii{O flag}{"cur" subdirectory}
\lineii{D flag}{T flag}
\lineii{F flag}{F flag}
\lineii{A flag}{R flag}
\end{tableii}
When an \class{mboxMessage} instance is created based upon an \class{MHMessage}
instance, the following conversions take place:
\begin{tableii}{l|l}{textrm}
{Resulting state}{\class{MHMessage} state}
\lineii{R flag and O flag}{no "unseen" sequence}
\lineii{O flag}{"unseen" sequence}
\lineii{F flag}{"flagged" sequence}
\lineii{A flag}{"replied" sequence}
\end{tableii}
When an \class{mboxMessage} instance is created based upon a
\class{BabylMessage} instance, the following conversions take place:
\begin{tableii}{l|l}{textrm}
{Resulting state}{\class{BabylMessage} state}
\lineii{R flag and O flag}{no "unseen" label}
\lineii{O flag}{"unseen" label}
\lineii{D flag}{"deleted" label}
\lineii{A flag}{"answered" label}
\end{tableii}
When a \class{Message} instance is created based upon an \class{MMDFMessage}
instance, the "From~" line is copied and all flags directly correspond:
\begin{tableii}{l|l}{textrm}
{Resulting state}{\class{MMDFMessage} state}
\lineii{R flag}{R flag}
\lineii{O flag}{O flag}
\lineii{D flag}{D flag}
\lineii{F flag}{F flag}
\lineii{A flag}{A flag}
\end{tableii}
\subsubsection{\class{MHMessage}}
\label{mailbox-mhmessage}
\begin{classdesc}{MHMessage}{\optional{message}}
A message with MH-specific behaviors. Parameter \var{message} has the same
meaning as with the \class{Message} constructor.
\end{classdesc}
MH messages do not support marks or flags in the traditional sense, but they do
support sequences, which are logical groupings of arbitrary messages. Some mail
reading programs (although not the standard \program{mh} and \program{nmh}) use
sequences in much the same way flags are used with other formats, as follows:
\begin{tableii}{l|l}{textrm}{Sequence}{Explanation}
\lineii{unseen}{Not read, but previously detected by MUA}
\lineii{replied}{Replied to}
\lineii{flagged}{Marked as important}
\end{tableii}
\class{MHMessage} instances offer the following methods:
\begin{methoddesc}{get_sequences}{}
Return a list of the names of sequences that include this message.
\end{methoddesc}
\begin{methoddesc}{set_sequences}{sequences}
Set the list of sequences that include this message.
\end{methoddesc}
\begin{methoddesc}{add_sequence}{sequence}
Add \var{sequence} to the list of sequences that include this message.
\end{methoddesc}
\begin{methoddesc}{remove_sequence}{sequence}
Remove \var{sequence} from the list of sequences that include this message.
\end{methoddesc}
When an \class{MHMessage} instance is created based upon a
\class{MaildirMessage} instance, the following conversions take place:
\begin{tableii}{l|l}{textrm}
{Resulting state}{\class{MaildirMessage} state}
\lineii{"unseen" sequence}{no S flag}
\lineii{"replied" sequence}{R flag}
\lineii{"flagged" sequence}{F flag}
\end{tableii}
When an \class{MHMessage} instance is created based upon an \class{mboxMessage}
or \class{MMDFMessage} instance, the \mailheader{Status} and
\mailheader{X-Status} headers are omitted and the following conversions take
place:
\begin{tableii}{l|l}{textrm}
{Resulting state}{\class{mboxMessage} or \class{MMDFMessage} state}
\lineii{"unseen" sequence}{no R flag}
\lineii{"replied" sequence}{A flag}
\lineii{"flagged" sequence}{F flag}
\end{tableii}
When an \class{MHMessage} instance is created based upon a \class{BabylMessage}
instance, the following conversions take place:
\begin{tableii}{l|l}{textrm}
{Resulting state}{\class{BabylMessage} state}
\lineii{"unseen" sequence}{"unseen" label}
\lineii{"replied" sequence}{"answered" label}
\end{tableii}
\subsubsection{\class{BabylMessage}}
\label{mailbox-babylmessage}
\begin{classdesc}{BabylMessage}{\optional{message}}
A message with Babyl-specific behaviors. Parameter \var{message} has the same
meaning as with the \class{Message} constructor.
\end{classdesc}
Certain message labels, called \dfn{attributes}, are defined by convention to
have special meanings. The attributes are as follows:
\begin{tableii}{l|l}{textrm}{Label}{Explanation}
\lineii{unseen}{Not read, but previously detected by MUA}
\lineii{deleted}{Marked for subsequent deletion}
\lineii{filed}{Copied to another file or mailbox}
\lineii{answered}{Replied to}
\lineii{forwarded}{Forwarded}
\lineii{edited}{Modified by the user}
\lineii{resent}{Resent}
\end{tableii}
By default, Rmail displays only
visible headers. The \class{BabylMessage} class, though, uses the original
headers because they are more complete. Visible headers may be accessed
explicitly if desired.
\class{BabylMessage} instances offer the following methods:
\begin{methoddesc}{get_labels}{}
Return a list of labels on the message.
\end{methoddesc}
\begin{methoddesc}{set_labels}{labels}
Set the list of labels on the message to \var{labels}.
\end{methoddesc}
\begin{methoddesc}{add_label}{label}
Add \var{label} to the list of labels on the message.
\end{methoddesc}
\begin{methoddesc}{remove_label}{label}
Remove \var{label} from the list of labels on the message.
\end{methoddesc}
\begin{methoddesc}{get_visible}{}
Return an \class{Message} instance whose headers are the message's visible
headers and whose body is empty.
\end{methoddesc}
\begin{methoddesc}{set_visible}{visible}
Set the message's visible headers to be the same as the headers in
\var{message}. Parameter \var{visible} should be a \class{Message} instance, an
\class{email.Message.Message} instance, a string, or a file-like object (which
should be open in text mode).
\end{methoddesc}
\begin{methoddesc}{update_visible}{}
When a \class{BabylMessage} instance's original headers are modified, the
visible headers are not automatically modified to correspond. This method
updates the visible headers as follows: each visible header with a
corresponding original header is set to the value of the original header, each
visible header without a corresponding original header is removed, and any of
\mailheader{Date}, \mailheader{From}, \mailheader{Reply-To}, \mailheader{To},
\mailheader{CC}, and \mailheader{Subject} that are present in the original
headers but not the visible headers are added to the visible headers.
\end{methoddesc}
When a \class{BabylMessage} instance is created based upon a
\class{MaildirMessage} instance, the following conversions take place:
\begin{tableii}{l|l}{textrm}
{Resulting state}{\class{MaildirMessage} state}
\lineii{"unseen" label}{no S flag}
\lineii{"deleted" label}{T flag}
\lineii{"answered" label}{R flag}
\lineii{"forwarded" label}{P flag}
\end{tableii}
When a \class{BabylMessage} instance is created based upon an
\class{mboxMessage} or \class{MMDFMessage} instance, the \mailheader{Status}
and \mailheader{X-Status} headers are omitted and the following conversions
take place:
\begin{tableii}{l|l}{textrm}
{Resulting state}{\class{mboxMessage} or \class{MMDFMessage} state}
\lineii{"unseen" label}{no R flag}
\lineii{"deleted" label}{D flag}
\lineii{"answered" label}{A flag}
\end{tableii}
When a \class{BabylMessage} instance is created based upon an \class{MHMessage}
instance, the following conversions take place:
\begin{tableii}{l|l}{textrm}
{Resulting state}{\class{MHMessage} state}
\lineii{"unseen" label}{"unseen" sequence}
\lineii{"answered" label}{"replied" sequence}
\end{tableii}
\subsubsection{\class{MMDFMessage}}
\label{mailbox-mmdfmessage}
\begin{classdesc}{MMDFMessage}{\optional{message}}
A message with MMDF-specific behaviors. Parameter \var{message} has the same
meaning as with the \class{Message} constructor.
\end{classdesc}
As with message in an mbox mailbox, MMDF messages are stored with the sender's
address and the delivery date in an initial line beginning with "From ".
Likewise, flags that indicate the state of the message are typically stored in
\mailheader{Status} and \mailheader{X-Status} headers.
Conventional flags for MMDF messages are identical to those of mbox message and
are as follows:
\begin{tableiii}{l|l|l}{textrm}{Flag}{Meaning}{Explanation}
\lineiii{R}{Read}{Read}
\lineiii{O}{Old}{Previously detected by MUA}
\lineiii{D}{Deleted}{Marked for subsequent deletion}
\lineiii{F}{Flagged}{Marked as important}
\lineiii{A}{Answered}{Replied to}
\end{tableiii}
The "R" and "O" flags are stored in the \mailheader{Status} header, and the
"D", "F", and "A" flags are stored in the \mailheader{X-Status} header. The
flags and headers typically appear in the order mentioned.
\class{MMDFMessage} instances offer the following methods, which are identical
to those offered by \class{mboxMessage}:
\begin{methoddesc}{get_from}{}
Return a string representing the "From~" line that marks the start of the
message in an mbox mailbox. The leading "From~" and the trailing newline are
excluded.
\end{methoddesc}
\begin{methoddesc}{set_from}{from_\optional{, time_=None}}
Set the "From~" line to \var{from_}, which should be specified without a
leading "From~" or trailing newline. For convenience, \var{time_} may be
specified and will be formatted appropriately and appended to \var{from_}. If
\var{time_} is specified, it should be a \class{struct_time} instance, a tuple
suitable for passing to \method{time.strftime()}, or \code{True} (to use
\method{time.gmtime()}).
\end{methoddesc}
\begin{methoddesc}{get_flags}{}
Return a string specifying the flags that are currently set. If the message
complies with the conventional format, the result is the concatenation in the
following order of zero or one occurrence of each of \character{R},
\character{O}, \character{D}, \character{F}, and \character{A}.
\end{methoddesc}
\begin{methoddesc}{set_flags}{flags}
Set the flags specified by \var{flags} and unset all others. Parameter
\var{flags} should be the concatenation in any order of zero or more
occurrences of each of \character{R}, \character{O}, \character{D},
\character{F}, and \character{A}.
\end{methoddesc}
\begin{methoddesc}{add_flag}{flag}
Set the flag(s) specified by \var{flag} without changing other flags. To add
more than one flag at a time, \var{flag} may be a string of more than one
character.
\end{methoddesc}
\begin{methoddesc}{remove_flag}{flag}
Unset the flag(s) specified by \var{flag} without changing other flags. To
remove more than one flag at a time, \var{flag} maybe a string of more than one
character.
\end{methoddesc}
When an \class{MMDFMessage} instance is created based upon a
\class{MaildirMessage} instance, a "From~" line is generated based upon the
\class{MaildirMessage} instance's delivery date, and the following conversions
take place:
\begin{tableii}{l|l}{textrm}
{Resulting state}{\class{MaildirMessage} state}
\lineii{R flag}{S flag}
\lineii{O flag}{"cur" subdirectory}
\lineii{D flag}{T flag}
\lineii{F flag}{F flag}
\lineii{A flag}{R flag}
\end{tableii}
When an \class{MMDFMessage} instance is created based upon an \class{MHMessage}
instance, the following conversions take place:
\begin{tableii}{l|l}{textrm}
{Resulting state}{\class{MHMessage} state}
\lineii{R flag and O flag}{no "unseen" sequence}
\lineii{O flag}{"unseen" sequence}
\lineii{F flag}{"flagged" sequence}
\lineii{A flag}{"replied" sequence}
\end{tableii}
When an \class{MMDFMessage} instance is created based upon a
\class{BabylMessage} instance, the following conversions take place:
\begin{tableii}{l|l}{textrm}
{Resulting state}{\class{BabylMessage} state}
\lineii{R flag and O flag}{no "unseen" label}
\lineii{O flag}{"unseen" label}
\lineii{D flag}{"deleted" label}
\lineii{A flag}{"answered" label}
\end{tableii}
When an \class{MMDFMessage} instance is created based upon an
\class{mboxMessage} instance, the "From~" line is copied and all flags directly
correspond:
\begin{tableii}{l|l}{textrm}
{Resulting state}{\class{mboxMessage} state}
\lineii{R flag}{R flag}
\lineii{O flag}{O flag}
\lineii{D flag}{D flag}
\lineii{F flag}{F flag}
\lineii{A flag}{A flag}
\end{tableii}
\subsection{Exceptions}
\label{mailbox-deprecated}
The following exception classes are defined in the \module{mailbox} module:
\begin{classdesc}{Error}{}
The based class for all other module-specific exceptions.
\end{classdesc}
\begin{classdesc}{NoSuchMailboxError}{}
Raised when a mailbox is expected but is not found, such as when instantiating
a \class{Mailbox} subclass with a path that does not exist (and with the
\var{create} parameter set to \code{False}), or when opening a folder that does
not exist.
\end{classdesc}
\begin{classdesc}{NotEmptyErrorError}{}
Raised when a mailbox is not empty but is expected to be, such as when deleting
a folder that contains messages.
\end{classdesc}
\begin{classdesc}{ExternalClashError}{}
Raised when some mailbox-related condition beyond the control of the program
causes it to be unable to proceed, such as when failing to acquire a lock that
another program already holds a lock, or when a uniquely-generated file name
already exists.
\end{classdesc}
\begin{classdesc}{FormatError}{}
Raised when the data in a file cannot be parsed, such as when an \class{MH}
instance attempts to read a corrupted \file{.mh_sequences} file.
\end{classdesc}
\subsection{Deprecated classes and methods}
\label{mailbox-deprecated}
Older versions of the \module{mailbox} module do not support modification of
mailboxes, such as adding or removing message, and do not provide classes to
represent format-specific message properties. For backward compatibility, the
older mailbox classes are still available, but the newer classes should be used
in preference to them.
Older mailbox objects support only iteration and provide a single public
method:
\begin{methoddesc}{next}{}
Return the next message in the mailbox, created with the optional \var{factory}
argument passed into the mailbox object's constructor. By default this is an
\class{rfc822.Message} object (see the \refmodule{rfc822} module). Depending
on the mailbox implementation the \var{fp} attribute of this object may be a
true file object or a class instance simulating a file object, taking care of
things like message boundaries if multiple mail messages are contained in a
single file, etc. If no more messages are available, this method returns
\code{None}.
\end{methoddesc}
Most of the older mailbox classes have names that differ from the current
mailbox class names, except for \class{Maildir}. For this reason, the new
\class{Maildir} class defines a \method{next()} method and its constructor
differs slightly from those of the other new mailbox classes.
The older mailbox classes whose names are not the same as their newer
counterparts are as follows:
\begin{classdesc}{UnixMailbox}{fp\optional{, factory}}
Access to a classic \UNIX-style mailbox, where all messages are
......@@ -68,12 +1309,6 @@ The name of the mailbox directory is passed in \var{dirname}.
\var{factory} is as with the \class{UnixMailbox} class.
\end{classdesc}
\begin{classdesc}{Maildir}{dirname\optional{, factory}}
Access a Qmail mail directory. All new and current mail for the
mailbox specified by \var{dirname} is made available.
\var{factory} is as with the \class{UnixMailbox} class.
\end{classdesc}
\begin{classdesc}{BabylMailbox}{fp\optional{, factory}}
Access a Babyl mailbox, which is similar to an MMDF mailbox. In
Babyl format, each message has two sets of headers, the
......@@ -89,11 +1324,8 @@ messages start with the EOOH line and end with a line containing only
\class{UnixMailbox} class.
\end{classdesc}
Note that because the \refmodule{rfc822} module is deprecated, it is
recommended that you use the \refmodule{email} package to create
message objects from a mailbox. (The default can't be changed for
backwards compatibility reasons.) The safest way to do this is with
bit of code:
If you wish to use the older mailbox classes with the \module{email} module
rather than the deprecated \module{rfc822} module, you can do so as follows:
\begin{verbatim}
import email
......@@ -111,11 +1343,8 @@ def msgfactory(fp):
mbox = mailbox.UnixMailbox(fp, msgfactory)
\end{verbatim}
The above wrapper is defensive against ill-formed MIME messages in the
mailbox, but you have to be prepared to receive the empty string from
the mailbox's \function{next()} method. On the other hand, if you
know your mailbox contains only well-formed MIME messages, you can
simplify this to:
Alternatively, if you know your mailbox contains only well-formed MIME
messages, you can simplify this to:
\begin{verbatim}
import email
......@@ -124,35 +1353,57 @@ import mailbox
mbox = mailbox.UnixMailbox(fp, email.message_from_file)
\end{verbatim}
\begin{seealso}
\seetitle[http://www.qmail.org/man/man5/mbox.html]{mbox -
file containing mail messages}{Description of the
traditional ``mbox'' mailbox format.}
\seetitle[http://www.qmail.org/man/man5/maildir.html]{maildir -
directory for incoming mail messages}{Description of the
``maildir'' mailbox format.}
\seetitle[http://home.netscape.com/eng/mozilla/2.0/relnotes/demo/content-length.html]{Configuring
Netscape Mail on \UNIX: Why the Content-Length Format is
Bad}{A description of problems with relying on the
\mailheader{Content-Length} header for messages stored in
mailbox files.}
\end{seealso}
\subsection{Examples}
\label{mailbox-examples}
A simple example of printing the subjects of all messages in a mailbox that
seem interesting:
\subsection{Mailbox Objects \label{mailbox-objects}}
\begin{verbatim}
import mailbox
for message in mailbox.mbox('~/mbox'):
subject = message['subject'] # Could possibly be None.
if subject and 'python' in subject.lower():
print subject
\end{verbatim}
All implementations of mailbox objects are iterable objects, and
have one externally visible method. This method is used by iterators
created from mailbox objects and may also be used directly.
A (surprisingly) simple example of copying all mail from a Babyl mailbox to an
MH mailbox, converting all of the format-specific information that can be
converted:
\begin{methoddesc}[mailbox]{next}{}
Return the next message in the mailbox, created with the optional
\var{factory} argument passed into the mailbox object's constructor.
By default this is an \class{rfc822.Message}
object (see the \refmodule{rfc822} module). Depending on the mailbox
implementation the \var{fp} attribute of this object may be a true
file object or a class instance simulating a file object, taking care
of things like message boundaries if multiple mail messages are
contained in a single file, etc. If no more messages are available,
this method returns \code{None}.
\end{methoddesc}
\begin{verbatim}
import mailbox
destination = mailbox.MH('~/Mail')
for message in mailbox.Babyl('~/RMAIL'):
destination.add(MHMessage(message))
\end{verbatim}
An example of sorting mail from numerous mailing lists, being careful to avoid
mail corruption due to concurrent modification by other programs, mail loss due
to interruption of the program, or premature termination due to malformed
messages in the mailbox:
\begin{verbatim}
import mailbox
import email.Errors
list_names = ('python-list', 'python-dev', 'python-bugs')
boxes = dict((name, mailbox.mbox('~/email/%s' % name)) for name in list_names)
inbox = mailbox.Maildir('~/Maildir', None)
for key in inbox.iterkeys():
try:
message = inbox[key]
except email.Errors.MessageParseError:
continue # The message is malformed. Just leave it.
for name in list_names:
list_id = message['list-id']
if list_id and name in list_id:
box = boxes[name]
box.lock()
box.add(message)
box.flush() # Write copy to disk before removing original.
box.unlock()
inbox.discard(key)
break # Found destination, so stop looking.
for box in boxes.itervalues():
box.close()
\end{verbatim}
#! /usr/bin/env python
"""Classes to handle Unix style, MMDF style, and MH style mailboxes."""
"""Read/write support for Maildir, mbox, MH, Babyl, and MMDF mailboxes."""
import rfc822
import os
import time
import calendar
import socket
import errno
import copy
import email
import email.Message
import email.Generator
import rfc822
import StringIO
try:
import fnctl
except ImportError:
fcntl = None
__all__ = ["UnixMailbox","MmdfMailbox","MHMailbox","Maildir","BabylMailbox",
"PortableUnixMailbox"]
__all__ = [ 'Mailbox', 'Maildir', 'mbox', 'MH', 'Babyl', 'MMDF',
'Message', 'MaildirMessage', 'mboxMessage', 'MHMessage',
'BabylMessage', 'MMDFMessage', 'UnixMailbox',
'PortableUnixMailbox', 'MmdfMailbox', 'MHMailbox', 'BabylMailbox' ]
class _Mailbox:
def __init__(self, fp, factory=rfc822.Message):
self.fp = fp
self.seekp = 0
self.factory = factory
class Mailbox:
"""A group of messages in a particular place."""
def __init__(self, path, factory=None, create=True):
"""Initialize a Mailbox instance."""
self._path = os.path.abspath(os.path.expanduser(path))
self._factory = factory
def add(self, message):
"""Add message and return assigned key."""
raise NotImplementedError('Method must be implemented by subclass')
def remove(self, key):
"""Remove the keyed message; raise KeyError if it doesn't exist."""
raise NotImplementedError('Method must be implemented by subclass')
def __delitem__(self, key):
self.remove(key)
def discard(self, key):
"""If the keyed message exists, remove it."""
try:
self.remove(key)
except KeyError:
pass
def __setitem__(self, key, message):
"""Replace the keyed message; raise KeyError if it doesn't exist."""
raise NotImplementedError('Method must be implemented by subclass')
def get(self, key, default=None):
"""Return the keyed message, or default if it doesn't exist."""
try:
return self.__getitem__(key)
except KeyError:
return default
def __getitem__(self, key):
"""Return the keyed message; raise KeyError if it doesn't exist."""
if not self._factory:
return self.get_message(key)
else:
return self._factory(self.get_file(key))
def get_message(self, key):
"""Return a Message representation or raise a KeyError."""
raise NotImplementedError('Method must be implemented by subclass')
def get_string(self, key):
"""Return a string representation or raise a KeyError."""
raise NotImplementedError('Method must be implemented by subclass')
def get_file(self, key):
"""Return a file-like representation or raise a KeyError."""
raise NotImplementedError('Method must be implemented by subclass')
def iterkeys(self):
"""Return an iterator over keys."""
raise NotImplementedError('Method must be implemented by subclass')
def keys(self):
"""Return a list of keys."""
return list(self.iterkeys())
def itervalues(self):
"""Return an iterator over all messages."""
for key in self.iterkeys():
try:
value = self[key]
except KeyError:
continue
yield value
def __iter__(self):
return iter(self.next, None)
return self.itervalues()
def values(self):
"""Return a list of messages. Memory intensive."""
return list(self.itervalues())
def iteritems(self):
"""Return an iterator over (key, message) tuples."""
for key in self.iterkeys():
try:
value = self[key]
except KeyError:
continue
yield (key, value)
def items(self):
"""Return a list of (key, message) tuples. Memory intensive."""
return list(self.iteritems())
def has_key(self, key):
"""Return True if the keyed message exists, False otherwise."""
raise NotImplementedError('Method must be implemented by subclass')
def __contains__(self, key):
return self.has_key(key)
def __len__(self):
"""Return a count of messages in the mailbox."""
raise NotImplementedError('Method must be implemented by subclass')
def clear(self):
"""Delete all messages."""
for key in self.iterkeys():
self.discard(key)
def pop(self, key, default=None):
"""Delete the keyed message and return it, or default."""
try:
result = self[key]
except KeyError:
return default
self.discard(key)
return result
def popitem(self):
"""Delete an arbitrary (key, message) pair and return it."""
for key in self.iterkeys():
return (key, self.pop(key)) # This is only run once.
else:
raise KeyError('No messages in mailbox')
def update(self, arg=None):
"""Change the messages that correspond to certain keys."""
if hasattr(arg, 'iteritems'):
source = arg.iteritems()
elif hasattr(arg, 'items'):
source = arg.items()
else:
source = arg
bad_key = False
for key, message in source:
try:
self[key] = message
except KeyError:
bad_key = True
if bad_key:
raise KeyError('No message with key(s)')
def flush(self):
"""Write any pending changes to the disk."""
raise NotImplementedError('Method must be implemented by subclass')
def lock(self):
"""Lock the mailbox."""
raise NotImplementedError('Method must be implemented by subclass')
def unlock(self):
"""Unlock the mailbox if it is locked."""
raise NotImplementedError('Method must be implemented by subclass')
def close(self):
"""Flush and close the mailbox."""
raise NotImplementedError('Method must be implemented by subclass')
def _dump_message(self, message, target, mangle_from_=False):
# Most files are opened in binary mode to allow predictable seeking.
# To get native line endings on disk, the user-friendly \n line endings
# used in strings and by email.Message are translated here.
"""Dump message contents to target file."""
if isinstance(message, email.Message.Message):
buffer = StringIO.StringIO()
gen = email.Generator.Generator(buffer, mangle_from_, 0)
gen.flatten(message)
buffer.seek(0)
target.write(buffer.read().replace('\n', os.linesep))
elif isinstance(message, str):
if mangle_from_:
message = message.replace('\nFrom ', '\n>From ')
message = message.replace('\n', os.linesep)
target.write(message)
elif hasattr(message, 'read'):
while True:
line = message.readline()
if line == '':
break
if mangle_from_ and line.startswith('From '):
line = '>From ' + line[5:]
line = line.replace('\n', os.linesep)
target.write(line)
else:
raise TypeError('Invalid message type: %s' % type(message))
class Maildir(Mailbox):
"""A qmail-style Maildir mailbox."""
colon = ':'
def __init__(self, dirname, factory=rfc822.Message, create=True):
"""Initialize a Maildir instance."""
Mailbox.__init__(self, dirname, factory, create)
if not os.path.exists(self._path):
if create:
os.mkdir(self._path, 0700)
os.mkdir(os.path.join(self._path, 'tmp'), 0700)
os.mkdir(os.path.join(self._path, 'new'), 0700)
os.mkdir(os.path.join(self._path, 'cur'), 0700)
else:
raise NoSuchMailboxError(self._path)
self._toc = {}
def add(self, message):
"""Add message and return assigned key."""
tmp_file = self._create_tmp()
try:
self._dump_message(message, tmp_file)
finally:
tmp_file.close()
if isinstance(message, MaildirMessage):
subdir = message.get_subdir()
suffix = self.colon + message.get_info()
if suffix == self.colon:
suffix = ''
else:
subdir = 'new'
suffix = ''
uniq = os.path.basename(tmp_file.name).split(self.colon)[0]
dest = os.path.join(self._path, subdir, uniq + suffix)
os.rename(tmp_file.name, dest)
if isinstance(message, MaildirMessage):
os.utime(dest, (os.path.getatime(dest), message.get_date()))
return uniq
def remove(self, key):
"""Remove the keyed message; raise KeyError if it doesn't exist."""
os.remove(os.path.join(self._path, self._lookup(key)))
def discard(self, key):
"""If the keyed message exists, remove it."""
# This overrides an inapplicable implementation in the superclass.
try:
self.remove(key)
except KeyError:
pass
except OSError, e:
if e.errno == errno.ENOENT:
pass
else:
raise
def __setitem__(self, key, message):
"""Replace the keyed message; raise KeyError if it doesn't exist."""
old_subpath = self._lookup(key)
temp_key = self.add(message)
temp_subpath = self._lookup(temp_key)
if isinstance(message, MaildirMessage):
# temp's subdir and suffix were specified by message.
dominant_subpath = temp_subpath
else:
# temp's subdir and suffix were defaults from add().
dominant_subpath = old_subpath
subdir = os.path.dirname(dominant_subpath)
if self.colon in dominant_subpath:
suffix = self.colon + dominant_subpath.split(self.colon)[-1]
else:
suffix = ''
self.discard(key)
new_path = os.path.join(self._path, subdir, key + suffix)
os.rename(os.path.join(self._path, temp_subpath), new_path)
if isinstance(message, MaildirMessage):
os.utime(new_path, (os.path.getatime(new_path),
message.get_date()))
def get_message(self, key):
"""Return a Message representation or raise a KeyError."""
subpath = self._lookup(key)
f = file(os.path.join(self._path, subpath), 'r')
try:
msg = MaildirMessage(f)
finally:
f.close()
subdir, name = os.path.split(subpath)
msg.set_subdir(subdir)
if self.colon in name:
msg.set_info(name.split(self.colon)[-1])
msg.set_date(os.path.getmtime(os.path.join(self._path, subpath)))
return msg
def get_string(self, key):
"""Return a string representation or raise a KeyError."""
f = file(os.path.join(self._path, self._lookup(key)), 'r')
try:
return f.read()
finally:
f.close()
def get_file(self, key):
"""Return a file-like representation or raise a KeyError."""
f = file(os.path.join(self._path, self._lookup(key)), 'rb')
return _ProxyFile(f)
def iterkeys(self):
"""Return an iterator over keys."""
self._refresh()
for key in self._toc:
try:
self._lookup(key)
except KeyError:
continue
yield key
def has_key(self, key):
"""Return True if the keyed message exists, False otherwise."""
self._refresh()
return key in self._toc
def __len__(self):
"""Return a count of messages in the mailbox."""
self._refresh()
return len(self._toc)
def flush(self):
"""Write any pending changes to disk."""
return # Maildir changes are always written immediately.
def lock(self):
"""Lock the mailbox."""
return
def unlock(self):
"""Unlock the mailbox if it is locked."""
return
def close(self):
"""Flush and close the mailbox."""
return
def list_folders(self):
"""Return a list of folder names."""
result = []
for entry in os.listdir(self._path):
if len(entry) > 1 and entry[0] == '.' and \
os.path.isdir(os.path.join(self._path, entry)):
result.append(entry[1:])
return result
def get_folder(self, folder):
"""Return a Maildir instance for the named folder."""
return Maildir(os.path.join(self._path, '.' + folder), create=False)
def add_folder(self, folder):
"""Create a folder and return a Maildir instance representing it."""
path = os.path.join(self._path, '.' + folder)
result = Maildir(path)
maildirfolder_path = os.path.join(path, 'maildirfolder')
if not os.path.exists(maildirfolder_path):
os.close(os.open(maildirfolder_path, os.O_CREAT | os.O_WRONLY))
return result
def remove_folder(self, folder):
"""Delete the named folder, which must be empty."""
path = os.path.join(self._path, '.' + folder)
for entry in os.listdir(os.path.join(path, 'new')) + \
os.listdir(os.path.join(path, 'cur')):
if len(entry) < 1 or entry[0] != '.':
raise NotEmptyError('Folder contains message(s): %s' % folder)
for entry in os.listdir(path):
if entry != 'new' and entry != 'cur' and entry != 'tmp' and \
os.path.isdir(os.path.join(path, entry)):
raise NotEmptyError("Folder contains subdirectory '%s': %s" %
(folder, entry))
for root, dirs, files in os.walk(path, topdown=False):
for entry in files:
os.remove(os.path.join(root, entry))
for entry in dirs:
os.rmdir(os.path.join(root, entry))
os.rmdir(path)
def clean(self):
"""Delete old files in "tmp"."""
now = time.time()
for entry in os.listdir(os.path.join(self._path, 'tmp')):
path = os.path.join(self._path, 'tmp', entry)
if now - os.path.getatime(path) > 129600: # 60 * 60 * 36
os.remove(path)
_count = 1 # This is used to generate unique file names.
def _create_tmp(self):
"""Create a file in the tmp subdirectory and open and return it."""
now = time.time()
hostname = socket.gethostname()
if '/' in hostname:
hostname = hostname.replace('/', r'\057')
if ':' in hostname:
hostname = hostname.replace(':', r'\072')
uniq = "%s.M%sP%sQ%s.%s" % (int(now), int(now % 1 * 1e6), os.getpid(),
Maildir._count, hostname)
path = os.path.join(self._path, 'tmp', uniq)
try:
os.stat(path)
except OSError, e:
if e.errno == errno.ENOENT:
Maildir._count += 1
return file(path, 'wb+')
else:
raise
else:
raise ExternalClashError('Name clash prevented file creation: %s' %
path)
def _refresh(self):
"""Update table of contents mapping."""
self._toc = {}
for subdir in ('new', 'cur'):
for entry in os.listdir(os.path.join(self._path, subdir)):
uniq = entry.split(self.colon)[0]
self._toc[uniq] = os.path.join(subdir, entry)
def _lookup(self, key):
"""Use TOC to return subpath for given key, or raise a KeyError."""
try:
if os.path.exists(os.path.join(self._path, self._toc[key])):
return self._toc[key]
except KeyError:
pass
self._refresh()
try:
return self._toc[key]
except KeyError:
raise KeyError('No message with key: %s' % key)
# This method is for backward compatibility only.
def next(self):
while 1:
self.fp.seek(self.seekp)
"""Return the next message in a one-time iteration."""
if not hasattr(self, '_onetime_keys'):
self._onetime_keys = self.iterkeys()
while True:
try:
self._search_start()
except EOFError:
self.seekp = self.fp.tell()
return self[self._onetime_keys.next()]
except StopIteration:
return None
start = self.fp.tell()
self._search_end()
self.seekp = stop = self.fp.tell()
if start != stop:
except KeyError:
continue
class _singlefileMailbox(Mailbox):
"""A single-file mailbox."""
def __init__(self, path, factory=None, create=True):
"""Initialize a single-file mailbox."""
Mailbox.__init__(self, path, factory, create)
try:
f = file(self._path, 'rb+')
except IOError, e:
if e.errno == errno.ENOENT:
if create:
f = file(self._path, 'wb+')
else:
raise NoSuchMailboxError(self._path)
elif e.errno == errno.EACCES:
f = file(self._path, 'rb')
else:
raise
self._file = f
self._toc = None
self._next_key = 0
self._pending = False # No changes require rewriting the file.
self._locked = False
def add(self, message):
"""Add message and return assigned key."""
self._lookup()
self._toc[self._next_key] = self._append_message(message)
self._next_key += 1
self._pending = True
return self._next_key - 1
def remove(self, key):
"""Remove the keyed message; raise KeyError if it doesn't exist."""
self._lookup(key)
del self._toc[key]
self._pending = True
def __setitem__(self, key, message):
"""Replace the keyed message; raise KeyError if it doesn't exist."""
self._lookup(key)
self._toc[key] = self._append_message(message)
self._pending = True
def iterkeys(self):
"""Return an iterator over keys."""
self._lookup()
for key in self._toc.keys():
yield key
def has_key(self, key):
"""Return True if the keyed message exists, False otherwise."""
self._lookup()
return key in self._toc
def __len__(self):
"""Return a count of messages in the mailbox."""
self._lookup()
return len(self._toc)
def lock(self):
"""Lock the mailbox."""
if not self._locked:
_lock_file(self._file)
self._locked = True
def unlock(self):
"""Unlock the mailbox if it is locked."""
if self._locked:
_unlock_file(self._file)
self._locked = False
def flush(self):
"""Write any pending changes to disk."""
if not self._pending:
return
self._lookup()
new_file = _create_temporary(self._path)
try:
new_toc = {}
self._pre_mailbox_hook(new_file)
for key in sorted(self._toc.keys()):
start, stop = self._toc[key]
self._file.seek(start)
self._pre_message_hook(new_file)
new_start = new_file.tell()
while True:
buffer = self._file.read(min(4096,
stop - self._file.tell()))
if buffer == '':
break
return self.factory(_Subfile(self.fp, start, stop))
new_file.write(buffer)
new_toc[key] = (new_start, new_file.tell())
self._post_message_hook(new_file)
except:
new_file.close()
os.remove(new_file.name)
raise
new_file.close()
self._file.close()
try:
os.rename(new_file.name, self._path)
except OSError, e:
if e.errno == errno.EEXIST:
os.remove(self._path)
os.rename(new_file.name, self._path)
else:
raise
self._file = file(self._path, 'rb+')
self._toc = new_toc
self._pending = False
if self._locked:
_lock_file(new_file, dotlock=False)
def _pre_mailbox_hook(self, f):
"""Called before writing the mailbox to file f."""
return
class _Subfile:
def _pre_message_hook(self, f):
"""Called before writing each message to file f."""
return
def __init__(self, fp, start, stop):
self.fp = fp
self.start = start
self.stop = stop
self.pos = self.start
def _post_message_hook(self, f):
"""Called after writing each message to file f."""
return
def close(self):
"""Flush and close the mailbox."""
self.flush()
if self._locked:
self.unlock()
self._file.close()
def _read(self, length, read_function):
if self.pos >= self.stop:
return ''
remaining = self.stop - self.pos
if length is None or length < 0 or length > remaining:
length = remaining
self.fp.seek(self.pos)
data = read_function(length)
self.pos = self.fp.tell()
return data
def read(self, length = None):
return self._read(length, self.fp.read)
def readline(self, length = None):
return self._read(length, self.fp.readline)
def readlines(self, sizehint = -1):
lines = []
while 1:
line = self.readline()
if not line:
def _lookup(self, key=None):
"""Return (start, stop) or raise KeyError."""
if self._toc is None:
self._generate_toc()
if key is not None:
try:
return self._toc[key]
except KeyError:
raise KeyError('No message with key: %s' % key)
def _append_message(self, message):
"""Append message to mailbox and return (start, stop) offsets."""
self._file.seek(0, 2)
self._pre_message_hook(self._file)
offsets = self._install_message(message)
self._post_message_hook(self._file)
self._file.flush()
return offsets
class _mboxMMDF(_singlefileMailbox):
"""An mbox or MMDF mailbox."""
_mangle_from_ = True
def get_message(self, key):
"""Return a Message representation or raise a KeyError."""
start, stop = self._lookup(key)
self._file.seek(start)
from_line = self._file.readline().replace(os.linesep, '')
string = self._file.read(stop - self._file.tell())
msg = self._message_factory(string.replace(os.linesep, '\n'))
msg.set_from(from_line[5:])
return msg
def get_string(self, key, from_=False):
"""Return a string representation or raise a KeyError."""
start, stop = self._lookup(key)
self._file.seek(start)
if not from_:
self._file.readline()
string = self._file.read(stop - self._file.tell())
return string.replace(os.linesep, '\n')
def get_file(self, key, from_=False):
"""Return a file-like representation or raise a KeyError."""
start, stop = self._lookup(key)
self._file.seek(start)
if not from_:
self._file.readline()
return _PartialFile(self._file, self._file.tell(), stop)
def _install_message(self, message):
"""Format a message and blindly write to self._file."""
from_line = None
if isinstance(message, str) and message.startswith('From '):
newline = message.find('\n')
if newline != -1:
from_line = message[:newline]
message = message[newline + 1:]
else:
from_line = message
message = ''
elif isinstance(message, _mboxMMDFMessage):
from_line = 'From ' + message.get_from()
elif isinstance(message, email.Message.Message):
from_line = message.get_unixfrom() # May be None.
if from_line is None:
from_line = 'From MAILER-DAEMON %s' % time.asctime(time.gmtime())
start = self._file.tell()
self._file.write(from_line + os.linesep)
self._dump_message(message, self._file, self._mangle_from_)
stop = self._file.tell()
return (start, stop)
class mbox(_mboxMMDF):
"""A classic mbox mailbox."""
_mangle_from_ = True
def __init__(self, path, factory=None, create=True):
"""Initialize an mbox mailbox."""
self._message_factory = mboxMessage
_mboxMMDF.__init__(self, path, factory, create)
def _pre_message_hook(self, f):
"""Called before writing each message to file f."""
if f.tell() != 0:
f.write(os.linesep)
def _generate_toc(self):
"""Generate key-to-(start, stop) table of contents."""
starts, stops = [], []
self._file.seek(0)
while True:
line_pos = self._file.tell()
line = self._file.readline()
if line.startswith('From '):
if len(stops) < len(starts):
stops.append(line_pos - len(os.linesep))
starts.append(line_pos)
elif line == '':
stops.append(line_pos)
break
self._toc = dict(enumerate(zip(starts, stops)))
self._next_key = len(self._toc)
class MMDF(_mboxMMDF):
"""An MMDF mailbox."""
def __init__(self, path, factory=None, create=True):
"""Initialize an MMDF mailbox."""
self._message_factory = MMDFMessage
_mboxMMDF.__init__(self, path, factory, create)
def _pre_message_hook(self, f):
"""Called before writing each message to file f."""
f.write('\001\001\001\001' + os.linesep)
def _post_message_hook(self, f):
"""Called after writing each message to file f."""
f.write(os.linesep + '\001\001\001\001' + os.linesep)
def _generate_toc(self):
"""Generate key-to-(start, stop) table of contents."""
starts, stops = [], []
self._file.seek(0)
next_pos = 0
while True:
line_pos = next_pos
line = self._file.readline()
next_pos = self._file.tell()
if line.startswith('\001\001\001\001' + os.linesep):
starts.append(next_pos)
while True:
line_pos = next_pos
line = self._file.readline()
next_pos = self._file.tell()
if line == '\001\001\001\001' + os.linesep:
stops.append(line_pos - len(os.linesep))
break
elif line == '':
stops.append(line_pos)
break
lines.append(line)
if sizehint >= 0:
sizehint = sizehint - len(line)
elif line == '':
break
self._toc = dict(enumerate(zip(starts, stops)))
self._next_key = len(self._toc)
class MH(Mailbox):
"""An MH mailbox."""
def __init__(self, path, factory=None, create=True):
"""Initialize an MH instance."""
Mailbox.__init__(self, path, factory, create)
if not os.path.exists(self._path):
if create:
os.mkdir(self._path, 0700)
os.close(os.open(os.path.join(self._path, '.mh_sequences'),
os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0600))
else:
raise NoSuchMailboxError(self._path)
self._locked = False
def add(self, message):
"""Add message and return assigned key."""
keys = self.keys()
if len(keys) == 0:
new_key = 1
else:
new_key = max(keys) + 1
new_path = os.path.join(self._path, str(new_key))
f = _create_carefully(new_path)
try:
if self._locked:
_lock_file(f)
try:
self._dump_message(message, f)
if isinstance(message, MHMessage):
self._dump_sequences(message, new_key)
finally:
if self._locked:
_unlock_file(f)
finally:
f.close()
return new_key
def remove(self, key):
"""Remove the keyed message; raise KeyError if it doesn't exist."""
path = os.path.join(self._path, str(key))
try:
f = file(path, 'rb+')
except IOError, e:
if e.errno == errno.ENOENT:
raise KeyError('No message with key: %s' % key)
else:
raise
try:
if self._locked:
_lock_file(f)
try:
f.close()
os.remove(os.path.join(self._path, str(key)))
finally:
if self._locked:
_unlock_file(f)
finally:
f.close()
def __setitem__(self, key, message):
"""Replace the keyed message; raise KeyError if it doesn't exist."""
path = os.path.join(self._path, str(key))
try:
f = file(path, 'rb+')
except IOError, e:
if e.errno == errno.ENOENT:
raise KeyError('No message with key: %s' % key)
else:
raise
try:
if self._locked:
_lock_file(f)
try:
os.close(os.open(path, os.O_WRONLY | os.O_TRUNC))
self._dump_message(message, f)
if isinstance(message, MHMessage):
self._dump_sequences(message, key)
finally:
if self._locked:
_unlock_file(f)
finally:
f.close()
def get_message(self, key):
"""Return a Message representation or raise a KeyError."""
try:
if self._locked:
f = file(os.path.join(self._path, str(key)), 'r+')
else:
f = file(os.path.join(self._path, str(key)), 'r')
except IOError, e:
if e.errno == errno.ENOENT:
raise KeyError('No message with key: %s' % key)
else:
raise
try:
if self._locked:
_lock_file(f)
try:
msg = MHMessage(f)
finally:
if self._locked:
_unlock_file(f)
finally:
f.close()
for name, key_list in self.get_sequences():
if key in key_list:
msg.add_sequence(name)
return msg
def get_string(self, key):
"""Return a string representation or raise a KeyError."""
try:
if self._locked:
f = file(os.path.join(self._path, str(key)), 'r+')
else:
f = file(os.path.join(self._path, str(key)), 'r')
except IOError, e:
if e.errno == errno.ENOENT:
raise KeyError('No message with key: %s' % key)
else:
raise
try:
if self._locked:
_lock_file(f)
try:
return f.read()
finally:
if self._locked:
_unlock_file(f)
finally:
f.close()
def get_file(self, key):
"""Return a file-like representation or raise a KeyError."""
try:
f = file(os.path.join(self._path, str(key)), 'rb')
except IOError, e:
if e.errno == errno.ENOENT:
raise KeyError('No message with key: %s' % key)
else:
raise
return _ProxyFile(f)
def iterkeys(self):
"""Return an iterator over keys."""
return iter(sorted(int(entry) for entry in os.listdir(self._path)
if entry.isdigit()))
def has_key(self, key):
"""Return True if the keyed message exists, False otherwise."""
return os.path.exists(os.path.join(self._path, str(key)))
def __len__(self):
"""Return a count of messages in the mailbox."""
return len(list(self.iterkeys()))
def lock(self):
"""Lock the mailbox."""
if not self._locked:
self._file = file(os.path.join(self._path, '.mh_sequences'), 'rb+')
_lock_file(self._file)
self._locked = True
def unlock(self):
"""Unlock the mailbox if it is locked."""
if self._locked:
_unlock_file(self._file)
self._file.close()
del self._file
self._locked = False
def flush(self):
"""Write any pending changes to the disk."""
return
def close(self):
"""Flush and close the mailbox."""
if self._locked:
self.unlock()
def list_folders(self):
"""Return a list of folder names."""
result = []
for entry in os.listdir(self._path):
if os.path.isdir(os.path.join(self._path, entry)):
result.append(entry)
return result
def get_folder(self, folder):
"""Return an MH instance for the named folder."""
return MH(os.path.join(self._path, folder), create=False)
def add_folder(self, folder):
"""Create a folder and return an MH instance representing it."""
return MH(os.path.join(self._path, folder))
def remove_folder(self, folder):
"""Delete the named folder, which must be empty."""
path = os.path.join(self._path, folder)
entries = os.listdir(path)
if entries == ['.mh_sequences']:
os.remove(os.path.join(path, '.mh_sequences'))
elif entries == []:
pass
else:
raise NotEmptyError('Folder not empty: %s' % self._path)
os.rmdir(path)
def get_sequences(self):
"""Return a name-to-key-list dictionary to define each sequence."""
results = {}
f = file(os.path.join(self._path, '.mh_sequences'), 'r')
try:
all_keys = set(self.keys())
for line in f:
try:
name, contents = line.split(':')
keys = set()
for spec in contents.split():
if spec.isdigit():
keys.add(int(spec))
else:
start, stop = (int(x) for x in spec.split('-'))
keys.update(range(start, stop + 1))
results[name] = [key for key in sorted(keys) \
if key in all_keys]
if len(results[name]) == 0:
del results[name]
except ValueError:
raise FormatError('Invalid sequence specification: %s' %
line.rstrip())
finally:
f.close()
return results
def set_sequences(self, sequences):
"""Set sequences using the given name-to-key-list dictionary."""
f = file(os.path.join(self._path, '.mh_sequences'), 'r+')
try:
os.close(os.open(f.name, os.O_WRONLY | os.O_TRUNC))
for name, keys in sequences.iteritems():
if len(keys) == 0:
continue
f.write('%s:' % name)
prev = None
completing = False
for key in sorted(set(keys)):
if key - 1 == prev:
if not completing:
completing = True
f.write('-')
elif completing:
completing = False
f.write('%s %s' % (prev, key))
else:
f.write(' %s' % key)
prev = key
if completing:
f.write(str(prev) + '\n')
else:
f.write('\n')
finally:
f.close()
def pack(self):
"""Re-name messages to eliminate numbering gaps. Invalidates keys."""
sequences = self.get_sequences()
prev = 0
changes = []
for key in self.iterkeys():
if key - 1 != prev:
changes.append((key, prev + 1))
f = file(os.path.join(self._path, str(key)), 'r+')
try:
if self._locked:
_lock_file(f)
try:
if hasattr(os, 'link'):
os.link(os.path.join(self._path, str(key)),
os.path.join(self._path, str(prev + 1)))
os.unlink(os.path.join(self._path, str(key)))
else:
f.close()
os.rename(os.path.join(self._path, str(key)),
os.path.join(self._path, str(prev + 1)))
finally:
if self._locked:
_unlock_file(f)
finally:
f.close()
prev += 1
self._next_key = prev + 1
if len(changes) == 0:
return
for name, key_list in sequences.items():
for old, new in changes:
if old in key_list:
key_list[key_list.index(old)] = new
self.set_sequences(sequences)
def _dump_sequences(self, message, key):
"""Inspect a new MHMessage and update sequences appropriately."""
pending_sequences = message.get_sequences()
all_sequences = self.get_sequences()
for name, key_list in all_sequences.iteritems():
if name in pending_sequences:
key_list.append(key)
elif key in key_list:
del key_list[key_list.index(key)]
for sequence in pending_sequences:
if sequence not in all_sequences:
all_sequences[sequence] = [key]
self.set_sequences(all_sequences)
class Babyl(_singlefileMailbox):
"""An Rmail-style Babyl mailbox."""
_special_labels = frozenset(('unseen', 'deleted', 'filed', 'answered',
'forwarded', 'edited', 'resent'))
def __init__(self, path, factory=None, create=True):
"""Initialize a Babyl mailbox."""
_singlefileMailbox.__init__(self, path, factory, create)
self._labels = {}
def add(self, message):
"""Add message and return assigned key."""
key = _singlefileMailbox.add(self, message)
if isinstance(message, BabylMessage):
self._labels[key] = message.get_labels()
return key
def remove(self, key):
"""Remove the keyed message; raise KeyError if it doesn't exist."""
_singlefileMailbox.remove(self, key)
if key in self._labels:
del self._labels[key]
def __setitem__(self, key, message):
"""Replace the keyed message; raise KeyError if it doesn't exist."""
_singlefileMailbox.__setitem__(self, key, message)
if isinstance(message, BabylMessage):
self._labels[key] = message.get_labels()
def get_message(self, key):
"""Return a Message representation or raise a KeyError."""
start, stop = self._lookup(key)
self._file.seek(start)
self._file.readline() # Skip '1,' line specifying labels.
original_headers = StringIO.StringIO()
while True:
line = self._file.readline()
if line == '*** EOOH ***' + os.linesep or line == '':
break
original_headers.write(line.replace(os.linesep, '\n'))
visible_headers = StringIO.StringIO()
while True:
line = self._file.readline()
if line == os.linesep or line == '':
break
visible_headers.write(line.replace(os.linesep, '\n'))
body = self._file.read(stop - self._file.tell()).replace(os.linesep,
'\n')
msg = BabylMessage(original_headers.getvalue() + body)
msg.set_visible(visible_headers.getvalue())
if key in self._labels:
msg.set_labels(self._labels[key])
return msg
def get_string(self, key):
"""Return a string representation or raise a KeyError."""
start, stop = self._lookup(key)
self._file.seek(start)
self._file.readline() # Skip '1,' line specifying labels.
original_headers = StringIO.StringIO()
while True:
line = self._file.readline()
if line == '*** EOOH ***' + os.linesep or line == '':
break
original_headers.write(line.replace(os.linesep, '\n'))
while True:
line = self._file.readline()
if line == os.linesep or line == '':
break
return original_headers.getvalue() + \
self._file.read(stop - self._file.tell()).replace(os.linesep,
'\n')
def get_file(self, key):
"""Return a file-like representation or raise a KeyError."""
return StringIO.StringIO(self.get_string(key).replace('\n',
os.linesep))
def get_labels(self):
"""Return a list of user-defined labels in the mailbox."""
self._lookup()
labels = set()
for label_list in self._labels.values():
labels.update(label_list)
labels.difference_update(self._special_labels)
return list(labels)
def _generate_toc(self):
"""Generate key-to-(start, stop) table of contents."""
starts, stops = [], []
self._file.seek(0)
next_pos = 0
label_lists = []
while True:
line_pos = next_pos
line = self._file.readline()
next_pos = self._file.tell()
if line == '\037\014' + os.linesep:
if len(stops) < len(starts):
stops.append(line_pos - len(os.linesep))
starts.append(next_pos)
labels = [label.strip() for label
in self._file.readline()[1:].split(',')
if label.strip() != '']
label_lists.append(labels)
elif line == '\037' or line == '\037' + os.linesep:
if len(stops) < len(starts):
stops.append(line_pos - len(os.linesep))
elif line == '':
stops.append(line_pos - len(os.linesep))
break
self._toc = dict(enumerate(zip(starts, stops)))
self._labels = dict(enumerate(label_lists))
self._next_key = len(self._toc)
def _pre_mailbox_hook(self, f):
"""Called before writing the mailbox to file f."""
f.write('BABYL OPTIONS:%sVersion: 5%sLabels:%s%s\037' %
(os.linesep, os.linesep, ','.join(self.get_labels()),
os.linesep))
def _pre_message_hook(self, f):
"""Called before writing each message to file f."""
f.write('\014' + os.linesep)
def _post_message_hook(self, f):
"""Called after writing each message to file f."""
f.write(os.linesep + '\037')
def _install_message(self, message):
"""Write message contents and return (start, stop)."""
start = self._file.tell()
if isinstance(message, BabylMessage):
special_labels = []
labels = []
for label in message.get_labels():
if label in self._special_labels:
special_labels.append(label)
else:
labels.append(label)
self._file.write('1')
for label in special_labels:
self._file.write(', ' + label)
self._file.write(',,')
for label in labels:
self._file.write(' ' + label + ',')
self._file.write(os.linesep)
else:
self._file.write('1,,' + os.linesep)
if isinstance(message, email.Message.Message):
orig_buffer = StringIO.StringIO()
orig_generator = email.Generator.Generator(orig_buffer, False, 0)
orig_generator.flatten(message)
orig_buffer.seek(0)
while True:
line = orig_buffer.readline()
self._file.write(line.replace('\n', os.linesep))
if line == '\n' or line == '':
break
self._file.write('*** EOOH ***' + os.linesep)
if isinstance(message, BabylMessage):
vis_buffer = StringIO.StringIO()
vis_generator = email.Generator.Generator(vis_buffer, False, 0)
vis_generator.flatten(message.get_visible())
while True:
line = vis_buffer.readline()
self._file.write(line.replace('\n', os.linesep))
if line == '\n' or line == '':
break
else:
orig_buffer.seek(0)
while True:
line = orig_buffer.readline()
self._file.write(line.replace('\n', os.linesep))
if line == '\n' or line == '':
break
while True:
buffer = orig_buffer.read(4096) # Buffer size is arbitrary.
if buffer == '':
break
self._file.write(buffer.replace('\n', os.linesep))
elif isinstance(message, str):
body_start = message.find('\n\n') + 2
if body_start - 2 != -1:
self._file.write(message[:body_start].replace('\n',
os.linesep))
self._file.write('*** EOOH ***' + os.linesep)
self._file.write(message[:body_start].replace('\n',
os.linesep))
self._file.write(message[body_start:].replace('\n',
os.linesep))
else:
self._file.write('*** EOOH ***' + os.linesep + os.linesep)
self._file.write(message.replace('\n', os.linesep))
elif hasattr(message, 'readline'):
original_pos = message.tell()
first_pass = True
while True:
line = message.readline()
self._file.write(line.replace('\n', os.linesep))
if line == '\n' or line == '':
self._file.write('*** EOOH ***' + os.linesep)
if first_pass:
first_pass = False
message.seek(original_pos)
else:
break
while True:
buffer = message.read(4096) # Buffer size is arbitrary.
if buffer == '':
break
self._file.write(buffer.replace('\n', os.linesep))
else:
raise TypeError('Invalid message type: %s' % type(message))
stop = self._file.tell()
return (start, stop)
class Message(email.Message.Message):
"""Message with mailbox-format-specific properties."""
def __init__(self, message=None):
"""Initialize a Message instance."""
if isinstance(message, email.Message.Message):
self._become_message(copy.deepcopy(message))
if isinstance(message, Message):
message._explain_to(self)
elif isinstance(message, str):
self._become_message(email.message_from_string(message))
elif hasattr(message, "read"):
self._become_message(email.message_from_file(message))
elif message is None:
email.Message.Message.__init__(self)
else:
raise TypeError('Invalid message type: %s' % type(message))
def _become_message(self, message):
"""Assume the non-format-specific state of message."""
for name in ('_headers', '_unixfrom', '_payload', '_charset',
'preamble', 'epilogue', 'defects', '_default_type'):
self.__dict__[name] = message.__dict__[name]
def _explain_to(self, message):
"""Copy format-specific state to message insofar as possible."""
if isinstance(message, Message):
return # There's nothing format-specific to explain.
else:
raise TypeError('Cannot convert to specified type')
class MaildirMessage(Message):
"""Message with Maildir-specific properties."""
def __init__(self, message=None):
"""Initialize a MaildirMessage instance."""
self._subdir = 'new'
self._info = ''
self._date = time.time()
Message.__init__(self, message)
def get_subdir(self):
"""Return 'new' or 'cur'."""
return self._subdir
def set_subdir(self, subdir):
"""Set subdir to 'new' or 'cur'."""
if subdir == 'new' or subdir == 'cur':
self._subdir = subdir
else:
raise ValueError("subdir must be 'new' or 'cur': %s" % subdir)
def get_flags(self):
"""Return as a string the flags that are set."""
if self._info.startswith('2,'):
return self._info[2:]
else:
return ''
def set_flags(self, flags):
"""Set the given flags and unset all others."""
self._info = '2,' + ''.join(sorted(flags))
def add_flag(self, flag):
"""Set the given flag(s) without changing others."""
self.set_flags(''.join(set(self.get_flags()) | set(flag)))
def remove_flag(self, flag):
"""Unset the given string flag(s) without changing others."""
if self.get_flags() != '':
self.set_flags(''.join(set(self.get_flags()) - set(flag)))
def get_date(self):
"""Return delivery date of message, in seconds since the epoch."""
return self._date
def set_date(self, date):
"""Set delivery date of message, in seconds since the epoch."""
try:
self._date = float(date)
except ValueError:
raise TypeError("can't convert to float: %s" % date)
def get_info(self):
"""Get the message's "info" as a string."""
return self._info
def set_info(self, info):
"""Set the message's "info" string."""
if isinstance(info, str):
self._info = info
else:
raise TypeError('info must be a string: %s' % type(info))
def _explain_to(self, message):
"""Copy Maildir-specific state to message insofar as possible."""
if isinstance(message, MaildirMessage):
message.set_flags(self.get_flags())
message.set_subdir(self.get_subdir())
message.set_date(self.get_date())
elif isinstance(message, _mboxMMDFMessage):
flags = set(self.get_flags())
if 'S' in flags:
message.add_flag('R')
if self.get_subdir() == 'cur':
message.add_flag('O')
if 'T' in flags:
message.add_flag('D')
if 'F' in flags:
message.add_flag('F')
if 'R' in flags:
message.add_flag('A')
message.set_from('MAILER-DAEMON', time.gmtime(self.get_date()))
elif isinstance(message, MHMessage):
flags = set(self.get_flags())
if 'S' not in flags:
message.add_sequence('unseen')
if 'R' in flags:
message.add_sequence('replied')
if 'F' in flags:
message.add_sequence('flagged')
elif isinstance(message, BabylMessage):
flags = set(self.get_flags())
if 'S' not in flags:
message.add_label('unseen')
if 'T' in flags:
message.add_label('deleted')
if 'R' in flags:
message.add_label('answered')
if 'P' in flags:
message.add_label('forwarded')
elif isinstance(message, Message):
pass
else:
raise TypeError('Cannot convert to specified type: %s' %
type(message))
class _mboxMMDFMessage(Message):
"""Message with mbox- or MMDF-specific properties."""
def __init__(self, message=None):
"""Initialize an mboxMMDFMessage instance."""
self.set_from('MAILER-DAEMON', True)
if isinstance(message, email.Message.Message):
unixfrom = message.get_unixfrom()
if unixfrom is not None and unixfrom.startswith('From '):
self.set_from(unixfrom[5:])
Message.__init__(self, message)
def get_from(self):
"""Return contents of "From " line."""
return self._from
def set_from(self, from_, time_=None):
"""Set "From " line, formatting and appending time_ if specified."""
if time_ is not None:
if time_ is True:
time_ = time.gmtime()
from_ += ' ' + time.asctime(time_)
self._from = from_
def get_flags(self):
"""Return as a string the flags that are set."""
return self.get('Status', '') + self.get('X-Status', '')
def set_flags(self, flags):
"""Set the given flags and unset all others."""
flags = set(flags)
status_flags, xstatus_flags = '', ''
for flag in ('R', 'O'):
if flag in flags:
status_flags += flag
flags.remove(flag)
for flag in ('D', 'F', 'A'):
if flag in flags:
xstatus_flags += flag
flags.remove(flag)
xstatus_flags += ''.join(sorted(flags))
try:
self.replace_header('Status', status_flags)
except KeyError:
self.add_header('Status', status_flags)
try:
self.replace_header('X-Status', xstatus_flags)
except KeyError:
self.add_header('X-Status', xstatus_flags)
def add_flag(self, flag):
"""Set the given flag(s) without changing others."""
self.set_flags(''.join(set(self.get_flags()) | set(flag)))
def remove_flag(self, flag):
"""Unset the given string flag(s) without changing others."""
if 'Status' in self or 'X-Status' in self:
self.set_flags(''.join(set(self.get_flags()) - set(flag)))
def _explain_to(self, message):
"""Copy mbox- or MMDF-specific state to message insofar as possible."""
if isinstance(message, MaildirMessage):
flags = set(self.get_flags())
if 'O' in flags:
message.set_subdir('cur')
if 'F' in flags:
message.add_flag('F')
if 'A' in flags:
message.add_flag('R')
if 'R' in flags:
message.add_flag('S')
if 'D' in flags:
message.add_flag('T')
del message['status']
del message['x-status']
maybe_date = ' '.join(self.get_from().split()[-5:])
try:
message.set_date(calendar.timegm(time.strptime(maybe_date,
'%a %b %d %H:%M:%S %Y')))
except (ValueError, OverflowError):
pass
elif isinstance(message, _mboxMMDFMessage):
message.set_flags(self.get_flags())
message.set_from(self.get_from())
elif isinstance(message, MHMessage):
flags = set(self.get_flags())
if 'R' not in flags:
message.add_sequence('unseen')
if 'A' in flags:
message.add_sequence('replied')
if 'F' in flags:
message.add_sequence('flagged')
del message['status']
del message['x-status']
elif isinstance(message, BabylMessage):
flags = set(self.get_flags())
if 'R' not in flags:
message.add_label('unseen')
if 'D' in flags:
message.add_label('deleted')
if 'A' in flags:
message.add_label('answered')
del message['status']
del message['x-status']
elif isinstance(message, Message):
pass
else:
raise TypeError('Cannot convert to specified type: %s' %
type(message))
class mboxMessage(_mboxMMDFMessage):
"""Message with mbox-specific properties."""
class MHMessage(Message):
"""Message with MH-specific properties."""
def __init__(self, message=None):
"""Initialize an MHMessage instance."""
self._sequences = []
Message.__init__(self, message)
def get_sequences(self):
"""Return a list of sequences that include the message."""
return self._sequences[:]
def set_sequences(self, sequences):
"""Set the list of sequences that include the message."""
self._sequences = list(sequences)
def add_sequence(self, sequence):
"""Add sequence to list of sequences including the message."""
if isinstance(sequence, str):
if not sequence in self._sequences:
self._sequences.append(sequence)
else:
raise TypeError('sequence must be a string: %s' % type(sequence))
def remove_sequence(self, sequence):
"""Remove sequence from the list of sequences including the message."""
try:
self._sequences.remove(sequence)
except ValueError:
pass
def _explain_to(self, message):
"""Copy MH-specific state to message insofar as possible."""
if isinstance(message, MaildirMessage):
sequences = set(self.get_sequences())
if 'unseen' in sequences:
message.set_subdir('cur')
else:
message.set_subdir('cur')
message.add_flag('S')
if 'flagged' in sequences:
message.add_flag('F')
if 'replied' in sequences:
message.add_flag('R')
elif isinstance(message, _mboxMMDFMessage):
sequences = set(self.get_sequences())
if 'unseen' not in sequences:
message.add_flag('RO')
else:
message.add_flag('O')
if 'flagged' in sequences:
message.add_flag('F')
if 'replied' in sequences:
message.add_flag('A')
elif isinstance(message, MHMessage):
for sequence in self.get_sequences():
message.add_sequence(sequence)
elif isinstance(message, BabylMessage):
sequences = set(self.get_sequences())
if 'unseen' in sequences:
message.add_label('unseen')
if 'replied' in sequences:
message.add_label('answered')
elif isinstance(message, Message):
pass
else:
raise TypeError('Cannot convert to specified type: %s' %
type(message))
class BabylMessage(Message):
"""Message with Babyl-specific properties."""
def __init__(self, message=None):
"""Initialize an BabylMessage instance."""
self._labels = []
self._visible = Message()
Message.__init__(self, message)
def get_labels(self):
"""Return a list of labels on the message."""
return self._labels[:]
def set_labels(self, labels):
"""Set the list of labels on the message."""
self._labels = list(labels)
def add_label(self, label):
"""Add label to list of labels on the message."""
if isinstance(label, str):
if label not in self._labels:
self._labels.append(label)
else:
raise TypeError('label must be a string: %s' % type(label))
def remove_label(self, label):
"""Remove label from the list of labels on the message."""
try:
self._labels.remove(label)
except ValueError:
pass
def get_visible(self):
"""Return a Message representation of visible headers."""
return Message(self._visible)
def set_visible(self, visible):
"""Set the Message representation of visible headers."""
self._visible = Message(visible)
def update_visible(self):
"""Update and/or sensibly generate a set of visible headers."""
for header in self._visible.keys():
if header in self:
self._visible.replace_header(header, self[header])
else:
del self._visible[header]
for header in ('Date', 'From', 'Reply-To', 'To', 'CC', 'Subject'):
if header in self and header not in self._visible:
self._visible[header] = self[header]
def _explain_to(self, message):
"""Copy Babyl-specific state to message insofar as possible."""
if isinstance(message, MaildirMessage):
labels = set(self.get_labels())
if 'unseen' in labels:
message.set_subdir('cur')
else:
message.set_subdir('cur')
message.add_flag('S')
if 'forwarded' in labels or 'resent' in labels:
message.add_flag('P')
if 'answered' in labels:
message.add_flag('R')
if 'deleted' in labels:
message.add_flag('T')
elif isinstance(message, _mboxMMDFMessage):
labels = set(self.get_labels())
if 'unseen' not in labels:
message.add_flag('RO')
else:
message.add_flag('O')
if 'deleted' in labels:
message.add_flag('D')
if 'answered' in labels:
message.add_flag('A')
elif isinstance(message, MHMessage):
labels = set(self.get_labels())
if 'unseen' in labels:
message.add_sequence('unseen')
if 'answered' in labels:
message.add_sequence('replied')
elif isinstance(message, BabylMessage):
message.set_visible(self.get_visible())
for label in self.get_labels():
message.add_label(label)
elif isinstance(message, Message):
pass
else:
raise TypeError('Cannot convert to specified type: %s' %
type(message))
class MMDFMessage(_mboxMMDFMessage):
"""Message with MMDF-specific properties."""
class _ProxyFile:
"""A read-only wrapper of a file."""
def __init__(self, f, pos=None):
"""Initialize a _ProxyFile."""
self._file = f
if pos is None:
self._pos = f.tell()
else:
self._pos = pos
def read(self, size=None):
"""Read bytes."""
return self._read(size, self._file.read)
def readline(self, size=None):
"""Read a line."""
return self._read(size, self._file.readline)
def readlines(self, sizehint=None):
"""Read multiple lines."""
result = []
for line in self:
result.append(line)
if sizehint is not None:
sizehint -= len(line)
if sizehint <= 0:
break
return lines
return result
def __iter__(self):
"""Iterate over lines."""
return iter(self.readline, "")
def tell(self):
return self.pos - self.start
"""Return the position."""
return self._pos
def seek(self, pos, whence=0):
def seek(self, offset, whence=0):
"""Change position."""
if whence == 1:
self._file.seek(self._pos)
self._file.seek(offset, whence)
self._pos = self._file.tell()
def close(self):
"""Close the file."""
del self._file
def _read(self, size, read_method):
"""Read size bytes using read_method."""
if size is None:
size = -1
self._file.seek(self._pos)
result = read_method(size)
self._pos = self._file.tell()
return result
class _PartialFile(_ProxyFile):
"""A read-only wrapper of part of a file."""
def __init__(self, f, start=None, stop=None):
"""Initialize a _PartialFile."""
_ProxyFile.__init__(self, f, start)
self._start = start
self._stop = stop
def tell(self):
"""Return the position with respect to start."""
return _ProxyFile.tell(self) - self._start
def seek(self, offset, whence=0):
"""Change position, possibly with respect to start or stop."""
if whence == 0:
self.pos = self.start + pos
elif whence == 1:
self.pos = self.pos + pos
self._pos = self._start
whence = 1
elif whence == 2:
self.pos = self.stop + pos
self._pos = self._stop
whence = 1
_ProxyFile.seek(self, offset, whence)
def close(self):
del self.fp
def _read(self, size, read_method):
"""Read size bytes using read_method, honoring start and stop."""
remaining = self._stop - self._pos
if remaining <= 0:
return ''
if size is None or size < 0 or size > remaining:
size = remaining
return _ProxyFile._read(self, size, read_method)
def _lock_file(f, dotlock=True):
"""Lock file f using lockf, flock, and dot locking."""
dotlock_done = False
try:
if fcntl:
try:
fcntl.lockf(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
except IOError, e:
if e.errno == errno.EAGAIN:
raise ExternalClashError('lockf: lock unavailable: %s' %
f.name)
else:
raise
try:
fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
except IOError, e:
if e.errno == errno.EWOULDBLOCK:
raise ExternalClashError('flock: lock unavailable: %s' %
f.name)
else:
raise
if dotlock:
try:
pre_lock = _create_temporary(f.name + '.lock')
pre_lock.close()
except IOError, e:
if e.errno == errno.EACCES:
return # Without write access, just skip dotlocking.
else:
raise
try:
if hasattr(os, 'link'):
os.link(pre_lock.name, f.name + '.lock')
dotlock_done = True
os.unlink(pre_lock.name)
else:
os.rename(pre_lock.name, f.name + '.lock')
dotlock_done = True
except OSError, e:
if e.errno == errno.EEXIST:
os.remove(pre_lock.name)
raise ExternalClashError('dot lock unavailable: %s' %
f.name)
else:
raise
except:
if fcntl:
fcntl.lockf(f, fcntl.LOCK_UN)
fcntl.flock(f, fcntl.LOCK_UN)
if dotlock_done:
os.remove(f.name + '.lock')
raise
def _unlock_file(f):
"""Unlock file f using lockf, flock, and dot locking."""
if fcntl:
fcntl.lockf(f, fcntl.LOCK_UN)
fcntl.flock(f, fcntl.LOCK_UN)
if os.path.exists(f.name + '.lock'):
os.remove(f.name + '.lock')
def _create_carefully(path):
"""Create a file if it doesn't exist and open for reading and writing."""
fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDWR)
try:
return file(path, 'rb+')
finally:
os.close(fd)
def _create_temporary(path):
"""Create a temp file based on path and open for reading and writing."""
return _create_carefully('%s.%s.%s.%s' % (path, int(time.time()),
socket.gethostname(),
os.getpid()))
## Start: classes from the original module (for backward compatibility).
# Note that the Maildir class, whose name is unchanged, itself offers a next()
# method for backward compatibility.
class _Mailbox:
def __init__(self, fp, factory=rfc822.Message):
self.fp = fp
self.seekp = 0
self.factory = factory
def __iter__(self):
return iter(self.next, None)
def next(self):
while 1:
self.fp.seek(self.seekp)
try:
self._search_start()
except EOFError:
self.seekp = self.fp.tell()
return None
start = self.fp.tell()
self._search_end()
self.seekp = stop = self.fp.tell()
if start != stop:
break
return self.factory(_PartialFile(self.fp, start, stop))
# Recommended to use PortableUnixMailbox instead!
class UnixMailbox(_Mailbox):
......@@ -213,36 +2029,6 @@ class MHMailbox:
return msg
class Maildir:
# Qmail directory mailbox
def __init__(self, dirname, factory=rfc822.Message):
self.dirname = dirname
self.factory = factory
# check for new mail
newdir = os.path.join(self.dirname, 'new')
boxes = [os.path.join(newdir, f)
for f in os.listdir(newdir) if f[0] != '.']
# Now check for current mail in this maildir
curdir = os.path.join(self.dirname, 'cur')
boxes += [os.path.join(curdir, f)
for f in os.listdir(curdir) if f[0] != '.']
boxes.reverse()
self.boxes = boxes
def __iter__(self):
return iter(self.next, None)
def next(self):
if not self.boxes:
return None
fn = self.boxes.pop()
fp = open(fn)
return self.factory(fp)
class BabylMailbox(_Mailbox):
def _search_start(self):
......@@ -263,59 +2049,20 @@ class BabylMailbox(_Mailbox):
self.fp.seek(pos)
return
## End: classes from the original module (for backward compatibility).
def _test():
import sys
args = sys.argv[1:]
if not args:
for key in 'MAILDIR', 'MAIL', 'LOGNAME', 'USER':
if key in os.environ:
mbox = os.environ[key]
break
else:
print "$MAIL, $LOGNAME nor $USER set -- who are you?"
return
else:
mbox = args[0]
if mbox[:1] == '+':
mbox = os.environ['HOME'] + '/Mail/' + mbox[1:]
elif not '/' in mbox:
if os.path.isfile('/var/mail/' + mbox):
mbox = '/var/mail/' + mbox
else:
mbox = '/usr/mail/' + mbox
if os.path.isdir(mbox):
if os.path.isdir(os.path.join(mbox, 'cur')):
mb = Maildir(mbox)
else:
mb = MHMailbox(mbox)
else:
fp = open(mbox, 'r')
mb = PortableUnixMailbox(fp)
class Error(Exception):
"""Raised for module-specific errors."""
msgs = []
while 1:
msg = mb.next()
if msg is None:
break
msgs.append(msg)
if len(args) <= 1:
msg.fp = None
if len(args) > 1:
num = int(args[1])
print 'Message %d body:'%num
msg = msgs[num-1]
msg.rewindbody()
sys.stdout.write(msg.fp.read())
else:
print 'Mailbox',mbox,'has',len(msgs),'messages:'
for msg in msgs:
f = msg.getheader('from') or ""
s = msg.getheader('subject') or ""
d = msg.getheader('date') or ""
print '-%20.20s %20.20s %-30.30s'%(f, d[5:], s)
if __name__ == '__main__':
_test()
class NoSuchMailboxError(Error):
"""The specified mailbox does not exist and won't be created."""
class NotEmptyError(Error):
"""The specified mailbox is not empty and deletion was requested."""
class ExternalClashError(Error):
"""Another process caused an action to fail."""
class FormatError(Error):
"""A file appears to have an invalid format."""
import mailbox
import os
import time
import unittest
import stat
import socket
import email
import email.Message
import rfc822
import re
import StringIO
from test import test_support
# cleanup earlier tests
import unittest
import mailbox
import glob
try:
os.unlink(test_support.TESTFN)
except os.error:
import fcntl
except ImportError:
pass
class TestBase(unittest.TestCase):
def _check_sample(self, msg):
# Inspect a mailbox.Message representation of the sample message
self.assert_(isinstance(msg, email.Message.Message))
self.assert_(isinstance(msg, mailbox.Message))
for key, value in _sample_headers.iteritems():
self.assert_(value in msg.get_all(key))
self.assert_(msg.is_multipart())
self.assert_(len(msg.get_payload()) == len(_sample_payloads))
for i, payload in enumerate(_sample_payloads):
part = msg.get_payload(i)
self.assert_(isinstance(part, email.Message.Message))
self.assert_(not isinstance(part, mailbox.Message))
self.assert_(part.get_payload() == payload)
def _delete_recursively(self, target):
# Delete a file or delete a directory recursively
if os.path.isdir(target):
for path, dirs, files in os.walk(target, topdown=False):
for name in files:
os.remove(os.path.join(path, name))
for name in dirs:
os.rmdir(os.path.join(path, name))
os.rmdir(target)
elif os.path.exists(target):
os.remove(target)
class TestMailbox(TestBase):
_factory = None # Overridden by subclasses to reuse tests
_template = 'From: foo\n\n%s'
def setUp(self):
self._path = test_support.TESTFN
self._box = self._factory(self._path)
def tearDown(self):
self._box.close()
self._delete_recursively(self._path)
def test_add(self):
# Add copies of a sample message
keys = []
keys.append(self._box.add(self._template % 0))
self.assert_(len(self._box) == 1)
keys.append(self._box.add(mailbox.Message(_sample_message)))
self.assert_(len(self._box) == 2)
keys.append(self._box.add(email.message_from_string(_sample_message)))
self.assert_(len(self._box) == 3)
keys.append(self._box.add(StringIO.StringIO(_sample_message)))
self.assert_(len(self._box) == 4)
keys.append(self._box.add(_sample_message))
self.assert_(len(self._box) == 5)
self.assert_(self._box.get_string(keys[0]) == self._template % 0)
for i in (1, 2, 3, 4):
self._check_sample(self._box[keys[i]])
def test_remove(self):
# Remove messages using remove()
self._test_remove_or_delitem(self._box.remove)
def test_delitem(self):
# Remove messages using __delitem__()
self._test_remove_or_delitem(self._box.__delitem__)
def _test_remove_or_delitem(self, method):
# (Used by test_remove() and test_delitem().)
key0 = self._box.add(self._template % 0)
key1 = self._box.add(self._template % 1)
self.assert_(len(self._box) == 2)
method(key0)
l = len(self._box)
self.assert_(l == 1, "actual l: %s" % l)
self.assertRaises(KeyError, lambda: self._box[key0])
self.assertRaises(KeyError, lambda: method(key0))
self.assert_(self._box.get_string(key1) == self._template % 1)
key2 = self._box.add(self._template % 2)
self.assert_(len(self._box) == 2)
method(key2)
l = len(self._box)
self.assert_(l == 1, "actual l: %s" % l)
self.assertRaises(KeyError, lambda: self._box[key2])
self.assertRaises(KeyError, lambda: method(key2))
self.assert_(self._box.get_string(key1) == self._template % 1)
method(key1)
self.assert_(len(self._box) == 0)
self.assertRaises(KeyError, lambda: self._box[key1])
self.assertRaises(KeyError, lambda: method(key1))
def test_discard(self, repetitions=10):
# Discard messages
key0 = self._box.add(self._template % 0)
key1 = self._box.add(self._template % 1)
self.assert_(len(self._box) == 2)
self._box.discard(key0)
self.assert_(len(self._box) == 1)
self.assertRaises(KeyError, lambda: self._box[key0])
self._box.discard(key0)
self.assert_(len(self._box) == 1)
self.assertRaises(KeyError, lambda: self._box[key0])
def test_get(self):
# Retrieve messages using get()
key0 = self._box.add(self._template % 0)
msg = self._box.get(key0)
self.assert_(msg['from'] == 'foo')
self.assert_(msg.get_payload() == '0')
self.assert_(self._box.get('foo') is None)
self.assert_(self._box.get('foo', False) is False)
self._box.close()
self._box = self._factory(self._path, factory=rfc822.Message)
key1 = self._box.add(self._template % 1)
msg = self._box.get(key1)
self.assert_(msg['from'] == 'foo')
self.assert_(msg.fp.read() == '1')
def test_getitem(self):
# Retrieve message using __getitem__()
key0 = self._box.add(self._template % 0)
msg = self._box[key0]
self.assert_(msg['from'] == 'foo')
self.assert_(msg.get_payload() == '0')
self.assertRaises(KeyError, lambda: self._box['foo'])
self._box.discard(key0)
self.assertRaises(KeyError, lambda: self._box[key0])
def test_get_message(self):
# Get Message representations of messages
key0 = self._box.add(self._template % 0)
key1 = self._box.add(_sample_message)
msg0 = self._box.get_message(key0)
self.assert_(isinstance(msg0, mailbox.Message))
self.assert_(msg0['from'] == 'foo')
self.assert_(msg0.get_payload() == '0')
self._check_sample(self._box.get_message(key1))
def test_get_string(self):
# Get string representations of messages
key0 = self._box.add(self._template % 0)
key1 = self._box.add(_sample_message)
self.assert_(self._box.get_string(key0) == self._template % 0)
self.assert_(self._box.get_string(key1) == _sample_message)
def test_get_file(self):
# Get file representations of messages
key0 = self._box.add(self._template % 0)
key1 = self._box.add(_sample_message)
self.assert_(self._box.get_file(key0).read().replace(os.linesep, '\n')
== self._template % 0)
self.assert_(self._box.get_file(key1).read().replace(os.linesep, '\n')
== _sample_message)
def test_iterkeys(self):
# Get keys using iterkeys()
self._check_iteration(self._box.iterkeys, do_keys=True, do_values=False)
def test_keys(self):
# Get keys using keys()
self._check_iteration(self._box.keys, do_keys=True, do_values=False)
def test_itervalues(self):
# Get values using itervalues()
self._check_iteration(self._box.itervalues, do_keys=False,
do_values=True)
def test_iter(self):
# Get values using __iter__()
self._check_iteration(self._box.__iter__, do_keys=False,
do_values=True)
def test_values(self):
# Get values using values()
self._check_iteration(self._box.values, do_keys=False, do_values=True)
def test_iteritems(self):
# Get keys and values using iteritems()
self._check_iteration(self._box.iteritems, do_keys=True,
do_values=True)
def test_items(self):
# Get keys and values using items()
self._check_iteration(self._box.items, do_keys=True, do_values=True)
def _check_iteration(self, method, do_keys, do_values, repetitions=10):
for value in method():
self.fail("Not empty")
keys, values = [], []
for i in xrange(repetitions):
keys.append(self._box.add(self._template % i))
values.append(self._template % i)
if do_keys and not do_values:
returned_keys = list(method())
elif do_values and not do_keys:
returned_values = list(method())
else:
returned_keys, returned_values = [], []
for key, value in method():
returned_keys.append(key)
returned_values.append(value)
if do_keys:
self.assert_(len(keys) == len(returned_keys))
self.assert_(set(keys) == set(returned_keys))
if do_values:
count = 0
for value in returned_values:
self.assert_(value['from'] == 'foo')
self.assert_(int(value.get_payload()) < repetitions)
count += 1
self.assert_(len(values) == count)
def test_has_key(self):
# Check existence of keys using has_key()
self._test_has_key_or_contains(self._box.has_key)
def test_contains(self):
# Check existence of keys using __contains__()
self._test_has_key_or_contains(self._box.__contains__)
def _test_has_key_or_contains(self, method):
# (Used by test_has_key() and test_contains().)
self.assert_(not method('foo'))
key0 = self._box.add(self._template % 0)
self.assert_(method(key0))
self.assert_(not method('foo'))
key1 = self._box.add(self._template % 1)
self.assert_(method(key1))
self.assert_(method(key0))
self.assert_(not method('foo'))
self._box.remove(key0)
self.assert_(not method(key0))
self.assert_(method(key1))
self.assert_(not method('foo'))
self._box.remove(key1)
self.assert_(not method(key1))
self.assert_(not method(key0))
self.assert_(not method('foo'))
def test_len(self, repetitions=10):
# Get message count
keys = []
for i in xrange(repetitions):
self.assert_(len(self._box) == i)
keys.append(self._box.add(self._template % i))
self.assert_(len(self._box) == i + 1)
for i in xrange(repetitions):
self.assert_(len(self._box) == repetitions - i)
self._box.remove(keys[i])
self.assert_(len(self._box) == repetitions - i - 1)
def test_set_item(self):
# Modify messages using __setitem__()
key0 = self._box.add(self._template % 'original 0')
self.assert_(self._box.get_string(key0) == \
self._template % 'original 0')
key1 = self._box.add(self._template % 'original 1')
self.assert_(self._box.get_string(key1) == \
self._template % 'original 1')
self._box[key0] = self._template % 'changed 0'
self.assert_(self._box.get_string(key0) == \
self._template % 'changed 0')
self._box[key1] = self._template % 'changed 1'
self.assert_(self._box.get_string(key1) == \
self._template % 'changed 1')
self._box[key0] = _sample_message
self._check_sample(self._box[key0])
self._box[key1] = self._box[key0]
self._check_sample(self._box[key1])
self._box[key0] = self._template % 'original 0'
self.assert_(self._box.get_string(key0) ==
self._template % 'original 0')
self._check_sample(self._box[key1])
self.assertRaises(KeyError,
lambda: self._box.__setitem__('foo', 'bar'))
self.assertRaises(KeyError, lambda: self._box['foo'])
self.assert_(len(self._box) == 2)
def test_clear(self, iterations=10):
# Remove all messages using clear()
keys = []
for i in xrange(iterations):
self._box.add(self._template % i)
for i, key in enumerate(keys):
self.assert_(self._box.get_string(key) == self._template % i)
self._box.clear()
self.assert_(len(self._box) == 0)
for i, key in enumerate(keys):
self.assertRaises(KeyError, lambda: self._box.get_string(key))
def test_pop(self):
# Get and remove a message using pop()
key0 = self._box.add(self._template % 0)
self.assert_(key0 in self._box)
key1 = self._box.add(self._template % 1)
self.assert_(key1 in self._box)
self.assert_(self._box.pop(key0).get_payload() == '0')
self.assert_(key0 not in self._box)
self.assert_(key1 in self._box)
key2 = self._box.add(self._template % 2)
self.assert_(key2 in self._box)
self.assert_(self._box.pop(key2).get_payload() == '2')
self.assert_(key2 not in self._box)
self.assert_(key1 in self._box)
self.assert_(self._box.pop(key1).get_payload() == '1')
self.assert_(key1 not in self._box)
self.assert_(len(self._box) == 0)
def test_popitem(self, iterations=10):
# Get and remove an arbitrary (key, message) using popitem()
keys = []
for i in xrange(10):
keys.append(self._box.add(self._template % i))
seen = []
for i in xrange(10):
key, msg = self._box.popitem()
self.assert_(key in keys)
self.assert_(key not in seen)
seen.append(key)
self.assert_(int(msg.get_payload()) == keys.index(key))
self.assert_(len(self._box) == 0)
for key in keys:
self.assertRaises(KeyError, lambda: self._box[key])
def test_update(self):
# Modify multiple messages using update()
key0 = self._box.add(self._template % 'original 0')
key1 = self._box.add(self._template % 'original 1')
key2 = self._box.add(self._template % 'original 2')
self._box.update({key0: self._template % 'changed 0',
key2: _sample_message})
self.assert_(len(self._box) == 3)
self.assert_(self._box.get_string(key0) ==
self._template % 'changed 0')
self.assert_(self._box.get_string(key1) ==
self._template % 'original 1')
self._check_sample(self._box[key2])
self._box.update([(key2, self._template % 'changed 2'),
(key1, self._template % 'changed 1'),
(key0, self._template % 'original 0')])
self.assert_(len(self._box) == 3)
self.assert_(self._box.get_string(key0) ==
self._template % 'original 0')
self.assert_(self._box.get_string(key1) ==
self._template % 'changed 1')
self.assert_(self._box.get_string(key2) ==
self._template % 'changed 2')
self.assertRaises(KeyError,
lambda: self._box.update({'foo': 'bar',
key0: self._template % "changed 0"}))
self.assert_(len(self._box) == 3)
self.assert_(self._box.get_string(key0) ==
self._template % "changed 0")
self.assert_(self._box.get_string(key1) ==
self._template % "changed 1")
self.assert_(self._box.get_string(key2) ==
self._template % "changed 2")
def test_flush(self):
# Write changes to disk
self._test_flush_or_close(self._box.flush)
def test_lock_unlock(self):
# Lock and unlock the mailbox
self.assert_(not os.path.exists(self._get_lock_path()))
self._box.lock()
self.assert_(os.path.exists(self._get_lock_path()))
self._box.unlock()
self.assert_(not os.path.exists(self._get_lock_path()))
def test_close(self):
# Close mailbox and flush changes to disk
self._test_flush_or_close(self._box.close)
def _test_flush_or_close(self, method):
contents = [self._template % i for i in xrange(3)]
self._box.add(contents[0])
self._box.add(contents[1])
self._box.add(contents[2])
method()
self._box = self._factory(self._path)
keys = self._box.keys()
self.assert_(len(keys) == 3)
for key in keys:
self.assert_(self._box.get_string(key) in contents)
def test_dump_message(self):
# Write message representations to disk
for input in (email.message_from_string(_sample_message),
_sample_message, StringIO.StringIO(_sample_message)):
output = StringIO.StringIO()
self._box._dump_message(input, output)
self.assert_(output.getvalue() ==
_sample_message.replace('\n', os.linesep))
output = StringIO.StringIO()
self.assertRaises(TypeError,
lambda: self._box._dump_message(None, output))
def _get_lock_path(self):
# Return the path of the dot lock file. May be overridden.
return self._path + '.lock'
class TestMailboxSuperclass(TestBase):
def test_notimplemented(self):
# Test that all Mailbox methods raise NotImplementedException.
box = mailbox.Mailbox('path')
self.assertRaises(NotImplementedError, lambda: box.add(''))
self.assertRaises(NotImplementedError, lambda: box.remove(''))
self.assertRaises(NotImplementedError, lambda: box.__delitem__(''))
self.assertRaises(NotImplementedError, lambda: box.discard(''))
self.assertRaises(NotImplementedError, lambda: box.__setitem__('', ''))
self.assertRaises(NotImplementedError, lambda: box.iterkeys())
self.assertRaises(NotImplementedError, lambda: box.keys())
self.assertRaises(NotImplementedError, lambda: box.itervalues().next())
self.assertRaises(NotImplementedError, lambda: box.__iter__().next())
self.assertRaises(NotImplementedError, lambda: box.values())
self.assertRaises(NotImplementedError, lambda: box.iteritems().next())
self.assertRaises(NotImplementedError, lambda: box.items())
self.assertRaises(NotImplementedError, lambda: box.get(''))
self.assertRaises(NotImplementedError, lambda: box.__getitem__(''))
self.assertRaises(NotImplementedError, lambda: box.get_message(''))
self.assertRaises(NotImplementedError, lambda: box.get_string(''))
self.assertRaises(NotImplementedError, lambda: box.get_file(''))
self.assertRaises(NotImplementedError, lambda: box.has_key(''))
self.assertRaises(NotImplementedError, lambda: box.__contains__(''))
self.assertRaises(NotImplementedError, lambda: box.__len__())
self.assertRaises(NotImplementedError, lambda: box.clear())
self.assertRaises(NotImplementedError, lambda: box.pop(''))
self.assertRaises(NotImplementedError, lambda: box.popitem())
self.assertRaises(NotImplementedError, lambda: box.update((('', ''),)))
self.assertRaises(NotImplementedError, lambda: box.flush())
self.assertRaises(NotImplementedError, lambda: box.lock())
self.assertRaises(NotImplementedError, lambda: box.unlock())
self.assertRaises(NotImplementedError, lambda: box.close())
class TestMaildir(TestMailbox):
_factory = lambda self, path, factory=None: mailbox.Maildir(path, factory)
def setUp(self):
TestMailbox.setUp(self)
if os.name == 'nt':
self._box.colon = '!'
def test_add_MM(self):
# Add a MaildirMessage instance
msg = mailbox.MaildirMessage(self._template % 0)
msg.set_subdir('cur')
msg.set_info('foo')
key = self._box.add(msg)
self.assert_(os.path.exists(os.path.join(self._path, 'cur', '%s%sfoo' %
(key, self._box.colon))))
def test_get_MM(self):
# Get a MaildirMessage instance
msg = mailbox.MaildirMessage(self._template % 0)
msg.set_subdir('cur')
msg.set_flags('RF')
key = self._box.add(msg)
msg_returned = self._box.get_message(key)
self.assert_(isinstance(msg_returned, mailbox.MaildirMessage))
self.assert_(msg_returned.get_subdir() == 'cur')
self.assert_(msg_returned.get_flags() == 'FR')
def test_set_MM(self):
# Set with a MaildirMessage instance
msg0 = mailbox.MaildirMessage(self._template % 0)
msg0.set_flags('TP')
key = self._box.add(msg0)
msg_returned = self._box.get_message(key)
self.assert_(msg_returned.get_subdir() == 'new')
self.assert_(msg_returned.get_flags() == 'PT')
msg1 = mailbox.MaildirMessage(self._template % 1)
self._box[key] = msg1
msg_returned = self._box.get_message(key)
self.assert_(msg_returned.get_subdir() == 'new')
self.assert_(msg_returned.get_flags() == '')
self.assert_(msg_returned.get_payload() == '1')
msg2 = mailbox.MaildirMessage(self._template % 2)
msg2.set_info('2,S')
self._box[key] = msg2
self._box[key] = self._template % 3
msg_returned = self._box.get_message(key)
self.assert_(msg_returned.get_subdir() == 'new')
self.assert_(msg_returned.get_flags() == 'S')
self.assert_(msg_returned.get_payload() == '3')
def test_initialize_new(self):
# Initialize a non-existent mailbox
self.tearDown()
self._box = mailbox.Maildir(self._path)
self._check_basics(factory=rfc822.Message)
self._delete_recursively(self._path)
self._box = self._factory(self._path, factory=None)
self._check_basics()
def test_initialize_existing(self):
# Initialize an existing mailbox
self.tearDown()
for subdir in '', 'tmp', 'new', 'cur':
os.mkdir(os.path.join(self._path, subdir))
self._box = mailbox.Maildir(self._path)
self._check_basics(factory=rfc822.Message)
self._box = mailbox.Maildir(self._path, factory=None)
self._check_basics()
def _check_basics(self, factory=None):
# (Used by test_open_new() and test_open_existing().)
self.assertEqual(self._box._path, os.path.abspath(self._path))
self.assertEqual(self._box._factory, factory)
for subdir in '', 'tmp', 'new', 'cur':
path = os.path.join(self._path, subdir)
mode = os.stat(path)[stat.ST_MODE]
self.assert_(stat.S_ISDIR(mode), "Not a directory: '%s'" % path)
def test_list_folders(self):
# List folders
self._box.add_folder('one')
self._box.add_folder('two')
self._box.add_folder('three')
self.assert_(len(self._box.list_folders()) == 3)
self.assert_(set(self._box.list_folders()) ==
set(('one', 'two', 'three')))
def test_get_folder(self):
# Open folders
self._box.add_folder('foo.bar')
folder0 = self._box.get_folder('foo.bar')
folder0.add(self._template % 'bar')
self.assert_(os.path.isdir(os.path.join(self._path, '.foo.bar')))
folder1 = self._box.get_folder('foo.bar')
self.assert_(folder1.get_string(folder1.keys()[0]) == \
self._template % 'bar')
def test_add_and_remove_folders(self):
# Delete folders
self._box.add_folder('one')
self._box.add_folder('two')
self.assert_(len(self._box.list_folders()) == 2)
self.assert_(set(self._box.list_folders()) == set(('one', 'two')))
self._box.remove_folder('one')
self.assert_(len(self._box.list_folders()) == 1)
self.assert_(set(self._box.list_folders()) == set(('two',)))
self._box.add_folder('three')
self.assert_(len(self._box.list_folders()) == 2)
self.assert_(set(self._box.list_folders()) == set(('two', 'three')))
self._box.remove_folder('three')
self.assert_(len(self._box.list_folders()) == 1)
self.assert_(set(self._box.list_folders()) == set(('two',)))
self._box.remove_folder('two')
self.assert_(len(self._box.list_folders()) == 0)
self.assert_(self._box.list_folders() == [])
def test_clean(self):
# Remove old files from 'tmp'
foo_path = os.path.join(self._path, 'tmp', 'foo')
bar_path = os.path.join(self._path, 'tmp', 'bar')
file(foo_path, 'w').close()
file(bar_path, 'w').close()
self._box.clean()
self.assert_(os.path.exists(foo_path))
self.assert_(os.path.exists(bar_path))
foo_stat = os.stat(foo_path)
os.utime(os.path.join(foo_path), (time.time() - 129600 - 2,
foo_stat.st_mtime))
self._box.clean()
self.assert_(not os.path.exists(foo_path))
self.assert_(os.path.exists(bar_path))
def test_create_tmp(self, repetitions=10):
# Create files in tmp directory
hostname = socket.gethostname()
if '/' in hostname:
hostname = hostname.replace('/', r'\057')
if ':' in hostname:
hostname = hostname.replace(':', r'\072')
pid = os.getpid()
pattern = re.compile(r"(?P<time>\d+)\.M(?P<M>\d{1,6})P(?P<P>\d+)"
r"Q(?P<Q>\d+)\.(?P<host>[^:/]+)")
previous_groups = None
for x in xrange(repetitions):
tmp_file = self._box._create_tmp()
head, tail = os.path.split(tmp_file.name)
self.assertEqual(head, os.path.abspath(os.path.join(self._path,
"tmp")),
"File in wrong location: '%s'" % head)
match = pattern.match(tail)
self.assert_(match != None, "Invalid file name: '%s'" % tail)
groups = match.groups()
if previous_groups != None:
self.assert_(int(groups[0] >= previous_groups[0]),
"Non-monotonic seconds: '%s' before '%s'" %
(previous_groups[0], groups[0]))
self.assert_(int(groups[1] >= previous_groups[1]) or
groups[0] != groups[1],
"Non-monotonic milliseconds: '%s' before '%s'" %
(previous_groups[1], groups[1]))
self.assert_(int(groups[2]) == pid,
"Process ID mismatch: '%s' should be '%s'" %
(groups[2], pid))
self.assert_(int(groups[3]) == int(previous_groups[3]) + 1,
"Non-sequential counter: '%s' before '%s'" %
(previous_groups[3], groups[3]))
self.assert_(groups[4] == hostname,
"Host name mismatch: '%s' should be '%s'" %
(groups[4], hostname))
previous_groups = groups
tmp_file.write(_sample_message)
tmp_file.seek(0)
self.assert_(tmp_file.read() == _sample_message)
tmp_file.close()
file_count = len(os.listdir(os.path.join(self._path, "tmp")))
self.assert_(file_count == repetitions,
"Wrong file count: '%s' should be '%s'" %
(file_count, repetitions))
def test_refresh(self):
# Update the table of contents
self.assert_(self._box._toc == {})
key0 = self._box.add(self._template % 0)
key1 = self._box.add(self._template % 1)
self.assert_(self._box._toc == {})
self._box._refresh()
self.assert_(self._box._toc == {key0: os.path.join('new', key0),
key1: os.path.join('new', key1)})
key2 = self._box.add(self._template % 2)
self.assert_(self._box._toc == {key0: os.path.join('new', key0),
key1: os.path.join('new', key1)})
self._box._refresh()
self.assert_(self._box._toc == {key0: os.path.join('new', key0),
key1: os.path.join('new', key1),
key2: os.path.join('new', key2)})
def test_lookup(self):
# Look up message subpaths in the TOC
self.assertRaises(KeyError, lambda: self._box._lookup('foo'))
key0 = self._box.add(self._template % 0)
self.assert_(self._box._lookup(key0) == os.path.join('new', key0))
os.remove(os.path.join(self._path, 'new', key0))
self.assert_(self._box._toc == {key0: os.path.join('new', key0)})
self.assertRaises(KeyError, lambda: self._box._lookup(key0))
self.assert_(self._box._toc == {})
def test_lock_unlock(self):
# Lock and unlock the mailbox. For Maildir, this does nothing.
self._box.lock()
self._box.unlock()
class _TestMboxMMDF(TestMailbox):
def tearDown(self):
self._box.close()
self._delete_recursively(self._path)
for lock_remnant in glob.glob(self._path + '.*'):
os.remove(lock_remnant)
def test_add_from_string(self):
# Add a string starting with 'From ' to the mailbox
key = self._box.add('From foo@bar blah\nFrom: foo\n\n0')
self.assert_(self._box[key].get_from() == 'foo@bar blah')
self.assert_(self._box[key].get_payload() == '0')
def test_add_mbox_or_mmdf_message(self):
# Add an mboxMessage or MMDFMessage
for class_ in (mailbox.mboxMessage, mailbox.MMDFMessage):
msg = class_('From foo@bar blah\nFrom: foo\n\n0')
key = self._box.add(msg)
def test_open_close_open(self):
# Open and inspect previously-created mailbox
values = [self._template % i for i in xrange(3)]
for value in values:
self._box.add(value)
self._box.close()
mtime = os.path.getmtime(self._path)
self._box = self._factory(self._path)
self.assert_(len(self._box) == 3)
for key in self._box.iterkeys():
self.assert_(self._box.get_string(key) in values)
self._box.close()
self.assert_(mtime == os.path.getmtime(self._path))
def test_add_and_close(self):
# Verifying that closing a mailbox doesn't change added items
self._box.add(_sample_message)
for i in xrange(3):
self._box.add(self._template % i)
self._box.add(_sample_message)
self._box._file.flush()
self._box._file.seek(0)
contents = self._box._file.read()
self._box.close()
self.assert_(contents == file(self._path, 'rb').read())
self._box = self._factory(self._path)
class TestMbox(_TestMboxMMDF):
_factory = lambda self, path, factory=None: mailbox.mbox(path, factory)
class TestMMDF(_TestMboxMMDF):
_factory = lambda self, path, factory=None: mailbox.MMDF(path, factory)
class TestMH(TestMailbox):
_factory = lambda self, path, factory=None: mailbox.MH(path, factory)
def test_list_folders(self):
# List folders
self._box.add_folder('one')
self._box.add_folder('two')
self._box.add_folder('three')
self.assert_(len(self._box.list_folders()) == 3)
self.assert_(set(self._box.list_folders()) ==
set(('one', 'two', 'three')))
def test_get_folder(self):
# Open folders
self._box.add_folder('foo.bar')
folder0 = self._box.get_folder('foo.bar')
folder0.add(self._template % 'bar')
self.assert_(os.path.isdir(os.path.join(self._path, 'foo.bar')))
folder1 = self._box.get_folder('foo.bar')
self.assert_(folder1.get_string(folder1.keys()[0]) == \
self._template % 'bar')
def test_add_and_remove_folders(self):
# Delete folders
self._box.add_folder('one')
self._box.add_folder('two')
self.assert_(len(self._box.list_folders()) == 2)
self.assert_(set(self._box.list_folders()) == set(('one', 'two')))
self._box.remove_folder('one')
self.assert_(len(self._box.list_folders()) == 1)
self.assert_(set(self._box.list_folders()) == set(('two',)))
self._box.add_folder('three')
self.assert_(len(self._box.list_folders()) == 2)
self.assert_(set(self._box.list_folders()) == set(('two', 'three')))
self._box.remove_folder('three')
self.assert_(len(self._box.list_folders()) == 1)
self.assert_(set(self._box.list_folders()) == set(('two',)))
self._box.remove_folder('two')
self.assert_(len(self._box.list_folders()) == 0)
self.assert_(self._box.list_folders() == [])
def test_sequences(self):
# Get and set sequences
self.assert_(self._box.get_sequences() == {})
msg0 = mailbox.MHMessage(self._template % 0)
msg0.add_sequence('foo')
key0 = self._box.add(msg0)
self.assert_(self._box.get_sequences() == {'foo':[key0]})
msg1 = mailbox.MHMessage(self._template % 1)
msg1.set_sequences(['bar', 'replied', 'foo'])
key1 = self._box.add(msg1)
self.assert_(self._box.get_sequences() ==
{'foo':[key0, key1], 'bar':[key1], 'replied':[key1]})
msg0.set_sequences(['flagged'])
self._box[key0] = msg0
self.assert_(self._box.get_sequences() ==
{'foo':[key1], 'bar':[key1], 'replied':[key1],
'flagged':[key0]})
self._box.remove(key1)
self.assert_(self._box.get_sequences() == {'flagged':[key0]})
def test_pack(self):
# Pack the contents of the mailbox
msg0 = mailbox.MHMessage(self._template % 0)
msg1 = mailbox.MHMessage(self._template % 1)
msg2 = mailbox.MHMessage(self._template % 2)
msg3 = mailbox.MHMessage(self._template % 3)
msg0.set_sequences(['foo', 'unseen'])
msg1.set_sequences(['foo'])
msg2.set_sequences(['foo', 'flagged'])
msg3.set_sequences(['foo', 'bar', 'replied'])
key0 = self._box.add(msg0)
key1 = self._box.add(msg1)
key2 = self._box.add(msg2)
key3 = self._box.add(msg3)
self.assert_(self._box.get_sequences() ==
{'foo':[key0,key1,key2,key3], 'unseen':[key0],
'flagged':[key2], 'bar':[key3], 'replied':[key3]})
self._box.remove(key2)
self.assert_(self._box.get_sequences() ==
{'foo':[key0,key1,key3], 'unseen':[key0], 'bar':[key3],
'replied':[key3]})
self._box.pack()
self.assert_(self._box.keys() == [1, 2, 3])
key0 = key0
key1 = key0 + 1
key2 = key1 + 1
self.assert_(self._box.get_sequences() ==
{'foo':[1, 2, 3], 'unseen':[1], 'bar':[3], 'replied':[3]})
def _get_lock_path(self):
return os.path.join(self._path, '.mh_sequences.lock')
class TestBabyl(TestMailbox):
_factory = lambda self, path, factory=None: mailbox.Babyl(path, factory)
def tearDown(self):
self._box.close()
self._delete_recursively(self._path)
for lock_remnant in glob.glob(self._path + '.*'):
os.remove(lock_remnant)
def test_labels(self):
# Get labels from the mailbox
self.assert_(self._box.get_labels() == [])
msg0 = mailbox.BabylMessage(self._template % 0)
msg0.add_label('foo')
key0 = self._box.add(msg0)
self.assert_(self._box.get_labels() == ['foo'])
msg1 = mailbox.BabylMessage(self._template % 1)
msg1.set_labels(['bar', 'answered', 'foo'])
key1 = self._box.add(msg1)
self.assert_(set(self._box.get_labels()) == set(['foo', 'bar']))
msg0.set_labels(['blah', 'filed'])
self._box[key0] = msg0
self.assert_(set(self._box.get_labels()) ==
set(['foo', 'bar', 'blah']))
self._box.remove(key1)
self.assert_(set(self._box.get_labels()) == set(['blah']))
class TestMessage(TestBase):
_factory = mailbox.Message # Overridden by subclasses to reuse tests
def setUp(self):
self._path = test_support.TESTFN
def tearDown(self):
self._delete_recursively(self._path)
def test_initialize_with_eMM(self):
# Initialize based on email.Message.Message instance
eMM = email.message_from_string(_sample_message)
msg = self._factory(eMM)
self._post_initialize_hook(msg)
self._check_sample(msg)
def test_initialize_with_string(self):
# Initialize based on string
msg = self._factory(_sample_message)
self._post_initialize_hook(msg)
self._check_sample(msg)
def test_initialize_with_file(self):
# Initialize based on contents of file
f = open(self._path, 'w+')
f.write(_sample_message)
f.seek(0)
msg = self._factory(f)
self._post_initialize_hook(msg)
self._check_sample(msg)
f.close()
def test_initialize_with_nothing(self):
# Initialize without arguments
msg = self._factory()
self._post_initialize_hook(msg)
self.assert_(isinstance(msg, email.Message.Message))
self.assert_(isinstance(msg, mailbox.Message))
self.assert_(isinstance(msg, self._factory))
self.assert_(msg.keys() == [])
self.assert_(not msg.is_multipart())
self.assert_(msg.get_payload() == None)
def test_initialize_incorrectly(self):
# Initialize with invalid argument
self.assertRaises(TypeError, lambda: self._factory(object()))
def test_become_message(self):
# Take on the state of another message
eMM = email.message_from_string(_sample_message)
msg = self._factory()
msg._become_message(eMM)
self._check_sample(msg)
def test_explain_to(self):
# Copy self's format-specific data to other message formats.
# This test is superficial; better ones are in TestMessageConversion.
msg = self._factory()
for class_ in (mailbox.Message, mailbox.MaildirMessage,
mailbox.mboxMessage, mailbox.MHMessage,
mailbox.BabylMessage, mailbox.MMDFMessage):
other_msg = class_()
msg._explain_to(other_msg)
other_msg = email.Message.Message()
self.assertRaises(TypeError, lambda: msg._explain_to(other_msg))
def _post_initialize_hook(self, msg):
# Overridden by subclasses to check extra things after initialization
pass
class TestMaildirMessage(TestMessage):
_factory = mailbox.MaildirMessage
def _post_initialize_hook(self, msg):
self.assert_(msg._subdir == 'new')
self.assert_(msg._info == '')
def test_subdir(self):
# Use get_subdir() and set_subdir()
msg = mailbox.MaildirMessage(_sample_message)
self.assert_(msg.get_subdir() == 'new')
msg.set_subdir('cur')
self.assert_(msg.get_subdir() == 'cur')
msg.set_subdir('new')
self.assert_(msg.get_subdir() == 'new')
self.assertRaises(ValueError, lambda: msg.set_subdir('tmp'))
self.assert_(msg.get_subdir() == 'new')
msg.set_subdir('new')
self.assert_(msg.get_subdir() == 'new')
self._check_sample(msg)
def test_flags(self):
# Use get_flags(), set_flags(), add_flag(), remove_flag()
msg = mailbox.MaildirMessage(_sample_message)
self.assert_(msg.get_flags() == '')
self.assert_(msg.get_subdir() == 'new')
msg.set_flags('F')
self.assert_(msg.get_subdir() == 'new')
self.assert_(msg.get_flags() == 'F')
msg.set_flags('SDTP')
self.assert_(msg.get_flags() == 'DPST')
msg.add_flag('FT')
self.assert_(msg.get_flags() == 'DFPST')
msg.remove_flag('TDRP')
self.assert_(msg.get_flags() == 'FS')
self.assert_(msg.get_subdir() == 'new')
self._check_sample(msg)
def test_date(self):
# Use get_date() and set_date()
msg = mailbox.MaildirMessage(_sample_message)
self.assert_(abs(msg.get_date() - time.time()) < 60)
msg.set_date(0.0)
self.assert_(msg.get_date() == 0.0)
def test_info(self):
# Use get_info() and set_info()
msg = mailbox.MaildirMessage(_sample_message)
self.assert_(msg.get_info() == '')
msg.set_info('1,foo=bar')
self.assert_(msg.get_info() == '1,foo=bar')
self.assertRaises(TypeError, lambda: msg.set_info(None))
self._check_sample(msg)
def test_info_and_flags(self):
# Test interaction of info and flag methods
msg = mailbox.MaildirMessage(_sample_message)
self.assert_(msg.get_info() == '')
msg.set_flags('SF')
self.assert_(msg.get_flags() == 'FS')
self.assert_(msg.get_info() == '2,FS')
msg.set_info('1,')
self.assert_(msg.get_flags() == '')
self.assert_(msg.get_info() == '1,')
msg.remove_flag('RPT')
self.assert_(msg.get_flags() == '')
self.assert_(msg.get_info() == '1,')
msg.add_flag('D')
self.assert_(msg.get_flags() == 'D')
self.assert_(msg.get_info() == '2,D')
self._check_sample(msg)
class _TestMboxMMDFMessage(TestMessage):
_factory = mailbox._mboxMMDFMessage
def _post_initialize_hook(self, msg):
self._check_from(msg)
def test_initialize_with_unixfrom(self):
# Initialize with a message that already has a _unixfrom attribute
msg = mailbox.Message(_sample_message)
msg.set_unixfrom('From foo@bar blah')
msg = mailbox.mboxMessage(msg)
self.assert_(msg.get_from() == 'foo@bar blah', msg.get_from())
def test_from(self):
# Get and set "From " line
msg = mailbox.mboxMessage(_sample_message)
self._check_from(msg)
msg.set_from('foo bar')
self.assert_(msg.get_from() == 'foo bar')
msg.set_from('foo@bar', True)
self._check_from(msg, 'foo@bar')
msg.set_from('blah@temp', time.localtime())
self._check_from(msg, 'blah@temp')
def test_flags(self):
# Use get_flags(), set_flags(), add_flag(), remove_flag()
msg = mailbox.mboxMessage(_sample_message)
self.assert_(msg.get_flags() == '')
msg.set_flags('F')
self.assert_(msg.get_flags() == 'F')
msg.set_flags('XODR')
self.assert_(msg.get_flags() == 'RODX')
msg.add_flag('FA')
self.assert_(msg.get_flags() == 'RODFAX')
msg.remove_flag('FDXA')
self.assert_(msg.get_flags() == 'RO')
self._check_sample(msg)
def _check_from(self, msg, sender=None):
# Check contents of "From " line
if sender is None:
sender = "MAILER-DAEMON"
self.assert_(re.match(sender + r" \w{3} \w{3} [\d ]\d [\d ]\d:\d{2}:"
r"\d{2} \d{4}", msg.get_from()) is not None)
class TestMboxMessage(_TestMboxMMDFMessage):
_factory = mailbox.mboxMessage
class TestMHMessage(TestMessage):
_factory = mailbox.MHMessage
def _post_initialize_hook(self, msg):
self.assert_(msg._sequences == [])
def test_sequences(self):
# Get, set, join, and leave sequences
msg = mailbox.MHMessage(_sample_message)
self.assert_(msg.get_sequences() == [])
msg.set_sequences(['foobar'])
self.assert_(msg.get_sequences() == ['foobar'])
msg.set_sequences([])
self.assert_(msg.get_sequences() == [])
msg.add_sequence('unseen')
self.assert_(msg.get_sequences() == ['unseen'])
msg.add_sequence('flagged')
self.assert_(msg.get_sequences() == ['unseen', 'flagged'])
msg.add_sequence('flagged')
self.assert_(msg.get_sequences() == ['unseen', 'flagged'])
msg.remove_sequence('unseen')
self.assert_(msg.get_sequences() == ['flagged'])
msg.add_sequence('foobar')
self.assert_(msg.get_sequences() == ['flagged', 'foobar'])
msg.remove_sequence('replied')
self.assert_(msg.get_sequences() == ['flagged', 'foobar'])
msg.set_sequences(['foobar', 'replied'])
self.assert_(msg.get_sequences() == ['foobar', 'replied'])
class TestBabylMessage(TestMessage):
_factory = mailbox.BabylMessage
def _post_initialize_hook(self, msg):
self.assert_(msg._labels == [])
def test_labels(self):
# Get, set, join, and leave labels
msg = mailbox.BabylMessage(_sample_message)
self.assert_(msg.get_labels() == [])
msg.set_labels(['foobar'])
self.assert_(msg.get_labels() == ['foobar'])
msg.set_labels([])
self.assert_(msg.get_labels() == [])
msg.add_label('filed')
self.assert_(msg.get_labels() == ['filed'])
msg.add_label('resent')
self.assert_(msg.get_labels() == ['filed', 'resent'])
msg.add_label('resent')
self.assert_(msg.get_labels() == ['filed', 'resent'])
msg.remove_label('filed')
self.assert_(msg.get_labels() == ['resent'])
msg.add_label('foobar')
self.assert_(msg.get_labels() == ['resent', 'foobar'])
msg.remove_label('unseen')
self.assert_(msg.get_labels() == ['resent', 'foobar'])
msg.set_labels(['foobar', 'answered'])
self.assert_(msg.get_labels() == ['foobar', 'answered'])
def test_visible(self):
# Get, set, and update visible headers
msg = mailbox.BabylMessage(_sample_message)
visible = msg.get_visible()
self.assert_(visible.keys() == [])
self.assert_(visible.get_payload() is None)
visible['User-Agent'] = 'FooBar 1.0'
visible['X-Whatever'] = 'Blah'
self.assert_(msg.get_visible().keys() == [])
msg.set_visible(visible)
visible = msg.get_visible()
self.assert_(visible.keys() == ['User-Agent', 'X-Whatever'])
self.assert_(visible['User-Agent'] == 'FooBar 1.0')
self.assert_(visible['X-Whatever'] == 'Blah')
self.assert_(visible.get_payload() is None)
msg.update_visible()
self.assert_(visible.keys() == ['User-Agent', 'X-Whatever'])
self.assert_(visible.get_payload() is None)
visible = msg.get_visible()
self.assert_(visible.keys() == ['User-Agent', 'Date', 'From', 'To',
'Subject'])
for header in ('User-Agent', 'Date', 'From', 'To', 'Subject'):
self.assert_(visible[header] == msg[header])
class TestMMDFMessage(_TestMboxMMDFMessage):
_factory = mailbox.MMDFMessage
class TestMessageConversion(TestBase):
def test_plain_to_x(self):
# Convert Message to all formats
for class_ in (mailbox.Message, mailbox.MaildirMessage,
mailbox.mboxMessage, mailbox.MHMessage,
mailbox.BabylMessage, mailbox.MMDFMessage):
msg_plain = mailbox.Message(_sample_message)
msg = class_(msg_plain)
self._check_sample(msg)
def test_x_to_plain(self):
# Convert all formats to Message
for class_ in (mailbox.Message, mailbox.MaildirMessage,
mailbox.mboxMessage, mailbox.MHMessage,
mailbox.BabylMessage, mailbox.MMDFMessage):
msg = class_(_sample_message)
msg_plain = mailbox.Message(msg)
self._check_sample(msg_plain)
def test_x_to_invalid(self):
# Convert all formats to an invalid format
for class_ in (mailbox.Message, mailbox.MaildirMessage,
mailbox.mboxMessage, mailbox.MHMessage,
mailbox.BabylMessage, mailbox.MMDFMessage):
self.assertRaises(TypeError, lambda: class_(False))
def test_maildir_to_maildir(self):
# Convert MaildirMessage to MaildirMessage
msg_maildir = mailbox.MaildirMessage(_sample_message)
msg_maildir.set_flags('DFPRST')
msg_maildir.set_subdir('cur')
date = msg_maildir.get_date()
msg = mailbox.MaildirMessage(msg_maildir)
self._check_sample(msg)
self.assert_(msg.get_flags() == 'DFPRST')
self.assert_(msg.get_subdir() == 'cur')
self.assert_(msg.get_date() == date)
def test_maildir_to_mboxmmdf(self):
# Convert MaildirMessage to mboxmessage and MMDFMessage
pairs = (('D', ''), ('F', 'F'), ('P', ''), ('R', 'A'), ('S', 'R'),
('T', 'D'), ('DFPRST', 'RDFA'))
for class_ in (mailbox.mboxMessage, mailbox.MMDFMessage):
msg_maildir = mailbox.MaildirMessage(_sample_message)
msg_maildir.set_date(0.0)
for setting, result in pairs:
msg_maildir.set_flags(setting)
msg = class_(msg_maildir)
self.assert_(msg.get_flags() == result)
self.assert_(msg.get_from() == 'MAILER-DAEMON %s' %
time.asctime(time.gmtime(0.0)))
msg_maildir.set_subdir('cur')
self.assert_(class_(msg_maildir).get_flags() == 'RODFA')
def test_maildir_to_mh(self):
# Convert MaildirMessage to MHMessage
msg_maildir = mailbox.MaildirMessage(_sample_message)
pairs = (('D', ['unseen']), ('F', ['unseen', 'flagged']),
('P', ['unseen']), ('R', ['unseen', 'replied']), ('S', []),
('T', ['unseen']), ('DFPRST', ['replied', 'flagged']))
for setting, result in pairs:
msg_maildir.set_flags(setting)
self.assert_(mailbox.MHMessage(msg_maildir).get_sequences() == \
result)
def test_maildir_to_babyl(self):
# Convert MaildirMessage to Babyl
msg_maildir = mailbox.MaildirMessage(_sample_message)
pairs = (('D', ['unseen']), ('F', ['unseen']),
('P', ['unseen', 'forwarded']), ('R', ['unseen', 'answered']),
('S', []), ('T', ['unseen', 'deleted']),
('DFPRST', ['deleted', 'answered', 'forwarded']))
for setting, result in pairs:
msg_maildir.set_flags(setting)
self.assert_(mailbox.BabylMessage(msg_maildir).get_labels() == \
result)
def test_mboxmmdf_to_maildir(self):
# Convert mboxMessage and MMDFMessage to MaildirMessage
for class_ in (mailbox.mboxMessage, mailbox.MMDFMessage):
msg_mboxMMDF = class_(_sample_message)
msg_mboxMMDF.set_from('foo@bar', time.gmtime(0.0))
pairs = (('R', 'S'), ('O', ''), ('D', 'T'), ('F', 'F'), ('A', 'R'),
('RODFA', 'FRST'))
for setting, result in pairs:
msg_mboxMMDF.set_flags(setting)
msg = mailbox.MaildirMessage(msg_mboxMMDF)
self.assert_(msg.get_flags() == result)
self.assert_(msg.get_date() == 0.0, msg.get_date())
msg_mboxMMDF.set_flags('O')
self.assert_(mailbox.MaildirMessage(msg_mboxMMDF).get_subdir() == \
'cur')
def test_mboxmmdf_to_mboxmmdf(self):
# Convert mboxMessage and MMDFMessage to mboxMessage and MMDFMessage
for class_ in (mailbox.mboxMessage, mailbox.MMDFMessage):
msg_mboxMMDF = class_(_sample_message)
msg_mboxMMDF.set_flags('RODFA')
msg_mboxMMDF.set_from('foo@bar')
for class2_ in (mailbox.mboxMessage, mailbox.MMDFMessage):
msg2 = class2_(msg_mboxMMDF)
self.assert_(msg2.get_flags() == 'RODFA')
self.assert_(msg2.get_from() == 'foo@bar')
def test_mboxmmdf_to_mh(self):
# Convert mboxMessage and MMDFMessage to MHMessage
for class_ in (mailbox.mboxMessage, mailbox.MMDFMessage):
msg_mboxMMDF = class_(_sample_message)
pairs = (('R', []), ('O', ['unseen']), ('D', ['unseen']),
('F', ['unseen', 'flagged']),
('A', ['unseen', 'replied']),
('RODFA', ['replied', 'flagged']))
for setting, result in pairs:
msg_mboxMMDF.set_flags(setting)
self.assert_(mailbox.MHMessage(msg_mboxMMDF).get_sequences() \
== result)
def test_mboxmmdf_to_babyl(self):
# Convert mboxMessage and MMDFMessage to BabylMessage
for class_ in (mailbox.mboxMessage, mailbox.MMDFMessage):
msg = class_(_sample_message)
pairs = (('R', []), ('O', ['unseen']),
('D', ['unseen', 'deleted']), ('F', ['unseen']),
('A', ['unseen', 'answered']),
('RODFA', ['deleted', 'answered']))
for setting, result in pairs:
msg.set_flags(setting)
self.assert_(mailbox.BabylMessage(msg).get_labels() == result)
def test_mh_to_maildir(self):
# Convert MHMessage to MaildirMessage
pairs = (('unseen', ''), ('replied', 'RS'), ('flagged', 'FS'))
for setting, result in pairs:
msg = mailbox.MHMessage(_sample_message)
msg.add_sequence(setting)
self.assert_(mailbox.MaildirMessage(msg).get_flags() == result)
self.assert_(mailbox.MaildirMessage(msg).get_subdir() == 'cur')
msg = mailbox.MHMessage(_sample_message)
msg.add_sequence('unseen')
msg.add_sequence('replied')
msg.add_sequence('flagged')
self.assert_(mailbox.MaildirMessage(msg).get_flags() == 'FR')
self.assert_(mailbox.MaildirMessage(msg).get_subdir() == 'cur')
def test_mh_to_mboxmmdf(self):
# Convert MHMessage to mboxMessage and MMDFMessage
pairs = (('unseen', 'O'), ('replied', 'ROA'), ('flagged', 'ROF'))
for setting, result in pairs:
msg = mailbox.MHMessage(_sample_message)
msg.add_sequence(setting)
for class_ in (mailbox.mboxMessage, mailbox.MMDFMessage):
self.assert_(class_(msg).get_flags() == result)
msg = mailbox.MHMessage(_sample_message)
msg.add_sequence('unseen')
msg.add_sequence('replied')
msg.add_sequence('flagged')
for class_ in (mailbox.mboxMessage, mailbox.MMDFMessage):
self.assert_(class_(msg).get_flags() == 'OFA')
def test_mh_to_mh(self):
# Convert MHMessage to MHMessage
msg = mailbox.MHMessage(_sample_message)
msg.add_sequence('unseen')
msg.add_sequence('replied')
msg.add_sequence('flagged')
self.assert_(mailbox.MHMessage(msg).get_sequences() == \
['unseen', 'replied', 'flagged'])
def test_mh_to_babyl(self):
# Convert MHMessage to BabylMessage
pairs = (('unseen', ['unseen']), ('replied', ['answered']),
('flagged', []))
for setting, result in pairs:
msg = mailbox.MHMessage(_sample_message)
msg.add_sequence(setting)
self.assert_(mailbox.BabylMessage(msg).get_labels() == result)
msg = mailbox.MHMessage(_sample_message)
msg.add_sequence('unseen')
msg.add_sequence('replied')
msg.add_sequence('flagged')
self.assert_(mailbox.BabylMessage(msg).get_labels() == \
['unseen', 'answered'])
def test_babyl_to_maildir(self):
# Convert BabylMessage to MaildirMessage
pairs = (('unseen', ''), ('deleted', 'ST'), ('filed', 'S'),
('answered', 'RS'), ('forwarded', 'PS'), ('edited', 'S'),
('resent', 'PS'))
for setting, result in pairs:
msg = mailbox.BabylMessage(_sample_message)
msg.add_label(setting)
self.assert_(mailbox.MaildirMessage(msg).get_flags() == result)
self.assert_(mailbox.MaildirMessage(msg).get_subdir() == 'cur')
msg = mailbox.BabylMessage(_sample_message)
for label in ('unseen', 'deleted', 'filed', 'answered', 'forwarded',
'edited', 'resent'):
msg.add_label(label)
self.assert_(mailbox.MaildirMessage(msg).get_flags() == 'PRT')
self.assert_(mailbox.MaildirMessage(msg).get_subdir() == 'cur')
def test_babyl_to_mboxmmdf(self):
# Convert BabylMessage to mboxMessage and MMDFMessage
pairs = (('unseen', 'O'), ('deleted', 'ROD'), ('filed', 'RO'),
('answered', 'ROA'), ('forwarded', 'RO'), ('edited', 'RO'),
('resent', 'RO'))
for setting, result in pairs:
for class_ in (mailbox.mboxMessage, mailbox.MMDFMessage):
msg = mailbox.BabylMessage(_sample_message)
msg.add_label(setting)
self.assert_(class_(msg).get_flags() == result)
msg = mailbox.BabylMessage(_sample_message)
for label in ('unseen', 'deleted', 'filed', 'answered', 'forwarded',
'edited', 'resent'):
msg.add_label(label)
for class_ in (mailbox.mboxMessage, mailbox.MMDFMessage):
self.assert_(class_(msg).get_flags() == 'ODA')
def test_babyl_to_mh(self):
# Convert BabylMessage to MHMessage
pairs = (('unseen', ['unseen']), ('deleted', []), ('filed', []),
('answered', ['replied']), ('forwarded', []), ('edited', []),
('resent', []))
for setting, result in pairs:
msg = mailbox.BabylMessage(_sample_message)
msg.add_label(setting)
self.assert_(mailbox.MHMessage(msg).get_sequences() == result)
msg = mailbox.BabylMessage(_sample_message)
for label in ('unseen', 'deleted', 'filed', 'answered', 'forwarded',
'edited', 'resent'):
msg.add_label(label)
self.assert_(mailbox.MHMessage(msg).get_sequences() == \
['unseen', 'replied'])
def test_babyl_to_babyl(self):
# Convert BabylMessage to BabylMessage
msg = mailbox.BabylMessage(_sample_message)
msg.update_visible()
for label in ('unseen', 'deleted', 'filed', 'answered', 'forwarded',
'edited', 'resent'):
msg.add_label(label)
msg2 = mailbox.BabylMessage(msg)
self.assert_(msg2.get_labels() == ['unseen', 'deleted', 'filed',
'answered', 'forwarded', 'edited',
'resent'])
self.assert_(msg.get_visible().keys() == msg2.get_visible().keys())
for key in msg.get_visible().keys():
self.assert_(msg.get_visible()[key] == msg2.get_visible()[key])
class TestProxyFileBase(TestBase):
def _test_read(self, proxy):
# Read by byte
proxy.seek(0)
self.assert_(proxy.read() == 'bar')
proxy.seek(1)
self.assert_(proxy.read() == 'ar')
proxy.seek(0)
self.assert_(proxy.read(2) == 'ba')
proxy.seek(1)
self.assert_(proxy.read(-1) == 'ar')
proxy.seek(2)
self.assert_(proxy.read(1000) == 'r')
def _test_readline(self, proxy):
# Read by line
proxy.seek(0)
self.assert_(proxy.readline() == 'foo' + os.linesep)
self.assert_(proxy.readline() == 'bar' + os.linesep)
self.assert_(proxy.readline() == 'fred' + os.linesep)
self.assert_(proxy.readline() == 'bob')
proxy.seek(2)
self.assert_(proxy.readline() == 'o' + os.linesep)
proxy.seek(6 + 2 * len(os.linesep))
self.assert_(proxy.readline() == 'fred' + os.linesep)
proxy.seek(6 + 2 * len(os.linesep))
self.assert_(proxy.readline(2) == 'fr')
self.assert_(proxy.readline(-10) == 'ed' + os.linesep)
def _test_readlines(self, proxy):
# Read multiple lines
proxy.seek(0)
self.assert_(proxy.readlines() == ['foo' + os.linesep,
'bar' + os.linesep,
'fred' + os.linesep, 'bob'])
proxy.seek(0)
self.assert_(proxy.readlines(2) == ['foo' + os.linesep])
proxy.seek(3 + len(os.linesep))
self.assert_(proxy.readlines(4 + len(os.linesep)) ==
['bar' + os.linesep, 'fred' + os.linesep])
proxy.seek(3)
self.assert_(proxy.readlines(1000) == [os.linesep, 'bar' + os.linesep,
'fred' + os.linesep, 'bob'])
def _test_iteration(self, proxy):
# Iterate by line
proxy.seek(0)
iterator = iter(proxy)
self.assert_(iterator.next() == 'foo' + os.linesep)
self.assert_(iterator.next() == 'bar' + os.linesep)
self.assert_(iterator.next() == 'fred' + os.linesep)
self.assert_(iterator.next() == 'bob')
self.assertRaises(StopIteration, lambda: iterator.next())
def _test_seek_and_tell(self, proxy):
# Seek and use tell to check position
proxy.seek(3)
self.assert_(proxy.tell() == 3)
self.assert_(proxy.read(len(os.linesep)) == os.linesep)
proxy.seek(2, 1)
self.assert_(proxy.read(1 + len(os.linesep)) == 'r' + os.linesep)
proxy.seek(-3 - len(os.linesep), 2)
self.assert_(proxy.read(3) == 'bar')
proxy.seek(2, 0)
self.assert_(proxy.read() == 'o' + os.linesep + 'bar' + os.linesep)
proxy.seek(100)
self.assert_(proxy.read() == '')
def _test_close(self, proxy):
# Close a file
proxy.close()
self.assertRaises(AttributeError, lambda: proxy.close())
class TestProxyFile(TestProxyFileBase):
def setUp(self):
self._path = test_support.TESTFN
self._file = file(self._path, 'wb+')
def tearDown(self):
self._file.close()
self._delete_recursively(self._path)
def test_initialize(self):
# Initialize and check position
self._file.write('foo')
pos = self._file.tell()
proxy0 = mailbox._ProxyFile(self._file)
self.assert_(proxy0.tell() == pos)
self.assert_(self._file.tell() == pos)
proxy1 = mailbox._ProxyFile(self._file, 0)
self.assert_(proxy1.tell() == 0)
self.assert_(self._file.tell() == pos)
def test_read(self):
self._file.write('bar')
self._test_read(mailbox._ProxyFile(self._file))
def test_readline(self):
self._file.write('foo%sbar%sfred%sbob' % (os.linesep, os.linesep,
os.linesep))
self._test_readline(mailbox._ProxyFile(self._file))
def test_readlines(self):
self._file.write('foo%sbar%sfred%sbob' % (os.linesep, os.linesep,
os.linesep))
self._test_readlines(mailbox._ProxyFile(self._file))
def test_iteration(self):
self._file.write('foo%sbar%sfred%sbob' % (os.linesep, os.linesep,
os.linesep))
self._test_iteration(mailbox._ProxyFile(self._file))
def test_seek_and_tell(self):
self._file.write('foo%sbar%s' % (os.linesep, os.linesep))
self._test_seek_and_tell(mailbox._ProxyFile(self._file))
def test_close(self):
self._file.write('foo%sbar%s' % (os.linesep, os.linesep))
self._test_close(mailbox._ProxyFile(self._file))
class TestPartialFile(TestProxyFileBase):
def setUp(self):
self._path = test_support.TESTFN
self._file = file(self._path, 'wb+')
def tearDown(self):
self._file.close()
self._delete_recursively(self._path)
def test_initialize(self):
# Initialize and check position
self._file.write('foo' + os.linesep + 'bar')
pos = self._file.tell()
proxy = mailbox._PartialFile(self._file, 2, 5)
self.assert_(proxy.tell() == 0)
self.assert_(self._file.tell() == pos)
def test_read(self):
self._file.write('***bar***')
self._test_read(mailbox._PartialFile(self._file, 3, 6))
def test_readline(self):
self._file.write('!!!!!foo%sbar%sfred%sbob!!!!!' %
(os.linesep, os.linesep, os.linesep))
self._test_readline(mailbox._PartialFile(self._file, 5,
18 + 3 * len(os.linesep)))
def test_readlines(self):
self._file.write('foo%sbar%sfred%sbob?????' %
(os.linesep, os.linesep, os.linesep))
self._test_readlines(mailbox._PartialFile(self._file, 0,
13 + 3 * len(os.linesep)))
def test_iteration(self):
self._file.write('____foo%sbar%sfred%sbob####' %
(os.linesep, os.linesep, os.linesep))
self._test_iteration(mailbox._PartialFile(self._file, 4,
17 + 3 * len(os.linesep)))
def test_seek_and_tell(self):
self._file.write('(((foo%sbar%s$$$' % (os.linesep, os.linesep))
self._test_seek_and_tell(mailbox._PartialFile(self._file, 3,
9 + 2 * len(os.linesep)))
def test_close(self):
self._file.write('&foo%sbar%s^' % (os.linesep, os.linesep))
self._test_close(mailbox._PartialFile(self._file, 1,
6 + 3 * len(os.linesep)))
## Start: tests from the original module (for backward compatibility).
FROM_ = "From some.body@dummy.domain Sat Jul 24 13:43:35 2004\n"
DUMMY_MESSAGE = """\
From: some.body@dummy.domain
......@@ -65,15 +1622,15 @@ class MaildirTestCase(unittest.TestCase):
# Test for regression on bug #117490:
# Make sure the boxes attribute actually gets set.
self.mbox = mailbox.Maildir(test_support.TESTFN)
self.assert_(hasattr(self.mbox, "boxes"))
self.assert_(len(self.mbox.boxes) == 0)
#self.assert_(hasattr(self.mbox, "boxes"))
#self.assert_(len(self.mbox.boxes) == 0)
self.assert_(self.mbox.next() is None)
self.assert_(self.mbox.next() is None)
def test_nonempty_maildir_cur(self):
self.createMessage("cur")
self.mbox = mailbox.Maildir(test_support.TESTFN)
self.assert_(len(self.mbox.boxes) == 1)
#self.assert_(len(self.mbox.boxes) == 1)
self.assert_(self.mbox.next() is not None)
self.assert_(self.mbox.next() is None)
self.assert_(self.mbox.next() is None)
......@@ -81,7 +1638,7 @@ class MaildirTestCase(unittest.TestCase):
def test_nonempty_maildir_new(self):
self.createMessage("new")
self.mbox = mailbox.Maildir(test_support.TESTFN)
self.assert_(len(self.mbox.boxes) == 1)
#self.assert_(len(self.mbox.boxes) == 1)
self.assert_(self.mbox.next() is not None)
self.assert_(self.mbox.next() is None)
self.assert_(self.mbox.next() is None)
......@@ -90,7 +1647,7 @@ class MaildirTestCase(unittest.TestCase):
self.createMessage("cur")
self.createMessage("new")
self.mbox = mailbox.Maildir(test_support.TESTFN)
self.assert_(len(self.mbox.boxes) == 2)
#self.assert_(len(self.mbox.boxes) == 2)
self.assert_(self.mbox.next() is not None)
self.assert_(self.mbox.next() is not None)
self.assert_(self.mbox.next() is None)
......@@ -108,12 +1665,99 @@ class MaildirTestCase(unittest.TestCase):
self.assertEqual(len(str(msg)), len(FROM_)+len(DUMMY_MESSAGE))
self.assertEqual(n, 1)
# XXX We still need more tests!
## End: classes from the original module (for backward compatibility).
_sample_message = """\
Return-Path: <gkj@gregorykjohnson.com>
X-Original-To: gkj+person@localhost
Delivered-To: gkj+person@localhost
Received: from localhost (localhost [127.0.0.1])
by andy.gregorykjohnson.com (Postfix) with ESMTP id 356ED9DD17
for <gkj+person@localhost>; Wed, 13 Jul 2005 17:23:16 -0400 (EDT)
Delivered-To: gkj@sundance.gregorykjohnson.com
Received: from localhost [127.0.0.1]
by localhost with POP3 (fetchmail-6.2.5)
for gkj+person@localhost (single-drop); Wed, 13 Jul 2005 17:23:16 -0400 (EDT)
Received: from andy.gregorykjohnson.com (andy.gregorykjohnson.com [64.32.235.228])
by sundance.gregorykjohnson.com (Postfix) with ESMTP id 5B056316746
for <gkj@gregorykjohnson.com>; Wed, 13 Jul 2005 17:23:11 -0400 (EDT)
Received: by andy.gregorykjohnson.com (Postfix, from userid 1000)
id 490CD9DD17; Wed, 13 Jul 2005 17:23:11 -0400 (EDT)
Date: Wed, 13 Jul 2005 17:23:11 -0400
From: "Gregory K. Johnson" <gkj@gregorykjohnson.com>
To: gkj@gregorykjohnson.com
Subject: Sample message
Message-ID: <20050713212311.GC4701@andy.gregorykjohnson.com>
Mime-Version: 1.0
Content-Type: multipart/mixed; boundary="NMuMz9nt05w80d4+"
Content-Disposition: inline
User-Agent: Mutt/1.5.9i
--NMuMz9nt05w80d4+
Content-Type: text/plain; charset=us-ascii
Content-Disposition: inline
This is a sample message.
--
Gregory K. Johnson
--NMuMz9nt05w80d4+
Content-Type: application/octet-stream
Content-Disposition: attachment; filename="text.gz"
Content-Transfer-Encoding: base64
H4sICM2D1UIAA3RleHQAC8nILFYAokSFktSKEoW0zJxUPa7wzJIMhZLyfIWczLzUYj0uAHTs
3FYlAAAA
--NMuMz9nt05w80d4+--
"""
_sample_headers = {
"Return-Path":"<gkj@gregorykjohnson.com>",
"X-Original-To":"gkj+person@localhost",
"Delivered-To":"gkj+person@localhost",
"Received":"""from localhost (localhost [127.0.0.1])
by andy.gregorykjohnson.com (Postfix) with ESMTP id 356ED9DD17
for <gkj+person@localhost>; Wed, 13 Jul 2005 17:23:16 -0400 (EDT)""",
"Delivered-To":"gkj@sundance.gregorykjohnson.com",
"Received":"""from localhost [127.0.0.1]
by localhost with POP3 (fetchmail-6.2.5)
for gkj+person@localhost (single-drop); Wed, 13 Jul 2005 17:23:16 -0400 (EDT)""",
"Received":"""from andy.gregorykjohnson.com (andy.gregorykjohnson.com [64.32.235.228])
by sundance.gregorykjohnson.com (Postfix) with ESMTP id 5B056316746
for <gkj@gregorykjohnson.com>; Wed, 13 Jul 2005 17:23:11 -0400 (EDT)""",
"Received":"""by andy.gregorykjohnson.com (Postfix, from userid 1000)
id 490CD9DD17; Wed, 13 Jul 2005 17:23:11 -0400 (EDT)""",
"Date":"Wed, 13 Jul 2005 17:23:11 -0400",
"From":""""Gregory K. Johnson" <gkj@gregorykjohnson.com>""",
"To":"gkj@gregorykjohnson.com",
"Subject":"Sample message",
"Mime-Version":"1.0",
"Content-Type":"""multipart/mixed; boundary="NMuMz9nt05w80d4+\"""",
"Content-Disposition":"inline",
"User-Agent": "Mutt/1.5.9i" }
_sample_payloads = ("""This is a sample message.
--
Gregory K. Johnson
""",
"""H4sICM2D1UIAA3RleHQAC8nILFYAokSFktSKEoW0zJxUPa7wzJIMhZLyfIWczLzUYj0uAHTs
3FYlAAAA
""")
def test_main():
test_support.run_unittest(MaildirTestCase)
tests = (TestMailboxSuperclass, TestMaildir, TestMbox, TestMMDF, TestMH,
TestBabyl, TestMessage, TestMaildirMessage, TestMboxMessage,
TestMHMessage, TestBabylMessage, TestMMDFMessage,
TestMessageConversion, TestProxyFile, TestPartialFile,
MaildirTestCase)
test_support.run_unittest(*tests)
if __name__ == "__main__":
if __name__ == '__main__':
test_main()
......@@ -313,6 +313,7 @@ Drew Jenkins
Flemming Kjr Jensen
Jiba
Orjan Johansen
Gregory K. Johnson
Simon Johnston
Evan Jones
Richard Jones
......
......@@ -99,6 +99,10 @@ Library
as ``walk_packages()`` to support working with packages that are either
in the filesystem or zip files.
- The mailbox module can now modify and delete messages from
mailboxes, in addition to simply reading them. Thanks to Gregory
K. Johnson for writing the code, and to the 2005 Google Summer of
Code for funding his work.
- The ``__del__`` method of class ``local`` in module ``_threading_local``
returned before accomplishing any of its intended cleanup.
......@@ -202,8 +206,8 @@ Core and builtins
especially long-running applications that, from time to time, temporarily
use a large number of small objects. Note that when Python returns an
arena to the platform C's ``free()``, there's no guarantee that the
platform C will in turn return that memory to the operating system. The
effect of the patch is to stop making that impossible, and in tests it
platform C library will in turn return that memory to the operating system.
The effect of the patch is to stop making that impossible, and in tests it
appears to be effective at least on Microsoft C and gcc-based systems.
Thanks to Evan Jones for hard work and patience.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment