diff --git a/qlinks/email.py b/qlinks/email.py new file mode 100644 index 0000000..01948bc --- /dev/null +++ b/qlinks/email.py @@ -0,0 +1,30 @@ +from django.conf import settings +from django.core.mail import send_mail +from django.template import Context +from django.template.loader import get_template +from django.utils.translation import gettext as _ + +from qlinks.models import Link + +template = get_template('qlinks/broken_email.txt') + + +def send_broken_email(link: Link): + if link.created_by and link.created_by.email: + emails = [link.created_by.email] + name = link.created_by.get_full_name() + else: + emails = [email for name, email in settings.ADMINS] + if not emails: + return + name = _('Admin') + + send_mail( + subject=_('Broken link: %s') % link.short_url, + message=template.render({ + 'name': name, + 'link': link, + }), + from_email=None, + recipient_list=emails, + ) diff --git a/qlinks/management/commands/qlinks_check.py b/qlinks/management/commands/qlinks_check.py index b659fad..2449441 100644 --- a/qlinks/management/commands/qlinks_check.py +++ b/qlinks/management/commands/qlinks_check.py @@ -6,6 +6,7 @@ from django.core.management import BaseCommand from django.db.models import Min from django.utils import timezone +from qlinks.email import send_broken_email from qlinks.models import Link logger = logging.getLogger('qlinks.checker') @@ -50,5 +51,6 @@ class Command(BaseCommand): link.check_url() if was_working and not link.is_working: self.stdout.write(f'URL for {link.short} just broke: {link.long}') + send_broken_email(link) time.sleep(settings.QLINKS_CHECK_THROTTLE) diff --git a/qlinks/settings/base.py b/qlinks/settings/base.py index 5758b68..e27ee9a 100644 --- a/qlinks/settings/base.py +++ b/qlinks/settings/base.py @@ -91,3 +91,4 @@ QLINKS_CDN_CACHE = None QLINKS_CHECK_MIN = timedelta(days=6) QLINKS_CHECK_MAX = timedelta(days=8) QLINKS_CHECK_THROTTLE = 1 +QLINKS_BROKEN_EMAIL = False diff --git a/qlinks/settings/template.py b/qlinks/settings/template.py index 2a230b4..157dbc4 100644 --- a/qlinks/settings/template.py +++ b/qlinks/settings/template.py @@ -22,6 +22,35 @@ DATABASES = { STATIC_ROOT = '/srv/example' # STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage' +# Email +# https://docs.djangoproject.com/en/4.0/topics/email/#email-backends +# The email all broken links notifications will come from, among other things. +# DEFAULT_FROM_MAIL = 'qlinks@example.com' + +# A tuple of (name, email) pairs that specifies those who will be mailed +# when the server experiences an error when DEBUG = False. +# ADMINS = ( +# ('Your Name', 'your.email@example.com'), +# ) + +# The following block is included for your convenience, if you want to use Gmail. +# EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +# EMAIL_USE_TLS = True +# EMAIL_HOST = 'smtp.gmail.com' +# EMAIL_HOST_USER = '@gmail.com' +# EMAIL_HOST_PASSWORD = '' +# EMAIL_PORT = 587 + +# To use Mailgun, uncomment this block. +# You will need to run `pip install django-mailgun` for to get `MailgunBackend`. +# EMAIL_BACKEND = 'django_mailgun.MailgunBackend' +# MAILGUN_ACCESS_KEY = '' +# MAILGUN_SERVER_NAME = '' + +# You can also use Sendgrid, with `pip install sendgrid-django`. +# EMAIL_BACKEND = 'sgbackend.SendGridBackend' +# SENDGRID_API_KEY = '' + # qlinks configuration QLINKS_ADMIN_HOST = r'admin' @@ -42,3 +71,6 @@ QLINKS_CANONICAL = None # Minimum time in seconds between two consecutive checks. # QLINKS_CHECK_THROTTLE = 1 + +# Enable emails. +# QLINKS_BROKEN_EMAIL = True diff --git a/qlinks/templates/qlinks/broken_email.txt b/qlinks/templates/qlinks/broken_email.txt new file mode 100644 index 0000000..420913b --- /dev/null +++ b/qlinks/templates/qlinks/broken_email.txt @@ -0,0 +1,8 @@ +{% load i18n %}{% blocktranslate %}Dear {{ name }},{% endblocktranslate %} + +{% translate "QLinks has found a broken link:" %} + +* {% translate "short link" %}: {{ link.short_url }} +* {% translate "destination" %}: {{ link.long }} + +{% translate "Please log onto the admin site and fix this." %} diff --git a/qlinks/tests/test_email.py b/qlinks/tests/test_email.py new file mode 100644 index 0000000..b592502 --- /dev/null +++ b/qlinks/tests/test_email.py @@ -0,0 +1,45 @@ +from django.contrib.auth.models import User +from django.core import mail +from django.test import TestCase +from django.utils import timezone + +from qlinks.email import send_broken_email +from qlinks.models import Link + + +class EmailTest(TestCase): + def create_link(self, user) -> Link: + return Link.objects.create( + short='short', + long='https://long.example.com', + created_by=user, + is_working=False, + last_check=timezone.now(), + ) + + def assert_email(self, link: Link, name: str, to: str): + self.assertEqual(len(mail.outbox), 1) + self.assertEqual([to], mail.outbox[0].to) + self.assertIn(link.short_url, mail.outbox[0].subject) + self.assertIn(link.short_url, mail.outbox[0].body) + self.assertIn(link.long, mail.outbox[0].body) + self.assertIn(f'Dear {name},', mail.outbox[0].body) + + def test_send_no_user(self): + link = self.create_link(None) + with self.settings(ADMINS=[('someone', 'someone@example.com')]): + send_broken_email(link=link) + self.assert_email(link, 'Admin', 'someone@example.com') + + def test_send_user(self): + link = self.create_link(User.objects.create_user( + 'test', email='user@example.com', first_name='Test', last_name='User' + )) + send_broken_email(link=link) + self.assert_email(link, 'Test User', 'user@example.com') + + def test_send_user_no_email(self): + link = self.create_link(User.objects.create_user('test', first_name='Test', last_name='User')) + with self.settings(ADMINS=[('someone', 'someone@example.com')]): + send_broken_email(link=link) + self.assert_email(link, 'Admin', 'someone@example.com')