File Coverage

blib/lib/Mail/MtPolicyd/Plugin/Accounting.pm
Criterion Covered Total %
statement 68 74 91.8
branch 14 20 70.0
condition 1 3 33.3
subroutine 11 11 100.0
pod 1 7 14.2
total 95 115 82.6


line stmt bran cond sub pod time code
1             package Mail::MtPolicyd::Plugin::Accounting;
2              
3 2     2   2486 use Moose;
  2         6  
  2         17  
4 2     2   13251 use namespace::autoclean;
  2         3  
  2         25  
5              
6             our $VERSION = '1.23'; # VERSION
7             # ABSTRACT: mtpolicyd plugin for accounting in sql tables
8              
9             extends 'Mail::MtPolicyd::Plugin';
10             with 'Mail::MtPolicyd::Plugin::Role::UserConfig' => {
11             'uc_attributes' => [ 'enabled' ],
12             };
13             with 'Mail::MtPolicyd::Plugin::Role::SqlUtils';
14              
15 2     2   247 use Mail::MtPolicyd::Plugin::Result;
  2         5  
  2         52  
16              
17 2     2   941 use Time::Piece;
  2         11679  
  2         17  
18              
19              
20             has 'enabled' => ( is => 'rw', isa => 'Str', default => 'on' );
21              
22             has 'fields' => ( is => 'rw', isa => 'Str', required => 1);
23             has '_fields' => ( is => 'ro', isa => 'ArrayRef', lazy => 1,
24             default => sub {
25             my $self = shift;
26             return [ split('\s*,\s*', $self->fields) ];
27             },
28             );
29              
30             has 'time_pattern' => ( is => 'rw', isa => 'Str', default => '%Y-%m');
31              
32             sub get_timekey {
33 117     117 0 176 my $self = shift;
34 117         458 return Time::Piece->new->strftime( $self->time_pattern );
35             }
36              
37             has 'table_prefix' => ( is => 'rw', isa => 'Str', default => 'acct_');
38              
39             sub run {
40 31     31 1 6260 my ( $self, $r ) = @_;
41 31         1272 my $session = $r->session;
42              
43 31 50       153 if( $self->get_uc( $session, 'enabled') eq 'off' ) {
44 0         0 return;
45             }
46              
47 31 50       1209 if( $r->is_already_done( $self->name.'-acct' ) ) {
48 0         0 $self->log( $r, 'accounting already done for this mail, skipping...');
49 0         0 return;
50             }
51              
52 31         102 my $metrics = $self->get_request_metrics( $r );
53              
54 31         137 foreach my $field ( @{$self->_fields} ) {
  31         1327  
55 93         4681 my $key = $r->attr($field);
56 93 50 33     654 if( ! defined $key || $key =~ /^\s*$/ ) {
57 0         0 $self->log( $r, $field.' not defined in request, skipping...');
58 0         0 next;
59             }
60 93         532 $self->log( $r, 'updating accounting info for '.$field.' '.$key);
61 93         303 $self->update_accounting($field, $key, $metrics);
62             }
63              
64 31         224 return;
65             }
66              
67             sub init {
68             my $self = shift;
69             $self->check_sql_tables( %{$self->_table_definitions} );
70             return;
71             }
72              
73             has '_single_table_create' => ( is => 'ro', isa => 'HashRef', lazy => 1,
74             default => sub { {
75             'mysql' => 'CREATE TABLE %TABLE_NAME% (
76             `id` int(11) NOT NULL AUTO_INCREMENT,
77             `key` VARCHAR(255) NOT NULL,
78             `time` VARCHAR(255) NOT NULL,
79             `count` INT UNSIGNED NOT NULL,
80             `count_rcpt` INT UNSIGNED NOT NULL,
81             `size` INT UNSIGNED NOT NULL,
82             `size_rcpt` INT UNSIGNED NOT NULL,
83             PRIMARY KEY (`id`),
84             UNIQUE KEY `time_key` (`key`, `time`),
85             KEY(`key`),
86             KEY(`time`)
87             ) ENGINE=MyISAM DEFAULT CHARSET=latin1',
88             'SQLite' => 'CREATE TABLE %TABLE_NAME% (
89             `id` INTEGER PRIMARY KEY AUTOINCREMENT,
90             `key` VARCHAR(255) NOT NULL,
91             `time` VARCHAR(255) NOT NULL,
92             `count` INT UNSIGNED NOT NULL,
93             `count_rcpt` INT UNSIGNED NOT NULL,
94             `size` INT UNSIGNED NOT NULL,
95             `size_rcpt` INT UNSIGNED NOT NULL
96             )',
97             } }
98             );
99              
100             sub get_table_name {
101 120     120 0 220 my ( $self, $field ) = @_;
102 120         5331 return( $self->table_prefix . $field );
103             }
104              
105             has '_table_definitions' => ( is => 'ro', isa => 'HashRef', lazy => 1,
106             default => sub {
107             my $self = shift;
108             my $tables = {};
109             foreach my $field ( @{$self->_fields} ) {
110             my $table_name = $self->get_table_name($field);
111             $tables->{$table_name} = $self->_single_table_create;
112             }
113             return $tables;
114             },
115             );
116              
117             sub get_request_metrics {
118 31     31 0 53 my ( $self, $r ) = @_;
119 31         1456 my $recipient_count = $r->attr('recipient_count');
120 31         1435 my $size = $r->attr('size');
121 31         65 my $metrics = {};
122 31 50       68 my $rcpt_cnt = defined $recipient_count ? $recipient_count : 1;
123 31 50       102 $metrics->{'size'} = defined $size ? $size : 0;
124 31         55 $metrics->{'count'} = 1;
125 31 100       72 $metrics->{'count_rcpt'} = $rcpt_cnt ? $rcpt_cnt : 1;
126 31 100       104 $metrics->{'size_rcpt'} = $rcpt_cnt ? $size * $rcpt_cnt : $size;
127              
128 31         67 return( $metrics );
129             }
130              
131             sub update_accounting {
132 93     93 0 222 my ( $self, $field, $key, $metrics ) = @_;
133              
134 93         178 eval {
135 93         213 $self->update_accounting_row($field, $key, $metrics);
136             };
137 93 100       372 if( $@ =~ /^accounting row does not exist/ ) {
    50          
138 24         69 $self->insert_accounting_row($field, $key, $metrics);
139             } elsif( $@ ) {
140 0         0 die( $@ );
141             }
142              
143 93         252 return;
144             }
145              
146             sub insert_accounting_row {
147 24     24 0 49 my ( $self, $field, $key, $metrics ) = @_;
148 24         100 my $dbh = Mail::MtPolicyd::SqlConnection->instance->dbh;
149 24         601 my $table_name = $dbh->quote_identifier( $self->get_table_name($field) );
150 24         594 my $values = {
151             'key' => $key,
152             'time' => $self->get_timekey,
153             %$metrics,
154             };
155             my $col_str = join(', ', map {
156 24         810 $dbh->quote_identifier($_)
  144         2802  
157             } keys %$values);
158             my $values_str = join(', ', map {
159 24         574 $dbh->quote($_)
  144         1311  
160             } values %$values);
161              
162 24         281 my $sql = "INSERT INTO $table_name ($col_str) VALUES ($values_str)";
163 24         115 $self->execute_sql($sql);
164              
165 24         103 return;
166             }
167              
168             sub update_accounting_row {
169 93     93 0 160 my ( $self, $field, $key, $metrics ) = @_;
170 93         432 my $dbh = Mail::MtPolicyd::SqlConnection->instance->dbh;
171 93         2271 my $table_name = $dbh->quote_identifier( $self->get_table_name($field) );
172 93         2330 my $where = {
173             'key' => $key,
174             'time' => $self->get_timekey,
175             };
176              
177             my $values_str = join(', ', map {
178 93         2992 $dbh->quote_identifier($_).'='.
179 372         14516 $dbh->quote_identifier($_).'+'.$dbh->quote($metrics->{$_})
180             } keys %$metrics);
181             my $where_str = join(' AND ', map {
182 93         4511 $dbh->quote_identifier($_).'='.$dbh->quote($where->{$_})
  186         3111  
183             } keys %$where );
184              
185 93         2794 my $sql = "UPDATE $table_name SET $values_str WHERE $where_str";
186 93         471 my $rows = $dbh->do($sql);
187 93 100       7686 if( $rows == 0 ) {
188 24         324 die('accounting row does not exist');
189             }
190              
191 69         296 return;
192             }
193              
194             __PACKAGE__->meta->make_immutable;
195              
196             1;
197              
198             __END__
199              
200             =pod
201              
202             =encoding UTF-8
203              
204             =head1 NAME
205              
206             Mail::MtPolicyd::Plugin::Accounting - mtpolicyd plugin for accounting in sql tables
207              
208             =head1 VERSION
209              
210             version 1.23
211              
212             =head1 SYNOPSIS
213              
214             <Plugin acct-clients>
215             module = "Accounting"
216             # per ip and user
217             fields = "client_address,sasl_username"
218             # statistics per month
219             time_pattern = "%Y-%m"
220             table_prefix = "acct_"
221             </Plugin>
222              
223             This will create a table acct_client_address and a table acct_sasl_username.
224              
225             If a request is received containing the field the plugin will update the row
226             in the fields table. The key is the fields value(ip or username) and the time
227             string build from the time_pattern.
228              
229             For each key the following counters are stored:
230              
231             * count
232             * count_rcpt (count per recipient)
233             * size
234             * size_rcpt (size * recipients)
235              
236             The resulting tables will look like:
237              
238             mysql> select * from acct_client_address;
239             +----+--------------+---------+-------+------------+--------+-----------+
240             | id | key | time | count | count_rcpt | size | size_rcpt |
241             +----+--------------+---------+-------+------------+--------+-----------+
242             | 1 | 192.168.0.1 | 2014-12 | 11 | 11 | 147081 | 147081 |
243             | 2 | 192.168.1.1 | 2014-12 | 1 | 1 | 13371 | 13371 |
244             | 12 | 192.168.2.1 | 2014-12 | 10 | 100 | 133710 | 1337100 |
245             ...
246              
247             =head2 PARAMETERS
248              
249             The module takes the following parameters:
250              
251             =over
252              
253             =item (uc_)enabled (default: on)
254              
255             Enable/disable this check.
256              
257             =item fields (required)
258              
259             A comma separated list of fields used for accounting.
260              
261             For each field a table will be created.
262              
263             For a list of available fields see postfix documentation:
264              
265             http://www.postfix.org/SMTPD_POLICY_README.html
266              
267             =item time_pattern (default: "%Y-%m")
268              
269             A format string for building the time key used to store counters.
270              
271             Default is to build counters on a monthly base.
272              
273             For example use:
274              
275             * "%Y-%W" for weekly
276             * "%Y-%m-%d" for daily
277              
278             See "man date" for format string sequences.
279              
280             =item table_prefix (default: "acct_")
281              
282             A prefix to add to every table.
283              
284             The table name will be the prefix + field_name.
285              
286             =back
287              
288             =head1 DESCRIPTION
289              
290             This plugin can be used to do accounting based on request fields.
291              
292             =head1 AUTHOR
293              
294             Markus Benning <ich@markusbenning.de>
295              
296             =head1 COPYRIGHT AND LICENSE
297              
298             This software is Copyright (c) 2014 by Markus Benning <ich@markusbenning.de>.
299              
300             This is free software, licensed under:
301              
302             The GNU General Public License, Version 2, June 1991
303              
304             =cut