]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 1612290 - Provide self-service UI for users to reactivate their account after...
authordklawren <dklawren@users.noreply.github.com>
Wed, 26 Feb 2020 19:32:54 +0000 (14:32 -0500)
committerGitHub <noreply@github.com>
Wed, 26 Feb 2020 19:32:54 +0000 (14:32 -0500)
13 files changed:
Bugzilla/App.pm
Bugzilla/App/BouncedEmails.pm [new file with mode: 0644]
Bugzilla/App/SES.pm
Bugzilla/Constants.pm
Bugzilla/User.pm
docker-compose.test.yml
docker-compose.yml
editusers.cgi
t/bmo/bounced-emails.t [new file with mode: 0644]
template/en/default/account/email/bounced-emails.html.tmpl [new file with mode: 0644]
template/en/default/admin/users/userdata.html.tmpl
template/en/default/global/header.html.tmpl
template/en/default/global/messages.html.tmpl

index 5990a6854005774a6d432952db71bbe8dd572e7b..5699f6203e3043db8536404b99afb060a83e6607 100644 (file)
@@ -21,11 +21,12 @@ use Bugzilla::Constants qw(bz_locations MAX_STS_AGE);
 use Bugzilla::Extension             ();
 use Bugzilla::Install::Requirements ();
 use Bugzilla::Logging;
+use Bugzilla::App::API;
+use Bugzilla::App::BouncedEmails;
 use Bugzilla::App::CGI;
+use Bugzilla::App::Main;
 use Bugzilla::App::OAuth2::Clients;
 use Bugzilla::App::SES;
-use Bugzilla::App::Main;
-use Bugzilla::App::API;
 use Bugzilla::App::Static;
 use Mojo::Loader qw( find_modules );
 use Module::Runtime qw( require_module );
