1966: AdminLTE3 optimizations & compression and caching r=mergify[bot] a=ghostwheel42

## What type of PR?

enhancement, bugfix

## What does this PR do?

Optimization and cleanup of styles and javascript code for AdminLTE 3
Adds caching headers, gzip and robots.txt to nginx.

### Related issue(s)

Makes #1800 even better. Thanks to `@DjVinnii` and `@Diman0` for the good work.
Closes #1905

## Prerequistes

Before we can consider review and merge, please make sure the following list is done and checked.
If an entry in not applicable, you can check it or remove it from the list.

- [X] In case of feature or enhancement: documentation updated accordingly
- [X] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Alexander Graf <ghostwheel42@users.noreply.github.com>
Co-authored-by: Dimitri Huisman <diman@huisman.xyz>
master
bors[bot] 3 years ago committed by GitHub
commit 4c5c6c3b5f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -3,33 +3,40 @@ ARG DISTRO=alpine:3.14.2
ARG ARCH="" ARG ARCH=""
FROM ${ARCH}node:16 as assets 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 ./ COPY package.json ./
RUN npm install RUN set -eu \
&& npm config set update-notifier false \
&& npm install --no-fund
COPY ./webpack.config.js ./ COPY webpack.config.js ./
COPY ./assets ./assets COPY assets ./assets
RUN mkdir static \ RUN set -eu \
&& ./node_modules/.bin/webpack-cli && sed -i 's/#007bff/#55a5d9/' 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 # Actual application
FROM $DISTRO FROM $DISTRO
COPY --from=balenalib/rpi-alpine:3.14 /usr/bin/qemu-arm-static /usr/bin/qemu-arm-static
# python3 shared with most images # python3 shared with most images
RUN apk add --no-cache \ RUN set -eu \
python3 py3-pip git bash \ && apk add --no-cache python3 py3-pip git bash \
&& pip3 install --upgrade pip && pip3 install --upgrade pip
RUN mkdir -p /app RUN mkdir -p /app
WORKDIR /app WORKDIR /app
COPY requirements-prod.txt requirements.txt COPY requirements-prod.txt requirements.txt
RUN apk add --no-cache libressl curl postgresql-libs mariadb-connector-c \ RUN set -eu \
&& apk add --no-cache --virtual build-dep \ && apk add --no-cache libressl curl postgresql-libs mariadb-connector-c \
libressl-dev libffi-dev python3-dev build-base postgresql-dev mariadb-connector-c-dev cargo \ && apk add --no-cache --virtual build-dep libressl-dev libffi-dev python3-dev build-base postgresql-dev mariadb-connector-c-dev cargo \
&& pip3 install -r requirements.txt \ && pip3 install -r requirements.txt \
&& apk del --no-cache build-dep && apk del --no-cache build-dep
COPY --from=assets static ./mailu/ui/static COPY --from=assets static ./mailu/ui/static
COPY mailu ./mailu COPY mailu ./mailu

@ -1,23 +1,54 @@
.select2-search--inline .select2-search__field:focus { /* mailu logo */
border: none; .mailu-logo {
opacity: .8;
}
.bg-mailu-logo {
background-color: #2980b9!important;
} }
.sidebar h4 { /* user image */
padding-left: 5px; .div-circle {
padding-right: 5px; position: relative;
overflow: hidden; width: 2.1rem;
text-overflow: ellipsis; 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 { /* nice round preformatted configuration display */
display: none !important; .pre-config {
padding: 9px;
margin: 0;
white-space: pre-wrap;
word-wrap: anywhere;
border-radius: 4px;
} }
.logo a { /* fieldset */
color: #fff; legend {
font-size: inherit;
}
fieldset:disabled :not(legend) label {
opacity: .5;
}
fieldset:disabled .form-control:disabled {
color: gray;
} }
.sidebar-toggle { /* fix animation for icons in menu text */
padding: unset !important; .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;
}

@ -1,17 +1,70 @@
require('./app.css'); require('./app.css');
import 'admin-lte/plugins/select2/js/select2.js'; import logo from './mailu.png';
import 'admin-lte/plugins/datatables/jquery.dataTables.js'; import modules from "./*.json";
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';
jQuery("document").ready(function() { // TODO: conditionally (or lazy) load select2 and dataTable
jQuery(".mailselect").select2({ $('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, 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');
}); });

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

@ -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 // 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/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/AdminLTE.js';
import 'admin-lte/build/js/Layout.js';
import 'admin-lte/build/js/ControlSidebar.js'; // fontawesome plugin
import 'admin-lte/build/js/PushMenu.js'; 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';

@ -14,8 +14,7 @@ def create_app_from_config(config):
app = flask.Flask(__name__) app = flask.Flask(__name__)
app.cli.add_command(manage.mailu) app.cli.add_command(manage.mailu)
# Bootstrap is used for basic JS and CSS loading # Bootstrap is used for error display and flash messages
# TODO: remove this and use statically generated assets instead
app.bootstrap = flask_bootstrap.Bootstrap(app) app.bootstrap = flask_bootstrap.Bootstrap(app)
# Initialize application extensions # 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() 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 # Initialize debugging tools
if app.config.get("DEBUG"): if app.config.get("DEBUG"):
debug.toolbar.init_app(app) debug.toolbar.init_app(app)
@ -43,8 +51,8 @@ def create_app_from_config(config):
def inject_defaults(): def inject_defaults():
signup_domains = models.Domain.query.filter_by(signup_enabled=True).all() signup_domains = models.Domain.query.filter_by(signup_enabled=True).all()
return dict( return dict(
signup_domains=signup_domains, signup_domains= signup_domains,
config=app.config config = app.config,
) )
# Import views # Import views

@ -57,6 +57,8 @@ DEFAULT_CONFIG = {
'WEBMAIL': 'none', 'WEBMAIL': 'none',
'RECAPTCHA_PUBLIC_KEY': '', 'RECAPTCHA_PUBLIC_KEY': '',
'RECAPTCHA_PRIVATE_KEY': '', 'RECAPTCHA_PRIVATE_KEY': '',
'LOGO_URL': None,
'LOGO_BACKGROUND': None,
# Advanced settings # Advanced settings
'LOG_LEVEL': 'WARNING', 'LOG_LEVEL': 'WARNING',
'SESSION_KEY_BITS': 128, 'SESSION_KEY_BITS': 128,
@ -144,6 +146,9 @@ class ConfigManager(dict):
self.config['SESSION_COOKIE_SAMESITE'] = 'Strict' self.config['SESSION_COOKIE_SAMESITE'] = 'Strict'
self.config['SESSION_COOKIE_HTTPONLY'] = True self.config['SESSION_COOKIE_HTTPONLY'] = True
self.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=int(self.config['SESSION_LIFETIME'])) self.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=int(self.config['SESSION_LIFETIME']))
hostnames = [host.strip() for host in self.config['HOSTNAMES'].split(',')]
self.config['HOSTNAMES'] = ','.join(hostnames)
self.config['HOSTNAME'] = hostnames[0]
# update the app config itself # update the app config itself
app.config = self app.config = self

@ -209,16 +209,16 @@ class Domain(Base):
os.unlink(file_path) os.unlink(file_path)
self._dkim_key_on_disk = self._dkim_key self._dkim_key_on_disk = self._dkim_key
@property @cached_property
def dns_mx(self): def dns_mx(self):
""" return MX record for domain """ """ 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}.' return f'{self.name}. 600 IN MX 10 {hostname}.'
@property @cached_property
def dns_spf(self): def dns_spf(self):
""" return SPF record for domain """ """ 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"' return f'{self.name}. 600 IN TXT "v=spf1 mx a:{hostname} ~all"'
@property @property
@ -226,12 +226,11 @@ class Domain(Base):
""" return DKIM record for domain """ """ return DKIM record for domain """
if self.dkim_key: if self.dkim_key:
selector = app.config['DKIM_SELECTOR'] selector = app.config['DKIM_SELECTOR']
return ( txt = f'v=DKIM1; k=rsa; p={self.dkim_publickey}'
f'{selector}._domainkey.{self.name}. 600 IN TXT' record = ' '.join(f'"{txt[p:p+250]}"' for p in range(0, len(txt), 250))
f'"v=DKIM1; k=rsa; p={self.dkim_publickey}"' return f'{selector}._domainkey.{self.name}. 600 IN TXT {record}'
)
@property @cached_property
def dns_dmarc(self): def dns_dmarc(self):
""" return DMARC record for domain """ """ return DMARC record for domain """
if self.dkim_key: if self.dkim_key:
@ -242,6 +241,34 @@ class Domain(Base):
ruf = f' ruf=mailto:{ruf}@{domain};' if ruf else '' 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"' 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 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 @property
def dkim_key(self): def dkim_key(self):
""" return private DKIM key """ """ return private DKIM key """

@ -88,7 +88,7 @@ class UserForm(flask_wtf.FlaskForm):
localpart = fields.StringField(_('E-mail'), [validators.DataRequired(), validators.Regexp(LOCALPART_REGEX)]) localpart = fields.StringField(_('E-mail'), [validators.DataRequired(), validators.Regexp(LOCALPART_REGEX)])
pw = fields.PasswordField(_('Password')) pw = fields.PasswordField(_('Password'))
pw2 = fields.PasswordField(_('Confirm password'), [validators.EqualTo('pw')]) 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_imap = fields.BooleanField(_('Allow IMAP access'), default=True)
enable_pop = fields.BooleanField(_('Allow POP3 access'), default=True) enable_pop = fields.BooleanField(_('Allow POP3 access'), default=True)
displayed_name = fields.StringField(_('Displayed name')) displayed_name = fields.StringField(_('Displayed name'))

@ -1,15 +1,15 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Add a global administrator{% endtrans %} {% trans %}Add a global administrator{% endtrans %}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.card() %} {%- call macros.card() %}
<form class="form" method="post" role="form"> <form class="form" method="post" role="form">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{{ macros.form_field(form.admin, class_='mailselect') }} {{ macros.form_field(form.admin, class_='mailselect') }}
{{ macros.form_field(form.submit) }} {{ macros.form_field(form.submit) }}
</form> </form>
{% endcall %} {%- endcall %}
{% endblock %} {%- endblock %}

@ -1,17 +1,17 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Global administrators{% endtrans %} {% trans %}Global administrators{% endtrans %}
{% endblock %} {%- endblock %}
{% block main_action %} {%- block main_action %}
<a class="btn btn-primary float-right" href="{{ url_for('.admin_create') }}"> <a class="btn btn-primary float-right" href="{{ url_for('.admin_create') }}">
{% trans %}Add administrator{% endtrans %} {% trans %}Add administrator{% endtrans %}
</a> </a>
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.table() %} {%- call macros.table() %}
<thead> <thead>
<tr> <tr>
<th>{% trans %}Actions{% endtrans %}</th> <th>{% trans %}Actions{% endtrans %}</th>
@ -19,14 +19,14 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for admin in admins %} {%- for admin in admins %}
<tr> <tr>
<td> <td>
<a href="{{ url_for('.admin_delete', admin=admin.email) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a> <a href="{{ url_for('.admin_delete', admin=admin.email) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
</td> </td>
<td>{{ admin }}</td> <td>{{ admin }}</td>
</tr> </tr>
{% endfor %} {%- endfor %}
</tbody> </tbody>
{% endcall %} {%- endcall %}
{% endblock %} {%- endblock %}

@ -1,15 +1,15 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Create alias{% endtrans %} {% trans %}Create alias{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ domain }} {{ domain }}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.card() %} {%- call macros.card() %}
<form class="form" method="post" role="form"> <form class="form" method="post" role="form">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{{ macros.form_field(form.localpart, append='<span class="input-group-text">@'+domain.name+'</span>') }} {{ macros.form_field(form.localpart, append='<span class="input-group-text">@'+domain.name+'</span>') }}
@ -18,5 +18,5 @@
{{ macros.form_field(form.comment) }} {{ macros.form_field(form.comment) }}
{{ macros.form_field(form.submit) }} {{ macros.form_field(form.submit) }}
</form> </form>
{% endcall %} {%- endcall %}
{% endblock %} {%- endblock %}

