Donāt expect any forewarning from Google, they cut mine off without notice ā¦
Once the dust had settled and Iād worked out how to send native django emails over the Gmail API, this became a much easier task as I could just clone the default SMTP Django backend and adjust. Means all those built-in apps using sendmail() and message.send() still work without needing to change anything there. The gmail_send_message()
method is redundant now.
# core/mail/backends.py
import base64
import copy
import logging
import threading
from django.conf import settings
from django.core.mail.backends.base import BaseEmailBackend
from django.core.mail.message import sanitize_address
from google.oauth2 import service_account
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
class GmailBackend(BaseEmailBackend):
"""
Django email backend for sending with Google Workspace
"""
def __init__(self, fail_silently=False, **kwargs):
self.fail_silently = fail_silently
self._lock = threading.RLock()
service_account_key = settings.GMAIL_SERVICE_KEY
# base_credentials used to build user specific credentials in send method
self.base_credentials = service_account.Credentials.from_service_account_info(
service_account_key,
scopes=['https://www.googleapis.com/auth/gmail.send']
)
def open(self):
""" retained for legacy code compaitibility """
return True
def close(self):
""" retained for legacy code compaitibility """
pass
def send_messages(self, email_messages, thread=False):
"""
Sends one or more EmailMessage objects and returns the number of email
messages sent.
"""
if not email_messages:
return 0
with self._lock:
num_sent = 0
for message in email_messages:
sent = self._send(message)
if sent:
num_sent += 1
return num_sent
def _send(self, email_message):
"""
A helper method that does the actual sending.
"""
if not email_message.recipients():
return False
try:
# sanitize addresses
sanitized_message = self.sanitize_addresses(email_message)
message = sanitized_message.message()
# Gmail api send() requires JSON format base64 string - convert .message() before sending
raw = {"raw": base64.urlsafe_b64encode(message.as_bytes(linesep="\r\n")).decode("utf-8")}
# Set delegated credentials according to the sender email address
delegated_credentials = self.base_credentials.with_subject(email_message.from_email)
service = build("gmail", "v1", credentials=delegated_credentials )
# Send email
service.users().messages().send(userId='me', body=raw).execute()
except HttpError as e:
logging.error(
f"{type(e).__name__} at line {e.__traceback__.tb_lineno} of {__file__}: {e}"
)
if not self.fail_silently:
raise
return False
return True
def sanitize_addresses(self, email_message):
"""
Make a copy of email message (email_message passed by reference), sanitize addresses
Ensure that email addresses are properly formatted & without potentially harmful characters
"""
message_copy = copy.copy(email_message)
encoding = email_message.encoding or getattr(settings, 'DEFAULT_CHARSET', 'utf-8')
message_copy.from_email = sanitize_address(email_message.from_email, encoding)
for field in ['to', 'cc', 'bcc', 'reply_to']:
setattr(message_copy, field, [sanitize_address(addr, encoding) for addr in getattr(email_message, field)])
return message_copy
The local settings just need the following to configure mail:
EMAIL_BACKEND = "core.mail.backends.GmailBackend"
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER = 'someone@somewhere.com'
GMAIL_SERVICE_KEY = { ... }
The delegation is all or nothing unfortunately - you canāt grant delegation rights to only certain mailboxes, it has to be domain wide. I tried granting send-on-behalf rights to the service account in the gmail client, computer said no, the service account isnāt recognised as a Google account. Nice consistency there, other Google apps will let you do this, just not Gmail. You can add some logic to the backend to restrict āfromā addresses but if someone gets access to the key then thereās no restriction. The alternative is working out how to generate the refresh tokens reliably and modifying the backend to use that instead.