Lots bounce emails related fixes
- Show bounce score on user admin page - Added reset bounce score button on user admin page - Only whitelisted email types are sent to emails with high bounce score - FIX: properly detect bounces even when there is no TO: header in the email - Don't desactivate a user when reaching the bounce threshold
This commit is contained in:
parent
f9fe9ac3ed
commit
1e57bbf5c8
|
@ -1,3 +1,4 @@
|
||||||
|
import computed from 'ember-addons/ember-computed-decorators';
|
||||||
import { propertyNotEqual } from 'discourse/lib/computed';
|
import { propertyNotEqual } from 'discourse/lib/computed';
|
||||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||||
import ApiKey from 'admin/models/api-key';
|
import ApiKey from 'admin/models/api-key';
|
||||||
|
@ -6,11 +7,42 @@ import TL3Requirements from 'admin/models/tl3-requirements';
|
||||||
|
|
||||||
const AdminUser = Discourse.User.extend({
|
const AdminUser = Discourse.User.extend({
|
||||||
|
|
||||||
customGroups: Em.computed.filter("groups", (g) => !g.automatic && Group.create(g)),
|
customGroups: Ember.computed.filter("groups", g => !g.automatic && Group.create(g)),
|
||||||
automaticGroups: Em.computed.filter("groups", (g) => g.automatic && Group.create(g)),
|
automaticGroups: Ember.computed.filter("groups", g => g.automatic && Group.create(g)),
|
||||||
|
|
||||||
canViewProfile: Ember.computed.or("active", "staged"),
|
canViewProfile: Ember.computed.or("active", "staged"),
|
||||||
|
|
||||||
|
@computed("bounce_score", "reset_bounce_score_after")
|
||||||
|
bounceScore(bounce_score, reset_bounce_score_after) {
|
||||||
|
if (bounce_score > 0) {
|
||||||
|
return `${bounce_score} - ${moment(reset_bounce_score_after).format('LL')}`;
|
||||||
|
} else {
|
||||||
|
return bounce_score;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed("bounce_score")
|
||||||
|
bounceScoreExplanation(bounce_score) {
|
||||||
|
if (bounce_score === 0) {
|
||||||
|
return I18n.t("admin.user.bounce_score_explanation.none");
|
||||||
|
} else if (bounce_score < Discourse.SiteSettings.bounce_score_threshold) {
|
||||||
|
return I18n.t("admin.user.bounce_score_explanation.some");
|
||||||
|
} else {
|
||||||
|
return I18n.t("admin.user.bounce_score_explanation.threshold_reached");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
canResetBounceScore: Ember.computed.gt("bounce_score", 0),
|
||||||
|
|
||||||
|
resetBounceScore() {
|
||||||
|
return Discourse.ajax(`/admin/users/${this.get("id")}/reset_bounce_score`, {
|
||||||
|
type: 'POST'
|
||||||
|
}).then(() => this.setProperties({
|
||||||
|
"bounce_score": 0,
|
||||||
|
"reset_bounce_score_after": null
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
generateApiKey() {
|
generateApiKey() {
|
||||||
const self = this;
|
const self = this;
|
||||||
return Discourse.ajax("/admin/users/" + this.get('id') + "/generate_api_key", {
|
return Discourse.ajax("/admin/users/" + this.get('id') + "/generate_api_key", {
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
<th>{{i18n 'admin.email.user'}}</th>
|
<th>{{i18n 'admin.email.user'}}</th>
|
||||||
<th>{{i18n 'admin.email.to_address'}}</th>
|
<th>{{i18n 'admin.email.to_address'}}</th>
|
||||||
<th>{{i18n 'admin.email.email_type'}}</th>
|
<th>{{i18n 'admin.email.email_type'}}</th>
|
||||||
<th>{{i18n 'admin.email.skipped_reason'}}</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
|
@ -15,7 +14,6 @@
|
||||||
<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>
|
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{{#each model as |l|}}
|
{{#each model as |l|}}
|
||||||
|
@ -31,16 +29,9 @@
|
||||||
</td>
|
</td>
|
||||||
<td><a href='mailto:{{unbound l.to_address}}'>{{l.to_address}}</a></td>
|
<td><a href='mailto:{{unbound l.to_address}}'>{{l.to_address}}</a></td>
|
||||||
<td>{{l.email_type}}</td>
|
<td>{{l.email_type}}</td>
|
||||||
<td>
|
|
||||||
{{#if l.post_url}}
|
|
||||||
<a href="{{l.post_url}}">{{l.skipped_reason}}</a>
|
|
||||||
{{else}}
|
|
||||||
{{l.skipped_reason}}
|
|
||||||
{{/if}}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{{else}}
|
{{else}}
|
||||||
<tr><td colspan="5">{{i18n 'admin.email.logs.none'}}</td></tr>
|
<tr><td colspan="4">{{i18n 'admin.email.logs.none'}}</td></tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -350,7 +350,20 @@
|
||||||
<div class="display-row">
|
<div class="display-row">
|
||||||
<div class='field'>{{i18n 'admin.user.staged'}}</div>
|
<div class='field'>{{i18n 'admin.user.staged'}}</div>
|
||||||
<div class='value'>{{model.staged}}</div>
|
<div class='value'>{{model.staged}}</div>
|
||||||
<div class='controls'>{{i18n 'admin.user.stage_explanation'}}</div>
|
<div class='controls'>{{i18n 'admin.user.staged_explanation'}}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="display-row">
|
||||||
|
<div class='field'>{{i18n 'admin.user.bounce_score'}}</div>
|
||||||
|
<div class='value'>{{model.bounceScore}}</div>
|
||||||
|
<div class='controls'>
|
||||||
|
{{#if model.canResetBounceScore}}
|
||||||
|
<button class='btn' {{action "resetBounceScore" target="content"}} title={{i18n "admin.user.reset_bounce_score.title"}}>
|
||||||
|
{{i18n "admin.user.reset_bounce_score.label"}}
|
||||||
|
</button>
|
||||||
|
{{/if}}
|
||||||
|
{{model.bounceScoreExplanation}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,8 @@ class Admin::UsersController < Admin::AdminController
|
||||||
:primary_group,
|
:primary_group,
|
||||||
:generate_api_key,
|
:generate_api_key,
|
||||||
:revoke_api_key,
|
:revoke_api_key,
|
||||||
:anonymize]
|
:anonymize,
|
||||||
|
:reset_bounce_score]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
users = ::AdminUserIndexQuery.new(params).find_users
|
users = ::AdminUserIndexQuery.new(params).find_users
|
||||||
|
@ -355,6 +356,12 @@ class Admin::UsersController < Admin::AdminController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def reset_bounce_score
|
||||||
|
guardian.ensure_can_reset_bounce_score!(@user)
|
||||||
|
@user.user_stat.update_columns(bounce_score: 0, reset_bounce_score_after: nil)
|
||||||
|
render json: success_json
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def fetch_user
|
def fetch_user
|
||||||
|
|
|
@ -48,7 +48,24 @@ module Jobs
|
||||||
@skip_context = { type: type, user_id: user_id, to_address: to_address, post_id: post_id }
|
@skip_context = { type: type, user_id: user_id, to_address: to_address, post_id: post_id }
|
||||||
end
|
end
|
||||||
|
|
||||||
NOTIFICATIONS_SENT_BY_MAILING_LIST ||= Set.new %w{posted replied mentioned group_mentioned quoted}
|
NOTIFICATIONS_SENT_BY_MAILING_LIST ||= Set.new %w{
|
||||||
|
posted
|
||||||
|
replied
|
||||||
|
mentioned
|
||||||
|
group_mentioned
|
||||||
|
quoted
|
||||||
|
}
|
||||||
|
|
||||||
|
CRITICAL_EMAIL_TYPES = Set.new %i{
|
||||||
|
account_created
|
||||||
|
admin_login
|
||||||
|
confirm_new_email
|
||||||
|
confirm_old_email
|
||||||
|
forgot_password
|
||||||
|
notify_old_email
|
||||||
|
signup
|
||||||
|
signup_after_approval
|
||||||
|
}
|
||||||
|
|
||||||
def message_for_email(user, post, type, notification,
|
def message_for_email(user, post, type, notification,
|
||||||
notification_type=nil, notification_data_hash=nil,
|
notification_type=nil, notification_data_hash=nil,
|
||||||
|
@ -109,7 +126,7 @@ module Jobs
|
||||||
email_args[:email_token] = email_token
|
email_args[:email_token] = email_token
|
||||||
end
|
end
|
||||||
|
|
||||||
if type == 'notify_old_email'
|
if type == :notify_old_email
|
||||||
email_args[:new_email] = user.email
|
email_args[:new_email] = user.email
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -117,7 +134,7 @@ module Jobs
|
||||||
return skip_message(I18n.t('email_log.exceeded_emails_limit'))
|
return skip_message(I18n.t('email_log.exceeded_emails_limit'))
|
||||||
end
|
end
|
||||||
|
|
||||||
if (user.user_stat.try(:bounce_score) || 0) >= SiteSetting.bounce_score_threshold
|
if !CRITICAL_EMAIL_TYPES.include?(type) && user.user_stat.bounce_score >= SiteSetting.bounce_score_threshold
|
||||||
return skip_message(I18n.t('email_log.exceeded_bounces_limit'))
|
return skip_message(I18n.t('email_log.exceeded_bounces_limit'))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ module Jobs
|
||||||
|
|
||||||
set_incoming_email_rejection_message(
|
set_incoming_email_rejection_message(
|
||||||
receiver.incoming_email,
|
receiver.incoming_email,
|
||||||
I18n.t("email.incoming.errors.bounced_email_report")
|
I18n.t("email.incoming.errors.bounced_email_error")
|
||||||
)
|
)
|
||||||
rescue Email::Receiver::AutoGeneratedEmailReplyError => e
|
rescue Email::Receiver::AutoGeneratedEmailReplyError => e
|
||||||
log_email_process_failure(mail_string, e)
|
log_email_process_failure(mail_string, e)
|
||||||
|
|
|
@ -20,7 +20,9 @@ class AdminDetailedUserSerializer < AdminUserSerializer
|
||||||
:primary_group_id,
|
:primary_group_id,
|
||||||
:badge_count,
|
:badge_count,
|
||||||
:warnings_received_count,
|
:warnings_received_count,
|
||||||
:user_fields
|
:user_fields,
|
||||||
|
:bounce_score,
|
||||||
|
:reset_bounce_score_after
|
||||||
|
|
||||||
has_one :approved_by, serializer: BasicUserSerializer, embed: :objects
|
has_one :approved_by, serializer: BasicUserSerializer, embed: :objects
|
||||||
has_one :api_key, serializer: ApiKeySerializer, embed: :objects
|
has_one :api_key, serializer: ApiKeySerializer, embed: :objects
|
||||||
|
@ -76,4 +78,12 @@ class AdminDetailedUserSerializer < AdminUserSerializer
|
||||||
object.user_fields.present?
|
object.user_fields.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def bounce_score
|
||||||
|
object.user_stat.bounce_score
|
||||||
|
end
|
||||||
|
|
||||||
|
def reset_bounce_score_after
|
||||||
|
object.user_stat.reset_bounce_score_after
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -2593,10 +2593,18 @@ en:
|
||||||
block_failed: 'There was a problem blocking the user.'
|
block_failed: 'There was a problem blocking the user.'
|
||||||
block_confirm: 'Are you sure you want to block this user? They will not be able to create any new topics or posts.'
|
block_confirm: 'Are you sure you want to block this user? They will not be able to create any new topics or posts.'
|
||||||
block_accept: 'Yes, block this user'
|
block_accept: 'Yes, block this user'
|
||||||
|
bounce_score: "Bounce Score"
|
||||||
|
reset_bounce_score:
|
||||||
|
label: "Reset"
|
||||||
|
title: "Reset bounce score back to 0"
|
||||||
deactivate_explanation: "A deactivated user must re-validate their email."
|
deactivate_explanation: "A deactivated user must re-validate their email."
|
||||||
suspended_explanation: "A suspended user can't log in."
|
suspended_explanation: "A suspended user can't log in."
|
||||||
block_explanation: "A blocked user can't post or start topics."
|
block_explanation: "A blocked user can't post or start topics."
|
||||||
stage_explanation: "A staged user can only post via email in specific topics."
|
staged_explanation: "A staged user can only post via email in specific topics."
|
||||||
|
bounce_score_explanation:
|
||||||
|
none: "No bounces were received recently from that email."
|
||||||
|
some: "Some bounces were received recently from that email."
|
||||||
|
threshold_reached: "Received too many bounces from that email."
|
||||||
trust_level_change_failed: "There was a problem changing the user's trust level."
|
trust_level_change_failed: "There was a problem changing the user's trust level."
|
||||||
suspend_modal_title: "Suspend User"
|
suspend_modal_title: "Suspend User"
|
||||||
trust_level_2_users: "Trust Level 2 Users"
|
trust_level_2_users: "Trust Level 2 Users"
|
||||||
|
|
|
@ -64,7 +64,7 @@ en:
|
||||||
reply_user_not_matching_error: "Happens when a reply came in from a different email address the notification was sent to."
|
reply_user_not_matching_error: "Happens when a reply came in from a different email address the notification was sent to."
|
||||||
topic_not_found_error: "Happens when a reply came in but the related topic has been deleted."
|
topic_not_found_error: "Happens when a reply came in but the related topic has been deleted."
|
||||||
topic_closed_error: "Happens when a reply came in but the related topic has been closed."
|
topic_closed_error: "Happens when a reply came in but the related topic has been closed."
|
||||||
bounced_email_report: "Email is a bounced email report."
|
bounced_email_error: "Email is a bounced email report."
|
||||||
auto_generated_email_reply: "Email contains a reply to an auto generated email."
|
auto_generated_email_reply: "Email contains a reply to an auto generated email."
|
||||||
screened_email_error: "Happens when the sender's email address was already screened."
|
screened_email_error: "Happens when the sender's email address was already screened."
|
||||||
|
|
||||||
|
|
|
@ -108,6 +108,7 @@ Discourse::Application.routes.draw do
|
||||||
get "leader_requirements" => "users#tl3_requirements"
|
get "leader_requirements" => "users#tl3_requirements"
|
||||||
get "tl3_requirements"
|
get "tl3_requirements"
|
||||||
put "anonymize"
|
put "anonymize"
|
||||||
|
post "reset_bounce_score"
|
||||||
end
|
end
|
||||||
get "users/:id.json" => 'users#show', defaults: {format: 'json'}
|
get "users/:id.json" => 'users#show', defaults: {format: 'json'}
|
||||||
get 'users/:id/:username' => 'users#show'
|
get 'users/:id/:username' => 'users#show'
|
||||||
|
|
|
@ -572,6 +572,7 @@ email:
|
||||||
type: list
|
type: list
|
||||||
block_auto_generated_emails: true
|
block_auto_generated_emails: true
|
||||||
bounce_score_threshold:
|
bounce_score_threshold:
|
||||||
|
client: true
|
||||||
default: 4
|
default: 4
|
||||||
min: 1
|
min: 1
|
||||||
|
|
||||||
|
|
|
@ -156,7 +156,7 @@ module Email
|
||||||
end
|
end
|
||||||
|
|
||||||
def verp
|
def verp
|
||||||
@verp ||= @mail.destinations.select { |to| to[/\+verp-\h{32}@/] }.first
|
@verp ||= all_destinations.select { |to| to[/\+verp-\h{32}@/] }.first
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_bounce_score(email, score)
|
def update_bounce_score(email, score)
|
||||||
|
@ -171,10 +171,8 @@ module Email
|
||||||
user.user_stat.reset_bounce_score_after = 30.days.from_now
|
user.user_stat.reset_bounce_score_after = 30.days.from_now
|
||||||
user.user_stat.save
|
user.user_stat.save
|
||||||
|
|
||||||
if user.active && user.user_stat.bounce_score >= SiteSetting.bounce_score_threshold
|
if user.user_stat.bounce_score >= SiteSetting.bounce_score_threshold
|
||||||
user.deactivate
|
|
||||||
StaffActionLogger.new(Discourse.system_user).log_revoke_email(user)
|
StaffActionLogger.new(Discourse.system_user).log_revoke_email(user)
|
||||||
EmailToken.where(email: user.email, confirmed: true).update_all(confirmed: false)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -292,16 +290,18 @@ module Email
|
||||||
user
|
user
|
||||||
end
|
end
|
||||||
|
|
||||||
def destinations
|
def all_destinations
|
||||||
[ @mail.destinations,
|
@all_destinations ||= [
|
||||||
|
@mail.destinations,
|
||||||
[@mail[:x_forwarded_to]].flatten.compact.map(&:decoded),
|
[@mail[:x_forwarded_to]].flatten.compact.map(&:decoded),
|
||||||
[@mail[:delivered_to]].flatten.compact.map(&:decoded),
|
[@mail[:delivered_to]].flatten.compact.map(&:decoded),
|
||||||
].flatten
|
].flatten.select(&:present?).uniq.lazy
|
||||||
.select(&:present?)
|
end
|
||||||
.uniq
|
|
||||||
.lazy
|
def destinations
|
||||||
.map { |d| check_address(d) }
|
all_destinations
|
||||||
.drop_while(&:blank?)
|
.map { |d| check_address(d) }
|
||||||
|
.drop_while(&:blank?)
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_address(address)
|
def check_address(address)
|
||||||
|
|
|
@ -51,6 +51,10 @@ module UserGuardian
|
||||||
is_staff? && !user.nil? && !user.staff?
|
is_staff? && !user.nil? && !user.staff?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def can_reset_bounce_score?(user)
|
||||||
|
user && is_staff?
|
||||||
|
end
|
||||||
|
|
||||||
def can_check_emails?(user)
|
def can_check_emails?(user)
|
||||||
is_admin? || (is_staff? && SiteSetting.show_email_on_profile)
|
is_admin? || (is_staff? && SiteSetting.show_email_on_profile)
|
||||||
end
|
end
|
||||||
|
|
|
@ -68,7 +68,7 @@ describe Email::Receiver do
|
||||||
|
|
||||||
let(:bounce_key) { "14b08c855160d67f2e0c2f8ef36e251e" }
|
let(:bounce_key) { "14b08c855160d67f2e0c2f8ef36e251e" }
|
||||||
let(:bounce_key_2) { "b542fb5a9bacda6d28cc061d18e4eb83" }
|
let(:bounce_key_2) { "b542fb5a9bacda6d28cc061d18e4eb83" }
|
||||||
let!(:user) { Fabricate(:user, email: "foo@bar.com", active: true) }
|
let!(:user) { Fabricate(:user, email: "foo@bar.com") }
|
||||||
let!(:email_log) { Fabricate(:email_log, user: user, bounce_key: bounce_key) }
|
let!(:email_log) { Fabricate(:email_log, user: user, bounce_key: bounce_key) }
|
||||||
let!(:email_log_2) { Fabricate(:email_log, user: user, bounce_key: bounce_key_2) }
|
let!(:email_log_2) { Fabricate(:email_log, user: user, bounce_key: bounce_key_2) }
|
||||||
|
|
||||||
|
@ -82,7 +82,6 @@ describe Email::Receiver do
|
||||||
|
|
||||||
email_log.reload
|
email_log.reload
|
||||||
expect(email_log.bounced).to eq(true)
|
expect(email_log.bounced).to eq(true)
|
||||||
expect(email_log.user.active).to eq(true)
|
|
||||||
expect(email_log.user.user_stat.bounce_score).to eq(1)
|
expect(email_log.user.user_stat.bounce_score).to eq(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -91,7 +90,6 @@ describe Email::Receiver do
|
||||||
|
|
||||||
email_log.reload
|
email_log.reload
|
||||||
expect(email_log.bounced).to eq(true)
|
expect(email_log.bounced).to eq(true)
|
||||||
expect(email_log.user.active).to eq(true)
|
|
||||||
expect(email_log.user.user_stat.bounce_score).to eq(2)
|
expect(email_log.user.user_stat.bounce_score).to eq(2)
|
||||||
|
|
||||||
Timecop.freeze(2.days.from_now) do
|
Timecop.freeze(2.days.from_now) do
|
||||||
|
@ -99,7 +97,6 @@ describe Email::Receiver do
|
||||||
|
|
||||||
email_log_2.reload
|
email_log_2.reload
|
||||||
expect(email_log_2.bounced).to eq(true)
|
expect(email_log_2.bounced).to eq(true)
|
||||||
expect(email_log_2.user.active).to eq(false)
|
|
||||||
expect(email_log_2.user.user_stat.bounce_score).to eq(4)
|
expect(email_log_2.user.user_stat.bounce_score).to eq(4)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue