outgoing — Common interface for multiple e-mail methods
GitHub | PyPI | Documentation | Issues | Changelog
Configuration
The Configuration File
outgoing
reads information on what sending method and parameters to use
from a TOML or JSON configuration file. The default configuration file is
TOML, and its location depends on your OS:
Linux |
|
macOS |
|
Windows |
|
Changed in version 0.5.0: Due to an upgrade to v3 of platformdirs
, the default configuration path
on macOS changed from ~/Library/Preferences/outgoing/outgoing.toml
to ~/Library/Application Support/outgoing/outgoing.toml
.
To find the exact path on your system, after installing outgoing
, run:
python3 -c "from outgoing import get_default_configpath; print(get_default_configpath())"
Within the configuration file, all of the outgoing
settings are contained
within a table named “outgoing
”. This table must include at least a
method
key giving the name of the sending method to use. The rest of the
table depends on the method chosen (see below). Unknown or inapplicable keys
in the table are ignored.
File & directory paths in the configuration file may start with a tilde (~
)
to refer to a path in the user’s home directory. Any relative paths are
resolved relative to the directory containing the configuration file.
Sending Methods
command
The command
method sends an e-mail by passing it as input to a command
(e.g., sendmail, sold separately).
Configuration fields:
command
string or list of strings (optional)Specify the command to run to send e-mail. This can be either a single command string that will be interpreted by the shell or a list of command arguments that will be executed directly without any shell processing. The default command is
sendmail -i -t
.Note
Relative paths in the command will not be resolved by
outgoing
(unlike other paths in the configuration file), as it is not possible to reliably determine what is a path and what is not.
Example command
configuration:
[outgoing]
method = "command"
command = ["/usr/local/bin/mysendmail", "-i", "-t"]
Another sample configuration:
[outgoing]
method = "command"
# A single string will be interpreted by the shell, so metacharacters like
# pipes have their special meanings:
command = "my-mail-munger | ~/some/dir/mysendmail"
smtp
The smtp
method sends an e-mail to a server over SMTP.
Configuration fields:
host
string (required)The domain name or IP address of the server to connect to
ssl
boolean or"starttls"
(optional)true
: Use SSL/TLS from the start of the connectionfalse
(default): Don’t use SSL/TLS"starttls"
: After connecting, switch to SSL/TLS with the STARTTLS command
port
integer (optional)The port on the server to connect to; the default depends on the value of
ssl
:true
— 465false
— 25"starttls"
— 587
username
string (optional)Username to log into the server with
password
password (optional)Password to log into the server with; can be given as either a string or a password specifier (see “Passwords”)
netrc
boolean or filepath (optional)If
true
, read the username & password from~/.netrc
instead of specifying them in the configuration file. If a filepath, read the credentials from the given netrc file. Iffalse
, do not use a netrc file.
Example smtp
configuration:
[outgoing]
method = "smtp"
host = "mx.example.com"
ssl = "starttls"
username = "myname"
password = { "file" = "~/secrets/smtp-password" }
Another sample configuration:
[outgoing]
method = "smtp"
host = "mail.nil"
port = 1337
ssl = true
# Read username & password from the "mail.nil" entry in this netrc file:
netrc = "~/secrets/net.rc"
mbox
The mbox
method appends e-mails to an mbox file on the local machine.
Configuration fields:
path
filepath (required)The location of the mbox file. If the file does not exist, it will be created when the sender object is entered.
Example mbox
configuration:
[outgoing]
method = "mbox"
path = "~/MAIL/inbox"
maildir
The maildir
method adds e-mails to a Maildir mailbox directory on the local
machine.
Configuration fields:
path
directory path (required)The location of the Maildir mailbox. If the directory does not exist, it will be created when the sender object is entered.
folder
string (optional)A folder within the Maildir mailbox in which to place e-mails
mh
The mh
method adds e-mails to an MH mailbox directory on the local machine.
Configuration fields:
path
directory path (required)The location of the MH mailbox. If the directory does not exist, it will be created when the sender object is entered.
folder
string or list of strings (optional)A folder within the Maildir mailbox in which to place e-mails; can be either the name of a single folder or a path through nested folders & subfolders
Example configuration:
[outgoing]
method = "mh"
path = "~/mail"
# Place e-mails inside the "work" folder inside the "important" folder:
folder = ["important", "work"]
mmdf
The mmdf
method adds e-mails to an MMDF mailbox file on the local machine.
Configuration fields:
path
filepath (required)The location of the MMDF mailbox. If the file does not exist, it will be created when the sender object is entered.
babyl
The babyl
method adds e-mails to a Babyl mailbox file on the local machine.
Configuration fields:
path
filepath (required)The location of the Babyl mailbox. If the file does not exist, it will be created when the sender object is entered.
null
Goes nowhere, does nothing, ignores all configuration keys.
Example null
configuration:
[outgoing]
# Just send my e-mails into a black hole
method = "null"
Passwords
When a sending method calls for a password, API key, or other secret, there are several ways to specify the value.
Using a string, naturally, supplies the value of that string as the password:
password = "hunter2"
Alternatively, passwords may instead be looked up in external resources. This is done by setting the value of the password field to a table with a single key-value pair, where the key identifies the password lookup scheme and the value is either a string or a sub-table, depending on the scheme.
The builtin password schemes are as follows. Extension packages can define additional password schemes.
base64
For slightly more security than a plaintext password, a password can be stored
in base64 by specifying a table with a single base64
key and the encoded
password as the value:
password = { base64 = "aHVudGVyMg==" }
Base64 passwords must decode to UTF-8 text.
file
A password can be read from a file by specifying a table with a single file
key and the filepath as the value:
password = { file = "path/to/file" }
The entire contents of the file, minus any leading or trailing whitespace, will then be used as the password. As with paths elsewhere in the configuration file, the path may start with a tilde, and relative paths are resolved relative to the directory containing the configuration file.
env
A password can be read from an environment variable by specifying a table with
a single env
key and the name of the environment variable as the value:
password = { env = "PROTOCOL_PASSWORD" }
dotenv
Passwords can be read from a key in a .env
-style file as supported by
python-dotenv like so:
password = { dotenv = { key = "NAME_OF_KEY_IN_FILE", file = "path/to/file" } }
The file
path is resolved following the same rules as other paths. If the
file
field is omitted, the given key will be looked up in a file named
.env
in the same directory as the configuration file.
keyring
Passwords can be retrieved from the system keyring using keyring. The basic format is:
password = { keyring = { service = "host_or_service_name", username = "your_username" } }
If the service
key is omitted, the value will default to the sending
method’s host value, if it has one; likewise, an omitted username
will
default to the username for the sending method, if there is one. A specific
keyring backend can be specified with the backend
key, and the directory
from which to load the backend can be specified with the keyring-path
key.
Core Python API
Functions
outgoing
provides the following functions for constructing e-mail sender
objects. Once you have a sender object, simply use it in a context manager to
open it up, and then call its send()
method with each
email.message.EmailMessage
object you want to send. See Examples for
examples.
- outgoing.from_config_file(path: str | bytes | PathLike[str] | PathLike[bytes] | None = None, section: str | None = 'outgoing', fallback: bool = True) Sender [source]
Read configuration from the table/field
section
(default “outgoing
”) in the file atpath
(default: the path returned byget_default_configpath()
) and construct a sender object from the specification. The file may be either TOML or JSON (type detected based on file extension). Ifsection
isNone
, the entire file, rather than only a single field, is used as the configuration. Iffallback
is true, the file is not the default config file, and the file either does not exist or does not contain the given section, fall back to reading from the default section of the default config file.- Raises:
InvalidConfigError – if the configuration is invalid
MissingConfigError – if no configuration file or section is present
- outgoing.from_dict(data: Mapping[str, Any], configpath: str | bytes | PathLike[str] | PathLike[bytes] | None = None) Sender [source]
Construct a sender object using the given
data
as the configuration. Ifconfigpath
is given, any paths in thedata
will be resolved relative toconfigpath
’s parent directory; otherwise, they will be resolved relative to the current directory.data
should not contain a"configpath"
key; such an entry will be discarded.- Raises:
InvalidConfigError – if the configuration is invalid
- outgoing.get_default_configpath() Path [source]
Returns the location of the default config file (regardless of whether it exists) as a
pathlib.Path
object
Sender Objects
- class outgoing.Sender[source]
Sender
is aProtocol
implemented by sender objects. The protocol requires the following behavior:Sender objects can be used as context managers, and their
__enter__
methods returnself
.Within its own context, calling a sender’s
send(msg: email.message.EmailMessage)
method sends the given e-mail.
- __exit__(exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None) bool | None [source]
- send(msg: EmailMessage) Any [source]
Send
msg
or raise an exception if that’s not possible
In addition to the base protocol, outgoing
’s built-in senders are
reentrant and reusable as context managers, and their send()
methods
can be called outside of a context.
Exceptions
- exception outgoing.Error[source]
Bases:
Exception
The superclass for all exceptions raised by
outgoing
- exception outgoing.InvalidConfigError(details: str, configpath: str | bytes | PathLike[str] | PathLike[bytes] | None = None)[source]
Bases:
Error
Raised on encountering an invalid configuration structure
- exception outgoing.InvalidPasswordError(details: str, configpath: str | bytes | PathLike[str] | PathLike[bytes] | None = None)[source]
Bases:
InvalidConfigError
Raised on encountering an invalid password specifier or when no password can be determined from a specifier
- exception outgoing.MissingConfigError(configpaths: Sequence[str | bytes | PathLike[str] | PathLike[bytes]])[source]
Bases:
Error
Raised when no configuration section can be found in any config files
- exception outgoing.NetrcLookupError[source]
Bases:
Error
Raised by
lookup_netrc()
on failure to find a match in a netrc file
Command-Line Program
outgoing [<options>] [<msg-file> ...]
You can use outgoing
to send fully-composed e-mails directly from the
command line with the outgoing command. Save your e-mail as a
complete message/rfc822 document and then run outgoing
path/to/email/file
to send it using the configuration in the default config
file (or specify another config file with the --config
option). Multiple
files can be passed to the command at once to send multiple e-mails. If no
files are specified on the command line, the command reads an e-mail from
standard input.
Options
- -c <file>, --config <file>
Specify a configuration file to use instead of the default configuration file
- -E <file>, --env <file>
New in version 0.2.0.
Load environment variables from the given
.env
file before reading the configuration file. By default, environment variables are loaded from the first file named “.env
” found by searching from the current directory upwards.
- -l <level>, --log-level <level>
New in version 0.2.0.
Set the logging level to the given value; default:
INFO
. The level can be given as a case-insensitive level name or as a numeric value.
- -s <key>, --section <key>
New in version 0.2.0.
Read the configuration from the given table or key in the configuration file; defaults to “
outgoing
”
- --no-section
New in version 0.2.0.
Read the configuration fields from the top level of the configuration file instead of expecting them to all be contained below a certain table/key
Available Extensions
It is possible to write packages for extending outgoing
with support for
further sending methods and password schemes. See Writing Extensions
for how to do so.
If you develop an extension package, please submit a PR so it can be listed on this page!
Sending Methods
outgoing-mailgun — Supports sending e-mail via Mailgun
Password Schemes
None yet. Be the first!
Writing Extensions
Writing Sending Methods
A sending method is implemented as a callable (usually a class) that accepts
the fields of a configuration structure as keyword arguments and returns a
sender object. The keyword arguments include the
method
field and also include a configpath
key specifying a
pathlib.Path
pointing to the configuration file (or None
if from_dict()
was called without setting a configpath
). Callables should accept any
keyword argument and ignore any that they do not recognize.
For example, given the following configuration:
[outgoing]
method = "foobar"
server = "www.example.nil"
password = { env = "SECRET_TOKEN" }
comment = "I like e-mail!"
the callable registered for the “foobar” method will be called with the following keyword arguments:
**{
"method": "foobar",
"server": "www.example.nil",
"password": {"env": "SECRET_TOKEN"},
"comment": "I like e-mail!",
"configpath": Path("path/to/configfile"),
}
If the configuration passed to a callable is invalid, the callable should raise
an InvalidConfigError
.
Callables can resolve password fields by passing them to resolve_password()
or by using pydantic and the Password
type. Callables should resolve paths
relative to the directory containing configpath
by using resolve_path()
or by using pydantic and the Path
, FilePath
, and/or DirectoryPath
types.
The last step of writing a sending method is to package it in a Python project
and declare the callable as an entry point in the outgoing.senders
entry
point group so that users can install & access it. For example, if your
project is built using setuptools, and the callable is a FooSender
class in
the foobar.senders
module, and you want it to be usable by setting method
= "foo"
, add the following to your setup.py
:
setup(
...
entry_points={
"outgoing.senders": [
"foo = foobar.senders:FooSender",
],
},
...
)
Writing Password Schemes
A password scheme is implemented as a function that takes the value
part of
a password = { scheme = value }
entry as an argument and returns the
corresponding password as a str
. If the function additionally accepts
arguments named host
, username
, and/or configpath
(either
explicitly or via **kwargs
), the corresponding values passed to
resolve_password()
will be forwarded to the scheme function.
If the value
structure is invalid, or if no password can be found, the
function should raise an InvalidPasswordError
.
The last step of writing a password scheme is to package it in a Python project
and declare the function as an entry point in the outgoing.password_schemes
entry point group so that users can install & access it. For example, if your
project is built using setuptools, and the function is foo_scheme()
in the
foobar.passwords
module, and you want it to be usable by writing password
= { foo = some-value }
, add the following to your setup.py
:
setup(
...
entry_points={
"outgoing.password_schemes": [
"foo = foobar.passwords:foo_scheme",
],
},
...
)
Changelog
v0.6.1 (in development)
Support platformdirs v4.0
Migrated from setuptools to hatch
v0.6.0 (2023-10-30)
Support python-dotenv v1.0
Always read JSON configuration files using UTF-8 encoding
Always read files for the “file” password method using UTF-8 encoding
Support Python 3.12
Correct the default Linux config file location listed in the README
Update pydantic to v2.0
v0.5.0 (2023-02-09)
Update
platformdirs
dependency to v3. This is a breaking change on macOS, where the default configuration path changes from~/Library/Preferences/outgoing/outgoing.toml
back to~/Library/Application Support/outgoing/outgoing.toml
.
v0.4.0 (2022-10-25)
Drop support for Python 3.6
Support Python 3.11
Use
tomllib
on Python 3.11
v0.3.2 (2022-09-03)
Overload
Password.__eq__
so that instances continue to compare equal topydantic.SecretStr
instances even under pydantic 1.10
v0.3.1 (2022-01-02)
Support tomli 2.0
v0.3.0 (2021-10-31)
Support Python 3.10
Replaced
entrypoints
dependency withimportlib-metadata
Replaced
appdirs
dependency withplatformdirs
. This is a breaking change on macOS, where the default configuration path changes from~/Library/Application Support/outgoing/outgoing.toml
to~/Library/Preferences/outgoing/outgoing.toml
.
v0.2.5 (2021-09-27)
outgoing.errors.UnsupportedEmailError
is now re-exported asoutgoing.UnsupportedEmailError
like all the other exception classes
v0.2.4 (2021-08-02)
Update for tomli 1.2.0
v0.2.3 (2021-07-04)
Read TOML files in UTF-8
v0.2.2 (2021-07-02)
Switch from toml to tomli
v0.2.1 (2021-05-12)
Support Click 8
v0.2.0 (2021-03-14)
Require the
port
field ofSMTPSender
to be non-negativeMark
Sender
asruntime_checkable
and export itGave the outgoing command
--section
,--no-section
, and--log-level
optionsAdded logging to built-in sender classes
The outgoing command now loads settings from
.env
files and has an--env
option
v0.1.0 (2021-03-06)
Initial release
outgoing
provides a common interface to multiple different e-mail sending
methods (SMTP, sendmail, mbox, etc.). Just construct a sender from a
configuration file or object, pass it an EmailMessage
instance, and let the magical internet daemons take care of the rest.
outgoing
itself provides support for only basic sending methods; additional
methods are provided by extension packages.
Installation
outgoing
requires Python 3.7 or higher. Just use pip for Python 3 (You have pip, right?) to install
outgoing
and its dependencies:
python3 -m pip install outgoing
Examples
A sample configuration file:
[outgoing]
method = "smtp"
host = "mx.example.com"
ssl = "starttls"
username = "myname"
password = { file = "~/secrets/smtp-password" }
Sending an e-mail based on a configuration file:
from email.message import EmailMessage
import outgoing
# Construct an EmailMessage object the standard Python way:
msg = EmailMessage()
msg["Subject"] = "Meet me"
msg["To"] = "my.beloved@love.love"
msg["From"] = "me@here.qq"
msg.set_content(
"Oh my beloved!\n"
"\n"
"Wilt thou dine with me on the morrow?\n"
"\n"
"We're having hot pockets.\n"
"\n"
"Love, Me\n"
)
# Construct a sender object based on the default config file (assuming it's
# populated)
with outgoing.from_config_file() as sender:
# Now send that letter!
sender.send(msg)
As an alternative to using a configuration file, you can specify an explicit
configuration by passing the configuration structure to the
outgoing.from_dict()
method, like so:
from email.message import EmailMessage
import outgoing
# Construct an EmailMessage object using the eletter library
# <https://github.com/jwodder/eletter>:
from eletter import compose
msg1 = compose(
subject="No.",
to=["me@here.qq"],
from_="my.beloved@love.love",
text=(
"Hot pockets? Thou disgusteth me.\n"
"\n"
"Pineapple pizza or RIOT.\n"
),
)
msg2 = compose(
subject="I'd like to place an order.",
to=["pete@za.aa"],
from_="my.beloved@love.love",
text="I need the usual. Twelve Hawaiian Abominations to go, please.\n",
)
SENDING_CONFIG = {
"method": "smtp",
"host": "smtp.love.love",
"username": "my.beloved",
"password": {"env": "SMTP_PASSWORD"},
"ssl": "starttls",
}
with outgoing.from_dict(SENDING_CONFIG) as sender:
sender.send(msg1)
sender.send(msg2)