Skip to content

Email System

The Obed email system sends HTML emails using Markdown templates. This part of the documentation explains how to use and extend the email functionality.

Learn More About This Technique

For a detailed explanation of the Markdown-powered email approach, see the blog post: Markdown-Powered Emails in Django

Overview

The email system converts Markdown templates into styled HTML emails with GitHub Flavored Markdown styling. This approach allows you to write emails in simple Markdown while delivering professional-looking HTML to recipients.

Key benefits:

  • Write emails in familiar Markdown syntax
  • Automatic HTML conversion with professional styling
  • CSS inlined for maximum email client compatibility
  • Reusable across the application

Quick Start

Sending a Simple Email

python
from obed.core.mail import send_email

send_email(
    subject="Welcome to Obed",
    to_email_list=["user@example.com"],
    template="emails/welcome.md",
    context={"name": "John"},
    md_to_html=True,
)

Creating an Email Template

Create a Markdown file in your app's templates directory:

markdown
# Hello {{ name }}

Welcome to **Obed**! We're excited to have you on board.

## Getting Started

Here are some helpful resources:

- [User Guide](https://example.com/guide)
- [Support](https://example.com/support)

---

Questions? Reply to this email anytime.

How It Works

Architecture

The email system consists of three main components:

  1. Email Utility (obed/core/mail.py) - Core function for sending emails
  2. Markdown Processor - Converts Markdown to HTML using pycmarkgfm
  3. CSS Inliner - Uses premailer to inline styles for email clients

Processing Flow

Markdown Template → Django Template Rendering → Markdown to HTML →
CSS Styling → CSS Inlining → Email Sent

API Reference

send_email()

Sends an email with optional Markdown-to-HTML conversion.

Location: obed/core/mail.py

Parameters:

  • subject (str) - Email subject line
  • to_email_list (List[str]) - List of recipient email addresses
  • template (str) - Path to template file (relative to template directories)
  • context (Optional[dict]) - Template context variables (default: {})
  • md_to_html (Optional[bool]) - Enable Markdown conversion (default: False)

Returns: None

Example:

python
send_email(
    subject="Password Reset",
    to_email_list=["user@example.com"],
    template="users/password_reset_email.md",
    context={
        "user": user,
        "reset_link": "https://example.com/reset/abc123",
    },
    md_to_html=True,
)

Creating Email Templates

Template Location

Place email templates in your Django app's templates directory following Django's template structure. For example, user-related emails go in the users app:

obed/
├── users/
│   └── templates/
│       └── users/
│           ├── password_reset_email.md
│           └── welcome_email.md
├── schedules/
│   └── templates/
│       └── schedules/
│           └── assignment_notification.md
└── core/
    └── templates/
        └── emails/
            └── base_notification.md

Template Organization

Group templates by app and feature for better maintainability. This follows Django's app-based structure and makes templates easier to find.

Markdown Syntax

Use standard GitHub Flavored Markdown:

markdown
# Heading 1

## Heading 2

**Bold text** and _italic text_

- Bulleted lists
- Work great

1. Numbered lists
2. Also supported

---

[Links](https://example.com) are clickable

> Blockquotes for emphasis

Django Template Variables

Combine Markdown with Django template syntax:

markdown
Hello {{ user.first_name }},

{% if urgent %}
**URGENT:** This requires immediate attention.
{% endif %}

Your assignment for **{{ event.title }}** is confirmed.

{% for task in tasks %}

- {{ task.name }}
  {% endfor %}

Customizing Email Styles

Default Styling

Emails use GitHub Markdown CSS (obed/assets/css/github-markdown.min.css) which provides:

  • Clean, professional appearance
  • Responsive design for mobile devices
  • Familiar GitHub-style formatting

Modifying Styles

The email stylesheet is minified for performance. To customize it:

  1. Get the source CSS from the original repository
  2. Make your changes to the unminified version
  3. Minify the CSS using a tool like CSS Minifier or via command line:
    bash
    npx csso github-markdown.css -o github-markdown.min.css
  4. Replace obed/assets/css/github-markdown.min.css with your minified version
  5. Restart the application (CSS is loaded at runtime)

Email Client Compatibility

Keep CSS simple and inline-friendly. Complex CSS may not render in all email clients. Test your changes across multiple email clients.

Source Information

See obed/assets/css/README.md for details about the stylesheet source and licensing.

Password Reset Example

This example shows how the password reset feature uses the email system.

Custom Form

Create a form that overrides send_mail():

python
# obed/users/forms.py
from django.contrib.auth.forms import PasswordResetForm as BasePasswordResetForm
from obed.core.mail import send_email as send_html_email

class PasswordResetForm(BasePasswordResetForm):
    def send_mail(self, subject_template_name, email_template_name,
                  context, from_email, to_email, html_email_template_name=None):
        from django.template import loader

        subject = loader.render_to_string(subject_template_name, context)
        subject = "".join(subject.splitlines())

        send_html_email(
            subject=subject,
            to_email_list=[to_email],
            template=email_template_name,
            context=context,
            md_to_html=True,
        )

View Configuration

Use the custom form in your view:

python
# obed/users/views.py
from django.contrib.auth import views as auth_views
from .forms import PasswordResetForm

class PasswordResetView(auth_views.PasswordResetView):
    form_class = PasswordResetForm

URL Configuration

python
# obed/users/urls.py
path(
    "password-reset/",
    PasswordResetView.as_view(
        email_template_name="users/password_reset_email.md",
    ),
    name="password_reset",
)

Testing

Unit Tests

Test the email utility function:

python
from django.core import mail
from django.test import TestCase
from obed.core.mail import send_email

class EmailTests(TestCase):
    def test_send_plain_email(self):
        send_email(
            subject="Test",
            to_email_list=["test@example.com"],
            template="test_template.txt",
            context={"name": "John"},
        )

        self.assertEqual(len(mail.outbox), 1)
        self.assertEqual(mail.outbox[0].subject, "Test")
        self.assertIn("John", mail.outbox[0].body)

Manual Testing with MailDev

  1. Start MailDev: docker compose up maildev
  2. Trigger an email action (e.g., password reset)
  3. View email at http://localhost:1080

MailDev Configuration

MailDev runs on port 1080 for the web UI and port 1025 for SMTP. Make sure both ports are exposed in your docker-compose.yml.

Troubleshooting

Emails Not Sending

Check SMTP configuration:

python
# obed/settings/dev.py
EMAIL_HOST = "localhost"
EMAIL_PORT = 1025

Verify MailDev is running:

bash
docker compose ps maildev

Quick Fix

If MailDev isn't running, start it with docker compose up -d maildev. The -d flag runs it in the background.

Markdown Not Converting

Ensure md_to_html=True:

python
send_email(..., md_to_html=True)  # Required for Markdown conversion

Common Mistake

Forgetting to set md_to_html=True is the most common reason emails appear as plain Markdown instead of styled HTML.

Styling Issues

Check CSS file exists:

bash
ls -lh obed/assets/css/github-markdown.min.css

Verify file path in code:

The path is relative to obed/core/mail.py:

python
css_path = os.path.join(
    os.path.dirname(os.path.dirname(__file__)),
    "assets/css/github-markdown.min.css",
)

Production Deployment

Site Configuration

Update the Django Site record for correct domain in emails:

python
from django.contrib.sites.models import Site

site = Site.objects.get(id=1)
site.domain = "yourdomain.com"
site.name = "Your App Name"
site.save()

Or update via Django admin at /admin/sites/site/.

Important for Production

Failing to update the site domain will result in emails containing localhost:8000 or example.com instead of your actual domain.

Email Backend

Switch to a production email backend:

python
# obed/settings/production.py
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "smtp.sendgrid.net"
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = env("SENDGRID_USERNAME")
EMAIL_HOST_PASSWORD = env("SENDGRID_PASSWORD")

Best Practices

Template Organization

  • Group templates by app and feature
  • Use descriptive filenames (e.g., assignment_confirmation.md)
  • Keep templates focused on a single purpose

Content Guidelines

  • Be concise - Email readers scan quickly
  • Use headings - Break up content for readability
  • Include clear calls-to-action - Make next steps obvious
  • Test on mobile - Many users read email on phones

Security

  • Never include sensitive data in email subjects
  • Use HTTPS links in production
  • Validate email addresses before sending
  • Rate limit email sending to prevent abuse

Security Best Practice

Always validate and sanitize user input before including it in email templates to prevent injection attacks.

Dependencies

The email system requires:

  • pycmarkgfm (^1.2.1) - GitHub Flavored Markdown parser
  • premailer (^3.10.0) - CSS inlining for email compatibility

Install via Poetry:

bash
poetry add pycmarkgfm premailer

Further Reading