From 96ed94dedde26cf914f6a665ac0b74cce6248e14 Mon Sep 17 00:00:00 2001 From: Michael Kuron Date: Sun, 2 Jul 2017 13:44:05 +0200 Subject: [PATCH 01/26] Client configuration guides - iOS mobileconfig generator - non-443 HTTPS ports in autoconfig etc. - disabling POP service via SRV record - fix display name in Outlook IMAP autodiscover - allow multiple calls to TLSA generator and support Sieve STARTTLS --- data/Dockerfiles/sogo/Dockerfile | 1 + data/web/autoconfig.php | 47 +++-- data/web/autodiscover.php | 54 ++--- data/web/clientconfig.php | 185 ++++++++++++++++++ data/web/inc/functions.inc.php | 24 ++- data/web/inc/vars.inc.php | 27 ++- data/web/lang/lang.de.php | 1 + data/web/lang/lang.en.php | 1 + data/web/mobileconfig.php | 167 ++++++++++++++++ data/web/thunderbird-plugins/build-plugins.sh | 6 +- data/web/user.php | 1 + docker-compose.yml | 2 +- 12 files changed, 460 insertions(+), 56 deletions(-) create mode 100644 data/web/clientconfig.php create mode 100644 data/web/mobileconfig.php diff --git a/data/Dockerfiles/sogo/Dockerfile b/data/Dockerfiles/sogo/Dockerfile index a3a1308d..b9db3c1c 100644 --- a/data/Dockerfiles/sogo/Dockerfile +++ b/data/Dockerfiles/sogo/Dockerfile @@ -8,6 +8,7 @@ ENV GOSU_VERSION 1.9 # Prerequisites RUN apt-get update && apt-get install -y --no-install-recommends \ apt-transport-https \ + bind9-host \ ca-certificates \ cron \ gnupg \ diff --git a/data/web/autoconfig.php b/data/web/autoconfig.php index d01bc724..523d6183 100644 --- a/data/web/autoconfig.php +++ b/data/web/autoconfig.php @@ -5,6 +5,16 @@ if (empty($mailcow_hostname)) { exit(); } +$domain_dot = strpos($_SERVER['HTTP_HOST'], '.'); +$domain_port = strpos($_SERVER['HTTP_HOST'], ':'); +if ($domain_port === FALSE) { + $domain = substr($_SERVER['HTTP_HOST'], $domain_dot+1); + $port = 443; +} else { + $domain = substr($_SERVER['HTTP_HOST'], $domain_dot+1, $domain_port-$domain_dot-1); + $port = substr($_SERVER['HTTP_HOST'], $domain_port+1); +} + header('Content-Type: application/xml'); ?> '; ?> @@ -15,52 +25,59 @@ header('Content-Type: application/xml'); mail server - - 993 + + SSL %EMAILADDRESS% password-cleartext - - 143 + + STARTTLS %EMAILADDRESS% password-cleartext + - - 995 + + SSL %EMAILADDRESS% password-cleartext + + - - 110 + + STARTTLS %EMAILADDRESS% password-cleartext + - - 465 + + SSL %EMAILADDRESS% password-cleartext - - - 587 + + STARTTLS %EMAILADDRESS% password-cleartext - + If you didn't change the password given to you by the administrator or if you didn't change it in a long time, please consider doing that now. Sollten Sie das Ihnen durch den Administrator vergebene Passwort noch nicht geändert haben, empfehlen wir dies nun zu tun. Auch ein altes Passwort sollte aus Sicherheitsgründen geändert werden. @@ -68,6 +85,6 @@ header('Content-Type: application/xml'); - + diff --git a/data/web/autodiscover.php b/data/web/autodiscover.php index fd8cd641..7f6081ae 100644 --- a/data/web/autodiscover.php +++ b/data/web/autodiscover.php @@ -12,13 +12,14 @@ error_reporting(0); $data = trim(file_get_contents("php://input")); // Desktop client needs IMAP, unless it's Outlook 2013 or higher on Windows -if (strpos($data, 'autodiscover/outlook/responseschema')) { // desktop client +if (strpos($data, 'autodiscover/outlook/responseschema') !== false) { // desktop client $configuration['autodiscoverType'] = 'imap'; if ($configuration['useEASforOutlook'] == 'yes' && - // Office for macOS does not support EAS - strpos($_SERVER['HTTP_USER_AGENT'], 'Mac') === false && - // Outlook 2013 (version 15) or higher - preg_match('/(Outlook|Office).+1[5-9]\./', $_SERVER['HTTP_USER_AGENT'])) { + // Office for macOS does not support EAS + strpos($_SERVER['HTTP_USER_AGENT'], 'Mac') === false && + // Outlook 2013 (version 15) or higher + preg_match('/(Outlook|Office).+1[5-9]\./', $_SERVER['HTTP_USER_AGENT']) + ) { $configuration['autodiscoverType'] = 'activesync'; } } @@ -60,8 +61,28 @@ else { Request->EMailAddress; + try { + $discover = new SimpleXMLElement($data); + $email = $discover->Request->EMailAddress; + } catch (Exception $e) { + $email = $_SERVER['PHP_AUTH_USER']; + } + + $username = trim($email); + try { + $stmt = $pdo->prepare("SELECT `name` FROM `mailbox` WHERE `username`= :username"); + $stmt->execute(array(':username' => $username)); + $MailboxData = $stmt->fetch(PDO::FETCH_ASSOC); + } + catch(PDOException $e) { + die("Failed to determine name from SQL"); + } + if (!empty($MailboxData['name'])) { + $displayname = utf8_encode($MailboxData['name']); + } + else { + $displayname = $email; + } if ($configuration['autodiscoverType'] == 'imap') { ?> @@ -96,13 +117,13 @@ else { CalDAV - /SOGo/dav//Calendar + https:///SOGo/dav//Calendar off CardDAV - /SOGo/dav//Contacts + https:///SOGo/dav//Contacts off @@ -111,21 +132,6 @@ else { prepare("SELECT `name` FROM `mailbox` WHERE `username`= :username"); - $stmt->execute(array(':username' => $username)); - $MailboxData = $stmt->fetch(PDO::FETCH_ASSOC); - } - catch(PDOException $e) { - die("Failed to determine name from SQL"); - } - if (!empty($MailboxData['name'])) { - $displayname = utf8_encode($MailboxData['name']); - } - else { - $displayname = $email; - } ?> en:en diff --git a/data/web/clientconfig.php b/data/web/clientconfig.php new file mode 100644 index 00000000..68722a89 --- /dev/null +++ b/data/web/clientconfig.php @@ -0,0 +1,185 @@ +prepare("SELECT `name` FROM `mailbox` WHERE `username`= :username"); + $stmt->execute(array(':username' => $username)); + $MailboxData = $stmt->fetch(PDO::FETCH_ASSOC); +} +catch(PDOException $e) { + die("Failed to determine name from SQL"); +} +if (!empty($MailboxData['name'])) { + $displayname = utf8_encode($MailboxData['name']); +} +else { + $displayname = $email; +} +?> +
+

Client Configuration Guide

+

Select your email client or operating system below to view a step-by-step guide to setting up your email account.

+ +

Apple macOS / iOS

+ + +

Android

+ + +

eM Client

+ + +

KDE Kontact

+ + +

Microsoft Outlook

+ + +

Mozilla Thunderbird

+ + +

Windows

+ + +

Windows Phone

