Merge branch 'master' into bf/2856

This commit is contained in:
Stephan Holl
2023-11-22 20:15:07 +01:00
114 changed files with 3622 additions and 561 deletions

View File

@@ -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

1
.gitignore vendored
View File

@@ -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

View File

@@ -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

View File

@@ -1,7 +1,15 @@
<!--
Thank you for opening an issue with Mailu. Please understand that issues are meant for bugs and enhancement-requests.
For **user-support questions**, reach out to us on [matrix](https://matrix.to/#/#mailu:tedomum.net).
Thank you for opening an issue with Mailu. Please understand that issues are meant for bugs only. The bug report should follow the issue template and provide clear replication steps and logs.
For **user-support questions**, reach out to us on [matrix](https://matrix.to/#/#mailu:tedomum.net) or [disussions](https://github.com/Mailu/Mailu/discussions/categories/user-support).
For anything but bug reports use the [matrix channel](https://matrix.to/#/#mailu:tedomum.net) or [disussions](https://github.com/Mailu/Mailu/discussions).
So use discussions for topics such as
* Checking announcements.
* General discussion about Mailu usage or using Mail software in general.
* Feature requests
* User support.
To be able to help you best, we need some more information.
@@ -10,21 +18,17 @@ Before you open your issue
- Check [documentation](https://mailu.io/master/) and [FAQ](https://mailu.io/master/faq.html). (Tip, use the search function on the documentation page)
- You understand `Mailu` is made by volunteers in their **free time** — be concise, civil and accept that delays can occur.
- The title of the issue should be short and simple. It should contain specific terms related to the actual issue. Be specific while writing the title.
- You understand issues are only meant for bug reports that follow the issue template. Non bug reports or bug reports that do not follow the template will be moved to [disussions](https://github.com/Mailu/Mailu/discussions)
Please put your text outside of the comment blocks to be visible. You can use the button "Preview" above to check.
If you do not follow the issue template suggested below your issue may be summarily closed.
-->
## Environment & Version
### Environment
- [ ] docker compose
- [ ] kubernetes
- [ ] docker swarm
### Version
- `docker compose version`
- Version: `master`
<!--
@@ -35,6 +39,11 @@ $> docker ps -a | grep mailu
$> grep MAILU_VERSION docker-compose.yml mailu.env
-->
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
<!--
Further explain the bug in a few words. It should be clear what the unexpected behaviour is. Share it in an easy-to-understand language.
@@ -63,9 +72,10 @@ You can get the logs via `docker logs <container name> --tail 1000`.
For example for the admin container: `docker logs mailu_admin_1 --tail 1000`
or using docker compose `docker compose -f /mailu/docker-compose.yml logs --tail 1000 admin`
If you can find the relevant section, please share only the parts that seem relevant. If you have any logs, please enclose them in code tags, like so:
If you can find the relevant section, please share only the parts that seem relevant. If you have any logs, please enclose them in code tags and in a section, like so:
```
Your logs here!
```
-->

View File

@@ -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

View File

@@ -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

View File

@@ -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"):

View File

@@ -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

View File

@@ -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

View File

@@ -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/<string:email>')
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('/<string:token_id>')
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

View File

@@ -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 users email box in bytes', example='1000000000'),
'quota_bytes_used': fields.Integer(description='The size of the users 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'),

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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 }}";

View File

@@ -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):

View File

@@ -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 ""

View File

@@ -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"

View File

@@ -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 <hosni.hossein@gmail.com>, 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 <hosni.hossein@gmail.com>\n"
"Language-Team: Persian <https://translate.tedomum.net/projects/mailu/admin/"
"en/>\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 <code>MX</code> points to this server"
msgstr ""
"برای ثبت دامنه جدید، ابتدا می‌بایست تنظیمات DNS ٔدامنه را به گونه‌ای تنظیم کنید "
"که رکورد <code>MX</code> آن به سرور ما اشاره کند"
#: mailu/ui/templates/domain/signup.html:18
msgid ""
"If you do not know how to setup an <code>MX</code> record for your DNS "
"zone,\n"
" please contact your DNS provider or administrator. Also, please wait a\n"
" couple minutes after the <code>MX</code> is set so the local server "
"cache\n"
" expires."
msgstr ""
"در صورتی که نمی‌دانید چگونه تنظیمات رکورد <code>MX</code> دامنه خود را انجام "
"دهید، می‌بایست با فروشنده دامنه خود ارتباط برقرار کنید.\n"
"همچنین بعد از تنظیم رکورد <code>MX</code> دقایقی را تا اعمال نتیجه آن منتظر "
"بمانید."
#: 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 "جایگاه‌های موجود"

View File

@@ -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 <sydorenkodanylo2021@gmail.com>, 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 <sydorenkodanylo2021@gmail.com>\n"
"Language-Team: Ukrainian <https://translate.tedomum.net/projects/mailu/admin/"
"uk/>\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 <code>MX</code> points to this server"
msgstr ""
"Для того, щоб зареєструвати новий домен, необхідно\n"
" спочатку налаштувати доменну <code>MX</code> так,\n"
" щоб домен вказував на цей сервер"
#: mailu/ui/templates/domain/signup.html:18
msgid ""
"If you do not know how to setup an <code>MX</code> record for your DNS "
"zone,\n"
" please contact your DNS provider or administrator. Also, please wait a\n"
" couple minutes after the <code>MX</code> is set so the local server "
"cache\n"
" expires."
msgstr ""
"Якщо ви не знаєте, як налаштувати <code>MX</code>-запис для вашої DNS-зони,\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 "Наявні слоти"

View File

@@ -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 <tryweb@ichiayi.com>\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 <code>MX</code> points to this server"
msgstr "為了註冊新網域,您必須先設定網域,並將網域 <code>MX</code> 指向這伺服主機。"
#: mailu/ui/templates/domain/signup.html:18
msgid ""
"If you do not know how to setup an <code>MX</code> record for your DNS zone,\n"
" please contact your DNS provider or administrator. Also, please wait a\n"
" couple minutes after the <code>MX</code> is set so the local server cache\n"
" expires."
msgstr ""
"如果您不知道如何為您的 DNS 網域設定 <code>MX</code> 記錄,請聯繫您的 DNS 提供商或管理員。"
"此外,請在設定 <code>MX</code> 之後等待幾分鐘 讓本地伺服主機的快取過期。"
#: 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 "轉寄郵件"

View File

@@ -60,6 +60,6 @@
{%- endcall %}
<blockquote>
{% trans %}If you use an Apple device,{% endtrans %}
<a href="/apple.mobileconfig">{% trans %}click here to autoconfigure it.{% endtrans %}</a>
<a href="/apple.mobileconfig">{% trans %}click here to auto-configure it.{% endtrans %}</a>
</blockquote>
{%- endblock %}

View File

@@ -10,13 +10,13 @@
{%- block main_action %}
{%- if current_user.global_admin %}
<a class="btn btn-primary float-right" href="{{ url_for(".domain_genkeys", domain_name=domain.name) }}">
<a class="btn btn-primary btn-group float-right" href="{{ url_for(".domain_genkeys", domain_name=domain.name) }}">
{%- if domain.dkim_publickey %}
{% trans %}Regenerate keys{% endtrans %}
{%- else %}
{% trans %}Generate keys{% endtrans %}
{%- endif %}
</a>
</a><a style="margin-right: 5px;" class="btn btn-primary btn-group float-right" href="{{ url_for(".domain_download_zonefile", domain_name=domain.name) }}"> {% trans %}Download zonefile{% endtrans %}</a>
{%- endif %}
{%- endblock %}
@@ -36,10 +36,6 @@
</td>
</tr>
{%- if domain.dkim_publickey %}
<tr>
<th>{% trans %}DKIM public key{% endtrans %}</th>
<td>{{ macros.clip("dkim_key") }}<pre id="dkim_key" class="pre-config border bg-light">{{ domain.dkim_publickey }}</pre></td>
</tr>
<tr>
<th>{% trans %}DNS DKIM entry{% endtrans %}</th>
<td>{{ macros.clip("dns_dkim") }}<pre id="dns_dkim" class="pre-config border bg-light">{{ domain.dns_dkim }}</pre></td>

View File

@@ -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/<domain_name>/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/<domain_name>', methods=['GET', 'POST'])
@access.domain_admin(models.Domain, 'domain_name')

View File

@@ -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()

View File

@@ -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" \

View File

@@ -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",

View File

@@ -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" \

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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/

View File

@@ -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
###############

View File

@@ -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)

View File

@@ -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/

View File

@@ -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

View File

@@ -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)

