discourse/app/jobs/regular/user_email.rb
Sam Saffron 30990006a9 DEV: enable frozen string literal on all files
This reduces chances of errors where consumers of strings mutate inputs
and reduces memory usage of the app.

Test suite passes now, but there may be some stuff left, so we will run
a few sites on a branch prior to merging
2019-05-13 09:31:32 +08:00

254 lines
8.5 KiB
Ruby

# frozen_string_literal: true
require_dependency 'email/sender'
require_dependency 'user_notifications'
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?
post = nil
notification = nil
type = args[:type]
user = User.find_by(id: args[:user_id])
to_address = args[:to_address].presence || user.try(: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]) unless user
if args[:post_id].present?
post = Post.find_by(id: args[:post_id])
unless post.present?
return skip(SkippedEmailLog.reason_types[:user_email_post_not_found])
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)
email_args[:email_token] = email_token if email_token.present?
email_args[:new_email] = user.email if type.to_s == "notify_old_email"
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?
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
return 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 == :user_private_message && user.user_option.email_messages_level == UserOption.email_level_types[:always]
end
def always_email_regular?(user, type)
type != :user_private_message && user.user_option.email_level == UserOption.email_level_types[:always]
end
end
end