The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

Mail::Postfixadmin - Interferes with a Postfix/MySQL virtual mailbox system

SYNOPSIS

Mail::Postfixadmin is an attempt to provide a bunch of functions that wrap around the tedious SQL involved in interfering with a Postfix/Dovecot/MySQL virtual mailbox mail system.

This is also completely not an object-orientated interface to the Postfix/Dovecot mailer, since it doesn't actually represent anything sensibly as objects. At best, it's an object-considering means of configuring it.

        use Mail::Postfixadmin;

        my $pfa = Mail::Postfixadmin->new();
        $pfa->createDomain(
                domain        => 'example.org',
                description   => 'an example',
                num_mailboxes => '0',
        );

        $pfa->createUser(
                username       => 'avi@example.com',
                password_plain => 'password',
                name           => 'avi',
        );

        my %dominfo = $pfa->getDomainInfo();

        my %userinfo = $pfa->getUserInfo();

        $pfa->changePassword('avi@example.com', 'complexpass');

CONSTRUCTOR AND STARTUP

new()

Creates and returns a new Mail::Postfixadmin object; will parse a Postfixadmin c<config.inc.php> file to get all the configuration. It will check some common locations for this file (c</var/www/postfixadmin>, c</etc/postfixadmin>) and you may specify the file to parse by passing c<postfixAdminConfigFile>:

  my $v = Mail::Postfixadmin->new(
        PostfixAdminConfigFile => '/home/alice/public_html/postfixadmin/config.inc.php';
  )

);

METHODS

getDomains()

Returns an array of domains on the system. This is all domains for which the system will accept mail, including aliases.

Accepts a pattern as an argument, which causes it to return only domains whose names match that pattern:

  @domains = $getDomains('com$');

getDomainsAndAliases()

Returns a hash describing all domains on the system. Keys are domain names and values are the domain for which the key is an alias, where appropirate.

As with getDomains, accepts a regex pattern as an argument.

  %domains = getDomainsAndAliases('org$');
  foreach(keys(%domains)){
        if($domains{$_} =~ /.+/){
                print "$_ is an alias of $domains{$_}\n";
        }else{
                print "$_ is a real domain\n";
        }
  }

getUsers()

Returns a list of all users. If a domain is passed, only returns users on that domain.

  @users = getUsers('example.org');

getUsersAndAliases()

Returns a hash describing all users on the system. Keys are users and values are their targets.

as with getUsers, accepts a pattern to match.

  %users = getUsersAndAliases('example.org');
  foreach(keys(%users)){
        if($users{$_} =~ /.+/){
                print "$_ is an alias of $users{$_}\n";
        }else{
                print "$_ is a real mailbox\n";
        }
  }

getRealUsers()

Returns a list of real users (i.e. those that are not aliases). If a domain is passed, returns only users on that domain, else returns a list of all real users on the system.

  @realUsers = getRealUsers('example.org');

getAliasUsers()

Returns a list of alias users on the system or, if a domain is passed as an argument, the domain.

  my @aliasUsers = $p->getAliasUsers('example.org');

domainExists()

Check for the existence of a domain. Returns the number found with that name if positive, undef if none are found.

  if($p->$domainExists('example.org')){
        print "example.org exists!\n";
  }     

userExists()

Check for the existence of a user. Returns the number found with that name if positive, undef if none are found.

  if($p->userExists('user@example.com')){
        print "user@example.com exists!\n";
  }

domainIsAlias()

