Class: Jobs::UserEmail

Inherits:
Base
  • Object
show all
Includes:
Skippable
Defined in:
app/jobs/regular/user_email.rb

Overview

Asynchronously send an email to a user

Direct Known Subclasses

CriticalUserEmail

Instance Method Summary collapse

Methods included from Skippable

#create_skipped_email_log

Methods inherited from Base

acquire_cluster_concurrency_lock!, clear_cluster_concurrency_lock!, cluster_concurrency, cluster_concurrency_redis_key, delayed_perform, #error_context, get_cluster_concurrency, #last_db_duration, #log, #perform, #perform_immediately

Instance Method Details

#execute(args) ⇒ Object



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'app/jobs/regular/user_email.rb', line 29

def execute(args)
  raise Discourse::InvalidParameters.new(:user_id) unless args[:user_id].present?
  raise Discourse::InvalidParameters.new(:type) unless args[:type].present?

  # This is for performance. Quit out fast without doing a bunch
  # of extra work when emails are disabled.
  return if quit_email_early?

  args[:type] = args[:type].to_s

  send_user_email(args)

  if args[:type] == "digest"
    # Record every attempt at sending a digest email, even if it was skipped
    UserStat.where(user_id: args[:user_id]).update_all(digest_attempted_at: Time.current)
  end
end

#message_for_email(user, post, type, notification, args = nil) ⇒ Object



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'app/jobs/regular/user_email.rb', line 99

def message_for_email(user, post, type, notification, args = nil)
  args ||= {}

  notification_type = args[:notification_type]
  notification_data_hash = args[:notification_data_hash]
  email_token = args[:email_token]
  to_address = args[:to_address]

  set_skip_context(type, user.id, to_address || user.email, post.try(:id))

  if user.anonymous?
    return skip_message(SkippedEmailLog.reason_types[:user_email_anonymous_user])
  end

  if user.suspended?
    if !type.in?(%w[user_private_message account_suspended])
      return skip_message(SkippedEmailLog.reason_types[:user_email_user_suspended_not_pm])
    elsif post&.topic&.group_pm?
      return skip_message(SkippedEmailLog.reason_types[:user_email_user_suspended])
    end
  end

  if type == "digest"
    return if user.staged
    if user.last_emailed_at &&
         user.last_emailed_at >
           (
             user.user_option&.digest_after_minutes ||
               SiteSetting.default_email_digest_frequency.to_i
           ).minutes.ago
      return
    end
  end

  seen_recently =
    (
      user.last_seen_at.present? &&
        user.last_seen_at > SiteSetting.email_time_window_mins.minutes.ago
    )
  if !args[:force_respect_seen_recently] &&
       (
         always_email_regular?(user, type) || always_email_private_message?(user, type) ||
           user.staged
       )
    seen_recently = false
  end

  email_args = {}

  if (post || notification || notification_type || args[:force_respect_seen_recently]) &&
       (seen_recently && !user.suspended?)
    return skip_message(SkippedEmailLog.reason_types[:user_email_seen_recently])
  end

  email_args[:post] = post if post

  if notification || notification_type
    email_args[:notification_type] ||= notification_type || notification.try(:notification_type)
    email_args[:notification_data_hash] ||= notification_data_hash ||
      notification.try(:data_hash)

    unless String === email_args[:notification_type]
      if Numeric === email_args[:notification_type]
        email_args[:notification_type] = Notification.types[email_args[:notification_type]]
      end
      email_args[:notification_type] = email_args[:notification_type].to_s
    end

    if !SiteSetting.disable_mailing_list_mode && user.user_option.mailing_list_mode? &&
         user.user_option.mailing_list_mode_frequency > 0 && # don't catch notifications for users on daily mailing list mode
         (!post.try(:topic).try(:private_message?)) &&
         NOTIFICATIONS_SENT_BY_MAILING_LIST.include?(email_args[:notification_type])
      # no need to log a reason when the mail was already sent via the mailing list job
      return nil, nil
    end

    unless always_email_regular?(user, type) || always_email_private_message?(user, type)
      if (notification && notification.read?) || (post && post.seen?(user))
        return skip_message(SkippedEmailLog.reason_types[:user_email_notification_already_read])
      end
    end
  end

  skip_reason_type = skip_email_for_post(post, user)
  return skip_message(skip_reason_type) if skip_reason_type.present?

  # Make sure that mailer exists
  unless UserNotifications.respond_to?(type)
    raise Discourse::InvalidParameters.new("type=#{type}")
  end

  if email_token.present?
    email_args[:email_token] = email_token

    if type == "confirm_new_email"
      change_req = EmailChangeRequest.find_by_new_token(email_token)

      email_args[:requested_by_admin] = change_req.requested_by_admin? if change_req
    end
  end

  email_args[:new_email] = args[:new_email] || user.email if type == "notify_old_email" ||
    type == "notify_old_email_add"

  if args[:client_ip] && args[:user_agent]
    email_args[:client_ip] = args[:client_ip]
    email_args[:user_agent] = args[:user_agent]
  end

  if EmailLog.reached_max_emails?(user, type)
    return skip_message(SkippedEmailLog.reason_types[:exceeded_emails_limit])
  end

  if !EmailLog::CRITICAL_EMAIL_TYPES.include?(type) &&
       user.user_stat.bounce_score >= SiteSetting.bounce_score_threshold
    return skip_message(SkippedEmailLog.reason_types[:exceeded_bounces_limit])
  end

  if args[:user_history_id]
    email_args[:user_history] = UserHistory.where(id: args[:user_history_id]).first
  end

  email_args[:reject_reason] = args[:reject_reason]

  message =
    EmailLog.unique_email_per_post(post, user) do
      UserNotifications.public_send(type, user, email_args)
    end

  # Update the to address if we have a custom one
  message.to = to_address if message && to_address.present?

  [message, nil]
