PERF: Split skipped email logs into a seperate table.

This commit is contained in:
Guo Xiang Tan 2018-07-24 12:55:43 +08:00
parent 9b84e78fdf
commit ae8b0a517f
21 changed files with 510 additions and 130 deletions

View File

@ -1,9 +1,9 @@
import AdminEmailLogsController from "admin/controllers/admin-email-logs"; import AdminEmailLogsController from 'admin/controllers/admin-email-logs';
import debounce from "discourse/lib/debounce"; import debounce from 'discourse/lib/debounce';
import EmailLog from "admin/models/email-log"; import EmailLog from 'admin/models/email-log';
export default AdminEmailLogsController.extend({ export default AdminEmailLogsController.extend({
filterEmailLogs: debounce(function() { filterEmailLogs: debounce(function() {
EmailLog.findAll(this.get("filter")).then(logs => this.set("model", logs)); EmailLog.findAll(this.get('filter')).then(logs => this.set('model', logs));
}, 250).observes("filter.{user,address,type,skipped_reason}") }, 250).observes('filter.{user,address,type}')
}); });

View File

@ -5,5 +5,5 @@ import EmailLog from "admin/models/email-log";
export default AdminEmailLogsController.extend({ export default AdminEmailLogsController.extend({
filterEmailLogs: debounce(function() { filterEmailLogs: debounce(function() {
EmailLog.findAll(this.get("filter")).then(logs => this.set("model", logs)); EmailLog.findAll(this.get("filter")).then(logs => this.set("model", logs));
}, 250).observes("filter.{user,address,type,skipped_reason}") }, 250).observes("filter.{user,address,type}")
}); });

View File

