Skip to content

Using the SMTP bridge server

This library ships with a simple SMTP server implementation based on aiosmtpd. This can be used to provide a "sidecar" container for applications which can only use SMTP to send email. The SMTP server is unauthenticated and so make sure to deploy it only in appropriate environments.

Important

In order to use the SMTP server you must have installed the library with the smtp extra, e.g. using pip install ucam-user-notify[smtp].

Although you can set the From address to whatever you like when sending email via SMTP, it must still match one of the allowed From addresses for the service.

Running the SMTP bridge via docker

The User Notify client library ships with a pre-built docker image which implements a SMTP bridge.

Running the docker image on ARM64 or Apple Silicon machines

If you're running Docker Desktop on Mac, you'll need to enable the host network driver first. You'll also need to add --platform linux/amd64 to the docker command line flags to use the image built for AMD64.

You can run the image locally, after running gcloud auth application-default login, using the following command:

docker run --rm --net host \
  -v ~/.config/gcloud/application_default_credentials.json:/app/credentials.json:ro \
  -e GOOGLE_APPLICATION_CREDENTIALS=/app/credentials.json \
  -e USER_NOTIFY_SERVICE_NAME=testing-development \
  -e USER_NOTIFY_IMPERSONATE_SERVICE_ACCOUNT=service-testing@user-notify-devel-2ff1dc8d.iam.gserviceaccount.com \
  -e USER_NOTIFY_ENVIRONMENT=development \
  -e USER_NOTIFY_SMTP_PORT=8025 \
  europe-west2-docker.pkg.dev/user-notify-meta-251d3b6f/public/ucam-user-notify/smtp-bridge:1.4.20

You can send a test email via this server using this client script:

SMTP sidecar client example
#!/usr/bin/env python
"""
An example of sending email via the unauthenticated SMTP server.

"""

import smtplib

# Note that the From address must correspond to one which this service
# is allowed to send from.
client = smtplib.SMTP("127.0.0.1", 8025)
client.sendmail(
    "testing@ses.devel.user-notify.gcp.uis.cam.ac.uk",
    "success@simulator.amazonses.com",
    "Subject: Test\n\nThis is a test email.",
)
client.quit()

# vim:tw=70

The following environment variables can be used to configure behaviour:

  • USER_NOTIFY_SERVICE_NAME (required) - The name of the service as registered with User Notify.
  • USER_NOTIFY_SMTP_PORT (optional) - The port which the service will listen on. Default: 1025.
  • USER_NOTIFY_IMPERSONATE_SERVICE_ACCOUNT (optional) - The email address identifier of a Google Service Account to impersonate when authenticating to User Notify.
  • USER_NOTIFY_ROLE_SESSION_NAME (optional) - Session name to use when assuming the AWS Role used to send email.
  • USER_NOTIFY_ENVIRONMENT (optional) - The variant of User Notify to use. Ordinarily you will not need to set this variable.

Important

By design, the server will only listen on the local loopback interface. This is because it is intended to be deployed as a "sidecar" container in Cloud Run and be accessible only to the main service container.

Running the SMTP bridge via the aiosmtpd command

If the smtp extra was installed then the aiosmtpd command-line program should be available. This can be used to start a SMTP server. Pass the registered User Notify service name on the command line.

USER_NOTIFY_ENVIRONMENT=development \
USER_NOTIFY_IMPERSONATE_SERVICE_ACCOUNT=service-testing@user-notify-devel-2ff1dc8d.iam.gserviceaccount.com \
aiosmtpd \
  --debug --listen 127.0.0.1:8025 --nosetuid \
  --class ucam_user_notify.aiosmtpd.SMTPHandler \
  testing-development

Set the USER_NOTIFY_IMPERSONATE_SERVICE_ACCOUNT environment variable to impersonate a Google Service Account when initialising the session.

Set the USER_NOTIFY_ROLE_SESSION_NAME environment variable to override the default AWS IAM Role session name when initialising the session.