+ +
+ + diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index 8e7b7cfd..ffca2bea 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -62,17 +62,17 @@ function hasMailboxObjectAccess($username, $role, $object) { } return false; } +function pem_to_der($pem_key) { + // Need to remove BEGIN/END PUBLIC KEY + $lines = explode("\n", trim($pem_key)); + unset($lines[count($lines)-1]); + unset($lines[0]); + return base64_decode(implode('', $lines)); +} function generate_tlsa_digest($hostname, $port, $starttls = null) { if (!is_valid_domain_name($hostname)) { return "Not a valid hostname"; } - function pem_to_der($pem_key) { - // Need to remove BEGIN/END PUBLIC KEY - $lines = explode("\n", trim($pem_key)); - unset($lines[count($lines)-1]); - unset($lines[0]); - return base64_decode(implode('', $lines)); - } if (empty($starttls)) { $context = stream_context_create(array("ssl" => array("capture_peer_cert" => true, 'verify_peer' => false, 'allow_self_signed' => true))); @@ -88,20 +88,24 @@ function generate_tlsa_digest($hostname, $port, $starttls = null) { return $error_nr . ': ' . $error_msg; } $banner = fread($stream, 512 ); - if (preg_match("/^220/i", $banner)) { + if (preg_match("/^220/i", $banner)) { // SMTP fwrite($stream,"HELO tlsa.generator.local\r\n"); fread($stream, 512); fwrite($stream,"STARTTLS\r\n"); fread($stream, 512); } - elseif (preg_match("/imap.+starttls/i", $banner)) { + elseif (preg_match("/imap.+starttls/i", $banner)) { // IMAP fwrite($stream,"A1 STARTTLS\r\n"); fread($stream, 512); } - elseif (preg_match("/^\+OK/", $banner)) { + elseif (preg_match("/^\+OK/", $banner)) { // POP3 fwrite($stream,"STLS\r\n"); fread($stream, 512); } + elseif (preg_match("/^OK/m", $banner)) { // Sieve + fwrite($stream,"STARTTLS\r\n"); + fread($stream, 512); + } else { return 'Unknown banner: "' . htmlspecialchars(trim($banner)) . '"'; } diff --git a/data/web/inc/vars.inc.php b/data/web/inc/vars.inc.php index a52442db..05ef25dd 100644 --- a/data/web/inc/vars.inc.php +++ b/data/web/inc/vars.inc.php @@ -18,31 +18,48 @@ $database_name = getenv('DBNAME'); $mailcow_hostname = getenv('MAILCOW_HOSTNAME'); // Autodiscover settings +$https_port = strpos($_SERVER['HTTP_HOST'], ':'); +if ($https_port === FALSE) { + $https_port = 443; +} else { + $https_port = substr($_SERVER['HTTP_HOST'], $https_port+1); +} $autodiscover_config = array( // Enable the autodiscover service for Outlook desktop clients 'useEASforOutlook' => 'yes', // General autodiscover service type: "activesync" or "imap" 'autodiscoverType' => 'activesync', - // Please don't use STARTTLS-enabled service ports here. + // Please don't use STARTTLS-enabled service ports in the "port" variable. // The autodiscover service will always point to SMTPS and IMAPS (TLS-wrapped services). + // The autoconfig service will additionally announce the STARTTLS-enabled ports, specified in the "tlsport" variable. 'imap' => array( 'server' => $mailcow_hostname, 'port' => getenv('IMAPS_PORT'), + 'tlsport' => getenv('IMAP_PORT'), + ), + 'pop3' => array( + 'server' => $mailcow_hostname, + 'port' => getenv('POPS_PORT'), + 'tlsport' => getenv('POP_PORT'), ), 'smtp' => array( 'server' => $mailcow_hostname, 'port' => getenv('SMTPS_PORT'), + 'tlsport' => getenv('SUBMISSION_PORT'), ), 'activesync' => array( - 'url' => 'https://'.$mailcow_hostname.'/Microsoft-Server-ActiveSync' + 'url' => 'https://'.$mailcow_hostname.($https_port == 443 ? '' : ':'.$https_port).'/Microsoft-Server-ActiveSync', ), 'caldav' => array( - 'url' => 'https://'.$mailcow_hostname + 'server' => $mailcow_hostname, + 'port' => $https_port, ), 'carddav' => array( - 'url' => 'https://'.$mailcow_hostname - ) + 'server' => $mailcow_hostname, + 'port' => $https_port, + ), ); +unset($https_port); // Where to go after adding and editing objects // Can be "form" or "previous" diff --git a/data/web/lang/lang.de.php b/data/web/lang/lang.de.php index eef13b3f..8706e4ba 100644 --- a/data/web/lang/lang.de.php +++ b/data/web/lang/lang.de.php @@ -108,6 +108,7 @@ $lang['user']['user_settings'] = 'Benutzereinstellungen'; $lang['user']['mailbox_settings'] = 'Mailbox-Einstellungen'; $lang['user']['mailbox_details'] = 'Mailbox-Details'; $lang['user']['change_password'] = 'Passwort ändern'; +$lang['user']['client_configuration'] = 'Konfigurationsanleitungen für E-Mail-Programme und Smartphones anzeigen'; $lang['user']['new_password'] = 'Neues Passwort'; $lang['user']['save_changes'] = 'Änderungen speichern'; $lang['user']['password_now'] = 'Aktuelles Passwort (Änderungen bestätigen)'; diff --git a/data/web/lang/lang.en.php b/data/web/lang/lang.en.php index 3be72fec..fceef80f 100644 --- a/data/web/lang/lang.en.php +++ b/data/web/lang/lang.en.php @@ -110,6 +110,7 @@ $lang['user']['user_settings'] = 'User settings'; $lang['user']['mailbox_settings'] = 'Mailbox settings'; $lang['user']['mailbox_details'] = 'Mailbox details'; $lang['user']['change_password'] = 'Change password'; +$lang['user']['client_configuration'] = 'Show configuration guides for email clients and smartphones'; $lang['user']['new_password'] = 'New password'; $lang['user']['save_changes'] = 'Save changes'; $lang['user']['password_now'] = 'Current password (confirm changes)'; diff --git a/data/web/mobileconfig.php b/data/web/mobileconfig.php new file mode 100644 index 00000000..198fa4d7 --- /dev/null +++ b/data/web/mobileconfig.php @@ -0,0 +1,167 @@ +prepare("SELECT `name` FROM `mailbox` WHERE `username`= :username"); + $stmt->execute(array(':username' => $email)); + $MailboxData = $stmt->fetch(PDO::FETCH_ASSOC); +} +catch(PDOException $e) { + die("Failed to determine name from SQL"); +} +if (!empty($MailboxData['name'])) { + $displayname = utf8_encode($MailboxData['name']); +} +else { + $displayname = $email; +} + +echo '' . "\n"; +?> + + + + PayloadContent + + + CalDAVAccountDescription + + CalDAVHostName + + CalDAVPort + + CalDAVPrincipalURL + /SOGo/dav/ + CalDAVUseSSL + + CalDAVUsername + + PayloadDescription + Configures CalDAV account. + PayloadDisplayName + CalDAV () + PayloadIdentifier + .CalDAV + PayloadOrganization + + PayloadType + com.apple.caldav.account + PayloadUUID + FC898573-EBA8-48AF-93BD-BFA0C9778FA7 + PayloadVersion + 1 + + + EmailAccountDescription + + EmailAccountType + EmailTypeIMAP + EmailAccountName + + EmailAddress + + IncomingMailServerAuthentication + EmailAuthPassword + IncomingMailServerHostName + + IncomingMailServerPortNumber + + IncomingMailServerUseSSL + + IncomingMailServerUsername + + OutgoingMailServerAuthentication + EmailAuthPassword + OutgoingMailServerHostName + + OutgoingMailServerPortNumber + + OutgoingMailServerUseSSL + + OutgoingMailServerUsername + + OutgoingPasswordSameAsIncomingPassword + + PayloadDescription + Configures email account. + PayloadDisplayName + IMAP Account () + PayloadIdentifier + .email + PayloadOrganization + + PayloadType + com.apple.mail.managed + PayloadUUID + 00294FBB-1016-413E-87B9-652D856D6875 + PayloadVersion + 1 + PreventAppSheet + + PreventMove + + SMIMEEnabled + + + + CardDAVAccountDescription + + CardDAVHostName + + CardDAVPort + + CardDAVPrincipalURL + /SOGo/dav/ + CardDAVUseSSL + + CardDAVUsername + + PayloadDescription + Configures CardDAV accounts + PayloadDisplayName + CardDAV () + PayloadIdentifier + .carddav + PayloadOrganization + + PayloadType + com.apple.carddav.account + PayloadUUID + 0797EF2B-B1F1-4BC7-ABCD-4580862252B4 + PayloadVersion + 1 + + + PayloadDescription + IMAP, CalDAV, CardDAV + PayloadDisplayName + Mailcow + PayloadIdentifier + + PayloadOrganization + + PayloadRemovalDisallowed + + PayloadType + Configuration + PayloadUUID + 5EE248C5-ACCB-42D8-9199-8F8ED08D5624 + PayloadVersion + 1 + + diff --git a/data/web/thunderbird-plugins/build-plugins.sh b/data/web/thunderbird-plugins/build-plugins.sh index 03986b78..53199e61 100755 --- a/data/web/thunderbird-plugins/build-plugins.sh +++ b/data/web/thunderbird-plugins/build-plugins.sh @@ -16,11 +16,15 @@ tar --strip-components=1 -C connector -xf connector.tar.gz # build custom integrator while read DOMAIN; do echo "Building SOGo Integrator for $DOMAIN hosted on $MAILHOST" + PORT_NUM=$(dig -t srv _autodiscover._tcp.$DOMAIN +short | tail -n 1 | awk '{print $3}') + if [[ -z ${PORT_NUM} ]]; then + PORT_NUM=443 + fi cd integrator echo > defaults/preferences/site.js mkdir -p custom/${DOMAIN} cp -r custom/sogo-demo/* custom/${DOMAIN}/ - sed -i "s/http:\/\/sogo-demo\.inverse\.ca/https:\/\/${MAILHOST}/g" custom/${DOMAIN}/chrome/content/extensions.rdf + sed -i "s/http:\/\/sogo-demo\.inverse\.ca/https:\/\/${MAILHOST}:${PORT_NUM}/g" custom/${DOMAIN}/chrome/content/extensions.rdf sed -i "s/plugins\/updates\.php[?]/thunderbird-plugins.php?domain=${DOMAIN}\&/g" custom/${DOMAIN}/chrome/content/extensions.rdf echo 'pref("sogo-integrator.autocomplete.server.urlid", "'${DOMAIN}'");' > custom/${DOMAIN}/defaults/preferences/site.js echo 'pref("mail.collect_email_address_outgoing", false);' >> custom/${DOMAIN}/defaults/preferences/site.js diff --git a/data/web/user.php b/data/web/user.php index 77ccc62d..ba7678e2 100644 --- a/data/web/user.php +++ b/data/web/user.php @@ -74,6 +74,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '

[]

+

[]


diff --git a/docker-compose.yml b/docker-compose.yml index 853dbc36..e8a7e0c4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -143,7 +143,7 @@ services: - phpfpm sogo-mailcow: - image: mailcow/sogo:1.0 + image: mailcow/sogo:1.1 build: ./data/Dockerfiles/sogo depends_on: unbound-mailcow: From b88190988e3b154b2b93e875f6266e8f6a4d131f Mon Sep 17 00:00:00 2001 From: Michael Kuron Date: Tue, 4 Jul 2017 19:58:30 +0200 Subject: [PATCH 02/26] Autodiscover: Strip bind addresses off of port environment variables --- data/web/inc/vars.inc.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/data/web/inc/vars.inc.php b/data/web/inc/vars.inc.php index 05ef25dd..775af1e1 100644 --- a/data/web/inc/vars.inc.php +++ b/data/web/inc/vars.inc.php @@ -34,18 +34,18 @@ $autodiscover_config = array( // The autoconfig service will additionally announce the STARTTLS-enabled ports, specified in the "tlsport" variable. 'imap' => array( 'server' => $mailcow_hostname, - 'port' => getenv('IMAPS_PORT'), - 'tlsport' => getenv('IMAP_PORT'), + 'port' => array_pop(explode(':', getenv('IMAPS_PORT'))), + 'tlsport' => array_pop(explode(':', getenv('IMAP_PORT'))), ), 'pop3' => array( 'server' => $mailcow_hostname, - 'port' => getenv('POPS_PORT'), - 'tlsport' => getenv('POP_PORT'), + 'port' => array_pop(explode(':', getenv('POPS_PORT'))), + 'tlsport' => array_pop(explode(':', getenv('POP_PORT'))), ), 'smtp' => array( 'server' => $mailcow_hostname, - 'port' => getenv('SMTPS_PORT'), - 'tlsport' => getenv('SUBMISSION_PORT'), + 'port' => array_pop(explode(':', getenv('SMTPS_PORT'))), + 'tlsport' => array_pop(explode(':', getenv('SUBMISSION_PORT'))), ), 'activesync' => array( 'url' => 'https://'.$mailcow_hostname.($https_port == 443 ? '' : ':'.$https_port).'/Microsoft-Server-ActiveSync', From 8946d692742bdf37814ad953c96ba5c3b4f41cde Mon Sep 17 00:00:00 2001 From: Michael Kuron Date: Wed, 5 Jul 2017 20:11:07 +0200 Subject: [PATCH 03/26] Fix merge conflict --- data/Dockerfiles/sogo/Dockerfile | 1 - data/web/thunderbird-plugins/build-plugins.sh | 6 +----- docker-compose.yml | 2 +- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/data/Dockerfiles/sogo/Dockerfile b/data/Dockerfiles/sogo/Dockerfile index b9db3c1c..a3a1308d 100644 --- a/data/Dockerfiles/sogo/Dockerfile +++ b/data/Dockerfiles/sogo/Dockerfile @@ -8,7 +8,6 @@ ENV GOSU_VERSION 1.9 # Prerequisites RUN apt-get update && apt-get install -y --no-install-recommends \ apt-transport-https \ - bind9-host \ ca-certificates \ cron \ gnupg \ diff --git a/data/web/thunderbird-plugins/build-plugins.sh b/data/web/thunderbird-plugins/build-plugins.sh index 53199e61..03986b78 100755 --- a/data/web/thunderbird-plugins/build-plugins.sh +++ b/data/web/thunderbird-plugins/build-plugins.sh @@ -16,15 +16,11 @@ tar --strip-components=1 -C connector -xf connector.tar.gz # build custom integrator while read DOMAIN; do echo "Building SOGo Integrator for $DOMAIN hosted on $MAILHOST" - PORT_NUM=$(dig -t srv _autodiscover._tcp.$DOMAIN +short | tail -n 1 | awk '{print $3}') - if [[ -z ${PORT_NUM} ]]; then - PORT_NUM=443 - fi cd integrator echo > defaults/preferences/site.js mkdir -p custom/${DOMAIN} cp -r custom/sogo-demo/* custom/${DOMAIN}/ - sed -i "s/http:\/\/sogo-demo\.inverse\.ca/https:\/\/${MAILHOST}:${PORT_NUM}/g" custom/${DOMAIN}/chrome/content/extensions.rdf + sed -i "s/http:\/\/sogo-demo\.inverse\.ca/https:\/\/${MAILHOST}/g" custom/${DOMAIN}/chrome/content/extensions.rdf sed -i "s/plugins\/updates\.php[?]/thunderbird-plugins.php?domain=${DOMAIN}\&/g" custom/${DOMAIN}/chrome/content/extensions.rdf echo 'pref("sogo-integrator.autocomplete.server.urlid", "'${DOMAIN}'");' > custom/${DOMAIN}/defaults/preferences/site.js echo 'pref("mail.collect_email_address_outgoing", false);' >> custom/${DOMAIN}/defaults/preferences/site.js diff --git a/docker-compose.yml b/docker-compose.yml index e8a7e0c4..853dbc36 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -143,7 +143,7 @@ services: - phpfpm sogo-mailcow: - image: mailcow/sogo:1.1 + image: mailcow/sogo:1.0 build: ./data/Dockerfiles/sogo depends_on: unbound-mailcow: From c1a1f98ee52a1c8bb8f19cbabd8dfd8635ca3ce7 Mon Sep 17 00:00:00 2001 From: Michael Kuron Date: Sun, 9 Jul 2017 10:01:27 +0200 Subject: [PATCH 04/26] DNS diagnostics page --- data/web/inc/header.inc.php | 5 + data/web/lang/lang.de.php | 7 + data/web/lang/lang.en.php | 7 + diagnostics.php | 272 ++++++++++++++++++++++++++++++++++++ 4 files changed, 291 insertions(+) create mode 100644 diagnostics.php diff --git a/data/web/inc/header.inc.php b/data/web/inc/header.inc.php index 533711de..8e5b6d07 100644 --- a/data/web/inc/header.inc.php +++ b/data/web/inc/header.inc.php @@ -77,6 +77,11 @@ > + > + > diff --git a/data/web/lang/lang.de.php b/data/web/lang/lang.de.php index eef13b3f..732171cc 100644 --- a/data/web/lang/lang.de.php +++ b/data/web/lang/lang.de.php @@ -214,6 +214,7 @@ $lang['header']['mailcow_settings'] = 'Konfiguration'; $lang['header']['administration'] = 'Administration'; $lang['header']['mailboxes'] = 'Mailboxen'; $lang['header']['user_settings'] = 'Benutzereinstellungen'; +$lang['header']['diagnostics'] = 'Diagnose'; $lang['header']['login'] = 'Anmeldung'; $lang['header']['logged_in_as_logout'] = 'Eingeloggt als %s (abmelden)'; $lang['header']['logged_in_as_logout_dual'] = 'Eingeloggt als %s [%s]'; @@ -495,3 +496,9 @@ $lang['admin']['add_forwarding_host'] = 'Weiterleitungs-Host hinzufügen'; $lang['delete']['remove_forwardinghost_warning'] = 'Warnung: Sie entfernen den Weiterleitungs-Host %s!'; $lang['success']['forwarding_host_removed'] = "Weiterleitungs-Host %s wurde entfernt"; $lang['success']['forwarding_host_added'] = "Weiterleitungs-Host %s wurde hinzugefügt"; +$lang['diagnostics']['dns_records'] = 'DNS-Einträge'; +$lang['diagnostics']['dns_records_24hours'] = 'Bitte beachten Sie, dass es bis zu 24 Stunden dauern kann, bis Änderungen an Ihren DNS-Einträgen als aktueller Status auf dieser Seite dargestellt werden. Diese Seite ist nur als Hilfsmittel gedacht, um die korrekten Werte für DNS-Einträge zu anzuzeigen und zu überprüfen, ob die Daten im DNS hinterlegt sind.'; +$lang['diagnostics']['dns_records_name'] = 'Name'; +$lang['diagnostics']['dns_records_type'] = 'Typ'; +$lang['diagnostics']['dns_records_data'] = 'Korrekte Daten'; +$lang['diagnostics']['dns_records_status'] = 'Aktueller Status'; diff --git a/data/web/lang/lang.en.php b/data/web/lang/lang.en.php index 3be72fec..391c7d01 100644 --- a/data/web/lang/lang.en.php +++ b/data/web/lang/lang.en.php @@ -216,6 +216,7 @@ $lang['header']['mailcow_settings'] = 'Configuration'; $lang['header']['administration'] = 'Administration'; $lang['header']['mailboxes'] = 'Mailboxes'; $lang['header']['user_settings'] = 'User settings'; +$lang['header']['diagnostics'] = 'Diagnostics'; $lang['header']['login'] = 'Login'; $lang['header']['logged_in_as_logout'] = 'Logged in as %s (logout)'; $lang['header']['logged_in_as_logout_dual'] = 'Logged in as %s [%s]'; @@ -508,3 +509,9 @@ $lang['admin']['add_forwarding_host'] = 'Add Forwarding Host'; $lang['delete']['remove_forwardinghost_warning'] = 'Warning: You are about to remove the forwarding host %s!'; $lang['success']['forwarding_host_removed'] = "Forwarding host %s has been removed"; $lang['success']['forwarding_host_added'] = "Forwarding host %s has been added"; +$lang['diagnostics']['dns_records'] = 'DNS Records'; +$lang['diagnostics']['dns_records_24hours'] = 'Please note that changes made to DNS may take up to 24 hours to correctly have their current state reflected on this page. It is intended as a way for you to easily see how to configure your DNS records and to check whether all your records are correctly stored in DNS.'; +$lang['diagnostics']['dns_records_name'] = 'Name'; +$lang['diagnostics']['dns_records_type'] = 'Type'; +$lang['diagnostics']['dns_records_data'] = 'Correct Data'; +$lang['diagnostics']['dns_records_status'] = 'Current State'; diff --git a/diagnostics.php b/diagnostics.php new file mode 100644 index 00000000..4de0082e --- /dev/null +++ b/diagnostics.php @@ -0,0 +1,272 @@ + 1) { + $mask = $net[1]; + } + $net = inet_pton($net[0]); + $addr = inet_pton($addr); + $length = strlen($net); // 4 for IPv4, 16 for IPv6 + if (strlen($net) != strlen($addr)) { + return false; + } + if (!isset($mask)) { + $mask = $length * 8; + } + $addr_bin = ''; + $net_bin = ''; + for ($i = 0; $i < $length; ++$i) { + $addr_bin .= str_pad(decbin(ord(substr($addr, $i, $i+1))), 8, '0', STR_PAD_LEFT); + $net_bin .= str_pad(decbin(ord(substr($net, $i, $i+1))), 8, '0', STR_PAD_LEFT); + } + return substr($addr_bin, 0, $mask) == substr($net_bin, 0, $mask); +} + +if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin") { +require_once("inc/header.inc.php"); + +$ch = curl_init('http://ipv4.mailcow.email'); +curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); +curl_setopt($ch, CURLOPT_VERBOSE, false); +curl_setopt($ch, CURLOPT_HEADER, false); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); +curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3); +$ip = curl_exec($ch); +curl_close($ch); + +$ch = curl_init('http://ipv6.mailcow.email'); +curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V6); +curl_setopt($ch, CURLOPT_VERBOSE, false); +curl_setopt($ch, CURLOPT_HEADER, false); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); +curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3); +$ip6 = curl_exec($ch); +curl_close($ch); + +$ptr = implode('.', array_reverse(explode('.', $ip))) . '.in-addr.arpa'; +if (!empty($ip6)) { + $ip6_full = str_replace('::', str_repeat(':', 9-substr_count($ip6, ':')), $ip6); + $ip6_full = str_replace('::', ':0:', $ip6_full); + $ip6_full = str_replace('::', ':0:', $ip6_full); + $ptr6 = ''; + foreach (explode(':', $ip6_full) as $part) { + $ptr6 .= str_pad($part, 4, '0', STR_PAD_LEFT); + } + $ptr6 = implode('.', array_reverse(str_split($ptr6, 1))) . '.ip6.arpa'; +} + +$https_port = strpos($_SERVER['HTTP_HOST'], ':'); +if ($https_port === FALSE) { + $https_port = 443; +} else { + $https_port = substr($_SERVER['HTTP_HOST'], $https_port+1); +} + +$records = array(); +$records[] = array($mailcow_hostname, 'A', $ip); +$records[] = array($ptr, 'PTR', $mailcow_hostname); +if (!empty($ip6)) { + $records[] = array($mailcow_hostname, 'AAAA', $ip6); + $records[] = array($ptr6, 'PTR', $mailcow_hostname); +} +$domains = mailbox('get', 'domains'); +foreach(mailbox('get', 'domains') as $domain) { + $domains = array_merge($domains, mailbox('get', 'alias_domains', $domain)); +} + +if (!isset($autodiscover_config['sieve'])) { + $autodiscover_config['sieve'] = array('server' => $mailcow_hostname, 'port' => array_pop(explode(':', getenv('SIEVE_PORT')))); +} + +$records[] = array('_25._tcp.' . $autodiscover_config['smtp']['server'], 'TLSA', generate_tlsa_digest($autodiscover_config['smtp']['server'], 25, 1)); +$records[] = array('_' . $https_port . '._tcp.' . $mailcow_hostname, 'TLSA', generate_tlsa_digest($mailcow_hostname, $https_port)); +$records[] = array('_' . $autodiscover_config['pop3']['tlsport'] . '._tcp.' . $autodiscover_config['pop3']['server'], 'TLSA', generate_tlsa_digest($autodiscover_config['pop3']['server'], $autodiscover_config['pop3']['tlsport'], 1)); +$records[] = array('_' . $autodiscover_config['imap']['tlsport'] . '._tcp.' . $autodiscover_config['imap']['server'], 'TLSA', generate_tlsa_digest($autodiscover_config['imap']['server'], $autodiscover_config['imap']['tlsport'], 1)); +$records[] = array('_' . $autodiscover_config['smtp']['port'] . '._tcp.' . $autodiscover_config['smtp']['server'], 'TLSA', generate_tlsa_digest($autodiscover_config['smtp']['server'], $autodiscover_config['smtp']['port'])); +$records[] = array('_' . $autodiscover_config['smtp']['tlsport'] . '._tcp.' . $autodiscover_config['smtp']['server'], 'TLSA', generate_tlsa_digest($autodiscover_config['smtp']['server'], $autodiscover_config['smtp']['tlsport'], 1)); +$records[] = array('_' . $autodiscover_config['imap']['port'] . '._tcp.' . $autodiscover_config['imap']['server'], 'TLSA', generate_tlsa_digest($autodiscover_config['imap']['server'], $autodiscover_config['imap']['port'])); +$records[] = array('_' . $autodiscover_config['pop3']['port'] . '._tcp.' . $autodiscover_config['pop3']['server'], 'TLSA', generate_tlsa_digest($autodiscover_config['pop3']['server'], $autodiscover_config['pop3']['port'])); +$records[] = array('_' . $autodiscover_config['sieve']['port'] . '._tcp.' . $autodiscover_config['sieve']['server'], 'TLSA', generate_tlsa_digest($autodiscover_config['sieve']['server'], $autodiscover_config['sieve']['port'], 1)); + +foreach ($domains as $domain) { + $records[] = array($domain, 'MX', $mailcow_hostname); + $records[] = array('autodiscover.' . $domain, 'CNAME', $mailcow_hostname); + $records[] = array('_autodiscover._tcp.' . $domain, 'SRV', $mailcow_hostname . ' ' . $https_port); + $records[] = array('autoconfig.' . $domain, 'CNAME', $mailcow_hostname); + $records[] = array($domain, 'TXT', 'v=spf1 mx -all'); + $records[] = array('_dmarc.' . $domain, 'TXT', 'v=DMARC1; p=reject', 'v=DMARC1; p='); + + if (!empty($dkim = dkim('details', $domain))) { + $records[] = array($dkim['dkim_selector'] . '._domainkey.' . $domain, 'TXT', $dkim['dkim_txt']); + } + + $current_records = dns_get_record('_pop3._tcp.' . $domain, DNS_SRV); + if (count($current_records) == 0 || $current_records[0]['target'] != '') { + if ($autodiscover_config['pop3']['tlsport'] != '110') { + $records[] = array('_pop3._tcp.' . $domain, 'SRV', $autodiscover_config['pop3']['server'] . ' ' . $autodiscover_config['pop3']['tlsport']); + } + } else { + $records[] = array('_pop3._tcp.' . $domain, 'SRV', '. 0'); + } + $current_records = dns_get_record('_pop3s._tcp.' . $domain, DNS_SRV); + if (count($current_records) == 0 || $current_records[0]['target'] != '') { + if ($autodiscover_config['pop3']['port'] != '995') { + $records[] = array('_pop3s._tcp.' . $domain, 'SRV', $autodiscover_config['pop3']['server'] . ' ' . $autodiscover_config['pop3']['port']); + } + } else { + $records[] = array('_pop3s._tcp.' . $domain, 'SRV', '. 0'); + } + if ($autodiscover_config['imap']['tlsport'] != '143') { + $records[] = array('_imap._tcp.' . $domain, 'SRV', $autodiscover_config['imap']['server'] . ' ' . $autodiscover_config['imap']['tlsport']); + } + if ($autodiscover_config['imap']['port'] != '993') { + $records[] = array('_imaps._tcp.' . $domain, 'SRV', $autodiscover_config['imap']['server'] . ' ' . $autodiscover_config['imap']['port']); + } + if ($autodiscover_config['smtp']['tlsport'] != '587') { + $records[] = array('_submission._tcp.' . $domain, 'SRV', $autodiscover_config['smtp']['server'] . ' ' . $autodiscover_config['smtp']['tlsport']); + } + if ($autodiscover_config['smtp']['port'] != '465') { + $records[] = array('_smtps._tcp.' . $domain, 'SRV', $autodiscover_config['smtp']['server'] . ' ' . $autodiscover_config['smtp']['port']); + } + if ($autodiscover_config['sieve']['port'] != '4190') { + $records[] = array('_sieve._tcp.' . $domain, 'SRV', $autodiscover_config['sieve']['server'] . ' ' . $autodiscover_config['sieve']['port']); + } +} + +define('state_good', "✓"); +define('state_missing', "✗"); +define('state_nomatch', "?"); + +$record_types = array( + 'A' => DNS_A, + 'AAAA' => DNS_AAAA, + 'CNAME' => DNS_CNAME, + 'MX' => DNS_MX, + 'PTR' => DNS_PTR, + 'SRV' => DNS_SRV, + 'TXT' => DNS_TXT, +); +$data_field = array( + 'A' => 'ip', + 'AAAA' => 'ipv6', + 'CNAME' => 'target', + 'MX' => 'target', + 'PTR' => 'target', + 'SRV' => 'data', + 'TLSA' => 'data', + 'TXT' => 'txt', +); +?> +
+

+

+
+ + + 0 && count($cname) > 0) { + if ($a[0]['ip'] == $cname[0]['ip']) { + $currents = array(array('host' => $record[0], 'class' => 'IN', 'type' => 'CNAME', 'target' => $record[2])); + + $aaaa = dns_get_record($record[0], DNS_AAAA); + $cname = dns_get_record($record[2], DNS_AAAA); + if (count($aaaa) == 0 || count($cname) == 0 || $aaaa[0]['ipv6'] != $cname[0]['ipv6']) { + $currents[0]['target'] = $aaaa[0]['ipv6']; + } + } else { + $currents = array(array('host' => $record[0], 'class' => 'IN', 'type' => 'CNAME', 'target' => $a[0]['ip'])); + } + } + } + + foreach ($currents as $current) { + $current['type'] == strtoupper($current['type']); + if ($current['type'] != $record[1]) + { + continue; + } + + elseif ($current['type'] == 'TXT' && strpos($record[0], '_dmarc.') === 0) { + $state = state_nomatch; + if (strpos($current[$data_field[$current['type']]], $record[3]) === 0) + $state = state_good . ' (' . current[$data_field[$current['type']]] . ')'; + } + else if ($current['type'] == 'TXT' && strpos($current['txt'], 'v=spf1') === 0) { + $allowed = get_spf_allowed_hosts($record[0]); + $spf_ok = FALSE; + $spf_ok6 = FALSE; + foreach ($allowed as $net) + { + if (in_net($ip, $net)) + $spf_ok = TRUE; + if (in_net($ip6, $net)) + $spf_ok6 = TRUE; + } + if ($spf_ok && (empty($ip6) || $spf_ok6)) + $state = state_good . ' (' . $current[$data_field[$current['type']]] . ')'; + } + else if ($current['type'] != 'TXT' && isset($data_field[$current['type']]) && $state != state_good) { + $state = state_nomatch; + if ($current[$data_field[$current['type']]] == $record[2]) + $state = state_good; + } + } + + if ($state == state_nomatch) { + $state = array(); + foreach ($currents as $current) { + $state[] = $current[$data_field[$current['type']]]; + } + $state = implode('
', $state); + } + + echo sprintf('', $record[0], $record[1], $record[2], $state); +} +?> +
%s%s%s%s
+
+
+ From 4c1537c1c2d8f99a98b7db09066f65ae66c938ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Peters?= Date: Mon, 10 Jul 2017 08:45:59 +0200 Subject: [PATCH 05/26] Revert "DNS diagnostics page" --- data/web/inc/header.inc.php | 5 - data/web/lang/lang.de.php | 7 - data/web/lang/lang.en.php | 7 - diagnostics.php | 272 ------------------------------------ 4 files changed, 291 deletions(-) delete mode 100644 diagnostics.php diff --git a/data/web/inc/header.inc.php b/data/web/inc/header.inc.php index 8e5b6d07..533711de 100644 --- a/data/web/inc/header.inc.php +++ b/data/web/inc/header.inc.php @@ -77,11 +77,6 @@ > - > - > diff --git a/data/web/lang/lang.de.php b/data/web/lang/lang.de.php index 54a497ab..8706e4ba 100644 --- a/data/web/lang/lang.de.php +++ b/data/web/lang/lang.de.php @@ -215,7 +215,6 @@ $lang['header']['mailcow_settings'] = 'Konfiguration'; $lang['header']['administration'] = 'Administration'; $lang['header']['mailboxes'] = 'Mailboxen'; $lang['header']['user_settings'] = 'Benutzereinstellungen'; -$lang['header']['diagnostics'] = 'Diagnose'; $lang['header']['login'] = 'Anmeldung'; $lang['header']['logged_in_as_logout'] = 'Eingeloggt als %s (abmelden)'; $lang['header']['logged_in_as_logout_dual'] = 'Eingeloggt als %s [%s]'; @@ -497,9 +496,3 @@ $lang['admin']['add_forwarding_host'] = 'Weiterleitungs-Host hinzufügen'; $lang['delete']['remove_forwardinghost_warning'] = 'Warnung: Sie entfernen den Weiterleitungs-Host %s!'; $lang['success']['forwarding_host_removed'] = "Weiterleitungs-Host %s wurde entfernt"; $lang['success']['forwarding_host_added'] = "Weiterleitungs-Host %s wurde hinzugefügt"; -$lang['diagnostics']['dns_records'] = 'DNS-Einträge'; -$lang['diagnostics']['dns_records_24hours'] = 'Bitte beachten Sie, dass es bis zu 24 Stunden dauern kann, bis Änderungen an Ihren DNS-Einträgen als aktueller Status auf dieser Seite dargestellt werden. Diese Seite ist nur als Hilfsmittel gedacht, um die korrekten Werte für DNS-Einträge zu anzuzeigen und zu überprüfen, ob die Daten im DNS hinterlegt sind.'; -$lang['diagnostics']['dns_records_name'] = 'Name'; -$lang['diagnostics']['dns_records_type'] = 'Typ'; -$lang['diagnostics']['dns_records_data'] = 'Korrekte Daten'; -$lang['diagnostics']['dns_records_status'] = 'Aktueller Status'; diff --git a/data/web/lang/lang.en.php b/data/web/lang/lang.en.php index f0608821..fceef80f 100644 --- a/data/web/lang/lang.en.php +++ b/data/web/lang/lang.en.php @@ -217,7 +217,6 @@ $lang['header']['mailcow_settings'] = 'Configuration'; $lang['header']['administration'] = 'Administration'; $lang['header']['mailboxes'] = 'Mailboxes'; $lang['header']['user_settings'] = 'User settings'; -$lang['header']['diagnostics'] = 'Diagnostics'; $lang['header']['login'] = 'Login'; $lang['header']['logged_in_as_logout'] = 'Logged in as %s (logout)'; $lang['header']['logged_in_as_logout_dual'] = 'Logged in as %s [%s]'; @@ -510,9 +509,3 @@ $lang['admin']['add_forwarding_host'] = 'Add Forwarding Host'; $lang['delete']['remove_forwardinghost_warning'] = 'Warning: You are about to remove the forwarding host %s!'; $lang['success']['forwarding_host_removed'] = "Forwarding host %s has been removed"; $lang['success']['forwarding_host_added'] = "Forwarding host %s has been added"; -$lang['diagnostics']['dns_records'] = 'DNS Records'; -$lang['diagnostics']['dns_records_24hours'] = 'Please note that changes made to DNS may take up to 24 hours to correctly have their current state reflected on this page. It is intended as a way for you to easily see how to configure your DNS records and to check whether all your records are correctly stored in DNS.'; -$lang['diagnostics']['dns_records_name'] = 'Name'; -$lang['diagnostics']['dns_records_type'] = 'Type'; -$lang['diagnostics']['dns_records_data'] = 'Correct Data'; -$lang['diagnostics']['dns_records_status'] = 'Current State'; diff --git a/diagnostics.php b/diagnostics.php deleted file mode 100644 index 4de0082e..00000000 --- a/diagnostics.php +++ /dev/null @@ -1,272 +0,0 @@ - 1) { - $mask = $net[1]; - } - $net = inet_pton($net[0]); - $addr = inet_pton($addr); - $length = strlen($net); // 4 for IPv4, 16 for IPv6 - if (strlen($net) != strlen($addr)) { - return false; - } - if (!isset($mask)) { - $mask = $length * 8; - } - $addr_bin = ''; - $net_bin = ''; - for ($i = 0; $i < $length; ++$i) { - $addr_bin .= str_pad(decbin(ord(substr($addr, $i, $i+1))), 8, '0', STR_PAD_LEFT); - $net_bin .= str_pad(decbin(ord(substr($net, $i, $i+1))), 8, '0', STR_PAD_LEFT); - } - return substr($addr_bin, 0, $mask) == substr($net_bin, 0, $mask); -} - -if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin") { -require_once("inc/header.inc.php"); - -$ch = curl_init('http://ipv4.mailcow.email'); -curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); -curl_setopt($ch, CURLOPT_VERBOSE, false); -curl_setopt($ch, CURLOPT_HEADER, false); -curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); -curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3); -$ip = curl_exec($ch); -curl_close($ch); - -$ch = curl_init('http://ipv6.mailcow.email'); -curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V6); -curl_setopt($ch, CURLOPT_VERBOSE, false); -curl_setopt($ch, CURLOPT_HEADER, false); -curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); -curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3); -$ip6 = curl_exec($ch); -curl_close($ch); - -$ptr = implode('.', array_reverse(explode('.', $ip))) . '.in-addr.arpa'; -if (!empty($ip6)) { - $ip6_full = str_replace('::', str_repeat(':', 9-substr_count($ip6, ':')), $ip6); - $ip6_full = str_replace('::', ':0:', $ip6_full); - $ip6_full = str_replace('::', ':0:', $ip6_full); - $ptr6 = ''; - foreach (explode(':', $ip6_full) as $part) { - $ptr6 .= str_pad($part, 4, '0', STR_PAD_LEFT); - } - $ptr6 = implode('.', array_reverse(str_split($ptr6, 1))) . '.ip6.arpa'; -} - -$https_port = strpos($_SERVER['HTTP_HOST'], ':'); -if ($https_port === FALSE) { - $https_port = 443; -} else { - $https_port = substr($_SERVER['HTTP_HOST'], $https_port+1); -} - -$records = array(); -$records[] = array($mailcow_hostname, 'A', $ip); -$records[] = array($ptr, 'PTR', $mailcow_hostname); -if (!empty($ip6)) { - $records[] = array($mailcow_hostname, 'AAAA', $ip6); - $records[] = array($ptr6, 'PTR', $mailcow_hostname); -} -$domains = mailbox('get', 'domains'); -foreach(mailbox('get', 'domains') as $domain) { - $domains = array_merge($domains, mailbox('get', 'alias_domains', $domain)); -} - -if (!isset($autodiscover_config['sieve'])) { - $autodiscover_config['sieve'] = array('server' => $mailcow_hostname, 'port' => array_pop(explode(':', getenv('SIEVE_PORT')))); -} - -$records[] = array('_25._tcp.' . $autodiscover_config['smtp']['server'], 'TLSA', generate_tlsa_digest($autodiscover_config['smtp']['server'], 25, 1)); -$records[] = array('_' . $https_port . '._tcp.' . $mailcow_hostname, 'TLSA', generate_tlsa_digest($mailcow_hostname, $https_port)); -$records[] = array('_' . $autodiscover_config['pop3']['tlsport'] . '._tcp.' . $autodiscover_config['pop3']['server'], 'TLSA', generate_tlsa_digest($autodiscover_config['pop3']['server'], $autodiscover_config['pop3']['tlsport'], 1)); -$records[] = array('_' . $autodiscover_config['imap']['tlsport'] . '._tcp.' . $autodiscover_config['imap']['server'], 'TLSA', generate_tlsa_digest($autodiscover_config['imap']['server'], $autodiscover_config['imap']['tlsport'], 1)); -$records[] = array('_' . $autodiscover_config['smtp']['port'] . '._tcp.' . $autodiscover_config['smtp']['server'], 'TLSA', generate_tlsa_digest($autodiscover_config['smtp']['server'], $autodiscover_config['smtp']['port'])); -$records[] = array('_' . $autodiscover_config['smtp']['tlsport'] . '._tcp.' . $autodiscover_config['smtp']['server'], 'TLSA', generate_tlsa_digest($autodiscover_config['smtp']['server'], $autodiscover_config['smtp']['tlsport'], 1)); -$records[] = array('_' . $autodiscover_config['imap']['port'] . '._tcp.' . $autodiscover_config['imap']['server'], 'TLSA', generate_tlsa_digest($autodiscover_config['imap']['server'], $autodiscover_config['imap']['port'])); -$records[] = array('_' . $autodiscover_config['pop3']['port'] . '._tcp.' . $autodiscover_config['pop3']['server'], 'TLSA', generate_tlsa_digest($autodiscover_config['pop3']['server'], $autodiscover_config['pop3']['port'])); -$records[] = array('_' . $autodiscover_config['sieve']['port'] . '._tcp.' . $autodiscover_config['sieve']['server'], 'TLSA', generate_tlsa_digest($autodiscover_config['sieve']['server'], $autodiscover_config['sieve']['port'], 1)); - -foreach ($domains as $domain) { - $records[] = array($domain, 'MX', $mailcow_hostname); - $records[] = array('autodiscover.' . $domain, 'CNAME', $mailcow_hostname); - $records[] = array('_autodiscover._tcp.' . $domain, 'SRV', $mailcow_hostname . ' ' . $https_port); - $records[] = array('autoconfig.' . $domain, 'CNAME', $mailcow_hostname); - $records[] = array($domain, 'TXT', 'v=spf1 mx -all'); - $records[] = array('_dmarc.' . $domain, 'TXT', 'v=DMARC1; p=reject', 'v=DMARC1; p='); - - if (!empty($dkim = dkim('details', $domain))) { - $records[] = array($dkim['dkim_selector'] . '._domainkey.' . $domain, 'TXT', $dkim['dkim_txt']); - } - - $current_records = dns_get_record('_pop3._tcp.' . $domain, DNS_SRV); - if (count($current_records) == 0 || $current_records[0]['target'] != '') { - if ($autodiscover_config['pop3']['tlsport'] != '110') { - $records[] = array('_pop3._tcp.' . $domain, 'SRV', $autodiscover_config['pop3']['server'] . ' ' . $autodiscover_config['pop3']['tlsport']); - } - } else { - $records[] = array('_pop3._tcp.' . $domain, 'SRV', '. 0'); - } - $current_records = dns_get_record('_pop3s._tcp.' . $domain, DNS_SRV); - if (count($current_records) == 0 || $current_records[0]['target'] != '') { - if ($autodiscover_config['pop3']['port'] != '995') { - $records[] = array('_pop3s._tcp.' . $domain, 'SRV', $autodiscover_config['pop3']['server'] . ' ' . $autodiscover_config['pop3']['port']); - } - } else { - $records[] = array('_pop3s._tcp.' . $domain, 'SRV', '. 0'); - } - if ($autodiscover_config['imap']['tlsport'] != '143') { - $records[] = array('_imap._tcp.' . $domain, 'SRV', $autodiscover_config['imap']['server'] . ' ' . $autodiscover_config['imap']['tlsport']); - } - if ($autodiscover_config['imap']['port'] != '993') { - $records[] = array('_imaps._tcp.' . $domain, 'SRV', $autodiscover_config['imap']['server'] . ' ' . $autodiscover_config['imap']['port']); - } - if ($autodiscover_config['smtp']['tlsport'] != '587') { - $records[] = array('_submission._tcp.' . $domain, 'SRV', $autodiscover_config['smtp']['server'] . ' ' . $autodiscover_config['smtp']['tlsport']); - } - if ($autodiscover_config['smtp']['port'] != '465') { - $records[] = array('_smtps._tcp.' . $domain, 'SRV', $autodiscover_config['smtp']['server'] . ' ' . $autodiscover_config['smtp']['port']); - } - if ($autodiscover_config['sieve']['port'] != '4190') { - $records[] = array('_sieve._tcp.' . $domain, 'SRV', $autodiscover_config['sieve']['server'] . ' ' . $autodiscover_config['sieve']['port']); - } -} - -define('state_good', "✓"); -define('state_missing', "✗"); -define('state_nomatch', "?"); - -$record_types = array( - 'A' => DNS_A, - 'AAAA' => DNS_AAAA, - 'CNAME' => DNS_CNAME, - 'MX' => DNS_MX, - 'PTR' => DNS_PTR, - 'SRV' => DNS_SRV, - 'TXT' => DNS_TXT, -); -$data_field = array( - 'A' => 'ip', - 'AAAA' => 'ipv6', - 'CNAME' => 'target', - 'MX' => 'target', - 'PTR' => 'target', - 'SRV' => 'data', - 'TLSA' => 'data', - 'TXT' => 'txt', -); -?> -
-

-

-
- - - 0 && count($cname) > 0) { - if ($a[0]['ip'] == $cname[0]['ip']) { - $currents = array(array('host' => $record[0], 'class' => 'IN', 'type' => 'CNAME', 'target' => $record[2])); - - $aaaa = dns_get_record($record[0], DNS_AAAA); - $cname = dns_get_record($record[2], DNS_AAAA); - if (count($aaaa) == 0 || count($cname) == 0 || $aaaa[0]['ipv6'] != $cname[0]['ipv6']) { - $currents[0]['target'] = $aaaa[0]['ipv6']; - } - } else { - $currents = array(array('host' => $record[0], 'class' => 'IN', 'type' => 'CNAME', 'target' => $a[0]['ip'])); - } - } - } - - foreach ($currents as $current) { - $current['type'] == strtoupper($current['type']); - if ($current['type'] != $record[1]) - { - continue; - } - - elseif ($current['type'] == 'TXT' && strpos($record[0], '_dmarc.') === 0) { - $state = state_nomatch; - if (strpos($current[$data_field[$current['type']]], $record[3]) === 0) - $state = state_good . ' (' . current[$data_field[$current['type']]] . ')'; - } - else if ($current['type'] == 'TXT' && strpos($current['txt'], 'v=spf1') === 0) { - $allowed = get_spf_allowed_hosts($record[0]); - $spf_ok = FALSE; - $spf_ok6 = FALSE; - foreach ($allowed as $net) - { - if (in_net($ip, $net)) - $spf_ok = TRUE; - if (in_net($ip6, $net)) - $spf_ok6 = TRUE; - } - if ($spf_ok && (empty($ip6) || $spf_ok6)) - $state = state_good . ' (' . $current[$data_field[$current['type']]] . ')'; - } - else if ($current['type'] != 'TXT' && isset($data_field[$current['type']]) && $state != state_good) { - $state = state_nomatch; - if ($current[$data_field[$current['type']]] == $record[2]) - $state = state_good; - } - } - - if ($state == state_nomatch) { - $state = array(); - foreach ($currents as $current) { - $state[] = $current[$data_field[$current['type']]]; - } - $state = implode('
', $state); - } - - echo sprintf('', $record[0], $record[1], $record[2], $state); -} -?> -
%s%s%s%s
-
-
- From beec3d47f70f3403f855a8b2a732bbeed0ed007f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Peters?= Date: Mon, 10 Jul 2017 08:46:20 +0200 Subject: [PATCH 06/26] Revert "Client configuration guides" --- data/web/autoconfig.php | 47 +++------ data/web/autodiscover.php | 54 +++++----- data/web/clientconfig.php | 185 --------------------------------- data/web/inc/functions.inc.php | 24 ++--- data/web/inc/vars.inc.php | 31 ++---- data/web/lang/lang.de.php | 1 - data/web/lang/lang.en.php | 1 - data/web/mobileconfig.php | 167 ----------------------------- data/web/user.php | 1 - 9 files changed, 56 insertions(+), 455 deletions(-) delete mode 100644 data/web/clientconfig.php delete mode 100644 data/web/mobileconfig.php diff --git a/data/web/autoconfig.php b/data/web/autoconfig.php index 523d6183..d01bc724 100644 --- a/data/web/autoconfig.php +++ b/data/web/autoconfig.php @@ -5,16 +5,6 @@ if (empty($mailcow_hostname)) { exit(); } -$domain_dot = strpos($_SERVER['HTTP_HOST'], '.'); -$domain_port = strpos($_SERVER['HTTP_HOST'], ':'); -if ($domain_port === FALSE) { - $domain = substr($_SERVER['HTTP_HOST'], $domain_dot+1); - $port = 443; -} else { - $domain = substr($_SERVER['HTTP_HOST'], $domain_dot+1, $domain_port-$domain_dot-1); - $port = substr($_SERVER['HTTP_HOST'], $domain_port+1); -} - header('Content-Type: application/xml'); ?> '; ?> @@ -25,59 +15,52 @@ header('Content-Type: application/xml'); mail server - - + + 993 SSL %EMAILADDRESS% password-cleartext - - + + 143 STARTTLS %EMAILADDRESS% password-cleartext - - - + + 995 SSL %EMAILADDRESS% password-cleartext - - - - + + 110 STARTTLS %EMAILADDRESS% password-cleartext - - - + + 465 SSL %EMAILADDRESS% password-cleartext + - - + + 587 STARTTLS %EMAILADDRESS% password-cleartext - + If you didn't change the password given to you by the administrator or if you didn't change it in a long time, please consider doing that now. Sollten Sie das Ihnen durch den Administrator vergebene Passwort noch nicht geändert haben, empfehlen wir dies nun zu tun. Auch ein altes Passwort sollte aus Sicherheitsgründen geändert werden. @@ -85,6 +68,6 @@ if (count($records) == 0 || $records[0]['target'] != '') { ?> - + diff --git a/data/web/autodiscover.php b/data/web/autodiscover.php index 7f6081ae..fd8cd641 100644 --- a/data/web/autodiscover.php +++ b/data/web/autodiscover.php @@ -12,14 +12,13 @@ error_reporting(0); $data = trim(file_get_contents("php://input")); // Desktop client needs IMAP, unless it's Outlook 2013 or higher on Windows -if (strpos($data, 'autodiscover/outlook/responseschema') !== false) { // desktop client +if (strpos($data, 'autodiscover/outlook/responseschema')) { // desktop client $configuration['autodiscoverType'] = 'imap'; if ($configuration['useEASforOutlook'] == 'yes' && - // Office for macOS does not support EAS - strpos($_SERVER['HTTP_USER_AGENT'], 'Mac') === false && - // Outlook 2013 (version 15) or higher - preg_match('/(Outlook|Office).+1[5-9]\./', $_SERVER['HTTP_USER_AGENT']) - ) { + // Office for macOS does not support EAS + strpos($_SERVER['HTTP_USER_AGENT'], 'Mac') === false && + // Outlook 2013 (version 15) or higher + preg_match('/(Outlook|Office).+1[5-9]\./', $_SERVER['HTTP_USER_AGENT'])) { $configuration['autodiscoverType'] = 'activesync'; } } @@ -61,28 +60,8 @@ else { Request->EMailAddress; - } catch (Exception $e) { - $email = $_SERVER['PHP_AUTH_USER']; - } - - $username = trim($email); - try { - $stmt = $pdo->prepare("SELECT `name` FROM `mailbox` WHERE `username`= :username"); - $stmt->execute(array(':username' => $username)); - $MailboxData = $stmt->fetch(PDO::FETCH_ASSOC); - } - catch(PDOException $e) { - die("Failed to determine name from SQL"); - } - if (!empty($MailboxData['name'])) { - $displayname = utf8_encode($MailboxData['name']); - } - else { - $displayname = $email; - } + $discover = new SimpleXMLElement($data); + $email = $discover->Request->EMailAddress; if ($configuration['autodiscoverType'] == 'imap') { ?> @@ -117,13 +96,13 @@ else { CalDAV - https:///SOGo/dav//Calendar + /SOGo/dav//Calendar off CardDAV - https:///SOGo/dav//Contacts + /SOGo/dav//Contacts off @@ -132,6 +111,21 @@ else { prepare("SELECT `name` FROM `mailbox` WHERE `username`= :username"); + $stmt->execute(array(':username' => $username)); + $MailboxData = $stmt->fetch(PDO::FETCH_ASSOC); + } + catch(PDOException $e) { + die("Failed to determine name from SQL"); + } + if (!empty($MailboxData['name'])) { + $displayname = utf8_encode($MailboxData['name']); + } + else { + $displayname = $email; + } ?> en:en diff --git a/data/web/clientconfig.php b/data/web/clientconfig.php deleted file mode 100644 index 68722a89..00000000 --- a/data/web/clientconfig.php +++ /dev/null @@ -1,185 +0,0 @@ -prepare("SELECT `name` FROM `mailbox` WHERE `username`= :username"); - $stmt->execute(array(':username' => $username)); - $MailboxData = $stmt->fetch(PDO::FETCH_ASSOC); -} -catch(PDOException $e) { - die("Failed to determine name from SQL"); -} -if (!empty($MailboxData['name'])) { - $displayname = utf8_encode($MailboxData['name']); -} -else { - $displayname = $email; -} -?> -
-

Client Configuration Guide

-

Select your email client or operating system below to view a step-by-step guide to setting up your email account.

- -

Apple macOS / iOS

- - -

Android

- - -

eM Client

- - -

KDE Kontact

- - -

Microsoft Outlook

- - -

Mozilla Thunderbird

- - -

Windows

- - -

Windows Phone

- -
- - diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index af93794b..b3420530 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -62,17 +62,17 @@ function hasMailboxObjectAccess($username, $role, $object) { } return false; } -function pem_to_der($pem_key) { - // Need to remove BEGIN/END PUBLIC KEY - $lines = explode("\n", trim($pem_key)); - unset($lines[count($lines)-1]); - unset($lines[0]); - return base64_decode(implode('', $lines)); -} function generate_tlsa_digest($hostname, $port, $starttls = null) { if (!is_valid_domain_name($hostname)) { return "Not a valid hostname"; } + function pem_to_der($pem_key) { + // Need to remove BEGIN/END PUBLIC KEY + $lines = explode("\n", trim($pem_key)); + unset($lines[count($lines)-1]); + unset($lines[0]); + return base64_decode(implode('', $lines)); + } if (empty($starttls)) { $context = stream_context_create(array("ssl" => array("capture_peer_cert" => true, 'verify_peer' => false, 'allow_self_signed' => true))); @@ -88,24 +88,20 @@ function generate_tlsa_digest($hostname, $port, $starttls = null) { return $error_nr . ': ' . $error_msg; } $banner = fread($stream, 512 ); - if (preg_match("/^220/i", $banner)) { // SMTP + if (preg_match("/^220/i", $banner)) { fwrite($stream,"HELO tlsa.generator.local\r\n"); fread($stream, 512); fwrite($stream,"STARTTLS\r\n"); fread($stream, 512); } - elseif (preg_match("/imap.+starttls/i", $banner)) { // IMAP + elseif (preg_match("/imap.+starttls/i", $banner)) { fwrite($stream,"A1 STARTTLS\r\n"); fread($stream, 512); } - elseif (preg_match("/^\+OK/", $banner)) { // POP3 + elseif (preg_match("/^\+OK/", $banner)) { fwrite($stream,"STLS\r\n"); fread($stream, 512); } - elseif (preg_match("/^OK/m", $banner)) { // Sieve - fwrite($stream,"STARTTLS\r\n"); - fread($stream, 512); - } else { return 'Unknown banner: "' . htmlspecialchars(trim($banner)) . '"'; } diff --git a/data/web/inc/vars.inc.php b/data/web/inc/vars.inc.php index 775af1e1..a52442db 100644 --- a/data/web/inc/vars.inc.php +++ b/data/web/inc/vars.inc.php @@ -18,48 +18,31 @@ $database_name = getenv('DBNAME'); $mailcow_hostname = getenv('MAILCOW_HOSTNAME'); // Autodiscover settings -$https_port = strpos($_SERVER['HTTP_HOST'], ':'); -if ($https_port === FALSE) { - $https_port = 443; -} else { - $https_port = substr($_SERVER['HTTP_HOST'], $https_port+1); -} $autodiscover_config = array( // Enable the autodiscover service for Outlook desktop clients 'useEASforOutlook' => 'yes', // General autodiscover service type: "activesync" or "imap" 'autodiscoverType' => 'activesync', - // Please don't use STARTTLS-enabled service ports in the "port" variable. + // Please don't use STARTTLS-enabled service ports here. // The autodiscover service will always point to SMTPS and IMAPS (TLS-wrapped services). - // The autoconfig service will additionally announce the STARTTLS-enabled ports, specified in the "tlsport" variable. 'imap' => array( 'server' => $mailcow_hostname, - 'port' => array_pop(explode(':', getenv('IMAPS_PORT'))), - 'tlsport' => array_pop(explode(':', getenv('IMAP_PORT'))), - ), - 'pop3' => array( - 'server' => $mailcow_hostname, - 'port' => array_pop(explode(':', getenv('POPS_PORT'))), - 'tlsport' => array_pop(explode(':', getenv('POP_PORT'))), + 'port' => getenv('IMAPS_PORT'), ), 'smtp' => array( 'server' => $mailcow_hostname, - 'port' => array_pop(explode(':', getenv('SMTPS_PORT'))), - 'tlsport' => array_pop(explode(':', getenv('SUBMISSION_PORT'))), + 'port' => getenv('SMTPS_PORT'), ), 'activesync' => array( - 'url' => 'https://'.$mailcow_hostname.($https_port == 443 ? '' : ':'.$https_port).'/Microsoft-Server-ActiveSync', + 'url' => 'https://'.$mailcow_hostname.'/Microsoft-Server-ActiveSync' ), 'caldav' => array( - 'server' => $mailcow_hostname, - 'port' => $https_port, + 'url' => 'https://'.$mailcow_hostname ), 'carddav' => array( - 'server' => $mailcow_hostname, - 'port' => $https_port, - ), + 'url' => 'https://'.$mailcow_hostname + ) ); -unset($https_port); // Where to go after adding and editing objects // Can be "form" or "previous" diff --git a/data/web/lang/lang.de.php b/data/web/lang/lang.de.php index 54a497ab..732171cc 100644 --- a/data/web/lang/lang.de.php +++ b/data/web/lang/lang.de.php @@ -108,7 +108,6 @@ $lang['user']['user_settings'] = 'Benutzereinstellungen'; $lang['user']['mailbox_settings'] = 'Mailbox-Einstellungen'; $lang['user']['mailbox_details'] = 'Mailbox-Details'; $lang['user']['change_password'] = 'Passwort ändern'; -$lang['user']['client_configuration'] = 'Konfigurationsanleitungen für E-Mail-Programme und Smartphones anzeigen'; $lang['user']['new_password'] = 'Neues Passwort'; $lang['user']['save_changes'] = 'Änderungen speichern'; $lang['user']['password_now'] = 'Aktuelles Passwort (Änderungen bestätigen)'; diff --git a/data/web/lang/lang.en.php b/data/web/lang/lang.en.php index f0608821..391c7d01 100644 --- a/data/web/lang/lang.en.php +++ b/data/web/lang/lang.en.php @@ -110,7 +110,6 @@ $lang['user']['user_settings'] = 'User settings'; $lang['user']['mailbox_settings'] = 'Mailbox settings'; $lang['user']['mailbox_details'] = 'Mailbox details'; $lang['user']['change_password'] = 'Change password'; -$lang['user']['client_configuration'] = 'Show configuration guides for email clients and smartphones'; $lang['user']['new_password'] = 'New password'; $lang['user']['save_changes'] = 'Save changes'; $lang['user']['password_now'] = 'Current password (confirm changes)'; diff --git a/data/web/mobileconfig.php b/data/web/mobileconfig.php deleted file mode 100644 index 198fa4d7..00000000 --- a/data/web/mobileconfig.php +++ /dev/null @@ -1,167 +0,0 @@ -prepare("SELECT `name` FROM `mailbox` WHERE `username`= :username"); - $stmt->execute(array(':username' => $email)); - $MailboxData = $stmt->fetch(PDO::FETCH_ASSOC); -} -catch(PDOException $e) { - die("Failed to determine name from SQL"); -} -if (!empty($MailboxData['name'])) { - $displayname = utf8_encode($MailboxData['name']); -} -else { - $displayname = $email; -} - -echo '' . "\n"; -?> - - - - PayloadContent - - - CalDAVAccountDescription - - CalDAVHostName - - CalDAVPort - - CalDAVPrincipalURL - /SOGo/dav/ - CalDAVUseSSL - - CalDAVUsername - - PayloadDescription - Configures CalDAV account. - PayloadDisplayName - CalDAV () - PayloadIdentifier - .CalDAV - PayloadOrganization - - PayloadType - com.apple.caldav.account - PayloadUUID - FC898573-EBA8-48AF-93BD-BFA0C9778FA7 - PayloadVersion - 1 - - - EmailAccountDescription - - EmailAccountType - EmailTypeIMAP - EmailAccountName - - EmailAddress - - IncomingMailServerAuthentication - EmailAuthPassword - IncomingMailServerHostName - - IncomingMailServerPortNumber - - IncomingMailServerUseSSL - - IncomingMailServerUsername - - OutgoingMailServerAuthentication - EmailAuthPassword - OutgoingMailServerHostName - - OutgoingMailServerPortNumber - - OutgoingMailServerUseSSL - - OutgoingMailServerUsername - - OutgoingPasswordSameAsIncomingPassword - - PayloadDescription - Configures email account. - PayloadDisplayName - IMAP Account () - PayloadIdentifier - .email - PayloadOrganization - - PayloadType - com.apple.mail.managed - PayloadUUID - 00294FBB-1016-413E-87B9-652D856D6875 - PayloadVersion - 1 - PreventAppSheet - - PreventMove - - SMIMEEnabled - - - - CardDAVAccountDescription - - CardDAVHostName - - CardDAVPort - - CardDAVPrincipalURL - /SOGo/dav/ - CardDAVUseSSL - - CardDAVUsername - - PayloadDescription - Configures CardDAV accounts - PayloadDisplayName - CardDAV () - PayloadIdentifier - .carddav - PayloadOrganization - - PayloadType - com.apple.carddav.account - PayloadUUID - 0797EF2B-B1F1-4BC7-ABCD-4580862252B4 - PayloadVersion - 1 - - - PayloadDescription - IMAP, CalDAV, CardDAV - PayloadDisplayName - Mailcow - PayloadIdentifier - - PayloadOrganization - - PayloadRemovalDisallowed - - PayloadType - Configuration - PayloadUUID - 5EE248C5-ACCB-42D8-9199-8F8ED08D5624 - PayloadVersion - 1 - - diff --git a/data/web/user.php b/data/web/user.php index ba7678e2..77ccc62d 100644 --- a/data/web/user.php +++ b/data/web/user.php @@ -74,7 +74,6 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '

[]

-

[]


From c5d90b821ae2c82507bf5e8973e7ae7a6d256cb1 Mon Sep 17 00:00:00 2001 From: andryyy Date: Mon, 10 Jul 2017 09:19:12 +0200 Subject: [PATCH 07/26] [Dovecot] Add extra.conf include to override Dovecot configuration changes --- .gitignore | 1 + data/conf/dovecot/dovecot.conf | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 9ea8b05b..b8558fdb 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ data/conf/rspamd/override.d/* !data/conf/nginx/dynmaps.conf !data/conf/nginx/site.conf data/conf/nginx/*.conf +data/conf/dovecot/extra.conf diff --git a/data/conf/dovecot/dovecot.conf b/data/conf/dovecot/dovecot.conf index 2fc271e8..056f31de 100644 --- a/data/conf/dovecot/dovecot.conf +++ b/data/conf/dovecot/dovecot.conf @@ -1,3 +1,6 @@ +# -------------------------------------------------------------------------- +# Please create a file "extra.conf" for persistent overrides to dovecot.conf +# -------------------------------------------------------------------------- auth_mechanisms = plain login #mail_debug = yes log_path = syslog @@ -256,3 +259,4 @@ service imap-postlogin { unix_listener imap-postlogin { } } +!include_try /usr/local/etc/dovecot/extra.conf From e1fc551bd2eabe075c29250a7be0d17be57f4a35 Mon Sep 17 00:00:00 2001 From: andryyy Date: Mon, 10 Jul 2017 21:27:49 +0200 Subject: [PATCH 08/26] [update.sh] Fix compose project name variable, add DOVEADM_PORT --- update.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/update.sh b/update.sh index 9da208b4..80aae031 100755 --- a/update.sh +++ b/update.sh @@ -7,7 +7,7 @@ if [[ -z $(which git) ]]; then echo "Cannot find git, exiting."; exit 1; fi if [[ -z $(which awk) ]]; then echo "Cannot find awk, exiting."; exit 1; fi if [[ -z $(which sha1sum) ]]; then echo "Cannot find sha1sum, exiting."; exit 1; fi -CONFIG_ARRAY=("SKIP_LETS_ENCRYPT" "SKIP_CLAMD" "SKIP_IP_CHECK" "SKIP_FAIL2BAN" "ADDITIONAL_SAN") +CONFIG_ARRAY=("SKIP_LETS_ENCRYPT" "SKIP_CLAMD" "SKIP_IP_CHECK" "SKIP_FAIL2BAN" "ADDITIONAL_SAN", "DOVEADM_PORT") echo >> mailcow.conf for option in ${CONFIG_ARRAY[@]}; do if [[ ${option} == "ADDITIONAL_SAN" ]]; then @@ -18,7 +18,12 @@ for option in ${CONFIG_ARRAY[@]}; do elif [[ ${option} == "COMPOSE_PROJECT_NAME" ]]; then if ! grep -q ${option} mailcow.conf; then echo "Adding new option \"${option}\" to mailcow.conf" - echo "${COMPOSE_PROJECT_NAME}=mailcow-dockerized" >> mailcow.conf + echo "COMPOSE_PROJECT_NAME=mailcow-dockerized" >> mailcow.conf + fi + elif [[ ${option} == "DOVEADM_PORT" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo "DOVEADM_PORT=127.0.0.1:19991" >> mailcow.conf fi elif ! grep -q ${option} mailcow.conf; then echo "Adding new option \"${option}\" to mailcow.conf" From 12e9620a070a9660d4d66995dcdd93a70ec36039 Mon Sep 17 00:00:00 2001 From: andryyy Date: Mon, 10 Jul 2017 21:28:24 +0200 Subject: [PATCH 09/26] [Compose] Add DOVEADM_PORT with default binding 127.0.0.1:19991 --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index d5337721..359b9d4a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -191,6 +191,7 @@ services: options: max-size: "5m" ports: + - "${DOVEADM_PORT:-127.0.0.1:19991}:12345" - "${IMAP_PORT:-143}:143" - "${IMAPS_PORT:-993}:993" - "${POP_PORT:-110}:110" From 54eea38b76d129f19ff586c3b86eb753a3fa15b6 Mon Sep 17 00:00:00 2001 From: andryyy Date: Mon, 10 Jul 2017 21:28:44 +0200 Subject: [PATCH 10/26] Fix README.md bitcoin address --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6befab5f..42b2a3f1 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=JWBSYHF4SMC68) -[![Donate (Bitcoin)](https://img.shields.io/badge/Donate-Bitcoin-blue.svg)](bitcoin:1E5rgzgA1sS3QH7r1ToWxRC3GEavfsGMrx) +**mailcow Bitcoin donations:** 1E5rgzgA1sS3QH7r1ToWxRC3GEavfsGMrx Please see [the official documentation](https://mailcow.github.io/mailcow-dockerized-docs/) for instructions. From c80d6056320c6763b5a51848105eafaa4f98a5d7 Mon Sep 17 00:00:00 2001 From: andryyy Date: Mon, 10 Jul 2017 21:29:03 +0200 Subject: [PATCH 11/26] Add DOVEADM_PORT to generate_config.sh --- generate_config.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/generate_config.sh b/generate_config.sh index 62a5ecaf..4ef1515f 100755 --- a/generate_config.sh +++ b/generate_config.sh @@ -68,6 +68,7 @@ IMAPS_PORT=993 POP_PORT=110 POPS_PORT=995 SIEVE_PORT=4190 +DOVEADM_PORT=127.0.0.1:19991 # Your timezone TZ=${TZ} From 08b99c8d74f9e0396c31ed4fe88975740fce6991 Mon Sep 17 00:00:00 2001 From: andryyy Date: Mon, 10 Jul 2017 21:30:45 +0200 Subject: [PATCH 12/26] [Dovecot] Add doveadm service --- data/conf/dovecot/dovecot.conf | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/data/conf/dovecot/dovecot.conf b/data/conf/dovecot/dovecot.conf index 056f31de..2d416d21 100644 --- a/data/conf/dovecot/dovecot.conf +++ b/data/conf/dovecot/dovecot.conf @@ -34,6 +34,12 @@ passdb { args = /usr/local/etc/dovecot/sql/dovecot-mysql.conf driver = sql } +# Set doveadm_password=your-secret-password in data/conf/dovecot/extra.conf (create if missing) +service doveadm { + inet_listener { + port = 12345 + } +} namespace inbox { inbox = yes location = From 99cd459655f1dfdd5cd619b0ee84e85cd72ac167 Mon Sep 17 00:00:00 2001 From: andryyy Date: Tue, 11 Jul 2017 17:06:36 +0200 Subject: [PATCH 13/26] [Compose] Remove logging restrictions to make it easier to implement other drivers; Push new Postfix, SOGo and Dovecot images --- docker-compose.yml | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 359b9d4a..68ba9d2b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -96,9 +96,6 @@ services: - dkim-vol-1:/data/dkim - rspamd-vol-1:/var/lib/rspamd restart: always - logging: - options: - max-size: "5m" dns: - 172.22.1.254 dns_search: mailcow-network @@ -133,9 +130,6 @@ services: - SMTPS_PORT=${SMTPS_PORT:-465} - SMTP_PORT=${SMTP_PORT:-25} restart: always - logging: - options: - max-size: "5m" dns: - 172.22.1.254 dns_search: mailcow-network @@ -145,7 +139,7 @@ services: - phpfpm sogo-mailcow: - image: mailcow/sogo:1.2 + image: mailcow/sogo:1.3 build: ./data/Dockerfiles/sogo depends_on: unbound-mailcow: @@ -159,9 +153,6 @@ services: volumes: - ./data/conf/sogo/:/etc/sogo/ restart: always - logging: - options: - max-size: "5m" dns: - 172.22.1.254 dns_search: mailcow-network @@ -172,7 +163,7 @@ services: - sogo dovecot-mailcow: - image: mailcow/dovecot:1.3 + image: mailcow/dovecot:1.4 build: ./data/Dockerfiles/dovecot depends_on: unbound-mailcow: @@ -187,9 +178,6 @@ services: - DBNAME=${DBNAME} - DBUSER=${DBUSER} - DBPASS=${DBPASS} - logging: - options: - max-size: "5m" ports: - "${DOVEADM_PORT:-127.0.0.1:19991}:12345" - "${IMAP_PORT:-143}:143" @@ -208,7 +196,7 @@ services: - dovecot postfix-mailcow: - image: mailcow/postfix:1.1 + image: mailcow/postfix:1.2 build: ./data/Dockerfiles/postfix depends_on: unbound-mailcow: @@ -227,9 +215,6 @@ services: - "${SMTPS_PORT:-465}:465" - "${SUBMISSION_PORT:-587}:587" restart: always - logging: - options: - max-size: "5m" dns: - 172.22.1.254 dns_search: mailcow-network @@ -276,9 +261,6 @@ services: - ./data/conf/rspamd/dynmaps:/dynmaps:ro - ./data/assets/ssl/:/etc/ssl/mail/:ro - ./data/conf/nginx/:/etc/nginx/conf.d/:rw - logging: - options: - max-size: "5m" ports: - "${HTTPS_BIND:-0.0.0.0}:${HTTPS_PORT:-443}:${HTTPS_PORT:-443}" - "${HTTP_BIND:-0.0.0.0}:${HTTP_PORT:-80}:${HTTP_PORT:-80}" From a31819fd6c73efc2684eb6c6f725803d39640c72 Mon Sep 17 00:00:00 2001 From: andryyy Date: Tue, 11 Jul 2017 17:08:06 +0200 Subject: [PATCH 14/26] [SOGo] Log to a pipe to not keep logs in a container --- data/Dockerfiles/sogo/supervisord.conf | 20 ++++++++++++-------- data/Dockerfiles/sogo/syslog-ng.conf | 18 ++++++++---------- data/conf/sogo/sogo.conf | 1 + 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/data/Dockerfiles/sogo/supervisord.conf b/data/Dockerfiles/sogo/supervisord.conf index b76173fe..30392e3b 100644 --- a/data/Dockerfiles/sogo/supervisord.conf +++ b/data/Dockerfiles/sogo/supervisord.conf @@ -3,9 +3,11 @@ nodaemon=true [program:syslog-ng] command=/usr/sbin/syslog-ng --foreground --no-caps -redirect_stderr=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 autostart=true -stdout_syslog=true priority=1 [program:cron] @@ -22,22 +24,24 @@ priority=4 [program:reconf-domains] command=/reconf-domains.sh +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 priority=3 autorestart=true [program:sogo] command="/usr/sbin/sogod" user=sogo +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 autorestart = unexpected autostart = false priority=5 -[program:sogo-syslog] -command=/usr/bin/tail -f /var/log/combined.log -stdout_logfile=/dev/fd/1 -stdout_logfile_maxbytes=0 -priority=6 - [inet_http_server] port=9191 diff --git a/data/Dockerfiles/sogo/syslog-ng.conf b/data/Dockerfiles/sogo/syslog-ng.conf index 4d17d9b1..264bca44 100644 --- a/data/Dockerfiles/sogo/syslog-ng.conf +++ b/data/Dockerfiles/sogo/syslog-ng.conf @@ -1,4 +1,4 @@ -@version: 3.5 +@version: 3.8 @include "scl.conf" options { chain_hostnames(off); @@ -14,12 +14,10 @@ source s_src { internal(); }; source s_sogo { - file("/var/log/sogo/sogo.log"); + pipe("/dev/sogo_log" owner(sogo) group(sogo)); }; -destination d_combined { - file("/var/log/combined.log"); -}; -destination d_redis_persistent_log { +destination d_stdout { pipe("/dev/stdout"); }; +destination d_redis_ui_log { redis( host("redis-mailcow") persist-name("redis1") @@ -37,11 +35,11 @@ destination d_redis_f2b_channel { }; log { source(s_sogo); - source(s_src); - destination(d_combined); + destination(d_redis_ui_log); + destination(d_redis_f2b_channel); }; log { source(s_sogo); - destination(d_redis_persistent_log); - destination(d_redis_f2b_channel); + source(s_src); + destination(d_stdout); }; diff --git a/data/conf/sogo/sogo.conf b/data/conf/sogo/sogo.conf index 32dc1e5e..151f5aa3 100644 --- a/data/conf/sogo/sogo.conf +++ b/data/conf/sogo/sogo.conf @@ -78,4 +78,5 @@ //MySQL4DebugEnabled = YES; //SOGoUIxDebugEnabled = YES; //WODontZipResponse = YES; + WOLogFile = "/dev/sogo_log"; } From f8ae5158cba45bf42f34832f2e8793486d540326 Mon Sep 17 00:00:00 2001 From: andryyy Date: Tue, 11 Jul 2017 17:09:20 +0200 Subject: [PATCH 15/26] [Postfix] Do not keep persistent logs in a container --- data/Dockerfiles/postfix/Dockerfile | 5 ++--- data/Dockerfiles/postfix/supervisord.conf | 11 ++++------- data/Dockerfiles/postfix/syslog-ng.conf | 9 ++++----- data/Dockerfiles/postfix/zeyple.conf | 2 +- 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/data/Dockerfiles/postfix/Dockerfile b/data/Dockerfiles/postfix/Dockerfile index 283adbe6..2e0e73df 100644 --- a/data/Dockerfiles/postfix/Dockerfile +++ b/data/Dockerfiles/postfix/Dockerfile @@ -25,14 +25,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ syslog-ng \ syslog-ng-core \ syslog-ng-mod-redis \ - && rm -rf /var/lib/apt/lists/* + && rm -rf /var/lib/apt/lists/* \ + && touch /etc/default/locale RUN addgroup --system --gid 600 zeyple RUN adduser --system --home /var/lib/zeyple --no-create-home --uid 600 --gid 600 --disabled-login zeyple RUN touch /var/log/zeyple.log && chown zeyple: /var/log/zeyple.log -RUN touch /etc/default/locale - COPY zeyple.py /usr/local/bin/zeyple.py COPY zeyple.conf /etc/zeyple.conf COPY supervisord.conf /etc/supervisor/supervisord.conf diff --git a/data/Dockerfiles/postfix/supervisord.conf b/data/Dockerfiles/postfix/supervisord.conf index 0968bb0a..55e76a95 100644 --- a/data/Dockerfiles/postfix/supervisord.conf +++ b/data/Dockerfiles/postfix/supervisord.conf @@ -3,19 +3,16 @@ nodaemon=true [program:syslog-ng] command=/usr/sbin/syslog-ng --foreground --no-caps -redirect_stderr=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 autostart=true -stdout_syslog=true [program:postfix] command=/opt/postfix.sh autorestart=true -[program:postfix-maillog] -command=/bin/tail -f /var/log/zeyple.log /var/log/combined.log -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 - [unix_http_server] file=/var/tmp/supervisord.sock chmod=0770 diff --git a/data/Dockerfiles/postfix/syslog-ng.conf b/data/Dockerfiles/postfix/syslog-ng.conf index c4bb63ba..808b06b7 100644 --- a/data/Dockerfiles/postfix/syslog-ng.conf +++ b/data/Dockerfiles/postfix/syslog-ng.conf @@ -13,9 +13,8 @@ source s_src { unix-stream("/dev/log"); internal(); }; - -destination d_combined { file("/var/log/combined.log"); }; -destination d_redis_persistent_log { +destination d_stdout { pipe("/dev/stdout"); }; +destination d_redis_ui_log { redis( host("redis-mailcow") persist-name("redis1") @@ -34,8 +33,8 @@ destination d_redis_f2b_channel { filter f_mail { facility(mail); }; log { source(s_src); - destination(d_combined); + destination(d_stdout); filter(f_mail); - destination(d_redis_persistent_log); + destination(d_redis_ui_log); destination(d_redis_f2b_channel); }; diff --git a/data/Dockerfiles/postfix/zeyple.conf b/data/Dockerfiles/postfix/zeyple.conf index 7f039582..cc176a0e 100644 --- a/data/Dockerfiles/postfix/zeyple.conf +++ b/data/Dockerfiles/postfix/zeyple.conf @@ -1,5 +1,5 @@ [zeyple] -log_file = /var/log/zeyple.log +log_file = /dev/null [gpg] home = /var/lib/zeyple/keys From 9e92c4a2ade3070445539de957212397dbb9fcf9 Mon Sep 17 00:00:00 2001 From: andryyy Date: Tue, 11 Jul 2017 17:09:31 +0200 Subject: [PATCH 16/26] [Dovecot] Do not keep persistent logs in a container --- data/Dockerfiles/dovecot/supervisord.conf | 11 ++++------- data/Dockerfiles/dovecot/syslog-ng.conf | 9 ++++----- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/data/Dockerfiles/dovecot/supervisord.conf b/data/Dockerfiles/dovecot/supervisord.conf index e2e02250..33d488b8 100644 --- a/data/Dockerfiles/dovecot/supervisord.conf +++ b/data/Dockerfiles/dovecot/supervisord.conf @@ -3,19 +3,16 @@ nodaemon=true [program:syslog-ng] command=/usr/sbin/syslog-ng --foreground --no-caps -redirect_stderr=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 autostart=true -stdout_syslog=true [program:dovecot] command=/usr/local/sbin/dovecot -F autorestart=true -[program:logfiles] -command=/usr/bin/tail -f /var/log/combined.log -stdout_logfile=/dev/fd/1 -stdout_logfile_maxbytes=0 - [program:cron] command=/usr/sbin/cron -f autorestart=true diff --git a/data/Dockerfiles/dovecot/syslog-ng.conf b/data/Dockerfiles/dovecot/syslog-ng.conf index b8cc44fd..0257d6a1 100644 --- a/data/Dockerfiles/dovecot/syslog-ng.conf +++ b/data/Dockerfiles/dovecot/syslog-ng.conf @@ -13,9 +13,8 @@ source s_src { unix-stream("/dev/log"); internal(); }; - -destination d_combined { file("/var/log/combined.log"); }; -destination d_redis_persistent_log { +destination d_stdout { pipe("/dev/stdout"); }; +destination d_redis_ui_log { redis( host("redis-mailcow") persist-name("redis1") @@ -34,8 +33,8 @@ destination d_redis_f2b_channel { filter f_mail { facility(mail); }; log { source(s_src); - destination(d_combined); + destination(d_stdout); filter(f_mail); - destination(d_redis_persistent_log); + destination(d_redis_ui_log); destination(d_redis_f2b_channel); }; From 52231ac38d63f610829b6999f99793aa83281051 Mon Sep 17 00:00:00 2001 From: andryyy Date: Tue, 11 Jul 2017 21:07:00 +0200 Subject: [PATCH 17/26] [update.sh] Remove , --- update.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/update.sh b/update.sh index 80aae031..998dc32b 100755 --- a/update.sh +++ b/update.sh @@ -7,7 +7,7 @@ if [[ -z $(which git) ]]; then echo "Cannot find git, exiting."; exit 1; fi if [[ -z $(which awk) ]]; then echo "Cannot find awk, exiting."; exit 1; fi if [[ -z $(which sha1sum) ]]; then echo "Cannot find sha1sum, exiting."; exit 1; fi -CONFIG_ARRAY=("SKIP_LETS_ENCRYPT" "SKIP_CLAMD" "SKIP_IP_CHECK" "SKIP_FAIL2BAN" "ADDITIONAL_SAN", "DOVEADM_PORT") +CONFIG_ARRAY=("SKIP_LETS_ENCRYPT" "SKIP_CLAMD" "SKIP_IP_CHECK" "SKIP_FAIL2BAN" "ADDITIONAL_SAN" "DOVEADM_PORT") echo >> mailcow.conf for option in ${CONFIG_ARRAY[@]}; do if [[ ${option} == "ADDITIONAL_SAN" ]]; then From 48318a499924365e2defba9cc005ccc3e8514280 Mon Sep 17 00:00:00 2001 From: andryyy Date: Tue, 11 Jul 2017 22:51:48 +0200 Subject: [PATCH 18/26] [Compose] nslookup against google.com --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 68ba9d2b..e29285ec 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: mysql-mailcow: condition: service_healthy healthcheck: - test: ["CMD", "nslookup", "mailcow.email", "127.0.0.1"] + test: ["CMD", "nslookup", "google.com", "127.0.0.1"] interval: 30s timeout: 3s retries: 10 From e6727b1fd65a3472a4e1894b600d8e4557e6e2fb Mon Sep 17 00:00:00 2001 From: andryyy Date: Thu, 13 Jul 2017 12:51:52 +0200 Subject: [PATCH 19/26] [ACME] Iterate alias domains, use hostname in subject field --- data/Dockerfiles/acme/docker-entrypoint.sh | 9 ++++++--- docker-compose.yml | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/data/Dockerfiles/acme/docker-entrypoint.sh b/data/Dockerfiles/acme/docker-entrypoint.sh index b8098a14..f20da206 100755 --- a/data/Dockerfiles/acme/docker-entrypoint.sh +++ b/data/Dockerfiles/acme/docker-entrypoint.sh @@ -77,9 +77,12 @@ while true; do # Container ids may have changed CONTAINERS_RESTART=($(curl --silent --unix-socket /var/run/docker.sock http/containers/json | jq -rc 'map(select(.Names[] | contains ("nginx-mailcow") or contains ("postfix-mailcow") or contains ("dovecot-mailcow"))) | .[] .Id' | tr "\n" " ")) - while read line; do - SQL_DOMAIN_ARR+=("${line}") + while read domain; do + SQL_DOMAIN_ARR+=("${domain}") done < <(mysql -h mysql-mailcow -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain" -Bs) + while read alias_domain; do + SQL_DOMAIN_ARR+=("${alias_domain}") + done < <(mysql -h mysql-mailcow -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT alias_domain FROM alias_domain" -Bs) for SQL_DOMAIN in "${SQL_DOMAIN_ARR[@]}"; do A_CONFIG=$(dig A autoconfig.${SQL_DOMAIN} +short | tail -n 1) @@ -138,7 +141,7 @@ while true; do done # Unique elements - ALL_VALIDATED=($(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} ${VALIDATED_MAILCOW_HOSTNAME} | xargs -n1 | sort -u | xargs)) + ALL_VALIDATED=($(echo ${VALIDATED_MAILCOW_HOSTNAME} ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs)) if [[ -z ${ALL_VALIDATED[*]} ]]; then echo "Cannot validate hostnames, skipping Let's Encrypt..." exit 0 diff --git a/docker-compose.yml b/docker-compose.yml index e29285ec..68ba9d2b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: mysql-mailcow: condition: service_healthy healthcheck: - test: ["CMD", "nslookup", "google.com", "127.0.0.1"] + test: ["CMD", "nslookup", "mailcow.email", "127.0.0.1"] interval: 30s timeout: 3s retries: 10 From 4e148c7026e4e73ba6eaf15f9ce4d6c899c16d84 Mon Sep 17 00:00:00 2001 From: andryyy Date: Thu, 13 Jul 2017 12:54:29 +0200 Subject: [PATCH 20/26] [Compose] Push Rspamd 1.4; Push acme 1.14; Add Dovecot ulimits --- docker-compose.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 68ba9d2b..04c81a56 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -80,7 +80,7 @@ services: - clamd rspamd-mailcow: - image: mailcow/rspamd:1.3 + image: mailcow/rspamd:1.4 build: ./data/Dockerfiles/rspamd command: > /bin/bash -c " @@ -186,6 +186,11 @@ services: - "${POPS_PORT:-995}:995" - "${SIEVE_PORT:-4190}:4190" restart: always + ulimits: + nproc: 65535 + nofile: + soft: 20000 + hard: 40000 dns: - 172.22.1.254 dns_search: mailcow-network @@ -277,7 +282,7 @@ services: acme-mailcow: depends_on: - nginx-mailcow - image: mailcow/acme:1.12 + image: mailcow/acme:1.13 build: ./data/Dockerfiles/acme dns: - 172.22.1.254 From 5f5872f78b0e4237c4932f58724ecff7fc6bf7cf Mon Sep 17 00:00:00 2001 From: andryyy Date: Thu, 13 Jul 2017 12:54:53 +0200 Subject: [PATCH 21/26] [Rspamd] Initial custom ratelimit support --- data/Dockerfiles/rspamd/Dockerfile | 1 + data/Dockerfiles/rspamd/ratelimit.lua | 717 ++++++++++++++++++++++++++ 2 files changed, 718 insertions(+) create mode 100644 data/Dockerfiles/rspamd/ratelimit.lua diff --git a/data/Dockerfiles/rspamd/Dockerfile b/data/Dockerfiles/rspamd/Dockerfile index 408a7a1d..cb5c7d40 100644 --- a/data/Dockerfiles/rspamd/Dockerfile +++ b/data/Dockerfiles/rspamd/Dockerfile @@ -19,6 +19,7 @@ RUN apt-get update && apt-get install -y \ && chown _rspamd:_rspamd /run/rspamd COPY settings.conf /etc/rspamd/modules.d/settings.conf +COPY ratelimit.lua /usr/share/rspamd/lua/ratelimit.lua COPY docker-entrypoint.sh /docker-entrypoint.sh ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/data/Dockerfiles/rspamd/ratelimit.lua b/data/Dockerfiles/rspamd/ratelimit.lua new file mode 100644 index 00000000..d9e8f42a --- /dev/null +++ b/data/Dockerfiles/rspamd/ratelimit.lua @@ -0,0 +1,717 @@ +--[[ +Copyright (c) 2011-2015, Vsevolod Stakhov + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +]]-- + +if confighelp then + return +end + +-- A plugin that implements ratelimits using redis or kvstorage server + +local E = {} + +-- Default settings for limits, 1-st member is burst, second is rate and the third is numeric type +local settings = { +} +-- Senders that are considered as bounce +local bounce_senders = {'postmaster', 'mailer-daemon', '', 'null', 'fetchmail-daemon', 'mdaemon'} +-- Do not check ratelimits for these recipients +local whitelisted_rcpts = {'postmaster', 'mailer-daemon'} +local whitelisted_ip +local whitelisted_user +local max_rcpt = 5 +local redis_params +local ratelimit_symbol +-- Do not delay mail after 1 day +local max_delay = 24 * 3600 +local use_ip_score = false +local rl_prefix = 'rl' +local ip_score_lower_bound = 10 +local ip_score_ham_multiplier = 1.1 +local ip_score_spam_divisor = 1.1 + +local message_func = function(_, limit_type) + return string.format('Ratelimit "%s" exceeded', limit_type) +end + +local rspamd_logger = require "rspamd_logger" +local rspamd_util = require "rspamd_util" +local rspamd_lua_utils = require "lua_util" +local fun = require "fun" + +local user_keywords = {'user'} + +local limit_parser +local function parse_string_limit(lim) + local function parse_time_suffix(s) + if s == 's' then + return 1 + elseif s == 'm' then + return 60 + elseif s == 'h' then + return 3600 + elseif s == 'd' then + return 86400 + end + end + local function parse_num_suffix(s) + if s == '' then + return 1 + elseif s == 'k' then + return 1000 + elseif s == 'm' then + return 1000000 + elseif s == 'g' then + return 1000000000 + end + end + local lpeg = require "lpeg" + + if not limit_parser then + local digit = lpeg.R("09") + limit_parser = {} + limit_parser.integer = + (lpeg.S("+-") ^ -1) * + (digit ^ 1) + limit_parser.fractional = + (lpeg.P(".") ) * + (digit ^ 1) + limit_parser.number = + (limit_parser.integer * + (limit_parser.fractional ^ -1)) + + (lpeg.S("+-") * limit_parser.fractional) + limit_parser.time = lpeg.Cf(lpeg.Cc(1) * + (limit_parser.number / tonumber) * + ((lpeg.S("smhd") / parse_time_suffix) ^ -1), + function (acc, val) return acc * val end) + limit_parser.suffixed_number = lpeg.Cf(lpeg.Cc(1) * + (limit_parser.number / tonumber) * + ((lpeg.S("kmg") / parse_num_suffix) ^ -1), + function (acc, val) return acc * val end) + limit_parser.limit = lpeg.Ct(limit_parser.suffixed_number * + (lpeg.S(" ") ^ 0) * lpeg.S("/") * (lpeg.S(" ") ^ 0) * + limit_parser.time) + end + local t = lpeg.match(limit_parser.limit, lim) + + if t and t[1] and t[2] and t[2] ~= 0 then + return t[1] / t[2], t[1] + end + + rspamd_logger.errx(rspamd_config, 'bad limit: %s', lim) + + return nil +end + +--- Parse atime and bucket of limit +local function parse_limits(data) + local function parse_limit_elt(str) + local elts = rspamd_str_split(str, ':') + if not elts or #elts < 2 then + return {0, 0, 0} + else + local atime = tonumber(elts[1]) + local bucket = tonumber(elts[2]) + local ctime = atime + + if elts[3] then + ctime = tonumber(elts[3]) + end + + if not ctime then + ctime = atime + end + + return {atime,bucket,ctime} + end + end + + return fun.iter(data):map(function(e) + if type(e) == 'string' then + return parse_limit_elt(e) + else + return {0, 0, 0} + end + end):totable() +end + +local function resize_element(x_score, x_total, element) + local x_ip_score + if not x_total then x_total = 0 end + if x_total < ip_score_lower_bound or x_total <= 0 then + x_score = 1 + else + x_score = x_score / x_total + end + if x_score > 0 then + x_ip_score = x_score / ip_score_spam_divisor + element = element * rspamd_util.tanh(2.718281 * x_ip_score) + elseif x_score < 0 then + x_ip_score = ((1 + (x_score * -1)) * ip_score_ham_multiplier) + element = element * x_ip_score + end + return element +end + +--- Check whether this addr is bounce +local function check_bounce(from) + return fun.any(function(b) return b == from end, bounce_senders) +end + +local custom_keywords = {} + +local keywords = { + ['ip'] = { + ['get_value'] = function(task) + local ip = task:get_ip() + if ip and ip:is_valid() then return ip end + return nil + end, + }, + ['rip'] = { + ['get_value'] = function(task) + local ip = task:get_ip() + if ip and ip:is_valid() and not ip:is_local() then return ip end + return nil + end, + }, + ['from'] = { + ['get_value'] = function(task) + local from = task:get_from(0) + if ((from or E)[1] or E).addr then + return from[1]['addr'] + end + return nil + end, + }, + ['bounce'] = { + ['get_value'] = function(task) + local from = task:get_from(0) + if not ((from or E)[1] or E).user then + return '_' + end + if check_bounce(from[1]['user']) then return '_' else return nil end + end, + }, + ['asn'] = { + ['get_value'] = function(task) + local asn = task:get_mempool():get_variable('asn') + if not asn then + return nil + else + return asn + end + end, + }, + ['user'] = { + ['get_value'] = function(task) + local auser = task:get_user() + if not auser then + return nil + else + return auser + end + end, + }, + ['to'] = { + ['get_value'] = function() + return '%s' -- 'to' is special + end, + }, +} + +local function dynamic_rate_key(task, rtype) + local key_t = {rl_prefix, rtype} + local key_keywords = rspamd_str_split(rtype, '_') + local have_to, have_user = false, false + for _, v in ipairs(key_keywords) do + if (custom_keywords[v] and type(custom_keywords[v]['condition']) == 'function') then + if not custom_keywords[v]['condition']() then return nil end + end + local ret + if custom_keywords[v] and type(custom_keywords[v]['get_value']) == 'function' then + ret = custom_keywords[v]['get_value'](task) + elseif keywords[v] and type(keywords[v]['get_value']) == 'function' then + ret = keywords[v]['get_value'](task) + end + if not ret then return nil end + for _, uk in ipairs(user_keywords) do + if v == uk then have_user = true end + if have_user then break end + end + if v == 'to' then have_to = true end + if type(ret) ~= 'string' then ret = tostring(ret) end + table.insert(key_t, ret) + end + if (not have_user) and task:get_user() then + return nil + end + if not have_to then + return table.concat(key_t, ":") + else + local rate_keys = {} + local rcpts = task:get_recipients(0) + if not ((rcpts or E)[1] or E).addr then + return nil + end + local key_s = table.concat(key_t, ":") + local total_rcpt = 0 + for _, r in ipairs(rcpts) do + if r['addr'] and total_rcpt < max_rcpt then + local key_f = string.format(key_s, r['addr']) + table.insert(rate_keys, key_f) + total_rcpt = total_rcpt + 1 + end + end + return rate_keys + end +end + +--- Check specific limit inside redis +local function check_limits(task, args) + + local key = fun.foldl(function(acc, k) return acc .. k[2] end, '', args) + local ret + --- Called when value is got from server + local function rate_get_cb(err, data) + if err then + rspamd_logger.infox(task, 'got error while getting limit: %1', err) + end + if not data then return end + local ntime = rspamd_util.get_time() + local asn_score,total_asn, + country_score,total_country, + ipnet_score,total_ipnet, + ip_score, total_ip + if use_ip_score then + asn_score,total_asn, + country_score,total_country, + ipnet_score,total_ipnet, + ip_score, total_ip = task:get_mempool():get_variable('ip_score', + 'double,double,double,double,double,double,double,double') + end + + fun.each(function(elt, limit, rtype) + local bucket = elt[2] + local rate = limit[2] + local threshold = limit[1] + local atime = elt[1] + local ctime = elt[3] + + if atime == 0 then return end + + if use_ip_score then + local key_keywords = rspamd_str_split(rtype, '_') + local has_asn, has_ip = false, false + for _, v in ipairs(key_keywords) do + if v == "asn" then has_asn = true end + if v == "ip" then has_ip = true end + if has_ip and has_asn then break end + end + if has_asn and not has_ip then + bucket = resize_element(asn_score, total_asn, bucket) + rate = resize_element(asn_score, total_asn, rate) + elseif has_ip then + if total_ip and total_ip > ip_score_lower_bound then + bucket = resize_element(ip_score, total_ip, bucket) + rate = resize_element(ip_score, total_ip, rate) + elseif total_ipnet and total_ipnet > ip_score_lower_bound then + bucket = resize_element(ipnet_score, total_ipnet, bucket) + rate = resize_element(ipnet_score, total_ipnet, rate) + elseif total_asn and total_asn > ip_score_lower_bound then + bucket = resize_element(asn_score, total_asn, bucket) + rate = resize_element(asn_score, total_asn, rate) + elseif total_country and total_country > ip_score_lower_bound then + bucket = resize_element(country_score, total_country, bucket) + rate = resize_element(country_score, total_country, rate) + else + bucket = resize_element(ip_score, total_ip, bucket) + rate = resize_element(ip_score, total_ip, rate) + end + end + end + + if atime - ctime > max_delay then + rspamd_logger.infox(task, 'limit is too old: %1 seconds; ignore it', + atime - ctime) + else + bucket = bucket - rate * (ntime - atime); + if bucket > 0 then + if ratelimit_symbol then + local mult = 2 * rspamd_util.tanh(bucket / (threshold * 2)) + + if mult > 0.5 then + task:insert_result(ratelimit_symbol, mult, + rtype .. ':' .. string.format('%.2f', mult)) + end + else + if bucket > threshold then + rspamd_logger.infox(task, + 'ratelimit "%s" exceeded: %s elements with %s limit', + rtype, bucket, threshold) + task:set_pre_result('soft reject', + message_func(task, rtype, bucket, threshold)) + end + end + end + end + end, fun.zip(parse_limits(data), fun.map(function(a) return a[1] end, args), + fun.map(function(a) return rspamd_str_split(a[2], ":")[2] end, args))) + end + + ret = rspamd_redis_make_request(task, + redis_params, -- connect params + key, -- hash key + false, -- is write + rate_get_cb, --callback + 'mget', -- command + fun.totable(fun.map(function(l) return l[2] end, args)) -- arguments + ) + if not ret then + rspamd_logger.errx(task, 'got error connecting to redis') + end +end + +--- Set specific limit inside redis +local function set_limits(task, args) + local key = fun.foldl(function(acc, k) return acc .. k[2] end, '', args) + local ret, upstream + + local function rate_set_cb(err) + if err then + rspamd_logger.infox(task, 'got error %s when setting ratelimit record on server %s', + err, upstream:get_addr()) + end + end + local function rate_get_cb(err, data) + if err then + rspamd_logger.infox(task, 'got error while setting limit: %1', err) + end + if not data then return end + local ntime = rspamd_util.get_time() + local values = {} + fun.each(function(elt, limit) + local bucket = elt[2] + local rate = limit[1][2] + local atime = elt[1] + local ctime = elt[3] + + if atime - ctime > max_delay then + rspamd_logger.infox(task, 'limit is too old: %1 seconds; start it over', + atime - ctime) + bucket = 1 + ctime = ntime + else + if bucket > 0 then + bucket = bucket - rate * (ntime - atime) + 1; + if bucket < 0 then + bucket = 1 + end + else + bucket = 1 + end + end + + if ctime == 0 then ctime = ntime end + + local lstr = string.format('%.3f:%.3f:%.3f', ntime, bucket, ctime) + table.insert(values, {limit[2], max_delay, lstr}) + end, fun.zip(parse_limits(data), fun.iter(args))) + + if #values > 0 then + local conn + ret,conn,upstream = rspamd_redis_make_request(task, + redis_params, -- connect params + key, -- hash key + true, -- is write + rate_set_cb, --callback + 'setex', -- command + values[1] -- arguments + ) + + if conn then + fun.each(function(v) + conn:add_cmd('setex', v) + end, fun.drop_n(1, values)) + else + rspamd_logger.errx(task, 'got error while connecting to redis') + end + end + end + + local _ + ret,_,upstream = rspamd_redis_make_request(task, + redis_params, -- connect params + key, -- hash key + false, -- is write + rate_get_cb, --callback + 'mget', -- command + fun.totable(fun.map(function(l) return l[2] end, args)) -- arguments + ) + if not ret then + rspamd_logger.errx(task, 'got error connecting to redis') + end +end + +--- Check or update ratelimit +local function rate_test_set(task, func) + local args = {} + -- Get initial task data + local ip = task:get_from_ip() + if ip and ip:is_valid() and whitelisted_ip then + if whitelisted_ip:get_key(ip) then + -- Do not check whitelisted ip + rspamd_logger.infox(task, 'skip ratelimit for whitelisted IP') + return + end + end + -- Parse all rcpts + local rcpts = task:get_recipients() + local rcpts_user = {} + if rcpts then + fun.each(function(r) table.insert(rcpts_user, r['user']) end, rcpts) + if fun.any(function(r) + fun.any(function(w) return r == w end, whitelisted_rcpts) end, + rcpts_user) then + + rspamd_logger.infox(task, 'skip ratelimit for whitelisted recipient') + return + end + end + -- Get user (authuser) + if whitelisted_user then + local auser = task:get_user() + if whitelisted_user:get_key(auser) then + rspamd_logger.infox(task, 'skip ratelimit for whitelisted user') + return + end + end + + local rate_key + for k in pairs(settings) do + rate_key = dynamic_rate_key(task, k) + if rate_key then + if type(rate_key) == 'table' then + for _, rk in ipairs(rate_key) do + if type(settings[k]) == 'table' then + table.insert(args, {settings[k], rk}) + elseif type(settings[k]) == 'string' and + (custom_keywords[settings[k]] and type(custom_keywords[settings[k]]['get_limit']) == 'function') then + local res = custom_keywords[settings[k]]['get_limit'](task) + if type(res) == 'table' then + table.insert(args, {res, rate_key}) + elseif type(res) == 'string' then + local plim, size = parse_string_limit(res) + if plim then + table.insert(args, {{size, plim, 1}, rate_key}) + end + end + end + end + else + if type(settings[k]) == 'table' then + table.insert(args, {settings[k], rate_key}) + elseif type(settings[k]) == 'string' and + (custom_keywords[settings[k]] and type(custom_keywords[settings[k]]['get_limit']) == 'function') then + local res = custom_keywords[settings[k]]['get_limit'](task) + if type(res) == 'table' then + table.insert(args, {res, rate_key}) + elseif type(res) == 'string' then + local plim, size = parse_string_limit(res) + if plim then + table.insert(args, {{size, plim, 1}, rate_key}) + end + end + end + end + end + end + + if #args > 0 then + func(task, args) + end +end + +--- Check limit +local function rate_test(task) + if rspamd_lua_utils.is_rspamc_or_controller(task) then return end + rate_test_set(task, check_limits) +end +--- Update limit +local function rate_set(task) + local action = task:get_metric_action('default') + + if action ~= 'soft reject' then + if rspamd_lua_utils.is_rspamc_or_controller(task) then return end + rate_test_set(task, set_limits) + end +end + + +--- Parse a single limit description +local function parse_limit(str) + local params = rspamd_str_split(str, ':') + + local function set_limit(limit, burst, rate) + limit[1] = tonumber(burst) + limit[2] = tonumber(rate) + end + + if #params ~= 3 then + rspamd_logger.errx(rspamd_config, 'invalid limit definition: ' .. str) + return + end + + local key_keywords = rspamd_str_split(params[1], '_') + for _, k in ipairs(key_keywords) do + if (custom_keywords[k] and type(custom_keywords[k]['get_value']) == 'function') or + (keywords[k] and type(keywords[k]['get_value']) == 'function') then + set_limit(settings[params[1]], params[2], params[3]) + else + rspamd_logger.errx(rspamd_config, 'invalid limit type: ' .. params[1]) + end + end +end + +local opts = rspamd_config:get_all_opt('ratelimit') +if opts then + local rates = opts['limit'] + if rates and type(rates) == 'table' then + fun.each(parse_limit, rates) + elseif rates and type(rates) == 'string' then + parse_limit(rates) + end + + if opts['rates'] and type(opts['rates']) == 'table' then + -- new way of setting limits + fun.each(function(t, lim) + if type(lim) == 'table' then + settings[t] = lim + elseif type(lim) == 'string' then + local plim, size = parse_string_limit(lim) + if plim then + settings[t] = {size, plim, 1} + end + end + end, opts['rates']) + end + + if opts['dynamic_rates'] and type(opts['dynamic_rates']) == 'table' then + fun.each(function(t, lim) + if type(lim) == 'string' then + settings[t] = lim + end + end, opts['dynamic_rates']) + end + + local enabled_limits = fun.totable(fun.map(function(t) + return t + end, fun.filter(function(_, lim) + return type(lim) == 'string' or + (type(lim) == 'table' and type(lim[1]) == 'number' and lim[1] > 0) + or (type(lim) == 'table' and (lim[3])) + end, settings))) + rspamd_logger.infox(rspamd_config, 'enabled rate buckets: [%1]', table.concat(enabled_limits, ',')) + + if opts['whitelisted_rcpts'] and type(opts['whitelisted_rcpts']) == 'string' then + whitelisted_rcpts = rspamd_str_split(opts['whitelisted_rcpts'], ',') + elseif type(opts['whitelisted_rcpts']) == 'table' then + whitelisted_rcpts = opts['whitelisted_rcpts'] + end + + if opts['whitelisted_ip'] then + whitelisted_ip = rspamd_map_add('ratelimit', 'whitelisted_ip', 'radix', + 'Ratelimit whitelist ip map') + end + + if opts['whitelisted_user'] then + whitelisted_user = rspamd_map_add('ratelimit', 'whitelisted_user', 'set', + 'Ratelimit whitelist user map') + end + + if opts['symbol'] then + -- We want symbol instead of pre-result + ratelimit_symbol = opts['symbol'] + end + + if opts['max_rcpt'] then + max_rcpt = tonumber(opts['max_rcpt']) + end + + if opts['max_delay'] then + max_rcpt = tonumber(opts['max_delay']) + end + + if opts['use_ip_score'] then + use_ip_score = true + local ip_score_opts = rspamd_config:get_all_opt('ip_score') + if ip_score_opts and ip_score_opts['lower_bound'] then + ip_score_lower_bound = ip_score_opts['lower_bound'] + end + end + + if opts['custom_keywords'] then + custom_keywords = dofile(opts['custom_keywords']) + end + + if opts['user_keywords'] then + user_keywords = opts['user_keywords'] + end + + if opts['message_func'] then + message_func = assert(load(opts['message_func']))() + end + + redis_params = rspamd_parse_redis_server('ratelimit') + if not redis_params then + rspamd_logger.infox(rspamd_config, 'no servers are specified, disabling module') + else + if not ratelimit_symbol and not use_ip_score then + rspamd_config:register_symbol({ + name = 'RATELIMIT_CHECK', + callback = rate_test, + type = 'prefilter', + priority = 4, + }) + else + local symbol + if not ratelimit_symbol then + symbol = 'RATELIMIT_CHECK' + else + symbol = ratelimit_symbol + end + local id = rspamd_config:register_symbol({ + name = symbol, + callback = rate_test, + }) + if use_ip_score then + rspamd_config:register_dependency(id, 'IP_SCORE') + end + end + rspamd_config:register_symbol({ + name = 'RATELIMIT_SET', + type = 'postfilter', + priority = 5, + callback = rate_set, + }) + for _, v in pairs(custom_keywords) do + if type(v) == 'table' and type(v['init']) == 'function' then + v['init']() + end + end + end +end + + From 256c9d86ddd6ea1caf2f4dc5dc252bf07bc9de0a Mon Sep 17 00:00:00 2001 From: andryyy Date: Thu, 13 Jul 2017 12:55:14 +0200 Subject: [PATCH 22/26] [Rspamd] Initial custom ratelimit support --- data/conf/rspamd/local.d/ratelimit.conf | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 data/conf/rspamd/local.d/ratelimit.conf diff --git a/data/conf/rspamd/local.d/ratelimit.conf b/data/conf/rspamd/local.d/ratelimit.conf deleted file mode 100644 index eca11a12..00000000 --- a/data/conf/rspamd/local.d/ratelimit.conf +++ /dev/null @@ -1,18 +0,0 @@ -rates { - # Limit for all mail per recipient (burst 100, rate 2 per minute) - to = [100, 0.033333333]; - # Limit for all mail per one source ip (burst 30, rate 1.5 per minute) - to_ip = [30, 0.025]; - # Limit for all mail per one source ip and from address (burst 20, rate 1 per minute) - to_ip_from = [20, 0.01666666667]; - # Limit for all bounce mail (burst 10, rate 2 per hour) - bounce_to = [10, 0.000555556]; - # Limit for bounce mail per one source ip (burst 5, rate 1 per hour) - bounce_to_ip = [5, 0.000277778]; - # Limit for all mail per authenticated user (burst 20, rate 1 per minute) - user = [20, 0.01666666667]; -} -# If symbol is specified, then it is inserted instead of setting result -#symbol = "R_RATELIMIT"; -whitelisted_rcpts = "postmaster,mailer-daemon"; -max_rcpt = 5; From 84ad579437d6e978923b4b82718867fdeb8c9b77 Mon Sep 17 00:00:00 2001 From: andryyy Date: Sun, 16 Jul 2017 11:03:28 +0200 Subject: [PATCH 23/26] [Web] Initial ratelimit support, more API actions --- data/web/admin.php | 4 +- data/web/css/edit.css | 3 + data/web/edit.php | 85 ++- data/web/inc/functions.domain_admin.inc.php | 507 +++++++++++++++++ data/web/inc/functions.inc.php | 573 +------------------- data/web/inc/functions.mailbox.inc.php | 134 ++++- data/web/inc/prerequisites.inc.php | 1 + data/web/inc/sessions.inc.php | 2 + data/web/inc/triggers.inc.php | 41 -- data/web/js/api.js | 20 +- data/web/json_api.php | 181 +++++-- data/web/user.php | 4 +- 12 files changed, 855 insertions(+), 700 deletions(-) create mode 100644 data/web/inc/functions.domain_admin.inc.php diff --git a/data/web/admin.php b/data/web/admin.php index 0e0003a8..284bf31b 100644 --- a/data/web/admin.php +++ b/data/web/admin.php @@ -56,7 +56,7 @@ $tfa_data = get_tfa();
- +
@@ -253,7 +253,7 @@ XYZ - +
diff --git a/data/web/css/edit.css b/data/web/css/edit.css index fe4d9fff..07d4e745 100644 --- a/data/web/css/edit.css +++ b/data/web/css/edit.css @@ -27,3 +27,6 @@ table.footable>tbody>tr.footable-empty>td { user-select: none; padding:10px 0 10px 0; } +.inputMissingAttr { + border-color: #FF4136; +} diff --git a/data/web/edit.php b/data/web/edit.php index d156dba0..1f250535 100644 --- a/data/web/edit.php +++ b/data/web/edit.php @@ -25,9 +25,8 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm ?>


-
"> + -
@@ -43,7 +42,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
- +
@@ -55,20 +54,19 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm


-
"> + -
@@ -122,7 +120,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
- +
@@ -139,14 +137,14 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm !empty($_GET["domain"])) { $domain = $_GET["domain"]; $result = mailbox('get', 'domain_details', $domain); + $rl = mailbox('get', 'domain_ratelimit', $domain); if (!empty($result)) { ?>

-
"> + -
@@ -203,7 +201,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
- +
@@ -223,6 +221,23 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm } ?>
+
+
+ + +
+
+ +
+
+ +
+
+

@@ -282,12 +297,12 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm !empty($_GET["aliasdomain"])) { $alias_domain = $_GET["aliasdomain"]; $result = mailbox('get', 'alias_domain_details', $alias_domain); + $rl = mailbox('get', 'domain_ratelimit', $alias_domain); if (!empty($result)) { ?>

-
"> + -
@@ -303,10 +318,27 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
- +
+
+
+
+ + +
+
+ +
+
+ +
+
@@ -334,10 +366,9 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm if (!empty($result)) { ?>

-
"> + -
@@ -355,7 +386,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
-
- +
@@ -439,10 +470,9 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm if (!empty($result)) { ?>

-
"> + -
@@ -475,7 +505,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
- +
@@ -501,11 +531,10 @@ elseif (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == if (!empty($result)) { ?>

-
"> + -
@@ -587,7 +616,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] ==
- +
diff --git a/data/web/inc/functions.domain_admin.inc.php b/data/web/inc/functions.domain_admin.inc.php new file mode 100644 index 00000000..aa434079 --- /dev/null +++ b/data/web/inc/functions.domain_admin.inc.php @@ -0,0 +1,507 @@ + 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + if (empty($domains)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['domain_invalid']) + ); + return false; + } + if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username)) || empty ($username)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['username_invalid']) + ); + return false; + } + try { + $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` + WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + $num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + + $stmt = $pdo->prepare("SELECT `username` FROM `admin` + WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + $num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + + $stmt = $pdo->prepare("SELECT `username` FROM `domain_admins` + WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + $num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + } + catch(PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + foreach ($num_results as $num_results_each) { + if ($num_results_each != 0) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['object_exists'], htmlspecialchars($username)) + ); + return false; + } + } + if (!empty($password) && !empty($password2)) { + if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['password_complexity']) + ); + return false; + } + if ($password != $password2) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['password_mismatch']) + ); + return false; + } + $password_hashed = hash_password($password); + foreach ($domains as $domain) { + if (!is_valid_domain_name($domain)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['domain_invalid']) + ); + return false; + } + try { + $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`) + VALUES (:username, :domain, :created, :active)"); + $stmt->execute(array( + ':username' => $username, + ':domain' => $domain, + ':created' => date('Y-m-d H:i:s'), + ':active' => $active + )); + } + catch (PDOException $e) { + domain_admin('delete', $username); + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } + try { + $stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `active`) + VALUES (:username, :password_hashed, '0', :active)"); + $stmt->execute(array( + ':username' => $username, + ':password_hashed' => $password_hashed, + ':active' => $active + )); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } + else { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['password_empty']) + ); + return false; + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => sprintf($lang['success']['domain_admin_added'], htmlspecialchars($username)) + ); + break; + case 'edit': + if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + // Administrator + if ($_SESSION['mailcow_cc_role'] == "admin") { + if (!is_array($_data['username'])) { + $usernames = array(); + $usernames[] = $_data['username']; + } + else { + $usernames = $_data['username']; + } + foreach ($usernames as $username) { + $is_now = domain_admin('details', $username); + $domains = (isset($_data['domains'])) ? (array)$_data['domains'] : null; + if (!empty($is_now)) { + $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int']; + $domains = (!empty($domains)) ? $domains : $is_now['selected_domains']; + $username_new = (!empty($_data['username_new'])) ? $_data['username_new'] : $is_now['username']; + } + else { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + $password = $_data['password']; + $password2 = $_data['password2']; + + if (!empty($domains)) { + foreach ($domains as $domain) { + if (!is_valid_domain_name($domain)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['domain_invalid']) + ); + return false; + } + } + } + if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username_new))) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['username_invalid']) + ); + return false; + } + if ($username_new != $username) { + if (!empty(domain_admin('details', $username_new)['username'])) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['username_invalid']) + ); + return false; + } + } + try { + $stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username"); + $stmt->execute(array( + ':username' => $username, + )); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + + if (!empty($domains)) { + foreach ($domains as $domain) { + try { + $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`) + VALUES (:username_new, :domain, :created, :active)"); + $stmt->execute(array( + ':username_new' => $username_new, + ':domain' => $domain, + ':created' => date('Y-m-d H:i:s'), + ':active' => $active + )); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } + } + + if (!empty($password) && !empty($password2)) { + if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['password_complexity']) + ); + return false; + } + if ($password != $password2) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['password_mismatch']) + ); + return false; + } + $password_hashed = hash_password($password); + try { + $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username"); + $stmt->execute(array( + ':password_hashed' => $password_hashed, + ':username_new' => $username_new, + ':username' => $username, + ':active' => $active + )); + if (isset($_data['disable_tfa'])) { + $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + } + else { + $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username"); + $stmt->execute(array(':username_new' => $username_new, ':username' => $username)); + } + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } + else { + try { + $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username"); + $stmt->execute(array( + ':username_new' => $username_new, + ':username' => $username, + ':active' => $active + )); + if (isset($_data['disable_tfa'])) { + $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + } + else { + $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username"); + $stmt->execute(array(':username_new' => $username_new, ':username' => $username)); + } + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => sprintf($lang['success']['domain_admin_modified'], htmlspecialchars(implode(', ', $usernames))) + ); + } + // Domain administrator + // Can only edit itself + elseif ($_SESSION['mailcow_cc_role'] == "domainadmin") { + $username = $_SESSION['mailcow_cc_username']; + $password_old = $_data['user_old_pass']; + $password_new = $_data['user_new_pass']; + $password_new2 = $_data['user_new_pass2']; + + $stmt = $pdo->prepare("SELECT `password` FROM `admin` + WHERE `username` = :user"); + $stmt->execute(array(':user' => $username)); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if (!verify_ssha256($row['password'], $password_old)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + + if (!empty($password_new2) && !empty($password_new)) { + if ($password_new2 != $password_new) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['password_mismatch']) + ); + return false; + } + if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password_new)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['password_complexity']) + ); + return false; + } + $password_hashed = hash_password($password_new); + try { + $stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed WHERE `username` = :username"); + $stmt->execute(array( + ':password_hashed' => $password_hashed, + ':username' => $username + )); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } + + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => sprintf($lang['success']['domain_admin_modified'], htmlspecialchars($username)) + ); + } + break; + case 'delete': + if ($_SESSION['mailcow_cc_role'] != "admin") { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + $usernames = (array)$_data['username']; + foreach ($usernames as $username) { + if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['username_invalid']) + ); + return false; + } + try { + $stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username"); + $stmt->execute(array( + ':username' => $username, + )); + $stmt = $pdo->prepare("DELETE FROM `admin` WHERE `username` = :username"); + $stmt->execute(array( + ':username' => $username, + )); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => sprintf($lang['success']['domain_admin_removed'], htmlspecialchars(implode(', ', $usernames))) + ); + break; + case 'get': + $domainadmins = array(); + if ($_SESSION['mailcow_cc_role'] != "admin") { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + try { + $stmt = $pdo->query("SELECT DISTINCT + `username` + FROM `domain_admins` + WHERE `username` IN ( + SELECT `username` FROM `admin` + WHERE `superadmin`!='1' + )"); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while ($row = array_shift($rows)) { + $domainadmins[] = $row['username']; + } + } + catch(PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + } + return $domainadmins; + break; + case 'details': + $domainadmindata = array(); + + if ($_SESSION['mailcow_cc_role'] == "domainadmin" && $_data != $_SESSION['mailcow_cc_username']) { + return false; + } + elseif ($_SESSION['mailcow_cc_role'] != "admin" || !isset($_data)) { + return false; + } + + if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $_data))) { + return false; + } + try { + $stmt = $pdo->prepare("SELECT + `tfa`.`active` AS `tfa_active_int`, + CASE `tfa`.`active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `tfa_active`, + `domain_admins`.`username`, + `domain_admins`.`created`, + `domain_admins`.`active` AS `active_int`, + CASE `domain_admins`.`active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active` + FROM `domain_admins` + LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`domain_admins`.`username` + WHERE `domain_admins`.`username`= :domain_admin"); + $stmt->execute(array( + ':domain_admin' => $_data + )); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if (empty($row)) { + return false; + } + $domainadmindata['username'] = $row['username']; + $domainadmindata['tfa_active'] = $row['tfa_active']; + $domainadmindata['active'] = $row['active']; + $domainadmindata['tfa_active_int'] = $row['tfa_active_int']; + $domainadmindata['active_int'] = $row['active_int']; + $domainadmindata['modified'] = $row['created']; + // GET SELECTED + $stmt = $pdo->prepare("SELECT `domain` FROM `domain` + WHERE `domain` IN ( + SELECT `domain` FROM `domain_admins` + WHERE `username`= :domain_admin)"); + $stmt->execute(array(':domain_admin' => $_data)); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while($row = array_shift($rows)) { + $domainadmindata['selected_domains'][] = $row['domain']; + } + // GET UNSELECTED + $stmt = $pdo->prepare("SELECT `domain` FROM `domain` + WHERE `domain` NOT IN ( + SELECT `domain` FROM `domain_admins` + WHERE `username`= :domain_admin)"); + $stmt->execute(array(':domain_admin' => $_data)); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while($row = array_shift($rows)) { + $domainadmindata['unselected_domains'][] = $row['domain']; + } + if (!isset($domainadmindata['unselected_domains'])) { + $domainadmindata['unselected_domains'] = ""; + } + } + catch(PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + } + return $domainadmindata; + break; + } +} \ No newline at end of file diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index b3420530..66dad50f 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -73,7 +73,6 @@ function generate_tlsa_digest($hostname, $port, $starttls = null) { unset($lines[0]); return base64_decode(implode('', $lines)); } - if (empty($starttls)) { $context = stream_context_create(array("ssl" => array("capture_peer_cert" => true, 'verify_peer' => false, 'allow_self_signed' => true))); $stream = stream_socket_client('tls://' . $hostname . ':' . $port, $error_nr, $error_msg, 5, STREAM_CLIENT_CONNECT, $context); @@ -113,7 +112,6 @@ function generate_tlsa_digest($hostname, $port, $starttls = null) { stream_socket_enable_crypto($stream, true, STREAM_CRYPTO_METHOD_ANY_CLIENT); stream_set_blocking($stream, false); } - $params = stream_context_get_params($stream); if (!empty($params['options']['ssl']['peer_certificate'])) { $key_resource = openssl_pkey_get_public($params['options']['ssl']['peer_certificate']); @@ -142,30 +140,6 @@ function verify_ssha256($hash, $password) { return false; } } -function doveadm_authenticate($hash, $algorithm, $password) { - $descr = array(0 => array('pipe', 'r'), 1 => array('pipe', 'w'), 2 => array('pipe', 'w')); - $pipes = array(); - $process = proc_open("/usr/bin/doveadm pw -s ".$algorithm." -t '".$hash."'", $descr, $pipes); - if (is_resource($process)) { - fputs($pipes[0], $password); - fclose($pipes[0]); - while ($f = fgets($pipes[1])) { - if (preg_match('/(verified)/', $f)) { - proc_close($process); - return true; - } - return false; - } - fclose($pipes[1]); - while ($f = fgets($pipes[2])) { - proc_close($process); - return false; - } - fclose($pipes[2]); - proc_close($process); - } - return false; -} function check_login($user, $pass) { global $pdo; global $redis; @@ -272,7 +246,6 @@ function edit_admin_account($postarray) { ); return false; } - if (!empty($password) && !empty($password2)) { if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) { $_SESSION['return'] = array( @@ -348,28 +321,20 @@ function edit_admin_account($postarray) { function edit_user_account($postarray) { global $lang; global $pdo; - if (isset($postarray['username']) && filter_var($postarray['username'], FILTER_VALIDATE_EMAIL)) { - if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $postarray['username'])) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['access_denied']) - ); - return false; - } - else { - $username = $postarray['username']; - } + $username = $_SESSION['mailcow_cc_username']; + $role = $_SESSION['mailcow_cc_role']; + $password_old = $postarray['user_old_pass']; + if (filter_var($username, FILTER_VALIDATE_EMAIL === false) || $role != 'user') { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; } - else { - $username = $_SESSION['mailcow_cc_username']; - } - $password_old = $postarray['user_old_pass']; - if (isset($postarray['user_new_pass']) && isset($postarray['user_new_pass2'])) { $password_new = $postarray['user_new_pass']; $password_new2 = $postarray['user_new_pass2']; } - $stmt = $pdo->prepare("SELECT `password` FROM `mailbox` WHERE `kind` NOT REGEXP 'location|thing|group' AND `username` = :user"); @@ -382,7 +347,6 @@ function edit_user_account($postarray) { ); return false; } - if (isset($password_new) && isset($password_new2)) { if (!empty($password_new2) && !empty($password_new)) { if ($password_new2 != $password_new) { @@ -486,293 +450,12 @@ function is_valid_domain_name($domain_name) { && preg_match("/^.{1,253}$/", $domain_name) && preg_match("/^[^\.]{1,63}(\.[^\.]{1,63})*$/", $domain_name)); } -function add_domain_admin($postarray) { - global $lang; - global $pdo; - $username = strtolower(trim($postarray['username'])); - $password = $postarray['password']; - $password2 = $postarray['password2']; - $domains = (array)$postarray['domains']; - $active = intval($postarray['active']); - - if ($_SESSION['mailcow_cc_role'] != "admin") { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['access_denied']) - ); - return false; - } - if (empty($domains)) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['domain_invalid']) - ); - return false; - } - if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username)) || empty ($username)) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['username_invalid']) - ); - return false; - } - try { - $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` - WHERE `username` = :username"); - $stmt->execute(array(':username' => $username)); - $num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - - $stmt = $pdo->prepare("SELECT `username` FROM `admin` - WHERE `username` = :username"); - $stmt->execute(array(':username' => $username)); - $num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - - $stmt = $pdo->prepare("SELECT `username` FROM `domain_admins` - WHERE `username` = :username"); - $stmt->execute(array(':username' => $username)); - $num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - } - catch(PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - return false; - } - foreach ($num_results as $num_results_each) { - if ($num_results_each != 0) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['object_exists'], htmlspecialchars($username)) - ); - return false; - } - } - if (!empty($password) && !empty($password2)) { - if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['password_complexity']) - ); - return false; - } - if ($password != $password2) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['password_mismatch']) - ); - return false; - } - $password_hashed = hash_password($password); - foreach ($domains as $domain) { - if (!is_valid_domain_name($domain)) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['domain_invalid']) - ); - return false; - } - try { - $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`) - VALUES (:username, :domain, :created, :active)"); - $stmt->execute(array( - ':username' => $username, - ':domain' => $domain, - ':created' => date('Y-m-d H:i:s'), - ':active' => $active - )); - } - catch (PDOException $e) { - delete_domain_admin(array('username' => $username)); - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - return false; - } - } - try { - $stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `active`) - VALUES (:username, :password_hashed, '0', :active)"); - $stmt->execute(array( - ':username' => $username, - ':password_hashed' => $password_hashed, - ':active' => $active - )); - } - catch (PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - return false; - } - } - else { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['password_empty']) - ); - return false; - } - $_SESSION['return'] = array( - 'type' => 'success', - 'msg' => sprintf($lang['success']['domain_admin_added'], htmlspecialchars($username)) - ); -} -function delete_domain_admin($postarray) { - global $pdo; - global $lang; - if ($_SESSION['mailcow_cc_role'] != "admin") { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['access_denied']) - ); - return false; - } - $usernames = (array)$postarray['username']; - foreach ($usernames as $username) { - if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['username_invalid']) - ); - return false; - } - try { - $stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username"); - $stmt->execute(array( - ':username' => $username, - )); - $stmt = $pdo->prepare("DELETE FROM `admin` WHERE `username` = :username"); - $stmt->execute(array( - ':username' => $username, - )); - } - catch (PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - return false; - } - } - $_SESSION['return'] = array( - 'type' => 'success', - 'msg' => sprintf($lang['success']['domain_admin_removed'], htmlspecialchars(implode(', ', $usernames))) - ); -} -function get_domain_admins() { - global $pdo; - global $lang; - $domainadmins = array(); - if ($_SESSION['mailcow_cc_role'] != "admin") { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['access_denied']) - ); - return false; - } - try { - $stmt = $pdo->query("SELECT DISTINCT - `username` - FROM `domain_admins` - WHERE `username` IN ( - SELECT `username` FROM `admin` - WHERE `superadmin`!='1' - )"); - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - while ($row = array_shift($rows)) { - $domainadmins[] = $row['username']; - } - } - catch(PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - } - return $domainadmins; -} -function get_domain_admin_details($domain_admin) { - global $pdo; - - global $lang; - $domainadmindata = array(); - if (isset($domain_admin) && $_SESSION['mailcow_cc_role'] != "admin") { - return false; - } - if (!isset($domain_admin) && $_SESSION['mailcow_cc_role'] != "domainadmin") { - return false; - } - (!isset($domain_admin)) ? $domain_admin = $_SESSION['mailcow_cc_username'] : null; - - if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $domain_admin))) { - return false; - } - try { - $stmt = $pdo->prepare("SELECT - `tfa`.`active` AS `tfa_active_int`, - CASE `tfa`.`active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `tfa_active`, - `domain_admins`.`username`, - `domain_admins`.`created`, - `domain_admins`.`active` AS `active_int`, - CASE `domain_admins`.`active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active` - FROM `domain_admins` - LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`domain_admins`.`username` - WHERE `domain_admins`.`username`= :domain_admin"); - $stmt->execute(array( - ':domain_admin' => $domain_admin - )); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - if (empty($row)) { - return false; - } - $domainadmindata['username'] = $row['username']; - $domainadmindata['tfa_active'] = $row['tfa_active']; - $domainadmindata['active'] = $row['active']; - $domainadmindata['tfa_active_int'] = $row['tfa_active_int']; - $domainadmindata['active_int'] = $row['active_int']; - $domainadmindata['modified'] = $row['created']; - // GET SELECTED - $stmt = $pdo->prepare("SELECT `domain` FROM `domain` - WHERE `domain` IN ( - SELECT `domain` FROM `domain_admins` - WHERE `username`= :domain_admin)"); - $stmt->execute(array(':domain_admin' => $domain_admin)); - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - while($row = array_shift($rows)) { - $domainadmindata['selected_domains'][] = $row['domain']; - } - // GET UNSELECTED - $stmt = $pdo->prepare("SELECT `domain` FROM `domain` - WHERE `domain` NOT IN ( - SELECT `domain` FROM `domain_admins` - WHERE `username`= :domain_admin)"); - $stmt->execute(array(':domain_admin' => $domain_admin)); - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - while($row = array_shift($rows)) { - $domainadmindata['unselected_domains'][] = $row['domain']; - } - if (!isset($domainadmindata['unselected_domains'])) { - $domainadmindata['unselected_domains'] = ""; - } - } - catch(PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - } - return $domainadmindata; -} function set_tfa($postarray) { global $lang; global $pdo; global $yubi; global $u2f; global $tfa; - if ($_SESSION['mailcow_cc_role'] != "domainadmin" && $_SESSION['mailcow_cc_role'] != "admin") { $_SESSION['return'] = array( @@ -847,7 +530,6 @@ function set_tfa($postarray) { 'msg' => sprintf($lang['success']['object_modified'], htmlspecialchars($username)) ); break; - case "u2f": $key_id = (!isset($postarray["key_id"])) ? 'unidentified' : $postarray["key_id"]; try { @@ -871,7 +553,6 @@ function set_tfa($postarray) { return false; } break; - case "totp": $key_id = (!isset($postarray["key_id"])) ? 'unidentified' : $postarray["key_id"]; if ($tfa->verifyCode($_POST['totp_secret'], $_POST['totp_confirm_token']) === true) { @@ -900,7 +581,6 @@ function set_tfa($postarray) { ); } break; - case "none": try { $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username"); @@ -977,7 +657,6 @@ function get_tfa($username = null) { elseif (empty($username)) { return false; } - $stmt = $pdo->prepare("SELECT * FROM `tfa` WHERE `username` = :username AND `active` = '1'"); $stmt->execute(array(':username' => $username)); @@ -1041,7 +720,6 @@ function verify_tfa_login($username, $token) { global $yubi; global $u2f; global $tfa; - $stmt = $pdo->prepare("SELECT `authmech` FROM `tfa` WHERE `username` = :username AND `active` = '1'"); $stmt->execute(array(':username' => $username)); @@ -1126,237 +804,6 @@ function verify_tfa_login($username, $token) { } return false; } -function edit_domain_admin($postarray) { - global $lang; - global $pdo; - - if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['access_denied']) - ); - return false; - } - // Administrator - if ($_SESSION['mailcow_cc_role'] == "admin") { - if (!is_array($postarray['username'])) { - $usernames = array(); - $usernames[] = $postarray['username']; - } - else { - $usernames = $postarray['username']; - } - foreach ($usernames as $username) { - $is_now = get_domain_admin_details($username); - $domains = (isset($postarray['domains'])) ? (array)$postarray['domains'] : null; - if (!empty($is_now)) { - $active = (isset($postarray['active'])) ? $postarray['active'] : $is_now['active_int']; - $domains = (!empty($domains)) ? $domains : $is_now['selected_domains']; - $username_new = (!empty($postarray['username_new'])) ? $postarray['username_new'] : $is_now['username']; - } - else { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['access_denied']) - ); - return false; - } - $password = $postarray['password']; - $password2 = $postarray['password2']; - - if (!empty($domains)) { - foreach ($domains as $domain) { - if (!is_valid_domain_name($domain)) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['domain_invalid']) - ); - return false; - } - } - } - if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username_new))) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['username_invalid']) - ); - return false; - } - if ($username_new != $username) { - if (!empty(get_domain_admin_details($username_new)['username'])) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['username_invalid']) - ); - return false; - } - } - try { - $stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username"); - $stmt->execute(array( - ':username' => $username, - )); - } - catch (PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - return false; - } - - if (!empty($domains)) { - foreach ($domains as $domain) { - try { - $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`) - VALUES (:username_new, :domain, :created, :active)"); - $stmt->execute(array( - ':username_new' => $username_new, - ':domain' => $domain, - ':created' => date('Y-m-d H:i:s'), - ':active' => $active - )); - } - catch (PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - return false; - } - } - } - - if (!empty($password) && !empty($password2)) { - if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['password_complexity']) - ); - return false; - } - if ($password != $password2) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['password_mismatch']) - ); - return false; - } - $password_hashed = hash_password($password); - try { - $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username"); - $stmt->execute(array( - ':password_hashed' => $password_hashed, - ':username_new' => $username_new, - ':username' => $username, - ':active' => $active - )); - if (isset($postarray['disable_tfa'])) { - $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); - $stmt->execute(array(':username' => $username)); - } - else { - $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username"); - $stmt->execute(array(':username_new' => $username_new, ':username' => $username)); - } - } - catch (PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - return false; - } - } - else { - try { - $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username"); - $stmt->execute(array( - ':username_new' => $username_new, - ':username' => $username, - ':active' => $active - )); - if (isset($postarray['disable_tfa'])) { - $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); - $stmt->execute(array(':username' => $username)); - } - else { - $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username"); - $stmt->execute(array(':username_new' => $username_new, ':username' => $username)); - } - } - catch (PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - return false; - } - } - } - $_SESSION['return'] = array( - 'type' => 'success', - 'msg' => sprintf($lang['success']['domain_admin_modified'], htmlspecialchars(implode(', ', $usernames))) - ); - } - // Domain administrator - // Can only edit itself - elseif ($_SESSION['mailcow_cc_role'] == "domainadmin") { - $username = $_SESSION['mailcow_cc_username']; - $password_old = $postarray['user_old_pass']; - $password_new = $postarray['user_new_pass']; - $password_new2 = $postarray['user_new_pass2']; - - $stmt = $pdo->prepare("SELECT `password` FROM `admin` - WHERE `username` = :user"); - $stmt->execute(array(':user' => $username)); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - if (!verify_ssha256($row['password'], $password_old)) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['access_denied']) - ); - return false; - } - - if (!empty($password_new2) && !empty($password_new)) { - if ($password_new2 != $password_new) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['password_mismatch']) - ); - return false; - } - if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password_new)) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['password_complexity']) - ); - return false; - } - $password_hashed = hash_password($password_new); - try { - $stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed WHERE `username` = :username"); - $stmt->execute(array( - ':password_hashed' => $password_hashed, - ':username' => $username - )); - } - catch (PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - return false; - } - } - - $_SESSION['return'] = array( - 'type' => 'success', - 'msg' => sprintf($lang['success']['domain_admin_modified'], htmlspecialchars($username)) - ); - } -} function get_admin_details() { // No parameter to be given, only one admin should exist global $pdo; @@ -1438,4 +885,4 @@ function get_logs($container, $lines = 100) { } return false; } -?> +?> \ No newline at end of file diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 597d9d87..de8bf0ac 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -879,7 +879,7 @@ function mailbox($_action, $_type, $_data = null) { $alias_domain = idn_to_ascii(strtolower(trim($alias_domain))); $is_now = mailbox('get', 'alias_domain_details', $alias_domain); if (!empty($is_now)) { - $active = (isset($_data['active'])) ? $_data['active'] : $is_now['active_int']; + $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int']; $target_domain = (!empty($_data['target_domain'])) ? idn_to_ascii(strtolower(trim($_data['target_domain']))) : $is_now['target_domain']; } else { @@ -903,7 +903,7 @@ function mailbox($_action, $_type, $_data = null) { ); return false; } - if (empty(mailbox('get', 'domain_details', $target_domain))) { + if (empty(mailbox('get', 'domain_details', $target_domain)) || !empty(mailbox('get', 'alias_domain_details', $target_domain))) { $_SESSION['return'] = array( 'type' => 'danger', 'msg' => sprintf($lang['danger']['target_domain_invalid']) @@ -950,12 +950,10 @@ function mailbox($_action, $_type, $_data = null) { ); return false; } - $tls_enforce_out = intval($_data['tls_enforce_out']); - $tls_enforce_in = intval($_data['tls_enforce_in']); $is_now = mailbox('get', 'tls_policy', $username); if (!empty($is_now)) { - $tls_enforce_in = (isset($_data['tls_enforce_in'])) ? $_data['tls_enforce_in'] : $is_now['tls_enforce_in']; - $tls_enforce_out = (isset($_data['tls_enforce_out'])) ? $_data['tls_enforce_out'] : $is_now['tls_enforce_out']; + $tls_enforce_in = (isset($_data['tls_enforce_in'])) ? intval($_data['tls_enforce_in']) : $is_now['tls_enforce_in']; + $tls_enforce_out = (isset($_data['tls_enforce_out'])) ? intval($_data['tls_enforce_out']) : $is_now['tls_enforce_out']; } else { $_SESSION['return'] = array( @@ -1136,6 +1134,63 @@ function mailbox($_action, $_type, $_data = null) { 'msg' => sprintf($lang['success']['mailbox_modified'], implode(', ', $usernames)) ); break; + case 'domain_ratelimit': + $rl_value = intval($_data['rl_value']); + $rl_frame = $_data['rl_frame']; + if (!in_array($rl_frame, array('s', 'm', 'h'))) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Ratelimit time frame is incorrect' + ); + return false; + } + if (!is_array($_data['domain'])) { + $domains = array(); + $domains[] = $_data['domain']; + } + else { + $domains = $_data['domain']; + } + foreach ($domains as $domain) { + if (!is_valid_domain_name($domain) || !hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + if (empty($rl_value)) { + try { + $redis->hDel('RL_OBJECT', $domain); + $redis->hDel('RL_VALUE', $domain); + } + catch (RedisException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Redis: '.$e + ); + return false; + } + } + else { + try { + $redis->hSet('RL_OBJECT', $domain, '1'); + $redis->hSet('RL_VALUE', $domain, $rl_value . ' / 1' . $rl_frame); + } + catch (RedisException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Redis: '.$e + ); + return false; + } + } + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => sprintf($lang['success']['domain_modified'], implode(', ', $domains)) + ); + break; case 'syncjob': if (!is_array($_data['id'])) { $ids = array(); @@ -1149,9 +1204,9 @@ function mailbox($_action, $_type, $_data = null) { if (!empty($is_now)) { $username = $is_now['user2']; $user1 = (!empty($_data['user1'])) ? $_data['user1'] : $is_now['user1']; - $active = (isset($_data['active'])) ? $_data['active'] : $is_now['active_int']; - $delete2duplicates = (isset($_data['delete2duplicates'])) ? $_data['delete2duplicates'] : $is_now['delete2duplicates']; - $delete1 = (isset($_data['delete1'])) ? $_data['delete1'] : $is_now['delete1']; + $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int']; + $delete2duplicates = (isset($_data['delete2duplicates'])) ? intval($_data['delete2duplicates']) : $is_now['delete2duplicates']; + $delete1 = (isset($_data['delete1'])) ? intval($_data['delete1']) : $is_now['delete1']; $port1 = (!empty($_data['port1'])) ? $_data['port1'] : $is_now['port1']; $password1 = (!empty($_data['password1'])) ? $_data['password1'] : $is_now['password1']; $host1 = (!empty($_data['host1'])) ? $_data['host1'] : $is_now['host1']; @@ -1253,7 +1308,7 @@ function mailbox($_action, $_type, $_data = null) { foreach ($addresses as $address) { $is_now = mailbox('get', 'alias_details', $address); if (!empty($is_now)) { - $active = (isset($_data['active'])) ? $_data['active'] : $is_now['active_int']; + $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int']; $goto = (!empty($_data['goto'])) ? $_data['goto'] : $is_now['goto']; } else { @@ -1383,9 +1438,9 @@ function mailbox($_action, $_type, $_data = null) { elseif ($_SESSION['mailcow_cc_role'] == "admin") { $is_now = mailbox('get', 'domain_details', $domain); if (!empty($is_now)) { - $active = (isset($_data['active'])) ? $_data['active'] : $is_now['active_int']; - $backupmx = (isset($_data['backupmx'])) ? $_data['backupmx'] : $is_now['backupmx_int']; - $relay_all_recipients = (isset($_data['relay_all_recipients'])) ? $_data['relay_all_recipients'] : $is_now['relay_all_recipients_int']; + $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int']; + $backupmx = (isset($_data['backupmx'])) ? intval($_data['backupmx']) : $is_now['backupmx_int']; + $relay_all_recipients = (isset($_data['relay_all_recipients'])) ? intval($_data['relay_all_recipients']) : $is_now['relay_all_recipients_int']; $aliases = (!empty($_data['aliases'])) ? $_data['aliases'] : $is_now['max_num_aliases_for_domain']; $mailboxes = (!empty($_data['mailboxes'])) ? $_data['mailboxes'] : $is_now['max_num_mboxes_for_domain']; $maxquota = (!empty($_data['maxquota'])) ? $_data['maxquota'] : ($is_now['max_quota_for_mbox'] / 1048576); @@ -1524,7 +1579,7 @@ function mailbox($_action, $_type, $_data = null) { } $is_now = mailbox('get', 'mailbox_details', $username); if (!empty($is_now)) { - $active = (isset($_data['active'])) ? $_data['active'] : $is_now['active_int']; + $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int']; $name = (!empty($_data['name'])) ? $_data['name'] : $is_now['name']; $domain = $is_now['domain']; $quota_m = (!empty($_data['quota'])) ? $_data['quota'] : ($is_now['quota'] / 1048576); @@ -1588,19 +1643,15 @@ function mailbox($_action, $_type, $_data = null) { mailbox('get', 'sender_acl_handles', $username)['sender_acl_addresses']['ro'] ); // Get sender_acl items from POST array - $sender_acl_domain_admin = ($_data['sender_acl'] == "0") ? array() : $_data['sender_acl']; + $sender_acl_domain_admin = ($_data['sender_acl'] == "0") ? array() : (array)$_data['sender_acl']; if (!empty($sender_acl_domain_admin) || !empty($sender_acl_admin)) { - // Check items in POST array - foreach ($sender_acl_domain_admin as $sender_acl) { - if (!filter_var($sender_acl, FILTER_VALIDATE_EMAIL) && !is_valid_domain_name(ltrim($sender_acl, '@'))) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['sender_acl_invalid']) - ); - return false; + // Check items in POST array and skip invalid + foreach ($sender_acl_domain_admin as $key => $val) { + if (!filter_var($val, FILTER_VALIDATE_EMAIL) && !is_valid_domain_name(ltrim($val, '@'))) { + unset($sender_acl_domain_admin[$key]); } - if (is_valid_domain_name(ltrim($sender_acl, '@'))) { - if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], ltrim($sender_acl, '@'))) { + if (is_valid_domain_name(ltrim($val, '@'))) { + if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], ltrim($val, '@'))) { $_SESSION['return'] = array( 'type' => 'danger', 'msg' => sprintf($lang['danger']['sender_acl_invalid']) @@ -1608,8 +1659,8 @@ function mailbox($_action, $_type, $_data = null) { return false; } } - if (filter_var($sender_acl, FILTER_VALIDATE_EMAIL)) { - if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $sender_acl)) { + if (filter_var($val, FILTER_VALIDATE_EMAIL)) { + if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $val)) { $_SESSION['return'] = array( 'type' => 'danger', 'msg' => sprintf($lang['danger']['sender_acl_invalid']) @@ -1761,8 +1812,8 @@ function mailbox($_action, $_type, $_data = null) { foreach ($names as $name) { $is_now = mailbox('get', 'resource_details', $name); if (!empty($is_now)) { - $active = (isset($_data['active'])) ? $_data['active'] : $is_now['active_int']; - $multiple_bookings = (isset($_data['multiple_bookings'])) ? $_data['multiple_bookings'] : $is_now['multiple_bookings_int']; + $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int']; + $multiple_bookings = (isset($_data['multiple_bookings'])) ? intval($_data['multiple_bookings']) : $is_now['multiple_bookings_int']; $description = (!empty($_data['description'])) ? $_data['description'] : $is_now['description']; $kind = (!empty($_data['kind'])) ? $_data['kind'] : $is_now['kind']; } @@ -2267,6 +2318,31 @@ function mailbox($_action, $_type, $_data = null) { } return $aliases; break; + case 'domain_ratelimit': + $aliases = array(); + if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) { + return false; + } + try { + if (($rl_value = $redis->hGet('RL_VALUE', $_data)) && $redis->hGet('RL_OBJECT', $_data)) { + $rl = explode(' / 1', $rl_value); + $data['value'] = $rl[0]; + $data['frame'] = $rl[1]; + return $data; + } + else { + return false; + } + } + catch (RedisException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Redis: '.$e + ); + return false; + } + return false; + break; case 'alias_details': $aliasdata = array(); try { diff --git a/data/web/inc/prerequisites.inc.php b/data/web/inc/prerequisites.inc.php index e00fd31a..ebc453f8 100644 --- a/data/web/inc/prerequisites.inc.php +++ b/data/web/inc/prerequisites.inc.php @@ -61,6 +61,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/lang/lang.en.php'; include $_SERVER['DOCUMENT_ROOT'] . '/lang/lang.'.$_SESSION['mailcow_locale'].'.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.mailbox.inc.php'; +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.domain_admin.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.policy.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.dkim.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fwdhost.inc.php'; diff --git a/data/web/inc/sessions.inc.php b/data/web/inc/sessions.inc.php index e2919f7a..d23d2cba 100644 --- a/data/web/inc/sessions.inc.php +++ b/data/web/inc/sessions.inc.php @@ -1,6 +1,8 @@ diff --git a/data/web/js/api.js b/data/web/js/api.js index ec26709a..1fd962fb 100644 --- a/data/web/js/api.js +++ b/data/web/js/api.js @@ -64,8 +64,23 @@ $(document).ready(function() { // If clicked element #edit_selected is in a form with the same data-id as the button, // we merge all input fields by {"name":"value"} into api-attr if ($(this).closest("form").data('id') == id) { - var attr_to_merge = $(this).closest("form").serializeObject(); - var api_attr = $.extend(api_attr, attr_to_merge) + var req_empty = false; + $(this).closest("form").find('select, textarea, input').each(function() { + if ($(this).prop('required')) { + if (!$(this).val()) { + req_empty = true; + $(this).addClass('inputMissingAttr'); + } else { + $(this).removeClass('inputMissingAttr'); + } + } + }); + if (!req_empty) { + var attr_to_merge = $(this).closest("form").serializeObject(); + var api_attr = $.extend(api_attr, attr_to_merge) + } else { + return false; + } } // If clicked element #edit_selected has data-item attribute, it is added to "items" if (typeof $(this).data('item') !== 'undefined') { @@ -77,6 +92,7 @@ $(document).ready(function() { } if (typeof multi_data[id] == "undefined") return; api_items = multi_data[id]; + // alert(JSON.stringify(api_attr)); if (Object.keys(api_items).length !== 0) { $.ajax({ type: "POST", diff --git a/data/web/json_api.php b/data/web/json_api.php index e4086520..56d4e741 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -426,7 +426,7 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u case "domain-admin": if (isset($_POST['attr'])) { $attr = (array)json_decode($_POST['attr'], true); - if (add_domain_admin($attr) === false) { + if (domain_admin('add', $attr) === false) { if (isset($_SESSION['return'])) { echo json_encode($_SESSION['return']); } @@ -826,10 +826,10 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u case "domain-admin": switch ($object) { case "all": - $domain_admins = get_domain_admins(); + $domain_admins = domain_admin('get'); if (!empty($domain_admins)) { foreach ($domain_admins as $domain_admin) { - if ($details = get_domain_admin_details($domain_admin)) { + if ($details = domain_admin('details', $domain_admin)) { $data[] = $details; } else { @@ -849,7 +849,7 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u break; default: - $data = get_domain_admin_details($object); + $data = domain_admin('details', $object); if (!isset($data) || empty($data)) { echo '{}'; } @@ -1385,7 +1385,7 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u if (isset($_POST['items'])) { $items = (array)json_decode($_POST['items'], true); if (is_array($items)) { - if (delete_domain_admin(array('username' => $items)) === false) { + if (domain_admin('delete', array('username' => $items)) === false) { if (isset($_SESSION['return'])) { echo json_encode($_SESSION['return']); } @@ -1603,6 +1603,7 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u } break; case "mailbox": + // sender_acl:0 removes all entries if (isset($_POST['items']) && isset($_POST['attr'])) { $items = (array)json_decode($_POST['items'], true); $attr = (array)json_decode($_POST['attr'], true); @@ -1778,6 +1779,50 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u )); } break; + case "domain-ratelimit": + if (isset($_POST['items']) && isset($_POST['attr'])) { + $items = (array)json_decode($_POST['items'], true); + $attr = (array)json_decode($_POST['attr'], true); + $postarray = array_merge(array('domain' => $items), $attr); + if (is_array($postarray['domain'])) { + if (mailbox('edit', 'domain_ratelimit', $postarray) === false) { + if (isset($_SESSION['return'])) { + echo json_encode($_SESSION['return']); + } + else { + echo json_encode(array( + 'type' => 'error', + 'msg' => 'Edit failed' + )); + } + exit(); + } + else { + if (isset($_SESSION['return'])) { + echo json_encode($_SESSION['return']); + } + else { + echo json_encode(array( + 'type' => 'success', + 'msg' => 'Task completed' + )); + } + } + } + else { + echo json_encode(array( + 'type' => 'error', + 'msg' => 'Incomplete post data' + )); + } + } + else { + echo json_encode(array( + 'type' => 'error', + 'msg' => 'Incomplete post data' + )); + } + break; case "alias-domain": if (isset($_POST['items']) && isset($_POST['attr'])) { $items = (array)json_decode($_POST['items'], true); @@ -1822,7 +1867,7 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u )); } break; - case "spam_score": + case "spam-score": if (isset($_POST['items']) && isset($_POST['attr'])) { $items = (array)json_decode($_POST['items'], true); $attr = (array)json_decode($_POST['attr'], true); @@ -1872,7 +1917,7 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u $attr = (array)json_decode($_POST['attr'], true); $postarray = array_merge(array('username' => $items), $attr); if (is_array($postarray['username'])) { - if (edit_domain_admin($postarray) === false) { + if (domain_admin('edit', $postarray) === false) { if (isset($_SESSION['return'])) { echo json_encode($_SESSION['return']); } @@ -1989,39 +2034,109 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u )); } break; - case "admin": - // No items as there is only one admin - if (isset($_POST['attr'])) { - $attr = (array)json_decode($_POST['attr'], true); - if (edit_admin_account($attr) === false) { - if (isset($_SESSION['return'])) { - echo json_encode($_SESSION['return']); + case "self": + // No items, logged-in user, users and domain admins + if ($_SESSION['mailcow_cc_role'] == "domainadmin") { + if (isset($_POST['attr'])) { + $attr = (array)json_decode($_POST['attr'], true); + if (domain_admin('edit', $attr) === false) { + if (isset($_SESSION['return'])) { + echo json_encode($_SESSION['return']); + } + else { + echo json_encode(array( + 'type' => 'error', + 'msg' => 'Edit failed' + )); + } + exit(); } else { - echo json_encode(array( - 'type' => 'error', - 'msg' => 'Edit failed' - )); + if (isset($_SESSION['return'])) { + echo json_encode($_SESSION['return']); + } + else { + echo json_encode(array( + 'type' => 'success', + 'msg' => 'Task completed' + )); + } } - exit(); } else { - if (isset($_SESSION['return'])) { - echo json_encode($_SESSION['return']); - } - else { - echo json_encode(array( - 'type' => 'success', - 'msg' => 'Task completed' - )); - } + echo json_encode(array( + 'type' => 'error', + 'msg' => 'Incomplete post data' + )); } } - else { - echo json_encode(array( - 'type' => 'error', - 'msg' => 'Incomplete post data' - )); + elseif ($_SESSION['mailcow_cc_role'] == "user") { + if (isset($_POST['attr'])) { + $attr = (array)json_decode($_POST['attr'], true); + if (edit_user_account($attr) === false) { + if (isset($_SESSION['return'])) { + echo json_encode($_SESSION['return']); + } + else { + echo json_encode(array( + 'type' => 'error', + 'msg' => 'Edit failed' + )); + } + exit(); + } + else { + if (isset($_SESSION['return'])) { + echo json_encode($_SESSION['return']); + } + else { + echo json_encode(array( + 'type' => 'success', + 'msg' => 'Task completed' + )); + } + } + } + else { + echo json_encode(array( + 'type' => 'error', + 'msg' => 'Incomplete post data' + )); + } + } + elseif ($_SESSION['mailcow_cc_role'] == "admin") { + if (isset($_POST['attr'])) { + $attr = (array)json_decode($_POST['attr'], true); + if (edit_admin_account($attr) === false) { + if (isset($_SESSION['return'])) { + echo json_encode($_SESSION['return']); + } + else { + echo json_encode(array( + 'type' => 'error', + 'msg' => 'Edit failed' + )); + } + exit(); + } + else { + if (isset($_SESSION['return'])) { + echo json_encode($_SESSION['return']); + } + else { + echo json_encode(array( + 'type' => 'success', + 'msg' => 'Task completed' + )); + } + } + } + else { + echo json_encode(array( + 'type' => 'error', + 'msg' => 'Incomplete post data' + )); + } } break; } diff --git a/data/web/user.php b/data/web/user.php index 77ccc62d..b283894c 100644 --- a/data/web/user.php +++ b/data/web/user.php @@ -357,7 +357,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "use