Search code examples
pythondjangoamazon-ses

django-ses module not working: Connect timeout when using django.core.mail


I am using django-ses==3.4.1 to send email via django.core.mail and both send_mail() and EmailMessage() get the connection timeout after one minute:

botocore.exceptions.ConnectTimeoutError: Connect timeout on endpoint URL: "https://email-smtp.eu-central-1.amazonaws.com/

Using this configuration in settings.py according to the instructions on https://pypi.org/project/django-ses/

EMAIL_BACKEND = 'django_ses.SESBackend'
AWS_SES_USER = 'my-verified@email'
AWS_SES_ACCESS_KEY_ID = '-my-access-key-'
AWS_SES_SECRET_ACCESS_KEY = '-my-secret-access-key-'
AWS_SES_REGION_NAME = 'eu-central-1'
AWS_SES_REGION_ENDPOINT = 'email-smtp.eu-central-1.amazonaws.com'

Where I use the variable AWS_SES_USER as from_email while calling

email = EmailMessage(subject, message, from_email, recipient_list)
email.content_subtype = 'html'
email.send()

I have also tested if the SES works without Django, i.e. simply using smtplib and it does.

The working example derived from https://realpython.com/python-send-email/#option-2-using-starttls

smtp_server = "email-smtp.eu-central-1.amazonaws.com"
port = 587

# Create a secure SSL context
context = ssl.create_default_context()
# Try to log in to server and send email
try:
    server = smtplib.SMTP(smtp_server,port)
    server.ehlo() # Can be omitted
    server.starttls(context=context) # Secure the connection
    server.ehlo() # Can be omitted
    server.login(AWS_SES_ACCESS_KEY_ID, AWS_SES_SECRET_ACCESS_KEY)
    
    message = """\
Subject: Test SES

This message is sent from Python."""

    receiver_email = 'my-recipient@email'
    server.sendmail(AWS_SES_USER, receiver_email, message)

I have tried changing the parameters in settings.py in many ways, but without success.


Solution

  • Eventually, the working solution without django-ses is simple. However, you have to replace the EmailMessage() with your own function containing the working example mentioned in the question.

    Include the following in settings.py:

    EMAIL_BACKEND = 'django_ses.SESBackend'
    EMAIL_PORT = 587 # TLS=587, SSL=465
    AWS_SES_USER = 'my-verified@email'
    AWS_SES_ACCESS_KEY_ID = '-my-access-key-'
    AWS_SES_SECRET_ACCESS_KEY = '-my-secret-access-key-'
    AWS_SES_REGION_NAME = 'eu-central-1'
    AWS_SES_REGION_ENDPOINT = 'email-smtp.eu-central-1.amazonaws.com'
    

    Then, you write the send_mail_ses() function in a file (e.g. emailer.py) that you will call from your view (e.g. from views.py):

    from django.conf.settings import EMAIL_BACKEND, EMAIL_PORT, \
        AWS_SES_USER, AWS_SES_ACCESS_KEY_ID, \
        AWS_SES_REGION_ENDPOINT, \
        AWS_SES_SECRET_ACCESS_KEY
    
    def send_mail_ses(request, data):
        message = MIMEMultipart("alternative")
        message["Subject"] = data['subject']
        message["From"] = data.get('from', AWS_SES_USER)
        message["To"] = data.get('to')
    
        part = MIMEText(data['message'], data['how'])    
        message.attach(part)
    
        context = ssl.create_default_context()
        try:
            server = smtplib.SMTP(AWS_SES_REGION_ENDPOINT, EMAIL_PORT)
            server.ehlo() # Can be omitted
            server.starttls(context=context) # Secure the connection
            server.ehlo() # Can be omitted
            server.login(AWS_SES_ACCESS_KEY_ID, AWS_SES_SECRET_ACCESS_KEY)
            receiver_email = message["To"]
            server.sendmail(
                AWS_SES_USER, receiver_email, message.as_string()
            )
            server.quit()
            return HttpResponse(status=204)
        except Exception as err:
            server.quit() # handle failure
            return HttpResponse(status=500)
    

    Note that you can handle the HttpResponse and any errors as you prefer.

    Then, you have two options for formatting the message before sending: plain or html.

    With the plain text, you define the data dictionary and pass it to the send_mail_ses() function:

    message = "This is the email message."
    data = {
        'how': 'plain',
        'subject': f"This is the subject",
        'message': message_text,
        'from': AWS_SES_USER,
        'to': '[email protected]',
    }
    send_mail_ses(request, data)
    

    With the html formatted:

    context = {'message_text': message_text}
    html_message = render_to_string("email_template.html", context)
    data = {
        'how': 'html',
        'subject': f"This is the subject",
        'message': html_message,
        'from': AWS_SES_USER,
        'to': '[email protected]',
    }
    
    send_mail_ses(request, data)
    

    In this case you will have to inject the {{message_text}} from the context into the email_template.html template.

    Note: this is untested because it is an extract out of my much more complex code.