Discourse Version: 2026.5.0-latest.1
Context
When an external user sends an email to the incoming mail handler using a Gmail “dot” variant (e.g., user.name@gmail.com) but their registered forum account is the non-dotted primary version (username@gmail.com), the incoming mail handler crashes with an unhandled exception: ActiveRecord::RecordInvalid (Validation failed: Primary email has already been taken).
Furthermore, attempting to resolve this by adding the dotted variant as a secondary email to the user profile—either via the UI or the Rails Console model layer using UserEmail.create!—fails with the exact same validation loop error. The only workaround is a raw SQL injection into the database, bypassing ActiveRecord.
Steps to Reproduce
-
Create a user account on Discourse with the primary email
username@gmail.com. -
Have that user send an incoming email to a category/reply address from
user.name@gmail.com. -
Observe the rejection in the incoming email logs due to
ActiveRecord::RecordInvalid. -
Try to add
user.name@gmail.comas a secondary email to theusername@gmail.comaccount via the Rails console:UserEmail.create!(user_id: target_id, email: 'user.name@gmail.com', primary: false) -
Observe the model validation crash.
Expected Behavior
Discourse handles Gmail normalization cleanly. It should either:
-
Recognize the incoming dotted Gmail variant as belonging to the primary account seamlessly during the incoming mail handling phase.
-
At the very least, allow an administrator to append the dotted variation as a secondary email to the main account without triggering a “Primary email taken” application block, since it belongs to the same user and is explicitly set to
primary: false.
Actual Behavior
The application layer gets stuck in a logic loop:
-
It sees
user.name@gmail.comas a “new” string, so it tries to act on it (create a staged user or append a secondary email). -
During the validation phase, the
UserEmailmodel runs its Gmail normalization logic, strips the dots, sees thatusername@gmail.comis already the primary email index for thatuser_id, and blocks its own execution under the mistaken assumption that a duplicate record conflict is occurring.
Workaround Used to Unblock
The only way to resolve this was to SSH into the container and execute raw SQL to bypass ActiveRecord validations completely:
sql = "INSERT INTO user_emails (user_id, email, \"primary\", created_at, updated_at) VALUES (X, 'user.name@gmail.com', false, NOW(), NOW())"
ActiveRecord::Base.connection.execute(sql)
Once forced via raw SQL, incoming mail tracking works perfectly. The validation code should be updated to account for this edge case.
Thanks!