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 ​