Module: Organizations::EmailNormalizer

Defined in:
lib/organizations/email_normalizer.rb

Overview

Default email normalization for verified joining.

Verified joining enforces "one proven email address => one membership per organization" (partial unique index on memberships.verified_email_normalized). That invariant is only meaningful if trivially-aliased addresses collapse to the same normalized value, otherwise one inbox can mint unlimited "distinct" identities via plus-addressing (user+1@corp.com, user+2@corp.com, ...).

Normalization rules (deliberately conservative):

  • downcase + strip surrounding whitespace
  • strip a single trailing dot from the domain (FQDN form: "corp.com.")
  • drop the +tag suffix from the local part (RFC 5233 subaddressing)

Gmail-style dot-collapsing in the local part is intentionally NOT applied: dots are significant in most corporate mail systems, and collapsing them would wrongly merge distinct mailboxes (j.doe@ vs jdoe@).

Hosts can replace this wholesale via:

config.verification_email_normalizer = ->(email) { ... }

Examples:

EmailNormalizer.normalize("  J.Doe+carpool@INIZIO.COM. ") # => "j.doe@inizio.com"
EmailNormalizer.domain_of("j.doe@inizio.com")             # => "inizio.com"
EmailNormalizer.domain_of("evil@a.com@b.com")             # => nil (multi-@ rejected)

Class Method Summary collapse

Class Method Details

.domain_of(email) ⇒ String?

Extract the domain of an email address, hardened against evasion shapes. Returns nil (instead of guessing) when the address doesn't have exactly one "@" — multi-@ addresses are a classic trick against naive splitting. A single trailing dot (FQDN form) is tolerated and stripped.

Parameters:

  • email (String, nil)

Returns:

  • (String, nil)

    lowercased domain, or nil if not extractable



54
55
56
57
58
59
60
61
62
63
# File 'lib/organizations/email_normalizer.rb', line 54

def domain_of(email)
  value = email.to_s.strip.downcase
  return nil if value.empty?
  return nil unless value.count("@") == 1

  domain = value.split("@", 2).last.to_s.chomp(".")
  return nil if domain.empty? || domain.include?("@")

  domain
end

.normalize(email) ⇒ String

Normalize an email address for uniqueness comparison.

Parameters:

  • email (String, nil)

Returns:

  • (String)

    normalized email ("" for blank input)



35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/organizations/email_normalizer.rb', line 35

def normalize(email)
  value = email.to_s.strip.downcase
  return "" if value.empty?

  local, at, domain = value.rpartition("@")
  return value if at.empty? # not an email shape; leave as-is (validation rejects it upstream)

  local = local.split("+", 2).first.to_s
  domain = domain.chomp(".")

  "#{local}@#{domain}"
end