end

#quit_email_early?Boolean

Can be overridden by subclass, for example critical email should always consider being sent

Returns:

  • (Boolean)


25
26
27
# File 'app/jobs/regular/user_email.rb', line 25

def quit_email_early?
  SiteSetting.disable_emails == "yes"
end

#send_user_email(args) ⇒ Object



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'app/jobs/regular/user_email.rb', line 47

def send_user_email(args)
  post = nil
  notification = nil
  type = args[:type]
  user = User.find_by(id: args[:user_id])
  to_address =
    args[:to_address].presence || user&.primary_email&.email.presence || "no_email_found"

  set_skip_context(type, args[:user_id], to_address, args[:post_id])

  return skip(SkippedEmailLog.reason_types[:user_email_no_user]) if !user
  if to_address == "no_email_found"
    return skip(SkippedEmailLog.reason_types[:user_email_no_email])
  end

  if args[:post_id].present?
    post = Post.find_by(id: args[:post_id])

    return skip(SkippedEmailLog.reason_types[:user_email_post_not_found]) if post.blank?

    if !Guardian.new(user).can_see?(post)
      return skip(SkippedEmailLog.reason_types[:user_email_access_denied])
    end
  end

  if args[:notification_id].present?
    notification = Notification.find_by(id: args[:notification_id])
  end

  message, skip_reason_type = message_for_email(user, post, type, notification, args)

  if message
    Email::Sender.new(message, type, user).send

    if (b = user.user_stat.bounce_score) > SiteSetting.bounce_score_erode_on_send
      # erode bounce score each time we send an email
      # this means that we are punished a lot less for bounces
      # and we can recover more quickly
      user.user_stat.update(bounce_score: b - SiteSetting.bounce_score_erode_on_send)
    end
  else
    skip_reason_type
  end
end

#set_skip_context(type, user_id, to_address, post_id) ⇒ Object



92
93
94
# File 'app/jobs/regular/user_email.rb', line 92

def set_skip_context(type, user_id, to_address, post_id)
  @skip_context = { type: type, user_id: user_id, to_address: to_address, post_id: post_id }
end