-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
288 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from django.apps import AppConfig | ||
|
||
|
||
class EmailsConfig(AppConfig): | ||
default_auto_field = 'django.db.models.BigAutoField' | ||
name = 'emails' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
from typing import Any | ||
|
||
from django.conf import settings | ||
from django.core.mail import get_connection | ||
from django.core.mail.backends.base import BaseEmailBackend | ||
from django.utils.module_loading import import_string | ||
|
||
from core.shared.tasks import create_async_task | ||
from emails.serialization import email_to_dict, dict_to_email | ||
|
||
DEFAULT_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' | ||
EMAIL_BACKEND = getattr(settings, 'DJANGO_Q_EMAIL_BACKEND', DEFAULT_BACKEND) | ||
EMAIL_ERROR_HANDLER = getattr(settings, 'DJANGO_Q_EMAIL_ERROR_HANDLER', None) | ||
DJANGO_Q_EMAIL_USE_DICTS = getattr(settings, 'DJANGO_Q_EMAIL_USE_DICTS', True) | ||
|
||
|
||
class DjangoQBackend(BaseEmailBackend): | ||
use_dicts = DJANGO_Q_EMAIL_USE_DICTS | ||
|
||
def send_messages(self, email_messages: Any) -> int: | ||
num_sent = 0 | ||
for email_message in email_messages: | ||
if self.use_dicts: | ||
email_message = email_to_dict(email_message) | ||
|
||
create_async_task(send_message, email_message) | ||
|
||
num_sent += 1 | ||
return num_sent | ||
|
||
|
||
def send_message(email_message: Any) -> None: | ||
""" | ||
Sends the specified email synchronously. | ||
See DjangoQBackend for sending in the background. | ||
""" | ||
try: | ||
if isinstance(email_message, dict): | ||
email_message = dict_to_email(email_message) | ||
|
||
connection = email_message.connection | ||
email_message.connection = get_connection(backend=EMAIL_BACKEND) | ||
try: | ||
email_message.send() | ||
finally: | ||
email_message.connection = connection | ||
|
||
except Exception as ex: | ||
if not EMAIL_ERROR_HANDLER: | ||
raise | ||
|
||
email_error_handler = import_string(EMAIL_ERROR_HANDLER) | ||
email_error_handler(email_message, ex) |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
from typing import Any | ||
|
||
from django.core.mail.message import EmailMessage, EmailMultiAlternatives | ||
|
||
|
||
def email_to_dict(email_message: Any) -> dict: | ||
""" | ||
Converts the specified email message to a dictionary representation. | ||
""" | ||
if type(email_message) not in [EmailMessage, EmailMultiAlternatives, dict]: | ||
raise ValueError( | ||
'The email_message argument must be an instance of ' | ||
'EmailMessage, EmailMultiAlternatives or dict.' | ||
) | ||
|
||
if isinstance(email_message, dict): | ||
return email_message | ||
|
||
email_message_data = { | ||
'subject': email_message.subject, | ||
'body': email_message.body, | ||
'from_email': email_message.from_email, | ||
'to': email_message.to, | ||
'bcc': email_message.bcc, | ||
'attachments': email_message.attachments, | ||
'headers': email_message.extra_headers, | ||
'cc': None, | ||
'reply_to': None, | ||
} | ||
|
||
if isinstance(email_message, EmailMultiAlternatives): | ||
email_message_data['alternatives'] = email_message.alternatives | ||
|
||
return email_message_data | ||
|
||
|
||
def dict_to_email(email_message_data: dict) -> EmailMessage | EmailMultiAlternatives: | ||
""" | ||
Creates an EmailMessage or EmailMultiAlternatives instance from the | ||
specified dictionary. | ||
""" | ||
kwargs = {**email_message_data} | ||
alternatives = kwargs.pop('alternatives', None) | ||
return ( | ||
EmailMessage(**kwargs) if not alternatives else | ||
EmailMultiAlternatives(alternatives=alternatives, **kwargs) | ||
) |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
from unittest.mock import patch | ||
|
||
from django.core.mail import EmailMessage, EmailMultiAlternatives | ||
from django.test import TestCase | ||
|
||
from emails.backends import DjangoQBackend, send_message | ||
from emails.serialization import email_to_dict | ||
|
||
patch_create_async_task = patch('emails.backends.create_async_task') | ||
|
||
|
||
class DjangoQEmailBackendTests(TestCase): | ||
|
||
@patch_create_async_task | ||
def test_send_messages_creates_async_task(self, mock_create_async_task): | ||
message = EmailMessage( | ||
subject='Subject', | ||
body='Body', | ||
from_email='test@example.com', | ||
to=['test1@example.com'] | ||
) | ||
dict_message = email_to_dict(message) | ||
|
||
backend = DjangoQBackend() | ||
backend.use_dicts = True | ||
backend.send_messages([message]) | ||
|
||
mock_create_async_task.assert_called_once_with(send_message, dict_message) | ||
|
||
@patch_create_async_task | ||
def test_send_messages_creates_multiple_async_tasks(self, mock_create_async_task): | ||
message1 = EmailMessage( | ||
subject='Subject', | ||
body='Body', | ||
from_email='test@example.com', | ||
to=['test1@example.com'] | ||
) | ||
message2 = EmailMultiAlternatives( | ||
subject='Subject', | ||
body='Body', | ||
from_email='test@example.com', | ||
to=['test1@example.com'] | ||
) | ||
message2.attach_alternative('<body>Hello world!</body>', 'text/html') | ||
|
||
backend = DjangoQBackend() | ||
backend.use_dicts = True | ||
backend.send_messages([message1, message2]) | ||
|
||
self.assertEqual(mock_create_async_task.call_count, 2) | ||
|
||
@patch_create_async_task | ||
def test_send_messages_creates_async_task_use_dicts_false(self, mock_create_async_task): | ||
message = EmailMessage( | ||
subject='Subject', | ||
body='Body', | ||
from_email='test@example.com', | ||
to=['test1@example.com'] | ||
) | ||
|
||
backend = DjangoQBackend() | ||
backend.use_dicts = False | ||
backend.send_messages([message]) | ||
|
||
mock_create_async_task.assert_called_once_with(send_message, message) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import email | ||
from unittest import TestCase | ||
|
||
from django.conf import settings | ||
from django.core.mail import EmailMessage, EmailMultiAlternatives | ||
|
||
from emails.serialization import email_to_dict, dict_to_email | ||
|
||
|
||
class EmailSerializationTests(TestCase): | ||
|
||
def test_email_message_to_dict(self): | ||
payload = EmailMessage( | ||
subject='Test email', | ||
body='This is a test email.', | ||
from_email='test@example.com', | ||
to=['test@example.com'] | ||
) | ||
result = email_to_dict(payload) | ||
self.assertEqual(result['subject'], 'Test email') | ||
self.assertEqual(result['body'], 'This is a test email.') | ||
self.assertEqual(result['from_email'], 'test@example.com') | ||
self.assertEqual(result['to'], ['test@example.com']) | ||
|
||
def test_email_multi_alternatives_to_dict(self): | ||
message = EmailMultiAlternatives( | ||
subject='Test email', | ||
body='This is a test email.', | ||
from_email='test@example.com', | ||
to=['test@example.com'] | ||
) | ||
message.attach_alternative( | ||
'<p>This is a test email.</p>', | ||
'text/html' | ||
) | ||
result = email_to_dict(message) | ||
self.assertEqual(result['subject'], message.subject) | ||
self.assertEqual(result['body'], message.body) | ||
self.assertEqual(result['from_email'], message.from_email) | ||
self.assertEqual(result['to'], message.to) | ||
self.assertEqual(result['alternatives'], message.alternatives) | ||
|
||
def test_email_dict_to_dict(self): | ||
payload = { | ||
'subject': 'Test email', | ||
'body': 'This is a test email.', | ||
'from_email': 'test@example.com' | ||
} | ||
result = email_to_dict(payload) | ||
self.assertEqual(result, payload) | ||
|
||
def test_email_to_dict_invalid_type(self): | ||
with self.assertRaises(ValueError): | ||
email_to_dict('invalid') | ||
|
||
with self.assertRaises(ValueError): | ||
email_to_dict(email.message.Message()) | ||
|
||
def test_dict_to_email(self): | ||
payload = { | ||
'subject': 'Test email', | ||
'body': 'This is a test email.', | ||
'from_email': 'test@example.com' | ||
} | ||
result = dict_to_email(payload) | ||
self.assertTrue(isinstance(result, EmailMessage)) | ||
self.assertEqual(result.subject, payload['subject']) | ||
self.assertEqual(result.body, payload['body']) | ||
self.assertEqual(result.from_email, payload['from_email']) | ||
|
||
def test_dict_to_email_with_alternatives(self): | ||
payload = { | ||
'subject': 'Test email', | ||
'body': 'This is a test email.', | ||
'from_email': 'test@example.com', | ||
'alternatives': [('<p>This is a test email.</p>', 'text/html')] | ||
} | ||
result = dict_to_email(payload) | ||
self.assertTrue(isinstance(result, EmailMultiAlternatives)) | ||
self.assertEqual(result.subject, payload['subject']) | ||
self.assertEqual(result.body, payload['body']) | ||
self.assertEqual(result.from_email, payload['from_email']) | ||
self.assertEqual(result.alternatives, payload['alternatives']) | ||
|
||
def test_dict_to_email_empty_dict(self): | ||
payload = {} | ||
result = dict_to_email(payload) | ||
self.assertEqual(result.subject, '') | ||
self.assertEqual(result.body, '') | ||
self.assertEqual(result.from_email, settings.DEFAULT_FROM_EMAIL) | ||
self.assertEqual(result.to, []) | ||
self.assertEqual(result.bcc, []) | ||
self.assertEqual(result.attachments, []) | ||
self.assertEqual(result.extra_headers, {}) | ||
self.assertEqual(result.cc, []) | ||
self.assertEqual(result.reply_to, []) |