The right way to email your users
Tags: notifications, email, sms, python
Published: 2024-07-02 14:53:50

---
title: The right way to email your users
published: true
description: Overcome common issues when email users from your web application, using open-source notification system Tattler. A tutorial from zero to integrated.
tags: notifications, email, sms, python
# cover_image: https://tattler.dev/images/logo/logo-dark.png
# Use a ratio of 100:42 for best results.
# published_at: 2024-06-14 13:32 +0000
---

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:

```python
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:

1. You want to include some special characters like '🙂' or 'schön'. The one-liner no longer works.
1. You need some conditional text for paying users. Which requires some **templating** logic.
1. Some users fail to receive your notifications because their **spam filter** distrusts your bare-bones emails. Figure out what they need and build it.
1. You want **branded** emails with your logo and stationery. You need to build complex MIME/multipart assemblies.
1. 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](https://tattler.dev) 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:

```python
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:

```bash
# 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:

```jinja
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:

{% embed http://asciinema.org/a/666512 %}

## Installation

Create this directory structure:

```bash
# 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:

```bash
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!

```bash
# 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:

```bash
#              [ 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](#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:

```bash
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.

```jinja
{# 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:

```jinja
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](https://caniemail.com) for help.

```jinja
{# 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:

```bash
#              [ 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:

```python
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 port `11503`
- Base URL of the API = `http://localhost:11503/notification/`
- POST request (not GET)!
- `website` is the scope name (see [Templates](#templates))
- `password_changed` is the template name (see [Templates](#templates))
- `user=foo@bar.com` tells tattler whom to send to

Here’s how to leverage this from a number of programming languages.

## Go

```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#

```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

```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

```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](https://docs.tattler.dev).

Tattler is [proudly open-source](https://github.com/tattler-community/tattler-community), 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](https://tattler.dev/#enterprise), plus more features like S/MIME, multilingualism and delivery with Telegram and WhatsApp.