The right way to email your users
Michele Mazzucchi
Posted on July 2, 2024
When you build a web application you need to email your users for password resets, orders placed and the like.
A trivial task, it seems. Here's a one-liner in django:
from django.core.mail import send_mail
send_mail("Password changed!",
f"Hey {request.user.first_name}!\n\nYour password changed...",
"support@yourservice.com",
[request.user.email],
fail_silently=False)
Reality kicks in
A one-liner. Problem solved, now moving on.
But soon after, issues arise:
- You want to include some special characters like 'π' or 'schΓΆn'. The one-liner no longer works.
- You need some conditional text for paying users. Which requires some templating logic.
- Some users fail to receive your notifications because their spam filter distrusts your bare-bones emails. Figure out what they need and build it.
- You want branded emails with your logo and stationery. You need to build complex MIME/multipart assemblies.
- You need to prevent delivery to actual users during development from your dev environment. You need environment-specific logic.
You can surely address each hurdle. Your one-liner grows into 10, 100, 1000 lines of code.
It may grow organically, adapted at every notification-sending point. Or structured - and get you to build your own DIY notification framework.
Notification system
You may build your DIY notification system, experiencing and overcoming a problem after the other.
But why go through the pain if the work is already done?
Tattler solves all the problems above and more. It's a lightweight service you deploy within minutes, and your notification one-liners stay a one-liner:
from tattler.client.tattler_py import send_notification
# trigger notification in Python code
send_notification('website', 'password_changed', request.user.email)
You can call tattler from any language and tech stack -- with one call to its REST API:
# trigger notification via REST API in any language
curl -XPOST http://localhost:11503/notification/website/password_changed?user=foo@bar.com
Notification templates look like this:
Hey {{ user_firstname }}! ππ»
A quick heads up that your password was changed.
If it was you, all good. If it wasn't, reply to this email without delay.
{% if user_account_type == 'premium' %}
P.S.: Thank you for supporting us with your premium account! π€©
{% endif %}
Deploying tattler
Install Tattler into an own folder ~/tattler_quickstart
, with this structure:
~/tattler_quickstart/
βββ conf/ # configuration files
βββ templates/ # templates for notifications
βββ venv/ # python virtual environment holding the code
Here's a terminal session performing the steps in this guide:
Installation
Create this directory structure:
# create the directory structure above
mkdir -p ~/tattler_quickstart
cd ~/tattler_quickstart
mkdir conf templates
# create and load a virtualenv to install into
python3 -m venv ~/tattler_quickstart/venv
. ~/tattler_quickstart/venv/bin/activate
# install tattler into it
pip install tattler
Configuration
Tattler's configuration is organized in a bunch of files (envdir), whose filename is the configuration key and content its value. We'll configure the following:
~/tattler_quickstart/
βββ conf/
βββ TATTLER_MASTER_MODE # actually deliver notifications
βββ TATTLER_SMTP_ADDRESS # IP:port of the SMTP server
βββ TATTLER_SMTP_AUTH # username:password SMTP credentials
Let's do it:
cd ~/tattler_quickstart/conf
echo 'production' > TATTLER_MASTER_MODE
# replace with your SMTP server
echo '127.0.0.1:25' > TATTLER_SMTP_ADDRESS
echo 'username:password' > TATTLER_SMTP_AUTH
chmod 400 TATTLER_SMTP_AUTH
Running tattler
At this point tattler is ready to run, so let's!
# run this from the terminal where you loaded your virtual environment
envdir ~/tattler_quickstart/conf tattler_server
And tattler will confirm:
INFO:tattler.server.pluginloader:Loading plugin PassThroughAddressbookPlugin (<class 'passthrough_addressbook_tattler_plugin.PassThroughAddressbookPlugin'>) from module passthrough_addressbook_tattler_plugin
INFO:tattler.server.tattlersrv_http:Using templates from /../tattler_quickstart/templates
INFO:tattler.server.tattlersrv_http:==> Meet tattler @ https://tattler.dev . If you like tattler, consider posting about it! ;-)
WARNING:tattler.server.tattlersrv_http:Tattler enterprise now serving at 127.0.0.1:11503
Done! β
Sending a test notification
Ask tattler to send a test notification with the tattler_notify
command:
# [ recipient ] [ scope ] [ event ] [ do send! ]
tattler_notify my@email.com demoscope demoevent -m production
This sends a demo notification embedded in tattler, so you don't need to write your own content. -m production
tells tattler to deliver to the actual recipient my@email.com
. Without it, tattler will safely operate in development mode and divert any delivery to a development address (configured in TATTLER_DEBUG_RECIPIENT_EMAIL
) so real users arenβt accidentally sent emails during development.
Proceed to your mailbox to find the result. If nothing is in, check the logs of tattler_server
. They should look like this:
[...]
INFO:tattler.server.sendable.vector_email:SMTP delivery to 127.0.0.1:25 completed successfully.
INFO:tattler.server.tattlersrv_http:Notification sent. [{'id': 'email:b386e396-7ad4-4d50-bc2c-1406bf6a8814', 'vector': 'email', 'resultCode': 0, 'result': 'success', 'detail': 'OK'}]
If they don't, you'll see an error description. Most likely your SMTP server is misconfigured, so check again Configuration and restart tattler_server
when fixed.
Next, you'll want to write your own content to notify users.
Templates
Write your own template to define what content to send upon an event like "password_changed".
Templates are folders organized in this directory structure:
~/tattler_quickstart/
βββ templates/ # base directory holding all notification templates
βββ website/ # an arbitrary 'scope' (ignore for now)
βββ password_changed/ # template to send when user changes password
βββ order_placed/ # template to send when user places an order
βββ ...
What does the content of the event template itself look like?
password_changed/ # arbitrary event name for the template
βββ email/
βββ body.html # template to expand for the HTML body
βββ body.txt # template to expand for the plain text body
βββ subject.txt # template to expand for the subject
Let's proceed to create the directories for one:
cd ~/tattler_quickstart
mkdir -p templates/password_changed/email
Plain text template
Now, edit file email/body.txt
with the content of the plain text email. This is seen by users of webmails or email applications that lack support for HTML emails.
{# file email/body.txt (this is a comment) #}
Hey {{ user_firstname }}! ππ»
A quick heads up that your password was changed.
If it was you, all good. If it wasn't, reply to this email without delay.
{% if user_account_type == 'premium' %}
P.S.: Thank you for supporting us with your premium account! π€©
{% endif %}
Subject
Next, edit file email/subject.txt
with the subject of the email:
Warning: password changed for account {{ user_email }}! β οΈ
Notice that the subject supports both non-ASCII characters and templating too! π
HTML template
Then, edit file email/body.html
with HTML content. Here's where you implement your company's stationery -- colors, logo, fonts, layout and all.
Make sure to stick to HTML constructs supported by email clients! That's MUCH less than your browser does. See caniemail.com for help.
{# file email/body.html (this is a comment) #}
<html>
<head><title>Your password was changed!</title></head>
<body>
<h1>Dear {{ user_firstname }}! ππ»</h1>
<p>A quick heads up that <strong>your password was changed</strong>!</p>
<p>If it was you, all good. If it wasn't, reply to this email without delay.</p>
{% if user_account_type == 'premium' %}
<p style="color: darkgray">P.S.: Thank you for supporting us with your premium account! π€©</p>
{% endif %}
</body>
</html>
Send your template
Now that you've got your own template, tell tattler to deliver it:
# [ recipient ] [ scope ] [ event ] [ do send! ]
tattler_notify my@email.com website password_changed -m production
Check your inbox and that's it!
Integration
Now that tattler runs and you provisioned your template, how to have it sent from your code?
Whenever your application wants to notify an event, like "password changed", you can fire it either:
- From python via tattler's native client API.
- From any other language via tattler's REST API.
Let's look at some concrete examples.
Python
Tattler is written in python, so python developers can use its native python API:
from tattler.client.tattler_py import send_notification
send_notification('website', 'password_changed', request.user.email)
REST API
This works with any language and tech stack. Simply make a POST request to 127.0.0.1:80
http://localhost:11503/notification/website/password_changed?user=foo@bar.com
[ API base URL ] [scope] [event name] [ recipient ]
Notice the following:
- Host
localhost
and TCP port11503
- Base URL of the API =
http://localhost:11503/notification/
- POST request (not GET)!
-
website
is the scope name (see Templates) -
password_changed
is the template name (see Templates) -
user=foo@bar.com
tells tattler whom to send to
Hereβs how to leverage this from a number of programming languages.
Go
package main
import (
"bytes"
"net/http"
)
func main() {
_, err := http.NewRequest("POST", "http://localhost:11503/notification/website/password_changed?user=my@email.com", bytes.NewBufferString(""))
if err != nil {
panic(err)
}
}
C
using System.Net.Http;
public class Program
{
public static void Main(string[] args)
{
using var client = new HttpClient();
var response = await client.PostAsync("http://localhost:11503/notification/website/password_changed?user=my@email.com", null);
}
}
Java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
public class App {
public static void main(String[] args) {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("http://localhost:11503/notification/website/password_changed?user=my@email.com"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
}
}
Swift
import Foundation
let url = URL(string: "http://localhost:11503/notification/website/password_changed?user=my@email.com")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = Data()
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let httpResponse = response as? HTTPURLResponse {
print("Response Code: \(httpResponse.statusCode)")
}
}
task.resume()
Conclusion
That's it. You installed and tested tattler within minutes, and integrated it into your code within a few more minutes.
As your needs grow, tattler smoothly accommodates them without bloating your codebase.
Advanced topics include passing variables to templates, sending SMS, having tattler plug-ins automatically retrieve user addresses or template variables. Find it all in tattler's thorough documentation.
Tattler is proudly open-source, with a vastly liberal license (BSD) allowing commercial use. And if your company's policies require support and maintenance for every dependency, tattler offers them in an enterprise subscription, plus more features like S/MIME, multilingualism and delivery with Telegram and WhatsApp.
Posted on July 2, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.