Class: EmailPreference

Inherits:
ApplicationRecord show all
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
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 Schedulable

Schedulable::SIMPLE_FORM_OPTIONS

Instance Attribute Summary collapse

Has many collapse

Class Method Summary collapse

Instance Method Summary collapse

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 Schedulable

config

Methods included from Models::AfterCommittable

#after_commit

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#emailObject (readonly)



31
# File 'app/models/email_preference.rb', line 31

validates :email, email_format: true, presence: true, uniqueness: true

Class Method Details

.can_receive_email_of_category(email, category) ⇒ Object



78
79
80
81
82
83
84
85
86
# File 'app/models/email_preference.rb', line 78

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

.completely_unsubscribedActiveRecord::Relation<EmailPreference>

A relation of EmailPreferences that are completely unsubscribed. Active Record Scope

Returns:

See Also:



37
38
39
40
41
42
43
44
# File 'app/models/email_preference.rb', line 37

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.



138
139
140
141
142
143
144
145
146
147
148
149
# File 'app/models/email_preference.rb', line 138

def self.decrypt_token(token)
  verified = message_verifier.verified(token, purpose: :email_preference)
  return verified if verified

  # Legacy fallback for in-flight customer-email URLs.
  res = Encryption.decrypt_string(token)
  return res if res.present?

  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.



131
132
133
# File 'app/models/email_preference.rb', line 131

def self.encrypt_email(email)
  message_verifier.generate(email.to_s, purpose: :email_preference)
end

.generate_email_preferences_form_url(email) ⇒ Object



88
89
90
91
# File 'app/models/email_preference.rb', line 88

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.(token: EmailPreference.encrypt_email(email), host: "https://#{WEB_HOSTNAME}", locale: false)
end

.import_suppressions(path) ⇒ Object



93
94
95
96
97
98
99
100
101
102
103
# File 'app/models/email_preference.rb', line 93

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_verifierObject



151
152
153
154
155
156
157
# File 'app/models/email_preference.rb', line 151

def self.message_verifier
  @message_verifier ||= ActiveSupport::MessageVerifier.new(
    Rails.application.key_generator.generate_key('email_preference/v1'),
    digest: 'SHA256',
    serializer: JSON
  )
end

.unsubscribe(emails, disable: true) ⇒ Object



105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'app/models/email_preference.rb', line 105

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

Instance Method Details

#accountsActiveRecord::Relation<Account>

Returns:

  • (ActiveRecord::Relation<Account>)

See Also:



34
# File 'app/models/email_preference.rb', line 34

has_many :accounts, foreign_key: :email, primary_key: :email

#any?Boolean

Returns:

  • (Boolean)


69
70
71
72
73
74
75
76
# File 'app/models/email_preference.rb', line 69

def any?
  disable_promotions ||
    disable_newsletters ||
    disable_announcements ||
    disable_events ||
    disable_webinars ||
    disable_reviews
end

#broadcast_updatesObject



169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'app/models/email_preference.rb', line 169

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_recipientsActiveRecord::Relation<CommunicationRecipient>

Returns:

See Also:



35
# File 'app/models/email_preference.rb', line 35

has_many :communication_recipients, foreign_key: :detail, primary_key: :email

#contact_pointsActiveRecord::Relation<ContactPoint>

Returns:

See Also:



33
# File 'app/models/email_preference.rb', line 33

has_many :contact_points, foreign_key: :detail, primary_key: :email

#disable_allObject

This is to allow the initial state on the form to be selected properly



60
61
62
63
64
65
66
67
# File 'app/models/email_preference.rb', line 60

def disable_all
  disable_promotions &&
    disable_newsletters &&
    disable_announcements &&
    disable_events &&
    disable_webinars &&
    disable_reviews
end

#to_sObject



159
160
161
# File 'app/models/email_preference.rb', line 159

def to_s
  email
end

#unsubscribed_categories_changesObject



163
164
165
166
167
# File 'app/models/email_preference.rb', line 163

def unsubscribed_categories_changes
  saved_changes.select do |k, v|
    k.starts_with?('disable_') && v.last == true
  end.keys.map { |k| k.scan(/disable_(.*)/)[0][0].to_sym }
end