Skip to content

fix: validate email format and add defensive mailer checks#2571

Merged
olleolleolle merged 3 commits intocodebar:masterfrom
mroderick:fix/invalid-emails
Apr 14, 2026
Merged

fix: validate email format and add defensive mailer checks#2571
olleolleolle merged 3 commits intocodebar:masterfrom
mroderick:fix/invalid-emails

Conversation

@mroderick
Copy link
Copy Markdown
Collaborator

@mroderick mroderick commented Apr 13, 2026

Summary

Fixes Net::SMTPSyntaxError: 501 Recipient syntax error that occurred in production when sending workshop attendance reminders.

Root Cause Analysis

The error occurs in WorkshopInvitationManagerConcerns#send_workshop_attendance_reminders when iterating over workshop.attendances.not_reminded and sending reminder emails via WorkshopInvitationMailer#attending_reminder.

Root cause: Member email addresses are not validated before SMTP delivery. The EmailHeaderHelper#mail_args method passes member.email directly to the mailer without format validation.

Affected Data

Found 18 members with malformed email addresses in production database. Examples include:

Member ID Email Issue
XXXX user@example.com Valid (for comparison)
XXXX missing-atsign.example.com Missing @
XXXX user@missingtld Missing TLD
XXXX user@nodottld No .
XXXX Matt at made by lamp dot com Human typo
XXXX singlechar Single character

These records likely originated from:

  • Very early data (before validation existed)
  • Manual admin entry errors
  • GitHub OAuth edge cases

Changes

1. Database Cleanup

Set invalid emails to NULL so they skip future mailings:

UPDATE members SET email = NULL 
WHERE email IS NOT NULL 
  AND email !~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$';

2. Member Model Validation

app/models/member.rb — Added email validation using email_validator gem:

validates :email, email: { mode: :strict }, if: :can_log_in?

3. Defensive Mailer Check

app/helpers/email_header_helper.rb — Returns nil for invalid emails with warning log:

def invalid_email?(email, member_id)
  return false if EmailValidator.valid?(email, mode: :strict)
  
  Rails.logger.warn("[EmailHeaderHelper] Invalid email for member_id=#{member_id}: #{email}")
  true
end

4. Added Gem

Added email_validator gem (32M+ downloads, MIT licensed) for:

  • Single source of truth for email validation
  • Multiple validation modes (:loose, :strict, :rfc)
  • Standalone API for use outside models

How to Verify

1. Run the cleanup SQL

Connect to production database (Heroku postgres or production Postgres):

# Preview affected records
SELECT id, email FROM members 
WHERE email IS NOT NULL 
  AND email !~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$';

# Set to NULL
UPDATE members SET email = NULL 
WHERE email IS NOT NULL 
  AND email !~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$';

2. Verify cleanup

-- Should return 0
SELECT COUNT(*) FROM members 
WHERE email IS NOT NULL 
  AND email !~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$';

3. Check logs

After deploying, watch for warning logs when rake tasks run:

[EmailHeaderHelper] Invalid email for member_id=XXXX: invalid-email

Tests

  • 6 new tests for email format validation in spec/models/member_spec.rb
  • 9 new tests for EmailHeaderHelper#mail_args in spec/helpers/email_header_helper_spec.rb

Run tests locally:

bundle exec rspec spec/models/member_spec.rb spec/helpers/email_header_helper_spec.rb

Impact

  • New validations: Members with can_log_in=true must provide valid email format
  • Backward compatibility: Existing members without can_log_in can still have NULL emails
  • Mailer defense: Even if bad data slips through, SMTP errors are prevented with logging

- Add email format validation to Member model (conditional on can_log_in)
- Add defensive validation in EmailHeaderHelper with warning logs
- Add tests for email format validation
- Add tests for EmailHeaderHelper#mail_args

The SMTP error was caused by invalid email addresses in the database.
Member 2413 had 'emmalepinay.yahoo.com' (missing @) causing 501 errors.

Resolves the root cause at three layers:
1. Database cleanup (set NULL for bad emails)
2. Member model validation (reject invalid at registration)
3. Mailer defense (skip invalid with log warning)
@mroderick mroderick requested a review from olleolleolle April 13, 2026 16:14
- Add email_validator gem for Rails-native email validation
- Replace custom EMAIL_REGEX with gem's strict mode
- Single source of truth for email validation
Use type: :helper to include EmailHeaderHelper automatically, making
the spec more idiomatic. Removed verbose anonymous class construction.
@mroderick
Copy link
Copy Markdown
Collaborator Author

Thank you for the suggestion! You're right - I've updated the spec to use the idiomatic pattern which automatically includes the helper module. Much cleaner now.

Commit: fcdf515

Copy link
Copy Markdown
Collaborator

@olleolleolle olleolleolle left a comment

Choose a reason for hiding this comment

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

Nice!

@olleolleolle olleolleolle merged commit f39dff6 into codebar:master Apr 14, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants