diff --git a/core/admin/Dockerfile b/core/admin/Dockerfile index fa75e8dc..edb8c1fd 100644 --- a/core/admin/Dockerfile +++ b/core/admin/Dockerfile @@ -3,33 +3,40 @@ ARG DISTRO=alpine:3.14 ARG ARCH="" FROM ${ARCH}node:16 as assets -COPY --from=balenalib/rpi-alpine:3.14 /usr/bin/qemu-arm-static /usr/bin/qemu-arm-static COPY package.json ./ -RUN npm install +RUN set -eu \ + && npm config set update-notifier false \ + && npm install --no-fund -COPY ./webpack.config.js ./ -COPY ./assets ./assets -RUN mkdir static \ - && ./node_modules/.bin/webpack-cli +COPY webpack.config.js ./ +COPY assets ./assets +RUN set -eu \ + && sed -i 's/#007bff/#367fa9/' node_modules/admin-lte/build/scss/_bootstrap-variables.scss \ + && for l in ca da de:de_de en:en-gb es:es_es eu fr:fr_fr he hu is it:it_it ja nb_NO:no_nb nl:nl_nl pl pt:pt_pt ru sv:sv_se zh_CN:zh; do \ + cp node_modules/datatables.net-plugins/i18n/${l#*:}.json assets/${l%:*}.json; \ + done \ + && node_modules/.bin/webpack-cli --color # Actual application FROM $DISTRO +COPY --from=balenalib/rpi-alpine:3.14 /usr/bin/qemu-arm-static /usr/bin/qemu-arm-static + # python3 shared with most images -RUN apk add --no-cache \ - python3 py3-pip git bash \ - && pip3 install --upgrade pip +RUN set -eu \ + && apk add --no-cache python3 py3-pip git bash \ + && pip3 install --upgrade pip RUN mkdir -p /app WORKDIR /app COPY requirements-prod.txt requirements.txt -RUN apk add --no-cache openssl curl postgresql-libs mariadb-connector-c \ - && apk add --no-cache --virtual build-dep \ - openssl-dev libffi-dev python3-dev build-base postgresql-dev mariadb-connector-c-dev cargo \ - && pip3 install -r requirements.txt \ - && apk del --no-cache build-dep +RUN set -eu \ + && apk add --no-cache openssl curl postgresql-libs mariadb-connector-c \ + && apk add --no-cache --virtual build-dep openssl-dev libffi-dev python3-dev build-base postgresql-dev mariadb-connector-c-dev cargo \ + && pip3 install -r requirements.txt \ + && apk del --no-cache build-dep COPY --from=assets static ./mailu/ui/static COPY mailu ./mailu diff --git a/core/admin/assets/app.css b/core/admin/assets/app.css index 8351eed8..12df605c 100644 --- a/core/admin/assets/app.css +++ b/core/admin/assets/app.css @@ -1,23 +1,51 @@ -.select2-search--inline .select2-search__field:focus { - border: none; +/* mailu logo */ +.mailu-logo { + opacity: .8; } -.sidebar h4 { - padding-left: 5px; - padding-right: 5px; - overflow: hidden; - text-overflow: ellipsis; +/* user image */ +.div-circle { + position: relative; + width: 2.1rem; + height: 2.1rem; + opacity: .8; + background-color: white; + border-radius: 50%; +} +.div-circle > i { + display: block; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) } -.sidebar-collapse .sidebar h4 { - display: none !important; +/* nice round preformatted configuration display */ +.pre-config { + padding: 9px; + margin: 0; + white-space: pre-wrap; + word-wrap: anywhere; + border-radius: 4px; } -.logo a { - color: #fff; +/* fieldset */ +legend { + font-size: inherit; +} +fieldset:disabled :not(legend) label { + opacity: .5; +} +fieldset:disabled .form-control:disabled { + color: gray; } -.sidebar-toggle { - padding: unset !important; +/* fix animation for icons in menu text */ +.sidebar .nav-link p i { + transition: margin-left .3s linear,opacity .3s ease,visibility .3s ease; } +/* fix select2 text color */ +.select2-container--default .select2-selection--multiple .select2-selection__choice { + color: black; +} diff --git a/core/admin/assets/app.js b/core/admin/assets/app.js index 364f8429..dc3414f2 100644 --- a/core/admin/assets/app.js +++ b/core/admin/assets/app.js @@ -1,17 +1,70 @@ require('./app.css'); -import 'admin-lte/plugins/select2/js/select2.js'; -import 'admin-lte/plugins/datatables/jquery.dataTables.js'; -import 'admin-lte/plugins/datatables-bs4/js/dataTables.bootstrap4.js'; -import 'admin-lte/plugins/datatables-responsive/js/dataTables.responsive.js'; -import 'admin-lte/plugins/datatables-responsive/js/responsive.bootstrap4.js'; +import logo from './mailu.png'; +import modules from "./*.json"; -jQuery("document").ready(function() { - jQuery(".mailselect").select2({ +// TODO: conditionally (or lazy) load select2 and dataTable +$('document').ready(function() { + + // intercept anchors with data-clicked attribute and open alternate location instead + $('[data-clicked]').click(function(e) { + e.preventDefault(); + window.location.href = $(this).data('clicked'); + }); + + // use post for language selection + $('#mailu-languages > a').click(function(e) { + e.preventDefault(); + $.post({ + url: $(this).attr('href'), + success: function() { + location.reload(); + }, + }); + }); + + // allow en-/disabling of inputs in fieldset with checkbox in legend + $('fieldset legend input[type=checkbox]').change(function() { + var fieldset = $(this).parents('fieldset'); + if (this.checked) { + fieldset.removeAttr('disabled'); + fieldset.find('input').not(this).removeAttr('disabled'); + } else { + fieldset.attr('disabled', ''); + fieldset.find('input').not(this).attr('disabled', ''); + } + }); + + // display of range input value + $('input[type=range]').each(function() { + var value_element = $('#'+this.id+'_value'); + if (value_element.length) { + value_element = $(value_element[0]); + var infinity = $(this).data('infinity'); + var step = $(this).attr('step'); + $(this).on('input', function() { + value_element.text((infinity && this.value == 0) ? '∞' : this.value/step); + }).trigger('input'); + } + }); + + // init select2 + $('.mailselect').select2({ tags: true, - tokenSeparators: [',', ' '] + tokenSeparators: [',', ' '], }); - jQuery(".dataTable").DataTable({ - "responsive": true, + + // init dataTable + var d = $(document.documentElement); + $('.dataTable').DataTable({ + 'responsive': true, + language: { + url: d.data('static') + d.attr('lang') + '.json', + }, }); + + // init clipboard.js + new ClipboardJS('.btn-clip'); + }); + diff --git a/core/admin/assets/mailu.png b/core/admin/assets/mailu.png new file mode 100644 index 00000000..e4f5021f Binary files /dev/null and b/core/admin/assets/mailu.png differ diff --git a/core/admin/assets/vendor.js b/core/admin/assets/vendor.js index fd43d918..906448cf 100644 --- a/core/admin/assets/vendor.js +++ b/core/admin/assets/vendor.js @@ -1,22 +1,24 @@ -// jQuery -import jQuery from 'jquery'; -import 'admin-lte/plugins/select2/css/select2.css'; - -// bootstrap -// import 'bootstrap/less/bootstrap.less'; -// import 'bootstrap'; - -// FontAwesome -import 'admin-lte/plugins/fontawesome-free/css/fontawesome.css'; -import 'admin-lte/plugins/fontawesome-free/css/regular.css'; -import 'admin-lte/plugins/fontawesome-free/css/solid.css'; - // AdminLTE +import 'admin-lte/plugins/jquery/jquery.min.js'; +import 'admin-lte/plugins/bootstrap/js/bootstrap.bundle.min.js'; import 'admin-lte/build/scss/adminlte.scss'; -import 'admin-lte/plugins/datatables-bs4/css/dataTables.bootstrap4.css'; -import 'admin-lte/plugins/datatables-responsive/css/responsive.bootstrap4.css'; -import 'admin-lte/plugins/bootstrap/js/bootstrap.js'; import 'admin-lte/build/js/AdminLTE.js'; -import 'admin-lte/build/js/Layout.js'; -import 'admin-lte/build/js/ControlSidebar.js'; -import 'admin-lte/build/js/PushMenu.js'; + +// fontawesome plugin +import 'admin-lte/plugins/fontawesome-free/css/all.min.css'; + +// select2 plugin +import 'admin-lte/plugins/select2/css/select2.min.css'; +import 'admin-lte/plugins/select2/js/select2.min.js'; + +// dataTables plugin +import 'admin-lte/plugins/datatables-bs4/css/dataTables.bootstrap4.min.css'; +import 'admin-lte/plugins/datatables-responsive/css/responsive.bootstrap4.min.css'; +import 'admin-lte/plugins/datatables/jquery.dataTables.min.js'; +import 'admin-lte/plugins/datatables-bs4/js/dataTables.bootstrap4.min.js'; +import 'admin-lte/plugins/datatables-responsive/js/dataTables.responsive.min.js'; +import 'admin-lte/plugins/datatables-responsive/js/responsive.bootstrap4.min.js'; + +// clipboard.js +import 'clipboard/dist/clipboard.min.js'; + diff --git a/core/admin/mailu/__init__.py b/core/admin/mailu/__init__.py index 8ab8ed0e..9b712512 100644 --- a/core/admin/mailu/__init__.py +++ b/core/admin/mailu/__init__.py @@ -14,8 +14,7 @@ def create_app_from_config(config): app = flask.Flask(__name__) app.cli.add_command(manage.mailu) - # Bootstrap is used for basic JS and CSS loading - # TODO: remove this and use statically generated assets instead + # Bootstrap is used for error display and flash messages app.bootstrap = flask_bootstrap.Bootstrap(app) # Initialize application extensions @@ -31,6 +30,15 @@ def create_app_from_config(config): app.temp_token_key = hmac.new(bytearray(app.secret_key, 'utf-8'), bytearray('WEBMAIL_TEMP_TOKEN_KEY', 'utf-8'), 'sha256').digest() + # Initialize list of translations + config.translations = { + str(locale): locale + for locale in sorted( + utils.babel.list_translations(), + key=lambda l: l.get_language_name().title() + ) + } + # Initialize debugging tools if app.config.get("DEBUG"): debug.toolbar.init_app(app) @@ -43,8 +51,8 @@ def create_app_from_config(config): def inject_defaults(): signup_domains = models.Domain.query.filter_by(signup_enabled=True).all() return dict( - signup_domains=signup_domains, - config=app.config + signup_domains= signup_domains, + config = app.config, ) # Import views diff --git a/core/admin/mailu/configuration.py b/core/admin/mailu/configuration.py index 7cd3a56b..419dc18d 100644 --- a/core/admin/mailu/configuration.py +++ b/core/admin/mailu/configuration.py @@ -143,6 +143,7 @@ class ConfigManager(dict): self.config['SESSION_COOKIE_SAMESITE'] = 'Strict' self.config['SESSION_COOKIE_HTTPONLY'] = True self.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=int(self.config['SESSION_LIFETIME'])) + self.config['HOSTNAME'] = self.config['HOSTNAMES'].split(',', 1)[0].strip() # update the app config itself app.config = self diff --git a/core/admin/mailu/models.py b/core/admin/mailu/models.py index 5760c27f..6b0791ad 100644 --- a/core/admin/mailu/models.py +++ b/core/admin/mailu/models.py @@ -209,16 +209,16 @@ class Domain(Base): os.unlink(file_path) self._dkim_key_on_disk = self._dkim_key - @property + @cached_property def dns_mx(self): """ return MX record for domain """ - hostname = app.config['HOSTNAMES'].split(',', 1)[0] + hostname = app.config['HOSTNAME'] return f'{self.name}. 600 IN MX 10 {hostname}.' - @property + @cached_property def dns_spf(self): """ return SPF record for domain """ - hostname = app.config['HOSTNAMES'].split(',', 1)[0] + hostname = app.config['HOSTNAME'] return f'{self.name}. 600 IN TXT "v=spf1 mx a:{hostname} ~all"' @property @@ -226,12 +226,11 @@ class Domain(Base): """ return DKIM record for domain """ if self.dkim_key: selector = app.config['DKIM_SELECTOR'] - return ( - f'{selector}._domainkey.{self.name}. 600 IN TXT' - f'"v=DKIM1; k=rsa; p={self.dkim_publickey}"' - ) + txt = f'v=DKIM1; k=rsa; p={self.dkim_publickey}' + record = ' '.join(f'"{txt[p:p+250]}"' for p in range(0, len(txt), 250)) + return f'{selector}._domainkey.{self.name}. 600 IN TXT {record}' - @property + @cached_property def dns_dmarc(self): """ return DMARC record for domain """ if self.dkim_key: @@ -242,6 +241,34 @@ class Domain(Base): ruf = f' ruf=mailto:{ruf}@{domain};' if ruf else '' return f'_dmarc.{self.name}. 600 IN TXT "v=DMARC1; p=reject;{rua}{ruf} adkim=s; aspf=s"' + @cached_property + def dns_autoconfig(self): + """ return list of auto configuration records (RFC6186) """ + hostname = app.config['HOSTNAME'] + protocols = [ + ('submission', 587), + ('imap', 143), + ('pop3', 110), + ] + if app.config['TLS_FLAVOR'] != 'notls': + protocols.extend([ + ('imaps', 993), + ('pop3s', 995), + ]) + return list([ + f'_{proto}._tcp.{self.name}. 600 IN SRV 1 1 {port} {hostname}.' + for proto, port + in protocols + ]) + + @cached_property + def dns_tlsa(self): + """ return TLSA record for domain when using letsencrypt """ + hostname = app.config['HOSTNAME'] + if True: #app.config['TLS_FLAVOR'] in ('letsencrypt', 'mail-letsencrypt'): + # current ISRG Root X1 (RSA 4096, O = Internet Security Research Group, CN = ISRG Root X1) @20210902 + return f'_25._tcp.{hostname}. 600 IN TLSA 2 1 1 0b9fa5a59eed715c26c1020c711b4f6ec42d58b0015e14337a39dad301c5afc3' + @property def dkim_key(self): """ return private DKIM key """ @@ -285,7 +312,7 @@ class Domain(Base): def check_mx(self): """ checks if MX record for domain points to mailu host """ try: - hostnames = set(app.config['HOSTNAMES'].split(',')) + hostnames = set(fqdn.strip() for fqdn in app.config['HOSTNAMES'].split(',')) return any( rset.exchange.to_text().rstrip('.') in hostnames for rset in dns.resolver.query(self.name, 'MX') diff --git a/core/admin/mailu/ui/forms.py b/core/admin/mailu/ui/forms.py index 32bb31ab..60be1699 100644 --- a/core/admin/mailu/ui/forms.py +++ b/core/admin/mailu/ui/forms.py @@ -88,7 +88,7 @@ class UserForm(flask_wtf.FlaskForm): localpart = fields.StringField(_('E-mail'), [validators.DataRequired(), validators.Regexp(LOCALPART_REGEX)]) pw = fields.PasswordField(_('Password')) pw2 = fields.PasswordField(_('Confirm password'), [validators.EqualTo('pw')]) - quota_bytes = fields_.IntegerSliderField(_('Quota'), default=1000000000) + quota_bytes = fields_.IntegerSliderField(_('Quota'), default=10**9) enable_imap = fields.BooleanField(_('Allow IMAP access'), default=True) enable_pop = fields.BooleanField(_('Allow POP3 access'), default=True) displayed_name = fields.StringField(_('Displayed name')) diff --git a/core/admin/mailu/ui/templates/admin/create.html b/core/admin/mailu/ui/templates/admin/create.html index 6c2413bc..071cb77f 100644 --- a/core/admin/mailu/ui/templates/admin/create.html +++ b/core/admin/mailu/ui/templates/admin/create.html @@ -1,15 +1,15 @@ -{% extends "base.html" %} +{%- extends "base.html" %} -{% block title %} +{%- block title %} {% trans %}Add a global administrator{% endtrans %} -{% endblock %} +{%- endblock %} -{% block content %} -{% call macros.card() %} +{%- block content %} +{%- call macros.card() %}
{{ form.hidden_tag() }} {{ macros.form_field(form.admin, class_='mailselect') }} {{ macros.form_field(form.submit) }}
-{% endcall %} -{% endblock %} +{%- endcall %} +{%- endblock %} diff --git a/core/admin/mailu/ui/templates/admin/list.html b/core/admin/mailu/ui/templates/admin/list.html index f2f5d229..84d954a0 100644 --- a/core/admin/mailu/ui/templates/admin/list.html +++ b/core/admin/mailu/ui/templates/admin/list.html @@ -1,17 +1,17 @@ -{% extends "base.html" %} +{%- extends "base.html" %} -{% block title %} +{%- block title %} {% trans %}Global administrators{% endtrans %} -{% endblock %} +{%- endblock %} -{% block main_action %} +{%- block main_action %} {% trans %}Add administrator{% endtrans %} -{% endblock %} +{%- endblock %} -{% block content %} -{% call macros.table() %} +{%- block content %} +{%- call macros.table() %} {% trans %}Actions{% endtrans %} @@ -19,14 +19,14 @@ - {% for admin in admins %} + {%- for admin in admins %} {{ admin }} - {% endfor %} + {%- endfor %} -{% endcall %} -{% endblock %} +{%- endcall %} +{%- endblock %} diff --git a/core/admin/mailu/ui/templates/alias/create.html b/core/admin/mailu/ui/templates/alias/create.html index 2079d191..ce9f8167 100644 --- a/core/admin/mailu/ui/templates/alias/create.html +++ b/core/admin/mailu/ui/templates/alias/create.html @@ -1,15 +1,15 @@ -{% extends "base.html" %} +{%- extends "base.html" %} -{% block title %} +{%- block title %} {% trans %}Create alias{% endtrans %} -{% endblock %} +{%- endblock %} -{% block subtitle %} +{%- block subtitle %} {{ domain }} -{% endblock %} +{%- endblock %} -{% block content %} -{% call macros.card() %} +{%- block content %} +{%- call macros.card() %}
{{ form.hidden_tag() }} {{ macros.form_field(form.localpart, append='@'+domain.name+'') }} @@ -18,5 +18,5 @@ {{ macros.form_field(form.comment) }} {{ macros.form_field(form.submit) }}
-{% endcall %} -{% endblock %} +{%- endcall %} +{%- endblock %} diff --git a/core/admin/mailu/ui/templates/alias/edit.html b/core/admin/mailu/ui/templates/alias/edit.html index b28ea170..4dc13cce 100644 --- a/core/admin/mailu/ui/templates/alias/edit.html +++ b/core/admin/mailu/ui/templates/alias/edit.html @@ -1,9 +1,9 @@ -{% extends "alias/create.html" %} +{%- extends "alias/create.html" %} -{% block title %} +{%- block title %} {% trans %}Edit alias{% endtrans %} -{% endblock %} +{%- endblock %} -{% block subtitle %} +{%- block subtitle %} {{ alias }} -{% endblock %} +{%- endblock %} diff --git a/core/admin/mailu/ui/templates/alias/list.html b/core/admin/mailu/ui/templates/alias/list.html index e8ddc862..0b784d52 100644 --- a/core/admin/mailu/ui/templates/alias/list.html +++ b/core/admin/mailu/ui/templates/alias/list.html @@ -1,19 +1,19 @@ -{% extends "base.html" %} +{%- extends "base.html" %} -{% block title %} +{%- block title %} {% trans %}Alias list{% endtrans %} -{% endblock %} +{%- endblock %} -{% block subtitle %} +{%- block subtitle %} {{ domain.name }} -{% endblock %} +{%- endblock %} -{% block main_action %} +{%- block main_action %} {% trans %}Add alias{% endtrans %} -{% endblock %} +{%- endblock %} -{% block content %} -{% call macros.table() %} +{%- block content %} +{%- call macros.table() %} {% trans %}Actions{% endtrans %} @@ -25,7 +25,7 @@ - {% for alias in domain.aliases %} + {%- for alias in domain.aliases %}   @@ -37,7 +37,7 @@ {{ alias.created_at }} {{ alias.updated_at or '' }} - {% endfor %} + {%- endfor %} -{% endcall %} -{% endblock %} +{%- endcall %} +{%- endblock %} diff --git a/core/admin/mailu/ui/templates/alternative/create.html b/core/admin/mailu/ui/templates/alternative/create.html index 75461c67..f10cb718 100644 --- a/core/admin/mailu/ui/templates/alternative/create.html +++ b/core/admin/mailu/ui/templates/alternative/create.html @@ -1,9 +1,9 @@ -{% extends "form.html" %} +{%- extends "form.html" %} -{% block title %} +{%- block title %} {% trans %}Create alternative domain{% endtrans %} -{% endblock %} +{%- endblock %} -{% block subtitle %} +{%- block subtitle %} {{ domain }} -{% endblock %} +{%- endblock %} diff --git a/core/admin/mailu/ui/templates/alternative/list.html b/core/admin/mailu/ui/templates/alternative/list.html index f123eb9f..b56cd751 100644 --- a/core/admin/mailu/ui/templates/alternative/list.html +++ b/core/admin/mailu/ui/templates/alternative/list.html @@ -1,19 +1,19 @@ -{% extends "base.html" %} +{%- extends "base.html" %} -{% block title %} +{%- block title %} {% trans %}Alternative domain list{% endtrans %} -{% endblock %} +{%- endblock %} -{% block subtitle %} +{%- block subtitle %} {{ domain.name }} -{% endblock %} +{%- endblock %} -{% block main_action %} +{%- block main_action %} {% trans %}Add alternative{% endtrans %} -{% endblock %} +{%- endblock %} -{% block content %} -{% call macros.table() %} +{%- block content %} +{%- call macros.table() %} {% trans %}Actions{% endtrans %} @@ -22,7 +22,7 @@ - {% for alternative in domain.alternatives %} + {%- for alternative in domain.alternatives %} @@ -30,7 +30,7 @@ {{ alternative }} {{ alternative.created_at }} - {% endfor %} + {%- endfor %} -{% endcall %} -{% endblock %} +{%- endcall %} +{%- endblock %} diff --git a/core/admin/mailu/ui/templates/announcement.html b/core/admin/mailu/ui/templates/announcement.html index acdbde1a..ed7fe772 100644 --- a/core/admin/mailu/ui/templates/announcement.html +++ b/core/admin/mailu/ui/templates/announcement.html @@ -1,16 +1,16 @@ -{% extends "base.html" %} +{%- extends "base.html" %} -{% block title %} +{%- block title %} {% trans %}Public announcement{% endtrans %} -{% endblock %} +{%- endblock %} -{% block content %} -{% call macros.card() %} +{%- block content %} +{%- call macros.card() %}
{{ form.hidden_tag() }} {{ macros.form_field(form.announcement_subject) }} {{ macros.form_field(form.announcement_body, rows=10) }} {{ macros.form_field(form.submit) }}
-{% endcall %} -{% endblock %} +{%- endcall %} +{%- endblock %} diff --git a/core/admin/mailu/ui/templates/antispam.html b/core/admin/mailu/ui/templates/antispam.html new file mode 100644 index 00000000..0b2713b9 --- /dev/null +++ b/core/admin/mailu/ui/templates/antispam.html @@ -0,0 +1,15 @@ +{%- extends "base.html" %} + +{%- block title %} +{% trans %}Antispam{% endtrans %} +{%- endblock %} + +{%- block subtitle %} +{% trans %}RSPAMD status page{% endtrans %} +{%- endblock %} + +{%- block content %} +
+ +
+{%- endblock %} diff --git a/core/admin/mailu/ui/templates/base.html b/core/admin/mailu/ui/templates/base.html index 89695e50..acef4b86 100644 --- a/core/admin/mailu/ui/templates/base.html +++ b/core/admin/mailu/ui/templates/base.html @@ -1,65 +1,83 @@ -{% import "macros.html" as macros %} -{% import "bootstrap/utils.html" as utils %} +{%- import "macros.html" as macros %} +{%- import "bootstrap/utils.html" as utils %} - + - + + + + + Mailu-Admin | {{ config["SITENAME"] }} - Mailu-Admin - {{ config["SITENAME"] }}
-