@ -1,9 +1,9 @@
{% extends "alias/create.html" %} {%- extends "alias/create.html" %}
{% block title %} {%- block title %}
{% trans %}Edit alias{% endtrans %} {% trans %}Edit alias{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ alias }} {{ alias }}
{% endblock %} {%- endblock %}

@ -1,19 +1,19 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Alias list{% endtrans %} {% trans %}Alias list{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ domain.name }} {{ domain.name }}
{% endblock %} {%- endblock %}
{% block main_action %} {%- block main_action %}
<a class="btn btn-primary float-right" href="{{ url_for('.alias_create', domain_name=domain.name) }}">{% trans %}Add alias{% endtrans %}</a> <a class="btn btn-primary float-right" href="{{ url_for('.alias_create', domain_name=domain.name) }}">{% trans %}Add alias{% endtrans %}</a>
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.table() %} {%- call macros.table() %}
<thead> <thead>
<tr> <tr>
<th>{% trans %}Actions{% endtrans %}</th> <th>{% trans %}Actions{% endtrans %}</th>
@ -25,7 +25,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for alias in domain.aliases %} {%- for alias in domain.aliases %}
<tr> <tr>
<td> <td>
<a href="{{ url_for('.alias_edit', alias=alias.email) }}" title="{% trans %}Edit{% endtrans %}"><i class="fa fa-pencil"></i></a>&nbsp; <a href="{{ url_for('.alias_edit', alias=alias.email) }}" title="{% trans %}Edit{% endtrans %}"><i class="fa fa-pencil"></i></a>&nbsp;
@ -37,7 +37,7 @@
<td>{{ alias.created_at }}</td> <td>{{ alias.created_at }}</td>
<td>{{ alias.updated_at or '' }}</td> <td>{{ alias.updated_at or '' }}</td>
</tr> </tr>
{% endfor %} {%- endfor %}
</tbody> </tbody>
{% endcall %} {%- endcall %}
{% endblock %} {%- endblock %}

@ -1,9 +1,9 @@
{% extends "form.html" %} {%- extends "form.html" %}
{% block title %} {%- block title %}
{% trans %}Create alternative domain{% endtrans %} {% trans %}Create alternative domain{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ domain }} {{ domain }}
{% endblock %} {%- endblock %}

@ -1,19 +1,19 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Alternative domain list{% endtrans %} {% trans %}Alternative domain list{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ domain.name }} {{ domain.name }}
{% endblock %} {%- endblock %}
{% block main_action %} {%- block main_action %}
<a class="btn btn-primary float-right" href="{{ url_for('.alternative_create', domain_name=domain.name) }}">{% trans %}Add alternative{% endtrans %}</a> <a class="btn btn-primary float-right" href="{{ url_for('.alternative_create', domain_name=domain.name) }}">{% trans %}Add alternative{% endtrans %}</a>
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.table() %} {%- call macros.table() %}
<thead> <thead>
<tr> <tr>
<th>{% trans %}Actions{% endtrans %}</th> <th>{% trans %}Actions{% endtrans %}</th>
@ -22,7 +22,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for alternative in domain.alternatives %} {%- for alternative in domain.alternatives %}
<tr> <tr>
<td> <td>
<a href="{{ url_for('.alternative_delete', alternative=alternative.name) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a> <a href="{{ url_for('.alternative_delete', alternative=alternative.name) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
@ -30,7 +30,7 @@
<td>{{ alternative }}</td> <td>{{ alternative }}</td>
<td>{{ alternative.created_at }}</td> <td>{{ alternative.created_at }}</td>
</tr> </tr>
{% endfor %} {%- endfor %}
</tbody> </tbody>
{% endcall %} {%- endcall %}
{% endblock %} {%- endblock %}

@ -1,16 +1,16 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Public announcement{% endtrans %} {% trans %}Public announcement{% endtrans %}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.card() %} {%- call macros.card() %}
<form class="form" method="post" role="form"> <form class="form" method="post" role="form">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{{ macros.form_field(form.announcement_subject) }} {{ macros.form_field(form.announcement_subject) }}
{{ macros.form_field(form.announcement_body, rows=10) }} {{ macros.form_field(form.announcement_body, rows=10) }}
{{ macros.form_field(form.submit) }} {{ macros.form_field(form.submit) }}
</form> </form>
{% endcall %} {%- endcall %}
{% endblock %} {%- endblock %}

@ -0,0 +1,15 @@
{%- extends "base.html" %}
{%- block title %}
{% trans %}Antispam{% endtrans %}
{%- endblock %}
{%- block subtitle %}
{% trans %}RSPAMD status page{% endtrans %}
{%- endblock %}
{%- block content %}
<div class="embed-responsive embed-responsive-1by1">
<iframe class="embed-responsive-item" src="{{ config["WEB_ADMIN"] }}/antispam/"></iframe>
</div>
{%- endblock %}

@ -1,65 +1,83 @@
{% import "macros.html" as macros %} {%- import "macros.html" as macros %}
{% import "bootstrap/utils.html" as utils %} {%- import "bootstrap/utils.html" as utils %}
<!doctype html> <!doctype html>
<html> <html lang="{{ session['language'] }}" data-static="{{ url_for('.static', filename='') }}">
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="{% trans %}Admin page for{% endtrans %} {{ config["SITENAME"] }}">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Mailu-Admin | {{ config["SITENAME"] }}</title>
<link rel="stylesheet" href="{{ url_for('.static', filename='vendor.css') }}"> <link rel="stylesheet" href="{{ url_for('.static', filename='vendor.css') }}">
<link rel="stylesheet" href="{{ url_for('.static', filename='app.css') }}"> <link rel="stylesheet" href="{{ url_for('.static', filename='app.css') }}">
<title>Mailu-Admin - {{ config["SITENAME"] }}</title>
</head> </head>
<body class="hold-transition sidebar-mini layout-fixed"> <body class="hold-transition sidebar-mini layout-fixed">
<div class="wrapper"> <div class="wrapper">
<nav class="main-header navbar navbar-expand navbar-white navbar-light"> <nav class="main-header navbar navbar-expand navbar-white navbar-light">
<ul class="navbar-nav"> <ul class="navbar-nav">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" data-widget="pushmenu" href="#" role="button"><i class="fas fa-bars"></i></a> <a class="nav-link" data-widget="pushmenu" href="#" role="button"><i class="fas fa-bars" title="{% trans %}toggle sidebar{% endtrans %}" aria-expanded="false"></i><span class="sr-only">{% trans %}toggle sidebar{% endtrans %}</span></a>
</li>
<li class="nav-item">
{%- for page, url in path %}
{%- if loop.index > 1 %}
<i class="fas fa-greater-than text-xs text-gray" aria-hidden="true"></i>
{%- endif %}
{%- if url %}
<a class="nav-link d-inline-block" href="{{ url }}" role="button">{{ page }}</a>
{%- else %}
<span class="nav-link d-inline-block">{{ page }}</span>
{%- endif %}
{%- endfor %}
</li> </li>
</ul> </ul>
<ul class="navbar-nav ml-auto"> <ul class="navbar-nav ml-auto">
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link" data-toggle="dropdown" href="#" aria-expanded="false">{{ session['language'] }}</a> <a class="nav-link" data-toggle="dropdown" href="#" aria-expanded="false">
<div class="dropdown-menu dropdown-menu-right p-0"> <i class="fas fa-language text-xl" aria-hidden="true" title="{% trans %}change language{% endtrans %}"></i><span class="sr-only">Language</span>
{% for language in session['available_languages'] %} <span class="badge badge-primary navbar-badge">{{ session['language'] }}</span></a>
<a class="dropdown-item {% if language == session['language'] %}active{% endif %} " href="{{ url_for('.set_language', language=language) }}">{{ language }}</a> <div class="dropdown-menu dropdown-menu-right p-0" id="mailu-languages">
{% endfor %} {%- for locale in config.translations.values() %}
<a class="dropdown-item{% if locale.language == session['language'] %} active{% endif %}" href="{{ url_for('.set_language', language=locale.language) }}">{{ locale.get_language_name().title() }}</a>
{%- endfor %}
</div> </div>
</li> </li>
</ul> </ul>
</nav> </nav>
<aside class="main-sidebar sidebar-dark-primary"> <aside class="main-sidebar sidebar-dark-primary nav-compact elevation-4">
<a href="{{ config["WEB_ADMIN"] }}" class="brand-link"> <a href="{{ url_for('.domain_list' if current_user.manager_of or current_user.global_admin else '.user_settings') }}" class="brand-link bg-mailu-logo"{% if config["LOGO_BACKGROUND"] %} style="background-color:{{ config["LOGO_BACKGROUND"] }}!important;"{% endif %}>
<span class="brand-text font-weight-light">{{ config["SITENAME"] }}</span> <img src="{{ config["LOGO_URL"] if config["LOGO_URL"] else url_for('.static', filename='mailu.png') }}" width="33" height="33" alt="Mailu" class="brand-image mailu-logo img-circle elevation-3">
<span class="brand-text font-weight-light">{{ config["SITENAME"] }}</span>
</a> </a>
{% block sidebar %} {%- include "sidebar.html" %}
{% include "sidebar.html" %}
{% endblock %}
</aside> </aside>
<div class="content-wrapper"> <div class="content-wrapper text-sm">
<section class="content-header"> <section class="content-header">
<div class="container-fluid"> <div class="container-fluid">
<div class="row mb-2"> <div class="row mb-2">
<div class="col-sm-6"> <div class="col-sm-6">
<h1 class="m-0">{% block title %}{% endblock %}</h1> <h1 class="m-0">{%- block title %}{%- endblock %}</h1>
<small>{% block subtitle %}{% endblock %}</small> <small>{% block subtitle %}{% endblock %}</small>
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
{% block main_action %} {%- block main_action %}{%- endblock %}
{% endblock %}
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<div class="content"> <div class="content">
{{ utils.flashed_messages(container=False) }} {{ utils.flashed_messages(container=False, default_category='success') }}
{% block content %}{% endblock %} {%- block content %}{%- endblock %}
</div> </div>
</div> </div>
<footer class="main-footer"> <footer class="main-footer">
Built with <i class="fa fa-heart"></i> using <a class="white-text" href="http://flask.pocoo.org/">Flask</a> and Built with <i class="fa fa-heart text-danger" aria-hidden="true"></i><span class="sr-only">love</span>
<a class="white-text" href="https://adminlte.io/themes/v3/index3.html">AdminLTE</a> using <a href="https://flask.palletsprojects.com/">Flask</a>
<span class="pull-right"><i class="fa fa-code-fork"></i>on <a class="white-text" href="https://github.com/Mailu/Mailu">Github</a></a></span> and <a href="https://adminlte.io/themes/v3/index3.html">AdminLTE</a>.
<span class="fa-pull-right">
<i class="fa fa-code-branch" aria-hidden="true"></i><span class="sr-only">fork</span>
on <a href="https://github.com/Mailu/Mailu">Github</a>
</span>
</footer> </footer>
</div> </div>
<script src="{{ url_for('.static', filename='vendor.js') }}"></script> <script src="{{ url_for('.static', filename='vendor.js') }}"></script>

@ -1,17 +1,15 @@
<!--TODO add translations for: configure your client, Incoming mail and Outgoing mail--> {%- extends "base.html" %}
{% extends "base.html" %} {%- block title %}
{% block title %}
{% trans %}Client setup{% endtrans %} {% trans %}Client setup{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
configure your email client {% trans %}configure your email client{% endtrans %}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.table(title="Incoming mail", datatable=False) %} {%- call macros.table(title=_("Incoming mail"), datatable=False) %}
<tbody> <tbody>
<tr> <tr>
<th>{% trans %}Mail protocol{% endtrans %}</th> <th>{% trans %}Mail protocol{% endtrans %}</th>
@ -23,20 +21,20 @@ configure your email client
</tr> </tr>
<tr> <tr>
<th>{% trans %}Server name{% endtrans %}</th> <th>{% trans %}Server name{% endtrans %}</th>
<td><pre>{{ config["HOSTNAMES"].split(',')[0] }}</pre></td> <td><pre class="pre-config border bg-light">{{ config["HOSTNAMES"] }}</pre></td>
</tr> </tr>
<tr> <tr>
<th>{% trans %}Username{% endtrans %}</th> <th>{% trans %}Username{% endtrans %}</th>
<td><pre>{{ current_user if current_user.is_authenticated else "******" }}</pre></td> <td><pre class="pre-config border bg-light">{{ current_user if current_user.is_authenticated else "******" }}</pre></td>
</tr> </tr>
<tr> <tr>
<th>{% trans %}Password{% endtrans %}</th> <th>{% trans %}Password{% endtrans %}</th>
<td><pre>*******</pre></td> <td><pre class="pre-config border bg-light">*******</pre></td>
</tr> </tr>
</tbody> </tbody>
{% endcall %} {%- endcall %}
{% call macros.table(title="Outgoing mail", datatable=False) %} {%- call macros.table(title=_("Outgoing mail"), datatable=False) %}
<tbody> <tbody>
<tr> <tr>
<th>{% trans %}Mail protocol{% endtrans %}</th> <th>{% trans %}Mail protocol{% endtrans %}</th>
@ -48,16 +46,16 @@ configure your email client
</tr> </tr>
<tr> <tr>
<th>{% trans %}Server name{% endtrans %}</th> <th>{% trans %}Server name{% endtrans %}</th>
<td><pre>{{ config["HOSTNAMES"].split(',')[0] }}</pre></td> <td><pre class="pre-config border bg-light">{{ config["HOSTNAMES"] }}</pre></td>
</tr> </tr>
<tr> <tr>
<th>{% trans %}Username{% endtrans %}</th> <th>{% trans %}Username{% endtrans %}</th>
<td><pre>{{ current_user if current_user.is_authenticated else "******" }}</pre></td> <td><pre class="pre-config border bg-light">{{ current_user if current_user.is_authenticated else "******" }}</pre></td>
</tr> </tr>
<tr> <tr>
<th>{% trans %}Password{% endtrans %}</th> <th>{% trans %}Password{% endtrans %}</th>
<td><pre>*******</pre></td> <td><pre class="pre-config border bg-light">*******</pre></td>
</tr> </tr>
</tbody> </tbody>
{% endcall %} {%- endcall %}
{% endblock %} {%- endblock %}

@ -1,16 +1,16 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Confirm action{% endtrans %} {% trans %}Confirm action{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ action }} {{ action }}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.card(theme="warning") %} {%- call macros.card(theme="warning") %}
<p>{% trans action %}You are about to {{ action }}. Please confirm your action.{% endtrans %}</p> <p>{% trans action %}You are about to {{ action }}. Please confirm your action.{% endtrans %}</p>
{{ macros.form(form) }} {{ macros.form(form) }}
{% endcall %} {%- endcall %}
{% endblock %} {%- endblock %}

