From ab4d478894783f3e57c913a2a99f41059c460162 Mon Sep 17 00:00:00 2001 From: Marcel de Rooy Date: Thu, 30 Mar 2023 14:56:55 +0200 Subject: [PATCH] Bug 33360: Add Koha::Notice::Util for mail domain limits Content-Type: text/plain; charset=utf-8 Ground work in new module. Includes unit test. Test plan: Run t/db_dependent/Koha/Notice_Util.t Signed-off-by: Marcel de Rooy --- Koha/Notice/Util.pm | 109 ++++++++++++++++++++++++++++++ t/db_dependent/Koha/Notice_Util.t | 109 ++++++++++++++++++++++++++++++ 2 files changed, 218 insertions(+) create mode 100644 Koha/Notice/Util.pm create mode 100755 t/db_dependent/Koha/Notice_Util.t diff --git a/Koha/Notice/Util.pm b/Koha/Notice/Util.pm new file mode 100644 index 0000000000..401a155548 --- /dev/null +++ b/Koha/Notice/Util.pm @@ -0,0 +1,109 @@ +package Koha::Notice::Util; + +# Copyright Rijksmuseum 2023 +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; +use Data::Dumper qw/Dumper/; + +use C4::Context; +use Koha::DateUtils qw/dt_from_string/; +use Koha::Notice::Messages; + +=head1 NAME + +Koha::Notice::Util - Utility class related to Koha notice messages + +=head1 CLASS METHODS + +=head2 load_domain_limits + + my $domain_limits = Koha::Notice::Util->load_domain_limits; + +=cut + +sub load_domain_limits { + my ( $class ) = @_; + + my $domain_limits; + my $entry = C4::Context->config('message_domain_limits'); + if( ref($entry) eq 'HASH' ) { + if( exists $entry->{domain} ) { + # Turn single hash entry into array + $domain_limits = ref($entry->{domain}) eq 'HASH' + ? [ $entry->{domain} ] + : $entry->{domain}; + # Convert to hash structure by domain name + $domain_limits = { map { lc $_->{name}, { limit => $_->{limit}, unit => $_->{unit}, count => 0 }} @$domain_limits }; + } + } + return _fill_domain_counts($domain_limits); +} + +=head2 exceeds_limit + + my $boolean = Koha::Notice::Util->exceeds_limit( $to_address, $domain_limits ); + +=cut + +sub exceeds_limit { + my ( $class, $to_address, $domain_limits ) = @_; + return 0 if !$domain_limits; + my $domain = q{}; + $domain = lc $1 if $to_address && $to_address =~ /@(.*)/; + return 0 if !exists $domain_limits->{$domain}; + return 1 if $domain_limits->{$domain}->{count} >= $domain_limits->{$domain}->{limit}; + $domain_limits->{$domain}->{count}++; + warn "Sending messages: domain $domain reached limit of ". + $domain_limits->{$domain}->{limit}. '/'. $domain_limits->{$domain}->{unit} + if $domain_limits->{$domain}->{count} == $domain_limits->{$domain}->{limit}; + return 0; +} + +=head1 PRIVATE METHODS + +=cut + +sub _fill_domain_counts { + my ( $limits ) = @_; + return $limits if !$limits; + my $dt_parser = Koha::Database->new->schema->storage->datetime_parser; + foreach my $domain ( keys %$limits ) { + my $start_dt = _convert_unit( undef, $limits->{$domain}->{unit} ); + $limits->{$domain}->{count} = Koha::Notice::Messages->search({ + message_transport_type => 'email', + status => 'sent', + to_address => { 'LIKE', '%'.$domain }, + updated_on => { '>=', $dt_parser->format_datetime($start_dt) }, # FIXME Would be nice if possible via filter_by_last_update + })->count; + } + return $limits; +} + +sub _convert_unit { # unit should be like \d+(m|h|d) + my ( $dt, $unit ) = @_; + $dt //= dt_from_string(); + if( $unit && $unit =~ /(\d+)([mhd])/ ) { + my $abbrev = { m => 'minutes', h => 'hours', d => 'days' }; + foreach my $h ( 0, 1 ) { # try hour before too when subtract fails (like: change to summertime) + eval { $dt->subtract( hours => $h )->subtract( $abbrev->{$2} => $1 ) } and last; + } + } + return $dt; +} + +1; diff --git a/t/db_dependent/Koha/Notice_Util.t b/t/db_dependent/Koha/Notice_Util.t new file mode 100755 index 0000000000..0129fe65c1 --- /dev/null +++ b/t/db_dependent/Koha/Notice_Util.t @@ -0,0 +1,109 @@ +#!/usr/bin/perl + +# Copyright 2023 Koha Development team +# +# This file is part of Koha +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; +#use Data::Dumper qw/Dumper/; +use Test::More tests => 2; +use Test::MockModule; +use Test::Warn; + +use C4::Context; +use Koha::Database; +use Koha::DateUtils qw/dt_from_string/; +use Koha::Notice::Util; + +use t::lib::TestBuilder; +use t::lib::Mocks; + +my $schema = Koha::Database->new->schema; +$schema->storage->txn_begin; + +my $builder = t::lib::TestBuilder->new; + +subtest 'load_domain_limits' => sub { + plan tests => 12; + + my $domain_limits; + t::lib::Mocks::mock_config( 'message_domain_limits', undef ); + is( Koha::Notice::Util->load_domain_limits, undef, 'koha-conf does not contain entry' ); + t::lib::Mocks::mock_config( 'message_domain_limits', q{} ); + is( Koha::Notice::Util->load_domain_limits, undef, 'koha-conf contains blank entry' ); + t::lib::Mocks::mock_config( 'message_domain_limits', { domain => { name => 'A', limit => 2, unit => '1d' } } ); + $domain_limits = Koha::Notice::Util->load_domain_limits; + is( keys %$domain_limits, 1, 'koha-conf contains one domain' ); + is( $domain_limits->{a}->{limit}, 2, 'check limit of first entry' ); + is( $domain_limits->{a}->{unit}, '1d', 'check unit of first entry' ); + t::lib::Mocks::mock_config( 'message_domain_limits', + { domain => [ { name => 'A', limit => 2, unit => '2d' }, { name => 'B', limit => 3, unit => '3h' } ] }, + ); + $domain_limits = Koha::Notice::Util->load_domain_limits; + is( keys %$domain_limits, 2, 'koha-conf contains two domains' ); + is( $domain_limits->{b}->{limit}, 3, 'check limit of second entry' ); + + # Check counting + my @values = ( message_transport_type => 'email', status => 'sent' ); + my $today = dt_from_string(); + $builder->build_object({ class => 'Koha::Notice::Messages', + value => { @values, to_address => 'a@A', updated_on => $today->clone->subtract( hours => 36 ) }}); + $builder->build_object({ class => 'Koha::Notice::Messages', + value => { @values, to_address => 'b@A', updated_on => $today->clone->subtract( hours => 49 ) }}); + $builder->build_object({ class => 'Koha::Notice::Messages', + value => { @values, to_address => 'c@A', updated_on => $today->clone->subtract( days => 3 ) }}); + $domain_limits = Koha::Notice::Util->load_domain_limits; + is( $domain_limits->{a}->{count}, 1, 'Three messages to A, 1 within unit of 2d' ); + t::lib::Mocks::mock_config( 'message_domain_limits', + { domain => [ { name => 'A', limit => 2, unit => '50h' }, { name => 'B', limit => 3, unit => '3h' } ] }, + ); + $domain_limits = Koha::Notice::Util->load_domain_limits; + is( $domain_limits->{a}->{count}, 2, 'Three messages to A, 2 within unit of 50h' ); + + # Date subtraction - edge case (start of summer time) + my $mock_context = Test::MockModule->new('C4::Context'); + $mock_context->mock( 'tz', sub { return DateTime::TimeZone->new( name => 'Europe/Amsterdam' )} ); + my $dt = dt_from_string( '2023-03-31 02:30:00', 'iso', '+02:00' ); + is( Koha::Notice::Util::_convert_unit( $dt, '4d')->stringify, '2023-03-27T02:30:00', '02:30 is fine' ); + is( Koha::Notice::Util::_convert_unit( $dt, '1d')->stringify, '2023-03-26T01:30:00', 'Moved 02:30 to 01:30' ); + # Test bad unit + is( Koha::Notice::Util::_convert_unit( $dt, 'y')->stringify, '2023-03-26T01:30:00', 'No further shift for bad unit' ); + $mock_context->unmock('tz'); +}; + +subtest 'exceeds_limit' => sub { + plan tests => 6; + + my $domain_limits; + + t::lib::Mocks::mock_config( 'message_domain_limits', undef ); + $domain_limits = Koha::Notice::Util->load_domain_limits; + is( Koha::Notice::Util->exceeds_limit( 'marcel@koha.nl', $domain_limits ), 0, 'False when having no limits' ); + + t::lib::Mocks::mock_config( 'message_domain_limits', + { domain => [ { name => 'A', limit => 0, unit => '1d' }, { name => 'B', limit => 1, unit => '5h' } ] }, + ); + $domain_limits = Koha::Notice::Util->load_domain_limits; + is( Koha::Notice::Util->exceeds_limit( '1@A', $domain_limits ), 1, 'Limit for A already reached' ); + my $result; + warning_like { $result = Koha::Notice::Util->exceeds_limit( '2@B', $domain_limits ) } + qr/Sending messages: domain b reached limit/, 'Check warn for reaching limit'; + is( $result, 0, 'Limit for B not yet exceeded' ); + is( Koha::Notice::Util->exceeds_limit( '3@B', $domain_limits ), 1, 'Limit for B already reached' ); + is( Koha::Notice::Util->exceeds_limit( '4@C', $domain_limits ), 0, 'No limits for C' ); +}; + +$schema->storage->txn_rollback; -- 2.30.2