mirror of
https://github.com/discourse/discourse.git
synced 2024-12-18 19:33:51 +08:00
16daf3c986
In specs, symbols are passed through correctly. But in production, all arguments are provided to jobs as strings.
Followup to c0293339b8
276 lines
9.3 KiB
Ruby
276 lines
9.3 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Jobs
|
|
|
|
# Asynchronously send an email to a user
|
|
class UserEmail < ::Jobs::Base
|
|
include Skippable
|
|
|
|
sidekiq_options queue: 'low'
|
|
|
|
# Can be overridden by subclass, for example critical email
|
|
# should always consider being sent
|
|
def quit_email_early?
|
|
SiteSetting.disable_emails == 'yes'
|
|
end
|
|
|
|
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?
|
|
|
|
send_user_email(args)
|
|
|
|
if args[:user_id].present? && args[:type].to_s == "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.zone.now)
|
|
end
|
|
end
|
|
|
|
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
|
|
return skip(SkippedEmailLog.reason_types[:user_email_no_email]) if to_address == "no_email_found"
|
|
|
|
if args[:post_id].present?
|
|
post = Post.find_by(id: args[:post_id])
|
|
|
|
if post.blank?
|
|
return skip(SkippedEmailLog.reason_types[:user_email_post_not_found])
|
|
end
|
|
|
|
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
|
|
|
|
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
|
|
|
|
NOTIFICATIONS_SENT_BY_MAILING_LIST ||= Set.new %w{
|
|
posted
|
|
replied
|
|
mentioned
|
|
group_mentioned
|
|
quoted
|
|
}
|
|
|
|
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? && !["user_private_message", "account_suspended"].include?(type.to_s)
|
|
return skip_message(SkippedEmailLog.reason_types[:user_email_user_suspended_not_pm])
|
|
end
|
|
|
|
if type.to_s == "digest"
|
|
return if user.staged
|
|
return if user.last_emailed_at &&
|
|
user.last_emailed_at >
|
|
(user.user_option&.digest_after_minutes || SiteSetting.default_email_digest_frequency.to_i).minutes.ago
|
|
end
|
|
|
|
seen_recently = (user.last_seen_at.present? && user.last_seen_at > SiteSetting.email_time_window_mins.minutes.ago)
|
|
seen_recently = false if always_email_regular?(user, type) || always_email_private_message?(user, type) || user.staged
|
|
|
|
email_args = {}
|
|
|
|
if (post || notification || notification_type) &&
|
|
(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 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
|
|
raise Discourse::InvalidParameters.new("type=#{type}") unless UserNotifications.respond_to?(type)
|
|
|
|
if email_token.present?
|
|
email_args[:email_token] = email_token
|
|
|
|
if type.to_s == "confirm_new_email"
|
|
change_req = EmailChangeRequest.find_by_new_token(email_token)
|
|
|
|
if change_req
|
|
email_args[:requested_by_admin] = change_req.requested_by_admin?
|
|
end
|
|
end
|
|
end
|
|
|
|
email_args[:new_email] = args[:new_email] || user.email if type.to_s == "notify_old_email" || type.to_s == "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.to_s)
|
|
return skip_message(SkippedEmailLog.reason_types[:exceeded_emails_limit])
|
|
end
|
|
|
|
if !EmailLog::CRITICAL_EMAIL_TYPES.include?(type.to_s) && 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
|
|
|
|
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
|
|
|
|
sidekiq_retry_in do |count, exception|
|
|
# retry in an hour when SMTP server is busy
|
|
# or use default sidekiq retry formula
|
|
case exception.wrapped
|
|
when Net::SMTPServerBusy
|
|
1.hour + (rand(30) * (count + 1))
|
|
else
|
|
::Jobs::UserEmail.seconds_to_delay(count)
|
|
end
|
|
end
|
|
|
|
# extracted from sidekiq
|
|
def self.seconds_to_delay(count)
|
|
(count**4) + 15 + (rand(30) * (count + 1))
|
|
end
|
|
|
|
private
|
|
|
|
def skip_message(reason)
|
|
[nil, skip(reason)]
|
|
end
|
|
|
|
# If this email has a related post, don't send an email if it's been deleted or seen recently.
|
|
def skip_email_for_post(post, user)
|
|
if post
|
|
if post.topic.blank?
|
|
return SkippedEmailLog.reason_types[:user_email_topic_nil]
|
|
end
|
|
|
|
if post.user.blank?
|
|
return SkippedEmailLog.reason_types[:user_email_post_user_deleted]
|
|
end
|
|
|
|
if post.user_deleted?
|
|
return SkippedEmailLog.reason_types[:user_email_post_deleted]
|
|
end
|
|
|
|
if user.suspended? && (!post.user&.staff? || !post.user&.human?)
|
|
return SkippedEmailLog.reason_types[:user_email_user_suspended]
|
|
end
|
|
|
|
already_read = user.user_option.email_level != UserOption.email_level_types[:always] && PostTiming.exists?(topic_id: post.topic_id, post_number: post.post_number, user_id: user.id)
|
|
if already_read
|
|
SkippedEmailLog.reason_types[:user_email_already_read]
|
|
end
|
|
else
|
|
false
|
|
end
|
|
end
|
|
|
|
def skip(reason_type)
|
|
create_skipped_email_log(
|
|
email_type: @skip_context[:type],
|
|
to_address: @skip_context[:to_address],
|
|
user_id: @skip_context[:user_id],
|
|
post_id: @skip_context[:post_id],
|
|
reason_type: reason_type
|
|
)
|
|
end
|
|
|
|
def always_email_private_message?(user, type)
|
|
type.to_s == "user_private_message" && user.user_option.email_messages_level == UserOption.email_level_types[:always]
|
|
end
|
|
|
|
def always_email_regular?(user, type)
|
|
type.to_s != "user_private_message" && user.user_option.email_level == UserOption.email_level_types[:always]
|
|
end
|
|
end
|
|
|
|
end
|