@ -1,14 +1,14 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Docker error{% endtrans %} {% trans %}Docker error{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ action }} {{ action }}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
<p>{% trans action %}An error occurred while talking to the Docker server.{% endtrans %}</p> <p>{% trans action %}An error occurred while talking to the Docker server.{% endtrans %}</p>
<pre>{{ error }}</pre> <pre class="pre-config border bg-light">{{ error }}</pre>
{% endblock %} {%- endblock %}

@ -1,21 +1,20 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}New domain{% endtrans %} {% trans %}New domain{% endtrans %}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.card() %} {%- call macros.card() %}
<form class="form" method="post" role="form"> <form class="form" method="post" role="form">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{{ macros.form_field(form.name) }} {{ macros.form_field(form.name) }}
{{ macros.form_fields((form.max_users, form.max_aliases)) }} {{ macros.form_fields((form.max_users, form.max_aliases)) }}
{{ macros.form_field(form.max_quota_bytes, step=1000000000, max=50000000000, {{ macros.form_field(form.max_quota_bytes, step=10**9, max=50*10**9, data_infinity="true",
prepend='<span class="input-group-text"><span id="quota">'+((form.max_quota_bytes.data//1000000000).__str__() if form.max_quota_bytes.data else '∞')+'</span> GiB</span>', prepend='<span class="input-group-text"><span id="max_quota_bytes_value"></span>&nbsp;GB</span>') }}
oninput='$("#quota").text(this.value == 0 ? "∞" : this.value/1000000000);') }}
{{ macros.form_field(form.signup_enabled) }} {{ macros.form_field(form.signup_enabled) }}
{{ macros.form_field(form.comment) }} {{ macros.form_field(form.comment) }}
{{ macros.form_field(form.submit) }} {{ macros.form_field(form.submit) }}
</form> </form>
{% endcall %} {%- endcall %}
{% endblock %} {%- endblock %}

@ -1,66 +1,69 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Domain details{% endtrans %} {% trans %}Domain details{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ domain.name }} {{ domain.name }}
{% endblock %} {%- endblock %}
{% block main_action %} {%- block main_action %}
{% if current_user.global_admin %} {%- if current_user.global_admin %}
<a class="btn btn-primary float-right" href="{{ url_for(".domain_genkeys", domain_name=domain.name) }}"> <a class="btn btn-primary float-right" href="{{ url_for(".domain_genkeys", domain_name=domain.name) }}">
{% if domain.dkim_publickey %} {%- if domain.dkim_publickey %}
{% trans %}Regenerate keys{% endtrans %} {% trans %}Regenerate keys{% endtrans %}
{% else %} {%- else %}
{% trans %}Generate keys{% endtrans %} {% trans %}Generate keys{% endtrans %}
{% endif %} {%- endif %}
</a> </a>
{% endif %} {%- endif %}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.table(datatable=False) %} {%- call macros.table(datatable=False) %}
{% set hostname = config["HOSTNAMES"].split(",")[0] %}
<tr> <tr>
<th>{% trans %}Domain name{% endtrans %}</th> <th>{% trans %}Domain name{% endtrans %}</th>
<td>{{ domain.name }}</td> <td>{{ domain.name }}</td>
</tr> </tr>
<tr> <tr>
<th>{% trans %}DNS MX entry{% endtrans %} <i class="fa {{ 'fa-check-circle' if domain.check_mx() else 'fa-exclamation-circle' }}"></i></th> <th>{% trans %}DNS MX entry{% endtrans %} <i class="fa {{ 'fa-check-circle text-success' if domain.check_mx() else 'fa-exclamation-circle text-danger' }}"></i></th>
<td><pre>{{ domain.name }}. 600 IN MX 10 {{ hostname }}.</pre></td> <td>{{ macros.clip("dns_mx") }}<pre id="dns_mx" class="pre-config border bg-light">{{ domain.dns_mx }}</pre></td>
</tr> </tr>
<tr> <tr>
<th>{% trans %}DNS SPF entries{% endtrans %}</th> <th>{% trans %}DNS SPF entries{% endtrans %}</th>
<td><pre> <td>{{ macros.clip("dns_spf") }}<pre id="dns_spf" class="pre-config border bg-light">{{ domain.dns_spf }}</pre>
{{ domain.name }}. 600 IN TXT "v=spf1 mx a:{{ hostname }} -all"</pre></td> </td>
</tr> </tr>
{% if domain.dkim_publickey %} {%- if domain.dkim_publickey %}
<tr> <tr>
<th>{% trans %}DKIM public key{% endtrans %}</th> <th>{% trans %}DKIM public key{% endtrans %}</th>
<td><pre style="white-space: pre-wrap; word-wrap: break-word;">{{ domain.dkim_publickey }}</pre></td> <td>{{ macros.clip("dkim_key") }}<pre id="dkim_key" class="pre-config border bg-light">{{ domain.dkim_publickey }}</pre></td>
</tr> </tr>
<tr> <tr>
<th>{% trans %}DNS DKIM entry{% endtrans %}</th> <th>{% trans %}DNS DKIM entry{% endtrans %}</th>
<td><pre style="white-space: pre-wrap; word-wrap: break-word;">{{ config["DKIM_SELECTOR"] }}._domainkey.{{ domain.name }}. 600 IN TXT "v=DKIM1; k=rsa; p={{ domain.dkim_publickey }}"</pre></td> <td>{{ macros.clip("dns_dkim") }}<pre id="dns_dkim" class="pre-config border bg-light">{{ domain.dns_dkim }}</pre></td>
</tr> </tr>
<tr> <tr>
<th>{% trans %}DNS DMARC entry{% endtrans %}</th> <th>{% trans %}DNS DMARC entry{% endtrans %}</th>
<td><pre>_dmarc.{{ domain.name }}. 600 IN TXT "v=DMARC1; p=reject;{% if config["DMARC_RUA"] %} rua=mailto:{{ config["DMARC_RUA"] }}@{{ config["DOMAIN"] }};{% endif %}{% if config["DMARC_RUF"] %} ruf=mailto:{{ config["DMARC_RUF"] }}@{{ config["DOMAIN"] }};{% endif %} adkim=s; aspf=s"</pre></td> <td>{{ macros.clip("dns_dmark") }}<pre id="dns_dmark" class="pre-config border bg-light">{{ domain.dns_dmarc }}</pre></td>
</tr> </tr>
{% endif %} {%- endif %}
{%- set tlsa_record=domain.dns_tlsa %}
{%- if tlsa_record %}
<tr>
<th>{% trans %}DNS TLSA entry{% endtrans %}</br><span class="text-secondary text-xs font-weight-normal">Let's Encrypt</br>ISRG Root X1</span></th>
<td>{{ macros.clip("dns_tlsa") }}<pre id="dns_tlsa" class="pre-config border bg-light">{{ tlsa_record }}</pre></td>
</tr>
{%- endif %}
<tr> <tr>
<th>{% trans %}DNS client auto-configuration (RFC6186) entries{% endtrans %}</th> <th>{% trans %}DNS client auto-configuration (RFC6186) entries{% endtrans %}</th>
<td> <td>
<pre style="white-space: pre-wrap; word-wrap: break-word;">_submission._tcp.{{ domain.name }}. 600 IN SRV 1 1 587 {{ config["HOSTNAMES"].split(',')[0] }}.</pre> {{ macros.clip("dns_autoconfig") }}<pre id="dns_autoconfig" class="pre-config border bg-light">
<pre style="white-space: pre-wrap; word-wrap: break-word;">_imap._tcp.{{ domain.name }}. 600 IN SRV 100 1 143 {{ config["HOSTNAMES"].split(',')[0] }}.</pre> {%- for line in domain.dns_autoconfig %}
<pre style="white-space: pre-wrap; word-wrap: break-word;">_pop3._tcp.{{ domain.name }}. 600 IN SRV 100 1 110 {{ config["HOSTNAMES"].split(',')[0] }}.</pre> {{ line }}
{% if config["TLS_FLAVOR"] != "notls" %} {%- endfor -%}
<pre style="white-space: pre-wrap; word-wrap: break-word;">_submissions._tcp.{{ domain.name }}. 600 IN SRV 10 1 465 {{ config["HOSTNAMES"].split(',')[0] }}.</pre> </pre></td>
<pre style="white-space: pre-wrap; word-wrap: break-word;">_imaps._tcp.{{ domain.name }}. 600 IN SRV 10 1 993 {{ config["HOSTNAMES"].split(',')[0] }}.</pre>
<pre style="white-space: pre-wrap; word-wrap: break-word;">_pop3s._tcp.{{ domain.name }}. 600 IN SRV 10 1 995 {{ config["HOSTNAMES"].split(',')[0] }}.</pre>
{% endif %}</td>
</tr> </tr>
{% endcall %} {%- endcall %}
{% endblock %} {%- endblock %}

@ -1,9 +1,9 @@
{% extends "domain/create.html" %} {%- extends "domain/create.html" %}
{% block title %} {%- block title %}
{% trans %}Edit domain{% endtrans %} {% trans %}Edit domain{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ domain }} {{ domain }}
{% endblock %} {%- endblock %}

@ -1,17 +1,17 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Domain list{% endtrans %} {% trans %}Domain list{% endtrans %}
{% endblock %} {%- endblock %}
{% block main_action %} {%- block main_action %}
{% if current_user.global_admin %} {%- if current_user.global_admin %}
<a class="btn btn-primary float-right" href="{{ url_for('.domain_create') }}">{% trans %}New domain{% endtrans %}</a> <a class="btn btn-primary float-right" href="{{ url_for('.domain_create') }}">{% trans %}New domain{% endtrans %}</a>
{% endif %} {%- endif %}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.table() %} {%- call macros.table() %}
<thead> <thead>
<tr> <tr>
<th>{% trans %}Actions{% endtrans %}</th> <th>{% trans %}Actions{% endtrans %}</th>
@ -25,22 +25,22 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for domain in current_user.get_managed_domains() %} {%- for domain in current_user.get_managed_domains() %}
<tr> <tr>
<td> <td>
<a href="{{ url_for('.domain_details', domain_name=domain.name) }}" title="{% trans %}Details{% endtrans %}"><i class="fa fa-list"></i></a>&nbsp; <a href="{{ url_for('.domain_details', domain_name=domain.name) }}" title="{% trans %}Details{% endtrans %}"><i class="fa fa-list"></i></a>&nbsp;
{% if current_user.global_admin %} {%- if current_user.global_admin %}
<a href="{{ url_for('.domain_edit', domain_name=domain.name) }}" title="{% trans %}Edit{% endtrans %}"><i class="fas fa-pencil-alt"></i></a>&nbsp; <a href="{{ url_for('.domain_edit', domain_name=domain.name) }}" title="{% trans %}Edit{% endtrans %}"><i class="fas fa-pencil-alt"></i></a>&nbsp;
<a href="{{ url_for('.domain_delete', domain_name=domain.name) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>&nbsp; <a href="{{ url_for('.domain_delete', domain_name=domain.name) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>&nbsp;
{% endif %} {%- endif %}
</td> </td>
<td> <td>
<a href="{{ url_for('.user_list', domain_name=domain.name) }}" title="{% trans %}Users{% endtrans %}"><i class="far fa-envelope"></i></a>&nbsp; <a href="{{ url_for('.user_list', domain_name=domain.name) }}" title="{% trans %}Users{% endtrans %}"><i class="far fa-envelope"></i></a>&nbsp;
<a href="{{ url_for('.alias_list', domain_name=domain.name) }}" title="{% trans %}Aliases{% endtrans %}"><i class="fa fa-at"></i></a>&nbsp; <a href="{{ url_for('.alias_list', domain_name=domain.name) }}" title="{% trans %}Aliases{% endtrans %}"><i class="fa fa-at"></i></a>&nbsp;
<a href="{{ url_for('.manager_list', domain_name=domain.name) }}" title="{% trans %}Managers{% endtrans %}"><i class="fa fa-user"></i></a>&nbsp; <a href="{{ url_for('.manager_list', domain_name=domain.name) }}" title="{% trans %}Managers{% endtrans %}"><i class="fa fa-user"></i></a>&nbsp;
{% if current_user.global_admin %} {%- if current_user.global_admin %}
<a href="{{ url_for('.alternative_list', domain_name=domain.name) }}" title="{% trans %}Alternatives{% endtrans %}"><i class="fa fa-asterisk"></i></a>&nbsp; <a href="{{ url_for('.alternative_list', domain_name=domain.name) }}" title="{% trans %}Alternatives{% endtrans %}"><i class="fa fa-asterisk"></i></a>&nbsp;
{% endif %} {%- endif %}
</td> </td>
<td>{{ domain.name }}</td> <td>{{ domain.name }}</td>
<td>{{ domain.users | count }} / {{ '∞' if domain.max_users == -1 else domain.max_users }}</td> <td>{{ domain.users | count }} / {{ '∞' if domain.max_users == -1 else domain.max_users }}</td>
@ -49,7 +49,7 @@
<td>{{ domain.created_at }}</td> <td>{{ domain.created_at }}</td>
<td>{{ domain.updated_at or '' }}</td> <td>{{ domain.updated_at or '' }}</td>
</tr> </tr>
{% endfor %} {%- endfor %}
</tbody> </tbody>
{% endcall %} {%- endcall %}
{% endblock %} {%- endblock %}

@ -1,18 +1,18 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Register a domain{% endtrans %} {% trans %}Register a domain{% endtrans %}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
<form class="form" method="post" role="form"> <form class="form" method="post" role="form">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{% call macros.card(title="Requirements") %} {%- call macros.card(title="Requirements") %}
<p>{% trans %}In order to register a new domain, you must first setup the <p>{% trans %}In order to register a new domain, you must first setup the
domain zone so that the domain <code>MX</code> points to this server{% endtrans %} domain zone so that the domain <code>MX</code> points to this server{% endtrans %}
(<code>{{ config["HOSTNAMES"].split(",")[0] }}</code>). (<code>{{ config["HOSTNAME"] }}</code>).
</p> </p>
<p> <p>
{% trans %}If you do not know how to setup an <code>MX</code> record for your DNS zone, {% trans %}If you do not know how to setup an <code>MX</code> record for your DNS zone,
@ -20,17 +20,17 @@
couple minutes after the <code>MX</code> is set so the local server cache couple minutes after the <code>MX</code> is set so the local server cache
expires.{% endtrans %} expires.{% endtrans %}
</p> </p>
{% endcall %} {%- endcall %}
{% call macros.card() %} {%- call macros.card() %}
{% if form.localpart %} {%- if form.localpart %}
{{ macros.form_fields((form.localpart, form.name), append='<span class="input-group-text">@</span>') }} {{ macros.form_fields((form.localpart, form.name), append='<span class="input-group-text">@</span>') }}
{{ macros.form_fields((form.pw, form.pw2)) }} {{ macros.form_fields((form.pw, form.pw2)) }}
{% else %} {%- else %}
{{ macros.form_field(form.name) }} {{ macros.form_field(form.name) }}
{% endif %} {%- endif %}
{{ macros.form_field(form.captcha) }} {{ macros.form_field(form.captcha) }}
{{ macros.form_field(form.submit) }} {{ macros.form_field(form.submit) }}
{% endcall %} {%- endcall %}
</form> </form>
{% endblock %} {%- endblock %}

@ -1,31 +1,31 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Add a fetched account{% endtrans %} {% trans %}Add a fetched account{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ user }} {{ user }}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
<form class="form" method="post" role="form"> <form class="form" method="post" role="form">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{% call macros.card(title="Remote server") %} {%- call macros.card(title="Remote server") %}
{{ macros.form_field(form.protocol) }} {{ macros.form_field(form.protocol) }}
{{ macros.form_fields((form.host, form.port)) }} {{ macros.form_fields((form.host, form.port)) }}
{{ macros.form_field(form.tls) }} {{ macros.form_field(form.tls) }}
{% endcall %} {%- endcall %}
{% call macros.card(title="Authentication") %} {%- call macros.card(title="Authentication") %}
{{ macros.form_field(form.username) }} {{ macros.form_field(form.username) }}
{{ macros.form_field(form.password) }} {{ macros.form_field(form.password) }}
{% endcall %} {%- endcall %}
{% call macros.card(title="Settings") %} {%- call macros.card(title="Settings") %}
{{ macros.form_field(form.keep) }} {{ macros.form_field(form.keep) }}
{% endcall %} {%- endcall %}
{{ macros.form_field(form.submit) }} {{ macros.form_field(form.submit) }}
</form> </form>
{% endblock %} {%- endblock %}

@ -1,9 +1,9 @@
{% extends "fetch/create.html" %} {%- extends "fetch/create.html" %}
{% block title %} {%- block title %}
{% trans %}Update a fetched account{% endtrans %} {% trans %}Update a fetched account{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ user }} {{ user }}
{% endblock %} {%- endblock %}

