-
Notifications
You must be signed in to change notification settings - Fork 0
Add comprehensive email validation for blocked users #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: blocked-email-validation-pre
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| class BlockedEmail < ActiveRecord::Base | ||
|
|
||
| before_validation :set_defaults | ||
|
|
||
| validates :email, presence: true, uniqueness: true | ||
|
|
||
| def self.actions | ||
| @actions ||= Enum.new(:block, :do_nothing) | ||
| end | ||
|
|
||
| def self.should_block?(email) | ||
| record = BlockedEmail.where(email: email).first | ||
| if record | ||
| record.match_count += 1 | ||
| record.last_match_at = Time.zone.now | ||
| record.save | ||
| end | ||
| record && record.action_type == actions[:block] | ||
| end | ||
|
|
||
| def set_defaults | ||
| self.action_type ||= BlockedEmail.actions[:block] | ||
| end | ||
|
|
||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| class CreateBlockedEmails < ActiveRecord::Migration | ||
| def change | ||
| create_table :blocked_emails do |t| | ||
| t.string :email, null: false | ||
| t.integer :action_type, null: false | ||
| t.integer :match_count, null: false, default: 0 | ||
| t.datetime :last_match_at | ||
| t.timestamps | ||
| end | ||
| add_index :blocked_emails, :email, unique: true | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,24 @@ | ||||||
| class EmailValidator < ActiveModel::EachValidator | ||||||
|
|
||||||
| def validate_each(record, attribute, value) | ||||||
| if (setting = SiteSetting.email_domains_whitelist).present? | ||||||
| unless email_in_restriction_setting?(setting, value) | ||||||
| record.errors.add(attribute, I18n.t(:'user.email.not_allowed')) | ||||||
| end | ||||||
| elsif (setting = SiteSetting.email_domains_blacklist).present? | ||||||
| if email_in_restriction_setting?(setting, value) | ||||||
| record.errors.add(attribute, I18n.t(:'user.email.not_allowed')) | ||||||
| end | ||||||
| end | ||||||
| if record.errors[attribute].blank? and BlockedEmail.should_block?(value) | ||||||
|
||||||
| if record.errors[attribute].blank? and BlockedEmail.should_block?(value) | |
| if record.errors[attribute].blank? && BlockedEmail.should_block?(value) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,23 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| require 'spec_helper' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| describe EmailValidator do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let(:record) { Fabricate.build(:user, email: "bad@spamclub.com") } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let(:validator) { described_class.new({attributes: :email}) } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| subject(:validate) { validator.validate_each(record,:email,record.email) } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| context "blocked email" do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it "doesn't add an error when email doesn't match a blocked email" do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BlockedEmail.stubs(:should_block?).with(record.email).returns(false) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| validate | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| record.errors[:email].should_not be_present | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it "adds an error when email matches a blocked email" do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BlockedEmail.stubs(:should_block?).with(record.email).returns(true) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| validate | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| record.errors[:email].should be_present | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+13
to
+19
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| record.errors[:email].should_not be_present | |
| end | |
| it "adds an error when email matches a blocked email" do | |
| BlockedEmail.stubs(:should_block?).with(record.email).returns(true) | |
| validate | |
| record.errors[:email].should be_present | |
| expect(record.errors[:email]).not_to be_present | |
| end | |
| it "adds an error when email matches a blocked email" do | |
| BlockedEmail.stubs(:should_block?).with(record.email).returns(true) | |
| validate | |
| expect(record.errors[:email]).to be_present |
Copilot
AI
Jan 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The deprecated RSpec syntax .should is used. Consider updating to the modern expect syntax: expect(record.errors[:email]).to be_present
| record.errors[:email].should_not be_present | |
| end | |
| it "adds an error when email matches a blocked email" do | |
| BlockedEmail.stubs(:should_block?).with(record.email).returns(true) | |
| validate | |
| record.errors[:email].should be_present | |
| expect(record.errors[:email]).not_to be_present | |
| end | |
| it "adds an error when email matches a blocked email" do | |
| BlockedEmail.stubs(:should_block?).with(record.email).returns(true) | |
| validate | |
| expect(record.errors[:email]).to be_present |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| Fabricator(:blocked_email) do | ||
| email { sequence(:email) { |n| "bad#{n}@spammers.org" } } | ||
| action_type BlockedEmail.actions[:block] | ||
| end |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,48 @@ | ||||||
| require 'spec_helper' | ||||||
|
|
||||||
| describe BlockedEmail do | ||||||
|
|
||||||
| let(:email) { 'block@spamfromhome.org' } | ||||||
|
|
||||||
| describe "new record" do | ||||||
| it "sets a default action_type" do | ||||||
| BlockedEmail.create(email: email).action_type.should == BlockedEmail.actions[:block] | ||||||
|
||||||
| end | ||||||
|
|
||||||
| it "last_match_at is null" do | ||||||
| # If we manually load the table with some emails, we can see whether those emails | ||||||
| # have ever been blocked by looking at last_match_at. | ||||||
| BlockedEmail.create(email: email).last_match_at.should be_nil | ||||||
|
||||||
| BlockedEmail.create(email: email).last_match_at.should be_nil | |
| expect(BlockedEmail.create(email: email).last_match_at).to be_nil |
Copilot
AI
Jan 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The deprecated RSpec syntax .should is used. Consider updating to the modern expect syntax: expect(subject).to be false
Copilot
AI
Jan 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The deprecated RSpec syntax .should is used. Consider updating to the modern expect syntax: expect(blocked_email.last_match_at).to be_within_one_second_of(Time.zone.now)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
should_block?method performs a write operation (incrementing match_count and updating last_match_at) on every check. This creates a database write for every email validation, even during read-only operations like form validation. Consider separating the blocking check from the statistics update, or using a background job to update statistics asynchronously.