mirror of
https://github.com/discourse/discourse.git
synced 2026-08-08 13:25:19 +08:00
With normalize_emails enabled an alias of an address already in use (dot or plus) can never be saved, but nothing checked for it up front. Adding a secondary email accepted the request and mailed a confirmation link, then failed on click with a raw "Email has already been taken". Incoming email from an alias hit the same error while staging a user, and bounced a generic "unrecognized error" reply. Both are now caught before anything is written or mailed — the user gets a clear error, the sender gets a bounce telling them to send from the address they signed up with. The alias is still rejected; normalize_emails enforces uniqueness on the normalized form.
138 lines
3.8 KiB
Ruby
Vendored
138 lines
3.8 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
class UserEmail < ActiveRecord::Base
|
|
belongs_to :user
|
|
|
|
attr_accessor :skip_validate_email
|
|
attr_accessor :skip_validate_unique_email
|
|
attr_accessor :skip_normalize_email
|
|
|
|
before_validation :strip_downcase_email
|
|
before_validation :normalize_email
|
|
|
|
validates :email, presence: true
|
|
validates :email, email: true, if: :validate_email?
|
|
|
|
validates :primary, uniqueness: { scope: [:user_id] }, if: %i[user_id primary]
|
|
validate :user_id_not_changed, if: :primary
|
|
validate :unique_email, if: :validate_unique_email?
|
|
|
|
scope :secondary, -> { where(primary: false) }
|
|
|
|
before_save -> { destroy_email_tokens(email_was) }, if: :will_save_change_to_email?
|
|
|
|
after_destroy { destroy_email_tokens(email) }
|
|
def self.ensure_consistency!
|
|
user_ids_without_primary_email = DB.query_single <<~SQL
|
|
SELECT u.id
|
|
FROM users u
|
|
LEFT JOIN user_emails ue ON u.id = ue.user_id AND ue.primary = true
|
|
WHERE ue.id IS NULL;
|
|
SQL
|
|
|
|
user_ids_without_primary_email.each do |user_id|
|
|
UserEmail.create!(
|
|
user_id: user_id,
|
|
# 64 max character length of local-part for the email address https://datatracker.ietf.org/doc/html/rfc5321#section-4.5.3.1.1
|
|
email: "#{SecureRandom.alphanumeric(64)}@missing-primary-email.invalid",
|
|
primary: true,
|
|
)
|
|
end
|
|
end
|
|
|
|
# Strips dots from the local part and everything between "+" and "@", so that
|
|
# aliases of the same address share a single normalized form.
|
|
def self.normalize(email)
|
|
local_part, domain = email.to_s.split("@", 2)
|
|
return if local_part.blank? || domain.blank?
|
|
|
|
"#{local_part.gsub(".", "").gsub(/\+.*/, "")}@#{domain}"
|
|
end
|
|
|
|
def self.find_by_normalized(email)
|
|
normalized = normalize(Email.downcase(email))
|
|
where("lower(normalized_email) = ?", normalized).first if normalized.present?
|
|
end
|
|
|
|
def normalize_email
|
|
self.normalized_email = self.class.normalize(email)
|
|
end
|
|
|
|
private
|
|
|
|
def strip_downcase_email
|
|
if email
|
|
self.email = email.strip
|
|
self.email = email.downcase
|
|
end
|
|
end
|
|
|
|
def validate_email?
|
|
return false if skip_validate_email
|
|
email_changed?
|
|
end
|
|
|
|
def validate_unique_email?
|
|
return false if skip_validate_unique_email
|
|
will_save_change_to_email?
|
|
end
|
|
|
|
def normalize_emails?
|
|
return false if skip_normalize_email
|
|
|
|
SiteSetting.normalize_emails?
|
|
end
|
|
|
|
def unique_email
|
|
scope = self.class
|
|
scope = scope.where.not(id: id) if persisted?
|
|
|
|
email_exists =
|
|
if normalize_emails?
|
|
scope.where(
|
|
"lower(email) = ? OR lower(normalized_email) = ?",
|
|
email,
|
|
normalized_email,
|
|
).exists?
|
|
else
|
|
scope.where("lower(email) = ?", email).exists?
|
|
end
|
|
|
|
errors.add(:email, :taken) if email_exists
|
|
end
|
|
|
|
def user_id_not_changed
|
|
if will_save_change_to_user_id? && persisted?
|
|
errors.add(
|
|
:user_id,
|
|
I18n.t(
|
|
"activerecord.errors.models.user_email.attributes.user_id.reassigning_primary_email",
|
|
),
|
|
)
|
|
end
|
|
end
|
|
|
|
def destroy_email_tokens(email)
|
|
EmailToken.where(email: email).destroy_all
|
|
end
|
|
end
|
|
|
|
# == Schema Information
|
|
#
|
|
# Table name: user_emails
|
|
#
|
|
# id :integer not null, primary key
|
|
# email :string(513) not null
|
|
# normalized_email :string
|
|
# primary :boolean default(FALSE), not null
|
|
# created_at :datetime not null
|
|
# updated_at :datetime not null
|
|
# user_id :integer not null
|
|
#
|
|
# 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"
|
|
#
|