@ -1,19 +1,19 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Fetched accounts{% endtrans %} {% trans %}Fetched accounts{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ user }} {{ user }}
{% endblock %} {%- endblock %}
{% block main_action %} {%- block main_action %}
<a class="btn btn-primary float-right" href="{{ url_for('.fetch_create', user_email=user.email) }}">{% trans %}Add an account{% endtrans %}</a> <a class="btn btn-primary float-right" href="{{ url_for('.fetch_create', user_email=user.email) }}">{% trans %}Add an account{% endtrans %}</a>
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.table() %} {%- call macros.table() %}
<thead> <thead>
<tr> <tr>
<th>{% trans %}Actions{% endtrans %}</th> <th>{% trans %}Actions{% endtrans %}</th>
@ -27,7 +27,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for fetch in user.fetches %} {%- for fetch in user.fetches %}
<tr> <tr>
<td> <td>
<a href="{{ url_for('.fetch_edit', fetch_id=fetch.id) }}" title="{% trans %}Edit{% endtrans %}"><i class="fa fa-pencil"></i></a>&nbsp; <a href="{{ url_for('.fetch_edit', fetch_id=fetch.id) }}" title="{% trans %}Edit{% endtrans %}"><i class="fa fa-pencil"></i></a>&nbsp;
@ -41,7 +41,7 @@
<td>{{ fetch.created_at }}</td> <td>{{ fetch.created_at }}</td>
<td>{{ fetch.updated_at or '' }}</td> <td>{{ fetch.updated_at or '' }}</td>
</tr> </tr>
{% endfor %} {%- endfor %}
</tbody> </tbody>
{% endcall %} {%- endcall %}
{% endblock %} {%- endblock %}

@ -1,7 +1,7 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block content %} {%- block content %}
{% call macros.card() %} {%- call macros.card() %}
{{ macros.form(form) }} {{ macros.form(form) }}
{% endcall %} {%- endcall %}
{% endblock %} {%- endblock %}