View File

@@ -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

8
core/oletools/start.py Executable file
View File

@@ -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())

View File

@@ -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)

View File

@@ -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"

View File

@@ -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..$';

View File

@@ -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"

View File

@@ -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"])

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
---------------------

View File

@@ -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 <override-label>`.
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<override-label>`.
.. _header_authentication:
Header authentication using an external proxy

View File

@@ -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

View File

@@ -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``,

View File

@@ -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

View File

@@ -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.

View File

@@ -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``

View File

@@ -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-<service name>
| where <service-name> 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

View File

@@ -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

View File

@@ -0,0 +1,517 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"data": {
"image/svg+xml": [
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n",
"<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n",
" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n",
"<!-- Generated by graphviz version 2.43.0 (0)\n",
" -->\n",
"<!-- Title: mailu Pages: 1 -->\n",
"<svg width=\"706pt\" height=\"553pt\"\n",
" viewBox=\"0.00 0.00 706.00 553.00\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n",
"<g id=\"graph0\" class=\"graph\" transform=\"scale(1 1) rotate(0) translate(4 549)\">\n",
"<title>mailu</title>\n",
"<polygon fill=\"white\" stroke=\"transparent\" points=\"-4,4 -4,-549 702,-549 702,4 -4,4\"/>\n",
"<text text-anchor=\"middle\" x=\"349\" y=\"-7.8\" font-family=\"arial\" font-size=\"14.00\">Mailu</text>\n",
"<!-- internet -->\n",
"<g id=\"node1\" class=\"node\">\n",
"<title>internet</title>\n",
"<polygon fill=\"#d3edea\" stroke=\"#d3edea\" points=\"297,-545 243,-545 243,-509 297,-509 297,-545\"/>\n",
"<text text-anchor=\"middle\" x=\"270\" y=\"-525.1\" font-family=\"arial\" font-size=\"8.00\">Internet</text>\n",
"</g>\n",
"<!-- front -->\n",
"<g id=\"node2\" class=\"node\">\n",
"<title>front</title>\n",
"<polygon fill=\"#d3edea\" stroke=\"#d3edea\" points=\"364,-464 310,-464 310,-428 364,-428 364,-464\"/>\n",
"<text text-anchor=\"middle\" x=\"337\" y=\"-444.1\" font-family=\"arial\" font-size=\"8.00\">Front</text>\n",
"</g>\n",
"<!-- internet&#45;&gt;front -->\n",
"<g id=\"edge1\" class=\"edge\">\n",
"<title>internet&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M242.81,-521.99C180.56,-512.79 33,-491 33,-491 33,-491 33,-482 33,-482 33,-482 218.14,-460.68 299.46,-451.32\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"300.22,-454.76 309.75,-450.14 299.42,-447.8 300.22,-454.76\"/>\n",
"<text text-anchor=\"middle\" x=\"46.5\" y=\"-484.6\" font-family=\"Times,serif\" font-size=\"8.00\">80/tcp</text>\n",
"</g>\n",
"<!-- internet&#45;&gt;front -->\n",
"<g id=\"edge2\" class=\"edge\">\n",
"<title>internet&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M242.81,-520.91C190.94,-511.2 83,-491 83,-491 83,-491 83,-482 83,-482 83,-482 228.93,-461.89 299.56,-452.16\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"300.39,-455.58 309.82,-450.75 299.43,-448.64 300.39,-455.58\"/>\n",
"<text text-anchor=\"middle\" x=\"99\" y=\"-484.6\" font-family=\"Times,serif\" font-size=\"8.00\">443/tcp</text>\n",
"</g>\n",
"<!-- internet&#45;&gt;front -->\n",
"<g id=\"edge3\" class=\"edge\">\n",
"<title>internet&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M242.97,-518.83C204.35,-508.59 138,-491 138,-491 138,-491 138,-482 138,-482 138,-482 241.91,-463.72 299.56,-453.59\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"300.5,-456.97 309.75,-451.79 299.29,-450.08 300.5,-456.97\"/>\n",
"<text text-anchor=\"middle\" x=\"151.5\" y=\"-484.6\" font-family=\"Times,serif\" font-size=\"8.00\">25/tcp</text>\n",
"</g>\n",
"<!-- internet&#45;&gt;front -->\n",
"<g id=\"edge4\" class=\"edge\">\n",
"<title>internet&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M242.7,-514.35C218.84,-504.16 188,-491 188,-491 188,-491 188,-482 188,-482 188,-482 255.7,-466.1 299.83,-455.73\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"300.74,-459.11 309.67,-453.42 299.14,-452.3 300.74,-459.11\"/>\n",
"<text text-anchor=\"middle\" x=\"204\" y=\"-484.6\" font-family=\"Times,serif\" font-size=\"8.00\">465/tcp</text>\n",
"</g>\n",
"<!-- internet&#45;&gt;front -->\n",
"<g id=\"edge5\" class=\"edge\">\n",
"<title>internet&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M256.82,-508.91C249.96,-500.03 243,-491 243,-491 243,-491 243,-482 243,-482 243,-482 273.97,-470.47 300.36,-460.64\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"301.66,-463.89 309.81,-457.12 299.22,-457.33 301.66,-463.89\"/>\n",
"<text text-anchor=\"middle\" x=\"259\" y=\"-484.6\" font-family=\"Times,serif\" font-size=\"8.00\">587/tcp</text>\n",
"</g>\n",
"<!-- internet&#45;&gt;front -->\n",
"<g id=\"edge6\" class=\"edge\">\n",
"<title>internet&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M280.91,-508.86C288.77,-496.51 298,-482 298,-482 298,-482 303.42,-477.13 310.23,-471.03\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"312.85,-473.37 317.96,-464.09 308.18,-468.16 312.85,-473.37\"/>\n",
"<text text-anchor=\"middle\" x=\"314\" y=\"-484.6\" font-family=\"Times,serif\" font-size=\"8.00\">110/tcp</text>\n",
"</g>\n",
"<!-- internet&#45;&gt;front -->\n",
"<g id=\"edge7\" class=\"edge\">\n",
"<title>internet&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M297.25,-517.71C331.26,-507.36 385,-491 385,-491 385,-491 385,-482 385,-482 385,-482 377.6,-476.6 368.6,-470.04\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"370.58,-467.15 360.44,-464.09 366.46,-472.81 370.58,-467.15\"/>\n",
"<text text-anchor=\"middle\" x=\"401\" y=\"-484.6\" font-family=\"Times,serif\" font-size=\"8.00\">995/tcp</text>\n",
"</g>\n",
"<!-- internet&#45;&gt;front -->\n",
"<g id=\"edge8\" class=\"edge\">\n",
"<title>internet&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M297.34,-519.66C340.86,-509.58 421,-491 421,-491 421,-491 421,-482 421,-482 421,-482 396.19,-471.66 373.54,-462.22\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"374.62,-458.88 364.04,-458.27 371.93,-465.34 374.62,-458.88\"/>\n",
"<text text-anchor=\"middle\" x=\"437\" y=\"-484.6\" font-family=\"Times,serif\" font-size=\"8.00\">143/tcp</text>\n",
"</g>\n",
"<!-- internet&#45;&gt;front -->\n",
"<g id=\"edge9\" class=\"edge\">\n",
"<title>internet&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M297.19,-510.14C313.07,-500.88 330,-491 330,-491 330,-491 331.24,-483.18 332.68,-474.13\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"336.16,-474.56 334.27,-464.14 329.25,-473.46 336.16,-474.56\"/>\n",
"<text text-anchor=\"middle\" x=\"348\" y=\"-484.6\" font-family=\"Times,serif\" font-size=\"8.00\">993/tcp</text>\n",
"</g>\n",
"<!-- internet&#45;&gt;front -->\n",
"<g id=\"edge10\" class=\"edge\">\n",
"<title>internet&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M297.19,-520.91C349.06,-511.2 457,-491 457,-491 457,-491 457,-482 457,-482 457,-482 409.48,-468.14 374.25,-457.86\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"374.81,-454.38 364.23,-454.94 372.85,-461.1 374.81,-454.38\"/>\n",
"<text text-anchor=\"middle\" x=\"475.5\" y=\"-484.6\" font-family=\"Times,serif\" font-size=\"8.00\">4190/tcp</text>\n",
"</g>\n",
"<!-- front&#45;&gt;front -->\n",
"<g id=\"edge11\" class=\"edge\">\n",
"<title>front&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M364.24,-449.75C374.02,-449.83 382,-448.58 382,-446 382,-444.43 379.04,-443.35 374.51,-442.77\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"374.4,-439.26 364.24,-442.25 374.05,-446.25 374.4,-439.26\"/>\n",
"<text text-anchor=\"middle\" x=\"400.5\" y=\"-444.1\" font-family=\"Times,serif\" font-size=\"8.00\">8008/tcp</text>\n",
"</g>\n",
"<!-- front&#45;&gt;front -->\n",
"<g id=\"edge12\" class=\"edge\">\n",
"<title>front&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M364.06,-452.31C389.18,-455.1 419,-452.99 419,-446 419,-439.94 396.57,-437.55 374.23,-438.84\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"373.73,-435.37 364.06,-439.69 374.31,-442.34 373.73,-435.37\"/>\n",
"<text text-anchor=\"middle\" x=\"437.5\" y=\"-444.1\" font-family=\"Times,serif\" font-size=\"8.00\">8000/tcp</text>\n",
"</g>\n",
"<!-- admin -->\n",
"<g id=\"node3\" class=\"node\">\n",
"<title>admin</title>\n",
"<polygon fill=\"#d3edea\" stroke=\"#d3edea\" points=\"556,-302 502,-302 502,-266 556,-266 556,-302\"/>\n",
"<text text-anchor=\"middle\" x=\"529\" y=\"-282.1\" font-family=\"arial\" font-size=\"8.00\">Admin</text>\n",
"</g>\n",
"<!-- front&#45;&gt;admin -->\n",
"<g id=\"edge13\" class=\"edge\">\n",
"<title>front&#45;&gt;admin</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M364.05,-434.12C389.59,-423.84 424,-410 424,-410 424,-410 465,-347 465,-347 465,-347 485.85,-326.8 503.75,-309.46\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"506.43,-311.74 511.17,-302.27 501.55,-306.71 506.43,-311.74\"/>\n",
"<text text-anchor=\"middle\" x=\"483.5\" y=\"-363.1\" font-family=\"Times,serif\" font-size=\"8.00\">8080/tcp</text>\n",
"</g>\n",
"<!-- smtp -->\n",
"<g id=\"node4\" class=\"node\">\n",
"<title>smtp</title>\n",
"<polygon fill=\"#d3edea\" stroke=\"#d3edea\" points=\"565,-383 511,-383 511,-347 565,-347 565,-383\"/>\n",
"<text text-anchor=\"middle\" x=\"538\" y=\"-363.1\" font-family=\"arial\" font-size=\"8.00\">SMTP</text>\n",
"</g>\n",
"<!-- front&#45;&gt;smtp -->\n",
"<g id=\"edge17\" class=\"edge\">\n",
"<title>front&#45;&gt;smtp</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M364.19,-439.53C413.12,-429.69 511,-410 511,-410 511,-410 516.33,-401.32 522.25,-391.67\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"525.24,-393.49 527.48,-383.14 519.27,-389.83 525.24,-393.49\"/>\n",
"<text text-anchor=\"middle\" x=\"529.5\" y=\"-403.6\" font-family=\"Times,serif\" font-size=\"8.00\">25/tcp</text>\n",
"</g>\n",
"<!-- front&#45;&gt;smtp -->\n",
"<g id=\"edge18\" class=\"edge\">\n",
"<title>front&#45;&gt;smtp</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M364.02,-440.5C420.74,-431.04 547,-410 547,-410 547,-410 547,-401 547,-401 547,-401 546.14,-397.65 544.95,-393.02\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"548.27,-391.9 542.39,-383.09 541.5,-393.65 548.27,-391.9\"/>\n",
"<text text-anchor=\"middle\" x=\"568.5\" y=\"-403.6\" font-family=\"Times,serif\" font-size=\"8.00\">10025/tcp</text>\n",
"</g>\n",
"<!-- antispam -->\n",
"<g id=\"node6\" class=\"node\">\n",
"<title>antispam</title>\n",
"<polygon fill=\"#d3edea\" stroke=\"#d3edea\" points=\"632,-140 578,-140 578,-104 632,-104 632,-140\"/>\n",
"<text text-anchor=\"middle\" x=\"605\" y=\"-120.1\" font-family=\"arial\" font-size=\"8.00\">Antispam</text>\n",
"</g>\n",
"<!-- front&#45;&gt;antispam -->\n",
"<g id=\"edge20\" class=\"edge\">\n",
"<title>front&#45;&gt;antispam</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M364.02,-441.32C430,-432.33 594,-410 594,-410 594,-410 649,-248 649,-248 649,-248 649,-239 649,-239 649,-239 620,-158 620,-158 620,-158 618.41,-154.3 616.27,-149.3\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"619.48,-147.9 612.32,-140.09 613.05,-150.66 619.48,-147.9\"/>\n",
"<text text-anchor=\"middle\" x=\"663.5\" y=\"-282.1\" font-family=\"Times,serif\" font-size=\"8.00\">11334/tcp</text>\n",
"</g>\n",
"<!-- imap -->\n",
"<g id=\"node8\" class=\"node\">\n",
"<title>imap</title>\n",
"<polygon fill=\"#d3edea\" stroke=\"#d3edea\" points=\"411,-221 357,-221 357,-185 411,-185 411,-221\"/>\n",
"<text text-anchor=\"middle\" x=\"384\" y=\"-201.1\" font-family=\"arial\" font-size=\"8.00\">IMAP</text>\n",
"</g>\n",
"<!-- front&#45;&gt;imap -->\n",
"<g id=\"edge14\" class=\"edge\">\n",
"<title>front&#45;&gt;imap</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M338.92,-427.88C342.75,-393.65 351,-320 351,-320 351,-320 370,-239 370,-239 370,-239 371.41,-235.47 373.34,-230.66\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"376.7,-231.67 377.16,-221.09 370.2,-229.07 376.7,-231.67\"/>\n",
"<text text-anchor=\"middle\" x=\"369.5\" y=\"-322.6\" font-family=\"Times,serif\" font-size=\"8.00\">4190/tcp</text>\n",
"</g>\n",
"<!-- front&#45;&gt;imap -->\n",
"<g id=\"edge15\" class=\"edge\">\n",
"<title>front&#45;&gt;imap</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M309.86,-441.8C236.54,-433.16 40,-410 40,-410 40,-410 0,-383 0,-383 0,-383 0,-347 0,-347 0,-347 286,-239 286,-239 286,-239 319.57,-227.01 347.42,-217.07\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"348.72,-220.32 356.96,-213.66 346.37,-213.72 348.72,-220.32\"/>\n",
"<text text-anchor=\"middle\" x=\"87\" y=\"-322.6\" font-family=\"Times,serif\" font-size=\"8.00\">143/tcp</text>\n",
"</g>\n",
"<!-- front&#45;&gt;imap -->\n",
"<g id=\"edge16\" class=\"edge\">\n",
"<title>front&#45;&gt;imap</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M346.28,-427.91C351.1,-419.03 356,-410 356,-410 356,-410 388,-329 388,-329 388,-329 388,-320 388,-320 388,-320 386.13,-265.76 384.95,-231.46\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"388.43,-230.99 384.59,-221.12 381.44,-231.23 388.43,-230.99\"/>\n",
"<text text-anchor=\"middle\" x=\"404\" y=\"-322.6\" font-family=\"Times,serif\" font-size=\"8.00\">110/tcp</text>\n",
"</g>\n",
"<!-- webdav -->\n",
"<g id=\"node9\" class=\"node\">\n",
"<title>webdav</title>\n",
"<polygon fill=\"#d3edea\" stroke=\"#d3edea\" points=\"63,-383 9,-383 9,-347 63,-347 63,-383\"/>\n",
"<text text-anchor=\"middle\" x=\"36\" y=\"-363.1\" font-family=\"arial\" font-size=\"8.00\">WebDAV</text>\n",
"</g>\n",
"<!-- front&#45;&gt;webdav -->\n",
"<g id=\"edge21\" class=\"edge\">\n",
"<title>front&#45;&gt;webdav</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M309.79,-441.74C237.3,-433.05 45,-410 45,-410 45,-410 43.4,-402.18 41.55,-393.13\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"44.94,-392.23 39.51,-383.14 38.08,-393.64 44.94,-392.23\"/>\n",
"<text text-anchor=\"middle\" x=\"63.5\" y=\"-403.6\" font-family=\"Times,serif\" font-size=\"8.00\">5232/tcp</text>\n",
"</g>\n",
"<!-- webmail -->\n",
"<g id=\"node10\" class=\"node\">\n",
"<title>webmail</title>\n",
"<polygon fill=\"#d3edea\" stroke=\"#d3edea\" points=\"211,-383 157,-383 157,-347 211,-347 211,-383\"/>\n",
"<text text-anchor=\"middle\" x=\"184\" y=\"-363.1\" font-family=\"arial\" font-size=\"8.00\">Webmail</text>\n",
"</g>\n",
"<!-- front&#45;&gt;webmail -->\n",
"<g id=\"edge19\" class=\"edge\">\n",
"<title>front&#45;&gt;webmail</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M310,-437.06C274.89,-426.73 218,-410 218,-410 218,-410 211.02,-400.97 203.39,-391.09\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"206.13,-388.91 197.24,-383.14 200.59,-393.19 206.13,-388.91\"/>\n",
"<text text-anchor=\"middle\" x=\"231.5\" y=\"-403.6\" font-family=\"Times,serif\" font-size=\"8.00\">80/tcp</text>\n",
"</g>\n",
"<!-- redis -->\n",
"<g id=\"node5\" class=\"node\">\n",
"<title>redis</title>\n",
"<polygon fill=\"#d3edea\" stroke=\"#d3edea\" points=\"537,-59 483,-59 483,-23 537,-23 537,-59\"/>\n",
"<text text-anchor=\"middle\" x=\"510\" y=\"-39.1\" font-family=\"arial\" font-size=\"8.00\">Redis</text>\n",
"</g>\n",
"<!-- admin&#45;&gt;redis -->\n",
"<g id=\"edge32\" class=\"edge\">\n",
"<title>admin&#45;&gt;redis</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M522.36,-265.88C509.84,-233.62 484,-167 484,-167 484,-167 484,-158 484,-158 484,-158 496.26,-103.29 503.95,-69.01\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"507.39,-69.64 506.16,-59.12 500.56,-68.11 507.39,-69.64\"/>\n",
"<text text-anchor=\"middle\" x=\"502.5\" y=\"-160.6\" font-family=\"Times,serif\" font-size=\"8.00\">6379/tcp</text>\n",
"</g>\n",
"<!-- admin&#45;&gt;imap -->\n",
"<g id=\"edge33\" class=\"edge\">\n",
"<title>admin&#45;&gt;imap</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M501.75,-274.71C467.74,-264.36 414,-248 414,-248 414,-248 408.08,-239.32 401.5,-229.67\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"404.21,-227.43 395.68,-221.14 398.43,-231.37 404.21,-227.43\"/>\n",
"<text text-anchor=\"middle\" x=\"432.5\" y=\"-241.6\" font-family=\"Times,serif\" font-size=\"8.00\">2525/tcp</text>\n",
"</g>\n",
"<!-- smtp&#45;&gt;front -->\n",
"<g id=\"edge23\" class=\"edge\">\n",
"<title>smtp&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M510.95,-379.68C485.41,-392.6 451,-410 451,-410 451,-410 407.25,-423.43 373.95,-433.65\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"372.55,-430.42 364.02,-436.71 374.6,-437.12 372.55,-430.42\"/>\n",
"<text text-anchor=\"middle\" x=\"485.5\" y=\"-403.6\" font-family=\"Times,serif\" font-size=\"8.00\">2525/tcp</text>\n",
"</g>\n",
"<!-- smtp&#45;&gt;admin -->\n",
"<g id=\"edge22\" class=\"edge\">\n",
"<title>smtp&#45;&gt;admin</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M536.05,-346.86C534.89,-336.71 533.4,-323.63 532.09,-312.12\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"535.56,-311.65 530.95,-302.11 528.6,-312.44 535.56,-311.65\"/>\n",
"<text text-anchor=\"middle\" x=\"551.5\" y=\"-322.6\" font-family=\"Times,serif\" font-size=\"8.00\">8080/tcp</text>\n",
"</g>\n",
"<!-- smtp&#45;&gt;antispam -->\n",
"<g id=\"edge24\" class=\"edge\">\n",
"<title>smtp&#45;&gt;antispam</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M555.58,-346.91C564.72,-338.03 574,-329 574,-329 574,-329 608,-221 608,-221 608,-221 608,-185 608,-185 608,-185 607.12,-166.91 606.32,-150.27\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"609.81,-150.09 605.84,-140.27 602.82,-150.43 609.81,-150.09\"/>\n",
"<text text-anchor=\"middle\" x=\"623.5\" y=\"-241.6\" font-family=\"Times,serif\" font-size=\"8.00\">11332/tcp</text>\n",
"</g>\n",
"<!-- antispam&#45;&gt;admin -->\n",
"<g id=\"edge35\" class=\"edge\">\n",
"<title>antispam&#45;&gt;admin</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M597.99,-140.14C592.94,-152.49 587,-167 587,-167 587,-167 559.17,-222.66 542.07,-256.87\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"538.9,-255.37 537.56,-265.88 545.16,-258.5 538.9,-255.37\"/>\n",
"<text text-anchor=\"middle\" x=\"590.5\" y=\"-201.1\" font-family=\"Times,serif\" font-size=\"8.00\">80/tcp</text>\n",
"</g>\n",
"<!-- antispam&#45;&gt;redis -->\n",
"<g id=\"edge34\" class=\"edge\">\n",
"<title>antispam&#45;&gt;redis</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M583.03,-103.91C571.6,-95.03 560,-86 560,-86 560,-86 548.91,-76.24 537.17,-65.91\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"539.29,-63.12 529.47,-59.14 534.67,-68.37 539.29,-63.12\"/>\n",
"<text text-anchor=\"middle\" x=\"578.5\" y=\"-79.6\" font-family=\"Times,serif\" font-size=\"8.00\">6379/tcp</text>\n",
"</g>\n",
"<!-- antivirus -->\n",
"<g id=\"node7\" class=\"node\">\n",
"<title>antivirus</title>\n",
"<polygon fill=\"#d3edea\" stroke=\"#d3edea\" points=\"698,-59 644,-59 644,-23 698,-23 698,-59\"/>\n",
"<text text-anchor=\"middle\" x=\"671\" y=\"-39.1\" font-family=\"arial\" font-size=\"8.00\">Anti&#45;Virus</text>\n",
"</g>\n",
"<!-- antispam&#45;&gt;antivirus -->\n",
"<g id=\"edge37\" class=\"edge\">\n",
"<title>antispam&#45;&gt;antivirus</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M626.48,-103.91C637.65,-95.03 649,-86 649,-86 649,-86 653.17,-77.67 657.87,-68.26\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"661.09,-69.65 662.43,-59.14 654.83,-66.52 661.09,-69.65\"/>\n",
"<text text-anchor=\"middle\" x=\"671.5\" y=\"-79.6\" font-family=\"Times,serif\" font-size=\"8.00\">3310/tcp</text>\n",
"</g>\n",
"<!-- oletools -->\n",
"<g id=\"node12\" class=\"node\">\n",
"<title>oletools</title>\n",
"<polygon fill=\"#d3edea\" stroke=\"#d3edea\" points=\"626,-59 572,-59 572,-23 626,-23 626,-59\"/>\n",
"<text text-anchor=\"middle\" x=\"599\" y=\"-39.1\" font-family=\"arial\" font-size=\"8.00\">Oletools</text>\n",
"</g>\n",
"<!-- antispam&#45;&gt;oletools -->\n",
"<g id=\"edge36\" class=\"edge\">\n",
"<title>antispam&#45;&gt;oletools</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M603.7,-103.86C602.93,-93.71 601.93,-80.63 601.06,-69.12\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"604.55,-68.81 600.3,-59.11 597.57,-69.34 604.55,-68.81\"/>\n",
"<text text-anchor=\"middle\" x=\"623.5\" y=\"-79.6\" font-family=\"Times,serif\" font-size=\"8.00\">11343/tcp</text>\n",
"</g>\n",
"<!-- imap&#45;&gt;front -->\n",
"<g id=\"edge27\" class=\"edge\">\n",
"<title>imap&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M374.23,-221.09C369.16,-229.97 364,-239 364,-239 364,-239 320,-320 320,-320 320,-320 320,-329 320,-329 320,-329 328.02,-383.71 333.04,-417.99\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"329.58,-418.5 334.49,-427.88 336.5,-417.48 329.58,-418.5\"/>\n",
"<text text-anchor=\"middle\" x=\"333.5\" y=\"-322.6\" font-family=\"Times,serif\" font-size=\"8.00\">25/tcp</text>\n",
"</g>\n",
"<!-- imap&#45;&gt;admin -->\n",
"<g id=\"edge25\" class=\"edge\">\n",
"<title>imap&#45;&gt;admin</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M411.25,-217.43C431.27,-227.3 455,-239 455,-239 455,-239 474.19,-250.41 493.02,-261.61\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"491.48,-264.76 501.87,-266.87 495.06,-258.75 491.48,-264.76\"/>\n",
"<text text-anchor=\"middle\" x=\"487.5\" y=\"-241.6\" font-family=\"Times,serif\" font-size=\"8.00\">8080/tcp</text>\n",
"</g>\n",
"<!-- imap&#45;&gt;antispam -->\n",
"<g id=\"edge26\" class=\"edge\">\n",
"<title>imap&#45;&gt;antispam</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M411.19,-195.25C452.16,-185.08 525,-167 525,-167 525,-167 547.74,-154.49 568.98,-142.81\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"570.84,-145.78 577.92,-137.9 567.47,-139.65 570.84,-145.78\"/>\n",
"<text text-anchor=\"middle\" x=\"561.5\" y=\"-160.6\" font-family=\"Times,serif\" font-size=\"8.00\">11334/tcp</text>\n",
"</g>\n",
"<!-- fts_attachments -->\n",
"<g id=\"node13\" class=\"node\">\n",
"<title>fts_attachments</title>\n",
"<polygon fill=\"#d3edea\" stroke=\"#d3edea\" points=\"411,-140 357,-140 357,-104 411,-104 411,-140\"/>\n",
"<text text-anchor=\"middle\" x=\"384\" y=\"-120.1\" font-family=\"arial\" font-size=\"8.00\">Tika</text>\n",
"</g>\n",
"<!-- imap&#45;&gt;fts_attachments -->\n",
"<g id=\"edge28\" class=\"edge\">\n",
"<title>imap&#45;&gt;fts_attachments</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M384,-184.86C384,-174.71 384,-161.63 384,-150.12\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"387.5,-150.11 384,-140.11 380.5,-150.11 387.5,-150.11\"/>\n",
"<text text-anchor=\"middle\" x=\"402.5\" y=\"-160.6\" font-family=\"Times,serif\" font-size=\"8.00\">9998/tcp</text>\n",
"</g>\n",
"<!-- webmail&#45;&gt;front -->\n",
"<g id=\"edge29\" class=\"edge\">\n",
"<title>webmail&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M211.04,-380.16C235.6,-393.03 268,-410 268,-410 268,-410 283.98,-418.1 300.56,-426.51\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"299.23,-429.76 309.73,-431.17 302.39,-423.52 299.23,-429.76\"/>\n",
"<text text-anchor=\"middle\" x=\"289.5\" y=\"-403.6\" font-family=\"Times,serif\" font-size=\"8.00\">14190/tcp</text>\n",
"</g>\n",
"<!-- webmail&#45;&gt;front -->\n",
"<g id=\"edge30\" class=\"edge\">\n",
"<title>webmail&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M156.96,-375.66C127.95,-386.02 86,-401 86,-401 86,-401 86,-410 86,-410 86,-410 229.5,-430.01 299.49,-439.77\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"299.27,-443.27 309.66,-441.19 300.24,-436.34 299.27,-443.27\"/>\n",
"<text text-anchor=\"middle\" x=\"107.5\" y=\"-403.6\" font-family=\"Times,serif\" font-size=\"8.00\">10025/tcp</text>\n",
"</g>\n",
"<!-- webmail&#45;&gt;front -->\n",
"<g id=\"edge31\" class=\"edge\">\n",
"<title>webmail&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M174.72,-383.09C169.9,-391.97 165,-401 165,-401 165,-401 165,-410 165,-410 165,-410 249.24,-427.14 299.81,-437.43\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"299.25,-440.89 309.75,-439.45 300.64,-434.03 299.25,-440.89\"/>\n",
"<text text-anchor=\"middle\" x=\"186.5\" y=\"-403.6\" font-family=\"Times,serif\" font-size=\"8.00\">10143/tcp</text>\n",
"</g>\n",
"<!-- fetchmail -->\n",
"<g id=\"node11\" class=\"node\">\n",
"<title>fetchmail</title>\n",
"<polygon fill=\"#d3edea\" stroke=\"#d3edea\" points=\"654,-545 600,-545 600,-509 654,-509 654,-545\"/>\n",
"<text text-anchor=\"middle\" x=\"627\" y=\"-525.1\" font-family=\"arial\" font-size=\"8.00\">Fetchmail</text>\n",
"</g>\n",
"<!-- fetchmail&#45;&gt;front -->\n",
"<g id=\"edge39\" class=\"edge\">\n",
"<title>fetchmail&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M599.96,-516.78C562.13,-503.87 498,-482 498,-482 498,-482 421.83,-465.44 374.28,-455.1\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"374.83,-451.64 364.31,-452.94 373.34,-458.48 374.83,-451.64\"/>\n",
"<text text-anchor=\"middle\" x=\"535.5\" y=\"-484.6\" font-family=\"Times,serif\" font-size=\"8.00\">25/tcp</text>\n",
"</g>\n",
"<!-- fetchmail&#45;&gt;front -->\n",
"<g id=\"edge40\" class=\"edge\">\n",
"<title>fetchmail&#45;&gt;front</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M599.87,-509.87C578.75,-497.31 553,-482 553,-482 553,-482 436.36,-463.1 374.51,-453.08\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"374.79,-449.58 364.36,-451.43 373.67,-456.49 374.79,-449.58\"/>\n",
"<text text-anchor=\"middle\" x=\"585.5\" y=\"-484.6\" font-family=\"Times,serif\" font-size=\"8.00\">2525/tcp</text>\n",
"</g>\n",
"<!-- fetchmail&#45;&gt;admin -->\n",
"<g id=\"edge38\" class=\"edge\">\n",
"<title>fetchmail&#45;&gt;admin</title>\n",
"<path fill=\"none\" stroke=\"black\" d=\"M622.57,-508.79C609.83,-459.28 574,-320 574,-320 574,-320 567.2,-314.71 558.88,-308.24\"/>\n",
"<polygon fill=\"black\" stroke=\"black\" points=\"561.02,-305.47 550.97,-302.09 556.72,-310.99 561.02,-305.47\"/>\n",
"<text text-anchor=\"middle\" x=\"614.5\" y=\"-403.6\" font-family=\"Times,serif\" font-size=\"8.00\">8080/tcp</text>\n",
"</g>\n",
"</g>\n",
"</svg>\n"
],
"text/plain": [
"<graphviz.sources.Source at 0x7f2c4e69e690>"
]
},
"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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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")

