🐛(aliases) alias destination can be devnull@devnull

devnull@devnull is not considered a valid email address by django's
EmailFieldValidator but it's a special address in dimail's config.

Make "destination" a CharField instead of an EmailField to replace
validator and add devnull to allowlist.
This commit is contained in:
Marie PUPO JEAMMET
2026-01-23 16:39:48 +01:00
committed by Marie
parent 5ebc88bcff
commit 99433a6722
6 changed files with 72 additions and 4 deletions

View File

@@ -8,6 +8,10 @@ and this project adheres to
## [Unreleased] ## [Unreleased]
### Fixed
- 🐛(aliases) authorize special domain devnull in alias destinations #1029
## [1.22.1] - 2026-01-21 ## [1.22.1] - 2026-01-21
- 🔒️(organization) the first user is not admin #776 - 🔒️(organization) the first user is not admin #776

View File

@@ -19,7 +19,7 @@ class Migration(migrations.Migration):
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created at')), ('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created at')),
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated at')), ('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated at')),
('local_part', models.CharField(max_length=100)), ('local_part', models.CharField(max_length=100)),
('destination', models.EmailField(max_length=254, verbose_name='destination address')), ('destination', models.CharField(max_length=254, validators=[django.core.validators.EmailValidator(allowlist=['localhost', 'devnull'])], verbose_name='destination address')),
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='aliases', to='mailbox_manager.maildomain')), ('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='aliases', to='mailbox_manager.maildomain')),
], ],
options={ options={

View File

@@ -9,6 +9,7 @@ from django.conf import settings
from django.contrib.auth.base_user import AbstractBaseUser from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.core import exceptions, mail, validators from django.core import exceptions, mail, validators
from django.core.validators import EmailValidator
from django.db import models from django.db import models
from django.db.models.functions import Lower from django.db.models.functions import Lower
from django.template.loader import render_to_string from django.template.loader import render_to_string
@@ -460,7 +461,15 @@ class Alias(BaseModel):
"""Model for aliases.""" """Model for aliases."""
local_part = models.CharField(max_length=100, blank=False) local_part = models.CharField(max_length=100, blank=False)
destination = models.EmailField(_("destination address"), null=False, blank=False) destination = models.CharField(
_("destination address"),
max_length=254,
null=False,
blank=False,
validators=[
EmailValidator(allowlist=["localhost", "devnull"]),
],
)
domain = models.ForeignKey( domain = models.ForeignKey(
MailDomain, MailDomain,
on_delete=models.CASCADE, on_delete=models.CASCADE,

View File

@@ -182,3 +182,33 @@ def test_api_aliases_create__existing_mailbox_ok(dimail_token_ok):
) )
assert response.status_code == status.HTTP_201_CREATED assert response.status_code == status.HTTP_201_CREATED
assert models.Alias.objects.exists() assert models.Alias.objects.exists()
@responses.activate
def test_api_aliases_create__devnull_destination_ok(dimail_token_ok):
"""Can create alias where destination is devnull@devnull."""
access = factories.MailDomainAccessFactory(
role="owner", domain=factories.MailDomainEnabledFactory()
)
client = APIClient()
client.force_login(access.user)
responses.post(
re.compile(rf".*/domains/{access.domain.name}/aliases/"),
json={
"username": "spammy-address",
"domain": access.domain.name,
"destination": "devnull@devnull",
"allow_to_send": False,
},
status=status.HTTP_201_CREATED,
content_type="application/json",
)
response = client.post(
f"/api/v1.0/mail-domains/{access.domain.slug}/aliases/",
{"local_part": "spammy-address", "destination": "devnull@devnull"},
)
assert response.status_code == status.HTTP_201_CREATED
assert models.Alias.objects.exists()

View File

@@ -0,0 +1,19 @@
"""
Unit tests for the Alias model
"""
import pytest
from mailbox_manager import factories, models
pytestmark = pytest.mark.django_db
def test_models_aliases__devnull_destination_ok():
"""Can create alias where destination is devnull@devnull."""
models.Alias.objects.create(
local_part="spam",
domain=factories.MailDomainEnabledFactory(),
destination="devnull@devnull",
)

View File

@@ -199,6 +199,12 @@ def test_dimail_synchronization__synchronize_aliases(dimail_token_ok): # pylint
"destination": existing_mailbox.secondary_email, "destination": existing_mailbox.secondary_email,
"allow_to_send": False, "allow_to_send": False,
}, },
{ # alias to devnull@devnull
"username": "spam",
"domain": alias.domain.name,
"destination": "devnull@devnull",
"allow_to_send": False,
},
] ]
responses.get( responses.get(
re.compile(rf".*/domains/{alias.domain.name}/aliases/"), re.compile(rf".*/domains/{alias.domain.name}/aliases/"),
@@ -209,8 +215,8 @@ def test_dimail_synchronization__synchronize_aliases(dimail_token_ok): # pylint
imported_aliases = dimail_client.import_aliases(alias.domain) imported_aliases = dimail_client.import_aliases(alias.domain)
assert len(imported_aliases) == 3 assert len(imported_aliases) == 4
assert models.Alias.objects.count() == 4 assert models.Alias.objects.count() == 5
@pytest.mark.parametrize( @pytest.mark.parametrize(