diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 50a1309f..3e3a1f0e 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -3,33 +3,94 @@ name: GitHubBuild on: [push] jobs: - build: - - runs-on: ubuntu-latest - + lint_etc: + runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - - name: Validate composer.json and composer.lock + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 7.4 + tools: composer + extensions: sqlite3 + + - name: run install.sh + run: /bin/bash install.sh + + - name: check composer run: composer validate - - name: setup templates_c - run: mkdir templates_c || true +# Needing to 'update' here isn't ideal, but we need to cope with tests that run under different PHP versions :-/ + - name: Install dependencies + run: composer update --prefer-dist -n + + - name: check formatting + run: composer check-format + + - name: touch config.local.php + run: touch config.local.php + + - name: psalm static analysis + run: composer psalm + + testsuite: + needs: [lint_etc] + runs-on: ubuntu-22.04 + strategy: + matrix: + php-versions: [ '7.4', '8.0', '8.1', '8.2', '8.3' ] + + steps: + - uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + tools: composer + extensons: sqlite3 + + - name: run install.sh + run: /bin/bash install.sh - name: touch config.local.php run: touch config.local.php && php -v - name: Install dependencies - run: composer install --prefer-dist --no-progress --no-suggest + run: composer install --prefer-dist -n - name: Build/test - run: composer build + run: composer test - - name: setup coveralls - run: mkdir -p build/logs || true + build_coverage_report: + needs: [testsuite] + continue-on-error: true + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + tools: composer + extensions: sqlite3 + + - name: run install.sh + run: /bin/bash install.sh + + - name: touch config.local.php + run: touch config.local.php && php -v + + - name: Install dependencies + run: composer update --prefer-dist -n + + - name: build coveralls coverage + run: php -d xdebug.mode=coverage vendor/bin/phpunit tests - name: Coveralls - run: php vendor/bin/php-coveralls -v --coverage_clover=coverage.xml || true + run: vendor/bin/php-coveralls --coverage_clover=build/logs/clover.xml -v || true env: - COVERALLS_RUN_LOCALLY: 1 - COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + diff --git a/.gitignore b/.gitignore index 4833c98d..ed493673 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,15 @@ /config.local.php /templates_c/*.tpl.php /templates_c/*menu.conf.php -/vendor/ /.php_cs.cache /.idea +/vendor /composer.lock +/coverage +/composer.phar +/composer_2.phar +/.php-cs-fixer.cache +/.phpunit.result.cache +/build +/tests/postfixadmin.sqlite.*.test +/phpstorm-docker-dev/ diff --git a/.php_cs.dist b/.php-cs-fixer.dist.php similarity index 77% rename from .php_cs.dist rename to .php-cs-fixer.dist.php index 018cbf5c..4aa913c8 100644 --- a/.php_cs.dist +++ b/.php-cs-fixer.dist.php @@ -9,13 +9,12 @@ $finder = PhpCsFixer\Finder::create() ->files()->notName('config.inc.php')->notName('config.local.php') ->in(__DIR__); -return PhpCsFixer\Config::create() +$config = new PhpCsFixer\Config(); + +return $config ->setFinder($finder) ->setRules(array( - '@PSR2' => true, - 'braces' => array( - 'position_after_functions_and_oop_constructs' => 'same', - ), + '@PSR12' => true, 'method_argument_space' => false, # don't break formatting in initStruct() 'no_spaces_inside_parenthesis' => false, # don't break formatting in initStruct() )); diff --git a/.travis.yml b/.travis.yml index 8810d01c..8f0179d7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ php: - 7.2 - 7.3 - 7.4 + - 8.0 services: - mysql diff --git a/ADDITIONS/README.TXT b/ADDITIONS/README.md similarity index 72% rename from ADDITIONS/README.TXT rename to ADDITIONS/README.md index eecb1c64..6e0238ab 100644 --- a/ADDITIONS/README.TXT +++ b/ADDITIONS/README.md @@ -1,52 +1,69 @@ -# -# Postfix Admin ADDITIONS -# -BEFORE YOU START ----------------- +# BEFORE YOU START + **** ALL THESE SCRIPTS ARE CREATED BY THIRD PARTIES **** **** THEY ARE AS IS, USE AT YOUR OWN RISK! **** -ADDITIONS ---------- +# ADDITIONS In this directory you will find additional scripts that are build by others. -- change_password.tgz +## change_password.tgz + by George Vieira SquirrelMail plugin to change your passwor -- cleanupdirs.pl +## cleanupdirs.pl + by jared bell Displays a list of mailboxes that need to be deleted -- mailbox_remover.pl +## mailbox_remover.pl + by Petr Znojemsky Deletes all unused mailboxes -- mkeveryone.pl +## mkeveryone.pl + by Joshua Preston Generate an 'everybody' alias for a domain. -- pfa_maildir_cleanup.pl +## pfa_maildir_cleanup.pl by Stephen Fulton Deletes all unused mailboxes -- postfixadmin-0.3-1.4.tar.gz +## postfixadmin-0.3-1.4.tar.gz + by Florian Kimmerl + The Postfixadmin SquirrelMail plugin let users change their virtual alias, vacation status/message and password. -- virtualmaildel.php +See also : https://github.com/postfixadmin/postfixadmin/tree/master/ADDITIONS/squirrelmail-plugin + + +## virtualmaildel.php + by George Vieira Deletes all unused mailboxes +## Example mailbox / domain scripts for Postfixadmin + - postfixadmin-mailbox-postcreation.sh - postfixadmin-mailbox-postdeletion.sh - postfixadmin-domain-postdeletion.sh by Troels Arvin + Examples of scripts relevant to the optional + + $CONF['mailbox_postcreation_script'], $CONF['mailbox_postdeletion_script'] and $CONF['domain_postdeletion_script'] configuration options. + + +## Cyrus Quota Usage + +See https://github.com/o-m-d/cyrus-quotausage-to-pfa + diff --git a/ADDITIONS/fetchmail.pl b/ADDITIONS/fetchmail.pl index 916bdd81..0b15ff0b 100755 --- a/ADDITIONS/fetchmail.pl +++ b/ADDITIONS/fetchmail.pl @@ -27,7 +27,7 @@ our $db_username="mail"; our $db_password="CHANGE_ME!"; # Where to create a lockfile; please ensure path exists. -our $run_dir="/var/run/fetchmail"; +our $run_dir="/var/lock/fetchmail"; # in case you want to use dovecot deliver to put the mail directly into the users mailbox, # set "mda" in the fetchmail table to the keyword "dovecot". @@ -54,6 +54,18 @@ sub log_and_die { die $message; } +sub escape_password { + $output = ""; + for $i (0..length($_[0])-1){ + $char = substr($_[0], $i, 1); + if ($char eq "\\" or $char eq "\"") + { + $output = $output . "\\"; + } + $output = $output . $char; + } + return $output; +} # read options and arguments $configfile = "/etc/fetchmail-all/config"; @@ -98,18 +110,18 @@ if($db_type eq "Pg") { } $sql = " - SELECT id,mailbox,src_server,src_auth,src_user,src_password,src_folder,fetchall,keep,protocol,mda,extra_options,usessl, sslcertck, sslcertpath, sslfingerprint + SELECT id,mailbox,src_server,src_auth,src_user,src_password,src_folder,fetchall,keep,protocol,mda,extra_options,usessl, sslcertck, sslcertpath, sslfingerprint, src_port FROM fetchmail WHERE $sql_cond > poll_time*60 "; my (%config); map{ - my ($id,$mailbox,$src_server,$src_auth,$src_user,$src_password,$src_folder,$fetchall,$keep,$protocol,$mda,$extra_options,$usessl,$sslcertck,$sslcertpath,$sslfingerprint)=@$_; + my ($id,$mailbox,$src_server,$src_auth,$src_user,$src_password,$src_folder,$fetchall,$keep,$protocol,$mda,$extra_options,$usessl,$sslcertck,$sslcertpath,$sslfingerprint,$src_port)=@$_; syslog("info","fetch ${src_user}@${src_server} for ${mailbox}"); - $cmd="user '${src_user}' there with password '".decode_base64($src_password)."'"; + $cmd="user '${src_user}' there with password '".escape_password(decode_base64($src_password))."'"; $cmd.=" folder '${src_folder}'" if ($src_folder); if ($mda) { @@ -148,7 +160,7 @@ TXT print $file_handler $text; close $file_handler; - $ret=`/usr/bin/fetchmail -f $filename -i $run_dir/fetchmail.pid`; + $ret=`/usr/bin/fetchmail -f $filename --pidfile $run_dir/fetchmail.pid`; unlink $filename; diff --git a/ADDITIONS/mailbox_remover.pl b/ADDITIONS/mailbox_remover.pl index 02416242..fa27ecc0 100644 --- a/ADDITIONS/mailbox_remover.pl +++ b/ADDITIONS/mailbox_remover.pl @@ -117,7 +117,7 @@ foreach my $maildir (keys(%directories)) { close(TOUCH); print "Archiving $maildir\n"; @args = ($archcmd, "cvzf", $archive, $maildir); - system(@args) == 0 or die "Creating archive for $maildir failed: $?" + system(@args) == 0 or die "Creating archive for $maildir failed: $?"; rmtree($maildir); print localtime() . " $maildir has been deleted.\n"; diff --git a/ADDITIONS/postfixadmin-domain-postdeletion.sh b/ADDITIONS/postfixadmin-domain-postdeletion.sh index 095274b7..a7a94b19 100644 --- a/ADDITIONS/postfixadmin-domain-postdeletion.sh +++ b/ADDITIONS/postfixadmin-domain-postdeletion.sh @@ -8,7 +8,7 @@ # "somedomain.com". If $basedir/somedomain.com exists, it will # be removed. -# The script will not actually delete the directory. I moves it +# The script will not actually delete the directory. It moves it # to a special directory which may once in a while be cleaned up # by the system administrator. diff --git a/ADDITIONS/postfixadmin-mailbox-postdeletion.sh b/ADDITIONS/postfixadmin-mailbox-postdeletion.sh index 800ef940..7c903e70 100644 --- a/ADDITIONS/postfixadmin-mailbox-postdeletion.sh +++ b/ADDITIONS/postfixadmin-mailbox-postdeletion.sh @@ -6,7 +6,7 @@ # The script looks at arguments 1 and 2, assuming that they # indicate username and domain, respectively. -# The script will not actually delete the maildir. I moves it +# The script will not actually delete the maildir. It moves it # to a special directory which may once in a while be cleaned up # by the system administrator. diff --git a/ADDITIONS/postfixadmin-mailbox-postpassword.sh b/ADDITIONS/postfixadmin-mailbox-postpassword.sh new file mode 100644 index 00000000..b6c6e77d --- /dev/null +++ b/ADDITIONS/postfixadmin-mailbox-postpassword.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# Example script for dovecot mail-crypt-plugin +# https://doc.dovecot.org/configuration_manual/mail_crypt_plugin/ + +IFS= read -r -d $'\0' OLD_PASSWORD +IFS= read -r -d $'\0' NEW_PASSWORD + +# New user +if [ -z "$OLD_PASSWORD" ]; then + OLD_PASSWORD="$(openssl rand -hex 16)" + doveadm -o plugin/mail_crypt_private_password="$OLD_PASSWORD" mailbox cryptokey generate -u "$1" -U +fi + +# If you're using dovecot >= 2.3.19, try this instead (See: https://github.com/postfixadmin/postfixadmin/issues/646) +# printf "%s\n%s\n" "$OLD_PASSWORD" "$NEW_PASSWORD" "$NEW_PASSWORD" | doveadm mailbox cryptokey password -u "$1" -N -O + +# Password change +printf "%s\n%s\n" "$OLD_PASSWORD" "$NEW_PASSWORD" | doveadm mailbox cryptokey password -u "$1" -N -O "" diff --git a/ADDITIONS/quota_usage.pl b/ADDITIONS/quota_usage.pl index ad91769a..2dbe06af 100644 --- a/ADDITIONS/quota_usage.pl +++ b/ADDITIONS/quota_usage.pl @@ -86,7 +86,11 @@ sub list_quota_usage { $usage = $usage + 500; $usage = int $usage / 1000; } - if($insert_db == 1){execSql("UPDATE mailbox set quota_usage = $usage, quota_usage_date = CAST(NOW() AS DATE) WHERE username = '$email'");} + + if($insert_db == 1) + { + execSql("INSERT INTO quota2 (username, bytes) values ('$email', $usage) ON DUPLICATE KEY UPDATE bytes = VALUES(bytes)"); + } print_list() if ($list == 1); } @@ -144,8 +148,4 @@ sub help { print "$0 [options...]\n"; print "-l|--list List quota used\n"; print "-i|--addmysql For insert quota used in database mysql\n"; - } - - - diff --git a/ADDITIONS/squirrelmail-plugin/common.php b/ADDITIONS/squirrelmail-plugin/common.php index 9774a3d6..0834d98d 100644 --- a/ADDITIONS/squirrelmail-plugin/common.php +++ b/ADDITIONS/squirrelmail-plugin/common.php @@ -1,4 +1,5 @@ "; } -function _display_password_form() { +function _display_password_form() +{ bindtextdomain('postfixadmin', SM_PATH . 'plugins/postfixadmin/locale'); textdomain('postfixadmin'); do_header('Postfixadmin Squirrelmail - Login'); @@ -31,7 +34,8 @@ function _display_password_form() { /** * This returns a Zend_XmlRpc_Client instance - unless we can't log you in... */ -function get_xmlrpc() { +function get_xmlrpc() +{ global $CONF; require_once('Zend/XmlRpc/Client.php'); $client = new Zend_XmlRpc_Client($CONF['xmlrpc_url']); @@ -73,7 +77,8 @@ function get_xmlrpc() { return $client; } -function include_if_exists($filename) { +function include_if_exists($filename) +{ if (file_exists($filename)) { include_once($filename); } @@ -87,7 +92,8 @@ $optmode = 'display'; // Action: Checks if email is valid and returns TRUE if this is the case. // Call: check_email (string email) // -function check_email($email) { +function check_email($email) +{ $return = filter_var($email, FILTER_VALIDATE_EMAIL); if ($return === false) { return false; diff --git a/ADDITIONS/squirrelmail-plugin/postfixadmin_changepass.php b/ADDITIONS/squirrelmail-plugin/postfixadmin_changepass.php index 25c1f8b4..6978fcb6 100644 --- a/ADDITIONS/squirrelmail-plugin/postfixadmin_changepass.php +++ b/ADDITIONS/squirrelmail-plugin/postfixadmin_changepass.php @@ -1,4 +1,5 @@ remove(); diff --git a/ADDITIONS/squirrelmail-plugin/setup.php b/ADDITIONS/squirrelmail-plugin/setup.php index 95abdb62..2e7fcd0b 100644 --- a/ADDITIONS/squirrelmail-plugin/setup.php +++ b/ADDITIONS/squirrelmail-plugin/setup.php @@ -1,22 +1,26 @@ 8.3) + - Make fetchmail.pl respect src_port (thanks @tempixx - see #784) + - Improve github action (php 8.2) + - Add Javascript username validation to prevent illegal characters (see #765; thanks @verdinago) + - Fix typos / broken links etc (see #761; thanks @knofte) + - Add initial support for OpenDKIM domain key handling (see #631; thanks @Freddo3000 ) + - vacation.pl updates - (log4perl warnings, typos etc, logging to file, localhost connection stuff - see https://github.com/postfixadmin/postfixadmin/discussions/648 thanks @puck) + - fix display of active buttons in the bootstrap theme (see #749 - thanks @reinks2112) + - fix theme logo + config interaction (see #718; thanks @dkatheininge) + - fix pacrypt + php_crypt:MD5 + - fix dovecot:CRYPT-METHOD (see #708; thanks @Coeur2Boeuf) + - fix CSV alias export (see #703; thanks @dennis2society) + - upgrade bundled jquery (v1.12.4 to v3.7.0) (fixes jquery ajax security problem, postfixadmin isn't vulnerable, see #734; thanks @tibor-banfalvi) + - Require 'composer' for library installation (see install.sh) + - Remove the dependency on 'doveadm pw' and support more hash mechanisms (see: #491) + - Update debian/ patch (see #529) + - Support $CONF['database_port'] when connecting to MySQL (see #549) + - Add optional Dovecot mail-crypt plugin support - see https://github.com/postfixadmin/postfixadmin/issues/408 + - Add $CONF['site_url'] to allow administrators to override a detected site url (e.g. used in password recovery emails; see https://github.com/postfixadmin/postfixadmin/issues/446 ) + - Code reformat as PHPCS has a mind of it's own (function/method opening brace change of position) + - Improved UTF8 support in vacation (see https://github.com/postfixadmin/postfixadmin/pull/484) + - Fix quota levels losing config control (see bfc7af5c8efe2a68c47286cc870b56cb4f929a3f) + - vacation.pl: improve autoreply detection (see https://github.com/postfixadmin/postfixadmin/pull/482 + - vacation.pl: improve headers in auto-reply mails (add: "Auto-Submitted: auto-replied") see https://github.com/postfixadmin/postfixadmin/pull/483 + - vacation.pl: allow smtp helo to be customised; see https://github.com/postfixadmin/postfixadmin/pull/495 + - dark theme - use `$CONF['theme_css'] = 'css/dark-theme.css';` to enable - see https://github.com/postfixadmin/postfixadmin/issues/569, thanks @polymeer + - support 'send email' using smtp+ssl, smtp+starttls or smtp - see https://github.com/postfixadmin/postfixadmin/pull/566, thanks @davidebeatrici + - support smtpd_sender_login_maps via documents - see https://github.com/postfixadmin/postfixadmin/pull/565, thanks @davidebeatrici + - fix database collation issues (try and use latin1_general_ci everywhere) + +Version 3.3.13 - 2022/12/08 +------------------------------------------------- + - MySQL collation fix for quota/quota2 tables ( see https://github.com/postfixadmin/postfixadmin/issues/690 ) + - Fix MySQL not liking "alter table drop constraint ..." which seems to be MariaDB only (see #561 and #687) (thank you @stefanomarty) + +Version 3.3.12 - 2022/12/04 +------------------------------------------------- + - MySQL collation setting change, see https://github.com/postfixadmin/postfixadmin/issues/595 (and #327 and #552) + - Upgrade Smarty to 4.3.0 (see also #541) + - Improve PHP 8.1/8.2 compatability (see #632) + +Version 3.3.11 - 2022/03/02 +------------------------------------------------- + - Fix PHP 8 compatability for crypt() usage (see https://github.com/postfixadmin/postfixadmin/issues/547) + - Support $CONF['database_port'] for MySQL databases (see https://github.com/postfixadmin/postfixadmin/issues/549 and https://github.com/postfixadmin/postfixadmin/issues/553) + +Version 3.3.10 - 2021/08/09 +------------------------------------------------- + - Merge password expiration fixes from https://github.com/postfixadmin/postfixadmin/pull/493 + - Remove html readonly attribute from user's vacation page to/from selectors. + - vacation.pl - allow smtp helo to be specified (see https://github.com/postfixadmin/postfixadmin/pull/495) + - Security fix - ClickJacking protection (thanks @huntr-helper / @ranjit-git) (see https://github.com/postfixadmin/postfixadmin/issues/523) + - Security fix (low risk) - Improve randomness with PFA_token for CSRF protection (thanks @michaellrowley) + - Fix viewlog to allow admins to see all domains (thanks @pgimalac, https://github.com/postfixadmin/postfixadmin/issues/516) + - Disable password autocompletion in edit forms (thanks @gabrielfin, see https://github.com/postfixadmin/postfixadmin/pull/510) + +Version 3.3.9 - 2021/05/12 +------------------------------------------------- + - Improve Ukrainian language (ua.lang) (thanks: andrew.kudrinov) + - Ensure we update timestamp fields (created / modified) when performing db operations, see: https://github.com/postfixadmin/postfixadmin/issues/469 + - Add domain_admins.id pk column for non-sqlite users, see: https://github.com/postfixadmin/postfixadmin/issues/475 + - Add fix for MySQL error where a default datetime value in the domain field breaks the upgrade.php db schema update, see https://github.com/postfixadmin/postfixadmin/issues/489 + - Bug fix quota levels (now user configurable again; thanks @csware, see https://github.com/postfixadmin/postfixadmin/commit/bfc7af5c8efe2a68c47286cc870b56cb4f929a3f + +Version 3.3.8 - 2021/03/04 +------------------------------------------------- + - Fix invalid template referenced in broadcast-message.php; see https://github.com/postfixadmin/postfixadmin/issues/465 + - Fix PostgreSQL boolean issue in setup (unable to add superuser); see https://github.com/postfixadmin/postfixadmin/issues/461 + - Fix SQL error on password change; see https://github.com/postfixadmin/postfixadmin/issues/456 + - Add Ukrainian language (thanks: andrew.kudrinov) + +Version 3.3.7 - 2021/01/17 +------------------------------------------------- + - Fix missing db_connection_string() function from master; see https://github.com/postfixadmin/postfixadmin/issues/454 + +Version 3.3.6 - 2021/01/17 - Do not use (setup.php broken) +------------------------------------------------- + - Improve setup.php - output error_log location, try and detect if there is a problem calling pacrypt() (dovecot). + +Version 3.3.5 - 2021/01/27 +------------------------------------------------- + - Fix include path for password-change.php and improve UI for password-recover / password-change (nav bar was missing, remove table layout, fix labels not visible) (see https://github.com/postfixadmin/postfixadmin/issues/430 + - Fix users/edit-alias to remove unnecessary space (see https://github.com/postfixadmin/postfixadmin/issues/442) + - Improve documentation + - Improve password length check example in config.inc.php (see //github.com/postfixadmin/postfixadmin/issues/423) + - Improve ADDITIONS/update_quota.pl (update to use quota2 table) + - Check for some config setting, and do not error if they are not set (see https://github.com/postfixadmin/postfixadmin/issues/437) + - Add pt-pt (portugese) translation (thanks Numo Carrilho/Nunix) + - Fix missing template variable 'domain_selected' + + +Version 3.3.4 - 2021/01/19 +------------------------------------------------- + - Fix forgot-password (theme + trying to use class before autoload registered) (see //github.com/postfixadmin/postfixadmin/issues/427) + - Fix PHP 8.0 issues (string{} offset in CLI, psalm warning about string + int in MailboxHandler) + - Add PHP 8.0 to travis build + hopefully fix build + - Fix editform to add linefeeds on for e.g. alias editing (see https://github.com/postfixadmin/postfixadmin/pull/424) + - Fix mysql_crypt password hash - not all MySQL variants have RANDOM_BYTES function, so use our PHP based salt instead. (see https://github.com/postfixadmin/postfixadmin/issues/422) + +Version 3.3.3 - 2021/01/14 +------------------------------------------------- + - Improve error handling around login (require non-empty password; cope with pacrypt() throwing an exception; see https://github.com/postfixadmin/postfixadmin/issues/420) + - Improve setup.php (show error messages in admin creation form, fix unable to create admin - see https://github.com/postfixadmin/postfixadmin/issues/418) + +Version 3.3.2 - 2021/01/13 +------------------------------------------------- + - Add in the ability to specify a hash prefix with php_crypt password format, useful for Dovecot replacement. ( https://github.com/postfixadmin/postfixadmin/issues/344 ) + - Add documentation (DOCUMENTS/HASHING.md) + - Fix issue with vacation form not saving; vacation start/end is now stored with time, and users/ nav links ( https://github.com/postfixadmin/postfixadmin/issues/416 ) + +Version 3.3.1 - 2021/01/11 ------------------------------------------------- + - Fix issue with cli not working ( see https://github.com/postfixadmin/postfixadmin/issues/415 ) + - Fix issue with theme not working (if $CONF['theme_css'] was defined in config). ( see https://github.com/postfixadmin/postfixadmin/issues/410 ) + - Fix links in footer ( see https://github.com/postfixadmin/postfixadmin/issues/412 ) + +Version 3.3 - 2021/01/09 +------------------------------------------------- + - PostfixAdmin requires PHP 7.0 or greater. + - Change setup.php to use PHP's password_hash() for the config setup_password . (breaking change, existing setup passwords will fail to work and need regenerating) + - Change setup.php to not reveal system paths etc until a setup_password is configured and provided (see: https://github.com/postfixadmin/postfixadmin/issues/402 ) + - Move to bootstrap theme ( see https://github.com/postfixadmin/postfixadmin/pull/172 ) + - Improve vacation.pl (better utf-8 support) + - Improve DB connections (PDO, SSL) + - Add sha512.b64 password hash support (see https://github.com/postfixadmin/postfixadmin/issues/58) - Add support for password expiration (see https://github.com/postfixadmin/postfixadmin/pull/200 and README.password_expiration ) - Improve ADDITIONS/postfixadmin-mailbox-postcreate.sh - Add Date header into smtp_from() (see https://github.com/postfixadmin/postfixadmin/issues/203 ) - PostgreSQL fixes ( 1e158245d613fd1d8d5c1d59e26e940eb71f5b32 ) - vacation.pl fixes (perl libraries; see https://github.com/postfixadmin/postfixadmin/pull/194 ) - - Add bootstrap theme (default not changed yet) ( see https://github.com/postfixadmin/postfixadmin/pull/172 ) - Improve CSV export from list.php - Various misc. changes from static analysis (psalm) - Update installation instructions. (see: https://github.com/postfixadmin/postfixadmin/issues/189 https://github.com/postfixadmin/postfixadmin/issues/188 ) @@ -23,6 +149,7 @@ Version X.X - master - MySQL 8 compatibility (see https://github.com/postfixadmin/postfixadmin/pull/175 ) - Internally the database functions have been refactored to use PDO rather than the lower level mysql_, mysqli_, pg_ etc functions. ( see: https://github.com/postfixadmin/postfixadmin/pull/231 ) - Usage of dovecot deliver as fetchmail mda + - Corrupted Turkish language file fixed and missing translations are added. Version 3.2 - 2018/05/02 ------------------------------------------------- diff --git a/DOCUMENTS/DOVECOT.txt b/DOCUMENTS/DOVECOT.txt index e73fe90d..db293da5 100644 --- a/DOCUMENTS/DOVECOT.txt +++ b/DOCUMENTS/DOVECOT.txt @@ -56,6 +56,16 @@ namespace inbox { protocols = "imap pop3" # change to 'no' if you don't have ssl cert/keys, and comment out ssl_cert/ssl_key ssl = yes + +# If you're using LetsEncrypt/certbot see e.g. /etc/letsencrypt/live/MyDomain/fullchain.pem. +# +# cat server.crt server.key > dovecot.pem +# +# Make sure dovecot can read these file(s) +# +# If you use doveadm for your PostfixAdmin hashing, the webserver will also need read access to these files +# See also : https://github.com/postfixadmin/postfixadmin/blob/master/DOCUMENTS/HASHING.md#dovecotmethod + ssl_cert = 'smtp' AND active = '1')) +user_query = SELECT CONCAT('/var/mail/vmail/', maildir) AS home, 1001 AS uid, 1001 AS gid, CONCAT('*:bytes=', quota) AS quota_rule FROM mailbox WHERE username = '%u' AND (('%s' = 'smtp' AND smtp_active = '1') OR ('%s' <> 'smtp' AND active = '1')) + +3. Permissions +-------------- + +Applicable to those older versions of Postfixadmin (before v 3.4) (see also https://github.com/postfixadmin/postfixadmin/pull/491) + +With Dovecot 2.3.11 (ish?), if you are using the Postfixadmin dovecot password hashing backend - so your Postfixadmin configuration looks like + + `$CONF['encrypt'] = 'dovecot:something';` + +then the system user account running the PostfixAdmin code (normally the webserver user +account, like www-data or http or nobody) will need ... + + * read access to any SSL certificate files defined in /etc/dovecot/dovecot.conf + (check: ssl_key, ssl_cert) + + * read/write access to /run/dovecot/stats-writer + * Fixable with: `usermod -aG dovecot www-data`` + +Please note, Postfixadmin does not need to run on the same server as the Dovecot server. + +See also the following tickets which contain discussions and solutions : + + * https://github.com/postfixadmin/postfixadmin/issues/381 (Unable to login after Dovecot upgrade) + * https://github.com/postfixadmin/postfixadmin/issues/398 (Dovecotpw needs to read my TLS cert and private key) - - -3. Dovecot v1.0 quota support (optional) +4. Dovecot v1.0 quota support (optional) ---------------------------------------- Please note that you need to use Dovecot's own local delivery agent to @@ -226,3 +267,13 @@ If you use dovecot 1.2 or newer, - use the 'quota2' table (also created by setup.php) - set $CONF['new_quota_table'] = 'YES' +5. Dovecot Allowed IPs and App Password support (optional) +---------------------------------------------------------- + +To enhance end user security, Postfixadmin supports a set of features that +need implementation in the Dovecot login SQL queries. + +The following features are available: + +* Restrict login to a list of allowed remote IP addresses. +* Allow login with app passwords. diff --git a/DOCUMENTS/FAQ.txt b/DOCUMENTS/FAQ.txt index 9d1f8952..fe806814 100644 --- a/DOCUMENTS/FAQ.txt +++ b/DOCUMENTS/FAQ.txt @@ -34,3 +34,24 @@ Frequently Asked Questions: if it doesn't for you, try editing $CONF['emailcheck_resolve_domain'] to 'NO' in config.inc.php and try again. + +5) Postfixadmin is telling me an error occurred, and I should look at the server error logs for more details. Where do I look? + + - This depends on your server setup (distribution, how PHP is executed etc). + + - Postfixadmin typically calls PHP's error_log() function to log some detail about errors. This may write to : + + * /var/log/syslog + * /var/log/apache2/error.log + * /var/log/php7.4-fpm.log (or equivalent for your PHP version) + * or somewhere you may have manually configured. + + You can change where it logs by adding something like : + +```PHP + ini_set('error_log', '/tmp/postfixadmin.log'); // example only +``` + + into config.local.php, or you can change your PHP configuration. + + See also: https://stackoverflow.com/questions/5127838/where-does-php-store-the-error-log-php5-apache-fastcgi-cpanel diff --git a/DOCUMENTS/HASHING.md b/DOCUMENTS/HASHING.md new file mode 100644 index 00000000..eca7f4e6 --- /dev/null +++ b/DOCUMENTS/HASHING.md @@ -0,0 +1,229 @@ +# PostfixAdmin Password Hash Support. + +How are your passwords stored in the database. + +They should not be stored in plain text. + +Whatever format you choose will need to be supported by your IMAP server (and whatever provides SASL auth for Postfix) + +If you can, use a format that includes a different salt per password (e.g. one of the crypt variants, like Blowfish (BLF-CRYPT) or Argon2I/Argon2ID). +Try and avoid formats that are unsalted hashes (md5, SHA1) as these offer minimal protection in the event of a data leak. + +## Configuration + +See config.inc.php (or config.local.php) and look for + +```php +$CONF['encrypt'] = 'something'; +``` + +## Supported Formats + +This document is probably not complete. + +It possibly provides better documentation than was present before. This may not say much. + +Supported hash formats include : + + * MD5-CRYPT (aka MD5), + * SHA1, + * SHA1-CRYPT, + * SSHA (4 char salted sha1), + * BLF-CRYPT (Blowfish), + * SHA512, + * SHA512-CRYPT, + * ARGON2I, + * ARGON2ID, + * SHA256, + * SHA256-CRYPT, + * PLAIN-MD5 (aka md5) + * CRYPT + +Historically PostfixAdmin has supported all dovecot algorithms (methods) by using the 'doveadm' system binary. As of early 2023 (?), we attempted to use a native/PHP implementation for a number of these to remove issues caused by use of proc_open / dovecot file permissions etc (see e.g. #379). + +It's recommended you use the algorithm/mechanism from your MTA, and configure PostfixAdmin with the same value prefixed by the MTA name - + +For example, if dovecot has `default_pass_scheme = SHA256` use `$CONF['encrypt'] = 'SHA256'; ` in PostfixAdmin. + + +| Dovecot pass scheme | PostfixAdmin `$CONF['encrypt']` setting | +|---------------------|-----------------------------------------| +| SHA256 | SHA256 | +| SHA256-CRYPT.B64 | SHA256-CRYPT.B64 | +| SHA256-CRYPT | SHA256-CRYPT | +| SHA512-CRYPT | SHA512-CRYPT | +| ARGON2I | ARGON2I | +| ARGON2ID | ARGON2ID | + + + +| Courier Example | PostfixAdmin | +|-----------------|--------------| +| md5 | courier:md5 | +| md5raw | courier:md5raw | +| sha1 | courier:sha1 | +| ssha | courier:ssha | +| sha256 | courier:sha256 | + + +### cleartext + +No hashing. May be useful for debugging. + +Insecure. Try to avoid. May be useful for legacy purposes. + +### mysql_encrypt + +Uses the MYSQL ENCRYPT() function (this uses 'crypt' underneath). + +Can be secure. + +Requires MySQL. + +Should use a sha512 salt for new values. + +### md5crypt + +md5crypt = uses md5crypt() function - in a 'crypt' like format. + +e.g. + +`$1$c9809462$M0zeLuOvixH61C2csGN.U0` + +You should not use this for new installations + +(it probably does not offer a high level of security) + +### md5 + +PHP's md5() function. + +You should not use this (it does not offer a high level of security), but is probably better than cleartext. + +### system + +Uses PHP's crypt function. + +Probably throws an E_NOTICE. + +Example : `$1$tWgqTIuF$1HFciCXrhVpACGjBMxNr/0` + +### authlib + +See source code. Presumably useful for Courier based installations. + +#### With `$CONF['authlib_default_flavor'] = 'md5raw`;` + +might give something like : + +`{md5raw}3858f62230ac3c915f300c664312c63f` + +Based on md5, so avoid. + +#### With `$CONF['authlib_default_flavor'] = 'crypt`;` + +Uses PHP Crypt. + +`{crypt}blfqitzeBpyAE` + +Presumably weak. + +#### With `$CONF['authlib_default_flavor'] = 'SHA';` + +Uses sha1, base64 encoded. Unsalted. Avoid. + +### dovecot:METHOD + +May use dovecot binary to produce hash, if the format you request isn't in PFACrypt::DOVECOT_NATIVE + +Using a format that PostfixAdmin doesn't support natively has the following pros/cons : + +#### Pros + +* Minimal dependency on PostfixAdmin / PHP code. +* Hash should definitely work with dovecot! + +#### Cons + +* file permissions and/or execution of doveadm by the web server may be problematic. +* requires: proc_open(...) - which might be blocked by e.g. safemode. +* doveadm may not be installed. +* possible issues with SELinux +* See https://github.com/postfixadmin/postfixadmin/issues/398 (file permissions) + +#### Incomplete list of CRYPT-METHOD + +* CRAM-MD5 +* SHA +* SHA1 +* SHA256 +* SHA512 +* CLEAR +* CLEARTEXT +* PLAIN +* PLAIN-TRUNC + +If in doubt, try `dovecot:SHA512` + +Dovecot generated passwords in your database should look a bit like : + +`{SHA256}JMQi5oHxwb0IKGx6r10jpfCI3NsLIZgGs6nleSRPAMU=` + +If you have problems, start by checking you can generate one on the command line using e.g + +`doveadm pw -s SHA256` + +### php_crypt + +Potentially the most secure. + +By default it will generate a SHA512 salt. Output in crypt format. + +Other methods : + +* BLOWFISH +* SHA512 +* SHA256 +* DES (avoid) +* MD5 (avoid) + +e.g. + +`$6$emcsNNrzGZSN64mI$A/bmacTGSp2UrdcPvaROrR2FPQS5KlnoU.a/0zmfpaubBO9o1ZcgyQIic4Qb59SMxA2H8YxgS1XILO1wZhjkZ0` + +You can specify the salting method using a :METHOD in the specification. + +e.g. + +`$CONF['encrypt'] = 'php_crypt:SHA512';` + +You can make the hashing more 'difficult' by specifying an additional parameter like : + +`$CONF['encrypt'] = 'php_crypt:SHA512:5000';` + +which should change the 'cost' (BLOWFISH) or rounds (SHA256, SHA512). + +finally you can ask that the generated hash has a specific prefix (e.g. {SHA512} ) like : + +`$CONF['encrypt'] = 'php_crypt:SHA512:5000:{SHA512-CRYPT}';` + +### sha512.b64 + +See https://github.com/postfixadmin/postfixadmin/issues/58 + +No dovecot dependency; should support migration from md5crypt + +Output is base64 encoded i.e. a hash like : + +* `$6$emcsNNrzGZSN64mI$A/bmacTGSp2UrdcPvaROrR2FPQS5KlnoU.a/0zmfpaubBO9o1ZcgyQIic4Qb59SMxA2H8YxgS1XILO1wZhjkZ0` + +is base64 encoded into : + +* JDYkZW1jc05OcnpHWlNONjRtSSRBL2JtY... + +and then formatted to become : + +* {SHA512-CRYPT.B64}JDYkZW1jc05OcnpHWlNONjRtSSRBL2JtY.... + +This format should support older passwords with a {MD5-CRYPT} prefix, to allow you to migrate. + diff --git a/DOCUMENTS/Migration.md b/DOCUMENTS/Migration.md new file mode 100644 index 00000000..a4d782b7 --- /dev/null +++ b/DOCUMENTS/Migration.md @@ -0,0 +1,56 @@ +# Migrating to Postfixadmin from other products + +## From Postfix + +Where a database structure like this exists : + +See also: https://github.com/postfixadmin/postfixadmin/issues/468 + + +```SQL + +CREATE TABLE `virtual_domains` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(50) CHARACTER SET latin1 NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=29 DEFAULT CHARSET=latin1 COLLATE=latin1_german1_ci; + +CREATE TABLE `virtual_aliases` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `domain_id` int(11) NOT NULL, + `source` varchar(40) CHARACTER SET latin1 NOT NULL, + `destination` varchar(80) CHARACTER SET latin1 NOT NULL, + PRIMARY KEY (`id`), + KEY `domain_id` (`domain_id`), + CONSTRAINT `virtual_aliases_ibfk_1` FOREIGN KEY (`domain_id`) REFERENCES `virtual_domains` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=465 DEFAULT CHARSET=latin1 COLLATE=latin1_german1_ci; + +CREATE TABLE `virtual_users` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `domain_id` int(11) NOT NULL, + `user` varchar(40) CHARACTER SET latin1 NOT NULL, + `password` varchar(32) CHARACTER SET latin1 NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `UNIQUE_EMAIL` (`domain_id`,`user`), + CONSTRAINT `virtual_users_ibfk_1` FOREIGN KEY (`domain_id`) REFERENCES `virtual_domains` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=114 DEFAULT CHARSET=latin1 COLLATE=latin1_german1_ci; + +``` + +## Possible route + +You'll need to modify the below to match how your mailboxes are stored on disk (the maildir). + +It assumes the password hash format is compatible. If not some sort of blanket reset is probably required. + +### Migrate domains + +`insert into domain (domain, description, transport) select name, name, 'virtual' from postfix_legacy.virtual_domains;` + +### Migrate users + +` insert into mailbox (username, password, name, maildir, local_part, domain) select concat(user, '@', d.name), password, user, concat(d.name, '/', user), user, d.name FROM postfix_legacy.virtual_users INNER JOIN postfix_legacy.virtual_domains d ON postfix_legacy.virtual_users.domain_id = d.id;` + +### Migrate Aliases + +`insert into alias (address, goto, domain) select concat(a.source, '@', d.name), a.destination, d.name FROM postfix_legacy.virtual_aliases a INNER JOIN postfix_legacy.virtual_domains d ON d.id = a.domain_id;` diff --git a/DOCUMENTS/OPENDKIM.txt b/DOCUMENTS/OPENDKIM.txt new file mode 100644 index 00000000..0d0fa6d3 --- /dev/null +++ b/DOCUMENTS/OPENDKIM.txt @@ -0,0 +1,37 @@ +# +# OpenDKIM configuration for Postfix Admin +# Originally written by: Fredrik Falk +# + +More complete OpenDKIM documentation: + +http://www.opendkim.org/opendkim.8.html +http://www.opendkim.org/opendkim.conf.5.html +http://www.opendkim.org/opendkim-genkey.8.html +https://github.com/trusteddomainproject/OpenDKIM/blob/master/opendkim/README.SQL + +Here are the relevant parts of OpenDKIM v2.11.x configuration for Postfixadmin setup. + +Please refer to OpenDKIM documentation for complete information. + +The setup gets KeyTable and SigningTable info from MySQL, allowing domain admins to edit and add domain keys as well as +assign which authors will use them. + + +1. PostfixAdmin Setup +----------------- + +Add `$CONF['dkim'] = 'YES';` to your `config.local.php`, and optionally ``$CONF['dkim_all_admins'] = 'YES';` to allow +non-super-admins to add domain keys and signtable entries. + + +2. OpenDKIM setup +----------------- + +Ensure that the version of OpenDKIM supports databases/OpenDBX. +After that, simply add the following to your /etc/opendkim.conf: +``` +SigningTable dsn:mysql://{USER}:{PASSWORD}@{HOST}/{DATABASE}/table=dkim_signing?keycol=author?datacol=dkim_id +KeyTable dsn:mysql://{USER}:{PASSWORD}@{HOST}/{DATABASE}/table=dkim?keycol=id?datacol=domain_name,selector,private_key +``` +Replace {USER}, {PASSWORD}, {HOST}, and {DATABASE} with your values. diff --git a/DOCUMENTS/POSTFIX_CONF.txt b/DOCUMENTS/POSTFIX_CONF.txt index ec4622e5..f76c1c58 100644 --- a/DOCUMENTS/POSTFIX_CONF.txt +++ b/DOCUMENTS/POSTFIX_CONF.txt @@ -32,6 +32,36 @@ transport_maps = proxy:mysql:/etc/postfix/sql/mysql_transport_maps.cf virtual_mailbox_base = /var/mail/vmail # or whereever you want to store the mails +If you are using dovecot sasl for authentication you can configure Postfix main.cf: + +smtpd_sasl_path = private/auth +smtpd_sasl_type = dovecot +smtpd_sasl_authenticated_header = yes + +If using the Postfix submission service you could configure as follows in master.cf + +submission inet n - n - - smtpd + -o syslog_name=postfix/submission + -o stress= + -o smtpd_sasl_auth_enable=yes + -o smtpd_delay_reject=no + -o smtpd_etrn_restrictions=reject + -o smtpd_helo_restrictions= + -o smtpd_client_restrictions=submission_client_checks + -o smtpd_sender_restrictions=submission_sender_checks + -o smtpd_recipient_restrictions=submission_recipient_checks + -o smtpd_tls_security_level=encrypt + +and in main.cf: + +smtpd_sender_login_maps = proxy:mysql:/etc/postfix/mysql-login_maps_dovecot.cf +smtpd_sasl_auth_enable = no +smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated reject_unauth_destination +smtpd_restriction_classes = submission_recipient_checks, submission_sender_checks +submission_recipient_checks = reject_unknown_recipient_domain,permit_sasl_authenticated,reject_unauth_destination +submission_sender_checks = reject_sender_login_mismatch +submission_client_checks = permit_sasl_authenticated,reject_unauth_destination + Where you chose to store the .cf files doesn't really matter, but they will have database passwords stored in plain text so they should be readable only by user postfix, or in a directory only accessible to user postfix. @@ -125,8 +155,9 @@ user = postfix password = password hosts = localhost dbname = postfix -query = SELECT transport FROM domain WHERE domain='%s' AND active = '1' - +#query = SELECT transport FROM domain WHERE domain='%s' AND active = '1' AND transport != 'virtual' +# Enforce virtual transport (catches internal virtual domains and avoid mails being lost in other transport maps) +query = SELECT REPLACE(transport, 'virtual', ':') AS transport FROM domain WHERE domain='%s' AND active = '1' (See above note re Concat + PostgreSQL) diff --git a/DOCUMENTS/Password_Expiration.md b/DOCUMENTS/Password_Expiration.md new file mode 100644 index 00000000..8e4db387 --- /dev/null +++ b/DOCUMENTS/Password_Expiration.md @@ -0,0 +1,110 @@ +# Description + +This extension adds support for password expiration. +It is designed to have expiration on users passwords. An email is sent when the password is expiring in 30 days, then 14 days, then 7 days. +It is strongly inspired by https://abridge2devnull.com/posts/2014/09/29/dovecot-user-password-expiration-notifications-updated-4122015/, and adapted to fit with Postfix Admin & Roundcube's password plugin + +Expiration unit is day + +Expiration value for domain is set through Postfix Admin GUI + +# Installation + +Password Expiration is merged with PostfixAdmin - so no additional database changes should be necessary. + + +## Database Fields + + * mailbox.password_expiry - timestamp, when the mailbox password expires. + * domain.password_expiry - default duration for when a password will expire + +Changes in MySQL/MariaDB mailbox table (as defined in `$CONF['database_tables']` from config.inc.php): + +## Changes in Postfix Admin : + +To enable password expiration, add the following to your config.inc.php file: + +`$CONF['password_expiration'] = 'YES';` + +## RoundCube Password Plugin + +If you are using Roundcube's password plugin, you should also adapt the `$config['password_query']` value. + +I recommend to use: + +`$config['password_query'] = 'UPDATE mailbox SET password=%c, modified = now(), password_expiry = now() + interval 90 day';` + +of course, you may adapt to the expiration value to suit. + + +## Changes in Dovecot (adapt if you use another LDA) + +Edit dovecot-mysql.conf file, and replace the user_query (and only this one) to be based on this query: + +``` +password_query = SELECT username as user, password, concat('/var/vmail/', maildir) as userdb_var, concat('maildir:/var/vmail/', maildir) as userdb_mail, 20001 as userdb_uid, 20001 as userdb_gid, m.domain FROM mailbox m, domain d where d.domain = m.domain and m.username = '%u' AND m.active = '1' AND (m.password_expiry > now() or d.password_expiry = 0) +``` + + +Of course, you may require to adapt the uid, gid, maildir and table to your setup. + + +## Changes in system + +You need to have a script running on a daily basis to check password expiration and send emails 30, 14 and 7 days before password expiration. An example is given below. + +Edit the script to adapt the variables to your setup. + +This script is using `postfixadmin.my.cnf` to read credentials, which might look a bit like : + +```ini +[client] +user = me +password = secret +host = hostname +``` + +Edit this file to enter a DB user that is allowed to access (read only) your database. + +You could create a new MySQL user with only SELECT permission on mailbox.username and mailbox.password_expiry. + +This file should be protected from other users (e.g. chmod 400). + +### Expiration Script + +```bash +#!/bin/bash + +# Adapt to your setup + +# Be careful who you run this script as; other system users may be able to write to the postfixadmin database, inject +# malicious data into e.g. mailbox.username and then be able to execute commands as the user running this script. + +# So, please try to avoid running this script as root. + +POSTFIX_DB="postfixadmin" +MYSQL_CREDENTIALS_FILE="postfixadmin.my.cnf" + +REPLY_ADDRESS="noreply@example.com" + +# Change this list to change notification times and when ... +for INTERVAL in 30 14 7 +do + LOWER=$(( $INTERVAL - 1 )) + + QUERY="SELECT username,password_expiry FROM mailbox WHERE password_expiry > now() + interval $LOWER DAY AND password_expiry < NOW() + interval $INTERVAL DAY" + + mysql --defaults-extra-file="$MYSQL_CREDENTIALS_FILE" "$POSTFIX_DB" -B -N -e "$QUERY" | while IFS=$'\t' read -a RESULT ; do + + EMAIL_TO=${RESULT[0]} + PASSWORD_EXPIRE=${RESULT[1]} + + # basic attempt at validating email address looks legit. + if [[ "$EMAIL_TO" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$ ]] + then + echo -e "Dear User, \n Your password will expire on ${PASSWORD_EXPIRE}" | mail -s "Password $INTERVAL days before expiration notification" -r $REPLY_ADDRESS "${EMAIL_TO}" + fi + done +done + +``` diff --git a/DOCUMENTS/Postfix-Dovecot-Postgresql-Example.md b/DOCUMENTS/Postfix-Dovecot-Postgresql-Example.md index e5773972..7ad75511 100644 --- a/DOCUMENTS/Postfix-Dovecot-Postgresql-Example.md +++ b/DOCUMENTS/Postfix-Dovecot-Postgresql-Example.md @@ -32,6 +32,7 @@ virtual_uid_maps = static:8 virtual_gid_maps = static:8 local_transport = virtual local_recipient_maps = $virtual_mailbox_maps +smtpd_sender_login_maps = proxy:pgsql:/etc/postfix/pgsql/virtual_sender_maps.cf ``` and for Postfix SASL support : @@ -46,6 +47,12 @@ smtpd_sasl_security_options = noanonymous broken_sasl_auth_clients = yes ``` +Please note that `smtpd_sender_login_maps` is only taken into account when a relevant restriction is specified in `smtpd_sender_restrictions`. + +By default a client can send emails from any addresses! + +For reference: http://www.postfix.org/postconf.5.html#reject_sender_login_mismatch + ## /etc/postfix/pgsql/relay_domains.cf ``` @@ -99,6 +106,16 @@ dbname = postfix query = SELECT maildir FROM mailbox WHERE username='%s' AND active = true ``` +## /etc/postfix/pgsql/virtual_sender_maps.cf + +``` +user = postfix +password = whatever +hosts = localhost +dbname = postfix +query = SELECT username FROM mailbox WHERE username='%s' AND active = true +``` + # Dovecot @@ -217,5 +234,21 @@ default_pass_scheme = MD5-CRYPT password_query = SELECT username AS user,password FROM mailbox WHERE username = '%u' AND active='1' # Query to retrieve user information, note uid matches dovecot.conf AND Postfix virtual_uid_maps parameter. -user_query = SELECT '/var/vmail/mail/' || maildir AS home, 8 as uid, 8 as gid FROM mailbox WHERE username = '%u' AND active = '1' +user_query = SELECT '/var/mail/vmail/' || maildir AS home, 8 as uid, 8 as gid FROM mailbox WHERE username = '%u' AND active = '1' +``` + +Password query with app password and allowed remote IP support: +``` +password query = SELECT user, password FROM (\ + SELECT username AS user, password, '0' AS is_app_password FROM\ + mailbox\ + UNION\ + SELECT username AS user, password, '1' AS is_app_password FROM mailbox_app_password\ +)\ +WHERE user='%u' AND password='%w' AND active=1 AND\ +(\ + "%r" IN (SELECT ip FROM totp_exception_address WHERE username="%u" OR username IS NULL OR username="@%d")\ + OR (SELECT totp_secret FROM mailbox WHERE usenamer="%u") IS NULL\ + OR is_app_password='1'\ +) ``` diff --git a/DOCUMENTS/README.password_expiration b/DOCUMENTS/README.password_expiration deleted file mode 100644 index 0f38174e..00000000 --- a/DOCUMENTS/README.password_expiration +++ /dev/null @@ -1,48 +0,0 @@ -*Description - -This extension adds support for password expiration. -It is designed to have expiration on users passwords. An email is sent when the password is expiring in 30 days, then 14 days, then 7 days. -It is strongly inspired by https://abridge2devnull.com/posts/2014/09/29/dovecot-user-password-expiration-notifications-updated-4122015/, and adapted to fit with Postfix Admin & Roundcube's password plugin -Expiration unit is day -Expiration value for domain is set through Postfix Admin GUI - -*Installation - -Perform the following changes: - -**Changes in MySQL/MariaDB mailbox table (as defined in $CONF['database_tables'] from config.inc.php): - -You are invited to backup your DB first, and ensure the table name is correct. - -Execute the attached SQL script (password_expiration.sql) that will add the required columns. The expiration value for existing users will be set to 90 days. If you want a different value, edit line 2 in the script and replace 90 by the required value. - -**Changes in Postfix Admin : - -To enable password expiration, add the following to your config.inc.php file: -$CONF['password_expiration'] = 'YES'; - -All my tests are performed using $CONF['encrypt'] = 'md5crypt'; - -**If you are using Roundcube's password plugin, you should also adapt the $config['password_query'] value. - -I recommend to use: - -$config['password_query'] = 'UPDATE mailbox SET password=%c, modified = now(), password_expiry = now() + interval 90 day'; - -of cource you may adapt to the expected expiration value - -All my tests are performed using $config['password_algorithm'] = 'md5-crypt'; - -**Changes in Dovecot (adapt if you use another LDA) - -Edit dovecot-mysql.conf file, and replace the user_query (and only this one) by this query: - -password_query = SELECT username as user, password, concat('/var/vmail/', maildir) as userdb_var, concat('maildir:/var/vmail/', maildir) as userdb_mail, 20001 as userdb_uid, 20001 as userdb_gid, m.domain FROM mailbox m, domain d where d.domain = m.domain and m.username = '%u' AND m.active = '1' AND (m.password_expiry > now() or d.password_expiry = 0) - -Of course you may require to adapt the uid, gid, maildir and table to your setup - -**Changes in system - -You need to have a script running on a daily basis to check password expiration and send emails 30, 14 and 7 days before password expiration (script attached: check_mailpass_expiration.sh). -Edit the script to adapt the variables to your setup. -This script is using postfixadmin.my.cnf to read credentials. Edit this file to enter a DB user that is allowed to access (read-write) your database. This file should be protected from any user (chmod 400). diff --git a/DOCUMENTS/Roundcubemail-TOTP-sync.example.sh b/DOCUMENTS/Roundcubemail-TOTP-sync.example.sh new file mode 100644 index 00000000..e69de29b diff --git a/DOCUMENTS/SECURITY.txt b/DOCUMENTS/SECURITY.txt index f3f86861..29e6ab57 100644 --- a/DOCUMENTS/SECURITY.txt +++ b/DOCUMENTS/SECURITY.txt @@ -1,13 +1,13 @@ Security and PostfixAdmin ------------------------- -While the developers of PostfixAdmin believe the software to be +While the developers of PostfixAdmin believe the software to be secure, there is no guarantee that it will continue to do be so in the future - especially as new types of exploit are discovered. (After all, this software is without warranty!) In the event you do discover a vulnerability in this software, -please report it to the development mailing list, or contact +please report it to the development mailing list, or contact one of the developers directly. @@ -19,26 +19,68 @@ DATABASE USER SECURITY You may wish to consider the following : 1. Postfix only requires READ access to the database tables. - 2. The virtual vacation support (if used) only needs to WRITE to + 2. The virtual vacation support (if used) only needs to WRITE to the vacation_notification table (and read alias and vacation). - 3. PostfixAdmin itself needs to be able to READ and WRITE to + 3. PostfixAdmin itself needs to be able to READ and WRITE to all the tables. 4. PostfixAdmin's setup.php additionally needs permissions to CREATE and ALTER tables in the PostfixAdmin database. For PostgreSQL, also - permissions for CREATE FUNCTION and CREATE TRIGGER are needed. - In other words: setup.php needs all permissions on the PostfixAdmin - database. + permissions for CREATE FUNCTION and CREATE TRIGGER are needed. + In other words: setup.php needs all permissions on the PostfixAdmin + database. -Using the above, you can improve security by creating separate -database user accounts for each of the above roles, and limit +Using the above, you can improve security by creating separate +database user accounts for each of the above roles, and limit the permissions available to them as appropriate. FILE SYSTEM SECURITY -------------------- -PostfixAdmin does not require write support on the underlying +PostfixAdmin does not require write support on the underlying filesystem with the following exceptions: - the templates_c directory where Smarty caches the templates - PHP's session.save_path to store session files + +END USER SECURITY +----------------- + +To enhance the security of admin and mailbox user accounts, Postfixadmin +supports a set of different features: + +1. Multi-factor authentication with TOTP for admin and mailbox users. +2. Synchronize the TOTP secret with a Mail front end, for example + Roundcubemail. This enables TRUSTED mail user clients (MUAs) to + implement MFA internally. +3. Enable MUAs with allowed IP addresses to log in with username and + password. Use this feature with care. It basically deactivates MFA + for specified IPs. This feature is intended for mail user clients + that implement MFA themselves, for example Roundcubemail. However, + this can also be used to deactivate MFA when a VPN is used or other + use cases. +4. Allow SMTP, IMAP and POP login with app passwords when a TOTP secret + is set. The app passwords cannot be used to log in at Postfixadmin + itself. That means only the normal user password plus the TOTP factor + allow adding, changing or removing app passwords. + +These features are DEACTIVATED by default because they need to be +supported by your MTA/MDA configuration to become effective. Please +read carefully through the documentation before activating these +features. + +To activate those features, run through the following procedure: +1. Change your MDA (and if required MTA) password query. You can + take a look at the example query listed in the + Postfix-Dovecot-Postgresql-Example.md file. The example should work + for Dovecot out of the box. +2. Set up synchronization of TOTP secrets with a mail user client + application. This is important. Otherwise MFA will not be used to + protect access to mails. + Use the mailbox_post_TOTP_change_secret_script setting in the + config.inc.php. The mailbox username and domain will be passed + as parameters, the shared secret via stdin. For Roundcubemail you can + have a look at the scripts/examples/sync-roundcubemail-totp.php example. +3. Activate TOTP and app passwords in the config.inc.php by setting + $CONF['totp'] = 'YES'; + $CONF['app_passwords'] = 'YES'; diff --git a/DOCUMENTS/SUPERADMIN.txt b/DOCUMENTS/SUPERADMIN.txt index 0022553f..8edcf408 100644 --- a/DOCUMENTS/SUPERADMIN.txt +++ b/DOCUMENTS/SUPERADMIN.txt @@ -1,16 +1,20 @@ ------------------------------------- - Recreating a superadmin account +# Recreating a superadmin account +Login to setup.php using the setup_password you have setup config.local.php to contain. -When you run setup.php you will be required to enter a super user name and password. -This user will be able to login and modify any domain or setting. Hence, superadmin!. +From setup.php you can add a 'superadmin' account. This account can access any domain or mailboxes defined within Postfixadmin. -With that login you can create new superadmins (and you should delete or change the -password of admin@domain.tld). If that user is no longer there or you didn't use -the .TXT files, you could add another manually from the database. +The 'superadmin' account is able to create additional 'admin' users which have their access restricted to domains of your choice. -In case you forgot your superadmin username or password, you can create a new -superadmin account using setup.php. +## Forgotten setup_password -If you also have forgotten your setup password, you can use setup.php to configure -a new setup password. +In case you forgot your superadmin username or password, you can create a new superadmin account using setup.php. + +## Forgotten superadmin username(s) + +Once you have authenticated with your setup_password on setup.php, a list of superadmin usernames is printed out. + +## Forgotten superadmin password + +The easiest approach is to create a new superadmin user, and then using a database tool of your choice update the old + user with the password hash to have the password hash of a new user. diff --git a/DOCUMENTS/UPGRADE.txt b/DOCUMENTS/UPGRADE.txt index 4167328a..03afd906 100644 --- a/DOCUMENTS/UPGRADE.txt +++ b/DOCUMENTS/UPGRADE.txt @@ -1,24 +1,14 @@ -# -# Postfix Admin -# by Mischa Peters -# Copyright (c) 2002 - 2005 High5! -# Licensed under GPL for more info check GPL-LICENSE.TXT -# REQUIRED!! ---------- - You are using Postfix 2.0 or higher. - You are using Apache 1.3.27 / Lighttpd 1.3.15 or higher. -- You are using PHP 5.1.2 or higher. -- You are using MySQL 3.23 or higher OR PostgreSQL v7.4+ - +- You are using PHP 7.0 or higher. +- You are using MySQL 5.6 or higher OR PostgreSQL v8+ READ THIS FIRST! ---------------- -This document describes upgrading from an older PostfixAdmin version -(>= v1.5x) - It's recommend that you install Postfix Admin in a new folder and not on-top of the old install!! (At the very least, make sure you have backups of the database and relevant filesystem!) @@ -26,7 +16,6 @@ the database and relevant filesystem!) When upgrading Postfix Admin, make sure you backup your database before running upgrade.php. - 1. Backup the Database ---------------------- When you install from a previous version make sure you backup your database @@ -66,7 +55,7 @@ needs to be writeable for your webserver. (if your Apache runs as user "www-data") -If you have SELinux enabled, also run (adust the path to match your setup) +If you have SELinux enabled, also run (adjust the path to match your setup) $ sudo semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/utils/pfadmin/public/templates_c(/.*)?" $ sudo restorecon -Rv /var/www/utils/pfadmin/ @@ -98,7 +87,7 @@ into the form, and setup.php will echo out the hashed value (which needs to go i The setup_password removes the requirement for you to delete setup.php, and also closes a security hole. Since version 2.2 of Postfixadmin, setup.php can perform the needed database -updates automatically . +updates automatically. If you update from 2.1 or older, also create a superadmin account using setup.php. diff --git a/DOCUMENTS/screenshots/README.md b/DOCUMENTS/screenshots/README.md new file mode 100644 index 00000000..be95761e --- /dev/null +++ b/DOCUMENTS/screenshots/README.md @@ -0,0 +1,82 @@ +# Some screenshots of Postfixadmin + +## 1. Setup process + +When you visit visit https://your-site.com/postfixadmin/setup.php you'll see this - + +![Initial setup greeting page](setup-step1.png?raw=true "Initial setup load") + +After creating and adding the setup password hash into your config file, and then logging into the setup page with that password, you should see : + +![Setup after auth](setup-step2.png?raw=true "Setup after auth") + +If there are any hosting errors, or issues with your environment, they may be listed here. + +Create a new admin account using your setup password .... then you can login as an admin and start creating domains and mailboxes. + +## 2. As an Admin user + +### Login + +![Admin Login](admin-login.png?raw=true "Admin Login") + +### Welcome page + +![Admin Welcome](admin-welcome.png?raw=true "Admin welcome") + +### View other admins + +![Admin list](admin-list.png?raw=true "Admin list") + + +### View mailboxes and aliases for domain + +![Virtual overview](mailboxes-and-forwards-for-domain.png?raw=true "Viewing aliases and mailboxes for a domain") + +### Add mailbox + +You can create as many mailboxes as you want ... + +![Mailbox adding](mailbox-adding.png?raw=true "Creating a new mailbox") + + +### Add aliases (forwards) + +![Foward adding](create-new-alias.png?raw=true "Creating a new forward") + +### Add Fetchmail config for mailbox + +![Setup Fetchmail](fetchmail-new-config.png?raw=true "Fetchmail settings") + +### Add a Domain Key for use with OpenDKIM + +![Add Domain Key](dkim-add-domain-key.png?raw=true "Fetchmail settings") + +### Add a Sign Table Entry for use with OpenDKIM + +![Add Sign Table Entry](dkim-add-sign-table-entry.png?raw=true "Fetchmail settings") + + +## 3. As a User + +### Login + +![User loginl](users-login.png?raw=true "User login") + +### Welcome page + + +![User welcome](users-welcome.png?raw=true "User welcome") + +### Change your mail forward + +![User - edit mail forward(s)](users-edit-mail-forward.png?raw=true "User mail forwards") + +### Set / Unset autoresponse (Vacation) + +![User - autoresponder](users-enable-vacation-autoresponse.png?raw=true "User setup autoresponder") + +### I forgot my password + + +![User - forgot password](users-forgotten-password.png?raw=true "User forgot password") diff --git a/DOCUMENTS/screenshots/README.txt b/DOCUMENTS/screenshots/README.txt deleted file mode 100644 index e97b5262..00000000 --- a/DOCUMENTS/screenshots/README.txt +++ /dev/null @@ -1,2 +0,0 @@ -Random Screenshots taken on 2007/09/25, using a version of Postfixadmin from subversion. - diff --git a/DOCUMENTS/screenshots/admin-list.png b/DOCUMENTS/screenshots/admin-list.png new file mode 100644 index 00000000..f659f659 Binary files /dev/null and b/DOCUMENTS/screenshots/admin-list.png differ diff --git a/DOCUMENTS/screenshots/admin-login.png b/DOCUMENTS/screenshots/admin-login.png new file mode 100644 index 00000000..895d40cf Binary files /dev/null and b/DOCUMENTS/screenshots/admin-login.png differ diff --git a/DOCUMENTS/screenshots/admin-welcome.png b/DOCUMENTS/screenshots/admin-welcome.png new file mode 100644 index 00000000..396ba9ae Binary files /dev/null and b/DOCUMENTS/screenshots/admin-welcome.png differ diff --git a/DOCUMENTS/screenshots/create-new-alias.png b/DOCUMENTS/screenshots/create-new-alias.png new file mode 100644 index 00000000..3876bb9c Binary files /dev/null and b/DOCUMENTS/screenshots/create-new-alias.png differ diff --git a/DOCUMENTS/screenshots/dkim-add-domain-key.png b/DOCUMENTS/screenshots/dkim-add-domain-key.png new file mode 100644 index 00000000..5e749e1c Binary files /dev/null and b/DOCUMENTS/screenshots/dkim-add-domain-key.png differ diff --git a/DOCUMENTS/screenshots/dkim-add-sign-table-entry.png b/DOCUMENTS/screenshots/dkim-add-sign-table-entry.png new file mode 100644 index 00000000..b01add82 Binary files /dev/null and b/DOCUMENTS/screenshots/dkim-add-sign-table-entry.png differ diff --git a/DOCUMENTS/screenshots/domain-audit-log.png b/DOCUMENTS/screenshots/domain-audit-log.png new file mode 100644 index 00000000..477047bd Binary files /dev/null and b/DOCUMENTS/screenshots/domain-audit-log.png differ diff --git a/DOCUMENTS/screenshots/domain-edit.png b/DOCUMENTS/screenshots/domain-edit.png new file mode 100644 index 00000000..3d1ebb5d Binary files /dev/null and b/DOCUMENTS/screenshots/domain-edit.png differ diff --git a/DOCUMENTS/screenshots/domain-list.png b/DOCUMENTS/screenshots/domain-list.png new file mode 100644 index 00000000..84d33960 Binary files /dev/null and b/DOCUMENTS/screenshots/domain-list.png differ diff --git a/DOCUMENTS/screenshots/fetchmail-new-config.png b/DOCUMENTS/screenshots/fetchmail-new-config.png new file mode 100644 index 00000000..428fb021 Binary files /dev/null and b/DOCUMENTS/screenshots/fetchmail-new-config.png differ diff --git a/DOCUMENTS/screenshots/mailbox-adding.png b/DOCUMENTS/screenshots/mailbox-adding.png new file mode 100644 index 00000000..55448e7f Binary files /dev/null and b/DOCUMENTS/screenshots/mailbox-adding.png differ diff --git a/DOCUMENTS/screenshots/mailboxes-and-forwards-for-domain.png b/DOCUMENTS/screenshots/mailboxes-and-forwards-for-domain.png new file mode 100644 index 00000000..44cd94a5 Binary files /dev/null and b/DOCUMENTS/screenshots/mailboxes-and-forwards-for-domain.png differ diff --git a/DOCUMENTS/screenshots/postfixadmin-admin-create-alias.jpg b/DOCUMENTS/screenshots/postfixadmin-admin-create-alias.jpg deleted file mode 100644 index a1a087fa..00000000 Binary files a/DOCUMENTS/screenshots/postfixadmin-admin-create-alias.jpg and /dev/null differ diff --git a/DOCUMENTS/screenshots/postfixadmin-admin-create-domain.jpg b/DOCUMENTS/screenshots/postfixadmin-admin-create-domain.jpg deleted file mode 100644 index 7a2f4e53..00000000 Binary files a/DOCUMENTS/screenshots/postfixadmin-admin-create-domain.jpg and /dev/null differ diff --git a/DOCUMENTS/screenshots/postfixadmin-admin-create-mailbox.jpg b/DOCUMENTS/screenshots/postfixadmin-admin-create-mailbox.jpg deleted file mode 100644 index 0e598ece..00000000 Binary files a/DOCUMENTS/screenshots/postfixadmin-admin-create-mailbox.jpg and /dev/null differ diff --git a/DOCUMENTS/screenshots/postfixadmin-admin-domain-list.jpg b/DOCUMENTS/screenshots/postfixadmin-admin-domain-list.jpg deleted file mode 100644 index 5d4fa15a..00000000 Binary files a/DOCUMENTS/screenshots/postfixadmin-admin-domain-list.jpg and /dev/null differ diff --git a/DOCUMENTS/screenshots/postfixadmin-admin-virtual-list.jpg b/DOCUMENTS/screenshots/postfixadmin-admin-virtual-list.jpg deleted file mode 100644 index daa2336a..00000000 Binary files a/DOCUMENTS/screenshots/postfixadmin-admin-virtual-list.jpg and /dev/null differ diff --git a/DOCUMENTS/screenshots/postfixadmin-inital-welcome.jpg b/DOCUMENTS/screenshots/postfixadmin-inital-welcome.jpg deleted file mode 100644 index 7f2fff85..00000000 Binary files a/DOCUMENTS/screenshots/postfixadmin-inital-welcome.jpg and /dev/null differ diff --git a/DOCUMENTS/screenshots/postfixadmin-mail-admin-login.jpg b/DOCUMENTS/screenshots/postfixadmin-mail-admin-login.jpg deleted file mode 100644 index 19ef94a0..00000000 Binary files a/DOCUMENTS/screenshots/postfixadmin-mail-admin-login.jpg and /dev/null differ diff --git a/DOCUMENTS/screenshots/postfixadmin-user-change-forward.jpg b/DOCUMENTS/screenshots/postfixadmin-user-change-forward.jpg deleted file mode 100644 index 262a6fb9..00000000 Binary files a/DOCUMENTS/screenshots/postfixadmin-user-change-forward.jpg and /dev/null differ diff --git a/DOCUMENTS/screenshots/postfixadmin-user-overview.jpg b/DOCUMENTS/screenshots/postfixadmin-user-overview.jpg deleted file mode 100644 index 1b2069fa..00000000 Binary files a/DOCUMENTS/screenshots/postfixadmin-user-overview.jpg and /dev/null differ diff --git a/DOCUMENTS/screenshots/postfixadmin-user-vacation.jpg b/DOCUMENTS/screenshots/postfixadmin-user-vacation.jpg deleted file mode 100644 index 44cc9d99..00000000 Binary files a/DOCUMENTS/screenshots/postfixadmin-user-vacation.jpg and /dev/null differ diff --git a/DOCUMENTS/screenshots/setup-step1.png b/DOCUMENTS/screenshots/setup-step1.png new file mode 100644 index 00000000..c8926fd4 Binary files /dev/null and b/DOCUMENTS/screenshots/setup-step1.png differ diff --git a/DOCUMENTS/screenshots/setup-step2.png b/DOCUMENTS/screenshots/setup-step2.png new file mode 100644 index 00000000..ec7505d0 Binary files /dev/null and b/DOCUMENTS/screenshots/setup-step2.png differ diff --git a/DOCUMENTS/screenshots/users-edit-mail-forward.png b/DOCUMENTS/screenshots/users-edit-mail-forward.png new file mode 100644 index 00000000..c7364d94 Binary files /dev/null and b/DOCUMENTS/screenshots/users-edit-mail-forward.png differ diff --git a/DOCUMENTS/screenshots/users-enable-vacation-autoresponse.png b/DOCUMENTS/screenshots/users-enable-vacation-autoresponse.png new file mode 100644 index 00000000..c7f7d051 Binary files /dev/null and b/DOCUMENTS/screenshots/users-enable-vacation-autoresponse.png differ diff --git a/DOCUMENTS/screenshots/users-forgotten-password.png b/DOCUMENTS/screenshots/users-forgotten-password.png new file mode 100644 index 00000000..f03f69b1 Binary files /dev/null and b/DOCUMENTS/screenshots/users-forgotten-password.png differ diff --git a/DOCUMENTS/screenshots/users-login.png b/DOCUMENTS/screenshots/users-login.png new file mode 100644 index 00000000..5c43e93c Binary files /dev/null and b/DOCUMENTS/screenshots/users-login.png differ diff --git a/DOCUMENTS/screenshots/users-welcome.png b/DOCUMENTS/screenshots/users-welcome.png new file mode 100644 index 00000000..9b324773 Binary files /dev/null and b/DOCUMENTS/screenshots/users-welcome.png differ diff --git a/INSTALL.TXT b/INSTALL.TXT index d3d3c334..d7f52237 100644 --- a/INSTALL.TXT +++ b/INSTALL.TXT @@ -7,14 +7,13 @@ REQUIREMENTS ------------ -- Postfix 2.0 or higher. -- Apache 1.3.27 / Lighttpd 1.3.15 or higher. -- PHP 5.1.2 or higher. +- Postfix +- Apache / Lighttpd +- PHP 7.0 or greater (for web server) - one of the following databases: - - MySQL 3.23 or higher (5.x recommended) - - MariaDB (counts as MySQL ;-) - - PostgreSQL 7.4 (or higher) - - SQLite 3.12 (or higher) + - MariaDB/MySQL + - PostgreSQL + - SQLite READ THIS FIRST! @@ -33,28 +32,39 @@ There are also lots of HOWTOs around the web. Be warned that many of them Please stick to the PostfixAdmin documentation, and use those HOWTOs only if you need some additional information that is missing in the PostfixAdmin DOCUMENTS/ folder. - - http://bliki.rimuhosting.com/space/knowledgebase/linux/mail/postfixadmin+on+debian+sarge (Postfix+MySQL+Postfixadmin+Dovecot) - - http://en.gentoo-wiki.com/wiki/Virtual_mail_server_using_Postfix,_Courier_and_PostfixAdmin (Postfix+MySQL+Postfixadmin+Courier) - + - https://www.linuxbabe.com/redhat/postfixadmin-create-virtual-mailboxes-centos-mail-server (Postfix+MySQL+Postfixadmin+Dovecot) 1. Unarchive new Postfix Admin ------------------------------ -(if you installed PostfixAdmin as RPM or DEB package, you can obviously skip this step.) +(if you installed PostfixAdmin as RPM or DEB package, you can skip this step.) -Assuming we are installing Postfixadmin into /srv/postfixadmin, then something like this should work : +Assuming we are installing Postfixadmin into /srv/postfixadmin, then something like this should work. Please check https://github.com/postfixadmin/postfixadmin/releases to get the latest stable release first (the 3.2.10 version/url below is probably stale) $ cd /srv/ - $ wget -O postfixadmin.tgz https://github.com/postfixadmin/postfixadmin/archive/postfixadmin-3.2.tar.gz + $ wget -O postfixadmin.tgz https://github.com/postfixadmin/postfixadmin/archive/postfixadmin-3.3.10.tar.gz $ tar -zxvf postfixadmin.tgz - $ mv postfixadmin-postfixadmin-3.2 postfixadmin + $ mv postfixadmin-postfixadmin-3.3 postfixadmin Alternatively : $ cd /srv $ git clone https://github.com/postfixadmin/postfixadmin.git $ cd postfixadmin - $ git checkout postfixadmin-3.2.2 + $ git checkout postfixadmin-3.3.10 + +If you're happy to try out newer functionality and perhaps hit unfixed bugs, you can try the 'master' branch by a `git checkout master` (or don't run the final git checkout in the above list). + +If you're using the 'master' branch, you'll need to also run : + +```bash +/bin/bash install.sh +``` + +Which will + * install the 'composer' tool locally (composer.phar) and + * download dependent PHP libraries. + * create a templates_c directory if one does not exist. 2. Setup Web Server ------------------- @@ -71,7 +81,7 @@ or setup an alias in your webserver config. For Apache, use: ------------------- With your chosen/preferred database server (i.e. MySQL or PostgreSQL), -you need to create a new database. A good name for this could be : +you need to create a new database. A good name for this could be: postfix @@ -79,21 +89,25 @@ The mechanics of creating the database vary depending on which server you are using. Most users will find using phpMyAdmin or phpPgAdmin the easiest route. -If you wish to use the command line, you'll need to do something like : +If you wish to use the command line, you'll need to do something like: For MySQL: CREATE DATABASE postfix; CREATE USER 'postfix'@'localhost' IDENTIFIED BY 'choose_a_password'; GRANT ALL PRIVILEGES ON `postfix` . * TO 'postfix'@'localhost'; + FLUSH PRIVILEGES; For PostgreSQL: CREATE USER postfix WITH PASSWORD 'whatever'; CREATE DATABASE postfix OWNER postfix ENCODING 'unicode'; For SQLite: - $mkdir /srv/postfixadmin/database - $touch /srv/postfixadmin/database/postfixadmin.db - $sudo chown -R www-data:www-data /srv/postfixadmin/database + +```bash + mkdir /srv/postfixadmin/database + touch /srv/postfixadmin/database/postfixadmin.db + sudo chown -R www-data:www-data /srv/postfixadmin/database +``` (both the directory and the database need to be writeable) 4. Configure PostfixAdmin so it can find the database @@ -103,10 +117,11 @@ Create /srv/postfixadmin/config.local.php file for your local configuration: @@ -115,7 +130,7 @@ See config.inc.php for all available config options and their default value. You can also edit config.inc.php instead of creating a config.local.php, but this will make updates harder and is therefore not recommended. -The most important settings are those for your database server. +The most important settings are those for your database server, and the hashing mechanism to be used to store passwords in your database. You must also change the line that says : @@ -137,32 +152,64 @@ The easiest way to do this is $ mkdir -p /srv/postfixadmin/templates_c $ chown -R www-data /srv/postfixadmin/templates_c -(If you're using e.g. CentOS or another distribution which enables SELinux, something like the following may be necessary as well : -```chcon -R -t httpd_sys_content_rw_t /usr/share/postfixadmin/templates_c``` -) + +4a. SELinux (CentOS/Fedora etc) +------------------------------- + +If you're using e.g. CentOS (or another distribution) which enables SELinux, something like the following will be necessary: + + +```bash + semanage fcontext -a -t httpd_sys_content_t "/srv/postfixadmin(/.*)?" + semanage fcontext -a -t httpd_sys_rw_content_t "/srv/postfixadmin/templates_c(/.*)?" + restorecon -R /srv/postfixadmin +``` + +(Allow the webserver to read /srv/postfixadmin/* and write to /srv/postfixadmin/templates_c/*) + + +And if the webserver (PHP) needs to make network connections out to a database server, you'll probably need this: + +```bash + semanage boolean -m --on httpd_can_network_connect_db +```` + +If additionally, needing the webserver (PHP) to talk to an imap server, then you'll probably also need: + +```bash + semanage boolean -m --on httpd_can_network_connect +```` + 5. Check settings, and create Admin user ---------------------------------------- Hit http://yourserver.tld/postfixadmin/setup.php in a web browser. -You should see a list of 'OK' messages. +You need to generate a 'setup_password' which is your way of proving you are the 'admin' responsible for this install. Alternatively, run : -The setup.php script will attempt to create the database structure -(or upgrade it if you're coming from a previous version). +```bash + php -r "echo password_hash('some password here', PASSWORD_DEFAULT);" +``` -Assuming everything is OK you can specify a password (which you'll -need to use setup.php again in the future); when you submit the form, -the hashed value (which you need to enter into config.inc.php is echoed -out - with appropriate instructions on what to do with it). +and put the output of that into your config.local.php file - e.g. -create the admin user using the form displayed. +```PHP +$CONF['setup_password'] = '$2y$10$3ybxsh278eAlZKlLf8Zp9e4hmuDaW/TCYd5IZagV7coeAfzBW/GzC'; +``` + +You need to specify that same password in the setup.php page, and click 'Login with setup_password' + +You should then see a list of 'OK' messages. + +The setup.php script will attempt to create the database structure (or upgrade it if you're coming from a previous version). + +You can then create an Superadmin user (or add another), using the form displayed (you'll need to re-enter the setup password). 6. Use PostfixAdmin ------------------- -This is all that is needed. Fire up your browser and go to the site that you -specified to host Postfix Admin. +This is all that is needed. Fire up your browser and go to the site that you specified to host Postfix Admin. Login with the Superadmin user you've just created. 7. Integration with Postfix, Dovecot etc. ----------------------------------------- @@ -184,7 +231,7 @@ See config.inc.php - see xmlrpc_enabled key (defaults to off). You'll need to install a copy of the Zend Framework (version 1.12.x) within Postfixadmin or your PHP include_path (see header within xmlrpc.php). NOTE: The XMLRPC interface is _not compatible_ with Zend Framework version 2.x. -You'll need to enable the xmlrpc link (see config.inc.php) +You'll need to enable the xmlrpc link (see config.inc.php). 8. More information ------------------- @@ -192,8 +239,8 @@ You'll need to enable the xmlrpc link (see config.inc.php) The code and issue tracker is on GitHub: https://github.com/postfixadmin/postfixadmin -IRC - a community of people may be able to help in #postfixadmin on irc.freenode.net. - See http://webchat.freenode.net/ +IRC - a community of people may be able to help in #postfixadmin on Libera.Chat. + See https://web.libera.chat/ Legacy forum posts are on SourceForce at https://sourceforge.net/projects/postfixadmin diff --git a/README.md b/README.md index 3657f5f6..fb5cb4a8 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ ![GitHubBuild](https://github.com/postfixadmin/postfixadmin/workflows/GitHubBuild/badge.svg) -[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/postfixadmin/Lobby) [![Coverage Status](https://coveralls.io/repos/github/postfixadmin/postfixadmin/badge.svg?branch=master)](https://coveralls.io/github/postfixadmin/postfixadmin?branch=master) ![GitHub repo size](https://img.shields.io/github/repo-size/postfixadmin/postfixadmin) -[![Chat](https://img.shields.io/badge/chat-on%20freenode-brightgreen.svg)](https://kiwiirc.com/nextclient/irc.freenode.net/#postfixadmin) - +[![IRC Chat - #postfixadmin](https://img.shields.io/badge/IRC%20libera-brightgreen.svg)](https://web.libera.chat/#postfixadmin) + + + # PostfixAdmin An open source, web based interface for managing domains/mailboxes/aliases etc on a Postfix based mail server. @@ -27,15 +28,34 @@ Integrates with : - Users have the ability to login, change their password or vacation (out of office) status. - Integration with Squirrelmail / Roundcube (via plugins) - Optional XMLRPC based API - - Supports PHP5.6+ + - Supports PHP7.2+ (older versions of PHP should work with older releases) + +[Some screenshots of Postfixadmin in action (as admin and user)](DOCUMENTS/screenshots/README.md) + +## Releases / Development note + + - While you can install PostfixAdmin from 'git' using the 'master' branch, 'master' is our main development version. It may work. It may contain funky new exciting stuff. It may "eat your data". + - If you want an easy life, use a published release - see: https://github.com/postfixadmin/postfixadmin/releases or it's branch (e.g. postfixadmin_3.3) + - Latest significant changes should be listed in the appropriate CHANGELOG.TXT file. ## Useful Links - [Probably all you need to read (pdf)](http://blog.cboltz.de/uploads/postfixadmin-30-english.pdf) - - http://postfixadmin.sf.net - the current homepage for the project - [Docker Images](https://github.com/postfixadmin/docker) - [What is it? (txt)](/DOCUMENTS/POSTFIXADMIN.txt) - [Installation instructions](/INSTALL.TXT) - [Wiki](https://sourceforge.net/p/postfixadmin/wiki/) - - [Mailing list](https://sourceforge.net/p/postfixadmin/discussion/676076) - - [IRC channel](irc://irc.freenode.net/postfixadmin) (#postfixadmin on irc.freenode.net). + - [IRC channel](irc://irc.libera.chat/#postfixadmin) (#postfixadmin on Libera.chat). + + +## Related Projects + + - https://github.com/aqeltech/Dockerised-GUI-Mailserver + - https://github.com/mailserver2/mailserver + mailserver2/mailserver is a simple and full-featured mail server build as a set of multiple docker images. Features: + Postfix, PostfixAdmin, Dovecot, Rspamd, Clamav, Zeyple, Sieve, Fetchmail, Rainloop, Unbound/NSD, Træfik, {Let's Encrypt,custom,Self-signed Certificate} SSL, Supports PostgeSQL, MySQL, (beta) LDAP backends. Automated builds on DockerHub and Integration tests with Travis CI + + + + + diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..44496b78 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +## Supported Versions + +As of 2021/08 - + +| Version | Supported | +| ------- | ------------------ | +| 'dev' | :x: GitHub 'master' branch, use at own risk! | +| 3.3.x | :white_check_mark: | +| 3.2.x | Security/critical fixes only | +| < 3.2.x | :x: | + +Releases are published at : + + * https://github.com/postfixadmin/postfixadmin/releases + * ocassionally at https://sourceforge.net/projects/postfixadmin/ - sometimes with RPM/DEB packages. + +## Reporting a Vulnerability + +Either message GingerDog or cboltz on the PostfixAdmin libera chat - IRC channel, or email. Email addresses can be found in the 'git' changelog. + + diff --git a/VIRTUAL_VACATION/Contributions.txt b/VIRTUAL_VACATION/Contributions.txt index 699af0ed..d8f5f076 100644 --- a/VIRTUAL_VACATION/Contributions.txt +++ b/VIRTUAL_VACATION/Contributions.txt @@ -91,3 +91,15 @@ Additional authors: sending vacation mails, even if one or multiple of the recipients the alias points to has vacation currently active. +2022-10-20 Jan Kruis + Add configuration parameter $replace_from ,$replace_until and $date_format for the subroutine replace_string + the subroutine replace_string replaces in the bodytext the text defined at $replace_from and $replace_until with the date of active from and active until in the format specified at $date_format. + + Add configuration parameter $account_check and $account_name if account_check is set it will add the value of name of the mailbox in front of the email and it is shown as sender, + if no name is specified at the mailbox, the variable friendly-name is placed in front of the email and it is shown as sender otherwise the email will show as sender. + +2023-08-18 Andrew Ruthven + Minor improvements to the use of Log4Perl + + Fix handling of the default $smtp_client setthing of 'localhost' + if we aren't connecting to the SMTP server on localhost. diff --git a/VIRTUAL_VACATION/vacation.pl b/VIRTUAL_VACATION/vacation.pl index 293bc95e..aaa4dd08 100644 --- a/VIRTUAL_VACATION/vacation.pl +++ b/VIRTUAL_VACATION/vacation.pl @@ -1,6 +1,6 @@ #!/usr/bin/perl # -# Virtual Vacation 4.2 +# Virtual Vacation 4.2.1 # # See Contributions.txt for a list of contributions. # https://github.com/postfixadmin/postfixadmin/blob/master/VIRTUAL_VACATION/Contributions.txt @@ -28,9 +28,11 @@ use Email::Sender::Transport::SMTP; use Email::Simple; use Email::Simple::Creator; use Try::Tiny; -use Log::Log4perl qw(get_logger :levels); +use Log::Log4perl qw(get_logger :levels :nowarn); use File::Basename; use Net::DNS; +use Time::Piece; + # ========== begin configuration ========== # IMPORTANT: If you put passwords into this script, then remember @@ -53,16 +55,25 @@ our $vacation_domain = 'autoreply.example.org'; our $recipient_delimiter = '+'; +# SMTP server used to send vacation e-mails, leave empty to look up the MX of the sending domain and deliver it directly (might break DKIM signatures, mail archiving etc.) +our $smtp_server = 'localhost'; # port to connect to; defaults to 25 for non-SSL, 465 for 'ssl', 587 for 'starttls' our $smtp_server_port = 25; -# this is the helo we [the vacation script] use on connection; you may need to change this to your hostname or something, -# depending upon what smtp helo restrictions you have in place within Postfix. +# this is the local address to connect from our $smtp_client = 'localhost'; +# this is the helo we [the vacation script] use on connection; you may need to change this to your hostname or something, +# depending upon what SMTP helo restrictions you have in place within Postfix. +our $smtp_helo = 'localhost.localdomain'; + # send mail encrypted or plaintext -# if 'starttls', use STARTTLS; if 'ssl' (or 1), connect securely; otherwise, no security -our $smtp_ssl = 'starttls'; +# if 1, connect securely via SSL +# if 'starttls', connect using starttls (plaintext+neg TLS) +# if 'maybestarttls' - try starttls, otherwise plaintext. +# if 0 (default), plain text, no security +# See also : https://metacpan.org/pod/Email::Sender::Transport::SMTP +our $smtp_ssl = 0; # maximum time in secs to wait for server; default is 120 our $smtp_timeout = '120'; @@ -72,15 +83,22 @@ our $smtp_authid = ''; # sasl_password: the password to use for auth; required if username is provided our $smtp_authpwd = ''; + + # This specifies the mail 'from' name which is shown to recipients of vacation replies. -# If you leave it empty, the vacation mail will contain: +# If you leave it empty, the vacation mail will contain: # From: # If you specify something here you'd instead see something like : # From: Some Friendly Name our $friendly_from = ''; +# If accountname_check is set it will add the account name in front of the email "AccountName " if the accountname is not a empty string +# otherwise $friendly_name will be set in front of the email if not empty. +our $accountname_check = 0; +our $account_name = ''; # leave this blank it will be filled with 'name' field from table 'mailbox' + # Set to 1 to enable logging to syslog. -our $syslog = 0; +our $syslog = 1; # path to logfile, when empty logging is suppressed # change to e.g. /dev/null if you want nothing logged. @@ -104,7 +122,7 @@ our $interval = 0; # be answered when $custom_noreply_pattern is set to 1. # default = 0 our $custom_noreply_pattern = 0; -our $noreply_pattern = 'bounce|do-not-reply|facebook|linkedin|list-|myspace|twitter'; +our $noreply_pattern = 'bounce|do-not-reply|facebook|linkedin|list-|myspace|twitter'; # Never send vacation mails for the following recipient email addresses. # Useful for e.g. aliases pointing to multiple recipients which have vacation active @@ -113,8 +131,23 @@ our $noreply_pattern = 'bounce|do-not-reply|facebook|linkedin|list-|myspace|twit # default = '' # preventing vacation notifications for recipient info@example.org would look like this: # our $no_vacation_pattern = 'info\@example\.org'; -our $no_vacation_pattern = 'info\@example\.org'; +our $no_vacation_pattern = 'info\@example\.org'; +# +# The subroutine replace_string replaces in the body text the text defined at $replace_from and $replace_until with the date of activefrom and activeuntil in the format specified at $date_format. +# +# Like : +# +# %Y/%m/%d => 2022/10/01 +# %d-%m-%Y => 01-10-2022 +# %d %b %y => 01 Oct 2022 +# +# see for more option +# https://www.tutorialspoint.com/perl/perl_date_time.htm + +our $replace_from = "<%From_Date>"; #You can place your own replacement text here for active from +our $replace_until = "<%Until_Date>"; #You can place your own replacement text here for active until +our $date_format = '%Y-%m-%d'; # instead of changing this script, you can put your settings to /etc/mail/postfixadmin/vacation.conf # or /etc/postfixadmin/vacation.conf just use Perl syntax there to fill the variables listed above @@ -130,6 +163,15 @@ if (-f '/etc/mail/postfixadmin/vacation.conf') { # =========== end configuration =========== +# Try and enable log_to_file if syslog is disabled +if ($syslog == 0 && $log_to_file == 0 && ( + (-f $logfile && -w $logfile) + || + (! -f $logfile && -w dirname($logfile))) + ) { + $log_to_file=1; +} + if($log_to_file == 1) { if (( ! -w $logfile ) && (! -w dirname($logfile))) { # Cannot log; no where to write to. @@ -159,7 +201,7 @@ if($test_mode == 1) { $appender->layout($log_layout); $logger->add_appender($appender); $logger->debug('Test mode enabled'); - + } else { $logger = get_logger(); if($log_to_file == 1) { @@ -208,7 +250,7 @@ if (!$dbh) { my $db_true; # MySQL and PgSQL use different values for TRUE, and unicode support... if ($db_type eq 'mysql') { - $dbh->do('SET CHARACTER SET utf8;'); + $dbh->do('SET CHARACTER SET utf8mb4;'); $db_true = '1'; } else { # Pg $dbh->do("SET CLIENT_ENCODING TO 'UTF8'"); @@ -220,7 +262,7 @@ if ($db_type eq 'mysql') { my $loopcount=0; # -# Get interval_time for email user from the vacation table +# Get interval_time for email user from the vacation table # sub get_interval { my ($to) = @_; @@ -312,7 +354,7 @@ sub already_notified { } # -# Check to see if there is a vacation record against a specific email address. +# Check to see if there is a vacation record against a specific email address. # sub check_for_vacation { my ($email_to_check) =@_; @@ -323,9 +365,67 @@ sub check_for_vacation { return $rv; } +# +# Get Accountname stored in name of table mailbox +# +sub get_accountname { + my ($from_mailbox) =@_; + my $logger = get_logger(); -# try and determine if email address has vacation turned on; we -# have to do alias searching, and domain aliasing resolution for this. + my $query = qq{SELECT name FROM mailbox WHERE username=? }; + my $stm = $dbh->prepare($query) or panic_prepare($query); + $stm->execute($to) or panic_execute($query,"username='$from_mailbox'"); + my @row = $stm->fetchrow_array; + my $rv = $stm->rows; + + my $accountname = $row[0]; + + return $accountname; +} + +# +# Replace <%From_Date> with date part of activefrom from the vacation table on base of email +# Replace <%Until_Date> with date part of activeuntil from the vacation table on base of email +# +# The variable $replace_from and $replace_until will have the <%From_Date> and <%Until_Date> replacement text + +sub replace_string { + my ($to) =@_; + my $logger = get_logger(); + + my $query = qq{SELECT body,activefrom,activeuntil FROM vacation WHERE email=? }; + my $stm = $dbh->prepare($query) or panic_prepare($query); + $stm->execute($to) or panic_execute($query,"email='$to'"); + my @row = $stm->fetchrow_array; + my $rv = $stm->rows; + + my $vacation_body = $row[0]; + my $f_date = $row[1]; + my $u_date = $row[2]; +# +# Note !! do not replace '%Y-%m-%d' with date_format because this is the format that f_date and u_date are are filled with date in this format +# $date_format is used to display the dates in your choice of format. +# + my $date_f = Time::Piece->strptime($f_date,'%Y-%m-%d'); + $f_date = $date_f->strftime($date_format); + my $date_u = Time::Piece->strptime($u_date,'%Y-%m-%d'); + $u_date = $date_u->strftime($date_format); + + $vacation_body = replace_a_string($vacation_body,$replace_from,$f_date); + $vacation_body = replace_a_string($vacation_body,$replace_until,$u_date); + + $logger->debug ("From = $f_date Until = $u_date ** for Email = $to Body = $vacation_body "); + + return $vacation_body; +} + +sub replace_a_string { + my ( $result,$what_goes_out,$what_goes_in) =@_; + $result =~ s/$what_goes_out/$what_goes_in/ig; + return $result; +} +# try and determine if email address has vacation turned on; +# we have to do alias searching, and domain aliasing resolution for this. # If found, return ($num_matches, $real_email); sub find_real_address { my ($email) = @_; @@ -438,9 +538,9 @@ sub send_vacation_email { } $logger->debug("Will send vacation response for $orig_messageid: FROM: $email (orig_to: $orig_to), TO: $orig_from; VACATION SUBJECT: $row[0] ; VACATION BODY: $row[1]"); - + my $subject = $row[0]; - $subject = Encode::decode_utf8( $subject ) if( !Encode::is_utf8( $subject ) ); + $subject = Encode::decode_utf8($subject) if (!Encode::is_utf8($subject)); $orig_subject = decode("mime-header", $orig_subject); $subject =~ s/\$SUBJECT/$orig_subject/g; if ($subject ne $row[0]) { @@ -448,24 +548,36 @@ sub send_vacation_email { } my $body = $row[1]; - $body = Encode::decode_utf8( $body ) if( !Encode::is_utf8( $body ) ); + $body = Encode::decode_utf8($body) if (!Encode::is_utf8($body)); + + ## Replace <%Time> marks in the Body string with dates form the table vacation + $body = replace_string ($email); + my $from = $email; my $to = $orig_from; - # part of the username in the email && part of the domain in the email - my ($email_username_part, $email_domain_part) = split(/@/, $email); + if ($smtp_server eq '') { + # part of the username in the email && part of the domain in the email + my (undef, $email_domain_part) = split(/@/, $email); - my $resolver = Net::DNS::Resolver->new; - my @mx = mx($resolver, $email_domain_part); - my $smtp_server; - if (@mx) { - $smtp_server = @mx[0]->exchange; - $logger->debug("Found MX record <$smtp_server> for user <$email>!"); - } else { - $logger->error("Unable to find MX record for user <$email>, error message: ".$resolver->errorstring); - exit(0); + my $resolver = Net::DNS::Resolver->new; + my @mx = mx($resolver, $email_domain_part); + if (@mx) { + $smtp_server = @mx[0]->exchange; + $logger->debug("Found MX record <$smtp_server> for user <$email>!"); + } else { + $logger->error("Unable to find MX record for user <$email>, error message: ".$resolver->errorstring); + exit(0); + } } + # We can't use localhost as the local bind interface if we're trying + # to connect to an SMTP server that isn't on localhost, we won't be + # able to route to that server. + if ($smtp_server ne 'localhost' && $smtp_client eq 'localhost') { + $smtp_client = undef; + }; + my $smtp_params = { host => $smtp_server, port => $smtp_server_port, @@ -475,6 +587,7 @@ sub send_vacation_email { ssl => $smtp_ssl, timeout => $smtp_timeout, localaddr => $smtp_client, + helo => $smtp_helo, debug => 0, }; @@ -486,16 +599,34 @@ sub send_vacation_email { my $transport = Email::Sender::Transport::SMTP->new($smtp_params); - $subject = Encode::encode_utf8( $subject ) if( Encode::is_utf8( $subject ) ); - $body = Encode::encode_utf8( $body ) if( Encode::is_utf8( $body ) ); +# QUESTION !! +#I think the two lines below (which I have commented out in) are no longer necessary, they appear earlier in the script see line 490 and 498 +# +# --> $subject = Encode::encode_utf8($subject) if(Encode::is_utf8($subject)); +# --> $body = Encode::encode_utf8($body) if(Encode::is_utf8($body)); +# + my $email_from = $from ; + my $account_name = get_accountname($from); + + if ($friendly_from ne'') { + $email_from = encode_mimewords($friendly_from, 'Charset', 'UTF-8') . " <$from>"; + } + + if (($accountname_check ==1) and ($account_name ne '')) { + $email_from = encode_mimewords($account_name, 'Charset', 'UTF-8') . " <$from>"; + } + + $logger->debug("** From = $from Email_from = $email_from Friendly_name = $friendly_from Accountname = $account_name **\n"); + $email = Email::Simple->create( header => [ To => $to, - From => $from, + From => $email_from, Subject => encode_mimewords($subject, 'Charset', 'UTF-8'), Precedence => 'junk', 'Content-Type' => "text/plain; charset=utf-8", 'X-Loop' => 'Postfix Admin Virtual Vacation', + 'Auto-Submitted' => 'auto-replied', ], body => $body, ); @@ -512,7 +643,7 @@ sub send_vacation_email { if (@_) { $logger->error("Failed to send vacation response to $to from $from subject $subject: @_"); } else { - $logger->debug("Vacation response sent to $to from $from subject $subject sent\n"); + $logger->debug("Vacation response sent to $to from $from subject $subject Email $email_from sent\n"); } } } @@ -556,14 +687,14 @@ sub strip_address { sub panic_prepare { my ($arg) = @_; my $logger = get_logger(); - $logger->error("Could not prepare sql statement: '$arg'"); + $logger->error("Could not prepare SQL statement: '$arg'"); exit(0); } sub panic_execute { my ($arg,$param) = @_; my $logger = get_logger(); - $logger->error("Could not execute sql statement - '$arg' with parameters '$param'"); + $logger->error("Could not execute SQL statement - '$arg' with parameters '$param'"); exit(0); } @@ -573,7 +704,7 @@ sub check_and_clean_from_address { my ($address) = @_; my $logger = get_logger(); - if($address =~ /^(noreply|postmaster|mailer\-daemon|listserv|majordomo|owner\-|request\-|bounces\-)/i || + if($address =~ /^(noreply|no\-reply|do_not_reply|no_reply|postmaster|mailer\-daemon|listserv|majordomo|owner\-|request\-|bounces\-)/i || $address =~ /\-(owner|request|bounces)\@/i || ($custom_noreply_pattern == 1 && $address =~ /^.*($noreply_pattern).*/i) ) { $logger->debug("sender $address contains $1 - will not send vacation message"); @@ -587,6 +718,7 @@ sub check_and_clean_from_address { #$logger->debug("Address cleaned up to $address"); return $address; } + ########################### main ################################# # Take headers apart @@ -606,6 +738,7 @@ while () { elsif (/^message\-id:\s*(.*)\s*\n$/i) { $messageid = $1; $lastheader = \$messageid; } elsif (/^x\-spam\-(flag|status):\s+yes/i) { $logger->debug("x-spam-$1: yes found; exiting"); exit (0); } elsif (/^x\-facebook\-notify:/i) { $logger->debug('Mail from facebook, ignoring'); exit(0); } + elsif (/^x\-amazon\-mail\-relay\-type:\s*notification/i) { $logger->debug('Notification mail from Amazon, ignoring'); exit(0); } elsif (/^precedence:\s+(bulk|list|junk)/i) { $logger->debug("precedence: $1 found; exiting"); exit (0); } elsif (/^x\-loop:\s+postfix\ admin\ virtual\ vacation/i) { $logger->debug('x-loop: postfix admin virtual vacation found; exiting'); exit (0); } elsif (/^Auto\-Submitted:\s*no/i) { next; } @@ -617,7 +750,7 @@ while () { elsif (/^(x\-(avas\-spam|spamtest|crm114|razor|pyzor)\-status):\s+(spam)/i) { $logger->debug("$1: $3 found; exiting"); exit (0); } elsif (/^(x\-osbf\-lua\-score):\s+[0-9\/\.\-\+]+\s+\[([-S])\]/i) { $logger->debug("$1: $2 found; exiting"); exit (0); } elsif (/^x\-autogenerated:\s*reply/i) { $logger->debug('x-autogenerated found; exiting'); exit (0); } - elsif (/^x\-auto\-response\-suppress:\s*oof/i) { $logger->debug('x-auto-response-suppress: oof found; exiting'); exit (0); } + elsif (/^(x\-auto\-response\-suppress):\s*(oof|all)/i) { $logger->debug("$1: $2 found; exiting"); exit (0); } else {$lastheader = '' ; } } @@ -641,9 +774,9 @@ if(!$from || !$to || !$messageid || !$smtp_sender || !$smtp_recipient) { } $logger->debug("Email headers have to: '$to' and From: '$from'"); -if ($to =~ /^.*($no_vacation_pattern).*/i) { +if ($to =~ /^.*($no_vacation_pattern).*/i) { $logger->debug("Will not send vacation reply for messages to $to"); - exit(0); + exit(0); } $to = strip_address($to); diff --git a/check_mailpass_expiration.sh b/check_mailpass_expiration.sh deleted file mode 100644 index a7b33728..00000000 --- a/check_mailpass_expiration.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash -#Adapt to your setup - -POSTFIX_DB="postfix_test" -MYSQL_CREDENTIALS_FILE="postfixadmin.my.cnf" - -REPLY_ADDRESS=noreply@example.com - -# Change this list to change notification times and when ... -for INTERVAL in 30 14 7 -do - LOWER=$(( $INTERVAL - 1 )) - - QUERY="SELECT username,password_expiry FROM mailbox WHERE password_expiry > now() + interval $LOWER DAY AND password_expiry < NOW() + interval $INTERVAL DAY" - - mysql --defaults-extra-file="$MYSQL_CREDENTIALS_FILE" "$POSTFIX_DB" -B -e "$QUERY" | while read -a RESULT ; do - echo -e "Dear User, \n Your password will expire on ${RESULT[1]}" | mail -s "Password 30 days before expiration notication" -r $REPLY_ADDRESS ${RESULT[0]} - done - -done diff --git a/common.php b/common.php index 7e13da7a..6a4279a1 100644 --- a/common.php +++ b/common.php @@ -1,4 +1,7 @@ setAll($CONF); + +$PALANG = []; require_once("$incpath/languages/language.php"); require_once("$incpath/functions.inc.php"); -if (extension_loaded('Phar') && ( version_compare(PHP_VERSION, '7.0.0') < 0)) { - require_once("$incpath/lib/random_compat.phar"); -} if (defined('POSTFIXADMIN_CLI')) { $language = 'en'; # TODO: make configurable or autodetect from locale settings @@ -87,13 +86,11 @@ if (!empty($CONF['language_hook']) && function_exists($CONF['language_hook'])) { Config::write('__LANG', $PALANG); -unset($incpath); - if (!defined('POSTFIXADMIN_CLI')) { - if (!is_file(dirname(__FILE__) . "/lib/smarty.inc.php")) { - die("smarty.inc.php is missing! Something is wrong..."); + if (!isset($PALANG)) { + die("environment not setup correctly"); } - require_once(dirname(__FILE__) . "/lib/smarty.inc.php"); + Smarty_Autoloader::register(); } /* vim: set expandtab softtabstop=4 tabstop=4 shiftwidth=4: */ diff --git a/composer-update.sh b/composer-update.sh new file mode 100644 index 00000000..3ae32d6b --- /dev/null +++ b/composer-update.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# for github : +composer update --no-dev + +# for local testing/dev: +# composer update diff --git a/composer.json b/composer.json index 7ed17b9a..b8b15560 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,10 @@ "name": "postfixadmin/postfixadmin", "description": "web based administration interface for Postfix mail servers", "type": "project", - "license": "GPL-2.0", + "license": "GPL-2.0-only", + "config": { + "preferred-install":"dist" + }, "scripts": { "build" : [ "@check-format", @@ -11,32 +14,41 @@ "@psalm", "@test" ], - "check-format": "php-cs-fixer fix --ansi --dry-run --diff", - "format": "php-cs-fixer fix --ansi", - "lint": "@php ./vendor/bin/parallel-lint --exclude vendor/ --exclude lib/block_random_int.php --exclude lib/array_column.php .", - "test": "DATABASE=sqlite ./vendor/bin/phpunit --coverage-clover ./coverage.xml tests/", + "check-format": "@php vendor/bin/php-cs-fixer fix --ansi --dry-run --diff", + "format": "@php vendor/bin/php-cs-fixer fix --ansi", + "lint": "@php ./vendor/bin/parallel-lint --exclude vendor/ .", + "test": "@php ./vendor/bin/phpunit --coverage-clover ./clover.xml tests/", "test-fixup": "mkdir -p templates_c ; test -f config.local.php || touch config.local.php", - "psalm": "@php ./vendor/bin/psalm --no-cache --show-info=false " + "psalm": "@php ./vendor/bin/psalm.phar --no-cache --show-info=false " }, "require": { - "php": ">=7.0" + "php": ">=7.4", + "smarty/smarty": "^3|^4", + "postfixadmin/password-hashing": "^0.0.1", + "endroid/qr-code": "^4.6", + "spomky-labs/otphp": "^10.0" }, "require-dev": { "ext-mysqli": "*", "ext-sqlite3": "*", - "friendsofphp/php-cs-fixer": "*", - "jakub-onderka/php-parallel-lint": "^1.0", - "php": ">7.2.0", - "php-coveralls/php-coveralls" : "*", - "phpunit/phpunit": "^6|^7", - "vimeo/psalm":"^3.0", + "ext-mbstring": "*", + "friendsofphp/php-cs-fixer": "^3.0", + "php-parallel-lint/php-parallel-lint": "^1.0", + "php": ">= 7.4", + "php-coveralls/php-coveralls": "*", + "phpunit/phpunit": "8.*", + "psalm/phar":"^4.0", "shardj/zf1-future" : "^1.12" }, "autoload": { + "classmap" : [ "model/" ], "files": [ "config.inc.php", - "functions.inc.php", - "lib/smarty/libs/bootstrap.php" + "functions.inc.php" ] + }, + "support": { + "irc": "irc://irc.libera.chat/postfixadmin", + "issues": "https://github.com/postfixadmin/postfixadmin/issues" } } diff --git a/config.inc.php b/config.inc.php index 6f72fd21..bf41e189 100644 --- a/config.inc.php +++ b/config.inc.php @@ -141,13 +141,18 @@ $CONF['database_tables'] = array ( 'vacation' => 'vacation', 'vacation_notification' => 'vacation_notification', 'quota' => 'quota', - 'quota2' => 'quota2', + 'quota2' => 'quota2', + 'dkim' => 'dkim', + 'dkim_signing' => 'dkim_signing', ); // Site Admin // Define the Site Admin's email address below. -// This will be used to send emails from to create mailboxes and -// from Send Email / Broadcast message pages. +// This will be used to send emails from to +// * create mailboxes and +// * Send Email / Broadcast message pages and +// * In password reset emails. +// // Leave blank to send email from the logged-in Admin's Email address. $CONF['admin_email'] = ''; @@ -167,34 +172,47 @@ $CONF['admin_name'] = 'Postmaster'; $CONF['smtp_server'] = 'localhost'; $CONF['smtp_port'] = '25'; +// The communication layer used. +// +// 'plain' Everything in plain text (standard port: 25). +// 'tls' TLS/SSL from the very beginning (standard port: 465). +// 'starttls' "STARTTLS" in plain text and then TLS/SSL (standard port: 587). +$CONF['smtp_type'] = 'plain'; + // SMTP Client // Hostname (FQDN) of the server hosting Postfix Admin // Used in the HELO when sending emails from Postfix Admin $CONF['smtp_client'] = ''; -// Set 'YES' to use TLS when sending emails. -$CONF['smtp_sendmail_tls'] = 'NO'; +// Encrypt - how passwords are stored/hashed in the database. +// +// See: https://github.com/postfixadmin/postfixadmin/blob/master/DOCUMENTS/HASHING.md +// +// - PLAIN, CLEAR or CLEARTEXT - plain text variants, may be useful for testing. +// +// - ARGON2ID, ARGON2I, SHA512-CRYPT, SHA256-CRYPT or BLF-CRYPT might be good options. +// +// - other, older variants are : +// - md5crypt, +// - md5, +// - system, +// - mysql_encrypt - mysql's password() +// - dovecot:CRYPT-METHOD = use dovecotpw -s 'CRYPT-METHOD'. +// - Note: dovecot relies on doveadm binary, and suitable permissions on config files - see https://github.com/postfixadmin/postfixadmin/issues/398 +// +// - authlib = support for courier-authlib style passwords - also set $CONF['authlib_default_flavor'] +// +// - php_crypt:CRYPT-METHOD:DIFFICULTY:PREFIX = use PHP built in crypt()-function. Example: php_crypt:SHA512:50000 +// - php_crypt CRYPT-METHOD: Supported values are DES, MD5, BLOWFISH, SHA256, SHA512 (default) +// - php_crypt - DIFFICULTY: Larger value is more secure, but uses more CPU and time for each login. +// - php_crypt - DIFFICULTY: Set this according to your CPU processing power. +// - php_crypt - DIFFICULTY: Supported values are BLOWFISH:4-31, SHA256:1000-999999999, SHA512:1000-999999999 +// - php_crypt - DIFFICULTY: leave empty to use default values (BLOWFISH:10, SHA256:5000, SHA512:5000). Example: php_crypt:SHA512 +// - php_crypt - PREFIX: hash has specified prefix - example: php_crypt:SHA512::{SHA256-CRYPT} +// +// - sha512.b64 - {SHA512-CRYPT.B64} (base64 encoded sha512 crypt) (no dovecot dependency; should support migration from md5crypt) -// Encrypt -// In what way do you want the passwords to be crypted? -// md5crypt = internal postfix admin md5 -// md5 = md5 sum of the password -// system = whatever you have set as your PHP system default -// cleartext = clear text passwords (ouch!) -// mysql_encrypt = useful for PAM integration -// authlib = support for courier-authlib style passwords - also set $CONF['authlib_default_flavor'] -// dovecot:CRYPT-METHOD = use dovecotpw -s 'CRYPT-METHOD'. Example: dovecot:CRAM-MD5 -// php_crypt:CRYPT-METHOD:DIFFICULTY = use PHP built in crypt()-function. Example: php_crypt:SHA512:50000 -// - php_crypt CRYPT-METHOD: Supported values are DES, MD5, BLOWFISH, SHA256, SHA512 -// - php_crypt DIFFICULTY: Larger value is more secure, but uses more CPU and time for each login. -// - php_crypt DIFFICULTY: Set this according to your CPU processing power. -// - php_crypt DIFFICULTY: Supported values are BLOWFISH:4-31, SHA256:1000-999999999, SHA512:1000-999999999 -// - php_crypt DIFFICULTY: leave empty to use default values (BLOWFISH:10, SHA256:5000, SHA512:5000). Example: php_crypt:SHA512 -// IMPORTANT: -// - don't use dovecot:* methods that include the username in the hash - you won't be able to login to PostfixAdmin in this case -// - you'll need at least dovecot 2.1 for salted passwords ('doveadm pw' 2.0.x doesn't support the '-t' option) -// - dovecot 2.0.0 - 2.0.7 is not supported -$CONF['encrypt'] = 'md5crypt'; +$CONF['encrypt'] = 'php_crypt'; // SHA512 // In what flavor should courier-authlib style passwords be encrypted? // (only used if $CONF['encrypt'] == 'authlib') @@ -227,8 +245,19 @@ $CONF['password_validation'] = array( '/.{5}/' => 'password_too_short 5', # minimum length 5 characters '/([a-zA-Z].*){3}/' => 'password_no_characters 3', # must contain at least 3 characters '/([0-9].*){2}/' => 'password_no_digits 2', # must contain at least 2 digits +# '/([!\".,*&^%$£)(_+=\-`\'#@~\[\]\\<>\/].*){1,}/' => 'password_no_special 1', # must contain at least 1 special character + + /* support a 'callable' value which if it returns a non-empty string will be assumed to have failed, non-empty string should be a PALANG key */ + // 'length_check' => function($password) { if (strlen(trim($password)) < 3) { return 'password_too_short'; } }, ); +// Username legal characters +// New/changed usernames will be checked against this regular expression with javascript +// during entry, offending characters not displaying. +// For example: +// $CONF['username_legal_chars'] = '^[a-zA-Z0-9-_.]+$'; +$CONF['username_legal_chars'] = ''; + // Generate Password // Generate a random password for a mailbox or admin and display it. // If you want to automagically generate passwords set this to 'YES'. @@ -314,7 +343,9 @@ function maildir_name_hook($domain, $user) { Note: Adding a field to $struct adds the handling of this field in PostfixAdmin, but it does not create it in the database. You have to do - that yourself. + that yourself. + Note: If you add fields here and you want them to be displayed in the + virtual lists, you must also modify the corresponding virtual-list template. Please follow the naming policy for custom database fields and tables on https://sourceforge.net/p/postfixadmin/wiki/Custom_fields/ to avoid clashes with future versions of PostfixAdmin. @@ -338,6 +369,8 @@ $CONF['alias_struct_hook'] = ''; $CONF['mailbox_struct_hook'] = ''; $CONF['alias_domain_struct_hook'] = ''; $CONF['fetchmail_struct_hook'] = ''; +$CONF['dkim_struct_hook'] = ''; +$CONF['dkim_signing_struct_hook'] = ''; // Default Domain Values @@ -510,6 +543,25 @@ $CONF['emailcheck_resolve_domain']='YES'; // from being the destination for an alias $CONF['emailcheck_localaliasonly']='NO'; +// Use TOTP for logging into Postfixadmin, can be overridden for listed +// IPs to allow access by software that provide their own checking. +// Exceptions can be of user, domain or global scope. +// This also bundles several menu items in a "security" dropdown. +$CONF['totp'] = 'NO'; + +// Use revokable application passwords to limit the risk of storing a +// password in another system. These passwords can not access Postfixadmin. +$CONF['app_passwords'] = 'NO'; + + +// OpenDKIM stuff +// Enable the dkim database component +$CONF['dkim'] = 'NO'; +// Allow regular admins to add/edit/remove dkim entries +$CONF['dkim_all_admins'] = 'NO'; +// End OpenDKIM stuff + + // Optional: // Analyze alias gotos and display a colored block in the first column // indicating if an alias or mailbox appears to deliver to a non-existent @@ -556,47 +608,94 @@ $CONF['show_custom_colors']=array("lightgreen","lightblue"); // Set to "" to disable this check. $CONF['recipient_delimiter'] = ""; -// Optional: +/** + * NOTE FOR OPTIONAL SCRIPTS BELOW. + * + * These scripts will probably be called by your webserver user (typically 'www-data'). + * + * Execution may fail for a number of reasons, perhaps : + * * PHP is running in 'safe mode' + * * you have operating system features like SELinux or Apparmor + * * Unix file ownership/permission restrictions + * + * Your mail system probably requires different ownership (e.g. courier, dovecot, mail ...) + * + * You will probably need to use 'sudo' either within the script, or when calling it, to resolve issues of ownership/permission. + * + * Details about errors from execution should be logged into PHP's error_log. + * + * See also: https://github.com/postfixadmin/postfixadmin/blob/master/DOCUMENTS/FAQ.txt + * + */ + +// Optional: See NOTE above. // Script to run after creation of mailboxes. -// Note that this may fail if PHP is run in "safe mode", or if -// operating system features (such as SELinux) or limitations -// prevent the web-server from executing external scripts. // Parameters: (1) username (2) domain (3) maildir (4) quota // $CONF['mailbox_postcreation_script']='sudo -u courier /usr/local/bin/postfixadmin-mailbox-postcreation.sh'; $CONF['mailbox_postcreation_script'] = ''; -// Optional: +// Optional: See NOTE above. // Script to run after alteration of mailboxes. -// Note that this may fail if PHP is run in "safe mode", or if -// operating system features (such as SELinux) or limitations -// prevent the web-server from executing external scripts. // Parameters: (1) username (2) domain (3) maildir (4) quota // $CONF['mailbox_postedit_script']='sudo -u courier /usr/local/bin/postfixadmin-mailbox-postedit.sh'; $CONF['mailbox_postedit_script'] = ''; -// Optional: +// Optional: See NOTE above. // Script to run after deletion of mailboxes. -// Note that this may fail if PHP is run in "safe mode", or if -// operating system features (such as SELinux) or limitations -// prevent the web-server from executing external scripts. // Parameters: (1) username (2) domain // $CONF['mailbox_postdeletion_script']='sudo -u courier /usr/local/bin/postfixadmin-mailbox-postdeletion.sh'; $CONF['mailbox_postdeletion_script'] = ''; -// Optional: +// Optional: See NOTE above. +// Script to run after setting a mailbox password. (New mailbox [old password = empty] or change existing password) +// Disables changing password without entering old password. +// Parameters: (1) username (2) domain +// STDIN: old password + \0 + new password +// $CONF['mailbox_postpassword_script']='sudo -u dovecot /usr/local/bin/postfixadmin-mailbox-postpassword.sh'; +$CONF['mailbox_postpassword_script'] = ''; + +// Optional: See NOTE above. +// Script to run after setting a mailbox TOTP secret. +// Parameters: (1) username (2) domain +// STDIN: TOTP secret + \0 +// $CONF['mailbox_post_TOTP_change_secret_script']='sudo -u dovecot /usr/local/bin/postfixadmin-mailbox-postpassword.sh'; +$CONF['mailbox_post_TOTP_change_secret_script'] = ''; + +// Optional: See NOTE above. +// Script to run after adding an exception address (disable TOTP). +// Parameters: (1) username (2) ip +// STDIN: TOTP secret + \0 +// $CONF['mailbox_post_exception_add_script']='sudo -u dovecot /usr/local/bin/postfixadmin-mailbox-postpassword.sh'; +$CONF['mailbox_post_totp_exception_add_script'] = ''; + +// Optional: See NOTE above. +// Script to run after deleting an exception address (disable TOTP). +// Parameters: (1) username (2) ip +// STDIN: TOTP secret + \0 +// $CONF['mailbox_post_totp_exception_delete_script']='sudo -u dovecot /usr/local/bin/postfixadmin-mailbox-postpassword.sh'; +$CONF['mailbox_post_totp_exception_delete_script'] = ''; + +// Optional: See NOTE above. +// Script to run after adding an app password. +// Parameters: (1) username (2) app description +// STDIN: password + \0 +// $CONF['mailbox_postapppassword_script']='sudo -u dovecot /usr/local/bin/postfixadmin-mailbox-postpassword.sh'; +$CONF['mailbox_postapppassword_script'] = ''; + +// Optional: See NOTE above. // Script to run after creation of domains. -// Note that this may fail if PHP is run in "safe mode", or if -// operating system features (such as SELinux) or limitations -// prevent the web-server from executing external scripts. // Parameters: (1) domain //$CONF['domain_postcreation_script']='sudo -u courier /usr/local/bin/postfixadmin-domain-postcreation.sh'; $CONF['domain_postcreation_script'] = ''; -// Optional: +// Optional: See NOTE above. +// Script to run after alteation of domains. +// Parameters: (1) domain +//$CONF['domain_postedit_script']='sudo -u courier /usr/local/bin/postfixadmin-domain-postedit.sh'; +$CONF['domain_postedit_script'] = ''; + +// Optional: See NOTE above. // Script to run after deletion of domains. -// Note that this may fail if PHP is run in "safe mode", or if -// operating system features (such as SELinux) or limitations -// prevent the web-server from executing external scripts. // Parameters: (1) domain // $CONF['domain_postdeletion_script']='sudo -u courier /usr/local/bin/postfixadmin-domain-postdeletion.sh'; $CONF['domain_postdeletion_script'] = ''; @@ -679,7 +778,7 @@ $CONF['theme'] = 'default'; // Specify your own favicon, logo and CSS file $CONF['theme_favicon'] = 'images/favicon.ico'; $CONF['theme_logo'] = 'images/logo-default.png'; -$CONF['theme_css'] = 'css/default.css'; +$CONF['theme_css'] = 'css/bootstrap.css'; // If you want to customize some styles without editing the $CONF['theme_css'] file, // you can add a custom CSS file. It will be included after $CONF['theme_css']. $CONF['theme_custom_css'] = ''; @@ -692,9 +791,26 @@ $CONF['xmlrpc_enabled'] = false; //Account expiration info //If enabled, mailbox passwords have a password_expiry field set, which is updated each time the password is changed, based on the parent domain's password_expiry (days) value. -//More details in README.password_expiration +//More details in Password_Expiration.md $CONF['password_expiration'] = 'YES'; +// If defined, use this rather than trying to construct it from $_SERVER parameters. +// used in (at least) password-recover.php. +$CONF['site_url'] = null; + +$CONF['version'] = '3.4-dev'; + +// The smtp_active_flag when set to YES enables editing of the smtp_active +// field of the mailbox table. The smtp_active field can be used to enable +// or disable smtp sending for a mailbox separately to other mailbox functions. +// This can be useful if you want the ability to stop a user sending email +// while still allowing receipt of new mail and reading existing email. +// Please refer to DOCUMENTS/DOVECOT.txt for an example of how to configure this. +// The default is NO for backwards compatibility. Only enable this if you +// have also set up the SQL queries that make use of the smtp_active field +// in your Dovecot SQL configuration. +$CONF['smtp_active_flag'] = 'NO'; + // If you want to keep most settings at default values and/or want to ensure // that future updates work without problems, you can use a separate config // file (config.local.php) instead of editing this file and override some diff --git a/configs/menu.conf b/configs/menu.conf index 55ae6b91..1f0540d0 100644 --- a/configs/menu.conf +++ b/configs/menu.conf @@ -17,8 +17,16 @@ url_fetchmail_new_entry = edit.php?table=fetchmail # sendmail url_sendmail = sendmail.php url_broadcast_message = broadcast-message.php +# dkim +url_dkim = list.php?table=dkim +url_dkim_signing = list.php?table=dkimsigning +url_dkim_newkey = edit.php?table=dkim +url_dkim_newsign = edit.php?table=dkimsigning # password url_password = edit.php?table=adminpassword +url_totp = totp.php +url_totp_exceptions = totp-exceptions.php +url_app_passwords = app-passwords.php # backup url_backup = backup.php # viewlog @@ -31,6 +39,7 @@ url_user_main = main.php url_user_edit_alias = edit-alias.php url_user_vacation = vacation.php url_user_password = password.php +url_user_totp = totp.php url_user_logout = login.php diff --git a/debian/README.Debian b/debian/README.Debian index 10ba713f..f93533ec 100644 --- a/debian/README.Debian +++ b/debian/README.Debian @@ -20,3 +20,5 @@ The first stop would be the Postfixadmin Website, Forum or IRC channel. See : - http://postfixadmin.sf.net - #postfixadmin on irc.freenode.net + + -- Christoph Martin , Mon, 27 Jun 2016 16:58:12 +0200 diff --git a/debian/changelog b/debian/changelog index 13ed2412..3a150abf 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,25 +1,153 @@ -postfixadmin (3.2-1) unstable; urgency=low +postfixadmin (3.3.10-2) unstable; urgency=medium - * New upstream release - PostfixAdmin v3.2 + * remove unnessesary fix for mysqli instead of mysql + * fix permissions of templates_c directory - -- David Goodwin Wed, 02 May 2018 21:36:01 +0100 + -- Christoph Martin Thu, 19 Aug 2021 23:30:30 +0200 -postfixadmin (3.1-1) unstable; urgency=low +postfixadmin (3.3.10-1) unstable; urgency=medium - * New upstream release + * update watch file + * New upstream version 3.3.10 + * fix lighttpd config (closes: #987998) + * include templates_c directory in /var/cache (closes: #926253) + * offer mysqli instead mysql as PHP database type (closes: #857791) + * symlink /etc/postfixadmin/config.local.php to + /usr/share/postfixadmin/config.local.php (closes: #926354) - -- David Goodwin Sun, 25 Jun 2017 16:27:01 +0000 + -- Christoph Martin Mon, 16 Aug 2021 19:05:26 +0200 -postfixadmin (3.0.2-1) unstable; urgency=low +postfixadmin (3.3.7-1) unstable; urgency=medium - * Security fix (don't delete protected aliases, CVE-2017-5930) + * New upstream version 3.3.7 + * recommend sqlite3 instead of sqlite (closes: #905726) + * fix alias in apache config (closes: #965075) + * fix dependency on mariadb-server (closes: #968931) + + -- Christoph Martin Wed, 03 Mar 2021 14:57:26 +0100 + +postfixadmin (3.3.5-1) unstable; urgency=medium + + * New upstream version 3.3.5 + + -- Christoph Martin Thu, 28 Jan 2021 08:23:28 +0100 + +postfixadmin (3.3.4-1) unstable; urgency=medium + + * New upstream version 3.3.4 + + -- Christoph Martin Mon, 25 Jan 2021 10:46:01 +0100 + +postfixadmin (3.3.3-1) unstable; urgency=medium + + * New upstream version 3.3.3 + + -- Christoph Martin Mon, 18 Jan 2021 16:23:33 +0100 + +postfixadmin (3.3.1-1) unstable; urgency=medium + + * New upstream version 3.3.1 + + -- Christoph Martin Tue, 12 Jan 2021 11:44:54 +0100 + +postfixadmin (3.2.4-1) unstable; urgency=medium + + * New upstream version 3.2.4 + + -- Christoph Martin Mon, 21 Sep 2020 11:13:46 +0200 + +postfixadmin (3.2.1-4) unstable; urgency=medium + + [ Debian Janitor ] + * Trim trailing whitespace. + * Wrap long lines in changelog entries: 3.0.2-2, 3.0.2-1, 3.0.1-2. + * Bump debhelper from deprecated 7 to 12. + * Set debhelper-compat version in Build-Depends. + * Set upstream metadata fields: Bug-Database, Bug-Submit, Repository, + Repository-Browse. + + -- Christoph Martin Mon, 14 Sep 2020 16:10:16 +0200 + +postfixadmin (3.2.1-3) unstable; urgency=medium + + [ Jean-Michel Vourgère ] + * Update apache2 configuration (closes: #926252) + + [ Christian Schrötter ] + * Fix missing SSL at persistent MySQLi reconnect + + -- Christoph Martin Mon, 10 Feb 2020 15:50:48 +0100 + +postfixadmin (3.2.1-2) unstable; urgency=medium + + * remove backported code which results in errors (closes: #920564) + * link /usr/share/postfixadmin/scripts/postfixadmin-cli to /usr/bin + closes: #920264) + + -- Christoph Martin Mon, 28 Jan 2019 14:16:05 +0100 + +postfixadmin (3.2.1-1) unstable; urgency=medium + + * fix debian/changelog + * New upstream version 3.2.1 + + -- Christoph Martin Mon, 14 Jan 2019 11:44:58 +0100 + +postfixadmin (3.2-4) unstable; urgency=medium + + [ Christoph Martin ] + * fix sqlite database upgrade (closes: #909263) + + [ Ondřej Nový ] + * d/rules: Remove trailing whitespaces + + -- Christoph Martin Fri, 28 Sep 2018 11:19:19 +0200 + +postfixadmin (3.2-3) unstable; urgency=medium + + * include missing lib folder (closes: #908605) + + -- Christoph Martin Wed, 12 Sep 2018 09:17:27 +0200 + +postfixadmin (3.2-2) unstable; urgency=medium + + * reupload with missing public folder included (closes: #908317) + + -- Christoph Martin Mon, 10 Sep 2018 11:40:37 +0200 + +postfixadmin (3.2-1) unstable; urgency=medium + + [ Christoph Martin ] + * Move watch target to github + * Move Vcs-* to Salsa + * New upstream version 3.2 + + -- Christoph Martin Fri, 04 May 2018 11:39:57 +0200 + +postfixadmin (3.0.2-2) unstable; urgency=medium + + * fix mysql connection problems (closes: #861260) see: + https://github.com/postfixadmin/postfixadmin/issues/38 and + https://github.com/postfixadmin/postfixadmin/commit/6ee85ac6cc427392a1d37339e45a5dbb4b96461c + + -- Christoph Martin Thu, 27 Apr 2017 12:43:15 +0200 + +postfixadmin (3.0.2-1) unstable; urgency=high + + [ David Goodwin ] + * Security fix (don't delete protected aliases, CVE-2017-5930) (closes: + #854742) * Fix MySQL vacation.cache column (regression fix) - -- David Goodwin Wed, 08 Feb 2017 19:30:00 +0000 + [ Christoph Martin ] + * remove recommends zendframework (closes: #780418) + + -- Christoph Martin Fri, 10 Feb 2017 15:08:46 +0100 postfixadmin (3.0.1-2) unstable; urgency=low - * Try and make dependencies less strict (and perhaps work for Ubuntu Precise as well) + * Try and make dependencies less strict (and perhaps work for Ubuntu Precise + as well) -- David Goodwin Mon, 10 Oct 2016 20:00:00 +0100 @@ -29,42 +157,101 @@ postfixadmin (3.0.1-1) unstable; urgency=low -- David Goodwin Mon, 19 Sep 2016 10:08:00 +0100 +postfixadmin (3.0-2) unstable; urgency=medium + + * include missing files (closes: #847074) + + -- Christoph Martin Thu, 08 Dec 2016 15:06:27 +0100 + postfixadmin (3.0-1) unstable; urgency=low + [ David Goodwin ] * New upstream release - -- David Goodwin Sun, 11 Sep 2016 18:42:00 +0100 + [ Christoph Martin ] + * merge Debian changes -postfixadmin (2.93-2) unstable; urgency=low + -- Christoph Martin Wed, 09 Nov 2016 15:43:41 +0100 +postfixadmin (2.93-2) unstable; urgency=medium + + [ Christoph Martin ] + * add fix for missing token bug (closes: #825151) + * add depend on php-mbstring + + [ David Goodwin ] * Replace debian/ using Debian v2.3.7-2. See Debian's #821643 * This adds : PHP7 and Apache 2.4 support. - -- David Goodwin Sun, 22 May 2016 19:41:01 +0100 + -- Christoph Martin Thu, 07 Jul 2016 16:36:08 +0200 postfixadmin (2.93-1) unstable; urgency=low - * New upstream release (effectively beta3 for v3.0) - * update dependencies to allow mariadb as database + [ David Goodwin ] + * New upstream release (closes: #819218) + * update dependencies to allow mariadb as database (closes: #778794) - -- David Goodwin Sat, 26 Sep 2015 15:05:00 +0100 + [ Christoph Martin ] + * merge to Debian build + * include patch remove code which needs mysql >= 5.6 + * update standards version to 3.9.6 -postfixadmin (2.92-1) unstable; urgency=low + -- Christoph Martin Mon, 27 Jun 2016 17:59:10 +0200 - * New upstream release (effectively beta2 for v3.0) +postfixadmin (2.3.7-2) unstable; urgency=medium - -- David Goodwin Wed, 28 Oct 2014 21:02:00 +0100 + * depend on php instead of php7 (closes: #821643) -postfixadmin (2.91-1) unstable; urgency=low + -- Christoph Martin Fri, 20 May 2016 15:34:04 +0200 - * New upstream release (effectively beta for v3.0) +postfixadmin (2.3.7-1) unstable; urgency=medium - -- David Goodwin Tue, 06 May 2014 21:36:00 +0100 + [ Norman Meßtorff ] + * [76ef] change recommends of postgresql-server (not existing) to postgresql. + Thanks to Michael Neuffer (Closes: 699602) + * [4bb5] Change suggestion of transitional package dovecot-common + to dovecot-core + * [f06f] Add new recommended package 'zendframework' + Thanks to Benedikt Trefzer (Closes: 684080) + * [d25a] rules: remove not needed target 'prep' + * [47f7] control: update standards-version to 3.9.5 without changes + * [6f51] Imported Upstream version 2.3.7 (Closes: #741969) + * [4256] Remove patch 0002-fix-sql-injection_show_alias + * [0e04] Update Vcs-* tags in control file + * [4f90] remove legacy apache configuration in prerm, if existing + + [ Gaudenz Steinlin ] + * [a5a3] Apache 2.4 transition (Closes: #669834) + * [68fe] Use doveadm pw instead of dovecotpw by default (Closes: #706698) + * [1da4] Remove wwwconfig-common support (Closes: #691936, #719933) + * [b509] Add myself as uploader + * [f628] Rename lighttpd config to 90-postfixadmin.conf + * [a0e6] Remove config symlink for lighttpd in postinst + + -- Gaudenz Steinlin Mon, 06 Oct 2014 13:27:25 +0200 + +postfixadmin (2.3.5-3) unstable; urgency=high + + [ Norman Messtorff ] + * [a620b76] fix possible SQL injection of $show_alias in function + gen_show status() + + -- Norman Messtorff Sun, 23 Mar 2014 19:07:23 +0100 + +postfixadmin (2.3.5-2+deb7u1) wheezy-security; urgency=high + + * Non-maintainer upload + * SECURITY: fix SQL injection in show_gen_status() + This is only exploitable by authenticated users able + to create new aliases. + Upstream commit: http://sourceforge.net/p/postfixadmin/code/1650 + + -- Gaudenz Steinlin Thu, 20 Mar 2014 10:41:47 +0100 postfixadmin (2.3.5-2) unstable; urgency=low - * Added .po translation files (Closes: 667951, #667962, #668202, #668288) - * Closes: #668298, #668301, #668405, #668635 + * Added .po translation files (Closes: #667951, #667962, #668202, #668288, + #668298, #668301, #668405, #668635) * Updated standards version to 3.9.3 without changes. -- Norman Messtorff Thu, 26 Apr 2012 20:55:57 +0200 @@ -74,4 +261,3 @@ postfixadmin (2.3.5-1) unstable; urgency=low * Initial Debian release (Closes: #247225) -- Norman Messtorff Sun, 15 Jan 2012 12:27:28 +0100 - diff --git a/debian/control b/debian/control index ad5a635d..ae609b7d 100644 --- a/debian/control +++ b/debian/control @@ -3,16 +3,16 @@ Section: admin Priority: optional Maintainer: Norman Messtorff Uploaders: Gaudenz Steinlin , Christoph Martin -Build-Depends: debhelper (>= 7), po-debconf, dh-apache2 -Standards-Version: 3.9.5 -Vcs-Git: git://anonscm.debian.org/collab-maint/postfixadmin.git -Vcs-Browser: https://anonscm.debian.org/cgit/collab-maint/postfixadmin.git +Build-Depends: debhelper-compat (= 12), po-debconf, dh-apache2 +Standards-Version: 3.9.6 +Vcs-Browser: https://salsa.debian.org/debian/postfixadmin +Vcs-Git: https://salsa.debian.org/debian/postfixadmin.git Homepage: http://postfixadmin.sourceforge.net Package: postfixadmin Architecture: all -Depends: debconf (>= 0.5), dbconfig-common, wwwconfig-common, apache2 | lighttpd | httpd, libapache2-mod-php | php-cgi | php-fpm | php, php-mysql | php-mysqlnd | php-pgsql | php-pgsql | php-sqlite3, php-mbstring, default-mysql-client | mysql-client | postgresql-client | mariadb-client -Recommends: postfix-mysql | postfix-pgsql, virtual-mysql-server | postgresql | sqlite (>= 3.12.0) | mariadb-server, zendframework, php-imap, dovecot-core | courier-authlib-mysql | courier-authlib-postgresql, php-cli +Depends: debconf (>= 0.5), dbconfig-common, wwwconfig-common, apache2 | lighttpd | httpd, libapache2-mod-php | php-cgi | php-fpm | php, php-imap, php-mysql | php-mysqlnd | php-pgsql | php-sqlite3, php-mbstring, default-mysql-client | postgresql-client | mariadb-client, ${misc:Depends} +Recommends: postfix-mysql | postfix-pgsql, virtual-mysql-server | postgresql | sqlite3 (>= 3.12.0) | mariadb-server, zendframework, dovecot-core | courier-authlib-mysql | courier-authlib-postgresql, php-cli Description: Virtual mail hosting interface for Postfix Postfixadmin is a web interface to manage virtual users and domains for a Postfix mail transport agent. It supports Virtual mailboxes, diff --git a/debian/patches/config-debian.diff b/debian/patches/config-debian.diff index 36e0e160..2ba83f2c 100644 --- a/debian/patches/config-debian.diff +++ b/debian/patches/config-debian.diff @@ -1,8 +1,8 @@ --- a/config.inc.php +++ b/config.inc.php -@@ -31,6 +31,16 @@ - ################################################################################ - +@@ -14,6 +14,16 @@ + * Contains configuration options. + */ +// Debian: This loads the automatic generated DB credentials from /etc/postfixadmin/dbconfig.inc.php +$db_config = dirname(__FILE__) . '/dbconfig.inc.php'; @@ -15,9 +15,9 @@ + $dbserver = 'localhost'; +} - /***************************************************************** - * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -@@ -39,7 +49,7 @@ + ################################################################################ + # # +@@ -38,7 +48,7 @@ * Doing this implies you have changed this file as required. * i.e. configuring database etc; specifying setup.php password etc. */ @@ -26,7 +26,7 @@ // In order to setup Postfixadmin, you MUST specify a hashed password here. // To create the hash, visit setup.php in a browser and type a password into the field, -@@ -98,11 +108,11 @@ function language_hook($PALANG, $language) { +@@ -97,11 +107,11 @@ // mysqli = MySQL 4.1+ or MariaDB // pgsql = PostgreSQL // sqlite = SQLite 3 @@ -41,5 +41,5 @@ +$CONF['database_password'] = $dbpass; +$CONF['database_name'] = $dbname; - // Database SSL Config + // Database SSL Config (PDO/MySQLi only) $CONF['database_use_ssl'] = false; diff --git a/debian/postfixadmin.dirs b/debian/postfixadmin.dirs index 88e88170..442e2290 100644 --- a/debian/postfixadmin.dirs +++ b/debian/postfixadmin.dirs @@ -1,8 +1,7 @@ -usr/share/postfixadmin -usr/share/postfixadmin/scripts -usr/share/postfixadmin/public -usr/share/postfixadmin/lib -usr/share/doc/postfixadmin -var/cache/postfixadmin usr/bin +usr/share/postfixadmin +#usr/share/postfixadmin/css +usr/share/doc/postfixadmin etc/postfixadmin +etc/apache2/conf-available +var/cache/postfixadmin/templates_c diff --git a/debian/postfixadmin.docs b/debian/postfixadmin.docs old mode 100755 new mode 100644 diff --git a/debian/postfixadmin.examples b/debian/postfixadmin.examples index 3d44bcc7..eceda75d 100644 --- a/debian/postfixadmin.examples +++ b/debian/postfixadmin.examples @@ -13,4 +13,4 @@ ADDITIONS/delete-mailq-by-domain.pl ADDITIONS/pfa_maildir_cleanup.pl ADDITIONS/quota_usage.pl ADDITIONS/fetchmail.pl -ADDITIONS/README.TXT +ADDITIONS/README.md diff --git a/debian/postfixadmin.install b/debian/postfixadmin.install index 96ddd070..9ef93ceb 100644 --- a/debian/postfixadmin.install +++ b/debian/postfixadmin.install @@ -1,9 +1,11 @@ *.php usr/share/postfixadmin public usr/share/postfixadmin -languages usr/share/postfixadmin -model usr/share/postfixadmin -templates usr/share/postfixadmin -lib usr/share/postfixadmin configs usr/share/postfixadmin +languages usr/share/postfixadmin +lib usr/share/postfixadmin +model usr/share/postfixadmin scripts usr/share/postfixadmin +templates usr/share/postfixadmin +configs usr/share/postfixadmin debian/lighttpd/90-postfixadmin.conf etc/lighttpd/conf-available +debian/apache/postfixadmin.conf etc/apache2/conf-available diff --git a/debian/postfixadmin.links b/debian/postfixadmin.links index e2d3c91c..3a18ba42 100644 --- a/debian/postfixadmin.links +++ b/debian/postfixadmin.links @@ -1,2 +1 @@ etc/postfixadmin/config.inc.php usr/share/postfixadmin/config.inc.php -var/cache/postfixadmin usr/share/postfixadmin/templates_c diff --git a/debian/postfixadmin.postinst b/debian/postfixadmin.postinst index fd38931b..26bc94a6 100644 --- a/debian/postfixadmin.postinst +++ b/debian/postfixadmin.postinst @@ -23,15 +23,9 @@ fi if [ -d /usr/share/postfixadmin/templates_c ]; then find /usr/share/postfixadmin/templates_c -type f -exec rm -r {} \; fi -if [ -d /usr/share/postfixadmin/templates_c ]; then - chown www-data /usr/share/postfixadmin/templates_c +if [ -d /var/cache/postfixadmin/templates_c ]; then + find /var/cache/postfixadmin/templates_c -type f -exec rm -r {} \; fi - -if [ -d /var/cache/postfixadmin ]; then - find /var/cache/postfixadmin -type f -exec rm -r {} \; - chown www-data /var/cache/postfixadmin -fi - #DEBHELPER# exit 0 diff --git a/debian/postfixadmin.prerm b/debian/postfixadmin.prerm index 27a398a4..3b553f1e 100644 --- a/debian/postfixadmin.prerm +++ b/debian/postfixadmin.prerm @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/sh set -e . /usr/share/debconf/confmodule diff --git a/debian/rules b/debian/rules index 7ba9c433..fa3e8041 100755 --- a/debian/rules +++ b/debian/rules @@ -11,7 +11,7 @@ export DEBVERSION = $(shell grep -E "postfixadmin .([0-9]+|\.)+" debian/changelo # Create a needed tar.gz file to build a non-nativ .dpkg prep: rm -f ../postfixadmin_*orig.tar.gz - cd ..; tar --exclude-vcs --exclude=$(notdir ${CURDIR})/debian --exclude=$(notdir ${CURDIR})/.pc -cvzf postfixadmin_${DEBVERSION}.orig.tar.gz $(notdir ${CURDIR}) + cd ..; tar --exclude-vcs --exclude=$(notdir ${CURDIR})/debian --exclude=$(notdir ${CURDIR})/.pc -cvzf postfixadmin_${DEBVERSION}.orig.tar.gz $(notdir ${CURDIR}) build: build-arch build-indep build-arch: build-stamp @@ -33,7 +33,10 @@ install: build dh_install dh_apache2 mv debian/postfixadmin/usr/share/postfixadmin/config.inc.php debian/postfixadmin/etc/postfixadmin/config.inc.php - ln -s /usr/share/postfixadmin/scripts/postfixadmin-cli debian/postfixadmin/usr/bin/postfixadmin-cli + ln -s /etc/postfixadmin/config.local.php debian/postfixadmin/usr/share/postfixadmin/config.local.php + find debian/postfixadmin -name .svn | xargs -r rm -r + ln -s ../share/postfixadmin/scripts/postfixadmin-cli debian/postfixadmin/usr/bin/ + ln -s /var/cache/postfixadmin/templates_c debian/postfixadmin/usr/share/postfixadmin/ # Build architecture-independent files here. binary-indep: build install @@ -46,6 +49,8 @@ binary-indep: build install dh_link dh_compress dh_fixperms + chown www-data debian/postfixadmin/var/cache/postfixadmin/templates_c -R + chmod 700 debian/postfixadmin/var/cache/postfixadmin/templates_c dh_installdeb dh_gencontrol dh_md5sums diff --git a/debian/upstream/metadata b/debian/upstream/metadata new file mode 100644 index 00000000..56199d8a --- /dev/null +++ b/debian/upstream/metadata @@ -0,0 +1,4 @@ +Bug-Database: https://github.com/postfixadmin/postfixadmin/issues +Bug-Submit: https://github.com/postfixadmin/postfixadmin/issues/new +Repository: https://github.com/postfixadmin/postfixadmin.git +Repository-Browse: https://github.com/postfixadmin/postfixadmin diff --git a/debian/watch b/debian/watch index 26cff355..a8ca515a 100644 --- a/debian/watch +++ b/debian/watch @@ -1,2 +1,2 @@ -version=3 -http://sf.net/postfixadmin/postfixadmin-([\d\.]+)\.tar\.gz +version=4 +https://github.com/postfixadmin/postfixadmin/releases .*/postfixadmin-(\d[\d.]*)\.tar\.gz diff --git a/functions.inc.php b/functions.inc.php index e2f2f18e..16d5288d 100644 --- a/functions.inc.php +++ b/functions.inc.php @@ -14,8 +14,25 @@ * Contains re-usable code. */ -$version = '3.2'; -$min_db_version = 1840; # update (at least) before a release with the latest function numbrer in upgrade.php + +$min_db_version = 1844; # update (at least) before a release with the latest function numbrer in upgrade.php + + +/** + * Check if the user already provided a password but not the second factor + * @return boolean + */ +function authentication_mfa_incomplete() +{ + if (isset($_SESSION['sessid'])) { + if (isset($_SESSION['sessid']['mfa_complete'])) { + if ($_SESSION['sessid']['mfa_complete'] == false) { + return true; + } + } + } + return false; +} /** * check_session @@ -23,7 +40,8 @@ $min_db_version = 1840; # update (at least) before a release with the latest fu * Call: check_session () * @return String username (e.g. foo@example.com) */ -function authentication_get_username() { +function authentication_get_username() +{ if (defined('POSTFIXADMIN_CLI')) { return 'CLI'; } @@ -45,7 +63,8 @@ function authentication_get_username() { * Returns false if neither (E.g. if not logged in) * @return string|bool admin or user or (boolean) false. */ -function authentication_get_usertype() { +function authentication_get_usertype() +{ if (isset($_SESSION['sessid'])) { if (isset($_SESSION['sessid']['type'])) { return $_SESSION['sessid']['type']; @@ -53,6 +72,7 @@ function authentication_get_usertype() { } return false; } + /** * * Used to determine whether a user has a particular role. @@ -60,7 +80,8 @@ function authentication_get_usertype() { * @return boolean True if they have the requested role in their session. * Note, user < admin < global-admin */ -function authentication_has_role($role) { +function authentication_has_role($role) +{ if (isset($_SESSION['sessid'])) { if (isset($_SESSION['sessid']['roles'])) { if (in_array($role, $_SESSION['sessid']['roles'])) { @@ -80,7 +101,8 @@ function authentication_has_role($role) { * @param string $role * @return bool */ -function authentication_require_role($role) { +function authentication_require_role($role) +{ // redirect to appropriate page? if (authentication_has_role($role)) { return true; @@ -97,13 +119,19 @@ function authentication_require_role($role) { * @param boolean $is_admin true if the user is an admin, false otherwise * @return boolean true on success */ -function init_session($username, $is_admin = false) { +function init_session($username, $is_admin = false, $mfa_complete = false) +{ $status = session_regenerate_id(true); $_SESSION['sessid'] = array(); $_SESSION['sessid']['roles'] = array(); - $_SESSION['sessid']['roles'][] = $is_admin ? 'admin' : 'user'; + if ($mfa_complete) { + $_SESSION['sessid']['roles'][] = $is_admin ? 'admin' : 'user'; + $_SESSION['sessid']['mfa_complete'] = true; + } else { + $_SESSION['sessid']['mfa_complete'] = false; + } $_SESSION['sessid']['username'] = $username; - $_SESSION['PFA_token'] = md5(uniqid("", true)); + $_SESSION['PFA_token'] = md5(random_bytes(8) . uniqid('pfa', true)); return $status; } @@ -113,10 +141,11 @@ function init_session($username, $is_admin = false) { * @param string|array $string message(s) to show. * * Stores string in session. Flushed through header template. - * @see _flash_string() * @return void + * @see _flash_string() */ -function flash_error($string) { +function flash_error($string) +{ _flash_string('error', $string); } @@ -124,19 +153,22 @@ function flash_error($string) { * Used to display an info message on successful update. * @param string|array $string message(s) to show. * Stores data in session. - * @see _flash_string() * @return void + * @see _flash_string() */ -function flash_info($string) { +function flash_info($string) +{ _flash_string('info', $string); } + /** * 'Private' method used for flash_info() and flash_error(). * @param string $type * @param array|string $string * @retrn void */ -function _flash_string($type, $string) { +function _flash_string($type, $string) +{ if (is_array($string)) { foreach ($string as $singlestring) { _flash_string($type, $singlestring); @@ -158,24 +190,32 @@ function _flash_string($type, $string) { * @return string e.g en * Try to figure out what language the user wants based on browser / cookie */ -function check_language($use_post = true) { +function check_language($use_post = true) +{ global $supported_languages; # from languages/languages.php + // prefer a $_POST['lang'] if present + if ($use_post && safepost('lang')) { + $lang = safepost('lang'); + if (is_string($lang) && array_key_exists($lang, $supported_languages)) { + return $lang; + } + } + + // Failing that, is there a $_COOKIE['lang'] ? + if (safecookie('lang')) { + $lang = safecookie('lang'); + if (is_string($lang) && array_key_exists($lang, $supported_languages)) { + return $lang; + } + } + $lang = Config::read_string('default_language'); - if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { + // If not, did the browser give us any hint(s)? + if (!empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { $lang_array = preg_split('/(\s*,\s*)/', $_SERVER['HTTP_ACCEPT_LANGUAGE']); - if (safecookie('lang')) { - array_unshift($lang_array, safecookie('lang')); # prefer language from cookie - } - if ($use_post && safepost('lang')) { - array_unshift($lang_array, safepost('lang')); # but prefer $_POST['lang'] even more - } - foreach ($lang_array as $value) { - if (!is_string($value)) { - continue; - } $lang_next = strtolower(trim($value)); $lang_next = preg_replace('/;.*$/', '', $lang_next); # remove things like ";q=0.8" if (array_key_exists($lang_next, $supported_languages)) { @@ -192,12 +232,13 @@ function check_language($use_post = true) { * * */ -function language_selector() { +function language_selector() +{ global $supported_languages; # from languages/languages.php $current_lang = check_language(); - $selector = ''; foreach ($supported_languages as $lang => $lang_name) { if ($lang == $current_lang) { @@ -212,8 +253,6 @@ function language_selector() { } - - /** * Checks if a domain is valid * @param string $domain @@ -222,12 +261,13 @@ function language_selector() { * @todo make check_domain able to handle as example .local domains * @todo skip DNS check if the domain exists in PostfixAdmin? */ -function check_domain($domain) { +function check_domain($domain) +{ if (!preg_match('/^([-0-9A-Z]+\.)+' . '([-0-9A-Z]){1,13}$/i', ($domain))) { return sprintf(Config::lang('pInvalidDomainRegex'), htmlentities($domain)); } - if (Config::bool('emailcheck_resolve_domain') && 'WINDOWS'!=(strtoupper(substr(php_uname('s'), 0, 7)))) { + if (Config::bool('emailcheck_resolve_domain') && 'WINDOWS' != (strtoupper(substr(php_uname('s'), 0, 7)))) { // Look for an AAAA, A, or MX record for the domain @@ -241,6 +281,9 @@ function check_domain($domain) { $retval = ''; } elseif (checkdnsrr($domain, 'MX')) { $retval = ''; + } elseif (checkdnsrr($domain, 'NS')) { + error_log("DNS is not correctly configured for $domain to send or receive email"); + $retval = ''; } else { $retval = sprintf(Config::lang('pInvalidDomainDNS'), htmlentities($domain)); } @@ -292,7 +335,8 @@ function check_localaliasonly($domain) { * @param string $domain - a string that may be a domain * @return int password expiration value for this domain (DAYS, or zero if not enabled) */ -function get_password_expiration_value($domain) { +function get_password_expiration_value(string $domain) +{ $table_domain = table_by_key('domain'); $query = "SELECT password_expiry FROM $table_domain WHERE domain= :domain"; @@ -306,12 +350,13 @@ function get_password_expiration_value($domain) { /** * check_email * Checks if an email is valid - if it is, return true, else false. - * @todo make check_email able to handle already added domains * @param string $email - a string that may be an email address. * @return string empty if it's a valid email address, otherwise string with the errormessage + * @todo make check_email able to handle already added domains */ -function check_email($email) { - $ce_email=$email; +function check_email($email) +{ + $ce_email = $email; //strip the vacation domain out if we are using it //and change from blah#foo.com@autoreply.foo.com to blah@foo.com @@ -335,7 +380,7 @@ function check_email($email) { // Determine domain name $matches = array(); if (preg_match('|@(.+)$|', $ce_email, $matches)) { - $domain=$matches[1]; + $domain = $matches[1]; # check domain name return "" . check_domain($domain); } @@ -344,7 +389,6 @@ function check_email($email) { } - /** * Clean a string, escaping any meta characters that could be * used to disrupt an SQL string. The method of the escaping is dependent on the underlying DB @@ -362,9 +406,10 @@ function check_email($email) { * @param int|string $string_or_int parameters to escape * @return string cleaned data, suitable for use within an SQL statement. */ -function escape_string($string_or_int) { +function escape_string($string_or_int) +{ $link = db_connect(); - $string_or_int = (string) $string_or_int; + $string_or_int = (string)$string_or_int; $quoted = $link->quote($string_or_int); return trim($quoted, "'"); } @@ -378,12 +423,13 @@ function escape_string($string_or_int) { * $param = safeget('param', 'default') * * @param string $param parameter name. - * @param string|array $default (optional) - default value if key is not set. - * @return string|array + * @param string $default (optional) - default value if key is not set. + * @return string */ -function safeget($param, $default = "") { +function safeget($param, $default = "") +{ $retval = $default; - if (isset($_GET[$param])) { + if (isset($_GET[$param]) && is_string($_GET[$param])) { $retval = $_GET[$param]; } return $retval; @@ -391,42 +437,29 @@ function safeget($param, $default = "") { /** * safepost - similar to safeget() but for $_POST - * @see safeget() * @param string $param parameter name * @param string $default (optional) default value (defaults to "") - * @return string|array - value in $_POST[$param] or $default + * @return string - value in $_POST[$param] or $default + * @see safeget() */ -function safepost($param, $default = "") { +function safepost($param, $default = "") +{ $retval = $default; - if (isset($_POST[$param])) { + if (isset($_POST[$param]) && is_string($_POST[$param])) { $retval = $_POST[$param]; } return $retval; } -/** - * safeserver - * @see safeget() - * @param string $param - * @param string $default (optional) - * @return string value from $_SERVER[$param] or $default - */ -function safeserver($param, $default = "") { - $retval = $default; - if (isset($_SERVER[$param])) { - $retval = $_SERVER[$param]; - } - return $retval; -} - /** * safecookie - * @see safeget() * @param string $param * @param string $default (optional) * @return string value from $_COOKIE[$param] or $default + * @see safeget() */ -function safecookie($param, $default = "") { +function safecookie($param, $default = "") +{ $retval = $default; if (isset($_COOKIE[$param])) { $retval = $_COOKIE[$param]; @@ -436,14 +469,15 @@ function safecookie($param, $default = "") { /** * safesession - * @see safeget() * @param string $param * @param string $default (optional) * @return string value from $_SESSION[$param] or $default + * @see safeget() */ -function safesession($param, $default = "") { +function safesession($param, $default = "") +{ $retval = $default; - if (isset($_SESSION[$param])) { + if (isset($_SESSION[$param]) && is_string($_SESSION[$param])) { $retval = $_SESSION[$param]; } return $retval; @@ -463,12 +497,13 @@ function safesession($param, $default = "") { * @param int or $not_in_db - if array, can contain the remaining parameters as associated array. Otherwise counts as $not_in_db * @return array for $struct */ -function pacol($allow_editing, $display_in_form, $display_in_list, $type, $PALANG_label, $PALANG_desc, $default = "", $options = array(), $multiopt=0, $dont_write_to_db=0, $select="", $extrafrom="", $linkto="") { +function pacol($allow_editing, $display_in_form, $display_in_list, $type, $PALANG_label, $PALANG_desc, $default = "", $options = array(), $multiopt = 0, $dont_write_to_db = 0, $select = "", $extrafrom = "", $linkto = "") +{ if ($PALANG_label != '') { $PALANG_label = Config::lang($PALANG_label); } - if ($PALANG_desc != '') { - $PALANG_desc = Config::lang($PALANG_desc); + if ($PALANG_desc != '') { + $PALANG_desc = Config::lang($PALANG_desc); } if (is_array($multiopt)) { # remaining parameters provided in named array @@ -481,19 +516,19 @@ function pacol($allow_editing, $display_in_form, $display_in_list, $type, $PALAN } return array( - 'editable' => $allow_editing, - 'display_in_form' => $display_in_form, - 'display_in_list' => $display_in_list, - 'type' => $type, - 'label' => $PALANG_label, # $PALANG field label - 'desc' => $PALANG_desc, # $PALANG field description - 'default' => $default, - 'options' => $options, - 'not_in_db' => $not_in_db, - 'dont_write_to_db' => $dont_write_to_db, - 'select' => $select, # replaces the field name after SELECT - 'extrafrom' => $extrafrom, # added after FROM xy - useful for JOINs etc. - 'linkto' => $linkto, # make the value a link - %s will be replaced with the ID + 'editable' => $allow_editing, + 'display_in_form' => $display_in_form, + 'display_in_list' => $display_in_list, + 'type' => $type, + 'label' => $PALANG_label, # $PALANG field label + 'desc' => $PALANG_desc, # $PALANG field description + 'default' => $default, + 'options' => $options, + 'not_in_db' => $not_in_db, + 'dont_write_to_db' => $dont_write_to_db, + 'select' => $select, # replaces the field name after SELECT + 'extrafrom' => $extrafrom, # added after FROM xy - useful for JOINs etc. + 'linkto' => $linkto, # make the value a link - %s will be replaced with the ID ); } @@ -502,7 +537,8 @@ function pacol($allow_editing, $display_in_form, $display_in_list, $type, $PALAN * @param string $domain * @return array */ -function get_domain_properties($domain) { +function get_domain_properties($domain) +{ $handler = new DomainHandler(); if (!$handler->init($domain)) { throw new Exception("Error: " . join("\n", $handler->errormsg)); @@ -525,9 +561,10 @@ function get_domain_properties($domain) { * @param string $querypart - core part of the query (starting at "FROM") e.g. FROM alias WHERE address like ... * @return array */ -function create_page_browser($idxfield, $querypart, $sql_params = []) { +function create_page_browser($idxfield, $querypart, $sql_params = []) +{ global $CONF; - $page_size = (int) $CONF['page_size']; + $page_size = (int)$CONF['page_size']; $label_len = 2; $pagebrowser = array(); @@ -541,7 +578,7 @@ function create_page_browser($idxfield, $querypart, $sql_params = []) { $query = "SELECT count(*) as counter FROM (SELECT $idxfield $querypart) AS tmp"; $result = db_query_one($query, $sql_params); if ($result && isset($result['counter'])) { - $count_results = $result['counter'] -1; # we start counting at 0, not 1 + $count_results = $result['counter'] - 1; # we start counting at 0, not 1 } if ($count_results < $page_size) { @@ -594,9 +631,9 @@ function create_page_browser($idxfield, $querypart, $sql_params = []) { # afterwards: DROP SEQUENCE foo $result = db_query_all($query, $sql_params); - for ($k = 0; $k < count($result); $k+=2) { + for ($k = 0; $k < count($result); $k += 2) { if (isset($result[$k + 1])) { - $label = substr($result[$k]['label'], 0, $label_len) . '-' . substr($result[$k+1]['label'], 0, $label_len); + $label = substr($result[$k]['label'], 0, $label_len) . '-' . substr($result[$k + 1]['label'], 0, $label_len); } else { $label = substr($result[$k]['label'], 0, $label_len); } @@ -616,11 +653,12 @@ function create_page_browser($idxfield, $querypart, $sql_params = []) { * @param int $quota * @return float */ -function divide_quota($quota) { +function divide_quota($quota) +{ if ($quota == -1) { return $quota; } - $value = round($quota / (int) Config::read_string('quota_multiplier'), 2); + $value = round($quota / (int)Config::read_string('quota_multiplier'), 2); return $value; } @@ -631,11 +669,12 @@ function divide_quota($quota) { * @param string $domain * @return bool */ -function check_owner($username, $domain) { +function check_owner($username, $domain) +{ $table_domain_admins = table_by_key('domain_admins'); $result = db_query_all( - "SELECT 1 FROM $table_domain_admins WHERE username= ? AND (domain = ? OR domain = 'ALL') AND active = ?" , + "SELECT 1 FROM $table_domain_admins WHERE username= ? AND (domain = ? OR domain = 'ALL') AND active = ?", array($username, $domain, db_get_boolean(true)) ); @@ -645,20 +684,20 @@ function check_owner($username, $domain) { } else { if (sizeof($result) > 2) { # more than 2 results means something really strange happened... flash_error("Permission check returned multiple results. Please go to 'edit admin' for your username and press the save " - . "button once to fix the database. If this doesn't help, open a bugreport."); + . "button once to fix the database. If this doesn't help, open a bugreport."); } return false; } } - /** * List domains for an admin user. - * @param String $username + * @param string $username * @return array of domain names. */ -function list_domains_for_admin($username) { +function list_domains_for_admin($username) +{ $table_domain = table_by_key('domain'); $table_domain_admins = table_by_key('domain_admins'); @@ -680,7 +719,7 @@ function list_domains_for_admin($username) { $query .= " LEFT JOIN $table_domain_admins ON $table_domain.domain=$table_domain_admins.domain "; $condition[] = "$table_domain_admins.username = :username "; $condition[] = "$table_domain.active = :active "; # TODO: does it really make sense to exclude inactive... - $condition[] = "$table_domain.backupmx = :backupmx" ; # TODO: ... and backupmx domains for non-superadmins? + $condition[] = "$table_domain.backupmx = :backupmx"; # TODO: ... and backupmx domains for non-superadmins? } $query .= " WHERE " . join(' AND ', $condition); @@ -696,7 +735,8 @@ function list_domains_for_admin($username) { * * @return array */ -function list_domains() { +function list_domains() +{ $list = array(); $table_domain = table_by_key('domain'); @@ -710,8 +750,6 @@ function list_domains() { } - - // // list_admins // Action: Lists all the admins @@ -719,7 +757,8 @@ function list_domains() { // // was admin_list_admins // -function list_admins() { +function list_admins() +{ $handler = new AdminHandler(); $handler->getList(''); @@ -728,13 +767,13 @@ function list_admins() { } - // // encode_header // Action: Encode a string according to RFC 1522 for use in headers if it contains 8-bit characters. // Call: encode_header (string header, string charset) // -function encode_header($string, $default_charset = "utf-8") { +function encode_header($string, $default_charset = "utf-8") +{ if (strtolower($default_charset) == 'iso-8859-1') { $string = str_replace("\240", ' ', $string); } @@ -757,9 +796,9 @@ function encode_header($string, $default_charset = "utf-8") { if ($iEncStart === false) { $iEncStart = $i; } - $cur_l+=3; - if ($cur_l > ($max_l-2)) { - $aRet[] = substr($string, $iOffset, $iEncStart-$iOffset); + $cur_l += 3; + if ($cur_l > ($max_l - 2)) { + $aRet[] = substr($string, $iOffset, $iEncStart - $iOffset); $aRet[] = "=?$default_charset?Q?$ret?="; $iOffset = $i; $cur_l = 0; @@ -772,7 +811,7 @@ function encode_header($string, $default_charset = "utf-8") { case '(': case ')': if ($iEncStart !== false) { - $aRet[] = substr($string, $iOffset, $iEncStart-$iOffset); + $aRet[] = substr($string, $iOffset, $iEncStart - $iOffset); $aRet[] = "=?$default_charset?Q?$ret?="; $iOffset = $i; $cur_l = 0; @@ -784,7 +823,7 @@ function encode_header($string, $default_charset = "utf-8") { if ($iEncStart !== false) { $cur_l++; if ($cur_l > $max_l) { - $aRet[] = substr($string, $iOffset, $iEncStart-$iOffset); + $aRet[] = substr($string, $iOffset, $iEncStart - $iOffset); $aRet[] = "=?$default_charset?Q?$ret?="; $iOffset = $i; $cur_l = 0; @@ -809,8 +848,8 @@ function encode_header($string, $default_charset = "utf-8") { } $cur_l += 3; // first we add the encoded string that reached it's max size - if ($cur_l > ($max_l-2)) { - $aRet[] = substr($string, $iOffset, $iEncStart-$iOffset); + if ($cur_l > ($max_l - 2)) { + $aRet[] = substr($string, $iOffset, $iEncStart - $iOffset); $aRet[] = "=?$default_charset?Q?$ret?= "; $cur_l = 3; $ret = ''; @@ -823,7 +862,7 @@ function encode_header($string, $default_charset = "utf-8") { if ($iEncStart !== false) { $cur_l++; if ($cur_l > $max_l) { - $aRet[] = substr($string, $iOffset, $iEncStart-$iOffset); + $aRet[] = substr($string, $iOffset, $iEncStart - $iOffset); $aRet[] = "=?$default_charset?Q?$ret?="; $iEncStart = false; $iOffset = $i; @@ -840,7 +879,7 @@ function encode_header($string, $default_charset = "utf-8") { } if ($enc_init) { if ($iEncStart !== false) { - $aRet[] = substr($string, $iOffset, $iEncStart-$iOffset); + $aRet[] = substr($string, $iOffset, $iEncStart - $iOffset); $aRet[] = "=?$default_charset?Q?$ret?="; } else { $aRet[] = substr($string, $iOffset); @@ -851,18 +890,14 @@ function encode_header($string, $default_charset = "utf-8") { } -if (!function_exists('random_int')) { // PHP version < 7.0 - require_once(dirname(__FILE__) . '/lib/block_random_int.php'); -} - - /** * Generate a random password of $length characters. * @param int $length (optional, default: 12) * @return string * */ -function generate_password($length = 12) { +function generate_password($length = 12) +{ // define possible characters $possible = "2345678923456789abcdefghijkmnpqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ"; # skip 0 and 1 to avoid confusion with O and l @@ -870,7 +905,7 @@ function generate_password($length = 12) { // add random characters to $password until $length is reached $password = ""; while (strlen($password) < $length) { - $random = random_int(0, strlen($possible) -1); + $random = random_int(0, strlen($possible) - 1); $char = substr($possible, $random, 1); // we don't want this character if it's already in the password @@ -883,22 +918,35 @@ function generate_password($length = 12) { } - /** * Check if a password is strong enough based on the conditions in $CONF['password_validation'] * @param string $password * @return array of error messages, or empty array if the password is ok */ -function validate_password($password) { +function validate_password($password) +{ $result = array(); - $val_conf = Config::read_array('password_validation'); - $minlen = (int) Config::read_string('min_password_length'); # used up to 2.3.x - check it for backward compatibility - if ($minlen > 0) { + $config = Config::getInstance()->getAll(); + + $val_conf = $config['password_validation'] ?? []; + + $minlen = $config['min_password_length'] ?? null; + + if (is_numeric($minlen) && $minlen > 0) { + $minlen = (int)$minlen; # used up to 2.3.x - $val_conf['/.{' . $minlen . '}/'] = "password_too_short $minlen"; } foreach ($val_conf as $regex => $message) { + if (is_callable($message)) { + $ret = $message($password); + if (!empty($ret)) { + $result[] = $ret; + } + continue; + } + if (!preg_match($regex, $password)) { $msgparts = preg_split("/ /", $message, 2); if (count($msgparts) == 1) { @@ -916,8 +964,10 @@ function validate_password($password) { * @param string $pw * @param string $pw_db - encrypted hash * @return string crypt'ed password, should equal $pw_db if $pw matches the original + * @deprecated */ -function _pacrypt_md5crypt($pw, $pw_db = '') { +function _pacrypt_md5crypt($pw, $pw_db = '') +{ if ($pw_db) { $split_salt = preg_split('/\$/', $pw_db); if (isset($split_salt[2])) { @@ -929,11 +979,19 @@ function _pacrypt_md5crypt($pw, $pw_db = '') { return md5crypt($pw); } -function _pacrypt_crypt($pw, $pw_db = '') { +/** + * @todo fix this to not throw an E_NOTICE or deprecate/remove. + * @deprecated + */ +function _pacrypt_crypt($pw, $pw_db = '') +{ if ($pw_db) { return crypt($pw, $pw_db); } - return crypt($pw); + // PHP8 - we have to specify a salt here.... + $salt = substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, 2); + + return crypt($pw, $salt); } /** @@ -943,14 +1001,22 @@ function _pacrypt_crypt($pw, $pw_db = '') { * @param string $pw_db (hashed password) * @return string if $pw_db and the return value match then $pw matches the original password. */ -function _pacrypt_mysql_encrypt($pw, $pw_db = '') { +function _pacrypt_mysql_encrypt($pw, $pw_db = '') +{ // See https://sourceforge.net/tracker/?func=detail&atid=937966&aid=1793352&group_id=191583 // this is apparently useful for pam_mysql etc. - if ( $pw_db ) { + if ($pw_db) { $res = db_query_one("SELECT ENCRYPT(:pw,:pw_db) as result", ['pw' => $pw, 'pw_db' => $pw_db]); } else { - $res= db_query_one("SELECT ENCRYPT(:pw) as result", ['pw' => $pw]); + // see https://security.stackexchange.com/questions/150687/is-it-safe-to-use-the-encrypt-function-in-mysql-to-hash-passwords + // if no existing password, use a random SHA512 salt. + $salt = _php_crypt_generate_crypt_salt(); + $res = db_query_one("SELECT ENCRYPT(:pw, CONCAT('$6$', '$salt')) as result", ['pw' => $pw]); + } + + if (!is_string($res['result'])) { + throw new \InvalidArgumentException("Unexpected DB result"); } return $res['result']; @@ -963,7 +1029,8 @@ function _pacrypt_mysql_encrypt($pw, $pw_db = '') { * @param string $pw_db (optional) * @return string crypted password - contains {xxx} prefix to identify mechanism. */ -function _pacrypt_authlib($pw, $pw_db) { +function _pacrypt_authlib($pw, $pw_db) +{ global $CONF; $flavor = $CONF['authlib_default_flavor']; $salt = substr(create_salt(), 0, 2); # courier-authlib supports only two-character salts @@ -995,16 +1062,17 @@ function _pacrypt_authlib($pw, $pw_db) { * @param string $pw_db - encrypted password, or '' for generation. * @return string crypted password */ -function _pacrypt_dovecot($pw, $pw_db = '') { +function _pacrypt_dovecot($pw, $pw_db = '') +{ global $CONF; $split_method = preg_split('/:/', $CONF['encrypt']); - $method = strtoupper($split_method[1]); + $method = strtoupper($split_method[1]); # If $pw_db starts with {method}, change $method accordingly if (!empty($pw_db) && preg_match('/^\{([A-Z0-9.-]+)\}.+/', $pw_db, $method_matches)) { $method = $method_matches[1]; } - if (! preg_match("/^[A-Z0-9.-]+$/", $method)) { + if (!preg_match("/^[A-Z0-9.-]+$/", $method)) { throw new Exception("invalid dovecot encryption method"); } @@ -1027,13 +1095,16 @@ function _pacrypt_dovecot($pw, $pw_db = '') { ); $nonsaltedtypes = "SHA|SHA1|SHA256|SHA512|CLEAR|CLEARTEXT|PLAIN|PLAIN-TRUNC|CRAM-MD5|HMAC-MD5|PLAIN-MD4|PLAIN-MD5|LDAP-MD5|LANMAN|NTLM|RPA"; - $salted = ! preg_match("/^($nonsaltedtypes)(\.B64|\.BASE64|\.HEX)?$/", strtoupper($method)); + $salted = !preg_match("/^($nonsaltedtypes)(\.B64|\.BASE64|\.HEX)?$/", strtoupper($method)); $dovepasstest = ''; if ($salted && (!empty($pw_db))) { # only use -t for salted passwords to be backward compatible with dovecot < 2.1 $dovepasstest = " -t " . escapeshellarg($pw_db); } + + $pipes = []; + $pipe = proc_open("$dovecotpw '-s' $method$dovepasstest", $spec, $pipes); if (!$pipe) { @@ -1043,24 +1114,31 @@ function _pacrypt_dovecot($pw, $pw_db = '') { // use dovecot's stdin, it uses getpass() twice (except when using -t) // Write pass in pipe stdin if (empty($dovepasstest)) { - fwrite($pipes[0], $pw . "\n", 1+strlen($pw)); + fwrite($pipes[0], $pw . "\n", 1 + strlen($pw)); usleep(1000); } - fwrite($pipes[0], $pw . "\n", 1+strlen($pw)); + + fwrite($pipes[0], $pw . "\n", 1 + strlen($pw)); fclose($pipes[0]); + $stderr_output = stream_get_contents($pipes[2]); + // Read hash from pipe stdout $password = fread($pipes[1], 200); + if (!empty($stderr_output) || empty($password)) { + error_log("Failed to read password from $dovecotpw ... stderr: $stderr_output, password: $password "); + throw new Exception("$dovecotpw failed, see error log for details"); + } + if (empty($dovepasstest)) { if (!preg_match('/^\{' . $method . '\}/', $password)) { - $stderr_output = stream_get_contents($pipes[2]); - error_log('dovecotpw password encryption failed. STDERR output: '. $stderr_output); + error_log("dovecotpw password encryption failed (method: $method) . stderr: $stderr_output"); throw new Exception("can't encrypt password with dovecotpw, see error log for details"); } } else { if (!preg_match('(verified)', $password)) { - $password="Thepasswordcannotbeverified"; + $password = "Thepasswordcannotbeverified"; } else { $password = rtrim(str_replace('(verified)', '', $password)); } @@ -1081,12 +1159,15 @@ function _pacrypt_dovecot($pw, $pw_db = '') { /** * Supports DES, MD5, BLOWFISH, SHA256, SHA512 methods. * + * Via config we support an optional prefix (e.g. if you need hashes to start with {SHA256-CRYPT} and optional rounds (hardness) setting. + * * @param string $pw * @param string $pw_db (can be empty if setting a new password) * @return string crypt'ed password; if it matches $pw_db then $pw is the original password. */ -function _pacrypt_php_crypt($pw, $pw_db) { - global $CONF; +function _pacrypt_php_crypt($pw, $pw_db) +{ + $configEncrypt = Config::read_string('encrypt'); // use PHPs crypt(), which uses the system's crypt() // same algorithms as used in /etc/shadow @@ -1094,109 +1175,126 @@ function _pacrypt_php_crypt($pw, $pw_db) { // the algorithm for a new hash is chosen by feeding a salt with correct magic to crypt() // set $CONF['encrypt'] to 'php_crypt' to use the default SHA512 crypt method // set $CONF['encrypt'] to 'php_crypt:METHOD' to use another method; methods supported: DES, MD5, BLOWFISH, SHA256, SHA512 + // set $CONF['encrypt'] to 'php_crypt:METHOD:difficulty' where difficulty is between 1000-999999999 + // set $CONF['encrypt'] to 'php_crypt:METHOD:difficulty:PREFIX' to prefix the hash with the {PREFIX} etc. // tested on linux + $prefix = ''; + if (strlen($pw_db) > 0) { // existing pw provided. send entire password hash as salt for crypt() to figure out $salt = $pw_db; + + // if there was a prefix in the password, use this (override anything given in the config). + + if (preg_match('/^\{([-A-Z0-9]+)\}(.+)$/', $pw_db, $method_matches)) { + $salt = $method_matches[2]; + $prefix = "{" . $method_matches[1] . "}"; + } } else { $salt_method = 'SHA512'; // hopefully a reasonable default (better than MD5) $hash_difficulty = ''; // no pw provided. create new password hash - if (strpos($CONF['encrypt'], ':') !== false) { + if (strpos($configEncrypt, ':') !== false) { // use specified hash method - $split_method = explode(':', $CONF['encrypt']); - $salt_method = $split_method[1]; - if (count($split_method) >= 3) { - $hash_difficulty = $split_method[2]; + $spec = explode(':', $configEncrypt); + $salt_method = $spec[1]; + if (isset($spec[2])) { + $hash_difficulty = $spec[2]; + } + if (isset($spec[3])) { + $prefix = $spec[3]; // hopefully something like {SHA256-CRYPT} } } // create appropriate salt for selected hash method $salt = _php_crypt_generate_crypt_salt($salt_method, $hash_difficulty); } - // send it to PHPs crypt() + $password = crypt($pw, $salt); - return $password; + + return "{$prefix}{$password}"; } + /** * @param string $hash_type must be one of: MD5, DES, BLOWFISH, SHA256 or SHA512 (default) * @param int hash difficulty * @return string */ -function _php_crypt_generate_crypt_salt($hash_type='SHA512', $hash_difficulty=null) { +function _php_crypt_generate_crypt_salt($hash_type = 'SHA512', $hash_difficulty = null) +{ // generate a salt (with magic matching chosen hash algorithm) for the PHP crypt() function // most commonly used alphabet $alphabet = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; switch ($hash_type) { - case 'DES': - $alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; - $length = 2; - $salt = _php_crypt_random_string($alphabet, $length); - return $salt; + case 'DES': + $alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + $length = 2; + $salt = _php_crypt_random_string($alphabet, $length); + return $salt; - case 'MD5': - $length = 12; - $algorithm = '1'; - $salt = _php_crypt_random_string($alphabet, $length); - return sprintf('$%s$%s', $algorithm, $salt); + case 'MD5': + $length = 12; + $algorithm = '1'; + $salt = _php_crypt_random_string($alphabet, $length); + return sprintf('$%s$%s', $algorithm, $salt); - case 'BLOWFISH': - $length = 22; - if (empty($hash_difficulty)) { - $cost = 10; - } else { - $cost = (int)$hash_difficulty; - if ($cost < 4 || $cost > 31) { - throw new Exception('invalid encrypt difficulty setting "' . $hash_difficulty . '" for ' . $hash_type . ', the valid range is 4-31'); + case 'BLOWFISH': + $length = 22; + if (empty($hash_difficulty)) { + $cost = 10; + } else { + $cost = (int)$hash_difficulty; + if ($cost < 4 || $cost > 31) { + throw new Exception('invalid encrypt difficulty setting "' . $hash_difficulty . '" for ' . $hash_type . ', the valid range is 4-31'); + } } - } - if (version_compare(PHP_VERSION, '5.3.7') >= 0) { - $algorithm = '2y'; // bcrypt, with fixed unicode problem - } else { - $algorithm = '2a'; // bcrypt - } - $salt = _php_crypt_random_string($alphabet, $length); - return sprintf('$%s$%02d$%s', $algorithm, $cost, $salt); - - case 'SHA256': - $length = 16; - $algorithm = '5'; - if (empty($hash_difficulty)) { - $rounds = ''; - } else { - $rounds = (int)$hash_difficulty; - if ($rounds < 1000 || $rounds > 999999999) { - throw new Exception('invalid encrypt difficulty setting "' . $hash_difficulty . '" for ' . $hash_type . ', the valid range is 1000-999999999'); + if (version_compare(PHP_VERSION, '5.3.7') >= 0) { + $algorithm = '2y'; // bcrypt, with fixed unicode problem + } else { + $algorithm = '2a'; // bcrypt } - } - $salt = _php_crypt_random_string($alphabet, $length); - if (!empty($rounds)) { - $rounds = sprintf('rounds=%d$', $rounds); - } - return sprintf('$%s$%s%s', $algorithm, $rounds, $salt); + $salt = _php_crypt_random_string($alphabet, $length); + return sprintf('$%s$%02d$%s', $algorithm, $cost, $salt); - case 'SHA512': - $length = 16; - $algorithm = '6'; - if (empty($hash_difficulty)) { - $rounds = ''; - } else { - $rounds = (int)$hash_difficulty; - if ($rounds < 1000 || $rounds > 999999999) { - throw new Exception('invalid encrypt difficulty setting "' . $hash_difficulty . '" for ' . $hash_type . ', the valid range is 1000-999999999'); + case 'SHA256': + $length = 16; + $algorithm = '5'; + if (empty($hash_difficulty)) { + $rounds = ''; + } else { + $rounds = (int)$hash_difficulty; + if ($rounds < 1000 || $rounds > 999999999) { + throw new Exception('invalid encrypt difficulty setting "' . $hash_difficulty . '" for ' . $hash_type . ', the valid range is 1000-999999999'); + } } - } - $salt = _php_crypt_random_string($alphabet, $length); - if (!empty($rounds)) { - $rounds = sprintf('rounds=%d$', $rounds); - } - return sprintf('$%s$%s%s', $algorithm, $rounds, $salt); + $salt = _php_crypt_random_string($alphabet, $length); + if (!empty($rounds)) { + $rounds = sprintf('rounds=%d$', $rounds); + } + return sprintf('$%s$%s%s', $algorithm, $rounds, $salt); - default: - throw new Exception("unknown hash type: '$hash_type'"); + case 'SHA512': + $length = 16; + $algorithm = '6'; + if (empty($hash_difficulty)) { + $rounds = ''; + } else { + $rounds = (int)$hash_difficulty; + if ($rounds < 1000 || $rounds > 999999999) { + throw new Exception('invalid encrypt difficulty setting "' . $hash_difficulty . '" for ' . $hash_type . ', the valid range is 1000-999999999'); + } + } + $salt = _php_crypt_random_string($alphabet, $length); + if (!empty($rounds)) { + $rounds = sprintf('rounds=%d$', $rounds); + } + return sprintf('$%s$%s%s', $algorithm, $rounds, $salt); + + default: + throw new Exception("unknown hash type: '$hash_type'"); } } @@ -1206,10 +1304,11 @@ function _php_crypt_generate_crypt_salt($hash_type='SHA512', $hash_difficulty=nu * @param int $length * @return string of given $length */ -function _php_crypt_random_string($characters, $length) { +function _php_crypt_random_string($characters, $length) +{ $string = ''; for ($p = 0; $p < $length; $p++) { - $string .= $characters[random_int(0, strlen($characters) -1)]; + $string .= $characters[random_int(0, strlen($characters) - 1)]; } return $string; } @@ -1226,33 +1325,72 @@ function _php_crypt_random_string($characters, $length) { * @param string $pw_db optional encrypted password * @return string encrypted password - if this matches $pw_db then the original password is $pw. */ -function pacrypt($pw, $pw_db="") { +function pacrypt($pw, $pw_db = "") +{ global $CONF; - switch ($CONF['encrypt']) { - case 'md5crypt': - return _pacrypt_md5crypt($pw, $pw_db); - case 'md5': - return md5($pw); - case 'system': - return _pacrypt_crypt($pw, $pw_db); - case 'cleartext': - return $pw; - case 'mysql_encrypt': - return _pacrypt_mysql_encrypt($pw, $pw_db); - case 'authlib': - return _pacrypt_authlib($pw, $pw_db); - } + $mechanism = strtoupper($CONF['encrypt'] ?? 'CRYPT'); - if (preg_match("/^dovecot:/", $CONF['encrypt'])) { - return _pacrypt_dovecot($pw, $pw_db); - } - if (substr($CONF['encrypt'], 0, 9) === 'php_crypt') { + if (preg_match('/^PHP_CRYPT:(DES|MD5|BLOWFISH|SHA256|SHA512):?/', $mechanism, $matches)) { return _pacrypt_php_crypt($pw, $pw_db); } - throw new Exception('unknown/invalid $CONF["encrypt"] setting: ' . $CONF['encrypt']); + + $crypts = ['PHP_CRYPT', 'MD5CRYPT']; + if (in_array($mechanism, $crypts)) { + $mechanism = 'CRYPT'; + } + + + if ($mechanism == 'AUTHLIB') { + return _pacrypt_authlib($pw, $pw_db); + } + + if (!empty($pw_db) && preg_match('/^{([0-9a-z-\.]+)}/i', $pw_db, $matches)) { + $method_in_hash = $matches[1]; + if ('DOVECOT:' . strtoupper($method_in_hash) == $mechanism || 'COURIER:' . strtoupper($method_in_hash) == $mechanism) { + // don't try and be clever. + } elseif ($mechanism != $method_in_hash) { + error_log("PostfixAdmin: configured to use $mechanism, but asked to crypt password using {$method_in_hash}; are you migrating algorithm/mechanism or is something wrong?"); + $mechanism = $method_in_hash; + } + } + + if ($mechanism == 'MD5RAW') { + $mechanism = 'COURIER:MD5RAW'; + } + + if (!empty($pw_db) && preg_match('/^\$[0-9]\$/i', $pw_db, $matches)) { + $method_in_hash = $matches[0]; + switch ($method_in_hash) { + case '$1$': + case '$6$': + $algorithm = 'SYSTEM'; + } + } + + if ($mechanism == 'SHA512.B64') { + // postfixadmin incorrectly uses this as a SHA512-CRYPT.B64 + $mechanism = 'SHA512-CRYPT.B64'; + + // backwards compatability - see https://github.com/postfixadmin/postfixadmin/issues/58 and https://github.com/postfixadmin/postfixadmin/issues/647 + // if we are configured for SHA512.B64, support existing passwords in MD5-CRYPT format. + if ($pw_db && strncmp($pw_db, '{MD5-CRYPT}', 11) == 0) { + $mechanism = 'MD5-CRYPT'; + } + } + + if (preg_match('/^DOVECOT:(.*)$/i', $mechanism, $matches)) { + return _pacrypt_dovecot($pw, $pw_db); + } + + if (empty($pw_db)) { + $pw_db = null; + } + + $hasher = new \PostfixAdmin\PasswordHashing\Crypt($mechanism); + return $hasher->crypt($pw, $pw_db); } /** @@ -1263,8 +1401,10 @@ function pacrypt($pw, $pw_db="") { * @param string $salt (optional) * @param string $magic (optional) * @return string hashed password in crypt format. + * @deprecated see PFACrypt::cryptMd5() (note this returns {MD5} prefix */ -function md5crypt($pw, $salt="", $magic="") { +function md5crypt($pw, $salt = "", $magic = "") +{ $MAGIC = "$1$"; if ($magic == "") { @@ -1282,7 +1422,7 @@ function md5crypt($pw, $salt="", $magic="") { $ctx = $pw . $magic . $salt; $final = hex2bin(md5($pw . $salt . $pw)); - for ($i=strlen($pw); $i>0; $i-=16) { + for ($i = strlen($pw); $i > 0; $i -= 16) { if ($i > 16) { $ctx .= substr($final, 0, 16); } else { @@ -1301,7 +1441,7 @@ function md5crypt($pw, $salt="", $magic="") { } $final = hex2bin(md5($ctx)); - for ($i=0;$i<1000;$i++) { + for ($i = 0; $i < 1000; $i++) { $ctx1 = ""; if ($i & 1) { $ctx1 .= $pw; @@ -1334,8 +1474,9 @@ function md5crypt($pw, $salt="", $magic="") { /** * @return string - should be random, 8 chars long */ -function create_salt() { - srand((int) microtime()*1000000); +function create_salt() +{ + srand((int)microtime() * 1000000); $salt = substr(md5("" . rand(0, 9999999)), 0, 8); return $salt; } @@ -1343,7 +1484,8 @@ function create_salt() { /* * remove item $item from array $array */ -function remove_from_array($array, $item) { +function remove_from_array($array, $item) +{ # array_diff might be faster, but doesn't provide an easy way to know if the value was found or not # return array_diff($array, array($item)); $ret = array_search($item, $array); @@ -1356,7 +1498,8 @@ function remove_from_array($array, $item) { return array($found, $array); } -function to64($v, $n) { +function to64($v, $n) +{ $ITOA64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; $ret = ""; while (($n - 1) >= 0) { @@ -1367,26 +1510,34 @@ function to64($v, $n) { return $ret; } - +function enable_socket_crypto($fh) +{ + stream_set_blocking($fh, true); + stream_socket_enable_crypto($fh, true, STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT); + stream_set_blocking($fh, true); +} /** * smtp_mail * Action: Send email * Call: smtp_mail (string to, string from, string subject, string body]) - or - * Call: smtp_mail (string to, string from, string data) - DEPRECATED - * @param String - To: - * @param String - From: - * @param String - Subject: (if called with 4 parameters) or full mail body (if called with 3 parameters) - * @param String (optional) - Password - * @param String (optional, but recommended) - mail body + * @param string $to + * @param string $from + * @param string $subject (if called with 4 parameters) or full mail body (if called with 3 parameters) + * @param string $password (optional) - Password + * @param string $body (optional, but recommended) - mail body * @return bool - true on success, otherwise false * TODO: Replace this with something decent like PEAR::Mail or Zend_Mail. */ -function smtp_mail($to, $from, $data, $password = "", $body = "") { +function smtp_mail($to, $from, $data, $password = "", $body = "") +{ global $CONF; + $smtpd_server = $CONF['smtp_server']; $smtpd_port = $CONF['smtp_port']; - //$smtp_server = $_SERVER["SERVER_NAME"]; + $smtpd_type = $CONF['smtp_type']; + $smtp_server = php_uname('n'); if (!empty($CONF['smtp_client'])) { $smtp_server = $CONF['smtp_client']; @@ -1405,8 +1556,7 @@ function smtp_mail($to, $from, $data, $password = "", $body = "") { . "Content-Type: text/plain; charset=utf-8\n" . "Content-Transfer-Encoding: 8bit\n" . "\n" - . $body - ; + . $body; } else { $maildata = $data; } @@ -1417,22 +1567,23 @@ function smtp_mail($to, $from, $data, $password = "", $body = "") { error_log("fsockopen failed - errno: $errno - errstr: $errstr"); return false; } else { + if ($smtpd_type === "tls") { + enable_socket_crypto($fh); + } + smtp_get_response($fh); - if (Config::bool('smtp_sendmail_tls')) { + if ($smtpd_type === "starttls") { fputs($fh, "STARTTLS\r\n"); smtp_get_response($fh); - - stream_set_blocking($fh, true); - stream_socket_enable_crypto($fh, true, STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT); - stream_set_blocking($fh, true); + enable_socket_crypto($fh); } fputs($fh, "EHLO $smtp_server\r\n"); smtp_get_response($fh); if (!empty($password)) { - fputs($fh,"AUTH LOGIN\r\n"); + fputs($fh, "AUTH LOGIN\r\n"); smtp_get_response($fh); fputs($fh, base64_encode($from) . "\r\n"); smtp_get_response($fh); @@ -1461,13 +1612,19 @@ function smtp_mail($to, $from, $data, $password = "", $body = "") { * Call: smtp_get_admin_email * @return string - username/mail address */ -function smtp_get_admin_email() { +function smtp_get_admin_email(bool $fallback_to_loggedin_user = true) +{ $admin_email = Config::read_string('admin_email'); if (!empty($admin_email)) { return $admin_email; - } else { + } + + if ($fallback_to_loggedin_user) { + /* may do a redirect to login */ return authentication_get_username(); } + error_log(__FILE__ . " WARNING : Please set a PostfixAdmin admin_email setting in your config.local.php file"); + return "PasswordReset "; // this isn't good. } /** @@ -1476,7 +1633,8 @@ function smtp_get_admin_email() { * Call: smtp_get_admin_password * @return string - admin smtp password */ -function smtp_get_admin_password() { +function smtp_get_admin_password() +{ return Config::read_string('admin_smtp_password'); } @@ -1486,8 +1644,9 @@ function smtp_get_admin_password() { // Action: Get response from mail server // Call: smtp_get_response (string FileHandle) // -function smtp_get_response($fh) { - $res =''; +function smtp_get_response($fh) +{ + $res = ''; do { $line = fgets($fh, 256); $res .= $line; @@ -1496,7 +1655,6 @@ function smtp_get_response($fh) { } - $DEBUG_TEXT = <<Please check the documentation and website for more information.