View File

@@ -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:

View File

@@ -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 %}

View File

@@ -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 }}

View File

@@ -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/<uid>", methods=["GET"])
@root_bp.route("/setup/<uid>", methods=["GET"])

2
setup/static/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -2,51 +2,17 @@
var token = $("#api_token").val();
$(document).ready(function() {
if ($("#webmail").val() == 'none') {
$("#webmail_path").hide();
$("#webmail_path").val("");
} else {
$("#webmail_path").show();
$("#webmail_path").val("/webmail");
}
$("#webmail").click(function() {
if (this.value == 'none') {
$("#webmail_path").hide();
$("#webmail_path").val("");
} else {
$("#webmail_path").show();
$("#webmail_path").val("/webmail");
}
});
});
$(document).ready(function() {
if ($('#admin').prop('checked')) {
$("#admin_path").show();
$("#admin_path").val("/admin");
}
$("#admin").change(function() {
if ($(this).is(":checked")) {
$("#admin_path").show();
$("#admin_path").val("/admin");
} else {
$("#admin_path").hide();
$("#admin_path").val("");
}
});
$("#no_java_script").hide();
$("#container").show();
});
$(document).ready(function() {
if ($('#api_enabled').prop('checked')) {
$("#api_path").show();
$("#api_path").val("/api")
$("#api_token").show();
$("#api_token").prop('required',true);
$("#api_token").val(token);
$("#api_token_label").show();
} else {
$("#api_path").hide();
$("#api_path").val("")
$("#api_token").hide();
$("#api_token").prop('required',false);
$("#api_token").val("");
@@ -54,15 +20,11 @@ $(document).ready(function() {
}
$("#api_enabled").change(function() {
if ($(this).is(":checked")) {
$("#api_path").show();
$("#api_path").val("/api");
$("#api_token").show();
$("#api_token").prop('required',true);
$("#api_token").val(token)
$("#api_token_label").show();
} else {
$("#api_path").hide();
$("#api_path").val("")
$("#api_token").hide();
$("#api_token").prop('required',false);
$("#api_token").val("");
@@ -71,45 +33,6 @@ $(document).ready(function() {
});
});
$(document).ready(function() {
if ($("#database").val() == 'sqlite') {
$("#external_db").hide();
} else {
$("#external_db").show();
}
$("#webmail").click(function() {
if (this.value == 'roundcube') {
$("#db_flavor_rc_sel").show();
} else {
$("#db_flavor_rc_sel").hide();
$("#roundcube_db_user,#roundcube_db_pw,#roundcube_db_url,#roundcube_db_name").prop('required',false);
}
});
$("#database").click(function() {
if (this.value == 'sqlite') {
$("#external_db").hide();
$("#db_user,#db_pw,#db_url,#db_name").prop('required',false);
$("#roundcube_db_user,#roundcube_db_pw,#roundcube_db_url,#roundcube_db_name").prop('required',false);
} else {
$("#external_db").show();
$("#db_user,#db_pw,#db_url,#db_name").prop('required',true);
}
});
$("#database_rc").click(function() {
if (this.value == 'sqlite'){
$("#roundcube_external_db").hide();
$("#roundcube_db_user,#roundcube_db_pw,#roundcube_db_url,#roundcube_db_name").prop('required',false);
}
else if ($("#webmail").val() == 'roundcube') {
$("#roundcube_external_db").show();
$("#roundcube_db_user,#roundcube_db_pw,#roundcube_db_url,#roundcube_db_name").prop('required',true);
}
});
});
$(document).ready(function() {
if ($('#enable_ipv6').prop('checked')) {
$("#ipv6").show();

View File

@@ -1,11 +1,18 @@
{% extends "bootstrap/base.html" %}
{% import "macros.html" as macros %}
{% from 'bootstrap/utils.html' import flashed_messages %}
{% block title %}Mailu setup{% endblock %}
{% block content %}
<div class="container">
<div id=no_java_script class="noscriptmsg">
JavaScript is not enabled or JavaScript files were blocked. The Mailu setup site does not function when JavaScript is disabled.
</div>
<div id="container" class="container" style="display:none;">
<h1>Mailu configuration</h1>
{{ flashed_messages() }}
<p>
Version
<select id=version_select onchange="window.location.href=this.value;" class="btn btn-primary dropdown-toggle">
@@ -27,4 +34,11 @@ For production scenarios we recommend to use the stable version.
</div>
<p></p>
{% endblock %}
{% block scripts %}
<script type="text/javascript" src="{{ url_for('static', filename='jquery.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='render.js') }}"></script>
{{super()}}
{% endblock %}

View File

@@ -9,7 +9,7 @@ bound to the internal IMAP and SMTP server for users to access their mailbox thr
the Web. By exposing a complex application such as a Webmail, you should be aware of
the security implications caused by such an increase of attack surface.<p>
<div class="form-group">
<label>Enable Web email client (and path to the Web email client)</label>
<label>Enable Web email client</label>
<br/>
<select class="btn btn-primary dropdown-toggle" name="webmail_type" id="webmail">
{% for webmailtype in ["none", "roundcube", "snappymail"] %}
@@ -17,9 +17,6 @@ the security implications caused by such an increase of attack surface.<p>
{% endfor %}
</select>
<p></p>
<div class="input-group">
<input class="form-control" type="text" name="webmail_path" id="webmail_path" style="display: none">
</div>
</div>
@@ -61,12 +58,16 @@ the security implications caused by such an increase of attack surface.<p>
Enable oletools
</label>
<i>Oletools scans documents in email attachements for malicious macros. It has a much lower memory footprint than a full-fledged anti-virus.</i>
<i>Oletools scans documents in email attachments for malicious macros. It has a much lower memory footprint than a full-fledged anti-virus.</i>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script type="text/javascript" src="{{ url_for('static', filename='render.js') }}"></script>
<div class="form-check form-check-inline">
<label class="form-check-label">
<input class="form-check-input" type="checkbox" name="tika_enabled" value="true">
Enable Tika
</label>
<i>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).</i>
</div>
{% endcall %}

View File

@@ -16,11 +16,10 @@ avoid generic all-interfaces addresses like <code>0.0.0.0</code> or <code>::</co
<div class="form-group">
<label>IPv4 listen address</label>
<!-- Validates IPv4 address -->
<input class="form-control" type="text" name="bind4" value="127.0.0.1"
pattern="^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$">
<label>Subnet of the docker network. This should not conflict with any networks to which your system is connected. (Internal and external!)</label>
<input class="form-control" type="text" name="subnet" required pattern="^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))$"
value="192.168.203.0/24">
<input class="form-control" type="text" name="bind4" value="127.0.0.1" >
<label>Subnet of the docker network. This should not conflict with any networks to which your system is connected. (Internal and external!)
Usually the format is '*.*.*.0/24'</label>
<input class="form-control" type="text" name="subnet" value="192.168.203.0/24">
</div>
<div class="form-check form-check-inline">
@@ -34,8 +33,7 @@ avoid generic all-interfaces addresses like <code>0.0.0.0</code> or <code>::</co
<p><span class="label label-danger">Read this:</span> Docker currently does not expose the IPv6 ports properly, as it does not interface with <code>ip6tables</code>. Read <a href="https://mailu.io/{{ version }}/faq.html#how-to-make-ipv6-work">FAQ section</a> and be <b>very careful</b>. We do <b>NOT</b> recommend that you enable this!</p>
<label>IPv6 listen address</label>
<!-- Validates IPv6 address -->
<input class="form-control" type="text" name="bind6" value="::1"
pattern="^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$">
<input class="form-control" type="text" name="bind6" value="::1" >
<label>Subnet of the docker network. This should not conflict with any networks to which your system is connected. (Internal and external!)</label>
<input class="form-control" type="text" name="subnet6" required value="{{ subnet6 }}:beef::/64">
</div>

View File

@@ -4,13 +4,13 @@
<div class="form-group">
<label>Mailu storage path: </label>
<!-- Validates path -->
<input class="form-control" type="text" name="root" value="/mailu" required pattern="^/[-_A-Za-z0-9]+(/[-_A-Za-z0-9]*)*">
<input class="form-control" type="text" name="root" value="/mailu" required pattern="^\/[\-_A-Za-z0-9\.]+(\/[\-_A-Za-z0-9\.]*)*">
</div>
<p>In the following sections we need to set the postmaster address. This is a combination of the <i>postmaster</i> local part and the <i>main mail domain</i>.
The <i>main mail domain</i> is also used as </i>"server display name"</i>. This is the way the SMTP server identifies itself when connecting to others.
The Postmaster will get an e-mail address &lt;postmaster&gt;@&lt;main_domain&gt;. 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.</p>
Or in plain English: if receivers start to classify your mail as spam, this postmaster will be informed.</p>
<div class="form-group">
<label>
@@ -60,8 +60,8 @@ Or in plain english: if receivers start to classify your mail as spam, this post
<div class="form-check form-check-inline">
<label class="form-check-label">
<input class="form-check-input" type="checkbox" name="disable_statistics" value="True">
Opt-out of statistics
<input class="form-check-input" type="checkbox" name="statistics_enabled" value="True">
Opt-in for anonymized statistics
</label>
</div>
@@ -82,8 +82,7 @@ manage your email domains, users, etc.</p>
<div class="form-group">
<input type="checkbox" name="admin_enabled" value="true" id="admin" checked>
<label>Enable the admin UI (and path to the admin UI)</label>
<input class="form-control" type="text" name="admin_path" id="admin_path" style="display: none">
<label>Enable the admin UI</label>
</div>
<p>The API interface is a RESTful API for changing the Mailu configuration.
@@ -93,14 +92,10 @@ manage your email domains, users, etc.</p>
<div class="form-group">
<input type="checkbox" name="api_enabled" value="true" id="api_enabled" >
<label>Enable the API (and path to the API)</label>
<input class="form-control" type="text" name="api_path" id="api_path" style="display: none">
<label>Enable the API</label>
<br>
<label name="api_token_label" id="api_token_label">API token</label>
<input class="form-control" type="text" name="api_token" id="api_token" style="display: none" value="{{ secret(32) }}">
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script type="text/javascript" src="{{ url_for('static', filename='render.js') }}"></script>
{% endcall %}

View File

@@ -49,7 +49,6 @@ group "default" {
"webmail",
"antivirus",
"fetchmail",
"resolver",
"traefik-certdumper",
@@ -207,15 +206,6 @@ target "webmail" {
# -----------------------------------------------------------------------------------------
# Optional images
# -----------------------------------------------------------------------------------------
target "antivirus" {
inherits = ["defaults"]
context = "optional/clamav/"
contexts = {
base = "docker-image://${DOCKER_ORG}/base:${MAILU_VERSION}"
}
tags = tag("clamav")
}
target "fetchmail" {
inherits = ["defaults"]
context = "optional/fetchmail/"

View File

@@ -45,7 +45,6 @@ group "default" {
"webmail",
"antivirus",
"fetchmail",
"resolver",
"traefik-certdumper",
@@ -201,15 +200,6 @@ target "webmail" {
# -----------------------------------------------------------------------------------------
# Optional images
# -----------------------------------------------------------------------------------------
target "antivirus" {
inherits = ["defaults"]
context = "optional/clamav/"
contexts = {
base = "target:base"
}
tags = tag("clamav")
}
target "fetchmail" {
inherits = ["defaults"]
context = "optional/fetchmail/"

View File

@@ -1,6 +1,10 @@
python3 tests/email_test.py message-virus "tests/compose/filters/eicar.com.txt"
if [ $? -eq 99 ]; then
exit 0
else
exit 1
if [ $? -ne 25 ]; then
exit 1
fi
python3 tests/email_test.py message-PUA "tests/compose/filters/PotentiallyUnwanted.exe_"
if [ $? -ne 25 ]; then
exit 1
fi
exit 0

View File

@@ -1,7 +1,7 @@
# GTUBE should be blocked, see https://rspamd.com/doc/gtube_patterns.html
python3 tests/email_test.py "XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X"
if [ $? -eq 25 ]; then
exit 0
else
if [ $? -ne 25 ]; then
exit 1
fi
exit 0

Binary file not shown.

View File

@@ -70,7 +70,7 @@ services:
hostname: oletools
restart: always
networks:
- noinet
- oletools
antispam:
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}rspamd:${MAILU_VERSION:-local}
@@ -78,21 +78,35 @@ services:
env_file: mailu.env
networks:
- default
- noinet
- oletools
- clamav
volumes:
- "/mailu/filter:/var/lib/rspamd"
- "/mailu/dkim:/dkim"
- "/mailu/overrides/rspamd:/etc/rspamd/override.d"
depends_on:
- front
- antivirus
- oletools
# Optional services
antivirus:
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}clamav:${MAILU_VERSION:-local}
image: clamav/clamav-debian:1.2.0-6
restart: always
env_file: mailu.env
logging:
driver: journald
options:
tag: mailu-clamav
networks:
- clamav
volumes:
- "/mailu/filter:/data"
- "/mailu/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
resolver:
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}unbound:${MAILU_VERSION:-local}
@@ -112,6 +126,8 @@ networks:
driver: default
config:
- subnet: 192.168.203.0/24
noinet:
clamav:
driver: bridge
oletools:
driver: bridge
internal: true

View File

@@ -21,19 +21,31 @@ if len(sys.argv) == 3:
part.add_header('Content-Disposition', "attachment; filename=%s" % ntpath.basename(sys.argv[2]))
msg.attach(part)
try:
smtp_server = smtplib.SMTP('localhost')
smtp_server.set_debuglevel(1)
smtp_server.connect('localhost', 587)
smtp_server.ehlo()
smtp_server.starttls()
smtp_server.ehlo()
smtp_server.login("admin@mailu.io", "password")
for i in range(5):
try:
smtp_server = smtplib.SMTP('localhost')
smtp_server.set_debuglevel(1)
smtp_server.connect('localhost', 587)
smtp_server.ehlo()
smtp_server.starttls()
smtp_server.ehlo()
smtp_server.login("admin@mailu.io", "password")
smtp_server.sendmail("admin@mailu.io", "user@mailu.io", msg.as_string())
smtp_server.quit()
except:
sys.exit(25)
smtp_server.sendmail("admin@mailu.io", "user@mailu.io", msg.as_string())
smtp_server.quit()
except smtplib.SMTPRecipientsRefused:
sys.exit(25)
except smtplib.SMTPDataError as e:
if e.smtp_code == 451:
print(f"Not ready attempt {i}")
time.sleep(5)
continue
if e.smtp_code >= 500 and e.smtp_code <600:
sys.exit(25)
sys.exit(2525)
except:
sys.exit(2525)
break
time.sleep(30)

View File

@@ -0,0 +1 @@
Switch to upstream's clamav image

View File

@@ -0,0 +1 @@
Enhance RESTful API user retrieval with quota used bytes. This is the current size of the user's email box in bytes.

View File

@@ -0,0 +1,5 @@
Setup:
Regular expression for checking the Mailu storage path was invalid.
Added checks to make sure JavaScript is enabled and that all JS files could be loaded. The setup site malfunctions if this is not the case.
Added server side validation of entered values in setup.
Simplified setup by removing the settings for configuring the WEB_* settings. Advanced users can still modify mailu.env.

View File

@@ -0,0 +1 @@
Upgrade dovecot to ensure we can proxy ipv6 via XCLIENT.

View File

@@ -0,0 +1 @@
fix fetchmail when used with POP3: disregard "folders"

View File

@@ -0,0 +1,3 @@
Upgrade to alpine 3.18.4: this will fix a bug whereby musl wasn't retrying using TCP when it received truncated DNS replies from its upstream. In practice, this has been seen in the wild when postfix complains of:
"Host or domain name not found. Name service error for name=outlook-com.olc.protection.outlook.com type=AAAA: Host found but no data record of requested type"

View File

@@ -0,0 +1 @@
Add Persian (aka Farsi) Translation

View File

@@ -0,0 +1 @@
Add ukrainian translation

View File

@@ -0,0 +1,2 @@
forbidden_file_extension.map could not be overridden. This file can be overriden to tweak with file extensions are allowed.
The instructions on https://mailu.io/master/antispam.html#can-i-change-the-list-of-authorized-file-attachments work again.

View File

@@ -0,0 +1,4 @@
Fixed log filter not filtering out log messages for dovecot/nginx/postfix.
Fixed postfix not logging to standard out.
Fixed not all containers logging to journald.
Removed POSTFIX_LOG_FILE functionality. Added documentation on how to achieve the same (log to file) via journald & rsyslogd (see new FAQ entry 'How can I view and export the logs of a Mailu container?').

View File

@@ -0,0 +1 @@
Upgrade webmails: roundcube 1.6.3, rcmcarddav 5.1.0, snappymail 2.28.4

View File

@@ -0,0 +1 @@
Add Traditional Chinese translation

View File

@@ -0,0 +1 @@
Upgrade to snuffleupagus 0.10.0

View File

@@ -0,0 +1 @@
Remove the version pinning on hardened malloc

View File

@@ -0,0 +1,3 @@
Update hardened malloc as the original package is not available from alpine anymore.
The newer version of hardened malloc requires AVX2: Disable it by default at startup and hint in the logs when it should be enabled instead.
Upgrade snappymail to v2.29.1

View File

@@ -0,0 +1 @@
Fix letsencrypt on master

Some files were not shown because too many files have changed in this diff Show More