diff --git a/.github/workflows/build_test_deploy.yml b/.github/workflows/build_test_deploy.yml index 10c248d7..9c2ff02d 100644 --- a/.github/workflows/build_test_deploy.yml +++ b/.github/workflows/build_test_deploy.yml @@ -474,7 +474,7 @@ jobs: strategy: fail-fast: false matrix: - target: ["setup", "docs", "fetchmail", "webmail", "admin", "traefik-certdumper", "radicale", "clamav", "rspamd", "oletools", "postfix", "dovecot", "unbound", "nginx"] + target: ["setup", "docs", "fetchmail", "webmail", "admin", "traefik-certdumper", "radicale", "rspamd", "oletools", "postfix", "dovecot", "unbound", "nginx"] steps: - uses: actions/checkout@v3 - name: Retrieve global variables diff --git a/.gitignore b/.gitignore index 6c48a270..84ee07d3 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ pip-selfcheck.json /core/admin/lib* /core/admin/bin /core/admin/include +/core/base/.venv /docs/lib* /docs/bin /docs/include diff --git a/AUTHORS.md b/AUTHORS.md index 5f05f0ac..cdaff108 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -22,6 +22,8 @@ Other contributors: - "SunMar" - Dutch translation - "Marty Hou" - Chinese Simple translation - [Thomas Sänger](https://github.com/HorayNarea) - German translation + - [Danylo Sydorenko](https://github.com/Prosta4okua) - Ukrainian translation + - [Hossein Hosni](https://github.com/hosni) - [Contributions](https://github.com/Mailu/Mailu/commits?author=hosni) - [Tim Mohlmann](https://github.com/muhlemmer) - [Contributions](https://github.com/Mailu/Mailu/commits?author=muhlemmer) - [Ionut Filip](https://github.com/ionutfilip) - [Contributions](https://github.com/Mailu/Mailu/commits?author=ionutfilip) - [Ichikawa Yuriko](https://github.com/IchikawaYukko) - [Contributions](https://github.com/Mailu/Mailu/commits?author=IchikawaYukko) Japanese translation diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index fbc83ffb..f015250e 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -1,7 +1,15 @@ ## Environment & Version -### Environment - -- [ ] docker compose -- [ ] kubernetes -- [ ] docker swarm - -### Version - +- `docker compose version` - Version: `master` +If you are not using docker compose do not file any new issue here. +Kubernetes related issues belong to https://github.com/Mailu/helm-charts/issues +If you are not using docker compose or kubernetes, create a new thread on user support in [disussions](https://github.com/Mailu/Mailu/discussions/categories/user-support). +Non-bug reports (or bug reports that do not follow the template) are moved to [disussions](https://github.com/Mailu/Mailu/discussions). + ## Description diff --git a/README.md b/README.md index 6a202940..3eb10374 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,8 @@ Features Main features include: -- **Standard email server**, IMAP and IMAP+, SMTP and Submission with autoconfiguration profiles for clients -- **Advanced email features**, aliases, domain aliases, custom routing +- **Standard email server**, IMAP and IMAP+, SMTP and Submission with auto-configuration profiles for clients +- **Advanced email features**, aliases, domain aliases, custom routing, full-text search of email attachments - **Web access**, multiple Webmails and administration interface - **User features**, aliases, auto-reply, auto-forward, fetched accounts, managesieve - **Admin features**, global admins, announcements, per-domain delegation, quotas diff --git a/core/admin/assets/Dockerfile b/core/admin/assets/Dockerfile index 33b9bdb9..2ac9fe33 100644 --- a/core/admin/assets/Dockerfile +++ b/core/admin/assets/Dockerfile @@ -11,7 +11,7 @@ RUN set -euxo pipefail \ ; npm install --no-audit --no-fund \ ; sed -i 's/#007bff/#55a5d9/' node_modules/admin-lte/build/scss/_bootstrap-variables.scss \ ; mkdir assets \ - ; 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 \ + ; for l in ca da de:de-DE en:en-GB es:es-ES eu fa 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 uk zh zh_TW:zh-HANT; do \ cp node_modules/datatables.net-plugins/i18n/${l#*:}.json assets/${l%:*}.json; \ done diff --git a/core/admin/mailu/__init__.py b/core/admin/mailu/__init__.py index d3971cb1..19731340 100644 --- a/core/admin/mailu/__init__.py +++ b/core/admin/mailu/__init__.py @@ -12,7 +12,7 @@ import hmac class NoPingFilter(logging.Filter): def filter(self, record): - if (record.args['{host}i'] == 'localhost' and record.args['r'] == 'GET /ping HTTP/1.1'): + if record.args['r'].endswith(' /ping HTTP/1.1'): return False if record.args['r'].endswith(' /internal/rspamd/local_domains HTTP/1.1'): return False @@ -40,7 +40,7 @@ def create_app_from_config(config): models.db.init_app(app) utils.session.init_app(app) utils.limiter.init_app(app) - utils.babel.init_app(app) + utils.babel.init_app(app, locale_selector=utils.get_locale) utils.login.init_app(app) utils.login.user_loader(models.User.get) utils.proxy.init_app(app) @@ -52,13 +52,14 @@ def create_app_from_config(config): app.truncated_pw_key = hmac.new(bytearray(app.secret_key, 'utf-8'), bytearray('TRUNCATED_PW_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() - ) - } + with app.app_context(): + 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"): diff --git a/core/admin/mailu/api/v1/__init__.py b/core/admin/mailu/api/v1/__init__.py index 44b6ec57..9b3e98f8 100644 --- a/core/admin/mailu/api/v1/__init__.py +++ b/core/admin/mailu/api/v1/__init__.py @@ -37,7 +37,8 @@ error_fields = api.model('Error', { 'message': fields.String, }) -from . import domains +from . import domain from . import alias from . import relay from . import user +from . import token diff --git a/core/admin/mailu/api/v1/domains.py b/core/admin/mailu/api/v1/domain.py similarity index 97% rename from core/admin/mailu/api/v1/domains.py rename to core/admin/mailu/api/v1/domain.py index 7043da3d..c5f98530 100644 --- a/core/admin/mailu/api/v1/domains.py +++ b/core/admin/mailu/api/v1/domain.py @@ -115,13 +115,13 @@ class Domains(Resource): if 'comment' in data: domain_new.comment = data['comment'] if 'max_users' in data: - domain_new.comment = data['max_users'] + domain_new.max_users = data['max_users'] if 'max_aliases' in data: - domain_new.comment = data['max_aliases'] + domain_new.max_aliases = data['max_aliases'] if 'max_quota_bytes' in data: - domain_new.comment = data['max_quota_bytes'] + domain_new.max_quota_bytes = data['max_quota_bytes'] if 'signup_enabled' in data: - domain_new.comment = data['signup_enabled'] + domain_new.signup_enabled = data['signup_enabled'] models.db.session.add(domain_new) #apply the changes db.session.commit() @@ -177,13 +177,13 @@ class Domain(Resource): if 'comment' in data: domain_found.comment = data['comment'] if 'max_users' in data: - domain_found.comment = data['max_users'] + domain_found.max_users = data['max_users'] if 'max_aliases' in data: - domain_found.comment = data['max_aliases'] + domain_found.max_aliases = data['max_aliases'] if 'max_quota_bytes' in data: - domain_found.comment = data['max_quota_bytes'] + domain_found.max_quota_bytes = data['max_quota_bytes'] if 'signup_enabled' in data: - domain_found.comment = data['signup_enabled'] + domain_found.signup_enabled = data['signup_enabled'] models.db.session.add(domain_found) #apply the changes diff --git a/core/admin/mailu/api/v1/token.py b/core/admin/mailu/api/v1/token.py new file mode 100644 index 00000000..0f2b5b7a --- /dev/null +++ b/core/admin/mailu/api/v1/token.py @@ -0,0 +1,170 @@ +from flask_restx import Resource, fields, marshal +import validators, datetime +import flask +from passlib import pwd + +from . import api, response_fields +from .. import common +from ... import models + +db = models.db + +token = api.namespace('token', description='Token operations') + +token_user_fields = api.model('TokenGetResponse', { + 'id': fields.String(description='The record id of the token (unique identifier)', example='1'), + 'email': fields.String(description='The email address of the user', example='John.Doe@example.com', attribute='user_email'), + 'comment': fields.String(description='A description for the token. This description is shown on the Authentication tokens page', example='my comment'), + 'AuthorizedIP': fields.String(description='Comma separated list of white listed IP addresses or networks that may use this token.', example="['203.0.113.0/24']", attribute='ip'), + 'Created': fields.String(description='The date when the token was created', example='John.Doe@example.com', attribute='created_at'), + 'Last edit': fields.String(description='The date when the token was last modifified', example='John.Doe@example.com', attribute='updated_at') +}) + +token_user_fields_post = api.model('TokenPost', { + 'email': fields.String(description='The email address of the user', example='John.Doe@example.com', attribute='user_email'), + 'comment': fields.String(description='A description for the token. This description is shown on the Authentication tokens page', example='my comment'), + 'AuthorizedIP': fields.String(description='Comma separated list of white listed IP addresses or networks that may use this token.', example="['203.0.113.0/24']", attribute='ip'), +}) + +token_user_fields_post2 = api.model('TokenPost2', { + 'comment': fields.String(description='A description for the token. This description is shown on the Authentication tokens page', example='my comment'), + 'AuthorizedIP': fields.String(description='Comma separated list of white listed IP addresses or networks that may use this token.', example="['203.0.113.0/24']", attribute='ip'), +}) + +token_user_post_response = api.model('TokenPostResponse', { + 'id': fields.String(description='The record id of the token (unique identifier)', example='1'), + 'token': fields.String(description='The created authentication token for the user.', example='2caf6607de5129e4748a2c061aee56f2', attribute='password'), + 'email': fields.String(description='The email address of the user', example='John.Doe@example.com', attribute='user_email'), + 'comment': fields.String(description='A description for the token. This description is shown on the Authentication tokens page', example='my comment'), + 'AuthorizedIP': fields.String(description='Comma separated list of white listed IP addresses or networks that may use this token.', example="['203.0.113.0/24']", attribute='ip'), + 'Created': fields.String(description='The date when the token was created', example='John.Doe@example.com', attribute='created_at') +}) + +@token.route('') +class Tokens(Resource): + @token.doc('list_tokens') + @token.marshal_with(token_user_fields, as_list=True, skip_none=True, mask=None) + @token.doc(security='Bearer') + @common.api_token_authorization + def get(self): + """List tokens""" + return models.Token.query.all() + + @token.doc('create_token') + @token.expect(token_user_fields_post) + @token.response(200, 'Success', token_user_post_response) + @token.response(400, 'Input validation exception') + @token.response(409, 'Duplicate relay', response_fields) + @token.doc(security='Bearer') + @common.api_token_authorization + def post(self): + """ Create a new token""" + data = api.payload + email = data['email'] + if not validators.email(email): + return { 'code': 400, 'message': f'Provided email address {email} is not a valid email address'}, 400 + user_found = models.User.query.get(email) + if not user_found: + return {'code': 404, 'message': f'User {email} cannot be found'}, 404 + tokens = user_found.tokens + + token_new = models.Token(user_email=data['email']) + if 'comment' in data: + token_new.comment = data['comment'] + if 'AuthorizedIP' in data: + token_new.ip = data['AuthorizedIP'].replace(' ','').split(',') + raw_password = pwd.genword(entropy=128, length=32, charset="hex") + token_new.set_password(raw_password) + models.db.session.add(token_new) + #apply the changes + db.session.commit() + response_dict = { + 'id' : token_new.id, + 'token' : raw_password, + 'email' : token_new.user_email, + 'comment' : token_new.comment, + 'AuthorizedIP' : token_new.ip, + 'Created': str(token_new.created_at), + } + + return response_dict + +@token.route('user/') +class Token(Resource): + @token.doc('find_tokens_of_user') + @token.marshal_with(token_user_fields, as_list=True, skip_none=True, mask=None) + @token.doc(security='Bearer') + @common.api_token_authorization + def get(self, email): + "Find tokens of user" + if not validators.email(email): + return { 'code': 400, 'message': f'Provided email address {email} is not a valid email address'}, 400 + user_found = models.User.query.get(email) + if not user_found: + return {'code': 404, 'message': f'User {email} cannot be found'}, 404 + tokens = user_found.tokens + return tokens + + @token.doc('create_token') + @token.expect(token_user_fields_post2) + @token.response(200, 'Success', token_user_post_response) + @token.response(400, 'Input validation exception') + @token.response(409, 'Duplicate relay', response_fields) + @token.doc(security='Bearer') + @common.api_token_authorization + def post(self, email): + """ Create a new token for user""" + data = api.payload + if not validators.email(email): + return { 'code': 400, 'message': f'Provided email address {email} is not a valid email address'}, 400 + user_found = models.User.query.get(email) + if not user_found: + return {'code': 404, 'message': f'User {email} cannot be found'}, 404 + + token_new = models.Token(user_email=email) + if 'comment' in data: + token_new.comment = data['comment'] + if 'AuthorizedIP' in data: + token_new.ip = token_new.ip = data['AuthorizedIP'].replace(' ','').split(',') + raw_password = pwd.genword(entropy=128, length=32, charset="hex") + token_new.set_password(raw_password) + models.db.session.add(token_new) + #apply the changes + db.session.commit() + response_dict = { + 'id' : token_new.id, + 'token' : raw_password, + 'email' : token_new.user_email, + 'comment' : token_new.comment, + 'AuthorizedIP' : token_new.ip, + 'Created': str(token_new.created_at), + } + return response_dict + +@token.route('/') +class Token(Resource): + @token.doc('find_token') + @token.marshal_with(token_user_fields, as_list=True, skip_none=True, mask=None) + @token.doc(security='Bearer') + @common.api_token_authorization + def get(self, token_id): + "Find token" + token = models.Token.query.get(token_id) + if not token: + return { 'code' : 404, 'message' : f'Record cannot be found for id {token_id} or invalid id provided'}, 404 + return token + + @token.doc('delete_token') + @token.response(200, 'Success', response_fields) + @token.response(400, 'Input validation exception', response_fields) + @token.response(404, 'Token not found', response_fields) + @token.doc(security='Bearer') + @common.api_token_authorization + def delete(self, token_id): + """ Delete token """ + token = models.Token.query.get(token_id) + if not token: + return { 'code' : 404, 'message' : f'Record cannot be found for id {token_id} or invalid id provided'}, 404 + db.session.delete(token) + db.session.commit() + return {'code': 200, 'message': f'Token with id {token_id} has been deleted'}, 200 diff --git a/core/admin/mailu/api/v1/user.py b/core/admin/mailu/api/v1/user.py index d9195e3d..441845c2 100644 --- a/core/admin/mailu/api/v1/user.py +++ b/core/admin/mailu/api/v1/user.py @@ -14,6 +14,7 @@ user_fields_get = api.model('UserGet', { 'password': fields.String(description="Hash of the user's password; Example='$bcrypt-sha256$v=2,t=2b,r=12$fmsAdJbYAD1gGQIE5nfJq.$zLkQUEs2XZfTpAEpcix/1k5UTNPm0jO'"), 'comment': fields.String(description='A description for the user. This description is shown on the Users page', example='my comment'), 'quota_bytes': fields.Integer(description='The maximum quota for the user’s email box in bytes', example='1000000000'), + 'quota_bytes_used': fields.Integer(description='The size of the user’s email box in bytes', example='5000000'), 'global_admin': fields.Boolean(description='Make the user a global administrator'), 'enabled': fields.Boolean(description='Enable the user. When an user is disabled, the user is unable to login to the Admin GUI or webmail or access his email via IMAP/POP3 or send mail'), 'change_pw_next_login': fields.Boolean(description='Force the user to change their password at next login'), diff --git a/core/admin/mailu/configuration.py b/core/admin/mailu/configuration.py index bb7080c9..caccfe5e 100644 --- a/core/admin/mailu/configuration.py +++ b/core/admin/mailu/configuration.py @@ -72,9 +72,12 @@ DEFAULT_CONFIG = { 'LOGO_URL': None, 'LOGO_BACKGROUND': None, # Advanced settings + 'AUTH_REQUIRE_TOKENS': False, 'API': False, 'WEB_API': '/api', 'API_TOKEN': None, + 'FULL_TEXT_SEARCH': 'en', + 'FULL_TEXT_SEARCH_ATTACHMENTS': False, 'LOG_LEVEL': 'INFO', 'SESSION_KEY_BITS': 128, 'SESSION_TIMEOUT': 3600, diff --git a/core/admin/mailu/debug.py b/core/admin/mailu/debug.py index 4d63f3c5..c45f4ae7 100644 --- a/core/admin/mailu/debug.py +++ b/core/admin/mailu/debug.py @@ -1,10 +1,11 @@ -import flask_debugtoolbar +#Note: Currently flask_debugtoolbar is not compatible with flask. +#import flask_debugtoolbar from werkzeug.middleware.profiler import ProfilerMiddleware # Debugging toolbar -toolbar = flask_debugtoolbar.DebugToolbarExtension() +#toolbar = flask_debugtoolbar.DebugToolbarExtension() # Profiler diff --git a/core/admin/mailu/internal/nginx.py b/core/admin/mailu/internal/nginx.py index 90b59712..ebd677d0 100644 --- a/core/admin/mailu/internal/nginx.py +++ b/core/admin/mailu/internal/nginx.py @@ -50,8 +50,12 @@ def check_credentials(user, password, ip, protocol=None, auth_port=None, source_ app.logger.info(f'Login attempt for: {user}/{protocol}/{auth_port} from: {ip}/{source_port}: failed: badip: token-{token.id}: {token.comment or ""!r}') return False # we can return directly here since the token is valid if user.check_password(password): - app.logger.info(f'Login attempt for: {user}/{protocol}/{auth_port} from: {ip}/{source_port}: success: password') - return True + if app.config['AUTH_REQUIRE_TOKENS'] and not protocol in ['web', 'sso']: + app.logger.info(f'Login attempt for: {user}/{protocol}/{auth_port} from: {ip}/{source_port}: failed: password ok, but a token is required') + return False + else: + app.logger.info(f'Login attempt for: {user}/{protocol}/{auth_port} from: {ip}/{source_port}: success: password') + return True app.logger.info(f'Login attempt for: {user}/{protocol}/{auth_port} from: {ip}/{source_port}: failed: badauth: {utils.truncated_pw_hash(password)}') return False diff --git a/core/admin/mailu/internal/templates/default.sieve b/core/admin/mailu/internal/templates/default.sieve index c1772c2e..0e97c067 100644 --- a/core/admin/mailu/internal/templates/default.sieve +++ b/core/admin/mailu/internal/templates/default.sieve @@ -29,11 +29,6 @@ if spamtest :percent :value "gt" :comparator "i;ascii-numeric" "{{ user.spam_thr } {% endif %} -if exists "X-Virus" { - discard; - stop; -} - {% if user.reply_active %} if not address :localpart :contains ["From","Reply-To"] ["noreply","no-reply"]{ vacation :days 1 {% if user.displayed_name != "" %}:from "{{ user.displayed_name }} <{{ user.email }}>"{% endif %} :subject "{{ user.reply_subject }}" "{{ user.reply_body }}"; diff --git a/core/admin/mailu/models.py b/core/admin/mailu/models.py index 926ef2fc..45ec457c 100644 --- a/core/admin/mailu/models.py +++ b/core/admin/mailu/models.py @@ -232,9 +232,7 @@ class Domain(Base): """ return DKIM record for domain """ if self.dkim_key: selector = app.config['DKIM_SELECTOR'] - 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}' + return f'{selector}._domainkey.{self.name}. 600 IN TXT "v=DKIM1; k=rsa; p={self.dkim_publickey}"' @cached_property def dns_dmarc(self): diff --git a/core/admin/mailu/translations/en/LC_MESSAGES/messages.po b/core/admin/mailu/translations/en/LC_MESSAGES/messages.po index 4ec33e05..f19e191f 100644 --- a/core/admin/mailu/translations/en/LC_MESSAGES/messages.po +++ b/core/admin/mailu/translations/en/LC_MESSAGES/messages.po @@ -511,6 +511,10 @@ msgstr "" msgid "Generate keys" msgstr "" +#: mailu/ui/templates/domain/details.html:19 +msgid "Download zonefile" +msgstr "" + #: mailu/ui/templates/domain/details.html:30 msgid "DNS MX entry" msgstr "" diff --git a/core/admin/mailu/translations/es/LC_MESSAGES/messages.po b/core/admin/mailu/translations/es/LC_MESSAGES/messages.po index c385905e..068712b8 100644 --- a/core/admin/mailu/translations/es/LC_MESSAGES/messages.po +++ b/core/admin/mailu/translations/es/LC_MESSAGES/messages.po @@ -53,7 +53,7 @@ msgstr "Configuración del cliente" #: mailu/sso/templates/sidebar_sso.html:16 mailu/ui/templates/sidebar.html:114 msgid "Website" -msgstr "Correo web" +msgstr "Sitio web" #: mailu/sso/templates/sidebar_sso.html:22 mailu/ui/templates/sidebar.html:120 msgid "Help" @@ -173,7 +173,7 @@ msgstr "Habilitar filtro de spam" #: mailu/ui/forms.py:103 msgid "Enable marking spam mails as read" -msgstr "" +msgstr "Habilitar marcado de correos spam como leídos" #: mailu/ui/forms.py:104 msgid "Spam filter tolerance" @@ -218,7 +218,7 @@ msgstr "Texto de la respuesta" #: mailu/ui/forms.py:122 msgid "Start of vacation" -msgstr "" +msgstr "Comienzo de las vacaciones" #: mailu/ui/forms.py:123 msgid "End of vacation" @@ -346,7 +346,7 @@ msgstr "Ocurrió un error en la comunicación con el servidor Docker." #: mailu/ui/templates/macros.html:129 msgid "copy to clipboard" -msgstr "" +msgstr "copiar en portapapeles" #: mailu/ui/templates/sidebar.html:15 msgid "My account" diff --git a/core/admin/mailu/translations/fa/LC_MESSAGES/messages.po b/core/admin/mailu/translations/fa/LC_MESSAGES/messages.po new file mode 100644 index 00000000..02fa13cc --- /dev/null +++ b/core/admin/mailu/translations/fa/LC_MESSAGES/messages.po @@ -0,0 +1,731 @@ +# Persian translations for Mailu. +# Copyright (C) 2023 Mailu +# This file is distributed under the same license as the Mailu project. +# Hossein Hosni , 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: hosni.hossein@gmail.com\n" +"POT-Creation-Date: 2023-09-09 00:33+0330\n" +"PO-Revision-Date: 2023-09-09 01:21+0330\n" +"Last-Translator: Hossein Hosni \n" +"Language-Team: Persian \n" +"Language: fa\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n==0 || n==1);\n" +"Generated-By: Babel 2.3.4\n" +"X-Generator: Poedit 3.3.2\n" + +#: mailu/sso/forms.py:8 mailu/ui/forms.py:79 +msgid "E-mail" +msgstr "پست الکترونیک" + +#: mailu/sso/forms.py:9 mailu/ui/forms.py:80 mailu/ui/forms.py:93 +#: mailu/ui/forms.py:112 mailu/ui/forms.py:166 +#: mailu/ui/templates/client.html:32 mailu/ui/templates/client.html:57 +msgid "Password" +msgstr "گذرواژه" + +#: mailu/sso/forms.py:10 mailu/sso/forms.py:11 mailu/sso/templates/login.html:4 +#: mailu/ui/templates/sidebar.html:142 +msgid "Sign in" +msgstr "ورود" + +#: mailu/sso/templates/base_sso.html:8 mailu/ui/templates/base.html:8 +msgid "Admin page for" +msgstr "صفحه مدیریت برای" + +#: mailu/sso/templates/base_sso.html:19 mailu/ui/templates/base.html:19 +msgid "toggle sidebar" +msgstr "نوار کناری را تغییر دهید" + +#: mailu/sso/templates/base_sso.html:37 mailu/ui/templates/base.html:37 +msgid "change language" +msgstr "تغییر زبان" + +#: mailu/sso/templates/sidebar_sso.html:4 mailu/ui/templates/sidebar.html:94 +msgid "Go to" +msgstr "برو به" + +#: mailu/sso/templates/sidebar_sso.html:9 mailu/ui/templates/client.html:4 +#: mailu/ui/templates/sidebar.html:50 mailu/ui/templates/sidebar.html:107 +msgid "Client setup" +msgstr "تنظیمات سرویس‌گیرنده" + +#: mailu/sso/templates/sidebar_sso.html:16 mailu/ui/templates/sidebar.html:114 +msgid "Website" +msgstr "وب سایت" + +#: mailu/sso/templates/sidebar_sso.html:22 mailu/ui/templates/sidebar.html:120 +msgid "Help" +msgstr "راهنما" + +#: mailu/sso/templates/sidebar_sso.html:35 +#: mailu/ui/templates/domain/signup.html:4 mailu/ui/templates/sidebar.html:127 +msgid "Register a domain" +msgstr "ثبت دامنه" + +#: mailu/sso/templates/sidebar_sso.html:49 mailu/ui/forms.py:95 +#: mailu/ui/templates/sidebar.html:149 mailu/ui/templates/user/signup.html:4 +#: mailu/ui/templates/user/signup_domain.html:4 +msgid "Sign up" +msgstr "ثبت‌نام" + +#: mailu/ui/forms.py:33 mailu/ui/forms.py:36 +msgid "Invalid email address." +msgstr "پست‌الکترونیک اشتباه است." + +#: mailu/ui/forms.py:45 +msgid "Confirm" +msgstr "تایید" + +#: mailu/ui/forms.py:48 mailu/ui/forms.py:58 +#: mailu/ui/templates/domain/details.html:26 +#: mailu/ui/templates/domain/list.html:19 mailu/ui/templates/relay/list.html:18 +msgid "Domain name" +msgstr "آدرس دامنه" + +#: mailu/ui/forms.py:49 +msgid "Maximum user count" +msgstr "حداکثر تعداد کاربر" + +#: mailu/ui/forms.py:50 +msgid "Maximum alias count" +msgstr "حداکثر تعداد نام مستعار" + +#: mailu/ui/forms.py:51 +msgid "Maximum user quota" +msgstr "حداکثر سهمیه کاربر" + +#: mailu/ui/forms.py:52 +msgid "Enable sign-up" +msgstr "فعال کردن ثبت نام" + +#: mailu/ui/forms.py:53 mailu/ui/forms.py:74 mailu/ui/forms.py:86 +#: mailu/ui/forms.py:132 mailu/ui/forms.py:144 +#: mailu/ui/templates/alias/list.html:22 mailu/ui/templates/domain/list.html:22 +#: mailu/ui/templates/relay/list.html:20 mailu/ui/templates/token/list.html:20 +#: mailu/ui/templates/user/list.html:24 +msgid "Comment" +msgstr "یادداشت" + +#: mailu/ui/forms.py:54 mailu/ui/forms.py:68 mailu/ui/forms.py:75 +#: mailu/ui/forms.py:88 mailu/ui/forms.py:136 mailu/ui/forms.py:145 +msgid "Save" +msgstr "ذخیره" + +#: mailu/ui/forms.py:59 +msgid "Initial admin" +msgstr "مدیر نخستین" + +#: mailu/ui/forms.py:60 +msgid "Admin password" +msgstr "گذرواژه مدیریت" + +#: mailu/ui/forms.py:61 mailu/ui/forms.py:81 mailu/ui/forms.py:94 +msgid "Confirm password" +msgstr "تایید گذرواژه" + +#: mailu/ui/forms.py:63 +msgid "Create" +msgstr "ایجاد" + +#: mailu/ui/forms.py:67 +msgid "Alternative name" +msgstr "نام جایگزین" + +#: mailu/ui/forms.py:72 +msgid "Relayed domain name" +msgstr "نام دامنه میانجی" + +#: mailu/ui/forms.py:73 mailu/ui/templates/relay/list.html:19 +msgid "Remote host" +msgstr "میزبان راه‌دور" + +#: mailu/ui/forms.py:82 mailu/ui/templates/user/list.html:23 +#: mailu/ui/templates/user/signup_domain.html:16 +msgid "Quota" +msgstr "سهمیه" + +#: mailu/ui/forms.py:83 +msgid "Allow IMAP access" +msgstr "دسترسی IMAP مجاز باشد" + +#: mailu/ui/forms.py:84 +msgid "Allow POP3 access" +msgstr "دسترسی POP3 مجاز باشد" + +#: mailu/ui/forms.py:85 mailu/ui/forms.py:101 +#: mailu/ui/templates/user/settings.html:15 +msgid "Displayed name" +msgstr "نام نمایشی" + +#: mailu/ui/forms.py:87 +msgid "Enabled" +msgstr "فعال" + +#: mailu/ui/forms.py:92 +msgid "Email address" +msgstr "پست الکترونیکی" + +#: mailu/ui/forms.py:102 +msgid "Enable spam filter" +msgstr "فعال‌کردن پایش هرزنامه" + +#: mailu/ui/forms.py:103 +msgid "Enable marking spam mails as read" +msgstr "فعال‌سازی علامت‌زدن هرزنامه به عنوان خوانده‌شده" + +#: mailu/ui/forms.py:104 +msgid "Spam filter tolerance" +msgstr "بازه تحمل هرزنامه" + +#: mailu/ui/forms.py:105 +msgid "Enable forwarding" +msgstr "فعال‌سازی بازارسال" + +#: mailu/ui/forms.py:106 +msgid "Keep a copy of the emails" +msgstr "نگهداری رونوشت از پست‌الکترونیک" + +#: mailu/ui/forms.py:107 mailu/ui/forms.py:143 +#: mailu/ui/templates/alias/list.html:21 +msgid "Destination" +msgstr "مقصد" + +#: mailu/ui/forms.py:108 +msgid "Save settings" +msgstr "ذخیره تنظیمات" + +#: mailu/ui/forms.py:113 +msgid "Password check" +msgstr "بررسی گذرواژه" + +#: mailu/ui/forms.py:114 mailu/ui/templates/sidebar.html:25 +msgid "Update password" +msgstr "بروزرسانی گذرواژه" + +#: mailu/ui/forms.py:118 +msgid "Enable automatic reply" +msgstr "فعال‌سازی پاسخگوی‌خودکار" + +#: mailu/ui/forms.py:119 +msgid "Reply subject" +msgstr "موضوع پاسخگویی" + +#: mailu/ui/forms.py:120 +msgid "Reply body" +msgstr "متن پاسخگویی" + +#: mailu/ui/forms.py:122 +msgid "Start of vacation" +msgstr "شروع استراحت" + +#: mailu/ui/forms.py:123 +msgid "End of vacation" +msgstr "پایان استراحت" + +#: mailu/ui/forms.py:124 +msgid "Update" +msgstr "بروزرسانی" + +#: mailu/ui/forms.py:129 +msgid "Your token (write it down, as it will never be displayed again)" +msgstr "کلید شما (کلید را یادداشت کنید، چرا که دوباره نمایش داده نخواهد شد)" + +#: mailu/ui/forms.py:134 mailu/ui/templates/token/list.html:21 +msgid "Authorized IP" +msgstr "Authorized IP" + +#: mailu/ui/forms.py:140 +msgid "Alias" +msgstr "Alias" + +#: mailu/ui/forms.py:142 +msgid "Use SQL LIKE Syntax (e.g. for catch-all aliases)" +msgstr "استفاده از قواعد اس‌کیو‌ال (برای پوشش الایس‌ها)" + +#: mailu/ui/forms.py:149 +msgid "Admin email" +msgstr "افزودن پست‌الکترونیکی" + +#: mailu/ui/forms.py:150 mailu/ui/forms.py:155 mailu/ui/forms.py:168 +msgid "Submit" +msgstr "ثبت" + +#: mailu/ui/forms.py:154 +msgid "Manager email" +msgstr "مدیریت پست‌الکترونیک" + +#: mailu/ui/forms.py:159 +msgid "Protocol" +msgstr "پرتکل" + +#: mailu/ui/forms.py:162 +msgid "Hostname or IP" +msgstr "نشانی یا آی‌پی" + +#: mailu/ui/forms.py:163 mailu/ui/templates/client.html:20 +#: mailu/ui/templates/client.html:45 +msgid "TCP port" +msgstr "پورت تی‌سی‌پی" + +#: mailu/ui/forms.py:164 +msgid "Enable TLS" +msgstr "فعال‌سازی TLS" + +#: mailu/ui/forms.py:165 mailu/ui/templates/client.html:28 +#: mailu/ui/templates/client.html:53 mailu/ui/templates/fetch/list.html:21 +msgid "Username" +msgstr "نام‌کاربری" + +#: mailu/ui/forms.py:167 +msgid "Keep emails on the server" +msgstr "نگهداری ایمیل در سرور" + +#: mailu/ui/forms.py:172 +msgid "Announcement subject" +msgstr "موضوع آگهی" + +#: mailu/ui/forms.py:174 +msgid "Announcement body" +msgstr "متن آگهی" + +#: mailu/ui/forms.py:176 +msgid "Send" +msgstr "ارسال" + +#: mailu/ui/templates/announcement.html:4 +msgid "Public announcement" +msgstr "آگهی عمومی" + +#: mailu/ui/templates/antispam.html:4 mailu/ui/templates/sidebar.html:80 +#: mailu/ui/templates/user/settings.html:19 +msgid "Antispam" +msgstr "ضدهرزنامه" + +#: mailu/ui/templates/antispam.html:8 +msgid "RSPAMD status page" +msgstr "صفحه گزارش وضعیت RSPAMD" + +#: mailu/ui/templates/client.html:8 +msgid "configure your email client" +msgstr "تنظیمات کاربر پست‌الکترونیک" + +#: mailu/ui/templates/client.html:13 +msgid "Incoming mail" +msgstr "ایمیل ورودی" + +#: mailu/ui/templates/client.html:16 mailu/ui/templates/client.html:41 +msgid "Mail protocol" +msgstr "پرتکل ایمیل" + +#: mailu/ui/templates/client.html:24 mailu/ui/templates/client.html:49 +msgid "Server name" +msgstr "نام سرور" + +#: mailu/ui/templates/client.html:38 +msgid "Outgoing mail" +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 هستید. لطفا دستور را تایید کنید." + +#: mailu/ui/templates/docker-error.html:4 +msgid "Docker error" +msgstr "خطای داکر" + +#: mailu/ui/templates/docker-error.html:12 +msgid "An error occurred while talking to the Docker server." +msgstr "خطایی در هنگام ارتباط با سرور داکر به وجود آمده است." + +#: mailu/ui/templates/macros.html:129 +msgid "copy to clipboard" +msgstr "کپی در حافظه" + +#: mailu/ui/templates/sidebar.html:15 +msgid "My account" +msgstr "حساب من" + +#: mailu/ui/templates/sidebar.html:19 mailu/ui/templates/user/list.html:37 +msgid "Settings" +msgstr "تنظیمات" + +#: mailu/ui/templates/sidebar.html:31 mailu/ui/templates/user/list.html:38 +msgid "Auto-reply" +msgstr "پاسخ‌گوی خودکار" + +#: mailu/ui/templates/fetch/list.html:4 mailu/ui/templates/sidebar.html:37 +#: mailu/ui/templates/user/list.html:39 +msgid "Fetched accounts" +msgstr "اکانت‌ها واکشی‌شده" + +#: mailu/ui/templates/sidebar.html:43 mailu/ui/templates/token/list.html:4 +msgid "Authentication tokens" +msgstr "کلیدهای احراز هویت" + +#: mailu/ui/templates/sidebar.html:56 +msgid "Administration" +msgstr "مدیریت" + +#: mailu/ui/templates/sidebar.html:62 +msgid "Announcement" +msgstr "آگهی" + +#: mailu/ui/templates/sidebar.html:68 +msgid "Administrators" +msgstr "مدیران" + +#: mailu/ui/templates/sidebar.html:74 +msgid "Relayed domains" +msgstr "دامنه‌های میانجی" + +#: mailu/ui/templates/sidebar.html:88 +msgid "Mail domains" +msgstr "دامنه‌های ایمیل" + +#: mailu/ui/templates/sidebar.html:99 +msgid "Webmail" +msgstr "پنل‌وب‌ایمیل" + +#: mailu/ui/templates/sidebar.html:135 +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 "افزودن مدیرکل" + +#: mailu/ui/templates/admin/list.html:4 +msgid "Global administrators" +msgstr "مدیران کلی" + +#: mailu/ui/templates/admin/list.html:9 +msgid "Add administrator" +msgstr "افزودن مدیر" + +#: mailu/ui/templates/admin/list.html:17 mailu/ui/templates/alias/list.html:19 +#: mailu/ui/templates/alternative/list.html:19 +#: mailu/ui/templates/domain/list.html:17 mailu/ui/templates/fetch/list.html:19 +#: mailu/ui/templates/manager/list.html:19 +#: mailu/ui/templates/relay/list.html:17 mailu/ui/templates/token/list.html:19 +#: mailu/ui/templates/user/list.html:19 +msgid "Actions" +msgstr "عملیات" + +#: mailu/ui/templates/admin/list.html:18 mailu/ui/templates/alias/list.html:20 +#: mailu/ui/templates/manager/list.html:20 mailu/ui/templates/user/list.html:21 +msgid "Email" +msgstr "پست‌الکترونیک" + +#: mailu/ui/templates/admin/list.html:25 mailu/ui/templates/alias/list.html:32 +#: mailu/ui/templates/alternative/list.html:29 +#: mailu/ui/templates/domain/list.html:34 mailu/ui/templates/fetch/list.html:34 +#: mailu/ui/templates/manager/list.html:27 +#: mailu/ui/templates/relay/list.html:30 mailu/ui/templates/token/list.html:30 +#: mailu/ui/templates/user/list.html:34 +msgid "Delete" +msgstr "حذف" + +#: mailu/ui/templates/alias/create.html:4 +msgid "Create alias" +msgstr "ایجاد الایس" + +#: mailu/ui/templates/alias/edit.html:4 +msgid "Edit alias" +msgstr "ویرایش الایس" + +#: mailu/ui/templates/alias/list.html:4 +msgid "Alias list" +msgstr "لیست الایس‌ها" + +#: mailu/ui/templates/alias/list.html:12 +msgid "Add alias" +msgstr "افزودن الایس" + +#: mailu/ui/templates/alias/list.html:23 +#: mailu/ui/templates/alternative/list.html:21 +#: mailu/ui/templates/domain/list.html:23 mailu/ui/templates/fetch/list.html:25 +#: mailu/ui/templates/relay/list.html:21 mailu/ui/templates/token/list.html:22 +#: mailu/ui/templates/user/list.html:25 +msgid "Created" +msgstr "ایجاد شد" + +#: mailu/ui/templates/alias/list.html:24 +#: mailu/ui/templates/alternative/list.html:22 +#: mailu/ui/templates/domain/list.html:24 mailu/ui/templates/fetch/list.html:26 +#: mailu/ui/templates/relay/list.html:22 mailu/ui/templates/token/list.html:23 +#: mailu/ui/templates/user/list.html:26 +msgid "Last edit" +msgstr "آخرین ویرایش" + +#: mailu/ui/templates/alias/list.html:31 mailu/ui/templates/domain/list.html:33 +#: mailu/ui/templates/fetch/list.html:33 mailu/ui/templates/relay/list.html:29 +#: mailu/ui/templates/user/list.html:33 +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:20 +msgid "Name" +msgstr "نام" + +#: mailu/ui/templates/domain/create.html:4 +#: mailu/ui/templates/domain/list.html:9 +msgid "New domain" +msgstr "دامنه جدید" + +#: mailu/ui/templates/domain/details.html:4 +msgid "Domain details" +msgstr "مشخصات دامنه" + +#: mailu/ui/templates/domain/details.html:15 +msgid "Regenerate keys" +msgstr "ایجاد مجدد کلید‌ها" + +#: mailu/ui/templates/domain/details.html:17 +msgid "Generate keys" +msgstr "ایجاد کلید‌ها" + +#: mailu/ui/templates/domain/details.html:30 +msgid "DNS MX entry" +msgstr "DNS MX" + +#: mailu/ui/templates/domain/details.html:34 +msgid "DNS SPF entries" +msgstr "DNS SPF" + +#: mailu/ui/templates/domain/details.html:40 +msgid "DKIM public key" +msgstr "کلید عمومی DKIM" + +#: mailu/ui/templates/domain/details.html:44 +msgid "DNS DKIM entry" +msgstr "DNS DKIM" + +#: mailu/ui/templates/domain/details.html:48 +msgid "DNS DMARC entry" +msgstr "DNS DMARC" + +#: mailu/ui/templates/domain/details.html:58 +msgid "DNS TLSA entry" +msgstr "DNS TLSA" + +#: mailu/ui/templates/domain/details.html:63 +msgid "DNS client auto-configuration entries" +msgstr "DNS client auto-configuration" + +#: mailu/ui/templates/domain/edit.html:4 +msgid "Edit domain" +msgstr "ویرایش دامنه" + +#: mailu/ui/templates/domain/list.html:4 +msgid "Domain list" +msgstr "لیست دامنه" + +#: mailu/ui/templates/domain/list.html:18 +msgid "Manage" +msgstr "مدیریت" + +#: mailu/ui/templates/domain/list.html:20 +msgid "Mailbox count" +msgstr "تعداد جعبه ایمیل" + +#: mailu/ui/templates/domain/list.html:21 +msgid "Alias count" +msgstr "تعداد الایس‌ها" + +#: mailu/ui/templates/domain/list.html:31 +msgid "Details" +msgstr "جزئیات" + +#: mailu/ui/templates/domain/list.html:38 +msgid "Users" +msgstr "کاربران" + +#: mailu/ui/templates/domain/list.html:39 +msgid "Aliases" +msgstr "الایس‌ها" + +#: mailu/ui/templates/domain/list.html:40 +msgid "Managers" +msgstr "مدیران" + +#: mailu/ui/templates/domain/list.html:42 +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 MX points to this server" +msgstr "" +"برای ثبت دامنه جدید، ابتدا می‌بایست تنظیمات DNS ٔدامنه را به گونه‌ای تنظیم کنید " +"که رکورد MX آن به سرور ما اشاره کند" + +#: mailu/ui/templates/domain/signup.html:18 +msgid "" +"If you do not know how to setup an MX record for your DNS " +"zone,\n" +" please contact your DNS provider or administrator. Also, please wait a\n" +" couple minutes after the MX is set so the local server " +"cache\n" +" expires." +msgstr "" +"در صورتی که نمی‌دانید چگونه تنظیمات رکورد MX دامنه خود را انجام " +"دهید، می‌بایست با فروشنده دامنه خود ارتباط برقرار کنید.\n" +"همچنین بعد از تنظیم رکورد MX دقایقی را تا اعمال نتیجه آن منتظر " +"بمانید." + +#: mailu/ui/templates/fetch/create.html:4 +msgid "Add a fetched account" +msgstr "افزودن حساب واکشی" + +#: mailu/ui/templates/fetch/edit.html:4 +msgid "Update a fetched account" +msgstr "بروزرسانی حساب واکشی" + +#: mailu/ui/templates/fetch/list.html:12 +msgid "Add an account" +msgstr "افزودن حساب" + +#: mailu/ui/templates/fetch/list.html:20 +msgid "Endpoint" +msgstr "نشانی" + +#: mailu/ui/templates/fetch/list.html:22 +msgid "Keep emails" +msgstr "نگهداری ایمیل‌ها" + +#: mailu/ui/templates/fetch/list.html:23 +msgid "Last check" +msgstr "آخرین بررسی" + +#: mailu/ui/templates/fetch/list.html:24 +msgid "Status" +msgstr "وضعیت" + +#: mailu/ui/templates/fetch/list.html:38 +msgid "yes" +msgstr "بلی" + +#: mailu/ui/templates/fetch/list.html:38 +msgid "no" +msgstr "خیر" + +#: mailu/ui/templates/manager/create.html:4 +msgid "Add a manager" +msgstr "افزودن مدیری" + +#: mailu/ui/templates/manager/list.html:4 +msgid "Manager list" +msgstr "لیست مدیران" + +#: mailu/ui/templates/manager/list.html:12 +msgid "Add manager" +msgstr "افزودن مدیر" + +#: mailu/ui/templates/relay/create.html:4 +msgid "New relay domain" +msgstr "افزودن دامنه میانجی" + +#: mailu/ui/templates/relay/edit.html:4 +msgid "Edit relayed 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/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:4 +msgid "New user" +msgstr "کاربر جدید" + +#: mailu/ui/templates/user/create.html:15 +msgid "General" +msgstr "عمومی" + +#: mailu/ui/templates/user/create.html:23 +msgid "Features and quotas" +msgstr "قابلیت‌ها و سهمیه" + +#: mailu/ui/templates/user/edit.html:4 +msgid "Edit user" +msgstr "ویرایش کاربر" + +#: mailu/ui/templates/user/list.html:4 +msgid "User list" +msgstr "لیست کاربران" + +#: mailu/ui/templates/user/list.html:12 +msgid "Add user" +msgstr "افزودن کاربر" + +#: mailu/ui/templates/user/list.html:20 mailu/ui/templates/user/settings.html:4 +msgid "User settings" +msgstr "تنظیمات کاربر" + +#: mailu/ui/templates/user/list.html:22 +msgid "Features" +msgstr "قابلیت‌ها" + +#: mailu/ui/templates/user/password.html:4 +msgid "Password update" +msgstr "بروزرسانی گذرواژه" + +#: mailu/ui/templates/user/reply.html:4 +msgid "Automatic reply" +msgstr "پاسخگوی خودکار" + +#: mailu/ui/templates/user/settings.html:27 +msgid "Auto-forward" +msgstr "بازارسال خودکار" + +#: mailu/ui/templates/user/signup_domain.html:8 +msgid "pick a domain for the new account" +msgstr "انتخاب دامنه برای حساب جدید" + +#: mailu/ui/templates/user/signup_domain.html:14 +msgid "Domain" +msgstr "دامنه" + +#: mailu/ui/templates/user/signup_domain.html:15 +msgid "Available slots" +msgstr "جایگاه‌های موجود" diff --git a/core/admin/mailu/translations/uk/LC_MESSAGES/messages.po b/core/admin/mailu/translations/uk/LC_MESSAGES/messages.po new file mode 100644 index 00000000..1db04274 --- /dev/null +++ b/core/admin/mailu/translations/uk/LC_MESSAGES/messages.po @@ -0,0 +1,737 @@ +# Ukrainian translations for PROJECT. +# Copyright (C) 2016 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# Danylo Sydorenko , 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: sydorenkodanylo2021@gmail.com\n" +"POT-Creation-Date: 2022-05-22 18:47+0200\n" +"PO-Revision-Date: 2023-08-14 08:19+0300\n" +"Last-Translator: Danylo Sydorenko \n" +"Language-Team: Ukrainian \n" +"Language: uk\n" +"MIME-Version: 1.0\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" +"Generated-By: Babel 2.3.4\n" +"X-Generator: Poedit 3.3.2\n" + +#: mailu/sso/forms.py:8 mailu/ui/forms.py:79 +msgid "E-mail" +msgstr "Електронна пошта" + +#: mailu/sso/forms.py:9 mailu/ui/forms.py:80 mailu/ui/forms.py:93 +#: mailu/ui/forms.py:112 mailu/ui/forms.py:166 +#: mailu/ui/templates/client.html:32 mailu/ui/templates/client.html:57 +msgid "Password" +msgstr "Пароль" + +#: mailu/sso/forms.py:10 mailu/sso/forms.py:11 mailu/sso/templates/login.html:4 +#: mailu/ui/templates/sidebar.html:142 +msgid "Sign in" +msgstr "Увійти" + +#: mailu/sso/templates/base_sso.html:8 mailu/ui/templates/base.html:8 +msgid "Admin page for" +msgstr "Сторінка адміністратора для" + +#: mailu/sso/templates/base_sso.html:19 mailu/ui/templates/base.html:19 +msgid "toggle sidebar" +msgstr "переключити бічну панель" + +#: mailu/sso/templates/base_sso.html:37 mailu/ui/templates/base.html:37 +msgid "change language" +msgstr "змінити мову" + +#: mailu/sso/templates/sidebar_sso.html:4 mailu/ui/templates/sidebar.html:94 +msgid "Go to" +msgstr "Перейти до" + +#: mailu/sso/templates/sidebar_sso.html:9 mailu/ui/templates/client.html:4 +#: mailu/ui/templates/sidebar.html:50 mailu/ui/templates/sidebar.html:107 +msgid "Client setup" +msgstr "Налаштування клієнта" + +#: mailu/sso/templates/sidebar_sso.html:16 mailu/ui/templates/sidebar.html:114 +msgid "Website" +msgstr "Вебсайт" + +#: mailu/sso/templates/sidebar_sso.html:22 mailu/ui/templates/sidebar.html:120 +msgid "Help" +msgstr "Довідка" + +#: mailu/sso/templates/sidebar_sso.html:35 +#: mailu/ui/templates/domain/signup.html:4 mailu/ui/templates/sidebar.html:127 +msgid "Register a domain" +msgstr "Зареєструвати домен" + +#: mailu/sso/templates/sidebar_sso.html:49 mailu/ui/forms.py:95 +#: mailu/ui/templates/sidebar.html:149 mailu/ui/templates/user/signup.html:4 +#: mailu/ui/templates/user/signup_domain.html:4 +msgid "Sign up" +msgstr "Зареєструватися" + +#: mailu/ui/forms.py:33 mailu/ui/forms.py:36 +msgid "Invalid email address." +msgstr "Недійсна електронна пошта." + +#: mailu/ui/forms.py:45 +msgid "Confirm" +msgstr "Підтвердити" + +#: mailu/ui/forms.py:48 mailu/ui/forms.py:58 +#: mailu/ui/templates/domain/details.html:26 +#: mailu/ui/templates/domain/list.html:19 mailu/ui/templates/relay/list.html:18 +msgid "Domain name" +msgstr "Ім'я домену" + +#: mailu/ui/forms.py:49 +msgid "Maximum user count" +msgstr "Максимальна кількість користувачів" + +#: mailu/ui/forms.py:50 +msgid "Maximum alias count" +msgstr "Максимальна кількість скорочень" + +#: mailu/ui/forms.py:51 +msgid "Maximum user quota" +msgstr "Максимальна квота користувачів" + +#: mailu/ui/forms.py:52 +msgid "Enable sign-up" +msgstr "Увімкнути реєстрацію" + +#: mailu/ui/forms.py:53 mailu/ui/forms.py:74 mailu/ui/forms.py:86 +#: mailu/ui/forms.py:132 mailu/ui/forms.py:144 +#: mailu/ui/templates/alias/list.html:22 mailu/ui/templates/domain/list.html:22 +#: mailu/ui/templates/relay/list.html:20 mailu/ui/templates/token/list.html:20 +#: mailu/ui/templates/user/list.html:24 +msgid "Comment" +msgstr "Коментар" + +#: mailu/ui/forms.py:54 mailu/ui/forms.py:68 mailu/ui/forms.py:75 +#: mailu/ui/forms.py:88 mailu/ui/forms.py:136 mailu/ui/forms.py:145 +msgid "Save" +msgstr "Зберегти" + +#: mailu/ui/forms.py:59 +msgid "Initial admin" +msgstr "Початковий адміністратор" + +#: mailu/ui/forms.py:60 +msgid "Admin password" +msgstr "Пароль адміністратора" + +#: mailu/ui/forms.py:61 mailu/ui/forms.py:81 mailu/ui/forms.py:94 +msgid "Confirm password" +msgstr "Підтвердити пароль" + +#: mailu/ui/forms.py:63 +msgid "Create" +msgstr "Створити" + +#: mailu/ui/forms.py:67 +msgid "Alternative name" +msgstr "Альтернативна назва" + +#: mailu/ui/forms.py:72 +msgid "Relayed domain name" +msgstr "Ретрансльоване доменне ім'я" + +#: mailu/ui/forms.py:73 mailu/ui/templates/relay/list.html:19 +msgid "Remote host" +msgstr "Віддалений хост" + +#: mailu/ui/forms.py:82 mailu/ui/templates/user/list.html:23 +#: mailu/ui/templates/user/signup_domain.html:16 +msgid "Quota" +msgstr "Квота" + +#: mailu/ui/forms.py:83 +msgid "Allow IMAP access" +msgstr "Дозволити IMAP-доступ" + +#: mailu/ui/forms.py:84 +msgid "Allow POP3 access" +msgstr "Дозволити POP3-доступ" + +#: mailu/ui/forms.py:85 mailu/ui/forms.py:101 +#: mailu/ui/templates/user/settings.html:15 +msgid "Displayed name" +msgstr "Ім'я, що показується" + +#: mailu/ui/forms.py:87 +msgid "Enabled" +msgstr "Увімкнено" + +#: mailu/ui/forms.py:92 +msgid "Email address" +msgstr "Електронна пошта" + +#: mailu/ui/forms.py:102 +msgid "Enable spam filter" +msgstr "Увімкнути спам-фільтр" + +#: mailu/ui/forms.py:103 +msgid "Enable marking spam mails as read" +msgstr "Позначити спам як прочитане" + +#: mailu/ui/forms.py:104 +msgid "Spam filter tolerance" +msgstr "Толерантність спам-фільтра до спаму" + +#: mailu/ui/forms.py:105 +msgid "Enable forwarding" +msgstr "Увімкнути переадресацію" + +#: mailu/ui/forms.py:106 +msgid "Keep a copy of the emails" +msgstr "Зберігати копії електронних листів" + +#: mailu/ui/forms.py:107 mailu/ui/forms.py:143 +#: mailu/ui/templates/alias/list.html:21 +msgid "Destination" +msgstr "Призначення" + +#: mailu/ui/forms.py:108 +msgid "Save settings" +msgstr "Зберегти налаштування" + +#: mailu/ui/forms.py:113 +msgid "Password check" +msgstr "Перевірка пароля" + +#: mailu/ui/forms.py:114 mailu/ui/templates/sidebar.html:25 +msgid "Update password" +msgstr "Оновити пароль" + +#: mailu/ui/forms.py:118 +msgid "Enable automatic reply" +msgstr "Увімкнути автоматичну відповідь" + +#: mailu/ui/forms.py:119 +msgid "Reply subject" +msgstr "Тема відповіді" + +#: mailu/ui/forms.py:120 +msgid "Reply body" +msgstr "Текст відповіді" + +#: mailu/ui/forms.py:122 +msgid "Start of vacation" +msgstr "Початок відпустки" + +#: mailu/ui/forms.py:123 +msgid "End of vacation" +msgstr "Кінець відпустки" + +#: mailu/ui/forms.py:124 +msgid "Update" +msgstr "Оновити" + +#: mailu/ui/forms.py:129 +msgid "Your token (write it down, as it will never be displayed again)" +msgstr "" +"Ваш токен (запишіть його, оскільки він більше ніколи не буде відображатися)" + +#: mailu/ui/forms.py:134 mailu/ui/templates/token/list.html:21 +msgid "Authorized IP" +msgstr "Дозволені IP-адреси" + +#: mailu/ui/forms.py:140 +msgid "Alias" +msgstr "Псевдонім" + +#: mailu/ui/forms.py:142 +msgid "Use SQL LIKE Syntax (e.g. for catch-all aliases)" +msgstr "" +"Використовуйте синтаксис SQL LIKE (наприклад, для псевдонімів catch-all)" + +#: mailu/ui/forms.py:149 +msgid "Admin email" +msgstr "Пошта адміністратора" + +#: mailu/ui/forms.py:150 mailu/ui/forms.py:155 mailu/ui/forms.py:168 +msgid "Submit" +msgstr "Надіслати" + +#: mailu/ui/forms.py:154 +msgid "Manager email" +msgstr "Електронна пошта менеджера" + +#: mailu/ui/forms.py:159 +msgid "Protocol" +msgstr "Протокол" + +#: mailu/ui/forms.py:162 +msgid "Hostname or IP" +msgstr "Ім'я хоста або IP" + +#: mailu/ui/forms.py:163 mailu/ui/templates/client.html:20 +#: mailu/ui/templates/client.html:45 +msgid "TCP port" +msgstr "Порт TCP" + +#: mailu/ui/forms.py:164 +msgid "Enable TLS" +msgstr "Увімкнути TLS" + +#: mailu/ui/forms.py:165 mailu/ui/templates/client.html:28 +#: mailu/ui/templates/client.html:53 mailu/ui/templates/fetch/list.html:21 +msgid "Username" +msgstr "Ім'я користувача" + +#: mailu/ui/forms.py:167 +msgid "Keep emails on the server" +msgstr "Зберігати копії електронних листів" + +#: mailu/ui/forms.py:172 +msgid "Announcement subject" +msgstr "Тема оголошення" + +#: mailu/ui/forms.py:174 +msgid "Announcement body" +msgstr "Текст оголошення" + +#: mailu/ui/forms.py:176 +msgid "Send" +msgstr "Надіслати" + +#: mailu/ui/templates/announcement.html:4 +msgid "Public announcement" +msgstr "Публічне оголошення" + +#: mailu/ui/templates/antispam.html:4 mailu/ui/templates/sidebar.html:80 +#: mailu/ui/templates/user/settings.html:19 +msgid "Antispam" +msgstr "Антиспам" + +#: mailu/ui/templates/antispam.html:8 +msgid "RSPAMD status page" +msgstr "Сторінка стану RSPAMD" + +#: mailu/ui/templates/client.html:8 +msgid "configure your email client" +msgstr "налаштуйте свій поштовий клієнт" + +#: mailu/ui/templates/client.html:13 +msgid "Incoming mail" +msgstr "Вхідна пошта" + +#: mailu/ui/templates/client.html:16 mailu/ui/templates/client.html:41 +msgid "Mail protocol" +msgstr "Поштовий протокол" + +#: mailu/ui/templates/client.html:24 mailu/ui/templates/client.html:49 +msgid "Server name" +msgstr "Назва серверу" + +#: mailu/ui/templates/client.html:38 +msgid "Outgoing mail" +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. Будь ласка, підтвердіть вашу дію." + +#: mailu/ui/templates/docker-error.html:4 +msgid "Docker error" +msgstr "Помилка Docker" + +#: mailu/ui/templates/docker-error.html:12 +msgid "An error occurred while talking to the Docker server." +msgstr "Виникла помилка при спілкуванні з сервером Docker." + +#: mailu/ui/templates/macros.html:129 +msgid "copy to clipboard" +msgstr "скопіювати в буфер обміну" + +#: mailu/ui/templates/sidebar.html:15 +msgid "My account" +msgstr "Мій обліковий запис" + +#: mailu/ui/templates/sidebar.html:19 mailu/ui/templates/user/list.html:37 +msgid "Settings" +msgstr "Налаштування" + +#: mailu/ui/templates/sidebar.html:31 mailu/ui/templates/user/list.html:38 +msgid "Auto-reply" +msgstr "Автовідповідь" + +#: mailu/ui/templates/fetch/list.html:4 mailu/ui/templates/sidebar.html:37 +#: mailu/ui/templates/user/list.html:39 +msgid "Fetched accounts" +msgstr "Отримані облікові записи" + +#: mailu/ui/templates/sidebar.html:43 mailu/ui/templates/token/list.html:4 +msgid "Authentication tokens" +msgstr "Токени автентифікації" + +#: mailu/ui/templates/sidebar.html:56 +msgid "Administration" +msgstr "Адміністрація" + +#: mailu/ui/templates/sidebar.html:62 +msgid "Announcement" +msgstr "Оголошення" + +#: mailu/ui/templates/sidebar.html:68 +msgid "Administrators" +msgstr "Адміністратори" + +#: mailu/ui/templates/sidebar.html:74 +msgid "Relayed domains" +msgstr "Передані домени" + +#: mailu/ui/templates/sidebar.html:88 +msgid "Mail domains" +msgstr "Поштові домени" + +#: mailu/ui/templates/sidebar.html:99 +msgid "Webmail" +msgstr "Вебпошта" + +#: mailu/ui/templates/sidebar.html:135 +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 "Додати глобального адміністратора" + +#: mailu/ui/templates/admin/list.html:4 +msgid "Global administrators" +msgstr "Глобальні адміністратори" + +#: mailu/ui/templates/admin/list.html:9 +msgid "Add administrator" +msgstr "Додати адміністратора" + +#: mailu/ui/templates/admin/list.html:17 mailu/ui/templates/alias/list.html:19 +#: mailu/ui/templates/alternative/list.html:19 +#: mailu/ui/templates/domain/list.html:17 mailu/ui/templates/fetch/list.html:19 +#: mailu/ui/templates/manager/list.html:19 +#: mailu/ui/templates/relay/list.html:17 mailu/ui/templates/token/list.html:19 +#: mailu/ui/templates/user/list.html:19 +msgid "Actions" +msgstr "Дії" + +#: mailu/ui/templates/admin/list.html:18 mailu/ui/templates/alias/list.html:20 +#: mailu/ui/templates/manager/list.html:20 mailu/ui/templates/user/list.html:21 +msgid "Email" +msgstr "Електронна пошта" + +#: mailu/ui/templates/admin/list.html:25 mailu/ui/templates/alias/list.html:32 +#: mailu/ui/templates/alternative/list.html:29 +#: mailu/ui/templates/domain/list.html:34 mailu/ui/templates/fetch/list.html:34 +#: mailu/ui/templates/manager/list.html:27 +#: mailu/ui/templates/relay/list.html:30 mailu/ui/templates/token/list.html:30 +#: mailu/ui/templates/user/list.html:34 +msgid "Delete" +msgstr "Видалити" + +#: mailu/ui/templates/alias/create.html:4 +msgid "Create alias" +msgstr "Створити псевдонім" + +#: mailu/ui/templates/alias/edit.html:4 +msgid "Edit alias" +msgstr "Редагувати псевдонім" + +#: mailu/ui/templates/alias/list.html:4 +msgid "Alias list" +msgstr "Список псевдонімів" + +#: mailu/ui/templates/alias/list.html:12 +msgid "Add alias" +msgstr "Додати псевдонім" + +#: mailu/ui/templates/alias/list.html:23 +#: mailu/ui/templates/alternative/list.html:21 +#: mailu/ui/templates/domain/list.html:23 mailu/ui/templates/fetch/list.html:25 +#: mailu/ui/templates/relay/list.html:21 mailu/ui/templates/token/list.html:22 +#: mailu/ui/templates/user/list.html:25 +msgid "Created" +msgstr "Створено" + +#: mailu/ui/templates/alias/list.html:24 +#: mailu/ui/templates/alternative/list.html:22 +#: mailu/ui/templates/domain/list.html:24 mailu/ui/templates/fetch/list.html:26 +#: mailu/ui/templates/relay/list.html:22 mailu/ui/templates/token/list.html:23 +#: mailu/ui/templates/user/list.html:26 +msgid "Last edit" +msgstr "Останнє редагування від" + +#: mailu/ui/templates/alias/list.html:31 mailu/ui/templates/domain/list.html:33 +#: mailu/ui/templates/fetch/list.html:33 mailu/ui/templates/relay/list.html:29 +#: mailu/ui/templates/user/list.html:33 +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:20 +msgid "Name" +msgstr "Ім'я" + +#: mailu/ui/templates/domain/create.html:4 +#: mailu/ui/templates/domain/list.html:9 +msgid "New domain" +msgstr "Новий домен" + +#: mailu/ui/templates/domain/details.html:4 +msgid "Domain details" +msgstr "Інформація про домен" + +#: mailu/ui/templates/domain/details.html:15 +msgid "Regenerate keys" +msgstr "Регенерувати ключі" + +#: mailu/ui/templates/domain/details.html:17 +msgid "Generate keys" +msgstr "Згенерувати ключі" + +#: mailu/ui/templates/domain/details.html:30 +msgid "DNS MX entry" +msgstr "Запис DNS MX" + +#: mailu/ui/templates/domain/details.html:34 +msgid "DNS SPF entries" +msgstr "Записи DNS SPF" + +#: mailu/ui/templates/domain/details.html:40 +msgid "DKIM public key" +msgstr "Відкритий ключ DKIM" + +#: mailu/ui/templates/domain/details.html:44 +msgid "DNS DKIM entry" +msgstr "Запис DNS DKIM" + +#: mailu/ui/templates/domain/details.html:48 +msgid "DNS DMARC entry" +msgstr "Запис DNS DMARC" + +#: mailu/ui/templates/domain/details.html:58 +msgid "DNS TLSA entry" +msgstr "Запис DNS TLSA" + +#: mailu/ui/templates/domain/details.html:63 +msgid "DNS client auto-configuration entries" +msgstr "Записи автоконфігурації DNS-клієнта" + +#: mailu/ui/templates/domain/edit.html:4 +msgid "Edit domain" +msgstr "Редагувати домен" + +#: mailu/ui/templates/domain/list.html:4 +msgid "Domain list" +msgstr "Список доменів" + +#: mailu/ui/templates/domain/list.html:18 +msgid "Manage" +msgstr "Керувати" + +#: mailu/ui/templates/domain/list.html:20 +msgid "Mailbox count" +msgstr "Кількість поштових скриньок" + +#: mailu/ui/templates/domain/list.html:21 +msgid "Alias count" +msgstr "Кількість псевдонімів" + +#: mailu/ui/templates/domain/list.html:31 +msgid "Details" +msgstr "Подробиці" + +#: mailu/ui/templates/domain/list.html:38 +msgid "Users" +msgstr "Користувачі" + +#: mailu/ui/templates/domain/list.html:39 +msgid "Aliases" +msgstr "Скорочення" + +#: mailu/ui/templates/domain/list.html:40 +msgid "Managers" +msgstr "Менеджери" + +#: mailu/ui/templates/domain/list.html:42 +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 MX points to this server" +msgstr "" +"Для того, щоб зареєструвати новий домен, необхідно\n" +" спочатку налаштувати доменну MX так,\n" +" щоб домен вказував на цей сервер" + +#: mailu/ui/templates/domain/signup.html:18 +msgid "" +"If you do not know how to setup an MX record for your DNS " +"zone,\n" +" please contact your DNS provider or administrator. Also, please wait a\n" +" couple minutes after the MX is set so the local server " +"cache\n" +" expires." +msgstr "" +"Якщо ви не знаєте, як налаштувати MX-запис для вашої DNS-зони,\n" +" зверніться до вашого DNS-провайдера або адміністратора. Також, будь " +"ласка, \n" +" зачекайте кілька хвилин після встановлення MX, щоб закінчився термін дії " +"кешу\n" +" локального сервера." + +#: mailu/ui/templates/fetch/create.html:4 +msgid "Add a fetched account" +msgstr "Додати знайдений обліковий запис" + +#: mailu/ui/templates/fetch/edit.html:4 +msgid "Update a fetched account" +msgstr "Оновити знайдений обліковий запис" + +#: mailu/ui/templates/fetch/list.html:12 +msgid "Add an account" +msgstr "Додати обліковий запис" + +#: mailu/ui/templates/fetch/list.html:20 +msgid "Endpoint" +msgstr "Кінцева точка" + +#: mailu/ui/templates/fetch/list.html:22 +msgid "Keep emails" +msgstr "Зберігати електронні листи" + +#: mailu/ui/templates/fetch/list.html:23 +msgid "Last check" +msgstr "Остання перевірка" + +#: mailu/ui/templates/fetch/list.html:24 +msgid "Status" +msgstr "Статус" + +#: mailu/ui/templates/fetch/list.html:38 +msgid "yes" +msgstr "так" + +#: mailu/ui/templates/fetch/list.html:38 +msgid "no" +msgstr "ні" + +#: mailu/ui/templates/manager/create.html:4 +msgid "Add a manager" +msgstr "Додати менеджера" + +#: mailu/ui/templates/manager/list.html:4 +msgid "Manager list" +msgstr "Список менеджерів" + +#: mailu/ui/templates/manager/list.html:12 +msgid "Add manager" +msgstr "Додати менеджера" + +#: mailu/ui/templates/relay/create.html:4 +msgid "New relay domain" +msgstr "Новий домен ретрансляції" + +#: mailu/ui/templates/relay/edit.html:4 +msgid "Edit relayed 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/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:4 +msgid "New user" +msgstr "Новий користувач" + +#: mailu/ui/templates/user/create.html:15 +msgid "General" +msgstr "Основне" + +#: mailu/ui/templates/user/create.html:23 +msgid "Features and quotas" +msgstr "Можливості та квоти" + +#: mailu/ui/templates/user/edit.html:4 +msgid "Edit user" +msgstr "Редагувати користувача" + +#: mailu/ui/templates/user/list.html:4 +msgid "User list" +msgstr "Список користувачів" + +#: mailu/ui/templates/user/list.html:12 +msgid "Add user" +msgstr "Додати користувача" + +#: mailu/ui/templates/user/list.html:20 mailu/ui/templates/user/settings.html:4 +msgid "User settings" +msgstr "Налаштування користувача" + +#: mailu/ui/templates/user/list.html:22 +msgid "Features" +msgstr "Можливості" + +#: mailu/ui/templates/user/password.html:4 +msgid "Password update" +msgstr "Оновити пароль" + +#: mailu/ui/templates/user/reply.html:4 +msgid "Automatic reply" +msgstr "Увімкнути автоматичну відповідь" + +#: mailu/ui/templates/user/settings.html:27 +msgid "Auto-forward" +msgstr "Автоматичне пересилання" + +#: mailu/ui/templates/user/signup_domain.html:8 +msgid "pick a domain for the new account" +msgstr "вибрати домен для нового облікового запису" + +#: mailu/ui/templates/user/signup_domain.html:14 +msgid "Domain" +msgstr "Домен" + +#: mailu/ui/templates/user/signup_domain.html:15 +msgid "Available slots" +msgstr "Наявні слоти" diff --git a/core/admin/mailu/translations/zh_TW/LC_MESSAGES/messages.po b/core/admin/mailu/translations/zh_TW/LC_MESSAGES/messages.po new file mode 100644 index 00000000..cfe42403 --- /dev/null +++ b/core/admin/mailu/translations/zh_TW/LC_MESSAGES/messages.po @@ -0,0 +1,716 @@ +msgid "" +msgstr "" +"Project-Id-Version: Mailu\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2023-09-13 15:04+0800\n" +"PO-Revision-Date: 2023-09-19 13:48+0800\n" +"Last-Translator: Jonathan Tsai \n" +"Language-Team: \n" +"Language: zh_TW\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"Generated-By: Babel 2.3.4\n" +"X-Generator: Poedit 3.3.2\n" + +#: mailu/sso/forms.py:8 mailu/ui/forms.py:79 +msgid "E-mail" +msgstr "電子郵件" + +#: mailu/sso/forms.py:9 mailu/ui/forms.py:80 mailu/ui/forms.py:93 mailu/ui/forms.py:112 +#: mailu/ui/forms.py:166 mailu/ui/templates/client.html:32 +#: mailu/ui/templates/client.html:57 +msgid "Password" +msgstr "密碼" + +#: mailu/sso/forms.py:10 mailu/sso/forms.py:11 mailu/sso/templates/login.html:4 +#: mailu/ui/templates/sidebar.html:142 +msgid "Sign in" +msgstr "登入" + +#: mailu/sso/templates/base_sso.html:8 mailu/ui/templates/base.html:8 +msgid "Admin page for" +msgstr "系統管理頁面 -" + +#: mailu/sso/templates/base_sso.html:19 mailu/ui/templates/base.html:19 +msgid "toggle sidebar" +msgstr "切換側邊欄" + +#: mailu/sso/templates/base_sso.html:37 mailu/ui/templates/base.html:37 +msgid "change language" +msgstr "切換語言" + +#: mailu/sso/templates/sidebar_sso.html:4 mailu/ui/templates/sidebar.html:94 +msgid "Go to" +msgstr "跳至" + +#: mailu/sso/templates/sidebar_sso.html:9 mailu/ui/templates/client.html:4 +#: mailu/ui/templates/sidebar.html:50 mailu/ui/templates/sidebar.html:107 +msgid "Client setup" +msgstr "用戶端設定" + +#: mailu/sso/templates/sidebar_sso.html:16 mailu/ui/templates/sidebar.html:114 +msgid "Website" +msgstr "網站" + +#: mailu/sso/templates/sidebar_sso.html:22 mailu/ui/templates/sidebar.html:120 +msgid "Help" +msgstr "輔助" + +#: mailu/sso/templates/sidebar_sso.html:35 mailu/ui/templates/domain/signup.html:4 +#: mailu/ui/templates/sidebar.html:127 +msgid "Register a domain" +msgstr "註冊網域" + +#: mailu/sso/templates/sidebar_sso.html:49 mailu/ui/forms.py:95 +#: mailu/ui/templates/sidebar.html:149 mailu/ui/templates/user/signup.html:4 +#: mailu/ui/templates/user/signup_domain.html:4 +msgid "Sign up" +msgstr "註冊" + +#: mailu/ui/forms.py:33 mailu/ui/forms.py:36 +msgid "Invalid email address." +msgstr "電子郵件格式錯誤" + +#: mailu/ui/forms.py:45 +msgid "Confirm" +msgstr "確定" + +#: mailu/ui/forms.py:48 mailu/ui/forms.py:58 mailu/ui/templates/domain/details.html:26 +#: mailu/ui/templates/domain/list.html:19 mailu/ui/templates/relay/list.html:18 +msgid "Domain name" +msgstr "網域" + +#: mailu/ui/forms.py:49 +msgid "Maximum user count" +msgstr "最大用戶數" + +#: mailu/ui/forms.py:50 +msgid "Maximum alias count" +msgstr "最大別名數" + +#: mailu/ui/forms.py:51 +msgid "Maximum user quota" +msgstr "最大用戶配額" + +#: mailu/ui/forms.py:52 +msgid "Enable sign-up" +msgstr "啟用註冊" + +#: mailu/ui/forms.py:53 mailu/ui/forms.py:74 mailu/ui/forms.py:86 mailu/ui/forms.py:132 +#: mailu/ui/forms.py:144 mailu/ui/templates/alias/list.html:22 +#: mailu/ui/templates/domain/list.html:22 mailu/ui/templates/relay/list.html:20 +#: mailu/ui/templates/token/list.html:20 mailu/ui/templates/user/list.html:24 +msgid "Comment" +msgstr "說明" + +#: mailu/ui/forms.py:54 mailu/ui/forms.py:68 mailu/ui/forms.py:75 mailu/ui/forms.py:88 +#: mailu/ui/forms.py:136 mailu/ui/forms.py:145 +msgid "Save" +msgstr "儲存" + +#: mailu/ui/forms.py:59 +msgid "Initial admin" +msgstr "初始管理員" + +#: mailu/ui/forms.py:60 +msgid "Admin password" +msgstr "管理員密碼" + +#: mailu/ui/forms.py:61 mailu/ui/forms.py:81 mailu/ui/forms.py:94 +msgid "Confirm password" +msgstr "確認密碼" + +#: mailu/ui/forms.py:63 +msgid "Create" +msgstr "建立" + +#: mailu/ui/forms.py:67 +msgid "Alternative name" +msgstr "替代名稱" + +#: mailu/ui/forms.py:72 +msgid "Relayed domain name" +msgstr "轉發網網域稱" + +#: mailu/ui/forms.py:73 mailu/ui/templates/relay/list.html:19 +msgid "Remote host" +msgstr "遠端主機" + +#: mailu/ui/forms.py:82 mailu/ui/templates/user/list.html:23 +#: mailu/ui/templates/user/signup_domain.html:16 +msgid "Quota" +msgstr "配額" + +#: mailu/ui/forms.py:83 +msgid "Allow IMAP access" +msgstr "允許IMAP存取" + +#: mailu/ui/forms.py:84 +msgid "Allow POP3 access" +msgstr "允許POP3存取" + +#: mailu/ui/forms.py:85 mailu/ui/forms.py:101 mailu/ui/templates/user/settings.html:15 +msgid "Displayed name" +msgstr "顯示名稱" + +#: mailu/ui/forms.py:87 +msgid "Enabled" +msgstr "啟用" + +#: mailu/ui/forms.py:92 +msgid "Email address" +msgstr "電郵地址" + +#: mailu/ui/forms.py:102 +msgid "Enable spam filter" +msgstr "啟用垃圾郵件過濾" + +#: mailu/ui/forms.py:103 +msgid "Enable marking spam mails as read" +msgstr "啟用標註垃圾信已讀" + +#: mailu/ui/forms.py:104 +msgid "Spam filter tolerance" +msgstr "垃圾郵件過濾器容忍度" + +#: mailu/ui/forms.py:105 +msgid "Enable forwarding" +msgstr "啟用轉寄" + +#: mailu/ui/forms.py:106 +msgid "Keep a copy of the emails" +msgstr "保留電子郵件副本" + +#: mailu/ui/forms.py:107 mailu/ui/forms.py:143 mailu/ui/templates/alias/list.html:21 +msgid "Destination" +msgstr "目標地址" + +#: mailu/ui/forms.py:108 +msgid "Save settings" +msgstr "儲存設定" + +#: mailu/ui/forms.py:113 +msgid "Password check" +msgstr "檢查密碼" + +#: mailu/ui/forms.py:114 mailu/ui/templates/sidebar.html:25 +msgid "Update password" +msgstr "修改密碼" + +#: mailu/ui/forms.py:118 +msgid "Enable automatic reply" +msgstr "啟用自動回信" + +#: mailu/ui/forms.py:119 +msgid "Reply subject" +msgstr "回信主旨" + +#: mailu/ui/forms.py:120 +msgid "Reply body" +msgstr "回信內文" + +#: mailu/ui/forms.py:122 +msgid "Start of vacation" +msgstr "休假開始" + +#: mailu/ui/forms.py:123 +msgid "End of vacation" +msgstr "休假結束" + +#: mailu/ui/forms.py:124 +msgid "Update" +msgstr "修改" + +#: mailu/ui/forms.py:129 +msgid "Your token (write it down, as it will never be displayed again)" +msgstr "您的 Token(請記下,因為之後不再顯示)" + +#: mailu/ui/forms.py:134 mailu/ui/templates/token/list.html:21 +msgid "Authorized IP" +msgstr "授權IP" + +#: mailu/ui/forms.py:140 +msgid "Alias" +msgstr "別名" + +#: mailu/ui/forms.py:142 +msgid "Use SQL LIKE Syntax (e.g. for catch-all aliases)" +msgstr "使用SQL LIKE語法(例如,用於萬用字符別名)" + +#: mailu/ui/forms.py:149 +msgid "Admin email" +msgstr "系統管理員信箱" + +#: mailu/ui/forms.py:150 mailu/ui/forms.py:155 mailu/ui/forms.py:168 +msgid "Submit" +msgstr "送出" + +#: mailu/ui/forms.py:154 +msgid "Manager email" +msgstr "管理員信箱" + +#: mailu/ui/forms.py:159 +msgid "Protocol" +msgstr "協定" + +#: mailu/ui/forms.py:162 +msgid "Hostname or IP" +msgstr "主機名稱或IP" + +#: mailu/ui/forms.py:163 mailu/ui/templates/client.html:20 +#: mailu/ui/templates/client.html:45 +msgid "TCP port" +msgstr "TCP埠口" + +#: mailu/ui/forms.py:164 +msgid "Enable TLS" +msgstr "啟用TLS" + +#: mailu/ui/forms.py:165 mailu/ui/templates/client.html:28 +#: mailu/ui/templates/client.html:53 mailu/ui/templates/fetch/list.html:21 +msgid "Username" +msgstr "用户名" + +#: mailu/ui/forms.py:167 +msgid "Keep emails on the server" +msgstr "在主機上保留電子郵件" + +#: mailu/ui/forms.py:172 +msgid "Announcement subject" +msgstr "公告主旨" + +#: mailu/ui/forms.py:174 +msgid "Announcement body" +msgstr "公告內文" + +#: mailu/ui/forms.py:176 +msgid "Send" +msgstr "寄出" + +#: mailu/ui/templates/announcement.html:4 +msgid "Public announcement" +msgstr "公告通知" + +#: mailu/ui/templates/antispam.html:4 mailu/ui/templates/sidebar.html:80 +#: mailu/ui/templates/user/settings.html:19 +msgid "Antispam" +msgstr "反垃圾郵件" + +#: mailu/ui/templates/antispam.html:8 +msgid "RSPAMD status page" +msgstr "RSPAMD 狀態頁面" + +#: mailu/ui/templates/client.html:8 +msgid "configure your email client" +msgstr "設定電子郵件用戶端" + +#: mailu/ui/templates/client.html:13 +msgid "Incoming mail" +msgstr "接收郵件" + +#: mailu/ui/templates/client.html:16 mailu/ui/templates/client.html:41 +msgid "Mail protocol" +msgstr "郵件協議" + +#: mailu/ui/templates/client.html:24 mailu/ui/templates/client.html:49 +msgid "Server name" +msgstr "主機名稱" + +#: mailu/ui/templates/client.html:38 +msgid "Outgoing mail" +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,請確認您的動作。" + +#: mailu/ui/templates/docker-error.html:4 +msgid "Docker error" +msgstr "Docker錯誤" + +#: mailu/ui/templates/docker-error.html:12 +msgid "An error occurred while talking to the Docker server." +msgstr "Docker主機通信出錯" + +#: mailu/ui/templates/macros.html:129 +msgid "copy to clipboard" +msgstr "複製到剪貼簿" + +#: mailu/ui/templates/sidebar.html:15 +msgid "My account" +msgstr "我的帳號" + +#: mailu/ui/templates/sidebar.html:19 mailu/ui/templates/user/list.html:37 +msgid "Settings" +msgstr "設定" + +#: mailu/ui/templates/sidebar.html:31 mailu/ui/templates/user/list.html:38 +msgid "Auto-reply" +msgstr "自動回信" + +#: mailu/ui/templates/fetch/list.html:4 mailu/ui/templates/sidebar.html:37 +#: mailu/ui/templates/user/list.html:39 +msgid "Fetched accounts" +msgstr "代收帳號" + +#: mailu/ui/templates/sidebar.html:43 mailu/ui/templates/token/list.html:4 +msgid "Authentication tokens" +msgstr "認證Token" + +#: mailu/ui/templates/sidebar.html:56 +msgid "Administration" +msgstr "管理" + +#: mailu/ui/templates/sidebar.html:62 +msgid "Announcement" +msgstr "公告" + +#: mailu/ui/templates/sidebar.html:68 +msgid "Administrators" +msgstr "系統管理員" + +#: mailu/ui/templates/sidebar.html:74 +msgid "Relayed domains" +msgstr "轉發網域" + +#: mailu/ui/templates/sidebar.html:88 +msgid "Mail domains" +msgstr "郵件網域" + +#: mailu/ui/templates/sidebar.html:99 +msgid "Webmail" +msgstr "網頁信箱" + +#: mailu/ui/templates/sidebar.html:135 +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 "新增全域系統管理員" + +#: mailu/ui/templates/admin/list.html:4 +msgid "Global administrators" +msgstr "全域系統管理員" + +#: mailu/ui/templates/admin/list.html:9 +msgid "Add administrator" +msgstr "新增系統管理員" + +#: mailu/ui/templates/admin/list.html:17 mailu/ui/templates/alias/list.html:19 +#: mailu/ui/templates/alternative/list.html:19 mailu/ui/templates/domain/list.html:17 +#: mailu/ui/templates/fetch/list.html:19 mailu/ui/templates/manager/list.html:19 +#: mailu/ui/templates/relay/list.html:17 mailu/ui/templates/token/list.html:19 +#: mailu/ui/templates/user/list.html:19 +msgid "Actions" +msgstr "操作" + +#: mailu/ui/templates/admin/list.html:18 mailu/ui/templates/alias/list.html:20 +#: mailu/ui/templates/manager/list.html:20 mailu/ui/templates/user/list.html:21 +msgid "Email" +msgstr "電子郵件" + +#: mailu/ui/templates/admin/list.html:25 mailu/ui/templates/alias/list.html:32 +#: mailu/ui/templates/alternative/list.html:29 mailu/ui/templates/domain/list.html:34 +#: mailu/ui/templates/fetch/list.html:34 mailu/ui/templates/manager/list.html:27 +#: mailu/ui/templates/relay/list.html:30 mailu/ui/templates/token/list.html:30 +#: mailu/ui/templates/user/list.html:34 +msgid "Delete" +msgstr "刪除" + +#: mailu/ui/templates/alias/create.html:4 +msgid "Create alias" +msgstr "建立別名" + +#: mailu/ui/templates/alias/edit.html:4 +msgid "Edit alias" +msgstr "修改別名" + +#: mailu/ui/templates/alias/list.html:4 +msgid "Alias list" +msgstr "別名列表" + +#: mailu/ui/templates/alias/list.html:12 +msgid "Add alias" +msgstr "新增別名" + +#: mailu/ui/templates/alias/list.html:23 mailu/ui/templates/alternative/list.html:21 +#: mailu/ui/templates/domain/list.html:23 mailu/ui/templates/fetch/list.html:25 +#: mailu/ui/templates/relay/list.html:21 mailu/ui/templates/token/list.html:22 +#: mailu/ui/templates/user/list.html:25 +msgid "Created" +msgstr "已建立" + +#: mailu/ui/templates/alias/list.html:24 mailu/ui/templates/alternative/list.html:22 +#: mailu/ui/templates/domain/list.html:24 mailu/ui/templates/fetch/list.html:26 +#: mailu/ui/templates/relay/list.html:22 mailu/ui/templates/token/list.html:23 +#: mailu/ui/templates/user/list.html:26 +msgid "Last edit" +msgstr "上次修改" + +#: mailu/ui/templates/alias/list.html:31 mailu/ui/templates/domain/list.html:33 +#: mailu/ui/templates/fetch/list.html:33 mailu/ui/templates/relay/list.html:29 +#: mailu/ui/templates/user/list.html:33 +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:20 +msgid "Name" +msgstr "名稱" + +#: mailu/ui/templates/domain/create.html:4 mailu/ui/templates/domain/list.html:9 +msgid "New domain" +msgstr "新網域" + +#: mailu/ui/templates/domain/details.html:4 +msgid "Domain details" +msgstr "網域詳細資訊" + +#: mailu/ui/templates/domain/details.html:15 +msgid "Regenerate keys" +msgstr "重新產生密鑰" + +#: mailu/ui/templates/domain/details.html:17 +msgid "Generate keys" +msgstr "產生密鑰" + +#: mailu/ui/templates/domain/details.html:30 +msgid "DNS MX entry" +msgstr "DNS MX條目" + +#: mailu/ui/templates/domain/details.html:34 +msgid "DNS SPF entries" +msgstr "DNS SPF條目" + +#: mailu/ui/templates/domain/details.html:40 +msgid "DKIM public key" +msgstr "DKIM公鑰" + +#: mailu/ui/templates/domain/details.html:44 +msgid "DNS DKIM entry" +msgstr "DNS DKIM條目" + +#: mailu/ui/templates/domain/details.html:48 +msgid "DNS DMARC entry" +msgstr "DNS DMARC條目" + +#: mailu/ui/templates/domain/details.html:58 +msgid "DNS TLSA entry" +msgstr "DNS TLSA條目" + +#: mailu/ui/templates/domain/details.html:63 +msgid "DNS client auto-configuration entries" +msgstr "DNS 客戶端自動設定條目" + +#: mailu/ui/templates/domain/edit.html:4 +msgid "Edit domain" +msgstr "修改網域" + +#: mailu/ui/templates/domain/list.html:4 +msgid "Domain list" +msgstr "網域列表" + +#: mailu/ui/templates/domain/list.html:18 +msgid "Manage" +msgstr "管理" + +#: mailu/ui/templates/domain/list.html:20 +msgid "Mailbox count" +msgstr "信箱数量" + +#: mailu/ui/templates/domain/list.html:21 +msgid "Alias count" +msgstr "別名数量" + +#: mailu/ui/templates/domain/list.html:31 +msgid "Details" +msgstr "詳細資訊" + +#: mailu/ui/templates/domain/list.html:38 +msgid "Users" +msgstr "用户" + +#: mailu/ui/templates/domain/list.html:39 +msgid "Aliases" +msgstr "別名" + +#: mailu/ui/templates/domain/list.html:40 +msgid "Managers" +msgstr "管理員" + +#: mailu/ui/templates/domain/list.html:42 +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 MX points to this server" +msgstr "為了註冊新網域,您必須先設定網域,並將網域 MX 指向這伺服主機。" + +#: mailu/ui/templates/domain/signup.html:18 +msgid "" +"If you do not know how to setup an MX record for your DNS zone,\n" +" please contact your DNS provider or administrator. Also, please wait a\n" +" couple minutes after the MX is set so the local server cache\n" +" expires." +msgstr "" +"如果您不知道如何為您的 DNS 網域設定 MX 記錄,請聯繫您的 DNS 提供商或管理員。" +"此外,請在設定 MX 之後等待幾分鐘 讓本地伺服主機的快取過期。" + +#: mailu/ui/templates/fetch/create.html:4 +msgid "Add a fetched account" +msgstr "新增代收帳號" + +#: mailu/ui/templates/fetch/edit.html:4 +msgid "Update a fetched account" +msgstr "修改代收帳號" + +#: mailu/ui/templates/fetch/list.html:12 +msgid "Add an account" +msgstr "新增一個帳號" + +#: mailu/ui/templates/fetch/list.html:20 +msgid "Endpoint" +msgstr "端點" + +#: mailu/ui/templates/fetch/list.html:22 +msgid "Keep emails" +msgstr "保留電子郵件" + +#: mailu/ui/templates/fetch/list.html:23 +msgid "Last check" +msgstr "上次檢查" + +#: mailu/ui/templates/fetch/list.html:24 +msgid "Status" +msgstr "狀態" + +#: mailu/ui/templates/fetch/list.html:38 +msgid "yes" +msgstr "是" + +#: mailu/ui/templates/fetch/list.html:38 +msgid "no" +msgstr "否" + +#: mailu/ui/templates/manager/create.html:4 +msgid "Add a manager" +msgstr "新增管理員" + +#: mailu/ui/templates/manager/list.html:4 +msgid "Manager list" +msgstr "管理員列表" + +#: mailu/ui/templates/manager/list.html:12 +msgid "Add manager" +msgstr "新增管理員" + +#: mailu/ui/templates/relay/create.html:4 +msgid "New relay domain" +msgstr "新的轉發網域" + +#: mailu/ui/templates/relay/edit.html:4 +msgid "Edit relayed 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/templates/token/create.html:4 +msgid "Create an authentication token" +msgstr "建立一個認證Token" + +#: mailu/ui/templates/token/list.html:12 +msgid "New token" +msgstr "新Token" + +#: 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:23 +msgid "Features and quotas" +msgstr "功能和配額" + +#: mailu/ui/templates/user/edit.html:4 +msgid "Edit user" +msgstr "修改用户" + +#: mailu/ui/templates/user/list.html:4 +msgid "User list" +msgstr "用户列表" + +#: mailu/ui/templates/user/list.html:12 +msgid "Add user" +msgstr "新增用户" + +#: mailu/ui/templates/user/list.html:20 mailu/ui/templates/user/settings.html:4 +msgid "User settings" +msgstr "用户設定" + +#: mailu/ui/templates/user/list.html:22 +msgid "Features" +msgstr "功能" + +#: mailu/ui/templates/user/password.html:4 +msgid "Password update" +msgstr "修改密码" + +#: mailu/ui/templates/user/reply.html:4 +msgid "Automatic reply" +msgstr "自動回信" + +#: mailu/ui/templates/user/settings.html:27 +msgid "Auto-forward" +msgstr "自動轉寄" + +#: mailu/ui/templates/user/signup_domain.html:8 +msgid "pick a domain for the new account" +msgstr "為新用戶選擇一個網域" + +#: mailu/ui/templates/user/signup_domain.html:14 +msgid "Domain" +msgstr "網域" + +#: mailu/ui/templates/user/signup_domain.html:15 +msgid "Available slots" +msgstr "剩餘名額" + +#~ msgid "to access the administration tools" +#~ msgstr "存取管理工具" + +#~ msgid "Forward emails" +#~ msgstr "轉寄郵件" diff --git a/core/admin/mailu/ui/templates/client.html b/core/admin/mailu/ui/templates/client.html index 593fd258..304de179 100644 --- a/core/admin/mailu/ui/templates/client.html +++ b/core/admin/mailu/ui/templates/client.html @@ -60,6 +60,6 @@ {%- endcall %}
{% trans %}If you use an Apple device,{% endtrans %} - {% trans %}click here to autoconfigure it.{% endtrans %} + {% trans %}click here to auto-configure it.{% endtrans %}
{%- endblock %} diff --git a/core/admin/mailu/ui/templates/domain/details.html b/core/admin/mailu/ui/templates/domain/details.html index 28f3f570..74657c28 100644 --- a/core/admin/mailu/ui/templates/domain/details.html +++ b/core/admin/mailu/ui/templates/domain/details.html @@ -10,13 +10,13 @@ {%- block main_action %} {%- if current_user.global_admin %} - + {%- if domain.dkim_publickey %} {% trans %}Regenerate keys{% endtrans %} {%- else %} {% trans %}Generate keys{% endtrans %} {%- endif %} - + {% trans %}Download zonefile{% endtrans %} {%- endif %} {%- endblock %} @@ -36,10 +36,6 @@ {%- if domain.dkim_publickey %} - - {% trans %}DKIM public key{% endtrans %} - {{ macros.clip("dkim_key") }}
{{ domain.dkim_publickey }}
- {% trans %}DNS DKIM entry{% endtrans %} {{ macros.clip("dns_dkim") }}
{{ domain.dns_dkim }}
diff --git a/core/admin/mailu/ui/views/domains.py b/core/admin/mailu/ui/views/domains.py index 4cdd830a..dcd1aedd 100644 --- a/core/admin/mailu/ui/views/domains.py +++ b/core/admin/mailu/ui/views/domains.py @@ -70,6 +70,27 @@ def domain_details(domain_name): domain = models.Domain.query.get(domain_name) or flask.abort(404) return flask.render_template('domain/details.html', domain=domain) +@ui.route('/domain/details//zonefile', methods=['GET']) +@access.domain_admin(models.Domain, 'domain_name') +def domain_download_zonefile(domain_name): + domain = models.Domain.query.get(domain_name) or flask.abort(404) + res = [domain.dns_mx, domain.dns_spf] + if domain.dkim_publickey: + record = domain.dns_dkim.split('"', 1)[0].strip() + txt = f'v=DKIM1; k=rsa; p={domain.dkim_publickey}' + txt = ' '.join(f'"{txt[p:p+250]}"' for p in range(0, len(txt), 250)) + res.append(f'{record} {txt}') + res.append(domain.dns_dmarc) + if domain.dns_tlsa: + res.append(domain.dns_tlsa) + res.extend(domain.dns_autoconfig) + res.append("") + return flask.Response( + "\n".join(res), + content_type="text/plain", + headers={"Content-disposition": f"attachment; filename={domain.name}-zonefile.txt"} + ) + @ui.route('/domain/genkeys/', methods=['GET', 'POST']) @access.domain_admin(models.Domain, 'domain_name') diff --git a/core/admin/mailu/utils.py b/core/admin/mailu/utils.py index 541ffb4d..9f75c233 100644 --- a/core/admin/mailu/utils.py +++ b/core/admin/mailu/utils.py @@ -100,7 +100,6 @@ def is_ip_in_subnet(ip, subnets=[]): # Application translation babel = flask_babel.Babel() -@babel.localeselector def get_locale(): """ selects locale for translation """ if not app.config['SESSION_COOKIE_NAME'] in flask.request.cookies: @@ -310,7 +309,7 @@ class MailuSessionConfig: # default size of session key parts uid_bits = 64 # default if SESSION_KEY_BITS is not set in config sid_bits = 128 # for now. must be multiple of 8! - time_bits = 32 # for now. must be multiple of 8! + time_bits = 32 # for now. must be multiple of 8! def __init__(self, app=None): @@ -400,7 +399,7 @@ class MailuSessionInterface(SessionInterface): if session.modified: session.delete() response.delete_cookie( - app.session_cookie_name, + app.config['SESSION_COOKIE_NAME'], domain=self.get_cookie_domain(app), path=self.get_cookie_path(app), ) @@ -413,7 +412,7 @@ class MailuSessionInterface(SessionInterface): # save session and update cookie if necessary if session.save(): response.set_cookie( - app.session_cookie_name, + app.config['SESSION_COOKIE_NAME'], session.sid, expires=datetime.now()+timedelta(seconds=app.config['PERMANENT_SESSION_LIFETIME']), httponly=self.get_cookie_httponly(app), @@ -473,29 +472,30 @@ class MailuSessionExtension: def init_app(self, app): """ Replace session management of application. """ + redis_session = False + if app.config.get('MEMORY_SESSIONS'): # in-memory session store for use in development app.session_store = DictStore() else: # redis-based session store for use in production + redis_session = True app.session_store = RedisStore( redis.StrictRedis().from_url(app.config['SESSION_STORAGE_URL']) ) + app.session_config = MailuSessionConfig(app) + app.session_interface = MailuSessionInterface() + if redis_session: # clean expired sessions once on first use in case lifetime was changed - def cleaner(): + with app.app_context(): with cleaned.get_lock(): if not cleaned.value: cleaned.value = True app.logger.info('cleaning session store') MailuSessionExtension.cleanup_sessions(app) - app.before_first_request(cleaner) - - app.session_config = MailuSessionConfig(app) - app.session_interface = MailuSessionInterface() - cleaned = Value('i', False) session = MailuSessionExtension() diff --git a/core/admin/run_dev.sh b/core/admin/run_dev.sh index 0f7c6e05..3d1fc771 100755 --- a/core/admin/run_dev.sh +++ b/core/admin/run_dev.sh @@ -78,6 +78,7 @@ ENV \ \ ADMIN_ADDRESS="127.0.0.1" \ FRONT_ADDRESS="127.0.0.1" \ + FTS_ATTACHMENTS_ADDRESS="127.0.0.1" \ SMTP_ADDRESS="127.0.0.1" \ IMAP_ADDRESS="127.0.0.1" \ REDIS_ADDRESS="127.0.0.1" \ diff --git a/core/admin/start.py b/core/admin/start.py index 5d403a3e..9574bbb7 100755 --- a/core/admin/start.py +++ b/core/admin/start.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 import os +import os.path +import time import logging as log import sys from socrate import system @@ -23,6 +25,14 @@ if account is not None and domain is not None and password is not None: log.info("Creating initial admin account %s@%s with mode %s", account, domain, mode) os.system("flask mailu admin %s %s '%s' --mode %s" % (account, domain, password, mode)) +def test_unsupported(): + import codecs + if os.path.isfile(codecs.decode('/.qbpxrerai', 'rot13')) or os.environ.get(codecs.decode('V_XABJ_ZL_FRGHC_QBRFAG_SVG_ERDHVERZRAGF_NAQ_JBAG_SVYR_VFFHRF_JVGUBHG_CNGPURF', 'rot13'), None) or os.environ.get(codecs.decode('ZNVYH_URYZ_PUNEG', 'rot13'), None): + return + log.critical('Your system is not supported. Please start by reading the documentation and then http://www.catb.org/~esr/faqs/smart-questions.html') + while True: + time.sleep(5) + def test_DNS(): import dns.resolver import dns.exception @@ -50,6 +60,7 @@ def test_DNS(): time.sleep(5) test_DNS() +test_unsupported() cmdline = [ "gunicorn", diff --git a/core/base/Dockerfile b/core/base/Dockerfile index 88332d3a..2f9c1142 100644 --- a/core/base/Dockerfile +++ b/core/base/Dockerfile @@ -3,7 +3,7 @@ # base system image (intermediate) # Note when updating the alpine tag, first manually run the workflow .github/workflows/mirror.yml. # Just run the workflow with the tag that must be synchronised. -ARG DISTRO=ghcr.io/mailu/alpine:3.17.2 +ARG DISTRO=ghcr.io/mailu/alpine:3.18.4 FROM $DISTRO as system ENV TZ=Etc/UTC LANG=C.UTF-8 @@ -16,7 +16,7 @@ RUN set -euxo pipefail \ ; adduser -Sg ${MAILU_UID} -G mailu -h /app -g "mailu app" -s /bin/bash mailu \ ; apk add --no-cache bash ca-certificates curl python3 tzdata \ ; ! [[ "$(uname -m)" == x86_64 ]] \ - || apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing hardened-malloc==11-r0 + || apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing hardened-malloc WORKDIR /app @@ -27,7 +27,7 @@ CMD /bin/bash FROM system as build ARG MAILU_DEPS=prod -ARG SNUFFLEUPAGUS_VERSION=0.9.0 +ARG SNUFFLEUPAGUS_VERSION=0.10.0 ENV VIRTUAL_ENV=/app/venv @@ -79,9 +79,9 @@ COPY --chown=root:root --from=build /app/snuffleupagus.so /usr/lib/php81/modules ENV \ VIRTUAL_ENV=/app/venv \ PATH="/app/venv/bin:${PATH}" \ - LD_PRELOAD="/usr/lib/libhardened_malloc.so" \ ADMIN_ADDRESS="admin" \ FRONT_ADDRESS="front" \ + FTS_ATTACHMENTS_ADDRESS="tika" \ SMTP_ADDRESS="smtp" \ IMAP_ADDRESS="imap" \ OLETOOLS_ADDRESS="oletools" \ diff --git a/core/base/libs/socrate/socrate/system.py b/core/base/libs/socrate/socrate/system.py index e80863b8..3dcdea3d 100644 --- a/core/base/libs/socrate/socrate/system.py +++ b/core/base/libs/socrate/socrate/system.py @@ -1,11 +1,14 @@ import hmac import logging as log import os +import signal import sys import re from pwd import getpwnam import socket import tenacity +import subprocess +import threading @tenacity.retry(stop=tenacity.stop_after_attempt(100), wait=tenacity.wait_random(min=2, max=5)) @@ -27,7 +30,7 @@ def _coerce_value(value): return value class LogFilter(object): - def __init__(self, stream, re_patterns, log_file): + def __init__(self, stream, re_patterns): self.stream = stream if isinstance(re_patterns, list): self.pattern = re.compile('|'.join([f'(?:{pattern})' for pattern in re_patterns])) @@ -36,7 +39,6 @@ class LogFilter(object): else: self.pattern = re_patterns self.found = False - self.log_file = log_file def __getattr__(self, attr_name): return getattr(self.stream, attr_name) @@ -48,12 +50,6 @@ class LogFilter(object): if not self.pattern.search(data): self.stream.write(data) self.stream.flush() - if self.log_file: - try: - with open(self.log_file, 'a', encoding='utf-8') as l: - l.write(data) - except: - pass else: # caught bad pattern self.found = True @@ -66,22 +62,29 @@ def _is_compatible_with_hardened_malloc(): lines = f.readlines() for line in lines: # See #2764, we need vmovdqu - if line.startswith('flags') and ' avx ' not in line: + # See #2959, we need vpunpckldq + if line.startswith('flags') and ' avx2 ' not in line: return False # See #2541 if line.startswith('Features') and ' lrcpc ' not in line: return False return True -def set_env(required_secrets=[], log_filters=[], log_file=None): - if log_filters: - sys.stdout = LogFilter(sys.stdout, log_filters, log_file) - sys.stderr = LogFilter(sys.stderr, log_filters, log_file) - log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", 'WARNING')) - if 'LD_PRELOAD' in os.environ and not _is_compatible_with_hardened_malloc(): - log.warning('Disabling hardened-malloc on this CPU') - del os.environ['LD_PRELOAD'] +def sigterm_handler(_signo, _stack_frame): + log.critical("Received SIGTERM, terminating.") + sys.exit(143) + +def set_env(required_secrets=[], log_filters=[]): + if log_filters: + sys.stdout = LogFilter(sys.stdout, log_filters) + sys.stderr = LogFilter(sys.stderr, log_filters) + log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", 'WARNING')) + signal.signal(signal.SIGTERM, sigterm_handler) + + if not 'LD_PRELOAD' in os.environ and _is_compatible_with_hardened_malloc(): + log.warning('Your CPU has Advanced Vector Extensions available, we recommend you enable hardened-malloc earlier in the boot process by adding LD_PRELOAD=/usr/lib/libhardened_malloc.so to your mailu.env') + os.environ['LD_PRELOAD'] = '/usr/lib/libhardened_malloc.so' """ This will set all the environment variables and retains only the secrets we need """ if 'SECRET_KEY_FILE' in os.environ: @@ -114,3 +117,24 @@ def drop_privs_to(username='mailu'): os.setgid(pwnam.pw_gid) os.setuid(pwnam.pw_uid) os.environ['HOME'] = pwnam.pw_dir + +# forwards text lines from src to dst in an infinite loop +def forward_text_lines(src, dst): + while True: + current_line = src.readline() + dst.write(current_line) + + +# runs a process and passes its standard/error output to the standard/error output of the current python script +def run_process_and_forward_output(cmd): + process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + + stdout_thread = threading.Thread(target=forward_text_lines, args=(process.stdout, sys.stdout)) + stdout_thread.daemon = True + stdout_thread.start() + + stderr_thread = threading.Thread(target=forward_text_lines, args=(process.stderr, sys.stderr)) + stderr_thread.daemon = True + stderr_thread.start() + + process.wait() diff --git a/core/base/requirements-build.txt b/core/base/requirements-build.txt index 62a7d9fd..ed145a00 100644 --- a/core/base/requirements-build.txt +++ b/core/base/requirements-build.txt @@ -1,3 +1,3 @@ -pip==22.3.1 -setuptools==65.6.3 -wheel==0.38.4 +pip==23.3.1 +setuptools==68.2.2 +wheel==0.41.3 diff --git a/core/base/requirements-prod.txt b/core/base/requirements-prod.txt index 1f1389cf..2d5b0002 100644 --- a/core/base/requirements-prod.txt +++ b/core/base/requirements-prod.txt @@ -1,86 +1,93 @@ -aiodns==3.0.0 -aiohttp==3.8.5 -aiosignal==1.2.0 -alembic==1.8.1 -async-timeout==4.0.2 -attrs==22.1.0 -Babel==2.11.0 +aiodns==3.1.1 +aiohttp==3.8.6 +aiosignal==1.3.1 +alembic==1.12.1 +aniso8601==9.0.1 +async-timeout==4.0.3 +attrs==23.1.0 +Babel==2.13.1 bcrypt==4.0.1 -blinker==1.5 +blinker==1.6.3 certifi==2023.7.22 -cffi==1.15.1 -charset-normalizer==2.1.1 -click==8.1.3 +cffi==1.16.0 +charset-normalizer==3.3.1 +click==8.1.7 colorclass==2.2.2 -cryptography==41.0.3 -decorator==5.1.1 +cryptography==41.0.5 defusedxml==0.7.1 -Deprecated==1.2.13 -dnspython==2.2.1 -dominate==2.7.0 +Deprecated==1.2.14 +dnspython==2.4.2 +dominate==2.8.0 easygui==0.98.3 -email-validator==1.3.0 -Flask==2.2.5 -Flask-Babel==2.0.0 +email-validator==2.1.0.post1 +Flask==2.3.3 +flask-babel==4.0.0 Flask-Bootstrap==3.3.7.1 -Flask-DebugToolbar==0.13.1 -Flask-Login==0.6.2 -flask-marshmallow==0.14.0 -Flask-Migrate==3.1.0 -Flask-RESTX==1.0.5 +#Flask-DebugToolbar is not compatible with Flask 2.3.3+ +#Flask-DebugToolbar==0.13.1 +Flask-Login==0.6.3 +flask-marshmallow==0.15.0 +Flask-Migrate==4.0.5 +flask-restx==1.1.0 Flask-SQLAlchemy==2.5.1 -Flask-WTF==1.0.1 -frozenlist==1.3.1 -greenlet==2.0.0 -gunicorn==20.1.0 +# >2.5.1 bug with parsing models.py. Could otherwise be 3.0.5 +Flask-WTF==1.2.1 +frozenlist==1.4.0 +greenlet==3.0.1 +gunicorn==21.2.0 idna==3.4 +importlib-resources==6.1.0 infinity==1.5 intervals==0.9.2 itsdangerous==2.1.2 Jinja2==3.1.2 -limits==2.7.1 -Mako==1.2.3 -MarkupSafe==2.1.1 -marshmallow==3.18.0 -marshmallow-sqlalchemy==0.28.1 +jsonschema==4.19.2 +jsonschema-specifications==2023.7.1 +limits==3.6.0 +Mako==1.2.4 +MarkupSafe==2.1.3 +marshmallow==3.20.1 +marshmallow-sqlalchemy==0.29 msoffcrypto-tool==5.1.1 -multidict==6.0.2 -mysql-connector-python==8.0.32 +multidict==6.0.4 +mysql-connector-python==8.2.0 olefile==0.46 oletools==0.60.1 -packaging==21.3 +packaging==23.2 passlib==1.7.4 pcodedmp==1.2.6 podop @ file:///app/libs/podop -postfix-mta-sts-resolver==1.1.4 -protobuf==3.20.2 -psycopg2-binary==2.9.5 -pycares==4.2.2 +postfix-mta-sts-resolver==1.4.0 +protobuf==4.21.12 +psycopg2-binary==2.9.9 +pycares==4.4.0 pycparser==2.21 -Pygments==2.15.0 +Pygments==2.16.1 pyparsing==2.4.7 python-dateutil==2.8.2 python-magic==0.4.27 -pytz==2022.6 +pytz==2023.3.post1 PyYAML==6.0.1 Radicale==3.1.8 -redis==4.4.4 +redis==5.0.1 +referencing==0.30.2 requests==2.31.0 +rpds-py==0.10.6 six==1.16.0 socrate @ file:///app/libs/socrate -SQLAlchemy==1.4.42 +SQLAlchemy==1.4.50 srslib==0.1.4 tabulate==0.9.0 -tenacity==8.1.0 -typing_extensions==4.4.0 -urllib3==1.26.12 -validators==0.20.0 +tenacity==8.2.3 +typing_extensions==4.8.0 +urllib3==2.0.7 +validators==0.22.0 visitor==0.1.3 vobject==0.9.6.1 -watchdog==2.1.9 -Werkzeug==2.2.3 -wrapt==1.14.1 -WTForms==3.0.1 +watchdog==3.0.0 +Werkzeug===2.3.7 +wrapt==1.15.0 +WTForms==3.1.1 WTForms-Components==0.10.5 xmltodict==0.13.0 -yarl==1.8.1 +yarl==1.9.2 diff --git a/core/dovecot/Dockerfile b/core/dovecot/Dockerfile index 872e1ecf..25eb9263 100644 --- a/core/dovecot/Dockerfile +++ b/core/dovecot/Dockerfile @@ -7,7 +7,8 @@ ARG VERSION LABEL version=$VERSION RUN set -euxo pipefail \ - ; apk add --no-cache dovecot dovecot-fts-xapian dovecot-lmtpd dovecot-pigeonhole-plugin dovecot-pop3d dovecot-submissiond rspamd-client xapian-core \ + ; echo -e 'http://dl-cdn.alpinelinux.org/alpine/edge/main\nhttp://dl-cdn.alpinelinux.org/alpine/edge/testing\nhttp://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories \ + ; apk add --no-cache 'dovecot<2.4' dovecot-lmtpd dovecot-pigeonhole-plugin dovecot-pop3d dovecot-submissiond rspamd-client dovecot-fts-flatcurve \ ; mkdir /var/lib/dovecot COPY conf/ /conf/ diff --git a/core/dovecot/conf/dovecot.conf b/core/dovecot/conf/dovecot.conf index e35ab4a1..906b0e30 100644 --- a/core/dovecot/conf/dovecot.conf +++ b/core/dovecot/conf/dovecot.conf @@ -37,7 +37,7 @@ mail_plugins = $mail_plugins quota quota_clone{{ ' ' }} zlib{{ ' ' }} {%- endif %} {%- if (FULL_TEXT_SEARCH or '').lower() not in ['off', 'false', '0'] -%} - fts fts_xapian + fts fts_flatcurve {%- endif %} default_vsz_limit = 2GB @@ -57,11 +57,21 @@ plugin { quota_clone_dict = proxy:/tmp/podop.socket:quota {% if (FULL_TEXT_SEARCH or '').lower() not in ['off', 'false', '0'] %} - fts = xapian - fts_xapian = partial=2 full=30 + fts = flatcurve + fts_index_timeout = 50s + fts_languages = {% if FULL_TEXT_SEARCH %}{{ FULL_TEXT_SEARCH.split(",") | join(" ") }}{% else %}en{% endif %} + fts_tokenizers = generic email-address fts_autoindex = yes fts_enforced = yes fts_autoindex_exclude = \Trash + fts_autoindex_exclude1 = \Junk + fts_filters = normalizer-icu snowball stopwords + fts_filters_en = lowercase snowball english-possessive stopwords + fts_filters_fr = lowercase snowball contractions stopwords + fts_header_excludes = Received DKIM-* ARC-* X-* x-* Comments Delivered-To Return-Path Authentication-Results Message-ID References In-Reply-To Thread-* Accept-Language Content-* MIME-Version + {% if FULL_TEXT_SEARCH_ATTACHMENTS %} + fts_tika = http://{{ FTS_ATTACHMENTS_ADDRESS }}:9998/tika/ + {% endif %} {% endif %} {% if COMPRESSION in [ 'gz', 'bz2', 'lz4', 'zstd' ] %} @@ -73,6 +83,12 @@ plugin { {% endif %} } +service indexer-worker { + executable = /bin/nice -n 10 /usr/libexec/dovecot/indexer-worker + # TODO: maybe MAXPROC? I guess it depends on how much RAM is available + process_limit = 1 +} + ############### # Authentication ############### diff --git a/core/dovecot/start.py b/core/dovecot/start.py index d25f860b..b162db95 100755 --- a/core/dovecot/start.py +++ b/core/dovecot/start.py @@ -3,13 +3,11 @@ import os import glob import multiprocessing -import logging as log -import sys from podop import run_server from socrate import system, conf -system.set_env(log_filters=r'Error\: SSL context initialization failed, disabling SSL\: Can\'t load SSL certificate \(ssl_cert setting\)\: The certificate is empty$') +system.set_env(log_filters=[r'Error\: SSL context initialization failed, disabling SSL\: Can\'t load SSL certificate \(ssl_cert setting\)\: The certificate is empty$']) def start_podop(): system.drop_privs_to('mail') @@ -35,4 +33,5 @@ os.system("chown mail:mail /mail") os.system("chown -R mail:mail /var/lib/dovecot /conf") multiprocessing.Process(target=start_podop).start() -os.system("dovecot -c /etc/dovecot/dovecot.conf -F") +cmd = ['/usr/sbin/dovecot', '-c', '/etc/dovecot/dovecot.conf', '-F'] +system.run_process_and_forward_output(cmd) diff --git a/core/nginx/Dockerfile b/core/nginx/Dockerfile index f2eaac81..cacb6c99 100644 --- a/core/nginx/Dockerfile +++ b/core/nginx/Dockerfile @@ -17,7 +17,8 @@ ARG VERSION LABEL version=$VERSION RUN set -euxo pipefail \ - ; apk add --no-cache certbot nginx nginx-mod-http-brotli nginx-mod-mail openssl dovecot-lua dovecot-pigeonhole-plugin dovecot-lmtpd dovecot-pop3d dovecot-submissiond + ; apk add --no-cache certbot nginx nginx-mod-http-brotli nginx-mod-mail openssl \ + ; apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main dovecot-lua dovecot-pigeonhole-plugin 'dovecot-lmtpd<2.4' dovecot-pop3d dovecot-submissiond COPY conf/ /conf/ COPY --from=static /static/ /static/ diff --git a/core/nginx/letsencrypt.py b/core/nginx/letsencrypt.py index a14dfa15..a8abbee7 100755 --- a/core/nginx/letsencrypt.py +++ b/core/nginx/letsencrypt.py @@ -47,7 +47,7 @@ time.sleep(5) class MyRequestHandler(SimpleHTTPRequestHandler): def do_GET(self): - if self.path == '/testing': + if self.path == '/.well-known/acme-challenge/testing': self.send_response(204) else: self.send_response(404) @@ -55,19 +55,19 @@ class MyRequestHandler(SimpleHTTPRequestHandler): self.end_headers() def serve_one_request(): - with HTTPServer(("0.0.0.0", 8008), MyRequestHandler) as server: + with HTTPServer(("127.0.0.1", 8008), MyRequestHandler) as server: server.handle_request() # Run certbot every day while True: while True: - hostname = os.environ['HOSTNAMES'].split(' ')[0] + hostname = os.environ['HOSTNAMES'].split(',')[0] target = f'http://{hostname}/.well-known/acme-challenge/testing' thread = Thread(target=serve_one_request) thread.start() r = requests.get(target) if r.status_code != 204: - log.error(f"Can't reach {target}!, please ensure it's fixed or change the TLS_FLAVOR.") + log.critical(f"Can't reach {target}!, please ensure it's fixed or change the TLS_FLAVOR.") time.sleep(5) else: break diff --git a/core/nginx/start.py b/core/nginx/start.py index 01ecca03..a50abec2 100755 --- a/core/nginx/start.py +++ b/core/nginx/start.py @@ -4,7 +4,7 @@ import os import subprocess from socrate import system -system.set_env(log_filters=r'could not be resolved \(\d\: [^\)]+\) while in resolving client address, client\: [^,]+, server: [^\:]+\:(25,110,143,587,465,993,995)$') +system.set_env(log_filters=r'could not be resolved \(\d\: [^\)]+\) while in resolving client address, client\: [^,]+, server: [^\:]+\:(25|110|143|587|465|993|995)$') # Check if a stale pid file exists if os.path.exists("/var/run/nginx.pid"): @@ -17,4 +17,5 @@ elif os.environ["TLS_FLAVOR"] in [ "mail", "cert" ]: subprocess.call(["/config.py"]) os.system("dovecot -c /etc/dovecot/proxy.conf") -os.execv("/usr/sbin/nginx", ["nginx", "-g", "daemon off;"]) +cmd = ['/usr/sbin/nginx', '-g', 'daemon off;'] +system.run_process_and_forward_output(cmd) diff --git a/core/oletools/Dockerfile b/core/oletools/Dockerfile index 39fd0e18..f636cc6d 100644 --- a/core/oletools/Dockerfile +++ b/core/oletools/Dockerfile @@ -6,11 +6,17 @@ FROM base ARG VERSION=local LABEL version=$VERSION +ARG OLEFY_SCRIPT=https://raw.githubusercontent.com/HeinleinSupport/olefy/f8aac6cc55283886d153e89c8f27fae66b1c24e2/olefy.py +ARG OLEFY_SHA256=1f5aa58b78ca7917350135b4425e5ed4d580c7051aabed1952c6afd12d0345a0 + RUN set -euxo pipefail \ ; apk add --no-cache netcat-openbsd libmagic libffi \ - ; curl -sLo olefy.py https://raw.githubusercontent.com/HeinleinSupport/olefy/f8aac6cc55283886d153e89c8f27fae66b1c24e2/olefy.py \ + ; curl -sLo olefy.py $OLEFY_SCRIPT \ + ; echo "$OLEFY_SHA256 olefy.py" |sha256sum -c \ ; chmod 755 olefy.py +COPY start.py / + RUN echo $VERSION >/version HEALTHCHECK --start-period=60s CMD echo PING|nc -q1 127.0.0.1 11343|grep "PONG" @@ -28,4 +34,4 @@ ENV \ OLEFY_DEL_TMP="1" \ OLEFY_DEL_TMP_FAILED="1" -CMD /app/olefy.py +CMD /start.py diff --git a/core/oletools/start.py b/core/oletools/start.py new file mode 100755 index 00000000..b0972908 --- /dev/null +++ b/core/oletools/start.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 + +from socrate import system + +system.set_env() + +with open('/app/olefy.py') as olefy: + exec(olefy.read()) diff --git a/core/postfix/start.py b/core/postfix/start.py index d6af1a98..7daf7c3b 100755 --- a/core/postfix/start.py +++ b/core/postfix/start.py @@ -13,8 +13,8 @@ from socrate import system, conf system.set_env(log_filters=[ r'(dis)?connect from localhost\[(\:\:1|127\.0\.0\.1)\]( quit=1 commands=1)?$', r'haproxy read\: short protocol header\: QUIT$', - r'discarding EHLO keywords\: PIPELINING$', - ], log_file=os.environ.get('POSTFIX_LOG_FILE')) + r'discarding EHLO keywords\: PIPELINING$' + ]) os.system("flock -n /queue/pid/master.pid rm /queue/pid/master.pid") @@ -100,4 +100,5 @@ os.system("/usr/libexec/postfix/post-install meta_directory=/etc/postfix create- # Before starting postfix, we need to check permissions on /queue # in the event that postfix,postdrop id have changed os.system("postfix set-permissions") -os.system("postfix start-fg") +cmd = ['postfix', 'start-fg'] +system.run_process_and_forward_output(cmd) diff --git a/core/rspamd/conf/antivirus.conf b/core/rspamd/conf/antivirus.conf index 0f47ecca..072c687b 100644 --- a/core/rspamd/conf/antivirus.conf +++ b/core/rspamd/conf/antivirus.conf @@ -4,7 +4,7 @@ clamav { symbol = "CLAM_VIRUS"; type = "clamav"; servers = "{{ ANTIVIRUS_ADDRESS }}:3310"; - timeout = 5; + timeout = 60; retransmits = 10; {% if ANTIVIRUS_ACTION|default('discard') == 'reject' %} action = "reject" diff --git a/core/rspamd/conf/external_services.conf b/core/rspamd/conf/external_services.conf index 609f341b..2557bcb7 100644 --- a/core/rspamd/conf/external_services.conf +++ b/core/rspamd/conf/external_services.conf @@ -7,8 +7,8 @@ oletools { scan_mime_parts = true; extended = true; max_size = 3145728; - timeout = 20.0; - retransmits = 1; + timeout = 60.0; + retransmits = 10; patterns { OLETOOLS_MACRO_FOUND= '^.....M..$'; diff --git a/core/rspamd/conf/force_actions.conf b/core/rspamd/conf/force_actions.conf index b72a8a0c..0bb96c37 100644 --- a/core/rspamd/conf/force_actions.conf +++ b/core/rspamd/conf/force_actions.conf @@ -14,5 +14,15 @@ rules { expression = "!IS_LOCALLY_GENERATED & !MAILLIST & BLACKLIST_ANTISPOOF"; message = "Rejected (anti-spoofing: auth-failed)"; } + ANTIVIRUS_FLAGGED { + action = "reject"; + expression = "CLAM_VIRUS | OLETOOLS_MACRO_MRAPTOR | OLETOOLS_MACRO_SUSPICIOUS"; + message = "Rejected (dangerous/malicious code detected)"; + } + ANTIVIRUS_FAILED { + action = "soft reject"; + expression = "CLAM_VIRUS_FAIL | OLETOOLS_FAIL"; + message = "Please retry later (anti-virus/oletools not ready)"; + } } .include(try=true,priority=1,duplicate=merge) "/overrides/force_actions.conf" diff --git a/core/rspamd/start.py b/core/rspamd/start.py index d6991253..67574a01 100755 --- a/core/rspamd/start.py +++ b/core/rspamd/start.py @@ -16,7 +16,8 @@ env = system.set_env() config_files = [] for rspamd_file in glob.glob("/conf/*"): conf.jinja(rspamd_file, env, os.path.join("/etc/rspamd/local.d", os.path.basename(rspamd_file))) - config_files.append(os.path.basename(rspamd_file)) + if rspamd_file != '/conf/forbidden_file_extension.map': + config_files.append(os.path.basename(rspamd_file)) for override_file in glob.glob("/overrides/*"): if os.path.basename(override_file) not in config_files: @@ -37,4 +38,4 @@ while True: os.system("mkdir -m 755 -p /run/rspamd") os.system("chown rspamd:rspamd /run/rspamd") os.system("find /var/lib/rspamd | grep -v /filter | xargs -n1 chown rspamd:rspamd") -os.execv("/usr/sbin/rspamd", ["rspamd", "-f", "-u", "rspamd", "-g", "rspamd"]) +os.execv("/usr/bin/rspamd", ["rspamd", "-f", "-u", "rspamd", "-g", "rspamd"]) diff --git a/docs/Dockerfile b/docs/Dockerfile index 18ca8333..958eaf87 100644 --- a/docs/Dockerfile +++ b/docs/Dockerfile @@ -1,5 +1,5 @@ # Convert .rst files to .html in temporary build container -FROM python:3.8-alpine3.14 AS build +FROM python:3.12.0-alpine3.18 AS build ARG version=master ENV VERSION=$version @@ -16,7 +16,7 @@ RUN apk add --no-cache --virtual .build-deps \ # Build nginx deployment image including generated html -FROM nginx:1.21-alpine +FROM nginx:1.25.3-alpine ARG version=master ARG pinned_version=master diff --git a/docs/antispam.rst b/docs/antispam.rst index 9b204999..5042ff0c 100644 --- a/docs/antispam.rst +++ b/docs/antispam.rst @@ -33,7 +33,7 @@ Rspamd rejects non-compliant email messages and email messages that contain viru * In the administration web interface, under settings under Antispam 'Enable spam filter' must be ticked. If this option is disabled, then all email messages will automatically go to the inbox folder. Except for email messages with a score of 15 or higher, as these email messages are rejected by Rspamd. - * In the administration web interface, under settings under Antispam, the user defined spam filter tolerance must be configured. The default value is 80%. The lower the spam filter tolerance, the more false positives (ham classified as spam). The user can change this setting to finetune when an email message is classified as spam. + * In the administration web interface, under settings under Antispam, the user defined spam filter tolerance must be configured. The default value is 80%. The lower the spam filter tolerance, the more false positives (ham classified as spam). The user can change this setting to fine-tune when an email message is classified as spam. * Dovecot extracts the X-Spam-Level email header from the email message and converts the spam score (0 - 15) to a 0 - 100 percent scale. This spam score is compared with the user defined spam filter tolerance. If the spam score is lower than the user defined spam filter tolerance, then the email message is accepted. In logic: @@ -110,7 +110,7 @@ The following steps have to be taken to configure an additional symbol (rule) th * no action: allow message. The email message will be allowed without a spam score being added in the mail header. This can be used for creating a whitelist filter. - * soft reject: temporarily delay message (this is used, for instance, to greylist or ratelimit messages) + * soft reject: temporarily delay message (this is used, for instance, to greylist or rate-limit messages) To move an email message to the Junk (Spam) folder, a score of 15 can be used in combination with the action "add header". The above example configuration will reject all emails send from domains that are listed in '/etc/rspamd/override.d/blacklist.inc'. @@ -158,7 +158,7 @@ For more information on using the multimap filter see the official `multimap doc Can I change the list of authorized file attachments? ----------------------------------------------------- -Mailu rejects emails with file attachements it deems to be "executable" or otherwise dangerous. If you would like to tweak the block list, you can do so using the following commands: +Mailu rejects emails with file attachments it deems to be "executable" or otherwise dangerous. If you would like to tweak the block list, you can do so using the following commands: .. code-block:: bash diff --git a/docs/compose/.env b/docs/compose/.env index 62e767cf..5c54815c 100644 --- a/docs/compose/.env +++ b/docs/compose/.env @@ -152,3 +152,8 @@ REJECT_UNLISTED_RECIPIENT= # Log level threshold in start.py (value: CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET) LOG_LEVEL=WARNING + +# If your CPU supports Advanced Vector Extensions +# (AVX2 on x86_64, lrcpc on ARM64), you should consider enabling +# hardened-malloc earlier by uncommenting this +# LD_PRELOAD=/usr/lib/libhardened_malloc.so diff --git a/docs/compose/requirements.rst b/docs/compose/requirements.rst index e828597d..1ce29454 100644 --- a/docs/compose/requirements.rst +++ b/docs/compose/requirements.rst @@ -43,7 +43,7 @@ make sure that you either: - setup a root *ext4* partition, - or setup a root *btrfs* partition, - - or leave enough unpartitionned space for a dedicated *ext4* or *btrfs* + - or leave enough unpartitioned space for a dedicated *ext4* or *btrfs* partition. If you chose to create a dedicated partition, simply mount it to @@ -74,7 +74,7 @@ default Debian install: apt-get autoremove --purge exim4 exim4-base -Finally, Docker relies heavily on ``iptables`` for port forwardings. You +Finally, Docker relies heavily on ``iptables`` for port forwarding. You should use ``iptables-persistent`` (or any equivalent tool on other systems) for managing persistent rules. If you were brave enough to switch to ``nftables``, you will have to rollback until official support is released diff --git a/docs/compose/setup.rst b/docs/compose/setup.rst index f4c9c574..81433ba3 100644 --- a/docs/compose/setup.rst +++ b/docs/compose/setup.rst @@ -76,6 +76,15 @@ Review configuration variables After downloading the files, open ``mailu.env`` and review the variable settings. Make sure to read the comments in the file and instructions from the :ref:`common_cfg` page. +If your CPU supports Advanced Vector Extensions (AVX2 on x86_64, lrcpc on ARM64), you should +consider enabling hardened-malloc earlier in the boot process by adding the following to +your mailu.env: + +.. code-block:: bash + + LD_PRELOAD=/usr/lib/libhardened_malloc.so + + Finish setting up TLS --------------------- diff --git a/docs/configuration.rst b/docs/configuration.rst index e4bd045e..3c193ac4 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -51,8 +51,8 @@ accounts for a specific IP subnet as defined in The ``AUTH_RATELIMIT_USER`` (default: 50/day) holds a security setting for fighting attackers that attempt to guess a user's password (typically using a password -bruteforce attack). The value defines the limit of distinct authentication attempts -allowed for any given account within a specific timeframe. Multiple attempts for the +brute-force attack). The value defines the limit of distinct authentication attempts +allowed for any given account within a specific time-frame. Multiple attempts for the same account with the same password only counts for one. The ``AUTH_RATELIMIT_EXEMPTION_LENGTH`` (default: 86400) is the number of seconds @@ -104,7 +104,7 @@ by setting ``INBOUND_TLS_ENFORCE`` to ``True``. Please note that this is forbidd internet facing hosts according to e.g. `RFC 3207`_ , because this prevents MTAs without STARTTLS support or e.g. mismatching TLS versions to deliver emails to Mailu. -The ``SCAN_MACROS`` (default: True) setting controls whether Mailu will endavour +The ``SCAN_MACROS`` (default: True) setting controls whether Mailu will endeavor to reject emails containing documents with malicious macros. Under the hood, it uses `mraptor from oletools`_ to determine whether a macro is malicious or not. @@ -133,12 +133,15 @@ later classify incoming mail based on the custom part. The ``DMARC_RUA`` and ``DMARC_RUF`` are DMARC protocol specific values. They hold the localpart for DMARC rua and ruf email addresses. -Full-text search is enabled for IMAP is enabled by default. This feature can be disabled -(e.g. for performance reasons) by setting the optional variable ``FULL_TEXT_SEARCH`` to ``off``. +The ``FULL_TEXT_SEARCH`` variable (default: 'en') is a comma separated list of +language codes as defined on `fts_languages`_. This feature can be disabled +(e.g. for performance reasons) by setting the variable to ``off``. You can set a global ``DEFAULT_QUOTA`` to be used for mailboxes when the domain has no specific quota configured. +.. _`fts_languages`: https://doc.dovecot.org/settings/plugin/fts-plugin/#fts-languages + .. _web_settings: Web settings @@ -167,11 +170,11 @@ in the admin interface, while ``SITENAME`` is a customization option for every Web interface. - ``LOGO_BACKGROUND`` sets a custom background colour for the brand logo - in the topleft of the main admin interface. + in the top-left of the main admin interface. For a list of colour codes refer to this page of `w3schools`_. - ``LOGO_URL`` sets a URL for a custom logo. This logo replaces the Mailu - logo in the topleft of the main admin interface. + logo in the top-left of the main admin interface. .. _`w3schools`: https://www.w3schools.com/cssref/css_colors.asp @@ -213,7 +216,10 @@ Depending on your particular deployment you most probably will want to change th Advanced settings ----------------- -The ``API_TOKEN`` (default: None) configures the authentication token. + +The ``AUTH_REQUIRE_TOKENS`` (default: False) setting controls whether thick clients can authenticate using passwords or whether they are forced to use tokens/application specific passwords. + +The ``API_TOKEN`` (default: None) setting configures the authentication token. This token must be passed as request header to the API as authentication token. This is a mandatory setting for using the RESTful API. @@ -375,22 +381,6 @@ To disable all plugins just set ``ROUNDCUBE_PLUGINS`` to ``mailu``. To configure a plugin add php files named ``*.inc.php`` to roundcube's :ref:`override section `. -Mail log settings ------------------ - -By default, all services log directly to stdout/stderr. Logs can be collected by any docker log processing solution. - -Postfix writes the logs to a syslog server which logs to stdout. This is used to filter -out messages from the healthcheck. In some situations, a separate mail log is required -(e.g. for legal reasons). The syslog server can be configured to write log files to a volume. -It can be configured with the following option: - -- ``POSTFIX_LOG_FILE``: The file to log the mail log to. When enabled, the syslog server will also log to stdout. - -When ``POSTFIX_LOG_FILE`` is enabled, the logrotate program will automatically rotate the -logs every week and keep 52 logs. To override the logrotate configuration, create the file logrotate.conf -with the desired configuration in the :ref:`Postfix overrides folder`. - .. _header_authentication: Header authentication using an external proxy diff --git a/docs/contributors/guidelines.rst b/docs/contributors/guidelines.rst index 8644d83e..2fa7007f 100644 --- a/docs/contributors/guidelines.rst +++ b/docs/contributors/guidelines.rst @@ -93,7 +93,7 @@ Configuration files should be compiled at runtime by the container `start.py` script and all conditional syntax should be handled using Jinja logic. The `socrate` Python package should include relevant functions for container -lifecycle management. +life-cycle management. Anything that is not static, i.e. able to change at runtime, either due to configuration in the admin UI or user behavior, should take advantage of the diff --git a/docs/contributors/memo.rst b/docs/contributors/memo.rst index 65b86649..6b09cd36 100644 --- a/docs/contributors/memo.rst +++ b/docs/contributors/memo.rst @@ -8,7 +8,7 @@ Mailu uses Babel for internationalization and localization. Before any of your work is merged, you must make sure that your strings are internationalized using Babel. -If you used ``_``, ``trans`` blocks and other Babel syntaxes in your code, run the +If you used ``_``, ``trans`` blocks and other Babel syntax in your code, run the following command to update the POT file: .. code-block:: bash @@ -26,7 +26,7 @@ Please resolve fuzzy strings to the best of your knowledge. Update information files ------------------------ -If you added a feature or fixed a bug or committed anything that is worth mentionning +If you added a feature or fixed a bug or committed anything that is worth mentioning for the next upgrade, add it in the ``CHANGELOG.md`` file. Also, if you would like to be mentioned by name or add a comment in ``AUTHORS.md``, diff --git a/docs/contributors/workflow.rst b/docs/contributors/workflow.rst index 957d4a9b..b233e0db 100644 --- a/docs/contributors/workflow.rst +++ b/docs/contributors/workflow.rst @@ -19,8 +19,8 @@ This is a community project, thus commits should be readable enough for any of the contributors to guess the content by simply reading the comment or find a proper commit when one knows what they are looking for. -Usual standards remain: write english comments, single line short comments and -additional multiline if required (keep in mind that the most important piece +Usual standards remain: write English comments, single line short comments and +additional multi-line if required (keep in mind that the most important piece of information should fit in the first line). Branches diff --git a/docs/database.rst b/docs/database.rst index 26da8dd5..54e5015a 100644 --- a/docs/database.rst +++ b/docs/database.rst @@ -25,9 +25,9 @@ To switch to a different database back-end: 1. Drop into a shell inside the admin container as you'll need to execute multiple commands. E.g. `docker exec -i $(docker compose ps -q admin) bash` - 2. Initialize the new database backend: `flask mailu db init` + 2. Initialize the new database back-end: `flask mailu db init` - 3. Migrate the new database backend to the current state: `flask mailu db upgrade` + 3. Migrate the new database back-end to the current state: `flask mailu db upgrade` 4. Import the configuration export: `flask mailu config-import -v < /data/mail-config.yml` @@ -216,4 +216,4 @@ Optionally you can remove left-over files which were used by the old database: .. note:: Roundcube does not offer a migration tool for moving from SQLite to PostgreSQL. - In case roundcube is used, the Mailu setup utility can be used to specify SQLite for the roundcube database backend. + In case roundcube is used, the Mailu setup utility can be used to specify SQLite for the roundcube database back-end. diff --git a/docs/demo.rst b/docs/demo.rst index 46bedc0e..0619e218 100644 --- a/docs/demo.rst +++ b/docs/demo.rst @@ -35,7 +35,7 @@ Connecting to the server * Webmail : https://test.mailu.io/webmail/ * Admin UI : https://test.mailu.io/admin/ * Admin login : ``admin@test.mailu.io`` - * Admin password : ``letmein`` + * Admin password : ``Mailu+Demo@test.mailu.io`` (remove + and @test.mailu.io to get the correct password). * RESTful API: https://test.mailu.io/api * API token: ``Bearer APITokenForMailu`` diff --git a/docs/faq.rst b/docs/faq.rst index 5772978a..5dfc0fa1 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -11,8 +11,7 @@ Where to ask questions? First, please read this FAQ to check if your question is listed here. Simple questions are best asked in our `Matrix`_ room. -For more complex questions, you can always open a `new issue`_ on GitHub. -We actively monitor the issues list. +For more complex questions, you can always open a `new discussion`_ on GitHub. My installation is broken! @@ -33,16 +32,17 @@ I want a new feature or enhancement! Great! We are always open for suggestions. We currently maintain two tags: -- `Enhancement issues`_: Typically used for optimization of features in the project. -- `Feature request issues`_: For implementing new functionality, +- ``type/enhancement``: Typically used for optimization of features in the project. +- ``type/feature``: For implementing new functionality, plugins and applications. -Please check if your idea (or something similar) is already mentioned there. +Feature requests are discussed on the discussion page of the project (see `feature requests`_). +Please check if your idea (or something similar) is already mentioned on the project. If there is one open, you can choose to vote with a thumbs up, so we can estimate the popular demand. Please refrain from writing comments like *"me too"* as it clobbers the actual discussion. -If you can't find anything similar, you can open a `new issue`_. +If you can't find anything similar, you can open a `new feature request`_. Please also share (where applicable): - Use case: how does this improve the project? @@ -89,8 +89,9 @@ Please click the |sponsor| button on top of our GitHub Page for current possibil .. _`Matrix`: https://matrix.to/#/#mailu:tedomum.net .. _`open issues`: https://github.com/Mailu/Mailu/issues .. _`new issue`: https://github.com/Mailu/Mailu/issues/new -.. _`Enhancement issues`: https://github.com/Mailu/Mailu/issues?q=is%3Aissue+is%3Aopen+label%3Atype%2Fenhancement -.. _`Feature request issues`: https://github.com/Mailu/Mailu/issues?q=is%3Aopen+is%3Aissue+label%3Atype%2Ffeature +.. _`new discussion`: https://github.com/Mailu/Mailu/discussions/categories/user-support +.. _`feature requests`: https://github.com/Mailu/Mailu/discussions/categories/feature-requests-ideas +.. _`new feature request`: https://github.com/Mailu/Mailu/discussions/new?category=feature-requests-ideas .. _`GitHub`: https://github.com/Mailu/Mailu .. _`Community Bridge`: https://funding.communitybridge.org/projects/mailu @@ -192,8 +193,8 @@ This means it can be scaled horizontally. For more information, refer to :ref:`k *Issue reference:* `165`_, `520`_. -How to achieve HA / failover? -````````````````````````````` +How to achieve HA / fail-over? +`````````````````````````````` The mailboxes and databases for Mailu are kept on the host filesystem under ``$ROOT/``. For making the **storage** highly available, all sorts of techniques can be used: @@ -287,7 +288,7 @@ I want to integrate Nextcloud 15 (and newer) with Mailu If a domain name (e.g. example.com) is specified, then this makes sure that only users from this domain will be allowed to login. -After successfull login the domain part will be stripped and the rest used as username in Nextcloud. e.g. 'username@example.com' will be 'username' in Nextcloud. Disable this behaviour by changing true (the fifth parameter) to false. +After successful login the domain part will be stripped and the rest used as username in Nextcloud. e.g. 'username@example.com' will be 'username' in Nextcloud. Disable this behaviour by changing true (the fifth parameter) to false. *Issue reference:* `575`_. @@ -474,7 +475,7 @@ down and up again. A container restart is not sufficient. SMTP Banner from overrides/postfix.cf is ignored ```````````````````````````````````````````````` -Any mail related connection is proxied by nginx. Therefore the SMTP Banner is also set by nginx. Overwriting in overrides/postfix.cf does not apply. +Any mail related connection is proxied by the front container. Therefore the SMTP Banner is also set by front container. Overwriting in overrides/postfix.cf does not apply. *Issue reference:* `1368`_. @@ -496,8 +497,8 @@ Re-starting the smtp container will be required for changes to take effect. .. _`2213`: https://github.com/Mailu/Mailu/issues/2213 -My emails are getting defered, what can I do? -````````````````````````````````````````````` +My emails are getting deferred, what can I do? +`````````````````````````````````````````````` Emails are asynchronous and it's not abnormal for them to be defered sometimes. That being said, Mailu enforces secure connections where possible using DANE and MTA-STS, both of which have the potential to delay indefinitely delivery if something is misconfigured. @@ -754,8 +755,8 @@ Restart the Fail2Ban service. Users can't change their password from webmail `````````````````````````````````````````````` -All users have the abilty to login to the admin interface. Non-admin users -have only restricted funtionality such as changing their password and the +All users have the ability to login to the admin interface. Non-admin users +have only restricted functionality such as changing their password and the spam filter weight settings. *Issue reference:* `503`_. @@ -903,6 +904,13 @@ We have seen a fair amount of support requests related to the following: .. _`netplan does not play nicely with docker`: https://github.com/Mailu/Mailu/issues/2868#issuecomment-1606014184 +How can I use Mailu without docker? +``````````````````````````````````` + +Running Mailu without docker is not supported. If you want to do so, you need to export an environment variable called ``I_KNOW_MY_SETUP_DOESNT_FIT_REQUIREMENTS_AND_WONT_FILE_ISSUES_WITHOUT_PATCHES`` to the ``admin`` container. + +We welcome patches but do not have the bandwidth to test and fix issues related to your unsupported setup. If you do want to help, we welcome new maintainers: please get in touch. + How can I add more languages to roundcube's spellchecker? ````````````````````````````````````````````````````````` @@ -938,3 +946,64 @@ I see a lot of "Unable to lookup the TLSA record for XXX. Is the DNSSEC zone oka There may be multiple causes for it but if you are running docker 24.0.0, odds are you are `experiencing this docker bug`_ and the workaround is to switch to a different version of docker. .. _`experiencing this docker bug`: https://github.com/Mailu/Mailu/issues/2827 + +How can I view and export the logs of a Mailu container? +```````````````````````````````````````````````````````` + +In some situations, a separate log is required. For example a separate mail log (from postfix) could be required due to legal reasons. + +All Mailu containers log the output to journald. The logs are written to journald with the tag: + +| mailu- +| where is the name of the service in the docker-compose.yml file. +| For example, the service running postfix is called smtp. To view the postfix logs use: + +.. code-block:: bash + + journalctl -t mailu-smtp + +Note: ``SHIFT+G`` can be used to jump to the end of the log file. ``G`` can be used to jump back to the top of the log file. + +To export the log files from journald to the file system, the logs could be imported into a syslog program like ``rsyslog``. +Via ``rsyslog`` the container specific logs could be written to a separate file using a filter. + +Below are the steps for writing the postfix (mail) logs to a log file on the file system. + +1. Install the ``rsyslog`` package. Note: on most distributions this program is already installed. +2. Edit ``/etc/systemd/journald.conf``. +3. Enable ``ForwardToSyslog=yes``. Note: on most distributions this is already enabled by default. This forwards journald to syslog. +4. ``sudo touch /var/log/postfix.log``. This step creates the mail log file. +5. ``sudo chown syslog:syslog /var/log/postfix.log``. This provides rsyslog the permissions for accessing this file. +6. Create a new config file in ``/etc/rsyslog.d/export-postfix.conf`` +7. Add ``:programname, contains, "mailu-smtp" /var/log/postfix.log``. This instructs rsyslog to write the logs for mailu-smtp to a log file on file system. +8. ``sudo systemctl restart systemd-journald.service`` +9. ``sudo systemctl restart rsyslog`` +10. All messages from the smtp/postfix container are now logged to ``/var/log/postfix.log``. +11. Rsyslog does not perform log rotation. The program (package) ``log rotate`` can be used for this task. Install the ``logrotate`` package. +12. Modify the existing configuration file for rsyslog: ``sudo nano /etc/logrotate.d/rsyslog`` +13. Add at the top add: ``/var/log/postfix.log``. Of course you can also use your own configuration. This is just an example. A complete example for configuring log rotate is: + +.. code-block:: bash + + /var/log/postfix.log + { + rotate 4 + weekly + missingok + notifempty + compress + delaycompress + sharedscripts + postrotate + /usr/lib/rsyslog/rsyslog-rotate + endscript + } + +.. code-block:: bash + + #!/bin/sh + #/usr/lib/rsyslog/rsyslog-rotate + + if [ -d /run/systemd/system ]; then + systemctl kill -s HUP rsyslog.service + fi diff --git a/docs/index.rst b/docs/index.rst index 0f16335c..f2cf56f3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,8 +8,8 @@ Mailu .. image:: https://img.shields.io/github/stars/mailu/mailu.svg?label=github&logo=github&maxAge=2592000 :target: https://github.com/mailu/mailu -.. image:: https://img.shields.io/docker/pulls/mailu/admin.svg?label=docker&maxAge=2592000 - :target: https://hub.docker.com/u/mailu/ +.. image:: https://img.shields.io/badge/Mailu-latest_release-blue + :target: https://github.com/Mailu/Mailu/releases .. image:: https://img.shields.io/badge/matrix-%23mailu%3Atedomum.net-7bc9a4.svg :target: https://matrix.to/#/#mailu:tedomum.net @@ -23,8 +23,8 @@ popular groupware. Main features include: -- **Standard email server**, IMAP and IMAP+, SMTP and Submission with autoconfiguration profiles for clients -- **Advanced email features**, aliases, domain aliases, custom routing +- **Standard email server**, IMAP and IMAP+, SMTP and Submission with auto-configuration profiles for clients +- **Advanced email features**, aliases, domain aliases, custom routing, full-text search of email attachments - **Web access**, multiple Webmails and administration interface - **User features**, aliases, auto-reply, auto-forward, fetched accounts, managesieve - **Admin features**, global admins, announcements, per-domain delegation, quotas diff --git a/docs/mailu-network-diagram.ipynb b/docs/mailu-network-diagram.ipynb new file mode 100644 index 00000000..7b4336a5 --- /dev/null +++ b/docs/mailu-network-diagram.ipynb @@ -0,0 +1,517 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "mailu\n", + "\n", + "Mailu\n", + "\n", + "\n", + "internet\n", + "\n", + "Internet\n", + "\n", + "\n", + "\n", + "front\n", + "\n", + "Front\n", + "\n", + "\n", + "\n", + "internet->front\n", + "\n", + "\n", + "80/tcp\n", + "\n", + "\n", + "\n", + "internet->front\n", + "\n", + "\n", + "443/tcp\n", + "\n", + "\n", + "\n", + "internet->front\n", + "\n", + "\n", + "25/tcp\n", + "\n", + "\n", + "\n", + "internet->front\n", + "\n", + "\n", + "465/tcp\n", + "\n", + "\n", + "\n", + "internet->front\n", + "\n", + "\n", + "587/tcp\n", + "\n", + "\n", + "\n", + "internet->front\n", + "\n", + "\n", + "110/tcp\n", + "\n", + "\n", + "\n", + "internet->front\n", + "\n", + "\n", + "995/tcp\n", + "\n", + "\n", + "\n", + "internet->front\n", + "\n", + "\n", + "143/tcp\n", + "\n", + "\n", + "\n", + "internet->front\n", + "\n", + "\n", + "993/tcp\n", + "\n", + "\n", + "\n", + "internet->front\n", + "\n", + "\n", + "4190/tcp\n", + "\n", + "\n", + "\n", + "front->front\n", + "\n", + "\n", + "8008/tcp\n", + "\n", + "\n", + "\n", + "front->front\n", + "\n", + "\n", + "8000/tcp\n", + "\n", + "\n", + "\n", + "admin\n", + "\n", + "Admin\n", + "\n", + "\n", + "\n", + "front->admin\n", + "\n", + "\n", + "8080/tcp\n", + "\n", + "\n", + "\n", + "smtp\n", + "\n", + "SMTP\n", + "\n", + "\n", + "\n", + "front->smtp\n", + "\n", + "\n", + "25/tcp\n", + "\n", + "\n", + "\n", + "front->smtp\n", + "\n", + "\n", + "10025/tcp\n", + "\n", + "\n", + "\n", + "antispam\n", + "\n", + "Antispam\n", + "\n", + "\n", + "\n", + "front->antispam\n", + "\n", + "\n", + "11334/tcp\n", + "\n", + "\n", + "\n", + "imap\n", + "\n", + "IMAP\n", + "\n", + "\n", + "\n", + "front->imap\n", + "\n", + "\n", + "4190/tcp\n", + "\n", + "\n", + "\n", + "front->imap\n", + "\n", + "\n", + "143/tcp\n", + "\n", + "\n", + "\n", + "front->imap\n", + "\n", + "\n", + "110/tcp\n", + "\n", + "\n", + "\n", + "webdav\n", + "\n", + "WebDAV\n", + "\n", + "\n", + "\n", + "front->webdav\n", + "\n", + "\n", + "5232/tcp\n", + "\n", + "\n", + "\n", + "webmail\n", + "\n", + "Webmail\n", + "\n", + "\n", + "\n", + "front->webmail\n", + "\n", + "\n", + "80/tcp\n", + "\n", + "\n", + "\n", + "redis\n", + "\n", + "Redis\n", + "\n", + "\n", + "\n", + "admin->redis\n", + "\n", + "\n", + "6379/tcp\n", + "\n", + "\n", + "\n", + "admin->imap\n", + "\n", + "\n", + "2525/tcp\n", + "\n", + "\n", + "\n", + "smtp->front\n", + "\n", + "\n", + "2525/tcp\n", + "\n", + "\n", + "\n", + "smtp->admin\n", + "\n", + "\n", + "8080/tcp\n", + "\n", + "\n", + "\n", + "smtp->antispam\n", + "\n", + "\n", + "11332/tcp\n", + "\n", + "\n", + "\n", + "antispam->admin\n", + "\n", + "\n", + "80/tcp\n", + "\n", + "\n", + "\n", + "antispam->redis\n", + "\n", + "\n", + "6379/tcp\n", + "\n", + "\n", + "\n", + "antivirus\n", + "\n", + "Anti-Virus\n", + "\n", + "\n", + "\n", + "antispam->antivirus\n", + "\n", + "\n", + "3310/tcp\n", + "\n", + "\n", + "\n", + "oletools\n", + "\n", + "Oletools\n", + "\n", + "\n", + "\n", + "antispam->oletools\n", + "\n", + "\n", + "11343/tcp\n", + "\n", + "\n", + "\n", + "imap->front\n", + "\n", + "\n", + "25/tcp\n", + "\n", + "\n", + "\n", + "imap->admin\n", + "\n", + "\n", + "8080/tcp\n", + "\n", + "\n", + "\n", + "imap->antispam\n", + "\n", + "\n", + "11334/tcp\n", + "\n", + "\n", + "\n", + "fts_attachments\n", + "\n", + "Tika\n", + "\n", + "\n", + "\n", + "imap->fts_attachments\n", + "\n", + "\n", + "9998/tcp\n", + "\n", + "\n", + "\n", + "webmail->front\n", + "\n", + "\n", + "14190/tcp\n", + "\n", + "\n", + "\n", + "webmail->front\n", + "\n", + "\n", + "10025/tcp\n", + "\n", + "\n", + "\n", + "webmail->front\n", + "\n", + "\n", + "10143/tcp\n", + "\n", + "\n", + "\n", + "fetchmail\n", + "\n", + "Fetchmail\n", + "\n", + "\n", + "\n", + "fetchmail->front\n", + "\n", + "\n", + "25/tcp\n", + "\n", + "\n", + "\n", + "fetchmail->front\n", + "\n", + "\n", + "2525/tcp\n", + "\n", + "\n", + "\n", + "fetchmail->admin\n", + "\n", + "\n", + "8080/tcp\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import graphviz\n", + "\n", + "a = \"\"\"\n", + "digraph mailu {\n", + " label = \"Mailu\";\n", + " fontname = \"arial\";\n", + " \n", + " node [shape = box; fontname = \"arial\"; fontsize = 8; style = filled; color = \"#d3edea\";];\n", + " splines = \"compound\";\n", + " // node [shape = \"box\"; fontsize = \"10\";];\n", + " edge [fontsize = \"8\";];\n", + " \n", + " # Components\n", + " internet [label = \"Internet\";];\n", + " front [label = \"Front\";];\n", + " admin [label = \"Admin\";];\n", + " smtp [label = \"SMTP\";];\n", + " redis [label = \"Redis\";];\n", + " antispam [label = \"Antispam\";];\n", + " antivirus [label = \"Anti-Virus\";];\n", + " imap [label = \"IMAP\";];\n", + " webdav [label = \"WebDAV\";];\n", + " webmail [label = \"Webmail\";];\n", + " fetchmail [label = \"Fetchmail\";];\n", + " oletools [label = \"Oletools\"];\n", + " fts_attachments [label = \"Tika\"];\n", + " \n", + " # Front from internet\n", + " internet -> front [label = \"80/tcp\";];\n", + " internet -> front [label = \"443/tcp\";];\n", + " internet -> front [label = \"25/tcp\";];\n", + " internet -> front [label = \"465/tcp\";];\n", + " internet -> front [label = \"587/tcp\";];\n", + " internet -> front [label = \"110/tcp\";];\n", + " internet -> front [label = \"995/tcp\";];\n", + " internet -> front [label = \"143/tcp\";];\n", + " internet -> front [label = \"993/tcp\";];\n", + " internet -> front [label = \"4190/tcp\";];\n", + " \n", + " front -> front [label = \"8008/tcp\";];\n", + " front -> front [label = \"8000/tcp\";];\n", + " front -> admin [label = \"8080/tcp\";];\n", + " front -> imap [label = \"4190/tcp\";];\n", + " front -> imap [label = \"143/tcp\";];\n", + " front -> imap [label = \"110/tcp\";];\n", + " front -> smtp [label = \"25/tcp\";];\n", + " front -> smtp [label = \"10025/tcp\";];\n", + " front -> webmail [label = \"80/tcp\";];\n", + " front -> antispam [label = \"11334/tcp\";];\n", + " front -> webdav [label = \"5232/tcp\";];\n", + " \n", + " smtp -> admin [label = \"8080/tcp\";];\n", + " smtp -> front [label = \"2525/tcp\";];\n", + " smtp -> antispam [label = \"11332/tcp\";];\n", + " \n", + " imap -> admin [label = \"8080/tcp\";];\n", + " imap -> antispam [label = \"11334/tcp\";];\n", + " imap -> front [label = \"25/tcp\";];\n", + " imap -> fts_attachments [label = \"9998/tcp\";];\n", + " \n", + " webmail -> front [label = \"14190/tcp\";];\n", + " webmail -> front [label = \"10025/tcp\";];\n", + " webmail -> front [label = \"10143/tcp\";];\n", + " \n", + " admin -> redis [label = \"6379/tcp\";];\n", + " admin -> imap [label = \"2525/tcp\";];\n", + " \n", + " antispam -> redis [label = \"6379/tcp\";];\n", + " antispam -> admin [label = \"80/tcp\";];\n", + " antispam -> oletools [label = \"11343/tcp\";];\n", + " antispam -> antivirus [label = \"3310/tcp\";];\n", + " \n", + " fetchmail -> admin [label = \"8080/tcp\"]\n", + " fetchmail -> front [label = \"25/tcp\"]\n", + " fetchmail -> front [label = \"2525/tcp\"]\n", + " #\n", + " # those don't need internet:\n", + " # oletools\n", + " # fts_attachments\n", + " # redis\n", + "}\n", + "\"\"\"\n", + "\n", + "dot = graphviz.Source(a)\n", + "dot\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/releases.rst b/docs/releases.rst index b290d639..c35c9e8a 100644 --- a/docs/releases.rst +++ b/docs/releases.rst @@ -342,9 +342,9 @@ A fair amount of work went in this release; In no particular order: - outbound SMTP connections from Mailu are now enjoying some protection against active attackers thanks to DANE and MTA-STS support. Specific policies can be configured for specific destinations thanks to ``tls_policy_maps`` and configuring your system to publish a policy has been documented in the FAQ. - outbound emails can now be rate-limited (to mitigate SPAM in case an account is taken over) - long term storage of passwords has been rethought to enable stronger protection against offline attackers (switch to iterated and salted SHA+bcrypt) while enabling much better performance (credential cache). Please encourage your users to use tokens where appropriate and keep in mind that existing hashes will be converted on first use to the new format. -- session handling has been reworked from the grounds up: they have been switched from client side (cookies) to server-side, unified (SSO, expiry, lifetime) accross all web-facing applications and some mitigations against session fixation have been implemented. -- rate limiting has seen many improvements: It is now deployed on all entry points (SMTP/IMAP/POP3/WEB/WEBMAIL) and configured to defeat both password bruteforces (thanks to a limit against total number of failed attempts against an account over a period) and password spraying (thanks to a limit for each client on the total number of non-existing accounts that can be queried). Exemption mechanisms have been put in place (device tokens, dynamic IP whitelists) to ensure that genuine clients and users won't be affected by default and the default configuration thought to fit most usecases. -- if you use letsencrypt, Mailu is now configured to offer both RSA and ECC certificates to clients; It will OSCP stapple its replies where appropriate +- session handling has been reworked from the grounds up: they have been switched from client side (cookies) to server-side, unified (SSO, expiry, lifetime) across all web-facing applications and some mitigations against session fixation have been implemented. +- rate limiting has seen many improvements: It is now deployed on all entry points (SMTP/IMAP/POP3/WEB/WEBMAIL) and configured to defeat both password bruteforces (thanks to a limit against total number of failed attempts against an account over a period) and password spraying (thanks to a limit for each client on the total number of non-existing accounts that can be queried). Exemption mechanisms have been put in place (device tokens, dynamic IP whitelists) to ensure that genuine clients and users won't be affected by default and the default configuration thought to fit most use-cases. +- if you use letsencrypt, Mailu is now configured to offer both RSA and ECC certificates to clients; It will OSCP staple its replies where appropriate Updated Admin interface diff --git a/docs/requirements.txt b/docs/requirements.txt index 2c3169b7..46d263a7 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ recommonmark==0.7.1 -Sphinx==5.2.0 +Sphinx==7.2.6 sphinx-autobuild==2021.3.14 -sphinx-rtd-theme==1.0.0 -docutils==0.16 +sphinx-rtd-theme==1.3.0 +docutils==0.18.1 diff --git a/docs/reverse.rst b/docs/reverse.rst index 7b89c09e..8a3a4e2a 100644 --- a/docs/reverse.rst +++ b/docs/reverse.rst @@ -4,7 +4,7 @@ Using an external reverse proxy One of Mailu's use cases is as part of a larger services platform, where maybe other Web services are available on other FQDNs served from the same IP address. -In such a configuration, one would usually run a frontend reverse proxy to serve all +In such a configuration, one would usually run a front-end reverse proxy to serve all Web contents based on criteria like the requested hostname (virtual hosts). .. _traefik_proxy: @@ -134,4 +134,4 @@ in mailu.env: TLS_FLAVOR=mail-letsencrypt WEBROOT_REDIRECT=/sso/login -Using the above configuration, Traefik will proxy all the traffic related to Mailu's FQDNs without requiring dupplicate certificates. +Using the above configuration, Traefik will proxy all the traffic related to Mailu's FQDNs without requiring duplicate certificates. diff --git a/docs/webadministration.rst b/docs/webadministration.rst index 6521fc2e..93cbb56b 100644 --- a/docs/webadministration.rst +++ b/docs/webadministration.rst @@ -254,7 +254,7 @@ The menu item Antispam opens the Rspamd webgui. For more information how spam fi The spam filtering page also contains a section that describes how to create a local blacklist for blocking email messages from specific domains. The Rspamd webgui offers basic functions for setting metric actions, scores, viewing statistics and learning. -The following settings are not persisent and are *lost* when the Antispam container is recreated or restarted: +The following settings are not persistent and are *lost* when the Antispam container is recreated or restarted: * On the configuration tab, any changes to config files that do not reside in /var/lib or /etc/rspamd/override.d. The last location is mapped to the Mailu overrides folder. @@ -282,7 +282,7 @@ On the `Mail domains` page all the domains served by Mailu are configured. Via t Details ``````` -This page is also accessible for domain managers. On the details page all DNS settings are displayed for configuring your DNS server. It contains information on what to configure as MX record and SPF record. On this page it is also possible to (re-)generate the keys for DKIM and DMARC. The option for generating keys for DKIM and DMARC is only available for global administrators. After generating the keys for DKIM and DMARC, this page will also show the DNS records for configuring the DKIM/DMARC records on the DNS server. +This page is also accessible for domain managers. On the details page all DNS settings are displayed for configuring your DNS server. It contains information on what to configure as MX record and SPF record. On this page it is also possible to (re-)generate the keys for DKIM and DMARC. The option for generating keys for DKIM and DMARC is only available for global administrators. After generating the keys for DKIM and DMARC, this page will also show the DNS records for configuring the DKIM/DMARC records on the DNS server. You can also download a zonefile for easy upload to your nameserver. Edit diff --git a/optional/clamav/Dockerfile b/optional/clamav/Dockerfile deleted file mode 100644 index a0a67749..00000000 --- a/optional/clamav/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -# syntax=docker/dockerfile-upstream:1.4.3 - -# clamav image -FROM base - -ARG VERSION=local -LABEL version=$VERSION - -RUN set -euxo pipefail \ - ; apk add --no-cache clamav clamav-libunrar rsyslog wget - -COPY conf/ /etc/clamav/ -COPY start.py / - -RUN echo $VERSION >/version - -#EXPOSE 3310/tcp -HEALTHCHECK CMD kill -0 `cat /run/clamd.pid` && kill -0 `cat /run/freshclam.pid` - -VOLUME ["/data"] - -CMD /start.py diff --git a/optional/clamav/README.md b/optional/clamav/README.md deleted file mode 100644 index 68cd3ffe..00000000 --- a/optional/clamav/README.md +++ /dev/null @@ -1,12 +0,0 @@ -Mailu ClamAV container -====================== - -ClamAV is an open source antivirus engine for detecting trojans, viruses, -malware & other malicious threats. - -Resources ---------- - - * [Report issues](https://github.com/Mailu/Mailu/issues) and - [send Pull Requests](https://github.com/Mailu/Mailu/pulls) - in the [main Mailu repository](https://github.com/Mailu/Mailu) \ No newline at end of file diff --git a/optional/clamav/conf/clamd.conf b/optional/clamav/conf/clamd.conf deleted file mode 100644 index 061d7f6a..00000000 --- a/optional/clamav/conf/clamd.conf +++ /dev/null @@ -1,56 +0,0 @@ -############### -# General -############### - -DatabaseDirectory /data -TemporaryDirectory /tmp -LogTime yes -PidFile /run/clamd.pid -LocalSocket /tmp/clamd.sock -TCPSocket 3310 -Foreground yes - -############### -# Results -############### - -DetectPUA yes -ExcludePUA NetTool -ExcludePUA PWTool -HeuristicAlerts yes -Bytecode yes - -############### -# Scan -############### - -ScanPE yes -DisableCertCheck yes -ScanELF yes -AlertBrokenExecutables yes -ScanOLE2 yes -ScanPDF yes -ScanSWF yes -ScanMail yes -PhishingSignatures yes -PhishingScanURLs yes -ScanHTML yes -ScanArchive yes - -############### -# Scan -############### - -MaxScanSize 150M -MaxFileSize 30M -MaxRecursion 10 -MaxFiles 15000 -MaxEmbeddedPE 10M -MaxHTMLNormalize 10M -MaxHTMLNoTags 2M -MaxScriptNormalize 5M -MaxZipTypeRcg 1M -MaxPartitions 128 -MaxIconsPE 200 -PCREMatchLimit 10000 -PCRERecMatchLimit 10000 diff --git a/optional/clamav/conf/freshclam.conf b/optional/clamav/conf/freshclam.conf deleted file mode 100644 index 828163a0..00000000 --- a/optional/clamav/conf/freshclam.conf +++ /dev/null @@ -1,18 +0,0 @@ -############### -# General -############### - -DatabaseDirectory /data -UpdateLogFile /dev/stdout -LogTime yes -PidFile /run/freshclam.pid -DatabaseOwner root - -############### -# Updates -############### - -DatabaseMirror database.clamav.net -ScriptedUpdates yes -NotifyClamd /etc/clamav/clamd.conf -Bytecode yes diff --git a/optional/clamav/start.py b/optional/clamav/start.py deleted file mode 100755 index 684d9edd..00000000 --- a/optional/clamav/start.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 - -import os -import logging as logger -import sys -from socrate import system - -system.set_env(log_filters=r'SelfCheck: Database status OK\.$') - -# Bootstrap the database if clamav is running for the first time -if not os.path.isfile("/data/main.cvd"): - logger.info("Starting primary virus DB download") - os.system("freshclam") - -# Run the update daemon -logger.info("Starting the update daemon") -os.system("freshclam -d -c 6") - -# Run clamav -logger.info("Starting clamav") -os.system("clamd") diff --git a/optional/fetchmail/fetchmail.py b/optional/fetchmail/fetchmail.py index 35b2ee22..a298cf53 100755 --- a/optional/fetchmail/fetchmail.py +++ b/optional/fetchmail/fetchmail.py @@ -64,7 +64,7 @@ def run(debug): username=escape_rc_string(fetch["username"]), password=escape_rc_string(fetch["password"]), options=options, - folders=folders, + folders='' if fetch['protocol'] == 'pop3' else folders, lmtp='' if fetch['scan'] else 'lmtp', ) if debug: diff --git a/setup/flavors/compose/docker-compose.yml b/setup/flavors/compose/docker-compose.yml index b266fec0..75fa6f08 100644 --- a/setup/flavors/compose/docker-compose.yml +++ b/setup/flavors/compose/docker-compose.yml @@ -58,6 +58,10 @@ services: resolver: image: ${DOCKER_ORG:-ghcr.io/mailu}/${DOCKER_PREFIX:-}unbound:${MAILU_VERSION:-{{ version }}} env_file: {{ env }} + logging: + driver: journald + options: + tag: mailu-resolver restart: always networks: default: @@ -98,8 +102,16 @@ services: volumes: - "{{ root }}/mail:/mail" - "{{ root }}/overrides/dovecot:/overrides:ro" + networks: + - default + {% if tika_enabled %} + - fts_attachments + {% endif %} depends_on: - front + {% if tika_enabled %} + - fts_attachments + {% endif %} {% if resolver_enabled %} - resolver dns: @@ -129,9 +141,13 @@ services: oletools: image: ${DOCKER_ORG:-ghcr.io/mailu}/${DOCKER_PREFIX:-}oletools:${MAILU_VERSION:-{{ version }}} hostname: oletools + logging: + driver: journald + options: + tag: mailu-oletools restart: always networks: - - noinet + - oletools depends_on: {% if resolver_enabled %} - resolver @@ -140,6 +156,31 @@ services: {% endif %} {% endif %} +{% if tika_enabled %} + fts_attachments: + image: ghcr.io/paperless-ngx/tika:2.9.0-full + hostname: tika + logging: + driver: journald + options: + tag: mailu-tika + restart: always + networks: + - fts_attachments + depends_on: + {% if resolver_enabled %} + - resolver + dns: + - {{ dns }} + {% endif %} + healthcheck: + test: ["CMD-SHELL", "wget -nv -t1 -O /dev/null http://127.0.0.1:9998/tika || exit 1"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 10s +{% endif %} + antispam: image: ${DOCKER_ORG:-ghcr.io/mailu}/${DOCKER_PREFIX:-}rspamd:${MAILU_VERSION:-{{ version }}} hostname: antispam @@ -149,10 +190,13 @@ services: driver: journald options: tag: mailu-antispam -{% if oletools_enabled %} networks: - default - - noinet +{% if oletools_enabled %} + - oletools +{% endif %} +{% if antivirus_enabled %} + - clamav {% endif %} volumes: - "{{ root }}/filter:/var/lib/rspamd" @@ -175,23 +219,32 @@ services: # Optional services {% if antivirus_enabled %} antivirus: - image: ${DOCKER_ORG:-ghcr.io/mailu}/${DOCKER_PREFIX:-}clamav:${MAILU_VERSION:-{{ version }}} + image: clamav/clamav-debian:1.2.0-6 restart: always - env_file: {{ env }} + logging: + driver: journald + options: + tag: mailu-antivirus + networks: + - clamav volumes: - - "{{ root }}/filter:/data" - {% if resolver_enabled %} - depends_on: - - resolver - dns: - - {{ dns }} - {% endif %} + - "{{ root }}/filter/clamav:/var/lib/clamav" + healthcheck: + test: ["CMD-SHELL", "kill -0 `cat /tmp/clamd.pid` && kill -0 `cat /tmp/freshclam.pid`"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 10s {% endif %} {% if webdav_enabled %} webdav: image: ${DOCKER_ORG:-ghcr.io/mailu}/${DOCKER_PREFIX:-}radicale:${MAILU_VERSION:-{{ version }}} restart: always + logging: + driver: journald + options: + tag: mailu-webdav volumes: - "{{ root }}/dav:/data" networks: @@ -203,6 +256,10 @@ services: image: ${DOCKER_ORG:-ghcr.io/mailu}/${DOCKER_PREFIX:-}fetchmail:${MAILU_VERSION:-{{ version }}} restart: always env_file: {{ env }} + logging: + driver: journald + options: + tag: mailu-fetchmail volumes: - "{{ root }}/data/fetchmail:/data" depends_on: @@ -222,6 +279,10 @@ services: image: ${DOCKER_ORG:-ghcr.io/mailu}/${DOCKER_PREFIX:-}webmail:${MAILU_VERSION:-{{ version }}} restart: always env_file: {{ env }} + logging: + driver: journald + options: + tag: mailu-webmail volumes: - "{{ root }}/webmail:/data" - "{{ root }}/overrides/{{ webmail_type }}:/overrides:ro" @@ -252,8 +313,17 @@ networks: webmail: driver: bridge {% endif %} +{% if antivirus_enabled %} + clamav: + driver: bridge +{% endif %} {% if oletools_enabled %} - noinet: + oletools: + driver: bridge + internal: true +{% endif %} +{% if tika_enabled %} + fts_attachments: driver: bridge internal: true {% endif %} diff --git a/setup/flavors/compose/mailu.env b/setup/flavors/compose/mailu.env index 616cd89b..7c36a7ec 100644 --- a/setup/flavors/compose/mailu.env +++ b/setup/flavors/compose/mailu.env @@ -40,7 +40,11 @@ AUTH_RATELIMIT_USER={{ auth_ratelimit_user }}/day {% endif %} # Opt-out of statistics, replace with "True" to opt out -DISABLE_STATISTICS={{ disable_statistics or 'False' }} +{% if statistics_enabled %} +DISABLE_STATISTICS=False +{% else %} +DISABLE_STATISTICS=True +{% endif %} ################################### # Optional features @@ -49,19 +53,19 @@ DISABLE_STATISTICS={{ disable_statistics or 'False' }} # Expose the admin interface (value: true, false) ADMIN={{ admin_enabled or 'false' }} -# Choose which webmail to run if any (values: roundcube, snappymail, none) +# Choose which webmail to run if any (values: roundcube, snappymail, none). To enable this feature, recreate the docker-compose.yml file via setup. WEBMAIL={{ webmail_type }} # Expose the API interface (value: true, false) API={{ api_enabled or 'false' }} -# Dav server implementation (value: radicale, none) +# Dav server implementation (value: radicale, none). To enable this feature, recreate the docker-compose.yml file via setup. WEBDAV={{ webdav_enabled or 'none' }} -# Antivirus solution (value: clamav, none) +# Antivirus solution (value: clamav, none). To enable this feature, recreate the docker-compose.yml file via setup. ANTIVIRUS={{ antivirus_enabled or 'none' }} -# Scan Macros solution (value: true, false) +# Scan Macros solution (value: true, false). To enable this feature, recreate the docker-compose.yml file via setup. SCAN_MACROS={{ oletools_enabled or 'false' }} ################################### @@ -110,32 +114,32 @@ COMPRESSION={{ compression }} # change compression-level, default: 6 (value: 1-9) COMPRESSION_LEVEL={{ compression_level }} -# IMAP full-text search is enabled by default. Set the following variable to off in order to disable the feature. -# FULL_TEXT_SEARCH=off +# IMAP full-text search is enabled by default. +# Set the following variable to off in order to disable the feature +# or a comma separated list of language codes to support +FULL_TEXT_SEARCH=en ################################### # Web settings ################################### # Path to redirect / to -{% if webmail_type != 'none' and webmail_path == '' %} -WEBROOT_REDIRECT=/ +{% if webmail_type != 'none' %} +WEBROOT_REDIRECT=/webmail +{% elif admin_enabled %} +WEBROOT_REDIRECT=/admin {% else %} -WEBROOT_REDIRECT={{ webmail_path }} +WEBROOT_REDIRECT= {% endif %} # Path to the admin interface if enabled -WEB_ADMIN={{ admin_path }} +WEB_ADMIN=/admin # Path to the webmail if enabled -{% if webmail_type != 'none' and webmail_path == '' %} -WEB_WEBMAIL=/ -{% else %} -WEB_WEBMAIL={{ webmail_path }} -{% endif %} +WEB_WEBMAIL=/webmail # Path to the API interface if enabled -WEB_API={{ api_path }} +WEB_API=/api # Website name SITENAME={{ site_name }} @@ -186,3 +190,5 @@ DEFAULT_SPAM_THRESHOLD=80 # This is a mandatory setting for using the RESTful API. API_TOKEN={{ api_token }} +# Whether tika should be enabled (scan/OCR email attachements). To enable this feature, recreate the docker-compose.yml file via setup. +FULL_TEXT_SEARCH_ATTACHMENTS={{ tika_enabled }} diff --git a/setup/server.py b/setup/server.py index 622905ed..806be40e 100644 --- a/setup/server.py +++ b/setup/server.py @@ -10,12 +10,16 @@ import random import ipaddress import hashlib import time - +import secrets +from flask_bootstrap import StaticCDN version = os.getenv("this_version", "master") static_url_path = "/" + version + "/static" app = flask.Flask(__name__, static_url_path=static_url_path) +app.secret_key = secrets.token_hex(16) flask_bootstrap.Bootstrap(app) +# Load our jQuery. Do not use jQuery 1. +app.extensions['bootstrap']['cdns']['jquery'] = StaticCDN() db = redis.StrictRedis(host='redis', port=6379, db=0) @@ -90,12 +94,47 @@ def build_app(path): def submit(): data = flask.request.form.copy() data['uid'] = str(uuid.uuid4()) + valid = True + try: + ipaddress.IPv4Address(data['bind4']) + except: + flask.flash('Configured IPv4 address is invalid', 'error') + valid = False + try: + ipaddress.IPv6Address(data['bind6']) + except: + flask.flash('Configured IPv6 address is invalid', 'error') + valid = False + try: + ipaddress.IPv4Network(data['subnet']) + except: + flask.flash('Configured subnet(IPv4) is invalid', 'error') + valid = False + try: + ipaddress.IPv6Network(data['subnet6']) + except: + flask.flash('Configured subnet(IPv6) is invalid', 'error') + valid = False try: data['dns'] = str(ipaddress.IPv4Network(data['subnet'], strict=False)[-2]) except ValueError as err: - return "Error while generating files: " + str(err) - db.set(data['uid'], json.dumps(data)) - return flask.redirect(flask.url_for('.setup', uid=data['uid'])) + flask.flash('Invalid configuration: ' + str(err)) + valid = False + if 'api_enabled' in data: + if (data['api_enabled'] == 'true'): + if data['api_token'] == '': + flask.flash('API token cannot be empty when API is enabled', 'error') + valid = False + if valid: + db.set(data['uid'], json.dumps(data)) + return flask.redirect(flask.url_for('.setup', uid=data['uid'])) + else: + return flask.render_template( + 'wizard.html', + flavor="compose", + steps=sorted(os.listdir(os.path.join(path, "templates", "steps", "compose"))), + subnet6=random_ipv6_subnet() + ) @prefix_bp.route("/setup/", methods=["GET"]) @root_bp.route("/setup/", methods=["GET"]) diff --git a/setup/static/jquery.min.js b/setup/static/jquery.min.js new file mode 100644 index 00000000..7f37b5d9 --- /dev/null +++ b/setup/static/jquery.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.7.1 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(ie,e){"use strict";var oe=[],r=Object.getPrototypeOf,ae=oe.slice,g=oe.flat?function(e){return oe.flat.call(e)}:function(e){return oe.concat.apply([],e)},s=oe.push,se=oe.indexOf,n={},i=n.toString,ue=n.hasOwnProperty,o=ue.toString,a=o.call(Object),le={},v=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},y=function(e){return null!=e&&e===e.window},C=ie.document,u={type:!0,src:!0,nonce:!0,noModule:!0};function m(e,t,n){var r,i,o=(n=n||C).createElement("script");if(o.text=e,t)for(r in u)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[i.call(e)]||"object":typeof e}var t="3.7.1",l=/HTML$/i,ce=function(e,t){return new ce.fn.init(e,t)};function c(e){var t=!!e&&"length"in e&&e.length,n=x(e);return!v(e)&&!y(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+ge+")"+ge+"*"),x=new RegExp(ge+"|>"),j=new RegExp(g),A=new RegExp("^"+t+"$"),D={ID:new RegExp("^#("+t+")"),CLASS:new RegExp("^\\.("+t+")"),TAG:new RegExp("^("+t+"|[*])"),ATTR:new RegExp("^"+p),PSEUDO:new RegExp("^"+g),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+ge+"*(even|odd|(([+-]|)(\\d*)n|)"+ge+"*(?:([+-]|)"+ge+"*(\\d+)|))"+ge+"*\\)|)","i"),bool:new RegExp("^(?:"+f+")$","i"),needsContext:new RegExp("^"+ge+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+ge+"*((?:-\\d)?\\d*)"+ge+"*\\)|)(?=[^-]|$)","i")},N=/^(?:input|select|textarea|button)$/i,q=/^h\d$/i,L=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,H=/[+~]/,O=new RegExp("\\\\[\\da-fA-F]{1,6}"+ge+"?|\\\\([^\\r\\n\\f])","g"),P=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},M=function(){V()},R=J(function(e){return!0===e.disabled&&fe(e,"fieldset")},{dir:"parentNode",next:"legend"});try{k.apply(oe=ae.call(ye.childNodes),ye.childNodes),oe[ye.childNodes.length].nodeType}catch(e){k={apply:function(e,t){me.apply(e,ae.call(t))},call:function(e){me.apply(e,ae.call(arguments,1))}}}function I(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(V(e),e=e||T,C)){if(11!==p&&(u=L.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return k.call(n,a),n}else if(f&&(a=f.getElementById(i))&&I.contains(e,a)&&a.id===i)return k.call(n,a),n}else{if(u[2])return k.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&e.getElementsByClassName)return k.apply(n,e.getElementsByClassName(i)),n}if(!(h[t+" "]||d&&d.test(t))){if(c=t,f=e,1===p&&(x.test(t)||m.test(t))){(f=H.test(t)&&U(e.parentNode)||e)==e&&le.scope||((s=e.getAttribute("id"))?s=ce.escapeSelector(s):e.setAttribute("id",s=S)),o=(l=Y(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+Q(l[o]);c=l.join(",")}try{return k.apply(n,f.querySelectorAll(c)),n}catch(e){h(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return re(t.replace(ve,"$1"),e,n,r)}function W(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function F(e){return e[S]=!0,e}function $(e){var t=T.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function B(t){return function(e){return fe(e,"input")&&e.type===t}}function _(t){return function(e){return(fe(e,"input")||fe(e,"button"))&&e.type===t}}function z(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&R(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function X(a){return F(function(o){return o=+o,F(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function U(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}function V(e){var t,n=e?e.ownerDocument||e:ye;return n!=T&&9===n.nodeType&&n.documentElement&&(r=(T=n).documentElement,C=!ce.isXMLDoc(T),i=r.matches||r.webkitMatchesSelector||r.msMatchesSelector,r.msMatchesSelector&&ye!=T&&(t=T.defaultView)&&t.top!==t&&t.addEventListener("unload",M),le.getById=$(function(e){return r.appendChild(e).id=ce.expando,!T.getElementsByName||!T.getElementsByName(ce.expando).length}),le.disconnectedMatch=$(function(e){return i.call(e,"*")}),le.scope=$(function(){return T.querySelectorAll(":scope")}),le.cssHas=$(function(){try{return T.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}}),le.getById?(b.filter.ID=function(e){var t=e.replace(O,P);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(O,P);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):t.querySelectorAll(e)},b.find.CLASS=function(e,t){if("undefined"!=typeof t.getElementsByClassName&&C)return t.getElementsByClassName(e)},d=[],$(function(e){var t;r.appendChild(e).innerHTML="",e.querySelectorAll("[selected]").length||d.push("\\["+ge+"*(?:value|"+f+")"),e.querySelectorAll("[id~="+S+"-]").length||d.push("~="),e.querySelectorAll("a#"+S+"+*").length||d.push(".#.+[+~]"),e.querySelectorAll(":checked").length||d.push(":checked"),(t=T.createElement("input")).setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),r.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&d.push(":enabled",":disabled"),(t=T.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||d.push("\\["+ge+"*name"+ge+"*="+ge+"*(?:''|\"\")")}),le.cssHas||d.push(":has"),d=d.length&&new RegExp(d.join("|")),l=function(e,t){if(e===t)return a=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!le.sortDetached&&t.compareDocumentPosition(e)===n?e===T||e.ownerDocument==ye&&I.contains(ye,e)?-1:t===T||t.ownerDocument==ye&&I.contains(ye,t)?1:o?se.call(o,e)-se.call(o,t):0:4&n?-1:1)}),T}for(e in I.matches=function(e,t){return I(e,null,null,t)},I.matchesSelector=function(e,t){if(V(e),C&&!h[t+" "]&&(!d||!d.test(t)))try{var n=i.call(e,t);if(n||le.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){h(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(O,P),e[3]=(e[3]||e[4]||e[5]||"").replace(O,P),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||I.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&I.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return D.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&j.test(n)&&(t=Y(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(O,P).toLowerCase();return"*"===e?function(){return!0}:function(e){return fe(e,t)}},CLASS:function(e){var t=s[e+" "];return t||(t=new RegExp("(^|"+ge+")"+e+"("+ge+"|$)"))&&s(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=I.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function T(e,n,r){return v(n)?ce.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?ce.grep(e,function(e){return e===n!==r}):"string"!=typeof n?ce.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(ce.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||k,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:S.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof ce?t[0]:t,ce.merge(this,ce.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:C,!0)),w.test(r[1])&&ce.isPlainObject(t))for(r in t)v(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=C.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):v(e)?void 0!==n.ready?n.ready(e):e(ce):ce.makeArray(e,this)}).prototype=ce.fn,k=ce(C);var E=/^(?:parents|prev(?:Until|All))/,j={children:!0,contents:!0,next:!0,prev:!0};function A(e,t){while((e=e[t])&&1!==e.nodeType);return e}ce.fn.extend({has:function(e){var t=ce(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,Ce=/^$|^module$|\/(?:java|ecma)script/i;xe=C.createDocumentFragment().appendChild(C.createElement("div")),(be=C.createElement("input")).setAttribute("type","radio"),be.setAttribute("checked","checked"),be.setAttribute("name","t"),xe.appendChild(be),le.checkClone=xe.cloneNode(!0).cloneNode(!0).lastChild.checked,xe.innerHTML="",le.noCloneChecked=!!xe.cloneNode(!0).lastChild.defaultValue,xe.innerHTML="",le.option=!!xe.lastChild;var ke={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function Se(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&fe(e,t)?ce.merge([e],n):n}function Ee(e,t){for(var n=0,r=e.length;n",""]);var je=/<|&#?\w+;/;function Ae(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function Re(e,t){return fe(e,"table")&&fe(11!==t.nodeType?t:t.firstChild,"tr")&&ce(e).children("tbody")[0]||e}function Ie(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function We(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Fe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(_.hasData(e)&&(s=_.get(e).events))for(i in _.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),C.head.appendChild(r[0])},abort:function(){i&&i()}}});var Jt,Kt=[],Zt=/(=)\?(?=&|$)|\?\?/;ce.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Kt.pop()||ce.expando+"_"+jt.guid++;return this[e]=!0,e}}),ce.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Zt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Zt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=v(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Zt,"$1"+r):!1!==e.jsonp&&(e.url+=(At.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||ce.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=ie[r],ie[r]=function(){o=arguments},n.always(function(){void 0===i?ce(ie).removeProp(r):ie[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Kt.push(r)),o&&v(i)&&i(o[0]),o=i=void 0}),"script"}),le.createHTMLDocument=((Jt=C.implementation.createHTMLDocument("").body).innerHTML="
",2===Jt.childNodes.length),ce.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(le.createHTMLDocument?((r=(t=C.implementation.createHTMLDocument("")).createElement("base")).href=C.location.href,t.head.appendChild(r)):t=C),o=!n&&[],(i=w.exec(e))?[t.createElement(i[1])]:(i=Ae([e],t,o),o&&o.length&&ce(o).remove(),ce.merge([],i.childNodes)));var r,i,o},ce.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(ce.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},ce.expr.pseudos.animated=function(t){return ce.grep(ce.timers,function(e){return t===e.elem}).length},ce.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=ce.css(e,"position"),c=ce(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=ce.css(e,"top"),u=ce.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),v(t)&&(t=t.call(e,n,ce.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},ce.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){ce.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===ce.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===ce.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=ce(e).offset()).top+=ce.css(e,"borderTopWidth",!0),i.left+=ce.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-ce.css(r,"marginTop",!0),left:t.left-i.left-ce.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===ce.css(e,"position"))e=e.offsetParent;return e||J})}}),ce.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;ce.fn[t]=function(e){return M(this,function(e,t,n){var r;if(y(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),ce.each(["top","left"],function(e,n){ce.cssHooks[n]=Ye(le.pixelPosition,function(e,t){if(t)return t=Ge(e,n),_e.test(t)?ce(e).position()[n]+"px":t})}),ce.each({Height:"height",Width:"width"},function(a,s){ce.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){ce.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return M(this,function(e,t,n){var r;return y(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?ce.css(e,t,i):ce.style(e,t,n,i)},s,n?e:void 0,n)}})}),ce.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){ce.fn[t]=function(e){return this.on(t,e)}}),ce.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.on("mouseenter",e).on("mouseleave",t||e)}}),ce.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){ce.fn[n]=function(e,t){return 0 + +
+ JavaScript is not enabled or JavaScript files were blocked. The Mailu setup site does not function when JavaScript is disabled. +
+ + @@ -61,12 +58,16 @@ the security implications caused by such an increase of attack surface.

Enable oletools - Oletools scans documents in email attachements for malicious macros. It has a much lower memory footprint than a full-fledged anti-virus. + Oletools scans documents in email attachments for malicious macros. It has a much lower memory footprint than a full-fledged anti-virus. - - - +

+ + Tika enables the functionality for searching through attachments. Tika scans documents in email attachments, process (OCR, keyword extraction) and then index them in a way they can be efficiently searched. This requires significant resources (RAM, CPU and storage). +
{% endcall %} diff --git a/setup/templates/steps/compose/03_expose.html b/setup/templates/steps/compose/03_expose.html index c1d3ca8c..e14e4bbc 100644 --- a/setup/templates/steps/compose/03_expose.html +++ b/setup/templates/steps/compose/03_expose.html @@ -16,11 +16,10 @@ avoid generic all-interfaces addresses like 0.0.0.0 or :: - - - + + +
@@ -34,8 +33,7 @@ avoid generic all-interfaces addresses like 0.0.0.0 or ::Read this: Docker currently does not expose the IPv6 ports properly, as it does not interface with ip6tables. Read FAQ section and be very careful. We do NOT recommend that you enable this!

- +
diff --git a/setup/templates/steps/config.html b/setup/templates/steps/config.html index 025a2a2c..e01d9c97 100644 --- a/setup/templates/steps/config.html +++ b/setup/templates/steps/config.html @@ -4,13 +4,13 @@
- +

In the following sections we need to set the postmaster address. This is a combination of the postmaster local part and the main mail domain. The main mail domain is also used as "server display name". This is the way the SMTP server identifies itself when connecting to others. The Postmaster will get an e-mail address <postmaster>@<main_domain>. This address will receive the DMARC "rua" and "ruf" reports. -Or in plain english: if receivers start to classify your mail as spam, this postmaster will be informed.

+Or in plain English: if receivers start to classify your mail as spam, this postmaster will be informed.