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   1624 use Moose;
  2         4  
  2         11  
4 2     2   8349 use namespace::autoclean;
  2         5  
  2         15  
5              
6             our $VERSION = '2.01'; # 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   200 use Mail::MtPolicyd::Plugin::Result;
  2         2  
  2         39  
15              
16 2     2   484 use Time::Piece;
  2         7039  
  2         13  
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 112 my $self = shift;
39 117         258 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 4104 my ( $self, $r ) = @_;
46 31         833 my $session = $r->session;
47              
48 31 50       89 if( $self->get_uc( $session, 'enabled') eq 'off' ) {
49 0         0 return;
50             }
51              
52 31 50       675 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         53 my $metrics = $self->get_request_metrics( $r );
58              
59 31         19 foreach my $field ( @{$self->_fields} ) {
  31         810  
60 93         3057 my $key = $r->attr($field);
61 93 50 33     470 if( ! defined $key || $key =~ /^\s*$/ ) {
62 0         0 $self->log( $r, $field.' not defined in request, skipping...');
63 0         0 next;
64             }
65 93         373 $self->log( $r, 'updating accounting info for '.$field.' '.$key);
66 93         227 $self->update_accounting($field, $key, $metrics);
67             }
68              
69 31         148 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 102 my ( $self, $field ) = @_;
107 120         3141 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 28 my ( $self, $r ) = @_;
124 31         832 my $recipient_count = $r->attr('recipient_count');
125 31         827 my $size = $r->attr('size');
126 31         33 my $metrics = {};
127 31 50       45 my $rcpt_cnt = defined $recipient_count ? $recipient_count : 1;
128 31 50       57 $metrics->{'size'} = defined $size ? $size : 0;
129 31         40 $metrics->{'count'} = 1;
130 31 100       37 $metrics->{'count_rcpt'} = $rcpt_cnt ? $rcpt_cnt : 1;
131 31 100       50 $metrics->{'size_rcpt'} = $rcpt_cnt ? $size * $rcpt_cnt : $size;
132              
133 31         41 return( $metrics );
134             }
135              
136             sub update_accounting {
137 93     93 0 123 my ( $self, $field, $key, $metrics ) = @_;
138              
139 93         80 eval {
140 93         132 $self->update_accounting_row($field, $key, $metrics);
141             };
142 93 100       266 if( $@ =~ /^accounting row does not exist/ ) {
    50          
143 24         53 $self->insert_accounting_row($field, $key, $metrics);
144             } elsif( $@ ) {
145 0         0 die( $@ );
146             }
147              
148 93         163 return;
149             }
150              
151             sub insert_accounting_row {
152 24     24 0 34 my ( $self, $field, $key, $metrics ) = @_;
153 24         765 my $dbh = $self->_db_handle;
154 24         40 my $table_name = $dbh->quote_identifier( $self->get_table_name($field) );
155 24         366 my $values = {
156             'key' => $key,
157             'time' => $self->get_timekey,
158             %$metrics,
159             };
160             my $col_str = join(', ', map {
161 24         492 $dbh->quote_identifier($_)
  144         1470  
162             } keys %$values);
163             my $values_str = join(', ', map {
164 24         289 $dbh->quote($_)
  144         626  
165             } values %$values);
166              
167 24         141 my $sql = "INSERT INTO $table_name ($col_str) VALUES ($values_str)";
168 24         57 $self->execute_sql($sql);
169              
170 24         67 return;
171             }
172              
173             sub update_accounting_row {
174 93     93 0 72 my ( $self, $field, $key, $metrics ) = @_;
175 93         3024 my $dbh = $self->_db_handle;
176 93         156 my $table_name = $dbh->quote_identifier( $self->get_table_name($field) );
177 93         1485 my $where = {
178             'key' => $key,
179             'time' => $self->get_timekey,
180             };
181              
182             my $values_str = join(', ', map {
183 93         1915 $dbh->quote_identifier($_).'='.
184 372         7756 $dbh->quote_identifier($_).'+'.$dbh->quote($metrics->{$_})
185             } keys %$metrics);
186             my $where_str = join(' AND ', map {
187 93         2410 $dbh->quote_identifier($_).'='.$dbh->quote($where->{$_})
  186         1622  
188             } keys %$where );
189              
190 93         1591 my $sql = "UPDATE $table_name SET $values_str WHERE $where_str";
191 93         285 my $rows = $dbh->do($sql);
192 93 100       5608 if( $rows == 0 ) {
193 24         179 die('accounting row does not exist');
194             }
195              
196 69         220 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.01
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