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

View File

@ -5,5 +5,5 @@ import EmailLog from "admin/models/email-log";
export default AdminEmailLogsController.extend({
filterEmailLogs: debounce(function() {
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.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.skipped_reason placeholderKey="admin.email.logs.filters.skipped_reason_placeholder"}}</td>
<td></td>
</tr>
{{#each model as |l|}}

View File

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

View File

@ -42,17 +42,26 @@ module Jobs
users.find_each do |user|
if Guardian.new(user).can_see?(post)
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
end
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
end
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
end
@ -70,14 +79,13 @@ module Jobs
end
def skip(to_address, user_id, post_id, reason)
EmailLog.create!(
def skip(to_address, user_id, post_id, reason_type)
SkippedEmailLog.create!(
email_type: 'mailing_list',
to_address: to_address,
user_id: user_id,
post_id: post_id,
skipped: true,
skipped_reason: "[MailingList] #{reason}"
reason_type: reason_type
)
end
end

View File

@ -20,18 +20,21 @@ module Jobs
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?
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
if args[:notification_id].present?
notification = Notification.find_by(id: args[:notification_id])
end
message, skip_reason = message_for_email(
message, skip_reason_type = message_for_email(
user,
post,
type,
@ -42,7 +45,7 @@ module Jobs
if message
Email::Sender.new(message, type, user).send
else
skip_reason
skip_reason_type
end
end
@ -68,10 +71,12 @@ module Jobs
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)
return skip_message(I18n.t("email_log.suspended_not_pm"))
return skip_message(SkippedEmailLog.reason_types[:user_email_user_suspended_not_pm])
end
return if user.staged && type.to_s == "digest"
@ -81,8 +86,10 @@ module Jobs
email_args = {}
if post || notification || notification_type
return skip_message(I18n.t('email_log.seen_recently')) if seen_recently && !user.suspended?
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
@ -108,13 +115,13 @@ module Jobs
unless user.user_option.email_always?
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
skip_reason = skip_email_for_post(post, user)
return skip_message(skip_reason) if skip_reason.present?
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)
@ -123,11 +130,11 @@ module Jobs
email_args[:new_email] = user.email if type.to_s == "notify_old_email"
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
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
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.
def skip_email_for_post(post, user)
if post
return I18n.t('email_log.topic_nil') if post.topic.blank?
return I18n.t('email_log.post_user_deleted') if post.user.blank?
return I18n.t('email_log.post_deleted') if post.user_deleted?
return I18n.t('email_log.user_suspended') if user.suspended? && !post.user&.staff?
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_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
false
end
end
def skip(reason)
EmailLog.create!(
def skip(reason_type)
SkippedEmailLog.create!(
email_type: @skip_context[:type],
to_address: @skip_context[:to_address],
user_id: @skip_context[:user_id],
post_id: @skip_context[:post_id],
skipped: true,
skipped_reason: "[UserEmail] #{reason}",
reason_type: reason_type
)
end

View File

@ -11,6 +11,8 @@ module Jobs
EmailLog.where(reply_key: nil)
.where("created_at < ?", threshold)
.delete_all
SkippedEmailLog.where("created_at < ?", threshold).delete_all
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
include EmailLogsMixin
attributes :id,
:reply_key,
:to_address,
:email_type,
:user_id,
:created_at,
:skipped,
:skipped_reason,
:post_url,
:post_description,
attributes :reply_key,
:bounced
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

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"
type_placeholder: "digest, signup..."
reply_key_placeholder: "reply key"
skipped_reason_placeholder: "reason"
moderation_history:
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."
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:
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"
skipped_email_log:
exceeded_emails_limit: "Exceeded max_emails_per_day_per_user"
exceeded_bounces_limit: "Exceeded bounce_score_threshold"
message_blank: "message is blank"
message_to_blank: "message.to is blank"
text_part_body_blank: "text_part.body is blank"
body_blank: "body is blank"
no_echo_mailing_list_mode: "Mailing list notifications disabled for user's own posts"
mailing_list_no_echo_mode: "Mailing list notifications disabled for user's own posts"
user_email_no_user: "Can't find user with id %{user_id}"
user_email_post_not_found: "Can't find a post with id %{post_id}"
user_email_anonymous_user: "User is anonymous"
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:
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.message rescue nil)
return skip(I18n.t('email_log.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_blank]) if @message.blank?
return skip(SkippedEmailLog.reason_types[:sender_message_to_blank]) if @message.to.blank?
if SiteSetting.disable_emails == "non-staff"
user = User.find_by_email(to_address)
@ -36,9 +36,13 @@ module Email
end
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
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
@message.charset = 'UTF-8'
@ -186,7 +190,7 @@ module Email
begin
@message.deliver_now
rescue *SMTP_CLIENT_ERRORS => e
return skip(e.message)
return skip(SkippedEmailLog.reason_types[:custom], custom_reason: e.message)
end
# Save and return the email log
@ -222,14 +226,16 @@ module Email
header.value
end
def skip(reason)
EmailLog.create!(
def skip(reason_type, custom_reason: nil)
attributes = {
email_type: @email_type,
to_address: to_address,
user_id: @user.try(:id),
skipped: true,
skipped_reason: "[Sender] #{reason}"
)
user_id: @user&.id,
reason_type: reason_type
}
attributes[:custom_reason] = custom_reason if custom_reason
SkippedEmailLog.create!(attributes)
end
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.weeks.ago)
Fabricate(:email_log, created_at: 2.days.ago)
Fabricate(:skipped_email_log, created_at: 2.years.ago)
Fabricate(:skipped_email_log)
end
it "removes old email logs without a reply_key" do
Jobs::CleanUpEmailLogs.new.execute({})
expect(EmailLog.count).to eq(3)
expect(SkippedEmailLog.count).to eq(1)
end
it "does not remove old email logs when delete_email_logs_after_days is 0" do
SiteSetting.delete_email_logs_after_days = 0
Jobs::CleanUpEmailLogs.new.execute({})
expect(EmailLog.count).to eq(4)
expect(SkippedEmailLog.count).to eq(2)
end
end

View File

@ -129,7 +129,13 @@ describe Jobs::NotifyMailingListSubscribers do
Jobs::NotifyMailingListSubscribers.new.execute(post_id: post.id)
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
@ -141,7 +147,13 @@ describe Jobs::NotifyMailingListSubscribers do
Jobs::NotifyMailingListSubscribers.new.execute(post_id: post.id)
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

View File

@ -79,38 +79,46 @@ describe Jobs::UserEmail do
end
context "email_log" do
let(:post) { Fabricate(:post) }
before do
SiteSetting.editing_grace_period = 0
Fabricate(:post)
post
end
it "creates an email log when the mail is sent (via Email::Sender)" do
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
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
expect(email_log.user.last_emailed_at).to_not eq(last_emailed_at)
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
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(email_log.skipped).to eq(true)
expect(email_log.skipped_reason).to be_present
expect(email_log.user_id).to eq(user.id)
expect(SkippedEmailLog.exists?(
email_type: "digest",
user: user,
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
expect(email_log.user.last_emailed_at).to eq(last_emailed_at)
expect(user.last_emailed_at).to eq(last_emailed_at)
end
end
@ -205,8 +213,15 @@ describe Jobs::UserEmail do
notification_data_hash: notification.data_hash
)
expect(message).to eq nil
expect(err.skipped_reason).to match(/notification.*already/)
expect(message).to eq(nil)
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
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
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(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_emails_limit]
)).to eq(true)
end
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(EmailLog.where(user_id: user.id, skipped: true).count).to eq(0)
expect do
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
it "does not send notification if bounce threshold is reached" do
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
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
describe '#skipped' do
let(:user) { Fabricate(:user) }
let!(:log1) { Fabricate(:skipped_email_log, user: user) }
let!(:log2) { Fabricate(:skipped_email_log) }
it "succeeds" do
get "/admin/email/skipped.json"
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
@ -54,6 +79,7 @@ describe Admin::EmailController do
context 'with an email address' do
it 'enqueues a test email job' do
post "/admin/email/test.json", params: { email_address: 'eviltrout@test.domain' }
expect(response.status).to eq(200)
expect(ActionMailer::Base.deliveries.map(&:to).flatten).to include('eviltrout@test.domain')
end