Validation failed: Primary email has already been taken when trying to handle Gmail dot variants or add secondary emails

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

  1. Create a user account on Discourse with the primary email username@gmail.com.

  2. Have that user send an incoming email to a category/reply address from user.name@gmail.com.

  3. Observe the rejection in the incoming email logs due to ActiveRecord::RecordInvalid.

  4. Try to add user.name@gmail.com as a secondary email to the username@gmail.com account via the Rails console:

    UserEmail.create!(user_id: target_id, email: 'user.name@gmail.com', primary: false)
    
    
  5. Observe the model validation crash.

Expected Behavior

Discourse handles Gmail normalization cleanly. It should either:

  1. Recognize the incoming dotted Gmail variant as belonging to the primary account seamlessly during the incoming mail handling phase.

  2. 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.com as a “new” string, so it tries to act on it (create a staged user or append a secondary email).

  • During the validation phase, the UserEmail model runs its Gmail normalization logic, strips the dots, sees that username@gmail.com is already the primary email index for that user_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!

2 Likes