From 880d037dcc60a6b04aed0ffde2cbe1e4244ede7b Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Fri, 31 May 2019 13:40:18 +0200 Subject: [PATCH 01/24] Use postfixadmin with admin smtp password support --- Dockerfile | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++-- README.md | 1 + 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 80d4705..5625fb2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,55 @@ -FROM hardware/postfixadmin -LABEL maintainer "pinguin@digitalkr.am" +FROM alpine:3.8 + +LABEL description "PostfixAdmin is a web based interface used to manage mailboxes" \ + maintainer="/e/ " + +ARG VERSION=3.2 + +# https://pgp.mit.edu/pks/lookup?search=0xC6A682EA63C82F1C&fingerprint=on&op=index +# pub 4096R/63C82F1C 2005-10-06 Christian Boltz (www.cboltz.de) +ARG GPG_SHORTID="0xC6A682EA63C82F1C" +ARG GPG_FINGERPRINT="70CA A060 DE04 2AAE B1B1 5196 C6A6 82EA 63C8 2F1C" +ARG SHA256_HASH="866d4c0ca870b2cac184e5837a4d201af8fcefecef09bc2c887a6e017a00cefe" + +RUN apk add -t build-dependencies \ + ca-certificates \ + gnupg \ + && apk add \ + su-exec \ + dovecot \ + tini \ + php7 \ + php7-phar \ + php7-fpm \ + php7-imap \ + php7-pgsql \ + php7-mysqli \ + php7-session \ + php7-mbstring \ + && cd /tmp \ + && PFA_TARBALL="postfixadmin-${VERSION}.tar.gz" \ +# && wget -q https://downloads.sourceforge.net/project/postfixadmin/postfixadmin/postfixadmin-${VERSION}/${PFA_TARBALL} \ +# && wget -q https://downloads.sourceforge.net/project/postfixadmin/postfixadmin/postfixadmin-${VERSION}/${PFA_TARBALL}.asc \ +# && ( \ +# gpg --keyserver pgp.mit.edu --recv-keys ${GPG_SHORTID} || \ +# gpg --keyserver keyserver.pgp.com --recv-keys ${GPG_SHORTID} || \ +# gpg --keyserver ha.pool.sks-keyservers.net --recv-keys ${GPG_SHORTID} \ +# ) \ +# && CHECKSUM=$(sha256sum ${PFA_TARBALL} | awk '{print $1}') \ +# && if [ "${CHECKSUM}" != "${SHA256_HASH}" ]; then echo "ERROR: Checksum does not match!" && exit 1; fi \ +# && FINGERPRINT="$(LANG=C gpg --verify ${PFA_TARBALL}.asc ${PFA_TARBALL} 2>&1 | sed -n "s#Primary key fingerprint: \(.*\)#\1#p")" \ +# && if [ -z "${FINGERPRINT}" ]; then echo "ERROR: Invalid GPG signature!" && exit 1; fi \ +# && if [ "${FINGERPRINT}" != "${GPG_FINGERPRINT}" ]; then echo "ERROR: Wrong GPG fingerprint!" && exit 1; fi \ +# TODO: no hash/signature check + && wget -q "https://github.com/Nutomic/postfixadmin/archive/admin-smtp-password.tar.gz" -O "$PFA_TARBALL" \ + && mkdir /postfixadmin && tar xzf ${PFA_TARBALL} -C /postfixadmin && mv /postfixadmin/postfixadmin-$VERSION/* /postfixadmin \ + && apk del build-dependencies \ + && rm -rf /var/cache/apk/* /tmp/* /root/.gnupg /postfixadmin/postfixadmin-$VERSION* COPY bin/run.sh /usr/local/bin COPY bin/postfixadmin-cli.php /postfixadmin/scripts/postfixadmin-cli.php RUN apk add --no-cache bash openssh \ && adduser -D -s /bin/bash -D pfexec + +EXPOSE 8888 +CMD ["tini", "--", "run.sh"] diff --git a/README.md b/README.md index 429bad2..e1f0e8d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # docker-postfixadmin +Based on [hardware/postfixadmin](https://hub.docker.com/r/hardware/postfixadmin) \ No newline at end of file -- GitLab From 84752966eeba1b0f99c80a6dbee32035765b5061 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Fri, 31 May 2019 15:43:52 +0200 Subject: [PATCH 02/24] fix build --- Dockerfile | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5625fb2..8f43935 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,6 +11,9 @@ ARG GPG_SHORTID="0xC6A682EA63C82F1C" ARG GPG_FINGERPRINT="70CA A060 DE04 2AAE B1B1 5196 C6A6 82EA 63C8 2F1C" ARG SHA256_HASH="866d4c0ca870b2cac184e5837a4d201af8fcefecef09bc2c887a6e017a00cefe" +# TODO: might want to use a commit/release instead +ARG BRANCH="admin-smtp-password" + RUN apk add -t build-dependencies \ ca-certificates \ gnupg \ @@ -41,10 +44,12 @@ RUN apk add -t build-dependencies \ # && if [ -z "${FINGERPRINT}" ]; then echo "ERROR: Invalid GPG signature!" && exit 1; fi \ # && if [ "${FINGERPRINT}" != "${GPG_FINGERPRINT}" ]; then echo "ERROR: Wrong GPG fingerprint!" && exit 1; fi \ # TODO: no hash/signature check - && wget -q "https://github.com/Nutomic/postfixadmin/archive/admin-smtp-password.tar.gz" -O "$PFA_TARBALL" \ - && mkdir /postfixadmin && tar xzf ${PFA_TARBALL} -C /postfixadmin && mv /postfixadmin/postfixadmin-$VERSION/* /postfixadmin \ + && PFA_TARBALL="postfixadmin-${VERSION}.tar.gz" \ + && wget -q "https://github.com/Nutomic/postfixadmin/archive/$BRANCH.tar.gz" -O ${PFA_TARBALL} \ + && mkdir /postfixadmin && tar xzf ${PFA_TARBALL} -C /postfixadmin \ + && mv /postfixadmin/postfixadmin-$BRANCH/* /postfixadmin \ && apk del build-dependencies \ - && rm -rf /var/cache/apk/* /tmp/* /root/.gnupg /postfixadmin/postfixadmin-$VERSION* + && rm -rf /var/cache/apk/* /tmp/* /root/.gnupg /postfixadmin/postfixadmin-$BRANCH* COPY bin/run.sh /usr/local/bin COPY bin/postfixadmin-cli.php /postfixadmin/scripts/postfixadmin-cli.php -- GitLab From 445582bc02f9ef664783591c5103fc02bce8ad50 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Mon, 3 Jun 2019 15:42:46 +0200 Subject: [PATCH 03/24] dont use custom postfixadmin-cli --- Dockerfile | 1 - bin/postfixadmin-cli.php | 411 --------------------------------------- 2 files changed, 412 deletions(-) delete mode 100755 bin/postfixadmin-cli.php diff --git a/Dockerfile b/Dockerfile index 8f43935..3f4a46d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -52,7 +52,6 @@ RUN apk add -t build-dependencies \ && rm -rf /var/cache/apk/* /tmp/* /root/.gnupg /postfixadmin/postfixadmin-$BRANCH* COPY bin/run.sh /usr/local/bin -COPY bin/postfixadmin-cli.php /postfixadmin/scripts/postfixadmin-cli.php RUN apk add --no-cache bash openssh \ && adduser -D -s /bin/bash -D pfexec diff --git a/bin/postfixadmin-cli.php b/bin/postfixadmin-cli.php deleted file mode 100755 index 1989943..0000000 --- a/bin/postfixadmin-cli.php +++ /dev/null @@ -1,411 +0,0 @@ -#!/usr/bin/php - - * Copyright 2005-2008, Cake Software Foundation, Inc. - * 1785 E. Sahara Avenue, Suite 490-204 - * Las Vegas, Nevada 89104 - * Modified for PostfixAdmin by Valkum 2011 - * Modified for PostfixAdmin by Christian Boltz 2011-2013 - * - * Copyright 2010 - * - * Licensed under The MIT License - * Redistributions of files must retain the above copyright notice. - * - * @filesource - * @copyright Copyright 2005-2008, Cake Software Foundation, Inc. - * @link http://postfixadmin.sourceforge.net/ Postfixadmin on Sourceforge - * @package postfixadmin - * @subpackage - - * @since - - * @version $Revision$ - * @modifiedby $LastChangedBy$ - * @lastmodified $Date$ - * @license http://www.opensource.org/licenses/mit-license.php The MIT License - */ - - -class PostfixAdmin { - /** - * Version - * - * @var string - */ - public $version ='0.2'; - - /** - * Standard input stream. - * - * @var resource - */ - public $stdin; - - /** - * Standard output stream. - * - * @var resource - */ - public $stdout; - - /** - * Standard error stream. - * - * @var resource - */ - public $stderr; - - /** - * Contains command switches parsed from the command line. - * - * @var array - */ - public $params = array(); - - /** - * Contains arguments parsed from the command line. - * - * @var array - */ - public $args = array(); - - /** - * The file name of the shell that was invoked. - * - * @var string - */ - public $shell; - - /** - * The class name of the shell that was invoked. - * - * @var string - */ - public $shellClass; - - /** - * The command called if public methods are available. - * - * @var string - */ - public $shellCommand; - - /** - * The name of the shell in camelized. - * - * @var string - */ - public $shellName; - - /** - * Constructor - * - * @param array $args the argv. - */ - public function __construct($args = array()) { - set_time_limit(0); - $this->__initConstants(); - $this->parseParams($args); - $this->__initEnvironment(); - } - - /** - * Defines core configuration. - */ - private function __initConstants() { - ini_set('display_errors', '1'); - ini_set('error_reporting', '' . E_ALL); - ini_set('html_errors', "0"); - ini_set('implicit_flush', "1"); - ini_set('max_execution_time', "0"); - } - - /** - * Defines current working environment. - */ - private function __initEnvironment() { - $this->stdin = fopen('php://stdin', 'r'); - $this->stdout = fopen('php://stdout', 'w'); - $this->stderr = fopen('php://stderr', 'w'); - - if (basename(__FILE__) != basename($this->args[0])) { - $this->stderr('Warning: the dispatcher may have been loaded incorrectly, which could lead to unexpected results...'); - if ($this->getInput('Continue anyway?', array('y', 'n'), 'y') == 'n') { - exit(1); - } - } - - $this->shiftArgs(); - } - - /** - * Dispatches a CLI request - */ - public function dispatch() { - check_db_version(); # ensure the database layout is up to date - - if (!isset($this->args[0])) { - $this->help(); - return; - } - - $this->shell = $this->args[0]; - $this->shiftArgs(); - $this->shellName = ucfirst($this->shell); - $this->shellClass = $this->shellName . 'Handler'; - - - if ($this->shell == 'help') { - $this->help(); - return; - } - - $command = 'help'; # not the worst default ;-) - if (isset($this->args[0])) { - $command = $this->args[0]; - } - - $this->shellCommand = $command; - $this->shellClass = 'Cli' . ucfirst($command); - - if (ucfirst($command) == 'Add' || ucfirst($command) == 'Update') { - $this->shellClass = 'CliEdit'; - } - - if (!class_exists($this->shellClass)) { - $this->stderr('Unknown task ' . $this->shellCommand); - return; - } - - $shell = new $this->shellClass($this); - - $shell->handler_to_use = ucfirst($this->shell) . 'Handler'; - - if (!class_exists($shell->handler_to_use)) { - $this->stderr('Unknown module ' . $this->shell); - return; - } - - $task = ucfirst($command); - - $shell->new = 0; - if ($task == 'Add') { - $shell->new = 1; - } - - # TODO: add a way to Cli* to signal if the selected handler is supported (for example, not all *Handler support changing the password) - - if (strtolower(get_parent_class($shell)) == 'shell') { - $handler = new $shell->handler_to_use; - if (in_array($task, $handler->taskNames)) { - $this->shiftArgs(); - $shell->startup(); - - if (isset($this->args[0]) && $this->args[0] == 'help') { - if (method_exists($shell, 'help')) { - $shell->help(); - exit(); - } else { - $this->help(); - } - } - - $shell->execute(); - return; - } - } - - $classMethods = get_class_methods($shell); - - $privateMethod = $missingCommand = false; - if ((in_array($command, $classMethods) || in_array(strtolower($command), $classMethods)) && strpos($command, '_', 0) === 0) { - $privateMethod = true; - } - - if (!in_array($command, $classMethods) && !in_array(strtolower($command), $classMethods)) { - $missingCommand = true; - } - - $protectedCommands = array( - 'in', 'out', 'err', 'hr', 'log', - '__construct', 'dispatch', 'stdout', 'stderr' - ); - - if (in_array(strtolower($command), $protectedCommands)) { - $missingCommand = true; - } - - if ($missingCommand && method_exists($shell, 'main')) { - $shell->startup(); - $shell->main(); - } elseif (!$privateMethod && method_exists($shell, $command)) { - $this->shiftArgs(); - $shell->startup(); - $shell->{$command}(); - } else { - $this->stderr("Unknown {$this->shellName} command '$command'.\nFor usage, try 'postfixadmin-cli {$this->shell} help'.\n\n"); - } - } - - /** - * Prompts the user for input, and returns it. - * - * @param string $prompt Prompt text. - * @param mixed $options Array or string of options. - * @param string $default Default input value. - * @return string Either the default value, or the user-provided input. - */ - public function getInput($prompt, $options = null, $default = null) { - if (!is_array($options)) { - $print_options = ''; - } else { - $print_options = '(' . implode('/', $options) . ')'; - } - - if ($default == null) { - $this->stdout($prompt . " $print_options \n" . '> ', false); - } else { - $this->stdout($prompt . " $print_options \n" . "[$default] > ", false); - } - $result = fgets($this->stdin); - - if ($result === false) { - exit(1); - } - $result = trim($result); - - if ($default != null && empty($result)) { - return $default; - } - return $result; - } - - /** - * Outputs to the stdout filehandle. - * - * @param string $string String to output. - * @param boolean $newline If true, the outputs gets an added newline. - */ - public function stdout($string, $newline = true) { - if ($newline) { - fwrite($this->stdout, $string . "\n"); - } else { - fwrite($this->stdout, $string); - } - } - - /** - * Outputs to the stderr filehandle. - * - * @param string $string Error text to output. - */ - public function stderr($string) { - fwrite($this->stderr, 'Error: '. $string . "\n"); - } - - /** - * Parses command line options - * - * @param array $params Parameters to parse - */ - public function parseParams($params) { - $this->__parseParams($params); - } - - /** - * Helper for recursively paraing params - */ - private function __parseParams($params) { - $count = count($params); - for ($i = 0; $i < $count; $i++) { - if (isset($params[$i])) { - if ($params[$i] != '' && $params[$i]{0} === '-') { - $key = substr($params[$i], 1); - $this->params[$key] = true; - unset($params[$i]); - if (isset($params[++$i])) { - # TODO: ideally we should know if a parameter can / must have a value instead of whitelisting known valid values starting with '-' (probably only bool doesn't need a value) - if ($params[$i]{0} !== '-' or $params[$i] != '-1') { - $this->params[$key] = $params[$i]; - unset($params[$i]); - } else { - $i--; - $this->__parseParams($params); - } - } - } else { - $this->args[] = $params[$i]; - unset($params[$i]); - } - } - } - } - - /** - * Removes first argument and shifts other arguments up - * - * @return boolean False if there are no arguments - */ - public function shiftArgs() { - if (empty($this->args)) { - return false; - } - unset($this->args[0]); - $this->args = array_values($this->args); - return true; - } - - /** - * prints help message and exits. - */ - public function help() { - $this->stdout("\nWelcome to Postfixadmin-CLI v" . $this->version); - $this->stdout("---------------------------------------------------------------"); - $this->stdout("Usage:"); - $this->stdout(" postfixadmin-cli [--option value --option2 value]"); - $this->stdout(""); - $this->stdout("Available modules:"); - - $modules = explode(',', 'admin,domain,mailbox,alias,aliasdomain,fetchmail'); - foreach ($modules as $module) { - $this->stdout(" $module"); - } - $this->stdout(""); - $this->stdout("Most modules support the following tasks:"); - $this->stdout(" view View an item"); - $this->stdout(" add Add an item"); - $this->stdout(" update Update an item"); - $this->stdout(" delete Delete an item"); - $this->stdout(" scheme Print database scheme (useful for developers only)"); - $this->stdout(" help Print help output"); - $this->stdout(""); - $this->stdout(""); - $this->stdout("For module-specific help, see:"); - $this->stdout(""); - $this->stdout(" postfixadmin-cli help"); - $this->stdout(" print a detailed list of available commands"); - $this->stdout(""); - $this->stdout(" postfixadmin-cli help"); - $this->stdout(" print a list of available options."); - $this->stdout(""); - - exit(); - } -} - - -define("POSTFIXADMIN_CLI", 1); - -require_once(dirname(__FILE__) . '/../common.php'); - -$dispatcher = new PostfixAdmin($argv); -$dispatcher->dispatch(); - -/* vim: set expandtab softtabstop=4 tabstop=4 shiftwidth=4: */ \ No newline at end of file -- GitLab From b658e2a2ffe6db5e170b7023ca25c1ac82d55dcf Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Mon, 3 Jun 2019 15:49:44 +0200 Subject: [PATCH 04/24] revert deps --- Dockerfile | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3f4a46d..1079744 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,21 +14,23 @@ ARG SHA256_HASH="866d4c0ca870b2cac184e5837a4d201af8fcefecef09bc2c887a6e017a00cef # TODO: might want to use a commit/release instead ARG BRANCH="admin-smtp-password" -RUN apk add -t build-dependencies \ +RUN echo "@community https://nl.alpinelinux.org/alpine/v3.8/community" >> /etc/apk/repositories \ + && apk -U upgrade \ + && apk add -t build-dependencies \ ca-certificates \ gnupg \ && apk add \ su-exec \ dovecot \ - tini \ - php7 \ + tini@community \ + php7@community \ php7-phar \ - php7-fpm \ - php7-imap \ - php7-pgsql \ - php7-mysqli \ - php7-session \ - php7-mbstring \ + php7-fpm@community \ + php7-imap@community \ + php7-pgsql@community \ + php7-mysqli@community \ + php7-session@community \ + php7-mbstring@community \ && cd /tmp \ && PFA_TARBALL="postfixadmin-${VERSION}.tar.gz" \ # && wget -q https://downloads.sourceforge.net/project/postfixadmin/postfixadmin/postfixadmin-${VERSION}/${PFA_TARBALL} \ -- GitLab From 38ab4680ce8e0fe672404640bd653ab3078d2a07 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Mon, 3 Jun 2019 15:54:36 +0200 Subject: [PATCH 05/24] explicitly install pdo --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 1079744..2814c80 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,6 +31,7 @@ RUN echo "@community https://nl.alpinelinux.org/alpine/v3.8/community" >> /etc/a php7-mysqli@community \ php7-session@community \ php7-mbstring@community \ + php7-pdo \ && cd /tmp \ && PFA_TARBALL="postfixadmin-${VERSION}.tar.gz" \ # && wget -q https://downloads.sourceforge.net/project/postfixadmin/postfixadmin/postfixadmin-${VERSION}/${PFA_TARBALL} \ -- GitLab From e05bc10098faf5e7b3cd44ac9ca620d1a1e7784a Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Mon, 3 Jun 2019 16:25:13 +0200 Subject: [PATCH 06/24] no community repo --- Dockerfile | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2814c80..cf284c1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,23 +14,21 @@ ARG SHA256_HASH="866d4c0ca870b2cac184e5837a4d201af8fcefecef09bc2c887a6e017a00cef # TODO: might want to use a commit/release instead ARG BRANCH="admin-smtp-password" -RUN echo "@community https://nl.alpinelinux.org/alpine/v3.8/community" >> /etc/apk/repositories \ - && apk -U upgrade \ - && apk add -t build-dependencies \ +RUN apk add -t build-dependencies \ ca-certificates \ gnupg \ && apk add \ su-exec \ dovecot \ - tini@community \ - php7@community \ + tini \ + php7 \ php7-phar \ - php7-fpm@community \ - php7-imap@community \ - php7-pgsql@community \ - php7-mysqli@community \ - php7-session@community \ - php7-mbstring@community \ + php7-fpm \ + php7-imap \ + php7-pgsql \ + php7-mysqli \ + php7-session \ + php7-mbstring \ php7-pdo \ && cd /tmp \ && PFA_TARBALL="postfixadmin-${VERSION}.tar.gz" \ -- GitLab From e8b7e64a308744d84cc35b37d956f1d03bf6bfe8 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Fri, 7 Jun 2019 11:32:57 +0200 Subject: [PATCH 07/24] add patched functions.php --- Dockerfile | 3 + bin/functions.inc.php | 2213 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 2216 insertions(+) create mode 100644 bin/functions.inc.php diff --git a/Dockerfile b/Dockerfile index cf284c1..ea809bd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -56,5 +56,8 @@ COPY bin/run.sh /usr/local/bin RUN apk add --no-cache bash openssh \ && adduser -D -s /bin/bash -D pfexec +# debug only +COPY bin/functions.inc.php /postfixadmin/functions.inc.php + EXPOSE 8888 CMD ["tini", "--", "run.sh"] diff --git a/bin/functions.inc.php b/bin/functions.inc.php new file mode 100644 index 0000000..8b8db8d --- /dev/null +++ b/bin/functions.inc.php @@ -0,0 +1,2213 @@ +'; + + foreach ($supported_languages as $lang => $lang_name) { + if ($lang == $current_lang) { + $selected = ' selected="selected"'; + } else { + $selected = ''; + } + $selector .= ""; + } + $selector .= ""; + return $selector; +} + + + + +/** + * Checks if a domain is valid + * @param string $domain + * @return string empty if the domain is valid, otherwise string with the errormessage + * + * @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) { + 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)))) { + + // Look for an AAAA, A, or MX record for the domain + + if (function_exists('checkdnsrr')) { + $start = microtime(true); # check for slow nameservers, part 1 + + // AAAA (IPv6) is only available in PHP v. >= 5 + if (version_compare(phpversion(), "5.0.0", ">=") && checkdnsrr($domain, 'AAAA')) { + $retval = ''; + } elseif (checkdnsrr($domain, 'A')) { + $retval = ''; + } elseif (checkdnsrr($domain, 'MX')) { + $retval = ''; + } else { + $retval = sprintf(Config::lang('pInvalidDomainDNS'), htmlentities($domain)); + } + + $end = microtime(true); # check for slow nameservers, part 2 + $time_needed = $end - $start; + if ($time_needed > 2) { + error_log("Warning: slow nameserver - lookup for $domain took $time_needed seconds"); + } + + return $retval; + } else { + return 'emailcheck_resolve_domain is enabled, but function (checkdnsrr) missing!'; + } + } + + return ''; +} + +/** + * Get password expiration value for a 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) { + $table_domain = table_by_key('domain'); + $query = "SELECT password_expiry FROM $table_domain WHERE domain= :domain"; + + $result = db_query_one($query, array('domain' => $domain)); + if (is_array($result) && isset($result['password_expiry'])) { + return $result['password_expiry']; + } + return 0; +} + +/** + * 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 + */ +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 + if (Config::bool('vacation')) { + $vacation_domain = Config::read_string('vacation_domain'); + $ce_email = preg_replace("/@$vacation_domain\$/", '', $ce_email); + $ce_email = preg_replace("/#/", '@', $ce_email); + } + + // Perform non-domain-part sanity checks + if (!preg_match('/^[-!#$%&\'*+\\.\/0-9=?A-Z^_{|}~]+' . '@' . '[^@]+$/i', $ce_email)) { + return "" . Config::lang_f('pInvalidMailRegex', $email); + } + + if (function_exists('filter_var')) { + $check = filter_var($email, FILTER_VALIDATE_EMAIL); + if (!$check) { + return "" . Config::lang_f('pInvalidMailRegex', $email); + } + } + // Determine domain name + $matches = array(); + if (preg_match('|@(.+)$|', $ce_email, $matches)) { + $domain=$matches[1]; + # check domain name + return "" . check_domain($domain); + } + + return "" . Config::lang_f('pInvalidMailRegex', $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 + * and MAY NOT be just \' ing. (e.g. sqlite and PgSQL change "it's" to "it''s". + * + * The PDO quote function surrounds what you pass in with quote marks; for legacy reasons we remove these, + * but assume the caller will actually add them back in (!). + * + * e.g. caller code looks like : + * + * + * $sql = "SELECT * FROM foo WHERE x = '" . escape_string('fish') . "'"; + * + * + * @param int|string $string parameters to escape + * @return string cleaned data, suitable for use within an SQL statement. + */ +function escape_string($string_or_int) { + $link = db_connect(); + $string_or_int = (string) $string_or_int; + $quoted = $link->quote($string_or_int); + return trim($quoted, "'"); +} + + +/** + * safeget + * Action: get value from $_GET[$param], or $default if $_GET[$param] is not set + * Call: $param = safeget('param') # replaces $param = $_GET['param'] + * - or - + * $param = safeget('param', 'default') + * + * @param string $param parameter name. + * @param string $default (optional) - default value if key is not set. + * @return string + */ +function safeget($param, $default = "") { + $retval = $default; + if (isset($_GET[$param])) { + $retval = $_GET[$param]; + } + return $retval; +} + +/** + * 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 + */ +function safepost($param, $default = "") { + $retval = $default; + if (isset($_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 + */ +function safecookie($param, $default = "") { + $retval = $default; + if (isset($_COOKIE[$param])) { + $retval = $_COOKIE[$param]; + } + return $retval; +} + +/** + * safesession + * @see safeget() + * @param string $param + * @param string $default (optional) + * @return string value from $_SESSION[$param] or $default + */ +function safesession($param, $default = "") { + $retval = $default; + if (isset($_SESSION[$param])) { + $retval = $_SESSION[$param]; + } + return $retval; +} + + +/** + * pacol + * @param int $allow_editing + * @param int $display_in_form + * @param int display_in_list + * @param string $type + * @param string PALANG_label + * @param string PALANG_desc + * @param any optional $default + * @param array $options optional options + * @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="") { + if ($PALANG_label != '') { + $PALANG_label = Config::lang($PALANG_label); + } + if ($PALANG_desc != '') { + $PALANG_desc = Config::lang($PALANG_desc); + } + + if (is_array($multiopt)) { # remaining parameters provided in named array + $not_in_db = 0; # keep default value + foreach ($multiopt as $key => $value) { + $$key = $value; # extract everything to the matching variable + } + } else { + $not_in_db = $multiopt; + } + + 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 + ); +} + +/** + * Action: Get all the properties of a domain. + * @param string $domain + * @return array + */ +function get_domain_properties($domain) { + $handler = new DomainHandler(); + if (!$handler->init($domain)) { + die("Error: " . join("\n", $handler->errormsg)); + } + + if (!$handler->view()) { + die("Error: " . join("\n", $handler->errormsg)); + } + + $result = $handler->result(); + return $result; +} + + +/** + * create_page_browser + * Action: Get page browser for a long list of mailboxes, aliases etc. + * Call: $pagebrowser = create_page_browser('table.field', 'query', 50) # replaces $param = $_GET['param'] + * + * @param string $idxfield - database field name to use as title + * @param string $querypart - core part of the query (starting at "FROM") + * @return array + */ +function create_page_browser($idxfield, $querypart, $sql_params = []) { + global $CONF; + $page_size = (int) $CONF['page_size']; + $label_len = 2; + $pagebrowser = array(); + + $count_results = 0; + + if ($page_size < 2) { # will break the page browser + die('$CONF[\'page_size\'] must be 2 or more!'); + } + + # get number of rows + $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 + } + + if ($count_results < $page_size) { + return array(); # only one page - no pagebrowser required + } + + # init row counter + $initcount = "SET @r=-1"; + if (db_pgsql()) { + $initcount = "CREATE TEMPORARY SEQUENCE rowcount MINVALUE 0"; + } + if (!db_sqlite()) { + db_execute($initcount); + } + + # get labels for relevant rows (first and last of each page) + $page_size_zerobase = $page_size - 1; + $query = " + SELECT * FROM ( + SELECT $idxfield AS label, @r := @r + 1 AS 'r' $querypart + ) idx WHERE MOD(idx.r, $page_size) IN (0,$page_size_zerobase) OR idx.r = $count_results + "; + + if (db_pgsql()) { + $query = " + SELECT * FROM ( + SELECT $idxfield AS label, nextval('rowcount') AS row $querypart + ) idx WHERE MOD(idx.row, $page_size) IN (0,$page_size_zerobase) OR idx.row = $count_results + "; + } + + if (db_sqlite()) { + $query = " + WITH idx AS (SELECT * $querypart) + SELECT $idxfield AS label, (SELECT (COUNT(*) - 1) FROM idx t1 WHERE t1.$idxfield <= t2.$idxfield) AS row + FROM idx t2 + WHERE (row % $page_size) IN (0,$page_size_zerobase) OR row = $count_results"; + } + + # PostgreSQL: + # http://www.postgresql.org/docs/8.1/static/sql-createsequence.html + # http://www.postgresonline.com/journal/archives/79-Simulating-Row-Number-in-PostgreSQL-Pre-8.4.html + # http://www.pg-forum.de/sql/1518-nummerierung-der-abfrageergebnisse.html + # CREATE TEMPORARY SEQUENCE foo MINVALUE 0 MAXVALUE $page_size_zerobase CYCLE + # afterwards: DROP SEQUENCE foo + + $result = db_query_all($query, $sql_params); + foreach ($result as $k => $row) { + if (isset($result[$k + 1])) { + $row2 = $result[$k + 1]; + $label = substr($row['label'], 0, $label_len) . '-' . substr($row2['label'], 0, $label_len); + } else { + $label = substr($row['label'], 0, $label_len); + } + $pagebrowser[] = $label; + } + + if (db_pgsql()) { + db_execute("DROP SEQUENCE rowcount"); + } + + return $pagebrowser; +} + + +/** + * Recalculates the quota from MBs to bytes (divide, /) + * @param int $quota + * @return float + */ +function divide_quota($quota) { + if ($quota == -1) { + return $quota; + } + $value = round($quota / (int) Config::read_string('quota_multiplier'), 2); + return $value; +} + + +/** + * Checks if the admin is the owner of the domain (or global-admin) + * @param string $username + * @param string $domain + * @return bool + */ +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 = ?" , + array($username, $domain, db_get_boolean(true)) + ); + + if (sizeof($result) == 1 || sizeof($result) == 2) { # "ALL" + specific domain permissions is possible + # TODO: if superadmin, check if given domain exists in the database + return true; + } 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."); + } + return false; + } +} + + + +/** + * List domains for an admin user. + * @param String $username + * @return array of domain names. + */ +function list_domains_for_admin($username) { + $table_domain = table_by_key('domain'); + $table_domain_admins = table_by_key('domain_admins'); + + $condition = array(); + + $E_username = escape_string($username); + + $query = "SELECT $table_domain.domain FROM $table_domain "; + $condition[] = "$table_domain.domain != 'ALL'"; + + $pvalues = array(); + + $result = db_query_one("SELECT username FROM $table_domain_admins WHERE username= :username AND domain='ALL'", array('username' => $username)); + if (empty($result)) { # not a superadmin + $pvalues['username'] = $username; + $pvalues['active'] = db_get_boolean(true); + $pvalues['backupmx'] = db_get_boolean(false); + + $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? + } + + $query .= " WHERE " . join(' AND ', $condition); + $query .= " ORDER BY $table_domain.domain"; + + $result = db_query_all($query, $pvalues); + + return array_column($result, 'domain'); +} + + +if (!function_exists('array_column')) { + require_once(dirname(__FILE__) . '/lib/array_column.php'); +} + +/** + * List all available domains. + * + * @return array + */ +function list_domains() { + $list = array(); + + $table_domain = table_by_key('domain'); + $result = db_query_all("SELECT domain FROM $table_domain WHERE domain!='ALL' ORDER BY domain"); + $i = 0; + foreach ($result as $row) { + $list[$i] = $row['domain']; + $i++; + } + return $list; +} + + + + +// +// list_admins +// Action: Lists all the admins +// Call: list_admins () +// +// was admin_list_admins +// +function list_admins() { + $handler = new AdminHandler(); + + $handler->getList(''); + + return $handler->result(); +} + + + +// +// 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") { + if (strtolower($default_charset) == 'iso-8859-1') { + $string = str_replace("\240", ' ', $string); + } + + $j = strlen($string); + $max_l = 75 - strlen($default_charset) - 7; + $aRet = array(); + $ret = ''; + $iEncStart = $enc_init = false; + $cur_l = $iOffset = 0; + + for ($i = 0; $i < $j; ++$i) { + switch ($string{$i}) { + case '=': + case '<': + case '>': + case ',': + case '?': + case '_': + if ($iEncStart === false) { + $iEncStart = $i; + } + $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; + $ret = ''; + $iEncStart = false; + } else { + $ret .= sprintf("=%02X", ord($string{$i})); + } + break; + case '(': + case ')': + if ($iEncStart !== false) { + $aRet[] = substr($string, $iOffset, $iEncStart-$iOffset); + $aRet[] = "=?$default_charset?Q?$ret?="; + $iOffset = $i; + $cur_l = 0; + $ret = ''; + $iEncStart = false; + } + break; + case ' ': + if ($iEncStart !== false) { + $cur_l++; + if ($cur_l > $max_l) { + $aRet[] = substr($string, $iOffset, $iEncStart-$iOffset); + $aRet[] = "=?$default_charset?Q?$ret?="; + $iOffset = $i; + $cur_l = 0; + $ret = ''; + $iEncStart = false; + } else { + $ret .= '_'; + } + } + break; + default: + $k = ord($string{$i}); + if ($k > 126) { + if ($iEncStart === false) { + // do not start encoding in the middle of a string, also take the rest of the word. + $sLeadString = substr($string, 0, $i); + $aLeadString = explode(' ', $sLeadString); + $sToBeEncoded = array_pop($aLeadString); + $iEncStart = $i - strlen($sToBeEncoded); + $ret .= $sToBeEncoded; + $cur_l += strlen($sToBeEncoded); + } + $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); + $aRet[] = "=?$default_charset?Q?$ret?= "; + $cur_l = 3; + $ret = ''; + $iOffset = $i; + $iEncStart = $i; + } + $enc_init = true; + $ret .= sprintf("=%02X", $k); + } else { + if ($iEncStart !== false) { + $cur_l++; + if ($cur_l > $max_l) { + $aRet[] = substr($string, $iOffset, $iEncStart-$iOffset); + $aRet[] = "=?$default_charset?Q?$ret?="; + $iEncStart = false; + $iOffset = $i; + $cur_l = 0; + $ret = ''; + } else { + $ret .= $string{$i}; + } + } + } + break; + # end switch + } + } + if ($enc_init) { + if ($iEncStart !== false) { + $aRet[] = substr($string, $iOffset, $iEncStart-$iOffset); + $aRet[] = "=?$default_charset?Q?$ret?="; + } else { + $aRet[] = substr($string, $iOffset); + } + $string = implode('', $aRet); + } + return $string; +} + + +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) { + + // define possible characters + $possible = "2345678923456789abcdefghijkmnpqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ"; # skip 0 and 1 to avoid confusion with O and l + + // add random characters to $password until $length is reached + $password = ""; + while (strlen($password) < $length) { + $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 + if (!strstr($password, $char)) { + $password .= $char; + } + } + + return $password; +} + + + +/** + * 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) { + $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) { + $val_conf['/.{' . $minlen . '}/'] = "password_too_short $minlen"; + } + + foreach ($val_conf as $regex => $message) { + if (!preg_match($regex, $password)) { + $msgparts = preg_split("/ /", $message, 2); + if (count($msgparts) == 1) { + $result[] = Config::lang($msgparts[0]); + } else { + $result[] = sprintf(Config::lang($msgparts[0]), $msgparts[1]); + } + } + } + + return $result; +} + +/** + * @param string $pw + * @param string $pw_db - encrypted hash + * @return string crypt'ed password, should equal $pw_db if $pw matches the original + */ +function _pacrypt_md5crypt($pw, $pw_db = '') { + if ($pw_db) { + $split_salt = preg_split('/\$/', $pw_db); + if (isset($split_salt[2])) { + $salt = $split_salt[2]; + return md5crypt($pw, $salt); + } + } + + return md5crypt($pw); +} + +function _pacrypt_crypt($pw, $pw_db = '') { + if ($pw_db) { + return crypt($pw, $pw_db); + } + return crypt($pw); +} + +/** + * Crypt with MySQL's ENCRYPT function + * + * @param string $pw + * @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 = '') { + // 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 ) { + $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]); + } + + return $res['result']; +} + +/** + * Create/Validate courier authlib style crypt'ed passwords. (md5, md5raw, crypt, sha1) + * + * @param string $pw + * @param string $pw_db (optional) + * @return string crypted password - contains {xxx} prefix to identify mechanism. + */ +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 + if (preg_match('/^{.*}/', $pw_db)) { + // we have a flavor in the db -> use it instead of default flavor + $result = preg_split('/[{}]/', $pw_db, 3); # split at { and/or } + $flavor = $result[1]; + $salt = substr($result[2], 0, 2); + } + + if (stripos($flavor, 'md5raw') === 0) { + $password = '{' . $flavor . '}' . md5($pw); + } elseif (stripos($flavor, 'md5') === 0) { + $password = '{' . $flavor . '}' . base64_encode(md5($pw, true)); + } elseif (stripos($flavor, 'crypt') === 0) { + $password = '{' . $flavor . '}' . crypt($pw, $salt); + } elseif (stripos($flavor, 'SHA') === 0) { + $password = '{' . $flavor . '}' . base64_encode(sha1($pw, true)); + } else { + die("authlib_default_flavor '" . $flavor . "' unknown. Valid flavors are 'md5raw', 'md5', 'SHA' and 'crypt'"); + } + return $password; +} + +/** + * Uses the doveadm pw command, crypted passwords have a {...} prefix to identify type. + * + * @param string $pw - plain text password + * @param string $pw_db - encrypted password, or '' for generation. + * @return string crypted password + */ +function _pacrypt_dovecot($pw, $pw_db = '') { + global $CONF; + + $split_method = preg_split('/:/', $CONF['encrypt']); + $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)) { + die("invalid dovecot encryption method"); + } + + # TODO: check against a fixed list? + # if (strtolower($method) == 'md5-crypt') die("\$CONF['encrypt'] = 'dovecot:md5-crypt' will not work because dovecotpw generates a random salt each time. Please use \$CONF['encrypt'] = 'md5crypt' instead."); + # $crypt_method = preg_match ("/.*-CRYPT$/", $method); + + # digest-md5 hashes include the username - until someone implements it, let's declare it as unsupported + if (strtolower($method) == 'digest-md5') { + die("Sorry, \$CONF['encrypt'] = 'dovecot:digest-md5' is not supported by PostfixAdmin."); + } + # TODO: add -u option for those hashes, or for everything that is salted (-u was available before dovecot 2.1 -> no problem with backward compatibility ) + + $dovecotpw = "doveadm pw"; + if (!empty($CONF['dovecotpw'])) { + $dovecotpw = $CONF['dovecotpw']; + } + + # Use proc_open call to avoid safe_mode problems and to prevent showing plain password in process table + $spec = array( + 0 => array("pipe", "r"), // stdin + 1 => array("pipe", "w"), // stdout + 2 => array("pipe", "w"), // stderr + ); + + $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)); + + $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); + } + $pipe = proc_open("$dovecotpw '-s' $method$dovepasstest", $spec, $pipes); + + if (!$pipe) { + die("can't proc_open $dovecotpw"); + } + + // 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)); + usleep(1000); + } + fwrite($pipes[0], $pw . "\n", 1+strlen($pw)); + fclose($pipes[0]); + + // Read hash from pipe stdout + $password = fread($pipes[1], 200); + + 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); + die("can't encrypt password with dovecotpw, see error log for details"); + } + } else { + if (!preg_match('(verified)', $password)) { + $password="Thepasswordcannotbeverified"; + } else { + $password = rtrim(str_replace('(verified)', '', $password)); + } + } + + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($pipe); + + if ((!empty($pw_db)) && (substr($pw_db, 0, 1) != '{')) { + # for backward compability with "old" dovecot passwords that don't have the {method} prefix + $password = str_replace('{' . $method . '}', '', $password); + } + + return rtrim($password); +} + +/** + * Supports DES, MD5, BLOWFISH, SHA256, SHA512 methods. + * + * @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; + + // use PHPs crypt(), which uses the system's crypt() + // same algorithms as used in /etc/shadow + // you can have mixed hash types in the database for authentication, changed passwords get specified hash type + // 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 + // tested on linux + + if (strlen($pw_db) > 0) { + // existing pw provided. send entire password hash as salt for crypt() to figure out + $salt = $pw_db; + } 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) { + // use specified hash method + $split_method = explode(':', $CONF['encrypt']); + $salt_method = $split_method[1]; + if (count($split_method) >= 3) { + $hash_difficulty = $split_method[2]; + } + } + // 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; +} + +/** + * @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) { + // 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 '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) { + die('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) { + die('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); + + case 'SHA512': + $length = 16; + $algorithm = '6'; + if (empty($hash_difficulty)) { + $rounds = ''; + } else { + $rounds = (int)$hash_difficulty; + if ($rounds < 1000 || $rounds > 999999999) { + die('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: + die("unknown hash type: '$hash_type'"); + } +} + +/** + * Generates a random string of specified $length from $characters. + * @param string $characters + * @param int $length + * @return string of given $length + */ +function _php_crypt_random_string($characters, $length) { + $string = ''; + for ($p = 0; $p < $length; $p++) { + $string .= $characters[random_int(0, strlen($characters) -1)]; + } + return $string; +} + + +/** + * Encrypt a password, using the apparopriate hashing mechanism as defined in + * config.inc.php ($CONF['encrypt']). + * + * When wanting to compare one pw to another, it's necessary to provide the salt used - hence + * the second parameter ($pw_db), which is the existing hash from the DB. + * + * @param string $pw + * @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="") { + 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); + } + + if (preg_match("/^dovecot:/", $CONF['encrypt'])) { + return _pacrypt_dovecot($pw, $pw_db); + } + + if (substr($CONF['encrypt'], 0, 9) === 'php_crypt') { + return _pacrypt_php_crypt($pw, $pw_db); + } + + die('unknown/invalid $CONF["encrypt"] setting: ' . $CONF['encrypt']); +} + +/** + * Creates MD5 based crypt formatted password. + * If salt is not provided we generate one. + * + * @param string $pw plain text password + * @param string $salt (optional) + * @param string $magic (optional) + * @return string hashed password in crypt format. + */ +function md5crypt($pw, $salt="", $magic="") { + $MAGIC = "$1$"; + + if ($magic == "") { + $magic = $MAGIC; + } + if ($salt == "") { + $salt = create_salt(); + } + $slist = explode("$", $salt); + if ($slist[0] == "1") { + $salt = $slist[1]; + } + + $salt = substr($salt, 0, 8); + $ctx = $pw . $magic . $salt; + $final = hex2bin(md5($pw . $salt . $pw)); + + for ($i=strlen($pw); $i>0; $i-=16) { + if ($i > 16) { + $ctx .= substr($final, 0, 16); + } else { + $ctx .= substr($final, 0, $i); + } + } + $i = strlen($pw); + + while ($i > 0) { + if ($i & 1) { + $ctx .= chr(0); + } else { + $ctx .= $pw[0]; + } + $i = $i >> 1; + } + $final = hex2bin(md5($ctx)); + + for ($i=0;$i<1000;$i++) { + $ctx1 = ""; + if ($i & 1) { + $ctx1 .= $pw; + } else { + $ctx1 .= substr($final, 0, 16); + } + if ($i % 3) { + $ctx1 .= $salt; + } + if ($i % 7) { + $ctx1 .= $pw; + } + if ($i & 1) { + $ctx1 .= substr($final, 0, 16); + } else { + $ctx1 .= $pw; + } + $final = hex2bin(md5($ctx1)); + } + $passwd = ""; + $passwd .= to64(((ord($final[0]) << 16) | (ord($final[6]) << 8) | (ord($final[12]))), 4); + $passwd .= to64(((ord($final[1]) << 16) | (ord($final[7]) << 8) | (ord($final[13]))), 4); + $passwd .= to64(((ord($final[2]) << 16) | (ord($final[8]) << 8) | (ord($final[14]))), 4); + $passwd .= to64(((ord($final[3]) << 16) | (ord($final[9]) << 8) | (ord($final[15]))), 4); + $passwd .= to64(((ord($final[4]) << 16) | (ord($final[10]) << 8) | (ord($final[5]))), 4); + $passwd .= to64(ord($final[11]), 2); + return "$magic$salt\$$passwd"; +} + +/** + * @return string - should be random, 8 chars long + */ +function create_salt() { + srand((int) microtime()*1000000); + $salt = substr(md5("" . rand(0, 9999999)), 0, 8); + return $salt; +} + +/* + * remove item $item from array $array + */ +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); + if ($ret === false) { + $found = 0; + } else { + $found = 1; + unset($array[$ret]); + } + return array($found, $array); +} + +function to64($v, $n) { + $ITOA64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + $ret = ""; + while (($n - 1) >= 0) { + $n--; + $ret .= $ITOA64[$v & 0x3f]; + $v = $v >> 6; + } + return $ret; +} + + + +/** + * 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 + * @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 = "") { + global $CONF; + $smtpd_server = $CONF['smtp_server']; + $smtpd_port = $CONF['smtp_port']; + //$smtp_server = $_SERVER["SERVER_NAME"]; + $smtp_server = php_uname('n'); + if (!empty($CONF['smtp_client'])) { + $smtp_server = $CONF['smtp_client']; + } + $errno = 0; + $errstr = "0"; + $timeout = 30; + + if ($body != "") { + $maildata = + "To: " . $to . "\n" + . "From: " . $from . "\n" + . "Subject: " . encode_header($data) . "\n" + . "MIME-Version: 1.0\n" + . "Date: " . date('r') . "\n" + . "Content-Type: text/plain; charset=utf-8\n" + . "Content-Transfer-Encoding: 8bit\n" + . "\n" + . $body + ; + } else { + $maildata = $data; + } + + $fh = @fsockopen($smtpd_server, $smtpd_port, $errno, $errstr, $timeout); + + if (!$fh) { + error_log("fsockopen failed - errno: $errno - errstr: $errstr"); + return false; + } else { + smtp_get_response($fh); + fputs($fh, "EHLO $smtp_server\r\n"); + smtp_get_response($fh); + + if (!empty($password)) { + fputs($fh,"AUTH LOGIN\r\n"); + smtp_get_response($fh); + fputs($fh, base64_encode($from) . "\r\n"); + smtp_get_response($fh); + fputs($fh, base64_encode($password) . "\r\n"); + smtp_get_response($fh); + } + + fputs($fh, "MAIL FROM:<$from>\r\n"); + smtp_get_response($fh); + fputs($fh, "RCPT TO:<$to>\r\n"); + smtp_get_response($fh); + fputs($fh, "DATA\r\n"); + smtp_get_response($fh); + fputs($fh, "$maildata\r\n.\r\n"); + smtp_get_response($fh); + fputs($fh, "QUIT\r\n"); + smtp_get_response($fh); + fclose($fh); + } + return true; +} + +/** + * smtp_get_admin_email + * Action: Get configured email address or current user if nothing configured + * Call: smtp_get_admin_email + * @return string - username/mail address + */ +function smtp_get_admin_email() { + $admin_email = Config::read_string('admin_email'); + if (!empty($admin_email)) { + return $admin_email; + } else { + return authentication_get_username(); + } +} + +/** + * smtp_get_admin_password + * Action: Get smtp password for admin email + * Call: smtp_get_admin_password + * @return string - admin smtp password + */ +function smtp_get_admin_password() { + return Config::read_string('admin_smtp_password'); +} + + +// +// smtp_get_response +// Action: Get response from mail server +// Call: smtp_get_response (string FileHandle) +// +function smtp_get_response($fh) { + $res =''; + do { + $line = fgets($fh, 256); + $res .= $line; + } while (preg_match("/^\d\d\d\-/", $line)); + return $res; +} + + + +$DEBUG_TEXT = <<Please check the documentation and website for more information.

