Class: EmailPreference
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- EmailPreference
- Includes:
- Models::Auditable
- Defined in:
- app/models/email_preference.rb
Overview
== Schema Information
Table name: email_preferences
Database name: primary
id :integer not null, primary key
disable_announcements :boolean default(FALSE), not null
disable_events :boolean default(FALSE), not null
disable_newsletters :boolean default(FALSE), not null
disable_promotions :boolean default(FALSE), not null
disable_reviews :boolean default(FALSE), not null
disable_webinars :boolean default(FALSE), not null
disable_email_tracking :boolean default(FALSE), not null
email :string not null
last_delivery_status :enum
last_delivery_status_at :datetime
last_delivery_status_notes :string
created_at :datetime
updated_at :datetime
creator_id :integer
updater_id :integer
Indexes
index_email_preferences_on_email (email) UNIQUE
Constant Summary collapse
- ALL_STATES =
Recognised all states.
%w[suppressed dropped deferred bounced delivered spammed opened clicked unsubscribed].freeze
Constants included from Models::Auditable
Models::Auditable::ALWAYS_IGNORED
Constants included from Models::Schedulable
Models::Schedulable::SIMPLE_FORM_OPTIONS
Instance Attribute Summary collapse
- #email ⇒ Object readonly
Has many collapse
- #accounts ⇒ ActiveRecord::Relation<Account>
- #communication_recipients ⇒ ActiveRecord::Relation<CommunicationRecipient>
- #contact_points ⇒ ActiveRecord::Relation<ContactPoint>
Class Method Summary collapse
- .can_receive_email_of_category(email, category) ⇒ Object
-
.can_track_email?(email) ⇒ Boolean
Whether open/click tracking should be enabled for emails to this address.
-
.completely_unsubscribed ⇒ ActiveRecord::Relation<EmailPreference>
A relation of EmailPreferences that are completely unsubscribed.
-
.decrypt_token(token) ⇒ Object
Returns the email encoded in the token, or nil if it can't be verified by any of the known formats.
-
.encrypt_email(email) ⇒ Object
Mints a signed token that encodes the customer's email address.
- .generate_email_preferences_form_url(email) ⇒ Object
- .import_suppressions(path) ⇒ Object
- .message_verifier ⇒ Object
- .unsubscribe(emails, disable: true) ⇒ Object
-
.valid_email_token?(value) ⇒ Boolean
True when a decoded token is a usable email (valid UTF-8 + email shape).
Instance Method Summary collapse
- #any? ⇒ Boolean
- #broadcast_updates ⇒ Object
-
#disable_all ⇒ Object
This is to allow the initial state on the form to be selected properly.
-
#eu_recipient? ⇒ Boolean
Whether this address belongs to a party on an EU-country catalog.
- #to_s ⇒ Object
- #unsubscribed_categories_changes ⇒ Object
Methods included from Models::Auditable
#all_skipped_columns, #audit_reference_data, #creator, #should_not_save_version, #stamp_record, #updater
Methods inherited from ApplicationRecord
ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation
Methods included from Models::Schedulable
Methods included from Models::AfterCommittable
Methods included from Models::EventPublishable
Instance Attribute Details
#email ⇒ Object (readonly)
32 |
# File 'app/models/email_preference.rb', line 32 validates :email, email_format: true, presence: true, uniqueness: true |
Class Method Details
.can_receive_email_of_category(email, category) ⇒ Object
79 80 81 82 83 84 85 86 87 |
# File 'app/models/email_preference.rb', line 79 def self.can_receive_email_of_category(email, category) ep = EmailPreference.find_by(email: email) return true if ep.nil? # no preference exist so all is possible category_method = "disable_#{category}" return true unless ep.respond_to?(category_method) # unknown category is always good !ep.send(category_method) end |
.can_track_email?(email) ⇒ Boolean
Whether open/click tracking should be enabled for emails to this address.
Defaults to true (tracking on) unless explicitly disabled.
Used to comply with CNIL (France) / Garante (Italy) requirements for
prior consent on marketing email tracking pixels.
93 94 95 96 97 98 |
# File 'app/models/email_preference.rb', line 93 def self.can_track_email?(email) ep = find_by(email: normalize_value_for(:email, email)) # match the stored :strip/:downcase normalization return true if ep.nil? !ep.disable_email_tracking end |
.completely_unsubscribed ⇒ ActiveRecord::Relation<EmailPreference>
A relation of EmailPreferences that are completely unsubscribed. Active Record Scope
38 39 40 41 42 43 44 45 |
# File 'app/models/email_preference.rb', line 38 scope :completely_unsubscribed, -> { where(disable_promotions: true, disable_newsletters: true, disable_announcements: true, disable_events: true, disable_webinars: true, disable_reviews: true) } |
.decrypt_token(token) ⇒ Object
Returns the email encoded in the token, or nil if it can't be verified
by any of the known formats. Tries MessageVerifier (new format) first,
then the legacy decoders.
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 |
# File 'app/models/email_preference.rb', line 169 def self.decrypt_token(token) verified = .verified(token, purpose: :email_preference) return verified if verified # Legacy fallback for in-flight customer-email URLs. Encryption.decrypt_string # is unauthenticated — a malformed token yields garbage bytes instead of # raising, and invalid-UTF-8 garbage otherwise blows up downstream at Postgres # (PG::CharacterNotInRepertoire). Only trust a result that is actually an email. res = Encryption.decrypt_string(token) return res if valid_email_token?(res) ActiveSupport::MessageEncryptor.new(Heatwave::Configuration.fetch(:secret_key_base)).decrypt_and_verify(token) rescue StandardError nil end |
.encrypt_email(email) ⇒ Object
Mints a signed token that encodes the customer's email address. Used by
unsubscribe / preference-center URLs in marketing email and in CRM
account UI.
New tokens are signed with ActiveSupport::MessageVerifier under the
purpose 'email_preference/v1' and a key derived from the application
key_generator — payload-bound and tampering-resistant. Legacy tokens
produced by Encryption.encrypt_string (Blowfish/AES, see lib/encryption.rb)
remain decodable via the fallback chain in decrypt_token so that
in-flight links sitting in customer mailboxes keep working through the
grace window. Drop the legacy decoders ~30 days after this lands when
outstanding emails have aged out.
162 163 164 |
# File 'app/models/email_preference.rb', line 162 def self.encrypt_email(email) .generate(email.to_s, purpose: :email_preference) end |
.generate_email_preferences_form_url(email) ⇒ Object
119 120 121 122 |
# File 'app/models/email_preference.rb', line 119 def self.generate_email_preferences_form_url(email) # locale: false ensures no locale prefix - Cloudflare worker will add appropriate locale based on user's country UrlHelper.instance.email_preferences_my_account_url(token: EmailPreference.encrypt_email(email), host: "https://#{WEB_HOSTNAME}", locale: false) end |
.import_suppressions(path) ⇒ Object
124 125 126 127 128 129 130 131 132 133 134 |
# File 'app/models/email_preference.rb', line 124 def self.import_suppressions(path) CSV.foreach(path) do |row| ep = find_or_create_by(email: row[0]) ep.update(disable_promotions: true, disable_newsletters: true, disable_announcements: true, disable_events: true, disable_webinars: true, disable_reviews: true) end end |
.message_verifier ⇒ Object
192 193 194 195 196 197 198 |
# File 'app/models/email_preference.rb', line 192 def self. @message_verifier ||= ActiveSupport::MessageVerifier.new( Rails.application.key_generator.generate_key('email_preference/v1'), digest: 'SHA256', serializer: JSON ) end |
.unsubscribe(emails, disable: true) ⇒ Object
136 137 138 139 140 141 142 143 144 145 146 147 148 |
# File 'app/models/email_preference.rb', line 136 def self.unsubscribe(emails, disable: true) email_preferences = EmailPreference.where(email: emails) email_preferences.each do |ep| ep.update( disable_promotions: disable, disable_newsletters: disable, disable_announcements: disable, disable_events: disable, disable_webinars: disable, disable_reviews: disable ) end end |
.valid_email_token?(value) ⇒ Boolean
True when a decoded token is a usable email (valid UTF-8 + email shape).
Guards the unauthenticated legacy decoders in decrypt_token from returning
junk for a malformed token.
188 189 190 |
# File 'app/models/email_preference.rb', line 188 def self.valid_email_token?(value) value.is_a?(String) && value.valid_encoding? && value.match?(URI::MailTo::EMAIL_REGEXP) end |
Instance Method Details
#accounts ⇒ ActiveRecord::Relation<Account>
35 |
# File 'app/models/email_preference.rb', line 35 has_many :accounts, foreign_key: :email, primary_key: :email |
#any? ⇒ Boolean
70 71 72 73 74 75 76 77 |
# File 'app/models/email_preference.rb', line 70 def any? disable_promotions || || disable_announcements || disable_events || disable_webinars || disable_reviews end |
#broadcast_updates ⇒ Object
210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 |
# File 'app/models/email_preference.rb', line 210 def broadcast_updates event_hash = { email: email } event_hash[:categories] = unsubscribed_categories_changes return if event_hash[:categories].blank? ActiveRecord.after_all_transactions_commit do Rails.configuration.event_store.publish( Events::EmailUnsubscribed.new(data: { email: event_hash[:email], categories: event_hash[:categories] }), stream_name: "EmailPreference-#{id}" ) end end |
#communication_recipients ⇒ ActiveRecord::Relation<CommunicationRecipient>
36 |
# File 'app/models/email_preference.rb', line 36 has_many :communication_recipients, foreign_key: :detail, primary_key: :email |
#contact_points ⇒ ActiveRecord::Relation<ContactPoint>
34 |
# File 'app/models/email_preference.rb', line 34 has_many :contact_points, foreign_key: :detail, primary_key: :email |
#disable_all ⇒ Object
This is to allow the initial state on the form to be selected properly
61 62 63 64 65 66 67 68 |
# File 'app/models/email_preference.rb', line 61 def disable_all disable_promotions && && disable_announcements && disable_events && disable_webinars && disable_reviews end |
#eu_recipient? ⇒ Boolean
Whether this address belongs to a party on an EU-country catalog.
Drives whether the preference center offers the open/click tracking control.
That toggle exists for CNIL (France) / Garante (Italy) prior-consent rules,
so it is noise on the North American catalogs. Note this gates the control
only — can_track_email? still enforces whatever is already stored, so a
hidden toggle never silently re-enables tracking.
Any linked party on an EU catalog is enough: an address shared across parties
should see the control rather than lose access to it.
112 113 114 115 116 117 |
# File 'app/models/email_preference.rb', line 112 def eu_recipient? Party.where(id: contact_points.select(:party_id)) .joins(catalog: :country) .where(countries: { iso: Country::EU_COUNTRIES_ISO }) .exists? end |
#to_s ⇒ Object
200 201 202 |
# File 'app/models/email_preference.rb', line 200 def to_s email end |
#unsubscribed_categories_changes ⇒ Object
204 205 206 207 208 |
# File 'app/models/email_preference.rb', line 204 def unsubscribed_categories_changes saved_changes.select do |k, v| k.starts_with?('disable_') && v.last == true && k != 'disable_email_tracking' end.keys.map { |k| k.scan(/disable_(.*)/)[0][0].to_sym } end |