Django Email/Contact Form Tutorial

Let's build a simple contact form that sends email for a Django 3.0 website. We can take advantage of Django's built-in email support to make this relatively painless and then send the emails for real using the SendGrid email service.

Initial setup

The first step is to create a dedicated directory for our code. On a Mac, a convenient location is the Desktop. From the command line, execute the following commands to navigate to the Desktop and create a new contact folder.

$ cd ~/Desktop
$ mkdir contact && cd contact

Note: If you need help additional help configuring your Django dev environment, please see this installation guide.

Now we can install Django and activate our virtual environment.

$ pipenv install django~=3.1.0
$ pipenv shell

Next let's create a new Django project called config and within it an app called sendemail:

(contact) $ django-admin startproject config .
(contact) $ python manage.py startapp sendemail

To make sure everything installed correctly let's migrate and then runserver.

(contact) $ python manage.py migrate
(contact) $ python manage.py runserver

If you open you browser to 127.0.0.1:8000 you should see the following screen:

Django welcome page

Update settings.py

We've created a new app so we need to explicitly add it to our Django project. Within your settings.py file, under INSTALLED_APPS add sendemail at the top.

# config/settings.py
INSTALLED_APPS [
    'sendemail.apps.SendemailConfig', # new
    ...
]

Then within the same settings.py file, specify the DEFAULT_FROM_EMAIL, which is [email protected] for me. And update EMAIL_BACKEND specifying which email backend we'll use, console, so that the email is outputted to the command line.

# config/settings.py
DEFAULT_FROM_EMAIL = '[email protected]'
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

Update urls.py

Since we've added an app to our Django project we need to update the root config/urls.py file, adding include to the top line and a new urlpattern for the app:

# config/urls.py
from django.contrib import admin
from django.urls import path, include # new

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('sendemail.urls')), # new
]

Next create a new sendemail/urls.py file in our app.

(contact) $ touch sendemail/urls.py

Then add the following code so that the main contact form is at email/ and a successful submission redirects to success/.

# sendemail/urls.py
from django.contrib import admin
from django.urls import path

from .views import contactView, successView

urlpatterns = [
    path('contact/', contactView, name='contact'),
    path('success/', successView, name='success'),
]

Create forms.py

Still within our sendemail app, create a new file forms.py.

(contact) $ touch sendemail/forms.py

This will contain the fields in our actual contact form. We'll require three: from_email, subject, and message.

# sendemail/forms.py
from django import forms

class ContactForm(forms.Form):
    from_email = forms.EmailField(required=True)
    subject = forms.CharField(required=True)
    message = forms.CharField(widget=forms.Textarea, required=True)

We're using Django's built-in Forms API here to quickly create three fields.

Create views.py

Let's create the view now that will do the bulk of the work for our contact form. Update the existing sendemail/views.py file:

# sendemail/views.py
from django.core.mail import send_mail, BadHeaderError
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import render, redirect
from .forms import ContactForm

def contactView(request):
    if request.method == 'GET':
        form = ContactForm()
    else:
        form = ContactForm(request.POST)
        if form.is_valid():
            subject = form.cleaned_data['subject']
            from_email = form.cleaned_data['from_email']
            message = form.cleaned_data['message']
            try:
                send_mail(subject, message, from_email, ['[email protected]'])
            except BadHeaderError:
                return HttpResponse('Invalid header found.')
            return redirect('success')
    return render(request, "email.html", {'form': form})

def successView(request):
    return HttpResponse('Success! Thank you for your message.')

There's a lot going on here! We start by importing send_mail and BadHeaderError for security reasons. At the bottom of the imports we reference ContactForm which we just created in our forms.py file.

Create templates

Final step! We need to create the templates for our email and success pages. I like to create a project-level templates folder and put all of my templates in there. So create a new directory called templates and create two new files in there.

(contact) $ mkdir templates
(contact) $ touch templates/email.html

Next update our settings.py file to tell Django to look in this directory for templates. Update the DIRS settings within TEMPLATES. This is a one-line change.

# config/settings.py
TEMPLATES = [
    {
        ...
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        ...
    },
]

Now update our template files with the following code:

<!-- templates/email.html -->
<h1>Contact Us</h1>
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <div class="form-actions">
      <button type="submit">Send</button>
    </div>
</form>

Send first email

Make sure the server is running with python manage.py runserver and load http://127.0.0.1:8000/email/ in your web browser, fill out the form, and click the Send button.

Contact Form

You will be redirected to the http://127.0.0.1:8000/success/ if the email goes through.

Success Page

And in your console you can see that the email was sent:

(contact) $ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
April 17, 2020 - 21:21:53
Django version 3.0.5, using settings 'config.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
[17/Apr/2020 21:21:59] "GET /contact/ HTTP/1.1" 200 622
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: Hello
From: will@learndjango.com
To: admin@example.com
Date: Fri, 17 Apr 2020 21:22:35 -0000
Message-ID: <158715855539.8761.15735317856240850309@williams-mbp.lan>

Does this email contact form work?
-------------------------------------------------------------------------------
[17/Apr/2020 21:22:35] "POST /contact/ HTTP/1.1" 302 0
[17/Apr/2020 21:22:35] "GET /success/ HTTP/1.1" 200 36

Email Service

To send actual emails you'll need a managed service like SendGrid, mailgun, or SES. Fortunately, Django makes switch to these services straightforward.

To implement with SendGrid, create a free account and select the SMTP option, which is easier to configure than the Web API approach.

SendGrid Setup Method Page

The next screen requires naming your API Key Name, which I've called newspaper but it could be named anything. Click the blue "Create Key" button next to it.

SendGrid Integrate Page

Update your settings.py as follows, switching from console to smtp for EMAIL_BACKEND, while adding several new fields. Update the EMAIL_HOST_PASSWORD with the SendGrid password for your account.

# config/settings.py
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' # new
DEFAULT_FROM_EMAIL = '[email protected]'
EMAIL_HOST = 'smtp.sendgrid.net' # new
EMAIL_HOST_USER = 'apikey' # new
EMAIL_HOST_PASSWORD = '<sendgrid_password>' # new
EMAIL_PORT = 587 # new
EMAIL_USE_TLS = True # new

As of April 6, 2020, there is now one additional step required, which is single sender verification. Essentially, this is an additional step to help SendGrid comply with anti-spam laws. Follow SendGrid's instructions to verify an email account. While previously it was possible to send emails from a free address at services like gmail.com or yahoo.com, that is no longer the case due to the DMARC email authentication protocol. So to send actual emails now you must use a custom, non-free email account which you can verify ownership of.

Conclusion

With this configuration in place, you should be able to send real emails from your Django app. Often this is used when a user signs up for the first time, needs to reset a password, and so on. All these steps, including the use of an environment variable for EMAIL_HOST_PASSWORD, and customizing each email are covered in the Django for Beginners book.