@ -1,9 +1,9 @@
{% extends "form.html" %} {%- extends "form.html" %}
{% block title %} {%- block title %}
{% trans %}Sign in{% endtrans %} {% trans %}Sign in{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{% trans %}to access the administration tools{% endtrans %} {% trans %}to access the administration tools{% endtrans %}
{% endblock %} {%- endblock %}

@ -1,104 +1,127 @@
{% macro form_errors(form) %} {%- macro form_errors(form) %}
{% if form.errors %} {%- if form.errors %}
{% for fieldname, errors in form.errors.items() %} {%- for fieldname, errors in form.errors.items() %}
{% if bootstrap_is_hidden_field(form[fieldname]) %} {%- if bootstrap_is_hidden_field(form[fieldname]) %}
{% for error in errors %} {%- for error in errors %}
<p class="error">{{error}}</p> <p class="error">{{error}}</p>
{% endfor %} {%- endfor %}
{% endif %} {%- endif %}
{% endfor %} {%- endfor %}
{% endif %} {%- endif %}
{% endmacro %} {%- endmacro %}
{% macro form_field_errors(field) %} {%- macro form_field_errors(field) %}
{% if field.errors %} {%- if field.errors %}
{% for error in field.errors %} {%- for error in field.errors %}
<p class="help-block inline">{{ error }}</p> <p class="help-block inline">{{ error }}</p>
{% endfor %} {%- endfor %}
{% endif %} {%- endif %}
{% endmacro %} {%- endmacro %}
{% macro form_fields(fields, prepend='', append='', label=True) %} {%- macro form_fields(fields, prepend='', append='', label=True) %}
{% set width = (12 / fields|length)|int %} {%- set width = (12 / fields|length)|int %}
<div class="form-group"> <div class="form-group">
<div class="row"> <div class="row">
{% for field in fields %} {%- for field in fields %}
<div class="col-lg-{{ width }} col-xs-12 {{ 'has-error' if field.errors else '' }}"> <div class="col-lg-{{ width }} col-xs-12 {{ 'has-error' if field.errors else '' }}">
{{ form_individual_field(field, prepend=prepend, append=append, label=label, **kwargs) }} {{ form_individual_field(field, prepend=prepend, append=append, label=label, **kwargs) }}
</div> </div>
{% endfor %} {%- endfor %}
</div> </div>
</div> </div>
{% endmacro %} {%- endmacro %}
{% macro form_individual_field(field, prepend='', append='', label=True, class_="") %} {%- macro form_individual_field(field, prepend='', append='', label=True, class_="") %}
{% if field.type == "BooleanField" %} {%- if field.type == "BooleanField" %}
{{ field(**kwargs) }}<span>&nbsp;&nbsp;</span> {{ field(**kwargs) }}<span>&nbsp;&nbsp;</span>{{ field.label if label else '' }}
{{ field.label if label else '' }} {%- else %}
{% else %}
{{ field.label if label else '' }}{{ form_field_errors(field) }} {{ field.label if label else '' }}{{ form_field_errors(field) }}
{% if prepend %}<div class="input-group-prepend">{% endif %} {%- if prepend %}<div class="input-group-prepend">{%- elif append %}<div class="input-group-append">{%- endif %}
{% if append %}<div class="input-group-append">{% endif %} {{ prepend|safe }}{{ field(class_=("form-control " + class_) if class_ else "form-control", **kwargs) }}{{ append|safe }}
{{ prepend|safe }}{{ field(class_="form-control " + class_, **kwargs) }}{{ append|safe }} {%- if prepend or append %}</div>{%- endif %}
{% if prepend or append %}</div>{% endif %} {%- endif %}
{% endif %} {%- endmacro %}
{% endmacro %}
{% macro form_field(field) %} {%- macro form_field(field) %}
{% if field.type == 'SubmitField' %} {%- if field.type == 'SubmitField' %}
{{ form_fields((field,), label=False, class="btn btn-default", **kwargs) }} {{- form_fields((field,), label=False, class="btn btn-default", **kwargs) }}
{% else %} {%- else %}
{{ form_fields((field,), **kwargs) }} {{- form_fields((field,), **kwargs) }}
{% endif %} {%- endif %}
{% endmacro %} {%- endmacro %}
{% macro form(form) %} {%- macro form(form) %}
<form class="form" method="post" role="form"> <form class="form" method="post" role="form">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{% for field in form %} {%- for field in form %}
{% if bootstrap_is_hidden_field(field) %} {%- if bootstrap_is_hidden_field(field) %}
{{ field() }} {{ field() }}
{% else %} {%- else %}
{{ form_field(field) }} {{ form_field(field) }}
{% endif %} {%- endif %}
{% endfor %} {%- endfor %}
</form> </form>
{% endmacro %} {%- endmacro %}
{% macro card(title=None, theme="primary", header=True) %} {%- macro card(title=None, theme="primary", header=True) %}
<div class="row"> <div class="row">
<div class="col-lg-12"> <div class="col-lg-12">
<div class="card card-outline card-{{ theme }}"> <div class="card card-outline card-{{ theme }}">
{% if header %} {%- if header %}
<div class="card-header border-0"> <div class="card-header border-0">
{% if title %} {%- if title %}
<h3 class="card-title">{{ title }}</h3> <h3 class="card-title">{{ title }}</h3>
{% endif %} {%- endif %}
</div> </div>
{% endif %} {%- endif %}
<div class="card-body"> <div class="card-body">
{{ caller() }} {{- caller() }}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endmacro %} {%- endmacro %}
{% macro table(title=None, theme="primary", datatable=True) %} {%- macro table(title=None, theme="primary", datatable=True) %}
<div class="row"> <div class="row">
<div class="col-lg-12"> <div class="col-lg-12">
<div class="card card-outline card-{{ theme }}"> <div class="card card-outline card-{{ theme }}">
{%- if title %}
<div class="card-header border-0"> <div class="card-header border-0">
{% if title %}
<h3 class="card-title">{{ title }}</h3> <h3 class="card-title">{{ title }}</h3>
{% endif %}
</div> </div>
{%- endif %}
<div class="card-body"> <div class="card-body">
<table class="table table-bordered {% if datatable %} dataTable {% endif %}"> <table class="table table-bordered{% if datatable %} dataTable{% endif %}">
{{ caller() }} {{- caller() }}
</table> </table>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endmacro %} {%- endmacro %}
{%- macro fieldset(title=None, field=None, enabled=None, fields=None) %}
{%- if field or title %}
<fieldset{% if not enabled %} disabled{% endif %}>
{%- if field %}
<legend>{{ form_individual_field(field) }}</legend>
{%- else %}
<legend>{{ title }}</legend>
{%- endif %}
{%- endif %}
{{- caller() }}
{%- if fields %}
{%- set kwargs = {"enabled" if enabled else "disabled": ""} %}
{%- for field in fields %}
{{ form_field(field, **kwargs) }}
{%- endfor %}
{%- endif %}
</fieldset>
{%- endmacro %}
{%- macro clip(target, title=_("copy to clipboard"), icon="copy", color="primary", action="copy") %}
<button class="btn btn-{{ color }} btn-xs btn-clip float-right ml-2 mt-1" data-clipboard-action="{{ action }}" data-clipboard-target="#{{ target }}">
<i class="fas fa-{{ icon }}" title="{{ title }}" aria-expanded="false"></i><span class="sr-only">{{ title }}</span>
</button>
{%- endmacro %}

@ -1,19 +1,19 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Add a manager{% endtrans %} {% trans %}Add a manager{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ domain }} {{ domain }}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.card() %} {%- call macros.card() %}
<form class="form" method="post" role="form"> <form class="form" method="post" role="form">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{{ macros.form_field(form.manager, class_='mailselect') }} {{ macros.form_field(form.manager, class_='mailselect') }}
{{ macros.form_field(form.submit) }} {{ macros.form_field(form.submit) }}
</form> </form>
{% endcall %} {%- endcall %}
{% endblock %} {%- endblock %}

@ -1,19 +1,19 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Manager list{% endtrans %} {% trans %}Manager list{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ domain.name }} {{ domain.name }}
{% endblock %} {%- endblock %}
{% block main_action %} {%- block main_action %}
<a class="btn btn-primary float-right" href="{{ url_for('.manager_create', domain_name=domain.name) }}">{% trans %}Add manager{% endtrans %}</a> <a class="btn btn-primary float-right" href="{{ url_for('.manager_create', domain_name=domain.name) }}">{% trans %}Add manager{% endtrans %}</a>
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.table() %} {%- call macros.table() %}
<thead> <thead>
<tr> <tr>
<th>{% trans %}Actions{% endtrans %}</th> <th>{% trans %}Actions{% endtrans %}</th>
@ -21,14 +21,14 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for manager in domain.managers %} {%- for manager in domain.managers %}
<tr> <tr>
<td> <td>
<a href="{{ url_for('.manager_delete', domain_name=domain.name, user_email=manager.email) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a> <a href="{{ url_for('.manager_delete', domain_name=domain.name, user_email=manager.email) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
</td> </td>
<td>{{ manager }}</td> <td>{{ manager }}</td>
</tr> </tr>
{% endfor %} {%- endfor %}
</tbody> </tbody>
{% endcall %} {%- endcall %}
{% endblock %} {%- endblock %}

@ -1,5 +1,5 @@
{% extends "form.html" %} {%- extends "form.html" %}
{% block title %} {%- block title %}
{% trans %}New relay domain{% endtrans %} {% trans %}New relay domain{% endtrans %}
{% endblock %} {%- endblock %}

@ -1,9 +1,9 @@
{% extends "form.html" %} {%- extends "form.html" %}
{% block title %} {%- block title %}
{% trans %}Edit relayd domain{% endtrans %} {% trans %}Edit relayd domain{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ relay }} {{ relay }}
{% endblock %} {%- endblock %}

@ -1,17 +1,17 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Relayed domain list{% endtrans %} {% trans %}Relayed domain list{% endtrans %}
{% endblock %} {%- endblock %}
{% block main_action %} {%- block main_action %}
{% if current_user.global_admin %} {%- if current_user.global_admin %}
<a class="btn btn-primary float-right" href="{{ url_for('.relay_create') }}">{% trans %}New relayed domain{% endtrans %}</a> <a class="btn btn-primary float-right" href="{{ url_for('.relay_create') }}">{% trans %}New relayed domain{% endtrans %}</a>
{% endif %} {%- endif %}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.table() %} {%- call macros.table() %}
<thead> <thead>
<tr> <tr>
<th>{% trans %}Actions{% endtrans %}</th> <th>{% trans %}Actions{% endtrans %}</th>
@ -23,7 +23,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for relay in relays %} {%- for relay in relays %}
<tr> <tr>
<td> <td>
<a href="{{ url_for('.relay_edit', relay_name=relay.name) }}" title="{% trans %}Edit{% endtrans %}"><i class="fa fa-pencil"></i></a>&nbsp; <a href="{{ url_for('.relay_edit', relay_name=relay.name) }}" title="{% trans %}Edit{% endtrans %}"><i class="fa fa-pencil"></i></a>&nbsp;
@ -35,7 +35,7 @@
<td>{{ relay.created_at }}</td> <td>{{ relay.created_at }}</td>
<td>{{ relay.updated_at or '' }}</td> <td>{{ relay.updated_at or '' }}</td>
</tr> </tr>
{% endfor %} {%- endfor %}
</tbody> </tbody>
{% endcall %} {%- endcall %}
{% endblock %} {%- endblock %}

