Sending Emails With SendGrid, Django, and AWS Route 53
Tags: Sendgrid, Email Marketing, Django, AWS, Python

To follow this tutorial you need the following items:

A SendGrid account

A registered domain

Configured Route 53 Host Zones

AWS account

Set Up A Domain on SendGrid

Log in to your SendGrid account and before anything create an API key. Click on the left sidebar, select Settings, then API Keys. Finally, click on Create API Key. In the pop-up window choose Restricted Access if you are only using this key for emails, plus doing so maintains other more important keys secure. Also don’t forget to give your new key a meaningful name to keep better track of what this key does, for example django_emails.

In the access details section choose Mail Send and give it mail send Full Access. This will allow your Django app to only send emails.

Now click on the Create & View button. On the next screen, you will be presented with your brand new key. Note that this key will never be shown again, so click on it to copy and paste it in a secure place to use it later. Click on Done to finalize this section.

Configure Django

For this tutorial, we’ll be using an existing application that you can clone from GitHub. Keep in mind this application is at a more advanced state and extra configuration is needed to run it locally on your computer. The README goes over some of the steps to accomplish that. Here is a screenshot of the contact form that will be sending out emails when someone fills it out and submits it.

To start the configuration go to the Django reactorlabs project and edit reactor/settings.py with your favorite text editor. Add the following variables to use Django’s SMTP interface. You can put the variables at the bottom of the file.

# reactor/settings.py .... # SendGrid

EMAIL_HOST = 'smtp.sendgrid.net'

EMAIL_PORT = 587

EMAIL_USE_TLS = True

EMAIL_HOST_USER = 'apikey'

EMAIL_HOST_PASSWORD = os.environ.get('SENDGRID_API_KEY')

Note that the EMAIL_HOST_PASSWORD points to an environment variable called SENDGRID_API_KEY. It is usually bad practice to include secrets in your source code so we have to import it from either a .env file or from postactivate if you are using virtualenvwrapper. There are other ways to do this but for now, follow the instructions in the README or do a quick search about setting Python environment variables.

This is the form the application is using on the home page. Look at the pages/forms.py in the project.

# pages/forms.py

from django.forms import ModelForm

from django.forms import TextInput, Textarea

from .models import Contact

from django.core.mail import send_mail

def send_email(self):

# send email using the self.cleaned_data dictionary

subject = f"New email from {self.cleaned_data['name']}"

message = f"Contact Name: {self.cleaned_data['name']}

" \

f"Contact Phone Number: {self.cleaned_data['phone']}

"\

f"Contact Email: {self.cleaned_data['email']}



" \

f"{self.cleaned_data['message']}"

sender = 'noreply@reactorlabs.studio'

recipient_list = [' class ContactForm(ModelForm):def send_email(self):# send email using the self.cleaned_data dictionarysubject = f"New email from {self.cleaned_data['name']}"message = f"Contact Name: {self.cleaned_data['name']}

" \f"Contact Phone Number: {self.cleaned_data['phone']}

"\f"Contact Email: {self.cleaned_data['email']}



" \f"{self.cleaned_data['message']}"sender = 'noreply@reactorlabs.studio'recipient_list = [' jqngl@icloud.com '] send_mail(

subject,

message,

sender,

recipient_list,

fail_silently=False,

) class Meta:

model = Contact

fields = ["name", "email", "phone", "message"]

widgets = {

"name": TextInput(

attrs={

"class": "form-control",

"id": "name",

"type": "text",

"placeholder": "Your Name *",

"required": True,

"data-validation-required-message": "Please enter your name.",

}

),

"email": TextInput(

attrs={

"class": "form-control",

"id": "email",

"type": "email",

"placeholder": "Your Email *",

"required": True,

"data-validation-required-message": "Please enter your email address.",

}

),

"phone": TextInput(

attrs={

"class": "form-control",

"id": "phone",

"type": "tel",

"placeholder": "Your Phone *",

"required": True,

"data-validation-required-message": "Please enter your phone number.",

}

),

"message": Textarea(

attrs={

"class": "form-control",

"id": "message",

"placeholder": "Your Message *",

"required": True,

"data-validation-required-message": "Please enter a message.",

"rows": 8

}

)

}

