FEATURE: Add email normalization rules setting (#14593)

When this setting is turned on, it will check that normalized emails
are unique. Normalized emails are emails without any dots or plus
aliases.

This setting can be used to block use of aliases of the same email
address.
This commit is contained in:
Bianca Nenciu 2021-11-24 11:30:06 +02:00 committed by GitHub
parent a6aff40e4b
commit 3ea8937157
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 76 additions and 8 deletions

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
module Jobs
class MigrateNormalizedEmails < ::Jobs::Onceoff
def execute_onceoff(args)
::UserEmail.find_each do |user_email|
user_email.update(normalized_email: user_email.normalize_email)
end
end
end
end

View File

@ -7,6 +7,7 @@ class UserEmail < ActiveRecord::Base
attr_accessor :skip_validate_unique_email
before_validation :strip_downcase_email
before_validation :normalize_email
validates :email, presence: true
validates :email, email: true, if: :validate_email?
@ -17,6 +18,14 @@ class UserEmail < ActiveRecord::Base
scope :secondary, -> { where(primary: false) }
def normalize_email
self.normalized_email = if self.email.present?
username, domain = self.email.split('@', 2)
username = username.gsub('.', '').gsub(/\+.*/, '')
"#{username}@#{domain}"
end
end
private
def strip_downcase_email
@ -37,9 +46,13 @@ class UserEmail < ActiveRecord::Base
end
def unique_email
if self.class.where("lower(email) = ?", email).exists?
self.errors.add(:email, :taken)
email_exists = if SiteSetting.normalize_emails?
self.class.where("lower(email) = ? OR lower(normalized_email) = ?", email, normalized_email).exists?
else
self.class.where("lower(email) = ?", email).exists?
end
self.errors.add(:email, :taken) if email_exists
end
def user_id_not_changed
@ -55,16 +68,18 @@ end
#
# Table name: user_emails
#
# id :integer not null, primary key
# user_id :integer not null
# email :string(513) not null
# primary :boolean default(FALSE), not null
# created_at :datetime not null
# updated_at :datetime not null
# id :integer not null, primary key
# user_id :integer not null
# email :string(513) not null
# primary :boolean default(FALSE), not null
# created_at :datetime not null
# updated_at :datetime not null
# normalized_email :string
#
# Indexes
#
# index_user_emails_on_email (lower((email)::text)) UNIQUE
# index_user_emails_on_normalized_email (lower((normalized_email)::text))
# index_user_emails_on_user_id (user_id)
# index_user_emails_on_user_id_and_primary (user_id,primary) UNIQUE WHERE "primary"
#

View File

@ -1649,6 +1649,7 @@ en:
allow_index_in_robots_txt: "Specify in robots.txt that this site is allowed to be indexed by web search engines. In exceptional cases you can permanently <a href='%{base_path}/admin/customize/robots'>override robots.txt</a>."
blocked_email_domains: "A pipe-delimited list of email domains that users are not allowed to register accounts with. Example: mailinator.com|trashmail.net"
allowed_email_domains: "A pipe-delimited list of email domains that users MUST register accounts with. WARNING: Users with email domains other than those listed will not be allowed!"
normalize_emails: "Check if normalized email is unique. Normalized email removes all dots from the username and everything between + and @ symbols."
auto_approve_email_domains: "Users with email addresses from this list of domains will be automatically approved."
hide_email_address_taken: "Don't inform users that an account exists with a given email address during signup and from the forgot password form."
log_out_strict: "When logging out, log out ALL sessions for the user on all devices"

View File

@ -510,6 +510,8 @@ login:
default: ""
type: list
list_type: simple
normalize_emails:
default: false
auto_approve_email_domains:
default: ""
type: list

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class AddNormalizedEmailToUserEmail < ActiveRecord::Migration[6.1]
def change
add_column :user_emails, :normalized_email, :string
execute "CREATE INDEX index_user_emails_on_normalized_email ON user_emails (LOWER(normalized_email))"
end
def down
execute "DROP INDEX index_user_emails_on_normalized_email"
drop_column :user_emails, :normalized_email, :string
end
end

View File

@ -26,6 +26,32 @@ describe UserEmail do
end
end
describe 'normalized_email' do
it 'checks if normalized email is unique' do
SiteSetting.normalize_emails = true
user_email = user.user_emails.create(email: "a.b+c@example.com", primary: false)
expect(user_email.normalized_email).to eq("ab@example.com")
expect(user_email).to be_valid
user_email = user.user_emails.create(email: "a.b+d@example.com", primary: false)
expect(user_email.normalized_email).to eq("ab@example.com")
expect(user_email).not_to be_valid
end
it 'does not check uniqueness if email normalization is not enabled' do
SiteSetting.normalize_emails = false
user_email = user.user_emails.create(email: "a.b+c@example.com", primary: false)
expect(user_email.normalized_email).to eq("ab@example.com")
expect(user_email).to be_valid
user_email = user.user_emails.create(email: "a.b+d@example.com", primary: false)
expect(user_email.normalized_email).to eq("ab@example.com")
expect(user_email).to be_valid
end
end
context "indexes" do
it "allows only one primary email" do
expect {