@ -1,144 +1,156 @@
<div class="sidebar"> <div class="sidebar text-sm">
{% if current_user.is_authenticated %} {%- if current_user.is_authenticated %}
<div class="user-panel mt-3 pb-3 mb-3 d-flex"> <div class="user-panel mt-3 pb-3 mb-3 d-flex">
<div class="image">
<div class="div-circle elevation-2"><i class="fa fa-user text-lg text-dark"></i></div>
</div>
<div class="info"> <div class="info">
<span class="text-center text-primary">{{ current_user }}</span> <a href="{{ url_for('.user_settings') }}" class="d-block">{{ current_user }}</a>
</div> </div>
</div> </div>
{% endif %} {%- endif %}
<nav class="mt-2"> <nav class="mt-2">
<ul class="nav nav-pills nav-sidebar flex-column" role="menu"> <ul class="nav nav-pills nav-sidebar flex-column" role="menu">
{% if current_user.is_authenticated %} {%- if current_user.is_authenticated %}
<li class="nav-header">{% trans %}My account{% endtrans %}</li> <li class="nav-header text-uppercase text-primary" role="none">{% trans %}My account{% endtrans %}</li>
<li class="nav-item"> <li class="nav-item" role="none">
<a href="{{ url_for('.user_settings') }}" class="nav-link"> <a href="{{ url_for('.user_settings') }}" class="nav-link" role="menuitem">
<i class="nav-icon fa fa-wrench"></i> <i class="nav-icon fa fa-wrench"></i>
<p class="text">{% trans %}Settings{% endtrans %}</p> <p>{% trans %}Settings{% endtrans %}</p>
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item" role="none">
<a href="{{ url_for('.user_password') }}" class="nav-link"> <a href="{{ url_for('.user_password') }}" class="nav-link" role="menuitem">
<i class="nav-icon fa fa-lock"></i> <i class="nav-icon fa fa-lock"></i>
<p class="text">{% trans %}Update password{% endtrans %}</p> <p>{% trans %}Update password{% endtrans %}</p>
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item" role="none">
<a href="{{ url_for('.user_reply') }}" class="nav-link"> <a href="{{ url_for('.user_reply') }}" class="nav-link" role="menuitem">
<i class="nav-icon fa fa-plane"></i> <i class="nav-icon fa fa-plane"></i>
<p class="text">{% trans %}Auto-reply{% endtrans %}</p> <p>{% trans %}Auto-reply{% endtrans %}</p>
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item" role="none">
<a href="{{ url_for('.fetch_list') }}" class="nav-link"> <a href="{{ url_for('.fetch_list') }}" class="nav-link" role="menuitem">
<i class="nav-icon fas fa-download"></i> <i class="nav-icon fas fa-download"></i>
<p class="text">{% trans %}Fetched accounts{% endtrans %}</p> <p>{% trans %}Fetched accounts{% endtrans %}</p>
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item" role="none">
<a href="{{ url_for('.token_list') }}" class="nav-link"> <a href="{{ url_for('.token_list') }}" class="nav-link" role="menuitem">
<i class="nav-icon fas fa-ticket-alt"></i> <i class="nav-icon fas fa-ticket-alt"></i>
<p class="text">{% trans %}Authentication tokens{% endtrans %}</p> <p>{% trans %}Authentication tokens{% endtrans %}</p>
</a> </a>
</li> </li>
{%- if current_user.is_authenticated %}
{% if current_user.manager_of or current_user.global_admin %} <li class="nav-item" role="none">
<li class="nav-header">{% trans %}Administration{% endtrans %}</li> <a href="{{ url_for('.client') }}" class="nav-link" role="menuitem">
{% endif %}
{% if current_user.global_admin %}
<li class="nav-item">
<a href="{{ url_for('.announcement') }}" class="nav-link">
<i class="nav-icon fa fa-bullhorn"></i>
<p class="text">{% trans %}Announcement{% endtrans %}</p>
</a>
</li>
<li class="nav-item">
<a href="{{ url_for('.admin_list') }}" class="nav-link">
<i class="nav-icon fa fa-user"></i>
<p class="text">{% trans %}Administrators{% endtrans %}</p>
</a>
</li>
<li class="nav-item">
<a href="{{ url_for('.relay_list') }}" class="nav-link">
<i class="nav-icon fa fa-reply-all"></i>
<p class="text">{% trans %}Relayed domains{% endtrans %}</p>
</a>
</li>
<li class="nav-item">
<a href="{{ config["WEB_ADMIN"] }}/antispam/" target="_blank" class="nav-link">
<i class="nav-icon fas fa-trash-alt"></i>
<p class="text">{% trans %}Antispam{% endtrans %}</p>
</a>
</li>
{% endif %}
{% if current_user.manager_of or current_user.global_admin %}
<li class="nav-item">
<a href="{{ url_for('.domain_list') }}" class="nav-link">
<i class="nav-icon fa fa-envelope"></i>
<p class="text">{% trans %}Mail domains{% endtrans %}</p>
</a>
</li>
{% endif %}
{% endif %}
<li class="nav-header">{% trans %}Go to{% endtrans %}</li>
{% if config["WEBMAIL"] != "none" %}
<li class="nav-item">
<a href="{{ config["WEB_WEBMAIL"] }}" target="_blank" class="nav-link">
<i class="nav-icon far fa-envelope"></i>
<p class="text">{% trans %}Webmail{% endtrans %}</p>
</a>
</li>
{% endif %}
<li class="nav-item">
<a href="{{ url_for('.client') }}" class="nav-link">
<i class="nav-icon fa fa-laptop"></i> <i class="nav-icon fa fa-laptop"></i>
<p class="text">{% trans %}Client setup{% endtrans %}</p> <p>{% trans %}Client setup{% endtrans %}</p>
</a> </a>
</li> </li>
<li class="nav-item"> {%- endif %}
<a href="{{ config["WEBSITE"] }}" target="_blank" class="nav-link">
{%- if current_user.manager_of or current_user.global_admin %}
<li class="nav-header text-uppercase text-primary" role="none">{% trans %}Administration{% endtrans %}</li>
{%- endif %}
{%- if current_user.global_admin %}
<li class="nav-item" role="none">
<a href="{{ url_for('.announcement') }}" class="nav-link" role="menuitem">
<i class="nav-icon fa fa-bullhorn"></i>
<p>{% trans %}Announcement{% endtrans %}</p>
</a>
</li>
<li class="nav-item" role="none">
<a href="{{ url_for('.admin_list') }}" class="nav-link" role="menuitem">
<i class="nav-icon fa fa-user"></i>
<p>{% trans %}Administrators{% endtrans %}</p>
</a>
</li>
<li class="nav-item" role="none">
<a href="{{ url_for('.relay_list') }}" class="nav-link" role="menuitem">
<i class="nav-icon fa fa-reply-all"></i>
<p>{% trans %}Relayed domains{% endtrans %}</p>
</a>
</li>
<li class="nav-item" role="none">
<a href="{{ config["WEB_ADMIN"] }}/antispam/" data-clicked="{{ url_for('.antispam') }}" target="_blank" class="nav-link" role="menuitem">
<i class="nav-icon fas fa-trash-alt"></i>
<p>{% trans %}Antispam{% endtrans %}</p>
</a>
</li>
{%- endif %}
{%- if current_user.manager_of or current_user.global_admin %}
<li class="nav-item" role="none">
<a href="{{ url_for('.domain_list') }}" class="nav-link" role="menuitem">
<i class="nav-icon fa fa-envelope"></i>
<p>{% trans %}Mail domains{% endtrans %}</p>
</a>
</li>
{%- endif %}
{%- endif %}
<li class="nav-header text-uppercase text-primary" role="none">{% trans %}Go to{% endtrans %}</li>
{%- if config["WEBMAIL"] != "none" %}
<li class="nav-item" role="none">
<a href="{{ config["WEB_WEBMAIL"] }}" target="_blank" class="nav-link" role="menuitem">
<i class="nav-icon far fa-envelope"></i>
<p>{% trans %}Webmail{% endtrans %} <i class="fas fa-external-link-alt text-xs"></i></p>
</a>
</li>
{%- endif %}
{%- if not current_user.is_authenticated %}
<li class="nav-item" role="none">
<a href="{{ url_for('.client') }}" class="nav-link" role="menuitem">
<i class="nav-icon fa fa-laptop"></i>
<p>{% trans %}Client setup{% endtrans %}</p>
</a>
</li>
{%- endif %}
<li class="nav-item" role="none">
<a href="{{ config["WEBSITE"] }}" target="_blank" class="nav-link" role="menuitem" rel="noreferrer">
<i class="nav-icon fa fa-globe"></i> <i class="nav-icon fa fa-globe"></i>
<p class="text">{% trans %}Website{% endtrans %}</p> <p>{% trans %}Website{% endtrans %} <i class="fas fa-external-link-alt text-xs"></i></p>
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item" role="none">
<a href="https://mailu.io" target="_blank" class="nav-link"> <a href="https://mailu.io" target="_blank" class="nav-link" role="menuitem" rel="noreferrer">
<i class="nav-icon fa fa-life-ring"></i> <i class="nav-icon fa fa-life-ring"></i>
<p class="text">{% trans %}Help{% endtrans %}</p> <p>{% trans %}Help{% endtrans %} <i class="fas fa-external-link-alt text-xs"></i></p>
</a> </a>
</li> </li>
{% if config['DOMAIN_REGISTRATION'] %} {%- if config['DOMAIN_REGISTRATION'] %}
<li class="nav-item"> <li class="nav-item" role="none">
<a href="{{ url_for('.domain_signup') }}" class="nav-link"> <a href="{{ url_for('.domain_signup') }}" class="nav-link" role="menuitem">
<i class="nav-icon fa fa-plus-square"></i> <i class="nav-icon fa fa-plus-square"></i>
<p class="text">{% trans %}Register a domain{% endtrans %}</p> <p>{% trans %}Register a domain{% endtrans %}</p>
</a> </a>
</li> </li>
{% endif %} {%- endif %}
{% if current_user.is_authenticated %} {%- if current_user.is_authenticated %}
<li class="nav-item"> <li class="nav-item" role="none">
<a href="{{ url_for('.logout') }}" class="nav-link"> <a href="{{ url_for('.logout') }}" class="nav-link" role="menuitem">
<i class="nav-icon fas fa-sign-out-alt"></i> <i class="nav-icon fas fa-sign-out-alt"></i>
<p class="text">{% trans %}Sign out{% endtrans %}</p> <p>{% trans %}Sign out{% endtrans %}</p>
</a> </a>
</li> </li>
{% else %} {%- else %}
<li class="nav-item"> <li class="nav-item" role="none">
<a href="{{ url_for('.login') }}" class="nav-link"> <a href="{{ url_for('.login') }}" class="nav-link" role="menuitem">
<i class="nav-icon fas fa-sign-in-alt"></i> <i class="nav-icon fas fa-sign-in-alt"></i>
<p class="text">{% trans %}Sign in{% endtrans %}</p> <p>{% trans %}Sign in{% endtrans %}</p>
</a> </a>
</li> </li>
{% if signup_domains %} {%- if signup_domains %}
<li class="nav-item"> <li class="nav-item" role="none">
<a href="{{ url_for('.user_signup') }}" class="nav-link"> <a href="{{ url_for('.user_signup') }}" class="nav-link" role="menuitem">
<i class="nav-icon fa fa-user-plus"></i> <i class="nav-icon fa fa-user-plus"></i>
<p class="text">{% trans %}Sign up{% endtrans %}</p> <p>{% trans %}Sign up{% endtrans %}</p>
</a> </a>
</li> </li>
{% endif %} {%- endif %}
{% endif %} {%- endif %}
</ul> </ul>
</nav> </nav>
</div> </div>

@ -1,9 +1,9 @@
{% extends "form.html" %} {%- extends "form.html" %}
{% block title %} {%- block title %}
{% trans %}Create an authentication token{% endtrans %} {% trans %}Create an authentication token{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ user }} {{ user }}
{% endblock %} {%- endblock %}

