Merge remote-tracking branch 'upstream/master' into passlib
@ -27,7 +27,7 @@ pull_request_rules:
|
||||
|
||||
- name: Trusted author and 1 approved review; trigger bors r+
|
||||
conditions:
|
||||
- author~=^(mergify|kaiyou|muhlemmer|mildred|HorayNarea|hoellen|ofthesun9|Nebukadneza|micw|lub|Diman0|3-w-c|decentral1se|ghostwheel42|nextgens|parisni)$
|
||||
- author~=^(mergify|kaiyou|muhlemmer|mildred|HorayNarea|hoellen|ofthesun9|Nebukadneza|micw|lub|Diman0|ghostwheel42|nextgens)$
|
||||
- -title~=(WIP|wip)
|
||||
- -label~=^(status/wip|status/blocked|review/need2)$
|
||||
- "#approved-reviews-by>=1"
|
||||
|
@ -8,7 +8,7 @@
|
||||
- Mention an issue like: #001
|
||||
- Auto close an issue like: closes #001
|
||||
|
||||
## Prerequistes
|
||||
## Prerequisites
|
||||
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.
|
||||
|
||||
|
@ -22,7 +22,7 @@ Main features include:
|
||||
- **Web access**, multiple Webmails and administration interface
|
||||
- **User features**, aliases, auto-reply, auto-forward, fetched accounts
|
||||
- **Admin features**, global admins, announcements, per-domain delegation, quotas
|
||||
- **Security**, enforced TLS, Letsencrypt!, outgoing DKIM, anti-virus scanner
|
||||
- **Security**, enforced TLS, DANE, MTA-STS, Letsencrypt!, outgoing DKIM, anti-virus scanner
|
||||
- **Antispam**, auto-learn, greylisting, DMARC and SPF
|
||||
- **Freedom**, all FOSS components, no tracker included
|
||||
|
||||
|
@ -1,40 +1,51 @@
|
||||
# First stage to build assets
|
||||
ARG DISTRO=alpine:3.14
|
||||
ARG DISTRO=alpine:3.14.2
|
||||
ARG ARCH=""
|
||||
|
||||
FROM ${ARCH}node:16 as assets
|
||||
COPY --from=balenalib/rpi-alpine:3.14 /usr/bin/qemu-arm-static /usr/bin/qemu-arm-static
|
||||
|
||||
COPY package.json ./
|
||||
RUN npm install
|
||||
RUN set -eu \
|
||||
&& npm config set update-notifier false \
|
||||
&& npm install --no-fund
|
||||
|
||||
COPY ./webpack.config.js ./
|
||||
COPY ./assets ./assets
|
||||
RUN mkdir static \
|
||||
&& ./node_modules/.bin/webpack-cli
|
||||
COPY webpack.config.js ./
|
||||
COPY assets ./assets
|
||||
RUN set -eu \
|
||||
&& sed -i 's/#007bff/#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; do \
|
||||
cp node_modules/datatables.net-plugins/i18n/${l#*:}.json assets/${l%:*}.json; \
|
||||
done \
|
||||
&& node_modules/.bin/webpack-cli --color
|
||||
|
||||
|
||||
# Actual application
|
||||
FROM $DISTRO
|
||||
COPY --from=balenalib/rpi-alpine:3.14 /usr/bin/qemu-arm-static /usr/bin/qemu-arm-static
|
||||
|
||||
ENV TZ Etc/UTC
|
||||
|
||||
# python3 shared with most images
|
||||
RUN apk add --no-cache \
|
||||
python3 py3-pip git bash \
|
||||
&& pip3 install --upgrade pip
|
||||
RUN set -eu \
|
||||
&& apk add --no-cache python3 py3-pip py3-wheel git bash tzdata \
|
||||
&& pip3 install --upgrade pip
|
||||
|
||||
RUN mkdir -p /app
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements-prod.txt requirements.txt
|
||||
RUN apk add --no-cache openssl curl postgresql-libs mariadb-connector-c \
|
||||
&& apk add --no-cache --virtual build-dep \
|
||||
openssl-dev libffi-dev python3-dev build-base postgresql-dev mariadb-connector-c-dev cargo \
|
||||
&& pip3 install -r requirements.txt \
|
||||
&& apk del --no-cache build-dep
|
||||
RUN set -eu \
|
||||
&& apk add --no-cache libressl curl postgresql-libs mariadb-connector-c \
|
||||
&& apk add --no-cache --virtual build-dep libressl-dev libffi-dev python3-dev build-base postgresql-dev mariadb-connector-c-dev cargo \
|
||||
&& pip install --upgrade pip \
|
||||
&& pip install -r requirements.txt \
|
||||
&& apk del --no-cache build-dep
|
||||
|
||||
COPY --from=assets static ./mailu/ui/static
|
||||
COPY --from=assets static ./mailu/static
|
||||
COPY mailu ./mailu
|
||||
COPY migrations ./migrations
|
||||
COPY start.py /start.py
|
||||
COPY audit.py /audit.py
|
||||
|
||||
RUN pybabel compile -d mailu/translations
|
||||
|
||||
@ -44,4 +55,4 @@ ENV FLASK_APP mailu
|
||||
|
||||
CMD /start.py
|
||||
|
||||
HEALTHCHECK CMD curl -f -L http://localhost/ui/login?next=ui.index || exit 1
|
||||
HEALTHCHECK CMD curl -f -L http://localhost/sso/login?next=ui.index || exit 1
|
||||
|
@ -1,23 +1,59 @@
|
||||
.select2-search--inline .select2-search__field:focus {
|
||||
border: none;
|
||||
/* mailu logo */
|
||||
.mailu-logo {
|
||||
opacity: .8;
|
||||
}
|
||||
.bg-mailu-logo {
|
||||
background-color: #2980b9!important;
|
||||
}
|
||||
|
||||
.sidebar h4 {
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
/* user image */
|
||||
.div-circle {
|
||||
position: relative;
|
||||
width: 2.1rem;
|
||||
height: 2.1rem;
|
||||
opacity: .8;
|
||||
background-color: white;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.div-circle > i {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%)
|
||||
}
|
||||
|
||||
.sidebar-collapse .sidebar h4 {
|
||||
display: none !important;
|
||||
/* nice round preformatted configuration display */
|
||||
.pre-config {
|
||||
padding: 9px;
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: anywhere;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.logo a {
|
||||
color: #fff;
|
||||
/* fieldset */
|
||||
legend {
|
||||
font-size: inherit;
|
||||
}
|
||||
fieldset:disabled :not(legend) label {
|
||||
opacity: .5;
|
||||
}
|
||||
fieldset:disabled .form-control:disabled {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.sidebar-toggle {
|
||||
padding: unset !important;
|
||||
/* fix animation for icons in menu text */
|
||||
.sidebar .nav-link p i {
|
||||
transition: margin-left .3s linear,opacity .3s ease,visibility .3s ease;
|
||||
}
|
||||
|
||||
/* fix select2 text color */
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__choice {
|
||||
color: black;
|
||||
}
|
||||
|
||||
/* range input spacing */
|
||||
.input-group-text {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
@ -1,17 +1,79 @@
|
||||
require('./app.css');
|
||||
|
||||
import 'admin-lte/plugins/select2/js/select2.js';
|
||||
import 'admin-lte/plugins/datatables/jquery.dataTables.js';
|
||||
import 'admin-lte/plugins/datatables-bs4/js/dataTables.bootstrap4.js';
|
||||
import 'admin-lte/plugins/datatables-responsive/js/dataTables.responsive.js';
|
||||
import 'admin-lte/plugins/datatables-responsive/js/responsive.bootstrap4.js';
|
||||
import logo from './mailu.png';
|
||||
import modules from "./*.json";
|
||||
|
||||
jQuery("document").ready(function() {
|
||||
jQuery(".mailselect").select2({
|
||||
// TODO: conditionally (or lazy) load select2 and dataTable
|
||||
$('document').ready(function() {
|
||||
|
||||
// intercept anchors with data-clicked attribute and open alternate location instead
|
||||
$('[data-clicked]').click(function(e) {
|
||||
e.preventDefault();
|
||||
window.location.href = $(this).data('clicked');
|
||||
});
|
||||
|
||||
// use post for language selection
|
||||
$('#mailu-languages > a').click(function(e) {
|
||||
e.preventDefault();
|
||||
$.post({
|
||||
url: $(this).attr('href'),
|
||||
success: function() {
|
||||
window.location = window.location.href;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// 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,textarea').not(this).removeAttr('disabled');
|
||||
} else {
|
||||
fieldset.attr('disabled', '');
|
||||
fieldset.find('input,textarea').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() {
|
||||
var num = (infinity && this.value == 0) ? '∞' : (this.value/step).toFixed(2);
|
||||
if (num.endsWith('.00')) num = num.substr(0, num.length - 3);
|
||||
value_element.text(num);
|
||||
}).trigger('input');
|
||||
}
|
||||
});
|
||||
|
||||
// init select2
|
||||
$('.mailselect').select2({
|
||||
tags: true,
|
||||
tokenSeparators: [',', ' ']
|
||||
tokenSeparators: [',', ' '],
|
||||
});
|
||||
jQuery(".dataTable").DataTable({
|
||||
"responsive": true,
|
||||
|
||||
// init dataTable
|
||||
var d = $(document.documentElement);
|
||||
$('.dataTable').DataTable({
|
||||
'responsive': true,
|
||||
language: {
|
||||
url: d.data('static') + d.attr('lang') + '.json',
|
||||
},
|
||||
});
|
||||
|
||||
// init clipboard.js
|
||||
new ClipboardJS('.btn-clip');
|
||||
|
||||
// disable login if not possible
|
||||
var l = $('#login_needs_https');
|
||||
if (l.length && window.location.protocol != 'https:') {
|
||||
l.removeClass("d-none");
|
||||
$('form :input').prop('disabled', true);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
BIN
core/admin/assets/mailu.png
Normal file
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
|
||||
import 'admin-lte/plugins/jquery/jquery.min.js';
|
||||
import 'admin-lte/plugins/bootstrap/js/bootstrap.bundle.min.js';
|
||||
import 'admin-lte/build/scss/adminlte.scss';
|
||||
import 'admin-lte/plugins/datatables-bs4/css/dataTables.bootstrap4.css';
|
||||
import 'admin-lte/plugins/datatables-responsive/css/responsive.bootstrap4.css';
|
||||
import 'admin-lte/plugins/bootstrap/js/bootstrap.js';
|
||||
import 'admin-lte/build/js/AdminLTE.js';
|
||||
import 'admin-lte/build/js/Layout.js';
|
||||
import 'admin-lte/build/js/ControlSidebar.js';
|
||||
import 'admin-lte/build/js/PushMenu.js';
|
||||
|
||||
// fontawesome plugin
|
||||
import 'admin-lte/plugins/fontawesome-free/css/all.min.css';
|
||||
|
||||
// select2 plugin
|
||||
import 'admin-lte/plugins/select2/css/select2.min.css';
|
||||
import 'admin-lte/plugins/select2/js/select2.min.js';
|
||||
|
||||
// dataTables plugin
|
||||
import 'admin-lte/plugins/datatables-bs4/css/dataTables.bootstrap4.min.css';
|
||||
import 'admin-lte/plugins/datatables-responsive/css/responsive.bootstrap4.min.css';
|
||||
import 'admin-lte/plugins/datatables/jquery.dataTables.min.js';
|
||||
import 'admin-lte/plugins/datatables-bs4/js/dataTables.bootstrap4.min.js';
|
||||
import 'admin-lte/plugins/datatables-responsive/js/dataTables.responsive.min.js';
|
||||
import 'admin-lte/plugins/datatables-responsive/js/responsive.bootstrap4.min.js';
|
||||
|
||||
// clipboard.js
|
||||
import 'clipboard/dist/clipboard.min.js';
|
||||
|
||||
|
30
core/admin/audit.py
Normal file → Executable file
@ -1,14 +1,19 @@
|
||||
from mailu import app
|
||||
#!/usr/bin/python3
|
||||
|
||||
import sys
|
||||
import tabulate
|
||||
|
||||
sys.path[0:0] = ['/app']
|
||||
|
||||
import mailu
|
||||
app = mailu.create_app()
|
||||
|
||||
|
||||
# Known endpoints without permissions
|
||||
known_missing_permissions = [
|
||||
"index",
|
||||
"static", "bootstrap.static",
|
||||
"admin.static", "admin.login"
|
||||
'index',
|
||||
'static', 'bootstrap.static',
|
||||
'admin.static', 'admin.login'
|
||||
]
|
||||
|
||||
|
||||
@ -16,7 +21,7 @@ known_missing_permissions = [
|
||||
missing_permissions = []
|
||||
permissions = {}
|
||||
for endpoint, function in app.view_functions.items():
|
||||
audit = function.__dict__.get("_audit_permissions")
|
||||
audit = function.__dict__.get('_audit_permissions')
|
||||
if audit:
|
||||
handler, args = audit
|
||||
if args:
|
||||
@ -28,16 +33,15 @@ for endpoint, function in app.view_functions.items():
|
||||
elif endpoint not in known_missing_permissions:
|
||||
missing_permissions.append(endpoint)
|
||||
|
||||
|
||||
# Fail if any endpoint is missing a permission check
|
||||
if missing_permissions:
|
||||
print("The following endpoints are missing permission checks:")
|
||||
print(missing_permissions.join(","))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# Display the permissions table
|
||||
print(tabulate.tabulate([
|
||||
[route, *permissions[route.endpoint]]
|
||||
for route in app.url_map.iter_rules() if route.endpoint in permissions
|
||||
]))
|
||||
|
||||
# Warn if any endpoint is missing a permission check
|
||||
if missing_permissions:
|
||||
print()
|
||||
print('The following endpoints are missing permission checks:')
|
||||
print(','.join(missing_permissions))
|
||||
|
||||
|
@ -11,11 +11,10 @@ import hmac
|
||||
def create_app_from_config(config):
|
||||
""" Create a new application based on the given configuration
|
||||
"""
|
||||
app = flask.Flask(__name__)
|
||||
app = flask.Flask(__name__, static_folder='static', static_url_path='/static')
|
||||
app.cli.add_command(manage.mailu)
|
||||
|
||||
# Bootstrap is used for basic JS and CSS loading
|
||||
# TODO: remove this and use statically generated assets instead
|
||||
# Bootstrap is used for error display and flash messages
|
||||
app.bootstrap = flask_bootstrap.Bootstrap(app)
|
||||
|
||||
# Initialize application extensions
|
||||
@ -29,7 +28,18 @@ def create_app_from_config(config):
|
||||
utils.proxy.init_app(app)
|
||||
utils.migrate.init_app(app, models.db)
|
||||
|
||||
app.device_cookie_key = hmac.new(bytearray(app.secret_key, 'utf-8'), bytearray('DEVICE_COOKIE_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()
|
||||
app.srs_key = hmac.new(bytearray(app.secret_key, 'utf-8'), bytearray('SRS_KEY', 'utf-8'), 'sha256').digest()
|
||||
|
||||
# Initialize list of translations
|
||||
app.config.translations = {
|
||||
str(locale): locale
|
||||
for locale in sorted(
|
||||
utils.babel.list_translations(),
|
||||
key=lambda l: l.get_language_name().title()
|
||||
)
|
||||
}
|
||||
|
||||
# Initialize debugging tools
|
||||
if app.config.get("DEBUG"):
|
||||
@ -43,15 +53,24 @@ def create_app_from_config(config):
|
||||
def inject_defaults():
|
||||
signup_domains = models.Domain.query.filter_by(signup_enabled=True).all()
|
||||
return dict(
|
||||
signup_domains=signup_domains,
|
||||
config=app.config
|
||||
signup_domains= signup_domains,
|
||||
config = app.config,
|
||||
)
|
||||
|
||||
# Import views
|
||||
from mailu import ui, internal
|
||||
app.register_blueprint(ui.ui, url_prefix='/ui')
|
||||
app.register_blueprint(internal.internal, url_prefix='/internal')
|
||||
# Jinja filters
|
||||
@app.template_filter()
|
||||
def format_date(value):
|
||||
return utils.flask_babel.format_date(value) if value else ''
|
||||
|
||||
@app.template_filter()
|
||||
def format_datetime(value):
|
||||
return utils.flask_babel.format_datetime(value) if value else ''
|
||||
|
||||
# Import views
|
||||
from mailu import ui, internal, sso
|
||||
app.register_blueprint(ui.ui, url_prefix=app.config['WEB_ADMIN'])
|
||||
app.register_blueprint(internal.internal, url_prefix='/internal')
|
||||
app.register_blueprint(sso.sso, url_prefix='/sso')
|
||||
return app
|
||||
|
||||
|
||||
@ -60,3 +79,4 @@ def create_app():
|
||||
"""
|
||||
config = configuration.ConfigManager()
|
||||
return create_app_from_config(config)
|
||||
|
||||
|
@ -2,6 +2,7 @@ import os
|
||||
|
||||
from datetime import timedelta
|
||||
from socrate import system
|
||||
import ipaddress
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
# Specific to the admin UI
|
||||
@ -35,8 +36,13 @@ DEFAULT_CONFIG = {
|
||||
'WILDCARD_SENDERS': '',
|
||||
'TLS_FLAVOR': 'cert',
|
||||
'INBOUND_TLS_ENFORCE': False,
|
||||
'AUTH_RATELIMIT': '1000/minute;10000/hour',
|
||||
'AUTH_RATELIMIT_SUBNET': False,
|
||||
'DEFER_ON_TLS_ERROR': True,
|
||||
'AUTH_RATELIMIT_IP': '60/hour',
|
||||
'AUTH_RATELIMIT_IP_V4_MASK': 24,
|
||||
'AUTH_RATELIMIT_IP_V6_MASK': 56,
|
||||
'AUTH_RATELIMIT_USER': '100/day',
|
||||
'AUTH_RATELIMIT_EXEMPTION': '',
|
||||
'AUTH_RATELIMIT_EXEMPTION_LENGTH': 86400,
|
||||
'DISABLE_STATISTICS': False,
|
||||
# Mail settings
|
||||
'DMARC_RUA': None,
|
||||
@ -48,20 +54,26 @@ DEFAULT_CONFIG = {
|
||||
'DKIM_PATH': '/dkim/{domain}.{selector}.key',
|
||||
'DEFAULT_QUOTA': 1000000000,
|
||||
'MESSAGE_RATELIMIT': '200/day',
|
||||
'MESSAGE_RATELIMIT_EXEMPTION': '',
|
||||
'RECIPIENT_DELIMITER': '',
|
||||
# Web settings
|
||||
'SITENAME': 'Mailu',
|
||||
'WEBSITE': 'https://mailu.io',
|
||||
'ADMIN' : 'none',
|
||||
'WEB_ADMIN': '/admin',
|
||||
'WEB_WEBMAIL': '/webmail',
|
||||
'WEBMAIL': 'none',
|
||||
'RECAPTCHA_PUBLIC_KEY': '',
|
||||
'RECAPTCHA_PRIVATE_KEY': '',
|
||||
'LOGO_URL': None,
|
||||
'LOGO_BACKGROUND': None,
|
||||
# Advanced settings
|
||||
'LOG_LEVEL': 'WARNING',
|
||||
'SESSION_KEY_BITS': 128,
|
||||
'SESSION_LIFETIME': 24,
|
||||
'SESSION_COOKIE_SECURE': True,
|
||||
'CREDENTIAL_ROUNDS': 12,
|
||||
'TZ': 'Etc/UTC',
|
||||
# Host settings
|
||||
'HOST_IMAP': 'imap',
|
||||
'HOST_LMTP': 'imap:2525',
|
||||
@ -78,7 +90,7 @@ DEFAULT_CONFIG = {
|
||||
'POD_ADDRESS_RANGE': None
|
||||
}
|
||||
|
||||
class ConfigManager(dict):
|
||||
class ConfigManager:
|
||||
""" Naive configuration manager that uses environment only
|
||||
"""
|
||||
|
||||
@ -93,19 +105,16 @@ class ConfigManager(dict):
|
||||
|
||||
def get_host_address(self, name):
|
||||
# if MYSERVICE_ADDRESS is defined, use this
|
||||
if '{}_ADDRESS'.format(name) in os.environ:
|
||||
return os.environ.get('{}_ADDRESS'.format(name))
|
||||
if f'{name}_ADDRESS' in os.environ:
|
||||
return os.environ.get(f'{name}_ADDRESS')
|
||||
# otherwise use the host name and resolve it
|
||||
return system.resolve_address(self.config['HOST_{}'.format(name)])
|
||||
return system.resolve_address(self.config[f'HOST_{name}'])
|
||||
|
||||
def resolve_hosts(self):
|
||||
self.config["IMAP_ADDRESS"] = self.get_host_address("IMAP")
|
||||
self.config["POP3_ADDRESS"] = self.get_host_address("POP3")
|
||||
self.config["AUTHSMTP_ADDRESS"] = self.get_host_address("AUTHSMTP")
|
||||
self.config["SMTP_ADDRESS"] = self.get_host_address("SMTP")
|
||||
self.config["REDIS_ADDRESS"] = self.get_host_address("REDIS")
|
||||
if self.config["WEBMAIL"] != "none":
|
||||
self.config["WEBMAIL_ADDRESS"] = self.get_host_address("WEBMAIL")
|
||||
for key in ['IMAP', 'POP3', 'AUTHSMTP', 'SMTP', 'REDIS']:
|
||||
self.config[f'{key}_ADDRESS'] = self.get_host_address(key)
|
||||
if self.config['WEBMAIL'] != 'none':
|
||||
self.config['WEBMAIL_ADDRESS'] = self.get_host_address('WEBMAIL')
|
||||
|
||||
def __get_env(self, key, value):
|
||||
key_file = key + "_FILE"
|
||||
@ -124,6 +133,7 @@ class ConfigManager(dict):
|
||||
return value
|
||||
|
||||
def init_app(self, app):
|
||||
# get current app config
|
||||
self.config.update(app.config)
|
||||
# get environment variables
|
||||
self.config.update({
|
||||
@ -137,31 +147,18 @@ class ConfigManager(dict):
|
||||
template = self.DB_TEMPLATES[self.config['DB_FLAVOR']]
|
||||
self.config['SQLALCHEMY_DATABASE_URI'] = template.format(**self.config)
|
||||
|
||||
self.config['RATELIMIT_STORAGE_URL'] = 'redis://{0}/2'.format(self.config['REDIS_ADDRESS'])
|
||||
self.config['QUOTA_STORAGE_URL'] = 'redis://{0}/1'.format(self.config['REDIS_ADDRESS'])
|
||||
self.config['SESSION_STORAGE_URL'] = 'redis://{0}/3'.format(self.config['REDIS_ADDRESS'])
|
||||
self.config['RATELIMIT_STORAGE_URL'] = f'redis://{self.config["REDIS_ADDRESS"]}/2'
|
||||
self.config['QUOTA_STORAGE_URL'] = f'redis://{self.config["REDIS_ADDRESS"]}/1'
|
||||
self.config['SESSION_STORAGE_URL'] = f'redis://{self.config["REDIS_ADDRESS"]}/3'
|
||||
self.config['SESSION_COOKIE_SAMESITE'] = 'Strict'
|
||||
self.config['SESSION_COOKIE_HTTPONLY'] = True
|
||||
self.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=int(self.config['SESSION_LIFETIME']))
|
||||
# update the app config itself
|
||||
app.config = self
|
||||
hostnames = [host.strip() for host in self.config['HOSTNAMES'].split(',')]
|
||||
self.config['AUTH_RATELIMIT_EXEMPTION'] = set(ipaddress.ip_network(cidr, False) for cidr in (cidr.strip() for cidr in self.config['AUTH_RATELIMIT_EXEMPTION'].split(',')) if cidr)
|
||||
self.config['MESSAGE_RATELIMIT_EXEMPTION'] = set([s for s in self.config['MESSAGE_RATELIMIT_EXEMPTION'].lower().replace(' ', '').split(',') if s])
|
||||
self.config['HOSTNAMES'] = ','.join(hostnames)
|
||||
self.config['HOSTNAME'] = hostnames[0]
|
||||
|
||||
def setdefault(self, key, value):
|
||||
if key not in self.config:
|
||||
self.config[key] = value
|
||||
return self.config[key]
|
||||
# update the app config
|
||||
app.config.update(self.config)
|
||||
|
||||
def get(self, *args):
|
||||
return self.config.get(*args)
|
||||
|
||||
def keys(self):
|
||||
return self.config.keys()
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.config.get(key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self.config[key] = value
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self.config
|
||||
|
@ -1,6 +1,6 @@
|
||||
import flask_debugtoolbar
|
||||
|
||||
from werkzeug.contrib import profiler as werkzeug_profiler
|
||||
from werkzeug.middleware.profiler import ProfilerMiddleware
|
||||
|
||||
|
||||
# Debugging toolbar
|
||||
@ -10,7 +10,7 @@ toolbar = flask_debugtoolbar.DebugToolbarExtension()
|
||||
# Profiler
|
||||
class Profiler(object):
|
||||
def init_app(self, app):
|
||||
app.wsgi_app = werkzeug_profiler.ProfilerMiddleware(
|
||||
app.wsgi_app = ProfilerMiddleware(
|
||||
app.wsgi_app, restrictions=[30]
|
||||
)
|
||||
|
||||
|
@ -5,6 +5,7 @@ import re
|
||||
import urllib
|
||||
import ipaddress
|
||||
import socket
|
||||
import sqlalchemy.exc
|
||||
import tenacity
|
||||
|
||||
SUPPORTED_AUTH_METHODS = ["none", "plain"]
|
||||
@ -19,6 +20,11 @@ STATUSES = {
|
||||
"encryption": ("Must issue a STARTTLS command first", {
|
||||
"smtp": "530 5.7.0"
|
||||
}),
|
||||
"ratelimit": ("Temporary authentication failure (rate-limit)", {
|
||||
"imap": "LIMIT",
|
||||
"smtp": "451 4.3.2",
|
||||
"pop3": "-ERR [LOGIN-DELAY] Retry later"
|
||||
}),
|
||||
}
|
||||
|
||||
def check_credentials(user, password, ip, protocol=None):
|
||||
@ -71,37 +77,46 @@ def handle_authentication(headers):
|
||||
}
|
||||
# Authenticated user
|
||||
elif method == "plain":
|
||||
server, port = get_server(headers["Auth-Protocol"], True)
|
||||
is_valid_user = False
|
||||
# According to RFC2616 section 3.7.1 and PEP 3333, HTTP headers should
|
||||
# be ASCII and are generally considered ISO8859-1. However when passing
|
||||
# the password, nginx does not transcode the input UTF string, thus
|
||||
# we need to manually decode.
|
||||
raw_user_email = urllib.parse.unquote(headers["Auth-User"])
|
||||
user_email = raw_user_email.encode("iso8859-1").decode("utf8")
|
||||
raw_password = urllib.parse.unquote(headers["Auth-Pass"])
|
||||
password = raw_password.encode("iso8859-1").decode("utf8")
|
||||
ip = urllib.parse.unquote(headers["Client-Ip"])
|
||||
service_port = int(urllib.parse.unquote(headers["Auth-Port"]))
|
||||
if service_port == 25:
|
||||
return {
|
||||
"Auth-Status": "AUTH not supported",
|
||||
"Auth-Error-Code": "502 5.5.1",
|
||||
"Auth-Wait": 0
|
||||
}
|
||||
user = models.User.query.get(user_email)
|
||||
if check_credentials(user, password, ip, protocol):
|
||||
return {
|
||||
"Auth-Status": "OK",
|
||||
"Auth-Server": server,
|
||||
"Auth-Port": port
|
||||
}
|
||||
user_email = 'invalid'
|
||||
try:
|
||||
user_email = raw_user_email.encode("iso8859-1").decode("utf8")
|
||||
password = raw_password.encode("iso8859-1").decode("utf8")
|
||||
ip = urllib.parse.unquote(headers["Client-Ip"])
|
||||
except:
|
||||
app.logger.warn(f'Received undecodable user/password from nginx: {raw_user_email!r}/{raw_password!r}')
|
||||
else:
|
||||
status, code = get_status(protocol, "authentication")
|
||||
return {
|
||||
"Auth-Status": status,
|
||||
"Auth-Error-Code": code,
|
||||
"Auth-Wait": 0
|
||||
}
|
||||
try:
|
||||
user = models.User.query.get(user_email)
|
||||
is_valid_user = True
|
||||
except sqlalchemy.exc.StatementError as exc:
|
||||
exc = str(exc).split('\n', 1)[0]
|
||||
app.logger.warn(f'Invalid user {user_email!r}: {exc}')
|
||||
else:
|
||||
ip = urllib.parse.unquote(headers["Client-Ip"])
|
||||
if check_credentials(user, password, ip, protocol):
|
||||
server, port = get_server(headers["Auth-Protocol"], True)
|
||||
return {
|
||||
"Auth-Status": "OK",
|
||||
"Auth-Server": server,
|
||||
"Auth-User": user_email,
|
||||
"Auth-User-Exists": is_valid_user,
|
||||
"Auth-Port": port
|
||||
}
|
||||
status, code = get_status(protocol, "authentication")
|
||||
return {
|
||||
"Auth-Status": status,
|
||||
"Auth-Error-Code": code,
|
||||
"Auth-User": user_email,
|
||||
"Auth-User-Exists": is_valid_user,
|
||||
"Auth-Wait": 0
|
||||
}
|
||||
# Unexpected
|
||||
return {}
|
||||
|
||||
|
@ -19,7 +19,7 @@ if header :index 2 :matches "Received" "from * by * for <*>; *"
|
||||
}
|
||||
|
||||
{% if user.spam_enabled %}
|
||||
if spamtest :percent :value "gt" :comparator "i;ascii-numeric" "{{ user.spam_threshold }}"
|
||||
if spamtest :percent :value "gt" :comparator "i;ascii-numeric" "{{ user.spam_threshold }}"
|
||||
{
|
||||
setflag "\\seen";
|
||||
fileinto :create "Junk";
|
||||
@ -32,6 +32,6 @@ if exists "X-Virus" {
|
||||
stop;
|
||||
}
|
||||
|
||||
{% if user.reply_active %}
|
||||
{% if user.reply_active %}
|
||||
vacation :days 1 {% if user.displayed_name != "" %}:from "{{ user.displayed_name }} <{{ user.email }}>"{% endif %} :subject "{{ user.reply_subject }}" "{{ user.reply_body }}";
|
||||
{% endif %}
|
||||
|
@ -1,3 +1,3 @@
|
||||
__all__ = [
|
||||
'auth', 'postfix', 'dovecot', 'fetch'
|
||||
'auth', 'postfix', 'dovecot', 'fetch', 'rspamd'
|
||||
]
|
||||
|
@ -5,19 +5,24 @@ from flask import current_app as app
|
||||
import flask
|
||||
import flask_login
|
||||
import base64
|
||||
import ipaddress
|
||||
|
||||
|
||||
@internal.route("/auth/email")
|
||||
def nginx_authentication():
|
||||
""" Main authentication endpoint for Nginx email server
|
||||
"""
|
||||
limiter = utils.limiter.get_limiter(app.config["AUTH_RATELIMIT"], "auth-ip")
|
||||
client_ip = flask.request.headers["Client-Ip"]
|
||||
if not limiter.test(client_ip):
|
||||
headers = flask.request.headers
|
||||
if headers["Auth-Port"] == '25' and headers['Auth-Method'] == 'plain':
|
||||
response = flask.Response()
|
||||
response.headers['Auth-Status'] = 'Authentication rate limit from one source exceeded'
|
||||
response.headers['Auth-Error-Code'] = '451 4.3.2'
|
||||
response.headers['Auth-Status'] = 'AUTH not supported'
|
||||
response.headers['Auth-Error-Code'] = '502 5.5.1'
|
||||
utils.limiter.rate_limit_ip(client_ip)
|
||||
return response
|
||||
if utils.limiter.should_rate_limit_ip(client_ip):
|
||||
status, code = nginx.get_status(flask.request.headers['Auth-Protocol'], 'ratelimit')
|
||||
response = flask.Response()
|
||||
response.headers['Auth-Status'] = status
|
||||
response.headers['Auth-Error-Code'] = code
|
||||
if int(flask.request.headers['Auth-Login-Attempt']) < 10:
|
||||
response.headers['Auth-Wait'] = '3'
|
||||
return response
|
||||
@ -25,14 +30,27 @@ def nginx_authentication():
|
||||
response = flask.Response()
|
||||
for key, value in headers.items():
|
||||
response.headers[key] = str(value)
|
||||
if ("Auth-Status" not in headers) or (headers["Auth-Status"] != "OK"):
|
||||
limit_subnet = str(app.config["AUTH_RATELIMIT_SUBNET"]) != 'False'
|
||||
subnet = ipaddress.ip_network(app.config["SUBNET"])
|
||||
if limit_subnet or ipaddress.ip_address(client_ip) not in subnet:
|
||||
limiter.hit(flask.request.headers["Client-Ip"])
|
||||
is_valid_user = False
|
||||
if response.headers.get("Auth-User-Exists"):
|
||||
username = response.headers["Auth-User"]
|
||||
if utils.limiter.should_rate_limit_user(username, client_ip):
|
||||
# FIXME could be done before handle_authentication()
|
||||
status, code = nginx.get_status(flask.request.headers['Auth-Protocol'], 'ratelimit')
|
||||
response = flask.Response()
|
||||
response.headers['Auth-Status'] = status
|
||||
response.headers['Auth-Error-Code'] = code
|
||||
if int(flask.request.headers['Auth-Login-Attempt']) < 10:
|
||||
response.headers['Auth-Wait'] = '3'
|
||||
return response
|
||||
is_valid_user = True
|
||||
if headers.get("Auth-Status") == "OK":
|
||||
utils.limiter.exempt_ip_from_ratelimits(client_ip)
|
||||
elif is_valid_user:
|
||||
utils.limiter.rate_limit_user(username, client_ip)
|
||||
else:
|
||||
utils.limiter.rate_limit_ip(client_ip)
|
||||
return response
|
||||
|
||||
|
||||
@internal.route("/auth/admin")
|
||||
def admin_authentication():
|
||||
""" Fails if the user is not an authenticated admin.
|
||||
@ -60,15 +78,29 @@ def user_authentication():
|
||||
def basic_authentication():
|
||||
""" Tries to authenticate using the Authorization header.
|
||||
"""
|
||||
client_ip = flask.request.headers.get('X-Real-IP', flask.request.remote_addr)
|
||||
if utils.limiter.should_rate_limit_ip(client_ip):
|
||||
response = flask.Response(status=401)
|
||||
response.headers["WWW-Authenticate"] = 'Basic realm="Authentication rate limit from one source exceeded"'
|
||||
response.headers['Retry-After'] = '60'
|
||||
return response
|
||||
authorization = flask.request.headers.get("Authorization")
|
||||
if authorization and authorization.startswith("Basic "):
|
||||
encoded = authorization.replace("Basic ", "")
|
||||
user_email, password = base64.b64decode(encoded).split(b":", 1)
|
||||
user = models.User.query.get(user_email.decode("utf8"))
|
||||
if nginx.check_credentials(user, password.decode('utf-8'), flask.request.remote_addr, "web"):
|
||||
user_email = user_email.decode("utf8")
|
||||
if utils.limiter.should_rate_limit_user(user_email, client_ip):
|
||||
response = flask.Response(status=401)
|
||||
response.headers["WWW-Authenticate"] = 'Basic realm="Authentication rate limit for this username exceeded"'
|
||||
response.headers['Retry-After'] = '60'
|
||||
return response
|
||||
user = models.User.query.get(user_email)
|
||||
if user and nginx.check_credentials(user, password.decode('utf-8'), client_ip, "web"):
|
||||
response = flask.Response()
|
||||
response.headers["X-User"] = models.IdnaEmail.process_bind_param(flask_login, user.email, "")
|
||||
utils.limiter.exempt_ip_from_ratelimits(client_ip)
|
||||
return response
|
||||
utils.limiter.rate_limit_user(user_email, client_ip) if user else utils.limiter.rate_limit_ip(client_ip)
|
||||
response = flask.Response(status=401)
|
||||
response.headers["WWW-Authenticate"] = 'Basic realm="Login Required"'
|
||||
return response
|
||||
|
@ -7,6 +7,9 @@ import idna
|
||||
import re
|
||||
import srslib
|
||||
|
||||
@internal.route("/postfix/dane/<domain_name>")
|
||||
def postfix_dane_map(domain_name):
|
||||
return flask.jsonify('dane-only') if utils.has_dane_record(domain_name) else flask.abort(404)
|
||||
|
||||
@internal.route("/postfix/domain/<domain_name>")
|
||||
def postfix_mailbox_domain(domain_name):
|
||||
@ -105,7 +108,7 @@ def postfix_recipient_map(recipient):
|
||||
|
||||
This is meant for bounces to go back to the original sender.
|
||||
"""
|
||||
srs = srslib.SRS(flask.current_app.config["SECRET_KEY"])
|
||||
srs = srslib.SRS(flask.current_app.srs_key)
|
||||
if srslib.SRS.is_srs_address(recipient):
|
||||
try:
|
||||
return flask.jsonify(srs.reverse(recipient))
|
||||
@ -120,7 +123,7 @@ def postfix_sender_map(sender):
|
||||
|
||||
This is for bounces to come back the reverse path properly.
|
||||
"""
|
||||
srs = srslib.SRS(flask.current_app.config["SECRET_KEY"])
|
||||
srs = srslib.SRS(flask.current_app.srs_key)
|
||||
domain = flask.current_app.config["DOMAIN"]
|
||||
try:
|
||||
localpart, domain_name = models.Email.resolve_domain(sender)
|
||||
@ -137,6 +140,7 @@ def postfix_sender_login(sender):
|
||||
localpart, domain_name = models.Email.resolve_domain(sender)
|
||||
if localpart is None:
|
||||
return flask.jsonify(",".join(wildcard_senders)) if wildcard_senders else flask.abort(404)
|
||||
localpart = localpart[:next((i for i, ch in enumerate(localpart) if ch in flask.current_app.config.get('RECIPIENT_DELIMITER')), None)]
|
||||
destination = models.Email.resolve_destination(localpart, domain_name, True)
|
||||
destination = [*destination, *wildcard_senders] if destination else [*wildcard_senders]
|
||||
return flask.jsonify(",".join(destination)) if destination else flask.abort(404)
|
||||
@ -145,6 +149,8 @@ def postfix_sender_login(sender):
|
||||
def postfix_sender_rate(sender):
|
||||
""" Rate limit outbound emails per sender login
|
||||
"""
|
||||
if sender in flask.current_app.config['MESSAGE_RATELIMIT_EXEMPTION']:
|
||||
flask.abort(404)
|
||||
user = models.User.get(sender) or flask.abort(404)
|
||||
return flask.abort(404) if user.sender_limiter.hit() else flask.jsonify("450 4.2.1 You are sending too many emails too fast.")
|
||||
|
||||
|
27
core/admin/mailu/internal/views/rspamd.py
Normal file
@ -0,0 +1,27 @@
|
||||
from mailu import models
|
||||
from mailu.internal import internal
|
||||
|
||||
import flask
|
||||
|
||||
def vault_error(*messages, status=404):
|
||||
return flask.make_response(flask.jsonify({'errors':messages}), status)
|
||||
|
||||
# rspamd key format:
|
||||
# {"selectors":[{"pubkey":"...","domain":"...","valid_start":TS,"valid_end":TS,"key":"...","selector":"...","bits":...,"alg":"..."}]}
|
||||
|
||||
# hashicorp vault answer format:
|
||||
# {"request_id":"...","lease_id":"","renewable":false,"lease_duration":2764800,"data":{...see above...},"wrap_info":null,"warnings":null,"auth":null}
|
||||
|
||||
@internal.route("/rspamd/vault/v1/dkim/<domain_name>", methods=['GET'])
|
||||
def rspamd_dkim_key(domain_name):
|
||||
selectors = []
|
||||
if domain := models.Domain.query.get(domain_name):
|
||||
if key := domain.dkim_key:
|
||||
selectors.append(
|
||||
{
|
||||
'domain' : domain.name,
|
||||
'key' : key.decode('utf8'),
|
||||
'selector': flask.current_app.config.get('DKIM_SELECTOR', 'dkim'),
|
||||
}
|
||||
)
|
||||
return flask.jsonify({'data': {'selectors': selectors}})
|
@ -1,7 +1,12 @@
|
||||
from mailu import utils
|
||||
from flask import current_app as app
|
||||
import base64
|
||||
import limits
|
||||
import limits.storage
|
||||
import limits.strategies
|
||||
|
||||
import hmac
|
||||
import secrets
|
||||
|
||||
class LimitWrapper(object):
|
||||
""" Wraps a limit by providing the storage, item and identifiers
|
||||
@ -31,4 +36,59 @@ class LimitWraperFactory(object):
|
||||
self.limiter = limits.strategies.MovingWindowRateLimiter(self.storage)
|
||||
|
||||
def get_limiter(self, limit, *args):
|
||||
return LimitWrapper(self.limiter, limits.parse(limit), *args)
|
||||
return LimitWrapper(self.limiter, limits.parse(limit), *args)
|
||||
|
||||
def is_subject_to_rate_limits(self, ip):
|
||||
return False if utils.is_exempt_from_ratelimits(ip) else not (self.storage.get(f'exempt-{ip}') > 0)
|
||||
|
||||
def exempt_ip_from_ratelimits(self, ip):
|
||||
self.storage.incr(f'exempt-{ip}', app.config["AUTH_RATELIMIT_EXEMPTION_LENGTH"], True)
|
||||
|
||||
def should_rate_limit_ip(self, ip):
|
||||
limiter = self.get_limiter(app.config["AUTH_RATELIMIT_IP"], 'auth-ip')
|
||||
client_network = utils.extract_network_from_ip(ip)
|
||||
is_rate_limited = self.is_subject_to_rate_limits(ip) and not limiter.test(client_network)
|
||||
if is_rate_limited:
|
||||
app.logger.warn(f'Authentication attempt from {ip} has been rate-limited.')
|
||||
return is_rate_limited
|
||||
|
||||
def rate_limit_ip(self, ip):
|
||||
if ip != app.config['WEBMAIL_ADDRESS']:
|
||||
limiter = self.get_limiter(app.config["AUTH_RATELIMIT_IP"], 'auth-ip')
|
||||
client_network = utils.extract_network_from_ip(ip)
|
||||
if self.is_subject_to_rate_limits(ip):
|
||||
limiter.hit(client_network)
|
||||
|
||||
def should_rate_limit_user(self, username, ip, device_cookie=None, device_cookie_name=None):
|
||||
limiter = self.get_limiter(app.config["AUTH_RATELIMIT_USER"], 'auth-user')
|
||||
is_rate_limited = self.is_subject_to_rate_limits(ip) and not limiter.test(device_cookie if device_cookie_name == username else username)
|
||||
if is_rate_limited:
|
||||
app.logger.warn(f'Authentication attempt from {ip} for {username} has been rate-limited.')
|
||||
return is_rate_limited
|
||||
|
||||
def rate_limit_user(self, username, ip, device_cookie=None, device_cookie_name=None):
|
||||
limiter = self.get_limiter(app.config["AUTH_RATELIMIT_USER"], 'auth-user')
|
||||
if self.is_subject_to_rate_limits(ip):
|
||||
limiter.hit(device_cookie if device_cookie_name == username else username)
|
||||
|
||||
""" Device cookies as described on:
|
||||
https://owasp.org/www-community/Slow_Down_Online_Guessing_Attacks_with_Device_Cookies
|
||||
"""
|
||||
def parse_device_cookie(self, cookie):
|
||||
try:
|
||||
login, nonce, _ = cookie.split('$')
|
||||
if hmac.compare_digest(cookie, self.device_cookie(login, nonce)):
|
||||
return nonce, login
|
||||
except:
|
||||
pass
|
||||
return None, None
|
||||
|
||||
""" Device cookies don't require strong crypto:
|
||||
72bits of nonce, 96bits of signature is more than enough
|
||||
and these values avoid padding in most cases
|
||||
"""
|
||||
def device_cookie(self, username, nonce=None):
|
||||
if not nonce:
|
||||
nonce = secrets.token_urlsafe(9)
|
||||
sig = str(base64.urlsafe_b64encode(hmac.new(app.device_cookie_key, bytearray(f'device_cookie|{username}|{nonce}', 'utf-8'), 'sha256').digest()[20:]), 'utf-8')
|
||||
return f'{username}${nonce}${sig}'
|
||||
|
@ -48,44 +48,44 @@ def advertise():
|
||||
@click.argument('localpart')
|
||||
@click.argument('domain_name')
|
||||
@click.argument('password')
|
||||
@click.option('-m', '--mode')
|
||||
@click.option('-m', '--mode', default='create', metavar='MODE', help='''\b'create' (default): create user. it's an error if user already exists
|
||||
'ifmissing': only update password if user is missing
|
||||
'update': create user or update password if user exists
|
||||
''')
|
||||
@with_appcontext
|
||||
def admin(localpart, domain_name, password, mode='create'):
|
||||
def admin(localpart, domain_name, password, mode):
|
||||
""" Create an admin user
|
||||
'mode' can be:
|
||||
- 'create' (default) Will try to create user and will raise an exception if present
|
||||
- 'ifmissing': if user exists, nothing happens, else it will be created
|
||||
- 'update': user is created or, if it exists, its password gets updated
|
||||
"""
|
||||
|
||||
if not mode in ('create', 'update', 'ifmissing'):
|
||||
raise click.ClickException(f'invalid mode: {mode!r}')
|
||||
|
||||
domain = models.Domain.query.get(domain_name)
|
||||
if not domain:
|
||||
domain = models.Domain(name=domain_name)
|
||||
db.session.add(domain)
|
||||
|
||||
user = None
|
||||
if mode == 'ifmissing' or mode == 'update':
|
||||
email = f'{localpart}@{domain_name}'
|
||||
user = models.User.query.get(email)
|
||||
|
||||
if user and mode == 'ifmissing':
|
||||
print('user %s exists, not updating' % email)
|
||||
email = f'{localpart}@{domain_name}'
|
||||
if user := models.User.query.get(email):
|
||||
if mode == 'ifmissing':
|
||||
print(f'user {email!r} exists, not updating')
|
||||
return
|
||||
|
||||
if not user:
|
||||
elif mode == 'update':
|
||||
user.set_password(password)
|
||||
db.session.commit()
|
||||
print("updated admin password")
|
||||
else:
|
||||
raise click.ClickException(f'user {email!r} exists, not created')
|
||||
else:
|
||||
user = models.User(
|
||||
localpart=localpart,
|
||||
domain=domain,
|
||||
global_admin=True
|
||||
)
|
||||
user.set_password(password)
|
||||
db.session.add(user)
|
||||
user.set_password(password)
|
||||
db.session.commit()
|
||||
print("created admin user")
|
||||
elif mode == 'update':
|
||||
user.set_password(password)
|
||||
db.session.commit()
|
||||
print("updated admin password")
|
||||
|
||||
|
||||
|
||||
@mailu.command()
|
||||
@ -119,7 +119,7 @@ def password(localpart, domain_name, password):
|
||||
""" Change the password of an user
|
||||
"""
|
||||
email = f'{localpart}@{domain_name}'
|
||||
user = models.User.query.get(email)
|
||||
user = models.User.query.get(email)
|
||||
if user:
|
||||
user.set_password(password)
|
||||
else:
|
||||
|
@ -19,7 +19,8 @@ import os
|
||||
import hmac
|
||||
import smtplib
|
||||
import idna
|
||||
import dns
|
||||
import dns.resolver
|
||||
import dns.exception
|
||||
|
||||
from flask import current_app as app
|
||||
from sqlalchemy.ext import declarative
|
||||
@ -38,6 +39,8 @@ class IdnaDomain(db.TypeDecorator):
|
||||
"""
|
||||
|
||||
impl = db.String(80)
|
||||
cache_ok = True
|
||||
python_type = str
|
||||
|
||||
def process_bind_param(self, value, dialect):
|
||||
""" encode unicode domain name to punycode """
|
||||
@ -47,16 +50,18 @@ class IdnaDomain(db.TypeDecorator):
|
||||
""" decode punycode domain name to unicode """
|
||||
return idna.decode(value)
|
||||
|
||||
python_type = str
|
||||
|
||||
class IdnaEmail(db.TypeDecorator):
|
||||
""" Stores a Unicode string in it's IDNA representation (ASCII only)
|
||||
"""
|
||||
|
||||
impl = db.String(255)
|
||||
cache_ok = True
|
||||
python_type = str
|
||||
|
||||
def process_bind_param(self, value, dialect):
|
||||
""" encode unicode domain part of email address to punycode """
|
||||
if not '@' in value:
|
||||
raise ValueError('invalid email address (no "@")')
|
||||
localpart, domain_name = value.lower().rsplit('@', 1)
|
||||
if '@' in localpart:
|
||||
raise ValueError('email local part must not contain "@"')
|
||||
@ -67,13 +72,13 @@ class IdnaEmail(db.TypeDecorator):
|
||||
localpart, domain_name = value.rsplit('@', 1)
|
||||
return f'{localpart}@{idna.decode(domain_name)}'
|
||||
|
||||
python_type = str
|
||||
|
||||
class CommaSeparatedList(db.TypeDecorator):
|
||||
""" Stores a list as a comma-separated string, compatible with Postfix.
|
||||
"""
|
||||
|
||||
impl = db.String
|
||||
cache_ok = True
|
||||
python_type = list
|
||||
|
||||
def process_bind_param(self, value, dialect):
|
||||
""" join list of items to comma separated string """
|
||||
@ -88,13 +93,13 @@ class CommaSeparatedList(db.TypeDecorator):
|
||||
""" split comma separated string to list """
|
||||
return list(filter(bool, (item.strip() for item in value.split(',')))) if value else []
|
||||
|
||||
python_type = list
|
||||
|
||||
class JSONEncoded(db.TypeDecorator):
|
||||
""" Represents an immutable structure as a json-encoded string.
|
||||
"""
|
||||
|
||||
impl = db.String
|
||||
cache_ok = True
|
||||
python_type = str
|
||||
|
||||
def process_bind_param(self, value, dialect):
|
||||
""" encode data as json """
|
||||
@ -104,8 +109,6 @@ class JSONEncoded(db.TypeDecorator):
|
||||
""" decode json to data """
|
||||
return json.loads(value) if value else None
|
||||
|
||||
python_type = str
|
||||
|
||||
class Base(db.Model):
|
||||
""" Base class for all models
|
||||
"""
|
||||
@ -209,16 +212,16 @@ class Domain(Base):
|
||||
os.unlink(file_path)
|
||||
self._dkim_key_on_disk = self._dkim_key
|
||||
|
||||
@property
|
||||
@cached_property
|
||||
def dns_mx(self):
|
||||
""" return MX record for domain """
|
||||
hostname = app.config['HOSTNAMES'].split(',', 1)[0]
|
||||
hostname = app.config['HOSTNAME']
|
||||
return f'{self.name}. 600 IN MX 10 {hostname}.'
|
||||
|
||||
@property
|
||||
@cached_property
|
||||
def dns_spf(self):
|
||||
""" return SPF record for domain """
|
||||
hostname = app.config['HOSTNAMES'].split(',', 1)[0]
|
||||
hostname = app.config['HOSTNAME']
|
||||
return f'{self.name}. 600 IN TXT "v=spf1 mx a:{hostname} ~all"'
|
||||
|
||||
@property
|
||||
@ -226,12 +229,11 @@ class Domain(Base):
|
||||
""" return DKIM record for domain """
|
||||
if self.dkim_key:
|
||||
selector = app.config['DKIM_SELECTOR']
|
||||
return (
|
||||
f'{selector}._domainkey.{self.name}. 600 IN TXT'
|
||||
f'"v=DKIM1; k=rsa; p={self.dkim_publickey}"'
|
||||
)
|
||||
txt = f'v=DKIM1; k=rsa; p={self.dkim_publickey}'
|
||||
record = ' '.join(f'"{txt[p:p+250]}"' for p in range(0, len(txt), 250))
|
||||
return f'{selector}._domainkey.{self.name}. 600 IN TXT {record}'
|
||||
|
||||
@property
|
||||
@cached_property
|
||||
def dns_dmarc(self):
|
||||
""" return DMARC record for domain """
|
||||
if self.dkim_key:
|
||||
@ -242,6 +244,41 @@ class Domain(Base):
|
||||
ruf = f' ruf=mailto:{ruf}@{domain};' if ruf else ''
|
||||
return f'_dmarc.{self.name}. 600 IN TXT "v=DMARC1; p=reject;{rua}{ruf} adkim=s; aspf=s"'
|
||||
|
||||
@cached_property
|
||||
def dns_dmarc_report(self):
|
||||
""" return DMARC report record for mailu server """
|
||||
if self.dkim_key:
|
||||
domain = app.config['DOMAIN']
|
||||
return f'{self.name}._report._dmarc.{domain}. 600 IN TXT "v=DMARC1"'
|
||||
|
||||
@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
|
||||
def dkim_key(self):
|
||||
""" return private DKIM key """
|
||||
@ -533,6 +570,8 @@ class User(Base, Email):
|
||||
""" verifies password against stored hash
|
||||
and updates hash if outdated
|
||||
"""
|
||||
if password == '':
|
||||
return False
|
||||
cache_result = self._credential_cache.get(self.get_id())
|
||||
current_salt = self.password.split('$')[3] if len(self.password.split('$')) == 5 else None
|
||||
if cache_result and current_salt:
|
||||
|
@ -145,6 +145,11 @@ class Logger:
|
||||
if history.has_changes() and history.deleted:
|
||||
before = history.deleted[-1]
|
||||
after = getattr(target, attr.key)
|
||||
# we don't have ordered lists
|
||||
if isinstance(before, list):
|
||||
before = set(before)
|
||||
if isinstance(after, list):
|
||||
after = set(after)
|
||||
# TODO: this can be removed when comment is not nullable in model
|
||||
if attr.key == 'comment' and not before and not after:
|
||||
pass
|
||||
|
5
core/admin/mailu/sso/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
from flask import Blueprint
|
||||
|
||||
sso = Blueprint('sso', __name__, static_folder=None, template_folder='templates')
|
||||
|
||||
from mailu.sso.views import *
|
11
core/admin/mailu/sso/forms.py
Normal file
@ -0,0 +1,11 @@
|
||||
from wtforms import validators, fields
|
||||
from flask_babel import lazy_gettext as _
|
||||
import flask_wtf
|
||||
|
||||
class LoginForm(flask_wtf.FlaskForm):
|
||||
class Meta:
|
||||
csrf = False
|
||||
email = fields.StringField(_('E-mail'), [validators.Email(), validators.DataRequired()])
|
||||
pw = fields.PasswordField(_('Password'), [validators.DataRequired()])
|
||||
submitAdmin = fields.SubmitField(_('Sign in'))
|
||||
submitWebmail = fields.SubmitField(_('Sign in'))
|
86
core/admin/mailu/sso/templates/base_sso.html
Normal file
@ -0,0 +1,86 @@
|
||||
{%- import "macros.html" as macros %}
|
||||
{%- import "bootstrap/utils.html" as utils %}
|
||||
<!doctype html>
|
||||
<html lang="{{ session['language'] }}" data-static="/static/">
|
||||
<head>
|
||||
<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='app.css') }}">
|
||||
</head>
|
||||
<body class="hold-transition sidebar-mini layout-fixed">
|
||||
<div class="wrapper">
|
||||
<nav class="main-header navbar navbar-expand navbar-white navbar-light">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<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>
|
||||
</ul>
|
||||
<ul class="navbar-nav ml-auto">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link" data-toggle="dropdown" href="#" aria-expanded="false">
|
||||
<i class="fas fa-language text-xl" aria-hidden="true" title="{% trans %}change language{% endtrans %}"></i><span class="sr-only">Language</span>
|
||||
<span class="badge badge-primary navbar-badge">{{ session['language'] }}</span></a>
|
||||
<div class="dropdown-menu dropdown-menu-right p-0" id="mailu-languages">
|
||||
{%- for locale in config.translations.values() %}
|
||||
<a class="dropdown-item{% if locale|string() == session['language'] %} active{% endif %}" href="{{ url_for('sso.set_language', language=locale) }}">{{ locale.get_language_name().title() }}</a>
|
||||
{%- endfor %}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<aside class="main-sidebar sidebar-dark-primary nav-compact elevation-4">
|
||||
<a class="brand-link bg-mailu-logo"{% if config["LOGO_BACKGROUND"] %} style="background-color:{{ config["LOGO_BACKGROUND"] }}!important;"{% endif %}>
|
||||
<img src="{{ config["LOGO_URL"] if config["LOGO_URL"] else '/static/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>
|
||||
{%- include "sidebar_sso.html" %}
|
||||
</aside>
|
||||
<div class="content-wrapper text-sm">
|
||||
<section class="content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-6">
|
||||
<h1 class="m-0">{%- block title %}{%- endblock %}</h1>
|
||||
<small>{% block subtitle %}{% endblock %}</small>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
{%- block main_action %}{%- endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="content">
|
||||
{{ utils.flashed_messages(container=False, default_category='success') }}
|
||||
{%- block content %}{%- endblock %}
|
||||
</div>
|
||||
</div>
|
||||
<footer class="main-footer">
|
||||
Built with <i class="fa fa-heart text-danger" aria-hidden="true"></i><span class="sr-only">love</span>
|
||||
using <a href="https://flask.palletsprojects.com/">Flask</a>
|
||||
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>
|
||||
</div>
|
||||
<script src="{{ url_for('static', filename='vendor.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='app.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
11
core/admin/mailu/sso/templates/form_sso.html
Normal file
@ -0,0 +1,11 @@
|
||||
{%- extends "base_sso.html" %}
|
||||
|
||||
{%- block content %}
|
||||
{%- call macros.card() %}
|
||||
<form class="form" method="post" role="form">
|
||||
{{ macros.form_field(form.email) }}
|
||||
{{ macros.form_field(form.pw) }}
|
||||
{{ macros.form_fields(fields, label=False, class="btn btn-default") }}
|
||||
</form>
|
||||
{%- endcall %}
|
||||
{%- endblock %}
|
5
core/admin/mailu/sso/templates/login.html
Normal file
@ -0,0 +1,5 @@
|
||||
{%- extends "form_sso.html" %}
|
||||
|
||||
{%- block title %}
|
||||
{% trans %}Sign in{% endtrans %}
|
||||
{%- endblock %}
|
55
core/admin/mailu/sso/templates/sidebar_sso.html
Normal file
@ -0,0 +1,55 @@
|
||||
<div class="sidebar text-sm">
|
||||
<nav class="mt-2">
|
||||
<ul class="nav nav-pills nav-sidebar flex-column" role="menu">
|
||||
<li class="nav-header text-uppercase text-primary" role="none">{% trans %}Go to{% endtrans %}</li>
|
||||
{%- if config['ADMIN'] %}
|
||||
<li class="nav-item">
|
||||
<a href="{{ url_for('ui.client') }}" class="nav-link">
|
||||
<i class="nav-icon fa fa-laptop"></i>
|
||||
<p class="text">{% 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>
|
||||
<p>{% trans %}Website{% endtrans %} <i class="fas fa-external-link-alt text-xs"></i></p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" role="none">
|
||||
<a href="https://mailu.io" target="_blank" class="nav-link" role="menuitem">
|
||||
<i class="nav-icon fa fa-life-ring"></i>
|
||||
<p class="text">{% trans %}Help{% endtrans %} <i class="fas fa-external-link-alt text-xs"></i></p>
|
||||
</a>
|
||||
</li>
|
||||
{#-
|
||||
Domain self-registration is only available when
|
||||
- Admin is available
|
||||
- Domain Self-registration is enabled
|
||||
- The current user is not logged on
|
||||
#}
|
||||
{%- if config['DOMAIN_REGISTRATION'] and not current_user.is_authenticated and config['ADMIN'] %}
|
||||
<li class="nav-item" role="none">
|
||||
<a href="{{ url_for('ui.domain_signup') }}" class="nav-link" role="menuitem">
|
||||
<i class="nav-icon fa fa-plus-square"></i>
|
||||
<p class="text">{% trans %}Register a domain{% endtrans %}</p>
|
||||
</a>
|
||||
</li>
|
||||
{%- endif %}
|
||||
{#-
|
||||
User self-registration is only available when
|
||||
- Admin is available
|
||||
- Self-registration is enabled
|
||||
- The current user is not logged on
|
||||
#}
|
||||
{%- if not current_user.is_authenticated and signup_domains and config['ADMIN'] %}
|
||||
<li class="nav-item" role="none">
|
||||
<a href="{{ url_for('ui.user_signup') }}" class="nav-link" role="menuitem">
|
||||
<i class="nav-icon fa fa-user-plus"></i>
|
||||
<p class="text">{% trans %}Sign up{% endtrans %}</p>
|
||||
</a>
|
||||
</li>
|
||||
{%- endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
3
core/admin/mailu/sso/views/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
__all__ = [
|
||||
'base', 'languages'
|
||||
]
|
57
core/admin/mailu/sso/views/base.py
Normal file
@ -0,0 +1,57 @@
|
||||
from werkzeug.utils import redirect
|
||||
from mailu import models, utils
|
||||
from mailu.sso import sso, forms
|
||||
from mailu.ui import access
|
||||
|
||||
from flask import current_app as app
|
||||
import flask
|
||||
import flask_login
|
||||
|
||||
@sso.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
client_ip = flask.request.headers.get('X-Real-IP', flask.request.remote_addr)
|
||||
form = forms.LoginForm()
|
||||
form.submitAdmin.label.text = form.submitAdmin.label.text + ' Admin'
|
||||
form.submitWebmail.label.text = form.submitWebmail.label.text + ' Webmail'
|
||||
|
||||
fields = []
|
||||
if str(app.config["ADMIN"]).upper() != "FALSE":
|
||||
fields.append(form.submitAdmin)
|
||||
if str(app.config["WEBMAIL"]).upper() != "NONE":
|
||||
fields.append(form.submitWebmail)
|
||||
fields = [fields]
|
||||
|
||||
if form.validate_on_submit():
|
||||
if form.submitAdmin.data:
|
||||
destination = app.config['WEB_ADMIN']
|
||||
elif form.submitWebmail.data:
|
||||
destination = app.config['WEB_WEBMAIL']
|
||||
device_cookie, device_cookie_username = utils.limiter.parse_device_cookie(flask.request.cookies.get('rate_limit'))
|
||||
username = form.email.data
|
||||
if username != device_cookie_username and utils.limiter.should_rate_limit_ip(client_ip):
|
||||
flask.flash('Too many attempts from your IP (rate-limit)', 'error')
|
||||
return flask.render_template('login.html', form=form)
|
||||
if utils.limiter.should_rate_limit_user(username, client_ip, device_cookie, device_cookie_username):
|
||||
flask.flash('Too many attempts for this user (rate-limit)', 'error')
|
||||
return flask.render_template('login.html', form=form)
|
||||
user = models.User.login(username, form.pw.data)
|
||||
if user:
|
||||
flask.session.regenerate()
|
||||
flask_login.login_user(user)
|
||||
response = flask.redirect(destination)
|
||||
response.set_cookie('rate_limit', utils.limiter.device_cookie(username), max_age=31536000, path=flask.url_for('sso.login'), secure=app.config['SESSION_COOKIE_SECURE'], httponly=True)
|
||||
flask.current_app.logger.info(f'Login succeeded for {username} from {client_ip}.')
|
||||
return response
|
||||
else:
|
||||
utils.limiter.rate_limit_user(username, client_ip, device_cookie, device_cookie_username) if models.User.get(username) else utils.limiter.rate_limit_ip(client_ip)
|
||||
flask.current_app.logger.warn(f'Login failed for {username} from {client_ip}.')
|
||||
flask.flash('Wrong e-mail or password', 'error')
|
||||
return flask.render_template('login.html', form=form, fields=fields)
|
||||
|
||||
@sso.route('/logout', methods=['GET'])
|
||||
@access.authenticated
|
||||
def logout():
|
||||
flask_login.logout_user()
|
||||
flask.session.destroy()
|
||||
return flask.redirect(flask.url_for('.login'))
|
||||
|
7
core/admin/mailu/sso/views/languages.py
Normal file
@ -0,0 +1,7 @@
|
||||
from mailu.sso import sso
|
||||
import flask
|
||||
|
||||
@sso.route('/language/<language>', methods=['POST'])
|
||||
def set_language(language=None):
|
||||
flask.session['language'] = language
|
||||
return flask.Response(status=200)
|
@ -1,188 +1,287 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Mailu\n"
|
||||
"Project-Id-Version: Mailu\n"
|
||||
"POT-Creation-Date: 2021-02-05 16:34+0100\n"
|
||||
"PO-Revision-Date: 2020-02-17 20:23+0000\n"
|
||||
"Last-Translator: NeroPcStation <dareknowacki2001@gmail.com>\n"
|
||||
"Language-Team: Polish <https://translate.tedomum.net/projects/mailu/admin/pl/"
|
||||
">\n"
|
||||
"Last-Translator: Marcin Siennicki <marcin@siennicki.eu>\n"
|
||||
"Language: pl\n"
|
||||
"Language-Team: Polish "
|
||||
"<https://translate.tedomum.net/projects/mailu/admin/pl/>\n"
|
||||
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && "
|
||||
"(n%100<10 || n%100>=20) ? 1 : 2\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
|
||||
"|| n%100>=20) ? 1 : 2;\n"
|
||||
"X-Generator: Weblate 3.3\n"
|
||||
"Generated-By: Babel 2.9.0\n"
|
||||
|
||||
#: mailu/ui/forms.py:32
|
||||
#: mailu/ui/forms.py:33 mailu/ui/forms.py:36
|
||||
msgid "Invalid email address."
|
||||
msgstr "Nieprawidłowy adres e-mail."
|
||||
|
||||
#: mailu/ui/forms.py:36
|
||||
#: mailu/ui/forms.py:45
|
||||
msgid "Confirm"
|
||||
msgstr "Zatwierdź"
|
||||
|
||||
#: mailu/ui/forms.py:40 mailu/ui/forms.py:77
|
||||
#: mailu/ui/forms.py:49 mailu/ui/forms.py:86
|
||||
msgid "E-mail"
|
||||
msgstr "E-mail"
|
||||
|
||||
#: mailu/ui/forms.py:41 mailu/ui/forms.py:78 mailu/ui/forms.py:90
|
||||
#: mailu/ui/forms.py:109 mailu/ui/forms.py:162
|
||||
#: mailu/ui/forms.py:50 mailu/ui/forms.py:87 mailu/ui/forms.py:100
|
||||
#: mailu/ui/forms.py:118 mailu/ui/forms.py:172
|
||||
#: mailu/ui/templates/client.html:32 mailu/ui/templates/client.html:59
|
||||
msgid "Password"
|
||||
msgstr "Hasło"
|
||||
|
||||
#: mailu/ui/forms.py:42 mailu/ui/templates/login.html:4
|
||||
#: mailu/ui/templates/sidebar.html:111
|
||||
#: mailu/ui/forms.py:51 mailu/ui/templates/login.html:4
|
||||
#: mailu/ui/templates/sidebar.html:108
|
||||
msgid "Sign in"
|
||||
msgstr "Zaloguj"
|
||||
|
||||
#: mailu/ui/forms.py:46 mailu/ui/forms.py:56
|
||||
#: mailu/ui/forms.py:55 mailu/ui/forms.py:65
|
||||
#: mailu/ui/templates/domain/details.html:27
|
||||
#: mailu/ui/templates/domain/list.html:18 mailu/ui/templates/relay/list.html:17
|
||||
msgid "Domain name"
|
||||
msgstr "Nazwa domeny"
|
||||
|
||||
#: mailu/ui/forms.py:47
|
||||
#: mailu/ui/forms.py:56
|
||||
msgid "Maximum user count"
|
||||
msgstr "Maksymalna liczba użytkowników"
|
||||
|
||||
#: mailu/ui/forms.py:48
|
||||
#: mailu/ui/forms.py:57
|
||||
msgid "Maximum alias count"
|
||||
msgstr "Maksymalna liczba aliasów"
|
||||
|
||||
#. Needs more context - is that a verb or a noun?
|
||||
#: mailu/ui/forms.py:51 mailu/ui/forms.py:72 mailu/ui/forms.py:83
|
||||
#: mailu/ui/forms.py:128 mailu/ui/forms.py:140
|
||||
#: mailu/ui/forms.py:58
|
||||
msgid "Maximum user quota"
|
||||
msgstr "Maksymalny przydział użytkownika"
|
||||
|
||||
#: mailu/ui/forms.py:59
|
||||
msgid "Enable sign-up"
|
||||
msgstr "Włącz rejestrację"
|
||||
|
||||
#: mailu/ui/forms.py:60 mailu/ui/forms.py:81 mailu/ui/forms.py:93
|
||||
#: mailu/ui/forms.py:138 mailu/ui/forms.py:150
|
||||
#: mailu/ui/templates/alias/list.html:21 mailu/ui/templates/domain/list.html:21
|
||||
#: mailu/ui/templates/relay/list.html:19 mailu/ui/templates/token/list.html:19
|
||||
#: mailu/ui/templates/user/list.html:23
|
||||
msgid "Comment"
|
||||
msgstr "Komentarz"
|
||||
|
||||
#: mailu/ui/forms.py:52 mailu/ui/forms.py:61 mailu/ui/forms.py:66
|
||||
#: mailu/ui/forms.py:73 mailu/ui/forms.py:132 mailu/ui/forms.py:141
|
||||
msgid "Create"
|
||||
msgstr "Utwórz"
|
||||
#: mailu/ui/forms.py:61 mailu/ui/forms.py:75 mailu/ui/forms.py:82
|
||||
#: mailu/ui/forms.py:95 mailu/ui/forms.py:142 mailu/ui/forms.py:151
|
||||
msgid "Save"
|
||||
msgstr "Zapisz"
|
||||
|
||||
#: mailu/ui/forms.py:59 mailu/ui/forms.py:79 mailu/ui/forms.py:91
|
||||
#: mailu/ui/forms.py:66
|
||||
msgid "Initial admin"
|
||||
msgstr "Początkowy administrator"
|
||||
|
||||
#: mailu/ui/forms.py:67
|
||||
msgid "Admin password"
|
||||
msgstr "Hasło administratora"
|
||||
|
||||
#: mailu/ui/forms.py:68 mailu/ui/forms.py:88 mailu/ui/forms.py:101
|
||||
msgid "Confirm password"
|
||||
msgstr "Potwierdź hasło"
|
||||
|
||||
#: mailu/ui/forms.py:80 mailu/ui/templates/user/list.html:22
|
||||
#: mailu/ui/forms.py:70
|
||||
msgid "Create"
|
||||
msgstr "Utwórz"
|
||||
|
||||
#: mailu/ui/forms.py:74
|
||||
msgid "Alternative name"
|
||||
msgstr "Alternatywna nazwa"
|
||||
|
||||
#: mailu/ui/forms.py:79
|
||||
msgid "Relayed domain name"
|
||||
msgstr "Domeny przekierowywane"
|
||||
|
||||
#: mailu/ui/forms.py:80 mailu/ui/templates/relay/list.html:18
|
||||
msgid "Remote host"
|
||||
msgstr "Zdalny host"
|
||||
|
||||
#: mailu/ui/forms.py:89 mailu/ui/templates/user/list.html:22
|
||||
#: mailu/ui/templates/user/signup_domain.html:16
|
||||
msgid "Quota"
|
||||
msgstr "Maksymalna przestrzeń na dysku"
|
||||
|
||||
#: mailu/ui/forms.py:81
|
||||
#: mailu/ui/forms.py:90
|
||||
msgid "Allow IMAP access"
|
||||
msgstr "Zezwalaj na dostęp przez protokół IMAP"
|
||||
|
||||
#: mailu/ui/forms.py:82
|
||||
#: mailu/ui/forms.py:91
|
||||
msgid "Allow POP3 access"
|
||||
msgstr "Zezwalaj na dostęp przez protokół POP3"
|
||||
|
||||
#: mailu/ui/forms.py:85
|
||||
msgid "Save"
|
||||
msgstr "Zapisz"
|
||||
|
||||
#: mailu/ui/forms.py:97
|
||||
#: mailu/ui/forms.py:92 mailu/ui/forms.py:108
|
||||
#: mailu/ui/templates/user/settings.html:15
|
||||
msgid "Displayed name"
|
||||
msgstr "Nazwa wyświetlana"
|
||||
|
||||
#: mailu/ui/forms.py:98
|
||||
#: mailu/ui/forms.py:94
|
||||
msgid "Enabled"
|
||||
msgstr "Włączone"
|
||||
|
||||
#: mailu/ui/forms.py:99
|
||||
msgid "Email address"
|
||||
msgstr "Adres e-mail"
|
||||
|
||||
#: mailu/ui/forms.py:102 mailu/ui/templates/sidebar.html:114
|
||||
#: mailu/ui/templates/user/signup.html:4
|
||||
#: mailu/ui/templates/user/signup_domain.html:4
|
||||
msgid "Sign up"
|
||||
msgstr "Utwórz konto"
|
||||
|
||||
#: mailu/ui/forms.py:109
|
||||
msgid "Enable spam filter"
|
||||
msgstr "Włącz filtr antyspamowy"
|
||||
|
||||
#: mailu/ui/forms.py:80
|
||||
msgid "Spam filter threshold"
|
||||
msgstr "Próg filtra antyspamowego"
|
||||
|
||||
#: mailu/ui/forms.py:105
|
||||
msgid "Save settings"
|
||||
msgstr "Zapisz ustawienia"
|
||||
|
||||
#: mailu/ui/forms.py:110
|
||||
msgid "Password check"
|
||||
msgstr ""
|
||||
msgid "Spam filter tolerance"
|
||||
msgstr "Tolerancja filtra spamu"
|
||||
|
||||
#: mailu/ui/forms.py:111 mailu/ui/templates/sidebar.html:16
|
||||
msgid "Update password"
|
||||
msgstr "Zaktualizuj hasło"
|
||||
|
||||
#: mailu/ui/forms.py:100
|
||||
#: mailu/ui/forms.py:111
|
||||
msgid "Enable forwarding"
|
||||
msgstr "Włącz przekierowanie poczty"
|
||||
|
||||
#: mailu/ui/forms.py:103 mailu/ui/forms.py:139
|
||||
#: mailu/ui/forms.py:112
|
||||
msgid "Keep a copy of the emails"
|
||||
msgstr "Przechowuj kopię wiadomości"
|
||||
|
||||
#: mailu/ui/forms.py:113 mailu/ui/forms.py:149
|
||||
#: mailu/ui/templates/alias/list.html:20
|
||||
msgid "Destination"
|
||||
msgstr "Adres docelowy"
|
||||
|
||||
#: mailu/ui/forms.py:120
|
||||
msgid "Update"
|
||||
msgstr "Aktualizuj"
|
||||
#: mailu/ui/forms.py:114
|
||||
msgid "Save settings"
|
||||
msgstr "Zapisz ustawienia"
|
||||
|
||||
#: mailu/ui/forms.py:115
|
||||
#: mailu/ui/forms.py:119
|
||||
msgid "Password check"
|
||||
msgstr "Powtórz hasło"
|
||||
|
||||
#: mailu/ui/forms.py:120 mailu/ui/templates/sidebar.html:16
|
||||
msgid "Update password"
|
||||
msgstr "Zaktualizuj hasło"
|
||||
|
||||
#: mailu/ui/forms.py:124
|
||||
msgid "Enable automatic reply"
|
||||
msgstr "Włącz automatyczną odpowiedź"
|
||||
|
||||
#: mailu/ui/forms.py:116
|
||||
#: mailu/ui/forms.py:125
|
||||
msgid "Reply subject"
|
||||
msgstr "Temat odpowiedzi"
|
||||
|
||||
#: mailu/ui/forms.py:117
|
||||
#: mailu/ui/forms.py:126
|
||||
msgid "Reply body"
|
||||
msgstr "Treść odpowiedzi"
|
||||
|
||||
#: mailu/ui/forms.py:136
|
||||
#: mailu/ui/forms.py:128
|
||||
#, fuzzy
|
||||
msgid "Start of vacation"
|
||||
msgstr "Rozpoczęcie nieobecności"
|
||||
|
||||
#: mailu/ui/forms.py:129
|
||||
msgid "End of vacation"
|
||||
msgstr "Koniec nieobecności"
|
||||
|
||||
#: mailu/ui/forms.py:130
|
||||
msgid "Update"
|
||||
msgstr "Aktualizuj"
|
||||
|
||||
#: mailu/ui/forms.py:135
|
||||
msgid "Your token (write it down, as it will never be displayed again)"
|
||||
msgstr "Twój token (zapisz go, ponieważ nigdy więcej nie będzie wyświetlany)"
|
||||
|
||||
#: mailu/ui/forms.py:140 mailu/ui/templates/token/list.html:20
|
||||
msgid "Authorized IP"
|
||||
msgstr "Autoryzowany adres IP"
|
||||
|
||||
#: mailu/ui/forms.py:146
|
||||
msgid "Alias"
|
||||
msgstr "Alias"
|
||||
|
||||
#: mailu/ui/forms.py:138
|
||||
#: mailu/ui/forms.py:148
|
||||
msgid "Use SQL LIKE Syntax (e.g. for catch-all aliases)"
|
||||
msgstr "Używaj składni SQL LIKE (np. do adresów catch-all)"
|
||||
|
||||
#: mailu/ui/forms.py:145
|
||||
#: mailu/ui/forms.py:155
|
||||
msgid "Admin email"
|
||||
msgstr "E-mail administratora"
|
||||
|
||||
#: mailu/ui/forms.py:146 mailu/ui/forms.py:151 mailu/ui/forms.py:164
|
||||
#: mailu/ui/forms.py:156 mailu/ui/forms.py:161 mailu/ui/forms.py:174
|
||||
msgid "Submit"
|
||||
msgstr "Prześlij"
|
||||
|
||||
#: mailu/ui/forms.py:150
|
||||
#: mailu/ui/forms.py:160
|
||||
msgid "Manager email"
|
||||
msgstr "E-mail menedżera"
|
||||
|
||||
#: mailu/ui/forms.py:155
|
||||
#: mailu/ui/forms.py:165
|
||||
msgid "Protocol"
|
||||
msgstr "Protokół"
|
||||
|
||||
#: mailu/ui/forms.py:158
|
||||
#: mailu/ui/forms.py:168
|
||||
msgid "Hostname or IP"
|
||||
msgstr "Nazwa hosta lub adres IP"
|
||||
|
||||
#: mailu/ui/forms.py:159 mailu/ui/templates/client.html:20
|
||||
#: mailu/ui/forms.py:169 mailu/ui/templates/client.html:20
|
||||
#: mailu/ui/templates/client.html:47
|
||||
msgid "TCP port"
|
||||
msgstr "Port TCP"
|
||||
|
||||
#: mailu/ui/forms.py:160
|
||||
#: mailu/ui/forms.py:170
|
||||
msgid "Enable TLS"
|
||||
msgstr "Włącz TLS"
|
||||
|
||||
#: mailu/ui/forms.py:161 mailu/ui/templates/client.html:28
|
||||
#: mailu/ui/forms.py:171 mailu/ui/templates/client.html:28
|
||||
#: mailu/ui/templates/client.html:55 mailu/ui/templates/fetch/list.html:20
|
||||
msgid "Username"
|
||||
msgstr "Nazwa użytkownika"
|
||||
|
||||
#: mailu/ui/forms.py:173
|
||||
msgid "Keep emails on the server"
|
||||
msgstr "Przechowuj wiadomości na serwerze"
|
||||
|
||||
#: mailu/ui/forms.py:178
|
||||
msgid "Announcement subject"
|
||||
msgstr "Temat ogłoszenia"
|
||||
|
||||
#: mailu/ui/forms.py:180
|
||||
msgid "Announcement body"
|
||||
msgstr "Treść ogłoszenia"
|
||||
|
||||
#: mailu/ui/forms.py:182
|
||||
msgid "Send"
|
||||
msgstr "Wyślij"
|
||||
|
||||
#: mailu/ui/templates/announcement.html:4
|
||||
msgid "Public announcement"
|
||||
msgstr "Publiczne ogłoszenie"
|
||||
|
||||
#: mailu/ui/templates/client.html:4 mailu/ui/templates/sidebar.html:79
|
||||
msgid "Client setup"
|
||||
msgstr "Konfiguracja klienta"
|
||||
|
||||
#: mailu/ui/templates/client.html:16 mailu/ui/templates/client.html:43
|
||||
msgid "Mail protocol"
|
||||
msgstr "Protokół poczty"
|
||||
|
||||
#: mailu/ui/templates/client.html:24 mailu/ui/templates/client.html:51
|
||||
msgid "Server name"
|
||||
msgstr "Nazwa serwera"
|
||||
|
||||
#: mailu/ui/templates/confirm.html:4
|
||||
msgid "Confirm action"
|
||||
msgstr "Potwierdź wykonanie czynności"
|
||||
|
||||
#: mailu/ui/templates/confirm.html:13
|
||||
#, python-format
|
||||
msgid "You are about to %(action)s. Please confirm your action."
|
||||
msgstr "Zamierzasz wykonać następujące czynności: %(action)s. Potwierdź wykonanie czynności."
|
||||
msgstr ""
|
||||
"Zamierzasz wykonać następujące czynności: %(action)s. Potwierdź wykonanie"
|
||||
" czynności."
|
||||
|
||||
#: mailu/ui/templates/docker-error.html:4
|
||||
msgid "Docker error"
|
||||
@ -192,54 +291,19 @@ msgstr "Błąd Dockera"
|
||||
msgid "An error occurred while talking to the Docker server."
|
||||
msgstr "Wystąpił błąd komunikacji z serwerem Dockera."
|
||||
|
||||
#: mailu/admin/templates/login.html:6
|
||||
msgid "Your account"
|
||||
msgstr "Twoje konto"
|
||||
|
||||
#: mailu/ui/templates/login.html:8
|
||||
msgid "to access the administration tools"
|
||||
msgstr "aby uzyskać dostęp do narzędzi administracyjnych"
|
||||
|
||||
#: mailu/ui/templates/services.html:4 mailu/ui/templates/sidebar.html:39
|
||||
msgid "Services status"
|
||||
msgstr "Status usług"
|
||||
|
||||
#: mailu/ui/templates/services.html:10
|
||||
msgid "Service"
|
||||
msgstr "Usługa"
|
||||
|
||||
#: mailu/ui/templates/fetch/list.html:23 mailu/ui/templates/services.html:11
|
||||
msgid "Status"
|
||||
msgstr "Status"
|
||||
|
||||
#: mailu/ui/templates/services.html:12
|
||||
msgid "PID"
|
||||
msgstr "PID"
|
||||
|
||||
#: mailu/ui/templates/services.html:13
|
||||
msgid "Image"
|
||||
msgstr "Obraz"
|
||||
|
||||
#: mailu/ui/templates/services.html:14
|
||||
msgid "Started"
|
||||
msgstr ""
|
||||
|
||||
#: mailu/ui/templates/services.html:15
|
||||
msgid "Last update"
|
||||
msgstr "Ostatnia aktualizacja"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:8
|
||||
#, fuzzy
|
||||
msgid "My account"
|
||||
msgstr "Moje konto"
|
||||
msgstr "Dodaj konto"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:11 mailu/ui/templates/user/list.html:34
|
||||
msgid "Settings"
|
||||
msgstr "Ustawienia"
|
||||
|
||||
#: mailu/ui/templates/user/settings.html:22
|
||||
msgid "Auto-forward"
|
||||
msgstr "Automatyczne przekierowanie"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:21 mailu/ui/templates/user/list.html:35
|
||||
msgid "Auto-reply"
|
||||
msgstr "Automatyczna odpowiedź"
|
||||
@ -247,28 +311,60 @@ msgstr "Automatyczna odpowiedź"
|
||||
#: mailu/ui/templates/fetch/list.html:4 mailu/ui/templates/sidebar.html:26
|
||||
#: mailu/ui/templates/user/list.html:36
|
||||
msgid "Fetched accounts"
|
||||
msgstr ""
|
||||
msgstr "Zewnętrzne konta e-mail"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:105
|
||||
msgid "Sign out"
|
||||
msgstr "Wyloguj"
|
||||
#: mailu/ui/templates/sidebar.html:31 mailu/ui/templates/token/list.html:4
|
||||
msgid "Authentication tokens"
|
||||
msgstr "Tokeny uwierzytelnienia"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:35
|
||||
#: mailu/ui/templates/sidebar.html:36
|
||||
msgid "Administration"
|
||||
msgstr "Administracja"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:49
|
||||
#: mailu/ui/templates/sidebar.html:41
|
||||
msgid "Announcement"
|
||||
msgstr "Ogłoszenie"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:46
|
||||
msgid "Administrators"
|
||||
msgstr "Administratorzy"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:66
|
||||
#: mailu/ui/templates/sidebar.html:51
|
||||
msgid "Relayed domains"
|
||||
msgstr "Domeny przekierowywane"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:56 mailu/ui/templates/user/settings.html:19
|
||||
msgid "Antispam"
|
||||
msgstr "Filtr antyspamowy"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:63
|
||||
msgid "Mail domains"
|
||||
msgstr "Domeny pocztowe"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:92
|
||||
#: mailu/ui/templates/sidebar.html:69
|
||||
msgid "Go to"
|
||||
msgstr "Przejdź do"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:73
|
||||
msgid "Webmail"
|
||||
msgstr "Twoja poczta"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:84
|
||||
msgid "Website"
|
||||
msgstr "Strona internetowa"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:89
|
||||
msgid "Help"
|
||||
msgstr "Pomoc"
|
||||
|
||||
#: mailu/ui/templates/domain/signup.html:4 mailu/ui/templates/sidebar.html:95
|
||||
msgid "Register a domain"
|
||||
msgstr "Zarejestruj domenę"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:102
|
||||
msgid "Sign out"
|
||||
msgstr "Wyloguj"
|
||||
|
||||
#: mailu/ui/templates/working.html:4
|
||||
msgid "We are still working on this feature!"
|
||||
msgstr "Nadal pracujemy nad tą funkcją!"
|
||||
@ -344,6 +440,22 @@ msgstr "Ostatnia edycja"
|
||||
msgid "Edit"
|
||||
msgstr "Edytuj"
|
||||
|
||||
#: mailu/ui/templates/alternative/create.html:4
|
||||
msgid "Create alternative domain"
|
||||
msgstr "Utwórz alternatywną domenę"
|
||||
|
||||
#: mailu/ui/templates/alternative/list.html:4
|
||||
msgid "Alternative domain list"
|
||||
msgstr "Alternatywna lista domen"
|
||||
|
||||
#: mailu/ui/templates/alternative/list.html:12
|
||||
msgid "Add alternative"
|
||||
msgstr "Dodaj alternatywę"
|
||||
|
||||
#: mailu/ui/templates/alternative/list.html:19
|
||||
msgid "Name"
|
||||
msgstr "Nazwa"
|
||||
|
||||
#: mailu/ui/templates/domain/create.html:4
|
||||
#: mailu/ui/templates/domain/list.html:9
|
||||
msgid "New domain"
|
||||
@ -357,6 +469,10 @@ msgstr "Szczegóły domeny"
|
||||
msgid "Regenerate keys"
|
||||
msgstr "Wygeneruj ponownie klucze"
|
||||
|
||||
#: mailu/ui/templates/domain/details.html:17
|
||||
msgid "Generate keys"
|
||||
msgstr "Wygeneruj klucze"
|
||||
|
||||
#: mailu/ui/templates/domain/details.html:31
|
||||
msgid "DNS MX entry"
|
||||
msgstr "Wpis MX DNS"
|
||||
@ -365,15 +481,15 @@ msgstr "Wpis MX DNS"
|
||||
msgid "DNS SPF entries"
|
||||
msgstr "Wpisy SPF DNS"
|
||||
|
||||
#: mailu/ui/templates/domain/details.html:42
|
||||
#: mailu/ui/templates/domain/details.html:41
|
||||
msgid "DKIM public key"
|
||||
msgstr "Publiczny klucz DKIM"
|
||||
|
||||
#: mailu/ui/templates/domain/details.html:46
|
||||
#: mailu/ui/templates/domain/details.html:45
|
||||
msgid "DNS DKIM entry"
|
||||
msgstr "Wpis DKIM DNS"
|
||||
|
||||
#: mailu/ui/templates/domain/details.html:50
|
||||
#: mailu/ui/templates/domain/details.html:49
|
||||
msgid "DNS DMARC entry"
|
||||
msgstr "Wpis DMARC DNS"
|
||||
|
||||
@ -413,13 +529,42 @@ msgstr "Aliasy"
|
||||
msgid "Managers"
|
||||
msgstr "Menedżerowie"
|
||||
|
||||
#: mailu/ui/templates/domain/list.html:39
|
||||
msgid "Alternatives"
|
||||
msgstr "Alternatywy"
|
||||
|
||||
#: mailu/ui/templates/domain/signup.html:13
|
||||
msgid ""
|
||||
"In order to register a new domain, you must first setup the\n"
|
||||
" domain zone so that the domain <code>MX</code> points to this server"
|
||||
msgstr ""
|
||||
"Aby zarejestrować nową domenę, musisz najpierw skonfigurować strefę "
|
||||
"domeny, aby domena <code> MX </code> wskazywała na ten serwer"
|
||||
|
||||
#: mailu/ui/templates/domain/signup.html:18
|
||||
msgid ""
|
||||
"If you do not know how to setup an <code>MX</code> record for your DNS "
|
||||
"zone,\n"
|
||||
" please contact your DNS provider or administrator. Also, please wait "
|
||||
"a\n"
|
||||
" couple minutes after the <code>MX</code> is set so the local server "
|
||||
"cache\n"
|
||||
" expires."
|
||||
msgstr ""
|
||||
"Jeśli nie wiesz, jak skonfigurować rekord <code> MX </code> dla swojej "
|
||||
"strefy DNS,\n"
|
||||
"skontaktuj się z dostawcą DNS lub administratorem. Proszę również "
|
||||
"poczekać\n"
|
||||
"kilka minut po ustawieniu <code> MX </code>, żeby pamięć podręczna "
|
||||
"serwera lokalnego wygasła."
|
||||
|
||||
#: mailu/ui/templates/fetch/create.html:4
|
||||
msgid "Add a fetched account"
|
||||
msgstr ""
|
||||
msgstr "Dodaj zewnętrzne konto pocztowe"
|
||||
|
||||
#: mailu/ui/templates/fetch/edit.html:4
|
||||
msgid "Update a fetched account"
|
||||
msgstr ""
|
||||
msgstr "Zaktualizuj konto"
|
||||
|
||||
#: mailu/ui/templates/fetch/list.html:12
|
||||
msgid "Add an account"
|
||||
@ -427,12 +572,28 @@ msgstr "Dodaj konto"
|
||||
|
||||
#: mailu/ui/templates/fetch/list.html:19
|
||||
msgid "Endpoint"
|
||||
msgstr ""
|
||||
msgstr "Serwer"
|
||||
|
||||
#: mailu/ui/templates/fetch/list.html:21
|
||||
msgid "Keep emails"
|
||||
msgstr "Przechowuj wiadomości"
|
||||
|
||||
#: mailu/ui/templates/fetch/list.html:22
|
||||
msgid "Last check"
|
||||
msgstr "Ostatnie sprawdzenie"
|
||||
|
||||
#: mailu/ui/templates/fetch/list.html:23
|
||||
msgid "Status"
|
||||
msgstr "Stan"
|
||||
|
||||
#: mailu/ui/templates/fetch/list.html:35
|
||||
msgid "yes"
|
||||
msgstr "Tak"
|
||||
|
||||
#: mailu/ui/templates/fetch/list.html:35
|
||||
msgid "no"
|
||||
msgstr "Nie"
|
||||
|
||||
#: mailu/ui/templates/manager/create.html:4
|
||||
msgid "Add a manager"
|
||||
msgstr "Dodaj menedżera"
|
||||
@ -445,34 +606,43 @@ msgstr "Lista menedżerów"
|
||||
msgid "Add manager"
|
||||
msgstr "Dodaj menedżera"
|
||||
|
||||
#: mailu/ui/forms.py:168
|
||||
msgid "Announcement subject"
|
||||
msgstr "Temat ogłoszenia"
|
||||
#: mailu/ui/templates/relay/create.html:4
|
||||
msgid "New relay domain"
|
||||
msgstr "Nowa domena do przekierowania"
|
||||
|
||||
#: mailu/ui/forms.py:170
|
||||
msgid "Announcement body"
|
||||
msgstr "Treść ogłoszenia"
|
||||
#: mailu/ui/templates/relay/edit.html:4
|
||||
#, fuzzy
|
||||
msgid "Edit relayd domain"
|
||||
msgstr "Edycja domeny"
|
||||
|
||||
#: mailu/ui/forms.py:172
|
||||
msgid "Send"
|
||||
msgstr "Wyślij"
|
||||
#: mailu/ui/templates/relay/list.html:4
|
||||
msgid "Relayed domain list"
|
||||
msgstr "Lista domen przekierowywanych"
|
||||
|
||||
#: mailu/ui/templates/announcement.html:4
|
||||
msgid "Public announcement"
|
||||
msgstr "Publiczne ogłoszenie"
|
||||
#: mailu/ui/templates/relay/list.html:9
|
||||
msgid "New relayed domain"
|
||||
msgstr "Nowa domena do przekierowania"
|
||||
|
||||
#: mailu/ui/templates/announcement.html:8
|
||||
msgid "from"
|
||||
msgstr "od"
|
||||
#: mailu/ui/templates/token/create.html:4
|
||||
msgid "Create an authentication token"
|
||||
msgstr "Utwórz token uwierzytelniający"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:44
|
||||
msgid "Announcement"
|
||||
msgstr "Ogłoszenie"
|
||||
#: mailu/ui/templates/token/list.html:12
|
||||
msgid "New token"
|
||||
msgstr "Nowy token"
|
||||
|
||||
#: mailu/ui/templates/user/create.html:4
|
||||
msgid "New user"
|
||||
msgstr "Nowy użytkownik"
|
||||
|
||||
#: mailu/ui/templates/user/create.html:15
|
||||
msgid "General"
|
||||
msgstr "Ogólne"
|
||||
|
||||
#: mailu/ui/templates/user/create.html:23
|
||||
msgid "Features and quotas"
|
||||
msgstr "Funkcje i limity"
|
||||
|
||||
#: mailu/ui/templates/user/edit.html:4
|
||||
msgid "Edit user"
|
||||
msgstr "Edytuj użytkownika"
|
||||
@ -505,202 +675,9 @@ msgstr "Zmiana hasła"
|
||||
msgid "Automatic reply"
|
||||
msgstr "Automatyczna odpowiedź"
|
||||
|
||||
#: mailu/ui/forms.py:49
|
||||
msgid "Maximum user quota"
|
||||
msgstr "Maksymalny przydział użytkownika"
|
||||
|
||||
#: mailu/ui/forms.py:101
|
||||
msgid "Keep a copy of the emails"
|
||||
msgstr "Przechowuj kopię wiadomości"
|
||||
|
||||
#: mailu/ui/forms.py:163
|
||||
msgid "Keep emails on the server"
|
||||
msgstr "Przechowuj wiadomości na serwerze"
|
||||
|
||||
#: mailu/ui/templates/fetch/list.html:21
|
||||
msgid "Keep emails"
|
||||
msgstr "Przechowuj wiadomości"
|
||||
|
||||
#: mailu/ui/templates/fetch/list.html:35
|
||||
msgid "yes"
|
||||
msgstr "Tak"
|
||||
|
||||
#: mailu/ui/templates/fetch/list.html:35
|
||||
msgid "no"
|
||||
msgstr "Nie"
|
||||
|
||||
#: mailu/ui/forms.py:65
|
||||
msgid "Alternative name"
|
||||
msgstr "Alternatywna nazwa"
|
||||
|
||||
#: mailu/ui/forms.py:70
|
||||
msgid "Relayed domain name"
|
||||
msgstr ""
|
||||
|
||||
#: mailu/ui/forms.py:71 mailu/ui/templates/relay/list.html:18
|
||||
msgid "Remote host"
|
||||
msgstr "Zdalny host"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:54
|
||||
msgid "Relayed domains"
|
||||
msgstr ""
|
||||
|
||||
#: mailu/ui/templates/alternative/create.html:4
|
||||
msgid "Create alternative domain"
|
||||
msgstr "Utwórz alternatywną domenę"
|
||||
|
||||
#: mailu/ui/templates/alternative/list.html:4
|
||||
msgid "Alternative domain list"
|
||||
msgstr "Alternatywna lista domen"
|
||||
|
||||
#: mailu/ui/templates/alternative/list.html:12
|
||||
msgid "Add alternative"
|
||||
msgstr "Dodaj alternatywę"
|
||||
|
||||
#: mailu/ui/templates/alternative/list.html:19
|
||||
msgid "Name"
|
||||
msgstr "Nazwa"
|
||||
|
||||
#: mailu/ui/templates/domain/list.html:39
|
||||
msgid "Alternatives"
|
||||
msgstr "Alternatywy"
|
||||
|
||||
#: mailu/ui/templates/relay/create.html:4
|
||||
msgid "New relay domain"
|
||||
msgstr ""
|
||||
|
||||
#: mailu/ui/templates/relay/edit.html:4
|
||||
msgid "Edit relayd domain"
|
||||
msgstr ""
|
||||
|
||||
#: mailu/ui/templates/relay/list.html:4
|
||||
msgid "Relayed domain list"
|
||||
msgstr ""
|
||||
|
||||
#: mailu/ui/templates/relay/list.html:9
|
||||
msgid "New relayed domain"
|
||||
msgstr ""
|
||||
|
||||
#: mailu/ui/forms.py:125
|
||||
msgid "Your token (write it down, as it will never be displayed again)"
|
||||
msgstr "Twój token (zapisz go, ponieważ nigdy więcej nie będzie wyświetlany)"
|
||||
|
||||
#: mailu/ui/forms.py:130 mailu/ui/templates/token/list.html:20
|
||||
msgid "Authorized IP"
|
||||
msgstr "Autoryzowany adres IP"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:31 mailu/ui/templates/token/list.html:4
|
||||
msgid "Authentication tokens"
|
||||
msgstr "Tokeny uwierzytelnienia"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:72
|
||||
msgid "Go to"
|
||||
msgstr "Przejdź do"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:76
|
||||
msgid "Webmail"
|
||||
msgstr ""
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:87
|
||||
msgid "Website"
|
||||
msgstr "Strona internetowa"
|
||||
|
||||
#: mailu/ui/templates/token/create.html:4
|
||||
msgid "Create an authentication token"
|
||||
msgstr "Utwórz token uwierzytelniający"
|
||||
|
||||
#: mailu/ui/templates/token/list.html:12
|
||||
msgid "New token"
|
||||
msgstr "Nowy token"
|
||||
|
||||
#: mailu/ui/templates/user/create.html:15
|
||||
msgid "General"
|
||||
msgstr ""
|
||||
|
||||
#: mailu/ui/templates/user/create.html:22
|
||||
msgid "Features and quotas"
|
||||
msgstr ""
|
||||
|
||||
#: mailu/ui/templates/user/settings.html:14
|
||||
msgid "General settings"
|
||||
msgstr "Ustawienia ogólne"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:59 mailu/ui/templates/user/settings.html:15
|
||||
msgid "Antispam"
|
||||
msgstr "Filtr antyspamowy"
|
||||
|
||||
#: mailu/ui/forms.py:99
|
||||
msgid "Spam filter tolerance"
|
||||
msgstr "Tolerancja filtra spamu"
|
||||
|
||||
#: mailu/ui/forms.py:50
|
||||
msgid "Enable sign-up"
|
||||
msgstr "Włącz rejestrację"
|
||||
|
||||
#: mailu/ui/forms.py:57
|
||||
msgid "Initial admin"
|
||||
msgstr "Początkowy administrator"
|
||||
|
||||
#: mailu/ui/forms.py:58
|
||||
msgid "Admin password"
|
||||
msgstr "hasło administratora"
|
||||
|
||||
#: mailu/ui/forms.py:84
|
||||
msgid "Enabled"
|
||||
msgstr "Włączone"
|
||||
|
||||
#: mailu/ui/forms.py:89
|
||||
msgid "Email address"
|
||||
msgstr "Adres e-mail"
|
||||
|
||||
#: mailu/ui/forms.py:93 mailu/ui/templates/sidebar.html:117
|
||||
#: mailu/ui/templates/user/signup.html:4
|
||||
#: mailu/ui/templates/user/signup_domain.html:4
|
||||
msgid "Sign up"
|
||||
msgstr ""
|
||||
|
||||
#: mailu/ui/forms.py:119
|
||||
msgid "End of vacation"
|
||||
msgstr "Koniec wakacji"
|
||||
|
||||
#: mailu/ui/templates/client.html:4 mailu/ui/templates/sidebar.html:82
|
||||
msgid "Client setup"
|
||||
msgstr "Konfiguracja klienta"
|
||||
|
||||
#: mailu/ui/templates/client.html:16 mailu/ui/templates/client.html:43
|
||||
msgid "Mail protocol"
|
||||
msgstr "Protokół poczty"
|
||||
|
||||
#: mailu/ui/templates/client.html:24 mailu/ui/templates/client.html:51
|
||||
msgid "Server name"
|
||||
msgstr "Nazwa serwera"
|
||||
|
||||
#: mailu/ui/templates/domain/signup.html:4 mailu/ui/templates/sidebar.html:98
|
||||
msgid "Register a domain"
|
||||
msgstr "Zarejestruj domenę"
|
||||
|
||||
#: mailu/ui/templates/domain/details.html:17
|
||||
msgid "Generate keys"
|
||||
msgstr "Wygeneruj klucze"
|
||||
|
||||
#: mailu/ui/templates/domain/signup.html:13
|
||||
msgid "In order to register a new domain, you must first setup the\n"
|
||||
" domain zone so that the domain <code>MX</code> points to this server"
|
||||
msgstr ""
|
||||
"Aby zarejestrować nową domenę, musisz najpierw skonfigurować strefę domeny, "
|
||||
"aby domena <code> MX </code> wskazywała na ten serwer"
|
||||
|
||||
#: mailu/ui/templates/domain/signup.html:18
|
||||
msgid "If you do not know how to setup an <code>MX</code> record for your DNS zone,\n"
|
||||
" please contact your DNS provider or administrator. Also, please wait a\n"
|
||||
" couple minutes after the <code>MX</code> is set so the local server cache\n"
|
||||
" expires."
|
||||
msgstr ""
|
||||
"Jeśli nie wiesz, jak skonfigurować rekord <code> MX </code> dla swojej "
|
||||
"strefy DNS,\n"
|
||||
"skontaktuj się z dostawcą DNS lub administratorem. Proszę również poczekać\n"
|
||||
"kilka minut po ustawieniu <code> MX </code> , żeby pamięć podręczna serwera "
|
||||
"lokalnego wygasła."
|
||||
#: mailu/ui/templates/user/settings.html:26
|
||||
msgid "Auto-forward"
|
||||
msgstr "Automatyczne przekierowanie"
|
||||
|
||||
#: mailu/ui/templates/user/signup_domain.html:8
|
||||
msgid "pick a domain for the new account"
|
||||
@ -713,3 +690,40 @@ msgstr "Domena"
|
||||
#: mailu/ui/templates/user/signup_domain.html:15
|
||||
msgid "Available slots"
|
||||
msgstr "Dostępne miejsca"
|
||||
|
||||
#~ msgid "Spam filter threshold"
|
||||
#~ msgstr "Próg filtra antyspamowego"
|
||||
|
||||
#~ msgid "Your account"
|
||||
#~ msgstr "Twoje konto"
|
||||
|
||||
#~ msgid "Services status"
|
||||
#~ msgstr "Status usług"
|
||||
|
||||
#~ msgid "Service"
|
||||
#~ msgstr "Usługa"
|
||||
|
||||
#~ msgid "Status"
|
||||
#~ msgstr "Status"
|
||||
|
||||
#~ msgid "PID"
|
||||
#~ msgstr "PID"
|
||||
|
||||
#~ msgid "Image"
|
||||
#~ msgstr "Obraz"
|
||||
|
||||
#~ msgid "Started"
|
||||
#~ msgstr "Uruchomione"
|
||||
|
||||
#~ msgid "Last update"
|
||||
#~ msgstr "Ostatnia aktualizacja"
|
||||
|
||||
#~ msgid "My account"
|
||||
#~ msgstr "Moje konto"
|
||||
|
||||
#~ msgid "from"
|
||||
#~ msgstr "od"
|
||||
|
||||
#~ msgid "General settings"
|
||||
#~ msgstr "Ustawienia ogólne"
|
||||
|
||||
|
@ -3,9 +3,11 @@ msgstr ""
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: POEditor.com\n"
|
||||
"X-Generator: Poedit 1.5.7\n"
|
||||
"Project-Id-Version: Mailu\n"
|
||||
"Language: zh-CN\n"
|
||||
"Language: zh\n"
|
||||
"Last-Translator: Chris Chuan <Chris.chuan@gmail.com>\n"
|
||||
"Language-Team: \n"
|
||||
|
||||
#: mailu/ui/forms.py:32
|
||||
msgid "Invalid email address."
|
||||
@ -28,7 +30,7 @@ msgstr "密码"
|
||||
#: mailu/ui/forms.py:42 mailu/ui/templates/login.html:4
|
||||
#: mailu/ui/templates/sidebar.html:111
|
||||
msgid "Sign in"
|
||||
msgstr "注册"
|
||||
msgstr "登录"
|
||||
|
||||
#: mailu/ui/forms.py:46 mailu/ui/forms.py:56
|
||||
#: mailu/ui/templates/domain/details.html:27
|
||||
@ -44,6 +46,14 @@ msgstr "最大用户数"
|
||||
msgid "Maximum alias count"
|
||||
msgstr "最大别名数"
|
||||
|
||||
#: mailu/ui/forms.py:49
|
||||
msgid "Maximum user quota"
|
||||
msgstr "最大用户配额"
|
||||
|
||||
#: mailu/ui/forms.py:50
|
||||
msgid "Enable sign-up"
|
||||
msgstr "启用注册"
|
||||
|
||||
#: mailu/ui/forms.py:51 mailu/ui/forms.py:72 mailu/ui/forms.py:83
|
||||
#: mailu/ui/forms.py:128 mailu/ui/forms.py:140
|
||||
#: mailu/ui/templates/alias/list.html:21 mailu/ui/templates/domain/list.html:21
|
||||
@ -57,10 +67,30 @@ msgstr "说明"
|
||||
msgid "Create"
|
||||
msgstr "创建"
|
||||
|
||||
#: mailu/ui/forms.py:57
|
||||
msgid "Initial admin"
|
||||
msgstr "初始管理员"
|
||||
|
||||
#: mailu/ui/forms.py:58
|
||||
msgid "Admin password"
|
||||
msgstr "管理员密码"
|
||||
|
||||
#: mailu/ui/forms.py:59 mailu/ui/forms.py:79 mailu/ui/forms.py:91
|
||||
msgid "Confirm password"
|
||||
msgstr "确认密码"
|
||||
|
||||
#: mailu/ui/forms.py:65
|
||||
msgid "Alternative name"
|
||||
msgstr "备用名称"
|
||||
|
||||
#: mailu/ui/forms.py:70
|
||||
msgid "Relayed domain name"
|
||||
msgstr "中继域域名"
|
||||
|
||||
#: mailu/ui/forms.py:71 mailu/ui/templates/relay/list.html:18
|
||||
msgid "Remote host"
|
||||
msgstr "远程主机"
|
||||
|
||||
#: mailu/ui/forms.py:80 mailu/ui/templates/user/list.html:22
|
||||
#: mailu/ui/templates/user/signup_domain.html:16
|
||||
msgid "Quota"
|
||||
@ -74,10 +104,24 @@ msgstr "允许IMAP访问"
|
||||
msgid "Allow POP3 access"
|
||||
msgstr "允许POP3访问"
|
||||
|
||||
#: mailu/ui/forms.py:84
|
||||
msgid "Enabled"
|
||||
msgstr "启用"
|
||||
|
||||
#: mailu/ui/forms.py:85
|
||||
msgid "Save"
|
||||
msgstr "保存"
|
||||
|
||||
#: mailu/ui/forms.py:89
|
||||
msgid "Email address"
|
||||
msgstr "邮件地址"
|
||||
|
||||
#: mailu/ui/forms.py:93 mailu/ui/templates/sidebar.html:117
|
||||
#: mailu/ui/templates/user/signup.html:4
|
||||
#: mailu/ui/templates/user/signup_domain.html:4
|
||||
msgid "Sign up"
|
||||
msgstr "注册"
|
||||
|
||||
#: mailu/ui/forms.py:97
|
||||
msgid "Displayed name"
|
||||
msgstr "显示名称"
|
||||
@ -86,10 +130,23 @@ msgstr "显示名称"
|
||||
msgid "Enable spam filter"
|
||||
msgstr "启用垃圾邮件过滤"
|
||||
|
||||
#: mailu/ui/forms.py:80
|
||||
msgid "Spam filter threshold"
|
||||
#: mailu/ui/forms.py:99
|
||||
msgid "Spam filter tolerance"
|
||||
msgstr "垃圾邮件过滤器阈值"
|
||||
|
||||
#: mailu/ui/forms.py:100
|
||||
msgid "Enable forwarding"
|
||||
msgstr "启用转发"
|
||||
|
||||
#: mailu/ui/forms.py:101
|
||||
msgid "Keep a copy of the emails"
|
||||
msgstr "保留电子邮件副本"
|
||||
|
||||
#: mailu/ui/forms.py:103 mailu/ui/forms.py:139
|
||||
#: mailu/ui/templates/alias/list.html:20
|
||||
msgid "Destination"
|
||||
msgstr "目的地址"
|
||||
|
||||
#: mailu/ui/forms.py:105
|
||||
msgid "Save settings"
|
||||
msgstr "保存设置"
|
||||
@ -102,19 +159,6 @@ msgstr "检查密码"
|
||||
msgid "Update password"
|
||||
msgstr "更新密码"
|
||||
|
||||
#: mailu/ui/forms.py:100
|
||||
msgid "Enable forwarding"
|
||||
msgstr "启用转发"
|
||||
|
||||
#: mailu/ui/forms.py:103 mailu/ui/forms.py:139
|
||||
#: mailu/ui/templates/alias/list.html:20
|
||||
msgid "Destination"
|
||||
msgstr "目的地址"
|
||||
|
||||
#: mailu/ui/forms.py:120
|
||||
msgid "Update"
|
||||
msgstr "更新"
|
||||
|
||||
#: mailu/ui/forms.py:115
|
||||
msgid "Enable automatic reply"
|
||||
msgstr "启用自动回复"
|
||||
@ -127,6 +171,22 @@ msgstr "回复主题"
|
||||
msgid "Reply body"
|
||||
msgstr "回复正文"
|
||||
|
||||
#: mailu/ui/forms.py:119
|
||||
msgid "End of vacation"
|
||||
msgstr "假期结束"
|
||||
|
||||
#: mailu/ui/forms.py:120
|
||||
msgid "Update"
|
||||
msgstr "更新"
|
||||
|
||||
#: mailu/ui/forms.py:125
|
||||
msgid "Your token (write it down, as it will never be displayed again)"
|
||||
msgstr "您的令牌(请记录,它只显示这一次)"
|
||||
|
||||
#: mailu/ui/forms.py:130 mailu/ui/templates/token/list.html:20
|
||||
msgid "Authorized IP"
|
||||
msgstr "授权IP"
|
||||
|
||||
#: mailu/ui/forms.py:136
|
||||
msgid "Alias"
|
||||
msgstr "别名"
|
||||
@ -169,11 +229,44 @@ msgstr "启用TLS"
|
||||
msgid "Username"
|
||||
msgstr "用户名"
|
||||
|
||||
#: mailu/ui/forms.py:163
|
||||
msgid "Keep emails on the server"
|
||||
msgstr "在服务器上保留电子邮件"
|
||||
|
||||
#: mailu/ui/forms.py:168
|
||||
msgid "Announcement subject"
|
||||
msgstr "公告主题"
|
||||
|
||||
#: mailu/ui/forms.py:170
|
||||
msgid "Announcement body"
|
||||
msgstr "公告正文"
|
||||
|
||||
#: mailu/ui/forms.py:172
|
||||
msgid "Send"
|
||||
msgstr "发送"
|
||||
|
||||
#: mailu/ui/templates/announcement.html:4
|
||||
msgid "Public announcement"
|
||||
msgstr "公开公告"
|
||||
|
||||
#: mailu/ui/templates/client.html:4 mailu/ui/templates/sidebar.html:82
|
||||
msgid "Client setup"
|
||||
msgstr "客户端设置"
|
||||
|
||||
#: mailu/ui/templates/client.html:16 mailu/ui/templates/client.html:43
|
||||
msgid "Mail protocol"
|
||||
msgstr "邮件协议"
|
||||
|
||||
#: mailu/ui/templates/client.html:24 mailu/ui/templates/client.html:51
|
||||
msgid "Server name"
|
||||
msgstr "服务器名称"
|
||||
|
||||
#: mailu/ui/templates/confirm.html:4
|
||||
msgid "Confirm action"
|
||||
msgstr "确认操作"
|
||||
|
||||
#: mailu/ui/templates/confirm.html:13
|
||||
#, python-format
|
||||
msgid "You are about to %(action)s. Please confirm your action."
|
||||
msgstr "即将%(action)s,请确认您的操作。"
|
||||
|
||||
@ -185,54 +278,18 @@ msgstr "Docker错误"
|
||||
msgid "An error occurred while talking to the Docker server."
|
||||
msgstr "Docker服务器通信出错"
|
||||
|
||||
#: mailu/admin/templates/login.html:6
|
||||
msgid "Your account"
|
||||
msgstr "你的帐户"
|
||||
|
||||
#: mailu/ui/templates/login.html:8
|
||||
msgid "to access the administration tools"
|
||||
msgstr "访问管理员工具"
|
||||
|
||||
#: mailu/ui/templates/services.html:4 mailu/ui/templates/sidebar.html:39
|
||||
msgid "Services status"
|
||||
msgstr "服务状态"
|
||||
|
||||
#: mailu/ui/templates/services.html:10
|
||||
msgid "Service"
|
||||
msgstr "服务"
|
||||
|
||||
#: mailu/ui/templates/fetch/list.html:23 mailu/ui/templates/services.html:11
|
||||
msgid "Status"
|
||||
msgstr "状态"
|
||||
|
||||
#: mailu/ui/templates/services.html:12
|
||||
msgid "PID"
|
||||
msgstr "进程ID"
|
||||
|
||||
#: mailu/ui/templates/services.html:13
|
||||
msgid "Image"
|
||||
msgstr "镜像"
|
||||
|
||||
#: mailu/ui/templates/services.html:14
|
||||
msgid "Started"
|
||||
msgstr "已开始"
|
||||
|
||||
#: mailu/ui/templates/services.html:15
|
||||
msgid "Last update"
|
||||
msgstr "最后更新"
|
||||
msgstr "访问管理工具"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:8
|
||||
msgid "My account"
|
||||
msgstr "我的帐户"
|
||||
msgstr "我的账户"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:11 mailu/ui/templates/user/list.html:34
|
||||
msgid "Settings"
|
||||
msgstr "设置"
|
||||
|
||||
#: mailu/ui/templates/user/settings.html:22
|
||||
msgid "Auto-forward"
|
||||
msgstr "自动转发"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:21 mailu/ui/templates/user/list.html:35
|
||||
msgid "Auto-reply"
|
||||
msgstr "自动回复"
|
||||
@ -240,39 +297,71 @@ msgstr "自动回复"
|
||||
#: mailu/ui/templates/fetch/list.html:4 mailu/ui/templates/sidebar.html:26
|
||||
#: mailu/ui/templates/user/list.html:36
|
||||
msgid "Fetched accounts"
|
||||
msgstr "代收帐户"
|
||||
msgstr "代收账户"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:105
|
||||
msgid "Sign out"
|
||||
msgstr "登出"
|
||||
#: mailu/ui/templates/sidebar.html:31 mailu/ui/templates/token/list.html:4
|
||||
msgid "Authentication tokens"
|
||||
msgstr "认证令牌"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:35
|
||||
msgid "Administration"
|
||||
msgstr "管理"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:44
|
||||
msgid "Announcement"
|
||||
msgstr "公告"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:49
|
||||
msgid "Administrators"
|
||||
msgstr "管理员"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:54
|
||||
msgid "Relayed domains"
|
||||
msgstr "中继域"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:59 mailu/ui/templates/user/settings.html:15
|
||||
msgid "Antispam"
|
||||
msgstr "反垃圾邮件"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:66
|
||||
msgid "Mail domains"
|
||||
msgstr "邮件域"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:72
|
||||
msgid "Go to"
|
||||
msgstr "转到"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:76
|
||||
msgid "Webmail"
|
||||
msgstr "网页邮箱"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:87
|
||||
msgid "Website"
|
||||
msgstr "网站"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:92
|
||||
msgid "Help"
|
||||
msgstr "帮助"
|
||||
|
||||
#: mailu/ui/templates/domain/signup.html:4 mailu/ui/templates/sidebar.html:98
|
||||
msgid "Register a domain"
|
||||
msgstr "注册域名"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:105
|
||||
msgid "Sign out"
|
||||
msgstr "登出"
|
||||
|
||||
#: mailu/ui/templates/working.html:4
|
||||
msgid "We are still working on this feature!"
|
||||
msgstr "该功能开发中……"
|
||||
|
||||
#: mailu/ui/templates/admin/create.html:4
|
||||
msgid "Add a global administrator"
|
||||
msgstr "添加超级管理员"
|
||||
msgstr "添加全局管理员"
|
||||
|
||||
#: mailu/ui/templates/admin/list.html:4
|
||||
msgid "Global administrators"
|
||||
msgstr "超级管理员"
|
||||
msgstr "全局管理员"
|
||||
|
||||
#: mailu/ui/templates/admin/list.html:9
|
||||
msgid "Add administrator"
|
||||
@ -323,7 +412,7 @@ msgstr "添加别名"
|
||||
#: mailu/ui/templates/relay/list.html:20 mailu/ui/templates/token/list.html:21
|
||||
#: mailu/ui/templates/user/list.html:24
|
||||
msgid "Created"
|
||||
msgstr "创建"
|
||||
msgstr "已创建"
|
||||
|
||||
#: mailu/ui/templates/alias/list.html:23 mailu/ui/templates/domain/list.html:23
|
||||
#: mailu/ui/templates/fetch/list.html:25 mailu/ui/templates/relay/list.html:21
|
||||
@ -337,6 +426,22 @@ msgstr "上次编辑"
|
||||
msgid "Edit"
|
||||
msgstr "编辑"
|
||||
|
||||
#: mailu/ui/templates/alternative/create.html:4
|
||||
msgid "Create alternative domain"
|
||||
msgstr "创建替代域"
|
||||
|
||||
#: mailu/ui/templates/alternative/list.html:4
|
||||
msgid "Alternative domain list"
|
||||
msgstr "替代域名列表"
|
||||
|
||||
#: mailu/ui/templates/alternative/list.html:12
|
||||
msgid "Add alternative"
|
||||
msgstr "添加替代"
|
||||
|
||||
#: mailu/ui/templates/alternative/list.html:19
|
||||
msgid "Name"
|
||||
msgstr "名称"
|
||||
|
||||
#: mailu/ui/templates/domain/create.html:4
|
||||
#: mailu/ui/templates/domain/list.html:9
|
||||
msgid "New domain"
|
||||
@ -344,11 +449,15 @@ msgstr "新域"
|
||||
|
||||
#: mailu/ui/templates/domain/details.html:4
|
||||
msgid "Domain details"
|
||||
msgstr "域详情"
|
||||
msgstr "域详细信息"
|
||||
|
||||
#: mailu/ui/templates/domain/details.html:15
|
||||
msgid "Regenerate keys"
|
||||
msgstr "重新生成密钥"
|
||||
msgstr "重新生成秘钥"
|
||||
|
||||
#: mailu/ui/templates/domain/details.html:17
|
||||
msgid "Generate keys"
|
||||
msgstr "生成秘钥"
|
||||
|
||||
#: mailu/ui/templates/domain/details.html:31
|
||||
msgid "DNS MX entry"
|
||||
@ -392,7 +501,7 @@ msgstr "别名数量"
|
||||
|
||||
#: mailu/ui/templates/domain/list.html:28
|
||||
msgid "Details"
|
||||
msgstr "详情"
|
||||
msgstr "详细信息"
|
||||
|
||||
#: mailu/ui/templates/domain/list.html:35
|
||||
msgid "Users"
|
||||
@ -406,26 +515,60 @@ msgstr "别名"
|
||||
msgid "Managers"
|
||||
msgstr "管理员"
|
||||
|
||||
#: mailu/ui/templates/domain/list.html:39
|
||||
msgid "Alternatives"
|
||||
msgstr "备选方案"
|
||||
|
||||
#: mailu/ui/templates/domain/signup.html:13
|
||||
msgid ""
|
||||
"In order to register a new domain, you must first setup the\n"
|
||||
" domain zone so that the domain <code>MX</code> points to this server"
|
||||
msgstr "在注册一个新的域名前,您必须先为该域名设置 <code>MX</code> 记录,并使其指向本服务器"
|
||||
|
||||
#: mailu/ui/templates/domain/signup.html:18
|
||||
msgid ""
|
||||
"If you do not know how to setup an <code>MX</code> record for your DNS "
|
||||
"zone,\n"
|
||||
" please contact your DNS provider or administrator. Also, please wait "
|
||||
"a\n"
|
||||
" couple minutes after the <code>MX</code> is set so the local server "
|
||||
"cache\n"
|
||||
" expires."
|
||||
msgstr "如果您不知道如何为域名设置 <code>MX</code> 记录,请联系你的DNS提供商或者系统管理员。在设置完成 <code>MX</code> 记录后,请等待本地域名服务器的缓存过期。"
|
||||
|
||||
|
||||
#: mailu/ui/templates/fetch/create.html:4
|
||||
msgid "Add a fetched account"
|
||||
msgstr "添加一个代收帐户"
|
||||
msgstr "添加一个代收账户"
|
||||
|
||||
#: mailu/ui/templates/fetch/edit.html:4
|
||||
msgid "Update a fetched account"
|
||||
msgstr "更新代收帐户"
|
||||
msgstr "更新代收账户"
|
||||
|
||||
#: mailu/ui/templates/fetch/list.html:12
|
||||
msgid "Add an account"
|
||||
msgstr "添加一个帐户"
|
||||
msgstr "添加一个账户"
|
||||
|
||||
#: mailu/ui/templates/fetch/list.html:19
|
||||
msgid "Endpoint"
|
||||
msgstr "端点"
|
||||
|
||||
#: mailu/ui/templates/fetch/list.html:21
|
||||
msgid "Keep emails"
|
||||
msgstr "保留电子邮件"
|
||||
|
||||
#: mailu/ui/templates/fetch/list.html:22
|
||||
msgid "Last check"
|
||||
msgstr "上次检查"
|
||||
|
||||
#: mailu/ui/templates/fetch/list.html:35
|
||||
msgid "yes"
|
||||
msgstr "是"
|
||||
|
||||
#: mailu/ui/templates/fetch/list.html:35
|
||||
msgid "no"
|
||||
msgstr "否"
|
||||
|
||||
#: mailu/ui/templates/manager/create.html:4
|
||||
msgid "Add a manager"
|
||||
msgstr "添加一个管理员"
|
||||
@ -438,41 +581,49 @@ msgstr "管理员列表"
|
||||
msgid "Add manager"
|
||||
msgstr "添加管理员"
|
||||
|
||||
#: mailu/ui/forms.py:168
|
||||
msgid "Announcement subject"
|
||||
msgstr "公告主题"
|
||||
#: mailu/ui/templates/relay/create.html:4
|
||||
msgid "New relay domain"
|
||||
msgstr "新的中继域"
|
||||
|
||||
#: mailu/ui/forms.py:170
|
||||
msgid "Announcement body"
|
||||
msgstr "公告正文"
|
||||
#: mailu/ui/templates/relay/edit.html:4
|
||||
msgid "Edit relayd domain"
|
||||
msgstr "编辑中继域"
|
||||
|
||||
#: mailu/ui/forms.py:172
|
||||
msgid "Send"
|
||||
msgstr "发送"
|
||||
#: mailu/ui/templates/relay/list.html:4
|
||||
msgid "Relayed domain list"
|
||||
msgstr "中继域列表"
|
||||
|
||||
#: mailu/ui/templates/announcement.html:4
|
||||
msgid "Public announcement"
|
||||
msgstr "公告"
|
||||
#: mailu/ui/templates/relay/list.html:9
|
||||
msgid "New relayed domain"
|
||||
msgstr "新的中继域"
|
||||
|
||||
#: mailu/ui/templates/announcement.html:8
|
||||
msgid "from"
|
||||
msgstr "来自"
|
||||
#: mailu/ui/templates/token/create.html:4
|
||||
msgid "Create an authentication token"
|
||||
msgstr "创建一个认证令牌"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:44
|
||||
msgid "Announcement"
|
||||
msgstr "公告"
|
||||
#: mailu/ui/templates/token/list.html:12
|
||||
msgid "New token"
|
||||
msgstr "新令牌"
|
||||
|
||||
#: mailu/ui/templates/user/create.html:4
|
||||
msgid "New user"
|
||||
msgstr "新用户"
|
||||
|
||||
#: mailu/ui/templates/user/create.html:15
|
||||
msgid "General"
|
||||
msgstr "通用"
|
||||
|
||||
#: mailu/ui/templates/user/create.html:22
|
||||
msgid "Features and quotas"
|
||||
msgstr "功能和配额"
|
||||
|
||||
#: mailu/ui/templates/user/edit.html:4
|
||||
msgid "Edit user"
|
||||
msgstr "编辑用户"
|
||||
|
||||
#: mailu/ui/templates/user/forward.html:4
|
||||
msgid "Forward emails"
|
||||
msgstr "转发电子邮件"
|
||||
msgstr "转发邮件"
|
||||
|
||||
#: mailu/ui/templates/user/list.html:4
|
||||
msgid "User list"
|
||||
@ -492,201 +643,15 @@ msgstr "功能"
|
||||
|
||||
#: mailu/ui/templates/user/password.html:4
|
||||
msgid "Password update"
|
||||
msgstr "密码更新"
|
||||
msgstr "更新密码"
|
||||
|
||||
#: mailu/ui/templates/user/reply.html:4
|
||||
msgid "Automatic reply"
|
||||
msgstr "自动回复"
|
||||
|
||||
#: mailu/ui/forms.py:49
|
||||
msgid "Maximum user quota"
|
||||
msgstr "最大用户容量"
|
||||
|
||||
#: mailu/ui/forms.py:101
|
||||
msgid "Keep a copy of the emails"
|
||||
msgstr "保留电子邮件副本"
|
||||
|
||||
#: mailu/ui/forms.py:163
|
||||
msgid "Keep emails on the server"
|
||||
msgstr "保留电子邮件在服务器上"
|
||||
|
||||
#: mailu/ui/templates/fetch/list.html:21
|
||||
msgid "Keep emails"
|
||||
msgstr "保存电子邮件"
|
||||
|
||||
#: mailu/ui/templates/fetch/list.html:35
|
||||
msgid "yes"
|
||||
msgstr "是"
|
||||
|
||||
#: mailu/ui/templates/fetch/list.html:35
|
||||
msgid "no"
|
||||
msgstr "否"
|
||||
|
||||
#: mailu/ui/forms.py:65
|
||||
msgid "Alternative name"
|
||||
msgstr "替代名称"
|
||||
|
||||
#: mailu/ui/forms.py:70
|
||||
msgid "Relayed domain name"
|
||||
msgstr "中继域域名"
|
||||
|
||||
#: mailu/ui/forms.py:71 mailu/ui/templates/relay/list.html:18
|
||||
msgid "Remote host"
|
||||
msgstr "远程主机"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:54
|
||||
msgid "Relayed domains"
|
||||
msgstr "中继域"
|
||||
|
||||
#: mailu/ui/templates/alternative/create.html:4
|
||||
msgid "Create alternative domain"
|
||||
msgstr "创建替代域"
|
||||
|
||||
#: mailu/ui/templates/alternative/list.html:4
|
||||
msgid "Alternative domain list"
|
||||
msgstr "替代域名列表"
|
||||
|
||||
#: mailu/ui/templates/alternative/list.html:12
|
||||
msgid "Add alternative"
|
||||
msgstr "添加替代"
|
||||
|
||||
#: mailu/ui/templates/alternative/list.html:19
|
||||
msgid "Name"
|
||||
msgstr "名称"
|
||||
|
||||
#: mailu/ui/templates/domain/list.html:39
|
||||
msgid "Alternatives"
|
||||
msgstr "备择方案"
|
||||
|
||||
#: mailu/ui/templates/relay/create.html:4
|
||||
msgid "New relay domain"
|
||||
msgstr "新的中继域"
|
||||
|
||||
#: mailu/ui/templates/relay/edit.html:4
|
||||
msgid "Edit relayd domain"
|
||||
msgstr "编辑中继域"
|
||||
|
||||
#: mailu/ui/templates/relay/list.html:4
|
||||
msgid "Relayed domain list"
|
||||
msgstr "中继域列表"
|
||||
|
||||
#: mailu/ui/templates/relay/list.html:9
|
||||
msgid "New relayed domain"
|
||||
msgstr "新的中继域"
|
||||
|
||||
#: mailu/ui/forms.py:125
|
||||
msgid "Your token (write it down, as it will never be displayed again)"
|
||||
msgstr "您的令牌(请记录,它只显示这一次)"
|
||||
|
||||
#: mailu/ui/forms.py:130 mailu/ui/templates/token/list.html:20
|
||||
msgid "Authorized IP"
|
||||
msgstr "授权IP"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:31 mailu/ui/templates/token/list.html:4
|
||||
msgid "Authentication tokens"
|
||||
msgstr "认证令牌"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:72
|
||||
msgid "Go to"
|
||||
msgstr "转到"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:76
|
||||
msgid "Webmail"
|
||||
msgstr "网页邮箱"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:87
|
||||
msgid "Website"
|
||||
msgstr "网站"
|
||||
|
||||
#: mailu/ui/templates/token/create.html:4
|
||||
msgid "Create an authentication token"
|
||||
msgstr "创建一个认证令牌"
|
||||
|
||||
#: mailu/ui/templates/token/list.html:12
|
||||
msgid "New token"
|
||||
msgstr "新的令牌"
|
||||
|
||||
#: mailu/ui/templates/user/create.html:15
|
||||
msgid "General"
|
||||
msgstr "通用"
|
||||
|
||||
#: mailu/ui/templates/user/create.html:22
|
||||
msgid "Features and quotas"
|
||||
msgstr "功能和配额"
|
||||
|
||||
#: mailu/ui/templates/user/settings.html:14
|
||||
msgid "General settings"
|
||||
msgstr "常规设置"
|
||||
|
||||
#: mailu/ui/templates/sidebar.html:59 mailu/ui/templates/user/settings.html:15
|
||||
msgid "Antispam"
|
||||
msgstr "反垃圾邮件"
|
||||
|
||||
#: mailu/ui/forms.py:99
|
||||
msgid "Spam filter tolerance"
|
||||
msgstr "垃圾邮件过滤器容忍度"
|
||||
|
||||
#: mailu/ui/forms.py:50
|
||||
msgid "Enable sign-up"
|
||||
msgstr "启用用户注册"
|
||||
|
||||
#: mailu/ui/forms.py:57
|
||||
msgid "Initial admin"
|
||||
msgstr "初始管理员"
|
||||
|
||||
#: mailu/ui/forms.py:58
|
||||
msgid "Admin password"
|
||||
msgstr "管理员密码"
|
||||
|
||||
#: mailu/ui/forms.py:84
|
||||
msgid "Enabled"
|
||||
msgstr "启用"
|
||||
|
||||
#: mailu/ui/forms.py:89
|
||||
msgid "Email address"
|
||||
msgstr "邮件地址"
|
||||
|
||||
#: mailu/ui/forms.py:93 mailu/ui/templates/sidebar.html:117
|
||||
#: mailu/ui/templates/user/signup.html:4
|
||||
#: mailu/ui/templates/user/signup_domain.html:4
|
||||
msgid "Sign up"
|
||||
msgstr "注册"
|
||||
|
||||
#: mailu/ui/forms.py:119
|
||||
msgid "End of vacation"
|
||||
msgstr "假期结束"
|
||||
|
||||
#: mailu/ui/templates/client.html:4 mailu/ui/templates/sidebar.html:82
|
||||
msgid "Client setup"
|
||||
msgstr "客户端设置"
|
||||
|
||||
#: mailu/ui/templates/client.html:16 mailu/ui/templates/client.html:43
|
||||
msgid "Mail protocol"
|
||||
msgstr "邮件协议"
|
||||
|
||||
#: mailu/ui/templates/client.html:24 mailu/ui/templates/client.html:51
|
||||
msgid "Server name"
|
||||
msgstr "服务器名"
|
||||
|
||||
#: mailu/ui/templates/domain/signup.html:4 mailu/ui/templates/sidebar.html:98
|
||||
msgid "Register a domain"
|
||||
msgstr "注册域名"
|
||||
|
||||
#: mailu/ui/templates/domain/details.html:17
|
||||
msgid "Generate keys"
|
||||
msgstr "生成密钥"
|
||||
|
||||
#: mailu/ui/templates/domain/signup.html:13
|
||||
msgid "In order to register a new domain, you must first setup the\n"
|
||||
" domain zone so that the domain <code>MX</code> points to this server"
|
||||
msgstr "在注册一个新的域名前,您必须先为该域名设置 <code>MX</code> 记录,并使其指向本服务器"
|
||||
|
||||
#: mailu/ui/templates/domain/signup.html:18
|
||||
msgid "If you do not know how to setup an <code>MX</code> record for your DNS zone,\n"
|
||||
" please contact your DNS provider or administrator. Also, please wait a\n"
|
||||
" couple minutes after the <code>MX</code> is set so the local server cache\n"
|
||||
" expires."
|
||||
msgstr "如果您不知道如何为域名设置 <code>MX</code> 记录,请联系你的DNS提供商或者系统管理员。在设置完成 <code>MX</code> 记录后,请等待本地域名服务器的缓存过期。"
|
||||
#: mailu/ui/templates/user/settings.html:22
|
||||
msgid "Auto-forward"
|
||||
msgstr "自动转发"
|
||||
|
||||
#: mailu/ui/templates/user/signup_domain.html:8
|
||||
msgid "pick a domain for the new account"
|
||||
@ -700,3 +665,14 @@ msgstr "域名"
|
||||
msgid "Available slots"
|
||||
msgstr "可用"
|
||||
|
||||
#~ msgid "Your account"
|
||||
#~ msgstr ""
|
||||
|
||||
#~ msgid "Spam filter threshold"
|
||||
#~ msgstr ""
|
||||
|
||||
#~ msgid "from"
|
||||
#~ msgstr ""
|
||||
|
||||
#~ msgid "General settings"
|
||||
#~ msgstr ""
|
@ -1,6 +1,6 @@
|
||||
from flask import Blueprint
|
||||
|
||||
|
||||
ui = Blueprint('ui', __name__, static_folder='static', template_folder='templates')
|
||||
ui = Blueprint('ui', __name__, static_folder=None, template_folder='templates')
|
||||
|
||||
from mailu.ui.views import *
|
||||
|
@ -44,15 +44,6 @@ class MultipleEmailAddressesVerify(object):
|
||||
class ConfirmationForm(flask_wtf.FlaskForm):
|
||||
submit = fields.SubmitField(_('Confirm'))
|
||||
|
||||
|
||||
class LoginForm(flask_wtf.FlaskForm):
|
||||
class Meta:
|
||||
csrf = False
|
||||
email = fields.StringField(_('E-mail'), [validators.Email()])
|
||||
pw = fields.PasswordField(_('Password'), [validators.DataRequired()])
|
||||
submit = fields.SubmitField(_('Sign in'))
|
||||
|
||||
|
||||
class DomainForm(flask_wtf.FlaskForm):
|
||||
name = fields.StringField(_('Domain name'), [validators.DataRequired()])
|
||||
max_users = fields_.IntegerField(_('Maximum user count'), [validators.NumberRange(min=-1)], default=10)
|
||||
@ -88,7 +79,7 @@ class UserForm(flask_wtf.FlaskForm):
|
||||
localpart = fields.StringField(_('E-mail'), [validators.DataRequired(), validators.Regexp(LOCALPART_REGEX)])
|
||||
pw = fields.PasswordField(_('Password'))
|
||||
pw2 = fields.PasswordField(_('Confirm password'), [validators.EqualTo('pw')])
|
||||
quota_bytes = fields_.IntegerSliderField(_('Quota'), default=1000000000)
|
||||
quota_bytes = fields_.IntegerSliderField(_('Quota'), default=10**9)
|
||||
enable_imap = fields.BooleanField(_('Allow IMAP access'), default=True)
|
||||
enable_pop = fields.BooleanField(_('Allow POP3 access'), default=True)
|
||||
displayed_name = fields.StringField(_('Displayed name'))
|
||||
|
@ -1,15 +1,15 @@
|
||||
{% extends "base.html" %}
|
||||
{%- extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{%- block title %}
|
||||
{% trans %}Add a global administrator{% endtrans %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call macros.card() %}
|
||||
{%- block content %}
|
||||
{%- call macros.card() %}
|
||||
<form class="form" method="post" role="form">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ macros.form_field(form.admin, class_='mailselect') }}
|
||||
{{ macros.form_field(form.submit) }}
|
||||
</form>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
{%- endcall %}
|
||||
{%- endblock %}
|
||||
|
@ -1,17 +1,17 @@
|
||||
{% extends "base.html" %}
|
||||
{%- extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{%- block title %}
|
||||
{% trans %}Global administrators{% endtrans %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block main_action %}
|
||||
{%- block main_action %}
|
||||
<a class="btn btn-primary float-right" href="{{ url_for('.admin_create') }}">
|
||||
{% trans %}Add administrator{% endtrans %}
|
||||
</a>
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call macros.table() %}
|
||||
{%- block content %}
|
||||
{%- call macros.table() %}
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans %}Actions{% endtrans %}</th>
|
||||
@ -19,14 +19,14 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for admin in admins %}
|
||||
{%- for admin in admins %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('.admin_delete', admin=admin.email) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
|
||||
</td>
|
||||
<td>{{ admin }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
</tbody>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
{%- endcall %}
|
||||
{%- endblock %}
|
||||
|
@ -1,15 +1,15 @@
|
||||
{% extends "base.html" %}
|
||||
{%- extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{%- block title %}
|
||||
{% trans %}Create alias{% endtrans %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{%- block subtitle %}
|
||||
{{ domain }}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call macros.card() %}
|
||||
{%- block content %}
|
||||
{%- call macros.card() %}
|
||||
<form class="form" method="post" role="form">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ 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.submit) }}
|
||||
</form>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
{%- endcall %}
|
||||
{%- endblock %}
|
||||
|
@ -1,9 +1,9 @@
|
||||
{% extends "alias/create.html" %}
|
||||
{%- extends "alias/create.html" %}
|
||||
|
||||
{% block title %}
|
||||
{%- block title %}
|
||||
{% trans %}Edit alias{% endtrans %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{%- block subtitle %}
|
||||
{{ alias }}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
@ -1,19 +1,19 @@
|
||||
{% extends "base.html" %}
|
||||
{%- extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{%- block title %}
|
||||
{% trans %}Alias list{% endtrans %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{%- block subtitle %}
|
||||
{{ domain.name }}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block main_action %}
|
||||
{%- block main_action %}
|
||||
<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 %}
|
||||
{% call macros.table() %}
|
||||
{%- block content %}
|
||||
{%- call macros.table() %}
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans %}Actions{% endtrans %}</th>
|
||||
@ -25,7 +25,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for alias in domain.aliases %}
|
||||
{%- for alias in domain.aliases %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('.alias_edit', alias=alias.email) }}" title="{% trans %}Edit{% endtrans %}"><i class="fa fa-pencil"></i></a>
|
||||
@ -34,10 +34,10 @@
|
||||
<td>{{ alias }}</td>
|
||||
<td>{{ alias.destination|join(', ') or '-' }}</td>
|
||||
<td>{{ alias.comment or '' }}</td>
|
||||
<td>{{ alias.created_at }}</td>
|
||||
<td>{{ alias.updated_at or '' }}</td>
|
||||
<td>{{ alias.created_at | format_date }}</td>
|
||||
<td>{{ alias.updated_at | format_date }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
</tbody>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
{%- endcall %}
|
||||
{%- endblock %}
|
||||
|
@ -1,9 +1,9 @@
|
||||
{% extends "form.html" %}
|
||||
{%- extends "form.html" %}
|
||||
|
||||
{% block title %}
|
||||
{%- block title %}
|
||||
{% trans %}Create alternative domain{% endtrans %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{%- block subtitle %}
|
||||
{{ domain }}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
@ -1,36 +1,38 @@
|
||||
{% extends "base.html" %}
|
||||
{%- extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{%- block title %}
|
||||
{% trans %}Alternative domain list{% endtrans %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{%- block subtitle %}
|
||||
{{ domain.name }}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block main_action %}
|
||||
{%- block main_action %}
|
||||
<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 %}
|
||||
{% call macros.table() %}
|
||||
{%- block content %}
|
||||
{%- call macros.table() %}
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans %}Actions{% endtrans %}</th>
|
||||
<th>{% trans %}Name{% endtrans %}</th>
|
||||
<th>{% trans %}Created{% endtrans %}</th>
|
||||
<th>{% trans %}Last edit{% endtrans %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for alternative in domain.alternatives %}
|
||||
{%- for alternative in domain.alternatives %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('.alternative_delete', alternative=alternative.name) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
|
||||
</td>
|
||||
<td>{{ alternative }}</td>
|
||||
<td>{{ alternative.created_at }}</td>
|
||||
<td>{{ alternative.created_at | format_date }}</td>
|
||||
<td>{{ alternative.updated_at | format_date }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
</tbody>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
{%- endcall %}
|
||||
{%- endblock %}
|
||||
|
@ -1,16 +1,16 @@
|
||||
{% extends "base.html" %}
|
||||
{%- extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{%- block title %}
|
||||
{% trans %}Public announcement{% endtrans %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call macros.card() %}
|
||||
{%- block content %}
|
||||
{%- call macros.card() %}
|
||||
<form class="form" method="post" role="form">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ macros.form_field(form.announcement_subject) }}
|
||||
{{ macros.form_field(form.announcement_body, rows=10) }}
|
||||
{{ macros.form_field(form.submit) }}
|
||||
</form>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
{%- endcall %}
|
||||
{%- endblock %}
|
||||
|
15
core/admin/mailu/ui/templates/antispam.html
Normal file
@ -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,68 +1,86 @@
|
||||
{% import "macros.html" as macros %}
|
||||
{% import "bootstrap/utils.html" as utils %}
|
||||
{%- import "macros.html" as macros %}
|
||||
{%- import "bootstrap/utils.html" as utils %}
|
||||
<!doctype html>
|
||||
<html>
|
||||
<html lang="{{ session['language'] }}" data-static="/static/">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="{{ url_for('.static', filename='vendor.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('.static', filename='app.css') }}">
|
||||
<title>Mailu-Admin - {{ config["SITENAME"] }}</title>
|
||||
<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='app.css') }}">
|
||||
</head>
|
||||
<body class="hold-transition sidebar-mini layout-fixed">
|
||||
<div class="wrapper">
|
||||
<nav class="main-header navbar navbar-expand navbar-white navbar-light">
|
||||
<ul class="navbar-nav">
|
||||
<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>
|
||||
</ul>
|
||||
<ul class="navbar-nav ml-auto">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link" data-toggle="dropdown" href="#" aria-expanded="false">{{ session['language'] }}</a>
|
||||
<div class="dropdown-menu dropdown-menu-right p-0">
|
||||
{% for language in session['available_languages'] %}
|
||||
<a class="dropdown-item {% if language == session['language'] %}active{% endif %} " href="{{ url_for('.set_language', language=language) }}">{{ language }}</a>
|
||||
{% endfor %}
|
||||
<a class="nav-link" data-toggle="dropdown" href="#" aria-expanded="false">
|
||||
<i class="fas fa-language text-xl" aria-hidden="true" title="{% trans %}change language{% endtrans %}"></i><span class="sr-only">Language</span>
|
||||
<span class="badge badge-primary navbar-badge">{{ session['language'] }}</span></a>
|
||||
<div class="dropdown-menu dropdown-menu-right p-0" id="mailu-languages">
|
||||
{%- for locale in config.translations.values() %}
|
||||
<a class="dropdown-item{% if locale|string() == session['language'] %} active{% endif %}" href="{{ url_for('.set_language', language=locale) }}">{{ locale.get_language_name().title() }}</a>
|
||||
{%- endfor %}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<aside class="main-sidebar sidebar-dark-primary">
|
||||
<a href="{{ config["WEB_ADMIN"] }}" class="brand-link">
|
||||
<span class="brand-text font-weight-light">{{ config["SITENAME"] }}</span>
|
||||
<aside class="main-sidebar sidebar-dark-primary nav-compact elevation-4">
|
||||
<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 %}>
|
||||
<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>
|
||||
{% block sidebar %}
|
||||
{% include "sidebar.html" %}
|
||||
{% endblock %}
|
||||
{%- include "sidebar.html" %}
|
||||
</aside>
|
||||
<div class="content-wrapper">
|
||||
<div class="content-wrapper text-sm">
|
||||
<section class="content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<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>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
{% block main_action %}
|
||||
{% endblock %}
|
||||
{%- block main_action %}{%- endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="content">
|
||||
{{ utils.flashed_messages(container=False) }}
|
||||
{% block content %}{% endblock %}
|
||||
{{ utils.flashed_messages(container=False, default_category='success') }}
|
||||
{%- block content %}{%- endblock %}
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
<a class="white-text" href="https://adminlte.io/themes/v3/index3.html">AdminLTE</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>
|
||||
Built with <i class="fa fa-heart text-danger" aria-hidden="true"></i><span class="sr-only">love</span>
|
||||
using <a href="https://flask.palletsprojects.com/">Flask</a>
|
||||
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>
|
||||
</div>
|
||||
<script src="{{ url_for('.static', filename='vendor.js') }}"></script>
|
||||
<script src="{{ url_for('.static', filename='app.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='vendor.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='app.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -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 %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
configure your email client
|
||||
{% endblock %}
|
||||
{%- block subtitle %}
|
||||
{% trans %}configure your email client{% endtrans %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call macros.table(title="Incoming mail", datatable=False) %}
|
||||
{%- block content %}
|
||||
{%- call macros.table(title=_("Incoming mail"), datatable=False) %}
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{% trans %}Mail protocol{% endtrans %}</th>
|
||||
@ -23,20 +21,20 @@ configure your email client
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
<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>
|
||||
<th>{% trans %}Password{% endtrans %}</th>
|
||||
<td><pre>*******</pre></td>
|
||||
<td><pre class="pre-config border bg-light">*******</pre></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
{% endcall %}
|
||||
{%- endcall %}
|
||||
|
||||
{% call macros.table(title="Outgoing mail", datatable=False) %}
|
||||
{%- call macros.table(title=_("Outgoing mail"), datatable=False) %}
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{% trans %}Mail protocol{% endtrans %}</th>
|
||||
@ -48,16 +46,16 @@ configure your email client
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
<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>
|
||||
<th>{% trans %}Password{% endtrans %}</th>
|
||||
<td><pre>*******</pre></td>
|
||||
<td><pre class="pre-config border bg-light">*******</pre></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
{%- endcall %}
|
||||
{%- endblock %}
|
||||
|
@ -1,16 +1,16 @@
|
||||
{% extends "base.html" %}
|
||||
{%- extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{%- block title %}
|
||||
{% trans %}Confirm action{% endtrans %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{%- block subtitle %}
|
||||
{{ action }}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call macros.card(theme="warning") %}
|
||||
{%- block content %}
|
||||
{%- call macros.card(theme="warning") %}
|
||||
<p>{% trans action %}You are about to {{ action }}. Please confirm your action.{% endtrans %}</p>
|
||||
{{ macros.form(form) }}
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
{%- endcall %}
|
||||
{%- endblock %}
|
||||
|
@ -1,14 +1,14 @@
|
||||
{% extends "base.html" %}
|
||||
{%- extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{%- block title %}
|
||||
{% trans %}Docker error{% endtrans %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{%- block subtitle %}
|
||||
{{ action }}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block content %}
|
||||
{%- block content %}
|
||||
<p>{% trans action %}An error occurred while talking to the Docker server.{% endtrans %}</p>
|
||||
<pre>{{ error }}</pre>
|
||||
{% endblock %}
|
||||
<pre class="pre-config border bg-light">{{ error }}</pre>
|
||||
{%- endblock %}
|
||||
|
@ -1,21 +1,20 @@
|
||||
{% extends "base.html" %}
|
||||
{%- extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{%- block title %}
|
||||
{% trans %}New domain{% endtrans %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call macros.card() %}
|
||||
{%- block content %}
|
||||
{%- call macros.card() %}
|
||||
<form class="form" method="post" role="form">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ macros.form_field(form.name) }}
|
||||
{{ macros.form_fields((form.max_users, form.max_aliases)) }}
|
||||
{{ macros.form_field(form.max_quota_bytes, step=1000000000, max=50000000000,
|
||||
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>',
|
||||
oninput='$("#quota").text(this.value == 0 ? "∞" : this.value/1000000000);') }}
|
||||
{{ 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="max_quota_bytes_value"></span> GB</span>') }}
|
||||
{{ macros.form_field(form.signup_enabled) }}
|
||||
{{ macros.form_field(form.comment) }}
|
||||
{{ macros.form_field(form.submit) }}
|
||||
</form>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
{%- endcall %}
|
||||
{%- endblock %}
|
||||
|
@ -1,66 +1,71 @@
|
||||
{% extends "base.html" %}
|
||||
{%- extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{%- block title %}
|
||||
{% trans %}Domain details{% endtrans %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{%- block subtitle %}
|
||||
{{ domain.name }}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block main_action %}
|
||||
{% if current_user.global_admin %}
|
||||
{%- block main_action %}
|
||||
{%- if current_user.global_admin %}
|
||||
<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 %}
|
||||
{% else %}
|
||||
{%- else %}
|
||||
{% trans %}Generate keys{% endtrans %}
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{%- endif %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call macros.table(datatable=False) %}
|
||||
{% set hostname = config["HOSTNAMES"].split(",")[0] %}
|
||||
{%- block content %}
|
||||
{%- call macros.table(datatable=False) %}
|
||||
<tr>
|
||||
<th>{% trans %}Domain name{% endtrans %}</th>
|
||||
<td>{{ domain.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans %}DNS MX entry{% endtrans %} <i class="fa {{ 'fa-check-circle' if domain.check_mx() else 'fa-exclamation-circle' }}"></i></th>
|
||||
<td><pre>{{ domain.name }}. 600 IN MX 10 {{ hostname }}.</pre></td>
|
||||
<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>{{ macros.clip("dns_mx") }}<pre id="dns_mx" class="pre-config border bg-light">{{ domain.dns_mx }}</pre></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans %}DNS SPF entries{% endtrans %}</th>
|
||||
<td><pre>
|
||||
{{ domain.name }}. 600 IN TXT "v=spf1 mx a:{{ hostname }} -all"</pre></td>
|
||||
<td>{{ macros.clip("dns_spf") }}<pre id="dns_spf" class="pre-config border bg-light">{{ domain.dns_spf }}</pre>
|
||||
</td>
|
||||
</tr>
|
||||
{% if domain.dkim_publickey %}
|
||||
{%- if domain.dkim_publickey %}
|
||||
<tr>
|
||||
<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>
|
||||
<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>
|
||||
<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_dmarc") }}<pre id="dns_dmarc" class="pre-config border bg-light">{{ domain.dns_dmarc }}</pre>
|
||||
{{ macros.clip("dns_dmarc_report") }}<pre id="dns_dmarc_report" class="pre-config border bg-light">{{ domain.dns_dmarc_report }}</pre>
|
||||
</td>
|
||||
</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>
|
||||
<th>{% trans %}DNS client auto-configuration (RFC6186) entries{% endtrans %}</th>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
{% if config["TLS_FLAVOR"] != "notls" %}
|
||||
<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 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>
|
||||
<td>{{ macros.clip("dns_autoconfig") }}<pre id="dns_autoconfig" class="pre-config border bg-light">
|
||||
{%- for line in domain.dns_autoconfig %}
|
||||
{{ line }}
|
||||
{%- endfor -%}
|
||||
</pre></td>
|
||||
</tr>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
{%- endcall %}
|
||||
{%- endblock %}
|
||||
|
@ -1,9 +1,9 @@
|
||||
{% extends "domain/create.html" %}
|
||||
{%- extends "domain/create.html" %}
|
||||
|
||||
{% block title %}
|
||||
{%- block title %}
|
||||
{% trans %}Edit domain{% endtrans %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{%- block subtitle %}
|
||||
{{ domain }}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
@ -1,17 +1,17 @@
|
||||
{% extends "base.html" %}
|
||||
{%- extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{%- block title %}
|
||||
{% trans %}Domain list{% endtrans %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block main_action %}
|
||||
{% if current_user.global_admin %}
|
||||
{%- block main_action %}
|
||||
{%- if current_user.global_admin %}
|
||||
<a class="btn btn-primary float-right" href="{{ url_for('.domain_create') }}">{% trans %}New domain{% endtrans %}</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{%- endif %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call macros.table() %}
|
||||
{%- block content %}
|
||||
{%- call macros.table() %}
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans %}Actions{% endtrans %}</th>
|
||||
@ -25,31 +25,31 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for domain in current_user.get_managed_domains() %}
|
||||
{%- for domain in current_user.get_managed_domains() %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('.domain_details', domain_name=domain.name) }}" title="{% trans %}Details{% endtrans %}"><i class="fa fa-list"></i></a>
|
||||
{% 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>
|
||||
<a href="{{ url_for('.domain_delete', domain_name=domain.name) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for('.user_list', domain_name=domain.name) }}" title="{% trans %}Users{% endtrans %}"><i class="far fa-envelope"></i></a>
|
||||
<a href="{{ url_for('.alias_list', domain_name=domain.name) }}" title="{% trans %}Aliases{% endtrans %}"><i class="fa fa-at"></i></a>
|
||||
<a href="{{ url_for('.manager_list', domain_name=domain.name) }}" title="{% trans %}Managers{% endtrans %}"><i class="fa fa-user"></i></a>
|
||||
{% 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>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
</td>
|
||||
<td>{{ domain.name }}</td>
|
||||
<td>{{ domain.users | count }} / {{ '∞' if domain.max_users == -1 else domain.max_users }}</td>
|
||||
<td>{{ domain.aliases | count }} / {{ '∞' if domain.max_aliases == -1 else domain.max_aliases }}</td>
|
||||
<td>{{ domain.comment or '' }}</td>
|
||||
<td>{{ domain.created_at }}</td>
|
||||
<td>{{ domain.updated_at or '' }}</td>
|
||||
<td>{{ domain.created_at | format_date }}</td>
|
||||
<td>{{ domain.updated_at | format_date }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
</tbody>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
{%- endcall %}
|
||||
{%- endblock %}
|
||||
|
@ -1,18 +1,18 @@
|
||||
{% extends "base.html" %}
|
||||
{%- extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{%- block title %}
|
||||
{% trans %}Register a domain{% endtrans %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block content %}
|
||||
{%- block content %}
|
||||
|
||||
<form class="form" method="post" role="form">
|
||||
{{ 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
|
||||
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>
|
||||
{% 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
|
||||
expires.{% endtrans %}
|
||||
</p>
|
||||
{% endcall %}
|
||||
{%- endcall %}
|
||||
|
||||
{% call macros.card() %}
|
||||
{% if form.localpart %}
|
||||
{%- call macros.card() %}
|
||||
{%- if form.localpart %}
|
||||
{{ macros.form_fields((form.localpart, form.name), append='<span class="input-group-text">@</span>') }}
|
||||
{{ macros.form_fields((form.pw, form.pw2)) }}
|
||||
{% else %}
|
||||
{%- else %}
|
||||
{{ macros.form_field(form.name) }}
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
{{ macros.form_field(form.captcha) }}
|
||||
{{ macros.form_field(form.submit) }}
|
||||
{% endcall %}
|
||||
{%- endcall %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
@ -1,31 +1,31 @@
|
||||
{% extends "base.html" %}
|
||||
{%- extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{%- block title %}
|
||||
{% trans %}Add a fetched account{% endtrans %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{%- block subtitle %}
|
||||
{{ user }}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block content %}
|
||||
{%- block content %}
|
||||
<form class="form" method="post" role="form">
|
||||
{{ form.hidden_tag() }}
|
||||
{% call macros.card(title="Remote server") %}
|
||||
{%- call macros.card(title="Remote server") %}
|
||||
{{ macros.form_field(form.protocol) }}
|
||||
{{ macros.form_fields((form.host, form.port)) }}
|
||||
{{ 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.password) }}
|
||||
{% endcall %}
|
||||
{%- endcall %}
|
||||
|
||||
{% call macros.card(title="Settings") %}
|
||||
{%- call macros.card(title="Settings") %}
|
||||
{{ macros.form_field(form.keep) }}
|
||||
{% endcall %}
|
||||
{%- endcall %}
|
||||
|
||||
{{ macros.form_field(form.submit) }}
|
||||
</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 %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{%- block subtitle %}
|
||||
{{ user }}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
@ -1,19 +1,19 @@
|
||||
{% extends "base.html" %}
|
||||
{%- extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{%- block title %}
|
||||
{% trans %}Fetched accounts{% endtrans %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{%- block subtitle %}
|
||||
{{ 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>
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call macros.table() %}
|
||||
{%- block content %}
|
||||
{%- call macros.table() %}
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans %}Actions{% endtrans %}</th>
|
||||
@ -27,7 +27,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for fetch in user.fetches %}
|
||||
{%- for fetch in user.fetches %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('.fetch_edit', fetch_id=fetch.id) }}" title="{% trans %}Edit{% endtrans %}"><i class="fa fa-pencil"></i></a>
|
||||
@ -36,12 +36,12 @@
|
||||
<td>{{ fetch.protocol }}{{ 's' if fetch.tls else '' }}://{{ fetch.host }}:{{ fetch.port }}</td>
|
||||
<td>{{ fetch.username }}</td>
|
||||
<td>{% if fetch.keep %}{% trans %}yes{% endtrans %}{% else %}{% trans %}no{% endtrans %}{% endif %}</td>
|
||||
<td>{{ fetch.last_check or '-' }}</td>
|
||||
<td>{{ fetch.last_check | format_datetime or '-' }}</td>
|
||||
<td>{{ fetch.error or '-' }}</td>
|
||||
<td>{{ fetch.created_at }}</td>
|
||||
<td>{{ fetch.updated_at or '' }}</td>
|
||||
<td>{{ fetch.created_at | format_date }}</td>
|
||||
<td>{{ fetch.updated_at | format_date }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
</tbody>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
{%- endcall %}
|
||||
{%- endblock %}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{%- extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
{% call macros.card() %}
|
||||
{%- block content %}
|
||||
{%- call macros.card() %}
|
||||
{{ macros.form(form) }}
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
{%- endcall %}
|
||||
{%- endblock %}
|
||||
|
@ -1,9 +0,0 @@
|
||||
{% extends "form.html" %}
|
||||
|
||||
{% block title %}
|
||||
{% trans %}Sign in{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{% trans %}to access the administration tools{% endtrans %}
|
||||
{% endblock %}
|
@ -1,104 +1,133 @@
|
||||
{% macro form_errors(form) %}
|
||||
{% if form.errors %}
|
||||
{% for fieldname, errors in form.errors.items() %}
|
||||
{% if bootstrap_is_hidden_field(form[fieldname]) %}
|
||||
{% for error in errors %}
|
||||
{%- macro form_errors(form) %}
|
||||
{%- if form.errors %}
|
||||
{%- for fieldname, errors in form.errors.items() %}
|
||||
{%- if bootstrap_is_hidden_field(form[fieldname]) %}
|
||||
{%- for error in errors %}
|
||||
<p class="error">{{error}}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
{%- endfor %}
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
{%- endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro form_field_errors(field) %}
|
||||
{% if field.errors %}
|
||||
{% for error in field.errors %}
|
||||
{%- macro form_field_errors(field) %}
|
||||
{%- if field.errors %}
|
||||
{%- for error in field.errors %}
|
||||
<p class="help-block inline">{{ error }}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
{%- endfor %}
|
||||
{%- endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro form_fields(fields, prepend='', append='', label=True) %}
|
||||
{% set width = (12 / fields|length)|int %}
|
||||
{%- macro form_fields(fields, prepend='', append='', label=True) %}
|
||||
{%- set width = (12 / fields|length)|int %}
|
||||
<div class="form-group">
|
||||
<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 '' }}">
|
||||
{{ form_individual_field(field, prepend=prepend, append=append, label=label, **kwargs) }}
|
||||
{%- if field.__class__.__name__ == 'list' %}
|
||||
{%- for subfield in field %}
|
||||
{{ form_individual_field(subfield, prepend=prepend, append=append, label=label, **kwargs) }}
|
||||
{%- endfor %}
|
||||
{%- else %}
|
||||
{{ form_individual_field(field, prepend=prepend, append=append, label=label, **kwargs) }}
|
||||
{%- endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro form_individual_field(field, prepend='', append='', label=True, class_="") %}
|
||||
{% if field.type == "BooleanField" %}
|
||||
{{ field(**kwargs) }}<span> </span>
|
||||
{{ field.label if label else '' }}
|
||||
{% else %}
|
||||
{%- macro form_individual_field(field, prepend='', append='', label=True, class_="") %}
|
||||
{%- if field.type == "BooleanField" %}
|
||||
{{ field(**kwargs) }}<span> </span>{{ field.label if label else '' }}
|
||||
{%- else %}
|
||||
{{ field.label if label else '' }}{{ form_field_errors(field) }}
|
||||
{% if prepend %}<div class="input-group-prepend">{% endif %}
|
||||
{% if append %}<div class="input-group-append">{% endif %}
|
||||
{{ prepend|safe }}{{ field(class_="form-control " + class_, **kwargs) }}{{ append|safe }}
|
||||
{% if prepend or append %}</div>{% endif %}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
{%- if prepend %}<div class="input-group-prepend">{%- elif append %}<div class="input-group-append">{%- endif %}
|
||||
{{ prepend|safe }}{{ field(class_=("form-control " + class_) if class_ else "form-control", **kwargs) }}{{ append|safe }}
|
||||
{%- if prepend or append %}</div>{%- endif %}
|
||||
{%- endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro form_field(field) %}
|
||||
{% if field.type == 'SubmitField' %}
|
||||
{{ form_fields((field,), label=False, class="btn btn-default", **kwargs) }}
|
||||
{% else %}
|
||||
{{ form_fields((field,), **kwargs) }}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
{%- macro form_field(field) %}
|
||||
{%- if field.type == 'SubmitField' %}
|
||||
{{- form_fields((field,), label=False, class="btn btn-default", **kwargs) }}
|
||||
{%- else %}
|
||||
{{- form_fields((field,), **kwargs) }}
|
||||
{%- endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro form(form) %}
|
||||
{%- macro form(form) %}
|
||||
<form class="form" method="post" role="form">
|
||||
{{ form.hidden_tag() }}
|
||||
{% for field in form %}
|
||||
{% if bootstrap_is_hidden_field(field) %}
|
||||
{%- for field in form %}
|
||||
{%- if bootstrap_is_hidden_field(field) %}
|
||||
{{ field() }}
|
||||
{% else %}
|
||||
{%- else %}
|
||||
{{ form_field(field) }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
</form>
|
||||
{% endmacro %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro card(title=None, theme="primary", header=True) %}
|
||||
{%- macro card(title=None, theme="primary", header=True) %}
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="card card-outline card-{{ theme }}">
|
||||
{% if header %}
|
||||
{%- if header %}
|
||||
<div class="card-header border-0">
|
||||
{% if title %}
|
||||
{%- if title %}
|
||||
<h3 class="card-title">{{ title }}</h3>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
<div class="card-body">
|
||||
{{ caller() }}
|
||||
{{- caller() }}
|
||||
</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="col-lg-12">
|
||||
<div class="card card-outline card-{{ theme }}">
|
||||
{%- if title %}
|
||||
<div class="card-header border-0">
|
||||
{% if title %}
|
||||
<h3 class="card-title">{{ title }}</h3>
|
||||
{% endif %}
|
||||
</div>
|
||||
{%- endif %}
|
||||
<div class="card-body">
|
||||
<table class="table table-bordered {% if datatable %} dataTable {% endif %}">
|
||||
{{ caller() }}
|
||||
<table class="table table-bordered{% if datatable %} dataTable{% endif %}">
|
||||
{{- caller() }}
|
||||
</table>
|
||||
</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 %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{%- block subtitle %}
|
||||
{{ domain }}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call macros.card() %}
|
||||
{%- block content %}
|
||||
{%- call macros.card() %}
|
||||
<form class="form" method="post" role="form">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ macros.form_field(form.manager, class_='mailselect') }}
|
||||
{{ macros.form_field(form.submit) }}
|
||||
</form>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
{%- endcall %}
|
||||
{%- endblock %}
|
||||
|
@ -1,19 +1,19 @@
|
||||
{% extends "base.html" %}
|
||||
{%- extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{%- block title %}
|
||||
{% trans %}Manager list{% endtrans %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{%- block subtitle %}
|
||||
{{ 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>
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call macros.table() %}
|
||||
{%- block content %}
|
||||
{%- call macros.table() %}
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans %}Actions{% endtrans %}</th>
|
||||
@ -21,14 +21,14 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for manager in domain.managers %}
|
||||
{%- for manager in domain.managers %}
|
||||
<tr>
|
||||
<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>
|
||||
</td>
|
||||
<td>{{ manager }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
</tbody>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
{%- endcall %}
|
||||
{%- endblock %}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{% extends "form.html" %}
|
||||
{%- extends "form.html" %}
|
||||
|
||||
{% block title %}
|
||||
{%- block title %}
|
||||
{% 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 %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{%- block subtitle %}
|
||||
{{ relay }}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
@ -1,17 +1,17 @@
|
||||
{% extends "base.html" %}
|
||||
{%- extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{%- block title %}
|
||||
{% trans %}Relayed domain list{% endtrans %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block main_action %}
|
||||
{% if current_user.global_admin %}
|
||||
{%- block main_action %}
|
||||
{%- if current_user.global_admin %}
|
||||
<a class="btn btn-primary float-right" href="{{ url_for('.relay_create') }}">{% trans %}New relayed domain{% endtrans %}</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{%- endif %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call macros.table() %}
|
||||
{%- block content %}
|
||||
{%- call macros.table() %}
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans %}Actions{% endtrans %}</th>
|
||||
@ -23,7 +23,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for relay in relays %}
|
||||
{%- for relay in relays %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('.relay_edit', relay_name=relay.name) }}" title="{% trans %}Edit{% endtrans %}"><i class="fa fa-pencil"></i></a>
|
||||
@ -32,10 +32,10 @@
|
||||
<td>{{ relay.name }}</td>
|
||||
<td>{{ relay.smtp or '-' }}</td>
|
||||
<td>{{ relay.comment or '' }}</td>
|
||||
<td>{{ relay.created_at }}</td>
|
||||
<td>{{ relay.updated_at or '' }}</td>
|
||||
<td>{{ relay.created_at | format_date }}</td>
|
||||
<td>{{ relay.updated_at | format_date }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
</tbody>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
{%- endcall %}
|
||||
{%- endblock %}
|
||||
|
@ -1,144 +1,156 @@
|
||||
<div class="sidebar">
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="sidebar text-sm">
|
||||
{%- if current_user.is_authenticated %}
|
||||
<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">
|
||||
<span class="text-center text-primary">{{ current_user }}</span>
|
||||
<a href="{{ url_for('.user_settings') }}" class="d-block">{{ current_user }}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{%- endif %}
|
||||
<nav class="mt-2">
|
||||
<ul class="nav nav-pills nav-sidebar flex-column" role="menu">
|
||||
{% if current_user.is_authenticated %}
|
||||
<li class="nav-header">{% trans %}My account{% endtrans %}</li>
|
||||
<li class="nav-item">
|
||||
<a href="{{ url_for('.user_settings') }}" class="nav-link">
|
||||
{%- if current_user.is_authenticated %}
|
||||
<li class="nav-header text-uppercase text-primary" role="none">{% trans %}My account{% endtrans %}</li>
|
||||
<li class="nav-item" role="none">
|
||||
<a href="{{ url_for('.user_settings') }}" class="nav-link" role="menuitem">
|
||||
<i class="nav-icon fa fa-wrench"></i>
|
||||
<p class="text">{% trans %}Settings{% endtrans %}</p>
|
||||
<p>{% trans %}Settings{% endtrans %}</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="{{ url_for('.user_password') }}" class="nav-link">
|
||||
<li class="nav-item" role="none">
|
||||
<a href="{{ url_for('.user_password') }}" class="nav-link" role="menuitem">
|
||||
<i class="nav-icon fa fa-lock"></i>
|
||||
<p class="text">{% trans %}Update password{% endtrans %}</p>
|
||||
<p>{% trans %}Update password{% endtrans %}</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="{{ url_for('.user_reply') }}" class="nav-link">
|
||||
<li class="nav-item" role="none">
|
||||
<a href="{{ url_for('.user_reply') }}" class="nav-link" role="menuitem">
|
||||
<i class="nav-icon fa fa-plane"></i>
|
||||
<p class="text">{% trans %}Auto-reply{% endtrans %}</p>
|
||||
<p>{% trans %}Auto-reply{% endtrans %}</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="{{ url_for('.fetch_list') }}" class="nav-link">
|
||||
<li class="nav-item" role="none">
|
||||
<a href="{{ url_for('.fetch_list') }}" class="nav-link" role="menuitem">
|
||||
<i class="nav-icon fas fa-download"></i>
|
||||
<p class="text">{% trans %}Fetched accounts{% endtrans %}</p>
|
||||
<p>{% trans %}Fetched accounts{% endtrans %}</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="{{ url_for('.token_list') }}" class="nav-link">
|
||||
<li class="nav-item" role="none">
|
||||
<a href="{{ url_for('.token_list') }}" class="nav-link" role="menuitem">
|
||||
<i class="nav-icon fas fa-ticket-alt"></i>
|
||||
<p class="text">{% trans %}Authentication tokens{% endtrans %}</p>
|
||||
<p>{% trans %}Authentication tokens{% endtrans %}</p>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if current_user.manager_of or current_user.global_admin %}
|
||||
<li class="nav-header">{% trans %}Administration{% endtrans %}</li>
|
||||
{% 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">
|
||||
{%- if 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 class="text">{% trans %}Client setup{% endtrans %}</p>
|
||||
<p>{% trans %}Client setup{% endtrans %}</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="{{ config["WEBSITE"] }}" target="_blank" class="nav-link">
|
||||
{%- endif %}
|
||||
|
||||
{%- 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" and current_user.is_authenticated %}
|
||||
<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>
|
||||
<p class="text">{% trans %}Website{% endtrans %}</p>
|
||||
<p>{% trans %}Website{% endtrans %} <i class="fas fa-external-link-alt text-xs"></i></p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="https://mailu.io" target="_blank" class="nav-link">
|
||||
<li class="nav-item" role="none">
|
||||
<a href="https://mailu.io" target="_blank" class="nav-link" role="menuitem" rel="noreferrer">
|
||||
<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>
|
||||
</li>
|
||||
{% if config['DOMAIN_REGISTRATION'] %}
|
||||
<li class="nav-item">
|
||||
<a href="{{ url_for('.domain_signup') }}" class="nav-link">
|
||||
{%- if config['DOMAIN_REGISTRATION'] %}
|
||||
<li class="nav-item" role="none">
|
||||
<a href="{{ url_for('.domain_signup') }}" class="nav-link" role="menuitem">
|
||||
<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>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if current_user.is_authenticated %}
|
||||
<li class="nav-item">
|
||||
<a href="{{ url_for('.logout') }}" class="nav-link">
|
||||
{%- endif %}
|
||||
{%- if current_user.is_authenticated %}
|
||||
<li class="nav-item" role="none">
|
||||
<a href="{{ url_for('sso.logout') }}" class="nav-link" role="menuitem">
|
||||
<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>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<a href="{{ url_for('.login') }}" class="nav-link">
|
||||
<li class="nav-item" role="none">
|
||||
<a href="{{ url_for('sso.login') }}" class="nav-link" role="menuitem">
|
||||
<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>
|
||||
</li>
|
||||
{% if signup_domains %}
|
||||
<li class="nav-item">
|
||||
<a href="{{ url_for('.user_signup') }}" class="nav-link">
|
||||
{%- if signup_domains %}
|
||||
<li class="nav-item" role="none">
|
||||
<a href="{{ url_for('.user_signup') }}" class="nav-link" role="menuitem">
|
||||
<i class="nav-icon fa fa-user-plus"></i>
|
||||
<p class="text">{% trans %}Sign up{% endtrans %}</p>
|
||||
<p>{% trans %}Sign up{% endtrans %}</p>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
@ -1,9 +1,9 @@
|
||||
{% extends "form.html" %}
|
||||
{%- extends "form.html" %}
|
||||
|
||||
{% block title %}
|
||||
{%- block title %}
|
||||
{% trans %}Create an authentication token{% endtrans %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{%- block subtitle %}
|
||||
{{ user }}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
@ -1,38 +1,40 @@
|
||||
{% extends "base.html" %}
|
||||
{%- extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{%- block title %}
|
||||
{% trans %}Authentication tokens{% endtrans %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{%- block subtitle %}
|
||||
{{ 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>
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call macros.table() %}
|
||||
{%- block content %}
|
||||
{%- call macros.table() %}
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans %}Actions{% endtrans %}</th>
|
||||
<th>{% trans %}Comment{% endtrans %}</th>
|
||||
<th>{% trans %}Authorized IP{% endtrans %}</th>
|
||||
<th>{% trans %}Created{% endtrans %}</th>
|
||||
<th>{% trans %}Last edit{% endtrans %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for token in user.tokens %}
|
||||
{%- for token in user.tokens %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('.token_delete', token_id=token.id) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
|
||||
</td>
|
||||
<td>{{ token.comment }}</td>
|
||||
<td>{{ token.ip or "any" }}</td>
|
||||
<td>{{ token.created_at }}</td>
|
||||
<td>{{ token.created_at | format_date }}</td>
|
||||
<td>{{ token.updated_at | format_date }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
</tbody>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
{%- endcall %}
|
||||
{%- endblock %}
|
||||
|
@ -1,33 +1,32 @@
|
||||
{% extends "base.html" %}
|
||||
{%- extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{%- block title %}
|
||||
{% trans %}New user{% endtrans %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{%- block subtitle %}
|
||||
{{ domain.name }}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block content %}
|
||||
{%- block content %}
|
||||
<form class="form" method="post" role="form">
|
||||
{{ 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_fields((form.pw, form.pw2)) }}
|
||||
{{ macros.form_field(form.displayed_name) }}
|
||||
{{ macros.form_field(form.comment) }}
|
||||
{{ macros.form_field(form.enabled) }}
|
||||
{% endcall %}
|
||||
{%- endcall %}
|
||||
|
||||
{% 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),
|
||||
prepend='<span class="input-group-text"><span id="quota">'+((form.quota_bytes.data//1000000000).__str__() if form.quota_bytes.data else '∞')+'</span> GiB</span>',
|
||||
oninput='$("#quota").text(this.value == 0 ? "∞" : this.value/1000000000);') }}
|
||||
{%- 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 50*10**9), data_infinity="true",
|
||||
prepend='<span class="input-group-text"><span id="quota_bytes_value"></span> GB</span>') }}
|
||||
{{ macros.form_field(form.enable_imap) }}
|
||||
{{ macros.form_field(form.enable_pop) }}
|
||||
{% endcall %}
|
||||
{%- endcall %}
|
||||
|
||||
{{ macros.form_field(form.submit) }}
|
||||
</form>
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
@ -1,9 +1,9 @@
|
||||
{% extends "user/create.html" %}
|
||||
{%- extends "user/create.html" %}
|
||||
|
||||
{% block title %}
|
||||
{%- block title %}
|
||||
{% trans %}Edit user{% endtrans %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{%- block subtitle %}
|
||||
{{ 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 %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{%- block subtitle %}
|
||||
{{ 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>
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call macros.table() %}
|
||||
{%- block content %}
|
||||
{%- call macros.table() %}
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans %}Actions{% endtrans %}</th>
|
||||
@ -27,8 +27,8 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in domain.users %}
|
||||
<tr {% if not user.enabled %}class="warning"{% endif %}>
|
||||
{%- for user in domain.users %}
|
||||
<tr{% if not user.enabled %} class="warning"{% endif %}>
|
||||
<td>
|
||||
<a href="{{ url_for('.user_edit', user_email=user.email) }}" title="{% trans %}Edit{% endtrans %}"><i class="fas fa-pencil-alt"></i></a>
|
||||
<a href="{{ url_for('.user_delete', user_email=user.email) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
|
||||
@ -45,10 +45,10 @@
|
||||
</td>
|
||||
<td>{{ user.quota_bytes_used | filesizeformat }} / {{ (user.quota_bytes | filesizeformat) if user.quota_bytes else '∞' }}</td>
|
||||
<td>{{ user.comment or '-' }}</td>
|
||||
<td>{{ user.created_at }}</td>
|
||||
<td>{{ user.updated_at or '' }}</td>
|
||||
<td>{{ user.created_at | format_date }}</td>
|
||||
<td>{{ user.updated_at | format_date }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
</tbody>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
{%- endcall %}
|
||||
{%- endblock %}
|
||||
|
@ -1,9 +1,9 @@
|
||||
{% extends "form.html" %}
|
||||
{%- extends "form.html" %}
|
||||
|
||||
{% block title %}
|
||||
{%- block title %}
|
||||
{% trans %}Password update{% endtrans %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{%- block subtitle %}
|
||||
{{ user }}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
@ -1,30 +1,23 @@
|
||||
{% extends "base.html" %}
|
||||
{%- extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{%- block title %}
|
||||
{% trans %}Automatic reply{% endtrans %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{%- block subtitle %}
|
||||
{{ user }}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call macros.card() %}
|
||||
{%- block content %}
|
||||
{%- call macros.card() %}
|
||||
<form class="form" method="post" role="form">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ macros.form_field(form.reply_enabled,
|
||||
onchange="if(this.checked){$('#reply_subject,#reply_body,#reply_enddate,#reply_startdate').removeAttr('readonly')}
|
||||
else{$('#reply_subject,#reply_body,#reply_enddate,#reply_startdate').attr('readonly', '')}") }}
|
||||
{{ macros.form_field(form.reply_subject,
|
||||
**{("rw" if user.reply_enabled else "readonly"): ""}) }}
|
||||
{{ 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"): ""}) }}
|
||||
|
||||
{%- call macros.fieldset(
|
||||
field=form.reply_enabled,
|
||||
enabled=user.reply_enabled,
|
||||
fields=[form.reply_subject, form.reply_body, form.reply_enddate, form.reply_startdate]) %}
|
||||
{%- endcall %}
|
||||
{{ macros.form_field(form.submit) }}
|
||||
</form>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
{%- endcall %}
|
||||
{%- endblock %}
|
||||
|
@ -1,38 +1,36 @@
|
||||
{% extends "base.html" %}
|
||||
{%- extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{%- block title %}
|
||||
{% trans %}User settings{% endtrans %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{%- block subtitle %}
|
||||
{{ user }}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block content %}
|
||||
{%- block content %}
|
||||
<form class="form" method="post" role="form">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
{% call macros.card(title=_("Displayed name")) %}
|
||||
|
||||
{%- call macros.card(title=_("Displayed name")) %}
|
||||
{{ macros.form_field(form.displayed_name) }}
|
||||
{% endcall %}
|
||||
{%- endcall %}
|
||||
|
||||
{% call macros.card(title=_("Antispam")) %}
|
||||
{{ macros.form_field(form.spam_enabled) }}
|
||||
{%- call macros.card(title=_("Antispam")) %}
|
||||
{%- call macros.fieldset(field=form.spam_enabled, enabled=user.spam_enabled) %}
|
||||
{{ 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> / 100</span>',
|
||||
oninput='$("#threshold").text(this.value);') }}
|
||||
{% endcall %}
|
||||
prepend='<span class="input-group-text"><span id="spam_threshold_value"></span> / 100</span>') }}
|
||||
{%- endcall %}
|
||||
{%- endcall %}
|
||||
|
||||
{% call macros.card(title=_("Auto-forward")) %}
|
||||
{{ 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"): ""}) }}
|
||||
{% endcall %}
|
||||
{%- call macros.card(title=_("Auto-forward")) %}
|
||||
{%- call macros.fieldset(
|
||||
field=form.forward_enabled,
|
||||
enabled=user.forward_enabled,
|
||||
fields=[form.forward_keep, form.forward_destination]) %}
|
||||
{%- endcall %}
|
||||
{%- endcall %}
|
||||
|
||||
{{ macros.form_field(form.submit) }}
|
||||
</form>
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
@ -1,23 +1,23 @@
|
||||
{% extends "base.html" %}
|
||||
{%- extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{%- block title %}
|
||||
{% trans %}Sign up{% endtrans %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{%- block subtitle %}
|
||||
{{ domain }}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block content %}
|
||||
{%- block content %}
|
||||
<form class="form" method="post" role="form">
|
||||
{{ 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_fields((form.pw, form.pw2)) }}
|
||||
{% if form.captcha %}
|
||||
{%- if form.captcha %}
|
||||
{{ macros.form_field(form.captcha) }}
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
{{ macros.form_field(form.submit) }}
|
||||
{% endcall %}
|
||||
{%- endcall %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
@ -1,26 +1,26 @@
|
||||
{% extends "base.html" %}
|
||||
{%- extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{%- block title %}
|
||||
{% trans %}Sign up{% endtrans %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{%- block subtitle %}
|
||||
{% trans %}pick a domain for the new account{% endtrans %}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% call macros.table() %}
|
||||
{%- block content %}
|
||||
{%- call macros.table() %}
|
||||
<tr>
|
||||
<th>{% trans %}Domain{% endtrans %}</th>
|
||||
<th>{% trans %}Available slots{% endtrans %}</th>
|
||||
<th>{% trans %}Quota{% endtrans %}</th>
|
||||
</tr>
|
||||
{% for domain_name, domain in available_domains.items() %}
|
||||
{%- for domain_name, domain in available_domains.items() %}
|
||||
<tr>
|
||||
<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>{{ domain.max_quota_bytes or config['DEFAULT_QUOTA'] | filesizeformat }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
{%- endfor %}
|
||||
{%- endcall %}
|
||||
{%- 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>
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
@ -1,4 +1,4 @@
|
||||
from mailu import models
|
||||
from mailu import models, utils
|
||||
from mailu.ui import ui, forms, access
|
||||
|
||||
from flask import current_app as app
|
||||
@ -11,31 +11,6 @@ import flask_login
|
||||
def index():
|
||||
return flask.redirect(flask.url_for('.user_settings'))
|
||||
|
||||
|
||||
@ui.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
form = forms.LoginForm()
|
||||
if form.validate_on_submit():
|
||||
user = models.User.login(form.email.data, form.pw.data)
|
||||
if user:
|
||||
flask.session.regenerate()
|
||||
flask_login.login_user(user)
|
||||
endpoint = flask.request.args.get('next', '.index')
|
||||
return flask.redirect(flask.url_for(endpoint)
|
||||
or flask.url_for('.index'))
|
||||
else:
|
||||
flask.flash('Wrong e-mail or password', 'error')
|
||||
return flask.render_template('login.html', form=form)
|
||||
|
||||
|
||||
@ui.route('/logout', methods=['GET'])
|
||||
@access.authenticated
|
||||
def logout():
|
||||
flask_login.logout_user()
|
||||
flask.session.destroy()
|
||||
return flask.redirect(flask.url_for('.index'))
|
||||
|
||||
|
||||
@ui.route('/announcement', methods=['GET', 'POST'])
|
||||
@access.global_admin
|
||||
def announcement():
|
||||
@ -57,3 +32,7 @@ def webmail():
|
||||
@ui.route('/client', methods=['GET'])
|
||||
def client():
|
||||
return flask.render_template('client.html')
|
||||
|
||||
@ui.route('/webui_antispam', methods=['GET'])
|
||||
def antispam():
|
||||
return flask.render_template('antispam.html')
|
||||
|
@ -5,7 +5,6 @@ from flask import current_app as app
|
||||
import flask
|
||||
import flask_login
|
||||
import wtforms_components
|
||||
import dns.resolver
|
||||
|
||||
|
||||
@ui.route('/domain', methods=['GET'])
|
||||
|
@ -2,8 +2,8 @@ from mailu.ui import ui, forms, access
|
||||
|
||||
import flask
|
||||
|
||||
|
||||
@ui.route('/language/<language>', methods=['GET'])
|
||||
@ui.route('/language/<language>', methods=['POST'])
|
||||
def set_language(language=None):
|
||||
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)
|
||||
|
||||
|
||||
@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/<path:user_email>', methods=['GET', 'POST'])
|
||||
@access.owner(models.User, 'user_email')
|
||||
|
@ -6,55 +6,96 @@ try:
|
||||
except ImportError:
|
||||
import pickle
|
||||
|
||||
import dns.resolver
|
||||
import dns.exception
|
||||
import dns.flags
|
||||
import dns.rdtypes
|
||||
import dns.rdatatype
|
||||
import dns.rdataclass
|
||||
|
||||
import hmac
|
||||
import secrets
|
||||
import time
|
||||
|
||||
from multiprocessing import Value
|
||||
|
||||
from mailu import limiter
|
||||
from flask import current_app as app
|
||||
|
||||
import flask
|
||||
import flask_login
|
||||
import flask_migrate
|
||||
import flask_babel
|
||||
import ipaddress
|
||||
import redis
|
||||
|
||||
from flask.sessions import SessionMixin, SessionInterface
|
||||
from itsdangerous.encoding import want_bytes
|
||||
from werkzeug.datastructures import CallbackDict
|
||||
from werkzeug.contrib import fixers
|
||||
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
|
||||
# Login configuration
|
||||
login = flask_login.LoginManager()
|
||||
login.login_view = "ui.login"
|
||||
login.login_view = "sso.login"
|
||||
|
||||
@login.unauthorized_handler
|
||||
def handle_needs_login():
|
||||
""" redirect unauthorized requests to login page """
|
||||
return flask.redirect(
|
||||
flask.url_for('ui.login', next=flask.request.endpoint)
|
||||
flask.url_for('sso.login')
|
||||
)
|
||||
|
||||
# DNS stub configured to do DNSSEC enabled queries
|
||||
resolver = dns.resolver.Resolver()
|
||||
resolver.use_edns(0, 0, 1232)
|
||||
resolver.flags = dns.flags.AD | dns.flags.RD
|
||||
|
||||
def has_dane_record(domain, timeout=10):
|
||||
try:
|
||||
result = resolver.query(f'_25._tcp.{domain}', dns.rdatatype.TLSA,dns.rdataclass.IN, lifetime=timeout)
|
||||
if result.response.flags & dns.flags.AD:
|
||||
for record in result:
|
||||
if isinstance(record, dns.rdtypes.ANY.TLSA.TLSA):
|
||||
record.validate()
|
||||
if record.usage in [2,3] and record.selector in [0,1] and record.mtype in [0,1,2]:
|
||||
return True
|
||||
except dns.resolver.NoNameservers:
|
||||
# If the DNSSEC data is invalid and the DNS resolver is DNSSEC enabled
|
||||
# we will receive this non-specific exception. The safe behaviour is to
|
||||
# accept to defer the email.
|
||||
app.logger.warn(f'Unable to lookup the TLSA record for {domain}. Is the DNSSEC zone okay on https://dnsviz.net/d/{domain}/dnssec/?')
|
||||
return app.config['DEFER_ON_TLS_ERROR']
|
||||
except dns.exception.Timeout:
|
||||
app.logger.warn(f'Timeout while resolving the TLSA record for {domain} ({timeout}s).')
|
||||
except dns.resolver.NXDOMAIN:
|
||||
pass # this is expected, not TLSA record is fine
|
||||
except Exception as e:
|
||||
app.logger.error(f'Error while looking up the TLSA record for {domain} {e}')
|
||||
pass
|
||||
|
||||
# Rate limiter
|
||||
limiter = limiter.LimitWraperFactory()
|
||||
|
||||
def extract_network_from_ip(ip):
|
||||
n = ipaddress.ip_network(ip)
|
||||
if n.version == 4:
|
||||
return str(n.supernet(prefixlen_diff=(32-int(app.config["AUTH_RATELIMIT_IP_V4_MASK"]))).network_address)
|
||||
else:
|
||||
return str(n.supernet(prefixlen_diff=(128-int(app.config["AUTH_RATELIMIT_IP_V6_MASK"]))).network_address)
|
||||
|
||||
def is_exempt_from_ratelimits(ip):
|
||||
ip = ipaddress.ip_address(ip)
|
||||
return any(ip in cidr for cidr in app.config['AUTH_RATELIMIT_EXEMPTION'])
|
||||
|
||||
# Application translation
|
||||
babel = flask_babel.Babel()
|
||||
|
||||
@babel.localeselector
|
||||
def get_locale():
|
||||
""" selects locale for translation """
|
||||
translations = list(map(str, babel.list_translations()))
|
||||
flask.session['available_languages'] = translations
|
||||
|
||||
try:
|
||||
language = flask.session['language']
|
||||
except KeyError:
|
||||
language = flask.request.accept_languages.best_match(translations)
|
||||
language = flask.session.get('language')
|
||||
if not language in app.config.translations:
|
||||
language = flask.request.accept_languages.best_match(app.config.translations.keys())
|
||||
flask.session['language'] = language
|
||||
|
||||
return language
|
||||
|
||||
|
||||
@ -65,13 +106,10 @@ class PrefixMiddleware(object):
|
||||
self.app = None
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
prefix = environ.get('HTTP_X_FORWARDED_PREFIX', '')
|
||||
if prefix:
|
||||
environ['SCRIPT_NAME'] = prefix
|
||||
return self.app(environ, start_response)
|
||||
|
||||
def init_app(self, app):
|
||||
self.app = fixers.ProxyFix(app.wsgi_app, x_for=1, x_proto=1)
|
||||
self.app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1)
|
||||
app.wsgi_app = self
|
||||
|
||||
proxy = PrefixMiddleware()
|
||||
@ -230,7 +268,7 @@ class MailuSession(CallbackDict, SessionMixin):
|
||||
|
||||
# set uid from dict data
|
||||
if self._uid is None:
|
||||
self._uid = self.app.session_config.gen_uid(self.get('user_id', ''))
|
||||
self._uid = self.app.session_config.gen_uid(self.get('_user_id', ''))
|
||||
|
||||
# create new session id for new or regenerated sessions and force setting the cookie
|
||||
if self._sid is None:
|
||||
@ -450,7 +488,7 @@ class MailuSessionExtension:
|
||||
with cleaned.get_lock():
|
||||
if not cleaned.value:
|
||||
cleaned.value = True
|
||||
flask.current_app.logger.error('cleaning')
|
||||
app.logger.info('cleaning session store')
|
||||
MailuSessionExtension.cleanup_sessions(app)
|
||||
|
||||
app.before_first_request(cleaner)
|
||||
|
@ -1,10 +1,12 @@
|
||||
from __future__ import with_statement
|
||||
import logging
|
||||
import tenacity
|
||||
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
from logging.config import fileConfig
|
||||
import logging
|
||||
import tenacity
|
||||
from tenacity import retry
|
||||
|
||||
from flask import current_app
|
||||
from mailu import models
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
@ -17,34 +19,26 @@ logger = logging.getLogger('alembic.env')
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
from flask import current_app
|
||||
config.set_main_option('sqlalchemy.url',
|
||||
current_app.config.get('SQLALCHEMY_DATABASE_URI'))
|
||||
#target_metadata = current_app.extensions['migrate'].db.metadata
|
||||
from mailu import models
|
||||
config.set_main_option(
|
||||
'sqlalchemy.url',
|
||||
current_app.config.get('SQLALCHEMY_DATABASE_URI')
|
||||
)
|
||||
target_metadata = models.Base.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
url = config.get_main_option('sqlalchemy.url')
|
||||
context.configure(url=url)
|
||||
|
||||
with context.begin_transaction():
|
||||
@ -69,28 +63,35 @@ def run_migrations_online():
|
||||
directives[:] = []
|
||||
logger.info('No changes in schema detected.')
|
||||
|
||||
engine = engine_from_config(config.get_section(config.config_ini_section),
|
||||
prefix='sqlalchemy.',
|
||||
poolclass=pool.NullPool)
|
||||
engine = engine_from_config(
|
||||
config.get_section(config.config_ini_section),
|
||||
prefix = 'sqlalchemy.',
|
||||
poolclass = pool.NullPool
|
||||
)
|
||||
|
||||
connection = tenacity.Retrying(
|
||||
stop=tenacity.stop_after_attempt(100),
|
||||
wait=tenacity.wait_random(min=2, max=5),
|
||||
before=tenacity.before_log(logging.getLogger("tenacity.retry"), logging.DEBUG),
|
||||
before_sleep=tenacity.before_sleep_log(logging.getLogger("tenacity.retry"), logging.INFO),
|
||||
after=tenacity.after_log(logging.getLogger("tenacity.retry"), logging.DEBUG)
|
||||
).call(engine.connect)
|
||||
@tenacity.retry(
|
||||
stop = tenacity.stop_after_attempt(100),
|
||||
wait = tenacity.wait_random(min=2, max=5),
|
||||
before = tenacity.before_log(logging.getLogger('tenacity.retry'), logging.DEBUG),
|
||||
before_sleep = tenacity.before_sleep_log(logging.getLogger('tenacity.retry'), logging.INFO),
|
||||
after = tenacity.after_log(logging.getLogger('tenacity.retry'), logging.DEBUG)
|
||||
)
|
||||
def try_connect(db):
|
||||
return db.connect()
|
||||
|
||||
context.configure(connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
process_revision_directives=process_revision_directives,
|
||||
**current_app.extensions['migrate'].configure_args)
|
||||
with try_connect(engine) as connection:
|
||||
|
||||
context.configure(
|
||||
connection = connection,
|
||||
target_metadata = target_metadata,
|
||||
process_revision_directives = process_revision_directives,
|
||||
**current_app.extensions['migrate'].configure_args
|
||||
)
|
||||
|
||||
try:
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
connection.close()
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
|
@ -12,20 +12,19 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.15.0",
|
||||
"@babel/preset-env": "^7.15.0",
|
||||
"admin-lte": "^3.1.0",
|
||||
"babel-loader": "^8.2.2",
|
||||
"clipboard": "^2.0.8",
|
||||
"compression-webpack-plugin": "^8.0.1",
|
||||
"css-loader": "^6.2.0",
|
||||
"expose-loader": "^3.0.0",
|
||||
"jquery": "^3.6.0",
|
||||
"less": "^4.1.1",
|
||||
"css-minimizer-webpack-plugin": "^3.0.2",
|
||||
"datatables.net-plugins": "^1.10.24",
|
||||
"import-glob": "^1.5.0",
|
||||
"less-loader": "^10.0.1",
|
||||
"mini-css-extract-plugin": "^2.2.0",
|
||||
"node-sass": "^6.0.1",
|
||||
"sass": "<1.33.0",
|
||||
"sass-loader": "^12.1.0",
|
||||
"select2": "^4.0.13",
|
||||
"webpack": "^5.50.0",
|
||||
"webpack-cli": "^4.7.2"
|
||||
"terser-webpack-plugin": "^5.2.0",
|
||||
"webpack-cli": "^4.8.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,56 +1,75 @@
|
||||
alembic==1.0.10
|
||||
asn1crypto==0.24.0
|
||||
Babel==2.6.0
|
||||
bcrypt==3.1.6
|
||||
alembic==1.7.4
|
||||
appdirs==1.4.4
|
||||
Babel==2.9.1
|
||||
bcrypt==3.2.0
|
||||
blinker==1.4
|
||||
cffi==1.12.3
|
||||
Click==7.0
|
||||
cryptography==3.4.7
|
||||
decorator==4.4.0
|
||||
dnspython==1.16.0
|
||||
dominate==2.3.5
|
||||
Flask==1.0.2
|
||||
Flask-Babel==0.12.2
|
||||
CacheControl==0.12.9
|
||||
certifi==2021.10.8
|
||||
cffi==1.15.0
|
||||
chardet==4.0.0
|
||||
click==8.0.3
|
||||
colorama==0.4.4
|
||||
contextlib2==21.6.0
|
||||
cryptography==35.0.0
|
||||
decorator==5.1.0
|
||||
# distlib==0.3.1
|
||||
# distro==1.5.0
|
||||
dnspython==2.1.0
|
||||
dominate==2.6.0
|
||||
email-validator==1.1.3
|
||||
Flask==2.0.2
|
||||
Flask-Babel==2.0.0
|
||||
Flask-Bootstrap==3.3.7.1
|
||||
Flask-DebugToolbar==0.10.1
|
||||
Flask-Limiter==1.0.1
|
||||
Flask-Login==0.4.1
|
||||
Flask-DebugToolbar==0.11.0
|
||||
Flask-Limiter==1.4
|
||||
Flask-Login==0.5.0
|
||||
flask-marshmallow==0.14.0
|
||||
Flask-Migrate==2.4.0
|
||||
Flask-Migrate==3.1.0
|
||||
Flask-Script==2.0.6
|
||||
Flask-SQLAlchemy==2.4.0
|
||||
Flask-WTF==0.14.2
|
||||
gunicorn==19.9.0
|
||||
idna==2.8
|
||||
infinity==1.4
|
||||
intervals==0.8.1
|
||||
itsdangerous==1.1.0
|
||||
Jinja2==2.11.3
|
||||
limits==1.3
|
||||
Mako==1.0.9
|
||||
MarkupSafe==1.1.1
|
||||
mysqlclient==1.4.2.post1
|
||||
marshmallow==3.10.0
|
||||
marshmallow-sqlalchemy==0.24.1
|
||||
Flask-SQLAlchemy==2.5.1
|
||||
Flask-WTF==0.15.1
|
||||
greenlet==1.1.2
|
||||
gunicorn==20.1.0
|
||||
html5lib==1.1
|
||||
idna==3.3
|
||||
infinity==1.5
|
||||
intervals==0.9.2
|
||||
itsdangerous==2.0.1
|
||||
Jinja2==3.0.2
|
||||
limits==1.5.1
|
||||
lockfile==0.12.2
|
||||
Mako==1.1.5
|
||||
MarkupSafe==2.0.1
|
||||
marshmallow==3.14.0
|
||||
marshmallow-sqlalchemy==0.26.1
|
||||
msgpack==1.0.2
|
||||
mysqlclient==2.0.3
|
||||
ordered-set==4.0.2
|
||||
# packaging==20.9
|
||||
passlib==1.7.4
|
||||
psycopg2==2.8.2
|
||||
pycparser==2.19
|
||||
Pygments==2.8.1
|
||||
pyOpenSSL==20.0.1
|
||||
python-dateutil==2.8.0
|
||||
python-editor==1.0.4
|
||||
pytz==2019.1
|
||||
PyYAML==5.4.1
|
||||
redis==3.2.1
|
||||
#alpine3:12 provides six==1.15.0
|
||||
#six==1.12.0
|
||||
socrate==0.1.1
|
||||
SQLAlchemy==1.3.3
|
||||
# pep517==0.10.0
|
||||
progress==1.6
|
||||
psycopg2==2.9.1
|
||||
pycparser==2.20
|
||||
Pygments==2.10.0
|
||||
pyOpenSSL==21.0.0
|
||||
pyparsing==3.0.4
|
||||
pytz==2021.3
|
||||
PyYAML==6.0
|
||||
redis==3.5.3
|
||||
requests==2.26.0
|
||||
retrying==1.3.3
|
||||
# six==1.15.0
|
||||
socrate==0.2.0
|
||||
SQLAlchemy==1.4.26
|
||||
srslib==0.1.4
|
||||
tabulate==0.8.3
|
||||
tenacity==5.0.4
|
||||
validators==0.12.6
|
||||
tabulate==0.8.9
|
||||
tenacity==8.0.1
|
||||
toml==0.10.2
|
||||
urllib3==1.26.7
|
||||
validators==0.18.2
|
||||
visitor==0.1.3
|
||||
Werkzeug==0.15.5
|
||||
WTForms==2.2.1
|
||||
WTForms-Components==0.10.4
|
||||
webencodings==0.5.1
|
||||
Werkzeug==2.0.2
|
||||
WTForms==2.3.3
|
||||
WTForms-Components==0.10.5
|
||||
|
@ -18,10 +18,8 @@ PyYAML
|
||||
PyOpenSSL
|
||||
Pygments
|
||||
dnspython
|
||||
bcrypt
|
||||
tenacity
|
||||
mysqlclient
|
||||
psycopg2
|
||||
idna
|
||||
srslib
|
||||
marshmallow
|
||||
|
@ -1,65 +1,76 @@
|
||||
var path = require("path");
|
||||
var webpack = require("webpack");
|
||||
var css = require("mini-css-extract-plugin");
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
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 = {
|
||||
mode: "development",
|
||||
mode: 'production',
|
||||
entry: {
|
||||
app: "./assets/app.js",
|
||||
vendor: "./assets/vendor.js"
|
||||
app: {
|
||||
import: './assets/app.js',
|
||||
dependOn: 'vendor',
|
||||
},
|
||||
vendor: './assets/vendor.js',
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, "static/"),
|
||||
filename: "[name].js"
|
||||
path: path.resolve(__dirname, 'static/'),
|
||||
filename: '[name].js',
|
||||
assetModuleFilename: '[name][ext]',
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: ['babel-loader']
|
||||
use: ['babel-loader', 'import-glob'],
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: [css.loader, 'css-loader', 'sass-loader']
|
||||
test: /\.s?css$/i,
|
||||
use: [css.loader, 'css-loader', 'sass-loader'],
|
||||
},
|
||||
{
|
||||
test: /\.less$/,
|
||||
use: [css.loader, 'css-loader', 'less-loader']
|
||||
test: /\.less$/i,
|
||||
use: [css.loader, 'css-loader', 'less-loader'],
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [css.loader, 'css-loader']
|
||||
test: /\.(json|png|svg|jpg|jpeg|gif)$/i,
|
||||
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: [
|
||||
new css({
|
||||
filename: "[name].css",
|
||||
chunkFilename: "[id].css"
|
||||
}),
|
||||
new css({
|
||||
filename: '[name].css',
|
||||
chunkFilename: '[id].css',
|
||||
}),
|
||||
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 },
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
ARG DISTRO=alpine:3.14
|
||||
ARG DISTRO=alpine:3.14.2
|
||||
FROM $DISTRO as builder
|
||||
WORKDIR /tmp
|
||||
RUN apk add git build-base automake autoconf libtool dovecot-dev xapian-core-dev icu-dev
|
||||
@ -11,9 +11,12 @@ RUN git clone https://github.com/grosjo/fts-xapian.git \
|
||||
&& make install
|
||||
|
||||
FROM $DISTRO
|
||||
|
||||
ENV TZ Etc/UTC
|
||||
|
||||
# python3 shared with most images
|
||||
RUN apk add --no-cache \
|
||||
python3 py3-pip git bash py3-multidict py3-yarl \
|
||||
python3 py3-pip git bash py3-multidict py3-yarl tzdata \
|
||||
&& pip3 install --upgrade pip
|
||||
|
||||
# Shared layer between nginx, dovecot, postfix, postgresql, rspamd, unbound, rainloop, roundcube
|
||||
|
@ -4,7 +4,7 @@ Mailu Dovecot container
|
||||
Dovecot is an open source IMAP and POP3 email server for Linux/UNIX-like
|
||||
systems, written with security primarily in mind. It's fast, simple to set
|
||||
up, requires no special administration and it uses very little memory.
|
||||
|
||||
|
||||
In the Mailu stack it is used as the IMAP/POP frontend service.
|
||||
|
||||
Resources
|
||||
|
@ -6,7 +6,7 @@ import multiprocessing
|
||||
import logging as log
|
||||
import sys
|
||||
|
||||
from podop import run_server
|
||||
from podop import run_server
|
||||
from socrate import system, conf
|
||||
|
||||
log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING"))
|
||||
|
@ -1,5 +1,8 @@
|
||||
ARG DISTRO=alpine:3.14
|
||||
ARG DISTRO=alpine:3.14.2
|
||||
FROM $DISTRO
|
||||
|
||||
ENV TZ Etc/UTC
|
||||
|
||||
# python3 shared with most images
|
||||
RUN apk add --no-cache \
|
||||
python3 py3-pip git bash py3-multidict \
|
||||
@ -9,13 +12,15 @@ RUN apk add --no-cache \
|
||||
RUN pip3 install socrate==0.2.0
|
||||
|
||||
# Image specific layers under this line
|
||||
RUN apk add --no-cache certbot nginx nginx-mod-mail openssl curl \
|
||||
RUN apk add --no-cache certbot nginx nginx-mod-mail openssl curl tzdata \
|
||||
&& pip3 install watchdog
|
||||
|
||||
COPY conf /conf
|
||||
COPY static /static
|
||||
COPY *.py /
|
||||
|
||||
RUN gzip -k9 /static/*.ico /static/*.txt; chmod a+rX -R /static
|
||||
|
||||
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 ["/overrides"]
|
||||
|
@ -1,24 +1,23 @@
|
||||
# Basic configuration
|
||||
# Basic configuration
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
error_log /dev/stderr info;
|
||||
error_log /dev/stderr notice;
|
||||
pid /var/run/nginx.pid;
|
||||
load_module "modules/ngx_mail_module.so";
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
# Standard HTTP configuration with slight hardening
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
access_log /dev/stdout;
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
keepalive_timeout 65;
|
||||
server_tokens off;
|
||||
absolute_redirect off;
|
||||
resolver {{ RESOLVER }} valid=30s;
|
||||
resolver {{ RESOLVER }} ipv6=off valid=30s;
|
||||
|
||||
{% if REAL_IP_HEADER %}
|
||||
real_ip_header {{ REAL_IP_HEADER }};
|
||||
@ -33,15 +32,33 @@ http {
|
||||
default $http_x_forwarded_proto;
|
||||
'' $scheme;
|
||||
}
|
||||
map $uri $expires {
|
||||
default off;
|
||||
~*\.(ico|css|js|gif|jpeg|jpg|png|woff2?|ttf|otf|svg|tiff|eot|webp)$ 97d;
|
||||
}
|
||||
|
||||
map $request_uri $loggable {
|
||||
/health 0;
|
||||
/auth/email 0;
|
||||
default 1;
|
||||
}
|
||||
access_log /dev/stdout combined if=$loggable;
|
||||
|
||||
# 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' ] %}
|
||||
# Enable the proxy for certbot if the flavor is letsencrypt and not on kubernetes
|
||||
#
|
||||
#
|
||||
server {
|
||||
# Listen over HTTP
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
{% if TLS_FLAVOR == 'letsencrypt' %}
|
||||
{% if TLS_FLAVOR == 'letsencrypt' %}
|
||||
location ^~ /.well-known/acme-challenge/ {
|
||||
proxy_pass http://127.0.0.1:8008;
|
||||
}
|
||||
@ -50,6 +67,7 @@ http {
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
@ -68,7 +86,7 @@ http {
|
||||
{% endif %}
|
||||
|
||||
# Listen on HTTP only in kubernetes or behind reverse proxy
|
||||
{% if KUBERNETES_INGRESS == 'true' or TLS_FLAVOR in [ 'mail-letsencrypt', 'notls', 'mail' ] %}
|
||||
{% if KUBERNETES_INGRESS == 'true' or TLS_FLAVOR in [ 'mail-letsencrypt', 'notls', 'mail' ] %}
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
{% endif %}
|
||||
@ -94,14 +112,14 @@ http {
|
||||
# Remove headers to prevent duplication and information disclosure
|
||||
proxy_hide_header X-XSS-Protection;
|
||||
proxy_hide_header X-Powered-By;
|
||||
|
||||
|
||||
add_header X-Frame-Options 'SAMEORIGIN';
|
||||
add_header X-Content-Type-Options 'nosniff';
|
||||
add_header X-Permitted-Cross-Domain-Policies 'none';
|
||||
add_header X-XSS-Protection '1; mode=block';
|
||||
add_header Referrer-Policy 'same-origin';
|
||||
|
||||
{% if TLS_FLAVOR == 'mail-letsencrypt' %}
|
||||
{% if TLS_FLAVOR == 'mail-letsencrypt' %}
|
||||
location ^~ /.well-known/acme-challenge/ {
|
||||
proxy_pass http://127.0.0.1:8008;
|
||||
}
|
||||
@ -113,12 +131,19 @@ http {
|
||||
return 403;
|
||||
}
|
||||
{% else %}
|
||||
|
||||
include /overrides/*.conf;
|
||||
|
||||
# Actual logic
|
||||
{% if ADMIN == 'true' or WEBMAIL != 'none' %}
|
||||
location ~ ^/(sso|static) {
|
||||
include /etc/nginx/proxy.conf;
|
||||
proxy_pass http://$admin;
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
{% if WEB_WEBMAIL != '/' and WEBROOT_REDIRECT != 'none' %}
|
||||
location / {
|
||||
expires $expires;
|
||||
{% if WEBROOT_REDIRECT %}
|
||||
try_files $uri {{ WEBROOT_REDIRECT }};
|
||||
{% else %}
|
||||
@ -135,10 +160,9 @@ http {
|
||||
{% endif %}
|
||||
include /etc/nginx/proxy.conf;
|
||||
client_max_body_size {{ MESSAGE_SIZE_LIMIT|int + 8388608 }};
|
||||
proxy_pass http://$webmail;
|
||||
{% if ADMIN == 'true' %}
|
||||
auth_request /internal/auth/user;
|
||||
error_page 403 @webmail_login;
|
||||
proxy_pass http://$webmail;
|
||||
}
|
||||
|
||||
location {{ WEB_WEBMAIL }}/sso.php {
|
||||
@ -153,27 +177,20 @@ http {
|
||||
auth_request_set $token $upstream_http_x_user_token;
|
||||
proxy_set_header X-Remote-User $user;
|
||||
proxy_set_header X-Remote-User-Token $token;
|
||||
proxy_pass http://$webmail;
|
||||
error_page 403 @webmail_login;
|
||||
proxy_pass http://$webmail;
|
||||
}
|
||||
|
||||
location @webmail_login {
|
||||
return 302 {{ WEB_ADMIN }}/ui/login?next=ui.webmail;
|
||||
return 302 /sso/login;
|
||||
}
|
||||
{% else %}
|
||||
}
|
||||
{% endif %}{% endif %}
|
||||
{% endif %}
|
||||
{% if ADMIN == 'true' %}
|
||||
location {{ WEB_ADMIN }} {
|
||||
return 301 {{ WEB_ADMIN }}/ui;
|
||||
}
|
||||
|
||||
location ~ {{ WEB_ADMIN }}/(ui|static) {
|
||||
rewrite ^{{ WEB_ADMIN }}/(.*) /$1 break;
|
||||
include /etc/nginx/proxy.conf;
|
||||
proxy_set_header X-Forwarded-Prefix {{ WEB_ADMIN }};
|
||||
proxy_pass http://$admin;
|
||||
}
|
||||
location {{ WEB_ADMIN }} {
|
||||
include /etc/nginx/proxy.conf;
|
||||
proxy_pass http://$admin;
|
||||
expires $expires;
|
||||
}
|
||||
|
||||
location {{ WEB_ADMIN }}/antispam {
|
||||
rewrite ^{{ WEB_ADMIN }}/antispam/(.*) /$1 break;
|
||||
@ -204,6 +221,7 @@ http {
|
||||
location /internal {
|
||||
internal;
|
||||
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Authorization $http_authorization;
|
||||
proxy_pass_header Authorization;
|
||||
proxy_pass http://$admin;
|
||||
@ -233,7 +251,8 @@ mail {
|
||||
server_name {{ HOSTNAMES.split(",")[0] }};
|
||||
auth_http http://127.0.0.1:8000/auth/email;
|
||||
proxy_pass_error_message on;
|
||||
resolver {{ RESOLVER }} valid=30s;
|
||||
resolver {{ RESOLVER }} ipv6=off valid=30s;
|
||||
error_log /dev/stderr info;
|
||||
|
||||
{% if TLS and not TLS_ERROR %}
|
||||
include /etc/nginx/tls.conf;
|
||||
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 978 B After Width: | Height: | Size: 924 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 3.8 KiB |
2
core/nginx/static/robots.txt
Normal file
@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
@ -1,6 +1,6 @@
|
||||
# This is an idle image to dynamically replace any component if disabled.
|
||||
|
||||
ARG DISTRO=alpine:3.14
|
||||
ARG DISTRO=alpine:3.14.2
|
||||
FROM $DISTRO
|
||||
|
||||
CMD sleep 1000000d
|
||||
|