#!/usr/bin/perl use 5.010_000; use strict; use warnings; use utf8; our $VERSION = '0.01'; =head1 NAME fripost - Fripost.org handling utility for virtual hosting =head1 SYNOPSIS B [I] { mkpass | add-user | search-user | user-passwd | add-domain | search-domain | add-alias | search-alias } B B<--man> =head1 COMMANDS =over 4 =item B mkpass [I] Create a random new password, and returns its hash. =item B add-user [I] [B<--password=>I] Add a new virtual mailbox. =item B search-user [I] List matching virtual users. =item B user-passwd [I] [B<--password=>I] Change user password. =item B add-domain [I [I]] Add a new virtual domain. =item B search-domain [I [I]] List matching virtual domains. =item B add-alias [B<--force>] [I [I...]] Add a new virtual alias. =item B search-alias [B<-f>|B<--from>] [B<-g>|B<--goto>] [I
] List matching virtual aliases. =back =head1 DESCRIPTION Unless one of the B<-h>, B<--help>, or B<--man> option is given, one of the following commands is required. =over 4 =item B mkpass [I] C is used to generate a salted SHA-1 hash of the given I. If no argument is given, the password is randomly generated, respecting Fripost's password policy. =item B add-user [I] [B<--password=>I] C is used to add a new virtual mailbox to the system, unless B<--pretend> is set. If I or I are not given, the user is prompted for their value. If I is not fully qualified, the domain C is appended. An error is raised if I is already an existing virtual user or alias. If I is given, is it used RAW (not hashed). This can be useful if the user does not want to give the clear copy but only a hash, for example. Using this option disables the sending of credentials. =item B search-user [I] C is used to list virtual mailboxes whose username matches exactly I. Wildcards I<*> can appear in I, to match zero or more characters. If no I is given, lists all existing mailboxes. If I has no domain part, C lists matching users for any domains. Otherwise, C looks up the matching user parts for each matching domain. Because of these multiple searches, the use of wildcards on the domain part of I may be inefficient. =item B user-passwd [I] [B<--password=>I] C is used to change the password of I, unless B<--pretend> is set. If I or I are not given, the user is prompted for their value. If I is not fully qualified, the domain C is appended. An error is raised if I is not an existing virtual user. If I is given, is it used RAW (not hashed). This can be useful if the user does not want to give the clear copy but only a hash, for example. Using this option disables the sending of credentials. =item B add-domain [I [I]] C is used add a new virtual domain to the system, unless B<--pretend> is set. If I is not given, the user is prompted for its value. By default, C prompts for the owner(s) of the new domain; Use the empty string I<''> to create a "global" domain, only managed by Fripost's administrators. If I is an existing virtual domain, the owner(s) are simply added to the list of managers. =item B search-domain [I [I]] C is used to list virtual domains matching exactly I, and whose owner is I. Wildcards I<*> can appear in I, to match zero or more characters. If no I is given, list all domains matching I, regardless of the owner; If I is the empty string I<''>, list only the non self-managed domains. If neither I nor I are given, C lists all existing virtual domains. =item B add-alias [B<--force>] [I [I...]] C is used to add a new virtual alias to the system, unless B<--pretend> is set. If I or I are not given, the user is prompted for their value. If I is not fully qualified, the domain C is appended. An error is raised if I is already an existing username, or if I is an existing alias and B<--force> is not set. Inserted aliases conform to Postfix's B(5) alias table format; I has to be of one of the following forms: =over 4 =item . I@I, to redirect emails for I@I to I, or =item . @I, to catch all emails for users in I and redirect them to I. This form has the lowest precedence: If there is an alias from I@I to I, emails to I@I will be redirected to I only. See B(5) for details and warnings. =back If serveral entries are matching, for instance if there are an alias from I@I to I and another for I@I to I, emails to I@I will be redirected to BOTH I and I. Note that C forbids the creation of such multi-recipient aliases, unless B<--force> is set. =item B search-alias [B<-f>|B<--from>] [B<-g>|B<--goto>] [I
] C is used to list virtual aliases whose value or target matches exactly I
. As of the current version, wilcards are not allowed in I
; This will be fixed soon. To list matching aliases (resp., targets) only, use the flag B<-f> (resp., B<-g>). If no I
is given, C lists all existing virtual aliases. =back =head1 OPTIONS =over 8 =item B<--pretend> Dry-run all operations that is, do not modify the virtual lookup tables. But still queries the LDAP server to ensure that the modification would be safe. (For instance, ensure that a new user is not already existing.) =item B<--server_host=>I The LDAP URI to connect to. Overrides the value read from the configuration file (see B) if present. =item B<--bind_dn=>I The Distinguished Name (DN) to bind to the LDAP directory. Overrides the value read from the configuration file (see B) if present. If not set (the default), B binds anonymously. =item B<--bind_pw=>I The password to to bind with. Overrides the value read from the configuration file (see B) if present. =item B<--base_dn=>I The root DN for every communication to the LDAP server. Overrides the value read from the configuration file (see B) if present. =item B<--sign>[B<=>I] Use I as the key to sign all non-empty emails. If I is empty or not given, use the first key found in the secret keyring, see B(1). A running gpg-agent is required if the private key is protected by a passphrase. Overrides the value read from the configuration file (see B) if present. =item B{I|I|I} Tells whether non-empty emails should be encrypted. No email will be encrypted if I is chosen (the default). I turns on opportunistic encryption that is, emails will be encrypted as soon as the recipient is a usable user ID in the public keyring. I will disallow the sending of all non-empty clear emails. Overrides the value read from the configuration file (see B) if present. =item BI If one of the I or I encryption level is chosen, encrypt for the user ID I. =item B<-v>, B<--verbose> Verbose mode. =item B<-d>, B<--debug> Debug mode. =back =head1 CONFIGURATION The configuration is read from the file C<$HOME/.fripost.yml>, and has a lower precedence than the I above. Valid keys include: =over 4 =item I The LDAP URI to connect to. Defaults to C. =item I The I e-mail address to use. Defaults to C. =item I The Distinguished Name (DN) to bind to the LDAP directory. (If not set, B binds anonymously.) =item I The password to to bind with. =item I The root DN for every communication to the LDAP server. =item I The key used to sign all non-empty emails. If no key is given, use the first one found in the secret keyring, see B(1). A running gpg-agent is required if the private key is protected by a passphrase. =item I Tells whether non-empty emails should be encrypted. No email will be encrypted if I is chosen (the default). I turns on opportunistic encryption that is, emails will be encrypted as soon as the recipient is a usable user ID in the public keyring. I will disallow the sending of all non-empty clear emails (not recommended). =back =cut use FindBin qw($Bin); use lib "$Bin/lib"; use Env qw /HOME/; use File::Spec::Functions; use Getopt::Long qw /:config noauto_abbrev no_ignore_case gnu_compat bundling permute nogetopt_compat auto_version/; use Pod::Usage; use YAML::Syck; use Fripost::Schema; use Fripost::Commands::mkpass; use Fripost::Commands::add_user; use Fripost::Commands::search_user; use Fripost::Commands::user_passwd; use Fripost::Commands::add_domain; use Fripost::Commands::search_domain; use Fripost::Commands::add_alias; use Fripost::Commands::search_alias; ## Get global command line options our $conf = LoadFile( catfile ($HOME, '.fripost.yml') ); GetOptions( 'server_host=s' => \$conf->{server_host}, 'base_dn=s' => \$conf->{base_dn}, 'bind_dn=s' => \$conf->{bind_dn}, 'bind_pw=s' => \$conf->{bind_pw}, 'pretend' => \$conf->{pretend}, 'sign:s' => \$conf->{sign}, 'encrypt=s' => \$conf->{encrypt}, 'encrypt-to=s' => \$conf->{encrypt_to}, 'd|debug' => \$conf->{debug}, 'v|verbose' => \$conf->{verbose}, 'h|help' => sub { pod2usage(-exitstatus => 0, -sections => [ qw/SYNOPSIS COMMANDS/ ], -verbose => 99) }, 'man' => sub { pod2usage(-exitstatus => 0, -verbose => 2) }, 'password=s' => \$conf->{password}, 'force' => \$conf->{force}, 'f|from' => \$conf->{from}, 'g|goto' => \$conf->{goto}, ) or pod2usage(2); ## Set the default values $conf->{server_host} //= 'ldap://127.0.0.1:389'; $conf->{bind_dn} //= ''; $conf->{base_dn} //= ''; $conf->{admin_email} //= 'admin@fripost.org'; $conf->{encrypt} //= 'never'; die "Illegal encrypt level: `$conf->{encrypt}'.\n" unless grep {$_ eq $conf->{encrypt}} qw /never may secure/; ## Get the command my $cmd = shift; my $main; $cmd //= ''; if ($cmd eq 'mkpass') { &Fripost::Commands::mkpass::main (@ARGV); exit 0; } elsif ($cmd eq 'add-user') { $main = "Fripost::Commands::add_user::main"; } elsif ($cmd eq 'search-user') { $main = "Fripost::Commands::search_user::main"; } elsif ($cmd eq 'user-passwd') { $main = "Fripost::Commands::user_passwd::main"; } elsif ($cmd eq 'add-domain') { $main = "Fripost::Commands::add_domain::main"; } elsif ($cmd eq 'search-domain') { $main = "Fripost::Commands::search_domain::main"; } elsif ($cmd eq 'add-alias') { $main = "Fripost::Commands::add_alias::main"; } elsif ($cmd eq 'search-alias') { $main = "Fripost::Commands::search_alias::main"; } else { pod2usage( -exitstatus => 1, -verbose => 0, -msg => "Unknown command: `$cmd'."); } ## Connect to the LDAP server my $ldap = Fripost::Schema->new( $conf ); { no strict "refs"; &$main ($ldap, $conf, @ARGV); } $ldap->unbind(); =head1 AUTHOR Stefan Kangas C<< >> Guilhem Moulin C<< >> =head1 COPYRIGHT Copyright 2010-2012 Stefan Kangas. Copyright 2012 Guilhem Moulin. =head1 LICENSE This program is free software; you can redistribute it and/or modify it under the same terms as perl itself. =cut