This form is using Django’s core email module and the built-in Form API clean method. Here is a shorter version of the changes. You can use Python’s new F-Strings feature to customize the way your email looks like and access the forms input values too.

# pages/forms.py .... from django.core.mail import send_mail

def send_email(self):

# send email using the self.cleaned_data dictionary

subject = f"New email from {self.cleaned_data['name']}"

message = f"Contact Name: {self.cleaned_data['name']}

" \

f"Contact Phone Number: {self.cleaned_data['phone']}

"\

f"Contact Email: {self.cleaned_data['email']}



" \

f"{self.cleaned_data['message']}"

sender = 'noreply@reactorlabs.studio'

recipient_list = [' class ContactForm(ModelForm):def send_email(self):# send email using the self.cleaned_data dictionarysubject = f"New email from {self.cleaned_data['name']}"message = f"Contact Name: {self.cleaned_data['name']}

" \f"Contact Phone Number: {self.cleaned_data['phone']}

"\f"Contact Email: {self.cleaned_data['email']}



" \f"{self.cleaned_data['message']}"sender = 'noreply@reactorlabs.studio'recipient_list = [' jqngl@icloud.com '] send_mail(

subject,

message,

sender,

recipient_list,

fail_silently=False,

)



...

Now take a look at pages/views.py HomePage view. This view checks if the form is valid and calls the send_email method from the contact form.

# pages/views.py

from django.views.generic import TemplateView

from django.views.generic.edit import CreateView

from .models import Contact

from django.urls import reverse_lazy

from django.http import HttpResponse

from .forms import ContactForm

# Create your views here. class HomePageView(CreateView):

template_name = 'home.html'

model = Contact

form_class = ContactForm

success_url = reverse_lazy("thanks") def form_valid(self, form):

form.send_email()

return super().form_valid(form) def thanks(request):

return HttpResponse("Thank you! Will get in touch soon.")

These are the details for more clarity.

# pages/views.py

... class HomePageView(CreateView):

...

def form_valid(self, form):

form.send_email()

return super().form_valid(form)

Validate Your Domain

I use AWS Route 53 for my DNS web service and my domain is from Google Domains. This had me confused for a minute as to where to install SendGrid’s DNS records. In this case, install them in Route 53 as opposed to Google Domains.

Go back to your SendGrid account and this time select Sender Authentication under Settings in the left sidebar. Select Domain Authentication and click on Authenticate Your Domain.

On the next screen find your DNS host from the list — in this case, Amazon Route 53. Check Yes to brand the links for your domain to make your emails nicer and more user-friendly.

On this screen add your domain. Here I’m using reactorlabs.studio. Click on Next to continue.

In the last step, you’ll receive the DNS Records to install in AWS Route 53. The table contains a Name and a Value which you’ll need to create five CNAME records.

Create New CNAME Records In AWS Route 53

Log in to your AWS account and navigate to the Route 53 console. Here you should already have a Hosted Zone for your domain. Click on Hosted Zones to continue to the Record Sets.

Select your Domain Name from the list and edit the record.

Create five new CNAME record sets. Make sure to omit the domain from the record name and add the value in the value section in Route 53. Here create the CNAME record with Name url1145.reactorlabs.studio and value sendgrid.net. Create all five records required by SendGrid following the same pattern.

You should end up with something similar to this.

Verify The Records Are Installed Correctly

Go back SendGrid and verify the records by checking on I’ve added these records and click on Verify.

If everything goes well you should get green checkmarks for all the records once you click Verify.

Now it’s time to go back and test out the contact form in Django. If all is configured correctly you should be able to start sending emails. I ran into a block error when testing things out. According to their documentation, it has to do with the IP Address SendGrid uses to send the email. You are more likely to run into this if you are using the free option. SendGrid’s documentation notes that they are already working on it and there should be a need to contact support but I did anyway.

This is an example of the error.

550 5.7.1 Mail from IP 167.89.12.138 was rejected due to listing in Spamhaus SBL. For details please see http://www.spamhaus.org/query/bl?ip=167.89.12.138

Don’t hesitate to reach out or connect on Twitter, let me know if there is anything missing, ask questions, or just give me some claps. Best.