Skip to content

Commit 23d0eb2

Browse files
committed
fix: validate email format and add defensive mailer checks
- 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)
1 parent 195221d commit 23d0eb2

4 files changed

Lines changed: 110 additions & 0 deletions

File tree

app/helpers/email_header_helper.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
module EmailHeaderHelper
2+
EMAIL_REGEX = /\A[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\z/
3+
24
private
35

46
def mail_args(member, subject, from_email = 'meetings@codebar.io', cc = '', bcc = '')
7+
return nil if invalid_email?(member.email, member.id)
8+
59
{ from: "codebar.io <#{from_email}>",
610
to: member.email,
711
cc: cc,
812
bcc: bcc,
913
subject: subject }
1014
end
15+
16+
def invalid_email?(email, member_id)
17+
return false if email.present? && email.match?(EMAIL_REGEX)
18+
19+
Rails.logger.warn("[EmailHeaderHelper] Invalid email for member_id=#{member_id}: #{email}")
20+
true
21+
end
1122
end

app/models/member.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,12 @@ class Member < ApplicationRecord
2323
has_many :announcements, -> { distinct }, through: :groups
2424
has_many :meeting_invitations
2525

26+
EMAIL_REGEX = /\A[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\z/
27+
2628
validates :auth_services, presence: true
2729
validates :name, :surname, :email, :about_you, presence: true, if: :can_log_in?
2830
validates :email, uniqueness: true
31+
validates :email, format: { with: EMAIL_REGEX }, if: :can_log_in?
2932
validates :about_you, length: { maximum: 255 }
3033

3134
DIETARY_RESTRICTIONS = %w[vegan vegetarian pescetarian halal gluten_free dairy_free other].freeze
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
RSpec.describe EmailHeaderHelper do
2+
subject do
3+
Class.new do
4+
include EmailHeaderHelper
5+
public :mail_args
6+
end.new
7+
end
8+
9+
describe '#mail_args' do
10+
let(:member) { Struct.new(:id, :email).new(1, 'test@example.com') }
11+
12+
it 'returns mail arguments for valid email' do
13+
result = subject.mail_args(member, 'Test Subject')
14+
expect(result[:to]).to eq('test@example.com')
15+
expect(result[:subject]).to eq('Test Subject')
16+
end
17+
18+
it 'returns nil for nil email' do
19+
member = Struct.new(:id, :email).new(1, nil)
20+
result = subject.mail_args(member, 'Test Subject')
21+
expect(result).to be_nil
22+
end
23+
24+
it 'returns nil for blank email' do
25+
member = Struct.new(:id, :email).new(1, '')
26+
result = subject.mail_args(member, 'Test Subject')
27+
expect(result).to be_nil
28+
end
29+
30+
it 'returns nil for invalid email format' do
31+
member = Struct.new(:id, :email).new(1, 'invalid-email')
32+
result = subject.mail_args(member, 'Test Subject')
33+
expect(result).to be_nil
34+
end
35+
36+
it 'returns nil for email missing @ symbol' do
37+
member = Struct.new(:id, :email).new(1, 'invalidexample.com')
38+
result = subject.mail_args(member, 'Test Subject')
39+
expect(result).to be_nil
40+
end
41+
42+
it 'returns nil for email missing TLD' do
43+
member = Struct.new(:id, :email).new(1, 'invalid@example')
44+
result = subject.mail_args(member, 'Test Subject')
45+
expect(result).to be_nil
46+
end
47+
48+
it 'returns mail arguments for valid email with plus addressing' do
49+
member = Struct.new(:id, :email).new(1, 'user+tag@example.com')
50+
result = subject.mail_args(member, 'Test Subject')
51+
expect(result[:to]).to eq('user+tag@example.com')
52+
end
53+
54+
it 'includes from email when provided' do
55+
result = subject.mail_args(member, 'Test Subject', 'custom@codebar.io')
56+
expect(result[:from]).to eq('codebar.io <custom@codebar.io>')
57+
end
58+
59+
it 'includes cc and bcc when provided' do
60+
result = subject.mail_args(member, 'Test Subject', 'from@codebar.io', 'cc@codebar.io', 'bcc@codebar.io')
61+
expect(result[:cc]).to eq('cc@codebar.io')
62+
expect(result[:bcc]).to eq('bcc@codebar.io')
63+
end
64+
end
65+
end

spec/models/member_spec.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,37 @@
1414
it { expect(member).to validate_presence_of(:surname) }
1515
it { expect(member).to validate_presence_of(:email) }
1616
it { expect(member).to validate_presence_of(:about_you) }
17+
18+
it 'accepts valid email format' do
19+
member.email = 'valid@example.com'
20+
expect(member).to be_valid
21+
end
22+
23+
it 'rejects invalid email format' do
24+
member.email = 'invalid-email'
25+
expect(member).not_to be_valid
26+
expect(member.errors[:email]).to include('is invalid')
27+
end
28+
29+
it 'rejects email missing @ symbol' do
30+
member.email = 'invalidexample.com'
31+
expect(member).not_to be_valid
32+
end
33+
34+
it 'rejects email missing TLD' do
35+
member.email = 'invalid@example'
36+
expect(member).not_to be_valid
37+
end
38+
39+
it 'accepts email with valid subdomains' do
40+
member.email = 'user@mail.example.com'
41+
expect(member).to be_valid
42+
end
43+
44+
it 'accepts email with plus addressing' do
45+
member.email = 'user+tag@example.com'
46+
expect(member).to be_valid
47+
end
1748
end
1849
end
1950

0 commit comments

Comments
 (0)