From 908480c98aa48a9d1caf09ee00f3cfe0863afec2 Mon Sep 17 00:00:00 2001 From: Byron Jones Date: Fri, 31 Oct 2014 15:20:42 +0800 Subject: Bug 1062739: add the ability for administrators to limit the number of emails sent to a user per minute and hour r=dylan,a=glob --- Bugzilla/Constants.pm | 18 ++++++++++++++ Bugzilla/DB/Schema.pm | 12 +++++++++ Bugzilla/Install/Requirements.pm | 4 +-- Bugzilla/Job/BugMail.pm | 16 +++--------- Bugzilla/Job/Mailer.pm | 20 +++++++++++---- Bugzilla/Mailer.pm | 53 ++++++++++++++++++++++++++++++++++++++-- 6 files changed, 101 insertions(+), 22 deletions(-) (limited to 'Bugzilla') diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm index 4c1f11003..a770e7eb7 100644 --- a/Bugzilla/Constants.pm +++ b/Bugzilla/Constants.pm @@ -195,6 +195,12 @@ use Memoize; MOST_FREQUENT_THRESHOLD MARKDOWN_TAB_WIDTH + + EMAIL_LIMIT_PER_MINUTE + EMAIL_LIMIT_PER_HOUR + EMAIL_LIMIT_EXCEPTION + + JOB_QUEUE_VIEW_MAX_JOBS ); @Bugzilla::Constants::EXPORT_OK = qw(contenttypes); @@ -641,6 +647,18 @@ use constant MOST_FREQUENT_THRESHOLD => 2; # by Markdown engine use constant MARKDOWN_TAB_WIDTH => 2; +# The maximum number of emails per minute and hour a recipient can receive. +# Email will be queued/backlogged to avoid exceeeding these limits. +# Setting a limit to 0 will disable this feature. +use constant EMAIL_LIMIT_PER_MINUTE => 1000; +use constant EMAIL_LIMIT_PER_HOUR => 2500; +# Don't change this exception message. +use constant EMAIL_LIMIT_EXCEPTION => "email_limit_exceeded\n"; + +# The maximum number of jobs to show when viewing the job queue +# (view_job_queue.cgi). +use constant JOB_QUEUE_VIEW_MAX_JOBS => 500; + sub bz_locations { # Force memoize() to re-compute data per project, to avoid # sharing the same data across different installations. diff --git a/Bugzilla/DB/Schema.pm b/Bugzilla/DB/Schema.pm index ebe2cb426..0698585bb 100644 --- a/Bugzilla/DB/Schema.pm +++ b/Bugzilla/DB/Schema.pm @@ -1640,6 +1640,18 @@ use constant ABSTRACT_SCHEMA => { ], }, + email_rates => { + FIELDS => [ + id => {TYPE => 'INTSERIAL', NOTNULL => 1, + PRIMARYKEY => 1}, + recipient => {TYPE => 'varchar(255)', NOTNULL => 1}, + message_ts => {TYPE => 'DATETIME', NOTNULL => 1}, + ], + INDEXES => [ + email_rates_idx => [qw(recipient message_ts)], + ], + }, + # THESCHWARTZ TABLES # ------------------ # Note: In the standard TheSchwartz schema, most integers are unsigned, diff --git a/Bugzilla/Install/Requirements.pm b/Bugzilla/Install/Requirements.pm index db3d7b028..491bf8a72 100644 --- a/Bugzilla/Install/Requirements.pm +++ b/Bugzilla/Install/Requirements.pm @@ -349,8 +349,8 @@ sub OPTIONAL_MODULES { { package => 'TheSchwartz', module => 'TheSchwartz', - # 1.07 supports the prioritization of jobs. - version => 1.07, + # 1.10 supports declining of jobs. + version => 1.10, feature => ['jobqueue'], }, { diff --git a/Bugzilla/Job/BugMail.pm b/Bugzilla/Job/BugMail.pm index e0b7f5448..b4887c470 100644 --- a/Bugzilla/Job/BugMail.pm +++ b/Bugzilla/Job/BugMail.pm @@ -14,19 +14,9 @@ use warnings; use Bugzilla::BugMail; BEGIN { eval "use parent qw(Bugzilla::Job::Mailer)"; } -sub work { - my ($class, $job) = @_; - my $success = eval { - Bugzilla::BugMail::dequeue($job->arg->{vars}); - 1; - }; - if (!$success) { - $job->failed($@); - undef $@; - } - else { - $job->completed; - } +sub process_job { + my ($class, $arg) = @_; + Bugzilla::BugMail::dequeue($arg->{vars}); } 1; diff --git a/Bugzilla/Job/Mailer.pm b/Bugzilla/Job/Mailer.pm index cd1c23445..7e7549de8 100644 --- a/Bugzilla/Job/Mailer.pm +++ b/Bugzilla/Job/Mailer.pm @@ -11,6 +11,7 @@ use 5.10.1; use strict; use warnings; +use Bugzilla::Constants; use Bugzilla::Mailer; BEGIN { eval "use parent qw(TheSchwartz::Worker)"; } @@ -32,15 +33,24 @@ sub retry_delay { sub work { my ($class, $job) = @_; - my $msg = $job->arg->{msg}; - my $success = eval { MessageToMTA($msg, 1); 1; }; - if (!$success) { - $job->failed($@); + eval { $class->process_job($job->arg) }; + if (my $error = $@) { + if ($error eq EMAIL_LIMIT_EXCEPTION) { + $job->declined(); + } + else { + $job->failed($error); + } undef $@; - } + } else { $job->completed; } } +sub process_job { + my ($class, $arg) = @_; + MessageToMTA($arg, 1); +} + 1; diff --git a/Bugzilla/Mailer.pm b/Bugzilla/Mailer.pm index 4447d4046..389b6f69e 100644 --- a/Bugzilla/Mailer.pm +++ b/Bugzilla/Mailer.pm @@ -41,6 +41,8 @@ sub MessageToMTA { return; } + my $dbh = Bugzilla->dbh; + my $email; if (ref $msg) { $email = $msg; @@ -58,14 +60,50 @@ sub MessageToMTA { # email immediately, in case the transaction is rolled back. Instead we # insert it into the mail_staging table, and bz_commit_transaction calls # send_staged_mail() after the transaction is committed. - if (! $send_now && Bugzilla->dbh->bz_in_transaction()) { + if (! $send_now && $dbh->bz_in_transaction()) { # The e-mail string may contain tainted values. my $string = $email->as_string; trick_taint($string); - Bugzilla->dbh->do("INSERT INTO mail_staging (message) VALUES(?)", undef, $string); + $dbh->do("INSERT INTO mail_staging (message) VALUES(?)", undef, $string); return; } + # Ensure that we are not sending emails too quickly to recipients. + if (Bugzilla->params->{use_mailer_queue} + && (EMAIL_LIMIT_PER_MINUTE || EMAIL_LIMIT_PER_HOUR)) + { + $dbh->do( + "DELETE FROM email_rates WHERE message_ts < " + . $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', '1', 'HOUR')); + + my $recipient = $email->header('To'); + + if (EMAIL_LIMIT_PER_MINUTE) { + my $minute_rate = $dbh->selectrow_array( + "SELECT COUNT(*) + FROM email_rates + WHERE recipient = ? AND message_ts >= " + . $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', '1', 'MINUTE'), + undef, + $recipient); + if ($minute_rate >= EMAIL_LIMIT_PER_MINUTE) { + die EMAIL_LIMIT_EXCEPTION; + } + } + if (EMAIL_LIMIT_PER_HOUR) { + my $hour_rate = $dbh->selectrow_array( + "SELECT COUNT(*) + FROM email_rates + WHERE recipient = ? AND message_ts >= " + . $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', '1', 'HOUR'), + undef, + $recipient); + if ($hour_rate >= EMAIL_LIMIT_PER_HOUR) { + die EMAIL_LIMIT_EXCEPTION; + } + } + } + # We add this header to uniquely identify all email that we # send as coming from this Bugzilla installation. # @@ -181,6 +219,17 @@ sub MessageToMTA { ThrowCodeError('mail_send_error', { msg => $@->message, mail => $email }); } } + + # insert into email_rates + if (Bugzilla->params->{use_mailer_queue} + && (EMAIL_LIMIT_PER_MINUTE || EMAIL_LIMIT_PER_HOUR)) + { + $dbh->do( + "INSERT INTO email_rates(recipient, message_ts) VALUES (?, LOCALTIMESTAMP(0))", + undef, + $email->header('To') + ); + } } # Builds header suitable for use as a threading marker in email notifications -- cgit v1.2.3-24-g4f1b