# frozen_string_literal: true # Copyright (c) 2008 - 2018, 2020, 2022, 2024 Peter H. Boling of RailsBling.com # Released under the MIT license # External Libraries require "mail" module SanitizeEmail # Tools for overriding addresses class OverriddenAddresses # Raised when there are no recipients after sanitization class MissingRecipients < StandardError; end # MissingTo is Deprecated class MissingTo < MissingRecipients; end # Raised if there is a recipient type that sanitize_email doesn't recognize. # If you get this error please report it. # recognized recipient types are: TO, CC, and BCC class UnknownOverride < StandardError; end REPLACE_AT = [/@/, " at "].freeze REPLACE_ALLIGATOR = [/[<>]/, "~"].freeze attr_accessor :tempmail, :overridden_to, :overridden_cc, :overridden_bcc, :overridden_personalizations, :good_list, # Allow-listed addresses will not be molested as to, cc, or bcc :bad_list, # Block-listed addresses will be removed from to, cc and bcc when sanitization is engaged :sanitized_to, :sanitized_cc, :sanitized_bcc # Replace non-allow-listed addresses with these sanitized addresses. def initialize(message, **args) # Not using extract_options! because non-rails compatibility is a goal args = SanitizeEmail::Config.to_init.merge(args) @sanitized_to = args[:sanitized_to] @sanitized_cc = args[:sanitized_cc] @sanitized_bcc = args[:sanitized_bcc] @good_list = args[:good_list] || [] @bad_list = args[:bad_list] || [] # Mail will do the username parsing for us. @tempmail = Mail.new tempmail.to = to_override(message.to) tempmail.cc = cc_override(message.cc) tempmail.bcc = bcc_override(message.bcc) # remove addresses from :cc / :bcc that are also in :to remove_duplicates @overridden_to = tempmail[:to].decoded @overridden_cc = tempmail[:cc].decoded @overridden_bcc = tempmail[:bcc].decoded if message["personalizations"].nil? raise MissingRecipients, "No recipients left post-sanitization" if (tempmail.to + tempmail.cc + tempmail.bcc).empty? else @overridden_personalizations = personalizations_override(message["personalizations"]) end end # Allow good listed email addresses, and then remove the bad listed addresses def good_listize(real_addresses) good_listed = clean_addresses(real_addresses, :good_list) good_listed = clean_addresses(good_listed, :bad_list) unless good_listed.empty? good_listed end def to_override(actual_addresses) to = override_email(:to, actual_addresses) to.join(",") end def cc_override(actual_addresses) override_email(:cc, actual_addresses).join(",") end def bcc_override(actual_addresses) override_email(:bcc, actual_addresses).join(",") end # Intended to result in compatibility with https://github.com/eddiezane/sendgrid-actionmailer def personalizations_override(actual_personalizations) actual_personalizations.unparsed_value.map do |actual_personalization| actual_personalization.merge( to: actual_personalization[:to]&.map do |to| to.merge(email: override_email(:to, to[:email]).join(",")) end, cc: actual_personalization[:cc]&.map do |cc| cc.merge(email: override_email(:cc, cc[:email]).join(",")) end, bcc: actual_personalization[:bcc]&.map do |bcc| bcc.merge(email: override_email(:bcc, bcc[:email]).join(",")) end, ) end end def override_email(type, actual_addresses) # Normalized to an arrays # puts "override_email 1: #{type} - #{actual_addresses}" real_addresses = Array(actual_addresses) # puts "override_email 2: #{type} - #{real_addresses}" # If there were no original recipients, then we DO NOT override the nil with the sanitized recipients return [] if real_addresses.empty? good_listed = good_listize(real_addresses) # puts "override_email 3: #{type} - #{good_listed}" # If there are good_list addresses to send to then use them as is, no mods needed return good_listed unless good_listed.empty? # TODO: Allow overriding if an addressed email is on the good list? # If there are no sanitized addresses we can't override! sanitized_addresses = sanitize_addresses(type) # puts "override_email 4: #{type} - #{sanitized_addresses}" return [] if sanitized_addresses.empty? # At this point it is assured that the address list will need to be sanitized # One more check to ensure none of the configured sanitized email addresses are on the bad_list sanitized_addresses = clean_addresses(sanitized_addresses, :bad_list) # puts "override_email 5: #{type} - #{sanitized_addresses}" # If we don't want to inject the "email" in the "user name" section of the sanitized recipients, # then just return the default sanitized recipients return sanitized_addresses unless SanitizeEmail.use_actual_email_as_sanitized_user_name with_user_names = inject_user_names(real_addresses, sanitized_addresses) # puts "real_addresses 2: #{real_addresses}" # puts "override_email 6: #{type} - #{with_user_names}" # Otherwise inject the email as the "user name" with_user_names end def address_list_filter(list_type, address) # TODO: How does this handle email addresses with user names like "Foo Example " has_address = send(list_type).include?(address) case list_type when :good_list then has_address ? address : nil when :bad_list then has_address ? nil : address else raise ArgumentError, "address_list_filter got unknown list_type: #{list_type}" end end def inject_user_names(real_addresses, sanitized_addresses) real_addresses.each_with_object([]) do |real_recipient, result| new_recipient = if real_recipient.nil? sanitized_addresses else # puts "SANITIZED: #{sanitized_addresses}" sanitized_addresses.map { |sanitized| "#{real_recipient.gsub(REPLACE_AT[0], REPLACE_AT[1]).gsub(/[<>]/, "~")} <#{sanitized}>" } end result << new_recipient end.flatten end def clean_addresses(addresses, list_type) # Normalize addresses just in case it isn't an array yet addresses.map do |address| # If this address is on the good list then let it pass address_list_filter(list_type, address) end.compact.uniq end def sanitize_addresses(type) case type when :to then Array(sanitized_to) when :cc then Array(sanitized_cc) when :bcc then Array(sanitized_bcc) else raise UnknownOverride, "unknown email override" end end private def remove_duplicates dedup_addresses = tempmail[:to].addresses tempmail[:cc].addrs.reject! do |addr| # If this email address is already in the :to list, then remove dedup_addresses.include?(addr.address) end dedup_addresses += tempmail[:cc].addresses tempmail[:bcc].addrs.reject! do |addr| # If this email address is already in the :to list, then remove dedup_addresses.include?(addr.address) end end end end