@ -15,7 +15,7 @@
<td>{{text-field value=filter.user placeholderKey="admin.email.logs.filters.user_placeholder"}}</td> <td>{{text-field value=filter.user placeholderKey="admin.email.logs.filters.user_placeholder"}}</td>
<td>{{text-field value=filter.address placeholderKey="admin.email.logs.filters.address_placeholder"}}</td> <td>{{text-field value=filter.address placeholderKey="admin.email.logs.filters.address_placeholder"}}</td>
<td>{{text-field value=filter.type placeholderKey="admin.email.logs.filters.type_placeholder"}}</td> <td>{{text-field value=filter.type placeholderKey="admin.email.logs.filters.type_placeholder"}}</td>
<td>{{text-field value=filter.skipped_reason placeholderKey="admin.email.logs.filters.skipped_reason_placeholder"}}</td> <td></td>
</tr> </tr>
{{#each model as |l|}} {{#each model as |l|}}

View File

@ -18,17 +18,17 @@ class Admin::EmailController < Admin::AdminController
end end
def sent def sent
email_logs = filter_email_logs(EmailLog.sent, params) email_logs = filter_logs(EmailLog, params)
render_serialized(email_logs, EmailLogSerializer) render_serialized(email_logs, EmailLogSerializer)
end end
def skipped def skipped
email_logs = filter_email_logs(EmailLog.skipped, params) skipped_email_logs = filter_logs(SkippedEmailLog, params)
render_serialized(email_logs, EmailLogSerializer) render_serialized(skipped_email_logs, SkippedEmailLogSerializer)
end end
def bounced def bounced
email_logs = filter_email_logs(EmailLog.bounced, params) email_logs = filter_logs(EmailLog.bounced, params)
render_serialized(email_logs, EmailLogSerializer) render_serialized(email_logs, EmailLogSerializer)
end end
@ -137,20 +137,20 @@ class Admin::EmailController < Admin::AdminController
private private
def filter_email_logs(email_logs, params) def filter_logs(logs, params)
email_logs = email_logs.includes(:user, post: :topic) table_name = logs.table_name
logs = logs.includes(:user, post: :topic)
.references(:user) .references(:user)
.order(created_at: :desc) .order(created_at: :desc)
.offset(params[:offset] || 0) .offset(params[:offset] || 0)
.limit(50) .limit(50)
email_logs = email_logs.where("users.username ILIKE ?", "%#{params[:user]}%") if params[:user].present? logs = logs.where("users.username ILIKE ?", "%#{params[:user]}%") if params[:user].present?
email_logs = email_logs.where("email_logs.to_address ILIKE ?", "%#{params[:address]}%") if params[:address].present? logs = logs.where("#{table_name}.to_address ILIKE ?", "%#{params[:address]}%") if params[:address].present?
email_logs = email_logs.where("email_logs.email_type ILIKE ?", "%#{params[:type]}%") if params[:type].present? logs = logs.where("#{table_name}.email_type ILIKE ?", "%#{params[:type]}%") if params[:type].present?
email_logs = email_logs.where("email_logs.reply_key ILIKE ?", "%#{params[:reply_key]}%") if params[:reply_key].present? logs = logs.where("#{table_name}.reply_key ILIKE ?", "%#{params[:reply_key]}%") if params[:reply_key].present?
email_logs = email_logs.where("email_logs.skipped_reason ILIKE ?", "%#{params[:skipped_reason]}%") if params[:skipped_reason].present? logs
email_logs
end end
def filter_incoming_emails(incoming_emails, params) def filter_incoming_emails(incoming_emails, params)

View File

@ -42,17 +42,26 @@ module Jobs
users.find_each do |user| users.find_each do |user|
if Guardian.new(user).can_see?(post) if Guardian.new(user).can_see?(post)
if EmailLog.reached_max_emails?(user) if EmailLog.reached_max_emails?(user)
skip(user.email, user.id, post.id, I18n.t('email_log.exceeded_emails_limit')) skip(user.email, user.id, post.id,
SkippedEmailLog.reason_types[:exceeded_emails_limit]
)
next next
end end
if user.user_stat.bounce_score >= SiteSetting.bounce_score_threshold if user.user_stat.bounce_score >= SiteSetting.bounce_score_threshold
skip(user.email, user.id, post.id, I18n.t('email_log.exceeded_bounces_limit')) skip(user.email, user.id, post.id,
SkippedEmailLog.reason_types[:exceeded_bounces_limit]
)
next next
end end
if (user.id == post.user_id) && (user.user_option.mailing_list_mode_frequency == 2) if (user.id == post.user_id) && (user.user_option.mailing_list_mode_frequency == 2)
skip(user.email, user.id, post.id, I18n.t('email_log.no_echo_mailing_list_mode')) skip(user.email, user.id, post.id,
SkippedEmailLog.reason_types[:mailing_list_no_echo_mode]
)
next next
end end
@ -70,14 +79,13 @@ module Jobs
end end
def skip(to_address, user_id, post_id, reason) def skip(to_address, user_id, post_id, reason_type)
EmailLog.create!( SkippedEmailLog.create!(
email_type: 'mailing_list', email_type: 'mailing_list',
to_address: to_address, to_address: to_address,
user_id: user_id, user_id: user_id,
post_id: post_id, post_id: post_id,
skipped: true, reason_type: reason_type
skipped_reason: "[MailingList] #{reason}"
) )
end end
end end

View File

@ -20,18 +20,21 @@ module Jobs
set_skip_context(type, args[:user_id], to_address, args[:post_id]) set_skip_context(type, args[:user_id], to_address, args[:post_id])
return skip(I18n.t("email_log.no_user", user_id: args[:user_id])) unless user return skip(SkippedEmailLog.reason_types[:user_email_no_user]) unless user
if args[:post_id].present? if args[:post_id].present?
post = Post.find_by(id: args[:post_id]) post = Post.find_by(id: args[:post_id])
return skip(I18n.t('email_log.post_not_found', post_id: args[:post_id])) unless post.present?
unless post.present?
return skip(SkippedEmailLog.reason_types[:user_email_post_not_found])
end
end end
if args[:notification_id].present? if args[:notification_id].present?
notification = Notification.find_by(id: args[:notification_id]) notification = Notification.find_by(id: args[:notification_id])
end end
message, skip_reason = message_for_email( message, skip_reason_type = message_for_email(
user, user,
post, post,
type, type,
@ -42,7 +45,7 @@ module Jobs
if message if message
Email::Sender.new(message, type, user).send Email::Sender.new(message, type, user).send
else else
skip_reason skip_reason_type
end end
end end
@ -68,10 +71,12 @@ module Jobs
set_skip_context(type, user.id, to_address || user.email, post.try(:id)) set_skip_context(type, user.id, to_address || user.email, post.try(:id))
return skip_message(I18n.t("email_log.anonymous_user")) if user.anonymous? 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) if user.suspended? && !["user_private_message", "account_suspended"].include?(type.to_s)
return skip_message(I18n.t("email_log.suspended_not_pm")) return skip_message(SkippedEmailLog.reason_types[:user_email_user_suspended_not_pm])
end end
return if user.staged && type.to_s == "digest" return if user.staged && type.to_s == "digest"
@ -81,8 +86,10 @@ module Jobs
email_args = {} email_args = {}
if post || notification || notification_type if (post || notification || notification_type) &&
return skip_message(I18n.t('email_log.seen_recently')) if seen_recently && !user.suspended? (seen_recently && !user.suspended?)
return skip_message(SkippedEmailLog.reason_types[:user_email_seen_recently])
end end
email_args[:post] = post if post email_args[:post] = post if post
@ -108,13 +115,13 @@ module Jobs
unless user.user_option.email_always? unless user.user_option.email_always?
if (notification && notification.read?) || (post && post.seen?(user)) if (notification && notification.read?) || (post && post.seen?(user))
return skip_message(I18n.t('email_log.notification_already_read')) return skip_message(SkippedEmailLog.reason_types[:user_email_notification_already_read])
end end
end end
end end
skip_reason = skip_email_for_post(post, user) skip_reason_type = skip_email_for_post(post, user)
return skip_message(skip_reason) if skip_reason.present? return skip_message(skip_reason_type) if skip_reason_type.present?
# Make sure that mailer exists # Make sure that mailer exists
raise Discourse::InvalidParameters.new("type=#{type}") unless UserNotifications.respond_to?(type) raise Discourse::InvalidParameters.new("type=#{type}") unless UserNotifications.respond_to?(type)
@ -123,11 +130,11 @@ module Jobs
email_args[:new_email] = user.email if type.to_s == "notify_old_email" email_args[:new_email] = user.email if type.to_s == "notify_old_email"
if EmailLog.reached_max_emails?(user, type.to_s) if EmailLog.reached_max_emails?(user, type.to_s)
return skip_message(I18n.t('email_log.exceeded_emails_limit')) return skip_message(SkippedEmailLog.reason_types[:exceeded_emails_limit])
end end
if !EmailLog::CRITICAL_EMAIL_TYPES.include?(type.to_s) && user.user_stat.bounce_score >= SiteSetting.bounce_score_threshold if !EmailLog::CRITICAL_EMAIL_TYPES.include?(type.to_s) && user.user_stat.bounce_score >= SiteSetting.bounce_score_threshold
return skip_message(I18n.t('email_log.exceeded_bounces_limit')) return skip_message(SkippedEmailLog.reason_types[:exceeded_bounces_limit])
end end
if args[:user_history_id] if args[:user_history_id]
@ -169,26 +176,38 @@ module Jobs
# If this email has a related post, don't send an email if it's been deleted or seen recently. # 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) def skip_email_for_post(post, user)
if post if post
return I18n.t('email_log.topic_nil') if post.topic.blank? if post.topic.blank?
return I18n.t('email_log.post_user_deleted') if post.user.blank? return SkippedEmailLog.reason_types[:user_email_topic_nil]
return I18n.t('email_log.post_deleted') if post.user_deleted? end
return I18n.t('email_log.user_suspended') if user.suspended? && !post.user&.staff?
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_always? && PostTiming.exists?(topic_id: post.topic_id, post_number: post.post_number, user_id: user.id) already_read = !user.user_option.email_always? && PostTiming.exists?(topic_id: post.topic_id, post_number: post.post_number, user_id: user.id)
return I18n.t('email_log.already_read') if already_read if already_read
return SkippedEmailLog.reason_types[:user_email_already_read]
end
else else
false false
end end
end end
def skip(reason) def skip(reason_type)
EmailLog.create!( SkippedEmailLog.create!(
email_type: @skip_context[:type], email_type: @skip_context[:type],
to_address: @skip_context[:to_address], to_address: @skip_context[:to_address],
user_id: @skip_context[:user_id], user_id: @skip_context[:user_id],
post_id: @skip_context[:post_id], post_id: @skip_context[:post_id],
skipped: true, reason_type: reason_type
skipped_reason: "[UserEmail] #{reason}",
) )
end end

View File

@ -11,6 +11,8 @@ module Jobs
EmailLog.where(reply_key: nil) EmailLog.where(reply_key: nil)
.where("created_at < ?", threshold) .where("created_at < ?", threshold)
.delete_all .delete_all
SkippedEmailLog.where("created_at < ?", threshold).delete_all
end end
end end

View File

@ -0,0 +1,85 @@
class SkippedEmailLog < ActiveRecord::Base
belongs_to :email_log
belongs_to :user
belongs_to :post
has_one :topic, through: :post
validates :email_type, :to_address, :reason_type, presence: true
validates :custom_reason, presence: true, if: -> { is_custom? }
validates :custom_reason, absence: true, if: -> { !is_custom? }
validate :ensure_valid_reason_type
def self.reason_types
Enum.new(
custom: 1,
exceeded_emails_limit: 2,
exceeded_bounces_limit: 3,
mailing_list_no_echo_mode: 4,
user_email_no_user: 5,
user_email_post_not_found: 6,
user_email_anonymous_user: 7,
user_email_user_suspended_not_pm: 8,
user_email_seen_recently: 9,
user_email_notification_already_read: 10,
user_email_topic_nil: 11,
user_email_post_user_deleted: 12,
user_email_post_deleted: 13,
user_email_user_suspended: 14,
user_email_already_read: 15,
sender_message_blank: 16,
sender_message_to_blank: 17,
sender_text_part_body_blank: 18,
sender_body_blank: 19
)
end
def reason
if is_custom?
self.custom_reason
else
type = self.reason_type
I18n.t(
"skipped_email_log.#{SkippedEmailLog.reason_types[type]}",
user_id: self.user_id,
post_id: self.post_id
)
end
end
private
def is_custom?
self.reason_type == self.class.reason_types[:custom]
end
def ensure_valid_reason_type
unless self.class.reason_types[self.reason_type]
self.errors.add(:reason_type, :invalid)
end
end
end
# == Schema Information
#
# Table name: skipped_email_logs
#
# id :bigint(8) not null, primary key
# email_type :string not null
# to_address :string not null
# user_id :integer
# post_id :integer
# reason_type :integer not null
# custom_reason :text
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_skipped_email_logs_on_created_at (created_at)
# index_skipped_email_logs_on_post_id (post_id)
# index_skipped_email_logs_on_reason_type (reason_type)
# index_skipped_email_logs_on_user_id (user_id)
#

View File

@ -0,0 +1,29 @@
module EmailLogsMixin
def self.included(klass)
klass.attributes :id,
:to_address,
:email_type,
:user_id,
:created_at,
:post_url,
:post_description
klass.has_one :user, serializer: BasicUserSerializer, embed: :objects
end
def post_url
object.post.url
end
def include_post_url?
object.post.present?
end
def include_post_description?
object.post.present? && object.post.topic.present?
end
def post_description
"#{object.post.topic.title} ##{object.post.post_number}"
end
end

View File

@ -1,37 +1,8 @@
class EmailLogSerializer < ApplicationSerializer class EmailLogSerializer < ApplicationSerializer
include EmailLogsMixin
attributes :id, attributes :reply_key,
:reply_key,
:to_address,
:email_type,
:user_id,
:created_at,
:skipped,
:skipped_reason,
:post_url,
:post_description,
:bounced :bounced
has_one :user, serializer: BasicUserSerializer, embed: :objects has_one :user, serializer: BasicUserSerializer, embed: :objects
def include_skipped_reason?
object.skipped
end
def post_url
object.post.url
end
def include_post_url?
object.post.present?
end
def include_post_description?
object.post.present? && object.post.topic.present?
end
def post_description
"#{object.post.topic.title} ##{object.post.post_number}"
end
end end

View File

@ -0,0 +1,9 @@
class SkippedEmailLogSerializer < ApplicationSerializer
include EmailLogsMixin
attributes :skipped_reason
def skipped_reason
object.reason
end
end

View File

@ -3352,7 +3352,6 @@ en:
address_placeholder: "name@example.com" address_placeholder: "name@example.com"
type_placeholder: "digest, signup..." type_placeholder: "digest, signup..."
reply_key_placeholder: "reply key" reply_key_placeholder: "reply key"
skipped_reason_placeholder: "reason"
moderation_history: moderation_history:
performed_by: "Performed By" performed_by: "Performed By"

View File

@ -3163,25 +3163,25 @@ en:
sockpuppet: "A new user created a topic, and another new user at the same IP address (%{ip_address}) replied. See the <a href='/admin/site_settings/category/spam'>`flag_sockpuppets`</a> site setting." sockpuppet: "A new user created a topic, and another new user at the same IP address (%{ip_address}) replied. See the <a href='/admin/site_settings/category/spam'>`flag_sockpuppets`</a> site setting."
spam_hosts: "This new user tried to create multiple posts with links to the same domain (%{domain}). See the <a href='/admin/site_settings/category/spam'>`newuser_spam_host_threshold`</a> site setting." spam_hosts: "This new user tried to create multiple posts with links to the same domain (%{domain}). See the <a href='/admin/site_settings/category/spam'>`newuser_spam_host_threshold`</a> site setting."
email_log: skipped_email_log:
post_user_deleted: "User of the post has been deleted."
no_user: "Can't find user with id %{user_id}"
anonymous_user: "User is anonymous"
suspended_not_pm: "User is suspended, not a message"
seen_recently: "User was seen recently"
post_not_found: "Can't find a post with id %{post_id}"
notification_already_read: "The notification this email is about has already been read"
topic_nil: "post.topic is nil"
post_deleted: "post was deleted by the author"
user_suspended: "user was suspended"
already_read: "user has already read this post"
exceeded_emails_limit: "Exceeded max_emails_per_day_per_user" exceeded_emails_limit: "Exceeded max_emails_per_day_per_user"
exceeded_bounces_limit: "Exceeded bounce_score_threshold" exceeded_bounces_limit: "Exceeded bounce_score_threshold"
message_blank: "message is blank" mailing_list_no_echo_mode: "Mailing list notifications disabled for user's own posts"
message_to_blank: "message.to is blank" user_email_no_user: "Can't find user with id %{user_id}"
text_part_body_blank: "text_part.body is blank" user_email_post_not_found: "Can't find a post with id %{post_id}"
body_blank: "body is blank" user_email_anonymous_user: "User is anonymous"
no_echo_mailing_list_mode: "Mailing list notifications disabled for user's own posts" user_email_user_suspended_not_pm: "User is suspended, not a message"
user_email_seen_recently: "User was seen recently"
user_email_notification_already_read: "The notification this email is about has already been read"
user_email_notification_topic_nil: "post.topic is nil"
user_email_post_user_deleted: "User of the post has been deleted."
user_email_post_deleted: "post was deleted by the author"
user_email_user_suspended: "user was suspended"
user_email_already_read: "user has already read this post"
sender_message_blank: "message is blank"
sender_message_to_blank: "message.to is blank"
sender_text_part_body_blank: "text_part.body is blank"
sender_text_body_blank: "body is blank"
color_schemes: color_schemes:
base_theme_name: "Base" base_theme_name: "Base"

View File

@ -0,0 +1,47 @@
require 'migration/column_dropper'
class CreateSkippedEmailLogs < ActiveRecord::Migration[5.2]
def change
create_table :skipped_email_logs do |t|
t.string :email_type, null: false
t.string :to_address, null: false
t.integer :user_id
t.integer :post_id
t.integer :reason_type, null: false
t.text :custom_reason
t.timestamps
end
add_index :skipped_email_logs, :created_at
add_index :skipped_email_logs, :user_id
add_index :skipped_email_logs, :post_id
add_index :skipped_email_logs, :reason_type
sql = <<~SQL
INSERT INTO skipped_email_logs (
email_type,
to_address,
user_id,
post_id,
reason_type,
custom_reason,
created_at,
updated_at
) SELECT
email_type,
to_address,
user_id,
post_id,
1,
skipped_reason,
created_at,
updated_at
FROM email_logs
WHERE skipped IS TRUE
SQL
execute(sql)
Migration::ColumnDropper.mark_readonly('email_logs', 'skipped_reason')
end
end

View File

@ -27,8 +27,8 @@ module Email
return if ActionMailer::Base::NullMail === @message return if ActionMailer::Base::NullMail === @message
return if ActionMailer::Base::NullMail === (@message.message rescue nil) return if ActionMailer::Base::NullMail === (@message.message rescue nil)
return skip(I18n.t('email_log.message_blank')) if @message.blank? return skip(SkippedEmailLog.reason_types[:sender_message_blank]) if @message.blank?
return skip(I18n.t('email_log.message_to_blank')) if @message.to.blank? return skip(SkippedEmailLog.reason_types[:sender_message_to_blank]) if @message.to.blank?
if SiteSetting.disable_emails == "non-staff" if SiteSetting.disable_emails == "non-staff"
user = User.find_by_email(to_address) user = User.find_by_email(to_address)
@ -36,9 +36,13 @@ module Email
end end
if @message.text_part if @message.text_part
return skip(I18n.t('email_log.text_part_body_blank')) if @message.text_part.body.to_s.blank? if @message.text_part.body.to_s.blank?
return skip(SkippedEmailLog.reason_types[:sender_text_part_body_blank])
end
else else
return skip(I18n.t('email_log.body_blank')) if @message.body.to_s.blank? if @message.body.to_s.blank?
return skip(SkippedEmailLog.reason_types[:sender_body_blank])
end
end end
@message.charset = 'UTF-8' @message.charset = 'UTF-8'
@ -186,7 +190,7 @@ module Email
begin begin
@message.deliver_now @message.deliver_now
rescue *SMTP_CLIENT_ERRORS => e rescue *SMTP_CLIENT_ERRORS => e
return skip(e.message) return skip(SkippedEmailLog.reason_types[:custom], custom_reason: e.message)
end end
# Save and return the email log # Save and return the email log
@ -222,14 +226,16 @@ module Email
header.value header.value
end end
def skip(reason) def skip(reason_type, custom_reason: nil)
EmailLog.create!( attributes = {
email_type: @email_type, email_type: @email_type,
to_address: to_address, to_address: to_address,
user_id: @user.try(:id), user_id: @user&.id,
skipped: true, reason_type: reason_type
skipped_reason: "[Sender] #{reason}" }
)
attributes[:custom_reason] = custom_reason if custom_reason
SkippedEmailLog.create!(attributes)
end end
def merge_json_x_header(name, value) def merge_json_x_header(name, value)

View File

@ -0,0 +1,5 @@
Fabricator(:skipped_email_log) do
to_address { sequence(:address) { |i| "blah#{i}@example.com" } }
email_type :invite
reason_type SkippedEmailLog.reason_types[:exceeded_emails_limit]
end

View File

@ -7,17 +7,22 @@ describe Jobs::CleanUpEmailLogs do
Fabricate(:email_log, created_at: 2.years.ago) Fabricate(:email_log, created_at: 2.years.ago)
Fabricate(:email_log, created_at: 2.weeks.ago) Fabricate(:email_log, created_at: 2.weeks.ago)
Fabricate(:email_log, created_at: 2.days.ago) Fabricate(:email_log, created_at: 2.days.ago)
Fabricate(:skipped_email_log, created_at: 2.years.ago)
Fabricate(:skipped_email_log)
end end
it "removes old email logs without a reply_key" do it "removes old email logs without a reply_key" do
Jobs::CleanUpEmailLogs.new.execute({}) Jobs::CleanUpEmailLogs.new.execute({})
expect(EmailLog.count).to eq(3) expect(EmailLog.count).to eq(3)
expect(SkippedEmailLog.count).to eq(1)
end end
it "does not remove old email logs when delete_email_logs_after_days is 0" do it "does not remove old email logs when delete_email_logs_after_days is 0" do
SiteSetting.delete_email_logs_after_days = 0 SiteSetting.delete_email_logs_after_days = 0
Jobs::CleanUpEmailLogs.new.execute({}) Jobs::CleanUpEmailLogs.new.execute({})
expect(EmailLog.count).to eq(4) expect(EmailLog.count).to eq(4)
expect(SkippedEmailLog.count).to eq(2)
end end
end end

View File

@ -129,7 +129,13 @@ describe Jobs::NotifyMailingListSubscribers do
Jobs::NotifyMailingListSubscribers.new.execute(post_id: post.id) Jobs::NotifyMailingListSubscribers.new.execute(post_id: post.id)
UserNotifications.expects(:mailing_list_notify).with(mailing_list_user, post).never UserNotifications.expects(:mailing_list_notify).with(mailing_list_user, post).never
expect(EmailLog.where(user: mailing_list_user, skipped: true).count).to eq(1) expect(SkippedEmailLog.exists?(
email_type: "mailing_list",
user: mailing_list_user,
post: post,
to_address: mailing_list_user.email,
reason_type: SkippedEmailLog.reason_types[:exceeded_emails_limit]
)).to eq(true)
end end
end end
@ -141,7 +147,13 @@ describe Jobs::NotifyMailingListSubscribers do
Jobs::NotifyMailingListSubscribers.new.execute(post_id: post.id) Jobs::NotifyMailingListSubscribers.new.execute(post_id: post.id)
UserNotifications.expects(:mailing_list_notify).with(mailing_list_user, post).never UserNotifications.expects(:mailing_list_notify).with(mailing_list_user, post).never
expect(EmailLog.where(user: mailing_list_user, skipped: true).count).to eq(1) expect(SkippedEmailLog.exists?(
email_type: "mailing_list",
user: mailing_list_user,
post: post,
to_address: mailing_list_user.email,
reason_type: SkippedEmailLog.reason_types[:exceeded_bounces_limit]
)).to eq(true)
end end
end end

View File

@ -79,38 +79,46 @@ describe Jobs::UserEmail do
end end
context "email_log" do context "email_log" do
let(:post) { Fabricate(:post) }
before do before do
SiteSetting.editing_grace_period = 0 SiteSetting.editing_grace_period = 0
Fabricate(:post) post
end end
it "creates an email log when the mail is sent (via Email::Sender)" do it "creates an email log when the mail is sent (via Email::Sender)" do
last_emailed_at = user.last_emailed_at last_emailed_at = user.last_emailed_at
expect { Jobs::UserEmail.new.execute(type: :digest, user_id: user.id) }.to change { EmailLog.count }.by(1) expect do
Jobs::UserEmail.new.execute(type: :digest, user_id: user.id,)
end.to change { EmailLog.count }.by(1)
email_log = EmailLog.last email_log = EmailLog.last
expect(email_log.skipped).to eq(false)
expect(email_log.user_id).to eq(user.id)
expect(email_log.user).to eq(user)
expect(email_log.post).to eq(nil)
# last_emailed_at should have changed # last_emailed_at should have changed
expect(email_log.user.last_emailed_at).to_not eq(last_emailed_at) expect(email_log.user.last_emailed_at).to_not eq(last_emailed_at)
end end
it "creates an email log when the mail is skipped" do it "creates a skipped email log when the mail is skipped" do
last_emailed_at = user.last_emailed_at last_emailed_at = user.last_emailed_at
user.update_columns(suspended_till: 1.year.from_now) user.update_columns(suspended_till: 1.year.from_now)
expect { Jobs::UserEmail.new.execute(type: :digest, user_id: user.id) }.to change { EmailLog.count }.by(1) expect do
Jobs::UserEmail.new.execute(type: :digest, user_id: user.id)
end.to change { SkippedEmailLog.count }.by(1)
email_log = EmailLog.last expect(SkippedEmailLog.exists?(
expect(email_log.skipped).to eq(true) email_type: "digest",
expect(email_log.skipped_reason).to be_present user: user,
expect(email_log.user_id).to eq(user.id) post: nil,
to_address: user.email,
reason_type: SkippedEmailLog.reason_types[:user_email_user_suspended_not_pm]
)).to eq(true)
# last_emailed_at doesn't change # last_emailed_at doesn't change
expect(email_log.user.last_emailed_at).to eq(last_emailed_at) expect(user.last_emailed_at).to eq(last_emailed_at)
end end
end end
@ -205,8 +213,15 @@ describe Jobs::UserEmail do
notification_data_hash: notification.data_hash notification_data_hash: notification.data_hash
) )
expect(message).to eq nil expect(message).to eq(nil)
expect(err.skipped_reason).to match(/notification.*already/)
expect(SkippedEmailLog.exists?(
email_type: "user_mentioned",
user: user,
post: post,
to_address: user.email,
reason_type: SkippedEmailLog.reason_types[:user_email_notification_already_read]
)).to eq(true)
end end
it "does send the email if the notification has been seen but the user is set for email_always" do it "does send the email if the notification has been seen but the user is set for email_always" do
@ -230,20 +245,59 @@ describe Jobs::UserEmail do
end end
it "does not send notification if limit is reached" do it "does not send notification if limit is reached" do
Jobs::UserEmail.new.execute(type: :user_mentioned, user_id: user.id, notification_id: notification.id, post_id: post.id) expect do
expect(EmailLog.where(user_id: user.id, skipped: true).count).to eq(1) Jobs::UserEmail.new.execute(
type: :user_mentioned,
user_id: user.id,
notification_id: notification.id,
post_id: post.id
)
end.to change { SkippedEmailLog.count }.by(1)
expect(SkippedEmailLog.exists?(
email_type: "user_mentioned",
user: user,
post: post,
to_address: user.email,
reason_type: SkippedEmailLog.reason_types[:exceeded_emails_limit]
)).to eq(true)
end end
it "sends critical email" do it "sends critical email" do
Jobs::UserEmail.new.execute(type: :forgot_password, user_id: user.id, notification_id: notification.id, post_id: post.id) expect do
expect(EmailLog.where(user_id: user.id, skipped: true).count).to eq(0) Jobs::UserEmail.new.execute(
type: :forgot_password,
user_id: user.id,
notification_id: notification.id,
)
end.to change { EmailLog.count }.by(1)
expect(EmailLog.exists?(
email_type: "forgot_password",
user: user,
)).to eq(true)
end end
end end
it "does not send notification if bounce threshold is reached" do it "does not send notification if bounce threshold is reached" do
user.user_stat.update(bounce_score: SiteSetting.bounce_score_threshold) user.user_stat.update(bounce_score: SiteSetting.bounce_score_threshold)
Jobs::UserEmail.new.execute(type: :user_mentioned, user_id: user.id, notification_id: notification.id, post_id: post.id)
expect(EmailLog.where(user_id: user.id, skipped: true).count).to eq(1) expect do
Jobs::UserEmail.new.execute(
type: :user_mentioned,
user_id: user.id,
notification_id: notification.id,
post_id: post.id
)
end.to change { SkippedEmailLog.count }.by(1)
expect(SkippedEmailLog.exists?(
email_type: "user_mentioned",
user: user,
post: post,
to_address: user.email,
reason_type: SkippedEmailLog.reason_types[:exceeded_bounces_limit]
)).to eq(true)
end end
it "doesn't send the mail if the user is using individual mailing list mode" do it "doesn't send the mail if the user is using individual mailing list mode" do

View File

@ -0,0 +1,103 @@
require 'rails_helper'
RSpec.describe SkippedEmailLog, type: :model do
let(:custom_skipped_email_log) do
Fabricate.build(:skipped_email_log,
reason_type: SkippedEmailLog.reason_types[:custom]
)
end
let(:skipped_email_log) { Fabricate.build(:skipped_email_log) }
describe 'validations' do
it { is_expected.to validate_presence_of(:email_type) }
it { is_expected.to validate_presence_of(:to_address) }
it { is_expected.to validate_presence_of(:reason_type) }
describe '#reason_type' do
describe 'when reason_type is not valid' do
it 'should not be valid' do
skipped_email_log.reason_type = 999999
expect(skipped_email_log.valid?).to eq(false)
expect(skipped_email_log.errors.messages).to include(:reason_type)
end
end
end
describe '#custom_reason' do
describe 'when log is a custom reason type' do
describe 'when custom reason is blank' do
it 'should not be valid' do
expect(custom_skipped_email_log.valid?).to eq(false)
expect(custom_skipped_email_log.errors.messages)
.to include(:custom_reason)
end
end
describe 'when custom reason is not blank' do
it 'should be valid' do
custom_skipped_email_log.custom_reason = 'test'
expect(custom_skipped_email_log.valid?).to eq(true)
end
end
end
describe 'when log is not a custom reason type' do
describe 'when custom reason is blank' do
it 'should be valid' do
expect(skipped_email_log.valid?).to eq(true)
end
end
describe 'when custom reason is not blank' do
it 'should not be valid' do
skipped_email_log.custom_reason = 'test'
expect(skipped_email_log.valid?).to eq(false)
expect(skipped_email_log.errors.messages).to include(:custom_reason)
end
end
end
end
end
describe '.reason_types' do
describe "verify enum sequence" do
it 'should return the right sequence' do
expect(SkippedEmailLog.reason_types[:custom]).to eq(1)
expect(SkippedEmailLog.reason_types[:user_email_already_read]).to eq(15)
end
end
end
describe '#reason' do
describe 'for a custom log' do
it 'should return the right output' do
custom_skipped_email_log.custom_reason = 'test'
expect(custom_skipped_email_log.reason).to eq('test')
end
end
describe 'for a non custom log' do
it 'should return the right output' do
expect(skipped_email_log.reason).to eq("
#{I18n.t('skipped_email_log.exceeded_emails_limit')}
".strip)
skipped_email_log.reason_type =
SkippedEmailLog.reason_types[:user_email_no_user]
skipped_email_log.user_id = 9999
expect(skipped_email_log.reason).to eq("
#{I18n.t(
'skipped_email_log.user_email_no_user', user_id: 9999
)}
".strip)
end
end
end
end

View File

@ -39,9 +39,34 @@ describe Admin::EmailController do
end end
describe '#skipped' do describe '#skipped' do
let(:user) { Fabricate(:user) }
let!(:log1) { Fabricate(:skipped_email_log, user: user) }
let!(:log2) { Fabricate(:skipped_email_log) }
it "succeeds" do it "succeeds" do
get "/admin/email/skipped.json" get "/admin/email/skipped.json"
expect(response.status).to eq(200) expect(response.status).to eq(200)
logs = JSON.parse(response.body)
expect(logs.first["id"]).to eq(log2.id)
expect(logs.last["id"]).to eq(log1.id)
end
describe 'when filtered by username' do
it 'should return the right response' do
get "/admin/email/skipped.json", params: {
user: user.username
}
expect(response.status).to eq(200)
logs = JSON.parse(response.body)
expect(logs.count).to eq(1)
expect(logs.first["id"]).to eq(log1.id)
end
end end
end end
@ -54,6 +79,7 @@ describe Admin::EmailController do
context 'with an email address' do context 'with an email address' do
it 'enqueues a test email job' do it 'enqueues a test email job' do
post "/admin/email/test.json", params: { email_address: 'eviltrout@test.domain' } post "/admin/email/test.json", params: { email_address: 'eviltrout@test.domain' }
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(ActionMailer::Base.deliveries.map(&:to).flatten).to include('eviltrout@test.domain') expect(ActionMailer::Base.deliveries.map(&:to).flatten).to include('eviltrout@test.domain')
end end