From a2a0b8a5b7f55a74958c9a07166e43cce9cf402d Mon Sep 17 00:00:00 2001
From: "J.-S. Caux" <J.S.Caux@uva.nl>
Date: Sat, 17 Oct 2020 10:23:50 +0200
Subject: [PATCH] Add apimail.models.Domain

---
 apimail/admin.py                              |  4 +++
 apimail/migrations/0021_auto_20201017_1016.py | 30 +++++++++++++++++++
 apimail/migrations/0022_auto_20201017_1018.py | 19 ++++++++++++
 apimail/models/__init__.py                    |  2 ++
 apimail/models/account.py                     | 10 +++++++
 apimail/models/domain.py                      | 24 +++++++++++++++
 apimail/validators.py                         | 16 ++++++++++
 7 files changed, 105 insertions(+)
 create mode 100644 apimail/migrations/0021_auto_20201017_1016.py
 create mode 100644 apimail/migrations/0022_auto_20201017_1018.py
 create mode 100644 apimail/models/domain.py

diff --git a/apimail/admin.py b/apimail/admin.py
index c9db40e95..6ea48c9d3 100644
--- a/apimail/admin.py
+++ b/apimail/admin.py
@@ -5,6 +5,7 @@ __license__ = "AGPL v3"
 from django.contrib import admin
 
 from .models import (
+    Domain,
     EmailAccount, EmailAccountAccess,
     AttachmentFile,
     ComposedMessage, ComposedMessageAPIResponse,
@@ -13,6 +14,9 @@ from .models import (
     UserTag)
 
 
+admin.site.register(Domain)
+
+
 class EmailAccountAccessInline(admin.StackedInline):
     model = EmailAccountAccess
     extra = 0
diff --git a/apimail/migrations/0021_auto_20201017_1016.py b/apimail/migrations/0021_auto_20201017_1016.py
new file mode 100644
index 000000000..01efa3ea1
--- /dev/null
+++ b/apimail/migrations/0021_auto_20201017_1016.py
@@ -0,0 +1,30 @@
+# Generated by Django 2.2.16 on 2020-10-17 08:16
+
+import apimail.validators
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('apimail', '0020_auto_20200214_1159'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Domain',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=100, unique=True, validators=[apimail.validators._simple_domain_name_validator])),
+            ],
+            options={
+                'ordering': ('name',),
+            },
+        ),
+        migrations.AddField(
+            model_name='emailaccount',
+            name='domain',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='email_accounts', to='apimail.Domain'),
+        ),
+    ]
diff --git a/apimail/migrations/0022_auto_20201017_1018.py b/apimail/migrations/0022_auto_20201017_1018.py
new file mode 100644
index 000000000..3230748f8
--- /dev/null
+++ b/apimail/migrations/0022_auto_20201017_1018.py
@@ -0,0 +1,19 @@
+# Generated by Django 2.2.16 on 2020-10-17 08:18
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('apimail', '0021_auto_20201017_1016'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='emailaccount',
+            name='domain',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='email_accounts', to='apimail.Domain'),
+        ),
+    ]
diff --git a/apimail/models/__init__.py b/apimail/models/__init__.py
index 4ef781dbb..4f8395d53 100644
--- a/apimail/models/__init__.py
+++ b/apimail/models/__init__.py
@@ -8,6 +8,8 @@ from .attachment import AttachmentFile
 
 from .composed_message import ComposedMessage, ComposedMessageAPIResponse
 
+from .domain import Domain
+
 from .event import Event
 
 from .stored_message import StoredMessage
diff --git a/apimail/models/account.py b/apimail/models/account.py
index c0537002d..fb59da3d1 100644
--- a/apimail/models/account.py
+++ b/apimail/models/account.py
@@ -3,6 +3,7 @@ __license__ = "AGPL v3"
 
 
 from django.conf import settings
+from django.core.exceptions import ValidationError
 from django.db import models
 
 from ..managers import EmailAccountAccessQuerySet
@@ -14,6 +15,11 @@ class EmailAccount(models.Model):
 
     Access is specified on a per-user basis through the related EmailAccountAccess model.
     """
+    domain = models.ForeignKey(
+        'apimail.Domain',
+        related_name='email_accounts',
+        on_delete=models.CASCADE
+    )
     name = models.CharField(max_length=256)
     email = models.EmailField(unique=True)
     description = models.TextField()
@@ -24,6 +30,10 @@ class EmailAccount(models.Model):
     def __str__(self):
         return('%s <%s>' % (self.name, self.email))
 
+    def clean(self):
+        if self.email.rpartition('@')[2] != self.domain.name:
+            raise ValidationError("Email domain does not match domain name.")
+
 
 class EmailAccountAccess(models.Model):
     """
diff --git a/apimail/models/domain.py b/apimail/models/domain.py
new file mode 100644
index 000000000..92184bd46
--- /dev/null
+++ b/apimail/models/domain.py
@@ -0,0 +1,24 @@
+_copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
+__license__ = "AGPL v3"
+
+
+from django.db import models
+
+from ..validators import _simple_domain_name_validator
+
+
+class Domain(models.Model):
+    """
+    Domain name information.
+    """
+    name = models.CharField(
+        max_length=100,
+        validators=[_simple_domain_name_validator],
+        unique=True,
+    )
+
+    class Meta:
+        ordering = ('name',)
+
+    def __str__(self):
+        return self.name
diff --git a/apimail/validators.py b/apimail/validators.py
index 2c5581249..8bd1602f0 100644
--- a/apimail/validators.py
+++ b/apimail/validators.py
@@ -2,11 +2,27 @@ __copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
 __license__ = "AGPL v3"
 
 
+import string
+
 from django.conf import settings
 from django.core.exceptions import ValidationError
 from django.template.defaultfilters import filesizeformat
 
 
+def _simple_domain_name_validator(value):
+    """
+    Validate that the given value contains no whitespaces to prevent common typos.
+
+    Taken from django.contrib.sites.models
+    """
+    checks = ((s in value) for s in string.whitespace)
+    if any(checks):
+        raise ValidationError(
+            "The domain name cannot contain any spaces or tabs.",
+            code='invalid',
+        )
+
+
 def validate_max_email_attachment_file_size(value):
     if value.size > int(settings.MAX_EMAIL_ATTACHMENT_FILE_SIZE):
         raise ValidationError(
-- 
GitLab