@@ -192,11 +193,12 @@ sub setup_routes {
   my ($self) = @_;
 
   my $r = $self->routes;
+  Bugzilla::App::API->setup_routes($r);
+  Bugzilla::App::BouncedEmails->setup_routes($r);
   Bugzilla::App::CGI->setup_routes($r);
   Bugzilla::App::Main->setup_routes($r);
-  Bugzilla::App::API->setup_routes($r);
-  Bugzilla::App::SES->setup_routes($r);
   Bugzilla::App::OAuth2::Clients->setup_routes($r);
+  Bugzilla::App::SES->setup_routes($r);
 
   $r->static_file('/__lbheartbeat__');
   $r->static_file(
diff --git a/Bugzilla/App/BouncedEmails.pm b/Bugzilla/App/BouncedEmails.pm
new file mode 100644 (file)
index 0000000..7765b5b
--- /dev/null
@@ -0,0 +1,63 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::App::BouncedEmails;
+
+use 5.10.1;
+use Mojo::Base qw( Mojolicious::Controller );
+
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Token;
+
+sub setup_routes {
+  my ($class, $r) = @_;
+  $r->any('/bounced_emails/:userid')->to('BouncedEmails#view');
+}
+
+sub view {
+  my ($self)     = @_;
+  my $user       = $self->bugzilla->login(LOGIN_REQUIRED);
+  my $other_user = Bugzilla::User->check({id => $self->param('userid')});
+
+  unless ($user->in_group('editusers')
+    || $user->in_group('disableusers')
+    || $user->id == $other_user->id)
+  {
+    ThrowUserError('auth_failure',
+      {reason => "not_visible", action => "modify", object => "user"});
+  }
+
+  if ( $self->param('enable_email')
+    && $user->id == $other_user->id
+    && $other_user->email_disabled)
+  {
+    my $token = $self->param('token');
+    check_token_data($token, 'bounced_emails');
+
+    $other_user->set_email_enabled(1);
+    $other_user->update();
+
+    return $self->redirect_to('/home');
+  }
+
+  my $token = issue_session_token('bounced_emails');
+  $self->stash(
+    {
+      bounce_max => BOUNCE_COUNT_MAX,
+      user       => $user,
+      other_user => $other_user,
+      token      => $token
+    }
+  );
+  return $self->render(
+    template => 'account/email/bounced-emails',
+    handler  => 'bugzilla'
+  );
+}
+
+1;
index 037d64534cbf42299860f10c14b00bd727bd06bd..ffebdbe9fe607f368a867737b7c11c3a0a8d7d5d 100644 (file)
@@ -10,7 +10,7 @@ package Bugzilla::App::SES;
 use 5.10.1;
 use Mojo::Base qw( Mojolicious::Controller );
 
-use Bugzilla::Constants qw(ERROR_MODE_DIE);
+use Bugzilla::Constants qw(BOUNCE_COUNT_MAX ERROR_MODE_DIE);
 use Bugzilla::Logging;
 use Bugzilla::Mailer qw(MessageToMTA);
 use Bugzilla::User ();
@@ -182,7 +182,7 @@ sub _process_bounce {
   # disable each account that is bouncing
   foreach my $recipient (@{$notification->{bounce}->{bouncedRecipients}}) {
     my $address = $recipient->{emailAddress};
-    my $reason = sprintf '(%s) %s', $recipient->{action} // 'error',
+    my $reason  = sprintf '(%s) %s', $recipient->{action} // 'error',
       $recipient->{diagnosticCode} // 'unknown';
 
     my $user = Bugzilla::User->new({name => $address, cache => 1});
@@ -199,16 +199,45 @@ sub _process_bounce {
           mta    => $notification->{bounce}->{reportingMTA} // 'unknown',
           reason => $reason,
         };
-        my $disable_text;
+        my $bounce_message;
         $template->process('admin/users/bounce-disabled.txt.tmpl',
-          $vars, \$disable_text)
+          $vars, \$bounce_message)
           || die $template->error();
 
-        $user->set_disabledtext($disable_text);
+        # Increment bounce count for user
+        my $bounce_count = $user->bounce_count + 1;
+
+        # If user has not had a bounce in less than 30 days, set the bounce count to 1 instead
+        my $dbh = Bugzilla->dbh;
+        my ($has_recent_bounce) = $dbh->selectrow_array(
+          "SELECT 1 FROM audit_log WHERE object_id = ? AND class = 'Bugzilla::User' AND field = 'bounce_message' AND ("
+            . $dbh->sql_date_math('at_time', '+', 30, 'DAY')
+            . ") > NOW()",
+          undef, $user->id
+        );
+        $bounce_count = 1 if !$has_recent_bounce;
+
         $user->set_disable_mail(1);
+        $user->set_bounce_count($bounce_count);
+
+        # if we hit the max amount, go ahead and disabled the account
+        # and an admin will need to reactivate the account.
+        if ($bounce_count == BOUNCE_COUNT_MAX) {
+          $user->set_disabledtext($bounce_message);
+        }
+
         $user->update();
+
+        # Do this outside of Object.pm as we do not want to
+        # store the messages anywhere else.
+        $dbh->do(
+          "INSERT INTO audit_log (user_id, class, object_id, field, added, at_time)
+           VALUES (?, 'Bugzilla::User', ?, 'bounce_message', ?, LOCALTIMESTAMP(0))",
+          undef, $user->id, $user->id, $bounce_message
+        );
+
         Bugzilla->audit(
-          "bounce for <$address> disabled userid-" . $user->id . ": $reason");
+          "bounce for <$address> disabled email for userid-" . $user->id . ": $reason");
       }
     }
 
