Skip to content

Commit f39dff6

Browse files
authored
Merge pull request #2571 from mroderick/fix/invalid-emails
fix: validate email format and add defensive mailer checks
2 parents 07c5322 + fcdf515 commit f39dff6

6 files changed

Lines changed: 105 additions & 0 deletions

File tree

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ gem 'csv' # LOCKED: csv was loaded from the standard library, but is not part of
1818
gem 'delayed_job'
1919
gem 'delayed_job_active_record'
2020
gem 'drb' # LOCKED: Added because of pry-remote
21+
gem 'email_validator'
2122
gem 'font_awesome5_rails'
2223
gem 'bootstrap', '~> 5'
2324
gem 'friendly_id'

Gemfile.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ GEM
193193
dotenv (= 3.2.0)
194194
railties (>= 6.1)
195195
drb (2.2.3)
196+
email_validator (2.2.4)
197+
activemodel
196198
erb (6.0.2)
197199
erubi (1.13.1)
198200
execjs (2.10.1)
@@ -634,6 +636,7 @@ DEPENDENCIES
634636
delayed_job_active_record
635637
dotenv-rails
636638
drb
639+
email_validator
637640
fabrication
638641
faker
639642
faraday

app/helpers/email_header_helper.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,19 @@ module EmailHeaderHelper
22
private
33

44
def mail_args(member, subject, from_email = 'meetings@codebar.io', cc = '', bcc = '')
5+
return nil if invalid_email?(member.email, member.id)
6+
57
{ from: "codebar.io <#{from_email}>",
68
to: member.email,
79
cc: cc,
810
bcc: bcc,
911
subject: subject }
1012
end
13+
14+
def invalid_email?(email, member_id)
15+
return false if EmailValidator.valid?(email, mode: :strict)
16+
17+
Rails.logger.warn("[EmailHeaderHelper] Invalid email for member_id=#{member_id}: #{email}")
18+
true
19+
end
1120
end

app/models/member.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class Member < ApplicationRecord
2626
validates :auth_services, presence: true
2727
validates :name, :surname, :email, :about_you, presence: true, if: :can_log_in?
2828
validates :email, uniqueness: true
29+
validates :email, email: { mode: :strict }, if: :can_log_in?
2930
validates :about_you, length: { maximum: 255 }
3031

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