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
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:
# 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:
- Email Utility (
obed/core/mail.py) - Core function for sending emails - Markdown Processor - Converts Markdown to HTML using
pycmarkgfm - CSS Inliner - Uses
premailerto inline styles for email clients
Processing Flow
Markdown Template → Django Template Rendering → Markdown to HTML →
CSS Styling → CSS Inlining → Email SentAPI Reference
send_email()
Sends an email with optional Markdown-to-HTML conversion.
Location: obed/core/mail.py
Parameters:
subject(str) - Email subject lineto_email_list(List[str]) - List of recipient email addressestemplate(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:
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.mdTemplate 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:
# 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 emphasisDjango Template Variables
Combine Markdown with Django template syntax:
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:
- Get the source CSS from the original repository
- Make your changes to the unminified version
- Minify the CSS using a tool like CSS Minifier or via command line:bash
npx csso github-markdown.css -o github-markdown.min.css - Replace
obed/assets/css/github-markdown.min.csswith your minified version - 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():
# 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:
# obed/users/views.py
from django.contrib.auth import views as auth_views
from .forms import PasswordResetForm
class PasswordResetView(auth_views.PasswordResetView):
form_class = PasswordResetFormURL Configuration
# 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:
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
- Start MailDev:
docker compose up maildev - Trigger an email action (e.g., password reset)
- 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:
# obed/settings/dev.py
EMAIL_HOST = "localhost"
EMAIL_PORT = 1025Verify MailDev is running:
docker compose ps maildevQuick 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:
send_email(..., md_to_html=True) # Required for Markdown conversionCommon 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:
ls -lh obed/assets/css/github-markdown.min.cssVerify file path in code:
The path is relative to obed/core/mail.py:
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:
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:
# 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 parserpremailer(^3.10.0) - CSS inlining for email compatibility
Install via Poetry:
poetry add pycmarkgfm premailer