From 637069c4e7e94b0f20b483dd1ec4f137b7c8106b Mon Sep 17 00:00:00 2001
From: Quantum <quantum2048@gmail.com>
Date: Mon, 24 Jan 2022 20:03:13 -0500
Subject: [PATCH] Implement broken link email handling

---
 qlinks/email.py                            | 30 +++++++++++++++
 qlinks/management/commands/qlinks_check.py |  2 +
 qlinks/settings/base.py                    |  1 +
 qlinks/settings/template.py                | 32 +++++++++++++++
 qlinks/templates/qlinks/broken_email.txt   |  8 ++++
 qlinks/tests/test_email.py                 | 45 ++++++++++++++++++++++
 6 files changed, 118 insertions(+)
 create mode 100644 qlinks/email.py
 create mode 100644 qlinks/templates/qlinks/broken_email.txt
 create mode 100644 qlinks/tests/test_email.py

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 = '<your account>@gmail.com'
+# EMAIL_HOST_PASSWORD = '<your 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 = '<your Mailgun access key>'
+# MAILGUN_SERVER_NAME = '<your Mailgun domain>'
+
+# You can also use Sendgrid, with `pip install sendgrid-django`.
+# EMAIL_BACKEND = 'sgbackend.SendGridBackend'
+# SENDGRID_API_KEY = '<Your 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')