From 32176c1a3222beccd0067708d4eb78b5a3ccaa77 Mon Sep 17 00:00:00 2001 From: andryyy Date: Mon, 27 Mar 2017 22:33:32 +0200 Subject: [PATCH 01/28] Add hint for local MTA and port blocking issues --- docs/first_steps.md | 10 ++++++++++ docs/install.md | 2 ++ 2 files changed, 12 insertions(+) diff --git a/docs/first_steps.md b/docs/first_steps.md index 7f835bc4..21c418db 100644 --- a/docs/first_steps.md +++ b/docs/first_steps.md @@ -136,6 +136,16 @@ server { } ``` +## Install a local MTA + +The easiest option would be to disable the listener on port 25/tcp. + +**Postfix** users disable the listener by commenting the following line (starting with `smtp` or `25`) in `/etc/postfix/master.cf`: +``` +#smtp inet n - - - - smtpd +``` +Restart Postfix after applying your changes. + ## Sender and receiver model When a mailbox is created, a user is allowed to send mail from and receive mail for his own mailbox address. diff --git a/docs/install.md b/docs/install.md index 7c383a63..6833f323 100644 --- a/docs/install.md +++ b/docs/install.md @@ -35,6 +35,8 @@ nano mailcow.conf ``` If you plan to use a reverse proxy, you can, for example, bind HTTPS to 127.0.0.1 on port 8443 and HTTP to 127.0.0.1 on port 8080. +You may need to stop an existing pre-installed MTA which blocks port 25/tcp. See [this chapter](https://andryyy.github.io/mailcow-dockerized/first_steps/#install-a-local-mta) to learn how to reconfigure Postfix to run besides mailcow after a successful installation. + 5\. Pull the images and run the composer file. The paramter `-d` will start mailcow: dockerized detached: ``` docker-compose pull From d3c0d0c16e64bbb8aa969a6cac1d5eeb8af3bc71 Mon Sep 17 00:00:00 2001 From: andryyy Date: Tue, 28 Mar 2017 11:47:03 +0200 Subject: [PATCH 02/28] Use footable for domain admins table --- data/web/admin.php | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/data/web/admin.php b/data/web/admin.php index 5377616f..e0e04aee 100644 --- a/data/web/admin.php +++ b/data/web/admin.php @@ -81,14 +81,14 @@ $tfa_data = get_tfa();
- +
- - - - - + + + + + @@ -299,8 +299,14 @@ $tfa_data = get_tfa(); - - + + Date: Tue, 28 Mar 2017 11:48:39 +0200 Subject: [PATCH 03/28] Show aliases left, check if quota 0 when creating domains, return modified instead of created for domain admin --- data/web/inc/functions.inc.php | 70 +++++++++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 6 deletions(-) diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index c37e39ab..cbc71102 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -1757,6 +1757,7 @@ function get_domain_admin_details($domain_admin) { 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`, @@ -1768,11 +1769,15 @@ function get_domain_admin_details($domain_admin) { ':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['active_int'] = $row['active_int']; $domainadmindata['tfa_active_int'] = $row['tfa_active_int']; - $domainadmindata['created'] = $row['created']; + $domainadmindata['active_int'] = $row['active_int']; + $domainadmindata['modified'] = $row['created']; // GET SELECTED $stmt = $pdo->prepare("SELECT `domain` FROM `domain` WHERE `domain` IN ( @@ -1793,6 +1798,9 @@ function get_domain_admin_details($domain_admin) { while($row = array_shift($rows)) { $domainadmindata['unselected_domains'][] = $row['domain']; } + if (!isset($domainadmindata['unselected_domains'])) { + $domainadmindata['unselected_domains'] = ""; + } } catch(PDOException $e) { $_SESSION['return'] = array( @@ -2134,6 +2142,14 @@ function edit_domain_admin($postarray) { } } + if (empty($postarray['domain'])) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['domain_invalid']) + ); + return false; + } + if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) { $_SESSION['return'] = array( 'type' => 'danger', @@ -2164,7 +2180,7 @@ function edit_domain_admin($postarray) { return false; } - if(isset($postarray['domain'])) { + if (isset($postarray['domain'])) { foreach ($postarray['domain'] as $domain) { try { $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`) @@ -2519,6 +2535,14 @@ function mailbox_add_domain($postarray) { return false; } + if ($maxquota == "0" || empty($maxquota)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['maxquota_empty']) + ); + return false; + } + isset($postarray['active']) ? $active = '1' : $active = '0'; isset($postarray['relay_all_recipients']) ? $relay_all_recipients = '1' : $relay_all_recipients = '0'; isset($postarray['backupmx']) ? $backupmx = '1' : $backupmx = '0'; @@ -2623,6 +2647,18 @@ function mailbox_add_alias($postarray) { return false; } + $stmt = $pdo->prepare("SELECT `address` FROM `alias` + WHERE `address`= :address"); + $stmt->execute(array(':address' => $address)); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results != 0) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['is_alias_or_mailbox'], htmlspecialchars($address)) + ); + return false; + } + foreach ($addresses as $address) { if (empty($address)) { continue; @@ -2632,6 +2668,15 @@ function mailbox_add_alias($postarray) { $local_part = strstr($address, '@', true); $address = $local_part.'@'.$domain; + $domaindata = mailbox_get_domain_details($domain); + if ($domaindata['aliases_left'] == 0) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['max_alias_exceeded']) + ); + return false; + } + try { $stmt = $pdo->prepare("SELECT `domain` FROM `domain` WHERE `domain`= :domain1 OR `domain` = (SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain2)"); @@ -3432,7 +3477,7 @@ function mailbox_edit_domain($postarray) { // aliases float // mailboxes float // maxquota float - // quota float (Byte) + // quota float (Byte) // active int global $lang; @@ -3519,6 +3564,14 @@ function mailbox_edit_domain($postarray) { return false; } + if ($maxquota == "0" || empty($maxquota)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['maxquota_empty']) + ); + return false; + } + if ($MailboxData['maxquota'] > $maxquota) { $_SESSION['return'] = array( 'type' => 'danger', @@ -4271,6 +4324,10 @@ function mailbox_get_domain_details($domain) { ':domain' => $domain, )); $row = $stmt->fetch(PDO::FETCH_ASSOC); + if (empty($row)) { + return false; + } + $stmt = $pdo->prepare("SELECT COUNT(*) AS `count`, COALESCE(SUM(`quota`), 0) as `in_use` FROM `mailbox` WHERE `kind` NOT REGEXP 'location|thing|group' AND `domain` = :domain"); $stmt->execute(array(':domain' => $row['domain'])); $MailboxDataDomain = $stmt->fetch(PDO::FETCH_ASSOC); @@ -4303,8 +4360,9 @@ function mailbox_get_domain_details($domain) { $stmt->execute(array( ':domain' => $domain, )); - $AliasData = $stmt->fetch(PDO::FETCH_ASSOC); - (isset($AliasData['alias_count'])) ? $domaindata['aliases_in_domain'] = $AliasData['alias_count'] : $domaindata['aliases_in_domain'] = "0"; + $AliasDataDomain = $stmt->fetch(PDO::FETCH_ASSOC); + (isset($AliasDataDomain['alias_count'])) ? $domaindata['aliases_in_domain'] = $AliasDataDomain['alias_count'] : $domaindata['aliases_in_domain'] = "0"; + $domaindata['aliases_left'] = $row['aliases'] - $AliasDataDomain['alias_count']; } catch (PDOException $e) { $_SESSION['return'] = array( From 297674d2569a713ef2d6426e66830256f9397835 Mon Sep 17 00:00:00 2001 From: andryyy Date: Tue, 28 Mar 2017 11:51:17 +0200 Subject: [PATCH 04/28] Various CSS fixes, remove sorttable, better API format --- data/web/css/mailbox.css | 28 +-- data/web/css/tables.css | 79 --------- data/web/inc/footer.inc.php | 12 +- data/web/inc/header.inc.php | 2 - data/web/js/add.js | 2 +- data/web/js/admin.js | 69 ++++---- data/web/js/mailbox.js | 22 +-- data/web/js/sorttable.js | 236 ------------------------- data/web/json_api.php | 336 ++++++++++++++++++++++-------------- data/web/lang/lang.de.php | 5 + data/web/lang/lang.en.php | 5 + data/web/mailbox.php | 34 +--- data/web/user.php | 4 +- 13 files changed, 293 insertions(+), 541 deletions(-) delete mode 100644 data/web/css/tables.css delete mode 100644 data/web/js/sorttable.js diff --git a/data/web/css/mailbox.css b/data/web/css/mailbox.css index b5c69343..23f5ad84 100644 --- a/data/web/css/mailbox.css +++ b/data/web/css/mailbox.css @@ -1,19 +1,19 @@ -.panel-heading div { - margin-top: -18px; - font-size: 15px; +table.footable>tbody>tr.footable-empty>td { + font-size:15px !important; + font-style:italic; } -.panel-heading div span { - margin-left:5px; +.pagination a { + text-decoration: none !important; } -.panel-body { - display: none; +.panel panel-default { + overflow: visible !important; } -.clickable { - cursor: pointer; +.table-responsive { + overflow: visible !important; } -.progress { - margin-bottom: 0px; -} -.table>thead>tr>th { - vertical-align: top !important; +.footer-add-item { + text-align:center; + font-style: italic; + display:block; + padding: 10px; } \ No newline at end of file diff --git a/data/web/css/tables.css b/data/web/css/tables.css deleted file mode 100644 index 651e1665..00000000 --- a/data/web/css/tables.css +++ /dev/null @@ -1,79 +0,0 @@ -ul[id*="sortable"] { word-wrap: break-word; list-style-type: none; float: left; padding: 0 15px 0 0; width: 48%; cursor:move} -ul[id$="sortable-active"] li {cursor:move; } -ul[id$="sortable-inactive"] li {cursor:move } -.list-heading { cursor:default !important} -.ui-state-disabled { cursor:no-drop; color:#ccc; } -.ui-state-highlight {background: #F5F5F5 !important; height: 41px !important; cursor:move } -table[data-sortable] { - border-collapse: collapse; - border-spacing: 0; -} -table[data-sortable] th { - vertical-align: bottom; - font-weight: bold; -} -table[data-sortable] th, table[data-sortable] td { - text-align: left; - padding: 10px; -} -table[data-sortable] th:not([data-sortable="false"]) { - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - -o-user-select: none; - user-select: none; - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); - -webkit-touch-callout: none; - cursor: pointer; -} -table[data-sortable] th:after { - content: ""; - visibility: hidden; - display: inline-block; - vertical-align: inherit; - height: 0; - width: 0; - border-width: 5px; - border-style: solid; - border-color: transparent; - margin-right: 1px; - margin-left: 10px; - float: right; -} -table[data-sortable] th[data-sortable="false"]:after { - display: none; -} -table[data-sortable] th[data-sorted="true"]:after { - visibility: visible; -} -table[data-sortable] th[data-sorted-direction="descending"]:after { - border-top-color: inherit; - margin-top: 8px; -} -table[data-sortable] th[data-sorted-direction="ascending"]:after { - border-bottom-color: inherit; - margin-top: 3px; -} -table[data-sortable].sortable-theme-bootstrap thead th { - border-bottom: 2px solid #e0e0e0; -} -table[data-sortable].sortable-theme-bootstrap th[data-sorted="true"] { - color: #3a87ad; - background: #d9edf7; - border-bottom-color: #bce8f1; -} -table[data-sortable].sortable-theme-bootstrap th[data-sorted="true"][data-sorted-direction="descending"]:after { - border-top-color: #3a87ad; -} -table[data-sortable].sortable-theme-bootstrap th[data-sorted="true"][data-sorted-direction="ascending"]:after { - border-bottom-color: #3a87ad; -} -table[data-sortable].sortable-theme-bootstrap.sortable-theme-bootstrap-striped tbody > tr:nth-child(odd) > td { - background-color: #f9f9f9; -} -#data td, #no-data td { - vertical-align: middle; -} -.sort-table:hover { - border-bottom-color: #00B7DC !important; -} \ No newline at end of file diff --git a/data/web/inc/footer.inc.php b/data/web/inc/footer.inc.php index 5523ec58..580c457f 100644 --- a/data/web/inc/footer.inc.php +++ b/data/web/inc/footer.inc.php @@ -50,11 +50,7 @@ $(document).ready(function() { type: "GET", cache: false, dataType: 'script', - url: "json_api.php", - data: { - 'action':'get_u2f_auth_challenge', - 'object':'', - }, + url: "/api/v1/u2f-authentication/", success: function(data){ data; } @@ -87,11 +83,7 @@ $(document).ready(function() { type: "GET", cache: false, dataType: 'script', - url: "json_api.php", - data: { - 'action':'get_u2f_reg_challenge', - 'object':'', - }, + url: "/api/v1/u2f-registration/", success: function(data){ data; } diff --git a/data/web/inc/header.inc.php b/data/web/inc/header.inc.php index a206a35d..a7ec1c52 100644 --- a/data/web/inc/header.inc.php +++ b/data/web/inc/header.inc.php @@ -16,10 +16,8 @@ - - ' : null;?> diff --git a/data/web/js/add.js b/data/web/js/add.js index 1ebcdbb2..54a70500 100644 --- a/data/web/js/add.js +++ b/data/web/js/add.js @@ -2,7 +2,7 @@ $(document).ready(function() { // add.php // Get max. possible quota for a domain when domain field changes $('#addSelectDomain').on('change', function() { - $.get("json_api.php", { action:"get_domain_details", object:this.value }, function(data){ + $.get("/api/v1/domain/" + this.value, function(data){ var result = jQuery.parseJSON( data ); max_new_mailbox_quota = ( result.max_new_mailbox_quota / 1048576); if (max_new_mailbox_quota != '0') { diff --git a/data/web/js/admin.js b/data/web/js/admin.js index a235a422..feaf3907 100644 --- a/data/web/js/admin.js +++ b/data/web/js/admin.js @@ -1,31 +1,42 @@ $(document).ready(function() { - // Postfix restrictions, drag and drop functions - $( "[id*=srr-sortable]" ).sortable({ - items: "li:not(.list-heading)", - cancel: ".ui-state-disabled", - connectWith: "[id*=srr-sortable]", - dropOnEmpty: true, - placeholder: "ui-state-highlight" - }); - $( "[id*=ssr-sortable]" ).sortable({ - items: "li:not(.list-heading)", - cancel: ".ui-state-disabled", - connectWith: "[id*=ssr-sortable]", - dropOnEmpty: true, - placeholder: "ui-state-highlight" - }); - $('#srr_form').submit(function(){ - var srr_joined_vals = $("[id^=srr-sortable-active] li").map(function() { - return $(this).data("value"); - }).get().join(', '); - var input = $("").attr("type", "hidden").attr("name", "srr_value").val(srr_joined_vals); - $('#srr_form').append($(input)); - }); - $('#ssr_form').submit(function(){ - var ssr_joined_vals = $("[id^=ssr-sortable-active] li").map(function() { - return $(this).data("value"); - }).get().join(', '); - var input = $("").attr("type", "hidden").attr("name", "ssr_value").val(ssr_joined_vals); - $('#ssr_form').append($(input)); - }); + $.ajax({ + dataType: 'json', + url: '/api/v1/domain-admin/all', + jsonp: false, + error: function () { + alert('Cannot draw domain administrator table'); + }, + success: function (data) { + $.each(data, function (i, item) { + item.action = ''; + }); + $('#domainadminstable').footable({ + "columns": [ + {"sorted": true,"name":"username","title":lang.username,"style":{"width":"250px"}}, + {"name":"selected_domains","title":lang.admin_domains,"breakpoints":"xs sm"}, + {"name":"tfa_active","title":"TFA", "filterable": false,"style":{"maxWidth":"80px","width":"80px"}}, + {"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active}, + {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"180px","width":"180px"},"type":"html","title":lang.action,"breakpoints":"xs sm"} + ], + "rows": data, + "empty": lang.empty, + "paging": { + "enabled": true, + "limit": 5, + "size": pagination_size + }, + "filtering": { + "enabled": true, + "position": "left", + "placeholder": lang.filter_table + }, + "sorting": { + "enabled": true + } + }); + } + }); }); \ No newline at end of file diff --git a/data/web/js/mailbox.js b/data/web/js/mailbox.js index 1ddf0c35..4e6b303d 100644 --- a/data/web/js/mailbox.js +++ b/data/web/js/mailbox.js @@ -15,7 +15,7 @@ $(document).ready(function() { $.ajax({ dataType: 'json', - url: '/json_api.php?action=domain_table_data', + url: '/api/v1/domain/all', jsonp: false, error: function () { alert('Cannot draw domain table'); @@ -70,7 +70,7 @@ $(document).ready(function() { $.ajax({ dataType: 'json', - url: '/json_api.php?action=mailbox_table_data', + url: '/api/v1/mailbox/all', jsonp: false, error: function () { alert('Cannot draw mailbox table'); @@ -102,12 +102,12 @@ $(document).ready(function() { {"sorted": true,"name":"username","title":lang.username,"style":{"width":"250px"}}, {"name":"name","title":lang.fname,"breakpoints":"xs sm"}, {"name":"domain","title":lang.domain,"breakpoints":"xs sm"}, - {"name":"quota","title":lang.domain_quota}, - {"name":"spam_aliases","filterable": false,"title":lang.spam_aliases,"breakpoints":"xs sm"}, - {"name":"in_use","filterable": false,"type":"html","title":lang.in_use}, - {"name":"messages","filterable": false,"style":{"width":"90px"},"title":lang.msg_num,"breakpoints":"xs sm"}, - {"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active}, - {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","width":"290px"},"type":"html","title":lang.action,"breakpoints":"xs sm"} + {"name":"quota","style":{"whiteSpace":"nowrap"},"title":lang.domain_quota}, + {"name":"spam_aliases","filterable": false,"title":lang.spam_aliases,"breakpoints":"xs sm md"}, + {"name":"in_use","filterable": false,"style":{"whiteSpace":"nowrap"},"type":"html","title":lang.in_use}, + {"name":"messages","filterable": false,"style":{"whiteSpace":"nowrap"},"title":lang.msg_num,"breakpoints":"xs sm md"}, + {"name":"active","filterable": false,"style":{"whiteSpace":"nowrap"},"title":lang.active}, + {"name":"action","filterable": false,"sortable": false,"style":{"whiteSpace":"nowrap","text-align":"right","width":"290px"},"type":"html","title":lang.action,"breakpoints":"xs sm md"} ], "empty": lang.empty, "rows": data, @@ -130,7 +130,7 @@ $(document).ready(function() { $.ajax({ dataType: 'json', - url: '/json_api.php?action=resource_table_data', + url: '/api/v1/resource/all', jsonp: false, error: function () { alert('Cannot draw resource table'); @@ -172,7 +172,7 @@ $(document).ready(function() { $.ajax({ dataType: 'json', - url: '/json_api.php?action=domain_alias_table_data', + url: '/api/v1/alias-domain/all', jsonp: false, error: function () { alert('Cannot draw alias domain table'); @@ -212,7 +212,7 @@ $(document).ready(function() { $.ajax({ dataType: 'json', - url: '/json_api.php?action=alias_table_data', + url: '/api/v1/alias/all', jsonp: false, error: function () { alert('Cannot draw alias table'); diff --git a/data/web/js/sorttable.js b/data/web/js/sorttable.js deleted file mode 100644 index cb3e293c..00000000 --- a/data/web/js/sorttable.js +++ /dev/null @@ -1,236 +0,0 @@ -(function() { - var SELECTOR, addEventListener, clickEvents, numberRegExp, sortable, touchDevice, trimRegExp; - - SELECTOR = 'table[data-sortable]'; - - numberRegExp = /^-?[£$¤]?[\d,.]+%?$/; - - trimRegExp = /^\s+|\s+$/g; - - clickEvents = ['click']; - - touchDevice = 'ontouchstart' in document.documentElement; - - if (touchDevice) { - clickEvents.push('touchstart'); - } - - addEventListener = function(el, event, handler) { - if (el.addEventListener != null) { - return el.addEventListener(event, handler, false); - } else { - return el.attachEvent("on" + event, handler); - } - }; - - sortable = { - init: function(options) { - var table, tables, _i, _len, _results; - if (options == null) { - options = {}; - } - if (options.selector == null) { - options.selector = SELECTOR; - } - tables = document.querySelectorAll(options.selector); - _results = []; - for (_i = 0, _len = tables.length; _i < _len; _i++) { - table = tables[_i]; - _results.push(sortable.initTable(table)); - } - return _results; - }, - initTable: function(table) { - var i, th, ths, _i, _len, _ref; - if (((_ref = table.tHead) != null ? _ref.rows.length : void 0) !== 1) { - return; - } - if (table.getAttribute('data-sortable-initialized') === 'true') { - return; - } - table.setAttribute('data-sortable-initialized', 'true'); - ths = table.querySelectorAll('th'); - for (i = _i = 0, _len = ths.length; _i < _len; i = ++_i) { - th = ths[i]; - if (th.getAttribute('data-sortable') !== 'false') { - sortable.setupClickableTH(table, th, i); - } - } - return table; - }, - setupClickableTH: function(table, th, i) { - var eventName, onClick, type, _i, _len, _results; - type = sortable.getColumnType(table, i); - onClick = function(e) { - var compare, item, newSortedDirection, position, row, rowArray, sorted, sortedDirection, tBody, ths, value, _compare, _i, _j, _k, _l, _len, _len1, _len2, _len3, _len4, _m, _ref, _ref1; - if (e.handled !== true) { - e.handled = true; - } else { - return false; - } - sorted = this.getAttribute('data-sorted') === 'true'; - sortedDirection = this.getAttribute('data-sorted-direction'); - if (sorted) { - newSortedDirection = sortedDirection === 'ascending' ? 'descending' : 'ascending'; - } else { - newSortedDirection = type.defaultSortDirection; - } - ths = this.parentNode.querySelectorAll('th'); - for (_i = 0, _len = ths.length; _i < _len; _i++) { - th = ths[_i]; - th.setAttribute('data-sorted', 'false'); - th.removeAttribute('data-sorted-direction'); - } - this.setAttribute('data-sorted', 'true'); - this.setAttribute('data-sorted-direction', newSortedDirection); - tBody = table.tBodies[0]; - rowArray = []; - if (!sorted) { - if (type.compare != null) { - _compare = type.compare; - } else { - _compare = function(a, b) { - return b - a; - }; - } - compare = function(a, b) { - if (a[0] === b[0]) { - return a[2] - b[2]; - } - if (type.reverse) { - return _compare(b[0], a[0]); - } else { - return _compare(a[0], b[0]); - } - }; - _ref = tBody.rows; - for (position = _j = 0, _len1 = _ref.length; _j < _len1; position = ++_j) { - row = _ref[position]; - value = sortable.getNodeValue(row.cells[i]); - if (type.comparator != null) { - value = type.comparator(value); - } - rowArray.push([value, row, position]); - } - rowArray.sort(compare); - for (_k = 0, _len2 = rowArray.length; _k < _len2; _k++) { - row = rowArray[_k]; - tBody.appendChild(row[1]); - } - } else { - _ref1 = tBody.rows; - for (_l = 0, _len3 = _ref1.length; _l < _len3; _l++) { - item = _ref1[_l]; - rowArray.push(item); - } - rowArray.reverse(); - for (_m = 0, _len4 = rowArray.length; _m < _len4; _m++) { - row = rowArray[_m]; - tBody.appendChild(row); - } - } - if (typeof window['CustomEvent'] === 'function') { - return typeof table.dispatchEvent === "function" ? table.dispatchEvent(new CustomEvent('Sortable.sorted', { - bubbles: true - })) : void 0; - } - }; - _results = []; - for (_i = 0, _len = clickEvents.length; _i < _len; _i++) { - eventName = clickEvents[_i]; - _results.push(addEventListener(th, eventName, onClick)); - } - return _results; - }, - getColumnType: function(table, i) { - var row, specified, text, type, _i, _j, _len, _len1, _ref, _ref1, _ref2; - specified = (_ref = table.querySelectorAll('th')[i]) != null ? _ref.getAttribute('data-sortable-type') : void 0; - if (specified != null) { - return sortable.typesObject[specified]; - } - _ref1 = table.tBodies[0].rows; - for (_i = 0, _len = _ref1.length; _i < _len; _i++) { - row = _ref1[_i]; - text = sortable.getNodeValue(row.cells[i]); - _ref2 = sortable.types; - for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) { - type = _ref2[_j]; - if (type.match(text)) { - return type; - } - } - } - return sortable.typesObject.alpha; - }, - getNodeValue: function(node) { - var dataValue; - if (!node) { - return ''; - } - dataValue = node.getAttribute('data-value'); - if (dataValue !== null) { - return dataValue; - } - if (typeof node.innerText !== 'undefined') { - return node.innerText.replace(trimRegExp, ''); - } - return node.textContent.replace(trimRegExp, ''); - }, - setupTypes: function(types) { - var type, _i, _len, _results; - sortable.types = types; - sortable.typesObject = {}; - _results = []; - for (_i = 0, _len = types.length; _i < _len; _i++) { - type = types[_i]; - _results.push(sortable.typesObject[type.name] = type); - } - return _results; - } - }; - - sortable.setupTypes([ - { - name: 'numeric', - defaultSortDirection: 'descending', - match: function(a) { - return a.match(numberRegExp); - }, - comparator: function(a) { - return parseFloat(a.replace(/[^0-9.-]/g, ''), 10) || 0; - } - }, { - name: 'date', - defaultSortDirection: 'ascending', - reverse: true, - match: function(a) { - return !isNaN(Date.parse(a)); - }, - comparator: function(a) { - return Date.parse(a) || 0; - } - }, { - name: 'alpha', - defaultSortDirection: 'ascending', - match: function() { - return true; - }, - compare: function(a, b) { - return a.localeCompare(b); - } - } - ]); - - setTimeout(sortable.init, 0); - - if (typeof define === 'function' && define.amd) { - define(function() { - return sortable; - }); - } else if (typeof exports !== 'undefined') { - module.exports = sortable; - } else { - window.Sortable = sortable; - } - -}).call(this); diff --git a/data/web/json_api.php b/data/web/json_api.php index f0715466..66b46cfb 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -2,168 +2,242 @@ require_once 'inc/prerequisites.inc.php'; error_reporting(E_ALL); if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_username'])) { - if (isset($_GET['action'])) { - $action = $_GET['action']; + if (isset($_GET['action']) && isset($_GET['object'])) { + $action = filter_input(INPUT_GET, 'action', FILTER_SANITIZE_STRING); + $object = filter_input(INPUT_GET, 'object', FILTER_SANITIZE_STRING); switch ($action) { - case "domain_table_data": - $domains = mailbox_get_domains(); - if (!empty($domains)) { - foreach ($domains as $domain) { - $data[] = mailbox_get_domain_details($domain); - } - if (!isset($data) || empty($data)) { - echo '{}'; - } - else { - echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); - } - } - else { - echo '{}'; - } - break; - case "mailbox_table_data": - $domains = mailbox_get_domains(); - if (!empty($domains)) { - foreach ($domains as $domain) { - $mailboxes = mailbox_get_mailboxes($domain); - if (!empty($mailboxes)) { - foreach ($mailboxes as $mailbox) { - $data[] = mailbox_get_mailbox_details($mailbox); + case "domain": + switch ($object) { + case "all": + $domains = mailbox_get_domains(); + if (!empty($domains)) { + foreach ($domains as $domain) { + $data[] = mailbox_get_domain_details($domain); + } + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); } } - } - if (!isset($data) || empty($data)) { - echo '{}'; - } - else { - echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); - } + else { + echo '{}'; + } + break; + + default: + $data = mailbox_get_domain_details($object); + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode(mailbox_get_domain_details($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + break; } - else { - echo '{}'; - } - break; - case "resource_table_data": - $domains = mailbox_get_domains(); - if (!empty($domains)) { - foreach ($domains as $domain) { - $resources = mailbox_get_resources($domain); - if (!empty($resources)) { - foreach ($resources as $resource) { - $data[] = mailbox_get_resource_details($resource); + break; + case "mailbox": + switch ($object) { + case "all": + $domains = mailbox_get_domains(); + if (!empty($domains)) { + foreach ($domains as $domain) { + $mailboxes = mailbox_get_mailboxes($domain); + if (!empty($mailboxes)) { + foreach ($mailboxes as $mailbox) { + $data[] = mailbox_get_mailbox_details($mailbox); + } + } + } + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); } } - } - if (!isset($data) || empty($data)) { - echo '{}'; - } - else { - echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); - } + else { + echo '{}'; + } + break; + + default: + $data = mailbox_get_mailbox_details($object); + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode(mailbox_get_mailbox_details($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + break; + } - else { - echo '{}'; - } - break; - case "domain_alias_table_data": - $domains = mailbox_get_domains(); - if (!empty($domains)) { - foreach ($domains as $domain) { - $alias_domains = mailbox_get_alias_domains($domain); - if (!empty($alias_domains)) { - foreach ($alias_domains as $alias_domain) { - $data[] = mailbox_get_alias_domain_details($alias_domain); + break; + case "resource": + switch ($object) { + case "all": + $domains = mailbox_get_domains(); + if (!empty($domains)) { + foreach ($domains as $domain) { + $resources = mailbox_get_resources($domain); + if (!empty($resources)) { + foreach ($resources as $resource) { + $data[] = mailbox_get_resource_details($resource); + } + } + } + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); } } - } - if (!isset($data) || empty($data)) { - echo '{}'; - } - else { - echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); - } + else { + echo '{}'; + } + break; + + default: + $data = mailbox_get_resource_details($object); + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode(mailbox_get_resource_details($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + break; + } - else { - echo '{}'; - } - break; - case "alias_table_data": - $domains = array_merge(mailbox_get_domains(), mailbox_get_alias_domains()); - if (!empty($domains)) { - foreach ($domains as $domain) { - $aliases = mailbox_get_aliases($domain); - if (!empty($aliases)) { - foreach ($aliases as $alias) { - $data[] = mailbox_get_alias_details($alias); + break; + case "alias-domain": + switch ($object) { + case "all": + $domains = mailbox_get_domains(); + if (!empty($domains)) { + foreach ($domains as $domain) { + $alias_domains = mailbox_get_alias_domains($domain); + if (!empty($alias_domains)) { + foreach ($alias_domains as $alias_domain) { + $data[] = mailbox_get_alias_domain_details($alias_domain); + } + } + } + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); } } - } - if (!isset($data) || empty($data)) { - echo '{}'; - } - else { - echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); - } + else { + echo '{}'; + } + break; + + default: + $data = mailbox_get_alias_domains($object); + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode(mailbox_get_alias_domains($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + break; } - else { - echo '{}'; + break; + case "alias": + switch ($object) { + case "all": + $domains = array_merge(mailbox_get_domains(), mailbox_get_alias_domains()); + if (!empty($domains)) { + foreach ($domains as $domain) { + $aliases = mailbox_get_aliases($domain); + if (!empty($aliases)) { + foreach ($aliases as $alias) { + $data[] = mailbox_get_alias_details($alias); + } + } + } + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + } + else { + echo '{}'; + } + break; + + default: + $data = mailbox_get_alias_details($object); + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode(mailbox_get_alias_details($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + break; } - break; - case "get_mailbox_details": - if (!isset($_GET['object'])) { return false; } - $object = $_GET['object']; - $data = mailbox_get_mailbox_details($object); - if (!isset($data) || empty($data)) { - echo '{}'; + break; + case "domain-admin": + switch ($object) { + case "all": + $domain_admins = get_domain_admins(); + if (!empty($domain_admins)) { + foreach ($domain_admins as $domain_admin) { + $data[] = get_domain_admin_details($domain_admin); + } + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + } + else { + echo '{}'; + } + break; + + default: + $data = get_domain_admin_details($object); + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode(get_domain_admin_details($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + break; } - else { - echo json_encode(mailbox_get_mailbox_details($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); - } - break; - case "get_domain_details": - if (!isset($_GET['object'])) { return false; } - $object = $_GET['object']; - $data = mailbox_get_domain_details($object); - if (!isset($data) || empty($data)) { - echo '{}'; - } - else { - echo json_encode(mailbox_get_domain_details($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); - } - break; - case "get_u2f_reg_challenge": - if (!isset($_GET['object'])) { return false; } - $object = $_GET['object']; - if ( - ($_SESSION["mailcow_cc_role"] == "admin" || $_SESSION["mailcow_cc_role"] == "domainadmin") - && - ($_SESSION["mailcow_cc_username"] == $object) - ) { + break; + case "u2f-registration": + if (($_SESSION["mailcow_cc_role"] == "admin" || $_SESSION["mailcow_cc_role"] == "domainadmin") && $_SESSION["mailcow_cc_username"] == $object) { $data = $u2f->getRegisterData(get_u2f_registrations($object)); list($req, $sigs) = $data; $_SESSION['regReq'] = json_encode($req); echo 'var req = ' . json_encode($req) . '; var sigs = ' . json_encode($sigs) . ';'; } else { - echo '{}'; + return; } - break; - case "get_u2f_auth_challenge": - if (!isset($_GET['object'])) { return false; } - $object = $_GET['object']; + break; + case "u2f-authentication": if (isset($_SESSION['pending_mailcow_cc_username']) && $_SESSION['pending_mailcow_cc_username'] == $object) { $reqs = json_encode($u2f->getAuthenticateData(get_u2f_registrations($object))); $_SESSION['authReq'] = $reqs; echo 'var req = ' . $reqs . ';'; } else { - echo '{}'; + return; } - break; + break; default: echo '{}'; - break; + break; } } } \ No newline at end of file diff --git a/data/web/lang/lang.de.php b/data/web/lang/lang.de.php index 160a306e..cd1b34b3 100644 --- a/data/web/lang/lang.de.php +++ b/data/web/lang/lang.de.php @@ -36,6 +36,7 @@ $lang['danger']['object_exists'] = 'Objekt %s existiert bereits'; $lang['danger']['domain_exists'] = 'Domain %s existiert bereits'; $lang['danger']['alias_goto_identical'] = 'Alias- und Ziel-Adresse dürfen nicht identisch sein'; $lang['danger']['aliasd_targetd_identical'] = 'Alias-Domain darf nicht gleich Ziel-Domain sein'; +$lang['danger']['maxquota_empty'] = 'Max. Speicherplatz pro Mailbox darf nicht 0 sein.'; $lang['success']['alias_added'] = 'Alias-Adresse(n) wurden angelegt'; $lang['success']['alias_modified'] = 'Änderungen an Alias %s wurden gespeichert'; $lang['success']['aliasd_modified'] = 'Änderungen an Alias-Domain %s wurden gespeichert'; @@ -70,6 +71,7 @@ $lang['danger']['is_spam_alias'] = '%s lautet bereits eine Spam-Alias-Adresse'; $lang['danger']['quota_not_0_not_numeric'] = 'Speicherplatz muss numerisch und >= 0 sein'; $lang['danger']['domain_not_found'] = 'Domain "%s" nicht gefunden.'; $lang['danger']['max_mailbox_exceeded'] = 'Anzahl an Mailboxen überschritten (%d von %d)'; +$lang['danger']['max_alias_exceeded'] = 'Anzahl an Alias-Adressen überschritten'; $lang['danger']['mailbox_quota_exceeded'] = 'Speicherplatz überschreitet das Limit (max. %d MiB)'; $lang['danger']['mailbox_quota_left_exceeded'] = 'Nicht genügend Speicherplatz vorhanden (Speicherplatz anwendbar: %d MiB)'; $lang['success']['mailbox_added'] = 'Mailbox %s wurde angelegt'; @@ -246,6 +248,7 @@ $lang['mailbox']['add_domain_alias'] = 'Domain-Alias hinzufügen'; $lang['mailbox']['add_mailbox'] = 'Mailbox hinzufügen'; $lang['mailbox']['add_resource'] = 'Ressource hinzufügen'; $lang['mailbox']['add_alias'] = 'Alias hinzufügen'; +$lang['mailbox']['empty'] = 'Keine Einträge vorhanden'; $lang['info']['no_action'] = 'Keine Aktion anwendbar'; @@ -448,4 +451,6 @@ $lang['admin']['site_not_found'] = 'Kann mailcow Site-Konfiguration nicht finden $lang['admin']['public_folder_empty'] = 'Public folder name must not be empty'; // NEEDS TRANSLATION $lang['admin']['set_rr_failed'] = 'Kann Postfix Restriktionen nicht setzen'; $lang['admin']['no_record'] = 'Kein Eintrag'; +$lang['admin']['filter_table'] = 'Tabelle Filtern'; +$lang['admin']['empty'] = 'Keine Einträge vorhanden'; ?> diff --git a/data/web/lang/lang.en.php b/data/web/lang/lang.en.php index 6b602faf..f46c9ac1 100644 --- a/data/web/lang/lang.en.php +++ b/data/web/lang/lang.en.php @@ -38,6 +38,7 @@ $lang['danger']['object_exists'] = "Object %s already exists"; $lang['danger']['domain_exists'] = "Domain %s already exists"; $lang['danger']['alias_goto_identical'] = "Alias and goto address must not be identical"; $lang['danger']['aliasd_targetd_identical'] = "Alias domain must not be equal to target domain"; +$lang['danger']['maxquota_empty'] = 'Max. quota per mailbox must not be 0.'; $lang['success']['alias_added'] = "Alias address/es has/have been added"; $lang['success']['alias_modified'] = "Changes to alias have been saved"; $lang['success']['aliasd_modified'] = "Changes to alias domain have been saved"; @@ -72,6 +73,7 @@ $lang['danger']['is_spam_alias'] = "%s is already known as a spam alias address" $lang['danger']['quota_not_0_not_numeric'] = "Quota must be numeric and >= 0"; $lang['danger']['domain_not_found'] = "Domain not found."; $lang['danger']['max_mailbox_exceeded'] = "Max. mailboxes exceeded (%d of %d)"; +$lang['danger']['max_alias_exceeded'] = 'Max. aliases exceeded'; $lang['danger']['mailbox_quota_exceeded'] = "Quota exceeds the domain limit (max. %d MiB)"; $lang['danger']['mailbox_quota_left_exceeded'] = "Not enough space left (space left: %d MiB)"; $lang['success']['mailbox_added'] = "Mailbox %s has been added"; @@ -249,6 +251,7 @@ $lang['mailbox']['add_mailbox'] = 'Add mailbox'; $lang['mailbox']['add_resource'] = 'Add resource'; $lang['mailbox']['add_alias'] = 'Add alias'; $lang['mailbox']['add_domain_record_first'] = 'Please add a domain first'; +$lang['mailbox']['empty'] = 'No results'; $lang['info']['no_action'] = 'No action applicable'; @@ -459,4 +462,6 @@ $lang['admin']['site_not_found'] = 'Cannot locate mailcow site configuration'; $lang['admin']['public_folder_empty'] = 'Public folder name must not be empty'; $lang['admin']['set_rr_failed'] = 'Cannot set Postfix restrictions'; $lang['admin']['no_record'] = 'No record'; +$lang['admin']['filter_table'] = 'Filter table'; +$lang['admin']['empty'] = 'No results'; ?> diff --git a/data/web/mailbox.php b/data/web/mailbox.php index bc8ca6d2..fe2cd803 100644 --- a/data/web/mailbox.php +++ b/data/web/mailbox.php @@ -5,33 +5,11 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm require_once "inc/header.inc.php"; $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; ?> -
-

tbody>tr.footable-empty>td { endif; ?>
+

@@ -49,14 +28,15 @@ table.footable>tbody>tr.footable-empty>td {
+
-

+

@@ -65,14 +45,15 @@ table.footable>tbody>tr.footable-empty>td {
+
-

+

@@ -80,14 +61,15 @@ table.footable>tbody>tr.footable-empty>td {
+
-

+

@@ -100,10 +82,10 @@ table.footable>tbody>tr.footable-empty>td {
-

+

diff --git a/data/web/user.php b/data/web/user.php index 94f2a762..03e1a22b 100644 --- a/data/web/user.php +++ b/data/web/user.php @@ -405,7 +405,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
- +
@@ -416,7 +416,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == ' - + From 8b7e3c718d034c5a509c63439b4a0de27afb8bf2 Mon Sep 17 00:00:00 2001 From: andryyy Date: Tue, 28 Mar 2017 11:51:31 +0200 Subject: [PATCH 05/28] API format changes --- data/conf/nginx/site.conf | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/data/conf/nginx/site.conf b/data/conf/nginx/site.conf index d724b4f2..a78c483f 100644 --- a/data/conf/nginx/site.conf +++ b/data/conf/nginx/site.conf @@ -18,6 +18,11 @@ server { access_log /var/log/nginx/access.log; root /web; + location /api/v1/ { + try_files $uri $uri/ /json_api.php?$args; + } + rewrite ^/api/v1/([^/]+)/([^/]+)/?$ /json_api.php?action=$1&object=$2? last; + location ^~ /.well-known/acme-challenge/ { allow all; default_type "text/plain"; @@ -166,6 +171,11 @@ server { access_log /var/log/nginx/access.log; root /web; + location /api/v1/ { + try_files $uri $uri/ /json_api.php?$args; + } + rewrite ^/api/v1/([^/]+)/([^/]+)/?$ /json_api.php?action=$1&object=$2? last; + location ^~ /.well-known/acme-challenge/ { allow all; default_type "text/plain"; From abcdf841cd0ff95a6d24d3977540bea490140d17 Mon Sep 17 00:00:00 2001 From: andryyy Date: Mon, 3 Apr 2017 19:55:13 +0200 Subject: [PATCH 06/28] Reset SQL users, logs: info about tail --- docs/u_and_e.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/docs/u_and_e.md b/docs/u_and_e.md index d0a52cc8..5114d6a3 100644 --- a/docs/u_and_e.md +++ b/docs/u_and_e.md @@ -215,6 +215,51 @@ source mailcow.conf docker-compose exec mysql-mailcow mysql -u${DBUSER} -p${DBPASS} ${DBNAME} < backup_file.sql ``` +### Reset MySQL passwords + +Stop the stack by running `docker-compose stop`. + +When the containers came to a stop, run this command: + +``` +docker-compose run --rm --entrypoint '/bin/sh -c "gosu mysql mysqld --skip-grant-tables & sleep 10 && mysql -hlocalhost -uroot && exit 0"' mysql-mailcow +``` + +**1\. Find database name** + +``` +MariaDB [(none)]> show databases; ++--------------------+ +| Database | ++--------------------+ +| information_schema | +| mailcow_database | <===== +| mysql | +| performance_schema | ++--------------------+ +4 rows in set (0.00 sec) +``` + +**2\. Reset one or more users + +Both "password" and "authentication_string" exist. Currently "password" is used, but better set both. + +``` +MariaDB [(none)]> SELECT user FROM mysql.user; ++--------------+ +| user | ++--------------+ +| mailcow_user | <===== +| root | ++--------------+ +2 rows in set (0.00 sec) + +MariaDB [(none)]> FLUSH PRIVILEGES; +MariaDB [(none)]> UPDATE mysql.user SET authentication_string = PASSWORD('gotr00t'), password = PASSWORD('gotr00t') WHERE User = 'root' AND Host = '%'; +MariaDB [(none)]> UPDATE mysql.user SET authentication_string = PASSWORD('mookuh'), password = PASSWORD('mookuh') WHERE User = 'mailcow' AND Host = '%'; +MariaDB [(none)]> FLUSH PRIVILEGES; +``` + ## Debugging You can use `docker-compose logs $service-name` for all containers. @@ -223,6 +268,8 @@ Run `docker-compose logs` for all logs at once. Follow the log output by running docker-compose with `logs -f`. +Limit the output by calling logs with `--tail=300` like `docker-compose logs --tail=300 mysql-mailcow`. + ## Redirect port 80 to 443 Since February the 28th 2017 mailcow does come with port 80 and 443 enabled. From 58d86dadcec43617d3fe58e998e958e4b3daf389 Mon Sep 17 00:00:00 2001 From: andryyy Date: Mon, 3 Apr 2017 20:06:49 +0200 Subject: [PATCH 07/28] Rebase Dovecot on Stretch slim, build from stable source with latest stable Pigeonhole for antispam replacement --- data/Dockerfiles/dovecot/Dockerfile | 86 +++++++++++-------- data/Dockerfiles/dovecot/docker-entrypoint.sh | 32 +++++-- data/Dockerfiles/dovecot/report-ham.sieve | 11 +++ data/Dockerfiles/dovecot/report-spam.sieve | 3 + data/Dockerfiles/dovecot/rspamd-pipe | 8 -- data/Dockerfiles/dovecot/rspamd-pipe-ham | 4 + data/Dockerfiles/dovecot/rspamd-pipe-spam | 4 + data/Dockerfiles/dovecot/supervisord.conf | 2 +- docker-compose.yml | 6 +- 9 files changed, 105 insertions(+), 51 deletions(-) create mode 100644 data/Dockerfiles/dovecot/report-ham.sieve create mode 100644 data/Dockerfiles/dovecot/report-spam.sieve delete mode 100755 data/Dockerfiles/dovecot/rspamd-pipe create mode 100755 data/Dockerfiles/dovecot/rspamd-pipe-ham create mode 100755 data/Dockerfiles/dovecot/rspamd-pipe-spam diff --git a/data/Dockerfiles/dovecot/Dockerfile b/data/Dockerfiles/dovecot/Dockerfile index abd07c1f..2d0578a2 100644 --- a/data/Dockerfiles/dovecot/Dockerfile +++ b/data/Dockerfiles/dovecot/Dockerfile @@ -1,33 +1,30 @@ -FROM ubuntu:xenial +FROM debian:stretch-slim +#ubuntu:xenial MAINTAINER Andre Peters ENV DEBIAN_FRONTEND noninteractive ENV LC_ALL C +ENV DOVECOT_VERSION 2.2.28 +ENV PIGEONHOLE_VERSION 0.4.17 -RUN dpkg-divert --local --rename --add /sbin/initctl \ - && ln -sf /bin/true /sbin/initctl \ - && dpkg-divert --local --rename --add /usr/bin/ischroot \ - && ln -sf /bin/true /usr/bin/ischroot - -RUN apt-get update -RUN apt-get -y install dovecot-common \ - dovecot-core \ - dovecot-imapd \ - dovecot-lmtpd \ - dovecot-managesieved \ - dovecot-sieve \ - dovecot-mysql \ - dovecot-pop3d \ - dovecot-dev \ +RUN apt-get update \ + && apt-get -y install libpam-dev \ + default-libmysqlclient-dev \ + lzma-dev \ + liblz-dev \ + libbz2-dev \ + liblz4-dev \ + liblzma-dev \ + build-essential \ + autotools-dev \ + automake \ syslog-ng \ syslog-ng-core \ ca-certificates \ supervisor \ wget \ curl \ - build-essential \ - autotools-dev \ - automake \ + libssl-dev \ libauthen-ntlm-perl \ libcrypt-ssleay-perl \ libdigest-hmac-perl \ @@ -52,36 +49,57 @@ RUN apt-get -y install dovecot-common \ make \ cpanminus + +RUN wget https://www.dovecot.org/releases/2.2/dovecot-$DOVECOT_VERSION.tar.gz -O - | tar xvz \ + && cd dovecot-$DOVECOT_VERSION \ + && ./configure --with-mysql --with-lzma --with-lz4 --with-ssl=openssl --with-notify=inotify --with-storages=mdbox,sdbox,maildir,mbox,imapc,pop3c --with-bzlib --with-zlib \ + && make -j3 \ + && make install \ + && make clean + +RUN wget https://pigeonhole.dovecot.org/releases/2.2/dovecot-2.2-pigeonhole-$PIGEONHOLE_VERSION.tar.gz -O - | tar xvz \ + && cd dovecot-2.2-pigeonhole-$PIGEONHOLE_VERSION \ + && ./configure \ + && make -j3 \ + && make install \ + && make clean + RUN sed -i -E 's/^(\s*)system\(\);/\1unix-stream("\/dev\/log");/' /etc/syslog-ng/syslog-ng.conf RUN cpanm Data::Uniqid Mail::IMAPClient String::Util RUN echo '* * * * * root /usr/local/bin/imapsync_cron.pl' > /etc/cron.d/imapsync RUN echo '30 3 * * * vmail /usr/bin/doveadm quota recalc -A' > /etc/cron.d/dovecot-sync -WORKDIR /tmp - -RUN wget http://hg.dovecot.org/dovecot-antispam-plugin/archive/tip.tar.gz -O - | tar xvz \ - && cd /tmp/dovecot-antispam* \ - && ./autogen.sh \ - && ./configure --prefix=/usr \ - && make \ - && make install - COPY ./imapsync /usr/local/bin/imapsync COPY ./postlogin.sh /usr/local/bin/postlogin.sh COPY ./imapsync_cron.pl /usr/local/bin/imapsync_cron.pl -COPY ./rspamd-pipe /usr/local/bin/rspamd-pipe +COPY ./report-spam.sieve /usr/local/lib/dovecot/sieve/report-spam.sieve +COPY ./report-ham.sieve /usr/local/lib/dovecot/sieve/report-ham.sieve +COPY ./rspamd-pipe-ham /usr/local/lib/dovecot/sieve/rspamd-pipe-ham +COPY ./rspamd-pipe-spam /usr/local/lib/dovecot/sieve/rspamd-pipe-spam COPY ./docker-entrypoint.sh / COPY ./supervisord.conf /etc/supervisor/supervisord.conf -RUN chmod +x /usr/local/bin/rspamd-pipe -RUN chmod +x /usr/local/bin/imapsync_cron.pl +RUN chmod +x /usr/local/lib/dovecot/sieve/rspamd-pipe-ham \ + /usr/local/lib/dovecot/sieve/rspamd-pipe-spam \ + /usr/local/bin/imapsync_cron.pl \ + /usr/local/bin/postlogin.sh \ + /usr/local/bin/imapsync -RUN groupadd -g 5000 vmail -RUN useradd -g vmail -u 5000 vmail -d /var/vmail +RUN groupadd -g 5000 vmail \ + && groupadd -g 142 dovecot \ + && groupadd -g 143 dovenull \ + && useradd -g vmail -u 5000 vmail -d /var/vmail \ + && useradd -c "Dovecot unprivileged user" -d /dev/null -u 142 -g dovecot -s /bin/false dovecot \ + && useradd -c "Dovecot login user" -d /dev/null -u 143 -g dovenull -s /bin/false dovenull EXPOSE 24 10001 ENTRYPOINT ["/docker-entrypoint.sh"] CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf -RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* +RUN apt-get clean \ + && rm -rf /var/lib/apt/lists/* \ + /tmp/* \ + /var/tmp/* \ + /dovecot-2.2-pigeonhole-$PIGEONHOLE_VERSION \ + /dovecot-$DOVECOT_VERSION diff --git a/data/Dockerfiles/dovecot/docker-entrypoint.sh b/data/Dockerfiles/dovecot/docker-entrypoint.sh index 8ef09dba..282eb157 100755 --- a/data/Dockerfiles/dovecot/docker-entrypoint.sh +++ b/data/Dockerfiles/dovecot/docker-entrypoint.sh @@ -6,12 +6,16 @@ sed -i "/^\$DBUSER/c\\\$DBUSER='${DBUSER}';" /usr/local/bin/imapsync_cron.pl sed -i "/^\$DBPASS/c\\\$DBPASS='${DBPASS}';" /usr/local/bin/imapsync_cron.pl sed -i "/^\$DBNAME/c\\\$DBNAME='${DBNAME}';" /usr/local/bin/imapsync_cron.pl -[[ ! -d /etc/dovecot/sql/ ]] && mkdir -p /etc/dovecot/sql/ +# Create SQL dict directory for Dovecot +[[ ! -d /usr/local/etc/dovecot/sql/ ]] && mkdir -p /usr/local/etc/dovecot/sql/ +[[ ! -d /var/vmail/sieve ]] && mkdir -p /var/vmail/sieve +[[ ! -d /etc/sogo ]] && mkdir -p /etc/sogo # Set Dovecot sql config parameters, escape " in db password DBPASS=$(echo ${DBPASS} | sed 's/"/\\"/g') -cat < /etc/dovecot/sql/dovecot-dict-sql.conf +# Create quota dict for Dovecot +cat < /usr/local/etc/dovecot/sql/dovecot-dict-sql.conf connect = "host=mysql dbname=${DBNAME} user=${DBNAME} password=${DBPASS}" map { pattern = priv/quota/storage @@ -27,7 +31,8 @@ map { } EOF -cat < /etc/dovecot/sql/dovecot-mysql.conf +# Create user and pass dict for Dovecot +cat < /usr/local/etc/dovecot/sql/dovecot-mysql.conf driver = mysql connect = "host=mysql dbname=${DBNAME} user=${DBNAME} password=${DBPASS}" default_pass_scheme = SSHA256 @@ -36,19 +41,32 @@ user_query = SELECT CONCAT('maildir:/var/vmail/',maildir) AS mail, 5000 AS uid, iterate_query = SELECT username FROM mailbox WHERE active='1'; EOF -[[ ! -d /var/vmail/sieve ]] && mkdir -p /var/vmail/sieve -[[ ! -d /etc/sogo ]] && mkdir -p /etc/sogo -cat /etc/dovecot/sieve_after > /var/vmail/sieve/global.sieve +# Create global sieve_after script +cat /usr/local/etc/dovecot/sieve_after > /var/vmail/sieve/global.sieve + +# Compile sieve scripts sievec /var/vmail/sieve/global.sieve +sievec /usr/local/lib/dovecot/sieve/report-spam.sieve +sievec /usr/local/lib/dovecot/sieve/report-ham.sieve + +# Fix sieve permission chown -R vmail:vmail /var/vmail/sieve +# Check permissions of vmail directory. # Do not do this every start-up, it may take a very long time. So we use a stat check here. if [[ $(stat -c %U /var/vmail/) != "vmail" ]] ; then chown -R vmail:vmail /var/vmail ; fi # Create random master for SOGo sieve features RAND_USER=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 16 | head -n 1) RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 24 | head -n 1) -echo ${RAND_USER}:$(doveadm pw -s SHA1 -p ${RAND_PASS}) > /etc/dovecot/dovecot-master.passwd +echo ${RAND_USER}:$(doveadm pw -s SHA1 -p ${RAND_PASS}) > /usr/local/etc/dovecot/dovecot-master.passwd echo ${RAND_USER}:${RAND_PASS} > /etc/sogo/sieve.creds +if [[ ! -f /mail_crypt/ecprivkey.pem || ! -f /mail_crypt/ecpubkey.pem ]]; then + openssl ecparam -name prime256v1 -genkey | openssl pkey -out /mail_crypt/ecprivkey.pem + openssl pkey -in /mail_crypt/ecprivkey.pem -pubout -out /mail_crypt/ecpubkey.pem + chown -R dovecot -R /mail_crypt/ + chattr + /mail_crypt/ecpubkey.pem /mail_crypt/ecprivkey.pem +fi + exec "$@" diff --git a/data/Dockerfiles/dovecot/report-ham.sieve b/data/Dockerfiles/dovecot/report-ham.sieve new file mode 100644 index 00000000..80c7f44e --- /dev/null +++ b/data/Dockerfiles/dovecot/report-ham.sieve @@ -0,0 +1,11 @@ +require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"]; + +if environment :matches "imap.mailbox" "*" { + set "mailbox" "${1}"; +} + +if string "${mailbox}" "Trash" { + stop; +} + +pipe :copy "rspamd-pipe-ham"; diff --git a/data/Dockerfiles/dovecot/report-spam.sieve b/data/Dockerfiles/dovecot/report-spam.sieve new file mode 100644 index 00000000..d44cb9a7 --- /dev/null +++ b/data/Dockerfiles/dovecot/report-spam.sieve @@ -0,0 +1,3 @@ +require ["vnd.dovecot.pipe", "copy"]; + +pipe :copy "rspamd-pipe-spam"; diff --git a/data/Dockerfiles/dovecot/rspamd-pipe b/data/Dockerfiles/dovecot/rspamd-pipe deleted file mode 100755 index f9236e17..00000000 --- a/data/Dockerfiles/dovecot/rspamd-pipe +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -if [[ ${2} == "learn_spam" ]]; then -/usr/bin/curl --data-binary @- http://rspamd:11334/learnspam < /dev/stdin -elif [[ ${2} == "learn_ham" ]]; then -/usr/bin/curl --data-binary @- http://rspamd:11334/learnham < /dev/stdin -fi -# Always return 0 to satisfy Dovecot... -exit 0 diff --git a/data/Dockerfiles/dovecot/rspamd-pipe-ham b/data/Dockerfiles/dovecot/rspamd-pipe-ham new file mode 100755 index 00000000..7c3ab03f --- /dev/null +++ b/data/Dockerfiles/dovecot/rspamd-pipe-ham @@ -0,0 +1,4 @@ +#!/bin/bash +/usr/bin/curl -s --data-binary @- http://rspamd:11334/learnham < /dev/stdin +# Always return 0 to satisfy Dovecot... +exit 0 diff --git a/data/Dockerfiles/dovecot/rspamd-pipe-spam b/data/Dockerfiles/dovecot/rspamd-pipe-spam new file mode 100755 index 00000000..67cccb2c --- /dev/null +++ b/data/Dockerfiles/dovecot/rspamd-pipe-spam @@ -0,0 +1,4 @@ +#!/bin/bash +/usr/bin/curl -s --data-binary @- http://rspamd:11334/learnspam < /dev/stdin +# Always return 0 to satisfy Dovecot... +exit 0 diff --git a/data/Dockerfiles/dovecot/supervisord.conf b/data/Dockerfiles/dovecot/supervisord.conf index 45f9ddd5..e5a66f22 100644 --- a/data/Dockerfiles/dovecot/supervisord.conf +++ b/data/Dockerfiles/dovecot/supervisord.conf @@ -8,7 +8,7 @@ autostart=true stdout_syslog=true [program:dovecot] -command=/usr/sbin/dovecot -F +command=/usr/local/sbin/dovecot -F autorestart=true [program:logfiles] diff --git a/docker-compose.yml b/docker-compose.yml index 76ca1b6b..825bf9a8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,6 +26,7 @@ services: volumes: - mysql-vol-1:/var/lib/mysql/ - ./data/conf/mysql/:/etc/mysql/conf.d/:ro + - ./data/assets/reset_mysql.sh:/reset_mysql.sh dns: - 172.22.1.254 dns_search: mailcow-network @@ -151,14 +152,16 @@ services: depends_on: - bind9-mailcow volumes: - - ./data/conf/dovecot:/etc/dovecot + - ./data/conf/dovecot:/usr/local/etc/dovecot - ./data/assets/ssl:/etc/ssl/mail/:ro - ./data/conf/sogo/:/etc/sogo/ - vmail-vol-1:/var/vmail + - crypt-vol-1:/mail_crypt/ environment: - DBNAME=${DBNAME} - DBUSER=${DBUSER} - DBPASS=${DBPASS} + - MAIL_CRYPT=${MAIL_CRYPT:-NO} ports: - "${IMAP_PORT:-143}:143" - "${IMAPS_PORT:-993}:993" @@ -266,3 +269,4 @@ volumes: redis-vol-1: rspamd-vol-1: postfix-vol-1: + crypt-vol-1: From 34bc24255484698a69e1e35703841547cce8947f Mon Sep 17 00:00:00 2001 From: andryyy Date: Wed, 5 Apr 2017 22:19:01 +0200 Subject: [PATCH 08/28] Add Zeyple filter --- data/conf/postfix/main.cf | 1 + docker-compose.yml | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/data/conf/postfix/main.cf b/data/conf/postfix/main.cf index cbfc4e0c..46de4759 100644 --- a/data/conf/postfix/main.cf +++ b/data/conf/postfix/main.cf @@ -91,3 +91,4 @@ smtpd_milters = inet:rmilter:9900 non_smtpd_milters = inet:rmilter:9900 milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen} mydestination = localhost.localdomain, localhost +content_filter=zeyple diff --git a/docker-compose.yml b/docker-compose.yml index 825bf9a8..ca42c788 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -161,7 +161,6 @@ services: - DBNAME=${DBNAME} - DBUSER=${DBUSER} - DBPASS=${DBPASS} - - MAIL_CRYPT=${MAIL_CRYPT:-NO} ports: - "${IMAP_PORT:-143}:143" - "${IMAPS_PORT:-993}:993" From 2b955d08ab31e3aa16c02a596dd9982b80e672f4 Mon Sep 17 00:00:00 2001 From: andryyy Date: Wed, 5 Apr 2017 22:19:41 +0200 Subject: [PATCH 09/28] Base on Stretch, add Zeyple --- data/Dockerfiles/postfix/Dockerfile | 13 +- data/Dockerfiles/postfix/postfix.sh | 14 +- data/Dockerfiles/postfix/supervisord.conf | 15 +- data/Dockerfiles/postfix/zeyple.conf | 9 + data/Dockerfiles/postfix/zeyple.py | 274 ++++++++++++++++++++++ 5 files changed, 319 insertions(+), 6 deletions(-) create mode 100644 data/Dockerfiles/postfix/zeyple.conf create mode 100755 data/Dockerfiles/postfix/zeyple.py diff --git a/data/Dockerfiles/postfix/Dockerfile b/data/Dockerfiles/postfix/Dockerfile index 6a36f443..0fcdc893 100644 --- a/data/Dockerfiles/postfix/Dockerfile +++ b/data/Dockerfiles/postfix/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:xenial +FROM debian:testing-slim MAINTAINER Andre Peters ENV DEBIAN_FRONTEND noninteractive @@ -19,10 +19,19 @@ RUN apt-get install -y --no-install-recommends supervisor \ postfix-pcre \ syslog-ng \ syslog-ng-core \ - ca-certificates + ca-certificates \ + gnupg \ + python-gpgme \ + sudo \ + dirmngr +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 sed -i -E 's/^(\s*)system\(\);/\1unix-stream("\/dev\/log");/' /etc/syslog-ng/syslog-ng.conf +COPY zeyple.py /usr/local/bin/zeyple.py +COPY zeyple.conf /etc/zeyple.conf COPY supervisord.conf /etc/supervisor/supervisord.conf COPY postfix.sh /opt/postfix.sh diff --git a/data/Dockerfiles/postfix/postfix.sh b/data/Dockerfiles/postfix/postfix.sh index c628e771..f2ba03e4 100755 --- a/data/Dockerfiles/postfix/postfix.sh +++ b/data/Dockerfiles/postfix/postfix.sh @@ -17,7 +17,7 @@ user = ${DBUSER} password = ${DBPASS} hosts = mysql dbname = ${DBNAME} -query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.address WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_in = '1' AND mailbox.active = '1'), 'reject_plaintext_session', 'DUNNO') AS 'tls_enforce_in'; +query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.goto WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_in = '1' AND mailbox.active = '1'), 'reject_plaintext_session', NULL) AS 'tls_enforce_in'; EOF cat < /opt/postfix/conf/sql/mysql_tls_enforce_out_policy.cf @@ -25,7 +25,7 @@ user = ${DBUSER} password = ${DBPASS} hosts = mysql dbname = ${DBNAME} -query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.address WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_out = '1' AND mailbox.active = '1'), 'smtp_enforced_tls:', 'DUNNO') AS 'tls_enforce_out'; +query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.goto WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_out = '1' AND mailbox.active = '1'), 'smtp_enforced_tls:', NULL) AS 'tls_enforce_out'; EOF cat < /opt/postfix/conf/sql/mysql_virtual_alias_domain_catchall_maps.cf @@ -92,11 +92,21 @@ dbname = ${DBNAME} query = SELECT goto FROM spamalias WHERE address='%s' AND validity >= UNIX_TIMESTAMP() EOF +# Reset GPG key permissions +mkdir -p /var/lib/zeyple/keys +chmod 700 /var/lib/zeyple/keys +chown -R 600:600 /var/lib/zeyple/keys + +# Fix Postfix permissions +chgrp -R postdrop /var/spool/postfix/public +chgrp -R postdrop /var/spool/postfix/maildrop +postfix set-permissions postconf -c /opt/postfix/conf if [[ $? != 0 ]]; then echo "Postfix configuration error, refusing to start." exit 1 else postfix -c /opt/postfix/conf start + supervisorctl restart postfix-maillog sleep 126144000 fi diff --git a/data/Dockerfiles/postfix/supervisord.conf b/data/Dockerfiles/postfix/supervisord.conf index 4268899d..72523a61 100644 --- a/data/Dockerfiles/postfix/supervisord.conf +++ b/data/Dockerfiles/postfix/supervisord.conf @@ -12,6 +12,17 @@ command=/opt/postfix.sh autorestart=true [program:postfix-maillog] -command=/usr/bin/tail -f /var/log/mail.log -stdout_logfile=/dev/fd/1 +command=/bin/tail -f /var/log/zeyple.log /var/log/mail.log +stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 + +[unix_http_server] +file=/var/tmp/supervisord.sock +chmod=0770 +chown=nobody:nogroup + +[supervisorctl] +serverurl=unix:///var/tmp/supervisord.sock + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface diff --git a/data/Dockerfiles/postfix/zeyple.conf b/data/Dockerfiles/postfix/zeyple.conf new file mode 100644 index 00000000..7f039582 --- /dev/null +++ b/data/Dockerfiles/postfix/zeyple.conf @@ -0,0 +1,9 @@ +[zeyple] +log_file = /var/log/zeyple.log + +[gpg] +home = /var/lib/zeyple/keys + +[relay] +host = localhost +port = 10026 diff --git a/data/Dockerfiles/postfix/zeyple.py b/data/Dockerfiles/postfix/zeyple.py new file mode 100755 index 00000000..bb218831 --- /dev/null +++ b/data/Dockerfiles/postfix/zeyple.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import sys +import os +import logging +import email +import email.mime.multipart +import email.mime.application +import email.encoders +import smtplib +import copy +from io import BytesIO + +try: + from configparser import SafeConfigParser # Python 3 +except ImportError: + from ConfigParser import SafeConfigParser # Python 2 + +import gpgme + +# Boiler plate to avoid dependency on six +# BBB: Python 2.7 support +PY3K = sys.version_info > (3, 0) + + +def message_from_binary(message): + if PY3K: + return email.message_from_bytes(message) + else: + return email.message_from_string(message) + + +def as_binary_string(email): + if PY3K: + return email.as_bytes() + else: + return email.as_string() + + +def encode_string(string): + if isinstance(string, bytes): + return string + else: + return string.encode('utf-8') + + +__title__ = 'Zeyple' +__version__ = '1.2.0' +__author__ = 'Cédric Félizard' +__license__ = 'AGPLv3+' +__copyright__ = 'Copyright 2012-2016 Cédric Félizard' + + +class Zeyple: + """Zeyple Encrypts Your Precious Log Emails""" + + def __init__(self, config_fname='zeyple.conf'): + self.config = self.load_configuration(config_fname) + + log_file = self.config.get('zeyple', 'log_file') + logging.basicConfig( + filename=log_file, level=logging.DEBUG, + format='%(asctime)s %(process)s %(levelname)s %(message)s' + ) + logging.info("Zeyple ready to encrypt outgoing emails") + + def load_configuration(self, filename): + """Reads and parses the config file""" + + config = SafeConfigParser() + config.read([ + os.path.join('/etc/', filename), + filename, + ]) + if not config.sections(): + raise IOError('Cannot open config file.') + return config + + @property + def gpg(self): + protocol = gpgme.PROTOCOL_OpenPGP + + if self.config.has_option('gpg', 'executable'): + executable = self.config.get('gpg', 'executable') + else: + executable = None # Default value + + home_dir = self.config.get('gpg', 'home') + + ctx = gpgme.Context() + ctx.set_engine_info(protocol, executable, home_dir) + ctx.armor = True + + return ctx + + def process_message(self, message_data, recipients): + """Encrypts the message with recipient keys""" + message_data = encode_string(message_data) + + in_message = message_from_binary(message_data) + logging.info( + "Processing outgoing message %s", in_message['Message-id']) + + if not recipients: + logging.warn("Cannot find any recipients, ignoring") + + sent_messages = [] + for recipient in recipients: + logging.info("Recipient: %s", recipient) + + key_id = self._user_key(recipient) + logging.info("Key ID: %s", key_id) + + if key_id: + out_message = self._encrypt_message(in_message, key_id) + + # Delete Content-Transfer-Encoding if present to default to + # "7bit" otherwise Thunderbird seems to hang in some cases. + del out_message["Content-Transfer-Encoding"] + else: + logging.warn("No keys found, message will be sent unencrypted") + out_message = copy.copy(in_message) + + self._add_zeyple_header(out_message) + self._send_message(out_message, recipient) + sent_messages.append(out_message) + + return sent_messages + + def _get_version_part(self): + ret = email.mime.application.MIMEApplication( + 'Version: 1\n', + 'pgp-encrypted', + email.encoders.encode_noop, + ) + ret.add_header( + 'Content-Description', + "PGP/MIME version identification", + ) + return ret + + def _get_encrypted_part(self, payload): + ret = email.mime.application.MIMEApplication( + payload, + 'octet-stream', + email.encoders.encode_noop, + name="encrypted.asc", + ) + ret.add_header('Content-Description', "OpenPGP encrypted message") + ret.add_header( + 'Content-Disposition', + 'inline', + filename='encrypted.asc', + ) + return ret + + def _encrypt_message(self, in_message, key_id): + if in_message.is_multipart(): + # get the body (after the first \n\n) + payload = in_message.as_string().split("\n\n", 1)[1].strip() + + # prepend the Content-Type including the boundary + content_type = "Content-Type: " + in_message["Content-Type"] + payload = content_type + "\n\n" + payload + + message = email.message.Message() + message.set_payload(payload) + + payload = message.get_payload() + + else: + payload = in_message.get_payload() + payload = encode_string(payload) + + quoted_printable = email.charset.Charset('ascii') + quoted_printable.body_encoding = email.charset.QP + + message = email.mime.nonmultipart.MIMENonMultipart( + 'text', 'plain', charset='utf-8' + ) + message.set_payload(payload, charset=quoted_printable) + + mixed = email.mime.multipart.MIMEMultipart( + 'mixed', + None, + [message], + ) + + # remove superfluous header + del mixed['MIME-Version'] + + payload = as_binary_string(mixed) + + encrypted_payload = self._encrypt_payload(payload, [key_id]) + + version = self._get_version_part() + encrypted = self._get_encrypted_part(encrypted_payload) + + out_message = copy.copy(in_message) + out_message.preamble = "This is an OpenPGP/MIME encrypted " \ + "message (RFC 4880 and 3156)" + + if 'Content-Type' not in out_message: + out_message['Content-Type'] = 'multipart/encrypted' + else: + out_message.replace_header( + 'Content-Type', + 'multipart/encrypted', + ) + + out_message.set_param('protocol', 'application/pgp-encrypted') + out_message.set_payload([version, encrypted]) + + return out_message + + def _encrypt_payload(self, payload, key_ids): + """Encrypts the payload with the given keys""" + payload = encode_string(payload) + + plaintext = BytesIO(payload) + ciphertext = BytesIO() + + self.gpg.armor = True + + recipient = [self.gpg.get_key(key_id) for key_id in key_ids] + + self.gpg.encrypt(recipient, gpgme.ENCRYPT_ALWAYS_TRUST, + plaintext, ciphertext) + + return ciphertext.getvalue() + + def _user_key(self, email): + """Returns the GPG key for the given email address""" + logging.info("Trying to encrypt for %s", email) + keys = [key for key in self.gpg.keylist(email)] + + if keys: + key = keys.pop() # NOTE: looks like keys[0] is the master key + key_id = key.subkeys[0].keyid + return key_id + + return None + + def _add_zeyple_header(self, message): + if self.config.has_option('zeyple', 'add_header') and \ + self.config.getboolean('zeyple', 'add_header'): + message.add_header( + 'X-Zeyple', + "processed by {0} v{1}".format(__title__, __version__) + ) + + def _send_message(self, message, recipient): + """Sends the given message through the SMTP relay""" + logging.info("Sending message %s", message['Message-id']) + + smtp = smtplib.SMTP(self.config.get('relay', 'host'), + self.config.get('relay', 'port')) + + smtp.sendmail(message['From'], recipient, message.as_string()) + smtp.quit() + + logging.info("Message %s sent", message['Message-id']) + + +if __name__ == '__main__': + recipients = sys.argv[1:] + + # BBB: Python 2.7 support + binary_stdin = sys.stdin.buffer if PY3K else sys.stdin + message = binary_stdin.read() + + zeyple = Zeyple() + zeyple.process_message(message, recipients) From 96c1a7c225aad56c710549e0c36eb5b0aa2275ea Mon Sep 17 00:00:00 2001 From: andryyy Date: Wed, 5 Apr 2017 22:21:20 +0200 Subject: [PATCH 10/28] Open 10026 for Zeyple --- data/conf/postfix/master.cf | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/data/conf/postfix/master.cf b/data/conf/postfix/master.cf index e48ea92e..3802c9a0 100644 --- a/data/conf/postfix/master.cf +++ b/data/conf/postfix/master.cf @@ -16,6 +16,7 @@ smtp_enforced_tls unix - - n - - smtp -o smtp_tls_security_level=encrypt -o syslog_name=enforced-tls-smtp -o smtp_delivery_status_filter=pcre:/opt/postfix/conf/smtp_dsn_filter + tlsproxy unix - - n - 0 tlsproxy dnsblog unix - - n - 0 dnsblog pickup fifo n - n 60 1 pickup @@ -43,3 +44,14 @@ anvil unix - - n - 1 anvil scache unix - - n - 1 scache maildrop unix - n n - - pipe flags=DRhu user=vmail argv=/usr/bin/maildrop -d ${recipient} +zeyple unix - n n - - pipe + user=zeyple argv=/usr/local/bin/zeyple.py ${recipient} +127.0.0.1:10026 inet n - n - 10 smtpd + -o content_filter= + -o receive_override_options=no_unknown_recipient_checks,no_header_body_checks,no_milters + -o smtpd_helo_restrictions= + -o smtpd_client_restrictions= + -o smtpd_sender_restrictions= + -o smtpd_recipient_restrictions=permit_mynetworks,reject + -o mynetworks=127.0.0.0/8 + -o smtpd_authorized_xforward_hosts=127.0.0.0/8 From b0d8b1344a8b623bd99755eb34e55ac76560cf85 Mon Sep 17 00:00:00 2001 From: andryyy Date: Wed, 5 Apr 2017 22:22:23 +0200 Subject: [PATCH 11/28] Remove obsolete parameters, use imapsieve instead of deprecated antispam module, use mail_crypt per default --- data/conf/dovecot/dovecot.conf | 43 ++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/data/conf/dovecot/dovecot.conf b/data/conf/dovecot/dovecot.conf index 1d05571e..6792eb34 100644 --- a/data/conf/dovecot/dovecot.conf +++ b/data/conf/dovecot/dovecot.conf @@ -10,9 +10,9 @@ disable_plaintext_auth = yes login_log_format_elements = "user=<%u> method=%m rip=%r lip=%l mpid=%e %c %k" mail_home = /var/vmail/%d/%n mail_location = maildir:~/ -mail_plugins = quota acl zlib antispam +mail_plugins = quota acl zlib mail_crypt auth_username_chars = abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-_@ -ssl_protocols = !SSLv3 !SSLv2 +#ssl_protocols = !SSLv3 !SSLv2 ssl_prefer_server_ciphers = yes ssl_cipher_list = EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA256:EECDH:+CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!IDEA:!ECDSA:kEDH:CAMELLIA128-SHA:AES128-SHA ssl_options = no_compression @@ -24,12 +24,12 @@ auth_master_user_separator = * mail_prefetch_count = 30 passdb { driver = passwd-file - args = /etc/dovecot/dovecot-master.passwd + args = /usr/local/etc/dovecot/dovecot-master.passwd master = yes pass = yes } passdb { - args = /etc/dovecot/sql/dovecot-mysql.conf + args = /usr/local/etc/dovecot/sql/dovecot-mysql.conf driver = sql } namespace inbox { @@ -202,15 +202,15 @@ listen = *,[::] ssl_cert = Date: Wed, 5 Apr 2017 22:25:16 +0200 Subject: [PATCH 12/28] Run sievec after adding keys, use fixed IDs for users --- data/Dockerfiles/dovecot/docker-entrypoint.sh | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/data/Dockerfiles/dovecot/docker-entrypoint.sh b/data/Dockerfiles/dovecot/docker-entrypoint.sh index 282eb157..4a2876d7 100755 --- a/data/Dockerfiles/dovecot/docker-entrypoint.sh +++ b/data/Dockerfiles/dovecot/docker-entrypoint.sh @@ -6,7 +6,7 @@ sed -i "/^\$DBUSER/c\\\$DBUSER='${DBUSER}';" /usr/local/bin/imapsync_cron.pl sed -i "/^\$DBPASS/c\\\$DBPASS='${DBPASS}';" /usr/local/bin/imapsync_cron.pl sed -i "/^\$DBNAME/c\\\$DBNAME='${DBNAME}';" /usr/local/bin/imapsync_cron.pl -# Create SQL dict directory for Dovecot +# Create missing directories [[ ! -d /usr/local/etc/dovecot/sql/ ]] && mkdir -p /usr/local/etc/dovecot/sql/ [[ ! -d /var/vmail/sieve ]] && mkdir -p /var/vmail/sieve [[ ! -d /etc/sogo ]] && mkdir -p /etc/sogo @@ -44,14 +44,6 @@ EOF # Create global sieve_after script cat /usr/local/etc/dovecot/sieve_after > /var/vmail/sieve/global.sieve -# Compile sieve scripts -sievec /var/vmail/sieve/global.sieve -sievec /usr/local/lib/dovecot/sieve/report-spam.sieve -sievec /usr/local/lib/dovecot/sieve/report-ham.sieve - -# Fix sieve permission -chown -R vmail:vmail /var/vmail/sieve - # Check permissions of vmail directory. # Do not do this every start-up, it may take a very long time. So we use a stat check here. if [[ $(stat -c %U /var/vmail/) != "vmail" ]] ; then chown -R vmail:vmail /var/vmail ; fi @@ -62,11 +54,22 @@ RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 24 | head -n 1) echo ${RAND_USER}:$(doveadm pw -s SHA1 -p ${RAND_PASS}) > /usr/local/etc/dovecot/dovecot-master.passwd echo ${RAND_USER}:${RAND_PASS} > /etc/sogo/sieve.creds +# 401 is user dovecot if [[ ! -f /mail_crypt/ecprivkey.pem || ! -f /mail_crypt/ecpubkey.pem ]]; then openssl ecparam -name prime256v1 -genkey | openssl pkey -out /mail_crypt/ecprivkey.pem openssl pkey -in /mail_crypt/ecprivkey.pem -pubout -out /mail_crypt/ecpubkey.pem - chown -R dovecot -R /mail_crypt/ - chattr + /mail_crypt/ecpubkey.pem /mail_crypt/ecprivkey.pem + chown 401 /mail_crypt/ecprivkey.pem /mail_crypt/ecpubkey.pem +else + chown 401 /mail_crypt/ecprivkey.pem /mail_crypt/ecpubkey.pem fi +# Compile sieve scripts +sievec /var/vmail/sieve/global.sieve +sievec /usr/local/lib/dovecot/sieve/report-spam.sieve +sievec /usr/local/lib/dovecot/sieve/report-ham.sieve + +# Fix permissions +chown -R vmail:vmail /var/vmail/sieve + + exec "$@" From 5bee39dc952ede74f330f5d84400db9e0c9f3ec2 Mon Sep 17 00:00:00 2001 From: andryyy Date: Wed, 5 Apr 2017 22:25:34 +0200 Subject: [PATCH 13/28] Use fixed IDs --- data/Dockerfiles/dovecot/Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/data/Dockerfiles/dovecot/Dockerfile b/data/Dockerfiles/dovecot/Dockerfile index 2d0578a2..90f7352d 100644 --- a/data/Dockerfiles/dovecot/Dockerfile +++ b/data/Dockerfiles/dovecot/Dockerfile @@ -86,11 +86,11 @@ RUN chmod +x /usr/local/lib/dovecot/sieve/rspamd-pipe-ham \ /usr/local/bin/imapsync RUN groupadd -g 5000 vmail \ - && groupadd -g 142 dovecot \ - && groupadd -g 143 dovenull \ + && groupadd -g 401 dovecot \ + && groupadd -g 402 dovenull \ && useradd -g vmail -u 5000 vmail -d /var/vmail \ - && useradd -c "Dovecot unprivileged user" -d /dev/null -u 142 -g dovecot -s /bin/false dovecot \ - && useradd -c "Dovecot login user" -d /dev/null -u 143 -g dovenull -s /bin/false dovenull + && useradd -c "Dovecot unprivileged user" -d /dev/null -u 401 -g dovecot -s /bin/false dovecot \ + && useradd -c "Dovecot login user" -d /dev/null -u 402 -g dovenull -s /bin/false dovenull EXPOSE 24 10001 From 8e3dceb512dc9c491043a9b324dcd1b114663f61 Mon Sep 17 00:00:00 2001 From: andryyy Date: Wed, 5 Apr 2017 22:25:59 +0200 Subject: [PATCH 14/28] Rebase images on stretch --- data/Dockerfiles/rmilter/Dockerfile | 9 ++------- data/Dockerfiles/rspamd/Dockerfile | 9 ++------- data/Dockerfiles/sogo/Dockerfile | 18 ++++++------------ 3 files changed, 10 insertions(+), 26 deletions(-) diff --git a/data/Dockerfiles/rmilter/Dockerfile b/data/Dockerfiles/rmilter/Dockerfile index 364c78c1..1d5db5b0 100644 --- a/data/Dockerfiles/rmilter/Dockerfile +++ b/data/Dockerfiles/rmilter/Dockerfile @@ -1,16 +1,11 @@ -FROM ubuntu:xenial +FROM debian:jessie-slim MAINTAINER Andre Peters ENV DEBIAN_FRONTEND noninteractive ENV LC_ALL C -RUN dpkg-divert --local --rename --add /sbin/initctl \ - && ln -sf /bin/true /sbin/initctl \ - && dpkg-divert --local --rename --add /usr/bin/ischroot \ - && ln -sf /bin/true /usr/bin/ischroot - RUN apt-key adv --fetch-keys http://rspamd.com/apt-stable/gpg.key \ - && echo "deb http://rspamd.com/apt-stable/ xenial main" > /etc/apt/sources.list.d/rspamd.list \ + && echo "deb http://rspamd.com/apt-stable/ jessie main" > /etc/apt/sources.list.d/rspamd.list \ && apt-get update \ && apt-get --no-install-recommends -y --force-yes install rmilter cron syslog-ng syslog-ng-core supervisor diff --git a/data/Dockerfiles/rspamd/Dockerfile b/data/Dockerfiles/rspamd/Dockerfile index 70a0bbab..46a97748 100644 --- a/data/Dockerfiles/rspamd/Dockerfile +++ b/data/Dockerfiles/rspamd/Dockerfile @@ -1,16 +1,11 @@ -FROM ubuntu:xenial +FROM debian:jessie-slim MAINTAINER Andre Peters ENV DEBIAN_FRONTEND noninteractive ENV LC_ALL C -RUN dpkg-divert --local --rename --add /sbin/initctl \ - && ln -sf /bin/true /sbin/initctl \ - && dpkg-divert --local --rename --add /usr/bin/ischroot \ - && ln -sf /bin/true /usr/bin/ischroot - RUN apt-key adv --fetch-keys http://rspamd.com/apt-stable/gpg.key \ - && echo "deb http://rspamd.com/apt-stable/ xenial main" > /etc/apt/sources.list.d/rspamd.list \ + && echo "deb http://rspamd.com/apt-stable/ jessie main" > /etc/apt/sources.list.d/rspamd.list \ && apt-get update \ && apt-get -y install rspamd ca-certificates python-pip diff --git a/data/Dockerfiles/sogo/Dockerfile b/data/Dockerfiles/sogo/Dockerfile index 43960438..348231de 100644 --- a/data/Dockerfiles/sogo/Dockerfile +++ b/data/Dockerfiles/sogo/Dockerfile @@ -1,17 +1,12 @@ -FROM ubuntu:xenial +FROM debian:jessie-slim MAINTAINER Andre Peters ENV DEBIAN_FRONTEND noninteractive ENV LC_ALL C ENV GOSU_VERSION 1.9 -RUN dpkg-divert --local --rename --add /sbin/initctl \ - && ln -sf /bin/true /sbin/initctl \ - && dpkg-divert --local --rename --add /usr/bin/ischroot \ - && ln -sf /bin/true /usr/bin/ischroot - RUN apt-get update \ - && apt-get install -y --no-install-recommends apt-transport-https \ + && apt-get install -y --no-install-recommends apt-transport-https gnupg \ ca-certificates \ wget \ syslog-ng \ @@ -29,8 +24,11 @@ RUN apt-get update \ && chmod +x /usr/local/bin/gosu \ && gosu nobody true +RUN mkdir /usr/share/doc/sogo +RUN touch /usr/share/doc/sogo/empty.sh + RUN apt-key adv --keyserver keys.gnupg.net --recv-key 0x810273C4 \ - && echo "deb http://packages.inverse.ca/SOGo/nightly/3/ubuntu/ xenial xenial" > /etc/apt/sources.list.d/sogo.list \ + && echo "deb http://packages.inverse.ca/SOGo/nightly/3/debian/ jessie jessie" > /etc/apt/sources.list.d/sogo.list \ && apt-get update \ && apt-get -y --force-yes install sogo sogo-activesync @@ -42,10 +40,6 @@ RUN echo '0 0 * * * sogo /usr/sbin/sogo-tool update-autoreply -p /etc/sogo/s COPY ./reconf-domains.sh / COPY supervisord.conf /etc/supervisor/supervisord.conf -#EXPOSE 20000 -#EXPOSE 9191 -#EXPOSE 9192 - CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* From 406a9ffc5bcb0282b9217200a1660bddb49bf993 Mon Sep 17 00:00:00 2001 From: andryyy Date: Wed, 5 Apr 2017 22:26:56 +0200 Subject: [PATCH 15/28] Change hint for TLS enforced messaging --- data/web/lang/lang.de.php | 2 +- data/web/lang/lang.en.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/web/lang/lang.de.php b/data/web/lang/lang.de.php index cd1b34b3..15325cd4 100644 --- a/data/web/lang/lang.de.php +++ b/data/web/lang/lang.de.php @@ -147,7 +147,7 @@ $lang['user']['spamfilter_default_score'] = 'Standardwert:'; $lang['user']['spamfilter_hint'] = 'Der erste Wert beschreibt den "low spam score", der zweite Wert den "high spam score".'; $lang['user']['spamfilter_table_domain_policy'] = "n.v. (Domainrichtlinie)"; -$lang['user']['tls_policy_warning'] = 'Vorsicht: Entscheiden Sie sich unverschlüsselte Verbindungen abzulehnen, kann dies dazu führen, dass Kontakte Sie nicht mehr erreichen.
Nachrichten, die die Richtlinie nicht erfüllen, werden durch einen Hard-Fail im Mailsystem abgewiesen.'; +$lang['user']['tls_policy_warning'] = 'Vorsicht: Entscheiden Sie sich unverschlüsselte Verbindungen abzulehnen, kann dies dazu führen, dass Kontakte Sie nicht mehr erreichen.
Nachrichten, die die Richtlinie nicht erfüllen, werden durch einen Hard-Fail im Mailsystem abgewiesen.
Diese Einstellung ist aktiv für die primäre Mailbox, für alle Alias-Adressen, die dieser Mailbox direkt zugeordnet sind (lediglich eine einzige Ziel-Adresse) und der Adressen, die sich aus Alias-Domains ergeben. Ausgeschlossen sind temporäre Aliasse ("Spam-Alias-Adressen"), Catch-All Alias-Adressen sowie Alias-Adressen mit mehreren Zielen.'; $lang['user']['tls_policy'] = 'Verschlüsselungsrichtlinie'; $lang['user']['tls_enforce_in'] = 'TLS eingehend erzwingen'; $lang['user']['tls_enforce_out'] = 'TLS ausgehend erzwingen'; diff --git a/data/web/lang/lang.en.php b/data/web/lang/lang.en.php index f46c9ac1..121e37b7 100644 --- a/data/web/lang/lang.en.php +++ b/data/web/lang/lang.en.php @@ -149,7 +149,7 @@ $lang['user']['spamfilter_default_score'] = 'Default values:'; $lang['user']['spamfilter_hint'] = 'The first value describes the "low spam score", the second represents the "high spam score".'; $lang['user']['spamfilter_table_domain_policy'] = "n/a (domain policy)"; -$lang['user']['tls_policy_warning'] = 'Warning: If you decide to enforce encrypted mail transfer, you may lose emails.
Messages to not satisfy the policy will be bounced with a hard fail by the mail system.'; +$lang['user']['tls_policy_warning'] = 'Warning: If you decide to enforce encrypted mail transfer, you may lose emails.
Messages to not satisfy the policy will be bounced with a hard fail by the mail system.
This option applies to your primary email address (login name), all addresses derived from alias domains as well as alias addresses with only this single mailbox as target.'; $lang['user']['tls_policy'] = 'Encryption policy'; $lang['user']['tls_enforce_in'] = 'Enforce TLS incoming'; $lang['user']['tls_enforce_out'] = 'Enforce TLS outgoing'; From cc29cfa0d75d46116fb850b0eb3b3b280d902889 Mon Sep 17 00:00:00 2001 From: andryyy Date: Wed, 5 Apr 2017 22:27:30 +0200 Subject: [PATCH 16/28] Add admin section css --- data/web/css/admin.css | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 data/web/css/admin.css diff --git a/data/web/css/admin.css b/data/web/css/admin.css new file mode 100644 index 00000000..61f23763 --- /dev/null +++ b/data/web/css/admin.css @@ -0,0 +1,13 @@ +table.footable>tbody>tr.footable-empty>td { + font-size:15px !important; + font-style:italic; +} +.pagination a { + text-decoration: none !important; +} +.panel panel-default { + overflow: visible !important; +} +.table-responsive { + overflow: visible !important; +} \ No newline at end of file From 6a9468c19127449155cadb1d4911cce00af8c4b6 Mon Sep 17 00:00:00 2001 From: andryyy Date: Wed, 5 Apr 2017 22:27:39 +0200 Subject: [PATCH 17/28] Add admin section css --- data/web/inc/header.inc.php | 1 + 1 file changed, 1 insertion(+) diff --git a/data/web/inc/header.inc.php b/data/web/inc/header.inc.php index a7ec1c52..ede05bed 100644 --- a/data/web/inc/header.inc.php +++ b/data/web/inc/header.inc.php @@ -19,6 +19,7 @@ ' : null;?> +' : null;?> From e96dd7fd77edf6eb795d73e922eaad74a80fcc89 Mon Sep 17 00:00:00 2001 From: andryyy Date: Wed, 5 Apr 2017 22:27:57 +0200 Subject: [PATCH 18/28] Fix typo --- docs/u_and_e.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/u_and_e.md b/docs/u_and_e.md index 5114d6a3..3f52b05e 100644 --- a/docs/u_and_e.md +++ b/docs/u_and_e.md @@ -240,7 +240,7 @@ MariaDB [(none)]> show databases; 4 rows in set (0.00 sec) ``` -**2\. Reset one or more users +**2\. Reset one or more users** Both "password" and "authentication_string" exist. Currently "password" is used, but better set both. From 655f6e713845eefc84755de7e9d72d0d3294467f Mon Sep 17 00:00:00 2001 From: andryyy Date: Wed, 5 Apr 2017 22:32:18 +0200 Subject: [PATCH 19/28] Add Zeyple volume to Postfix --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index ca42c788..8864489d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -186,6 +186,7 @@ services: - ./data/conf/postfix:/opt/postfix/conf - ./data/assets/ssl:/etc/ssl/mail/:ro - postfix-vol-1:/var/spool/postfix + - crypt-vol-1:/var/lib/zeyple environment: - DBNAME=${DBNAME} - DBUSER=${DBUSER} From b63adcab6690bb42e154f3424427dbf73f486d91 Mon Sep 17 00:00:00 2001 From: Emilien Devos Date: Thu, 6 Apr 2017 19:17:19 +0200 Subject: [PATCH 20/28] Warn the user about installing mailcow on Debian 8 --- docs/install.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/install.md b/docs/install.md index 7c383a63..bbffb21a 100644 --- a/docs/install.md +++ b/docs/install.md @@ -1,5 +1,7 @@ ## Install mailcow +**WARNING**: Please use Ubuntu 16.04 instead of Debian 8 or [switch to the kernel 4.9 from jessie backports](https://packages.debian.org/jessie-backports/linux-image-amd64) because there is a bug (kernel panic) with the kernel 3.16 when running docker containers with healthchecks! Full details here: [github.com/docker/docker/issues/30402](https://github.com/docker/docker/issues/30402) and [forum.mailcow.email/t/solved-mailcow-docker-causes-kernel-panic-edit/448](https://forum.mailcow.email/t/solved-mailcow-docker-causes-kernel-panic-edit/448) + You need Docker and Docker Compose. 1\. Learn how to install [Docker](https://docs.docker.com/engine/installation/linux/) and [Docker Compose](https://docs.docker.com/compose/install/). From 022739dcab47ccb6aaf242ec72ea974452a73174 Mon Sep 17 00:00:00 2001 From: andryyy Date: Sat, 8 Apr 2017 23:35:58 +0200 Subject: [PATCH 21/28] Remove reset mysql script --- docker-compose.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8864489d..b3d18790 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,7 +26,6 @@ services: volumes: - mysql-vol-1:/var/lib/mysql/ - ./data/conf/mysql/:/etc/mysql/conf.d/:ro - - ./data/assets/reset_mysql.sh:/reset_mysql.sh dns: - 172.22.1.254 dns_search: mailcow-network From e03136bf214f4f2fdc3050bfbadcd1e549567969 Mon Sep 17 00:00:00 2001 From: andryyy Date: Sat, 8 Apr 2017 23:36:46 +0200 Subject: [PATCH 22/28] Add description to command --- data/Dockerfiles/postfix/postfix.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data/Dockerfiles/postfix/postfix.sh b/data/Dockerfiles/postfix/postfix.sh index f2ba03e4..640538b0 100755 --- a/data/Dockerfiles/postfix/postfix.sh +++ b/data/Dockerfiles/postfix/postfix.sh @@ -101,7 +101,10 @@ chown -R 600:600 /var/lib/zeyple/keys chgrp -R postdrop /var/spool/postfix/public chgrp -R postdrop /var/spool/postfix/maildrop postfix set-permissions + +# Check Postfix configuration postconf -c /opt/postfix/conf + if [[ $? != 0 ]]; then echo "Postfix configuration error, refusing to start." exit 1 From 7339bd446ee92dc5c8c20c14e40eb0219ebd4bea Mon Sep 17 00:00:00 2001 From: andryyy Date: Sat, 8 Apr 2017 23:37:13 +0200 Subject: [PATCH 23/28] How to add a relayhost --- docs/first_steps.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/first_steps.md b/docs/first_steps.md index 21c418db..ab7876dc 100644 --- a/docs/first_steps.md +++ b/docs/first_steps.md @@ -136,6 +136,29 @@ server { } ``` +## Optional: Setup a relayhost + +Insert these lines to `data/conf/postfix/main.cf`. "relayhost" does already exist (empty), just change its value. +``` +relayhost = [your-relayhost]:587 +smtp_sasl_password_maps = hash:/opt/postfix/conf/smarthost_passwd +smtp_sasl_auth_enable = yes +``` + +Create the credentials file: +``` +echo "your-relayhost username:password" > data/conf/postfix/smarthost_passwd +``` + +Run: +``` +docker-compose exec postfix-mailcow postmap /opt/postfix/conf/smarthost_passwd +docker-compose exec postfix-mailcow chown root:postfix /opt/postfix/conf/smarthost_passwd /opt/postfix/conf/smarthost_passwd.db +docker-compose exec postfix-mailcow chmod 660 /opt/postfix/conf/smarthost_passwd /opt/postfix/conf/smarthost_passwd.db +docker-compose exec postfix-mailcow postfix reload +``` + + ## Install a local MTA The easiest option would be to disable the listener on port 25/tcp. From 7f8d6556b8a623cde9c1a322f1dbdff90cf5a405 Mon Sep 17 00:00:00 2001 From: andryyy Date: Mon, 10 Apr 2017 13:07:39 +0200 Subject: [PATCH 24/28] imapsync: added option to delete from source after transfer --- data/conf/dovecot/dovecot.conf | 3 +-- data/web/add.php | 7 +++++++ data/web/edit.php | 14 ++++++++++++++ data/web/inc/functions.inc.php | 15 ++++++++++++--- data/web/lang/lang.de.php | 3 +++ data/web/lang/lang.en.php | 3 +++ data/web/lang/lang.ru.php | 1 + 7 files changed, 41 insertions(+), 5 deletions(-) diff --git a/data/conf/dovecot/dovecot.conf b/data/conf/dovecot/dovecot.conf index 6792eb34..f90c6d50 100644 --- a/data/conf/dovecot/dovecot.conf +++ b/data/conf/dovecot/dovecot.conf @@ -11,8 +11,7 @@ login_log_format_elements = "user=<%u> method=%m rip=%r lip=%l mpid=%e %c %k" mail_home = /var/vmail/%d/%n mail_location = maildir:~/ mail_plugins = quota acl zlib mail_crypt -auth_username_chars = abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-_@ -#ssl_protocols = !SSLv3 !SSLv2 +ssl_protocols = !SSLv3 ssl_prefer_server_ciphers = yes ssl_cipher_list = EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA256:EECDH:+CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!IDEA:!ECDSA:kEDH:CAMELLIA128-SHA:AES128-SHA ssl_options = no_compression diff --git a/data/web/add.php b/data/web/add.php index 9361d14b..39017e98 100644 --- a/data/web/add.php +++ b/data/web/add.php @@ -350,6 +350,13 @@ elseif (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == +
+
+
+ +
+
+
diff --git a/data/web/edit.php b/data/web/edit.php index 5879df6d..3f3311d0 100644 --- a/data/web/edit.php +++ b/data/web/edit.php @@ -620,6 +620,20 @@ elseif (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] ==
+
+
+
+ +
+
+
+
+
+
+ +
+
+
diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index cbc71102..465ab2ac 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -109,6 +109,11 @@ function init_db_schema() { if ($num_results == 0) { $pdo->query("ALTER TABLE `mailbox` ADD `multiple_bookings` tinyint(1) NOT NULL DEFAULT '0'"); } + $stmt = $pdo->query("SHOW COLUMNS FROM `imapsync` LIKE 'delete1'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results == 0) { + $pdo->query("ALTER TABLE `imapsync` ADD `delete1` tinyint(1) NOT NULL DEFAULT '0'"); + } $stmt = $pdo->query("SHOW COLUMNS FROM `mailbox` LIKE 'wants_tagged_subject'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); if ($num_results == 0) { @@ -1075,6 +1080,7 @@ function add_syncjob($postarray) { } isset($postarray['active']) ? $active = '1' : $active = '0'; isset($postarray['delete2duplicates']) ? $delete2duplicates = '1' : $delete2duplicates = '0'; + isset($postarray['delete1']) ? $delete1 = '1' : $delete1 = '0'; $port1 = $postarray['port1']; $host1 = $postarray['host1']; $password1 = $postarray['password1']; @@ -1147,12 +1153,13 @@ function add_syncjob($postarray) { return false; } try { - $stmt = $pdo->prepare("INSERT INTO `imapsync` (`user2`, `exclude`, `maxage`, `subfolder2`, `host1`, `authmech1`, `user1`, `password1`, `mins_interval`, `port1`, `enc1`, `delete2duplicates`, `active`) - VALUES (:user2, :exclude, :maxage, :subfolder2, :host1, :authmech1, :user1, :password1, :mins_interval, :port1, :enc1, :delete2duplicates, :active)"); + $stmt = $pdo->prepare("INSERT INTO `imapsync` (`user2`, `exclude`, `delete1`, `maxage`, `subfolder2`, `host1`, `authmech1`, `user1`, `password1`, `mins_interval`, `port1`, `enc1`, `delete2duplicates`, `active`) + VALUES (:user2, :exclude, :maxage, :delete1, :subfolder2, :host1, :authmech1, :user1, :password1, :mins_interval, :port1, :enc1, :delete2duplicates, :active)"); $stmt->execute(array( ':user2' => $username, ':exclude' => $exclude, ':maxage' => $maxage, + ':delete1' => $delete1, ':subfolder2' => $subfolder2, ':host1' => $host1, ':authmech1' => 'PLAIN', @@ -1200,6 +1207,7 @@ function edit_syncjob($postarray) { } isset($postarray['active']) ? $active = '1' : $active = '0'; isset($postarray['delete2duplicates']) ? $delete2duplicates = '1' : $delete2duplicates = '0'; + isset($postarray['delete1']) ? $delete1 = '1' : $delete1 = '0'; $id = $postarray['id']; $port1 = $postarray['port1']; $host1 = $postarray['host1']; @@ -1273,10 +1281,11 @@ function edit_syncjob($postarray) { return false; } try { - $stmt = $pdo->prepare("UPDATE `imapsync` set `maxage` = :maxage, `subfolder2` = :subfolder2, `exclude` = :exclude, `host1` = :host1, `user1` = :user1, `password1` = :password1, `mins_interval` = :mins_interval, `port1` = :port1, `enc1` = :enc1, `delete2duplicates` = :delete2duplicates, `active` = :active + $stmt = $pdo->prepare("UPDATE `imapsync` set `delete1` = :delete1, `maxage` = :maxage, `subfolder2` = :subfolder2, `exclude` = :exclude, `host1` = :host1, `user1` = :user1, `password1` = :password1, `mins_interval` = :mins_interval, `port1` = :port1, `enc1` = :enc1, `delete2duplicates` = :delete2duplicates, `active` = :active WHERE `user2` = :user2 AND `id` = :id"); $stmt->execute(array( ':user2' => $username, + ':delete1' => $delete1, ':id' => $id, ':exclude' => $exclude, ':maxage' => $maxage, diff --git a/data/web/lang/lang.de.php b/data/web/lang/lang.de.php index b55ad03f..07d23d70 100644 --- a/data/web/lang/lang.de.php +++ b/data/web/lang/lang.de.php @@ -329,6 +329,9 @@ $lang['add']['subfolder2'] = 'Sync into subfolder on destination'; $lang['add']['mins_interval'] = 'Abrufintervall (Minuten)'; $lang['add']['exclude'] = 'Elemente ausschließen (Regex)'; $lang['add']['delete2duplicates'] = 'Lösche Duplikate im Ziel'; +$lang['add']['delete1'] = 'Lösche Nachricht nach Übertragung vom Quell-Server'; +$lang['edit']['delete2duplicates'] = 'Lösche Duplikate im Ziel'; +$lang['edit']['delete1'] = 'Lösche Nachricht nach Übertragung vom Quell-Server'; $lang['add']['title'] = 'Objekt anlegen'; $lang['add']['domain'] = 'Domain'; diff --git a/data/web/lang/lang.en.php b/data/web/lang/lang.en.php index 05e37698..978ee713 100644 --- a/data/web/lang/lang.en.php +++ b/data/web/lang/lang.en.php @@ -333,6 +333,9 @@ $lang['add']['maxage'] = 'Maximum age of messages that will be polled from remot $lang['add']['subfolder2'] = 'Sync into subfolder on destination'; $lang['add']['exclude'] = 'Exclude objects (regex)'; $lang['add']['delete2duplicates'] = 'Delete duplicates on destination'; +$lang['add']['delete1'] = 'Delete from source when completed'; +$lang['edit']['delete2duplicates'] = 'Delete duplicates on destination'; +$lang['edit']['delete1'] = 'Delete from source when completed'; $lang['add']['title'] = 'Add object'; $lang['add']['domain'] = 'Domain'; diff --git a/data/web/lang/lang.ru.php b/data/web/lang/lang.ru.php index e2502b83..d07bbc9f 100644 --- a/data/web/lang/lang.ru.php +++ b/data/web/lang/lang.ru.php @@ -321,6 +321,7 @@ $lang['add']['maxage'] = 'Maximum age of messages that will be polled from remot $lang['add']['subfolder2'] = "Синхронизировать в подпапку по назначению"; $lang['add']['exclude'] = "Исключить объекты (regex)"; $lang['add']['delete2duplicates'] = "Удалить дубликаты в получателях"; +$lang['edit']['delete2duplicates'] = "Удалить дубликаты в получателях"; $lang['add']['title'] = "Добавить объект"; $lang['add']['domain'] = "Домен"; $lang['add']['active'] = "Активный"; From 06928ec84109ecb980d811d494be003def9ec07a Mon Sep 17 00:00:00 2001 From: andryyy Date: Mon, 10 Apr 2017 13:08:02 +0200 Subject: [PATCH 25/28] imapsync: added option to delete from source after transfer --- data/Dockerfiles/dovecot/imapsync_cron.pl | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/data/Dockerfiles/dovecot/imapsync_cron.pl b/data/Dockerfiles/dovecot/imapsync_cron.pl index 5c47eb47..a3cbf8a1 100755 --- a/data/Dockerfiles/dovecot/imapsync_cron.pl +++ b/data/Dockerfiles/dovecot/imapsync_cron.pl @@ -21,7 +21,7 @@ open my $file, '<', "/etc/sogo/sieve.creds"; my $creds = <$file>; close $file; my ($master_user, $master_pass) = split /:/, $creds; -my $sth = $dbh->prepare("SELECT id, user1, user2, host1, authmech1, password1, exclude, port1, enc1, delete2duplicates, maxage, subfolder2 FROM imapsync WHERE active = 1 AND (UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(last_run) > mins_interval * 60 OR last_run IS NULL)"); +my $sth = $dbh->prepare("SELECT id, user1, user2, host1, authmech1, password1, exclude, port1, enc1, delete2duplicates, maxage, subfolder2, delete1 FROM imapsync WHERE active = 1 AND (UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(last_run) > mins_interval * 60 OR last_run IS NULL)"); $sth->execute(); my $row; @@ -39,6 +39,7 @@ while ($row = $sth->fetchrow_arrayref()) { $delete2duplicates = @$row[9]; $maxage = @$row[10]; $subfolder2 = @$row[11]; + $delete1 = @$row[12]; if ($enc1 eq "TLS") { $enc1 = "--tls1"; } elsif ($enc1 eq "SSL") { $enc1 = "--ssl1"; } else { undef $enc1; } @@ -46,11 +47,12 @@ while ($row = $sth->fetchrow_arrayref()) { "--timeout1", "10", "--tmpdir", "/tmp", "--subscribeall", - ($exclude eq "" ? () : ("--exclude", $exclude)), - ($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)), - ($maxage eq "0" ? () : ('--maxage', $maxage)), + ($exclude eq "" ? () : ("--exclude", $exclude)), + ($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)), + ($maxage eq "0" ? () : ('--maxage', $maxage)), ($delete2duplicates ne "1" ? () : ('--delete2duplicates')), - (!defined($enc1) ? () : ($enc1)), + ($delete1 ne "1" ? () : ('--delete')), + (!defined($enc1) ? () : ($enc1)), "--host1", $host1, "--user1", $user1, "--password1", $password1, From c460636a702a7d1176c7d1c3c39eda0edf74180e Mon Sep 17 00:00:00 2001 From: andryyy Date: Mon, 10 Apr 2017 13:09:33 +0200 Subject: [PATCH 26/28] mail_crypt is not enabled by default --- data/conf/dovecot/dovecot.conf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/conf/dovecot/dovecot.conf b/data/conf/dovecot/dovecot.conf index f90c6d50..b4c2287a 100644 --- a/data/conf/dovecot/dovecot.conf +++ b/data/conf/dovecot/dovecot.conf @@ -239,9 +239,9 @@ plugin { sieve_max_script_size = 1M sieve_quota_max_scripts = 0 sieve_quota_max_storage = 0 - mail_crypt_global_private_key = Date: Mon, 10 Apr 2017 13:15:48 +0200 Subject: [PATCH 27/28] mail_crypt is not enabled by default --- data/conf/dovecot/dovecot.conf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/conf/dovecot/dovecot.conf b/data/conf/dovecot/dovecot.conf index b4c2287a..b4501e1a 100644 --- a/data/conf/dovecot/dovecot.conf +++ b/data/conf/dovecot/dovecot.conf @@ -10,7 +10,7 @@ disable_plaintext_auth = yes login_log_format_elements = "user=<%u> method=%m rip=%r lip=%l mpid=%e %c %k" mail_home = /var/vmail/%d/%n mail_location = maildir:~/ -mail_plugins = quota acl zlib mail_crypt +mail_plugins = quota acl zlib #mail_crypt ssl_protocols = !SSLv3 ssl_prefer_server_ciphers = yes ssl_cipher_list = EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA256:EECDH:+CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!IDEA:!ECDSA:kEDH:CAMELLIA128-SHA:AES128-SHA @@ -205,10 +205,10 @@ userdb { driver = sql } protocol imap { - mail_plugins = quota imap_quota imap_acl acl zlib imap_zlib imap_sieve mail_crypt + mail_plugins = quota imap_quota imap_acl acl zlib imap_zlib imap_sieve #mail_crypt } protocol lmtp { - mail_plugins = quota sieve acl zlib mail_crypt + mail_plugins = quota sieve acl zlib #mail_crypt auth_socket_path = /usr/local/var/run/dovecot/auth-master } protocol sieve { From d0d87ead49332a32b022c4540e0e53c41715da4e Mon Sep 17 00:00:00 2001 From: andryyy Date: Mon, 10 Apr 2017 13:16:40 +0200 Subject: [PATCH 28/28] Zeyple is not enabled by default --- data/conf/postfix/main.cf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/conf/postfix/main.cf b/data/conf/postfix/main.cf index 46de4759..b28f6eb9 100644 --- a/data/conf/postfix/main.cf +++ b/data/conf/postfix/main.cf @@ -91,4 +91,4 @@ smtpd_milters = inet:rmilter:9900 non_smtpd_milters = inet:rmilter:9900 milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen} mydestination = localhost.localdomain, localhost -content_filter=zeyple +#content_filter=zeyple
Server:Port Log