Check whether a domain is an alias. Returns the number of 'targets' a domain has if that's a positive number, else undef.

  if($p->domainIsAlias('example.net'){
      print 'Mail for example.net is forwarded to ". getAliasDomainTarget('example.net');
  }

getAliasDomainTarget()

Returns the target of a domain if it's an alias, undef otherwise.

  if($p->domainIsAlias('example.net'){
      print 'Mail for example.net is forwarded to ". getAliasDomainTarget('example.net');
  }

userIsAlias()

Checks whether a user is an alias to another address.

  if($p->userIsAlias('user@example.net'){
      print 'Mail for user@example.net is forwarded to ". getAliasUserTarget('user@example.net');
  }

getAliasUserTargets()

Returns an array of addresses for which the current user is an alias.

 my @targets = $p->getAliasUserTargets($user);

  if($p->domainIsAlias('user@example.net'){
      print 'Mail for example.net is forwarded to ". join(", ", getAliasDomainTarget('user@example.net'));
  }

getUserInfo()

Returns a hash containing info about the user:

        username        Username. Should be an email address.
        password        The crypted password of the user
        name            The human name associated with the username
        domain          The domain the user is associated with
        local_part      The local part of the email address
        maildir         The path to the maildir *relative to the maildir root 
                        configured in Postfix/Dovecot*
        active          Whether or not the user is active
        created         Creation date
        modified        Last modified data

Returns undef if the user doesn't exist.

getDomainInfo()

Returns a hash containing info about a domain. Keys:

        domain          The domain name
        description     Content of the description field
        quota           Mailbox size quota
        transport       Postfix transport (usually virtual)
        active          Whether the domain is active or not
        backupmx0       Whether this is a  backup MX for the domain
        mailboxes       Array of mailbox names associated with the domain 
                        (note: the full username, not just the local part)
        modified        last modified date 
        num_mailboxes   Count of the mailboxes (effectively, the length of the 
                        array in `mailboxes`)
        created         Creation data
        aliases         Alias quota for the domain
        maxquota        Mailbox quota for teh domain

Returns undef if the domain doesn't exist.

Passwords

cryptPassword()

This probably has no real use, except for where other functions use it. It should let you specify a salt for the password, but doesn't yet. It expects a cleartext password as an argument, and returns the crypted sort.

cryptPasswordGPG()

Encrypts a password with GPG. Only likely to work if storeGPGPasswords is set to a non-zero value but happy to try without it.

cryptPasswordGPG()

Decrypts a password with GPG. Only likely to work if storeGPGPasswords is set to a non-zero value but happy to try without it.

changePassword()

Changes the password of a user. Expects two arguments, a username and a new password:

        $p->changePassword("user@domain.com", "password");

The salt is picked at pseudo-random; successive runs will (should) produce different results.

changeCryptedPassword()

changeCryptedPassword operates in exactly the same way as changePassword, but it expects to be passed an already-encrypted password, rather than a clear text one. It does no processing at all of its arguments, just writes it into the database.

Creating things

createDomain()

Expects to be passed a hash of options, with the keys being the same as those output by getDomainInfo(). None are necessary except domain.

Defaults are set as follows:

        domain          None; required.
        description     An empty string
        quota           MySQL's default
        transport       'virtual'
        active          1 (active)
        backupmx0       MySQL's default
        modified        now
        created         now
        aliases         MySQL's default
        maxquota        MySQL's default

Defaults are only set on keys that haven't been instantiated. If you set a key to an empty string, it will not be set to the default - null will be passed to the DB and it may set its own default.

On both success and failure the function will return a hash containing the options used to configure the domain - you can inspect this to see which defaults were used if you like.

If the domain already exists, it will not alter it, instead it will return '2' rather than a hash.

createUser()

Expects to be passed a hash of options, with the keys being the same as those output by getUserInfo(). None are necessary except username.

If both password_plain and <password_crypt> are in the passed hash, password_crypt will be used. If only password_plain is passed it will be crypted with cryptPasswd() and then used.

Defaults are mostly sane where values aren't explicitly passed:

 username       required; no default
 password       null
 name           null
 maildir        deduced from PostfixAdmin config. 
 quota          MySQL default (normally zero, which represents infinite)
 local_part     the part of the username to the left of the first '@'
 domain         the part of the username to the right of the last '@'
 created        now
 modified       now
 active         MySQL's default

On success, returns a hash describing the user. You can inspect this to see which defaults were set if you like.

This will not alter existing users. Instead, it returns '2' rather than a hash.

createAliasDomain()

Creates an alias domain:

 $p->createAliasDomain( 
        target => 'target.com',
        alias  => 'alias.com'
 );

something@target.com. Notably, it does not check that the domain is not already aliased elsewhere, so you can end up aliasing one domain to two targets which is probably not what you want.

You can pass three other keys in the hash, though only target and alias are required: created 'created' date. Is passed verbatim to the db so should be in a format it understands. modified Ditto but for the modified date active The status of the domain. Again, passed verbatim to the db, but probably should be a '1' or a '0'.

createAliasUser()

Creates an alias user:

 $p->createAliasUser( 
        target => 'target@example.org');
        alias  => 'alias@example.net
 );

will cause all mail sent to alias@example.com to be delivered to target@example.net.

You may forward to more than one address by passing a comma-separated string:

 $p->createAliasDomain( 
        target => 'target@example.org, target@example.net',
        alias  => 'alias@example.net',
 );

For some reason, the domain is stored separately in the db. If you pass a domain key in the hash, this is used. If not a regex is applied to the username ( /\@(.+)$/ ). If that doesn't match, it Croaks.

You may pass three other keys in the hash, though only target and alias are required:

 created   'created' date. Is passed verbatim to the db so should be in a format it understands.
 modified  Ditto but for the modified date
 active    The status of the domain. Again, passed verbatim to the db, but probably should be a '1' or a '0'.

In full:

 $p->createAliasUser(
                source   => 'someone@example.org',
                target   => "target@example.org, target@example.net",
                domain   => 'example.org',
will cause all mail sent to something@alias.com to be delivered to 
                modified => $p->now;
                created  => $p->now;
                active   => 1
 );

On success a hash of the arguments is returned, with an addtional key: scalarTarget. This is the value of target as it was actually inserted into the DB. It will either be exactly the same as target if you've passed a scalar, or the array passed joined on a comma.

Deleting things

removeUser();

Removes the passed user;

Returns 1 on successful removal of a user, 2 if the user didn't exist to start with.

removeDomain()

Removes the passed domain, and all of its attached users (using removeUser() on each).

Returns 1 on successful removal of a user, 2 if the user didn't exist to start with.

removeAliasDomain()

Removes the alias property of a domain. An alias domain is just a normal domain which happens to be listed in a table matching it with a target. This simply removes that row out of that table; you probably want removeDomain if you want to neatly remove an alias domain.

removeAliasUser()

Removes the alias property of a user. An alias user is just a normal user which happens to be listed in a table matching it with a target. This simply removes that row out of that table; you probably want removeUser if you want to neatly remove an alias user.

Admin Users

getAdminUsers()

Returns a hash describing admin users, with usernames as the keys, and an arrayref of domains as values. Accepts a a domain as an optional argument, when that is supplied will only return users who are admins of that domain, and each user's array will be a single value (that domain).

    my %admins = $pfa->getAdminUsers();
    foreach my $username (keys(%admins)){
        print "$username is an admin of ", join(" ", @{$admins{$username}}), "\n";
    }

createAdminUser()

Creates an admin user:

$pfa->createAdminUser( username => 'someone@somedomain.net', domains => [ "example.net", "example.com", "example.mil" ], password_clear => 'password', );

Alternatively, create an admin of a single domain:

$pfa->createAdminUser( username => 'someone@somedomain.net', domain => 'example.org', password_clear => 'password', );

If domain is set to 'ALL' then the user is set as an admin of all domains.

Creating an admin user involves both adding a username and password to the admin table, and then a domain/user pairing to domain_admins. The former is only attempted if you pass a password to this function; calling this with only a username and a domain simply adds that pair to the domain_admin table.

If you call this with a password and a username that already exists, the row in the admin table will remain unchanged, and a warning will be raised. The user/domain pairing will still be written to the domain_admins table.

Utilities

generatePassword()

Generates a password. It's what all the internal things that offer to generate passwords use.

getOptions()

Returns a hash of the options passed to the constructor plus whatever defaults were set, in the form that the constructor expects.

getTables getFields setTables setFields

getters return a hash defining the table and field names respectively, the setters accept hashes in the same format for redefining the table layout.

Note that this is a representation of what the object assumes the db to be - there's no guessing at all as to what shape the db is so you'll need to tell the object through these if you want to change them.

getdbCredentials()

Returns a hash of the db Credentials as expected by the constructor. Keys are dbi dbuser and dbpass. These are the three arguments for the DBI constructor; dbi is the full connection string (including DBI:mysql at the beginning.

version()

Returns the version string

Private Methods

If you use these and they eat your cat feel free to tell me, but don't expect me to fix it.

_createMailboxPath()

Deals with the 'mailboxes' bit of the config, the 'canonical' version of which can be found about halfway down create-mailbox.php:

  // Mailboxes
  // If you want to store the mailboxes per domain set this to 'YES'.
  // Examples:
  //   YES: /usr/local/virtual/domain.tld/username@domain.tld
  //   NO:  /usr/local/virtual/username@domain.tld
  $CONF['domain_path'] = 'YES';
  // If you don't want to have the domain in your mailbox set this to 'NO'.
  // Examples: 
  //   YES: /usr/local/virtual/domain.tld/username@domain.tld
  //   NO:  /usr/local/virtual/domain.tld/username
  // Note: If $CONF['domain_path'] is set to NO, this setting will be forced to YES.
  $CONF['domain_in_mailbox'] = 'NO';
  // If you want to define your own function to generate a maildir path set this to the name of the function.
  // Notes: 
  //   - this configuration directive will override both domain_path and domain_in_mailbox
  //   - the maildir_name_hook() function example is present below, commented out
  //   - if the function does not exist the program will default to the above domain_path and domain_in_mailbox settings
  $CONF['maildir_name_hook'] = 'NO';

"/usr/local/virtual/" is assumed to be configured in Dovecot; the path stored in the db is concatenated onto the relevant base in Dovecot's own SQL.

_findPostfixAdminConfigFile()

Tries to find a PostfixAdmin config file, checks /var/www/postfixadmin/config.inc.php and /etc/phpmyadmin/config.inc.php. Called by _parsePostfixAdminConfigFile() unless it's passed a path

_parsePostfixAdminConfigFile()

Returns a hash reference that's an approximation of the $CONF associative array used by PostfixAdmin for its configuration.

_parsePostfixConfigFile()

Postfix config files trying to find some DB credentials.

now()

Returns the current time in a format suitable for passing straight to the database. Currently is just in MySQL datetime format (YYYY-MM-DD HH-MM-SS).

This shouldn't need to exist, really.

_tables()

Returns a hashref describing the default tablee schema. The keys are the names as used in this module and the values should be the names of the tables themselves.

_fields()

Returns a hashref describing the default field names. The keys are the names as used in this module and the values should be the names of the fields themselves.

_dbCanStoreCleartestPasswords()

Attempts to ascertain whether the DB can store cleartext passwords. Basically checks that whatever _fields() reckons is the name of the field for storing cleartext passwords in is the name of a column that exists in the db.

_dbCanStoreGPGPasswords()

Attempts to ascertain whether the DB can store GPG passwords. Basically checks that whatever _fields() reckons is the name of the field for storing GPG passwords in is the name of a column that exists in the db.

_createDBI()

Creates a DBI object. Called by the constructor and passed a reference to the %conf hash, containing the configuration and contructor options.

_dbInsert()

Hopefully, a generic sub to pawn all db inserts off onto:

        _dbInsert(
                data => (
                        field1 => value1,
                        field2 => value2,
                        field3 => value3,
                );
                table  => 'table name',
        )

_dbSelect()

Hopefully, a generic sub to pawn all db lookups off onto

        _dbSelect(
                table     => 'table',
                fields    => [ field1, field2, field2],
                equals    => ["field", "What it equals"],
                like      => ["field", "what it's like"],
                orderby   => 'field4 desc'
                count     => something
        }

If count *exists*, a count is returned. If not, it isn't. More than one pair of 'equals' may be passed by passing an array of arrays. In this case you can specify whether these are an 'and' or an 'or' with the 'equalsandor' param:

        _dbSelect(
                table        => 'table',
                fields       => ['field1', 'field2'],
                equals       => [
                                        ['field2', "something"],
                                        ['field7', "something else"],
                                ],
                equals_or => "or";
        );
If this is set to anything other than 'or' it is an 'and' search.

Returns an array of hashes, each hash representing a row from the db with keys as field names.

_mysqlNow()

 Returns a timestamp of its time of execution in a format ready for inserting into MySQL
 (YYYY-MM-DD hh:mm:ss)

_fieldExists()

Checks whether a field exists in the db. Must exist in the _field hash.

_warn() and _error()

Handy wrappers for when I want to simply warn or spit out an error.

CLASS VARIABLES

dbi

dbi is the dbi object used by the rest of the module, having guessed/set the appropriate credentials. You can use it as you would the return directly from a $dbi->connect:

  my $sth = $p->{'_dbi'}->prepare($query);
  $sth->execute;

params

params is the hash passed to the constructor, including any interpreting it does. If you've chosen to authenticate by passing the path to a main.cf file, for example, you can use the database credentials keys (dbuser, dbpass and dbi) to initiate your own connection to the db (though you may as well use dbi, above).

Other variables are likely to be put here as I decide I'd like to use them :)

DIAGNOSTICS

Functions generally return:

  • null on failure

  • 1 on success

  • 2 where there was nothing to do (as if their job had already been performed)

See errstr and infostr for better diagnostics.

The DB schema

Internally, the db schema is stored in two hashes.

%_tables is a hash storing the names of the tables. The keys are the values used internally to refer to the tables, and the values are the names of the tables in the db.

%_fields is a hash of hashes. The 'top' hash has as keys the internal names for the tables (as found in getTables()), with the values being hashes representing the tables. Here, the key is the name as used internally, and the value the names of those fields in the SQL.

Currently, the assumptions made of the database schema are very small. We asssume four tables, 'mailbox', 'domain', 'alias' and 'alias domain':

 mysql> describe mailbox;
 +------------+--------------+------+-----+---------------------+-------+
 | Field      | Type         | Null | Key | Default             | Extra |
 +------------+--------------+------+-----+---------------------+-------+
 | username   | varchar(255) | NO   | PRI | NULL                |       |
 | password   | varchar(255) | NO   |     | NULL                |       |
 | name       | varchar(255) | NO   |     | NULL                |       |
 | maildir    | varchar(255) | NO   |     | NULL                |       |
 | quota      | bigint(20)   | NO   |     | 0                   |       |
 | local_part | varchar(255) | NO   |     | NULL                |       |
 | domain     | varchar(255) | NO   | MUL | NULL                |       |
 | created    | datetime     | NO   |     | 0000-00-00 00:00:00 |       |
 | modified   | datetime     | NO   |     | 0000-00-00 00:00:00 |       |
 | active     | tinyint(1)   | NO   |     | 1                   |       |
 +------------+--------------+------+-----+---------------------+-------+
 10 rows in set (0.00 sec)
   
 mysql> describe domain;
 +-------------+--------------+------+-----+---------------------+-------+
 | Field       | Type         | Null | Key | Default             | Extra |
 +-------------+--------------+------+-----+---------------------+-------+
 | domain      | varchar(255) | NO   | PRI | NULL                |       |
 | description | varchar(255) | NO   |     | NULL                |       |
 | aliases     | int(10)      | NO   |     | 0                   |       |
 | mailboxes   | int(10)      | NO   |     | 0                   |       |
 | maxquota    | bigint(20)   | NO   |     | 0                   |       |
 | quota       | bigint(20)   | NO   |     | 0                   |       |
 | transport   | varchar(255) | NO   |     | NULL                |       |
 | backupmx    | tinyint(1)   | NO   |     | 0                   |       |
 | created     | datetime     | NO   |     | 0000-00-00 00:00:00 |       |
 | modified    | datetime     | NO   |     | 0000-00-00 00:00:00 |       |
 | active      | tinyint(1)   | NO   |     | 1                   |       |
 +-------------+--------------+------+-----+---------------------+-------+
 11 rows in set (0.00 sec)

 mysql> describe alias_domain;
 +---------------+--------------+------+-----+---------------------+-------+
 | Field         | Type         | Null | Key | Default             | Extra |
 +---------------+--------------+------+-----+---------------------+-------+
 | alias_domain  | varchar(255) | NO   | PRI | NULL                |       |
 | target_domain | varchar(255) | NO   | MUL | NULL                |       |
 | created       | datetime     | NO   |     | 0000-00-00 00:00:00 |       |
 | modified      | datetime     | NO   |     | 0000-00-00 00:00:00 |       |
 | active        | tinyint(1)   | NO   | MUL | 1                   |       |
 +---------------+--------------+------+-----+---------------------+-------+
 5 rows in set (0.00 sec)

 mysql> describe alias;
 +----------+--------------+------+-----+---------------------+-------+
 | Field    | Type         | Null | Key | Default             | Extra |
 +----------+--------------+------+-----+---------------------+-------+
 | address  | varchar(255) | NO   | PRI | NULL                |       |
 | goto     | text         | NO   |     | NULL                |       |
 | domain   | varchar(255) | NO   | MUL | NULL                |       |
 | created  | datetime     | NO   |     | 0000-00-00 00:00:00 |       |
 | modified | datetime     | NO   |     | 0000-00-00 00:00:00 |       |
 | active   | tinyint(1)   | NO   |     | 1                   |       |
 +----------+--------------+------+-----+---------------------+-------+
 6 rows in set (0.00 sec)

And, er, that's it. If you wish to store cleartext passwords (by passing a value greater than 0 for 'storeCleartextPassword' to the constructor) you'll need a 'password_cleartext' column on the mailbox field.

getFields returns %_fields, getTables %_tables. setFields and setTables resets them to the hash passed as an argument. It does not merge the two hashes.

This is the only way you should be interfering with those hashes.

Since the module does no guesswork as to the db schema (yet), you might need to use these to get it to load yours. Even when it does do that, it might guess wrongly.

REQUIRES

  • Perl 5.10

  • Crypt::PasswdMD5

  • Carp

  • DBI

Crypt::PasswdMD5 is libcyrpt-passwdmd5-perl in Debian, DBI is libdbi-perl

1 POD Error

The following errors were encountered while parsing the POD:

Around line 1497:

=cut found outside a pod block. Skipping to next block.