From 5f02d81eb2a99c99b06745652846b4bca66b594f Mon Sep 17 00:00:00 2001
From: "J.-S. Caux" <J.S.Caux@uva.nl>
Date: Sat, 24 Oct 2020 19:46:45 +0200
Subject: [PATCH] Add ValidatedAddress model

---
 apimail/admin.py                              | 16 +++-
 ...0026_addressvalidation_validatedaddress.py | 38 +++++++++
 apimail/models/__init__.py                    |  2 +
 apimail/models/stored_message.py              |  3 +-
 apimail/models/validated_address.py           | 83 +++++++++++++++++++
 5 files changed, 139 insertions(+), 3 deletions(-)
 create mode 100644 apimail/migrations/0026_addressvalidation_validatedaddress.py
 create mode 100644 apimail/models/validated_address.py

diff --git a/apimail/admin.py b/apimail/admin.py
index 6ea48c9d3..e7be3ceba 100644
--- a/apimail/admin.py
+++ b/apimail/admin.py
@@ -11,7 +11,9 @@ from .models import (
     ComposedMessage, ComposedMessageAPIResponse,
     Event,
     StoredMessage,
-    UserTag)
+    UserTag,
+    ValidatedAddress, AddressValidation
+)
 
 
 admin.site.register(Domain)
@@ -66,3 +68,15 @@ class UserTagAdmin(admin.ModelAdmin):
     pass
 
 admin.site.register(UserTag, UserTagAdmin)
+
+
+class AddressValidationInline(admin.StackedInline):
+    model = AddressValidation
+    extra = 0
+    min_num = 0
+
+
+class ValidatedAddressAdmin(admin.ModelAdmin):
+    inlines = [AddressValidationInline,]
+
+admin.site.register(ValidatedAddress, ValidatedAddressAdmin)
diff --git a/apimail/migrations/0026_addressvalidation_validatedaddress.py b/apimail/migrations/0026_addressvalidation_validatedaddress.py
new file mode 100644
index 000000000..355e9a2e0
--- /dev/null
+++ b/apimail/migrations/0026_addressvalidation_validatedaddress.py
@@ -0,0 +1,38 @@
+# Generated by Django 2.2.16 on 2020-10-24 15:51
+
+import datetime
+import django.contrib.postgres.fields.jsonb
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('apimail', '0025_composedmessage_headers_added'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='ValidatedAddress',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('address', models.EmailField(max_length=512, unique=True)),
+            ],
+            options={
+                'ordering': ['address'],
+            },
+        ),
+        migrations.CreateModel(
+            name='AddressValidation',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('data', django.contrib.postgres.fields.jsonb.JSONField(default=dict)),
+                ('datestamp', models.DateField(default=datetime.date.today)),
+                ('address', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='validations', to='apimail.ValidatedAddress')),
+            ],
+            options={
+                'ordering': ['address__address', '-datestamp'],
+            },
+        ),
+    ]
diff --git a/apimail/models/__init__.py b/apimail/models/__init__.py
index 4f8395d53..d4cc703bc 100644
--- a/apimail/models/__init__.py
+++ b/apimail/models/__init__.py
@@ -15,3 +15,5 @@ from .event import Event
 from .stored_message import StoredMessage
 
 from .tag import UserTag
+
+from .validated_address import ValidatedAddress, AddressValidation
diff --git a/apimail/models/stored_message.py b/apimail/models/stored_message.py
index 08b87f260..24690aee9 100644
--- a/apimail/models/stored_message.py
+++ b/apimail/models/stored_message.py
@@ -1,8 +1,7 @@
 __copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
 __license__ = "AGPL v3"
 
-
-#import pytz
+import pytz
 import uuid as uuid_lib
 
 from django.conf import settings
diff --git a/apimail/models/validated_address.py b/apimail/models/validated_address.py
new file mode 100644
index 000000000..2d0341285
--- /dev/null
+++ b/apimail/models/validated_address.py
@@ -0,0 +1,83 @@
+__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
+__license__ = "AGPL v3"
+
+
+import datetime
+import requests
+
+from django.conf import settings
+from django.contrib.postgres.fields import JSONField
+from django.db import models
+
+
+class ValidatedAddress(models.Model):
+    """
+    Email address (lowercased) with related validation info.
+
+    The Mailgun email validation v4 API is queried at least once per year
+    and the response is saved as a related AddressValidation object.
+    """
+    address = models.EmailField(
+        max_length=512, # as per Mailgun limit
+        unique=True
+    )
+
+    class Meta:
+        ordering = ['address',]
+
+    def save(self, *args, **kwargs):
+        self.address = self.address.lower()
+        return super().save(*args, **kwargs)
+
+    def __str__(self):
+        return self.address
+
+    @property
+    def is_good_for_sending(self):
+        """
+        Return the status of the latest Mailgun validation.
+        """
+        self.update_mailgun_validation()
+        try:
+            return self.validations.first().data['result'] \
+                in ('deliverable', 'catch_all', 'unknown')
+        except AttributeError:
+            return False
+
+    def update_mailgun_validation(self):
+        """
+        If no validation check within last year, call the Mailgun validation v4 API.
+        """
+        one_year_ago = datetime.date.today() - datetime.timedelta(days=365)
+        if not self.validations.filter(datestamp__gt=one_year_ago).exists():
+            response = requests.get(
+                "https://api.mailgun.net/v4/address/validate",
+                auth=("api", settings.MAILGUN_API_KEY),
+                params={"address": self.address}
+            ).json()
+            validation = AddressValidation(
+                address=self,
+                data=response
+            )
+            validation.save()
+
+
+class AddressValidation(models.Model):
+    """
+    For a given ValidatedAddress, timestamped response from a Mailgun API validation v4 query.
+    """
+    address = models.ForeignKey(
+        'apimail.ValidatedAddress',
+        related_name='validations',
+        on_delete=models.CASCADE)
+    data = JSONField(default=dict)
+    datestamp = models.DateField(default=datetime.date.today)
+
+    class Meta:
+        ordering = [
+            'address__address',
+            '-datestamp'
+        ]
+
+    def __str__(self):
+        return "%s: %s (%s)" % (self.address, self.data['result'], self.datestamp)
-- 
GitLab