diff --git a/data/Dockerfiles/dovecot/docker-entrypoint.sh b/data/Dockerfiles/dovecot/docker-entrypoint.sh index 9c626fa9..756dea7e 100755 --- a/data/Dockerfiles/dovecot/docker-entrypoint.sh +++ b/data/Dockerfiles/dovecot/docker-entrypoint.sh @@ -199,6 +199,8 @@ if [[ $(stat -c %U /var/vmail_index) != "vmail" ]] ; then chown -R vmail:vmail / # Cleanup random user maildirs rm -rf /var/vmail/mailcow.local/* +# Cleanup PIDs +[[ -f /tmp/quarantine_notify.pid ]] && rm /tmp/quarantine_notify.pid # create sni configuration echo "" > /etc/dovecot/sni.conf diff --git a/data/Dockerfiles/dovecot/quarantine_notify.py b/data/Dockerfiles/dovecot/quarantine_notify.py index adf3171c..3ab4430b 100755 --- a/data/Dockerfiles/dovecot/quarantine_notify.py +++ b/data/Dockerfiles/dovecot/quarantine_notify.py @@ -2,6 +2,7 @@ import smtplib import os +import sys import mysql.connector from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText @@ -15,137 +16,154 @@ import time import html2text import socket -while True: - try: - r = redis.StrictRedis(host='redis', decode_responses=True, port=6379, db=0) - r.ping() - except Exception as ex: - print('%s - trying again...' % (ex)) - time.sleep(3) - else: - break +pid = str(os.getpid()) +pidfile = "/tmp/quarantine_notify.pid" -time_now = int(time.time()) -mailcow_hostname = '__MAILCOW_HOSTNAME__' +if os.path.isfile(pidfile): + print("%s already exists, exiting" % (pidfile)) + sys.exit() -max_score = float(r.get('Q_MAX_SCORE') or "9999.0") -if max_score == "": - max_score = 9999.0 +pid = str(os.getpid()) +f = open(pidfile, 'w') +f.write(pid) +f.close() + +try: -def query_mysql(query, headers = True, update = False): while True: try: - cnx = mysql.connector.connect(unix_socket = '/var/run/mysqld/mysqld.sock', user='__DBUSER__', passwd='__DBPASS__', database='__DBNAME__', charset="utf8") + r = redis.StrictRedis(host='redis', decode_responses=True, port=6379, db=0) + r.ping() except Exception as ex: print('%s - trying again...' % (ex)) time.sleep(3) else: break - cur = cnx.cursor() - cur.execute(query) - if not update: - result = [] - columns = tuple( [d[0] for d in cur.description] ) - for row in cur: - if headers: - result.append(dict(list(zip(columns, row)))) - else: - result.append(row) - cur.close() - cnx.close() - return result - else: - cnx.commit() - cur.close() - cnx.close() -def notify_rcpt(rcpt, msg_count, quarantine_acl, category): - if category == "add_header": category = "add header" - meta_query = query_mysql('SELECT SHA2(CONCAT(id, qid), 256) AS qhash, id, subject, score, sender, created, action FROM quarantine WHERE notified = 0 AND rcpt = "%s" AND score < %f AND (action = "%s" OR "all" = "%s")' % (rcpt, max_score, category, category)) - print("%s: %d of %d messages qualify for notification" % (rcpt, len(meta_query), msg_count)) - if len(meta_query) == 0: - return - msg_count = len(meta_query) - if r.get('Q_HTML'): - try: - template = Template(r.get('Q_HTML')) - except: - print("Error: Cannot parse quarantine template, falling back to default template.") + time_now = int(time.time()) + mailcow_hostname = '__MAILCOW_HOSTNAME__' + + max_score = float(r.get('Q_MAX_SCORE') or "9999.0") + if max_score == "": + max_score = 9999.0 + + def query_mysql(query, headers = True, update = False): + while True: + try: + cnx = mysql.connector.connect(unix_socket = '/var/run/mysqld/mysqld.sock', user='__DBUSER__', passwd='__DBPASS__', database='__DBNAME__', charset="utf8") + except Exception as ex: + print('%s - trying again...' % (ex)) + time.sleep(3) + else: + break + cur = cnx.cursor() + cur.execute(query) + if not update: + result = [] + columns = tuple( [d[0] for d in cur.description] ) + for row in cur: + if headers: + result.append(dict(list(zip(columns, row)))) + else: + result.append(row) + cur.close() + cnx.close() + return result + else: + cnx.commit() + cur.close() + cnx.close() + + def notify_rcpt(rcpt, msg_count, quarantine_acl, category): + if category == "add_header": category = "add header" + meta_query = query_mysql('SELECT SHA2(CONCAT(id, qid), 256) AS qhash, id, subject, score, sender, created, action FROM quarantine WHERE notified = 0 AND rcpt = "%s" AND score < %f AND (action = "%s" OR "all" = "%s")' % (rcpt, max_score, category, category)) + print("%s: %d of %d messages qualify for notification" % (rcpt, len(meta_query), msg_count)) + if len(meta_query) == 0: + return + msg_count = len(meta_query) + if r.get('Q_HTML'): + try: + template = Template(r.get('Q_HTML')) + except: + print("Error: Cannot parse quarantine template, falling back to default template.") + with open('/templates/quarantine.tpl') as file_: + template = Template(file_.read()) + else: with open('/templates/quarantine.tpl') as file_: template = Template(file_.read()) - else: - with open('/templates/quarantine.tpl') as file_: - template = Template(file_.read()) - html = template.render(meta=meta_query, username=rcpt, counter=msg_count, hostname=mailcow_hostname, quarantine_acl=quarantine_acl) - text = html2text.html2text(html) - count = 0 - while count < 15: - count += 1 + html = template.render(meta=meta_query, username=rcpt, counter=msg_count, hostname=mailcow_hostname, quarantine_acl=quarantine_acl) + text = html2text.html2text(html) + count = 0 + while count < 15: + count += 1 + try: + server = smtplib.SMTP('postfix', 590, 'quarantine') + server.ehlo() + msg = MIMEMultipart('alternative') + msg_from = r.get('Q_SENDER') or "quarantine@localhost" + # Remove non-ascii chars from field + msg['From'] = ''.join([i if ord(i) < 128 else '' for i in msg_from]) + msg['Subject'] = r.get('Q_SUBJ') or "Spam Quarantine Notification" + msg['Date'] = formatdate(localtime = True) + text_part = MIMEText(text, 'plain', 'utf-8') + html_part = MIMEText(html, 'html', 'utf-8') + msg.attach(text_part) + msg.attach(html_part) + msg['To'] = str(rcpt) + bcc = r.get('Q_BCC') or "" + redirect = r.get('Q_REDIRECT') or "" + text = msg.as_string() + if bcc == '': + if redirect == '': + server.sendmail(msg['From'], str(rcpt), text) + else: + server.sendmail(msg['From'], str(redirect), text) + else: + if redirect == '': + server.sendmail(msg['From'], [str(rcpt)] + [str(bcc)], text) + else: + server.sendmail(msg['From'], [str(redirect)] + [str(bcc)], text) + server.quit() + for res in meta_query: + query_mysql('UPDATE quarantine SET notified = 1 WHERE id = "%d"' % (res['id']), update = True) + r.hset('Q_LAST_NOTIFIED', record['rcpt'], time_now) + break + except Exception as ex: + server.quit() + print('%s' % (ex)) + time.sleep(3) + + records = query_mysql('SELECT IFNULL(user_acl.quarantine, 0) AS quarantine_acl, count(id) AS counter, rcpt FROM quarantine LEFT OUTER JOIN user_acl ON user_acl.username = rcpt WHERE notified = 0 AND score < %f AND rcpt in (SELECT username FROM mailbox) GROUP BY rcpt' % (max_score)) + + for record in records: + attrs = '' + attrs_json = '' + time_trans = { + "hourly": 3600, + "daily": 86400, + "weekly": 604800 + } try: - server = smtplib.SMTP('postfix', 590, 'quarantine') - server.ehlo() - msg = MIMEMultipart('alternative') - msg_from = r.get('Q_SENDER') or "quarantine@localhost" - # Remove non-ascii chars from field - msg['From'] = ''.join([i if ord(i) < 128 else '' for i in msg_from]) - msg['Subject'] = r.get('Q_SUBJ') or "Spam Quarantine Notification" - msg['Date'] = formatdate(localtime = True) - text_part = MIMEText(text, 'plain', 'utf-8') - html_part = MIMEText(html, 'html', 'utf-8') - msg.attach(text_part) - msg.attach(html_part) - msg['To'] = str(rcpt) - bcc = r.get('Q_BCC') or "" - redirect = r.get('Q_REDIRECT') or "" - text = msg.as_string() - if bcc == '': - if redirect == '': - server.sendmail(msg['From'], str(rcpt), text) - else: - server.sendmail(msg['From'], str(redirect), text) - else: - if redirect == '': - server.sendmail(msg['From'], [str(rcpt)] + [str(bcc)], text) - else: - server.sendmail(msg['From'], [str(redirect)] + [str(bcc)], text) - server.quit() - for res in meta_query: - query_mysql('UPDATE quarantine SET notified = 1 WHERE id = "%d"' % (res['id']), update = True) - r.hset('Q_LAST_NOTIFIED', record['rcpt'], time_now) - break + last_notification = int(r.hget('Q_LAST_NOTIFIED', record['rcpt'])) + if last_notification > time_now: + print('Last notification is > time now, assuming never') + last_notification = 0 except Exception as ex: - server.quit() - print('%s' % (ex)) - time.sleep(3) - -records = query_mysql('SELECT IFNULL(user_acl.quarantine, 0) AS quarantine_acl, count(id) AS counter, rcpt FROM quarantine LEFT OUTER JOIN user_acl ON user_acl.username = rcpt WHERE notified = 0 AND score < %f AND rcpt in (SELECT username FROM mailbox) GROUP BY rcpt' % (max_score)) - -for record in records: - attrs = '' - attrs_json = '' - time_trans = { - "hourly": 3600, - "daily": 86400, - "weekly": 604800 - } - try: - last_notification = int(r.hget('Q_LAST_NOTIFIED', record['rcpt'])) - if last_notification > time_now: - print('Last notification is > time now, assuming never') + print('Could not determine last notification for %s, assuming never' % (record['rcpt'])) last_notification = 0 - except Exception as ex: - print('Could not determine last notification for %s, assuming never' % (record['rcpt'])) - last_notification = 0 - attrs_json = query_mysql('SELECT attributes FROM mailbox WHERE username = "%s"' % (record['rcpt'])) - attrs = attrs_json[0]['attributes'] - if isinstance(attrs, str): - # if attr is str then just load it - attrs = json.loads(attrs) - else: - # if it's bytes then decode and load it - attrs = json.loads(attrs.decode('utf-8')) - if attrs['quarantine_notification'] not in ('hourly', 'daily', 'weekly'): - continue - if last_notification == 0 or (last_notification + time_trans[attrs['quarantine_notification']]) < time_now: - print("Notifying %s: Considering %d new items in quarantine (policy: %s)" % (record['rcpt'], record['counter'], attrs['quarantine_notification'])) - notify_rcpt(record['rcpt'], record['counter'], record['quarantine_acl'], attrs['quarantine_category']) + attrs_json = query_mysql('SELECT attributes FROM mailbox WHERE username = "%s"' % (record['rcpt'])) + attrs = attrs_json[0]['attributes'] + if isinstance(attrs, str): + # if attr is str then just load it + attrs = json.loads(attrs) + else: + # if it's bytes then decode and load it + attrs = json.loads(attrs.decode('utf-8')) + if attrs['quarantine_notification'] not in ('hourly', 'daily', 'weekly'): + continue + if last_notification == 0 or (last_notification + time_trans[attrs['quarantine_notification']]) < time_now: + print("Notifying %s: Considering %d new items in quarantine (policy: %s)" % (record['rcpt'], record['counter'], attrs['quarantine_notification'])) + notify_rcpt(record['rcpt'], record['counter'], record['quarantine_acl'], attrs['quarantine_category']) + +finally: + os.unlink(pidfile) \ No newline at end of file diff --git a/data/conf/sogo/custom-theme.js b/data/conf/sogo/custom-theme.js new file mode 100644 index 00000000..66fd36a5 --- /dev/null +++ b/data/conf/sogo/custom-theme.js @@ -0,0 +1,86 @@ +/* -*- Mode: javascript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ + +(function() { + 'use strict'; + + angular.module('SOGo.Common') + .config(configure) + + /** + * @ngInject + */ + configure.$inject = ['$mdThemingProvider']; + function configure($mdThemingProvider) { + + /** + * The SOGo palettes are defined in js/Common/Common.app.js: + * + * - sogo-green + * - sogo-blue + * - sogo-grey + * + * The Material palettes are also available: + * + * - red + * - pink + * - purple + * - deep-purple + * - indigo + * - blue + * - light-blue + * - cyan + * - teal + * - green + * - light-green + * - lime + * - yellow + * - amber + * - orange + * - deep-orange + * - brown + * - grey + * - blue-grey + * + * See https://material.angularjs.org/latest/Theming/01_introduction + * and https://material.io/archive/guidelines/style/color.html#color-color-palette + * + * You can also define your own palettes. See js/Common/Common.app.js. + */ + + // Create new background palette from grey palette + var greyMap = $mdThemingProvider.extendPalette('grey', { + // background color of sidebar selected item, + // background color of right panel, + // background color of menus (autocomplete and contextual menus) + '200': 'F5F5F5', + // background color of sidebar + '300': 'F3F3F3', + // background color of the busy periods of the attendees editor + '1000': '4C566A' + }); + var greenCow = $mdThemingProvider.extendPalette('green', { + '600': 'f3f3f3' + }); + + $mdThemingProvider.definePalette('frost-grey', greyMap); + $mdThemingProvider.definePalette('green-cow', greenCow); + + // Apply new palettes to the default theme, remap some of the hues + $mdThemingProvider.theme('default') + .primaryPalette('green-cow', { + 'default': '400', // background color of top toolbars + 'hue-1': '400', + 'hue-2': '600', // background color of sidebar toolbar + 'hue-3': 'A700' + }) + .accentPalette('green', { + 'default': '600', // background color of fab buttons + 'hue-1': '300', // background color of center list toolbar + 'hue-2': '300', + 'hue-3': 'A700' + }) + .backgroundPalette('frost-grey'); + + $mdThemingProvider.generateThemesOnDemand(false); + } +})(); diff --git a/data/conf/sogo/sogo-full.svg b/data/conf/sogo/sogo-full.svg index 98ff2fc3..b5d3adac 100644 --- a/data/conf/sogo/sogo-full.svg +++ b/data/conf/sogo/sogo-full.svg @@ -1,44 +1,55 @@ - - - -]> - - - + + + +image/svg+xml + + \ No newline at end of file diff --git a/data/conf/sogo/sogo.conf b/data/conf/sogo/sogo.conf index 78791d58..9f6568f5 100644 --- a/data/conf/sogo/sogo.conf +++ b/data/conf/sogo/sogo.conf @@ -14,7 +14,12 @@ SOGoEnableEMailAlarms = YES; SOGoFoldersSendEMailNotifications = YES; SOGoForwardEnabled = YES; - SOGoUIAdditionalJSFiles = (js/custom-sogo.js); + + SOGoUIAdditionalJSFiles = ( + js/theme.js, + js/custom-sogo.js + ); + SOGoEnablePublicAccess = YES; // Multi-domain setup @@ -80,7 +85,7 @@ //LDAPDebugEnabled = YES; //PGDebugEnabled = YES; //MySQL4DebugEnabled = YES; - //SOGoUIxDebugEnabled = YES; + SOGoUIxDebugEnabled = YES; //WODontZipResponse = YES; WOLogFile = "/dev/sogo_log"; } diff --git a/docker-compose.yml b/docker-compose.yml index 9de2c6c4..948f4f22 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -160,7 +160,7 @@ services: - phpfpm sogo-mailcow: - image: mailcow/sogo:1.94 + image: mailcow/sogo:1.95 environment: - DBNAME=${DBNAME} - DBUSER=${DBUSER} @@ -183,6 +183,7 @@ services: - ./data/conf/sogo/:/etc/sogo/:z - ./data/web/inc/init_db.inc.php:/init_db.inc.php:Z - ./data/conf/sogo/custom-sogo.js:/usr/lib/GNUstep/SOGo/WebServerResources/js/custom-sogo.js:z + - ./data/conf/sogo/custom-theme.js:/usr/lib/GNUstep/SOGo/WebServerResources/js/theme.js:z - mysql-socket-vol-1:/var/run/mysqld/:z - sogo-web-vol-1:/sogo_web:z - sogo-userdata-backup-vol-1:/sogo_backup:Z