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