+ +EOF; + +/** + * db_connect + * Action: Makes a connection to the database if it doesn't exist + * Call: db_connect () + * + * Return value: + * + * @return \PDO + */ +function db_connect() { + list($link, $_) = db_connect_with_errors(); + unset($_); + + if (!$link instanceof PDO) { + throw new Exception("Database connection failed"); + } + + return $link; +} + +/** + * @param bool $ignore_errors + * @return array [PDO link | false, string $error_text]; + */ +function db_connect_with_errors() { + global $CONF; + global $DEBUG_TEXT; + + $error_text = ''; + + static $link; + if (isset($link) && $link) { + return array($link, $error_text); + } + $link = false; + + $options = array( + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + ); + $username_password = true; + + $queries = array(); + + $dsn = null; + + if (db_mysql()) { + $socket = Config::read_string('database_socket'); + $database_name = Config::read_string('database_name'); + + if ($socket) { + $dsn = "mysql:unix_socket={$socket};dbname={$database_name};charset=UTF8"; + } else { + $dsn = "mysql:host={$CONF['database_host']};dbname={$database_name};charset=UTF8"; + } + if (Config::bool('database_use_ssl')) { + $options[PDO::MYSQL_ATTR_SSL_CA] = Config::read_string('database_ssl_ca'); + $options[PDO::MYSQL_ATTR_SSL_CAPATH] = Config::read_string('database_ssl_ca_path'); + $options[PDO::MYSQL_ATTR_SSL_CERT] = Config::read_string('database_ssl_cert'); + $options[PDO::MYSQL_ATTR_SSL_CIPHER] = Config::read_string('database_ssl_cipher'); + } + $queries[] = 'SET CHARACTER SET utf8'; + $queries[] = "SET COLLATION_CONNECTION='utf8_general_ci'"; + } elseif (db_sqlite()) { + $db = $CONF['database_name']; + + if (!file_exists($db)) { + $error_text = 'SQLite database missing: '. $db; + return array($link, $error_text); + } + + if (!is_writeable($db)) { + $error_text = 'SQLite database not writeable: '. $db; + return array($link, $error_text); + } + + if (!is_writeable(dirname($db))) { + $error_text = 'The directory the SQLite database is in is not writeable: '. dirname($db); + return array($link, $error_text); + } + + $dsn = "sqlite:{$db}"; + $username_password = false; + } elseif (db_pgsql()) { + if (!isset($CONF['database_port'])) { + $CONF['database_port'] = '5432'; + } + $dsn = "pgsql:host={$CONF['database_host']};port={$CONF['database_port']};dbname={$CONF['database_name']};options='-c client_encoding=utf8'"; + } else { + die("

FATAL Error:
Invalid \$CONF['database_type']! Please fix your config.inc.php!

"); + } + + if ($username_password) { + $link = new PDO($dsn, Config::read_string('database_user'), Config::read_string('database_password'), $options); + } else { + $link = new PDO($dsn, null, null, $options); + } + + if (!empty($queries)) { + foreach ($queries as $q) { + $link->exec($q); + } + } + + return array($link, $error_text); +} + +/** + * Returns the appropriate boolean value for the database. + * + * @param bool $bool + * @return string|int as appropriate for underlying db platform + */ +function db_get_boolean($bool) { + if (! (is_bool($bool) || $bool == '0' || $bool == '1')) { + error_log("Invalid usage of 'db_get_boolean($bool)'"); + die("Invalid usage of 'db_get_boolean($bool)'"); + } + + if (db_pgsql()) { + // return either true or false (unquoted strings) + if ($bool) { + return 't'; + } + return 'f'; + } elseif (db_mysql() || db_sqlite()) { + if ($bool) { + return 1; + } + return 0; + } else { + die('Unknown value in $CONF[database_type]'); + } +} + +/** + * Returns a query that reports the used quota ("x / y") + * @param string column containing used quota + * @param string column containing allowed quota + * @param string column that will contain "x / y" + * @return string + */ +function db_quota_text($count, $quota, $fieldname) { + if (db_pgsql() || db_sqlite()) { + // SQLite and PostgreSQL use || to concatenate strings + return " CASE $quota + WHEN '-1' THEN (coalesce($count,0) || ' / -') + WHEN '0' THEN (coalesce($count,0) || ' / " . escape_string(html_entity_decode('∞')) . "') + ELSE (coalesce($count,0) || ' / ' || $quota) + END AS $fieldname"; + } else { + return " CASE $quota + WHEN '-1' THEN CONCAT(coalesce($count,0), ' / -') + WHEN '0' THEN CONCAT(coalesce($count,0), ' / ', '" . escape_string(html_entity_decode('∞')) . "') + ELSE CONCAT(coalesce($count,0), ' / ', $quota) + END AS $fieldname"; + } +} + +/** + * Returns a query that reports the used quota ("x / y") + * @param string column containing used quota + * @param string column containing allowed quota + * @param string column that will contain "x / y" + * @return string + */ +function db_quota_percent($count, $quota, $fieldname) { + return " CASE $quota + WHEN '-1' THEN -1 + WHEN '0' THEN -1 + ELSE round(100 * coalesce($count,0) / $quota) + END AS $fieldname"; +} + +/** + * @return boolean true if it's a MySQL database variant. + */ +function db_mysql() { + $type = Config::Read('database_type'); + + if ($type == 'mysql' || $type == 'mysqli') { + return true; + } + return false; +} + +/** + * @return bool true if PostgreSQL is used, false otherwise + */ +function db_pgsql() { + return Config::read_string('database_type') == 'pgsql'; +} + +/** + * returns true if SQLite is used, false otherwise + */ +function db_sqlite() { + if (Config::Read('database_type')=='sqlite') { + return true; + } else { + return false; + } +} + +/** + * @param string $sql + * @param array $values + * @return array + */ +function db_query_all($sql, array $values = []) { + $r = db_query($sql, $values); + return $r['result']->fetchAll(PDO::FETCH_ASSOC); +} + +/** + * @param string $sql + * @param array $values + * @return array + */ +function db_query_one($sql, array $values = []) { + $r = db_query($sql, $values); + return $r['result']->fetch(PDO::FETCH_ASSOC); +} + + +/** + * @param string $sql e.g. UPDATE foo SET bar = :baz + * @param array $values - parameters for the prepared statement e.g. ['baz' => 1234] + * @param bool $throw_exceptions + * @return int number of rows affected by the query + */ +function db_execute($sql, array $values = [], $throw_exceptions = false) { + $link = db_connect(); + + try { + $stmt = $link->prepare($sql); + $stmt->execute($values); + } catch (PDOException $e) { + $error_text = "Invalid query: " . $e->getMessage() . " caused by " . $sql ; + error_log($error_text); + if ($throw_exceptions) { + throw $e; + } + + return 0; + } + + return $stmt->rowCount(); +} + +/** + * @param string $sql + * @param array $values + * @param bool $ignore_errors - set to true to ignore errors. + * @return array e.g. ['result' => PDOStatement, 'error' => string ] + */ +function db_query($sql, array $values = array(), $ignore_errors = false) { + $link = db_connect(); + $error_text = ''; + + try { + $stmt = $link->prepare($sql); + $stmt->execute($values); + } catch (PDOException $e) { + $error_text = "Invalid query: " . $e->getMessage() . " caused by " . $sql ; + error_log($error_text); + if (!$ignore_errors) { + die("DEBUG INFORMATION: " . $e->getMessage() . "
Check your error_log for the failed query"); + } + } + + return array( + "result" => $stmt, + "error" => $error_text, + ); +} + + + + + +/** + * Delete a row from the specified table. + * + * DELETE FROM $table WHERE $where = $delete $aditionalWhere + * + * @param string $table + * @param string $where - should never be a user supplied value + * @param string $delete + * @param string $additionalwhere (default ''). + * @return int|mixed rows deleted. + */ +function db_delete($table, $where, $delete, $additionalwhere='') { + $table = table_by_key($table); + + $query = "DELETE FROM $table WHERE $where = ? $additionalwhere"; + + return db_execute($query, array($delete)); +} + + + +/** + * db_insert + * Action: Inserts a row from a specified table + * Call: db_insert (string table, array values [, array timestamp]) + * + * @param string - table name + * @param array $values - key/value map of data to insert into the table. + * @param array $timestamp (optional) - array of fields to set to now() - default: array('created', 'modified') + * @param boolean $throw_errors + * @return int - number of inserted rows + */ +function db_insert($table, array $values, $timestamp = array('created', 'modified'), $throw_exceptions = false) { + $table = table_by_key($table); + + foreach ($timestamp as $key) { + if (db_sqlite()) { + $values[$key] = "datetime('now')"; + } else { + $values[$key] = "now()"; + } + } + + $value_string = ''; + $comma = ''; + $prepared_statment_values = $values; + + foreach ($values as $field => $value) { + if (in_array($field, $timestamp)) { + $value_string .= $comma . $value; // see above. + unset($prepared_statment_values[$field]); + } else { + $value_string .= $comma . ":{$field}"; + } + $comma = ','; + } + + + return db_execute( + "INSERT INTO $table (" . implode(",", array_keys($values)) .") VALUES ($value_string)", + $prepared_statment_values, + $throw_exceptions); +} + + +/** + * db_update + * Action: Updates a specified table + * Call: db_update (string table, string where_col, string where_value, array values [, array timestamp]) + * @param string $table - table name + * @param string $where_col - column of WHERE condition + * @param string $where_value - value of WHERE condition + * @param array $values - key/value map of data to insert into the table. + * @param array $timestamp (optional) - array of fields to set to now() - default: array('modified') + * @return int - number of updated rows + */ +function db_update($table, $where_col, $where_value, $values, $timestamp = array('modified'), $throw_exceptions = false) { + $table_key = table_by_key($table); + + $sql = "UPDATE $table_key SET "; + + $pvalues = array(); + + $set = array(); + foreach ($values as $key => $value) { + if (in_array($key, $timestamp)) { + if (db_sqlite()) { + $set[] = " $key = datetime('now') "; + } else { + $set[] = " $key = now() "; + } + } else { + $set[] = " $key = :$key "; + $pvalues[$key] = $value; + } + } + + $pvalues['where'] = $where_value; + + + $sql="UPDATE $table_key SET " . implode(",", $set) . " WHERE $where_col = :where"; + + return db_execute($sql, $pvalues, $throw_exceptions); +} + + +/** + * db_log + * Action: Logs actions from admin + * Call: db_log (string domain, string action, string data) + * Possible actions are defined in $LANG["pViewlog_action_$action"] + */ +function db_log($domain, $action, $data) { + if (!Config::bool('logging')) { + return true; + } + + $REMOTE_ADDR = getRemoteAddr(); + + $username = authentication_get_username(); + + if (Config::Lang("pViewlog_action_$action") == '') { + die("Invalid log action : $action"); // could do with something better? + } + + + $logdata = array( + 'username' => "$username ($REMOTE_ADDR)", + 'domain' => $domain, + 'action' => $action, + 'data' => $data, + ); + $result = db_insert('log', $logdata, array('timestamp')); + if ($result != 1) { + return false; + } else { + return true; + } +} + +/** + * db_in_clause + * Action: builds and returns the "field in(x, y)" clause for database queries + * Call: db_in_clause (string field, array values) + * @param string $field + * @param array $values + * @return string + */ +function db_in_clause($field, array $values) { + $v = array_map('escape_string', array_values($values)); + return " $field IN ('" . implode("','", $v) . "') "; +} + +/** + * db_where_clause + * Action: builds and returns a WHERE clause for database queries. All given conditions will be AND'ed. + * Call: db_where_clause (array $conditions, array $struct) + * @param array $condition - array('field' => 'value', 'field2' => 'value2, ...) + * @param array $struct - field structure, used for automatic bool conversion + * @param string $additional_raw_where - raw sniplet to include in the WHERE part - typically needs to start with AND + * @param array $searchmode - operators to use (=, <, > etc.) - defaults to = if not specified for a field (see + * $allowed_operators for available operators) + * Note: the $searchmode operator will only be used if a $condition for that field is set. + * This also means you'll need to set a (dummy) condition for NULL and NOTNULL. + */ +function db_where_clause($condition, $struct, $additional_raw_where = '', $searchmode = array()) { + if (!is_array($condition)) { + die('db_where_cond: parameter $cond is not an array!'); + } elseif (!is_array($searchmode)) { + die('db_where_cond: parameter $searchmode is not an array!'); + } elseif (count($condition) == 0 && trim($additional_raw_where) == '') { + die("db_where_cond: parameter is an empty array!"); # die() might sound harsh, but can prevent information leaks + } elseif (!is_array($struct)) { + die('db_where_cond: parameter $struct is not an array!'); + } + + $allowed_operators = array('<', '>', '>=', '<=', '=', '!=', '<>', 'CONT', 'LIKE', 'NULL', 'NOTNULL'); + $where_parts = array(); + $having_parts = array(); + + foreach ($condition as $field => $value) { + if (isset($struct[$field]) && $struct[$field]['type'] == 'bool') { + $value = db_get_boolean($value); + } + $operator = '='; + if (isset($searchmode[$field])) { + if (in_array($searchmode[$field], $allowed_operators)) { + $operator = $searchmode[$field]; + + if ($operator == 'CONT') { # CONT - as in "contains" + $operator = ' LIKE '; # add spaces + $value = '%' . $value . '%'; + } elseif ($operator == 'LIKE') { # LIKE -without adding % wildcards (the search value can contain %) + $operator = ' LIKE '; # add spaces + } + } else { + die('db_where_clause: Invalid searchmode for ' . $field); + } + } + + if ($operator == "NULL") { + $querypart = $field . ' IS NULL'; + } elseif ($operator == "NOTNULL") { + $querypart = $field . ' IS NOT NULL'; + } else { + $querypart = $field . $operator . "'" . escape_string($value) . "'"; + + // might need other types adding here. + if (db_pgsql() && isset($struct[$field]) && in_array($struct[$field]['type'], array('ts', 'num')) && $value === '') { + $querypart = $field . $operator . " NULL"; + } + } + + if (!empty($struct[$field]['select'])) { + $having_parts[$field] = $querypart; + } else { + $where_parts[$field] = $querypart; + } + } + $query = ' WHERE 1=1 '; + $query .= " $additional_raw_where "; + if (count($where_parts) > 0) { + $query .= " AND ( " . join(" AND ", $where_parts) . " ) "; + } + if (count($having_parts) > 0) { + $query .= " HAVING ( " . join(" AND ", $having_parts) . " ) "; + } + + return $query; +} + +/** + * Convert a programmatic db table name into what may be the actual name. + * + * Takes into consideration any CONF database_prefix or database_tables map + * + * If it's a MySQL database, then we return the name with backticks around it (`). + * + * @param string database table name. + * @return string - database table name with appropriate prefix (and quoting if MySQL) + */ +function table_by_key($table_key) { + global $CONF; + + $table = $table_key; + + if (!empty($CONF['database_tables'][$table_key])) { + $table = $CONF['database_tables'][$table_key]; + } + + $table = $CONF['database_prefix'] . $table; + + if (db_mysql()) { + return "`" . $table . "`"; + } + + return $table; +} + + +/** + * check if the database layout is up to date + * returns the current 'version' value from the config table + * if $error_out is True (default), die() with a message that recommends to run setup.php. + * @param bool $error_out + * @return int + */ +function check_db_version($error_out = true) { + global $min_db_version; + + $table = table_by_key('config'); + + $sql = "SELECT value FROM $table WHERE name = 'version'"; + $row = db_query_one($sql); + if (isset($row['value'])) { + $dbversion = (int) $row['value']; + } else { + db_execute("INSERT INTO $table (name, value) VALUES ('version', '0')"); + $dbversion = 0; + } + + if (($dbversion < $min_db_version) && $error_out == true) { + echo "ERROR: The PostfixAdmin database layout is outdated (you have r$dbversion, but r$min_db_version is expected).\nPlease run setup.php to upgrade the database.\n"; + exit(1); + } + + return $dbversion; +} + + +/** + * + * Action: Return a string of colored  's that indicate + * the if an alias goto has an error or is sent to + * addresses list in show_custom_domains + * + * @param string $show_alias + * @return string + */ +function gen_show_status($show_alias) { + global $CONF; + $table_alias = table_by_key('alias'); + $stat_string = ""; + + $stat_goto = ""; + $stat_result = db_query_one("SELECT goto FROM $table_alias WHERE address=?", array($show_alias)); + + if ($stat_result) { + $stat_goto = $stat_result['goto']; + } + + $delimiter_regex = null; + + if (!empty($CONF['recipient_delimiter'])) { + $delimiter = preg_quote($CONF['recipient_delimiter'], "/"); + $delimiter_regex = '/' .$delimiter. '[^' .$delimiter. '@]*@/'; + } + + // UNDELIVERABLE CHECK + if ($CONF['show_undeliverable'] == 'YES') { + $gotos=array(); + $gotos=explode(',', $stat_goto); + $undel_string=""; + + //make sure this alias goes somewhere known + $stat_ok = 1; + foreach ($gotos as $g) { + if (!$stat_ok) { + break; + } + if (strpos($g, '@') === false) { + continue; + } + + list($local_part, $stat_domain) = explode('@', $g); + + $v = array(); + + $stat_delimiter = ""; + + $sql = "SELECT address FROM $table_alias WHERE address = ? OR address = ?"; + $v[] = $g; + $v[] = '@' . $stat_domain; + + if (!empty($CONF['recipient_delimiter']) && isset($delimiter_regex)) { + $v[] = preg_replace($delimiter_regex, "@", $g); + $sql .= " OR address = ? "; + } + + $stat_result = db_query_one($sql, $v); + + if (empty($stat_result)) { + $stat_ok = 0; + } + + if ($stat_ok == 0) { + if ($stat_domain == $CONF['vacation_domain'] || in_array($stat_domain, $CONF['show_undeliverable_exceptions'])) { + $stat_ok = 1; + } + } + } // while + if ($stat_ok == 0) { + $stat_string .= "" . $CONF['show_status_text'] . " "; + } else { + $stat_string .= $CONF['show_status_text'] . " "; + } + } + + // Vacation CHECK + if ( $CONF['show_vacation'] == 'YES' ) { + $stat_result = db_query_one("SELECT * FROM ". $CONF['database_tables']['vacation'] ." WHERE email = ? AND active = ? ", array($show_alias, db_get_boolean(true) )) ; + if (!empty($stat_result)) { + $stat_string .= "" . $CONF['show_status_text'] . " "; + } else { + $stat_string .= $CONF['show_status_text'] . " "; + } + } + + // Disabled CHECK + if ( $CONF['show_disabled'] == 'YES' ) { + $stat_result = db_query_one( + "SELECT * FROM ". $CONF['database_tables']['mailbox'] ." WHERE username = ? AND active = ?", + array($show_alias, db_get_boolean(false)) + ); + if (!empty($stat_result)) { + $stat_string .= "" . $CONF['show_status_text'] . " "; + } else { + $stat_string .= $CONF['show_status_text'] . " "; + } + } + + // Expired CHECK + if ( Config::bool('password_expiration') && Config::bool('show_expired') ) { + $now = 'now()'; + if (db_sqlite()) { + $now = "datetime('now')"; + } + + $stat_result = db_query_one("SELECT * FROM ". $CONF['database_tables']['mailbox'] ." WHERE username = ? AND password_expiry <= ? AND active = ?", array( $show_alias , $now , db_get_boolean(true) )); + + if (!empty($stat_result)) { + $stat_string .= "" . $CONF['show_status_text'] . " "; + } else { + $stat_string .= $CONF['show_status_text'] . " "; + } + } + + // POP/IMAP CHECK + if ($CONF['show_popimap'] == 'YES') { + $stat_delimiter = ""; + if (!empty($CONF['recipient_delimiter']) && isset($delimiter_regex)) { + $stat_delimiter = ',' . preg_replace($delimiter_regex, "@", $stat_goto); + } + + //if the address passed in appears in its own goto field, its POP/IMAP + # TODO: or not (might also be an alias loop) -> check mailbox table! + if (preg_match('/,' . $show_alias . ',/', ',' . $stat_goto . $stat_delimiter . ',')) { + $stat_string .= "" . $CONF['show_status_text'] . " "; + } else { + $stat_string .= $CONF['show_status_text'] . " "; + } + } + + // CUSTOM DESTINATION CHECK + if (count($CONF['show_custom_domains']) > 0) { + for ($i = 0; $i < sizeof($CONF['show_custom_domains']); $i++) { + if (preg_match('/^.*' . $CONF['show_custom_domains'][$i] . '.*$/', $stat_goto)) { + $stat_string .= "" . $CONF['show_status_text'] . " "; + } else { + $stat_string .= $CONF['show_status_text'] . " "; + } + } + } else { + $stat_string .= "; "; + } + + // $stat_string .= "    " . + // "    "; + return $stat_string; +} + +/** + * @return string + */ +function getRemoteAddr() { + $REMOTE_ADDR = 'localhost'; + if (isset($_SERVER['REMOTE_ADDR'])) { + $REMOTE_ADDR = $_SERVER['REMOTE_ADDR']; + } + + return $REMOTE_ADDR; +} + +/* vim: set expandtab softtabstop=4 tabstop=4 shiftwidth=4: */ -- GitLab From ebf8a4bd5f9fc002302a312f18a7fbf934ce3fbd Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Fri, 7 Jun 2019 11:51:28 +0200 Subject: [PATCH 08/24] use mysqli database type --- bin/functions.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/functions.inc.php b/bin/functions.inc.php index 8b8db8d..d9f7181 100644 --- a/bin/functions.inc.php +++ b/bin/functions.inc.php @@ -1527,7 +1527,7 @@ function db_connect_with_errors() { if ($socket) { $dsn = "mysql:unix_socket={$socket};dbname={$database_name};charset=UTF8"; } else { - $dsn = "mysql:host={$CONF['database_host']};dbname={$database_name};charset=UTF8"; + $dsn = "mysqli:host={$CONF['database_host']};dbname={$database_name};charset=UTF8"; } if (Config::bool('database_use_ssl')) { $options[PDO::MYSQL_ATTR_SSL_CA] = Config::read_string('database_ssl_ca'); -- GitLab From e97299f934ded0e63ebc29901ff7a7136f525604 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Fri, 7 Jun 2019 12:02:28 +0200 Subject: [PATCH 09/24] add correct database driver --- Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index ea809bd..ea47d5e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,11 +25,10 @@ RUN apk add -t build-dependencies \ php7-phar \ php7-fpm \ php7-imap \ - php7-pgsql \ - php7-mysqli \ php7-session \ php7-mbstring \ php7-pdo \ + php7-pdo_mysql \ && cd /tmp \ && PFA_TARBALL="postfixadmin-${VERSION}.tar.gz" \ # && wget -q https://downloads.sourceforge.net/project/postfixadmin/postfixadmin/postfixadmin-${VERSION}/${PFA_TARBALL} \ -- GitLab From d947b1bc6a9601399ee4dfb9d7ee875865c160e0 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Fri, 7 Jun 2019 12:06:55 +0200 Subject: [PATCH 10/24] Revert "use mysqli database type" This reverts commit 8a6d4f65f96a01a7fb641f0a70b43eb9d6be1062. --- bin/functions.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/functions.inc.php b/bin/functions.inc.php index d9f7181..8b8db8d 100644 --- a/bin/functions.inc.php +++ b/bin/functions.inc.php @@ -1527,7 +1527,7 @@ function db_connect_with_errors() { if ($socket) { $dsn = "mysql:unix_socket={$socket};dbname={$database_name};charset=UTF8"; } else { - $dsn = "mysqli:host={$CONF['database_host']};dbname={$database_name};charset=UTF8"; + $dsn = "mysql:host={$CONF['database_host']};dbname={$database_name};charset=UTF8"; } if (Config::bool('database_use_ssl')) { $options[PDO::MYSQL_ATTR_SSL_CA] = Config::read_string('database_ssl_ca'); -- GitLab From 0bcd0a189c7d4644d3a8e1cc6e6f646edd148b0b Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Fri, 7 Jun 2019 12:12:13 +0200 Subject: [PATCH 11/24] need db functions still? --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index ea47d5e..7816913 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,6 +27,7 @@ RUN apk add -t build-dependencies \ php7-imap \ php7-session \ php7-mbstring \ + php7-mysqli \ php7-pdo \ php7-pdo_mysql \ && cd /tmp \ -- GitLab From 2f4df512f1428d5f31725bc230cb76ac94da3aac Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Fri, 7 Jun 2019 15:04:10 +0200 Subject: [PATCH 12/24] set admin_smtp_password in config --- bin/run.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/run.sh b/bin/run.sh index c26f9eb..dbc967a 100755 --- a/bin/run.sh +++ b/bin/run.sh @@ -56,6 +56,7 @@ cat > /postfixadmin/config.local.php < Date: Fri, 7 Jun 2019 15:46:47 +0200 Subject: [PATCH 13/24] output smtp responses --- bin/functions.inc.php | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/functions.inc.php b/bin/functions.inc.php index 8b8db8d..301b274 100644 --- a/bin/functions.inc.php +++ b/bin/functions.inc.php @@ -1462,6 +1462,7 @@ function smtp_get_response($fh) { $line = fgets($fh, 256); $res .= $line; } while (preg_match("/^\d\d\d\-/", $line)); + echo $res; return $res; } -- GitLab From d0301acd51cd192146c007050b3dd30467fb963f Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Mon, 10 Jun 2019 11:24:46 +0200 Subject: [PATCH 14/24] send mail via tls --- bin/functions.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/functions.inc.php b/bin/functions.inc.php index 301b274..1984e26 100644 --- a/bin/functions.inc.php +++ b/bin/functions.inc.php @@ -1391,7 +1391,7 @@ function smtp_mail($to, $from, $data, $password = "", $body = "") { $maildata = $data; } - $fh = @fsockopen($smtpd_server, $smtpd_port, $errno, $errstr, $timeout); + $fh = @fsockopen("tls://".$smtpd_server, $smtpd_port, $errno, $errstr, $timeout); if (!$fh) { error_log("fsockopen failed - errno: $errno - errstr: $errstr"); -- GitLab From 90fdb3fa3d6d781da5bb811106d8ba3a196c232d Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Mon, 10 Jun 2019 12:04:01 +0200 Subject: [PATCH 15/24] install openssl --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 7816913..999fb0f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,6 +30,7 @@ RUN apk add -t build-dependencies \ php7-mysqli \ php7-pdo \ php7-pdo_mysql \ + openssl \ && cd /tmp \ && PFA_TARBALL="postfixadmin-${VERSION}.tar.gz" \ # && wget -q https://downloads.sourceforge.net/project/postfixadmin/postfixadmin/postfixadmin-${VERSION}/${PFA_TARBALL} \ -- GitLab From e7a4e9981383dae75448aee1c6e76eed9e05cdff Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Mon, 10 Jun 2019 12:09:56 +0200 Subject: [PATCH 16/24] add php7-openssl --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 999fb0f..b170402 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,6 +30,7 @@ RUN apk add -t build-dependencies \ php7-mysqli \ php7-pdo \ php7-pdo_mysql \ + php7-openssl \ openssl \ && cd /tmp \ && PFA_TARBALL="postfixadmin-${VERSION}.tar.gz" \ -- GitLab From f5310bdc4f3d9ef9e4c9d8ea7b986724044bfa78 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Mon, 10 Jun 2019 12:44:25 +0200 Subject: [PATCH 17/24] starttls --- bin/functions.inc.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bin/functions.inc.php b/bin/functions.inc.php index 1984e26..a0c841a 100644 --- a/bin/functions.inc.php +++ b/bin/functions.inc.php @@ -1401,6 +1401,14 @@ function smtp_mail($to, $from, $data, $password = "", $body = "") { fputs($fh, "EHLO $smtp_server\r\n"); smtp_get_response($fh); + fputs($fh, "STARTTLS\r\n"); + smtp_get_response($fh); + + if(false == stream_socket_enable_crypto($fh, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)){ + // fclose($smtp); // unsure if you need to close as I haven't run into a security fail at this point + die("unable to start tls encryption"); + } + if (!empty($password)) { fputs($fh,"AUTH LOGIN\r\n"); smtp_get_response($fh); -- GitLab From 9cd161ec52f5ddb5d1b3b90a244d674a44495d65 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Tue, 18 Jun 2019 15:33:10 +0200 Subject: [PATCH 18/24] try fsockopen without tls:// --- bin/functions.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/functions.inc.php b/bin/functions.inc.php index a0c841a..910071a 100644 --- a/bin/functions.inc.php +++ b/bin/functions.inc.php @@ -1391,7 +1391,7 @@ function smtp_mail($to, $from, $data, $password = "", $body = "") { $maildata = $data; } - $fh = @fsockopen("tls://".$smtpd_server, $smtpd_port, $errno, $errstr, $timeout); + $fh = @fsockopen($smtpd_server, $smtpd_port, $errno, $errstr, $timeout); if (!$fh) { error_log("fsockopen failed - errno: $errno - errstr: $errstr"); -- GitLab From 85fd15380152577adc6e7c2fd19872b717174bb1 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Tue, 2 Jul 2019 13:40:22 +0200 Subject: [PATCH 19/24] Move ssl changes to own branch --- Dockerfile | 5 +- bin/functions.inc.php | 2222 ----------------------------------------- 2 files changed, 1 insertion(+), 2226 deletions(-) delete mode 100644 bin/functions.inc.php diff --git a/Dockerfile b/Dockerfile index b170402..cbee6c0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ ARG GPG_FINGERPRINT="70CA A060 DE04 2AAE B1B1 5196 C6A6 82EA 63C8 2F1C" ARG SHA256_HASH="866d4c0ca870b2cac184e5837a4d201af8fcefecef09bc2c887a6e017a00cefe" # TODO: might want to use a commit/release instead -ARG BRANCH="admin-smtp-password" +ARG BRANCH="ssl-sendmail" RUN apk add -t build-dependencies \ ca-certificates \ @@ -58,8 +58,5 @@ COPY bin/run.sh /usr/local/bin RUN apk add --no-cache bash openssh \ && adduser -D -s /bin/bash -D pfexec -# debug only -COPY bin/functions.inc.php /postfixadmin/functions.inc.php - EXPOSE 8888 CMD ["tini", "--", "run.sh"] diff --git a/bin/functions.inc.php b/bin/functions.inc.php deleted file mode 100644 index 910071a..0000000 --- a/bin/functions.inc.php +++ /dev/null @@ -1,2222 +0,0 @@ -'; - - foreach ($supported_languages as $lang => $lang_name) { - if ($lang == $current_lang) { - $selected = ' selected="selected"'; - } else { - $selected = ''; - } - $selector .= ""; - } - $selector .= ""; - return $selector; -} - - - - -/** - * Checks if a domain is valid - * @param string $domain - * @return string empty if the domain is valid, otherwise string with the errormessage - * - * @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) { - 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)))) { - - // Look for an AAAA, A, or MX record for the domain - - if (function_exists('checkdnsrr')) { - $start = microtime(true); # check for slow nameservers, part 1 - - // AAAA (IPv6) is only available in PHP v. >= 5 - if (version_compare(phpversion(), "5.0.0", ">=") && checkdnsrr($domain, 'AAAA')) { - $retval = ''; - } elseif (checkdnsrr($domain, 'A')) { - $retval = ''; - } elseif (checkdnsrr($domain, 'MX')) { - $retval = ''; - } else { - $retval = sprintf(Config::lang('pInvalidDomainDNS'), htmlentities($domain)); - } - - $end = microtime(true); # check for slow nameservers, part 2 - $time_needed = $end - $start; - if ($time_needed > 2) { - error_log("Warning: slow nameserver - lookup for $domain took $time_needed seconds"); - } - - return $retval; - } else { - return 'emailcheck_resolve_domain is enabled, but function (checkdnsrr) missing!'; - } - } - - return ''; -} - -/** - * Get password expiration value for a 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) { - $table_domain = table_by_key('domain'); - $query = "SELECT password_expiry FROM $table_domain WHERE domain= :domain"; - - $result = db_query_one($query, array('domain' => $domain)); - if (is_array($result) && isset($result['password_expiry'])) { - return $result['password_expiry']; - } - return 0; -} - -/** - * 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 - */ -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 - if (Config::bool('vacation')) { - $vacation_domain = Config::read_string('vacation_domain'); - $ce_email = preg_replace("/@$vacation_domain\$/", '', $ce_email); - $ce_email = preg_replace("/#/", '@', $ce_email); - } - - // Perform non-domain-part sanity checks - if (!preg_match('/^[-!#$%&\'*+\\.\/0-9=?A-Z^_{|}~]+' . '@' . '[^@]+$/i', $ce_email)) { - return "" . Config::lang_f('pInvalidMailRegex', $email); - } - - if (function_exists('filter_var')) { - $check = filter_var($email, FILTER_VALIDATE_EMAIL); - if (!$check) { - return "" . Config::lang_f('pInvalidMailRegex', $email); - } - } - // Determine domain name - $matches = array(); - if (preg_match('|@(.+)$|', $ce_email, $matches)) { - $domain=$matches[1]; - # check domain name - return "" . check_domain($domain); - } - - return "" . Config::lang_f('pInvalidMailRegex', $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 - * and MAY NOT be just \' ing. (e.g. sqlite and PgSQL change "it's" to "it''s". - * - * The PDO quote function surrounds what you pass in with quote marks; for legacy reasons we remove these, - * but assume the caller will actually add them back in (!). - * - * e.g. caller code looks like : - * - * - * $sql = "SELECT * FROM foo WHERE x = '" . escape_string('fish') . "'"; - * - * - * @param int|string $string parameters to escape - * @return string cleaned data, suitable for use within an SQL statement. - */ -function escape_string($string_or_int) { - $link = db_connect(); - $string_or_int = (string) $string_or_int; - $quoted = $link->quote($string_or_int); - return trim($quoted, "'"); -} - - -/** - * safeget - * Action: get value from $_GET[$param], or $default if $_GET[$param] is not set - * Call: $param = safeget('param') # replaces $param = $_GET['param'] - * - or - - * $param = safeget('param', 'default') - * - * @param string $param parameter name. - * @param string $default (optional) - default value if key is not set. - * @return string - */ -function safeget($param, $default = "") { - $retval = $default; - if (isset($_GET[$param])) { - $retval = $_GET[$param]; - } - return $retval; -} - -/** - * 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 - */ -function safepost($param, $default = "") { - $retval = $default; - if (isset($_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 - */ -function safecookie($param, $default = "") { - $retval = $default; - if (isset($_COOKIE[$param])) { - $retval = $_COOKIE[$param]; - } - return $retval; -} - -/** - * safesession - * @see safeget() - * @param string $param - * @param string $default (optional) - * @return string value from $_SESSION[$param] or $default - */ -function safesession($param, $default = "") { - $retval = $default; - if (isset($_SESSION[$param])) { - $retval = $_SESSION[$param]; - } - return $retval; -} - - -/** - * pacol - * @param int $allow_editing - * @param int $display_in_form - * @param int display_in_list - * @param string $type - * @param string PALANG_label - * @param string PALANG_desc - * @param any optional $default - * @param array $options optional options - * @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="") { - if ($PALANG_label != '') { - $PALANG_label = Config::lang($PALANG_label); - } - if ($PALANG_desc != '') { - $PALANG_desc = Config::lang($PALANG_desc); - } - - if (is_array($multiopt)) { # remaining parameters provided in named array - $not_in_db = 0; # keep default value - foreach ($multiopt as $key => $value) { - $$key = $value; # extract everything to the matching variable - } - } else { - $not_in_db = $multiopt; - } - - 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 - ); -} - -/** - * Action: Get all the properties of a domain. - * @param string $domain - * @return array - */ -function get_domain_properties($domain) { - $handler = new DomainHandler(); - if (!$handler->init($domain)) { - die("Error: " . join("\n", $handler->errormsg)); - } - - if (!$handler->view()) { - die("Error: " . join("\n", $handler->errormsg)); - } - - $result = $handler->result(); - return $result; -} - - -/** - * create_page_browser - * Action: Get page browser for a long list of mailboxes, aliases etc. - * Call: $pagebrowser = create_page_browser('table.field', 'query', 50) # replaces $param = $_GET['param'] - * - * @param string $idxfield - database field name to use as title - * @param string $querypart - core part of the query (starting at "FROM") - * @return array - */ -function create_page_browser($idxfield, $querypart, $sql_params = []) { - global $CONF; - $page_size = (int) $CONF['page_size']; - $label_len = 2; - $pagebrowser = array(); - - $count_results = 0; - - if ($page_size < 2) { # will break the page browser - die('$CONF[\'page_size\'] must be 2 or more!'); - } - - # get number of rows - $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 - } - - if ($count_results < $page_size) { - return array(); # only one page - no pagebrowser required - } - - # init row counter - $initcount = "SET @r=-1"; - if (db_pgsql()) { - $initcount = "CREATE TEMPORARY SEQUENCE rowcount MINVALUE 0"; - } - if (!db_sqlite()) { - db_execute($initcount); - } - - # get labels for relevant rows (first and last of each page) - $page_size_zerobase = $page_size - 1; - $query = " - SELECT * FROM ( - SELECT $idxfield AS label, @r := @r + 1 AS 'r' $querypart - ) idx WHERE MOD(idx.r, $page_size) IN (0,$page_size_zerobase) OR idx.r = $count_results - "; - - if (db_pgsql()) { - $query = " - SELECT * FROM ( - SELECT $idxfield AS label, nextval('rowcount') AS row $querypart - ) idx WHERE MOD(idx.row, $page_size) IN (0,$page_size_zerobase) OR idx.row = $count_results - "; - } - - if (db_sqlite()) { - $query = " - WITH idx AS (SELECT * $querypart) - SELECT $idxfield AS label, (SELECT (COUNT(*) - 1) FROM idx t1 WHERE t1.$idxfield <= t2.$idxfield) AS row - FROM idx t2 - WHERE (row % $page_size) IN (0,$page_size_zerobase) OR row = $count_results"; - } - - # PostgreSQL: - # http://www.postgresql.org/docs/8.1/static/sql-createsequence.html - # http://www.postgresonline.com/journal/archives/79-Simulating-Row-Number-in-PostgreSQL-Pre-8.4.html - # http://www.pg-forum.de/sql/1518-nummerierung-der-abfrageergebnisse.html - # CREATE TEMPORARY SEQUENCE foo MINVALUE 0 MAXVALUE $page_size_zerobase CYCLE - # afterwards: DROP SEQUENCE foo - - $result = db_query_all($query, $sql_params); - foreach ($result as $k => $row) { - if (isset($result[$k + 1])) { - $row2 = $result[$k + 1]; - $label = substr($row['label'], 0, $label_len) . '-' . substr($row2['label'], 0, $label_len); - } else { - $label = substr($row['label'], 0, $label_len); - } - $pagebrowser[] = $label; - } - - if (db_pgsql()) { - db_execute("DROP SEQUENCE rowcount"); - } - - return $pagebrowser; -} - - -/** - * Recalculates the quota from MBs to bytes (divide, /) - * @param int $quota - * @return float - */ -function divide_quota($quota) { - if ($quota == -1) { - return $quota; - } - $value = round($quota / (int) Config::read_string('quota_multiplier'), 2); - return $value; -} - - -/** - * Checks if the admin is the owner of the domain (or global-admin) - * @param string $username - * @param string $domain - * @return bool - */ -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 = ?" , - array($username, $domain, db_get_boolean(true)) - ); - - if (sizeof($result) == 1 || sizeof($result) == 2) { # "ALL" + specific domain permissions is possible - # TODO: if superadmin, check if given domain exists in the database - return true; - } 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."); - } - return false; - } -} - - - -/** - * List domains for an admin user. - * @param String $username - * @return array of domain names. - */ -function list_domains_for_admin($username) { - $table_domain = table_by_key('domain'); - $table_domain_admins = table_by_key('domain_admins'); - - $condition = array(); - - $E_username = escape_string($username); - - $query = "SELECT $table_domain.domain FROM $table_domain "; - $condition[] = "$table_domain.domain != 'ALL'"; - - $pvalues = array(); - - $result = db_query_one("SELECT username FROM $table_domain_admins WHERE username= :username AND domain='ALL'", array('username' => $username)); - if (empty($result)) { # not a superadmin - $pvalues['username'] = $username; - $pvalues['active'] = db_get_boolean(true); - $pvalues['backupmx'] = db_get_boolean(false); - - $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? - } - - $query .= " WHERE " . join(' AND ', $condition); - $query .= " ORDER BY $table_domain.domain"; - - $result = db_query_all($query, $pvalues); - - return array_column($result, 'domain'); -} - - -if (!function_exists('array_column')) { - require_once(dirname(__FILE__) . '/lib/array_column.php'); -} - -/** - * List all available domains. - * - * @return array - */ -function list_domains() { - $list = array(); - - $table_domain = table_by_key('domain'); - $result = db_query_all("SELECT domain FROM $table_domain WHERE domain!='ALL' ORDER BY domain"); - $i = 0; - foreach ($result as $row) { - $list[$i] = $row['domain']; - $i++; - } - return $list; -} - - - - -// -// list_admins -// Action: Lists all the admins -// Call: list_admins () -// -// was admin_list_admins -// -function list_admins() { - $handler = new AdminHandler(); - - $handler->getList(''); - - return $handler->result(); -} - - - -// -// 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") { - if (strtolower($default_charset) == 'iso-8859-1') { - $string = str_replace("\240", ' ', $string); - } - - $j = strlen($string); - $max_l = 75 - strlen($default_charset) - 7; - $aRet = array(); - $ret = ''; - $iEncStart = $enc_init = false; - $cur_l = $iOffset = 0; - - for ($i = 0; $i < $j; ++$i) { - switch ($string{$i}) { - case '=': - case '<': - case '>': - case ',': - case '?': - case '_': - if ($iEncStart === false) { - $iEncStart = $i; - } - $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; - $ret = ''; - $iEncStart = false; - } else { - $ret .= sprintf("=%02X", ord($string{$i})); - } - break; - case '(': - case ')': - if ($iEncStart !== false) { - $aRet[] = substr($string, $iOffset, $iEncStart-$iOffset); - $aRet[] = "=?$default_charset?Q?$ret?="; - $iOffset = $i; - $cur_l = 0; - $ret = ''; - $iEncStart = false; - } - break; - case ' ': - if ($iEncStart !== false) { - $cur_l++; - if ($cur_l > $max_l) { - $aRet[] = substr($string, $iOffset, $iEncStart-$iOffset); - $aRet[] = "=?$default_charset?Q?$ret?="; - $iOffset = $i; - $cur_l = 0; - $ret = ''; - $iEncStart = false; - } else { - $ret .= '_'; - } - } - break; - default: - $k = ord($string{$i}); - if ($k > 126) { - if ($iEncStart === false) { - // do not start encoding in the middle of a string, also take the rest of the word. - $sLeadString = substr($string, 0, $i); - $aLeadString = explode(' ', $sLeadString); - $sToBeEncoded = array_pop($aLeadString); - $iEncStart = $i - strlen($sToBeEncoded); - $ret .= $sToBeEncoded; - $cur_l += strlen($sToBeEncoded); - } - $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); - $aRet[] = "=?$default_charset?Q?$ret?= "; - $cur_l = 3; - $ret = ''; - $iOffset = $i; - $iEncStart = $i; - } - $enc_init = true; - $ret .= sprintf("=%02X", $k); - } else { - if ($iEncStart !== false) { - $cur_l++; - if ($cur_l > $max_l) { - $aRet[] = substr($string, $iOffset, $iEncStart-$iOffset); - $aRet[] = "=?$default_charset?Q?$ret?="; - $iEncStart = false; - $iOffset = $i; - $cur_l = 0; - $ret = ''; - } else { - $ret .= $string{$i}; - } - } - } - break; - # end switch - } - } - if ($enc_init) { - if ($iEncStart !== false) { - $aRet[] = substr($string, $iOffset, $iEncStart-$iOffset); - $aRet[] = "=?$default_charset?Q?$ret?="; - } else { - $aRet[] = substr($string, $iOffset); - } - $string = implode('', $aRet); - } - return $string; -} - - -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) { - - // define possible characters - $possible = "2345678923456789abcdefghijkmnpqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ"; # skip 0 and 1 to avoid confusion with O and l - - // add random characters to $password until $length is reached - $password = ""; - while (strlen($password) < $length) { - $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 - if (!strstr($password, $char)) { - $password .= $char; - } - } - - return $password; -} - - - -/** - * 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) { - $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) { - $val_conf['/.{' . $minlen . '}/'] = "password_too_short $minlen"; - } - - foreach ($val_conf as $regex => $message) { - if (!preg_match($regex, $password)) { - $msgparts = preg_split("/ /", $message, 2); - if (count($msgparts) == 1) { - $result[] = Config::lang($msgparts[0]); - } else { - $result[] = sprintf(Config::lang($msgparts[0]), $msgparts[1]); - } - } - } - - return $result; -} - -/** - * @param string $pw - * @param string $pw_db - encrypted hash - * @return string crypt'ed password, should equal $pw_db if $pw matches the original - */ -function _pacrypt_md5crypt($pw, $pw_db = '') { - if ($pw_db) { - $split_salt = preg_split('/\$/', $pw_db); - if (isset($split_salt[2])) { - $salt = $split_salt[2]; - return md5crypt($pw, $salt); - } - } - - return md5crypt($pw); -} - -function _pacrypt_crypt($pw, $pw_db = '') { - if ($pw_db) { - return crypt($pw, $pw_db); - } - return crypt($pw); -} - -/** - * Crypt with MySQL's ENCRYPT function - * - * @param string $pw - * @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 = '') { - // 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 ) { - $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]); - } - - return $res['result']; -} - -/** - * Create/Validate courier authlib style crypt'ed passwords. (md5, md5raw, crypt, sha1) - * - * @param string $pw - * @param string $pw_db (optional) - * @return string crypted password - contains {xxx} prefix to identify mechanism. - */ -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 - if (preg_match('/^{.*}/', $pw_db)) { - // we have a flavor in the db -> use it instead of default flavor - $result = preg_split('/[{}]/', $pw_db, 3); # split at { and/or } - $flavor = $result[1]; - $salt = substr($result[2], 0, 2); - } - - if (stripos($flavor, 'md5raw') === 0) { - $password = '{' . $flavor . '}' . md5($pw); - } elseif (stripos($flavor, 'md5') === 0) { - $password = '{' . $flavor . '}' . base64_encode(md5($pw, true)); - } elseif (stripos($flavor, 'crypt') === 0) { - $password = '{' . $flavor . '}' . crypt($pw, $salt); - } elseif (stripos($flavor, 'SHA') === 0) { - $password = '{' . $flavor . '}' . base64_encode(sha1($pw, true)); - } else { - die("authlib_default_flavor '" . $flavor . "' unknown. Valid flavors are 'md5raw', 'md5', 'SHA' and 'crypt'"); - } - return $password; -} - -/** - * Uses the doveadm pw command, crypted passwords have a {...} prefix to identify type. - * - * @param string $pw - plain text password - * @param string $pw_db - encrypted password, or '' for generation. - * @return string crypted password - */ -function _pacrypt_dovecot($pw, $pw_db = '') { - global $CONF; - - $split_method = preg_split('/:/', $CONF['encrypt']); - $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)) { - die("invalid dovecot encryption method"); - } - - # TODO: check against a fixed list? - # if (strtolower($method) == 'md5-crypt') die("\$CONF['encrypt'] = 'dovecot:md5-crypt' will not work because dovecotpw generates a random salt each time. Please use \$CONF['encrypt'] = 'md5crypt' instead."); - # $crypt_method = preg_match ("/.*-CRYPT$/", $method); - - # digest-md5 hashes include the username - until someone implements it, let's declare it as unsupported - if (strtolower($method) == 'digest-md5') { - die("Sorry, \$CONF['encrypt'] = 'dovecot:digest-md5' is not supported by PostfixAdmin."); - } - # TODO: add -u option for those hashes, or for everything that is salted (-u was available before dovecot 2.1 -> no problem with backward compatibility ) - - $dovecotpw = "doveadm pw"; - if (!empty($CONF['dovecotpw'])) { - $dovecotpw = $CONF['dovecotpw']; - } - - # Use proc_open call to avoid safe_mode problems and to prevent showing plain password in process table - $spec = array( - 0 => array("pipe", "r"), // stdin - 1 => array("pipe", "w"), // stdout - 2 => array("pipe", "w"), // stderr - ); - - $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)); - - $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); - } - $pipe = proc_open("$dovecotpw '-s' $method$dovepasstest", $spec, $pipes); - - if (!$pipe) { - die("can't proc_open $dovecotpw"); - } - - // 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)); - usleep(1000); - } - fwrite($pipes[0], $pw . "\n", 1+strlen($pw)); - fclose($pipes[0]); - - // Read hash from pipe stdout - $password = fread($pipes[1], 200); - - 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); - die("can't encrypt password with dovecotpw, see error log for details"); - } - } else { - if (!preg_match('(verified)', $password)) { - $password="Thepasswordcannotbeverified"; - } else { - $password = rtrim(str_replace('(verified)', '', $password)); - } - } - - fclose($pipes[1]); - fclose($pipes[2]); - proc_close($pipe); - - if ((!empty($pw_db)) && (substr($pw_db, 0, 1) != '{')) { - # for backward compability with "old" dovecot passwords that don't have the {method} prefix - $password = str_replace('{' . $method . '}', '', $password); - } - - return rtrim($password); -} - -/** - * Supports DES, MD5, BLOWFISH, SHA256, SHA512 methods. - * - * @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; - - // use PHPs crypt(), which uses the system's crypt() - // same algorithms as used in /etc/shadow - // you can have mixed hash types in the database for authentication, changed passwords get specified hash type - // 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 - // tested on linux - - if (strlen($pw_db) > 0) { - // existing pw provided. send entire password hash as salt for crypt() to figure out - $salt = $pw_db; - } 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) { - // use specified hash method - $split_method = explode(':', $CONF['encrypt']); - $salt_method = $split_method[1]; - if (count($split_method) >= 3) { - $hash_difficulty = $split_method[2]; - } - } - // 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; -} - -/** - * @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) { - // 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 '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) { - die('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) { - die('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); - - case 'SHA512': - $length = 16; - $algorithm = '6'; - if (empty($hash_difficulty)) { - $rounds = ''; - } else { - $rounds = (int)$hash_difficulty; - if ($rounds < 1000 || $rounds > 999999999) { - die('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: - die("unknown hash type: '$hash_type'"); - } -} - -/** - * Generates a random string of specified $length from $characters. - * @param string $characters - * @param int $length - * @return string of given $length - */ -function _php_crypt_random_string($characters, $length) { - $string = ''; - for ($p = 0; $p < $length; $p++) { - $string .= $characters[random_int(0, strlen($characters) -1)]; - } - return $string; -} - - -/** - * Encrypt a password, using the apparopriate hashing mechanism as defined in - * config.inc.php ($CONF['encrypt']). - * - * When wanting to compare one pw to another, it's necessary to provide the salt used - hence - * the second parameter ($pw_db), which is the existing hash from the DB. - * - * @param string $pw - * @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="") { - 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); - } - - if (preg_match("/^dovecot:/", $CONF['encrypt'])) { - return _pacrypt_dovecot($pw, $pw_db); - } - - if (substr($CONF['encrypt'], 0, 9) === 'php_crypt') { - return _pacrypt_php_crypt($pw, $pw_db); - } - - die('unknown/invalid $CONF["encrypt"] setting: ' . $CONF['encrypt']); -} - -/** - * Creates MD5 based crypt formatted password. - * If salt is not provided we generate one. - * - * @param string $pw plain text password - * @param string $salt (optional) - * @param string $magic (optional) - * @return string hashed password in crypt format. - */ -function md5crypt($pw, $salt="", $magic="") { - $MAGIC = "$1$"; - - if ($magic == "") { - $magic = $MAGIC; - } - if ($salt == "") { - $salt = create_salt(); - } - $slist = explode("$", $salt); - if ($slist[0] == "1") { - $salt = $slist[1]; - } - - $salt = substr($salt, 0, 8); - $ctx = $pw . $magic . $salt; - $final = hex2bin(md5($pw . $salt . $pw)); - - for ($i=strlen($pw); $i>0; $i-=16) { - if ($i > 16) { - $ctx .= substr($final, 0, 16); - } else { - $ctx .= substr($final, 0, $i); - } - } - $i = strlen($pw); - - while ($i > 0) { - if ($i & 1) { - $ctx .= chr(0); - } else { - $ctx .= $pw[0]; - } - $i = $i >> 1; - } - $final = hex2bin(md5($ctx)); - - for ($i=0;$i<1000;$i++) { - $ctx1 = ""; - if ($i & 1) { - $ctx1 .= $pw; - } else { - $ctx1 .= substr($final, 0, 16); - } - if ($i % 3) { - $ctx1 .= $salt; - } - if ($i % 7) { - $ctx1 .= $pw; - } - if ($i & 1) { - $ctx1 .= substr($final, 0, 16); - } else { - $ctx1 .= $pw; - } - $final = hex2bin(md5($ctx1)); - } - $passwd = ""; - $passwd .= to64(((ord($final[0]) << 16) | (ord($final[6]) << 8) | (ord($final[12]))), 4); - $passwd .= to64(((ord($final[1]) << 16) | (ord($final[7]) << 8) | (ord($final[13]))), 4); - $passwd .= to64(((ord($final[2]) << 16) | (ord($final[8]) << 8) | (ord($final[14]))), 4); - $passwd .= to64(((ord($final[3]) << 16) | (ord($final[9]) << 8) | (ord($final[15]))), 4); - $passwd .= to64(((ord($final[4]) << 16) | (ord($final[10]) << 8) | (ord($final[5]))), 4); - $passwd .= to64(ord($final[11]), 2); - return "$magic$salt\$$passwd"; -} - -/** - * @return string - should be random, 8 chars long - */ -function create_salt() { - srand((int) microtime()*1000000); - $salt = substr(md5("" . rand(0, 9999999)), 0, 8); - return $salt; -} - -/* - * remove item $item from array $array - */ -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); - if ($ret === false) { - $found = 0; - } else { - $found = 1; - unset($array[$ret]); - } - return array($found, $array); -} - -function to64($v, $n) { - $ITOA64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - $ret = ""; - while (($n - 1) >= 0) { - $n--; - $ret .= $ITOA64[$v & 0x3f]; - $v = $v >> 6; - } - return $ret; -} - - - -/** - * 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 - * @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 = "") { - global $CONF; - $smtpd_server = $CONF['smtp_server']; - $smtpd_port = $CONF['smtp_port']; - //$smtp_server = $_SERVER["SERVER_NAME"]; - $smtp_server = php_uname('n'); - if (!empty($CONF['smtp_client'])) { - $smtp_server = $CONF['smtp_client']; - } - $errno = 0; - $errstr = "0"; - $timeout = 30; - - if ($body != "") { - $maildata = - "To: " . $to . "\n" - . "From: " . $from . "\n" - . "Subject: " . encode_header($data) . "\n" - . "MIME-Version: 1.0\n" - . "Date: " . date('r') . "\n" - . "Content-Type: text/plain; charset=utf-8\n" - . "Content-Transfer-Encoding: 8bit\n" - . "\n" - . $body - ; - } else { - $maildata = $data; - } - - $fh = @fsockopen($smtpd_server, $smtpd_port, $errno, $errstr, $timeout); - - if (!$fh) { - error_log("fsockopen failed - errno: $errno - errstr: $errstr"); - return false; - } else { - smtp_get_response($fh); - fputs($fh, "EHLO $smtp_server\r\n"); - smtp_get_response($fh); - - fputs($fh, "STARTTLS\r\n"); - smtp_get_response($fh); - - if(false == stream_socket_enable_crypto($fh, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)){ - // fclose($smtp); // unsure if you need to close as I haven't run into a security fail at this point - die("unable to start tls encryption"); - } - - if (!empty($password)) { - fputs($fh,"AUTH LOGIN\r\n"); - smtp_get_response($fh); - fputs($fh, base64_encode($from) . "\r\n"); - smtp_get_response($fh); - fputs($fh, base64_encode($password) . "\r\n"); - smtp_get_response($fh); - } - - fputs($fh, "MAIL FROM:<$from>\r\n"); - smtp_get_response($fh); - fputs($fh, "RCPT TO:<$to>\r\n"); - smtp_get_response($fh); - fputs($fh, "DATA\r\n"); - smtp_get_response($fh); - fputs($fh, "$maildata\r\n.\r\n"); - smtp_get_response($fh); - fputs($fh, "QUIT\r\n"); - smtp_get_response($fh); - fclose($fh); - } - return true; -} - -/** - * smtp_get_admin_email - * Action: Get configured email address or current user if nothing configured - * Call: smtp_get_admin_email - * @return string - username/mail address - */ -function smtp_get_admin_email() { - $admin_email = Config::read_string('admin_email'); - if (!empty($admin_email)) { - return $admin_email; - } else { - return authentication_get_username(); - } -} - -/** - * smtp_get_admin_password - * Action: Get smtp password for admin email - * Call: smtp_get_admin_password - * @return string - admin smtp password - */ -function smtp_get_admin_password() { - return Config::read_string('admin_smtp_password'); -} - - -// -// smtp_get_response -// Action: Get response from mail server -// Call: smtp_get_response (string FileHandle) -// -function smtp_get_response($fh) { - $res =''; - do { - $line = fgets($fh, 256); - $res .= $line; - } while (preg_match("/^\d\d\d\-/", $line)); - echo $res; - return $res; -} - - - -$DEBUG_TEXT = <<Please check the documentation and website for more information.

- -EOF; - -/** - * db_connect - * Action: Makes a connection to the database if it doesn't exist - * Call: db_connect () - * - * Return value: - * - * @return \PDO - */ -function db_connect() { - list($link, $_) = db_connect_with_errors(); - unset($_); - - if (!$link instanceof PDO) { - throw new Exception("Database connection failed"); - } - - return $link; -} - -/** - * @param bool $ignore_errors - * @return array [PDO link | false, string $error_text]; - */ -function db_connect_with_errors() { - global $CONF; - global $DEBUG_TEXT; - - $error_text = ''; - - static $link; - if (isset($link) && $link) { - return array($link, $error_text); - } - $link = false; - - $options = array( - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - ); - $username_password = true; - - $queries = array(); - - $dsn = null; - - if (db_mysql()) { - $socket = Config::read_string('database_socket'); - $database_name = Config::read_string('database_name'); - - if ($socket) { - $dsn = "mysql:unix_socket={$socket};dbname={$database_name};charset=UTF8"; - } else { - $dsn = "mysql:host={$CONF['database_host']};dbname={$database_name};charset=UTF8"; - } - if (Config::bool('database_use_ssl')) { - $options[PDO::MYSQL_ATTR_SSL_CA] = Config::read_string('database_ssl_ca'); - $options[PDO::MYSQL_ATTR_SSL_CAPATH] = Config::read_string('database_ssl_ca_path'); - $options[PDO::MYSQL_ATTR_SSL_CERT] = Config::read_string('database_ssl_cert'); - $options[PDO::MYSQL_ATTR_SSL_CIPHER] = Config::read_string('database_ssl_cipher'); - } - $queries[] = 'SET CHARACTER SET utf8'; - $queries[] = "SET COLLATION_CONNECTION='utf8_general_ci'"; - } elseif (db_sqlite()) { - $db = $CONF['database_name']; - - if (!file_exists($db)) { - $error_text = 'SQLite database missing: '. $db; - return array($link, $error_text); - } - - if (!is_writeable($db)) { - $error_text = 'SQLite database not writeable: '. $db; - return array($link, $error_text); - } - - if (!is_writeable(dirname($db))) { - $error_text = 'The directory the SQLite database is in is not writeable: '. dirname($db); - return array($link, $error_text); - } - - $dsn = "sqlite:{$db}"; - $username_password = false; - } elseif (db_pgsql()) { - if (!isset($CONF['database_port'])) { - $CONF['database_port'] = '5432'; - } - $dsn = "pgsql:host={$CONF['database_host']};port={$CONF['database_port']};dbname={$CONF['database_name']};options='-c client_encoding=utf8'"; - } else { - die("

FATAL Error:
Invalid \$CONF['database_type']! Please fix your config.inc.php!

"); - } - - if ($username_password) { - $link = new PDO($dsn, Config::read_string('database_user'), Config::read_string('database_password'), $options); - } else { - $link = new PDO($dsn, null, null, $options); - } - - if (!empty($queries)) { - foreach ($queries as $q) { - $link->exec($q); - } - } - - return array($link, $error_text); -} - -/** - * Returns the appropriate boolean value for the database. - * - * @param bool $bool - * @return string|int as appropriate for underlying db platform - */ -function db_get_boolean($bool) { - if (! (is_bool($bool) || $bool == '0' || $bool == '1')) { - error_log("Invalid usage of 'db_get_boolean($bool)'"); - die("Invalid usage of 'db_get_boolean($bool)'"); - } - - if (db_pgsql()) { - // return either true or false (unquoted strings) - if ($bool) { - return 't'; - } - return 'f'; - } elseif (db_mysql() || db_sqlite()) { - if ($bool) { - return 1; - } - return 0; - } else { - die('Unknown value in $CONF[database_type]'); - } -} - -/** - * Returns a query that reports the used quota ("x / y") - * @param string column containing used quota - * @param string column containing allowed quota - * @param string column that will contain "x / y" - * @return string - */ -function db_quota_text($count, $quota, $fieldname) { - if (db_pgsql() || db_sqlite()) { - // SQLite and PostgreSQL use || to concatenate strings - return " CASE $quota - WHEN '-1' THEN (coalesce($count,0) || ' / -') - WHEN '0' THEN (coalesce($count,0) || ' / " . escape_string(html_entity_decode('∞')) . "') - ELSE (coalesce($count,0) || ' / ' || $quota) - END AS $fieldname"; - } else { - return " CASE $quota - WHEN '-1' THEN CONCAT(coalesce($count,0), ' / -') - WHEN '0' THEN CONCAT(coalesce($count,0), ' / ', '" . escape_string(html_entity_decode('∞')) . "') - ELSE CONCAT(coalesce($count,0), ' / ', $quota) - END AS $fieldname"; - } -} - -/** - * Returns a query that reports the used quota ("x / y") - * @param string column containing used quota - * @param string column containing allowed quota - * @param string column that will contain "x / y" - * @return string - */ -function db_quota_percent($count, $quota, $fieldname) { - return " CASE $quota - WHEN '-1' THEN -1 - WHEN '0' THEN -1 - ELSE round(100 * coalesce($count,0) / $quota) - END AS $fieldname"; -} - -/** - * @return boolean true if it's a MySQL database variant. - */ -function db_mysql() { - $type = Config::Read('database_type'); - - if ($type == 'mysql' || $type == 'mysqli') { - return true; - } - return false; -} - -/** - * @return bool true if PostgreSQL is used, false otherwise - */ -function db_pgsql() { - return Config::read_string('database_type') == 'pgsql'; -} - -/** - * returns true if SQLite is used, false otherwise - */ -function db_sqlite() { - if (Config::Read('database_type')=='sqlite') { - return true; - } else { - return false; - } -} - -/** - * @param string $sql - * @param array $values - * @return array - */ -function db_query_all($sql, array $values = []) { - $r = db_query($sql, $values); - return $r['result']->fetchAll(PDO::FETCH_ASSOC); -} - -/** - * @param string $sql - * @param array $values - * @return array - */ -function db_query_one($sql, array $values = []) { - $r = db_query($sql, $values); - return $r['result']->fetch(PDO::FETCH_ASSOC); -} - - -/** - * @param string $sql e.g. UPDATE foo SET bar = :baz - * @param array $values - parameters for the prepared statement e.g. ['baz' => 1234] - * @param bool $throw_exceptions - * @return int number of rows affected by the query - */ -function db_execute($sql, array $values = [], $throw_exceptions = false) { - $link = db_connect(); - - try { - $stmt = $link->prepare($sql); - $stmt->execute($values); - } catch (PDOException $e) { - $error_text = "Invalid query: " . $e->getMessage() . " caused by " . $sql ; - error_log($error_text); - if ($throw_exceptions) { - throw $e; - } - - return 0; - } - - return $stmt->rowCount(); -} - -/** - * @param string $sql - * @param array $values - * @param bool $ignore_errors - set to true to ignore errors. - * @return array e.g. ['result' => PDOStatement, 'error' => string ] - */ -function db_query($sql, array $values = array(), $ignore_errors = false) { - $link = db_connect(); - $error_text = ''; - - try { - $stmt = $link->prepare($sql); - $stmt->execute($values); - } catch (PDOException $e) { - $error_text = "Invalid query: " . $e->getMessage() . " caused by " . $sql ; - error_log($error_text); - if (!$ignore_errors) { - die("DEBUG INFORMATION: " . $e->getMessage() . "
Check your error_log for the failed query"); - } - } - - return array( - "result" => $stmt, - "error" => $error_text, - ); -} - - - - - -/** - * Delete a row from the specified table. - * - * DELETE FROM $table WHERE $where = $delete $aditionalWhere - * - * @param string $table - * @param string $where - should never be a user supplied value - * @param string $delete - * @param string $additionalwhere (default ''). - * @return int|mixed rows deleted. - */ -function db_delete($table, $where, $delete, $additionalwhere='') { - $table = table_by_key($table); - - $query = "DELETE FROM $table WHERE $where = ? $additionalwhere"; - - return db_execute($query, array($delete)); -} - - - -/** - * db_insert - * Action: Inserts a row from a specified table - * Call: db_insert (string table, array values [, array timestamp]) - * - * @param string - table name - * @param array $values - key/value map of data to insert into the table. - * @param array $timestamp (optional) - array of fields to set to now() - default: array('created', 'modified') - * @param boolean $throw_errors - * @return int - number of inserted rows - */ -function db_insert($table, array $values, $timestamp = array('created', 'modified'), $throw_exceptions = false) { - $table = table_by_key($table); - - foreach ($timestamp as $key) { - if (db_sqlite()) { - $values[$key] = "datetime('now')"; - } else { - $values[$key] = "now()"; - } - } - - $value_string = ''; - $comma = ''; - $prepared_statment_values = $values; - - foreach ($values as $field => $value) { - if (in_array($field, $timestamp)) { - $value_string .= $comma . $value; // see above. - unset($prepared_statment_values[$field]); - } else { - $value_string .= $comma . ":{$field}"; - } - $comma = ','; - } - - - return db_execute( - "INSERT INTO $table (" . implode(",", array_keys($values)) .") VALUES ($value_string)", - $prepared_statment_values, - $throw_exceptions); -} - - -/** - * db_update - * Action: Updates a specified table - * Call: db_update (string table, string where_col, string where_value, array values [, array timestamp]) - * @param string $table - table name - * @param string $where_col - column of WHERE condition - * @param string $where_value - value of WHERE condition - * @param array $values - key/value map of data to insert into the table. - * @param array $timestamp (optional) - array of fields to set to now() - default: array('modified') - * @return int - number of updated rows - */ -function db_update($table, $where_col, $where_value, $values, $timestamp = array('modified'), $throw_exceptions = false) { - $table_key = table_by_key($table); - - $sql = "UPDATE $table_key SET "; - - $pvalues = array(); - - $set = array(); - foreach ($values as $key => $value) { - if (in_array($key, $timestamp)) { - if (db_sqlite()) { - $set[] = " $key = datetime('now') "; - } else { - $set[] = " $key = now() "; - } - } else { - $set[] = " $key = :$key "; - $pvalues[$key] = $value; - } - } - - $pvalues['where'] = $where_value; - - - $sql="UPDATE $table_key SET " . implode(",", $set) . " WHERE $where_col = :where"; - - return db_execute($sql, $pvalues, $throw_exceptions); -} - - -/** - * db_log - * Action: Logs actions from admin - * Call: db_log (string domain, string action, string data) - * Possible actions are defined in $LANG["pViewlog_action_$action"] - */ -function db_log($domain, $action, $data) { - if (!Config::bool('logging')) { - return true; - } - - $REMOTE_ADDR = getRemoteAddr(); - - $username = authentication_get_username(); - - if (Config::Lang("pViewlog_action_$action") == '') { - die("Invalid log action : $action"); // could do with something better? - } - - - $logdata = array( - 'username' => "$username ($REMOTE_ADDR)", - 'domain' => $domain, - 'action' => $action, - 'data' => $data, - ); - $result = db_insert('log', $logdata, array('timestamp')); - if ($result != 1) { - return false; - } else { - return true; - } -} - -/** - * db_in_clause - * Action: builds and returns the "field in(x, y)" clause for database queries - * Call: db_in_clause (string field, array values) - * @param string $field - * @param array $values - * @return string - */ -function db_in_clause($field, array $values) { - $v = array_map('escape_string', array_values($values)); - return " $field IN ('" . implode("','", $v) . "') "; -} - -/** - * db_where_clause - * Action: builds and returns a WHERE clause for database queries. All given conditions will be AND'ed. - * Call: db_where_clause (array $conditions, array $struct) - * @param array $condition - array('field' => 'value', 'field2' => 'value2, ...) - * @param array $struct - field structure, used for automatic bool conversion - * @param string $additional_raw_where - raw sniplet to include in the WHERE part - typically needs to start with AND - * @param array $searchmode - operators to use (=, <, > etc.) - defaults to = if not specified for a field (see - * $allowed_operators for available operators) - * Note: the $searchmode operator will only be used if a $condition for that field is set. - * This also means you'll need to set a (dummy) condition for NULL and NOTNULL. - */ -function db_where_clause($condition, $struct, $additional_raw_where = '', $searchmode = array()) { - if (!is_array($condition)) { - die('db_where_cond: parameter $cond is not an array!'); - } elseif (!is_array($searchmode)) { - die('db_where_cond: parameter $searchmode is not an array!'); - } elseif (count($condition) == 0 && trim($additional_raw_where) == '') { - die("db_where_cond: parameter is an empty array!"); # die() might sound harsh, but can prevent information leaks - } elseif (!is_array($struct)) { - die('db_where_cond: parameter $struct is not an array!'); - } - - $allowed_operators = array('<', '>', '>=', '<=', '=', '!=', '<>', 'CONT', 'LIKE', 'NULL', 'NOTNULL'); - $where_parts = array(); - $having_parts = array(); - - foreach ($condition as $field => $value) { - if (isset($struct[$field]) && $struct[$field]['type'] == 'bool') { - $value = db_get_boolean($value); - } - $operator = '='; - if (isset($searchmode[$field])) { - if (in_array($searchmode[$field], $allowed_operators)) { - $operator = $searchmode[$field]; - - if ($operator == 'CONT') { # CONT - as in "contains" - $operator = ' LIKE '; # add spaces - $value = '%' . $value . '%'; - } elseif ($operator == 'LIKE') { # LIKE -without adding % wildcards (the search value can contain %) - $operator = ' LIKE '; # add spaces - } - } else { - die('db_where_clause: Invalid searchmode for ' . $field); - } - } - - if ($operator == "NULL") { - $querypart = $field . ' IS NULL'; - } elseif ($operator == "NOTNULL") { - $querypart = $field . ' IS NOT NULL'; - } else { - $querypart = $field . $operator . "'" . escape_string($value) . "'"; - - // might need other types adding here. - if (db_pgsql() && isset($struct[$field]) && in_array($struct[$field]['type'], array('ts', 'num')) && $value === '') { - $querypart = $field . $operator . " NULL"; - } - } - - if (!empty($struct[$field]['select'])) { - $having_parts[$field] = $querypart; - } else { - $where_parts[$field] = $querypart; - } - } - $query = ' WHERE 1=1 '; - $query .= " $additional_raw_where "; - if (count($where_parts) > 0) { - $query .= " AND ( " . join(" AND ", $where_parts) . " ) "; - } - if (count($having_parts) > 0) { - $query .= " HAVING ( " . join(" AND ", $having_parts) . " ) "; - } - - return $query; -} - -/** - * Convert a programmatic db table name into what may be the actual name. - * - * Takes into consideration any CONF database_prefix or database_tables map - * - * If it's a MySQL database, then we return the name with backticks around it (`). - * - * @param string database table name. - * @return string - database table name with appropriate prefix (and quoting if MySQL) - */ -function table_by_key($table_key) { - global $CONF; - - $table = $table_key; - - if (!empty($CONF['database_tables'][$table_key])) { - $table = $CONF['database_tables'][$table_key]; - } - - $table = $CONF['database_prefix'] . $table; - - if (db_mysql()) { - return "`" . $table . "`"; - } - - return $table; -} - - -/** - * check if the database layout is up to date - * returns the current 'version' value from the config table - * if $error_out is True (default), die() with a message that recommends to run setup.php. - * @param bool $error_out - * @return int - */ -function check_db_version($error_out = true) { - global $min_db_version; - - $table = table_by_key('config'); - - $sql = "SELECT value FROM $table WHERE name = 'version'"; - $row = db_query_one($sql); - if (isset($row['value'])) { - $dbversion = (int) $row['value']; - } else { - db_execute("INSERT INTO $table (name, value) VALUES ('version', '0')"); - $dbversion = 0; - } - - if (($dbversion < $min_db_version) && $error_out == true) { - echo "ERROR: The PostfixAdmin database layout is outdated (you have r$dbversion, but r$min_db_version is expected).\nPlease run setup.php to upgrade the database.\n"; - exit(1); - } - - return $dbversion; -} - - -/** - * - * Action: Return a string of colored  's that indicate - * the if an alias goto has an error or is sent to - * addresses list in show_custom_domains - * - * @param string $show_alias - * @return string - */ -function gen_show_status($show_alias) { - global $CONF; - $table_alias = table_by_key('alias'); - $stat_string = ""; - - $stat_goto = ""; - $stat_result = db_query_one("SELECT goto FROM $table_alias WHERE address=?", array($show_alias)); - - if ($stat_result) { - $stat_goto = $stat_result['goto']; - } - - $delimiter_regex = null; - - if (!empty($CONF['recipient_delimiter'])) { - $delimiter = preg_quote($CONF['recipient_delimiter'], "/"); - $delimiter_regex = '/' .$delimiter. '[^' .$delimiter. '@]*@/'; - } - - // UNDELIVERABLE CHECK - if ($CONF['show_undeliverable'] == 'YES') { - $gotos=array(); - $gotos=explode(',', $stat_goto); - $undel_string=""; - - //make sure this alias goes somewhere known - $stat_ok = 1; - foreach ($gotos as $g) { - if (!$stat_ok) { - break; - } - if (strpos($g, '@') === false) { - continue; - } - - list($local_part, $stat_domain) = explode('@', $g); - - $v = array(); - - $stat_delimiter = ""; - - $sql = "SELECT address FROM $table_alias WHERE address = ? OR address = ?"; - $v[] = $g; - $v[] = '@' . $stat_domain; - - if (!empty($CONF['recipient_delimiter']) && isset($delimiter_regex)) { - $v[] = preg_replace($delimiter_regex, "@", $g); - $sql .= " OR address = ? "; - } - - $stat_result = db_query_one($sql, $v); - - if (empty($stat_result)) { - $stat_ok = 0; - } - - if ($stat_ok == 0) { - if ($stat_domain == $CONF['vacation_domain'] || in_array($stat_domain, $CONF['show_undeliverable_exceptions'])) { - $stat_ok = 1; - } - } - } // while - if ($stat_ok == 0) { - $stat_string .= "" . $CONF['show_status_text'] . " "; - } else { - $stat_string .= $CONF['show_status_text'] . " "; - } - } - - // Vacation CHECK - if ( $CONF['show_vacation'] == 'YES' ) { - $stat_result = db_query_one("SELECT * FROM ". $CONF['database_tables']['vacation'] ." WHERE email = ? AND active = ? ", array($show_alias, db_get_boolean(true) )) ; - if (!empty($stat_result)) { - $stat_string .= "" . $CONF['show_status_text'] . " "; - } else { - $stat_string .= $CONF['show_status_text'] . " "; - } - } - - // Disabled CHECK - if ( $CONF['show_disabled'] == 'YES' ) { - $stat_result = db_query_one( - "SELECT * FROM ". $CONF['database_tables']['mailbox'] ." WHERE username = ? AND active = ?", - array($show_alias, db_get_boolean(false)) - ); - if (!empty($stat_result)) { - $stat_string .= "" . $CONF['show_status_text'] . " "; - } else { - $stat_string .= $CONF['show_status_text'] . " "; - } - } - - // Expired CHECK - if ( Config::bool('password_expiration') && Config::bool('show_expired') ) { - $now = 'now()'; - if (db_sqlite()) { - $now = "datetime('now')"; - } - - $stat_result = db_query_one("SELECT * FROM ". $CONF['database_tables']['mailbox'] ." WHERE username = ? AND password_expiry <= ? AND active = ?", array( $show_alias , $now , db_get_boolean(true) )); - - if (!empty($stat_result)) { - $stat_string .= "" . $CONF['show_status_text'] . " "; - } else { - $stat_string .= $CONF['show_status_text'] . " "; - } - } - - // POP/IMAP CHECK - if ($CONF['show_popimap'] == 'YES') { - $stat_delimiter = ""; - if (!empty($CONF['recipient_delimiter']) && isset($delimiter_regex)) { - $stat_delimiter = ',' . preg_replace($delimiter_regex, "@", $stat_goto); - } - - //if the address passed in appears in its own goto field, its POP/IMAP - # TODO: or not (might also be an alias loop) -> check mailbox table! - if (preg_match('/,' . $show_alias . ',/', ',' . $stat_goto . $stat_delimiter . ',')) { - $stat_string .= "" . $CONF['show_status_text'] . " "; - } else { - $stat_string .= $CONF['show_status_text'] . " "; - } - } - - // CUSTOM DESTINATION CHECK - if (count($CONF['show_custom_domains']) > 0) { - for ($i = 0; $i < sizeof($CONF['show_custom_domains']); $i++) { - if (preg_match('/^.*' . $CONF['show_custom_domains'][$i] . '.*$/', $stat_goto)) { - $stat_string .= "" . $CONF['show_status_text'] . " "; - } else { - $stat_string .= $CONF['show_status_text'] . " "; - } - } - } else { - $stat_string .= "; "; - } - - // $stat_string .= "    " . - // "    "; - return $stat_string; -} - -/** - * @return string - */ -function getRemoteAddr() { - $REMOTE_ADDR = 'localhost'; - if (isset($_SERVER['REMOTE_ADDR'])) { - $REMOTE_ADDR = $_SERVER['REMOTE_ADDR']; - } - - return $REMOTE_ADDR; -} - -/* vim: set expandtab softtabstop=4 tabstop=4 shiftwidth=4: */ -- GitLab From cd92611d7a2a400061ee07fff648ee20f54f4b2b Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Wed, 3 Jul 2019 13:15:38 +0200 Subject: [PATCH 20/24] use port 587 for smtp --- bin/run.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/run.sh b/bin/run.sh index dbc967a..e713740 100755 --- a/bin/run.sh +++ b/bin/run.sh @@ -50,6 +50,7 @@ cat > /postfixadmin/config.local.php < Date: Thu, 11 Jul 2019 12:51:20 +0200 Subject: [PATCH 21/24] Revert "use port 587 for smtp" This reverts commit f0e96414275c6012e57649f7e1991196ccbd7d16. --- bin/run.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/run.sh b/bin/run.sh index e713740..dbc967a 100755 --- a/bin/run.sh +++ b/bin/run.sh @@ -50,7 +50,6 @@ cat > /postfixadmin/config.local.php < Date: Fri, 19 Jul 2019 16:44:49 +0200 Subject: [PATCH 22/24] Revert "Revert "use port 587 for smtp"" This reverts commit 388a5c3037ebc9365d7e5e7b44633f036d4d1c53. --- bin/run.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/run.sh b/bin/run.sh index dbc967a..e713740 100755 --- a/bin/run.sh +++ b/bin/run.sh @@ -50,6 +50,7 @@ cat > /postfixadmin/config.local.php < Date: Mon, 22 Jul 2019 12:58:55 +0200 Subject: [PATCH 23/24] Set smtp_sendmail_tls option --- bin/run.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/run.sh b/bin/run.sh index e713740..1e69432 100755 --- a/bin/run.sh +++ b/bin/run.sh @@ -51,6 +51,7 @@ cat > /postfixadmin/config.local.php < Date: Wed, 24 Jul 2019 13:49:05 +0200 Subject: [PATCH 24/24] use postfixadmin version from master branch --- Dockerfile | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index cbee6c0..f3ba297 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,9 +11,6 @@ ARG GPG_SHORTID="0xC6A682EA63C82F1C" ARG GPG_FINGERPRINT="70CA A060 DE04 2AAE B1B1 5196 C6A6 82EA 63C8 2F1C" ARG SHA256_HASH="866d4c0ca870b2cac184e5837a4d201af8fcefecef09bc2c887a6e017a00cefe" -# TODO: might want to use a commit/release instead -ARG BRANCH="ssl-sendmail" - RUN apk add -t build-dependencies \ ca-certificates \ gnupg \ @@ -34,6 +31,7 @@ RUN apk add -t build-dependencies \ openssl \ && cd /tmp \ && PFA_TARBALL="postfixadmin-${VERSION}.tar.gz" \ +# TODO: no hash/signature check # && wget -q https://downloads.sourceforge.net/project/postfixadmin/postfixadmin/postfixadmin-${VERSION}/${PFA_TARBALL} \ # && wget -q https://downloads.sourceforge.net/project/postfixadmin/postfixadmin/postfixadmin-${VERSION}/${PFA_TARBALL}.asc \ # && ( \ @@ -46,13 +44,12 @@ RUN apk add -t build-dependencies \ # && FINGERPRINT="$(LANG=C gpg --verify ${PFA_TARBALL}.asc ${PFA_TARBALL} 2>&1 | sed -n "s#Primary key fingerprint: \(.*\)#\1#p")" \ # && if [ -z "${FINGERPRINT}" ]; then echo "ERROR: Invalid GPG signature!" && exit 1; fi \ # && if [ "${FINGERPRINT}" != "${GPG_FINGERPRINT}" ]; then echo "ERROR: Wrong GPG fingerprint!" && exit 1; fi \ -# TODO: no hash/signature check - && PFA_TARBALL="postfixadmin-${VERSION}.tar.gz" \ - && wget -q "https://github.com/Nutomic/postfixadmin/archive/$BRANCH.tar.gz" -O ${PFA_TARBALL} \ - && mkdir /postfixadmin && tar xzf ${PFA_TARBALL} -C /postfixadmin \ - && mv /postfixadmin/postfixadmin-$BRANCH/* /postfixadmin \ + && PFA_TARBALL="postfixadmin.tar.gz" \ + && wget "https://github.com/postfixadmin/postfixadmin/archive/d788c6ac9991f60d8256c4f986b72d9fd10ff9d3.tar.gz" -O ${PFA_TARBALL} \ + && mkdir /postfixadmin \ + && tar xzf ${PFA_TARBALL} --strip 1 -C /postfixadmin \ && apk del build-dependencies \ - && rm -rf /var/cache/apk/* /tmp/* /root/.gnupg /postfixadmin/postfixadmin-$BRANCH* + && rm -rf /var/cache/apk/* /tmp/* /root/.gnupg ${PFA_TARBALL} COPY bin/run.sh /usr/local/bin RUN apk add --no-cache bash openssh \ -- GitLab