Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/backend/core/mda/inbound.py
Original file line number Diff line number Diff line change
Expand Up @@ -630,8 +630,10 @@ def deliver_inbound_message( # pylint: disable=too-many-branches, too-many-stat
models.Contact(email=email).full_clean(
exclude=["mailbox", "name"]
) # Validate

recipient_contact, created = models.Contact.objects.get_or_create(
email=email,
name=name or email.split("@")[0],
mailbox=mailbox, # Associate contact with the recipient mailbox
defaults={"name": name or email.split("@")[0], "email": email},
)
Expand Down
17 changes: 17 additions & 0 deletions src/backend/core/migrations/0010_alter_contact_unique_together.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 5.1.12 on 2025-10-03 11:53

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('core', '0009_messagetemplate_blob_maildomain_alter_blob_mailbox_and_more'),
]

operations = [
migrations.AlterUniqueTogether(
name='contact',
unique_together={('email', 'mailbox', 'name')},
),
]
2 changes: 1 addition & 1 deletion src/backend/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1126,7 +1126,7 @@ class Meta:
db_table = "messages_contact"
verbose_name = _("contact")
verbose_name_plural = _("contacts")
unique_together = ("email", "mailbox")
unique_together = ("email", "mailbox", "name")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Risk: nullable field in unique constraint.

Adding name to unique_together when the field is nullable (line 1117) can lead to unexpected behavior. In most SQL databases (including PostgreSQL), NULL != NULL, so multiple contacts with the same email and mailbox but NULL names would be allowed, potentially creating duplicates.

Consider one of these solutions:

Solution 1: Make name required

-    name = models.CharField(_("name"), max_length=255, null=True, blank=True)
+    name = models.CharField(_("name"), max_length=255)

Solution 2: Use application-level default

If name must remain nullable in the database, ensure the application always provides a default value (like the email local-part) before saving, and document this requirement clearly.

Solution 3: Add a database constraint

Add a CheckConstraint to ensure name is never NULL:

class Meta:
    db_table = "messages_contact"
    verbose_name = _("contact")
    verbose_name_plural = _("contacts")
    unique_together = ("email", "mailbox", "name")
    constraints = [
        models.CheckConstraint(
            check=~models.Q(name__isnull=True),
            name="contact_name_not_null",
        ),
    ]


def __str__(self):
if self.name:
Expand Down
8 changes: 5 additions & 3 deletions src/backend/core/tests/importer/test_imap_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def email_with_duplicate_recipients():
msg["From"] = "[email protected]"
msg["To"] = "[email protected], [email protected]" # Duplicate TO
msg["Cc"] = "[email protected], [email protected]" # Duplicate CC
msg["Bcc"] = "Jean BCC <[email protected]>, Marie BCC <[email protected]>, [email protected]" # Duplicate BCC
msg["Subject"] = "Test Subject with Duplicates"
msg["Message-ID"] = "<[email protected]>"
msg["Date"] = "Thu, 1 Jan 2024 12:00:00 +0000"
Expand Down Expand Up @@ -442,9 +443,6 @@ def test_imap_import_task_duplicate_recipients(
recipients = message.recipients.all()
recipient_emails = [r.contact.email for r in recipients]

# Should have unique recipients (no duplicates)
assert len(recipient_emails) == len(set(recipient_emails))

# Should have the expected recipients
assert "[email protected]" in recipient_emails
assert "[email protected]" in recipient_emails
Expand All @@ -456,9 +454,13 @@ def test_imap_import_task_duplicate_recipients(
cc_recipients = message.recipients.filter(
type=enums.MessageRecipientTypeChoices.CC
)
bcc_recipients = message.recipients.filter(
type=enums.MessageRecipientTypeChoices.BCC
)

assert to_recipients.count() == 1 # Only one TO recipient (duplicate removed)
assert cc_recipients.count() == 1 # Only one CC recipient (duplicate removed)
assert bcc_recipients.count() == 3 # There is differents names for the same email so 3 contacts are created

# Verify the content
assert (
Expand Down
Loading