@@ -224,7 +253,7 @@ sub _process_complaint {
   state $check = compile(Self, ComplaintNotification);
   my ($self, $notification) = $check->(@_);
   my $template = Bugzilla->template_inner();
-  my $json = JSON::MaybeXS->new(pretty => 1, utf8 => 1, canonical => 1,);
+  my $json     = JSON::MaybeXS->new(pretty => 1, utf8 => 1, canonical => 1,);
 
   foreach my $recipient (@{$notification->{complaint}->{complainedRecipients}}) {
     my $reason  = $notification->{complaint}->{complaintFeedbackType} // 'unknown';
index 53223421d6656eea14ede70480fb68417574aede..6b1f0e58a2fcfdc81ead33c1bca88480cd3297f1 100644 (file)
@@ -203,6 +203,8 @@ use Memoize;
   EMAIL_LIMIT_EXCEPTION
 
   JOB_QUEUE_VIEW_MAX_JOBS
+
+  BOUNCE_COUNT_MAX
 );
 
 @Bugzilla::Constants::EXPORT_OK = qw(contenttypes);
@@ -666,6 +668,10 @@ use constant EMAIL_LIMIT_EXCEPTION => "email_limit_exceeded\n";
 # (view_job_queue.cgi).
 use constant JOB_QUEUE_VIEW_MAX_JOBS => 2500;
 
+# Maximum number of times an email can bounce for an account
+# before the account is completely disabled.
+use constant BOUNCE_COUNT_MAX => 5;
+
 sub bz_locations {
 
   # Force memoize() to re-compute data per project, to avoid
index aa0ad090135b70b2e449f4d48d17581deea9a144..911c39164d7392f173dbed9c93bce574956a7aaa 100644 (file)
@@ -85,7 +85,8 @@ sub DB_COLUMNS {
     'profiles.password_change_reason',
     'profiles.mfa',
     'profiles.mfa_required_date',
-    'profiles.nickname'
+    'profiles.nickname',
+    'profiles.bounce_count'
     ),
     ;
 }
@@ -106,6 +107,7 @@ use constant VALIDATORS => {
   password_change_required => \&Bugzilla::Object::check_boolean,
   password_change_reason   => \&_check_password_change_reason,
   mfa                      => \&_check_mfa,
+  bounce_count             => \&_check_numeric,
 };
 
 sub UPDATE_COLUMNS {
@@ -122,6 +124,7 @@ sub UPDATE_COLUMNS {
     mfa
     mfa_required_date
     nickname
+    bounce_count
   );
   push(@cols, 'cryptpassword') if exists $self->{cryptpassword};
   return @cols;
@@ -473,6 +476,16 @@ sub _check_mfa {
   return '';
 }
 
+sub _check_numeric {
+  my ($self, $value) = (@_);
+  if ($value !~ /^[0-9]+$/) {
+    ThrowCodeError('param_must_be_numeric',
+        {param => $value, function => 'Bugzilla::User::_check_numeric'});
+    return "must be a numeric value";
+  }
+  return $value;
+}
+
 ################################################################################
 # Mutators
 ################################################################################
@@ -2753,6 +2766,33 @@ sub account_ip_login_failures {
   return $self->{account_ip_login_failures};
 }
 
+#################
+# Email Bounces #
+#################
+
+sub bounce_count { $_[0]->{bounce_count}; }
+
+sub bounce_messages {
+  my ($self)       = @_;
+  my $dbh          = Bugzilla->dbh;
+  my $bounce_count = $self->bounce_count;
+  return $self->{bounce_messages} ||= $dbh->selectall_arrayref(
+    "SELECT " . $dbh->sql_date_format('at_time', '%Y-%m-%d %H:%i') . "
+     AS bounce_when, added AS bounce_message FROM audit_log
+     WHERE object_id = ? AND class = 'Bugzilla::User' AND field = 'bounce_message'
+     ORDER BY at_time DESC LIMIT $bounce_count",
+    {Slice => {}},
+    $self->id
+  );
+}
+
+sub set_bounce_count {
+  my ($self, $count) = @_;
+  $self->set('bounce_count', $count);
+  $self->{bounce_count} = $count;
+}
+
+
 ###############
 # Subroutines #
 ###############
index 4bae22506c2e39a318f784296bd8798871f17798..1fb0655893f25f2b3bdf55d86ee3ddb46b5b2eec 100644 (file)
@@ -19,6 +19,8 @@ services:
       - BMO_db_user=bugs
       - BMO_memcached_namespace=bugzilla
       - BMO_memcached_servers=memcached:11211
+      - BMO_ses_username=ses@mozilla.bugs
+      - BMO_ses_password=password123456789!
       - BMO_urlbase=AUTOMATIC
       - BUGZILLA_ALLOW_INSECURE_HTTP=1
       - BZ_ANSWERS_FILE=/app/conf/checksetup_answers.txt
@@ -42,6 +44,8 @@ services:
 
   bmo.db:
     image: mozillabteam/bmo-mysql:5.7
+    tmpfs:
+      - /tmp
     logging:
       driver: "none"
     environment:
index 36ab67d02cd83d9a904e69780e36c90d6ad80323..2bfedb8955a553213f44f0eacdf2c61c16f8b4e1 100644 (file)
@@ -24,6 +24,8 @@ services:
       - BMO_db_user=bugs
       - BMO_memcached_namespace=bugzilla
       - BMO_memcached_servers=memcached:11211
+      - BMO_ses_username=ses@mozilla.bugs
+      - BMO_ses_password=password123456789!
       - BMO_urlbase=http://bmo.test/
       - BUGZILLA_ALLOW_INSECURE_HTTP=1
       - BZ_ANSWERS_FILE=/app/conf/checksetup_answers.txt
index 278393c6fd46b41450b7ca4362cbb9a3e8266d4c..00cfb4bb40cd03f9228e6d96ce2fd8e7141ec993 100755 (executable)
@@ -285,6 +285,7 @@ elsif ($action eq 'update') {
     $otherUser->set_name($cgi->param('name'));
     $otherUser->set_disabledtext($cgi->param('disabledtext'));
     $otherUser->set_disable_mail($cgi->param('disable_mail'));
+    $otherUser->set_bounce_count(0) if $cgi->param('reset_bounce');
   }
 
   $changes = $otherUser->update();
diff --git a/t/bmo/bounced-emails.t b/t/bmo/bounced-emails.t
new file mode 100644 (file)
index 0000000..b61e066
--- /dev/null
@@ -0,0 +1,143 @@
+#!/usr/bin/env perl
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+use strict;
+use warnings;
+use 5.10.1;
+use lib qw( . lib local/lib/perl5 );
+
+BEGIN {
+  $ENV{LOG4PERL_CONFIG_FILE}     = 'log4perl-t.conf';
+  $ENV{BUGZILLA_DISABLE_HOSTAGE} = 1;
+}
+
+use Mojo::URL;
+use Mojo::UserAgent;
+use Test2::V0;
+use Test::Selenium::Remote::Driver;
+
+my $ADMIN_LOGIN  = $ENV{BZ_TEST_ADMIN}      // 'admin@mozilla.bugs';
+my $ADMIN_PW_OLD = $ENV{BZ_TEST_ADMIN_PASS} // 'Te6Oovohch';
+my $SES_USERNAME = $ENV{BMO_ses_username}   // 'ses@mozilla.bugs';
+my $SES_PASSWORD = $ENV{BMO_ses_password}   // 'password123456789!';
+
+my @require_env = qw(
+  BZ_BASE_URL
+  BZ_TEST_NEWBIE
+  BZ_TEST_NEWBIE_PASS
+  TWD_HOST
+  TWD_PORT
+);
+
+my @missing_env = grep { !exists $ENV{$_} } @require_env;
+BAIL_OUT("Missing env: @missing_env") if @missing_env;
+
+my $sel = Test::Selenium::Remote::Driver->new(
+  base_url   => $ENV{BZ_BASE_URL},
+  browser    => 'firefox',
+  version    => '',
+  javascript => 1
+);
+
+my $ua = Mojo::UserAgent->new;
+$ua->on(
+  start => sub {
+    my ($ua, $tx) = @_;
+    $tx->req->headers->header('X-Amz-SNS-Message-Type' => 'Notification');
+  }
+);
+
+my $ses_data = <DATA>;
+my $ses_url  = Mojo::URL->new($ENV{BZ_BASE_URL} . 'ses/index.cgi')
+  ->userinfo("$SES_USERNAME:$SES_PASSWORD");
+
+# First bounce
+my $result = $ua->post($ses_url => $ses_data)->result;
+ok($result->is_success, 'Posting first bounce was successful');
+
+# Allow user to reset their email
+$sel->set_implicit_wait_timeout(600);
+login_ok($sel, $ENV{BZ_TEST_NEWBIE}, $ENV{BZ_TEST_NEWBIE_PASS});
+$sel->body_text_contains('Change notification emails have been disabled',
+  'Email disabled warning is displayed');
+$sel->click_element_ok('//a[@id="bounced_emails_link"]');
+sleep(2);
+$sel->title_is('Bounced Emails');
+$sel->click_element_ok('//input[@id="enable_email"]');
+submit($sel, '//input[@value="Submit"]');
+sleep(2);
+$sel->title_is('Bugzilla Main Page');
+$sel->body_text_lacks(
+  'Change notification emails have been disabled',
+  'Email disabled warning is no longer displayed'
+);
+logout_ok($sel);
+
+# Bounce 4 more times causing account to be locked
+$result = $ua->post($ses_url => $ses_data)->result;
+ok($result->is_success, 'Posting third bounce was successful');
+$result = $ua->post($ses_url => $ses_data)->result;
+ok($result->is_success, 'Posting fourth bounce was successful');
+$result = $ua->post($ses_url => $ses_data)->result;
+ok($result->is_success, 'Posting fifth bounce was successful');
+$result = $ua->post($ses_url => $ses_data)->result;
+ok($result->is_success, 'Posting fifth bounce was successful');
+
+# User should not be able to login again
+login($sel, $ENV{BZ_TEST_NEWBIE}, $ENV{BZ_TEST_NEWBIE_PASS});
+$sel->title_is('Account Disabled');
+$sel->body_text_contains(
+  'Your Bugzilla account has been disabled due to issues delivering emails to your address.',
+  'Account disabled message is displayed'
+);
+
+done_testing;
+
+sub submit {
+  my ($sel, $xpath) = @_;
+  $sel->find_element($xpath, 'xpath')->click_ok('Submit OK');
+}
+
+sub click_and_type {
+  my ($sel, $name, $text) = @_;
+
+  eval {
+    my $el
+      = $sel->find_element(qq{//*[\@id="bugzilla-body"]//input[\@name="$name"]},
+      'xpath');
+    $el->click();
+    $sel->send_keys_to_active_element($text);
+    pass("found $name and typed $text");
+  };
+  if ($@) {
+    fail("failed to find $name");
+  }
+}
+
+sub login {
+  my ($sel, $login, $password) = @_;
+  $sel->get_ok("/login");
+  $sel->title_is("Log in to Bugzilla");
+  click_and_type($sel, 'Bugzilla_login',    $login);
+  click_and_type($sel, 'Bugzilla_password', $password);
+  submit($sel, '//input[@id="log_in"]');
+}
+
+sub login_ok {
+  my ($sel) = @_;
+  login(@_);
+  $sel->title_is('Bugzilla Main Page');
+}
+
+sub logout_ok {
+  my ($sel) = @_;
+  $sel->get_ok('/index.cgi?logout=1');
+  $sel->title_is("Logged Out");
+}
+
+__DATA__
+{"Type":"Notification","Message":"{\"eventType\":\"Bounce\",\"bounce\":{\"bounceType\":\"Permanent\",\"bounceSubType\":\"General\",\"bouncedRecipients\":[{\"emailAddress\":\"newbie@mozilla.example\",\"action\":\"failed\",\"status\":\"5.1.1\",\"diagnosticCode\":\"smtp;5505.1.1userunknown\"}],\"timestamp\":\"2017-08-05T00:41:02.669Z\",\"feedbackId\":\"01000157c44f053b-61b59c11-9236-11e6-8f96-7be8aexample-000000\",\"reportingMTA\":\"dsn;mta.example.com\"},\"mail\":{\"timestamp\":\"2017-08-05T00:40:02.012Z\",\"source\":\"BugzillaDaemon<bugzilla@mozilla.bugs>\",\"sourceArn\":\"arn:aws:ses:us-east-1:123456789012:identity/bugzilla@mozilla.bugs\",\"sendingAccountId\":\"123456789012\",\"messageId\":\"EXAMPLE7c191be45-e9aedb9a-02f9-4d12-a87d-dd0099a07f8a-000000\",\"destination\":[\"newbie@mozilla.example\"],\"headersTruncated\":false,\"headers\":[{\"name\":\"From\",\"value\":\"BugzillaDaemon<bugzilla@mozilla.bugs>\"},{\"name\":\"To\",\"value\":\"newbie@mozilla.example\"},{\"name\":\"Subject\",\"value\":\"MessagesentfromAmazonSES\"},{\"name\":\"MIME-Version\",\"value\":\"1.0\"},{\"name\":\"Content-Type\",\"value\":\"multipart/alternative;boundary=\"}],\"commonHeaders\":{\"from\":[\"BugzillaDaemon<bugzilla@mozilla.bugs>\"],\"to\":[\"newbie@mozilla.example\"],\"messageId\":\"EXAMPLE7c191be45-e9aedb9a-02f9-4d12-a87d-dd0099a07f8a-000000\",\"subject\":\"MessagesentfromAmazonSES\"},\"tags\":{\"ses:configuration-set\":[\"ConfigSet\"],\"ses:source-ip\":[\"192.0.2.0\"],\"ses:from-domain\":[\"example.com\"],\"ses:caller-identity\":[\"ses_user\"]}}}"}
diff --git a/template/en/default/account/email/bounced-emails.html.tmpl b/template/en/default/account/email/bounced-emails.html.tmpl
new file mode 100644 (file)
index 0000000..ba0b749
--- /dev/null
@@ -0,0 +1,61 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+  # License, v. 2.0. If a copy of the MPL was not distributed with this
+  # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+  #
+  # This Source Code Form is "Incompatible With Secondary Licenses", as
+  # defined by the Mozilla Public License, v. 2.0.
+  #%]
+
+[% PROCESS global/header.html.tmpl
+  title = "Bounced Emails"
+  style_urls = [ "skins/standard/describecomponents.css" ]
+%]
+
+<h2>[% title FILTER html %]</h2>
+
+[% IF user.id == other_user.id AND other_user.bounce_count AND other_user.email_disabled %]
+<p>
+  Due to issues delivering email to your account, we have temporarily disabled email notifications
+  being sent to it. If you feel like the issue has been resolved, you may reactivate email delivery
+  below. After a maximum of [% bounce_max FILTER html %] bounces, we will disable logging in to your
+  account and you will need to contact an administrator to reactivate it.</p>
+
+<form action="/bounced_emails/[% other_user.id FILTER uri %]">
+  <input type="hidden" name="token" value="[% token FILTER html %]">
+  <table>
+    <tr>
+      <td><input type="checkbox" name="enable_email" id="enable_email" value="1"></td>
+      <td>I have resolved the issue and would like email delivery for my account to be reactivated.</td>
+    </tr>
+  </table>
+  <br>
+  <input type="submit" value="Submit">
+</form>
+[% END %]
+
+[% IF (user.id == other_user.id OR user.in_group('editusers') OR user.in_group('disableusers'))
+       AND other_user.bounce_count %]
+<h3>History</h3>
+
+<div class="list">
+  [% FOREACH bounce = other_user.bounce_messages %]
+    [% IF loop.first %]
+    <strong>Current</strong>
+    [% ELSIF loop.count == 2 %]
+    <strong>Older</strong>
+    [% END %]
+    <section class="component">
+    <header>
+      <h2>[% bounce.bounce_when FILTER time FILTER html %]</h2>
+    </header>
+    <div>
+      <p class="description">[% bounce.bounce_message FILTER html %]</p>
+    </div>
+  </section>
+  [% END %]
+</div>
+[% ELSE %]
+No bounced email messages have been recorded.
+[% END %]
+
+[% PROCESS global/footer.html.tmpl %]
index 30396f22d86ffbca250c0b1fd301809c98def628..0abca2baf51552dddd5db151a3b56ff5e043466e 100644 (file)
       </label>
     </td>
   </tr>
+  [% IF otheruser.bounce_count %]
+  <tr>
+    <th><label for="reset_bounce">Reset Bounce Count</label></th>
+    <td>
+      <input type="checkbox" name="reset_bounce" id="reset_bounce" value="1">
+      (<a href="/bounced_emails/[% otheruser.id FILTER uri %]" title="View bounce email history">
+        [% otheruser.bounce_count FILTER html %]</a>)
+    </td>
+  </tr>
+  [% END %]
   <tr>
     <th><label for="disabledtext">Disable text:</label></th>
     <td>
index 9d60d34a7d23f12da10aa5dd9908ba9e137aa7b4..fa324d4262909ab75c51db3781e834b747849fc0 100644 (file)
   <h2>[% header FILTER none %]</h2>
 [% END %]
 
+[%# Show banner for users who have email disabled due to bounces %]
+[% IF user.bounce_count AND user.email_disabled %]
+  <div id="message">
+    Change notification emails have been disabled for your account due to issues delivering to your address.
+    <a href="/bounced_emails/[% user.id FILTER uri %]" id="bounced_emails_link">View recent errors and reactivate email</a>.
+  </div>
+[% END %]
+
 [% IF message %]
   <div id="message">[% message %]</div>
 [% END %]
index 6c94548aef212c76110507fd5c2dbe2512b4cb2e..ce0b31cac13a627b3645b4f7dd81071e20c359ca 100644 (file)
@@ -74,6 +74,8 @@
               [% ELSE %]
                 [% terms.Bug %]mail has been enabled.
               [% END %]
+            [% ELSIF field == 'bounce_count' %]
+              Bounced email count has been reset.
             [% ELSIF field == 'password_change_required' %]
               The user [% otheruser.password_change_required ? "must" : "no longer needs to" %] update their password.
             [% ELSIF field == 'password_change_reason' %]