Set the USER_NOTIFY_ENVIRONMENT environment variable to use a non-production instance of the User Notify service.

Important

When running in a sidecar container with workload identity you ordinarily should not need to set any of the USER_NOTIFY_... environment variables.

Running the SMTP bridge from Python

Note

This is currently the only way to require LOGIN or PLAIN authentication to the SMTP server.

The following is an example of a SMTP server which listens on a hard-coded bind address and port and forwards incoming email via User Notify. Authentication is configured with a static username and password.

Authenticated SMTP bridge server example
#!/usr/bin/env python
"""
An example of creating an unauthenticated SMTP server which uses User
Notify to send email.

NOTE: ucam-user-notify must be installed with the 'smtp' extra in
order to use the SMTP server.

Once started, you can test the server via the examples/smtp_client.py
script.
"""

import logging
import time
from typing import Optional

from aiosmtpd.controller import Controller

from ucam_user_notify import Session
from ucam_user_notify.aiosmtpd import (
    SMTPHandler,
    UsernamePasswordAuthenticator,
)

# ID of service as registered with the user notify service.
SERVICE_ID = "testing-development"

# Host and port to bind SMTP server to.
SMTP_HOST = "127.0.0.1"
SMTP_PORT = 8025

# Credentials required for authentication.
USERNAME = "example-user"
PASSWORD = "example-password"

# If you're running this code from a Cloud Run service, do not use
# impersonation and instead set this to None or omit it.
IMPERSONATE_GOOGLE_SERVICE_ACCOUNT: Optional[str] = (
    "service-testing@user-notify-devel-2ff1dc8d.iam.gserviceaccount.com"
)

# Create a logging object and configure logging.
LOG = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)

# A session will perform some one-time authentication and fetch of
# configuration when constructed so that this overhead is not present
# for each call to send_email().
LOG.info("Creating User Notify session.")
session = Session.for_service(
    SERVICE_ID,
    impersonate_service_account=IMPERSONATE_GOOGLE_SERVICE_ACCOUNT,
    # DO NOT SET environment WHEN YOU USE THIS LIBRARY. THIS IS ONLY
    # HERE TO ENSURE THAT THIS EXAMPLE CANNOT SEND EMAIL OUTSIDE OF
    # THE EMAIL SANDBOX.
    environment="development",
)

if session.default_from_address is not None:
    LOG.info(f"Default from address: {session.default_from_address}")

# Create the SMTP handler for the User Notify session and start the
# SMTP server.
LOG.info(f"Starting SMTP relay server on {SMTP_HOST}:{SMTP_PORT}.")
handler = SMTPHandler(session)
controller = Controller(
    handler,
    hostname=SMTP_HOST,
    port=SMTP_PORT,
    # We do require auth but we don't require TLS. This causes
    # aiosmtpd to (rightfully) generate a warning.
    authenticator=UsernamePasswordAuthenticator(USERNAME, PASSWORD),
    auth_require_tls=False,
    auth_required=True,
)
controller.start()

# Sleep while the server runs. Ensure that the server is stopped on,
# e.g., a KeyboardInterrupt.
try:
    while True:
        time.sleep(3600)
except KeyboardInterrupt:
    LOG.info("Keyboard interrupt received.")
finally:
    LOG.info("Stopping SMTP relay server.")
    controller.stop()

# vim:tw=70

You can send a test email via this server using this client script:

SMTP client example
#!/usr/bin/env python
"""
Companion to examples/smtp_server.py. An example of sending email via
the SMTP server.

"""

import smtplib

# Note that the From address must correspond to one which this service
# is allowed to send from.
client = smtplib.SMTP("127.0.0.1", 8025)
client.login("example-user", "example-password")
client.sendmail(
    "testing@ses.devel.user-notify.gcp.uis.cam.ac.uk",
    "success@simulator.amazonses.com",
    "Subject: Test\n\nThis is a test email.",
)
client.quit()

# vim:tw=70