File Coverage

blib/lib/Mail/Postfixadmin.pm
Criterion Covered Total %
statement 18 605 2.9
branch 0 226 0.0
condition 0 3 0.0
subroutine 6 54 11.1
pod 29 35 82.8
total 53 923 5.7


line stmt bran cond sub pod time code
1             #! /usr/bin/perl
2              
3              
4             package Mail::Postfixadmin;
5              
6 1     1   24041 use strict;
  1         2  
  1         39  
7 1     1   28 use 5.010;
  1         4  
  1         37  
8 1     1   2322 use DBI; # libdbi-perl
  1         19814  
  1         103  
9 1     1   1169 use Crypt::PasswdMD5; # libcrypt-passwdmd5-perl
  1         1233  
  1         71  
10 1     1   8 use Carp;
  1         2  
  1         86  
11 1     1   1126 use Data::Dumper;
  1         11778  
  1         8672  
12              
13             our $VERSION;
14             $VERSION = "0.20130624";
15              
16             =pod
17              
18             =head1 NAME
19              
20             Mail::Postfixadmin - Interferes with a Postfix/MySQL virtual mailbox system
21              
22             =head1 SYNOPSIS
23              
24             Mail::Postfixadmin is an attempt to provide a bunch of functions that wrap
25             around the tedious SQL involved in interfering with a Postfix/Dovecot/MySQL
26             virtual mailbox mail system.
27              
28             This is also completely not an object-orientated interface to the
29             Postfix/Dovecot mailer, since it doesn't actually represent anything sensibly
30             as objects. At best, it's an object-considering means of configuring it.
31              
32             use Mail::Postfixadmin;
33            
34             my $pfa = Mail::Postfixadmin->new();
35             $pfa->createDomain(
36             domain => 'example.org',
37             description => 'an example',
38             num_mailboxes => '0',
39             );
40            
41             $pfa->createUser(
42             username => 'avi@example.com',
43             password_plain => 'password',
44             name => 'avi',
45             );
46            
47             my %dominfo = $pfa->getDomainInfo();
48            
49             my %userinfo = $pfa->getUserInfo();
50            
51             $pfa->changePassword('avi@example.com', 'complexpass');
52              
53              
54             =head1 CONSTRUCTOR AND STARTUP
55              
56             =head2 new()
57              
58             Creates and returns a new Mail::Postfixadmin object; will parse a Postfixadmin
59             c file to get all the configuration. It will check some common
60             locations for this file (c, c) and you
61             may specify the file to parse by passing c:
62              
63             my $v = Mail::Postfixadmin->new(
64             PostfixAdminConfigFile => '/home/alice/public_html/postfixadmin/config.inc.php';
65             )
66              
67              
68             );
69              
70              
71             =cut
72              
73              
74             sub new() {
75 0     0 1   my $class = shift;
76 0           my %defaults = (
77             );
78 0           my %params = @_;
79 0           my %conf = (%defaults, %params);
80 0           my $self = {};
81              
82 0           my %_tables = _tables();
83 0           $self->{'_tables'} = _tables();
84 0           $self->{'_fields'} = _fields();
85 0           $self->{'_postfixAdminConfig'} = _parsePostfixAdminConfigFile($conf{'postfixAdminConfigFile'});
86              
87             #As much config as possible comes from PostfixAdmin's config file:
88 0           foreach(qw/database_password database_host database_prefix database_name database_type database_user/){
89 0 0         $conf{$_} = $self->{'_postfixAdminConfig'}->{$_} unless exists($conf{$_});
90             }
91              
92 0           $self->{'_dbi'} = _createDBI(\%conf);
93              
94 0           bless($self,$class);
95 0           return $self;
96             }
97              
98              
99              
100             =head1 METHODS
101              
102             =head3 getDomains()
103              
104             Returns an array of domains on the system. This is all domains for
105             which the system will accept mail, including aliases.
106              
107             Accepts a pattern as an argument, which causes it to return only
108             domains whose names match that pattern:
109              
110             @domains = $getDomains('com$');
111              
112             =cut
113              
114             sub getDomains(){
115 0     0 1   my $self = shift;
116 0           my $regex = shift;
117 0           my @results;
118 0           @results = $self->_dbSelect(
119             table => 'domain',
120             fields => [ "domain" ],
121             );
122 0 0         if($regex){
123 0           @results = grep (/$regex/, @results);
124             }
125 0           my @domains = map ($_->{'domain'}, @results);
126 0           return @domains;
127             }
128              
129             =head3 getDomainsAndAliases()
130              
131             Returns a hash describing all domains on the system. Keys are domain names
132             and values are the domain for which the key is an alias, where appropirate.
133              
134             As with getDomains, accepts a regex pattern as an argument.
135              
136             %domains = getDomainsAndAliases('org$');
137             foreach(keys(%domains)){
138             if($domains{$_} =~ /.+/){
139             print "$_ is an alias of $domains{$_}\n";
140             }else{
141             print "$_ is a real domain\n";
142             }
143             }
144              
145             =cut
146              
147             sub getDomainsAndAliases(){
148 0     0 1   my $self = shift;
149 0           my $regex = shift;
150 0           my @domains = $self->getDomains($regex);
151             # prepend a null string so that we definitely get a domain every odd-
152             # numbered element of the list map returns, else the hash looks a bit
153             # weird
154 0           my %domainsWithAliases = map {$_ => "".$self->getAliasDomainTarget($_)} @domains;
  0            
155 0           return %domainsWithAliases;
156             }
157              
158             =head3 getUsers()
159              
160             Returns a list of all users. If a domain is passed, only returns users on that domain.
161              
162             @users = getUsers('example.org');
163              
164             =cut
165              
166             sub getUsers(){
167 0     0 1   my $self = shift;
168 0           my $domain = shift;
169 0           my (@users,@aliases);
170 0           @users = $self->getRealUsers($domain), $self->getAliasUsers($domain);
171 0           return @users;
172             }
173              
174             =head3 getUsersAndAliases()
175              
176             Returns a hash describing all users on the system. Keys are users and values are
177             their targets.
178              
179             as with C, accepts a pattern to match.
180              
181             %users = getUsersAndAliases('example.org');
182             foreach(keys(%users)){
183             if($users{$_} =~ /.+/){
184             print "$_ is an alias of $users{$_}\n";
185             }else{
186             print "$_ is a real mailbox\n";
187             }
188             }
189             =cut
190              
191             sub getUsersAndAliases(){
192 0     0 1   my $self = shift;
193 0           my $regex = shift;
194 0           my @users = $self->getUsers($regex);
195             # prepend a zero-length string so that we definitely have a domain at
196             # every odd-numbered element returned by the map else the hash looks a bit
197             # weird
198 0           my %usersWithAliases = map {$_ => "".$self->getAliasUserTarget($_)} @users;
  0            
199 0           return %usersWithAliases;
200             }
201              
202             =head3 getRealUsers()
203              
204             Returns a list of real users (i.e. those that are not aliases). If a domain is
205             passed, returns only users on that domain, else returns a list of all real
206             users on the system.
207              
208             @realUsers = getRealUsers('example.org');
209              
210             =cut
211              
212             sub getRealUsers(){
213 0     0 1   my $self = shift;
214 0           my $domain = shift;
215 0           my $query;
216             my @results;
217 0 0         if ($domain =~ /.+/){
218 0           @results = $self->_dbSelect(
219             table => 'mailbox',
220             fields => [ 'username' ],
221             equals => [ 'domain', $domain],
222             );
223             }else{
224 0           @results = $self->_dbSelect(
225             table => 'alias',
226             fields => [ 'address' ],
227             equals => [ 'goto', ''],
228             );
229             }
230 0           my @users;
231 0           @users = map ($_->{'username'}, @results);
232 0           return @users;
233             }
234              
235             =head3 getAliasUsers()
236              
237             Returns a list of alias users on the system or, if a domain is passed as an argument,
238             the domain.
239              
240             my @aliasUsers = $p->getAliasUsers('example.org');
241              
242             =cut
243              
244             #TODO: getAliasUsers to return a hash of Alias=>Target
245              
246             sub getAliasUsers() {
247 0     0 1   my $self = shift;
248 0           my $domain = shift;
249 0           my @results;
250 0 0         if ( $domain ){
251 0           my $like = '%'.$domain;
252 0           @results = $self->_dbSelect(
253             table => 'alias',
254             fields => ['address'],
255             like => [ 'goto' , $like ] ,
256             );
257             }else{
258 0           @results = $self->_dbSelect(
259             table => 'alias',
260             fields => ['address'],
261             );
262             }
263 0           my @aliases = map ($_->{'address'}, @results);
264 0           return @aliases;
265             }
266              
267             =head3 domainExists()
268              
269             Check for the existence of a domain. Returns the number found with that name if
270             positive, undef if none are found.
271              
272             if($p->$domainExists('example.org')){
273             print "example.org exists!\n";
274             }
275              
276             =cut
277              
278             sub domainExists(){
279 0     0 1   my $self = shift;
280 0           my $domain = shift;
281 0           my $regex = shift;
282 0 0         if ($domain eq ''){
283 0           _error("No domain passed to domainExists");
284             }
285 0 0         if($self->domainIsAlias($domain) > 0){
286 0           return $self->domainIsAlias($domain);
287             }
288 0           my $query = "select count(*) from $self->{'_tables'}->{domain} where $self->{'_fields'}->{domain}->{domain} = \'$domain\'";
289 0           my $sth = $self->{'_dbi'}->prepare($query);
290 0           $sth->execute;
291 0           my $count = ($sth->fetchrow_array())[0];
292 0           $self->{infostr} = $query;
293 0 0         if ($count > 0){
294 0           return $count;
295             }else{
296 0           return;
297             }
298             }
299              
300             =head3 userExists()
301              
302             Check for the existence of a user. Returns the number found with that name if
303             positive, undef if none are found.
304              
305             if($p->userExists('user@example.com')){
306             print "user@example.com exists!\n";
307             }
308              
309              
310             =cut
311              
312             sub userExists(){
313 0     0 1   my $self = shift;
314 0           my $user = shift;
315              
316 0 0         if ($user eq ''){
317 0           _error("No username passed to userExists");
318             }
319              
320 0 0         if ($self->userIsAlias($user)){
321 0           return $self->userIsAlias($user);
322             }
323 0           my $query = "select count(*) from $self->{'_tables'}->{mailbox} where $self->{'_fields'}->{mailbox}->{username} = '$user'";
324 0           my $sth = $self->{'_dbi'}->prepare($query);
325 0           $sth->execute;
326 0           my $count = ($sth->fetchrow_array())[0];
327 0           $self->{infostr} = $query;
328 0 0         if ($count > 0){
329 0           return $count;
330             }else{
331 0           return;
332             }
333             }
334              
335             =head3 domainIsAlias()
336              
337             Check whether a domain is an alias. Returns the number of 'targets' a domain has if
338             that's a positive number, else undef.
339              
340             if($p->domainIsAlias('example.net'){
341             print 'Mail for example.net is forwarded to ". getAliasDomainTarget('example.net');
342             }
343              
344             =cut
345              
346             sub domainIsAlias(){
347 0     0 1   my $self = shift;
348 0           my $domain = shift;
349              
350 0 0         _error("No domain passed to domainIsAlias") if $domain eq '';
351              
352 0           my $query = "select count(*) from $self->{'_tables'}->{alias_domain} where $self->{'_fields'}->{alias_domain}->{alias_domain} = '$domain'";
353 0           my $sth = $self->{'_dbi'}->prepare($query);
354 0           $sth->execute;
355 0           my $count = ($sth->fetchrow_array())[0];
356 0           $self->{infostr} = $query;
357 0 0         if ($count > 0){
358 0           return $count;
359             }else{
360 0           return;
361             }
362             }
363              
364             =head3 getAliasDomainTarget()
365              
366             Returns the target of a domain if it's an alias, undef otherwise.
367              
368             if($p->domainIsAlias('example.net'){
369             print 'Mail for example.net is forwarded to ". getAliasDomainTarget('example.net');
370             }
371              
372             =cut
373              
374             sub getAliasDomainTarget(){
375 0     0 1   my $self = shift;
376 0           my $domain = shift;
377 0 0         if ($domain eq ''){
378 0           _error("No domain passed to getAliasDomainTarget");
379             }
380 0 0         unless ( $self->domainIsAlias($domain) ){
381 0           return;
382             }
383 0           my @output = $self->_dbSelect(
384             table => 'alias_domain',
385             fields => [ 'target_domain' ],
386             equals => [ 'alias_domain', $domain ],
387             );
388 0           my %result = %{$output[0]};
  0            
389 0           return $result{'target_domain'};
390             }
391            
392              
393             =head3 userIsAlias()
394              
395             Checks whether a user is an alias to another address.
396              
397             if($p->userIsAlias('user@example.net'){
398             print 'Mail for user@example.net is forwarded to ". getAliasUserTarget('user@example.net');
399             }
400              
401             =cut
402              
403             sub userIsAlias{
404 0     0 1   my $self = shift;
405 0           my $user = shift;
406 0 0         if ($user eq ''){ _error("No user passed to userIsAlias");}
  0            
407 0           my $query = "select count(*) from $self->{'_tables'}->{alias} where $self->{'_fields'}->{alias}->{address} = '$user'";
408 0           my $sth = $self->{'_dbi'}->prepare($query);
409 0           $sth->execute;
410 0           my $count = ($sth->fetchrow_array())[0];
411 0           $self->{infostr} = $query;
412 0 0         if ($count > 0){
413 0           return $count;
414             }else{
415 0           return;
416             }
417             }
418              
419             =head3 getAliasUserTargets()
420              
421             Returns an array of addresses for which the current user is an alias.
422            
423             my @targets = $p->getAliasUserTargets($user);
424              
425             if($p->domainIsAlias('user@example.net'){
426             print 'Mail for example.net is forwarded to ". join(", ", getAliasDomainTarget('user@example.net'));
427             }
428              
429              
430             =cut
431              
432             sub getAliasUserTargets{
433 0     0 1   my $self = shift;
434 0           my $user = shift;
435 0 0         if ($user eq ''){ _error("No user passed to getAliasUserTargetArray");}
  0            
436              
437 0           my @gotos = $self->_dbSelect(
438             table => 'alias',
439             fields => ['goto'],
440             equals => [ 'address', $user ],
441             );
442 0           return split(/,/, $gotos[0]->{'goto'});
443             }
444              
445             =head3 getUserInfo()
446              
447             Returns a hash containing info about the user:
448              
449             username Username. Should be an email address.
450             password The crypted password of the user
451             name The human name associated with the username
452             domain The domain the user is associated with
453             local_part The local part of the email address
454             maildir The path to the maildir *relative to the maildir root
455             configured in Postfix/Dovecot*
456             active Whether or not the user is active
457             created Creation date
458             modified Last modified data
459              
460             Returns undef if the user doesn't exist.
461              
462             =cut
463              
464             sub getUserInfo(){
465 0     0 1   my $self = shift;
466 0           my $user = shift;
467 0 0         _error("No user passed to getUserInfo") if $user eq '';
468 0 0         return unless $self->userExists($user);
469 0           my %userinfo;
470 0           my @results = $self->_dbSelect(
471             table => 'mailbox',
472             fields => ['*'],
473             equals => ['username', $user]
474             );
475 0           return $results[0];
476             }
477              
478             =head3 getDomainInfo()
479              
480             Returns a hash containing info about a domain. Keys:
481              
482             domain The domain name
483             description Content of the description field
484             quota Mailbox size quota
485             transport Postfix transport (usually 'virtua')
486             active Whether the domain is active or not (0 or 1)
487             backupmx Whether this is a backup MX for the domain (0 or 1)
488             mailboxes Array of mailbox names associated with the domain
489             (note: the full username, not just the local part)
490             modified last modified date as returned by the DB
491             num_mailboxes Count of the mailboxes (effectively, the length of the
492             array in `mailboxes`)
493             created Creation date
494             aliases Alias quota for the domain
495             maxquota Mailbox quota for the domain
496              
497             Returns undef if the domain doesn't exist.
498              
499             =cut
500              
501             sub getDomainInfo(){
502 0     0 1   my $self = shift;
503 0           my $domain = shift;
504              
505 0 0         _error("No domain passed to getDomainInfo") if $domain eq '';
506 0 0         return unless $self->domainExists($domain);
507              
508 0           my $query = "select * from `$self->{'_tables'}->{domain}` where $self->{'_fields'}->{domain}->{domain} = '$domain'";
509 0           my $domaininfo = $self->{'_dbi'}->selectrow_hashref($query);
510            
511             # This is exactly the same data acrobatics as getUserInfo() above, to get consistent
512             # output:
513 0           my %return;
514 0           my %domainhash = %{$self->{'_fields'}->{domain}};
  0            
515 0           my ($k,$v);
516 0           while ( ($k,$v) = each ( %{$self->{'_fields'}->{domain}} ) ){
  0            
517 0           my $myname = $k;
518 0           my $theirname = $v;
519 0           my $info = $$domaininfo{$theirname};
520 0           $return{$myname} = $info;
521             }
522 0           $self->{infostr} = $query;
523 0           $query = "select username from `$self->{'_tables'}->{mailbox}` where $self->{'_fields'}->{mailbox}->{domain} = '$domain'";
524 0           $self->{infostr}.=";".$query;
525 0           my $sth = $self->{'_dbi'}->prepare($query);
526 0           $sth->execute;
527 0           my @mailboxes;
528 0           while (my @rows = $sth->fetchrow()){
529 0           push(@mailboxes,$rows[0]);
530             }
531            
532 0           $return{mailboxes} = \@mailboxes;
533 0           $return{num_mailboxes} = scalar @mailboxes;
534            
535 0           return %return;
536             }
537              
538             =head2 Passwords
539              
540             =head3 cryptPassword()
541              
542             This probably has no real use, except for where other functions use it, but
543             it will always be the currently-favoured Dovecot encrytion scheme. Takes the
544             cleartext as its argument, returns the crypt.
545              
546             =cut
547              
548             sub cryptPassword(){
549 0     0 1   my $self = shift;
550 0           my $password = shift;
551 0           my $cryptedPassword = Crypt::PasswdMD5::unix_md5_crypt($password);
552 0           return $cryptedPassword;
553             }
554              
555             =head3 changePassword()
556              
557             Changes the password of a user. Expects two arguments, a username and a new
558             password:
559              
560             $p->changePassword("user@domain.com", "password");
561              
562             The salt is picked at pseudo-random; successive runs will (should) produce
563             different results.
564              
565             =cut
566              
567             sub changePassword(){
568 0     0 1   my $self = shift;
569 0           my $user = shift;
570 0           my $password = shift;
571 0 0         if ($user eq ''){
572 0           _error("No user passed to changePassword");
573             }
574 0           my $cryptedPassword = $self->cryptPassword($password);
575 0           $self->changeCryptedPassword($user,$cryptedPassword,$password);
576 0           return $cryptedPassword;
577             }
578              
579             =head3 changeCryptedPassword()
580              
581             changeCryptedPassword operates in exactly the same way as changePassword, but it
582             expects to be passed an already-encrypted password, rather than a clear text
583             one. It does no processing at all of its arguments, just writes it into the
584             database.
585              
586             =cut
587              
588             sub changeCryptedPassword(){
589 0     0 1   my $self = shift;
590 0           my $user = shift;;
591              
592 0 0         if ($user eq ''){
593 0           _error("No user passed to changeCryptedPassword");
594             }
595 0           my $cryptedPassword = shift;
596 0           my $clearPassword = shift;
597              
598 0           my $query = "update $self->{'_tables'}->{'mailbox'} set ";
599 0           $query.="`$self->{'_fields'}->{'mailbox'}->{'password'}`= '$cryptedPassword'";
600 0 0         if($self->{'storeCleartextPassword'} > 0){
601 0           $query.= ", `$self->{'_fields'}->{'mailbox'}->{'password_clear'}` = '$clearPassword'";
602             }
603 0 0         if($self->{'storeGPGPassword'} > 0){
604 0           my $gpgPassword = $self->cryptPasswordGPG($clearPassword);
605 0           $query.= ", `$self->{'_fields'}->{'mailbox'}->{'password_gpg'}` = '$gpgPassword'";
606             }
607 0           $query.="where `$self->{'_fields'}->{'mailbox'}->{'username'}` = '$user'";
608              
609 0           my $sth = $self->{'_dbi'}->prepare($query);
610 0           $sth->execute();
611              
612 0           return $cryptedPassword;
613             }
614              
615             =head2 Creating things
616              
617             =head3 createDomain()
618              
619             Expects to be passed a hash of options, with the keys being the same as those
620             output by C. None are necessary except C.
621              
622             Defaults are set as follows:
623              
624             domain None; required.
625             description ""
626             quota MySQL's default
627             transport 'virtual'
628             active 1 (active)
629             backupmx0 MySQL's default
630             modified now
631             created now
632             aliases MySQL's default
633             maxquota MySQL's default
634              
635             Defaults are only set on keys that haven't been instantiated. If you set a key
636             to an empty string, it will not be set to the default - null will be passed to
637             the DB and it may set its own default.
638              
639             On both success and failure the function will return a hash containing the
640             options used to configure the domain - you can inspect this to see which
641             defaults were used if you like.
642              
643             If the domain already exists, it will not alter it, instead it will return '2'
644             rather than a hash.
645              
646             =cut
647              
648             sub createDomain(){
649 0     0 1   my $self = shift;
650 0           my %opts = @_;
651 0           my $fields;
652             my $values;
653 0           my $domain = $opts{'domain'};
654              
655 0 0         _error("No domain passed to createDomain") if $domain !~ /.+/;
656              
657 0 0         if($domain eq ''){
658 0           _error("No domain passed to createDomain");
659             }
660              
661 0 0         if ($self->domainExists($domain)){
662 0           $self->{infostr} = "Domain '$domain' already exists";
663 0           return 2;
664             }
665              
666 0 0         $opts{modified} = $self->_mysqlNow unless exists($opts{modified});
667 0 0         $opts{created} = $self->_mysqlNow unless exists($opts{created});
668 0 0         $opts{active} = '1' unless exists($opts{active});
669 0 0         $opts{transport} = 'virtual' unless exists($opts{quota});
670 0           foreach(keys(%opts)){
671 0           $fields.= $self->{'_fields'}->{domain}->{$_}.", ";
672 0           $values.= "'$opts{$_}', ";;
673             }
674 0           $fields =~ s/, $//;
675 0           $values =~ s/, $//;
676 0           my $query = "insert into `$self->{'_tables'}->{domain}` ";
677 0           $query.= " ( $fields ) values ( $values )";
678 0           my $sth = $self->{'_dbi'}->prepare($query);
679 0           $sth->execute();
680 0           $self->{infostr} = $query;
681 0 0         if($self->domainExists($domain)){
682 0           return %opts;
683             }else{
684 0           $self->{errstr} = "Everything appeared to succeed, but the domain doesn't exist";
685 0           return;
686             }
687             }
688              
689             =head3 createUser()
690              
691             Expects to be passed a hash of options, with the keys being the same as those
692             output by C. None are necessary except C.
693              
694             If both C and are in the passed hash,
695             C will be used. If only password_plain is passed it will be
696             crypted with C and then used.
697              
698             Defaults are mostly sane where values aren't explicitly passed:
699              
700             username required; no default
701             password null
702             name null
703             maildir deduced from PostfixAdmin config.
704             quota MySQL default (normally zero, which represents infinite)
705             local_part the part of the username to the left of the first '@'
706             domain the part of the username to the right of the last '@'
707             created now
708             modified now
709             active MySQL's default
710              
711              
712             On success, returns a hash describing the user. You can inspect this to see
713             which defaults were set if you like.
714              
715             This will not alter existing users. Instead, it returns '2' rather than a hash.
716              
717             =cut
718              
719             sub createUser(){
720 0     0 1   my $self = shift;
721 0           my %opts = @_;
722 0           my $fields;
723             my $values;
724              
725 0 0         _error("no username passed to createUser") if $opts{"username"} eq '';
726            
727 0           my $user = $opts{"username"};
728              
729 0 0         if($self->userExists($user)){
730 0           $self->{infostr} = "User already exists ($user)";
731 0           return 2;
732             }
733 0 0         if($opts{password_crypt}){
    0          
734 0           $opts{password} = $opts{password_crypt};
735             }elsif($opts{password_clear}){
736 0           $opts{password} = $self->cryptPassword($opts{password_clear});
737             }
738              
739 0 0         unless(exists $opts{maildir}){
740 0           $opts{maildir} = $self->_createMailboxPath($user);
741             }
742 0 0         unless(exists $opts{local_part}){
743 0 0         if($opts{username} =~ /^(.+)\@/){
744 0           $opts{local_part} = $1;
745             }
746             }
747 0 0         unless(exists $opts{domain}){
748 0 0         if($opts{username} =~ /\@(.+)$/){
749 0           $opts{domain} = $1;
750             }
751             }
752 0 0         unless(exists $opts{created}){
753 0           $opts{created} = $self->_mysqlNow;
754             }
755 0 0         unless(exists $opts{modified}){
756 0           $opts{modified} = $self->_mysqlNow;
757             }
758 0           foreach(keys(%opts)){
759 0 0         unless( /_(clear|cryp)$/){
760 0           $fields.= $self->{'_fields'}->{mailbox}->{$_}.", ";
761 0           $values.= "'$opts{$_}', ";
762             }
763             }
764 0 0         if ($opts{username} eq ''){
765 0           _error("No user passed to createUser");
766             }
767 0           $values =~ s/, $//;
768 0           $fields =~ s/, $//;
769 0           my $query = "insert into `$self->{'_tables'}->{mailbox}` ";
770 0           $query.= " ( $fields ) values ( $values )";
771 0           my $sth = $self->{'_dbi'}->prepare($query);
772 0           $sth->execute();
773 0           $self->{infostr} = $query;
774 0           $self->createAliasUser(
775             target => $user,
776             alias => $user,
777             );
778 0 0         if($self->userExists($user)){
779 0           return %opts;
780             }else{
781 0           $self->{errstr} = "Everything appeared to succeed, but the user doesn't exist";
782 0           return;
783             }
784             }
785              
786             =head3 createAliasDomain()
787              
788             Creates an alias domain:
789              
790             $p->createAliasDomain(
791             target => 'target.com',
792             alias => 'alias.com'
793             );
794              
795             Will cause mail sent to any address at alias.com to be forwarded on to the same
796             left-hand-side at target.com
797              
798             You can pass three other keys in the hash, though only C and C
799             are required:
800             created 'created' date. Is passed verbatim to the db so should be in a
801             format it understands.
802             modified Ditto but for the modified date
803             active The status of the domain. Again, passed verbatim to the db,
804             but probably should be a '1' or a '0'.
805              
806             =cut
807              
808              
809             sub createAliasDomain {
810 0     0 1   my $self = shift;
811 0           my %opts = @_;
812 0           my $domain = $opts{'alias'};
813 0           my $target = $opts{'target'};
814              
815 0 0         _error("No alias passed to createAliasDomain") if $domain !~ /.+/;
816 0 0         _error("No target passed to createAliasDomain") if $target !~ /.+/;
817              
818 0 0         if($self->domainIsAlias($domain)){
819 0           $self->{errstr} = "Domain $domain is already an alias";
820             ##TODO: createAliasDomain return current target if the domain is already an alias
821 0           return;
822             }
823 0 0         unless($self->domainExists("domain" => $domain)){
824 0           $self->createDomain( "domain" => $domain);
825             }
826 0           my $fields = "$self->{'_fields'}->{alias_domain}->{alias_domain}, $self->{'_fields'}->{alias_domain}->{target_domain}";
827 0           my $values = " '$domain', '$opts{target}'";
828              
829 0           $fields.=", $self->{'_fields'}->{alias_domain}->{created}";
830 0 0         if(exists($opts{'created'})){
831 0           $values.=", '$opts{'created'}'";
832             }else{
833 0           $values.=", '".$self->_mysqlNow."'";
834             }
835              
836 0           $fields.=", $self->{'_fields'}->{alias_domain}->{modified}";
837 0 0         if(exists($opts{'modified'})){
838 0           $values.=", '$opts{'modified'}'";
839             }else{
840 0           $values.=", '".$self->_mysqlNow."'";
841             }
842 0 0         if(exists($opts{'active'})){
843 0           $fields.=", $self->{'_fields'}->{alias_domain}->{active}";
844 0           $values.=", '$opts{'active'}'";
845             }
846 0           my $query = "insert into $self->{'_tables'}->{alias_domain} ( $fields ) values ( $values )";
847 0           my $sth = $self->{'_dbi'}->prepare($query);
848 0           $sth->execute;
849 0 0         if($self->domainExists($domain)){
850 0           $self->{infostr} = $query;
851 0           return %opts;
852              
853             }else{
854 0           $self->{infostr} = $query;
855 0           $self->{errstr} = "Everything appeared to succeed but the domain doesn't exist";
856 0           return;
857             }
858             }
859              
860             =head3 createAliasUser()
861              
862             Creates an alias user:
863              
864             $p->createAliasUser(
865             target => 'target@example.org');
866             alias => 'alias@example.net
867             );
868              
869             will cause all mail sent to alias@example.com to be delivered to target@example.net.
870              
871             You may forward to more than one address by passing a comma-separated string:
872              
873             $p->createAliasDomain(
874             target => 'target@example.org, target@example.net',
875             alias => 'alias@example.net',
876             );
877              
878             The domain is stored separately in the db. If you pass a C key in the hash,
879             this is used. If not a regex is applied to the username ( C ). If that
880             doesn't match, it Croaks.
881              
882             You may pass three other keys in the hash, though only C and C are required:
883              
884             created 'created' date. Is passed verbatim to the db so should be in a format it understands.
885             modified Ditto but for the modified date
886             active The status of the domain. Again, passed verbatim to the db, but probably should be a '1' or a '0'.
887              
888             In full:
889              
890             $p->createAliasUser(
891             source => 'someone@example.org',
892             target => "target@example.org, target@example.net",
893             domain => 'example.org',
894             modified => $p->now;
895             created => $p->now;
896             active => 1
897             );
898              
899             On success a hash of the arguments is returned, with an addtional key: scalarTarget. This is the
900             value of C as it was actually inserted into the DB. It will either be exactly the same as
901             C if you've passed a scalar, or the array passed joined on a comma.
902              
903             =cut
904              
905              
906             sub createAliasUser {
907 0     0 1   my $self = shift;
908 0           my %opts = @_;
909 0           my $user = $opts{"alias"};
910 0 0         if ($user eq ''){
911 0           _error("No alias key in hash passed to createAliasUser");
912             }
913 0 0         unless(exists($opts{'target'})){
914 0           _error("No target key in hash passed to createAliasUser");
915             }
916             # The PFA web ui creates an alias for each user (with itself as the target)
917             # and so we must either be able to create aliases for users that already
918             # exist, or have some special case. I can't see a reason for this to be a
919             # special case so I'm removing the check, but leaving a relic of it to remind
920             # me that it did once look like a good idea.
921             # if($self->userExists($user)){
922             # _error("User $user already exists (passed as alias to createAliasUser)");
923             # }
924 0 0         if($self->userIsAlias($user)){
925 0           _error("User $user is already an alias (passed to createAliasUser)");
926             }
927 0 0         unless(exists($opts{domain})){
928 0 0         if($user =~ /\@(.+)$/){
929 0           $opts{domain} = $1;
930             }else{
931 0           _error("Error determining domain from user '$user' in createAliasUser");
932             }
933             }
934             #TODO: createAliasUser should accept an array of targets
935 0           $opts{scalarTarget} = $opts{target};
936              
937 0           my $fields = "$self->{'_fields'}->{alias}->{address}, $self->{'_fields'}->{alias}->{goto}, $self->{'_fields'}->{alias}->{domain}";
938 0           my $values = "\'$opts{alias}\', \'$opts{scalarTarget}\', \'$opts{domain}\'";
939            
940 0           $fields.=", $self->{'_fields'}->{alias_domain}->{created}";
941 0 0         if(exists($opts{'created'})){
942 0           $values.=", '$opts{'created'}'";
943             }else{
944 0           $values.=", '".$self->_mysqlNow."'";
945             }
946              
947 0           $fields.=", $self->{'_fields'}->{alias_domain}->{modified}";
948 0 0         if(exists($opts{'modified'})){
949 0           $values.=", $opts{'modified'}";
950             }else{
951 0           $values.=", '".$self->_mysqlNow."'";
952             }
953              
954 0 0         if(exists($opts{'active'})){
955 0           $fields.=", $self->{'_fields'}->{alias_domain}->{active}";
956 0           $values.=", '$opts{'active'}'";
957             }
958 0           my $query = "insert into $self->{'_tables'}->{alias} ( $fields ) values ( $values )";
959 0           my $sth = $self->{'_dbi'}->prepare($query);
960 0           $sth->execute;
961            
962 0 0         if($self->userIsAlias($user)){
963 0           return %opts;
964             }else{
965 0           return;
966             }
967              
968             }
969              
970             =head2 Deleting things
971              
972             =head3 removeUser();
973              
974             Removes the passed user;
975              
976             Returns 1 on successful removal of a user, 2 if the user didn't exist to start with.
977              
978             =cut
979              
980             ##Todo: Accept a hash of field=>MySQL regex with which to define users to delete
981             sub removeUser(){
982 0     0 1   my $self = shift;
983 0           my $user = shift;
984 0 0         if($user eq ''){
985 0           _error("No user passed to removeUser");
986             }
987 0 0         if (!$self->userExists($user)){
988 0           $self->{infostr} = "User doesn't exist ($user) ";
989 0           return 2;
990             }
991 0           my $query = "delete from $self->{'_tables'}->{mailbox} where $self->{'_fields'}->{mailbox}->{username} = '$user'";
992 0           my $sth = $self->{'_dbi'}->prepare($query);
993 0           $sth->execute();
994 0           $self->{infostr} = $query;
995 0           $self->removeAliasUser($user);
996 0 0         if ($self->userExists($user)){
997 0           $self->{errstr} = "Everything appeared successful but user $user still exists";
998 0           return;
999             }else{
1000 0           return 1;
1001             }
1002             }
1003            
1004              
1005             =head3 removeDomain()
1006              
1007             Removes the passed domain, and all of its attached users (using C on each).
1008              
1009             Returns 1 on successful removal of a user, 2 if the user didn't exist to start with.
1010              
1011             =cut
1012              
1013             sub removeDomain(){
1014 0     0 1   my $self = shift;
1015 0           my $domain = shift;
1016 0 0         _error("No domain passed to removeDomain") if $domain eq '';
1017            
1018 0 0         unless ($self->domainExists($domain) > 0){
1019 0           $self->{errstr} = "Domain doesn't exist";
1020 0           return 2;
1021             }
1022 0           my @users = $self->getUsers($domain);
1023 0           foreach my $user (@users){
1024 0           $self->removeUser($user);
1025             }
1026 0 0         if($self->domainIsAlias($domain)){
1027 0           $self->removeAliasDomain($domain);
1028             }
1029 0           my $query = "delete from $self->{'_tables'}->{domain} where $self->{'_fields'}->{domain}->{domain} = '$domain'";
1030 0           my $sth = $self->{'_dbi'}->prepare($query);
1031 0           $sth->execute;
1032 0 0         if ($self->domainExists($domain)){
1033 0           $self->{errstr} = "Everything appeared successful but domain $domain still exists";
1034 0           $self->{infostr} = $query;
1035 0           return;
1036             }else{
1037 0           $self->{infostr} = $query;
1038 0           return 2;
1039             }
1040              
1041             }
1042              
1043             =head3 removeAliasDomain()
1044              
1045             Removes the alias property of a domain. An alias domain is just a normal domain which happens to be listed
1046             in a table matching it with a target. This simply removes that row out of that table; you probably want
1047             C if you want to neatly remove an alias domain.
1048              
1049             =cut
1050              
1051             sub removeAliasDomain{
1052 0     0 1   my $self = shift;
1053 0           my $domain = shift;
1054 0 0         if ($domain eq ''){
1055 0           _error("No domain passed to removeAliasDomain");
1056             }
1057 0 0         if ( !$self->domainIsAlias($domain) ){
1058 0           $self->{infostr} = "Domain is not an alias ($domain)";
1059 0           return 3;
1060             }
1061 0           my $query = "delete from $self->{'_tables'}->{alias_domain} where $self->{'_fields'}->{alias_domain}->{alias_domain} = '$domain'";
1062 0           my $sth = $self->{'_dbi'}->prepare($query);
1063 0           $sth->execute;
1064             }
1065              
1066             =head3 removeAliasUser()
1067              
1068             Removes the alias property of a user. An alias user is just a normal user which happens to be listed
1069             in a table matching it with a target. This simply removes that row out of that table; you probably want
1070             C if you want to neatly remove an alias user.
1071              
1072             =cut
1073             sub removeAliasUser{
1074 0     0 1   my $self = shift;
1075 0           my $user = shift;
1076 0 0         if ($user eq ''){
1077 0           _error("No user passed to removeAliasUser");
1078             }
1079 0 0         if (!$self->userIsAlias($user)){
1080 0           $self->{infoStr} = "user is not an alias ($user)";
1081 0           return 3;
1082             }
1083 0           my $query = "delete from $self->{'_tables'}->{alias} where $self->{'_fields'}->{alias}->{address} = '$user'";
1084 0           my $sth = $self->{'_dbi'}->prepare($query);
1085 0           $sth->execute;
1086 0           return 1;
1087             }
1088              
1089             =head2 Admin Users
1090              
1091             =head3 getAdminUsers()
1092              
1093             Returns a hash describing admin users, with usernames as the keys, and
1094             an arrayref of domains as values. Accepts a a domain as an optional
1095             argument, when that is supplied will only return users who are admins
1096             of that domain, and each user's array will be a single value (that domain).
1097              
1098             my %admins = $pfa->getAdminUsers();
1099             foreach my $username (keys(%admins)){
1100             print "$username is an admin of ", join(" ", @{$admins{$username}}), "\n";
1101             }
1102              
1103             =cut
1104              
1105             sub getAdminUsers {
1106 0     0 1   my $self = shift;
1107 0           my $domain = shift;
1108 0           my $query;
1109             my @results;
1110 0 0         if ($domain =~ /.+/){
1111 0           @results = $self->_dbSelect(
1112             table => 'domain_admins',
1113             fields => [ 'username', 'domain' ],
1114             equals => [
1115             ['domain', $domain],
1116             ['domain', 'ALL'],
1117             ],
1118             equals_andor => 'or',
1119             );
1120             }else{
1121 0           @results = $self->_dbSelect(
1122             table => 'domain_admins',
1123             fields => [ 'username', 'domain' ],
1124             );
1125             }
1126 0           my %return;
1127 0           foreach(@results){
1128 0 0         if($_->{'domain'} =~ /^ALL$/){
1129 0           foreach my $domain ($self->getDomains()){
1130 0 0         push(@{$return{$_->{'username'}}}, $domain) unless $domain =~ /^ALL$/;
  0            
1131             }
1132             }else{
1133 0           push(@{$return{$_->{'username'}}}, $_->{'domain'});
  0            
1134             }
1135             }
1136 0           return %return;
1137             }
1138              
1139             =head3 createAdminUser()
1140              
1141             Creates an admin user:
1142              
1143             $pfa->createAdminUser(
1144             username => 'someone@somedomain.net',
1145             domains => [ "example.net", "example.com", "example.mil" ],
1146             password_clear => 'password',
1147             );
1148              
1149             Alternatively, create an admin of a single domain:
1150              
1151             $pfa->createAdminUser(
1152             username => 'someone@somedomain.net',
1153             domain => 'example.org',
1154             password_clear => 'password',
1155             );
1156              
1157             If domain is set to 'ALL' then the user is set as an admin of all domains.
1158              
1159             Creating an admin user involves both adding a username and password to the admin
1160             table, and then a domain/user pairing to domain_admins.
1161             The former is only attempted if you pass a password to this function; calling this
1162             with only a username and a domain simply adds that pair to the domain_admin table.
1163              
1164             If you call this with a password and a username that already exists, the row in the
1165             admin table will remain unchanged, and a warning will be raised. The user/domain
1166             pairing will still be written to the domain_admins table.
1167              
1168             =cut
1169              
1170             sub createAdminUser{
1171 0     0 1   my $self = shift;
1172 0           my %opts = @_;
1173 0 0         _error("No username passed to createAdminUser") unless $opts{'username'};
1174 0 0         _error("No domain passed to createAdminUser") unless $opts{'domain'};
1175 0 0         if($opts{'password_crypt'}){
    0          
1176 0           $opts{'password'} = $opts{'password_crypt'};
1177             }elsif($opts{'password_clear'}){
1178 0           $opts{'password'} = $self->cryptPassword($opts{'password_clear'});
1179             }
1180            
1181 0           my @domains;
1182 0 0         if(exists($opts{'domains'})){
1183 0           @domains = @{$opts{'domains'}};
  0            
1184             };
1185 0 0         if(exists($opts{'domain'})){
1186 0           push(@domains, $opts{'domain'});
1187             }
1188             # Only insert a username and password if there's not already
1189             # that username:
1190 0 0         if($opts{'password'}){
1191 0           my @usernameIsAlreadyAdmin = $self->_dbSelect(
1192             table => 'admin',
1193             count => 1,
1194             equals => [ 'username', $opts{'username'} ],
1195             ) ;
1196            
1197             # say "============================";
1198             # say Dumper(@usernameIsAlreadyAdmin);
1199             # say "============================";
1200 0 0         if(@usernameIsAlreadyAdmin[0] > 0){
1201 0           $self->_warn("Admin '$opts{'username'}' already exists; not adding to admin table");
1202             }else{
1203 0           $self->_dbInsert(
1204             data => {
1205             username => $opts{'username'},
1206             password => $opts{'password'},
1207             },
1208             table => 'admin',
1209             );
1210             }
1211             }
1212 0           foreach my $domain(@domains){
1213 0           $self->_dbInsert(
1214             data => {
1215             username => $opts{'username'},
1216             domain => $domain,
1217             },
1218             table => 'domain_admins'
1219             )
1220             }
1221             }
1222              
1223             # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
1224             # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
1225             # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
1226              
1227             #=head2 Utilities
1228             #
1229             #=head3 generatePassword()
1230             #
1231             #Generates a password. It's what all the internal things that offer to
1232             #generate passwords use.
1233             #
1234             #=cut
1235              
1236             sub generatePassword() {
1237 0     0 0   my $self = shift;
1238 0           my $length = shift;
1239 0 0         _error("generatePassword() called with no arguments (length required)") if $length =~ /^$/;
1240 0 0         _error("generatePassword() called with non-numeric argument (length expected)") if $length !~ /^\s*\d+\.?\d*\s*$/;
1241 0           my @characters = qw/a b c d e f g h i j k l m n o p q r s t u v w x y z
1242             A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
1243             1 2 3 4 5 6 7 8 9 0 - =
1244             ! " $ % ^ & * ( ) _ +
1245             [ ] ; # , . : @ ~ < > ?
1246             /;
1247 0           my $password;
1248 0           for( my $i = 0; $i<$length; $i++ ){
1249 0           $password .= $characters[rand($#characters)];
1250             }
1251 0           return $password;
1252             }
1253             #=head3 getOptions()
1254             #
1255             #Returns a hash of the options passed to the constructor plus whatever defaults
1256             #were set, in the form that the constructor expects.
1257             #
1258             #=cut
1259              
1260             sub getOptions{
1261 0     0 0   my $self = shift;
1262 0           my %params = %{$self->{_params}};
  0            
1263 0           return %params;
1264             }
1265             #=head3 getTables getFields setTables setFields
1266             #
1267             #Cters return a hash defining the table and field names respectively, the
1268             #Cters accept hashes in the same format for redefining the table layout.
1269             #
1270             #Note that this is a representation of what the object assumes the db to be -
1271             #there's no guessing at all as to what shape the db is so you'll need to tell
1272             #the object through these if you want to change them.
1273             #
1274             #=cut
1275              
1276             sub getTables(){
1277 0     0 0   my $self = shift;
1278 0           return $self->{'_tables'}
1279             }
1280             sub getFields(){
1281 0     0 0   my $self = shift;
1282 0           return $self->{'_fields'}
1283             }
1284              
1285             sub setTables(){
1286 0     0 0   my $self = shift;
1287 0           $self->{'_tables'} = @_;
1288 0           return $self->{'_tables'};
1289             }
1290              
1291             sub setFields(){
1292 0     0 0   my $self = shift;
1293 0           $self->{'_fields'} = @_;
1294 0           return $self->{'_fields'};
1295             }
1296              
1297              
1298             =head3 version()
1299              
1300             Returns the version string
1301              
1302             =cut;
1303              
1304             sub version{
1305 0     0 1   my $self = shift;
1306 0           return $VERSION
1307             }
1308              
1309              
1310             =head2 Private Methods
1311              
1312             If you use these and they eat your cat feel free to tell me, but don't expect me to fix it.
1313              
1314             =head3 _createMailboxPath()
1315              
1316             Deals with the 'mailboxes' bit of the config, the 'canonical' version of which can be found
1317             about halfway down the create-mailbox.php shipped with Postfixadmin:
1318              
1319             // Mailboxes
1320             // If you want to store the mailboxes per domain set this to 'YES'.
1321             // Examples:
1322             // YES: /usr/local/virtual/domain.tld/username@domain.tld
1323             // NO: /usr/local/virtual/username@domain.tld
1324             $CONF['domain_path'] = 'YES';
1325             // If you don't want to have the domain in your mailbox set this to 'NO'.
1326             // Examples:
1327             // YES: /usr/local/virtual/domain.tld/username@domain.tld
1328             // NO: /usr/local/virtual/domain.tld/username
1329             // Note: If $CONF['domain_path'] is set to NO, this setting will be forced to YES.
1330             $CONF['domain_in_mailbox'] = 'NO';
1331             // If you want to define your own function to generate a maildir path set this to the name of the function.
1332             // Notes:
1333             // - this configuration directive will override both domain_path and domain_in_mailbox
1334             // - the maildir_name_hook() function example is present below, commented out
1335             // - if the function does not exist the program will default to the above domain_path and domain_in_mailbox settings
1336             $CONF['maildir_name_hook'] = 'NO';
1337              
1338             "/usr/local/virtual/" is assumed to be configured in Dovecot; the path stored in
1339             the db is concatenated onto the relevant base in Dovecot's own SQL.
1340              
1341             =cut
1342              
1343             sub _createMailboxPath(){
1344 0     0     my $self = shift;
1345 0           my $mailbox = shift;
1346 0           my $p = $self->{'_postfixAdminConfig'};
1347 0           my ($user,$domain) = split('@', $mailbox);
1348 0           my $maildir;
1349              
1350 0 0 0       if(exists($p->{'maildir_name_hook'}) && ($p->{'maildir_name_hook'} !~ /NO/)){
    0          
1351 0           $self->_warn("'maildir_name_hook' not yet inplemented in Mail::Postfixadmin");
1352             }elsif($p->{'domain_path'} eq "YES"){
1353 0 0         if($p->{'domain_in_mailbox'} eq "YES"){
1354 0           $maildir = $domain."/".$mailbox."/";
1355             }else{
1356 0           $maildir = $domain."/".$user."/";
1357             }
1358             }else{
1359 0           $maildir = $mailbox;
1360             }
1361 0           return $maildir;
1362             }
1363              
1364              
1365             =head3 _findPostfixAdminConfigFile()
1366              
1367             Tries to find a PostfixAdmin config file, checks /var/www/postfixadmin/config.inc.php
1368             and /etc/phpmyadmin/config.inc.php. Called by C<_parsePostfixAdminConfigFile()> unless
1369             it's passed a path
1370              
1371             =cut
1372              
1373             sub _findPostfixAdminConfigFile{
1374 0     0     my $file = shift;
1375 0           my @candidates = qw# /var/www/postfixadmin/config.inc.php /etc/phpmyadmin/config.inc.php#;
1376 0           unshift(@candidates, $file);
1377 0           reverse(@candidates);
1378 0           foreach my $file (@candidates){
1379 0 0         return $file if -r $file;
1380             }
1381             }
1382              
1383             =head3 _parsePostfixAdminConfigFile()
1384              
1385             Returns a hash reference that's an approximation of the $CONF associative array used
1386             by PostfixAdmin for its configuration.
1387              
1388             =cut
1389             sub _parsePostfixAdminConfigFile{
1390             # my $self = shift;
1391 0     0     my $arg = shift ;
1392 0           my $file = _findPostfixAdminConfigFile($arg);
1393 0 0         _error("Couldn't find PostfixAdmin config file") unless $file;
1394 0 0         open(my $fh, "<", $file) or _warn("Error parsing PostfixAdmin config file '$file' : $!");
1395 0           my %pfaConf;
1396 0           while(<$fh>){
1397 0 0         if(/^\s*\$CONF\['([^']+)'\]\s*=\s*'?([^']*)'?\s*;\s*$/){
1398 0           $pfaConf{$1} = $2;
1399             }
1400             }
1401 0           return \%pfaConf;
1402             }
1403              
1404             =cut
1405              
1406             sub dbCanStoreCleartextPasswords(){
1407             my $self = shift;
1408             my @fields = $self->{'_dbi'}->selectrow_array("show columns from $self->{'_tables'}->{mailbox}");
1409             if (grep(/($self->{'_fields'}->{mailbox}->{password_cleartext})/, @fields)){
1410             return $1;
1411             }else{
1412             return
1413             }
1414             }
1415              
1416             #=head3 now()
1417             #
1418             #Returns the current time in a format suitable for passing straight to the database. Currently is just in MySQL
1419             #datetime format (YYYY-MM-DD HH-MM-SS).
1420             #
1421             #This shouldn't need to exist, really.
1422             #
1423             #=cut
1424              
1425             sub now{
1426             return _mysqlNow();
1427             }
1428              
1429              
1430             =head3 _tables()
1431              
1432             Returns a hashref describing the default tablee schema. The keys are the names as used in this
1433             module and the values should be the names of the tables themselves.
1434              
1435             =cut
1436              
1437             sub _tables(){
1438 0     0     my %tables = (
1439             'admin' => 'admin',
1440             'alias' => 'alias',
1441             'alias_domain' => 'alias_domain',
1442             'config' => 'config',
1443             'domain' => 'domain',
1444             'domain_admins' => 'domain_admins',
1445             'fetchmail' => 'fetchmail',
1446             'log' => 'log',
1447             'mailbox' => 'mailbox',
1448             'quota' => 'quota',
1449             'quota2' => 'quota2',
1450             'vacation' => 'vacation',
1451             'vacation_notification' => 'vacation_notification'
1452             );
1453 0           return \%tables;
1454             }
1455              
1456             =head3 _fields()
1457              
1458             Returns a hashref describing the default field names. The keys are the names as used in this
1459             module and the values should be the names of the fields themselves.
1460              
1461             =cut
1462              
1463             sub _fields(){
1464 0     0     my %fields;
1465 0           $fields{'admin'} = {
1466             'domain' => 'domain',
1467             'username' => 'username',
1468             'password' => 'password',
1469             'created' => 'created',
1470             'modified' => 'modified',
1471             'active' => 'active'
1472             };
1473 0           $fields{'alias'} = {
1474             'address' => 'address',
1475             'goto' => 'goto', # Really should have been called 'target'
1476             'domain' => 'domain',
1477             'created' => 'created',
1478             'modified' => 'modified',
1479             'active' => 'active'
1480              
1481             };
1482 0           $fields{'domain'} = {
1483             'domain' => 'domain',
1484             'description' => 'description',
1485             'aliases' => 'aliases',
1486             'mailboxes' => 'mailboxes',
1487             'maxquota' => 'maxquota',
1488             'quota' => 'quota',
1489             'transport' => 'transport',
1490             'backupmx' => 'backupmx',
1491             'created' => 'created',
1492             'modified' => 'modified',
1493             'active' => 'active'
1494             };
1495 0           $fields{'mailbox'} = {
1496             'username' => 'username',
1497             'password' => 'password',
1498             'name' => 'name',
1499             'maildir' => 'maildir',
1500             'quota' => 'quota',
1501             'local_part' => 'local_part',
1502             'domain' => 'domain',
1503             'created' => 'created',
1504             'modified' => 'modified',
1505             'active' => 'active',
1506             'password_clear'=> 'password_clear',
1507             'password_gpg' => 'password_gpg',
1508             };
1509 0           $fields{'domain_admins'} = {
1510             'domain' => 'domain',
1511             'username' => 'username'
1512             };
1513 0           $fields{'alias_domain'} = {
1514             'alias_domain' => 'alias_domain',
1515             'target_domain' => 'target_domain',
1516             'created' => 'created',
1517             'modified' => 'modified',
1518             'active' => 'active'
1519             };
1520 0           return \%fields;
1521             }
1522              
1523              
1524             =head3 _dbCanStoreCleartestPasswords()
1525              
1526             Attempts to ascertain whether the DB can store cleartext passwords. Basically
1527             checks that whatever C<_fields()> reckons is the name of the field for storing
1528             cleartext passwords in is the name of a column that exists in the db.
1529              
1530             =cut
1531              
1532             sub _dbCanStoreCleartextPasswords{
1533 0     0     my $self = shift;
1534 0           my $dbName = (split(/:/, $self->{'_params'}->{'_dbi'}))[2];
1535 0           my $tableName = $self->{'_tables'}->{'mailbox'};
1536 0           my $fieldName = $self->{'_fields'}->{'mailbox'}->{'password_clear'};
1537 0 0         if(_fieldExists($self->{'_dbi'}, $dbName, $tableName, $fieldName)){
1538 0           return;
1539             }
1540 0           return 1;
1541             }
1542              
1543             =head3 _createDBI()
1544              
1545             Creates a DBI object. Called by the constructor and passed a reference
1546             to the C<%conf> hash, containing the configuration and contructor
1547             options.
1548              
1549             =cut
1550              
1551             sub _createDBI{
1552 0     0     my $conf = shift;
1553 0           my $dataSource = "DBI:".$conf->{'database_type'}.":".$conf->{'database_name'};
1554 0           my $username = $conf->{'database_user'};
1555 0           my $password = $conf->{'database_password'};
1556 0           my $dbi = DBI->connect($dataSource, $username, $password);
1557 0 0         if (!$dbi){
1558 0           _warn("No dbi object created");
1559 0           return;
1560             }else{
1561 0           return $dbi;
1562             }
1563             }
1564              
1565             =head3 _dbInsert()
1566              
1567             A generic sub to pawn all db inserts off onto:
1568              
1569             _dbInsert(
1570             data => (
1571             field1 => value1,
1572             field2 => value2,
1573             field3 => value3,
1574             );
1575             table => 'table name',
1576             )
1577             =cut
1578              
1579             sub _dbInsert {
1580 0     0     my $self = shift;
1581 0           my %opts = @_;
1582 0 0         _error("_dbInsert called with no table name (this is probably a bug in the module)") unless $opts{'table'};
1583 0           my $table = $self->_tables->{$opts{'table'}};
1584 0 0         _error ("_dbInsert couldn't resolve passed table ($opts{'table'}) name into a proper table name") unless $table;
1585              
1586 0 0         _error("_dbInsert called with no data to insert") unless $opts{'data'};
1587              
1588 0           my(@fields, @values);
1589 0           foreach(keys(%{$opts{'data'}})){
  0            
1590 0           push(@fields, $_);
1591 0           push(@values, $opts{'data'}->{$_});
1592             }
1593              
1594 0           my $query = "insert into `$table` ";
1595 0           $query.="(`";
1596 0           $query.=join("`, `", @fields);
1597 0           $query.="`) ";
1598              
1599 0           $query.= "values (";
1600 0           foreach(@values){
1601 0           $query.="?, ";
1602             }
1603 0           $query =~ s/, $//;
1604 0           $query.=")";
1605              
1606 0           my $sth = $self->{'_dbi'}->prepare($query);
1607 0 0         $sth->execute(@values) or _error ("_dbInsert execute failed: $!\nQuery: $query");
1608 0           return $?;
1609              
1610             }
1611              
1612             =head3 _dbSelect()
1613              
1614             Hopefully, a generic sub to pawn all db lookups off onto
1615              
1616             _dbSelect(
1617             table => 'table',
1618             fields => [ field1, field2, field2],
1619             equals => ["field", "What it equals"],
1620             like => ["field", "what it's like"],
1621             orderby => 'field4 desc'
1622             count => something
1623             }
1624              
1625             If count *exists*, a count is returned. If not, it isn't. More
1626             than one pair of 'equals' may be passed by passing an array of
1627             arrays. In this case you can specify whether these are an 'and'
1628             or an 'or' with the 'equalsandor' param:
1629              
1630             _dbSelect(
1631             table => 'table',
1632             fields => ['field1', 'field2'],
1633             equals => [
1634             ['field2', "something"],
1635             ['field7', "something else"],
1636             ],
1637             equals_or => "or";
1638             );
1639             If this is set to anything other than 'or' it is an 'and' search.
1640              
1641             Returns an array of hashes, each hash representing a row from
1642             the db with keys as field names.
1643              
1644             =cut
1645              
1646             sub _dbSelect{
1647 0     0     my $self = shift;
1648 0           my %opts = @_;
1649 0           my $table = $opts{'table'};
1650 0           my @return;
1651             my @fields;
1652              
1653 0 0         if(exists($self->{'_tables'}->{$table})){
1654 0           $table = $self->{'_tables'}->{$table};
1655             }else{
1656 0           _error("Table '$table' not defined in %_tables");
1657             }
1658              
1659 0           foreach my $field (@{$opts{'fields'}}){
  0            
1660 0 0         if($field =~ /^\*$/){
1661 0           push(@fields, $field);
1662             }else{
1663 0 0         unless(exists($self->{'_fields'}->{$table}->{$field})){
1664 0           _error("Field $self->{'_fields'}->{$table}->{$field} in table $table not defined in %_fields");
1665             }
1666 0           push (@fields, $self->{'_fields'}->{$table}->{$field});
1667             }
1668             }
1669 0           my $query = "select ";
1670 0 0         if (exists($opts{count})){
1671 0           $query .= "count(*) ";
1672             }else{
1673 0           $query .= join(", ", @fields);
1674             }
1675              
1676 0           $query .= " from $table ";
1677 0 0         if ($opts{'equals'} > 0){
    0          
1678 0           $query.="where ";
1679 0           my $andor;
1680 0 0         if($opts{'equals_andor'} =~ /^or$/i){
1681 0           $andor = "or";
1682             }else{
1683 0           $andor = "and";
1684             }
1685             # We may be passed one of two things to 'equals'; an array of
1686             # two elements (element 1 must equal element 2) or an array of
1687             # arrays, each of which is of that form. Here, if we're passed a
1688             # one-dimensional array, we move it to being the first element of
1689             # a two-dimensional one:
1690 0 0         if(ref($opts{'equals'}->[0]) eq ''){
1691 0           my $equals = $opts{'equals'};
1692 0           delete($opts{'equals'});
1693 0           push(@{$opts{'equals'}}, $equals);
  0            
1694             }
1695 0           foreach my $equals (@{$opts{'equals'}}){
  0            
1696 0           my ($field,$value) = @{$equals};
  0            
1697 0 0         if (exists($self->{'_fields'}->{$table}->{$field})){
1698 0           $field = $self->{'_fields'}->{$table}->{$field};
1699             }else{
1700 0           _error("Field $field in table $table (used in SQL conditional) not defined");
1701             }
1702 0           $query .= " $field = '$value' $andor ";
1703             }
1704 0           $query =~ s/$andor $//;
1705             }elsif ($opts{'like'} > 0){
1706 0           my ($field,$value) = @{$opts{'like'}};
  0            
1707 0 0         if (exists($self->{'_fields'}->{$table}->{$field})){
1708 0           $field = $self->{'_fields'}->{$table}->{$field};
1709             }else{
1710 0           _error("Field $field in table $table (used in SQL conditional) not defined");
1711             }
1712 0           $field = $self->{'_fields'}->{$table}->{$field};
1713 0           $query .= " where $field like '$value'";
1714             }
1715 0           my $dbi = $self->{'_dbi'};
1716 0           my $sth = $self->{'_dbi'}->prepare($query);
1717 0 0         $sth->execute() or _error("execute failed: $!");
1718 0           while(my $row = $sth->fetchrow_hashref){
1719 0           push(@return, $row);
1720             }
1721 0           return @return;
1722             }
1723              
1724             #=head3 _mysqlNow()
1725             #
1726             # Returns a timestamp of its time of execution in a format ready for inserting into MySQL
1727             # (YYYY-MM-DD hh:mm:ss)
1728             #
1729             #=cut
1730              
1731             sub _mysqlNow() {
1732            
1733 0     0     my ($y,$m,$d,$hr,$mi,$se)=(localtime(time))[5,4,3,2,1,0];
1734 0           my $date = $y + 1900 ."-".sprintf("%02d",$m)."-$d";
1735 0           my $time = "$hr:$mi:$se";
1736 0           return "$date $time";
1737             }
1738              
1739              
1740             #=head3 _fieldExists()
1741             #
1742             #Checks whether a field exists in the db. Must exist in the _field hash.
1743             #
1744             #=cut
1745              
1746             sub _fieldExists() {
1747 0     0     my ($dbi,$dbName,$tableName,$fieldName) = @_;
1748 0           my $query = "select count(*) from information_schema.COLUMNS where ";
1749 0           $query.= "TABLE_SCHEMA='$dbName' and TABLE_NAME='$tableName' and ";
1750 0           $query.= "COLUMN_NAME='$fieldName'";
1751 0           my $sth = $dbi->prepare($query);
1752 0           $sth->execute;
1753 0           my $count = ($sth->fetchrow_array())[0];
1754 0 0         return($count) if ($count > 0);
1755 0           return;
1756             }
1757              
1758             #=head3 _warn() and _error()
1759             #
1760             #Handy wrappers for when I want to simply warn or spit out an error.
1761             #
1762             #=cut
1763              
1764             sub _warn{
1765 0     0     my $message = pop;
1766 0           chomp $message;
1767 0           Carp::carp($message);
1768             }
1769             sub _error{
1770 0     0     my $message = shift;
1771 0           chomp $message;
1772 0           Carp::croak($message."\n");
1773             }
1774              
1775             #=head1 CLASS VARIABLES
1776             #
1777             #=cut
1778              
1779              
1780             #=head3 dbi
1781             #
1782             #C is the dbi object used by the rest of the module, having guessed/set the appropriate credentials.
1783             #You can use it as you would the return directly from a $dbi->connect:
1784             #
1785             # my $sth = $p->{'_dbi'}->prepare($query);
1786             # $sth->execute;
1787             #
1788             #=head3 params
1789             #
1790             #C is the hash passed to the constructor, including any interpreting it does. If you've chosen to authenticate by passing
1791             #the path to a main.cf file, for example, you can use the database credentials keys (C) to initiate your
1792             #own connection to the db (though you may as well use dbi, above).
1793             #
1794             #Other variables are likely to be put here as I decide I'd like to use them :)
1795             #
1796             #=head1 DIAGNOSTICS
1797             #
1798             #Functions generally return:
1799             #
1800             #=over
1801             #
1802             #=item * null on failure
1803             #
1804             #=item * 1 on success
1805             #
1806             #=item * 2 where there was nothing to do (as if their job had already been performed)
1807             #
1808             #=back
1809             #
1810             #See C and C for better diagnostics.
1811             #
1812             #=head2 The DB schema
1813             #
1814             #Internally, the db schema is stored in two hashes.
1815             #
1816             #C<%_tables> is a hash storing the names of the tables. The keys are the values used internally to refer to the
1817             #tables, and the values are the names of the tables in the db.
1818             #
1819             #C<%_fields> is a hash of hashes. The 'top' hash has as keys the internal names for the tables (as found in
1820             #C), with the values being hashes representing the tables. Here, the key is the name as used internally,
1821             #and the value the names of those fields in the SQL.
1822             #
1823             #Currently, the assumptions made of the database schema are very small. We asssume four tables, 'mailbox', 'domain',
1824             #'alias' and 'alias domain':
1825             #
1826             # mysql> describe mailbox;
1827             # +------------+--------------+------+-----+---------------------+-------+
1828             # | Field | Type | Null | Key | Default | Extra |
1829             # +------------+--------------+------+-----+---------------------+-------+
1830             # | username | varchar(255) | NO | PRI | NULL | |
1831             # | password | varchar(255) | NO | | NULL | |
1832             # | name | varchar(255) | NO | | NULL | |
1833             # | maildir | varchar(255) | NO | | NULL | |
1834             # | quota | bigint(20) | NO | | 0 | |
1835             # | local_part | varchar(255) | NO | | NULL | |
1836             # | domain | varchar(255) | NO | MUL | NULL | |
1837             # | created | datetime | NO | | 0000-00-00 00:00:00 | |
1838             # | modified | datetime | NO | | 0000-00-00 00:00:00 | |
1839             # | active | tinyint(1) | NO | | 1 | |
1840             # +------------+--------------+------+-----+---------------------+-------+
1841             # 10 rows in set (0.00 sec)
1842             #
1843             # mysql> describe domain;
1844             # +-------------+--------------+------+-----+---------------------+-------+
1845             # | Field | Type | Null | Key | Default | Extra |
1846             # +-------------+--------------+------+-----+---------------------+-------+
1847             # | domain | varchar(255) | NO | PRI | NULL | |
1848             # | description | varchar(255) | NO | | NULL | |
1849             # | aliases | int(10) | NO | | 0 | |
1850             # | mailboxes | int(10) | NO | | 0 | |
1851             # | maxquota | bigint(20) | NO | | 0 | |
1852             # | quota | bigint(20) | NO | | 0 | |
1853             # | transport | varchar(255) | NO | | NULL | |
1854             # | backupmx | tinyint(1) | NO | | 0 | |
1855             # | created | datetime | NO | | 0000-00-00 00:00:00 | |
1856             # | modified | datetime | NO | | 0000-00-00 00:00:00 | |
1857             # | active | tinyint(1) | NO | | 1 | |
1858             # +-------------+--------------+------+-----+---------------------+-------+
1859             # 11 rows in set (0.00 sec)
1860             #
1861             # mysql> describe alias_domain;
1862             # +---------------+--------------+------+-----+---------------------+-------+
1863             # | Field | Type | Null | Key | Default | Extra |
1864             # +---------------+--------------+------+-----+---------------------+-------+
1865             # | alias_domain | varchar(255) | NO | PRI | NULL | |
1866             # | target_domain | varchar(255) | NO | MUL | NULL | |
1867             # | created | datetime | NO | | 0000-00-00 00:00:00 | |
1868             # | modified | datetime | NO | | 0000-00-00 00:00:00 | |
1869             # | active | tinyint(1) | NO | MUL | 1 | |
1870             # +---------------+--------------+------+-----+---------------------+-------+
1871             # 5 rows in set (0.00 sec)
1872             #
1873             # mysql> describe alias;
1874             # +----------+--------------+------+-----+---------------------+-------+
1875             # | Field | Type | Null | Key | Default | Extra |
1876             # +----------+--------------+------+-----+---------------------+-------+
1877             # | address | varchar(255) | NO | PRI | NULL | |
1878             # | goto | text | NO | | NULL | |
1879             # | domain | varchar(255) | NO | MUL | NULL | |
1880             # | created | datetime | NO | | 0000-00-00 00:00:00 | |
1881             # | modified | datetime | NO | | 0000-00-00 00:00:00 | |
1882             # | active | tinyint(1) | NO | | 1 | |
1883             # +----------+--------------+------+-----+---------------------+-------+
1884             # 6 rows in set (0.00 sec)
1885             #
1886             #And, er, that's it. If you wish to store cleartext passwords (by passing a value greater than 0 for 'storeCleartextPassword'
1887             #to the constructor) you'll need a 'password_cleartext' column on the mailbox field.
1888             #
1889             #C returns C<%_fields>, C. C and C resets them to the hash passed as an
1890             #argument. It does not merge the two hashes.
1891             #
1892             #This is the only way you should be interfering with those hashes.
1893             #
1894             #Since the module does no guesswork as to the db schema (yet), you might need to use these to get it to load
1895             #yours. Even when it does do that, it might guess wrongly.
1896              
1897              
1898              
1899             =head1 REQUIRES
1900              
1901             =over
1902              
1903             =item * Perl 5.10
1904              
1905             =item * Crypt::PasswdMD5
1906              
1907             =item * Carp
1908              
1909             =item * DBI
1910              
1911             =back
1912              
1913             Crypt::PasswdMD5 is C in Debian,
1914             DBI is C
1915              
1916             =cut
1917              
1918             1