Skip to content

Upgrades and new function to send via SMTP #7

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*.swp
*.pyc
__pycache__
.tox/*
10 changes: 4 additions & 6 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
language: python
python:
- 2.7
- 3.6
- 3.8
env:
- DJANGO=1.8
- DJANGO=1.9
- DJANGO=1.10
- DJANGO=1.11
- DJANGO=2.0
- DJANGO=2.2
- DJANGO=3.2

matrixs:
exclude:
- python: 3.6
Expand Down
41 changes: 41 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
Change Log
==========


Under development
~~~~~~~~~~~~~~~~~~
*

2023.02.08 v05.9
~~~~~~~~~~~~~~~~
* Adds arabic translation; Mark strings are translatable.
* Adds check for change permission to send email from admin

2022.07.19 v05.7
~~~~~~~~~~~~~~~~
* Adds `DB_EMAIL_FILTER_FUNCTION_PATH` setting to allow filtering out email from DB recording.
* Enhances in read me and strings translations.

2022.07.18 v05.5
~~~~~~~~~~~~~~~~
* Fixes issues in case of error sending the mail.

2022.04.21 v05.4
~~~~~~~~~~~~~~~~
* Adds `SMTP_EMAIL_FILTER_FUNCTION_PATH` setting to allow filtering out email from smtp sending.
* Enhance apps
* Rename has_error to succeeded for clearer more relaxed visual


2022.04.20 v05.1
~~~~~~~~~~~~~~~~
* Adds `SMTPDBEmailBackend` , a mixture between SMTP and DB mail backend.
* Remove south migrations
* Allow only superusers to access email body.
* Adds has_error, and error ro Email model


2021.11.29
~~~~~~~~~~
* Upgrade compatibility from the main fork to work with Django 3.2 +
* Add option on admin to (re)send the mail using the smtp backend
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
include README.md LICENSE
recursive-include db_email_backend/locale *
25 changes: 0 additions & 25 deletions README.md

This file was deleted.

53 changes: 53 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
Django DB Email Backend
=======================

Record Email Messages Sent to database , with the ability to also send them via SMTP.


Usage
-----

Install ::

pip install kn-django-db-email-backend

In settings.py::

INSTALLED_APPS += ['db_email_backend']

EMAIL_BACKEND = 'db_email_backend.backend.DBEmailBackend'
# record the email message to database

# OR
EMAIL_BACKEND = 'db_email_backend.backend.SMTPDBEmailBackend'
# Record to database and send via SMTP.
# IF errors happened you can see it in the Admin and resend it again.

Migrate::

$ python manage.py migrate


Configuration
=============

SMTP_EMAIL_FILTER_FUNCTION_PATH default to `db_email_backend.utils.smtp_filter_email_function`. a dotted path to the smtp email filter function.
A filter function for the smtp email, takes the email_message (django.core.mail.message.EmailMessage) as a parameter, and return False if this message should be filter out out and not sent via the backend.

Example::

def only_allow_emails_to_mahad(message):
return True if '[email protected]' in message.to else False


DB_EMAIL_FILTER_FUNCTION_PATH default to `db_email_backend.utils.db_filter_email_function`. a dotted path to the db email filter function. same as the SMTP one but for the Database.

Example::

def dont_record_error_emails(message):
return False if settings.EMAIL_SUBJECT_PREFIX in message.subject else True

Admin
-----

Package have and admin integration ready where you can send the email SMTP even if the EMAIL_HOST = "db_email_backend.backend.DBEmailBackend"
4 changes: 3 additions & 1 deletion db_email_backend/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
VERSION = (0, 4, 1)
default_app_config = 'db_email_backend.apps.DBEmailBackendConfig'

VERSION = (0, 5, 9)
__version__ = '.'.join(map(str, VERSION))
55 changes: 47 additions & 8 deletions db_email_backend/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
from __future__ import unicode_literals

from django.contrib import admin
from django.core.mail import EmailMessage
from django.test import override_settings
from django.utils.translation import gettext_lazy as _

from .models import Email, EmailAlternative, EmailAttachment

Expand All @@ -26,26 +29,62 @@ class EmailAdmin(admin.ModelAdmin):
fields = (
('from_email', 'create_date', 'content_subtype'),
('to', 'cc', 'bcc'),
'subject', 'body', 'headers')
'subject', 'body', 'headers', 'succeeded', 'error')
readonly_fields = (
'create_date','from_email', 'to', 'cc', 'bcc', 'subject', 'body', 'content_subtype', 'headers')
list_display = ('subject', 'to', 'from_email', 'create_date', 'attachment_count', 'alternative_count')
list_filter = ('content_subtype',)
'create_date', 'from_email', 'to', 'cc', 'bcc', 'subject', 'body', 'content_subtype', 'headers', 'succeeded',
'error')
list_display = (
'subject', 'to', 'from_email', 'create_date', 'attachment_count', 'alternative_count', 'succeeded', 'error')
list_filter = ('succeeded', 'content_subtype',)
date_hierarchy = 'create_date'
search_fields = ('to', 'from_email', 'cc', 'bcc', 'subject', 'body')
inlines = (EmailAlternativeInline, EmailAttachmentInline)

actions = ['send_mail']

def get_actions(self, request):
actions = super().get_actions(request)
if not self.has_change_permission(request):
actions.pop('send_mail')
return actions

def get_fields(self, request, obj=None):
fields = super().get_fields(request, obj)
if not request.user.is_superuser:
fields = list(fields)
fields.remove('body')
return fields

@override_settings(EMAIL_BACKEND='django.core.mail.backends.smtp.EmailBackend')
def send_mail(self, request, queryset):
for row in queryset:
msg = EmailMessage()
msg.subject = row.subject
msg.body = row.body
msg.content_subtype = row.content_subtype
msg.from_email = row.from_email
msg.to = [x for x in row.to.split('; ') if x]
msg.cc = [x for x in row.cc.split('; ') if x]
msg.bcc = [x for x in row.bcc.split('; ') if x]

msg.send(fail_silently=False)

self.message_user(request, f'{queryset.count()} Emails sent', 25)

send_mail.short_description = _("Send Email")

def attachment_count(self, instance):
return instance.attachments.count()
attachment_count.short_description = 'Attachments'

attachment_count.short_description = _('Attachments')

def alternative_count(self, instance):
return instance.alternatives.count()
alternative_count.short_description = 'Alternatives'

alternative_count.short_description = _('Alternatives')

def has_add_permission(self, request, obj=None):
return False

admin.site.register(Email, EmailAdmin)


admin.site.register(Email, EmailAdmin)
16 changes: 16 additions & 0 deletions db_email_backend/app_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django.conf import settings
from django.utils.module_loading import import_string

SMTP_EMAIL_FILTER_FUNCTION_PATH = getattr(settings, 'SMTP_EMAIL_FILTER_FUNCTION_PATH',
'db_email_backend.utils.smtp_filter_email_function')
DB_EMAIL_FILTER_FUNCTION_PATH = getattr(settings, 'SMTP_EMAIL_FILTER_FUNCTION_PATH',
'db_email_backend.utils.db_filter_email_function')

try:
email_filter = import_string(SMTP_EMAIL_FILTER_FUNCTION_PATH)
except Exception as e:
raise e
try:
db_email_filter = import_string(DB_EMAIL_FILTER_FUNCTION_PATH)
except Exception as e:
raise e
9 changes: 9 additions & 0 deletions db_email_backend/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from __future__ import unicode_literals

from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _


class DBEmailBackendConfig(AppConfig):
name = 'db_email_backend'
verbose_name = _('Email Backend')
119 changes: 88 additions & 31 deletions db_email_backend/backend.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,100 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import logging

from django.core.files.base import ContentFile
from django.core.mail.backends.base import BaseEmailBackend
from django.core.mail.backends.smtp import EmailBackend as SMTPEmailBackend

from .models import Email, EmailAlternative, EmailAttachment
from .app_settings import email_filter, db_email_filter

logger = logging.getLogger('db_mail_backend')


def record_email_message(msg, fail_silently):
try:
email = Email.objects.create(
subject=msg.subject,
body=msg.body,
content_subtype=msg.content_subtype,
from_email=msg.from_email,
to='; '.join(msg.to),
cc='; '.join(msg.cc),
bcc='; '.join(msg.bcc),
headers='\n'.join('{}: {}'.format(k, v)
for k, v in msg.extra_headers.items()),
)
# output.append(email)

alternatives = getattr(msg, 'alternatives', [])
for content, mimetype in alternatives:
EmailAlternative.objects.create(
email=email,
content=content,
mimetype=mimetype or '',
)

for filename, content, mimetype in msg.attachments:
attachment = EmailAttachment.objects.create(
email=email,
filename=filename,
mimetype=mimetype or '',
)
attachment.file.save(filename, ContentFile(content))
except:
email = None
if not fail_silently:
raise

return email


class DBEmailBackend(BaseEmailBackend):
def send_messages(self, email_messages):
for msg in email_messages:
try:
email = Email.objects.create(
subject=msg.subject,
body=msg.body,
content_subtype=msg.content_subtype,
from_email=msg.from_email,
to='; '.join(msg.to),
cc='; '.join(msg.cc),
bcc='; '.join(msg.bcc),
headers='\n'.join('{}: {}'.format(k, v)
for k, v in msg.extra_headers.items()),
)
alternatives = getattr(msg, 'alternatives', [])
for content, mimetype in alternatives:
EmailAlternative.objects.create(
email=email,
content=content,
mimetype=mimetype or '',
)

for filename, content, mimetype in msg.attachments:
attachment = EmailAttachment.objects.create(
email=email,
filename=filename,
mimetype=mimetype or '',
)
attachment.file.save(filename, ContentFile(content))
except:
if not self.fail_silently:
raise

if db_email_filter(msg):
return record_email_message(msg, fail_silently=self.fail_silently)
return len(email_messages)


class SMTPDBEmailBackend(SMTPEmailBackend):
"""
This backend is mixture between SMTP and DB mail backend
It writes to the database then send the email over smtp,
if any errors happen while sending it is reflected in the email model
"""

def send_messages(self, email_messages):
"""
Send one or more EmailMessage objects and return the number of email
messages sent.
"""
if not email_messages:
return 0
with self._lock:
new_conn_created = self.open()
if not self.connection or new_conn_created is None:
# We failed silently on open().
# Trying to send would be pointless.
return 0
num_sent = 0
for message in email_messages:
try:
if db_email_filter(message):
email_inst = record_email_message(message, fail_silently=self.fail_silently)
except Exception as e:
logger.error(e)
try:
if email_filter(message):
sent = self._send(message)
if sent:
num_sent += 1
except Exception as e:
if email_inst:
Email.objects.filter(pk=email_inst.pk).update(error=str(e), succeeded=False)
raise e
if new_conn_created:
self.close()
return num_sent
Binary file added db_email_backend/locale/ar/LC_MESSAGES/django.mo
Binary file not shown.
Loading