diff --git a/apimail/admin.py b/apimail/admin.py index c9db40e95a0b6cd8c460d50c71f5adce6213e51a..6ea48c9d3e78691675e90ac96297cc8bfc46672b 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 0000000000000000000000000000000000000000..01efa3ea1549c47d830d495cb25ac7be581989da --- /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 0000000000000000000000000000000000000000..3230748f828d7b533e809795d1f4515d7e3e0383 --- /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 4ef781dbb2148bb2a03bf4a46e32342c3422c54a..4f8395d53ee2ec0bc63bbef7012e0b5bfe279768 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 c0537002de32642e3b76e77a7a4134fc05fa8a5d..fb59da3d1db1ef86471d81717800aad1dcf081f6 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 0000000000000000000000000000000000000000..92184bd46732ccf44aa275285e1a4919dffc8155 --- /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 2c5581249aedefa5434ef6b9b0d4b3b1d616872b..8bd1602f02242c48f9f0d19f23a61fa60d619724 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(