@ -1,19 +1,19 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Authentication tokens{% endtrans %} {% trans %}Authentication tokens{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ user }} {{ user }}
{% endblock %} {%- endblock %}
{% block main_action %} {%- block main_action %}
<a class="btn btn-primary float-right" href="{{ url_for('.token_create', user_email=user.email) }}">{% trans %}New token{% endtrans %}</a> <a class="btn btn-primary float-right" href="{{ url_for('.token_create', user_email=user.email) }}">{% trans %}New token{% endtrans %}</a>
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.table() %} {%- call macros.table() %}
<thead> <thead>
<tr> <tr>
<th>{% trans %}Actions{% endtrans %}</th> <th>{% trans %}Actions{% endtrans %}</th>
@ -23,7 +23,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for token in user.tokens %} {%- for token in user.tokens %}
<tr> <tr>
<td> <td>
<a href="{{ url_for('.token_delete', token_id=token.id) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a> <a href="{{ url_for('.token_delete', token_id=token.id) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
@ -32,7 +32,7 @@
<td>{{ token.ip or "any" }}</td> <td>{{ token.ip or "any" }}</td>
<td>{{ token.created_at }}</td> <td>{{ token.created_at }}</td>
</tr> </tr>
{% endfor %} {%- endfor %}
</tbody> </tbody>
{% endcall %} {%- endcall %}
{% endblock %} {%- endblock %}

@ -1,33 +1,32 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}New user{% endtrans %} {% trans %}New user{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ domain.name }} {{ domain.name }}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
<form class="form" method="post" role="form"> <form class="form" method="post" role="form">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{% call macros.card(_("General")) %} {%- call macros.card(_("General")) %}
{{ macros.form_field(form.localpart, append='<span class="input-group-text">@'+domain.name+'</span>') }} {{ macros.form_field(form.localpart, append='<span class="input-group-text">@'+domain.name+'</span>') }}
{{ macros.form_fields((form.pw, form.pw2)) }} {{ macros.form_fields((form.pw, form.pw2)) }}
{{ macros.form_field(form.displayed_name) }} {{ macros.form_field(form.displayed_name) }}
{{ macros.form_field(form.comment) }} {{ macros.form_field(form.comment) }}
{{ macros.form_field(form.enabled) }} {{ macros.form_field(form.enabled) }}
{% endcall %} {%- endcall %}
{% call macros.card(_("Features and quotas"), theme="success") %} {%- call macros.card(_("Features and quotas"), theme="success") %}
{{ macros.form_field(form.quota_bytes, step=1000000000, max=(max_quota_bytes or domain.max_quota_bytes or 50000000000), {{ macros.form_field(form.quota_bytes, step=1000000000, max=(max_quota_bytes or domain.max_quota_bytes or 50*10**9), data_infinity="true",
prepend='<span class="input-group-text"><span id="quota">'+((form.quota_bytes.data//1000000000).__str__() if form.quota_bytes.data else '∞')+'</span> GiB</span>', prepend='<span class="input-group-text"><span id="quota_bytes_value"></span>&nbsp;GB</span>') }}
oninput='$("#quota").text(this.value == 0 ? "∞" : this.value/1000000000);') }}
{{ macros.form_field(form.enable_imap) }} {{ macros.form_field(form.enable_imap) }}
{{ macros.form_field(form.enable_pop) }} {{ macros.form_field(form.enable_pop) }}
{% endcall %} {%- endcall %}
{{ macros.form_field(form.submit) }} {{ macros.form_field(form.submit) }}
</form> </form>
{% endblock %} {%- endblock %}

@ -1,9 +1,9 @@
{% extends "user/create.html" %} {%- extends "user/create.html" %}
{% block title %} {%- block title %}
{% trans %}Edit user{% endtrans %} {% trans %}Edit user{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ user }} {{ user }}
{% endblock %} {%- endblock %}

@ -1,25 +0,0 @@
{% extends "base.html" %}
{% block title %}
{% trans %}Forward emails{% endtrans %}
{% endblock %}
{% block subtitle %}
{{ user }}
{% endblock %}
{% block content %}
{% call macros.card() %}
<form class="form" method="post" role="form">
{{ form.hidden_tag() }}
{{ macros.form_field(form.forward_enabled,
onchange="if(this.checked){$('#forward_destination,#forward_keep').removeAttr('disabled')}
else{$('#forward_destination,#forward_keep').attr('disabled', '')}") }}
{{ macros.form_field(form.forward_keep,
**{("enabled" if user.forward_enabled else "disabled"): ""}) }}
{{ macros.form_field(form.forward_destination,
**{("enabled" if user.forward_enabled else "disabled"): ""}) }}
{{ macros.form_field(form.submit) }}
</form>
{% endcall %}
{% endblock %}

@ -1,19 +1,19 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}User list{% endtrans %} {% trans %}User list{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ domain.name }} {{ domain.name }}
{% endblock %} {%- endblock %}
{% block main_action %} {%- block main_action %}
<a class="btn btn-primary float-right" href="{{ url_for('.user_create', domain_name=domain.name) }}">{% trans %}Add user{% endtrans %}</a> <a class="btn btn-primary float-right" href="{{ url_for('.user_create', domain_name=domain.name) }}">{% trans %}Add user{% endtrans %}</a>
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.table() %} {%- call macros.table() %}
<thead> <thead>
<tr> <tr>
<th>{% trans %}Actions{% endtrans %}</th> <th>{% trans %}Actions{% endtrans %}</th>
@ -27,8 +27,8 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for user in domain.users %} {%- for user in domain.users %}
<tr {% if not user.enabled %}class="warning"{% endif %}> <tr{% if not user.enabled %} class="warning"{% endif %}>
<td> <td>
<a href="{{ url_for('.user_edit', user_email=user.email) }}" title="{% trans %}Edit{% endtrans %}"><i class="fas fa-pencil-alt"></i></a>&nbsp; <a href="{{ url_for('.user_edit', user_email=user.email) }}" title="{% trans %}Edit{% endtrans %}"><i class="fas fa-pencil-alt"></i></a>&nbsp;
<a href="{{ url_for('.user_delete', user_email=user.email) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a> <a href="{{ url_for('.user_delete', user_email=user.email) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
@ -48,7 +48,7 @@
<td>{{ user.created_at }}</td> <td>{{ user.created_at }}</td>
<td>{{ user.updated_at or '' }}</td> <td>{{ user.updated_at or '' }}</td>
</tr> </tr>
{% endfor %} {%- endfor %}
</tbody> </tbody>
{% endcall %} {%- endcall %}
{% endblock %} {%- endblock %}

@ -1,9 +1,9 @@
{% extends "form.html" %} {%- extends "form.html" %}
{% block title %} {%- block title %}
{% trans %}Password update{% endtrans %} {% trans %}Password update{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ user }} {{ user }}
{% endblock %} {%- endblock %}

@ -1,30 +1,23 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Automatic reply{% endtrans %} {% trans %}Automatic reply{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ user }} {{ user }}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.card() %} {%- call macros.card() %}
<form class="form" method="post" role="form"> <form class="form" method="post" role="form">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{{ macros.form_field(form.reply_enabled, {%- call macros.fieldset(
onchange="if(this.checked){$('#reply_subject,#reply_body,#reply_enddate,#reply_startdate').removeAttr('readonly')} field=form.reply_enabled,
else{$('#reply_subject,#reply_body,#reply_enddate,#reply_startdate').attr('readonly', '')}") }} enabled=user.reply_enabled,
{{ macros.form_field(form.reply_subject, fields=[form.reply_subject, form.reply_body, form.reply_enddate, form.reply_startdate]) %}
**{("rw" if user.reply_enabled else "readonly"): ""}) }} {%- endcall %}
{{ macros.form_field(form.reply_body, rows=10,
**{("rw" if user.reply_enabled else "readonly"): ""}) }}
{{ macros.form_field(form.reply_enddate,
**{("rw" if user.reply_enabled else "readonly"): ""}) }}
{{ macros.form_field(form.reply_startdate,
**{("rw" if user.reply_enabled else "readonly"): ""}) }}
{{ macros.form_field(form.submit) }} {{ macros.form_field(form.submit) }}
</form> </form>
{% endcall %} {%- endcall %}
{% endblock %} {%- endblock %}

@ -1,38 +1,36 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}User settings{% endtrans %} {% trans %}User settings{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ user }} {{ user }}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
<form class="form" method="post" role="form"> <form class="form" method="post" role="form">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{% call macros.card(title=_("Displayed name")) %} {%- call macros.card(title=_("Displayed name")) %}
{{ macros.form_field(form.displayed_name) }} {{ macros.form_field(form.displayed_name) }}
{% endcall %} {%- endcall %}
{% call macros.card(title=_("Antispam")) %} {%- call macros.card(title=_("Antispam")) %}
{{ macros.form_field(form.spam_enabled) }} {%- call macros.fieldset(field=form.spam_enabled, enabled=user.spam_enabled) %}
{{ macros.form_field(form.spam_threshold, step=1, max=100, {{ macros.form_field(form.spam_threshold, step=1, max=100,
prepend='<span class="input-group-text"><span id="threshold">'+form.spam_threshold.data.__str__()+'</span>&nbsp;/&nbsp;100</span>', prepend='<span class="input-group-text"><span id="spam_threshold_value"></span>&nbsp;/&nbsp;100</span>') }}
oninput='$("#threshold").text(this.value);') }} {%- endcall %}
{% endcall %} {%- endcall %}
{% call macros.card(title=_("Auto-forward")) %} {%- call macros.card(title=_("Auto-forward")) %}
{{ macros.form_field(form.forward_enabled, {%- call macros.fieldset(
onchange="if(this.checked){$('#forward_destination,#forward_keep').removeAttr('disabled')} field=form.forward_enabled,
else{$('#forward_destination,#forward_keep').attr('disabled', '')}") }} enabled=user.forward_enabled,
{{ macros.form_field(form.forward_keep, fields=[form.forward_keep, form.forward_destination]) %}
**{("enabled" if user.forward_enabled else "disabled"): ""}) }} {%- endcall %}
{{ macros.form_field(form.forward_destination, {%- endcall %}
**{("enabled" if user.forward_enabled else "disabled"): ""}) }}
{% endcall %}
{{ macros.form_field(form.submit) }} {{ macros.form_field(form.submit) }}
</form> </form>
{% endblock %} {%- endblock %}

@ -1,23 +1,23 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Sign up{% endtrans %} {% trans %}Sign up{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{{ domain }} {{ domain }}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
<form class="form" method="post" role="form"> <form class="form" method="post" role="form">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{% call macros.card() %} {%- call macros.card() %}
{{ macros.form_field(form.localpart, append='<span class="input-group-text">@'+domain.name+'</span>') }} {{ macros.form_field(form.localpart, append='<span class="input-group-text">@'+domain.name+'</span>') }}
{{ macros.form_fields((form.pw, form.pw2)) }} {{ macros.form_fields((form.pw, form.pw2)) }}
{% if form.captcha %} {%- if form.captcha %}
{{ macros.form_field(form.captcha) }} {{ macros.form_field(form.captcha) }}
{% endif %} {%- endif %}
{{ macros.form_field(form.submit) }} {{ macros.form_field(form.submit) }}
{% endcall %} {%- endcall %}
</form> </form>
{% endblock %} {%- endblock %}

@ -1,26 +1,26 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block title %} {%- block title %}
{% trans %}Sign up{% endtrans %} {% trans %}Sign up{% endtrans %}
{% endblock %} {%- endblock %}
{% block subtitle %} {%- block subtitle %}
{% trans %}pick a domain for the new account{% endtrans %} {% trans %}pick a domain for the new account{% endtrans %}
{% endblock %} {%- endblock %}
{% block content %} {%- block content %}
{% call macros.table() %} {%- call macros.table() %}
<tr> <tr>
<th>{% trans %}Domain{% endtrans %}</th> <th>{% trans %}Domain{% endtrans %}</th>
<th>{% trans %}Available slots{% endtrans %}</th> <th>{% trans %}Available slots{% endtrans %}</th>
<th>{% trans %}Quota{% endtrans %}</th> <th>{% trans %}Quota{% endtrans %}</th>
</tr> </tr>
{% for domain_name, domain in available_domains.items() %} {%- for domain_name, domain in available_domains.items() %}
<tr> <tr>
<td><a href="{{ url_for('.user_signup', domain_name=domain_name) }}">{{ domain_name }}</a></td> <td><a href="{{ url_for('.user_signup', domain_name=domain_name) }}">{{ domain_name }}</a></td>
<td>{{ '∞' if domain.max_users == -1 else domain.max_users - (domain.users | count)}}</td> <td>{{ '∞' if domain.max_users == -1 else domain.max_users - (domain.users | count)}}</td>
<td>{{ domain.max_quota_bytes or config['DEFAULT_QUOTA'] | filesizeformat }}</td> <td>{{ domain.max_quota_bytes or config['DEFAULT_QUOTA'] | filesizeformat }}</td>
</tr> </tr>
{% endfor %} {%- endfor %}
{% endcall %} {%- endcall %}
{% endblock %} {%- endblock %}

@ -1,5 +1,5 @@
{% extends "base.html" %} {%- extends "base.html" %}
{% block content %} {%- block content %}
<div class="alert alert-warning" role="alert">{% trans %}We are still working on this feature!{% endtrans %}</div> <div class="alert alert-warning" role="alert">{% trans %}We are still working on this feature!{% endtrans %}</div>
{% endblock %} {%- endblock %}

@ -57,3 +57,8 @@ def webmail():
@ui.route('/client', methods=['GET']) @ui.route('/client', methods=['GET'])
def client(): def client():
return flask.render_template('client.html') return flask.render_template('client.html')
@ui.route('/antispam', methods=['GET'])
def antispam():
return flask.render_template('antispam.html')

@ -2,8 +2,8 @@ from mailu.ui import ui, forms, access
import flask import flask
@ui.route('/language/<language>', methods=['POST'])
@ui.route('/language/<language>', methods=['GET'])
def set_language(language=None): def set_language(language=None):
flask.session['language'] = language flask.session['language'] = language
return flask.redirect(flask.url_for('.user_settings')) return flask.Response(status=200)

@ -129,23 +129,6 @@ def user_password(user_email):
return flask.render_template('user/password.html', form=form, user=user) return flask.render_template('user/password.html', form=form, user=user)
@ui.route('/user/forward', methods=['GET', 'POST'], defaults={'user_email': None})
@ui.route('/user/forward/<path:user_email>', methods=['GET', 'POST'])
@access.owner(models.User, 'user_email')
def user_forward(user_email):
user_email_or_current = user_email or flask_login.current_user.email
user = models.User.query.get(user_email_or_current) or flask.abort(404)
form = forms.UserForwardForm(obj=user)
if form.validate_on_submit():
form.populate_obj(user)
models.db.session.commit()
flask.flash('Forward destination updated for %s' % user)
if user_email:
return flask.redirect(
flask.url_for('.user_list', domain_name=user.domain.name))
return flask.render_template('user/forward.html', form=form, user=user)
@ui.route('/user/reply', methods=['GET', 'POST'], defaults={'user_email': None}) @ui.route('/user/reply', methods=['GET', 'POST'], defaults={'user_email': None})
@ui.route('/user/reply/<path:user_email>', methods=['GET', 'POST']) @ui.route('/user/reply/<path:user_email>', methods=['GET', 'POST'])
@access.owner(models.User, 'user_email') @access.owner(models.User, 'user_email')

@ -76,15 +76,10 @@ babel = flask_babel.Babel()
@babel.localeselector @babel.localeselector
def get_locale(): def get_locale():
""" selects locale for translation """ """ selects locale for translation """
translations = list(map(str, babel.list_translations())) language = flask.session.get('language')
flask.session['available_languages'] = translations if not language in flask.current_app.config.translations:
language = flask.request.accept_languages.best_match(flask.current_app.config.translations.keys())
try:
language = flask.session['language']
except KeyError:
language = flask.request.accept_languages.best_match(translations)
flask.session['language'] = language flask.session['language'] = language
return language return language
@ -480,7 +475,7 @@ class MailuSessionExtension:
with cleaned.get_lock(): with cleaned.get_lock():
if not cleaned.value: if not cleaned.value:
cleaned.value = True cleaned.value = True
flask.current_app.logger.error('cleaning') flask.current_app.logger.info('cleaning session store')
MailuSessionExtension.cleanup_sessions(app) MailuSessionExtension.cleanup_sessions(app)
app.before_first_request(cleaner) app.before_first_request(cleaner)

@ -12,20 +12,19 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@babel/core": "^7.15.0",
"@babel/preset-env": "^7.15.0",
"admin-lte": "^3.1.0", "admin-lte": "^3.1.0",
"babel-loader": "^8.2.2", "babel-loader": "^8.2.2",
"clipboard": "^2.0.8",
"compression-webpack-plugin": "^8.0.1",
"css-loader": "^6.2.0", "css-loader": "^6.2.0",
"expose-loader": "^3.0.0", "css-minimizer-webpack-plugin": "^3.0.2",
"jquery": "^3.6.0", "datatables.net-plugins": "^1.10.24",
"less": "^4.1.1", "import-glob": "^1.5.0",
"less-loader": "^10.0.1", "less-loader": "^10.0.1",
"mini-css-extract-plugin": "^2.2.0", "mini-css-extract-plugin": "^2.2.0",
"node-sass": "^6.0.1", "sass": "<1.33.0",
"sass-loader": "^12.1.0", "sass-loader": "^12.1.0",
"select2": "^4.0.13", "terser-webpack-plugin": "^5.2.0",
"webpack": "^5.50.0", "webpack-cli": "^4.8.0"
"webpack-cli": "^4.7.2"
} }
} }

@ -20,7 +20,7 @@ Flask-Migrate==2.4.0
Flask-Script==2.0.6 Flask-Script==2.0.6
Flask-SQLAlchemy==2.4.0 Flask-SQLAlchemy==2.4.0
Flask-WTF==0.14.2 Flask-WTF==0.14.2
gunicorn==19.9.0 gunicorn==20.1.0
idna==2.8 idna==2.8
infinity==1.4 infinity==1.4
intervals==0.8.1 intervals==0.8.1

@ -1,65 +1,76 @@
var path = require("path"); const path = require('path');
var webpack = require("webpack"); const webpack = require('webpack');
var css = require("mini-css-extract-plugin"); const css = require('mini-css-extract-plugin');
const mini = require('css-minimizer-webpack-plugin');
const terse = require('terser-webpack-plugin');
const compress = require('compression-webpack-plugin');
module.exports = { module.exports = {
mode: "development", mode: 'production',
entry: { entry: {
app: "./assets/app.js", app: {
vendor: "./assets/vendor.js" import: './assets/app.js',
dependOn: 'vendor',
},
vendor: './assets/vendor.js',
}, },
output: { output: {
path: path.resolve(__dirname, "static/"), path: path.resolve(__dirname, 'static/'),
filename: "[name].js" filename: '[name].js',
assetModuleFilename: '[name][ext]',
}, },
module: { module: {
rules: [ rules: [
{ {
test: /\.js$/, test: /\.js$/,
use: ['babel-loader'] use: ['babel-loader', 'import-glob'],
}, },
{ {
test: /\.scss$/, test: /\.s?css$/i,
use: [css.loader, 'css-loader', 'sass-loader'] use: [css.loader, 'css-loader', 'sass-loader'],
}, },
{ {
test: /\.less$/, test: /\.less$/i,
use: [css.loader, 'css-loader', 'less-loader'] use: [css.loader, 'css-loader', 'less-loader'],
}, },
{ {
test: /\.css$/, test: /\.(json|png|svg|jpg|jpeg|gif)$/i,
use: [css.loader, 'css-loader'] type: 'asset/resource',
}, },
{ ],
// Exposes jQuery for use outside Webpack build
test: require.resolve('jquery'),
use: [{
loader: 'expose-loader',
options: {
exposes: [
{
globalName: '$',
override: true,
},
{
globalName: 'jQuery',
override: true,
},
]
},
}]
}
]
}, },
plugins: [ plugins: [
new css({ new css({
filename: "[name].css", filename: '[name].css',
chunkFilename: "[id].css" chunkFilename: '[id].css',
}), }),
new webpack.ProvidePlugin({ new webpack.ProvidePlugin({
$: "jquery", $: 'jquery',
jQuery: "jquery" jQuery: 'jquery',
}) ClipboardJS: 'clipboard',
] }),
} new compress({
filename: '[path][base].gz',
algorithm: "gzip",
exclude: /\.(png|gif|jpe?g)$/,
threshold: 5120,
minRatio: 0.8,
deleteOriginalAssets: false,
}),
],
optimization: {
minimize: true,
minimizer: [
new terse(),
new mini({
minimizerOptions: {
preset: [
'default', {
discardComments: { removeAll: true },
},
],
},
}),
],
},
};

@ -16,6 +16,8 @@ COPY conf /conf
COPY static /static COPY static /static
COPY *.py / COPY *.py /
RUN gzip -k9 /static/*.ico /static/*.txt
EXPOSE 80/tcp 443/tcp 110/tcp 143/tcp 465/tcp 587/tcp 993/tcp 995/tcp 25/tcp 10025/tcp 10143/tcp EXPOSE 80/tcp 443/tcp 110/tcp 143/tcp 465/tcp 587/tcp 993/tcp 995/tcp 25/tcp 10025/tcp 10143/tcp
VOLUME ["/certs"] VOLUME ["/certs"]
VOLUME ["/overrides"] VOLUME ["/overrides"]

@ -33,6 +33,17 @@ http {
default $http_x_forwarded_proto; default $http_x_forwarded_proto;
'' $scheme; '' $scheme;
} }
map $uri $expires {
default off;
~*\.(ico|css|js|gif|jpeg|jpg|png|woff2?|ttf|otf|svg|tiff|eot|webp)$ 97d;
}
# compression
gzip on;
gzip_static on;
gzip_types text/plain text/css application/xml application/javascript
gzip_min_length 1024;
# TODO: figure out how to server pre-compressed assets from admin container
{% if KUBERNETES_INGRESS != 'true' and TLS_FLAVOR in [ 'letsencrypt', 'cert' ] %} {% if KUBERNETES_INGRESS != 'true' and TLS_FLAVOR in [ 'letsencrypt', 'cert' ] %}
# Enable the proxy for certbot if the flavor is letsencrypt and not on kubernetes # Enable the proxy for certbot if the flavor is letsencrypt and not on kubernetes
@ -50,6 +61,7 @@ http {
location / { location / {
return 301 https://$host$request_uri; return 301 https://$host$request_uri;
} }
} }
{% endif %} {% endif %}
@ -94,7 +106,7 @@ http {
# Remove headers to prevent duplication and information disclosure # Remove headers to prevent duplication and information disclosure
proxy_hide_header X-XSS-Protection; proxy_hide_header X-XSS-Protection;
proxy_hide_header X-Powered-By; proxy_hide_header X-Powered-By;
add_header X-Frame-Options 'SAMEORIGIN'; add_header X-Frame-Options 'SAMEORIGIN';
add_header X-Content-Type-Options 'nosniff'; add_header X-Content-Type-Options 'nosniff';
add_header X-Permitted-Cross-Domain-Policies 'none'; add_header X-Permitted-Cross-Domain-Policies 'none';
@ -113,12 +125,12 @@ http {
return 403; return 403;
} }
{% else %} {% else %}
include /overrides/*.conf; include /overrides/*.conf;
# Actual logic # Actual logic
{% if WEB_WEBMAIL != '/' and WEBROOT_REDIRECT != 'none' %} {% if WEB_WEBMAIL != '/' and WEBROOT_REDIRECT != 'none' %}
location / { location / {
expires $expires;
{% if WEBROOT_REDIRECT %} {% if WEBROOT_REDIRECT %}
try_files $uri {{ WEBROOT_REDIRECT }}; try_files $uri {{ WEBROOT_REDIRECT }};
{% else %} {% else %}
@ -173,6 +185,7 @@ http {
include /etc/nginx/proxy.conf; include /etc/nginx/proxy.conf;
proxy_set_header X-Forwarded-Prefix {{ WEB_ADMIN }}; proxy_set_header X-Forwarded-Prefix {{ WEB_ADMIN }};
proxy_pass http://$admin; proxy_pass http://$admin;
expires $expires;
} }
location {{ WEB_ADMIN }}/antispam { location {{ WEB_ADMIN }}/antispam {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 978 B

After

Width:  |  Height:  |  Size: 924 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

@ -0,0 +1,2 @@
User-agent: *
Disallow: /

@ -128,6 +128,13 @@ Both ``SITENAME`` and ``WEBSITE`` are customization options for the panel menu
in the admin interface, while ``SITENAME`` is a customization option for in the admin interface, while ``SITENAME`` is a customization option for
every Web interface. every Web interface.
- ``LOGO_BACKGROUND`` sets a custom background colour for the brand logo in the topleft of the main admin interface.
For a list of colour codes refer to this page of `w3schools`_.
- ``LOGO_URL`` sets a URL for a custom logo. This logo replaces the Mailu logo in the topleft of the main admin interface.
.. _`w3schools`: https://www.w3schools.com/cssref/css_colors.asp
.. _admin_account: .. _admin_account:
Admin account - automatic creation Admin account - automatic creation

@ -0,0 +1,33 @@
AdminLTE3 design optimizations, asset compression and caching
- fixed copy of qemu-arm-static for alpine
- added 'set -eu' safeguard
- silenced npm update notification
- added color to webpack call
- changed Admin-LTE default blue
- AdminLTE 3 style tweaks
- localized datatables
- moved external javascript code to vendor.js
- added mailu logo
- moved all inline javascript to app.js
- added iframe display of rspamd page
- updated language-selector to display full language names and use post
- added fieldset to group and en/disable input fields
- added clipboard copy buttons
- cleaned external javascript imports
- pre-split first hostname for further use
- cache dns_* properties of domain object (immutable during runtime)
- fixed and splitted dns_dkim property of domain object (space missing)
- added autoconfig and tlsa properties to domain object
- suppressed extra vertical spacing in jinja2 templates
- improved accessibility for screen reader
- deleted unused/broken /user/forward route
- updated gunicorn to 20.1.0 to get rid of buffering error at startup
- switched webpack to production mode
- added css and javascript minimization
- added pre-compression of assets (gzip)
- removed obsolete dependencies
- switched from node-sass to dart-sass
- changed startup cleaning message from error to info
- move client config to "my account" section when logged in

@ -38,7 +38,8 @@ RUN apt-get update && apt-get install -y \
&& sed -i 's,^php_value.*post_max_size,#&,g' .htaccess \ && sed -i 's,^php_value.*post_max_size,#&,g' .htaccess \
&& sed -i 's,^php_value.*upload_max_filesize,#&,g' .htaccess \ && sed -i 's,^php_value.*upload_max_filesize,#&,g' .htaccess \
&& chown -R www-data: logs temp \ && chown -R www-data: logs temp \
&& rm -rf /var/lib/apt/lists && rm -rf /var/lib/apt/lists \
&& a2enmod rewrite deflate expires headers
COPY php.ini /php.ini COPY php.ini /php.ini
COPY config.inc.php /var/www/html/config/ COPY config.inc.php /var/www/html/config/

Loading…
Cancel
Save