-
Notifications
You must be signed in to change notification settings - Fork 2
Add comprehensive email validation for blocked users #3
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-copilot
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 | ||||||||||||||||
|
Comment on lines
+14
to
+16
|
||||||||||||||||
| record.match_count += 1 | |
| record.last_match_at = Time.zone.now | |
| record.save | |
| record.update_columns( | |
| match_count: record.match_count + 1, | |
| last_match_at: Time.zone.now | |
| ) |
| 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) | ||
| record.errors.add(attribute, I18n.t(:'user.email.blocked')) | ||
| end | ||
| end | ||
|
|
||
| def email_in_restriction_setting?(setting, value) | ||
| domains = setting.gsub('.', '\.') | ||
| regexp = Regexp.new("@(#{domains})", true) | ||
| value =~ regexp | ||
| end | ||
|
|
||
| end |
| 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
Jul 26, 2025
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.
Use the newer RSpec expectation syntax expect(record.errors[:email]).to be_present instead of the deprecated should syntax.
| 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| describe "#should_block?" do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| subject { BlockedEmail.should_block?(email) } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it "returns false if a record with the email doesn't exist" do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| subject.should be_false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| shared_examples "when a BlockedEmail record matches" do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it "updates statistics" do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Timecop.freeze(Time.zone.now) do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect { subject }.to change { blocked_email.reload.match_count }.by(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| blocked_email.last_match_at.should be_within_one_second_of(Time.zone.now) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| context "action_type is :block" do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let!(:blocked_email) { Fabricate(:blocked_email, email: email, action_type: BlockedEmail.actions[:block]) } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it { should be_true } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| include_examples "when a BlockedEmail record matches" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| context "action_type is :do_nothing" do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let!(:blocked_email) { Fabricate(:blocked_email, email: email, action_type: BlockedEmail.actions[:do_nothing]) } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it { should be_false } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+23
to
+43
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| subject.should be_false | |
| end | |
| shared_examples "when a BlockedEmail record matches" do | |
| it "updates statistics" do | |
| Timecop.freeze(Time.zone.now) do | |
| expect { subject }.to change { blocked_email.reload.match_count }.by(1) | |
| blocked_email.last_match_at.should be_within_one_second_of(Time.zone.now) | |
| end | |
| end | |
| end | |
| context "action_type is :block" do | |
| let!(:blocked_email) { Fabricate(:blocked_email, email: email, action_type: BlockedEmail.actions[:block]) } | |
| it { should be_true } | |
| include_examples "when a BlockedEmail record matches" | |
| end | |
| context "action_type is :do_nothing" do | |
| let!(:blocked_email) { Fabricate(:blocked_email, email: email, action_type: BlockedEmail.actions[:do_nothing]) } | |
| it { should be_false } | |
| expect(subject).to be_falsey | |
| end | |
| shared_examples "when a BlockedEmail record matches" do | |
| it "updates statistics" do | |
| Timecop.freeze(Time.zone.now) do | |
| expect { subject }.to change { blocked_email.reload.match_count }.by(1) | |
| expect(blocked_email.last_match_at).to be_within_one_second_of(Time.zone.now) | |
| end | |
| end | |
| end | |
| context "action_type is :block" do | |
| let!(:blocked_email) { Fabricate(:blocked_email, email: email, action_type: BlockedEmail.actions[:block]) } | |
| it { expect(subject).to be_truthy } | |
| include_examples "when a BlockedEmail record matches" | |
| end | |
| context "action_type is :do_nothing" do | |
| let!(:blocked_email) { Fabricate(:blocked_email, email: email, action_type: BlockedEmail.actions[:do_nothing]) } | |
| it { expect(subject).to be_falsey } |
Copilot
AI
Jul 26, 2025
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.
Use the newer RSpec expectation syntax it { is_expected.to be_truthy } instead of the deprecated should syntax.
| subject.should be_false | |
| end | |
| shared_examples "when a BlockedEmail record matches" do | |
| it "updates statistics" do | |
| Timecop.freeze(Time.zone.now) do | |
| expect { subject }.to change { blocked_email.reload.match_count }.by(1) | |
| blocked_email.last_match_at.should be_within_one_second_of(Time.zone.now) | |
| end | |
| end | |
| end | |
| context "action_type is :block" do | |
| let!(:blocked_email) { Fabricate(:blocked_email, email: email, action_type: BlockedEmail.actions[:block]) } | |
| it { should be_true } | |
| include_examples "when a BlockedEmail record matches" | |
| end | |
| context "action_type is :do_nothing" do | |
| let!(:blocked_email) { Fabricate(:blocked_email, email: email, action_type: BlockedEmail.actions[:do_nothing]) } | |
| it { should be_false } | |
| is_expected.to be_falsey | |
| end | |
| shared_examples "when a BlockedEmail record matches" do | |
| it "updates statistics" do | |
| Timecop.freeze(Time.zone.now) do | |
| expect { subject }.to change { blocked_email.reload.match_count }.by(1) | |
| expect(blocked_email.last_match_at).to be_within_one_second_of(Time.zone.now) | |
| end | |
| end | |
| end | |
| context "action_type is :block" do | |
| let!(:blocked_email) { Fabricate(:blocked_email, email: email, action_type: BlockedEmail.actions[:block]) } | |
| it { is_expected.to be_truthy } | |
| include_examples "when a BlockedEmail record matches" | |
| end | |
| context "action_type is :do_nothing" do | |
| let!(:blocked_email) { Fabricate(:blocked_email, email: email, action_type: BlockedEmail.actions[:do_nothing]) } | |
| it { is_expected.to be_falsey } |
Copilot
AI
Jul 26, 2025
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.
Use the newer RSpec expectation syntax it { is_expected.to be_falsey } instead of the deprecated should syntax.
| it { should be_false } | |
| it { is_expected.to be_falsey } |
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 increment and save operations are not atomic, which could lead to race conditions in concurrent environments. Consider using
increment!(:match_count)orupdate_countersfor atomic counter updates.