Email

Introduction

Websauna provides facilities to send out emails

  • Outgoing email is tied to the success of transaction - if your code fails in some point no email is sent
  • Rich-text HTML emails are supported with premailer package which will turn CSS styles to inline styles for emails
  • pyramid_mailer package provides low-level interface for outgoing email

Configuring email

Local development: no outgoing email traffic by default

When you run a development server, no email goes out by default. Instead it is printed into your console where ws-pserve is running. This is the default behavior of development.ini.

Setting up real SMTP service

For actual outgoing emails you need to have an SMTP service agreement from some of the providers. You may or may not want to use Postfix server as a local buffer. The default behavior of production.ini is to use local SMTP server at localhost:25.

Note

You need to change outbound email settings in development.ini if you want to test email out from your local laptop.

See Configuring outbound email below for more information.

Sending out email

Mixed HTML and plain text email

Websauna supports mixed HTML + plain text emails. Websauna email messages are assembled from three different templates with a specific naming convention. Email templates are not different from web page templates, same Jinja templating engine is utilized.

  • $message.body.html - HTML email body
  • $message.body.txt - Plain text email body
  • $message.subject.txt - Email subject

Below is a sample email.

email/welcome.subject.txt:

{% extends "email/base_subject.txt" %}
{% block subject %}Welcome to {{site_name}}{% endblock %}

email/welcome.body.txt:

Welcome to {{ site_name }}!

Thank you for signing up!
Please visit the link below to see how {{ site_name }} will make your life simpler.

{{ request.route_url('home') }}

email/welcome.body.html:

{% extends "email/base.html" %}

{% block content %}
    <p>
    Welcome to {{site_name}},
    </p>

    <p>
    Thank you for signing up! Please visit the link below to see how {{ site_name }} will make your life simpler.
    </p>

    <p style="text-align: center">
        <a class="btn-primary" href="{{ request.route_url('home') }}">Visit {{ site_name }}</a>
    </p>

{% endblock %}

To send out this email use websauna.system.mail.send_templated_mail():

from websauna.system.mail import send_templated_mail

def my_view(request):
    user = request.user
    send_templated_mail(request, [user.email], "email/welcome", context={})

Email and transaction bounderies

Email is send out only if the transaction commits. If the request fails (HTTP 500) and the transaction is aborted then no email is sent.

If you are doing email out from command line jobs or Tasks make sure you close your transactions properly or there is no email out.

If you are sending email outside the normal transaction lifecycle check out immediate parameter of websauna.system.mail.send_templated_mail():

# Do not wait for the commit
send_templated_mail(request, [user.email], "email/welcome", context={}, immediate=True)

Sender envelope header

If you want to have the email “To:” header to contain the full name of the receiver you can do the following.

TODO

Raw pyramid_mail API

Sending out test mail with raw pyramid_mailer:

from pyramid_mailer import get_mailer
from pyramid_mailer.message import Message

sender = request.registry.settings["mail.default_sender"]

message = Message(subject="pyramid_mailer test", sender="[email protected]", recipients=["[email protected]"], body="yyy")

mailer = get_mailer(request)
mailer.send_immediately(message)

HTML layout

To edit HTML layout and CSS styles make a copy of email/base.html to your application. Edit syles inside <style>.

Testing HTML layout

You can render a dummy HTML email in your browser by going to:

See websauna.sample_html_email configuration for more information.

Configuring outbound email

Below is an INI configuration example to send emails through Sparkpost. This will make pyramid_mailer directly to talk remote SMTP server. These settings are good for local development when you need to see the actual outbound email message content properly.

External service example:

[main]

# ...
# other settings go here
# ...

websauna.mailer = mail
mail.default_sender = [email protected]
mail.default_sender_name = Example Tech Corp
mail.tls = true
mail.host = smtp.sparkpostmail.com
mail.port = 587
mail.username = SMTP_Injection
mail.password = <your Sparkpost API token>

Local Postix example:

[main]

# ...
# other settings go here
# ...

websauna.mailer = mail
mail.host = localhost
mail.port = 25
mail.username =
mail.password =

For more complex production environment outbound email with local Postfix buffering, see outbound email chapter in Ansible playbook.

Testing outbound email from console

You can test outbound email in Python console (Notebook and IPython shell or ws-shell):

from pyramid_mailer import get_mailer
from pyramid_mailer.message import Message
from websauna.utils.time import now

sender = "[email protected]"
recipients = ["[email protected]"]
subject = "Test mail"
text_body = "This is a test message {}".format(now())
mailer = get_mailer(request)

message = Message(subject=subject, sender=sender, recipients=recipients, body=text_body)
message.validate()
mailer.send_immediately(message)

Tests and email

Checking if email has been sent by your view

This demostrators how to test if views accessed through Splinter browser have sent email.

Make sure your tests use stdout mailer, as set in your test.ini:

websauna.mailer = websauna.system.mail.mailer.ThreadFriendlyDummyMailer

Then follow the example to how to detect outgoing mail happening outside the main test thread:

import transaction

from websauna.tests.utils import create_user, EMAIL, PASSWORD
from websauna.tests.utils import wait_until
from websauna.system.mail.mailer import ThreadFriendlyDummyMailer


def test_invite_by_email(web_server, browser, dbsession):

    b = browser
    with transaction.manager:
        create_user(email=EMAIL, password=PASSWORD)

    # Reset test mailer at the beginnign of the test
    ThreadFriendlyDummyMailer.reset()

    # Login
    b.visit(web_server + "/login")
    b.fill("username", EMAIL)
    b.fill("password", PASSWORD)
    b.find_by_name("Log_in").click()

    # We should waiting for the payment m
    b.find_by_css("#nav-invite-friends").click()

    b.fill("email", "[email protected]")
    b.find_by_name("invite").click()

    # Transaction happens in another thread and mailer does do actual sending until the transaction is finished. We need to wait in the test main thread to see this to happen.
    wait_until(callback=lambda: len(ThreadFriendlyDummyMailer.outbox), expected=1)

Check if test code has sent email

This example shows how to check if test code itself has sent email. In this case, we call email sending event chain directly from unit test, not going through a test web server.

from sqlalchemy.orm.session import Session
from pyramid.registry import Registry
from pyramid_mailer.mailer import DummyMailer


from websauna.tests.utils import create_user, make_dummy_request, make_routable_request
from websauna.system.mail.utils import get_mailer


def test_push_render_email(dbsession: Session, registry, user_id):
    """Create a new activity and generates rendered email notification.."""

    # Create a request with route_url()
    request = make_routable_request(dbsession, registry)

    # Reset test mailer at the beginnign of the test
    mailer = get_mailer(registry)

    # Check we got a right type of mailer for our unit test
    assert isinstance(mailer, DummyMailer)
    assert len(mailer.outbox) == 0

    with transaction.manager:
        u = dbsession.query(User).get(user_id)

        # Create an activity
        a = create_activity(request, "demo_msg", {}, uuid4(), u)

        # Push it through notification channel
        channel = Email(request)
        channel.push_notification(a)

        # DummyMailer updates it outbox immediately, no need to wait transaction.commit
        assert len(mailer.outbox) == 1