From 29b41a763897ea5a4ecc2f72ca08d79be0b66df6 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 28 Nov 2018 02:59:12 -0400 Subject: [PATCH] Mailing: Add support for a from field Add support to the mailing profiles for specifying a "from" address. Closes GitLab issue #522. This commit adds a new backend class property "class_fields" which differs from the normal "fields" property. The "class_fields" property specifies which of the backend fields will be used to initialize a backend's driver class. This is to avoid passing fields that the driver doesn't expect and getting an error. When sending emails, the "send" method will attempt to get a "from" key from the backend data and use that when sending emails. If no "from" key is found a None is passes. Django's behavior in this situation dictates that the "from" value will then be taken from the DEFAULT_FROM_EMAIL setting. Signed-off-by: Roberto Rosario --- HISTORY.rst | 3 ++ docs/releases/3.2.rst | 4 +++ mayan/apps/mailer/classes.py | 5 +++ mayan/apps/mailer/mailers.py | 43 +++++++++++++++++++++++--- mayan/apps/mailer/models.py | 40 +++++++++++++++++++++--- mayan/apps/mailer/tests/literals.py | 1 + mayan/apps/mailer/tests/mixins.py | 13 ++++++-- mayan/apps/mailer/tests/test_models.py | 11 +++++-- mayan/apps/mailer/tests/test_views.py | 20 +++++++++--- 9 files changed, 123 insertions(+), 17 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index fe4d9c961b..fb0598c2df 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -64,6 +64,9 @@ * Use FilteredSelectionForm for IndexTemplateFilteredForm. * Use FilteredSelectionForm for DocumentVersionSignatureCreateForm. * Improve document signatures tests. +* Add docstrings to most models. +* Add support to the mailing profiles for specifying a from + address. Closes GitLab issue #522. 3.1.11 (2019-04-XX) =================== diff --git a/docs/releases/3.2.rst b/docs/releases/3.2.rst index bf6189b508..b627424499 100644 --- a/docs/releases/3.2.rst +++ b/docs/releases/3.2.rst @@ -96,6 +96,9 @@ Other changes * Use FilteredSelectionForm for IndexTemplateFilteredForm. * Use FilteredSelectionForm for DocumentVersionSignatureCreateForm. * Improve document signatures tests. +* Add docstrings to most models. +* Add support to the mailing profiles for specifying a from + address. Closes GitLab issue #522. Removals -------- @@ -166,6 +169,7 @@ Bugs fixed or issues closed --------------------------- * :gitlab-issue:`498` Can't scan subdirectories +* :gitlab-issue:`522` Office 365 SMTP * :gitlab-issue:`563` Recursive Watch Folder .. _PyPI: https://pypi.python.org/pypi/mayan-edms/ diff --git a/mayan/apps/mailer/classes.py b/mayan/apps/mailer/classes.py index 7b6285fffc..df6b20ddc2 100644 --- a/mayan/apps/mailer/classes.py +++ b/mayan/apps/mailer/classes.py @@ -48,6 +48,11 @@ class MailerBackendBase(object): class_path = '' # Dot path to the actual class that will handle the mail fields = {} + @classmethod + def get_class_fields(cls): + backend_field_list = getattr(cls, 'fields', {}).keys() + return getattr(cls, 'class_fields', backend_field_list) + class MailerBackend(six.with_metaclass(MailerBackendMetaclass, MailerBackendBase)): @classmethod diff --git a/mayan/apps/mailer/mailers.py b/mayan/apps/mailer/mailers.py index 5e958e1094..9e4660f2ba 100644 --- a/mayan/apps/mailer/mailers.py +++ b/mayan/apps/mailer/mailers.py @@ -4,13 +4,31 @@ from django.utils.translation import ugettext_lazy as _ from .classes import MailerBackend -__all__ = ('DjangoSMTP', 'DjangoFileBased') +__all__ = ('DjangoFileBased', 'DjangoSMTP') class DjangoSMTP(MailerBackend): + """ + Backend that wraps Django's SMTP backend + """ + class_fields = ( + 'host', 'port', 'use_tls', 'use_ssl', 'user', 'password' + ) class_path = 'django.core.mail.backends.smtp.EmailBackend' + field_order = ( + 'host', 'port', 'use_tls', 'use_ssl', 'user', 'password', 'from' + ) fields = { - 'host': { + 'from': { + 'label': _('From'), + 'class': 'django.forms.CharField', 'default': '', + 'help_text': _( + 'The sender\'s address. Some system will refuse to send ' + 'messages if this value is not set.' + ), 'kwargs': { + 'max_length': 48 + }, 'required': False + }, 'host': { 'label': _('Host'), 'class': 'django.forms.CharField', 'default': 'localhost', 'help_text': _('The host to use for sending email.'), @@ -64,7 +82,7 @@ class DjangoSMTP(MailerBackend): }, 'required': False }, } - field_order = ('host', 'port', 'use_tls', 'use_ssl', 'user', 'password') + label = _('Django SMTP backend') widgets = { 'password': { 'class': 'django.forms.widgets.PasswordInput', @@ -73,17 +91,32 @@ class DjangoSMTP(MailerBackend): } } } - label = _('Django SMTP backend') class DjangoFileBased(MailerBackend): + """ + Mailing backend that wraps Django's file based email backend + """ + class_fields = ('file_path',) class_path = 'django.core.mail.backends.filebased.EmailBackend' + field_order = ( + 'file_path', 'from' + ) fields = { 'file_path': { 'label': _('File path'), 'class': 'django.forms.CharField', 'kwargs': { 'max_length': 48 } - }, + }, 'from': { + 'label': _('From'), + 'class': 'django.forms.CharField', 'default': '', + 'help_text': _( + 'The sender\'s address. Some system will refuse to send ' + 'messages if this value is not set.' + ), 'kwargs': { + 'max_length': 48 + }, 'required': False + } } label = _('Django file based backend') diff --git a/mayan/apps/mailer/models.py b/mayan/apps/mailer/models.py index caa4cfcef4..f10e781c47 100644 --- a/mayan/apps/mailer/models.py +++ b/mayan/apps/mailer/models.py @@ -69,21 +69,49 @@ class UserMailer(models.Model): return self.label def backend_label(self): + """ + Return the label that the backend itself provides. The backend is + loaded but not initialized. As such the label returned is a class + property. + """ return self.get_backend().label def dumps(self, data): + """ + Serialize the backend configuration data. + """ self.backend_data = json.dumps(data) self.save() + def get_class_data(self): + """ + Return the actual mailing class initialization data + """ + backend = self.get_backend() + return { + key: value for key, value in self.loads().items() if key in backend.get_class_fields() + } + def get_backend(self): + """ + Retrieves the backend by importing the module and the class + """ return import_string(self.backend_path) def get_connection(self): + """ + Establishes a reusable connection to the server by loading the + backend, initializing it, and the using the backend instance to get + a connection. + """ return mail.get_connection( - backend=self.get_backend().class_path, **self.loads() + backend=self.get_backend().class_path, **self.get_class_data() ) def loads(self): + """ + Deserialize the stored backend data. + """ return json.loads(self.backend_data) def natural_key(self): @@ -104,10 +132,12 @@ class UserMailer(models.Model): filename, content, and mimetype. """ recipient_list = split_recipient_list(recipients=[to]) + backend_data = self.loads() with self.get_connection() as connection: email_message = mail.EmailMultiAlternatives( - body=strip_tags(body), connection=connection, subject=subject, + body=strip_tags(body), connection=connection, + from_email=backend_data.get('from'), subject=subject, to=recipient_list, ) @@ -152,13 +182,15 @@ class UserMailer(models.Model): with document.open() as file_object: attachments.append( { - 'filename': document.label, 'content': file_object.read(), + 'content': file_object.read(), + 'filename': document.label, 'mimetype': document.file_mimetype } ) return self.send( - subject=subject_text, body=body_html_content, to=to, attachments=attachments + attachments=attachments, body=body_html_content, + subject=subject_text, to=to, ) def test(self, to): diff --git a/mayan/apps/mailer/tests/literals.py b/mayan/apps/mailer/tests/literals.py index 4358eec279..ae3b603355 100644 --- a/mayan/apps/mailer/tests/literals.py +++ b/mayan/apps/mailer/tests/literals.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals TEST_BODY_HTML = 'test body' TEST_EMAIL_ADDRESS = 'test@example.com' +TEST_EMAIL_FROM_ADDRESS = 'from.test@example.com' TEST_RECIPIENTS_MULTIPLE_COMMA = 'test@example.com,test2@example.com' TEST_RECIPIENTS_MULTIPLE_COMMA_RESULT = [ 'test@example.com', 'test2@example.com' diff --git a/mayan/apps/mailer/tests/mixins.py b/mayan/apps/mailer/tests/mixins.py index c6cd6bc7b3..3cee7df777 100644 --- a/mayan/apps/mailer/tests/mixins.py +++ b/mayan/apps/mailer/tests/mixins.py @@ -1,8 +1,13 @@ from __future__ import unicode_literals +import json + from ..models import UserMailer -from .literals import TEST_USER_MAILER_BACKEND_PATH, TEST_USER_MAILER_LABEL +from .literals import ( + TEST_EMAIL_FROM_ADDRESS, TEST_USER_MAILER_BACKEND_PATH, + TEST_USER_MAILER_LABEL +) class MailerTestMixin(object): @@ -12,5 +17,9 @@ class MailerTestMixin(object): enabled=True, label=TEST_USER_MAILER_LABEL, backend_path=TEST_USER_MAILER_BACKEND_PATH, - backend_data='{}' + backend_data=json.dumps( + { + 'from': TEST_EMAIL_FROM_ADDRESS + } + ) ) diff --git a/mayan/apps/mailer/tests/test_models.py b/mayan/apps/mailer/tests/test_models.py index 62c0d20671..0e98e7f88b 100644 --- a/mayan/apps/mailer/tests/test_models.py +++ b/mayan/apps/mailer/tests/test_models.py @@ -5,8 +5,9 @@ from django.core import mail from mayan.apps.documents.tests.test_models import GenericDocumentTestCase from .literals import ( - TEST_BODY_HTML, TEST_EMAIL_ADDRESS, TEST_RECIPIENTS_MULTIPLE_COMMA, - TEST_RECIPIENTS_MULTIPLE_COMMA_RESULT, TEST_RECIPIENTS_MULTIPLE_SEMICOLON, + TEST_BODY_HTML, TEST_EMAIL_ADDRESS, TEST_EMAIL_FROM_ADDRESS, + TEST_RECIPIENTS_MULTIPLE_COMMA, TEST_RECIPIENTS_MULTIPLE_COMMA_RESULT, + TEST_RECIPIENTS_MULTIPLE_SEMICOLON, TEST_RECIPIENTS_MULTIPLE_SEMICOLON_RESULT, TEST_RECIPIENTS_MULTIPLE_MIXED, TEST_RECIPIENTS_MULTIPLE_MIXED_RESULT, ) @@ -19,6 +20,7 @@ class ModelTestCase(MailerTestMixin, GenericDocumentTestCase): self.user_mailer.send(to=TEST_EMAIL_ADDRESS) self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].from_email, TEST_EMAIL_FROM_ADDRESS) self.assertEqual(mail.outbox[0].to, [TEST_EMAIL_ADDRESS]) def test_send_simple_with_html(self): @@ -26,6 +28,7 @@ class ModelTestCase(MailerTestMixin, GenericDocumentTestCase): self.user_mailer.send(to=TEST_EMAIL_ADDRESS, body=TEST_BODY_HTML) self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].from_email, TEST_EMAIL_FROM_ADDRESS) self.assertEqual(mail.outbox[0].to, [TEST_EMAIL_ADDRESS]) self.assertEqual(mail.outbox[0].alternatives[0][0], TEST_BODY_HTML) @@ -36,6 +39,7 @@ class ModelTestCase(MailerTestMixin, GenericDocumentTestCase): ) self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].from_email, TEST_EMAIL_FROM_ADDRESS) self.assertEqual(mail.outbox[0].to, [TEST_EMAIL_ADDRESS]) with self.document.open() as file_object: self.assertEqual( @@ -50,6 +54,7 @@ class ModelTestCase(MailerTestMixin, GenericDocumentTestCase): self.user_mailer.send(to=TEST_RECIPIENTS_MULTIPLE_COMMA) self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].from_email, TEST_EMAIL_FROM_ADDRESS) self.assertEqual( mail.outbox[0].to, TEST_RECIPIENTS_MULTIPLE_COMMA_RESULT ) @@ -59,6 +64,7 @@ class ModelTestCase(MailerTestMixin, GenericDocumentTestCase): self.user_mailer.send(to=TEST_RECIPIENTS_MULTIPLE_SEMICOLON) self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].from_email, TEST_EMAIL_FROM_ADDRESS) self.assertEqual( mail.outbox[0].to, TEST_RECIPIENTS_MULTIPLE_SEMICOLON_RESULT ) @@ -68,6 +74,7 @@ class ModelTestCase(MailerTestMixin, GenericDocumentTestCase): self.user_mailer.send(to=TEST_RECIPIENTS_MULTIPLE_MIXED) self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].from_email, TEST_EMAIL_FROM_ADDRESS) self.assertEqual( mail.outbox[0].to, TEST_RECIPIENTS_MULTIPLE_MIXED_RESULT ) diff --git a/mayan/apps/mailer/tests/test_views.py b/mayan/apps/mailer/tests/test_views.py index 4f093cbb20..fce44c68a6 100644 --- a/mayan/apps/mailer/tests/test_views.py +++ b/mayan/apps/mailer/tests/test_views.py @@ -12,10 +12,10 @@ from ..permissions import ( ) from .literals import ( - TEST_EMAIL_ADDRESS, TEST_USER_MAILER_BACKEND_PATH, TEST_USER_MAILER_LABEL, - TEST_RECIPIENTS_MULTIPLE_COMMA, TEST_RECIPIENTS_MULTIPLE_COMMA_RESULT, - TEST_RECIPIENTS_MULTIPLE_MIXED, TEST_RECIPIENTS_MULTIPLE_MIXED_RESULT, - TEST_RECIPIENTS_MULTIPLE_SEMICOLON, + TEST_EMAIL_ADDRESS, TEST_EMAIL_FROM_ADDRESS, TEST_USER_MAILER_BACKEND_PATH, + TEST_USER_MAILER_LABEL, TEST_RECIPIENTS_MULTIPLE_COMMA, + TEST_RECIPIENTS_MULTIPLE_COMMA_RESULT, TEST_RECIPIENTS_MULTIPLE_MIXED, + TEST_RECIPIENTS_MULTIPLE_MIXED_RESULT, TEST_RECIPIENTS_MULTIPLE_SEMICOLON, TEST_RECIPIENTS_MULTIPLE_SEMICOLON_RESULT ) from .mailers import TestBackend @@ -92,6 +92,7 @@ class MailerViewsTestCase(MailerTestMixin, GenericDocumentViewTestCase): self._request_document_link_send() self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].from_email, TEST_EMAIL_FROM_ADDRESS) self.assertEqual(mail.outbox[0].to, [TEST_EMAIL_ADDRESS]) def test_mail_document_view_no_permissions(self): @@ -111,6 +112,7 @@ class MailerViewsTestCase(MailerTestMixin, GenericDocumentViewTestCase): self._request_document_send() self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].from_email, TEST_EMAIL_FROM_ADDRESS) self.assertEqual(mail.outbox[0].to, [TEST_EMAIL_ADDRESS]) def test_user_mailer_create_view_no_permissions(self): @@ -198,6 +200,7 @@ class MailerViewsTestCase(MailerTestMixin, GenericDocumentViewTestCase): self.assertEqual(response.status_code, 200) self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].from_email, TEST_EMAIL_FROM_ADDRESS) self.assertEqual(mail.outbox[0].to, [TEST_EMAIL_ADDRESS]) def test_send_multiple_recipients_comma(self): @@ -211,6 +214,7 @@ class MailerViewsTestCase(MailerTestMixin, GenericDocumentViewTestCase): response = self._request_user_mailer_test() self.assertEqual(response.status_code, 200) self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].from_email, TEST_EMAIL_FROM_ADDRESS) self.assertEqual( mail.outbox[0].to, TEST_RECIPIENTS_MULTIPLE_COMMA_RESULT ) @@ -226,6 +230,7 @@ class MailerViewsTestCase(MailerTestMixin, GenericDocumentViewTestCase): response = self._request_user_mailer_test() self.assertEqual(response.status_code, 200) self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].from_email, TEST_EMAIL_FROM_ADDRESS) self.assertEqual( mail.outbox[0].to, TEST_RECIPIENTS_MULTIPLE_MIXED_RESULT ) @@ -241,6 +246,7 @@ class MailerViewsTestCase(MailerTestMixin, GenericDocumentViewTestCase): response = self._request_user_mailer_test() self.assertEqual(response.status_code, 200) self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].from_email, TEST_EMAIL_FROM_ADDRESS) self.assertEqual( mail.outbox[0].to, TEST_RECIPIENTS_MULTIPLE_SEMICOLON_RESULT ) @@ -255,6 +261,7 @@ class MailerViewsTestCase(MailerTestMixin, GenericDocumentViewTestCase): self._request_document_link_send() self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].from_email, TEST_EMAIL_FROM_ADDRESS) self.assertEqual( mail.outbox[0].to, TEST_RECIPIENTS_MULTIPLE_COMMA_RESULT ) @@ -269,6 +276,7 @@ class MailerViewsTestCase(MailerTestMixin, GenericDocumentViewTestCase): self._request_document_link_send() self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].from_email, TEST_EMAIL_FROM_ADDRESS) self.assertEqual( mail.outbox[0].to, TEST_RECIPIENTS_MULTIPLE_MIXED_RESULT ) @@ -283,6 +291,7 @@ class MailerViewsTestCase(MailerTestMixin, GenericDocumentViewTestCase): self._request_document_link_send() self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].from_email, TEST_EMAIL_FROM_ADDRESS) self.assertEqual( mail.outbox[0].to, TEST_RECIPIENTS_MULTIPLE_SEMICOLON_RESULT ) @@ -297,6 +306,7 @@ class MailerViewsTestCase(MailerTestMixin, GenericDocumentViewTestCase): self._request_document_send() self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].from_email, TEST_EMAIL_FROM_ADDRESS) self.assertEqual( mail.outbox[0].to, TEST_RECIPIENTS_MULTIPLE_COMMA_RESULT ) @@ -311,6 +321,7 @@ class MailerViewsTestCase(MailerTestMixin, GenericDocumentViewTestCase): self._request_document_send() self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].from_email, TEST_EMAIL_FROM_ADDRESS) self.assertEqual( mail.outbox[0].to, TEST_RECIPIENTS_MULTIPLE_MIXED_RESULT ) @@ -325,6 +336,7 @@ class MailerViewsTestCase(MailerTestMixin, GenericDocumentViewTestCase): self._request_document_send() self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].from_email, TEST_EMAIL_FROM_ADDRESS) self.assertEqual( mail.outbox[0].to, TEST_RECIPIENTS_MULTIPLE_SEMICOLON_RESULT )