From 8f86ffc6fd414c458ae3ee002023ed620e120b06 Mon Sep 17 00:00:00 2001 From: Dimitri Huisman Date: Mon, 22 Jan 2024 10:44:30 +0000 Subject: [PATCH 01/58] Fix #3113. RESTful API was not correctly documented. --- core/admin/mailu/api/v1/alias.py | 14 +++++----- core/admin/mailu/api/v1/domain.py | 40 +++++++++++++++++------------ core/admin/mailu/api/v1/relay.py | 17 ++++++------ core/admin/mailu/api/v1/token.py | 23 +++++++++-------- core/admin/mailu/api/v1/user.py | 15 ++++++----- towncrier/newsfragments/3113.bugfix | 1 + 6 files changed, 61 insertions(+), 49 deletions(-) create mode 100644 towncrier/newsfragments/3113.bugfix diff --git a/core/admin/mailu/api/v1/alias.py b/core/admin/mailu/api/v1/alias.py index 600ccc04..a40295a8 100644 --- a/core/admin/mailu/api/v1/alias.py +++ b/core/admin/mailu/api/v1/alias.py @@ -27,7 +27,7 @@ class Aliases(Resource): @alias.doc(security='Bearer') @common.api_token_authorization def get(self): - """ List aliases """ + """ List all aliases """ return models.Alias.query.all() @alias.doc('create_alias') @@ -63,7 +63,7 @@ class Alias(Resource): @alias.doc(security='Bearer') @common.api_token_authorization def get(self, alias): - """ Find alias """ + """ Look up the specified alias """ alias_found = models.Alias.query.filter_by(email = alias).first() if alias_found is None: return { 'code': 404, 'message': f'Alias {alias} cannot be found'}, 404 @@ -78,7 +78,7 @@ class Alias(Resource): @alias.doc(security='Bearer') @common.api_token_authorization def patch(self, alias): - """ Update alias """ + """ Update the specfied alias """ data = api.payload alias_found = models.Alias.query.filter_by(email = alias).first() if alias_found is None: @@ -99,7 +99,7 @@ class Alias(Resource): @alias.doc(security='Bearer') @common.api_token_authorization def delete(self, alias): - """ Delete alias """ + """ Delete the specified alias """ alias_found = models.Alias.query.filter_by(email = alias).first() if alias_found is None: return { 'code': 404, 'message': f'Alias {alias} cannot be found'}, 404 @@ -110,12 +110,12 @@ class Alias(Resource): @alias.route('/destination/') class AliasWithDest(Resource): @alias.doc('find_alias_filter_domain') - @alias.response(200, 'Success', alias_fields) + @alias.marshal_with(alias_fields, code=200, description='Success' ,as_list=True, skip_none=True, mask=None) @alias.response(404, 'Alias or domain not found', response_fields) @alias.doc(security='Bearer') @common.api_token_authorization def get(self, domain): - """ Find aliases of domain """ + """ Look up the aliases of the specified domain """ domain_found = models.Domain.query.filter_by(name=domain).first() if domain_found is None: return { 'code': 404, 'message': f'Domain {domain} cannot be found'}, 404 @@ -123,4 +123,4 @@ class AliasWithDest(Resource): if aliases_found.count == 0: return { 'code': 404, 'message': f'No alias can be found for domain {domain}'}, 404 else: - return marshal(aliases_found, alias_fields), 200 + return marshal(aliases_found, alias_fields, as_list=True), 200 diff --git a/core/admin/mailu/api/v1/domain.py b/core/admin/mailu/api/v1/domain.py index c5f98530..08ca9475 100644 --- a/core/admin/mailu/api/v1/domain.py +++ b/core/admin/mailu/api/v1/domain.py @@ -81,7 +81,7 @@ class Domains(Resource): @dom.doc(security='Bearer') @common.api_token_authorization def get(self): - """ List domains """ + """ List all domains """ return models.Domain.query.all() @dom.doc('create_domain') @@ -131,12 +131,13 @@ class Domains(Resource): class Domain(Resource): @dom.doc('find_domain') - @dom.response(200, 'Success', domain_fields) + @dom.marshal_with(domain_fields, code=200, description='Success', as_list=False, skip_none=True, mask=None) + @dom.response(400, 'Input validation exception', response_fields) @dom.response(404, 'Domain not found', response_fields) @dom.doc(security='Bearer') @common.api_token_authorization def get(self, domain): - """ Find domain by name """ + """ Look up the specified domain """ if not validators.domain(domain): return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400 domain_found = models.Domain.query.get(domain) @@ -153,7 +154,7 @@ class Domain(Resource): @dom.doc(security='Bearer') @common.api_token_authorization def patch(self, domain): - """ Update an existing domain """ + """ Update the specified domain """ if not validators.domain(domain): return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400 domain_found = models.Domain.query.get(domain) @@ -197,7 +198,7 @@ class Domain(Resource): @dom.doc(security='Bearer') @common.api_token_authorization def delete(self, domain): - """ Delete domain """ + """ Delete the specified domain """ if not validators.domain(domain): return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400 domain_found = models.Domain.query.get(domain) @@ -216,7 +217,7 @@ class Domain(Resource): @dom.doc(security='Bearer') @common.api_token_authorization def post(self, domain): - """ Generate new DKIM/DMARC keys for domain """ + """ Generate new DKIM/DMARC keys for the specified domain """ if not validators.domain(domain): return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400 domain_found = models.Domain.query.get(domain) @@ -229,13 +230,13 @@ class Domain(Resource): @dom.route('//manager') class Manager(Resource): @dom.doc('list_managers') - @dom.marshal_with(manager_fields, as_list=True, skip_none=True, mask=None) + @dom.marshal_with(manager_fields, code=200, description='Success', as_list=True, skip_none=True, mask=None) @dom.response(400, 'Input validation exception', response_fields) @dom.response(404, 'domain not found', response_fields) @dom.doc(security='Bearer') @common.api_token_authorization def get(self, domain): - """ List managers of domain """ + """ List all managers of the specified domain """ if not validators.domain(domain): return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400 if not domain: @@ -252,7 +253,7 @@ class Manager(Resource): @dom.doc(security='Bearer') @common.api_token_authorization def post(self, domain): - """ Create a new domain manager """ + """ Create a new domain manager for the specified domain """ data = api.payload if not validators.email(data['user_email']): return {'code': 400, 'message': f'Invalid email address {data["user_email"]}'}, 400 @@ -273,12 +274,13 @@ class Manager(Resource): @dom.route('//manager/') class Domain(Resource): @dom.doc('find_manager') - @dom.response(200, 'Success', manager_fields) + @dom.marshal_with(manager_fields, code=200, description='Success', as_list=False, skip_none=True, mask=None) + @dom.response(400, 'Input validation exception', response_fields) @dom.response(404, 'Manager not found', response_fields) @dom.doc(security='Bearer') @common.api_token_authorization def get(self, domain, email): - """ Find manager by email address """ + """ Look up the specified manager of the specified domain """ if not validators.email(email): return {'code': 400, 'message': f'Invalid email address {email}'}, 400 if not validators.domain(domain): @@ -304,6 +306,7 @@ class Domain(Resource): @dom.doc(security='Bearer') @common.api_token_authorization def delete(self, domain, email): + """ Delete the specified manager of the specified domain """ if not validators.email(email): return {'code': 400, 'message': f'Invalid email address {email}'}, 400 if not validators.domain(domain): @@ -324,13 +327,13 @@ class Domain(Resource): @dom.route('//users') class User(Resource): @dom.doc('list_user_domain') - @dom.marshal_with(user.user_fields_get, as_list=True, skip_none=True, mask=None) + @dom.marshal_with(user.user_fields_get, code=200, description='Success', as_list=True, skip_none=True, mask=None) @dom.response(400, 'Input validation exception', response_fields) @dom.response(404, 'Domain not found', response_fields) @dom.doc(security='Bearer') @common.api_token_authorization def get(self, domain): - """ List users from domain """ + """ List all the users from the specified domain """ if not validators.domain(domain): return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400 domain_found = models.Domain.query.get(domain) @@ -346,7 +349,7 @@ class Alternatives(Resource): @alt.doc(security='Bearer') @common.api_token_authorization def get(self): - """ List alternatives """ + """ List all alternatives """ return models.Alternative.query.all() @@ -359,7 +362,7 @@ class Alternatives(Resource): @alt.doc(security='Bearer') @common.api_token_authorization def post(self): - """ Create new alternative (for domain) """ + """ Create a new alternative (for domain) """ data = api.payload if not validators.domain(data['name']): return { 'code': 400, 'message': f'Alternative domain {data["name"]} is not a valid domain'}, 400 @@ -380,9 +383,12 @@ class Alternatives(Resource): class Alternative(Resource): @alt.doc('find_alternative') @alt.doc(security='Bearer') + @alt.marshal_with(alternative_fields, code=200, description='Success' ,as_list=True, skip_none=True, mask=None) + @alt.response(400, 'Input validation exception', response_fields) + @alt.response(404, 'Alternative not found or missing', response_fields) @common.api_token_authorization def get(self, alt): - """ Find alternative (of domain) """ + """ Look up the specified alternative (of domain) """ if not validators.domain(alt): return { 'code': 400, 'message': f'Alternative domain {alt} is not a valid domain'}, 400 alternative = models.Alternative.query.filter_by(name=alt).first() @@ -398,7 +404,7 @@ class Alternative(Resource): @alt.doc(security='Bearer') @common.api_token_authorization def delete(self, alt): - """ Delete alternative (for domain) """ + """ Delete the specified alternative (for domain) """ if not validators.domain(alt): return { 'code': 400, 'message': f'Alternative domain {alt} is not a valid domain'}, 400 alternative = models.Alternative.query.filter_by(name=alt).scalar() diff --git a/core/admin/mailu/api/v1/relay.py b/core/admin/mailu/api/v1/relay.py index 356f8426..c250d9d8 100644 --- a/core/admin/mailu/api/v1/relay.py +++ b/core/admin/mailu/api/v1/relay.py @@ -27,22 +27,22 @@ class Relays(Resource): @relay.doc(security='Bearer') @common.api_token_authorization def get(self): - "List relays" + "List all relays" return models.Relay.query.all() @relay.doc('create_relay') @relay.expect(relay_fields) @relay.response(200, 'Success', response_fields) - @relay.response(400, 'Input validation exception') + @relay.response(400, 'Input validation exception', response_fields) @relay.response(409, 'Duplicate relay', response_fields) @relay.doc(security='Bearer') @common.api_token_authorization def post(self): - """ Create relay """ + """ Create a new relay """ data = api.payload - if not validators.domain(name): - return { 'code': 400, 'message': f'Relayed domain {name} is not a valid domain'}, 400 + if not validators.domain(data['name']): + return { 'code': 400, 'message': f'Relayed domain {data["name"]} is not a valid domain'}, 400 if common.fqdn_in_use(data['name']): return { 'code': 409, 'message': f'Duplicate domain {data["name"]}'}, 409 @@ -58,12 +58,13 @@ class Relays(Resource): @relay.route('/') class Relay(Resource): @relay.doc('find_relay') + @relay.marshal_with(relay_fields, code=200, description='Success', as_list=False, skip_none=True, mask=None) @relay.response(400, 'Input validation exception', response_fields) @relay.response(404, 'Relay not found', response_fields) @relay.doc(security='Bearer') @common.api_token_authorization def get(self, name): - """ Find relay """ + """ Look up the specified relay """ if not validators.domain(name): return { 'code': 400, 'message': f'Relayed domain {name} is not a valid domain'}, 400 @@ -81,7 +82,7 @@ class Relay(Resource): @relay.doc(security='Bearer') @common.api_token_authorization def patch(self, name): - """ Update relay """ + """ Update the specified relay """ data = api.payload if not validators.domain(name): @@ -107,7 +108,7 @@ class Relay(Resource): @relay.doc(security='Bearer') @common.api_token_authorization def delete(self, name): - """ Delete relay """ + """ Delete the specified relay """ if not validators.domain(name): return { 'code': 400, 'message': f'Relayed domain {name} is not a valid domain'}, 400 relay_found = models.Relay.query.filter_by(name=name).first() diff --git a/core/admin/mailu/api/v1/token.py b/core/admin/mailu/api/v1/token.py index 0f2b5b7a..70276239 100644 --- a/core/admin/mailu/api/v1/token.py +++ b/core/admin/mailu/api/v1/token.py @@ -47,13 +47,13 @@ class Tokens(Resource): @token.doc(security='Bearer') @common.api_token_authorization def get(self): - """List tokens""" + """List all 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.marshal_with(token_user_post_response, code=200, description='Success', as_list=False, skip_none=True, mask=None) + @token.response(400, 'Input validation exception', response_fields) @token.response(409, 'Duplicate relay', response_fields) @token.doc(security='Bearer') @common.api_token_authorization @@ -92,11 +92,13 @@ class Tokens(Resource): @token.route('user/') class Token(Resource): @token.doc('find_tokens_of_user') - @token.marshal_with(token_user_fields, as_list=True, skip_none=True, mask=None) + @token.marshal_with(token_user_fields, code=200, description='Success', as_list=True, skip_none=True, mask=None) + @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 get(self, email): - "Find tokens of user" + """ Look up all the tokens of the specified 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) @@ -108,12 +110,12 @@ class Token(Resource): @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(400, 'Input validation exception', response_fields) @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""" + """ Create a new token for the specified 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 @@ -144,11 +146,12 @@ class Token(Resource): @token.route('/') class Token(Resource): @token.doc('find_token') - @token.marshal_with(token_user_fields, as_list=True, skip_none=True, mask=None) + @token.marshal_with(token_user_fields, code=200, description='Success', as_list=False, skip_none=True, mask=None) + @token.response(404, 'Token not found', response_fields) @token.doc(security='Bearer') @common.api_token_authorization def get(self, token_id): - "Find token" + "Find the specified 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 @@ -161,7 +164,7 @@ class Token(Resource): @token.doc(security='Bearer') @common.api_token_authorization def delete(self, token_id): - """ Delete token """ + """ Delete the specified 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 diff --git a/core/admin/mailu/api/v1/user.py b/core/admin/mailu/api/v1/user.py index 441845c2..33aef74c 100644 --- a/core/admin/mailu/api/v1/user.py +++ b/core/admin/mailu/api/v1/user.py @@ -87,23 +87,23 @@ user_fields_put = api.model('UserUpdate', { @user.route('') class Users(Resource): - @user.doc('list_users') + @user.doc('list_user') @user.marshal_with(user_fields_get, as_list=True, skip_none=True, mask=None) @user.doc(security='Bearer') @common.api_token_authorization def get(self): - "List users" + "List all users" return models.User.query.all() @user.doc('create_user') @user.expect(user_fields_post) @user.response(200, 'Success', response_fields) - @user.response(400, 'Input validation exception') + @user.response(400, 'Input validation exception', response_fields) @user.response(409, 'Duplicate user', response_fields) @user.doc(security='Bearer') @common.api_token_authorization def post(self): - """ Create user """ + """ Create a new user """ data = api.payload if not validators.email(data['email']): return { 'code': 400, 'message': f'Provided email address {data["email"]} is not a valid email address'}, 400 @@ -168,12 +168,13 @@ class Users(Resource): @user.route('/') class User(Resource): @user.doc('find_user') + @user.marshal_with(user_fields_get, code=200, description='Success', as_list=False, skip_none=True, mask=None) @user.response(400, 'Input validation exception', response_fields) @user.response(404, 'User not found', response_fields) @user.doc(security='Bearer') @common.api_token_authorization def get(self, email): - """ Find user """ + """ Look up the specified user """ if not validators.email(email): return { 'code': 400, 'message': f'Provided email address {email} is not a valid email address'}, 400 @@ -191,7 +192,7 @@ class User(Resource): @user.doc(security='Bearer') @common.api_token_authorization def patch(self, email): - """ Update user """ + """ Update the specified user """ data = api.payload if not validators.email(email): return { 'code': 400, 'message': f'Provided email address {data["email"]} is not a valid email address'}, 400 @@ -258,7 +259,7 @@ class User(Resource): @user.doc(security='Bearer') @common.api_token_authorization def delete(self, email): - """ Delete user """ + """ Delete the specified user """ if not validators.email(email): return { 'code': 400, 'message': f'Provided email address {email} is not a valid email address'}, 400 diff --git a/towncrier/newsfragments/3113.bugfix b/towncrier/newsfragments/3113.bugfix new file mode 100644 index 00000000..4d84e335 --- /dev/null +++ b/towncrier/newsfragments/3113.bugfix @@ -0,0 +1 @@ +Some RESTful API interfaces were incorrectly documented. \ No newline at end of file From 6627dd2924de5495e5dfb0b49e5d06f9f147c4b6 Mon Sep 17 00:00:00 2001 From: Dimitri Huisman Date: Wed, 24 Jan 2024 10:57:30 +0000 Subject: [PATCH 02/58] API: Create user did not handle exception of duplicate user --- .github/workflows/build_test_deploy.yml | 2 +- core/admin/mailu/api/v1/relay.py | 1 - core/admin/mailu/api/v1/token.py | 2 -- core/admin/mailu/api/v1/user.py | 5 ++++- tests/compose/core/mailu.env | 6 +----- 5 files changed, 6 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build_test_deploy.yml b/.github/workflows/build_test_deploy.yml index 9c2ff02d..ee95ae2d 100644 --- a/.github/workflows/build_test_deploy.yml +++ b/.github/workflows/build_test_deploy.yml @@ -418,7 +418,7 @@ jobs: strategy: fail-fast: false matrix: - target: ["core", "fetchmail", "filters", "webmail", "webdav"] + target: ["api", "core", "fetchmail", "filters", "webmail", "webdav"] time: ["2"] include: - target: "filters" diff --git a/core/admin/mailu/api/v1/relay.py b/core/admin/mailu/api/v1/relay.py index c250d9d8..d9542de6 100644 --- a/core/admin/mailu/api/v1/relay.py +++ b/core/admin/mailu/api/v1/relay.py @@ -78,7 +78,6 @@ class Relay(Resource): @relay.response(200, 'Success', response_fields) @relay.response(400, 'Input validation exception', response_fields) @relay.response(404, 'Relay not found', response_fields) - @relay.response(409, 'Duplicate relay', response_fields) @relay.doc(security='Bearer') @common.api_token_authorization def patch(self, name): diff --git a/core/admin/mailu/api/v1/token.py b/core/admin/mailu/api/v1/token.py index 70276239..041ff322 100644 --- a/core/admin/mailu/api/v1/token.py +++ b/core/admin/mailu/api/v1/token.py @@ -54,7 +54,6 @@ class Tokens(Resource): @token.expect(token_user_fields_post) @token.marshal_with(token_user_post_response, code=200, description='Success', as_list=False, skip_none=True, mask=None) @token.response(400, 'Input validation exception', response_fields) - @token.response(409, 'Duplicate relay', response_fields) @token.doc(security='Bearer') @common.api_token_authorization def post(self): @@ -111,7 +110,6 @@ class Token(Resource): @token.expect(token_user_fields_post2) @token.response(200, 'Success', token_user_post_response) @token.response(400, 'Input validation exception', response_fields) - @token.response(409, 'Duplicate relay', response_fields) @token.doc(security='Bearer') @common.api_token_authorization def post(self, email): diff --git a/core/admin/mailu/api/v1/user.py b/core/admin/mailu/api/v1/user.py index 33aef74c..c135cfbb 100644 --- a/core/admin/mailu/api/v1/user.py +++ b/core/admin/mailu/api/v1/user.py @@ -111,6 +111,10 @@ class Users(Resource): domain_found = models.Domain.query.get(domain_name) if not domain_found: return { 'code': 404, 'message': f'Domain {domain_name} does not exist'}, 404 + email_found = models.User.query.filter_by(email=data['email']).first() + if email_found: + return { 'code': 409, 'message': f'User {data["email"]} already exists'}, 409 + user_new = models.User(email=data['email']) if 'raw_password' in data: @@ -188,7 +192,6 @@ class User(Resource): @user.response(200, 'Success', response_fields) @user.response(400, 'Input validation exception', response_fields) @user.response(404, 'User not found', response_fields) - @user.response(409, 'Duplicate user', response_fields) @user.doc(security='Bearer') @common.api_token_authorization def patch(self, email): diff --git a/tests/compose/core/mailu.env b/tests/compose/core/mailu.env index e7456679..3d996aef 100644 --- a/tests/compose/core/mailu.env +++ b/tests/compose/core/mailu.env @@ -41,7 +41,7 @@ POSTMASTER=admin TLS_FLAVOR=cert # Authentication rate limit (per source IP address) -AUTH_RATELIMIT=10/minute;1000/hour +AUTH_RATELIMIT=10/minute;1000/hour # Opt-out of statistics, replace with "True" to opt out DISABLE_STATISTICS=False @@ -143,7 +143,3 @@ REAL_IP_FROM= # choose wether mailu bounces (no) or rejects (yes) mail when recipient is unknown (value: yes, no) REJECT_UNLISTED_RECIPIENT= -# Test for initial admin create -INITIAL_ADMIN_ACCOUNT=admin -INITIAL_ADMIN_DOMAIN=mailu.io -INITIAL_ADMIN_PW=FooBar From 26a8c2b6bb34f6ce0a8ece6f5fe4f9c63c9519b3 Mon Sep 17 00:00:00 2001 From: migs35323 <92784574+migs35323@users.noreply.github.com> Date: Mon, 12 Feb 2024 15:58:01 +0000 Subject: [PATCH 03/58] correction: config-export had wrong example. fixing the example command flag. --- docs/database.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/database.rst b/docs/database.rst index 54e5015a..e200555a 100644 --- a/docs/database.rst +++ b/docs/database.rst @@ -17,7 +17,7 @@ This means it is not possible to switch the database back-end used by roundcube To switch to a different database back-end: -1. Run config-export to export the configuration. E.g. `docker compose exec admin flask mailu config-export --secrets --output /data/mail-config.yml` +1. Run config-export to export the configuration. E.g. `docker compose exec admin flask mailu config-export --secrets --output-file /data/mail-config.yml` 2. Set up your new database server. Refer to the subsequent sections for tips for creating the database. 3. Modify the database settings (SQLAlchemy database URL) in mailu.env. Refer to the :ref:`configuration guide (link) ` for the exact settings. 4. Start your Mailu deployment. From b136c16f9603c6023c7b2981d066151c78863c51 Mon Sep 17 00:00:00 2001 From: Dimitri Huisman Date: Sat, 16 Mar 2024 15:54:09 +0000 Subject: [PATCH 04/58] Revert commit. Will add api testing in a later PR --- .github/workflows/build_test_deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_test_deploy.yml b/.github/workflows/build_test_deploy.yml index ee95ae2d..9c2ff02d 100644 --- a/.github/workflows/build_test_deploy.yml +++ b/.github/workflows/build_test_deploy.yml @@ -418,7 +418,7 @@ jobs: strategy: fail-fast: false matrix: - target: ["api", "core", "fetchmail", "filters", "webmail", "webdav"] + target: ["core", "fetchmail", "filters", "webmail", "webdav"] time: ["2"] include: - target: "filters" From 1f6907477120fcf6abf725de2675c354fb799d29 Mon Sep 17 00:00:00 2001 From: Dimitri Huisman Date: Sat, 16 Mar 2024 16:49:04 +0000 Subject: [PATCH 05/58] Undo unintended changes to this file --- tests/compose/core/mailu.env | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/compose/core/mailu.env b/tests/compose/core/mailu.env index 3d996aef..30ecd830 100644 --- a/tests/compose/core/mailu.env +++ b/tests/compose/core/mailu.env @@ -143,3 +143,7 @@ REAL_IP_FROM= # choose wether mailu bounces (no) or rejects (yes) mail when recipient is unknown (value: yes, no) REJECT_UNLISTED_RECIPIENT= +# Test for initial admin create +INITIAL_ADMIN_ACCOUNT=admin +INITIAL_ADMIN_DOMAIN=mailu.io +INITIAL_ADMIN_PW=FooBar From b6743019e886fec77f8c631641b5fa022ed68937 Mon Sep 17 00:00:00 2001 From: Dimitri Huisman Date: Mon, 18 Mar 2024 14:28:53 +0000 Subject: [PATCH 06/58] Address CVE-2024-23829 (CVE for aiohttp) --- core/base/requirements-prod.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/base/requirements-prod.txt b/core/base/requirements-prod.txt index 5bd0c5a1..20820040 100644 --- a/core/base/requirements-prod.txt +++ b/core/base/requirements-prod.txt @@ -1,5 +1,5 @@ aiodns==3.1.1 -aiohttp==3.9.1 +aiohttp==3.9.3 aiosignal==1.3.1 alembic==1.13.1 aniso8601==9.0.1 From 854e9b0a457d376dfb2968bc27728366783b55bd Mon Sep 17 00:00:00 2001 From: Dimitri Huisman Date: Mon, 18 Mar 2024 14:34:58 +0000 Subject: [PATCH 07/58] cli.rst contained wrong example --- docs/cli.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cli.rst b/docs/cli.rst index 88d8edc5..7c36dbef 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -162,7 +162,7 @@ Attributes explicitly specified in filters are automatically exported: there is .. code-block:: bash - $ docker compose exec admin flask mailu config-export --output mail-config.yml + $ docker compose exec admin flask mailu config-export --output-file mail-config.yml $ docker compose exec -T admin flask mailu config-export domain.dns_mx domain.dns_spf From a8cdd6ca9c4b130675f2b9b160e20de3c756abbe Mon Sep 17 00:00:00 2001 From: Dimitri Huisman Date: Mon, 18 Mar 2024 15:12:10 +0000 Subject: [PATCH 08/58] Update actions in CI github workflow files --- .github/workflows/build_test_deploy.yml | 72 ++++++++++++------------- .github/workflows/multiarch.yml | 2 +- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/.github/workflows/build_test_deploy.yml b/.github/workflows/build_test_deploy.yml index 9c2ff02d..39ee200e 100644 --- a/.github/workflows/build_test_deploy.yml +++ b/.github/workflows/build_test_deploy.yml @@ -92,7 +92,7 @@ jobs: matrix: ${{ steps.targets.outputs.matrix }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Create matrix id: targets run: | @@ -114,7 +114,7 @@ jobs: contents: read packages: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Retrieve global variables shell: bash run: | @@ -123,13 +123,13 @@ jobs: echo "PINNED_MAILU_VERSION=${{ inputs.pinned_mailu_version }}" >> $GITHUB_ENV echo "DOCKER_ORG=${{ inputs.docker_org }}" >> $GITHUB_ENV - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - uses: crazy-max/ghaction-github-runtime@v2 + uses: docker/setup-qemu-action@v3 + - uses: crazy-max/ghaction-github-runtime@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Helper to convert docker org to lowercase id: string - uses: ASzc/change-string-case-action@v5 + uses: ASzc/change-string-case-action@v6 with: string: ${{ github.repository_owner }} - name: Get uuid @@ -148,7 +148,7 @@ jobs: DOCKER_LOGIN: ${{ secrets.Docker_Login }} DOCKER_PASSW: ${{ secrets.Docker_Password }} BUILDX_NO_DEFAULT_ATTESTATIONS: 1 - uses: nick-fields/retry@v2 + uses: nick-fields/retry@v3 with: timeout_minutes: 20 retry_wait_seconds: 30 @@ -189,7 +189,7 @@ jobs: contents: read packages: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Retrieve global variables shell: bash run: | @@ -198,11 +198,11 @@ jobs: echo "PINNED_MAILU_VERSION=${{ inputs.pinned_mailu_version }}" >> $GITHUB_ENV echo "DOCKER_ORG=${{ inputs.docker_org }}" >> $GITHUB_ENV - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - uses: crazy-max/ghaction-github-runtime@v2 + uses: docker/setup-qemu-action@v3 + - uses: crazy-max/ghaction-github-runtime@v3 - name: Helper to convert docker org to lowercase id: string - uses: ASzc/change-string-case-action@v5 + uses: ASzc/change-string-case-action@v6 with: string: ${{ github.repository_owner }} #This is to prevent to shared runners from generating the same uuid @@ -222,7 +222,7 @@ jobs: DOCKER_LOGIN2: ${{ secrets.Docker_Login2 }} DOCKER_PASSW2: ${{ secrets.Docker_Password2 }} BUILDX_NO_DEFAULT_ATTESTATIONS: 1 - uses: nick-fields/retry@v2 + uses: nick-fields/retry@v3 with: timeout_minutes: 30 retry_wait_seconds: 30 @@ -266,7 +266,7 @@ jobs: contents: read packages: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Retrieve global variables shell: bash run: | @@ -275,13 +275,13 @@ jobs: echo "PINNED_MAILU_VERSION=${{ inputs.pinned_mailu_version }}" >> $GITHUB_ENV echo "DOCKER_ORG=${{ inputs.docker_org }}" >> $GITHUB_ENV - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - uses: crazy-max/ghaction-github-runtime@v2 + uses: docker/setup-qemu-action@v3 + - uses: crazy-max/ghaction-github-runtime@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Helper to convert docker org to lowercase id: string - uses: ASzc/change-string-case-action@v5 + uses: ASzc/change-string-case-action@v6 with: string: ${{ github.repository_owner }} - name: Get uuid @@ -300,7 +300,7 @@ jobs: DOCKER_LOGIN: ${{ secrets.Docker_Login }} DOCKER_PASSW: ${{ secrets.Docker_Password }} BUILDX_NO_DEFAULT_ATTESTATIONS: 1 - uses: nick-fields/retry@v2 + uses: nick-fields/retry@v3 with: timeout_minutes: 20 retry_wait_seconds: 30 @@ -344,7 +344,7 @@ jobs: contents: read packages: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Retrieve global variables shell: bash run: | @@ -353,11 +353,11 @@ jobs: echo "PINNED_MAILU_VERSION=${{ inputs.pinned_mailu_version }}" >> $GITHUB_ENV echo "DOCKER_ORG=${{ inputs.docker_org }}" >> $GITHUB_ENV - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - uses: crazy-max/ghaction-github-runtime@v2 + uses: docker/setup-qemu-action@v3 + - uses: crazy-max/ghaction-github-runtime@v3 - name: Helper to convert docker org to lowercase id: string - uses: ASzc/change-string-case-action@v5 + uses: ASzc/change-string-case-action@v6 with: string: ${{ github.repository_owner }} #This is to prevent to shared runners from generating the same uuid @@ -377,7 +377,7 @@ jobs: DOCKER_LOGIN2: ${{ secrets.Docker_Login2 }} DOCKER_PASSW2: ${{ secrets.Docker_Password2 }} BUILDX_NO_DEFAULT_ATTESTATIONS: 1 - uses: nick-fields/retry@v2 + uses: nick-fields/retry@v3 with: timeout_minutes: 30 retry_wait_seconds: 30 @@ -427,7 +427,7 @@ jobs: - target: "filters" time: "2" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Retrieve global variables shell: bash run: | @@ -436,10 +436,10 @@ jobs: echo "PINNED_MAILU_VERSION=${{ inputs.pinned_mailu_version }}" >> $GITHUB_ENV echo "DOCKER_ORG=${{ inputs.docker_org }}" >> $GITHUB_ENV - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - uses: crazy-max/ghaction-github-runtime@v2 + uses: docker/setup-qemu-action@v3 + - uses: crazy-max/ghaction-github-runtime@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to GitHub Container Registry uses: docker/login-action@v2 with: @@ -448,7 +448,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Helper to convert docker org to lowercase id: string - uses: ASzc/change-string-case-action@v5 + uses: ASzc/change-string-case-action@v6 with: string: ${{ github.repository_owner }} - name: Install python packages @@ -476,7 +476,7 @@ jobs: matrix: target: ["setup", "docs", "fetchmail", "webmail", "admin", "traefik-certdumper", "radicale", "rspamd", "oletools", "postfix", "dovecot", "unbound", "nginx"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Retrieve global variables shell: bash run: | @@ -485,10 +485,10 @@ jobs: echo "PINNED_MAILU_VERSION=${{ inputs.pinned_mailu_version }}" >> $GITHUB_ENV echo "DOCKER_ORG=${{ inputs.docker_org }}" >> $GITHUB_ENV - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - uses: crazy-max/ghaction-github-runtime@v2 + uses: docker/setup-qemu-action@v3 + - uses: crazy-max/ghaction-github-runtime@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to GitHub Container Registry uses: docker/login-action@v2 with: @@ -497,7 +497,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Helper to convert docker org to lowercase id: string - uses: ASzc/change-string-case-action@v5 + uses: ASzc/change-string-case-action@v6 with: string: ${{ github.repository_owner }} - name: Push multiarch image to Github (ghcr.io) @@ -539,13 +539,13 @@ jobs: needs: - deploy steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: # fetch-depth 0 is required to also retrieve all tags. fetch-depth: 0 - # A bug in actions/checkout@v3 results in all files having mtime of the job running. + # A bug in actions/checkout@v4 results in all files having mtime of the job running. - name: Restore Timestamps - uses: chetan/git-restore-mtime-action@v1 + uses: chetan/git-restore-mtime-action@v2 - name: Retrieve global variables shell: bash run: | diff --git a/.github/workflows/multiarch.yml b/.github/workflows/multiarch.yml index b65fc6d9..3185591d 100644 --- a/.github/workflows/multiarch.yml +++ b/.github/workflows/multiarch.yml @@ -29,7 +29,7 @@ jobs: DEPLOY: ${{ env.DEPLOY }} RELEASE: ${{ env.RELEASE }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: # fetch-depth 0 is required to also retrieve all tags. fetch-depth: 0 From df6dcf0d44cc1e750ca350f8541871c6abe403e1 Mon Sep 17 00:00:00 2001 From: Dimitri Huisman Date: Mon, 18 Mar 2024 15:24:43 +0000 Subject: [PATCH 09/58] update docker/login-action@v2 to docker/login-action@v3 --- .github/workflows/build_test_deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_test_deploy.yml b/.github/workflows/build_test_deploy.yml index 39ee200e..71e8d099 100644 --- a/.github/workflows/build_test_deploy.yml +++ b/.github/workflows/build_test_deploy.yml @@ -441,7 +441,7 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to GitHub Container Registry - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -490,7 +490,7 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to GitHub Container Registry - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} From 1d40ba635d1b318c039f7fc2b081d5cd3a4011b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nico=20Winkelstr=C3=A4ter?= Date: Wed, 20 Mar 2024 16:30:48 +0100 Subject: [PATCH 10/58] Change class from `warning` to `text-muted`. `warning` ist not available in AdminLTE3 anymore. --- core/admin/mailu/ui/templates/user/list.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/admin/mailu/ui/templates/user/list.html b/core/admin/mailu/ui/templates/user/list.html index 45a9cb10..7796359e 100644 --- a/core/admin/mailu/ui/templates/user/list.html +++ b/core/admin/mailu/ui/templates/user/list.html @@ -28,7 +28,7 @@ {%- for user in domain.users %} - +   From 2558ae3bc943207076bda476955968534955097d Mon Sep 17 00:00:00 2001 From: Dimitri Huisman Date: Fri, 22 Mar 2024 15:01:37 +0000 Subject: [PATCH 11/58] Add automatic tests for RESTful API. Fix all remaining issues that I could find with the API. --- .github/workflows/build_test_deploy.yml | 2 +- core/admin/mailu/api/v1/alias.py | 47 +++++- core/admin/mailu/api/v1/domain.py | 55 ++++--- core/admin/mailu/api/v1/relay.py | 7 +- core/admin/mailu/api/v1/token.py | 91 +++++++++-- core/admin/mailu/api/v1/user.py | 19 ++- tests/compose/api/00_create_users.sh | 86 ++++++++++ tests/compose/api/01_test_user_interfaces.sh | 80 ++++++++++ .../compose/api/02_test_domain_interfaces.sh | 145 +++++++++++++++++ tests/compose/api/03_test_token_interfaces.sh | 107 +++++++++++++ tests/compose/api/04_test_relay_interfaces.sh | 98 ++++++++++++ tests/compose/api/05_test_alias_interfaces.sh | 111 +++++++++++++ tests/compose/api/docker-compose.yml | 112 +++++++++++++ tests/compose/api/mailu.env | 151 ++++++++++++++++++ 14 files changed, 1065 insertions(+), 46 deletions(-) create mode 100755 tests/compose/api/00_create_users.sh create mode 100755 tests/compose/api/01_test_user_interfaces.sh create mode 100755 tests/compose/api/02_test_domain_interfaces.sh create mode 100755 tests/compose/api/03_test_token_interfaces.sh create mode 100755 tests/compose/api/04_test_relay_interfaces.sh create mode 100755 tests/compose/api/05_test_alias_interfaces.sh create mode 100644 tests/compose/api/docker-compose.yml create mode 100644 tests/compose/api/mailu.env diff --git a/.github/workflows/build_test_deploy.yml b/.github/workflows/build_test_deploy.yml index 71e8d099..1ce4f117 100644 --- a/.github/workflows/build_test_deploy.yml +++ b/.github/workflows/build_test_deploy.yml @@ -418,7 +418,7 @@ jobs: strategy: fail-fast: false matrix: - target: ["core", "fetchmail", "filters", "webmail", "webdav"] + target: ["api", "core", "fetchmail", "filters", "webmail", "webdav"] time: ["2"] include: - target: "filters" diff --git a/core/admin/mailu/api/v1/alias.py b/core/admin/mailu/api/v1/alias.py index a40295a8..ffe0177d 100644 --- a/core/admin/mailu/api/v1/alias.py +++ b/core/admin/mailu/api/v1/alias.py @@ -2,6 +2,7 @@ from flask_restx import Resource, fields, marshal from . import api, response_fields from .. import common from ... import models +import validators db = models.db @@ -15,7 +16,7 @@ alias_fields_update = alias.model('AliasUpdate', { alias_fields = alias.inherit('Alias',alias_fields_update, { 'email': fields.String(description='the alias email address', example='user@example.com', required=True), - 'destination': fields.List(fields.String(description='alias email address', example='user@example.com', required=True)), + 'destination': fields.List(fields.String(description='destination email address', example='user@example.com', required=True)), }) @@ -24,6 +25,7 @@ alias_fields = alias.inherit('Alias',alias_fields_update, { class Aliases(Resource): @alias.doc('list_alias') @alias.marshal_with(alias_fields, as_list=True, skip_none=True, mask=None) + @alias.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @alias.doc(security='Bearer') @common.api_token_authorization def get(self): @@ -34,6 +36,8 @@ class Aliases(Resource): @alias.expect(alias_fields) @alias.response(200, 'Success', response_fields) @alias.response(400, 'Input validation exception', response_fields) + @alias.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) + @alias.response(404, 'Not found', response_fields) @alias.response(409, 'Duplicate alias', response_fields) @alias.doc(security='Bearer') @common.api_token_authorization @@ -41,6 +45,20 @@ class Aliases(Resource): """ Create a new alias """ data = api.payload + if not validators.email(data['email']): + return { 'code': 400, 'message': f'Provided alias {data["email"]} is not a valid email address'}, 400 + localpart, domain_name = data['email'].lower().rsplit('@', 1) + domain_found = models.Domain.query.get(domain_name) + if not domain_found: + return { 'code': 404, 'message': f'Domain {domain_name} does not exist ({data["email"]})'}, 404 + if not domain_found.max_aliases == -1 and len(domain_found.aliases) >= domain_found.max_aliases: + return { 'code': 409, 'message': f'Too many aliases for domain {domain_name}'}, 409 + for dest in data['destination']: + if not validators.email(dest): + return { 'code': 400, 'message': f'Provided destination email address {dest} is not a valid email address'}, 400 + elif models.User.query.filter_by(email=dest).first() is None: + return { 'code': 404, 'message': f'Provided destination email address {dest} does not exist'}, 404 + alias_found = models.Alias.query.filter_by(email = data['email']).first() if alias_found: return { 'code': 409, 'message': f'Duplicate alias {data["email"]}'}, 409 @@ -53,17 +71,21 @@ class Aliases(Resource): db.session.add(alias_model) db.session.commit() - return {'code': 200, 'message': f'Alias {data["email"]} to destination {data["destination"]} has been created'}, 200 + return {'code': 200, 'message': f'Alias {data["email"]} to destination(s) {data["destination"]} has been created'}, 200 @alias.route('/') class Alias(Resource): @alias.doc('find_alias') @alias.response(200, 'Success', alias_fields) + @alias.response(400, 'Input validation exception', response_fields) + @alias.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @alias.response(404, 'Alias not found', response_fields) @alias.doc(security='Bearer') @common.api_token_authorization def get(self, alias): """ Look up the specified alias """ + if not validators.email(alias): + return { 'code': 400, 'message': f'Provided alias (email address) {alias} is not a valid email address'}, 400 alias_found = models.Alias.query.filter_by(email = alias).first() if alias_found is None: return { 'code': 404, 'message': f'Alias {alias} cannot be found'}, 404 @@ -73,6 +95,7 @@ class Alias(Resource): @alias.doc('update_alias') @alias.expect(alias_fields_update) @alias.response(200, 'Success', response_fields) + @alias.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @alias.response(404, 'Alias not found', response_fields) @alias.response(400, 'Input validation exception', response_fields) @alias.doc(security='Bearer') @@ -80,6 +103,9 @@ class Alias(Resource): def patch(self, alias): """ Update the specfied alias """ data = api.payload + + if not validators.email(alias): + return { 'code': 400, 'message': f'Provided alias (email address) {alias} is not a valid email address'}, 400 alias_found = models.Alias.query.filter_by(email = alias).first() if alias_found is None: return { 'code': 404, 'message': f'Alias {alias} cannot be found'}, 404 @@ -87,6 +113,11 @@ class Alias(Resource): alias_found.comment = data['comment'] if 'destination' in data: alias_found.destination = data['destination'] + for dest in data['destination']: + if not validators.email(dest): + return { 'code': 400, 'message': f'Provided destination email address {dest} is not a valid email address'}, 400 + elif models.User.query.filter_by(email=dest).first() is None: + return { 'code': 404, 'message': f'Provided destination email address {dest} does not exist'}, 404 if 'wildcard' in data: alias_found.wildcard = data['wildcard'] db.session.add(alias_found) @@ -95,11 +126,15 @@ class Alias(Resource): @alias.doc('delete_alias') @alias.response(200, 'Success', response_fields) + @alias.response(400, 'Input validation exception', response_fields) + @alias.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @alias.response(404, 'Alias not found', response_fields) @alias.doc(security='Bearer') @common.api_token_authorization def delete(self, alias): """ Delete the specified alias """ + if not validators.email(alias): + return { 'code': 400, 'message': f'Provided alias (email address) {alias} is not a valid email address'}, 400 alias_found = models.Alias.query.filter_by(email = alias).first() if alias_found is None: return { 'code': 404, 'message': f'Alias {alias} cannot be found'}, 404 @@ -110,12 +145,16 @@ class Alias(Resource): @alias.route('/destination/') class AliasWithDest(Resource): @alias.doc('find_alias_filter_domain') - @alias.marshal_with(alias_fields, code=200, description='Success' ,as_list=True, skip_none=True, mask=None) + @alias.response(200, 'Success', alias_fields) + @alias.response(400, 'Input validation exception', response_fields) + @alias.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @alias.response(404, 'Alias or domain not found', response_fields) @alias.doc(security='Bearer') @common.api_token_authorization def get(self, domain): """ Look up the aliases of the specified domain """ + if not validators.domain(domain): + return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400 domain_found = models.Domain.query.filter_by(name=domain).first() if domain_found is None: return { 'code': 404, 'message': f'Domain {domain} cannot be found'}, 404 @@ -123,4 +162,4 @@ class AliasWithDest(Resource): if aliases_found.count == 0: return { 'code': 404, 'message': f'No alias can be found for domain {domain}'}, 404 else: - return marshal(aliases_found, alias_fields, as_list=True), 200 + return marshal(aliases_found, alias_fields), 200 diff --git a/core/admin/mailu/api/v1/domain.py b/core/admin/mailu/api/v1/domain.py index 1d60ab8c..f5048b5d 100644 --- a/core/admin/mailu/api/v1/domain.py +++ b/core/admin/mailu/api/v1/domain.py @@ -16,7 +16,7 @@ domain_fields = api.model('Domain', { 'max_aliases': fields.Integer(description='maximum number of aliases', min=-1, default=-1), 'max_quota_bytes': fields.Integer(description='maximum quota for mailbox', min=0), 'signup_enabled': fields.Boolean(description='allow signup'), - 'alternatives': fields.List(fields.String(attribute='name', description='FQDN', example='example2.com')), + 'alternatives': fields.List(fields.String(attribute='name', description='FQDN'), example='["example.com"]'), }) domain_fields_update = api.model('DomainUpdate', { @@ -25,17 +25,18 @@ domain_fields_update = api.model('DomainUpdate', { 'max_aliases': fields.Integer(description='maximum number of aliases', min=-1, default=-1), 'max_quota_bytes': fields.Integer(description='maximum quota for mailbox', min=0), 'signup_enabled': fields.Boolean(description='allow signup'), - 'alternatives': fields.List(fields.String(attribute='name', description='FQDN', example='example2.com')), + 'alternatives': fields.List(fields.String(attribute='name', description='FQDN'), example='["example.com"]'), }) domain_fields_get = api.model('DomainGet', { 'name': fields.String(description='FQDN (e.g. example.com)', example='example.com', required=True), 'comment': fields.String(description='a comment'), + 'managers': fields.List(fields.String(attribute='email', description='manager of domain')), 'max_users': fields.Integer(description='maximum number of users', min=-1, default=-1), 'max_aliases': fields.Integer(description='maximum number of aliases', min=-1, default=-1), 'max_quota_bytes': fields.Integer(description='maximum quota for mailbox', min=0), 'signup_enabled': fields.Boolean(description='allow signup'), - 'alternatives': fields.List(fields.String(attribute='name', description='FQDN', example='example2.com')), + 'alternatives': fields.List(fields.String(attribute='name', description='FQDN'), example='["example.com"]'), 'dns_autoconfig': fields.List(fields.String(description='DNS client auto-configuration entry')), 'dns_mx': fields.String(Description='MX record for domain'), 'dns_spf': fields.String(Description='SPF record for domain'), @@ -56,8 +57,7 @@ domain_fields_dns = api.model('DomainDNS', { }) manager_fields = api.model('Manager', { - 'domain_name': fields.String(description='domain managed by manager'), - 'user_email': fields.String(description='email address of manager'), + 'managers': fields.List(fields.String(attribute='email', description='manager of domain')), }) manager_fields_create = api.model('ManagerCreate', { @@ -78,6 +78,7 @@ alternative_fields = api.model('AlternativeDomain', { class Domains(Resource): @dom.doc('list_domain') @dom.marshal_with(domain_fields_get, as_list=True, skip_none=True, mask=None) + @dom.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @dom.doc(security='Bearer') @common.api_token_authorization def get(self): @@ -88,6 +89,7 @@ class Domains(Resource): @dom.expect(domain_fields) @dom.response(200, 'Success', response_fields) @dom.response(400, 'Input validation exception', response_fields) + @dom.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @dom.response(409, 'Duplicate domain/alternative name', response_fields) @dom.doc(security='Bearer') @common.api_token_authorization @@ -131,15 +133,16 @@ class Domains(Resource): class Domain(Resource): @dom.doc('find_domain') - @dom.marshal_with(domain_fields, code=200, description='Success', as_list=False, skip_none=True, mask=None) + @dom.response(200, 'Success', domain_fields_get) @dom.response(400, 'Input validation exception', response_fields) + @dom.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @dom.response(404, 'Domain not found', response_fields) @dom.doc(security='Bearer') @common.api_token_authorization def get(self, domain): """ Look up the specified domain """ if not validators.domain(domain): - return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400 + return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 200 domain_found = models.Domain.query.get(domain) if not domain_found: return { 'code': 404, 'message': f'Domain {domain} does not exist'}, 404 @@ -149,6 +152,7 @@ class Domain(Resource): @dom.expect(domain_fields_update) @dom.response(200, 'Success', response_fields) @dom.response(400, 'Input validation exception', response_fields) + @dom.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @dom.response(404, 'Domain not found', response_fields) @dom.response(409, 'Duplicate domain/alternative name', response_fields) @dom.doc(security='Bearer') @@ -158,7 +162,7 @@ class Domain(Resource): if not validators.domain(domain): return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400 domain_found = models.Domain.query.get(domain) - if not domain: + if not domain_found: return { 'code': 404, 'message': f'Domain {data["name"]} does not exist'}, 404 data = api.payload @@ -172,7 +176,7 @@ class Domain(Resource): if not validators.domain(item): return { 'code': 400, 'message': f'Alternative domain {item} is not a valid domain'}, 400 for item in data['alternatives']: - alternative = models.Alternative(name=item, domain_name=data['name']) + alternative = models.Alternative(name=item, domain_name=domain) models.db.session.add(alternative) if 'comment' in data: @@ -194,6 +198,7 @@ class Domain(Resource): @dom.doc('delete_domain') @dom.response(200, 'Success', response_fields) @dom.response(400, 'Input validation exception', response_fields) + @dom.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @dom.response(404, 'Domain not found', response_fields) @dom.doc(security='Bearer') @common.api_token_authorization @@ -202,7 +207,7 @@ class Domain(Resource): if not validators.domain(domain): return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400 domain_found = models.Domain.query.get(domain) - if not domain: + if not domain_found: return { 'code': 404, 'message': f'Domain {domain} does not exist'}, 404 db.session.delete(domain_found) db.session.commit() @@ -213,6 +218,7 @@ class Domain(Resource): @dom.doc('generate_dkim') @dom.response(200, 'Success', response_fields) @dom.response(400, 'Input validation exception', response_fields) + @dom.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @dom.response(404, 'Domain not found', response_fields) @dom.doc(security='Bearer') @common.api_token_authorization @@ -230,8 +236,9 @@ class Domain(Resource): @dom.route('//manager') class Manager(Resource): @dom.doc('list_managers') - @dom.marshal_with(manager_fields, code=200, description='Success', as_list=True, skip_none=True, mask=None) + @dom.response(200, 'Success', manager_fields) @dom.response(400, 'Input validation exception', response_fields) + @dom.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @dom.response(404, 'domain not found', response_fields) @dom.doc(security='Bearer') @common.api_token_authorization @@ -239,15 +246,16 @@ class Manager(Resource): """ List all managers of the specified domain """ if not validators.domain(domain): return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400 - if not domain: + domain_found = models.Domain.query.get(domain) + if not domain_found: return { 'code': 404, 'message': f'Domain {domain} does not exist'}, 404 - domain = models.Domain.query.filter_by(name=domain) - return domain.managers + return marshal(domain_found, manager_fields), 200 @dom.doc('create_manager') @dom.expect(manager_fields_create) @dom.response(200, 'Success', response_fields) @dom.response(400, 'Input validation exception', response_fields) + @dom.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @dom.response(404, 'User or domain not found', response_fields) @dom.response(409, 'Duplicate domain manager', response_fields) @dom.doc(security='Bearer') @@ -274,13 +282,14 @@ class Manager(Resource): @dom.route('//manager/') class Domain(Resource): @dom.doc('find_manager') - @dom.marshal_with(manager_fields, code=200, description='Success', as_list=False, skip_none=True, mask=None) + @dom.response(200, 'Success', response_fields) @dom.response(400, 'Input validation exception', response_fields) + @dom.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @dom.response(404, 'Manager not found', response_fields) @dom.doc(security='Bearer') @common.api_token_authorization def get(self, domain, email): - """ Look up the specified manager of the specified domain """ + """ Check if the specified user is a manager of the specified domain """ if not validators.email(email): return {'code': 400, 'message': f'Invalid email address {email}'}, 400 if not validators.domain(domain): @@ -294,7 +303,7 @@ class Domain(Resource): if user in domain.managers: for manager in domain.managers: if manager.email == email: - return marshal(manager, manager_fields),200 + return { 'code': 200, 'message': f'User {email} is a manager of the domain {domain}'}, 200 else: return { 'code': 404, 'message': f'User {email} is not a manager of the domain {domain}'}, 404 @@ -302,6 +311,7 @@ class Domain(Resource): @dom.doc('delete_manager') @dom.response(200, 'Success', response_fields) @dom.response(400, 'Input validation exception', response_fields) + @dom.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @dom.response(404, 'Manager not found', response_fields) @dom.doc(security='Bearer') @common.api_token_authorization @@ -327,8 +337,9 @@ class Domain(Resource): @dom.route('//users') class User(Resource): @dom.doc('list_user_domain') - @dom.marshal_with(user.user_fields_get, code=200, description='Success', as_list=True, skip_none=True, mask=None) + @dom.response(200, 'Success', user.user_fields_get) @dom.response(400, 'Input validation exception', response_fields) + @dom.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @dom.response(404, 'Domain not found', response_fields) @dom.doc(security='Bearer') @common.api_token_authorization @@ -339,13 +350,14 @@ class User(Resource): domain_found = models.Domain.query.get(domain) if not domain_found: return { 'code': 404, 'message': f'Domain {domain} does not exist'}, 404 - return models.User.query.filter_by(domain=domain_found).all() + return marshal(models.User.query.filter_by(domain=domain_found).all(), user.user_fields_get),200 @alt.route('') class Alternatives(Resource): @alt.doc('list_alternative') @alt.marshal_with(alternative_fields, as_list=True, skip_none=True, mask=None) + @alt.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @alt.doc(security='Bearer') @common.api_token_authorization def get(self): @@ -357,6 +369,7 @@ class Alternatives(Resource): @alt.expect(alternative_fields) @alt.response(200, 'Success', response_fields) @alt.response(400, 'Input validation exception', response_fields) + @alt.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @alt.response(404, 'Domain not found or missing', response_fields) @alt.response(409, 'Duplicate alternative domain name', response_fields) @alt.doc(security='Bearer') @@ -383,8 +396,9 @@ class Alternatives(Resource): class Alternative(Resource): @alt.doc('find_alternative') @alt.doc(security='Bearer') - @alt.marshal_with(alternative_fields, code=200, description='Success' ,as_list=True, skip_none=True, mask=None) + @alt.response(200, 'Success', alternative_fields) @alt.response(400, 'Input validation exception', response_fields) + @alt.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @alt.response(404, 'Alternative not found or missing', response_fields) @common.api_token_authorization def get(self, alt): @@ -399,6 +413,7 @@ class Alternative(Resource): @alt.doc('delete_alternative') @alt.response(200, 'Success', response_fields) @alt.response(400, 'Input validation exception', response_fields) + @alt.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @alt.response(404, 'Alternative/Domain not found or missing', response_fields) @alt.response(409, 'Duplicate domain name', response_fields) @alt.doc(security='Bearer') diff --git a/core/admin/mailu/api/v1/relay.py b/core/admin/mailu/api/v1/relay.py index d9542de6..0f160947 100644 --- a/core/admin/mailu/api/v1/relay.py +++ b/core/admin/mailu/api/v1/relay.py @@ -24,6 +24,7 @@ relay_fields_update = api.model('RelayUpdate', { class Relays(Resource): @relay.doc('list_relays') @relay.marshal_with(relay_fields, as_list=True, skip_none=True, mask=None) + @relay.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @relay.doc(security='Bearer') @common.api_token_authorization def get(self): @@ -34,6 +35,7 @@ class Relays(Resource): @relay.expect(relay_fields) @relay.response(200, 'Success', response_fields) @relay.response(400, 'Input validation exception', response_fields) + @relay.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @relay.response(409, 'Duplicate relay', response_fields) @relay.doc(security='Bearer') @common.api_token_authorization @@ -58,8 +60,9 @@ class Relays(Resource): @relay.route('/') class Relay(Resource): @relay.doc('find_relay') - @relay.marshal_with(relay_fields, code=200, description='Success', as_list=False, skip_none=True, mask=None) + @relay.response(200, 'Success', relay_fields) @relay.response(400, 'Input validation exception', response_fields) + @relay.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @relay.response(404, 'Relay not found', response_fields) @relay.doc(security='Bearer') @common.api_token_authorization @@ -77,6 +80,7 @@ class Relay(Resource): @relay.expect(relay_fields_update) @relay.response(200, 'Success', response_fields) @relay.response(400, 'Input validation exception', response_fields) + @relay.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @relay.response(404, 'Relay not found', response_fields) @relay.doc(security='Bearer') @common.api_token_authorization @@ -103,6 +107,7 @@ class Relay(Resource): @relay.doc('delete_relay') @relay.response(200, 'Success', response_fields) @relay.response(400, 'Input validation exception', response_fields) + @relay.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @relay.response(404, 'Relay not found', response_fields) @relay.doc(security='Bearer') @common.api_token_authorization diff --git a/core/admin/mailu/api/v1/token.py b/core/admin/mailu/api/v1/token.py index 041ff322..ba23dc79 100644 --- a/core/admin/mailu/api/v1/token.py +++ b/core/admin/mailu/api/v1/token.py @@ -15,20 +15,20 @@ 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'), + 'AuthorizedIP': fields.List(fields.String(description='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'), + 'email': fields.String(description='The email address of the user', example='John.Doe@example.com', attribute='user_email', required=True), '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'), + 'AuthorizedIP': fields.List(fields.String(description='White listed IP addresses or networks that may use this token.', example="203.0.113.0/24")), }) 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'), + 'AuthorizedIP': fields.List(fields.String(description='White listed IP addresses or networks that may use this token.', example="203.0.113.0/24")), }) token_user_post_response = api.model('TokenPostResponse', { @@ -36,14 +36,17 @@ token_user_post_response = api.model('TokenPostResponse', { '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'), + 'AuthorizedIP': fields.List(fields.String(description='White listed IP addresses or networks that may use this token.', example="203.0.113.0/24")), '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(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @token.doc(security='Bearer') @common.api_token_authorization def get(self): @@ -52,8 +55,10 @@ class Tokens(Resource): @token.doc('create_token') @token.expect(token_user_fields_post) - @token.marshal_with(token_user_post_response, code=200, description='Success', as_list=False, skip_none=True, mask=None) + @token.response(200, 'Success', token_user_post_response) @token.response(400, 'Input validation exception', response_fields) + @token.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) + @token.response(404, 'User not found', response_fields) @token.doc(security='Bearer') @common.api_token_authorization def post(self): @@ -71,7 +76,11 @@ class Tokens(Resource): if 'comment' in data: token_new.comment = data['comment'] if 'AuthorizedIP' in data: - token_new.ip = data['AuthorizedIP'].replace(' ','').split(',') + token_new.ip = data['AuthorizedIP'] + for ip in token_new.ip: + if (not validators.ip_address.ipv4(ip,cidr=True, strict=False, host_bit=False) and + not validators.ip_address.ipv6(ip,cidr=True, strict=False, host_bit=False)): + return { 'code': 400, 'message': f'Provided AuthorizedIP {ip} in {token_new.ip} is invalid'}, 400 raw_password = pwd.genword(entropy=128, length=32, charset="hex") token_new.set_password(raw_password) models.db.session.add(token_new) @@ -91,8 +100,9 @@ class Tokens(Resource): @token.route('user/') class Token(Resource): @token.doc('find_tokens_of_user') - @token.marshal_with(token_user_fields, code=200, description='Success', as_list=True, skip_none=True, mask=None) + @token.response(200, 'Success', token_user_fields) @token.response(400, 'Input validation exception', response_fields) + @token.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @token.response(404, 'Token not found', response_fields) @token.doc(security='Bearer') @common.api_token_authorization @@ -104,12 +114,25 @@ class Token(Resource): if not user_found: return {'code': 404, 'message': f'User {email} cannot be found'}, 404 tokens = user_found.tokens - return tokens + response_list = [] + for token in tokens: + response_dict = { + 'id' : token.id, + 'email' : token.user_email, + 'comment' : token.comment, + 'AuthorizedIP' : token.ip, + 'Created': str(token.created_at), + 'Last edit': str(token.updated_at) + } + response_list.append(response_dict) + return response_list @token.doc('create_token') @token.expect(token_user_fields_post2) @token.response(200, 'Success', token_user_post_response) @token.response(400, 'Input validation exception', response_fields) + @token.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) + @token.response(404, 'User not found', response_fields) @token.doc(security='Bearer') @common.api_token_authorization def post(self, email): @@ -125,7 +148,11 @@ class Token(Resource): if 'comment' in data: token_new.comment = data['comment'] if 'AuthorizedIP' in data: - token_new.ip = token_new.ip = data['AuthorizedIP'].replace(' ','').split(',') + token_new.ip = token_new.ip = data['AuthorizedIP'] + for ip in token_new.ip: + if (not validators.ip_address.ipv4(ip,cidr=True, strict=False, host_bit=False) and + not validators.ip_address.ipv6(ip,cidr=True, strict=False, host_bit=False)): + return { 'code': 400, 'message': f'Provided AuthorizedIP {ip} in {token_new.ip} is invalid'}, 400 raw_password = pwd.genword(entropy=128, length=32, charset="hex") token_new.set_password(raw_password) models.db.session.add(token_new) @@ -144,7 +171,8 @@ class Token(Resource): @token.route('/') class Token(Resource): @token.doc('find_token') - @token.marshal_with(token_user_fields, code=200, description='Success', as_list=False, skip_none=True, mask=None) + @token.response(200, 'Success', token_user_fields) + @token.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @token.response(404, 'Token not found', response_fields) @token.doc(security='Bearer') @common.api_token_authorization @@ -153,11 +181,48 @@ class Token(Resource): 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 + response_dict = { + 'id' : token.id, + 'email' : token.user_email, + 'comment' : token.comment, + 'AuthorizedIP' : token.ip, + 'Created': str(token.created_at), + 'Last edit': str(token.updated_at) + } + return response_dict + + @token.doc('update_token') + @token.expect(token_user_fields_post2) + @token.response(200, 'Success', response_fields) + @token.response(400, 'Input validation exception', response_fields) + @token.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) + @token.response(404, 'User not found', response_fields) + @token.doc(security='Bearer') + def patch(self, token_id): + """ Update the specified token """ + data = api.payload + + 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 + + if 'comment' in data: + token.comment = data['comment'] + if 'AuthorizedIP' in data: + token.ip = token.ip = data['AuthorizedIP'] + for ip in token.ip: + if (not validators.ip_address.ipv4(ip,cidr=True, strict=False, host_bit=False) and + not validators.ip_address.ipv6(ip,cidr=True, strict=False, host_bit=False)): + return { 'code': 400, 'message': f'Provided AuthorizedIP {ip} in {token.ip} is invalid'}, 400 + models.db.session.add(token) + #apply the changes + db.session.commit() + return {'code': 200, 'message': f'Token with id {token_id} has been updated'}, 200 + @token.doc('delete_token') @token.response(200, 'Success', response_fields) - @token.response(400, 'Input validation exception', response_fields) + @token.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @token.response(404, 'Token not found', response_fields) @token.doc(security='Bearer') @common.api_token_authorization diff --git a/core/admin/mailu/api/v1/user.py b/core/admin/mailu/api/v1/user.py index c135cfbb..9dc6279e 100644 --- a/core/admin/mailu/api/v1/user.py +++ b/core/admin/mailu/api/v1/user.py @@ -22,7 +22,7 @@ user_fields_get = api.model('UserGet', { 'enable_pop': fields.Boolean(description='Allow email retrieval via POP3'), 'allow_spoofing': fields.Boolean(description='Allow the user to spoof the sender (send email as anyone)'), 'forward_enabled': fields.Boolean(description='Enable auto forwarding'), - 'forward_destination': fields.List(fields.String(description='Email address to forward emails to'), example='Other@example.com'), + 'forward_destination': fields.List(fields.String(description='Email address to forward emails to'), example='["Other@example.com"]'), 'forward_keep': fields.Boolean(description='Keep a copy of the forwarded email in the inbox'), 'reply_enabled': fields.Boolean(description='Enable automatic replies. This is also known as out of office (ooo) or out of facility (oof) replies'), 'reply_subject': fields.String(description='Optional subject for the automatic reply', example='Out of office'), @@ -47,7 +47,7 @@ user_fields_post = api.model('UserCreate', { 'enable_pop': fields.Boolean(description='Allow email retrieval via POP3'), 'allow_spoofing': fields.Boolean(description='Allow the user to spoof the sender (send email as anyone)'), 'forward_enabled': fields.Boolean(description='Enable auto forwarding'), - 'forward_destination': fields.List(fields.String(description='Email address to forward emails to'), example='Other@example.com'), + 'forward_destination': fields.List(fields.String(description='Email address to forward emails to'), example='["Other@example.com"]'), 'forward_keep': fields.Boolean(description='Keep a copy of the forwarded email in the inbox'), 'reply_enabled': fields.Boolean(description='Enable automatic replies. This is also known as out of office (ooo) or out of facility (oof) replies'), 'reply_subject': fields.String(description='Optional subject for the automatic reply', example='Out of office'), @@ -71,7 +71,7 @@ user_fields_put = api.model('UserUpdate', { 'enable_pop': fields.Boolean(description='Allow email retrieval via POP3'), 'allow_spoofing': fields.Boolean(description='Allow the user to spoof the sender (send email as anyone)'), 'forward_enabled': fields.Boolean(description='Enable auto forwarding'), - 'forward_destination': fields.List(fields.String(description='Email address to forward emails to'), example='Other@example.com'), + 'forward_destination': fields.List(fields.String(description='Email address to forward emails to'), example='["Other@example.com"]'), 'forward_keep': fields.Boolean(description='Keep a copy of the forwarded email in the inbox'), 'reply_enabled': fields.Boolean(description='Enable automatic replies. This is also known as out of office (ooo) or out of facility (oof) replies'), 'reply_subject': fields.String(description='Optional subject for the automatic reply', example='Out of office'), @@ -89,6 +89,7 @@ user_fields_put = api.model('UserUpdate', { class Users(Resource): @user.doc('list_user') @user.marshal_with(user_fields_get, as_list=True, skip_none=True, mask=None) + @user.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @user.doc(security='Bearer') @common.api_token_authorization def get(self): @@ -99,6 +100,7 @@ class Users(Resource): @user.expect(user_fields_post) @user.response(200, 'Success', response_fields) @user.response(400, 'Input validation exception', response_fields) + @user.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @user.response(409, 'Duplicate user', response_fields) @user.doc(security='Bearer') @common.api_token_authorization @@ -111,11 +113,12 @@ class Users(Resource): domain_found = models.Domain.query.get(domain_name) if not domain_found: return { 'code': 404, 'message': f'Domain {domain_name} does not exist'}, 404 + if not domain_found.max_users == -1 and len(domain_found.users) >= domain_found.max_users: + return { 'code': 409, 'message': f'Too many users for domain {domain_name}'}, 409 email_found = models.User.query.filter_by(email=data['email']).first() if email_found: return { 'code': 409, 'message': f'User {data["email"]} already exists'}, 409 - user_new = models.User(email=data['email']) if 'raw_password' in data: user_new.set_password(data['raw_password']) @@ -168,12 +171,12 @@ class Users(Resource): return {'code': 200,'message': f'User {data["email"]} has been created'}, 200 - @user.route('/') class User(Resource): @user.doc('find_user') - @user.marshal_with(user_fields_get, code=200, description='Success', as_list=False, skip_none=True, mask=None) + @user.response(200, 'Success', user_fields_get) @user.response(400, 'Input validation exception', response_fields) + @user.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @user.response(404, 'User not found', response_fields) @user.doc(security='Bearer') @common.api_token_authorization @@ -191,6 +194,7 @@ class User(Resource): @user.expect(user_fields_put) @user.response(200, 'Success', response_fields) @user.response(400, 'Input validation exception', response_fields) + @user.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @user.response(404, 'User not found', response_fields) @user.doc(security='Bearer') @common.api_token_authorization @@ -198,7 +202,7 @@ class User(Resource): """ Update the specified user """ data = api.payload if not validators.email(email): - return { 'code': 400, 'message': f'Provided email address {data["email"]} is not a valid email address'}, 400 + 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 @@ -258,6 +262,7 @@ class User(Resource): @user.doc('delete_user') @user.response(200, 'Success', response_fields) @user.response(400, 'Input validation exception', response_fields) + @user.doc(responses={401: 'Authorization header missing', 403: 'Invalid authorization header'}) @user.response(404, 'User not found', response_fields) @user.doc(security='Bearer') @common.api_token_authorization diff --git a/tests/compose/api/00_create_users.sh b/tests/compose/api/00_create_users.sh new file mode 100755 index 00000000..0a96e968 --- /dev/null +++ b/tests/compose/api/00_create_users.sh @@ -0,0 +1,86 @@ +# create user admin@maiu.io +echo "Create users" +curl --silent --insecure -X 'POST' \ + 'https://localhost/api/v1/user' \ + -H 'accept: application/json' \ + -H 'Authorization: Bearer apitest' \ + -H 'Content-Type: application/json' \ + -d '{ + "email": "admin@mailu.io", + "raw_password": "password", + "comment": "created for testing RESTful API", + "global_admin": true, + "enabled": true, + "change_pw_next_login": false, + "enable_imap": true, + "enable_pop": true, + "allow_spoofing": false, + "forward_enabled": false, + "reply_enabled": false, + "displayed_name": "admin", + "spam_enabled": true, + "spam_mark_as_read": true +}' | grep 200 + +if [ $? -ne 0 ]; then + exit 1 +fi +echo "Created admin user (admin@mailu.io) successfully" + +# Test if creating duplicate returns 409 HTTP response. +curl --silent --insecure -X 'POST' \ + 'https://localhost/api/v1/user' \ + -H 'accept: application/json' \ + -H 'Authorization: Bearer apitest' \ + -H 'Content-Type: application/json' \ + -d '{ + "email": "admin@mailu.io", + "raw_password": "password", + "comment": "created for testing RESTful API", + "global_admin": true, + "enabled": true, + "change_pw_next_login": false, + "enable_imap": true, + "enable_pop": true, + "allow_spoofing": false, + "forward_enabled": false, + "reply_enabled": false, + "displayed_name": "admin", + "spam_enabled": true, + "spam_mark_as_read": true +}' | grep 409 + +if [ $? -ne 0 ]; then + exit 1 +fi +echo "OK. Failed creating duplicate user." + +# create user user@mailu.io +curl --silent --insecure -X 'POST' \ + 'https://localhost/api/v1/user' \ + -H 'accept: application/json' \ + -H 'Authorization: Bearer apitest' \ + -H 'Content-Type: application/json' \ + -d '{ + "email": "user@mailu.io", + "raw_password": "password", + "comment": "created for testing RESTful API", + "global_admin": false, + "enabled": true, + "change_pw_next_login": false, + "enable_imap": true, + "enable_pop": true, + "allow_spoofing": false, + "forward_enabled": false, + "reply_enabled": false, + "displayed_name": "admin", + "spam_enabled": true, + "spam_mark_as_read": true +}' | grep 200 + +if [ $? -ne 0 ]; then + exit 1 +fi +echo "Created user (user@mailu.io) successfully" + +echo "Finished 00_create_users.sh" \ No newline at end of file diff --git a/tests/compose/api/01_test_user_interfaces.sh b/tests/compose/api/01_test_user_interfaces.sh new file mode 100755 index 00000000..f63258c3 --- /dev/null +++ b/tests/compose/api/01_test_user_interfaces.sh @@ -0,0 +1,80 @@ +echo "Test user interfaces" +# create user user@mailu.io for testing deletion +curl --silent --insecure -X 'POST' \ + 'https://localhost/api/v1/user' \ + -H 'accept: application/json' \ + -H 'Authorization: Bearer apitest' \ + -H 'Content-Type: application/json' \ + -d '{ + "email": "user2@mailu.io", + "raw_password": "password", + "comment": "created for testing RESTful API", + "global_admin": false, + "enabled": true, + "change_pw_next_login": false, + "enable_imap": true, + "enable_pop": true, + "allow_spoofing": false, + "forward_enabled": false, + "reply_enabled": false, + "displayed_name": "admin", + "spam_enabled": true, + "spam_mark_as_read": true +}' | grep 200 + +if [ $? -ne 0 ]; then + exit 1 +fi +echo "created user (user2@mailu.io) successfully" + +#delete user2@mailu.io +curl --silent --insecure -X 'DELETE' \ + 'https://localhost/api/v1/user/user2%40mailu.io' \ + -H 'accept: application/json' \ + -H 'Authorization: Bearer apitest' \ + | grep 200 + +if [ $? -ne 0 ]; then + exit 1 +fi +echo "Deleted user2 (user2@mailu.io) successfully" + +#Check if updating user works +curl --silent --insecure -X 'PATCH' \ + 'https://localhost/api/v1/user/user%40mailu.io' \ + -H 'accept: application/json' \ + -H 'Authorization: Bearer apitest' \ + -H 'Content-Type: application/json' \ + -d '{ + "comment": "updated_comment" +}' | grep 200 + +if [ $? -ne 0 ]; then + exit 1 +fi +echo "Updated user(user@mailu.io) successfully" + +curl --silent --insecure -X 'GET' \ + 'https://localhost/api/v1/user/user%40mailu.io' \ + -H 'accept: application/json' \ + -H 'Authorization: Bearer apitest' \ + | grep updated_comment + +if [ $? -ne 0 ]; then + exit 1 +fi +echo "Confirmed that comment attribute of user was correctly updated" + +# try get all users. At this moment we should have 2 users total +curl --silent --insecure -X 'GET' \ + 'https://localhost/api/v1/user' \ + -H 'accept: application/json' \ + -H 'Authorization: Bearer apitest' \ + | grep -o "email" | grep -c "email" | grep 2 + +if [ $? -ne 0 ]; then + exit 1 +fi +echo "Retrieved all users successfully" + +echo "Finished 01_test_user_interfaces.sh" \ No newline at end of file diff --git a/tests/compose/api/02_test_domain_interfaces.sh b/tests/compose/api/02_test_domain_interfaces.sh new file mode 100755 index 00000000..c989f7a6 --- /dev/null +++ b/tests/compose/api/02_test_domain_interfaces.sh @@ -0,0 +1,145 @@ +echo "Test Domain interfaces" + +curl --silent --insecure -X 'POST' \ + 'https://localhost/api/v1/domain' \ + -H 'accept: application/json' \ + -H 'Authorization: Bearer apitest' \ + -H 'Content-Type: application/json' \ + -d '{ + "name": "mailu2.io", + "comment": "internal domain for testing", + "max_users": -1, + "max_aliases": -1, + "max_quota_bytes": 0, + "signup_enabled": false +}' | grep 200 + +if [ $? -ne 0 ]; then + exit 1 +fi +echo "Domain mail2.io has been created successfully" + +curl --silent --insecure -X 'PATCH' \ + 'https://localhost/api/v1/domain/mailu2.io' \ + -H 'accept: application/json' \ + -H 'Authorization: Bearer apitest' \ + -H 'Content-Type: application/json' \ + -d '{ + "comment": "updated_domain" +}' | grep 200 + +if [ $? -ne 0 ]; then + exit 1 +fi +echo "Domain mail2.io has been updated" + +curl --silent --insecure -X 'GET' \ + 'https://localhost/api/v1/domain/mailu2.io' \ + -H 'accept: application/json' \ + -H 'Authorization: apitest' \ + | grep updated_domain + +if [ $? -ne 0 ]; then + exit 1 +fi +echo "Confirmed that comment attribute of domain mailu2.io was correctly updated" + +# try get all domains. At this moment we should have 2 domains total +curl --silent --insecure -X 'GET' \ + 'https://localhost/api/v1/domain' \ + -H 'accept: application/json' \ + -H 'Authorization: Bearer apitest' \ + | grep -o "name" | grep -c "name" | grep 2 + +if [ $? -ne 0 ]; then + exit 1 +fi +echo "Retrieved all domains successfully" + +# try create dkim keys +curl --silent --insecure -X 'POST' \ + 'https://mailutest/api/v1/domain/mailu2.io/dkim' \ + -H 'accept: application/json' \ + -H 'Authorization: apitest' \ + -d '' \ + | grep 200 + +if [ $? -ne 0 ]; then + exit 1 +fi +echo "dkim keys were created successfully for domain mailu2.io" + +# try deleting a domain +curl --silent --insecure -X 'DELETE' \ + 'https://localhost/api/v1/domain/mailu2.io' \ + -H 'accept: application/json' \ + -H 'Authorization: Bearer apitest' \ + | grep 200 + +if [ $? -ne 0 ]; then + exit 1 +fi +echo "Domain mailu2.io was deleted successfully" + +# try looking up all users of a domain. There should be 2 users. +curl --silent --insecure -X 'GET' \ + 'https://mailutest/api/v1/domain/mailu.io/users' \ + -H 'accept: application/json' \ + -H 'Authorization: apitest' \ + | grep -o "email" | grep -c "email" | grep 2 + +if [ $? -ne 0 ]; then + exit 1 +fi +echo "Retrieved all users of domain mailu.io successfully" + + +#### Alternatives + +#try to create an alternative +curl --silent --insecure -X 'POST' \ + 'https://mailutest/api/v1/alternative' \ + -H 'accept: application/json' \ + -H 'Authorization: apitest' \ + -H 'Content-Type: application/json' \ + -d '{ + "name": "mailu2.io", + "domain": "mailu.io" +}' | grep 200 + +if [ $? -ne 0 ]; then + exit 1 +fi +echo "Alternative mailu2.io for domain mailu.io was created successfully" + +# try get all alternatives. At this moment we should have 1 alternative total +curl --silent --insecure -X 'GET' \ + 'https://localhost/api/v1/alternative' \ + -H 'accept: application/json' \ + -H 'Authorization: Bearer apitest' \ + | grep -o "name" | grep -c "name" | grep 1 + +if [ $? -ne 0 ]; then + exit 1 +fi +echo "Retrieved all alternatives successfully" + +# try to check if an alternative exists +curl --silent --insecure -X 'GET' \ + 'https://mailutest/api/v1/alternative/mailu2.io' \ + -H 'accept: application/json' \ + -H 'Authorization: apitest' \ + | grep '{"name": "mailu2.io", "domain": "mailu.io"}' + +if [ $? -ne 0 ]; then + exit 1 +fi +echo "Lookup for alternative mailu2.io was successful" + +# try to delete an alternative +curl --silent --insecure -X 'DELETE' \ + 'https://mailutest/api/v1/alternative/mailu2.io' \ + -H 'accept: application/json' \ + -H 'Authorization: apitest' + +echo "Finshed 02_test_domain_interfaces.sh" \ No newline at end of file diff --git a/tests/compose/api/03_test_token_interfaces.sh b/tests/compose/api/03_test_token_interfaces.sh new file mode 100755 index 00000000..9180bc7a --- /dev/null +++ b/tests/compose/api/03_test_token_interfaces.sh @@ -0,0 +1,107 @@ +echo "start token tests" + +# Try creating a token /token +curl --silent --insecure -X 'POST' \ + 'https://mailutest/api/v1/token' \ + -H 'accept: application/json' \ + -H 'Authorization: apitest' \ + -H 'Content-Type: application/json' \ + -d '{ + "email": "user@mailu.io", + "comment": "my token related comment", + "AuthorizedIP": [ + "203.0.113.0/24", + "203.2.114.2/32" + ] +}' | grep '"token": "' +if [ $? -ne 0 ]; then + exit 1 +fi +echo "created a token for user@mailu.io successfully" + +# Try create a token for a specific user /tokenuser/{email} +curl --silent --insecure -X 'POST' \ + 'https://mailutest/api/v1/tokenuser/user%40mailu.io' \ + -H 'accept: application/json' \ + -H 'Authorization: apitest' \ + -H 'Content-Type: application/json' \ + -d '{ + "comment": "token test" +}' | grep '"token": "' +if [ $? -ne 0 ]; then + exit 1 +fi +echo "created a second token for user@mailu.io successfully" + +# Try retrieving all tokens /token. We expect to retrieve 2 in total. +curl --silent --insecure -X 'GET' \ + 'https://mailutest/api/v1/token' \ + -H 'accept: application/json' \ + -H 'Authorization: apitest' \ + | grep -o "id" | grep -c "id" | grep 2 +if [ $? -ne 0 ]; then + exit 1 +fi +echo "Retrieved all tokens (2 in total) successfully" + +# Try finding a specific token /token/{token_id} +curl --silent --insecure -X 'GET' \ + 'https://mailutest/api/v1/token/2' \ + -H 'accept: application/json' \ + -H 'Authorization: apitest' \ + | grep '"id": 2' +if [ $? -ne 0 ]; then + exit 1 +fi +echo "Retrieved token with id 2 successfully" + +# Try deleting a token /token/{token_id} +curl --silent --insecure -X 'DELETE' \ + 'https://mailutest/api/v1/token/1' \ + -H 'accept: application/json' \ + -H 'Authorization: apitest' \ + | grep 200 +if [ $? -ne 0 ]; then + exit 1 +fi +echo "Deleted token with id 1 successfully" + +# Try updating a token /token/{token_id} +curl --silent --insecure -X 'PATCH' \ + 'https://mailutest/api/v1/token/2' \ + -H 'accept: application/json' \ + -H 'Authorization: apitest' \ + -H 'Content-Type: application/json' \ + -d '{ + "comment": "updated_comment", + "AuthorizedIP": [ + "203.0.112.0/24" + ] +}' | grep 200 +if [ $? -ne 0 ]; then + exit 1 +fi +echo "Updated token with id 2 successfully" + +curl --silent --insecure -X 'GET' \ + 'https://mailutest/api/v1/token/2' \ + -H 'accept: application/json' \ + -H 'Authorization: apitest' \ + | grep 'comment": "updated_comment"' +if [ $? -ne 0 ]; then + exit 1 +fi +echo "Confirmed that comment field of token with id 2 was correctly updated" + +# Try looking up all tokens of a specific user /tokenuser/{email} +curl --silent --insecure -X 'GET' \ + 'https://mailutest/api/v1/tokenuser/user%40mailu.io' \ + -H 'accept: application/json' \ + -H 'Authorization: apitest' \ + | grep -o "id" | grep -c "id" | grep 1 +if [ $? -ne 0 ]; then + exit 1 +fi +echo "Retrieved all tokens (1 in total) for user@mailu.io successfully" + +echo "Finished 03_test_token_interfaces.sh" \ No newline at end of file diff --git a/tests/compose/api/04_test_relay_interfaces.sh b/tests/compose/api/04_test_relay_interfaces.sh new file mode 100755 index 00000000..9fb493e1 --- /dev/null +++ b/tests/compose/api/04_test_relay_interfaces.sh @@ -0,0 +1,98 @@ +echo "Start 04_test_relay_interfaces.sh" + +# Try creating a new relay /relay +curl --silent --insecure -X 'POST' \ + 'https://mailutest/api/v1/relay' \ + -H 'accept: application/json' \ + -H 'Authorization: apitest' \ + -H 'Content-Type: application/json' \ + -d '{ + "name": "relay1.mailu.io", + "smtp": "relay1.mailu.io:8755", + "comment": "backup relay1" +}' | grep 200 +if [ $? -ne 0 ]; then + exit 1 +fi +echo "created a relay for domain relay1.mailu.io successfully" + +curl --silent --insecure -X 'POST' \ + 'https://mailutest/api/v1/relay' \ + -H 'accept: application/json' \ + -H 'Authorization: apitest' \ + -H 'Content-Type: application/json' \ + -d '{ + "name": "relay2.mailu.io", + "comment": "backup relay2" +}' | grep 200 +if [ $? -ne 0 ]; then + exit 1 +fi +echo "created a relay for domain relay2.mailu.io successfully" + +# Try retrieving all relays /relay. We expect to retrieve 2 in total +curl --silent --insecure -X 'GET' \ + 'https://mailutest/api/v1/relay' \ + -H 'accept: application/json' \ + -H 'Authorization: apitest' \ + | grep -o '"name":' | grep -c '"name":' | grep 2 +if [ $? -ne 0 ]; then + exit 1 +fi +echo "Retrieved all relays (2 in total) successfully" + +# Try looking up a specific relay /relay/{name} +curl --silent --insecure -X 'GET' \ + 'https://mailutest/api/v1/relay/relay1.mailu.io' \ + -H 'accept: application/json' \ + -H 'Authorization: apitest' \ + | grep '"name": "relay1.mailu.io"' +if [ $? -ne 0 ]; then + exit 1 +fi +echo "Retrieved the specified relay (relay1.mailu.io) successfully" + +# Try deleting a specific relay /relay/{name} +curl -silent --insecure -X 'DELETE' \ + 'https://mailutest/api/v1/relay/relay2.mailu.io' \ + -H 'accept: application/json' \ + -H 'Authorization: apitest' \ + | grep 200 +if [ $? -ne 0 ]; then + exit 1 +fi +echo "Deleted relay2.mailu.io successfully" + +curl --silent --insecure -X 'GET' \ + 'https://mailutest/api/v1/relay' \ + -H 'accept: application/json' \ + -H 'Authorization: apitest' \ + | grep -o '"name":' | grep -c '"name":' | grep 1 +if [ $? -ne 0 ]; then + exit 1 +fi +echo "confirmed we only have 1 relay now" + +# Try updating a specific relay /relay/{name} +curl --silent --insecure -X 'PATCH' \ + 'https://mailutest/api/v1/relay/relay1.mailu.io' \ + -H 'accept: application/json' \ + -H 'Authorization: apitest' \ + -H 'Content-Type: application/json' \ + -d '{ + "smtp": "anotherName", + "comment": "updated_comment" +}' | grep 200 +if [ $? -ne 0 ]; then + exit 1 +fi +echo "update of relay was succcessful" + +curl --silent --insecure -X 'GET' \ + 'https://mailutest/api/v1/relay/relay1.mailu.io' \ + -H 'accept: application/json' \ + -H 'Authorization: apitest' \ + | grep anotherName | grep updated_comment +echo "confirmed that smtp attribute and comment attribute were correctly updated" + +echo "Finished 04_test_relay_interfaces.sh" \ No newline at end of file diff --git a/tests/compose/api/05_test_alias_interfaces.sh b/tests/compose/api/05_test_alias_interfaces.sh new file mode 100755 index 00000000..be23c92e --- /dev/null +++ b/tests/compose/api/05_test_alias_interfaces.sh @@ -0,0 +1,111 @@ +# try create, find, lookup, delete + +echo "Start 05_test_alias_interfaces.sh" + +# Try creating a new alias /alias +curl --silent --insecure -X 'POST' \ + 'https://mailutest/api/v1/alias' \ + -H 'accept: application/json' \ + -H 'Authorization: apitest' \ + -H 'Content-Type: application/json' \ + -d '{ + "comment": "test alias for user@mailu.io and admin@mailu.io", + "destination": [ + "user@mailu.io", + "admin@mailu.io" + ], + "wildcard": false, + "email": "test@mailu.io" +}' | grep 200 +if [ $? -ne 0 ]; then + exit 1 +fi +echo "Created alias test@mailu.io succcessfully for user@mailu.io and admin@mailu.io" + +curl --silent --insecure -X 'POST' \ + 'https://mailutest/api/v1/alias' \ + -H 'accept: application/json' \ + -H 'Authorization: apitest' \ + -H 'Content-Type: application/json' \ + -d '{ + "comment": "test2 alias for user@mailu.io", + "destination": [ + "user@mailu.io" + ], + "wildcard": false, + "email": "test2@mailu.io" +}' | grep 200 +if [ $? -ne 0 ]; then + exit 1 +fi +echo "Created alias test2@mailu.io succcessfully for user@mailu.io " + +# Try retrieving all aliases /alias. We expect to retrieve 2 +curl --silent --insecure -X 'GET' \ + 'https://mailutest/api/v1/alias' \ + -H 'accept: application/json' \ + -H 'Authorization: apitest' \ + | grep -o '"destination":' | grep -c '"destination":' | grep 2 +if [ $? -ne 0 ]; then + exit 1 +fi +echo "Successfully retrieved 2 aliases" + +# Try looking up the aliases for a specific domain /alias/destination/{domain}. We expect to retrieve 2 +curl --silent --insecure -X 'GET' \ + 'https://mailutest/api/v1/alias/destination/mailu.io' \ + -H 'accept: application/json' \ + -H 'Authorization: apitest' \ + | grep -o '"destination":' | grep -c '"destination":' | grep 2 +if [ $? -ne 0 ]; then + exit 1 +fi +echo "Successfully retrieved 2 aliases" + +# Try deleting a specific alias /alias/{alias} +curl --silent --insecure -X 'DELETE' \ + 'https://mailutest/api/v1/alias/test2%40mailu.io' \ + -H 'accept: application/json' \ + -H 'Authorization: apitest' \ + | grep 200 +if [ $? -ne 0 ]; then + exit 1 +fi +echo "Deleted alias test2@mailu.io succcessfully" + +# Try updating a specific alias /alias/{alias} +curl --silent --insecure -X 'PATCH' \ + 'https://mailutest/api/v1/alias/test%40mailu.io' \ + -H 'accept: application/json' \ + -H 'Authorization: apitest' \ + -H 'Content-Type: application/json' \ + -d '{ + "comment": "updated_comment", + "destination": [ + "user@mailu.io" + ], + "wildcard": true +}' | grep 200 +if [ $? -ne 0 ]; then + exit 1 +fi +echo "Updated alias test2@mailu.io succcessfully" + +# Try looking up a specific alias /alias/{alias}. +#Check if values were updated correctyly in previous step. +response=$(curl --silent --insecure -X 'GET' \ + 'https://mailutest/api/v1/alias/test%40mailu.io' \ + -H 'accept: application/json' \ + -H 'Authorization: apitest') +echo $response | grep 'admin@mailu.io' +if [ $? -ne 1 ]; then + exit 1 +fi +echo "Confirmed that destination admin@mailu.io is removed from alias test@mailu.io" +echo $response | grep 'updated_comment' +if [ $? -ne 0 ]; then + exit 1 +fi +echo "Confirmed that comment attribute is updated successfully" + +echo "Finished 05_test_alias_interfaces.sh" \ No newline at end of file diff --git a/tests/compose/api/docker-compose.yml b/tests/compose/api/docker-compose.yml new file mode 100644 index 00000000..384e89cd --- /dev/null +++ b/tests/compose/api/docker-compose.yml @@ -0,0 +1,112 @@ +# This file is auto-generated by the Mailu configuration wizard. +# Please read the documentation before attempting any change. +# Generated for compose flavor + +version: '3.6' + +services: + + # External dependencies + redis: + image: redis:alpine + restart: always + volumes: + - "/mailu/redis:/data" + + # Core services + front: + image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}nginx:${MAILU_VERSION:-local} + restart: always + env_file: mailu.env + logging: + driver: json-file + ports: + - "127.0.0.1:80:80" + - "127.0.0.1:443:443" + - "127.0.0.1:25:25" + - "127.0.0.1:465:465" + - "127.0.0.1:587:587" + - "127.0.0.1:110:110" + - "127.0.0.1:995:995" + - "127.0.0.1:143:143" + - "127.0.0.1:993:993" + - "127.0.0.1:4190:4190" + volumes: + - "/mailu/certs:/certs" + + admin: + image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}admin:${MAILU_VERSION:-local} + restart: always + env_file: mailu.env + volumes: + - "/mailu/data:/data" + - "/mailu/dkim:/dkim" + dns: + - 192.168.203.254 + depends_on: + - redis + - resolver + + imap: + image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}dovecot:${MAILU_VERSION:-local} + restart: always + env_file: mailu.env + volumes: + - "/mailu/mail:/mail" + - "/mailu/overrides:/overrides" + depends_on: + - front + + smtp: + image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}postfix:${MAILU_VERSION:-local} + restart: always + env_file: mailu.env + volumes: + - "/mailu/overrides:/overrides" + depends_on: + - front + + oletools: + image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}oletools:${MAILU_VERSION:-local} + hostname: oletools + restart: always + networks: + - noinet + + antispam: + image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}rspamd:${MAILU_VERSION:-local} + restart: always + env_file: mailu.env + networks: + - default + - noinet + volumes: + - "/mailu/filter:/var/lib/rspamd" + - "/mailu/dkim:/dkim" + - "/mailu/overrides/rspamd:/etc/rspamd/override.d" + depends_on: + - front + + # Optional services + + resolver: + image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}unbound:${MAILU_VERSION:-local} + env_file: mailu.env + restart: always + networks: + default: + ipv4_address: 192.168.203.254 + + # Webmail + + +networks: + default: + driver: bridge + ipam: + driver: default + config: + - subnet: 192.168.203.0/24 + noinet: + driver: bridge + internal: true diff --git a/tests/compose/api/mailu.env b/tests/compose/api/mailu.env new file mode 100644 index 00000000..d9789150 --- /dev/null +++ b/tests/compose/api/mailu.env @@ -0,0 +1,151 @@ +# Mailu main configuration file +# +# Generated for compose flavor +# +# This file is autogenerated by the configuration management wizard. +# For a detailed list of configuration variables, see the documentation at +# https://mailu.io + +################################### +# Common configuration variables +################################### + +# Set this to the path where Mailu data and configuration is stored +# This variable is now set directly in `docker-compose.yml by the setup utility +# ROOT=/mailu + +# Mailu version to run (1.0, 1.1, etc. or master) +#VERSION=master + +# Set to a randomly generated 16 bytes string +SECRET_KEY=HGZCYGVI6FVG31HS + +# Address where listening ports should bind +# This variables are now set directly in `docker-compose.yml by the setup utility +# PUBLIC_IPV4= 127.0.0.1 (default: 127.0.0.1) +# PUBLIC_IPV6= (default: ::1) + +# Subnet of the docker network. This should not conflict with any networks to which your system is connected. (Internal and external!) +SUBNET=192.168.203.0/24 + +# Main mail domain +DOMAIN=mailu.io + +# Hostnames for this server, separated with commas +HOSTNAMES=localhost + +# Postmaster local part (will append the main mail domain) +POSTMASTER=admin + +# Choose how secure connections will behave (value: letsencrypt, cert, notls, mail, mail-letsencrypt) +TLS_FLAVOR=cert + +# Authentication rate limit (per source IP address) +AUTH_RATELIMIT=10/minute;1000/hour + +# Opt-out of statistics, replace with "True" to opt out +DISABLE_STATISTICS=False + +################################### +# Optional features +################################### + +# Expose the admin interface (value: true, false) +ADMIN=true + +# Choose which webmail to run if any (values: roundcube, snappymail, none) +WEBMAIL=none + +# Dav server implementation (value: radicale, none) +WEBDAV=none + +# Antivirus solution (value: clamav, none) +#ANTIVIRUS=none + +#Antispam solution +ANTISPAM=none + +#RESTful API +API=true + +# Scan Macros solution (value: true, false) +SCAN_MACROS=True + +################################### +# Mail settings +################################### + +# Message size limit in bytes +# Default: accept messages up to 50MB +MESSAGE_SIZE_LIMIT=50000000 + +# Networks granted relay permissions +# Use this with care, all hosts in this networks will be able to send mail without authentication! +RELAYNETS= + +# Will relay all outgoing mails if configured +RELAYHOST= + +# Show fetchmail functionality in admin interface +FETCHMAIL_ENABLED=false + +# Fetchmail delay +FETCHMAIL_DELAY=600 + +# Recipient delimiter, character used to delimiter localpart from custom address part +RECIPIENT_DELIMITER=+ + +# DMARC rua and ruf email +DMARC_RUA=admin +DMARC_RUF=admin + + +# Maildir Compression +# choose compression-method, default: none (value: gz, bz2, lz4, zstd) +COMPRESSION= +# change compression-level, default: 6 (value: 1-9) +COMPRESSION_LEVEL= + +################################### +# Web settings +################################### + +# Path to the admin interface if enabled +WEB_ADMIN=/admin + +# Path to the webmail if enabled +WEB_WEBMAIL=/webmail + +WEB_API=/api + +# Website name +SITENAME=Mailu + +# Linked Website URL +WEBSITE=https://mailu.io + + + +################################### +# Advanced settings +################################### + +# Log driver for front service. Possible values: +# json-file (default) +# journald (On systemd platforms, useful for Fail2Ban integration) +# syslog (Non systemd platforms, Fail2Ban integration. Disables `docker compose log` for front!) +# LOG_DRIVER=json-file + +# Docker-compose project name, this will prepended to containers names. +COMPOSE_PROJECT_NAME=mailu + +# Header to take the real ip from +REAL_IP_HEADER= + +# IPs for nginx set_real_ip_from (CIDR list separated by commas) +REAL_IP_FROM= + +# choose wether mailu bounces (no) or rejects (yes) mail when recipient is unknown (value: yes, no) +REJECT_UNLISTED_RECIPIENT= + +API_TOKEN=apitest \ No newline at end of file From 18d5fb8a1b566407a6ffbd0bcc4c73b894270db0 Mon Sep 17 00:00:00 2001 From: Dimitri Huisman Date: Fri, 22 Mar 2024 15:23:12 +0000 Subject: [PATCH 12/58] Forgot to create the mailu.io domain as the first step in tests --- tests/compose/api/00_create_users.sh | 20 ++++++++++++++++++++ tests/compose/api/mailu.env | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/compose/api/00_create_users.sh b/tests/compose/api/00_create_users.sh index 0a96e968..54a8e4e2 100755 --- a/tests/compose/api/00_create_users.sh +++ b/tests/compose/api/00_create_users.sh @@ -1,5 +1,25 @@ # create user admin@maiu.io echo "Create users" + +curl --silent --insecure -X 'POST' \ + 'https://localhost/api/v1/domain' \ + -H 'accept: application/json' \ + -H 'Authorization: Bearer apitest' \ + -H 'Content-Type: application/json' \ + -d '{ + "name": "mailu.io", + "comment": "internal domain for testing", + "max_users": -1, + "max_aliases": -1, + "max_quota_bytes": 0, + "signup_enabled": false +}' | grep 200 + +if [ $? -ne 0 ]; then + exit 1 +fi +echo "Domain mail.io has been created successfully" + curl --silent --insecure -X 'POST' \ 'https://localhost/api/v1/user' \ -H 'accept: application/json' \ diff --git a/tests/compose/api/mailu.env b/tests/compose/api/mailu.env index d9789150..d667496c 100644 --- a/tests/compose/api/mailu.env +++ b/tests/compose/api/mailu.env @@ -148,4 +148,4 @@ REAL_IP_FROM= # choose wether mailu bounces (no) or rejects (yes) mail when recipient is unknown (value: yes, no) REJECT_UNLISTED_RECIPIENT= -API_TOKEN=apitest \ No newline at end of file +API_TOKEN=apitest From acb878a43f8ea72998e65e6b17a3b2bf01528b1f Mon Sep 17 00:00:00 2001 From: Dimitri Huisman Date: Fri, 22 Mar 2024 15:38:46 +0000 Subject: [PATCH 13/58] Use the correct hostname --- tests/compose/api/02_test_domain_interfaces.sh | 10 +++++----- tests/compose/api/03_test_token_interfaces.sh | 16 ++++++++-------- tests/compose/api/04_test_relay_interfaces.sh | 16 ++++++++-------- tests/compose/api/05_test_alias_interfaces.sh | 14 +++++++------- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/tests/compose/api/02_test_domain_interfaces.sh b/tests/compose/api/02_test_domain_interfaces.sh index c989f7a6..b9238594 100755 --- a/tests/compose/api/02_test_domain_interfaces.sh +++ b/tests/compose/api/02_test_domain_interfaces.sh @@ -58,7 +58,7 @@ echo "Retrieved all domains successfully" # try create dkim keys curl --silent --insecure -X 'POST' \ - 'https://mailutest/api/v1/domain/mailu2.io/dkim' \ + 'https://localhost/api/v1/domain/mailu2.io/dkim' \ -H 'accept: application/json' \ -H 'Authorization: apitest' \ -d '' \ @@ -83,7 +83,7 @@ echo "Domain mailu2.io was deleted successfully" # try looking up all users of a domain. There should be 2 users. curl --silent --insecure -X 'GET' \ - 'https://mailutest/api/v1/domain/mailu.io/users' \ + 'https://localhost/api/v1/domain/mailu.io/users' \ -H 'accept: application/json' \ -H 'Authorization: apitest' \ | grep -o "email" | grep -c "email" | grep 2 @@ -98,7 +98,7 @@ echo "Retrieved all users of domain mailu.io successfully" #try to create an alternative curl --silent --insecure -X 'POST' \ - 'https://mailutest/api/v1/alternative' \ + 'https://localhost/api/v1/alternative' \ -H 'accept: application/json' \ -H 'Authorization: apitest' \ -H 'Content-Type: application/json' \ @@ -126,7 +126,7 @@ echo "Retrieved all alternatives successfully" # try to check if an alternative exists curl --silent --insecure -X 'GET' \ - 'https://mailutest/api/v1/alternative/mailu2.io' \ + 'https://localhost/api/v1/alternative/mailu2.io' \ -H 'accept: application/json' \ -H 'Authorization: apitest' \ | grep '{"name": "mailu2.io", "domain": "mailu.io"}' @@ -138,7 +138,7 @@ echo "Lookup for alternative mailu2.io was successful" # try to delete an alternative curl --silent --insecure -X 'DELETE' \ - 'https://mailutest/api/v1/alternative/mailu2.io' \ + 'https://localhost/api/v1/alternative/mailu2.io' \ -H 'accept: application/json' \ -H 'Authorization: apitest' diff --git a/tests/compose/api/03_test_token_interfaces.sh b/tests/compose/api/03_test_token_interfaces.sh index 9180bc7a..f81cc9be 100755 --- a/tests/compose/api/03_test_token_interfaces.sh +++ b/tests/compose/api/03_test_token_interfaces.sh @@ -2,7 +2,7 @@ echo "start token tests" # Try creating a token /token curl --silent --insecure -X 'POST' \ - 'https://mailutest/api/v1/token' \ + 'https://localhost/api/v1/token' \ -H 'accept: application/json' \ -H 'Authorization: apitest' \ -H 'Content-Type: application/json' \ @@ -21,7 +21,7 @@ echo "created a token for user@mailu.io successfully" # Try create a token for a specific user /tokenuser/{email} curl --silent --insecure -X 'POST' \ - 'https://mailutest/api/v1/tokenuser/user%40mailu.io' \ + 'https://localhost/api/v1/tokenuser/user%40mailu.io' \ -H 'accept: application/json' \ -H 'Authorization: apitest' \ -H 'Content-Type: application/json' \ @@ -35,7 +35,7 @@ echo "created a second token for user@mailu.io successfully" # Try retrieving all tokens /token. We expect to retrieve 2 in total. curl --silent --insecure -X 'GET' \ - 'https://mailutest/api/v1/token' \ + 'https://localhost/api/v1/token' \ -H 'accept: application/json' \ -H 'Authorization: apitest' \ | grep -o "id" | grep -c "id" | grep 2 @@ -46,7 +46,7 @@ echo "Retrieved all tokens (2 in total) successfully" # Try finding a specific token /token/{token_id} curl --silent --insecure -X 'GET' \ - 'https://mailutest/api/v1/token/2' \ + 'https://localhost/api/v1/token/2' \ -H 'accept: application/json' \ -H 'Authorization: apitest' \ | grep '"id": 2' @@ -57,7 +57,7 @@ echo "Retrieved token with id 2 successfully" # Try deleting a token /token/{token_id} curl --silent --insecure -X 'DELETE' \ - 'https://mailutest/api/v1/token/1' \ + 'https://localhost/api/v1/token/1' \ -H 'accept: application/json' \ -H 'Authorization: apitest' \ | grep 200 @@ -68,7 +68,7 @@ echo "Deleted token with id 1 successfully" # Try updating a token /token/{token_id} curl --silent --insecure -X 'PATCH' \ - 'https://mailutest/api/v1/token/2' \ + 'https://localhost/api/v1/token/2' \ -H 'accept: application/json' \ -H 'Authorization: apitest' \ -H 'Content-Type: application/json' \ @@ -84,7 +84,7 @@ fi echo "Updated token with id 2 successfully" curl --silent --insecure -X 'GET' \ - 'https://mailutest/api/v1/token/2' \ + 'https://localhost/api/v1/token/2' \ -H 'accept: application/json' \ -H 'Authorization: apitest' \ | grep 'comment": "updated_comment"' @@ -95,7 +95,7 @@ echo "Confirmed that comment field of token with id 2 was correctly updated" # Try looking up all tokens of a specific user /tokenuser/{email} curl --silent --insecure -X 'GET' \ - 'https://mailutest/api/v1/tokenuser/user%40mailu.io' \ + 'https://localhost/api/v1/tokenuser/user%40mailu.io' \ -H 'accept: application/json' \ -H 'Authorization: apitest' \ | grep -o "id" | grep -c "id" | grep 1 diff --git a/tests/compose/api/04_test_relay_interfaces.sh b/tests/compose/api/04_test_relay_interfaces.sh index 9fb493e1..87d1a173 100755 --- a/tests/compose/api/04_test_relay_interfaces.sh +++ b/tests/compose/api/04_test_relay_interfaces.sh @@ -2,7 +2,7 @@ echo "Start 04_test_relay_interfaces.sh" # Try creating a new relay /relay curl --silent --insecure -X 'POST' \ - 'https://mailutest/api/v1/relay' \ + 'https://localhost/api/v1/relay' \ -H 'accept: application/json' \ -H 'Authorization: apitest' \ -H 'Content-Type: application/json' \ @@ -17,7 +17,7 @@ fi echo "created a relay for domain relay1.mailu.io successfully" curl --silent --insecure -X 'POST' \ - 'https://mailutest/api/v1/relay' \ + 'https://localhost/api/v1/relay' \ -H 'accept: application/json' \ -H 'Authorization: apitest' \ -H 'Content-Type: application/json' \ @@ -32,7 +32,7 @@ echo "created a relay for domain relay2.mailu.io successfully" # Try retrieving all relays /relay. We expect to retrieve 2 in total curl --silent --insecure -X 'GET' \ - 'https://mailutest/api/v1/relay' \ + 'https://localhost/api/v1/relay' \ -H 'accept: application/json' \ -H 'Authorization: apitest' \ | grep -o '"name":' | grep -c '"name":' | grep 2 @@ -43,7 +43,7 @@ echo "Retrieved all relays (2 in total) successfully" # Try looking up a specific relay /relay/{name} curl --silent --insecure -X 'GET' \ - 'https://mailutest/api/v1/relay/relay1.mailu.io' \ + 'https://localhost/api/v1/relay/relay1.mailu.io' \ -H 'accept: application/json' \ -H 'Authorization: apitest' \ | grep '"name": "relay1.mailu.io"' @@ -54,7 +54,7 @@ echo "Retrieved the specified relay (relay1.mailu.io) successfully" # Try deleting a specific relay /relay/{name} curl -silent --insecure -X 'DELETE' \ - 'https://mailutest/api/v1/relay/relay2.mailu.io' \ + 'https://localhost/api/v1/relay/relay2.mailu.io' \ -H 'accept: application/json' \ -H 'Authorization: apitest' \ | grep 200 @@ -64,7 +64,7 @@ fi echo "Deleted relay2.mailu.io successfully" curl --silent --insecure -X 'GET' \ - 'https://mailutest/api/v1/relay' \ + 'https://localhost/api/v1/relay' \ -H 'accept: application/json' \ -H 'Authorization: apitest' \ | grep -o '"name":' | grep -c '"name":' | grep 1 @@ -75,7 +75,7 @@ echo "confirmed we only have 1 relay now" # Try updating a specific relay /relay/{name} curl --silent --insecure -X 'PATCH' \ - 'https://mailutest/api/v1/relay/relay1.mailu.io' \ + 'https://localhost/api/v1/relay/relay1.mailu.io' \ -H 'accept: application/json' \ -H 'Authorization: apitest' \ -H 'Content-Type: application/json' \ @@ -89,7 +89,7 @@ fi echo "update of relay was succcessful" curl --silent --insecure -X 'GET' \ - 'https://mailutest/api/v1/relay/relay1.mailu.io' \ + 'https://localhost/api/v1/relay/relay1.mailu.io' \ -H 'accept: application/json' \ -H 'Authorization: apitest' \ | grep anotherName | grep updated_comment diff --git a/tests/compose/api/05_test_alias_interfaces.sh b/tests/compose/api/05_test_alias_interfaces.sh index be23c92e..036091e6 100755 --- a/tests/compose/api/05_test_alias_interfaces.sh +++ b/tests/compose/api/05_test_alias_interfaces.sh @@ -4,7 +4,7 @@ echo "Start 05_test_alias_interfaces.sh" # Try creating a new alias /alias curl --silent --insecure -X 'POST' \ - 'https://mailutest/api/v1/alias' \ + 'https://localhost/api/v1/alias' \ -H 'accept: application/json' \ -H 'Authorization: apitest' \ -H 'Content-Type: application/json' \ @@ -23,7 +23,7 @@ fi echo "Created alias test@mailu.io succcessfully for user@mailu.io and admin@mailu.io" curl --silent --insecure -X 'POST' \ - 'https://mailutest/api/v1/alias' \ + 'https://localhost/api/v1/alias' \ -H 'accept: application/json' \ -H 'Authorization: apitest' \ -H 'Content-Type: application/json' \ @@ -42,7 +42,7 @@ echo "Created alias test2@mailu.io succcessfully for user@mailu.io " # Try retrieving all aliases /alias. We expect to retrieve 2 curl --silent --insecure -X 'GET' \ - 'https://mailutest/api/v1/alias' \ + 'https://localhost/api/v1/alias' \ -H 'accept: application/json' \ -H 'Authorization: apitest' \ | grep -o '"destination":' | grep -c '"destination":' | grep 2 @@ -53,7 +53,7 @@ echo "Successfully retrieved 2 aliases" # Try looking up the aliases for a specific domain /alias/destination/{domain}. We expect to retrieve 2 curl --silent --insecure -X 'GET' \ - 'https://mailutest/api/v1/alias/destination/mailu.io' \ + 'https://localhost/api/v1/alias/destination/mailu.io' \ -H 'accept: application/json' \ -H 'Authorization: apitest' \ | grep -o '"destination":' | grep -c '"destination":' | grep 2 @@ -64,7 +64,7 @@ echo "Successfully retrieved 2 aliases" # Try deleting a specific alias /alias/{alias} curl --silent --insecure -X 'DELETE' \ - 'https://mailutest/api/v1/alias/test2%40mailu.io' \ + 'https://localhost/api/v1/alias/test2%40mailu.io' \ -H 'accept: application/json' \ -H 'Authorization: apitest' \ | grep 200 @@ -75,7 +75,7 @@ echo "Deleted alias test2@mailu.io succcessfully" # Try updating a specific alias /alias/{alias} curl --silent --insecure -X 'PATCH' \ - 'https://mailutest/api/v1/alias/test%40mailu.io' \ + 'https://localhost/api/v1/alias/test%40mailu.io' \ -H 'accept: application/json' \ -H 'Authorization: apitest' \ -H 'Content-Type: application/json' \ @@ -94,7 +94,7 @@ echo "Updated alias test2@mailu.io succcessfully" # Try looking up a specific alias /alias/{alias}. #Check if values were updated correctyly in previous step. response=$(curl --silent --insecure -X 'GET' \ - 'https://mailutest/api/v1/alias/test%40mailu.io' \ + 'https://localhost/api/v1/alias/test%40mailu.io' \ -H 'accept: application/json' \ -H 'Authorization: apitest') echo $response | grep 'admin@mailu.io' From 9935cb48cd9abd4575296c60280770f638e89955 Mon Sep 17 00:00:00 2001 From: Dimitri Huisman Date: Fri, 22 Mar 2024 16:53:11 +0000 Subject: [PATCH 14/58] Fix bug 3068. Spam messages were always marked as read. --- core/dovecot/conf/report-spam.sieve | 1 - docs/webadministration.rst | 2 ++ towncrier/newsfragments/3068.bugfix | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 towncrier/newsfragments/3068.bugfix diff --git a/core/dovecot/conf/report-spam.sieve b/core/dovecot/conf/report-spam.sieve index 87fd515e..df7d276c 100644 --- a/core/dovecot/conf/report-spam.sieve +++ b/core/dovecot/conf/report-spam.sieve @@ -1,5 +1,4 @@ require "imap4flags"; require "vnd.dovecot.execute"; -setflag "\\seen"; execute :pipe "spam"; diff --git a/docs/webadministration.rst b/docs/webadministration.rst index 3ce53fe8..3a3af6a6 100644 --- a/docs/webadministration.rst +++ b/docs/webadministration.rst @@ -93,6 +93,8 @@ The exception to this rule, are email messages with an extremely high spam score When the spam filter is enabled, received email messages will be moved to the logged in user's inbox folder or junk folder depending on the user defined spam filter tolerance. +When `Enable marking spam mails as read` is enabled. Received messages moved to the Junk folder are marked as read. When this setting is disabled. Received messages moved to the Junk folder are not marked as read. They remain marked as unread. + The user defined spam filter tolerance determines when an email is classified as ham (moved to the inbox folder) or spam (moved to the junk folder). The default value is 80%. The lower the spam filter tolerance, the more false positives (ham classified as spam). The higher the spam filter tolerance, the more false negatives (spam classified as ham). For more information see the :ref:`antispam documentation `. diff --git a/towncrier/newsfragments/3068.bugfix b/towncrier/newsfragments/3068.bugfix new file mode 100644 index 00000000..b7689088 --- /dev/null +++ b/towncrier/newsfragments/3068.bugfix @@ -0,0 +1,2 @@ +When "Enable marking spam mails as read" was disabled, new spam messages were still marked as read. +Updated documentation with the setting "Enable marking spam mails as read". From 9e468100a034285cbb17f70a18f68c01c25a33c1 Mon Sep 17 00:00:00 2001 From: Dimitri Huisman Date: Sat, 23 Mar 2024 14:14:43 +0000 Subject: [PATCH 15/58] Fix issues with forward_destination in api and user form * form * Fixed: Internal error occurred if an empty forward_destination was entered and forward_enabled was false * Fixed: form did not check if forward_destination is empty. * Fixed: form marked forward_destination field as read-only upon reloading form upon validation error * api - create user and update/patch user * Create/Patch user did not check if forward_destination email address is valid * Create/Patch user did not check if forward_destination is present and forward_enabled is true --- core/admin/mailu/api/v1/user.py | 21 ++++++++++++++++++--- core/admin/mailu/ui/views/users.py | 10 +++++++++- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/core/admin/mailu/api/v1/user.py b/core/admin/mailu/api/v1/user.py index 9dc6279e..b0c9d4da 100644 --- a/core/admin/mailu/api/v1/user.py +++ b/core/admin/mailu/api/v1/user.py @@ -109,6 +109,10 @@ class Users(Resource): data = api.payload if not validators.email(data['email']): return { 'code': 400, 'message': f'Provided email address {data["email"]} is not a valid email address'}, 400 + if 'forward_destination' in data and len(data['forward_destination']) > 0: + for dest in data['forward_destination']: + if not validators.email(dest): + return { 'code': 400, 'message': f'Provided forward destination email address {dest} is not a valid email address'}, 400 localpart, domain_name = data['email'].lower().rsplit('@', 1) domain_found = models.Domain.query.get(domain_name) if not domain_found: @@ -118,6 +122,9 @@ class Users(Resource): email_found = models.User.query.filter_by(email=data['email']).first() if email_found: return { 'code': 409, 'message': f'User {data["email"]} already exists'}, 409 + if 'forward_enabled' in data and data['forward_enabled'] is True: + if ('forward_destination' in data and len(data['forward_destination']) == 0) or 'forward_destination' not in data: + return { 'code': 400, 'message': f'forward_destination is mandatory when forward_enabled is true'}, 400 user_new = models.User(email=data['email']) if 'raw_password' in data: @@ -140,7 +147,7 @@ class Users(Resource): user_new.allow_spoofing = data['allow_spoofing'] if 'forward_enabled' in data: user_new.forward_enabled = data['forward_enabled'] - if 'forward_destination' in data: + if 'forward_destination' in data and len(data['forward_destination']) > 0: user_new.forward_destination = data['forward_destination'] if 'forward_keep' in data: user_new.forward_keep = data['forward_keep'] @@ -203,9 +210,16 @@ class User(Resource): data = api.payload if not validators.email(email): return { 'code': 400, 'message': f'Provided email address {email} is not a valid email address'}, 400 + if 'forward_destination' in data and len(data['forward_destination']) > 0: + for dest in data['forward_destination']: + if not validators.email(dest): + return { 'code': 400, 'message': f'Provided forward destination email address {dest} 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 + if ('forward_enabled' in data and data['forward_enabled'] is True) or ('forward_enabled' not in data and user_found.forward_enabled): + if ('forward_destination' in data and len(data['forward_destination']) == 0): + return { 'code': 400, 'message': f'forward_destination is mandatory when forward_enabled is true'}, 400 if 'raw_password' in data: user_found.set_password(data['raw_password']) @@ -227,8 +241,9 @@ class User(Resource): user_found.allow_spoofing = data['allow_spoofing'] if 'forward_enabled' in data: user_found.forward_enabled = data['forward_enabled'] - if 'forward_destination' in data: - user_found.forward_destination = data['forward_destination'] + if 'forward_destination' in data and len(data['forward_destination']) > 0: + if len(data['forward_destination']) == 0: + user_found.forward_destination = data['forward_destination'] if 'forward_keep' in data: user_found.forward_keep = data['forward_keep'] if 'reply_enabled' in data: diff --git a/core/admin/mailu/ui/views/users.py b/core/admin/mailu/ui/views/users.py index b0a945ad..d3816c4a 100644 --- a/core/admin/mailu/ui/views/users.py +++ b/core/admin/mailu/ui/views/users.py @@ -93,7 +93,12 @@ def user_settings(user_email): form = forms.UserSettingsForm(obj=user) utils.formatCSVField(form.forward_destination) if form.validate_on_submit(): - form.forward_destination.data = form.forward_destination.data.replace(" ","").split(",") + if form.forward_enabled.data and (form.forward_destination.data in ['', None] or type(form.forward_destination.data) is list): + flask.flash('Destination email address is missing', 'error') + user.forward_enabled = True + return flask.render_template('user/settings.html', form=form, user=user) + if form.forward_enabled.data: + form.forward_destination.data = form.forward_destination.data.replace(" ","").split(",") form.populate_obj(user) models.db.session.commit() form.forward_destination.data = ", ".join(form.forward_destination.data) @@ -101,6 +106,9 @@ def user_settings(user_email): if user_email: return flask.redirect( flask.url_for('.user_list', domain_name=user.domain.name)) + elif form.is_submitted() and not form.validate(): + user.forward_enabled = form.forward_enabled.data + return flask.render_template('user/settings.html', form=form, user=user) return flask.render_template('user/settings.html', form=form, user=user) def _process_password_change(form, user_email): From 2896078f1f2c5e47e01326e7f7cd8b2e858ad112 Mon Sep 17 00:00:00 2001 From: AJ Jordan Date: Mon, 25 Mar 2024 23:31:26 -0400 Subject: [PATCH 16/58] Fix typo --- setup/templates/steps/compose/03_expose.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/templates/steps/compose/03_expose.html b/setup/templates/steps/compose/03_expose.html index e14e4bbc..e928fccf 100644 --- a/setup/templates/steps/compose/03_expose.html +++ b/setup/templates/steps/compose/03_expose.html @@ -49,7 +49,7 @@ avoid generic all-interfaces addresses like 0.0.0.0 or ::Read this: Mailu requires a validating, DNSSEC-enabled DNS resolver to function. Be sure to read our FAQ entry on the topic.

-

You server will be available under a main hostname but may expose multiple public +

Your server will be available under a main hostname but may expose multiple public hostnames. Every e-mail domain that points to this server must have one of the hostnames in its MX record. Hostnames must be comma-separated. If you're having trouble accessing your admin interface, make sure it is the first entry here (and possibly the From dbf021a0f324ede2b56d87e1160bef154821288a Mon Sep 17 00:00:00 2001 From: pavuki Date: Tue, 26 Mar 2024 08:47:34 +0100 Subject: [PATCH 17/58] Add Belarusian translation. --- .../be_BY/LC_MESSAGES/messages.po | 767 ++++++++++++++++++ 1 file changed, 767 insertions(+) create mode 100644 core/admin/mailu/translations/be_BY/LC_MESSAGES/messages.po diff --git a/core/admin/mailu/translations/be_BY/LC_MESSAGES/messages.po b/core/admin/mailu/translations/be_BY/LC_MESSAGES/messages.po new file mode 100644 index 00000000..3681a902 --- /dev/null +++ b/core/admin/mailu/translations/be_BY/LC_MESSAGES/messages.po @@ -0,0 +1,767 @@ +msgid "" +msgstr "" +"Project-Id-Version: Mailu\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2022-05-22 18:47+0200\n" +"PO-Revision-Date: 2024-03-26 08:46+0100\n" +"Last-Translator: Jaume Barber \n" +"Language-Team: \n" +"Language: be_BY\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%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n" +"Generated-By: Babel 2.3.4\n" +"X-Generator: Poedit 3.4.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-падобны сынтаксіс" + +#: mailu/ui/forms.py:149 +msgid "Admin email" +msgstr "Адрас адміністратара" + +#: mailu/ui/forms.py:150 mailu/ui/forms.py:155 mailu/ui/forms.py:168 +msgid "Submit" +msgstr "Прыняць" + +#: mailu/ui/forms.py:154 +msgid "Manager email" +msgstr "Адрас мэнэджэра" + +#: mailu/ui/forms.py:159 +msgid "Protocol" +msgstr "Пратакол" + +#: mailu/ui/forms.py:162 +msgid "Hostname or IP" +msgstr "Імя хосту альбо IP" + +#: mailu/ui/forms.py:163 mailu/ui/templates/client.html:20 +#: mailu/ui/templates/client.html:45 +msgid "TCP port" +msgstr "Порт TCP" + +#: mailu/ui/forms.py:164 +msgid "Enable TLS" +msgstr "Уключыць TLS" + +#: mailu/ui/forms.py:165 mailu/ui/templates/client.html:28 +#: mailu/ui/templates/client.html:53 mailu/ui/templates/fetch/list.html:21 +msgid "Username" +msgstr "Імя карыстальніка" + +#: mailu/ui/forms.py:167 +msgid "Keep emails on the server" +msgstr "Захоўваць лісты на сэрвэры" + +#: mailu/ui/forms.py:172 +msgid "Announcement subject" +msgstr "Тэма абвесткі" + +#: mailu/ui/forms.py:174 +msgid "Announcement body" +msgstr "Зьмест абвесткі" + +#: mailu/ui/forms.py:176 +msgid "Send" +msgstr "Адаслаць" + +#: mailu/ui/templates/announcement.html:4 +msgid "Public announcement" +msgstr "Публічная аб'ява" + +#: mailu/ui/templates/antispam.html:4 mailu/ui/templates/sidebar.html:80 +#: mailu/ui/templates/user/settings.html:19 +msgid "Antispam" +msgstr "Антыспам" + +#: mailu/ui/templates/antispam.html:8 +msgid "RSPAMD status page" +msgstr "Старонка статусу RSPAMD" + +#: mailu/ui/templates/client.html:8 +msgid "configure your email client" +msgstr "Наладзьце свой паштовы кліент" + +#: mailu/ui/templates/client.html:13 +msgid "Incoming mail" +msgstr "Уваходная пошта" + +#: mailu/ui/templates/client.html:16 mailu/ui/templates/client.html:41 +msgid "Mail protocol" +msgstr "Паштовы пратакол" + +#: mailu/ui/templates/client.html:24 mailu/ui/templates/client.html:49 +msgid "Server name" +msgstr "Імя сэрвэру" + +#: mailu/ui/templates/client.html:38 +msgid "Outgoing mail" +msgstr "Выходная пошта" + +#: mailu/ui/templates/confirm.html:4 +msgid "Confirm action" +msgstr "Пацьвердзіце дзеяньне" + +#: mailu/ui/templates/confirm.html:13 +#, python-format +msgid "You are about to %(action)s. Please confirm your action." +msgstr "" +"Вы зьбіраецеся зьдзейсьніць %(action)s. Калі ласка пацьвердзіце ваша " +"дзеяньне." + +#: mailu/ui/templates/docker-error.html:4 +msgid "Docker error" +msgstr "Памылка Docker" + +#: mailu/ui/templates/docker-error.html:12 +msgid "An error occurred while talking to the Docker server." +msgstr "Адбылася памылка пры зьвяртаньні да сэрвэру Docker." + +#: mailu/ui/templates/macros.html:129 +msgid "copy to clipboard" +msgstr "Скапіраваць у буфэр абмену" + +#: mailu/ui/templates/sidebar.html:15 +msgid "My account" +msgstr "Мой уліковы запіс" + +#: mailu/ui/templates/sidebar.html:19 mailu/ui/templates/user/list.html:37 +msgid "Settings" +msgstr "Налады" + +#: mailu/ui/templates/sidebar.html:31 mailu/ui/templates/user/list.html:38 +msgid "Auto-reply" +msgstr "Аўтаматычны адказ" + +#: mailu/ui/templates/fetch/list.html:4 mailu/ui/templates/sidebar.html:37 +#: mailu/ui/templates/user/list.html:39 +msgid "Fetched accounts" +msgstr "Уліковая запісы пабочных сэрвэраў" + +#: mailu/ui/templates/sidebar.html:43 mailu/ui/templates/token/list.html:4 +msgid "Authentication tokens" +msgstr "Аўтэнтыфікацыйныя токены" + +#: mailu/ui/templates/sidebar.html:56 +msgid "Administration" +msgstr "Адміністраваньне " + +#: mailu/ui/templates/sidebar.html:62 +msgid "Announcement" +msgstr "Абвестка" + +#: mailu/ui/templates/sidebar.html:68 +msgid "Administrators" +msgstr "Адміністратары" + +#: mailu/ui/templates/sidebar.html:74 +msgid "Relayed domains" +msgstr "Рэлейныя дамены" + +#: mailu/ui/templates/sidebar.html:88 +msgid "Mail domains" +msgstr "Паштовыя дамены" + +#: mailu/ui/templates/sidebar.html:99 +msgid "Webmail" +msgstr "Электронная пошта" + +#: mailu/ui/templates/sidebar.html:135 +msgid "Sign out" +msgstr "Выйсьці" + +#: mailu/ui/templates/working.html:4 +msgid "We are still working on this feature!" +msgstr "Мы яшчэ працуем над дадзеным функцыяналам!" + +#: mailu/ui/templates/admin/create.html:4 +msgid "Add a global administrator" +msgstr "Дадаць глябальнага адміністратара" + +#: mailu/ui/templates/admin/list.html:4 +msgid "Global administrators" +msgstr "Глябальныя адміністратары" + +#: mailu/ui/templates/admin/list.html:9 +msgid "Add administrator" +msgstr "Дадаць адміністратара" + +#: mailu/ui/templates/admin/list.html:17 mailu/ui/templates/alias/list.html:19 +#: mailu/ui/templates/alternative/list.html:19 +#: mailu/ui/templates/domain/list.html:17 mailu/ui/templates/fetch/list.html:19 +#: mailu/ui/templates/manager/list.html:19 +#: mailu/ui/templates/relay/list.html:17 mailu/ui/templates/token/list.html:19 +#: mailu/ui/templates/user/list.html:19 +msgid "Actions" +msgstr "Дзеяньні" + +#: mailu/ui/templates/admin/list.html:18 mailu/ui/templates/alias/list.html:20 +#: mailu/ui/templates/manager/list.html:20 mailu/ui/templates/user/list.html:21 +msgid "Email" +msgstr "Электронная пошта" + +#: mailu/ui/templates/admin/list.html:25 mailu/ui/templates/alias/list.html:32 +#: mailu/ui/templates/alternative/list.html:29 +#: mailu/ui/templates/domain/list.html:34 mailu/ui/templates/fetch/list.html:34 +#: mailu/ui/templates/manager/list.html:27 +#: mailu/ui/templates/relay/list.html:30 mailu/ui/templates/token/list.html:30 +#: mailu/ui/templates/user/list.html:34 +msgid "Delete" +msgstr "Выдаліць" + +#: mailu/ui/templates/alias/create.html:4 +msgid "Create alias" +msgstr "Стварыць псэўданім" + +#: mailu/ui/templates/alias/edit.html:4 +msgid "Edit alias" +msgstr "Зьмяніць псэўданім" + +#: mailu/ui/templates/alias/list.html:4 +msgid "Alias list" +msgstr "Сьпіс псэўданімаў" + +#: mailu/ui/templates/alias/list.html:12 +msgid "Add alias" +msgstr "Дадаць псэўданім" + +#: mailu/ui/templates/alias/list.html:23 +#: mailu/ui/templates/alternative/list.html:21 +#: mailu/ui/templates/domain/list.html:23 mailu/ui/templates/fetch/list.html:25 +#: mailu/ui/templates/relay/list.html:21 mailu/ui/templates/token/list.html:22 +#: mailu/ui/templates/user/list.html:25 +msgid "Created" +msgstr "Створана" + +#: mailu/ui/templates/alias/list.html:24 +#: mailu/ui/templates/alternative/list.html:22 +#: mailu/ui/templates/domain/list.html:24 mailu/ui/templates/fetch/list.html:26 +#: mailu/ui/templates/relay/list.html:22 mailu/ui/templates/token/list.html:23 +#: mailu/ui/templates/user/list.html:26 +msgid "Last edit" +msgstr "Апошняя зьмена" + +#: mailu/ui/templates/alias/list.html:31 mailu/ui/templates/domain/list.html:33 +#: mailu/ui/templates/fetch/list.html:33 mailu/ui/templates/relay/list.html:29 +#: mailu/ui/templates/user/list.html:33 +msgid "Edit" +msgstr "Зьмяніць" + +#: mailu/ui/templates/alternative/create.html:4 +msgid "Create alternative domain" +msgstr "Стварыць альтэрнатыўны дамен" + +#: mailu/ui/templates/alternative/list.html:4 +msgid "Alternative domain list" +msgstr "Сьпіс альтэрнатыўных даменаў" + +#: mailu/ui/templates/alternative/list.html:12 +msgid "Add alternative" +msgstr "Дадаць альтэрнатыўны дамен" + +#: mailu/ui/templates/alternative/list.html:20 +msgid "Name" +msgstr "Імя" + +#: mailu/ui/templates/domain/create.html:4 +#: mailu/ui/templates/domain/list.html:9 +msgid "New domain" +msgstr "Новы дамен" + +#: mailu/ui/templates/domain/details.html:4 +msgid "Domain details" +msgstr "Падрабязнасьці дамену" + +#: mailu/ui/templates/domain/details.html:15 +msgid "Regenerate keys" +msgstr "Згенерыраваць ключы нанова" + +#: mailu/ui/templates/domain/details.html:17 +msgid "Generate keys" +msgstr "Згенерыраваць ключы" + +#: mailu/ui/templates/domain/details.html:30 +msgid "DNS MX entry" +msgstr "Запіс DNS MX" + +#: mailu/ui/templates/domain/details.html:34 +msgid "DNS SPF entries" +msgstr "Запісы DNS SPF" + +#: mailu/ui/templates/domain/details.html:40 +msgid "DKIM public key" +msgstr "Публічны ключ DKIM" + +#: mailu/ui/templates/domain/details.html:44 +msgid "DNS DKIM entry" +msgstr "Запіс DNS DKIM" + +#: mailu/ui/templates/domain/details.html:48 +msgid "DNS DMARC entry" +msgstr "Запіс DNS DMARC" + +#: mailu/ui/templates/domain/details.html:58 +msgid "DNS TLSA entry" +msgstr "Запіс DNS TLSA" + +#: mailu/ui/templates/domain/details.html:63 +msgid "DNS client auto-configuration entries" +msgstr "Запісы аўтаканфігурацыі DNS" + +#: mailu/ui/templates/domain/edit.html:4 +msgid "Edit domain" +msgstr "Зьмяніць дамен" + +#: mailu/ui/templates/domain/list.html:4 +msgid "Domain list" +msgstr "Сьпіс даменаў" + +#: mailu/ui/templates/domain/list.html:18 +msgid "Manage" +msgstr "Кіраваньне" + +#: mailu/ui/templates/domain/list.html:20 +msgid "Mailbox count" +msgstr "Колькасьць паштовых скрынь" + +#: mailu/ui/templates/domain/list.html:21 +msgid "Alias count" +msgstr "Колькасьць псэўданімаў" + +#: mailu/ui/templates/domain/list.html:31 +msgid "Details" +msgstr "Падрабязна" + +#: mailu/ui/templates/domain/list.html:38 +msgid "Users" +msgstr "Карыстальнікі" + +#: mailu/ui/templates/domain/list.html:39 +msgid "Aliases" +msgstr "Псэўданімы" + +#: mailu/ui/templates/domain/list.html:40 +msgid "Managers" +msgstr "Мэнэджэры" + +#: mailu/ui/templates/domain/list.html:42 +msgid "Alternatives" +msgstr "Альтэрнатыўныя дамены" + +#: mailu/ui/templates/domain/signup.html:13 +msgid "" +"In order to register a new domain, you must first setup the\n" +" domain zone so that the domain MX points to this server" +msgstr "" +"Каб зарэгістраваць новы дамен, вы мусіце спачатку наладзіць\n" +"    даменная зона, так каб дамен MX паказваў на гэты сэрвер" + +#: mailu/ui/templates/domain/signup.html:18 +msgid "" +"If you do not know how to setup an MX record for your DNS " +"zone,\n" +" please contact your DNS provider or administrator. Also, please wait a\n" +" couple minutes after the MX is set so the local server " +"cache\n" +" expires." +msgstr "" +"Калі вы не ведаеце як наладзіць запіс MX для сваёй зоны " +"DNS,\n" +"    калі ласка, зьвяжыцеся з вашым пастаўшчыком DNS альбо адміністратарам. " +"Таксама, калі ласка, пачакайце\n" +"    некалькі хвілін пасьля таго як запіс MX быў зададзены, " +"каб тэрмін дзеяньня кэшу лякальнага\n" +"    сэрверу скончыўся." + +#: mailu/ui/templates/fetch/create.html:4 +msgid "Add a fetched account" +msgstr "Дадаць уліковы запіс пабочнага сэрвэру" + +#: mailu/ui/templates/fetch/edit.html:4 +msgid "Update a fetched account" +msgstr "Абнавіць уліковы запіс пабочнага сэрвэру" + +#: mailu/ui/templates/fetch/list.html:12 +msgid "Add an account" +msgstr "Дадаць уліковы запіс" + +#: mailu/ui/templates/fetch/list.html:20 +msgid "Endpoint" +msgstr "Канчатковы пункт" + +#: mailu/ui/templates/fetch/list.html:22 +msgid "Keep emails" +msgstr "Захоўваць лісты" + +#: mailu/ui/templates/fetch/list.html:23 +msgid "Last check" +msgstr "Апошняя праверка" + +#: mailu/ui/templates/fetch/list.html:24 +msgid "Status" +msgstr "Статус" + +#: mailu/ui/templates/fetch/list.html:38 +msgid "yes" +msgstr "так" + +#: mailu/ui/templates/fetch/list.html:38 +msgid "no" +msgstr "не" + +#: mailu/ui/templates/manager/create.html:4 +msgid "Add a manager" +msgstr "Дадаць мэнэджара" + +#: mailu/ui/templates/manager/list.html:4 +msgid "Manager list" +msgstr "Сьпіс мэнэджараў" + +#: mailu/ui/templates/manager/list.html:12 +msgid "Add manager" +msgstr "Дадаць мэнэджэра" + +#: mailu/ui/templates/relay/create.html:4 +msgid "New relay domain" +msgstr "Новы рэлейны дамен" + +#: mailu/ui/templates/relay/edit.html:4 +msgid "Edit relayed domain" +msgstr "Зьмяніць рэлейным дамен" + +#: mailu/ui/templates/relay/list.html:4 +msgid "Relayed domain list" +msgstr "Сьпіс рэлейных даменаў" + +#: mailu/ui/templates/relay/list.html:9 +msgid "New relayed domain" +msgstr "Новы рэлейны дамен" + +#: mailu/ui/templates/token/create.html:4 +msgid "Create an authentication token" +msgstr "Стварыць аўтэнтыфікацыйны токен" + +#: mailu/ui/templates/token/list.html:12 +msgid "New token" +msgstr "Новы токен" + +#: mailu/ui/templates/user/create.html:4 +msgid "New user" +msgstr "Новы карыстальнік" + +#: mailu/ui/templates/user/create.html:15 +msgid "General" +msgstr "Агульныя" + +#: mailu/ui/templates/user/create.html:23 +msgid "Features and quotas" +msgstr "Функцыі і квоты" + +#: mailu/ui/templates/user/edit.html:4 +msgid "Edit user" +msgstr "Зьмяніць карыстальніка" + +#: mailu/ui/templates/user/list.html:4 +msgid "User list" +msgstr "Сьпіс карыстальнікаў" + +#: mailu/ui/templates/user/list.html:12 +msgid "Add user" +msgstr "Дадаць карыстальніка" + +#: mailu/ui/templates/user/list.html:20 mailu/ui/templates/user/settings.html:4 +msgid "User settings" +msgstr "Налады карыстальніка" + +#: mailu/ui/templates/user/list.html:22 +msgid "Features" +msgstr "Функцыі" + +#: mailu/ui/templates/user/password.html:4 +msgid "Password update" +msgstr "Зьмена паролю" + +#: mailu/ui/templates/user/reply.html:4 +msgid "Automatic reply" +msgstr "Аўтаматычны адказ" + +#: mailu/ui/templates/user/settings.html:27 +msgid "Auto-forward" +msgstr "Аўтаматычная перасылка" + +#: mailu/ui/templates/user/signup_domain.html:8 +msgid "pick a domain for the new account" +msgstr "выбраць дамен для новага ўліковага запісу" + +#: mailu/ui/templates/user/signup_domain.html:14 +msgid "Domain" +msgstr "Дамен" + +#: mailu/ui/templates/user/signup_domain.html:15 +msgid "Available slots" +msgstr "Даступныя адтуліны" + +#~ msgid "Spam filter threshold" +#~ msgstr "Порог чувствительности спам-фильтра" + +#~ msgid "Your account" +#~ msgstr "Ваша учетная запись" + +#~ msgid "to access the administration tools" +#~ msgstr "для доступа к утилитам администрирования" + +#~ msgid "Services status" +#~ msgstr "Статусы сервисов" + +#~ msgid "Service" +#~ msgstr "Сервис" + +#~ msgid "PID" +#~ msgstr "PID" + +#~ msgid "Image" +#~ msgstr "Изображение" + +#~ msgid "Started" +#~ msgstr "Начато" + +#~ msgid "Last update" +#~ msgstr "Последнее обновление" + +#~ msgid "from" +#~ msgstr "От" + +#~ msgid "Forward emails" +#~ msgstr "Перенаправлять письма" + +#~ msgid "General settings" +#~ msgstr "Общие настройки" From 9f4d9c5ea2565ff856e87d5c1130e3eebe993386 Mon Sep 17 00:00:00 2001 From: pavuki Date: Tue, 26 Mar 2024 08:55:16 +0100 Subject: [PATCH 18/58] Update AUTHORS.md. --- AUTHORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.md b/AUTHORS.md index cdaff108..3e8bf0c1 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -21,6 +21,7 @@ Other contributors: - "ofthesun9" - French translation - "SunMar" - Dutch translation - "Marty Hou" - Chinese Simple translation + - "Spoooyders" - Belarusian 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) From 6ef37cad741035efe2520a0aa51a179ff2869ea3 Mon Sep 17 00:00:00 2001 From: pavuki Date: Tue, 26 Mar 2024 08:58:58 +0100 Subject: [PATCH 19/58] Add an entry to newsfragments. --- towncrier/newsfragments/3207.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 towncrier/newsfragments/3207.feature diff --git a/towncrier/newsfragments/3207.feature b/towncrier/newsfragments/3207.feature new file mode 100644 index 00000000..a3ea2baf --- /dev/null +++ b/towncrier/newsfragments/3207.feature @@ -0,0 +1 @@ +Add belarusian translation From 211ffb6d1f5e7bf6b4ed0ef13ef07156649f9844 Mon Sep 17 00:00:00 2001 From: pavuki Date: Tue, 26 Mar 2024 09:08:00 +0100 Subject: [PATCH 20/58] Fix Belarusian translation. --- .../mailu/translations/be_BY/LC_MESSAGES/messages.po | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/admin/mailu/translations/be_BY/LC_MESSAGES/messages.po b/core/admin/mailu/translations/be_BY/LC_MESSAGES/messages.po index 3681a902..0db35f2a 100644 --- a/core/admin/mailu/translations/be_BY/LC_MESSAGES/messages.po +++ b/core/admin/mailu/translations/be_BY/LC_MESSAGES/messages.po @@ -3,8 +3,8 @@ msgstr "" "Project-Id-Version: Mailu\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "POT-Creation-Date: 2022-05-22 18:47+0200\n" -"PO-Revision-Date: 2024-03-26 08:46+0100\n" -"Last-Translator: Jaume Barber \n" +"PO-Revision-Date: 2024-03-26 09:07+0100\n" +"Last-Translator: spoooyders \n" "Language-Team: \n" "Language: be_BY\n" "MIME-Version: 1.0\n" @@ -309,7 +309,7 @@ msgstr "Старонка статусу RSPAMD" #: mailu/ui/templates/client.html:8 msgid "configure your email client" -msgstr "Наладзьце свой паштовы кліент" +msgstr "наладзьце свой паштовы кліент" #: mailu/ui/templates/client.html:13 msgid "Incoming mail" @@ -348,7 +348,7 @@ msgstr "Адбылася памылка пры зьвяртаньні да сэ #: mailu/ui/templates/macros.html:129 msgid "copy to clipboard" -msgstr "Скапіраваць у буфэр абмену" +msgstr "скапіраваць у буфэр абмену" #: mailu/ui/templates/sidebar.html:15 msgid "My account" @@ -373,7 +373,7 @@ msgstr "Аўтэнтыфікацыйныя токены" #: mailu/ui/templates/sidebar.html:56 msgid "Administration" -msgstr "Адміністраваньне " +msgstr "Адміністраваньне" #: mailu/ui/templates/sidebar.html:62 msgid "Announcement" From f21bfa84d49d123a6c040e3037f744fada4b7c3c Mon Sep 17 00:00:00 2001 From: pavuki Date: Tue, 26 Mar 2024 09:24:02 +0100 Subject: [PATCH 21/58] Fix Belarusian translation. --- .../be_BY/LC_MESSAGES/messages.po | 117 +++++------------- 1 file changed, 34 insertions(+), 83 deletions(-) diff --git a/core/admin/mailu/translations/be_BY/LC_MESSAGES/messages.po b/core/admin/mailu/translations/be_BY/LC_MESSAGES/messages.po index 0db35f2a..28cbf486 100644 --- a/core/admin/mailu/translations/be_BY/LC_MESSAGES/messages.po +++ b/core/admin/mailu/translations/be_BY/LC_MESSAGES/messages.po @@ -3,15 +3,15 @@ msgstr "" "Project-Id-Version: Mailu\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "POT-Creation-Date: 2022-05-22 18:47+0200\n" -"PO-Revision-Date: 2024-03-26 09:07+0100\n" +"PO-Revision-Date: 2024-03-26 09:23+0100\n" "Last-Translator: spoooyders \n" "Language-Team: \n" "Language: be_BY\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%10==1 && n%100!=11 ? 0 : n%10>=2 && " -"n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && " +"(n%100<12 || n%100>14) ? 1 : 2);\n" "Generated-By: Babel 2.3.4\n" "X-Generator: Poedit 3.4.2\n" @@ -20,8 +20,8 @@ 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 +#: 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 "Пароль" @@ -59,8 +59,8 @@ msgstr "Сайт" msgid "Help" msgstr "Дапамога" -#: mailu/sso/templates/sidebar_sso.html:35 -#: mailu/ui/templates/domain/signup.html:4 mailu/ui/templates/sidebar.html:127 +#: 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 "Зарэгістраваць дамен" @@ -78,8 +78,7 @@ msgstr "Няправільны адрас электроннай пошты." msgid "Confirm" msgstr "Пацвердзіць" -#: mailu/ui/forms.py:48 mailu/ui/forms.py:58 -#: mailu/ui/templates/domain/details.html:26 +#: 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 "Даменнае імя" @@ -101,15 +100,14 @@ 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 +#: 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 +#: 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 "Захаваць" @@ -154,8 +152,7 @@ msgstr "Дазволіць доступ праз IMAP" msgid "Allow POP3 access" msgstr "Дазволіць доступ праз POP3" -#: mailu/ui/forms.py:85 mailu/ui/forms.py:101 -#: mailu/ui/templates/user/settings.html:15 +#: mailu/ui/forms.py:85 mailu/ui/forms.py:101 mailu/ui/templates/user/settings.html:15 msgid "Displayed name" msgstr "Паказваць імя" @@ -187,8 +184,7 @@ msgstr "Уключыць пераадрасацыю" 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 +#: mailu/ui/forms.py:107 mailu/ui/forms.py:143 mailu/ui/templates/alias/list.html:21 msgid "Destination" msgstr "Адрас атрымальніка" @@ -226,7 +222,7 @@ msgstr "Канец водпуску" #: mailu/ui/forms.py:124 msgid "Update" -msgstr "Обновить" +msgstr "Абнавіць" #: mailu/ui/forms.py:129 msgid "Your token (write it down, as it will never be displayed again)" @@ -242,7 +238,7 @@ msgstr "Псэўданім" #: mailu/ui/forms.py:142 msgid "Use SQL LIKE Syntax (e.g. for catch-all aliases)" -msgstr "Выкарыстоўваць SQL-падобны сынтаксіс" +msgstr "Выкарыстоўваць SQL-падобны сынтаксіс (напрылкад, для ўсеагульных псэўданімаў)" #: mailu/ui/forms.py:149 msgid "Admin email" @@ -335,8 +331,7 @@ msgstr "Пацьвердзіце дзеяньне" #, python-format msgid "You are about to %(action)s. Please confirm your action." msgstr "" -"Вы зьбіраецеся зьдзейсьніць %(action)s. Калі ласка пацьвердзіце ваша " -"дзеяньне." +"Вы зьбіраецеся зьдзейсьніць %(action)s. Калі ласка пацьвердзіце ваша дзеяньне." #: mailu/ui/templates/docker-error.html:4 msgid "Docker error" @@ -416,9 +411,8 @@ 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/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" @@ -430,9 +424,8 @@ 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/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" @@ -454,16 +447,14 @@ msgstr "Сьпіс псэўданімаў" msgid "Add alias" msgstr "Дадаць псэўданім" -#: mailu/ui/templates/alias/list.html:23 -#: mailu/ui/templates/alternative/list.html:21 +#: 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/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 @@ -492,8 +483,7 @@ msgstr "Дадаць альтэрнатыўны дамен" msgid "Name" msgstr "Імя" -#: mailu/ui/templates/domain/create.html:4 -#: mailu/ui/templates/domain/list.html:9 +#: mailu/ui/templates/domain/create.html:4 mailu/ui/templates/domain/list.html:9 msgid "New domain" msgstr "Новы дамен" @@ -587,20 +577,17 @@ msgstr "" #: mailu/ui/templates/domain/signup.html:18 msgid "" -"If you do not know how to setup an MX record for your DNS " -"zone,\n" +"If you do not know how to setup an MX record for your DNS zone,\n" " please contact your DNS provider or administrator. Also, please wait a\n" -" couple minutes after the MX is set so the local server " -"cache\n" +" couple minutes after the MX is set so the local server cache\n" " expires." msgstr "" -"Калі вы не ведаеце як наладзіць запіс MX для сваёй зоны " -"DNS,\n" -"    калі ласка, зьвяжыцеся з вашым пастаўшчыком DNS альбо адміністратарам. " -"Таксама, калі ласка, пачакайце\n" -"    некалькі хвілін пасьля таго як запіс MX быў зададзены, " -"каб тэрмін дзеяньня кэшу лякальнага\n" -"    сэрверу скончыўся." +"Калі вы не ведаеце як наладзіць запіс MX для сваёй зоны DNS,\n" +"    калі ласка, зьвяжыцеся з вашым пастаўшчыком DNS альбо адміністратарам. Таксама, " +"калі ласка, пачакайце\n" +"    некалькі хвілін пасьля таго як запіс MX быў зададзены, каб " +"скончыўся тэрмін дзеяньня кэшу\n" +"    лякальнага сэрвэру." #: mailu/ui/templates/fetch/create.html:4 msgid "Add a fetched account" @@ -728,40 +715,4 @@ msgstr "Дамен" #: mailu/ui/templates/user/signup_domain.html:15 msgid "Available slots" -msgstr "Даступныя адтуліны" - -#~ msgid "Spam filter threshold" -#~ msgstr "Порог чувствительности спам-фильтра" - -#~ msgid "Your account" -#~ msgstr "Ваша учетная запись" - -#~ msgid "to access the administration tools" -#~ msgstr "для доступа к утилитам администрирования" - -#~ msgid "Services status" -#~ msgstr "Статусы сервисов" - -#~ msgid "Service" -#~ msgstr "Сервис" - -#~ msgid "PID" -#~ msgstr "PID" - -#~ msgid "Image" -#~ msgstr "Изображение" - -#~ msgid "Started" -#~ msgstr "Начато" - -#~ msgid "Last update" -#~ msgstr "Последнее обновление" - -#~ msgid "from" -#~ msgstr "От" - -#~ msgid "Forward emails" -#~ msgstr "Перенаправлять письма" - -#~ msgid "General settings" -#~ msgstr "Общие настройки" +msgstr "Наяўныя адтуліны" From 9205d42ae9310bc761d02b24b3c54929403ee5a9 Mon Sep 17 00:00:00 2001 From: Dimitri Huisman Date: Wed, 27 Mar 2024 09:55:05 +0000 Subject: [PATCH 22/58] Add missing translations for Dutch --- .../translations/nl/LC_MESSAGES/messages.po | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/core/admin/mailu/translations/nl/LC_MESSAGES/messages.po b/core/admin/mailu/translations/nl/LC_MESSAGES/messages.po index 18728723..98b3f8fd 100644 --- a/core/admin/mailu/translations/nl/LC_MESSAGES/messages.po +++ b/core/admin/mailu/translations/nl/LC_MESSAGES/messages.po @@ -24,7 +24,7 @@ msgstr "E-mail" msgid "Password" msgstr "Wachtwoord" -#: mailu/sso/forms.py:10 mailu/sso/forms.py:11 mailu/sso/templates/login.html:4 +#: mailu/sso/forms.py:11 mailu/sso/forms.py:12 mailu/sso/templates/login.html:4 #: mailu/ui/templates/sidebar.html:142 msgid "Sign in" msgstr "Aanmelden" @@ -83,6 +83,14 @@ msgstr "Bevestigen" msgid "Domain name" msgstr "Domeinnaam" +#: mailu/ui/templates/domain/details.html:19 +msgid "Download zonefile" +msgstr "Download zone-bestand" + +#: mailu/ui/forms.py:98 +msgid "Allow the user to spoof the sender (send email as anyone)" +msgstr "Sta toe dat de gebruiker de verzender kan vervalsen (namens iedereen email versturen)" + #: mailu/ui/forms.py:49 msgid "Maximum user count" msgstr "Maximaal aantal gebruikers" @@ -99,6 +107,10 @@ msgstr "Maximum quotum gebruikers" msgid "Enable sign-up" msgstr "Schakel registreren in" +#: mailu/ui/forms.py:102 +msgid "Force password change at next login" +msgstr "Forceer dat bij de volgende aanmelding het wachtwoord veranderd moet worden" + #: 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 @@ -124,6 +136,10 @@ msgstr "Beheerder wachtwoord" msgid "Confirm password" msgstr "Bevestig wachtwoord" +#: mailu/ui/forms.py:134 +msgid "Current password" +msgstr "Huidig wachtwoord" + #: mailu/ui/forms.py:63 msgid "Create" msgstr "Aanmaken" From a0877e61813718e414b652695aa7175016e14c9d Mon Sep 17 00:00:00 2001 From: Dimitri Huisman Date: Wed, 27 Mar 2024 10:01:12 +0000 Subject: [PATCH 23/58] Add changelog entry for PR 3029 --- towncrier/newsfragments/3029.bugfix | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 towncrier/newsfragments/3029.bugfix diff --git a/towncrier/newsfragments/3029.bugfix b/towncrier/newsfragments/3029.bugfix new file mode 100644 index 00000000..a3602429 --- /dev/null +++ b/towncrier/newsfragments/3029.bugfix @@ -0,0 +1,21 @@ +Added missing translations for Dutch, German and French. +4 new strings were introduced after 2.0. These must be translated for all languages. +If this translation is missing for your native language, please submit a PR with the translation, +or open a new issue where you mention the translated strings. + +The missing translations are: +#: mailu/ui/templates/domain/details.html:19 +msgid "Download zonefile" +msgstr "translation of Download zonefile" + +#: mailu/ui/forms.py:134 +msgid "Current password" +msgstr "translation of Current password" + +#: mailu/ui/forms.py:102 +msgid "Force password change at next login" +msgstr "translation of password change at next login" + +#: mailu/ui/forms.py:98 +msgid "Allow the user to spoof the sender (send email as anyone)" +msgstr "translation of Allow the user to spoof the sender (send email as anyone)" \ No newline at end of file From 7cd5090d8d0c4bdd889c7be2971ec597d333419b Mon Sep 17 00:00:00 2001 From: pavuki Date: Sat, 30 Mar 2024 11:46:27 +0100 Subject: [PATCH 24/58] Update Belarusian translation. --- .../be_BY/LC_MESSAGES/messages.po | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/core/admin/mailu/translations/be_BY/LC_MESSAGES/messages.po b/core/admin/mailu/translations/be_BY/LC_MESSAGES/messages.po index 28cbf486..26793e85 100644 --- a/core/admin/mailu/translations/be_BY/LC_MESSAGES/messages.po +++ b/core/admin/mailu/translations/be_BY/LC_MESSAGES/messages.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: Mailu\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "POT-Creation-Date: 2022-05-22 18:47+0200\n" -"PO-Revision-Date: 2024-03-26 09:23+0100\n" +"PO-Revision-Date: 2024-03-30 11:45+0100\n" "Last-Translator: spoooyders \n" "Language-Team: \n" "Language: be_BY\n" @@ -19,8 +19,8 @@ msgstr "" 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/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 "Пароль" @@ -99,8 +99,8 @@ msgstr "Максымальная квота карыстальніка" 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/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" @@ -330,8 +330,7 @@ msgstr "Пацьвердзіце дзеяньне" #: mailu/ui/templates/confirm.html:13 #, python-format msgid "You are about to %(action)s. Please confirm your action." -msgstr "" -"Вы зьбіраецеся зьдзейсьніць %(action)s. Калі ласка пацьвердзіце ваша дзеяньне." +msgstr "Вы зьбіраецеся зьдзейсьніць %(action)s. Калі ласка пацьвердзіце ваша дзеяньне." #: mailu/ui/templates/docker-error.html:4 msgid "Docker error" @@ -716,3 +715,27 @@ msgstr "Дамен" #: mailu/ui/templates/user/signup_domain.html:15 msgid "Available slots" msgstr "Наяўныя адтуліны" + +#: mailu/ui/templates/client.html:62 +msgid "If you use an Apple device," +msgstr "Калі вы выкарыстоўваеце прыладу Apple," + +#: mailu/ui/templates/client.html:63 +msgid "click here to auto-configure it." +msgstr "націсьніце сюды каб аўтаматычна наладзіць яе." + +#: mailu/ui/templates/domain/details.html:19 +msgid "Download zonefile" +msgstr "Спампаваць файл зоны" + +#: mailu/ui/forms.py:134 +msgid "Current password" +msgstr "Бягучы пароль" + +#: mailu/ui/forms.py:102 +msgid "Force password change at next login" +msgstr "Прымусова зьмяніць пароль пры наступным уваходзе" + +#: mailu/ui/forms.py:98 +msgid "Allow the user to spoof the sender (send email as anyone)" +msgstr "Дазволіць карыстальніку падрабляць адпраўшчыка (адсылаць пошту як хто заўгодна)" From dfdd663157e5a33f1d0d185548a07e4a5a5a57a2 Mon Sep 17 00:00:00 2001 From: pavuki Date: Sat, 30 Mar 2024 12:18:16 +0100 Subject: [PATCH 25/58] Update Belarusian translation. --- core/admin/mailu/translations/be_BY/LC_MESSAGES/messages.po | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/admin/mailu/translations/be_BY/LC_MESSAGES/messages.po b/core/admin/mailu/translations/be_BY/LC_MESSAGES/messages.po index 26793e85..474af0ee 100644 --- a/core/admin/mailu/translations/be_BY/LC_MESSAGES/messages.po +++ b/core/admin/mailu/translations/be_BY/LC_MESSAGES/messages.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: Mailu\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "POT-Creation-Date: 2022-05-22 18:47+0200\n" -"PO-Revision-Date: 2024-03-30 11:45+0100\n" +"PO-Revision-Date: 2024-03-30 12:18+0100\n" "Last-Translator: spoooyders \n" "Language-Team: \n" "Language: be_BY\n" @@ -572,7 +572,7 @@ msgid "" " domain zone so that the domain MX points to this server" msgstr "" "Каб зарэгістраваць новы дамен, вы мусіце спачатку наладзіць\n" -"    даменная зона, так каб дамен MX паказваў на гэты сэрвер" +"    даменная зона, так каб дамен MX паказваў на гэты сэрвэр" #: mailu/ui/templates/domain/signup.html:18 msgid "" From e16e4924977ed510116bdd2299c2c2e721e71b5d Mon Sep 17 00:00:00 2001 From: Dimitri Huisman Date: Tue, 2 Apr 2024 07:43:06 +0000 Subject: [PATCH 26/58] Add (hopefully) last incomplete translation for Dutch --- core/admin/mailu/translations/nl/LC_MESSAGES/messages.po | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/admin/mailu/translations/nl/LC_MESSAGES/messages.po b/core/admin/mailu/translations/nl/LC_MESSAGES/messages.po index 98b3f8fd..dee24864 100644 --- a/core/admin/mailu/translations/nl/LC_MESSAGES/messages.po +++ b/core/admin/mailu/translations/nl/LC_MESSAGES/messages.po @@ -293,6 +293,14 @@ msgstr "TLS inschakelen" msgid "Username" msgstr "Gebruikersnaam" +#: mailu/ui/templates/client.html:62 +msgid "If you use an Apple device," +msgstr "Indien u een Apple-apparaat gebruikt," + +#: mailu/ui/templates/client.html:63 +msgid "click here to auto-configure it." +msgstr "kunt u hier klikken om deze automatisch te configureren." + #: mailu/ui/forms.py:167 msgid "Keep emails on the server" msgstr "Behoud de e-mails op de server" From 4191c6aee261e0be16a98576b38d655dfcce92f3 Mon Sep 17 00:00:00 2001 From: Dimitri Huisman Date: Tue, 2 Apr 2024 11:11:29 +0000 Subject: [PATCH 27/58] Add missing translations for French --- .../translations/fr/LC_MESSAGES/messages.po | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/core/admin/mailu/translations/fr/LC_MESSAGES/messages.po b/core/admin/mailu/translations/fr/LC_MESSAGES/messages.po index 8ee21235..214fa521 100644 --- a/core/admin/mailu/translations/fr/LC_MESSAGES/messages.po +++ b/core/admin/mailu/translations/fr/LC_MESSAGES/messages.po @@ -84,6 +84,22 @@ msgstr "Confirmer" msgid "Domain name" msgstr "Nom de domaine" +#: mailu/ui/forms.py:134 +msgid "Current password" +msgstr "Mot de passe actuel" + +#: mailu/ui/forms.py:98 +msgid "Allow the user to spoof the sender (send email as anyone)" +msgstr "Permettre à l'utilisateur de changer l'expéditeur (envoyer un e-mail en tant que n'importe qui)" + +#: mailu/ui/forms.py:102 +msgid "Force password change at next login" +msgstr "Forcer le changement du mot de passe à la prochaine connexion" + +#: mailu/ui/templates/domain/details.html:19 +msgid "Download zonefile" +msgstr "Télécharger le fichier de zone" + #: mailu/ui/forms.py:49 msgid "Maximum user count" msgstr "Nombre maximum d'utilisateurs" @@ -269,6 +285,14 @@ msgstr "Nom d'hôte ou adresse IP" msgid "TCP port" msgstr "Port TCP" +#: mailu/ui/templates/client.html:62 +msgid "If you use an Apple device," +msgstr "Si vous utilisez un appareil Apple," + +#: mailu/ui/templates/client.html:63 +msgid "click here to auto-configure it." +msgstr "cliquez ici pour le configurer automatiquement." + #: mailu/ui/forms.py:164 msgid "Enable TLS" msgstr "Activer TLS" From 956accebc3809530f07cfbad7f921b73c4c7a6b9 Mon Sep 17 00:00:00 2001 From: Dimitri Huisman Date: Thu, 4 Apr 2024 07:44:24 +0000 Subject: [PATCH 28/58] Add missing translations for Polish --- .../translations/pl/LC_MESSAGES/messages.po | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/core/admin/mailu/translations/pl/LC_MESSAGES/messages.po b/core/admin/mailu/translations/pl/LC_MESSAGES/messages.po index 1312cbc6..3c87530b 100644 --- a/core/admin/mailu/translations/pl/LC_MESSAGES/messages.po +++ b/core/admin/mailu/translations/pl/LC_MESSAGES/messages.po @@ -85,6 +85,22 @@ msgstr "Zatwierdź" msgid "Domain name" msgstr "Nazwa domeny" +#: mailu/ui/templates/domain/details.html:19 +msgid "Download zonefile" +msgstr "Pobierz plik strefy DNS" + +#: mailu/ui/forms.py:134 +msgid "Current password" +msgstr "Aktualne hasło" + +#: mailu/ui/forms.py:102 +msgid "Force password change at next login" +msgstr "Wymuś zmianę hasła przy następnym logowaniu" + +#: mailu/ui/forms.py:98 +msgid "Allow the user to spoof the sender (send email as anyone)" +msgstr "Zezwól użytkownikowi na podrobienie adresu nadawcy (wysłanie emaila w czyimkolwiek imieniu)" + #: mailu/ui/forms.py:49 msgid "Maximum user count" msgstr "Maksymalna liczba użytkowników" @@ -271,6 +287,14 @@ msgstr "Nazwa hosta lub adres IP" msgid "TCP port" msgstr "Port TCP" +#: mailu/ui/templates/client.html:62 +msgid "If you use an Apple device," +msgstr "Jeśli używasz urządzenia firmy Apple," + +#: mailu/ui/templates/client.html:63 +msgid "click here to auto-configure it." +msgstr "kliknij tutaj by skonfigurować je automatycznie." + #: mailu/ui/forms.py:164 msgid "Enable TLS" msgstr "Włącz TLS" From e0b64a9e540b5f2dbea7d39c53a0e824bb51663d Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 6 Apr 2024 17:28:38 +0200 Subject: [PATCH 29/58] simplify config with TLS, PORTS and PROXY_PROTOCOL --- core/dovecot/conf/dovecot.conf | 2 +- core/nginx/conf/nginx.conf | 34 +++++++++++------------ core/nginx/conf/proxy.conf | 2 +- core/nginx/config.py | 40 ++++++++++++++++++++++++++++ core/nginx/dovecot/proxy.conf | 34 ++++++++++++++--------- towncrier/newsfragments/3061.feature | 1 + 6 files changed, 81 insertions(+), 32 deletions(-) create mode 100644 towncrier/newsfragments/3061.feature diff --git a/core/dovecot/conf/dovecot.conf b/core/dovecot/conf/dovecot.conf index 4ea4fc43..75bd3a66 100644 --- a/core/dovecot/conf/dovecot.conf +++ b/core/dovecot/conf/dovecot.conf @@ -5,7 +5,7 @@ log_path = /dev/stderr protocols = imap pop3 lmtp sieve postmaster_address = {{ POSTMASTER }}@{{ DOMAIN }} hostname = {{ HOSTNAMES.split(",")[0] }} -{%- if PROXY_PROTOCOL in ['all', 'all-but-http', 'mail'] %} +{%- if PROXY_PROTOCOL_SMTP %} submission_host = {{ HOSTNAMES.split(",")[0] }} {% else %} submission_host = {{ FRONT_ADDRESS }} diff --git a/core/nginx/conf/nginx.conf b/core/nginx/conf/nginx.conf index 214744ee..a29c570b 100644 --- a/core/nginx/conf/nginx.conf +++ b/core/nginx/conf/nginx.conf @@ -22,7 +22,7 @@ http { {% if REAL_IP_HEADER %} real_ip_header {{ REAL_IP_HEADER }}; - {% elif PROXY_PROTOCOL in ['all', 'all-but-http', 'http'] %} + {% elif (PROXY_PROTOCOL_HTTPS or PROXY_PROTOCOL_HTTP) and REAL_IP_FROM %} real_ip_header proxy_protocol; {% endif %} @@ -54,14 +54,14 @@ http { gzip_min_length 1024; # TODO: figure out how to server pre-compressed assets from admin container - {% if not KUBERNETES_INGRESS and TLS_FLAVOR in [ 'letsencrypt', 'cert' ] %} + {% if PORT_80 and TLS_FLAVOR in [ 'letsencrypt', 'cert' ] %} # Enable the proxy for certbot if the flavor is letsencrypt and not on kubernetes # server { # Listen over HTTP - listen 80{% if PROXY_PROTOCOL in ['all', 'http'] %} proxy_protocol{% endif %}; + listen 80{% if PROXY_PROTOCOL_HTTP %} proxy_protocol{% endif %}; {% if SUBNET6 %} - listen [::]:80{% if PROXY_PROTOCOL in ['all', 'http'] %} proxy_protocol{% endif %}; + listen [::]:80{% if PROXY_PROTOCOL_HTTP %} proxy_protocol{% endif %}; {% endif %} {% if TLS_FLAVOR in ['letsencrypt', 'mail-letsencrypt'] %} location ^~ /.well-known/acme-challenge/ { @@ -95,18 +95,18 @@ http { client_max_body_size {{ MESSAGE_SIZE_LIMIT|int + 8388608 }}; # Listen on HTTP only in kubernetes or behind reverse proxy - {% if KUBERNETES_INGRESS or TLS_FLAVOR in [ 'mail-letsencrypt', 'notls', 'mail' ] %} - listen 80{% if PROXY_PROTOCOL in ['all', 'http'] %} proxy_protocol{% endif %}; + {% if TLS_FLAVOR in [ 'mail-letsencrypt', 'notls', 'mail' ] %} + listen 80{% if PROXY_PROTOCOL_HTTP %} proxy_protocol{% endif %}; {% if SUBNET6 %} - listen [::]:80{% if PROXY_PROTOCOL in ['all', 'http'] %} proxy_protocol{% endif %}; + listen [::]:80{% if PROXY_PROTOCOL_HTTP %} proxy_protocol{% endif %}; {% endif %} {% endif %} - # Only enable HTTPS if TLS is enabled with no error and not on kubernetes - {% if not KUBERNETES_INGRESS and TLS and not TLS_ERROR %} - listen 443 ssl http2{% if PROXY_PROTOCOL in ['all', 'all-but-http', 'http'] %} proxy_protocol{% endif %}; + # Only enable HTTPS if TLS is enabled with no error + {% if TLS_443 and not TLS_ERROR %} + listen 443 ssl http2{% if PROXY_PROTOCOL_HTTPS %} proxy_protocol{% endif %}; {% if SUBNET6 %} - listen [::]:443 ssl http2{% if PROXY_PROTOCOL in ['all', 'all-but-http', 'http'] %} proxy_protocol{% endif %}; + listen [::]:443 ssl http2{% if PROXY_PROTOCOL_HTTPS %} proxy_protocol{% endif %}; {% endif %} include /etc/nginx/tls.conf; @@ -162,7 +162,7 @@ http { {% endif %} # If TLS is failing, prevent access to anything except certbot - {% if not KUBERNETES_INGRESS and TLS_ERROR and not (TLS_FLAVOR in [ 'mail-letsencrypt', 'mail' ]) %} + {% if TLS_ERROR and not (TLS_FLAVOR in [ 'mail-letsencrypt', 'mail' ]) %} location / { return 403; } @@ -310,12 +310,12 @@ mail { resolver {{ RESOLVER }} valid=30s; error_log /dev/stderr info; - {% if TLS and not TLS_ERROR %} + {% if TLS_25 and not TLS_ERROR %} include /etc/nginx/tls.conf; ssl_session_cache shared:SSLMAIL:3m; {% endif %} - {% if PROXY_PROTOCOL in ['all', 'all-but-http', 'mail'] and REAL_IP_FROM %}{% for from_ip in REAL_IP_FROM.split(',') %} + {% if PROXY_PROTOCOL_SMTP and REAL_IP_FROM %}{% for from_ip in REAL_IP_FROM.split(',') %} set_real_ip_from {{ from_ip }}; {% endfor %}{% endif %} @@ -324,11 +324,11 @@ mail { # SMTP is always enabled, to avoid losing emails when TLS is failing server { - listen 25{% if PROXY_PROTOCOL in ['all', 'all-but-http', 'mail'] %} proxy_protocol{% endif %}; + listen 25{% if PROXY_PROTOCOL_SMTP %} proxy_protocol{% endif %}; {% if SUBNET6 %} - listen [::]:25{% if PROXY_PROTOCOL in ['all', 'all-but-http', 'mail'] %} proxy_protocol{% endif %}; + listen [::]:25{% if PROXY_PROTOCOL_SMTP %} proxy_protocol{% endif %}; {% endif %} - {% if TLS and not TLS_ERROR %} + {% if TLS_25 and not TLS_ERROR %} {% if TLS_FLAVOR in ['letsencrypt','mail-letsencrypt'] %} ssl_certificate /certs/letsencrypt/live/mailu/DANE-chain.pem; ssl_certificate /certs/letsencrypt/live/mailu-ecdsa/DANE-chain.pem; diff --git a/core/nginx/conf/proxy.conf b/core/nginx/conf/proxy.conf index ebbd30aa..9f6402cd 100644 --- a/core/nginx/conf/proxy.conf +++ b/core/nginx/conf/proxy.conf @@ -6,7 +6,7 @@ proxy_hide_header True-Client-IP; proxy_hide_header CF-Connecting-IP; proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; -{% if (REAL_IP_HEADER or (PROXY_PROTOCOL in ['http', 'all'])) and REAL_IP_FROM %} +{% if (REAL_IP_HEADER or (PROXY_PROTOCOL_HTTP or PROXY_PROTOCOL_HTTPS)) and REAL_IP_FROM %} proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-By $realip_remote_addr; {% else %} diff --git a/core/nginx/config.py b/core/nginx/config.py index 73cc085c..966a402c 100755 --- a/core/nginx/config.py +++ b/core/nginx/config.py @@ -70,6 +70,44 @@ with open("/etc/resolv.conf") as handle: resolver = content[content.index("nameserver") + 1] args["RESOLVER"] = f"[{resolver}]" if ":" in resolver else resolver +# Configure PROXY_PROTOCOL +PROTO_MAIL=['SMTP', 'POP3', 'POP3S', 'IMAP', 'IMAPS', 'SUBMISSION', 'SUBMISSIONS', 'MANAGESIEVE'] +PROTO_ALL_BUT_HTTP=PROTO_MAIL.copy() +PROTO_ALL_BUT_HTTP.extend(['HTTPS']) +PROTO_ALL=PROTO_ALL_BUT_HTTP.copy() +PROTO_ALL.extend(['HTTP']) +for item in args.get('PROXY_PROTOCOL', '').split(','): + match item: + case '25': args['PROXY_PROTOCOL_SMTP']=True; continue + case '80': args['PROXY_PROTOCOL_HTTP']=True; continue + case '110': args['PROXY_PROTOCOL_POP3']=True; continue + case '143': args['PROXY_PROTOCOL_IMAP']=True; continue + case '443': args['PROXY_PROTOCOL_HTTPS']=True; continue + case '465': args['PROXY_PROTOCOL_SUBMISSIONS']=True; continue + case '587': args['PROXY_PROTOCOL_SUBMISSION']=True; continue + case '993': args['PROXY_PROTOCOL_IMAPS']=True; continue + case '995': args['PROXY_PROTOCOL_POP3S']=True; continue + case '4190': args['PROXY_PROTOCOL_MANAGESIEVE']=True; continue + case 'mail': + for p in PROTO_MAIL: args[f'PROXY_PROTOCOL_{p}']=True; continue + case 'all-but-http': + for p in PROTO_ALL_BUT_HTTP: args[f'PROXY_PROTOCOL_{p}']=True; continue + case 'all': + for p in PROTO_ALL: args[f'PROXY_PROTOCOL_{p}']=True; continue + +PORTS_REQUIRING_TLS=['443', '465', '993', '995'] +ALL_PORTS='25,80,443,465,587,993,995,4190' +for item in args.get('PORTS', ALL_PORTS).split(','): + if item in PORTS_REQUIRING_TLS and args['TLS_FLAVOR'] == 'notls': + continue + args[f'PORT_{item}']=True + +for item in args.get('TLS', ALL_PORTS).split(','): + if item in PORTS_REQUIRING_TLS: + if args['TLS_FLAVOR'] == 'notls': + continue + args[f'TLS_{item}']=True + # TLS configuration cert_name = args.get("TLS_CERT_FILENAME", "cert.pem") keypair_name = args.get("TLS_KEYPAIR_FILENAME", "key.pem") @@ -129,6 +167,8 @@ if args["TLS"] and not all(os.path.exists(file_path) for file_path in args["TLS" print("Missing cert or key file, disabling TLS") args["TLS_ERROR"] = "yes" +args['TLS_PERMISSIVE'] = str(args.get('TLS_PERMISSIVE')).lower() not in ('false', 'no') + # Build final configuration paths conf.jinja("/conf/tls.conf", args, "/etc/nginx/tls.conf") conf.jinja("/conf/proxy.conf", args, "/etc/nginx/proxy.conf") diff --git a/core/nginx/dovecot/proxy.conf b/core/nginx/dovecot/proxy.conf index db5a5f03..40ed607a 100644 --- a/core/nginx/dovecot/proxy.conf +++ b/core/nginx/dovecot/proxy.conf @@ -75,11 +75,12 @@ service anvil { } } +{%- if PORT_4190 %} service managesieve-login { executable = managesieve-login inet_listener sieve { port = 4190 -{%- if PROXY_PROTOCOL in ['all', 'all-but-http', 'mail'] %} +{%- if PROXY_PROTOCOL_MANAGESIEVE %} haproxy = yes {% endif %} } @@ -87,6 +88,7 @@ service managesieve-login { port = 14190 } } +{% endif %} protocol imap { mail_max_userip_connections = 20 @@ -94,42 +96,46 @@ protocol imap { } service imap-login { +{%- if PORT_143 %} inet_listener imap { port = 143 -{%- if PROXY_PROTOCOL in ['all', 'all-but-http', 'mail'] %} +{%- if PROXY_PROTOCOL_IMAP %} haproxy = yes {% endif %} } +{% endif %} +{%- if TLS_993 and PORT_993 %} inet_listener imaps { port = 993 -{%- if TLS %} ssl = yes -{% endif %} -{%- if PROXY_PROTOCOL in ['all', 'all-but-http', 'mail'] %} +{%- if PROXY_PROTOCOL_IMAPS %} haproxy = yes {% endif %} } +{% endif %} inet_listener imap-webmail { port = 10143 } } service pop3-login { +{%- if PORT_110 %} inet_listener pop3 { port = 110 -{%- if PROXY_PROTOCOL in ['all', 'all-but-http', 'mail'] %} +{%- if PROXY_PROTOCOL_POP3 %} haproxy = yes {% endif %} } +{% endif %} +{%- if TLS_995 and PORT_995 %} inet_listener pop3s { port = 995 -{%- if TLS %} ssl = yes -{% endif %} -{%- if PROXY_PROTOCOL in ['all', 'all-but-http', 'mail'] %} +{%- if PROXY_PROTOCOL_POP3S %} haproxy = yes {% endif %} } +{% endif %} } recipient_delimiter = {{ RECIPIENT_DELIMITER }} @@ -141,21 +147,23 @@ service lmtp { } service submission-login { +{%- if PORT_587 %} inet_listener submission { port = 587 -{%- if PROXY_PROTOCOL in ['all', 'all-but-http', 'mail'] %} +{%- if PROXY_PROTOCOL_SUBMISSION %} haproxy = yes {% endif %} } +{% endif %} +{%- if TLS_465 and PORT_465 %} inet_listener submissions { port = 465 -{%- if TLS %} ssl = yes -{% endif %} -{%- if PROXY_PROTOCOL in ['all', 'all-but-http', 'mail'] %} +{%- if PROXY_PROTOCOL_SUBMISSIONS %} haproxy = yes {% endif %} } +{% endif %} inet_listener submission-webmail { port = 10025 } diff --git a/towncrier/newsfragments/3061.feature b/towncrier/newsfragments/3061.feature new file mode 100644 index 00000000..66b6e669 --- /dev/null +++ b/towncrier/newsfragments/3061.feature @@ -0,0 +1 @@ +Introduce new settings for configuring proxying and TLS. Drop TLS_FLAVOR=mail-letsencrypt From 2b6405227b0e681f2374747b237cf9421f29ed8b Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 6 Apr 2024 18:16:52 +0200 Subject: [PATCH 30/58] fix #3162: ensure snappymail works with notls --- webmails/snappymail/defaults/default.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/webmails/snappymail/defaults/default.json b/webmails/snappymail/defaults/default.json index 12252aee..dee93332 100644 --- a/webmails/snappymail/defaults/default.json +++ b/webmails/snappymail/defaults/default.json @@ -3,7 +3,7 @@ "IMAP": { "host": "{{ FRONT_ADDRESS }}", "port": 10143, - "secure": 2, + "secure": 3, "shortLogin": false, "ssl": { "verify_peer": false, @@ -20,7 +20,7 @@ "SMTP": { "host": "{{ FRONT_ADDRESS }}", "port": 10025, - "secure": 2, + "secure": 3, "shortLogin": false, "ssl": { "verify_peer": false, @@ -37,7 +37,7 @@ "Sieve": { "host": "{{ FRONT_ADDRESS }}", "port": 14190, - "type": 2, + "type": 3, "shortLogin": false, "ssl": { "verify_peer": false, From c701358c9d526909d7af8bc646bea6aa83db65f0 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 8 Apr 2024 09:02:09 +0200 Subject: [PATCH 31/58] simplify --- core/dovecot/conf/dovecot.conf | 2 +- core/nginx/conf/nginx.conf | 20 ++++++++-------- core/nginx/conf/proxy.conf | 2 +- core/nginx/config.py | 42 ++++++++++++++-------------------- core/nginx/dovecot/proxy.conf | 21 +++++++++-------- 5 files changed, 41 insertions(+), 46 deletions(-) diff --git a/core/dovecot/conf/dovecot.conf b/core/dovecot/conf/dovecot.conf index 75bd3a66..ef28efb1 100644 --- a/core/dovecot/conf/dovecot.conf +++ b/core/dovecot/conf/dovecot.conf @@ -5,7 +5,7 @@ log_path = /dev/stderr protocols = imap pop3 lmtp sieve postmaster_address = {{ POSTMASTER }}@{{ DOMAIN }} hostname = {{ HOSTNAMES.split(",")[0] }} -{%- if PROXY_PROTOCOL_SMTP %} +{%- if PROXY_PROTOCOL_25 %} submission_host = {{ HOSTNAMES.split(",")[0] }} {% else %} submission_host = {{ FRONT_ADDRESS }} diff --git a/core/nginx/conf/nginx.conf b/core/nginx/conf/nginx.conf index a29c570b..d67ce65a 100644 --- a/core/nginx/conf/nginx.conf +++ b/core/nginx/conf/nginx.conf @@ -22,7 +22,7 @@ http { {% if REAL_IP_HEADER %} real_ip_header {{ REAL_IP_HEADER }}; - {% elif (PROXY_PROTOCOL_HTTPS or PROXY_PROTOCOL_HTTP) and REAL_IP_FROM %} + {% elif (PROXY_PROTOCOL_80 or PROXY_PROTOCOL_443) and REAL_IP_FROM %} real_ip_header proxy_protocol; {% endif %} @@ -59,9 +59,9 @@ http { # server { # Listen over HTTP - listen 80{% if PROXY_PROTOCOL_HTTP %} proxy_protocol{% endif %}; + listen 80{% if PROXY_PROTOCOL_80 %} proxy_protocol{% endif %}; {% if SUBNET6 %} - listen [::]:80{% if PROXY_PROTOCOL_HTTP %} proxy_protocol{% endif %}; + listen [::]:80{% if PROXY_PROTOCOL_80 %} proxy_protocol{% endif %}; {% endif %} {% if TLS_FLAVOR in ['letsencrypt', 'mail-letsencrypt'] %} location ^~ /.well-known/acme-challenge/ { @@ -96,17 +96,17 @@ http { # Listen on HTTP only in kubernetes or behind reverse proxy {% if TLS_FLAVOR in [ 'mail-letsencrypt', 'notls', 'mail' ] %} - listen 80{% if PROXY_PROTOCOL_HTTP %} proxy_protocol{% endif %}; + listen 80{% if PROXY_HTTPPROTOCOL_80 %} proxy_protocol{% endif %}; {% if SUBNET6 %} - listen [::]:80{% if PROXY_PROTOCOL_HTTP %} proxy_protocol{% endif %}; + listen [::]:80{% if PROXY_PROTOCOL_80 %} proxy_protocol{% endif %}; {% endif %} {% endif %} # Only enable HTTPS if TLS is enabled with no error {% if TLS_443 and not TLS_ERROR %} - listen 443 ssl http2{% if PROXY_PROTOCOL_HTTPS %} proxy_protocol{% endif %}; + listen 443 ssl http2{% if PROXY_PROTOCOL_443 %} proxy_protocol{% endif %}; {% if SUBNET6 %} - listen [::]:443 ssl http2{% if PROXY_PROTOCOL_HTTPS %} proxy_protocol{% endif %}; + listen [::]:443 ssl http2{% if PROXY_PROTOCOL_443 %} proxy_protocol{% endif %}; {% endif %} include /etc/nginx/tls.conf; @@ -315,7 +315,7 @@ mail { ssl_session_cache shared:SSLMAIL:3m; {% endif %} - {% if PROXY_PROTOCOL_SMTP and REAL_IP_FROM %}{% for from_ip in REAL_IP_FROM.split(',') %} + {% if PROXY_PROTOCOL_25 and REAL_IP_FROM %}{% for from_ip in REAL_IP_FROM.split(',') %} set_real_ip_from {{ from_ip }}; {% endfor %}{% endif %} @@ -324,9 +324,9 @@ mail { # SMTP is always enabled, to avoid losing emails when TLS is failing server { - listen 25{% if PROXY_PROTOCOL_SMTP %} proxy_protocol{% endif %}; + listen 25{% if PROXY_PROTOCOL_25 %} proxy_protocol{% endif %}; {% if SUBNET6 %} - listen [::]:25{% if PROXY_PROTOCOL_SMTP %} proxy_protocol{% endif %}; + listen [::]:25{% if PROXY_PROTOCOL_25 %} proxy_protocol{% endif %}; {% endif %} {% if TLS_25 and not TLS_ERROR %} {% if TLS_FLAVOR in ['letsencrypt','mail-letsencrypt'] %} diff --git a/core/nginx/conf/proxy.conf b/core/nginx/conf/proxy.conf index 9f6402cd..7a3d8721 100644 --- a/core/nginx/conf/proxy.conf +++ b/core/nginx/conf/proxy.conf @@ -6,7 +6,7 @@ proxy_hide_header True-Client-IP; proxy_hide_header CF-Connecting-IP; proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; -{% if (REAL_IP_HEADER or (PROXY_PROTOCOL_HTTP or PROXY_PROTOCOL_HTTPS)) and REAL_IP_FROM %} +{% if (REAL_IP_HEADER or (PROXY_PROTOCOL_80 or PROXY_PROTOCOL_443)) and REAL_IP_FROM %} proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-By $realip_remote_addr; {% else %} diff --git a/core/nginx/config.py b/core/nginx/config.py index 966a402c..3d92d1b9 100755 --- a/core/nginx/config.py +++ b/core/nginx/config.py @@ -71,29 +71,22 @@ with open("/etc/resolv.conf") as handle: args["RESOLVER"] = f"[{resolver}]" if ":" in resolver else resolver # Configure PROXY_PROTOCOL -PROTO_MAIL=['SMTP', 'POP3', 'POP3S', 'IMAP', 'IMAPS', 'SUBMISSION', 'SUBMISSIONS', 'MANAGESIEVE'] +PROTO_MAIL=['25', '110', '995', '143', '993', '587', '465', '4190'] PROTO_ALL_BUT_HTTP=PROTO_MAIL.copy() -PROTO_ALL_BUT_HTTP.extend(['HTTPS']) +PROTO_ALL_BUT_HTTP.extend(['443']) PROTO_ALL=PROTO_ALL_BUT_HTTP.copy() -PROTO_ALL.extend(['HTTP']) +PROTO_ALL.extend(['80']) for item in args.get('PROXY_PROTOCOL', '').split(','): - match item: - case '25': args['PROXY_PROTOCOL_SMTP']=True; continue - case '80': args['PROXY_PROTOCOL_HTTP']=True; continue - case '110': args['PROXY_PROTOCOL_POP3']=True; continue - case '143': args['PROXY_PROTOCOL_IMAP']=True; continue - case '443': args['PROXY_PROTOCOL_HTTPS']=True; continue - case '465': args['PROXY_PROTOCOL_SUBMISSIONS']=True; continue - case '587': args['PROXY_PROTOCOL_SUBMISSION']=True; continue - case '993': args['PROXY_PROTOCOL_IMAPS']=True; continue - case '995': args['PROXY_PROTOCOL_POP3S']=True; continue - case '4190': args['PROXY_PROTOCOL_MANAGESIEVE']=True; continue - case 'mail': - for p in PROTO_MAIL: args[f'PROXY_PROTOCOL_{p}']=True; continue - case 'all-but-http': - for p in PROTO_ALL_BUT_HTTP: args[f'PROXY_PROTOCOL_{p}']=True; continue - case 'all': - for p in PROTO_ALL: args[f'PROXY_PROTOCOL_{p}']=True; continue + if item.isdigit(): + args[f'PROXY_PROTOCOL_{item}']=True + elif item == 'mail': + for p in PROTO_MAIL: args[f'PROXY_PROTOCOL_{p}']=True + elif item == 'all-but-http': + for p in PROTO_ALL_BUT_HTTP: args[f'PROXY_PROTOCOL_{p}']=True + elif item == 'all': + for p in PROTO_ALL: args[f'PROXY_PROTOCOL_{p}']=True + else: + log.error(f'Not sure what to do with {item} in PROXY_PROTOCOL ({args.get("PROXY_PROTOCOL")})') PORTS_REQUIRING_TLS=['443', '465', '993', '995'] ALL_PORTS='25,80,443,465,587,993,995,4190' @@ -102,11 +95,10 @@ for item in args.get('PORTS', ALL_PORTS).split(','): continue args[f'PORT_{item}']=True -for item in args.get('TLS', ALL_PORTS).split(','): - if item in PORTS_REQUIRING_TLS: - if args['TLS_FLAVOR'] == 'notls': - continue - args[f'TLS_{item}']=True +if args['TLS_FLAVOR'] != 'notls': + for item in args.get('TLS', ALL_PORTS).split(','): + if item in PORTS_REQUIRING_TLS: + args[f'TLS_{item}']=True # TLS configuration cert_name = args.get("TLS_CERT_FILENAME", "cert.pem") diff --git a/core/nginx/dovecot/proxy.conf b/core/nginx/dovecot/proxy.conf index 40ed607a..95972547 100644 --- a/core/nginx/dovecot/proxy.conf +++ b/core/nginx/dovecot/proxy.conf @@ -80,7 +80,7 @@ service managesieve-login { executable = managesieve-login inet_listener sieve { port = 4190 -{%- if PROXY_PROTOCOL_MANAGESIEVE %} +{%- if PROXY_PROTOCOL_4190 %} haproxy = yes {% endif %} } @@ -99,7 +99,7 @@ service imap-login { {%- if PORT_143 %} inet_listener imap { port = 143 -{%- if PROXY_PROTOCOL_IMAP %} +{%- if PROXY_PROTOCOL_143 %} haproxy = yes {% endif %} } @@ -108,7 +108,7 @@ service imap-login { inet_listener imaps { port = 993 ssl = yes -{%- if PROXY_PROTOCOL_IMAPS %} +{%- if PROXY_PROTOCOL_993 %} haproxy = yes {% endif %} } @@ -122,7 +122,7 @@ service pop3-login { {%- if PORT_110 %} inet_listener pop3 { port = 110 -{%- if PROXY_PROTOCOL_POP3 %} +{%- if PROXY_PROTOCOL_110 %} haproxy = yes {% endif %} } @@ -131,7 +131,7 @@ service pop3-login { inet_listener pop3s { port = 995 ssl = yes -{%- if PROXY_PROTOCOL_POP3S %} +{%- if PROXY_PROTOCOL_995 %} haproxy = yes {% endif %} } @@ -147,19 +147,22 @@ service lmtp { } service submission-login { -{%- if PORT_587 %} inet_listener submission { +{%- if PORT_587 %} port = 587 -{%- if PROXY_PROTOCOL_SUBMISSION %} +{%- if PROXY_PROTOCOL_587 %} haproxy = yes {% endif %} - } +{%- else %} +# if the section is unset the port is bound anyways + port = 0 {% endif %} + } {%- if TLS_465 and PORT_465 %} inet_listener submissions { port = 465 ssl = yes -{%- if PROXY_PROTOCOL_SUBMISSIONS %} +{%- if PROXY_PROTOCOL_645 %} haproxy = yes {% endif %} } From 614042344db14ea91262056ed2af09dcd0796495 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 8 Apr 2024 09:46:39 +0200 Subject: [PATCH 32/58] document --- core/nginx/conf/nginx.conf | 4 ++-- core/nginx/config.py | 2 +- docs/compose/setup.rst | 12 ++++------- docs/configuration.rst | 30 ++++++++++++++++++---------- docs/reverse.rst | 28 ++------------------------ towncrier/newsfragments/3061.feature | 7 ++++++- 6 files changed, 35 insertions(+), 48 deletions(-) diff --git a/core/nginx/conf/nginx.conf b/core/nginx/conf/nginx.conf index d67ce65a..bbd86fdc 100644 --- a/core/nginx/conf/nginx.conf +++ b/core/nginx/conf/nginx.conf @@ -310,7 +310,7 @@ mail { resolver {{ RESOLVER }} valid=30s; error_log /dev/stderr info; - {% if TLS_25 and not TLS_ERROR %} + {% if TLS and not TLS_ERROR %} include /etc/nginx/tls.conf; ssl_session_cache shared:SSLMAIL:3m; {% endif %} @@ -328,7 +328,7 @@ mail { {% if SUBNET6 %} listen [::]:25{% if PROXY_PROTOCOL_25 %} proxy_protocol{% endif %}; {% endif %} - {% if TLS_25 and not TLS_ERROR %} + {% if TLS and not TLS_ERROR %} {% if TLS_FLAVOR in ['letsencrypt','mail-letsencrypt'] %} ssl_certificate /certs/letsencrypt/live/mailu/DANE-chain.pem; ssl_certificate /certs/letsencrypt/live/mailu-ecdsa/DANE-chain.pem; diff --git a/core/nginx/config.py b/core/nginx/config.py index 3d92d1b9..1f06424b 100755 --- a/core/nginx/config.py +++ b/core/nginx/config.py @@ -89,7 +89,7 @@ for item in args.get('PROXY_PROTOCOL', '').split(','): log.error(f'Not sure what to do with {item} in PROXY_PROTOCOL ({args.get("PROXY_PROTOCOL")})') PORTS_REQUIRING_TLS=['443', '465', '993', '995'] -ALL_PORTS='25,80,443,465,587,993,995,4190' +ALL_PORTS='25,80,443,465,993,995,4190' for item in args.get('PORTS', ALL_PORTS).split(','): if item in PORTS_REQUIRING_TLS and args['TLS_FLAVOR'] == 'notls': continue diff --git a/docs/compose/setup.rst b/docs/compose/setup.rst index 81433ba3..ae1a6387 100644 --- a/docs/compose/setup.rst +++ b/docs/compose/setup.rst @@ -31,18 +31,14 @@ Sets the ``TLS_FLAVOR`` to one of the following values: - ``cert`` is the default and requires certificates to be setup manually; -- ``letsencrypt`` will use the *Letsencrypt!* CA to generate automatic certificates; -- ``mail`` is similar to ``cert`` except that TLS will only be served for - emails (IMAP and SMTP), not HTTP (use it behind reverse proxies); -- ``mail-letsencrypt`` is similar to ``letsencrypt`` except that TLS will only be served for - emails (IMAP and SMTP), not HTTP (use it behind reverse proxies); +- ``letsencrypt`` will use the *Letsencrypt!* CA to obtain certificates automatically; - ``notls`` will disable TLS, this is not recommended except for testing. .. note:: When using *Letsencrypt!* you have to make sure that the DNS ``A`` and ``AAAA`` records for the - all hostnames mentioned in the ``HOSTNAMES`` variable match with the ip addresses of you server. - Or else certificate generation will fail! See also: :ref:`dns_setup`. + all hostnames mentioned in the ``HOSTNAMES`` variable match with the ip addresses of you server + or else certificate generation will fail! See also: :ref:`dns_setup`. Bind address ```````````` @@ -91,7 +87,7 @@ Finish setting up TLS Mailu relies heavily on TLS and must have a key pair and a certificate available, at least for the hostname configured in the ``mailu.env`` file. -If you set ``TLS_FLAVOR`` to ``cert`` or ``mail`` then you must create a ``certs`` directory +If you set ``TLS_FLAVOR`` to ``cert`` then you must create a ``certs`` directory in your root path and setup a key-certificate pair there: - ``cert.pem`` contains the certificate (override with ``TLS_CERT_FILENAME``), diff --git a/docs/configuration.rst b/docs/configuration.rst index 50a576fd..f5818509 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -62,8 +62,7 @@ The ``AUTH_RATELIMIT_EXEMPTION`` (default: '') is a comma separated list of netw CIDRs that won't be subject to any form of rate limiting. Specifying ``0.0.0.0/0, ::/0`` there is a good way to disable rate limiting altogether. -The ``TLS_FLAVOR`` sets how Mailu handles TLS connections. Setting this value to -``notls`` will cause Mailu not to serve any web content! More on :ref:`tls_flavor`. +The ``TLS_FLAVOR`` sets how Mailu obtains a x509 certificate. More on :ref:`tls_flavor`. The ``DEFAULT_SPAM_THRESHOLD`` (default: 80) is the default spam tolerance used when creating a new user. @@ -248,9 +247,20 @@ but slows down the performance of modern devices. .. _`android handsets older than 7.1.1`: https://community.letsencrypt.org/t/production-chain-changes/150739 -The ``TLS_PERMISSIVE`` (default: true) setting controls whether ciphers and protocols offered on port 25 for STARTTLS are optimized for maximum compatibility. We **strongly recommend** that you do **not** change this setting on the basis that any encryption beats no encryption. If you are subject to compliance requirements and are not afraid of losing emails as a result of artificially reducing compatibility, set it to 'false'. Keep in mind that servers that are running a software stack old enough to not be compatible with the current TLS requirements will either a) deliver in plaintext b) bounce emails c) silently drop emails; moreover, modern servers will benefit from various downgrade protections (DOWNGRD, RFC7507) making the security argument mostly a moot point. +The ``TLS_PERMISSIVE`` (default: true) setting controls whether ciphers and protocols offered on port 25 +for STARTTLS are optimized for maximum compatibility. We **strongly recommend** that you do **not** change +this setting on the basis that any encryption beats no encryption. If you are subject to compliance +requirements and are not afraid of losing emails as a result of artificially reducing compatibility, +set it to 'false'. Keep in mind that servers that are running a software stack old enough to not be +compatible with the current TLS requirements will either a) deliver in plaintext b) bounce emails +c) silently drop emails; moreover, modern servers will benefit from various downgrade protections +(DOWNGRD, RFC7507) making the security argument mostly a moot point. -The ``COMPRESSION`` (default: unset) setting controls whether emails are stored compressed at rest on disk. Valid values are ``gz``, ``bz2`` or ``zstd`` and additional settings can be configured via ``COMPRESSION_LEVEL``, see `zlib_save_level`_ for accepted values. If the underlying filesystem supports compression natively you should use it instead of this setting as it will be more efficient and will improve compatibility with 3rd party tools. +The ``COMPRESSION`` (default: unset) setting controls whether emails are stored compressed at rest on disk. +Valid values are ``gz``, ``bz2`` or ``zstd`` and additional settings can be configured via +``COMPRESSION_LEVEL``, see `zlib_save_level`_ for accepted values. If the underlying filesystem +supports compression natively you should use it instead of this setting as it will be more efficient +and will improve compatibility with 3rd party tools. .. _`zlib_save_level`: https://doc.dovecot.org/settings/plugin/zlib-plugin/#plugin_setting-zlib-zlib_save_level @@ -267,13 +277,13 @@ The ``TZ`` sets the timezone Mailu will use. The timezone naming convention usua .. _`TZ database name`: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones -The ``PROXY_PROTOCOL`` (default: unset) allows the the front container to receive TCP and HTTP connections with -the `PROXY protocol`_ (originally introduced in HAProxy, now also configurable in other proxy servers). -It can be set to: -* ``http`` to accept the ``PROXY`` protocol on nginx's HTTP proxy ports -* ``mail`` to accept the ``PROXY`` protocol on nginx's mail proxy ports -* ``all`` to accept the ``PROXY`` protocol on all nginx's HTTP and mail proxy ports +The ``PORTS`` (default: '25,80,443,465,993,995,4190') setting determines which services should be enabled. It is a comma delimited list of ports numbers. +If you need to re-enable IMAP, POP3 and Submission, you can append '110,143,587' to that list. + +The ``PROXY_PROTOCOL`` (default: unset) setting allows the the front container to receive TCP and HTTP connections with +the `PROXY protocol`_ (originally introduced in HAProxy, now also configurable in other proxy servers). +It can be set to a comma delimited list of ports on which it should be enabled. .. _`PROXY protocol`: https://github.com/haproxy/haproxy/blob/master/doc/proxy-protocol.txt diff --git a/docs/reverse.rst b/docs/reverse.rst index de427d5d..56958d9b 100644 --- a/docs/reverse.rst +++ b/docs/reverse.rst @@ -28,11 +28,8 @@ and add a section like follows: - "--entrypoints.web.address=:http" - "--entrypoints.websecure.address=:https" - "--entrypoints.smtp.address=:smtp" - - "--entrypoints.submission.address=:submission" - "--entrypoints.submissions.address=:submissions" - - "--entrypoints.imap.address=:imap" - "--entrypoints.imaps.address=:imaps" - - "--entrypoints.pop3.address=:pop3" - "--entrypoints.pop3s.address=:pop3s" - "--entrypoints.sieve.address=:sieve" # - "--api.insecure=true" @@ -42,11 +39,8 @@ and add a section like follows: - "80:80" - "443:443" - "465:465" - - "587:587" - "993:993" - "995:995" - - "110:110" - - "143:143" - "4190:4190" # The Web UI (enabled by --api.insecure=true) # - "8080:8080" @@ -80,36 +74,18 @@ and then add the following to the front section: - "traefik.tcp.services.smtp.loadbalancer.server.port=25" - "traefik.tcp.services.smtp.loadbalancer.proxyProtocol.version=2" - - "traefik.tcp.routers.submission.rule=HostSNI(`*`)" - - "traefik.tcp.routers.submission.entrypoints=submission" - - "traefik.tcp.routers.submission.service=submission" - - "traefik.tcp.services.submission.loadbalancer.server.port=587" - - "traefik.tcp.services.submission.loadbalancer.proxyProtocol.version=2" - - "traefik.tcp.routers.submissions.rule=HostSNI(`*`)" - "traefik.tcp.routers.submissions.entrypoints=submissions" - "traefik.tcp.routers.submissions.service=submissions" - "traefik.tcp.services.submissions.loadbalancer.server.port=465" - "traefik.tcp.services.submissions.loadbalancer.proxyProtocol.version=2" - - "traefik.tcp.routers.imap.rule=HostSNI(`*`)" - - "traefik.tcp.routers.imap.entrypoints=imap" - - "traefik.tcp.routers.imap.service=imap" - - "traefik.tcp.services.imap.loadbalancer.server.port=143" - - "traefik.tcp.services.imap.loadbalancer.proxyProtocol.version=2" - - "traefik.tcp.routers.imaps.rule=HostSNI(`*`)" - "traefik.tcp.routers.imaps.entrypoints=imaps" - "traefik.tcp.routers.imaps.service=imaps" - "traefik.tcp.services.imaps.loadbalancer.server.port=993" - "traefik.tcp.services.imaps.loadbalancer.proxyProtocol.version=2" - - "traefik.tcp.routers.pop3.rule=HostSNI(`*`)" - - "traefik.tcp.routers.pop3.entrypoints=pop3" - - "traefik.tcp.routers.pop3.service=pop3" - - "traefik.tcp.services.pop3.loadbalancer.server.port=110" - - "traefik.tcp.services.pop3.loadbalancer.proxyProtocol.version=2" - - "traefik.tcp.routers.pop3s.rule=HostSNI(`*`)" - "traefik.tcp.routers.pop3s.entrypoints=pop3s" - "traefik.tcp.routers.pop3s.service=pop3s" @@ -129,9 +105,9 @@ in mailu.env: .. code-block:: docker REAL_IP_FROM=192.168.203.0/24 - PROXY_PROTOCOL=all-but-http + PROXY_PROTOCOL=25,443,465,993,995,4190 TRAEFIK_VERSION=v2 - TLS_FLAVOR=mail-letsencrypt + TLS_FLAVOR=letsencrypt WEBROOT_REDIRECT=/sso/login Using the above configuration, Traefik will proxy all the traffic related to Mailu's FQDNs without requiring duplicate certificates. diff --git a/towncrier/newsfragments/3061.feature b/towncrier/newsfragments/3061.feature index 66b6e669..b8f3e4cd 100644 --- a/towncrier/newsfragments/3061.feature +++ b/towncrier/newsfragments/3061.feature @@ -1 +1,6 @@ -Introduce new settings for configuring proxying and TLS. Drop TLS_FLAVOR=mail-letsencrypt +Introduce new settings for configuring proxying and TLS. Disable POP3, IMAP and SUBMISSION by default, see https://nostarttls.secvuln.info/ +- Drop TLS_FLAVOR=mail-* +- Change the meaning of PROXY_PROTOCOL, introduce PORTS +- Disable POP3, IMAP and SUBMISSION ports by default, to re-enable ensure PORTS include 110, 143 and 587 + +MANAGESIEVE with implicit TLS is not a thing clients support... so 4190 is enabled by default. From 4e28a053e3f883014e503231b419d52c4d4eae0d Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 8 Apr 2024 09:55:20 +0200 Subject: [PATCH 33/58] enable all ports for tests --- tests/compose/core/mailu.env | 2 ++ tests/compose/fetchmail/mailu.env | 2 ++ tests/compose/filters/mailu.env | 2 ++ tests/compose/webdav/mailu.env | 2 ++ tests/compose/webmail/mailu.env | 2 ++ 5 files changed, 10 insertions(+) diff --git a/tests/compose/core/mailu.env b/tests/compose/core/mailu.env index 30ecd830..5b99d33f 100644 --- a/tests/compose/core/mailu.env +++ b/tests/compose/core/mailu.env @@ -147,3 +147,5 @@ REJECT_UNLISTED_RECIPIENT= INITIAL_ADMIN_ACCOUNT=admin INITIAL_ADMIN_DOMAIN=mailu.io INITIAL_ADMIN_PW=FooBar + +PORTS=25,80,443,110,995,143,993,587,465,4190 \ No newline at end of file diff --git a/tests/compose/fetchmail/mailu.env b/tests/compose/fetchmail/mailu.env index af736912..570fccd4 100644 --- a/tests/compose/fetchmail/mailu.env +++ b/tests/compose/fetchmail/mailu.env @@ -142,3 +142,5 @@ REAL_IP_FROM= # choose wether mailu bounces (no) or rejects (yes) mail when recipient is unknown (value: yes, no) REJECT_UNLISTED_RECIPIENT= + +PORTS=25,80,443,110,995,143,993,587,465,4190 \ No newline at end of file diff --git a/tests/compose/filters/mailu.env b/tests/compose/filters/mailu.env index 0c48baf3..f8536e65 100644 --- a/tests/compose/filters/mailu.env +++ b/tests/compose/filters/mailu.env @@ -142,3 +142,5 @@ REAL_IP_FROM= # choose wether mailu bounces (no) or rejects (yes) mail when recipient is unknown (value: yes, no) REJECT_UNLISTED_RECIPIENT= + +PORTS=25,80,443,110,995,143,993,587,465,4190 \ No newline at end of file diff --git a/tests/compose/webdav/mailu.env b/tests/compose/webdav/mailu.env index 88f3c671..cbda1e67 100644 --- a/tests/compose/webdav/mailu.env +++ b/tests/compose/webdav/mailu.env @@ -142,3 +142,5 @@ REAL_IP_FROM= # choose wether mailu bounces (no) or rejects (yes) mail when recipient is unknown (value: yes, no) REJECT_UNLISTED_RECIPIENT= + +PORTS=25,80,443,110,995,143,993,587,465,4190 \ No newline at end of file diff --git a/tests/compose/webmail/mailu.env b/tests/compose/webmail/mailu.env index 53b86cba..fc78eeab 100644 --- a/tests/compose/webmail/mailu.env +++ b/tests/compose/webmail/mailu.env @@ -142,3 +142,5 @@ REAL_IP_FROM= # choose wether mailu bounces (no) or rejects (yes) mail when recipient is unknown (value: yes, no) REJECT_UNLISTED_RECIPIENT= + +PORTS=25,80,443,110,995,143,993,587,465,4190 \ No newline at end of file From 4837a05c71fd23872643e27ee1301b848628c2f6 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Thu, 18 Apr 2024 18:50:13 +0200 Subject: [PATCH 34/58] simplify the logic --- core/admin/mailu/api/common.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/core/admin/mailu/api/common.py b/core/admin/mailu/api/common.py index 331fdf4e..72e6c269 100644 --- a/core/admin/mailu/api/common.py +++ b/core/admin/mailu/api/common.py @@ -24,19 +24,11 @@ def api_token_authorization(func): if utils.limiter.should_rate_limit_ip(client_ip): abort(429, 'Too many attempts from your IP (rate-limit)' ) if not request.headers.get('Authorization'): - abort(401, 'A valid Bearer token is expected which is provided as request header') - #Client provides 'Authentication: Bearer ' - if (' ' in request.headers.get('Authorization') - and not hmac.compare_digest(request.headers.get('Authorization'), 'Bearer ' + v1.api_token)): + abort(401, 'A valid Authorization header is mandatory') + if (not hmac.compare_digest(request.headers.get('Authorization').removeprefix('Bearer '), v1.api_token)): utils.limiter.rate_limit_ip(client_ip) flask.current_app.logger.warn(f'Invalid API token provided by {client_ip}.') - abort(403, 'A valid Bearer token is expected which is provided as request header') - #Client provides 'Authentication: ' - elif (' ' not in request.headers.get('Authorization') - and not hmac.compare_digest(request.headers.get('Authorization'), v1.api_token)): - utils.limiter.rate_limit_ip(client_ip) - flask.current_app.logger.warn(f'Invalid API token provided by {client_ip}.') - abort(403, 'A valid Bearer token is expected which is provided as request header') + abort(403, 'Invalid API token') flask.current_app.logger.info(f'Valid API token provided by {client_ip}.') return func(*args, **kwds) return decorated_function From 2db75921a2f49fca8bfbb79efd1b762336f1d920 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 20 Apr 2024 08:46:47 +0200 Subject: [PATCH 35/58] Ensure we have an api_token --- core/admin/mailu/api/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/admin/mailu/api/common.py b/core/admin/mailu/api/common.py index 72e6c269..6dc75a88 100644 --- a/core/admin/mailu/api/common.py +++ b/core/admin/mailu/api/common.py @@ -25,7 +25,7 @@ def api_token_authorization(func): abort(429, 'Too many attempts from your IP (rate-limit)' ) if not request.headers.get('Authorization'): abort(401, 'A valid Authorization header is mandatory') - if (not hmac.compare_digest(request.headers.get('Authorization').removeprefix('Bearer '), v1.api_token)): + if len(v1.api_token) < 4 or not hmac.compare_digest(request.headers.get('Authorization').removeprefix('Bearer '), v1.api_token): utils.limiter.rate_limit_ip(client_ip) flask.current_app.logger.warn(f'Invalid API token provided by {client_ip}.') abort(403, 'Invalid API token') From 1d8f041b8710803dba4ba66c9373a95a82078f92 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 20 Apr 2024 23:19:59 +0200 Subject: [PATCH 36/58] access_logs are DEBUG on admin --- core/admin/mailu/__init__.py | 9 ++++++++- setup/flavors/compose/mailu.env | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/core/admin/mailu/__init__.py b/core/admin/mailu/__init__.py index 19731340..dc9dc97d 100644 --- a/core/admin/mailu/__init__.py +++ b/core/admin/mailu/__init__.py @@ -11,7 +11,14 @@ import logging import hmac class NoPingFilter(logging.Filter): + skipAccessLogs = False + + def __init__(self, filterAccessLogs=False): + self.skipAccessLogs = filterAccessLogs + def filter(self, record): + if self.skipAccessLogs and record.args['r'].endswith(' HTTP/1.1'): + return False if record.args['r'].endswith(' /ping HTTP/1.1'): return False if record.args['r'].endswith(' /internal/rspamd/local_domains HTTP/1.1'): @@ -24,7 +31,7 @@ class Logger(glogging.Logger): # Add filters to Gunicorn logger logger = logging.getLogger("gunicorn.access") - logger.addFilter(NoPingFilter()) + logger.addFilter(NoPingFilter(logger.getEffectiveLevel()>logging.DEBUG)) def create_app_from_config(config): """ Create a new application based on the given configuration diff --git a/setup/flavors/compose/mailu.env b/setup/flavors/compose/mailu.env index 7c36a7ec..496a8324 100644 --- a/setup/flavors/compose/mailu.env +++ b/setup/flavors/compose/mailu.env @@ -178,7 +178,7 @@ REAL_IP_FROM={{ real_ip_from }} REJECT_UNLISTED_RECIPIENT={{ reject_unlisted_recipient }} # Log level threshold in start.py (value: CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET) -LOG_LEVEL=WARNING +LOG_LEVEL=INFO # Timezone for the Mailu containers. See this link for all possible values https://en.wikipedia.org/wiki/List_of_tz_database_time_zones TZ=Etc/UTC From 67a53671f44eb38232f79a56aebb70c84230a8e0 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Fri, 3 May 2024 14:31:18 +0200 Subject: [PATCH 37/58] Fix purge_user.sh --- scripts/purge_user.sh | 2 +- towncrier/newsfragments/3238.bugfix | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 towncrier/newsfragments/3238.bugfix diff --git a/scripts/purge_user.sh b/scripts/purge_user.sh index 07d4a354..926d861d 100755 --- a/scripts/purge_user.sh +++ b/scripts/purge_user.sh @@ -1,7 +1,7 @@ #!/bin/bash # get id of running admin container -admin="$(docker compose ps admin --format=json | jq -r '.[].ID')" +admin="$(docker compose ps admin --format=json | jq -r '.ID')" if [[ -z "${admin}" ]]; then echo "Sorry, can't find running mailu admin container." echo "You need to start this in the path containing your docker-compose.yml." diff --git a/towncrier/newsfragments/3238.bugfix b/towncrier/newsfragments/3238.bugfix new file mode 100644 index 00000000..adfa975a --- /dev/null +++ b/towncrier/newsfragments/3238.bugfix @@ -0,0 +1 @@ +Fix purge_user.sh From 5b5d526d79d207eefb1416c58ff2038b148f8c4c Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Wed, 8 May 2024 16:10:10 +0200 Subject: [PATCH 38/58] Fix CVE-2024-1135 --- core/base/requirements-prod.txt | 2 +- towncrier/newsfragments/3251.bugfix | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 towncrier/newsfragments/3251.bugfix diff --git a/core/base/requirements-prod.txt b/core/base/requirements-prod.txt index 20820040..0ae4b0e2 100644 --- a/core/base/requirements-prod.txt +++ b/core/base/requirements-prod.txt @@ -31,7 +31,7 @@ Flask-SQLAlchemy==3.1.1 Flask-WTF==1.2.1 frozenlist==1.4.1 greenlet==3.0.3 -gunicorn==21.2.0 +gunicorn==22.0.0 idna==3.6 importlib-resources==6.1.1 infinity==1.5 diff --git a/towncrier/newsfragments/3251.bugfix b/towncrier/newsfragments/3251.bugfix new file mode 100644 index 00000000..e096521f --- /dev/null +++ b/towncrier/newsfragments/3251.bugfix @@ -0,0 +1 @@ +Fix CVE-2024-1135 From 3565ab9a3b049756f6c81e655acd1bd67fc7ec83 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Fri, 17 May 2024 08:32:39 +0200 Subject: [PATCH 39/58] Fix #3260: double-quotes should be allowed in ooo --- core/admin/mailu/internal/templates/default.sieve | 2 +- towncrier/newsfragments/3260.bugfix | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 towncrier/newsfragments/3260.bugfix diff --git a/core/admin/mailu/internal/templates/default.sieve b/core/admin/mailu/internal/templates/default.sieve index 0e97c067..d5c28c8a 100644 --- a/core/admin/mailu/internal/templates/default.sieve +++ b/core/admin/mailu/internal/templates/default.sieve @@ -31,6 +31,6 @@ if spamtest :percent :value "gt" :comparator "i;ascii-numeric" "{{ user.spam_thr {% 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 }}"; + vacation :days 1 {% if user.displayed_name != "" %}:from "{{ user.displayed_name | replace("\"", "\\\"") }} <{{ user.email | replace("\"", "\\\"") }}>"{% endif %} :subject "{{ user.reply_subject | replace("\"", "\\\"") }}" "{{ user.reply_body | replace("\"", "\\\"") }}"; } {% endif %} diff --git a/towncrier/newsfragments/3260.bugfix b/towncrier/newsfragments/3260.bugfix new file mode 100644 index 00000000..268da281 --- /dev/null +++ b/towncrier/newsfragments/3260.bugfix @@ -0,0 +1 @@ +Fix a bug preventing double quotes from being used in ooo messages From e79e055ac1cd3f33e9c0c83e6a5d15883e6b9b2a Mon Sep 17 00:00:00 2001 From: ctrl-i <1422608+ctrl-i@users.noreply.github.com> Date: Mon, 20 May 2024 07:47:39 +0100 Subject: [PATCH 40/58] Update Dockerfile Roundcube incremented to 1.6.7 due to XSS vulnerabilities --- webmails/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webmails/Dockerfile b/webmails/Dockerfile index d233a1a8..03389ff6 100644 --- a/webmails/Dockerfile +++ b/webmails/Dockerfile @@ -29,7 +29,7 @@ RUN set -euxo pipefail \ ; mkdir -p /run/nginx /conf # roundcube -ENV ROUNDCUBE_URL https://github.com/roundcube/roundcubemail/releases/download/1.6.6/roundcubemail-1.6.6-complete.tar.gz +ENV ROUNDCUBE_URL https://github.com/roundcube/roundcubemail/releases/download/1.6.7/roundcubemail-1.6.7-complete.tar.gz ENV CARDDAV_URL https://github.com/mstilkerich/rcmcarddav/releases/download/v5.1.0/carddav-v5.1.0.tar.gz RUN set -euxo pipefail \ From a53b869d8a6af94ad8790e206eee8a39f7e349fc Mon Sep 17 00:00:00 2001 From: ctrl-i <1422608+ctrl-i@users.noreply.github.com> Date: Mon, 20 May 2024 07:49:49 +0100 Subject: [PATCH 41/58] Create 3261.bugfix Updated roundcube to 1.6.7 due to known XSS vulnerabilities --- towncrier/newsfragments/3261.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 towncrier/newsfragments/3261.bugfix diff --git a/towncrier/newsfragments/3261.bugfix b/towncrier/newsfragments/3261.bugfix new file mode 100644 index 00000000..2564c1dc --- /dev/null +++ b/towncrier/newsfragments/3261.bugfix @@ -0,0 +1 @@ +Updated roundcube to version 1.6.7 From a55a9d89bacc2f3e5aabff865f51d42970787c88 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 6 May 2024 10:21:15 +0200 Subject: [PATCH 42/58] Update all dependencies --- core/base/requirements-build.txt | 6 +-- core/base/requirements-prod.txt | 84 +++++++++++++++---------------- docs/Dockerfile | 6 +-- docs/requirements.txt | 8 +-- tests/requirements.txt | 6 +-- towncrier/newsfragments/3032.misc | 1 + webmails/Dockerfile | 2 +- webmails/snuffleupagus.rules | 4 +- 8 files changed, 60 insertions(+), 57 deletions(-) diff --git a/core/base/requirements-build.txt b/core/base/requirements-build.txt index ed145a00..ea2e4f3f 100644 --- a/core/base/requirements-build.txt +++ b/core/base/requirements-build.txt @@ -1,3 +1,3 @@ -pip==23.3.1 -setuptools==68.2.2 -wheel==0.41.3 +pip==24.0 +setuptools==69.5.1 +wheel==0.43.0 diff --git a/core/base/requirements-prod.txt b/core/base/requirements-prod.txt index 0ae4b0e2..6152a45a 100644 --- a/core/base/requirements-prod.txt +++ b/core/base/requirements-prod.txt @@ -1,87 +1,87 @@ -aiodns==3.1.1 -aiohttp==3.9.3 +aiodns==3.2.0 +aiohttp==3.9.5 aiosignal==1.3.1 alembic==1.13.1 aniso8601==9.0.1 attrs==23.2.0 -Babel==2.14.0 -bcrypt==4.1.2 -blinker==1.7.0 -certifi==2023.11.17 +Babel==2.15.0 +bcrypt==4.1.3 +blinker==1.8.1 +certifi==2024.2.2 cffi==1.16.0 charset-normalizer==3.3.2 click==8.1.7 colorclass==2.2.2 -cryptography==42.0.5 +cryptography==42.0.6 defusedxml==0.7.1 Deprecated==1.2.14 -dnspython==2.5.0 +dnspython==2.6.1 dominate==2.9.1 easygui==0.98.3 -email-validator==2.1.0.post1 -Flask==3.0.1 +email-validator==2.1.1 +Flask==3.0.3 flask-babel==4.0.0 Flask-Bootstrap==3.3.7.1 -Flask-DebugToolbar==0.14.1 +Flask-DebugToolbar==0.15.1 Flask-Login==0.6.3 -flask-marshmallow==1.1.0 -Flask-Migrate==4.0.5 +flask-marshmallow==1.2.1 +Flask-Migrate==4.0.7 flask-restx==1.3.0 Flask-SQLAlchemy==3.1.1 Flask-WTF==1.2.1 frozenlist==1.4.1 greenlet==3.0.3 gunicorn==22.0.0 -idna==3.6 -importlib-resources==6.1.1 +idna==3.7 +importlib-resources==6.4.0 infinity==1.5 intervals==0.9.2 -itsdangerous==2.1.2 -Jinja2==3.1.3 -jsonschema==4.21.1 +itsdangerous==2.2.0 +Jinja2==3.1.4 +jsonschema==4.22.0 jsonschema-specifications==2023.12.1 -limits==3.7.0 -Mako==1.3.0 -MarkupSafe==2.1.4 -marshmallow==3.20.2 -marshmallow-sqlalchemy==0.30.0 -msoffcrypto-tool==5.3.1 -multidict==6.0.4 -mysql-connector-python==8.3.0 +limits==3.11.0 +Mako==1.3.3 +MarkupSafe==2.1.5 +marshmallow==3.21.2 +marshmallow-sqlalchemy==1.0.0 +msoffcrypto-tool==5.4.0 +multidict==6.0.5 +mysql-connector-python==8.4.0 olefile==0.47 oletools==0.60.1 -packaging==23.2 +packaging==24.0 passlib==1.7.4 pcodedmp==1.2.6 podop @ file:///app/libs/podop postfix-mta-sts-resolver==1.4.0 psycopg2-binary==2.9.9 pycares==4.4.0 -pycparser==2.21 -Pygments==2.17.2 +pycparser==2.22 +Pygments==2.18.0 pyparsing==2.4.7 -python-dateutil==2.8.2 +python-dateutil==2.9.0.post0 python-magic==0.4.27 -pytz==2023.3.post1 +pytz==2024.1 PyYAML==6.0.1 -Radicale==3.1.8 -redis==5.0.1 -referencing==0.32.1 +Radicale==3.1.9 +redis==5.0.4 +referencing==0.35.1 requests==2.31.0 -rpds-py==0.17.1 +rpds-py==0.18.0 six==1.16.0 socrate @ file:///app/libs/socrate -SQLAlchemy==2.0.25 +SQLAlchemy==2.0.30 srslib==0.1.4 tabulate==0.9.0 tenacity==8.2.3 -typing_extensions==4.9.0 -urllib3==2.1.0 -validators==0.22.0 +typing_extensions==4.11.0 +urllib3==2.2.1 +validators==0.28.1 visitor==0.1.3 -vobject==0.9.6.1 -watchdog==3.0.0 -Werkzeug==3.0.1 +vobject==0.9.7 +watchdog==4.0.0 +Werkzeug==3.0.3 wrapt==1.16.0 WTForms==3.1.2 WTForms-Components==0.10.5 diff --git a/docs/Dockerfile b/docs/Dockerfile index 958eaf87..a9dbb109 100644 --- a/docs/Dockerfile +++ b/docs/Dockerfile @@ -1,5 +1,5 @@ # Convert .rst files to .html in temporary build container -FROM python:3.12.0-alpine3.18 AS build +FROM python:3.12.3-alpine3.19 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.25.3-alpine +FROM nginx:1.25.5-alpine ARG version=master ARG pinned_version=master @@ -30,4 +30,4 @@ COPY --from=build /build/$VERSION /build/$VERSION EXPOSE 80/tcp CMD nginx -g "daemon off;" -RUN echo $pinned_version >> /version \ No newline at end of file +RUN echo $pinned_version >> /version diff --git a/docs/requirements.txt b/docs/requirements.txt index 46d263a7..56a54f27 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ recommonmark==0.7.1 -Sphinx==7.2.6 -sphinx-autobuild==2021.3.14 -sphinx-rtd-theme==1.3.0 -docutils==0.18.1 +Sphinx==7.3.7 +sphinx-autobuild==2024.4.16 +sphinx-rtd-theme==2.0.0 +docutils==0.20.1 diff --git a/tests/requirements.txt b/tests/requirements.txt index 67060918..013248c9 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,3 +1,3 @@ -docker==4.2.2 -colorama==0.4.3 -managesieve==0.7.1 +docker==7.0.0 +colorama==0.4.6 +managesieve==0.8 diff --git a/towncrier/newsfragments/3032.misc b/towncrier/newsfragments/3032.misc index 1ecc2b57..6d6f4d86 100644 --- a/towncrier/newsfragments/3032.misc +++ b/towncrier/newsfragments/3032.misc @@ -1 +1,2 @@ Update all python dependencies in preparation of next Mailu release. +Update snappymail to 2.36.1 diff --git a/webmails/Dockerfile b/webmails/Dockerfile index 03389ff6..8477126a 100644 --- a/webmails/Dockerfile +++ b/webmails/Dockerfile @@ -55,7 +55,7 @@ COPY roundcube/config/config.inc.carddav.php /var/www/roundcube/plugins/carddav/ # snappymail -ENV SNAPPYMAIL_URL https://github.com/the-djmaze/snappymail/releases/download/v2.31.0/snappymail-2.31.0.tar.gz +ENV SNAPPYMAIL_URL https://github.com/the-djmaze/snappymail/releases/download/v2.36.1/snappymail-2.36.1.tar.gz RUN set -euxo pipefail \ ; mkdir /var/www/snappymail \ diff --git a/webmails/snuffleupagus.rules b/webmails/snuffleupagus.rules index b3f69819..4cbe966d 100644 --- a/webmails/snuffleupagus.rules +++ b/webmails/snuffleupagus.rules @@ -71,6 +71,7 @@ sp.disable_function.function("include").drop() # Prevent `system`-related injections sp.disable_function.function("system").param("command").value_r("[$|;&`\\n\\(\\)\\\\]").drop(); +sp.disable_function.function("exec_shell").filename_r("/var/www/snappymail/snappymail/v/[0-9]+\.[0-9]+\.[0-9]+/app/libraries/snappymail/gpg/base.php").allow(); sp.disable_function.function("shell_exec").param("command").value_r("[$|;&`\\n\\(\\)\\\\]").drop(); sp.disable_function.function("exec").param("command").value_r("[$|;&`\\n\\(\\)\\\\]").drop(); # This is **very** broad but doing better is non-straightforward @@ -91,17 +92,18 @@ sp.disable_function.function("ini_get").filename("/var/www/roundcube/plugins/man sp.disable_function.function("ini_get").param("option").value("allow_url_fopen").drop(); sp.disable_function.function("ini_get").param("option").value("open_basedir").drop(); sp.disable_function.function("ini_get").param("option").value_r("suhosin").drop(); +sp.disable_function.function("function_exists").filename_r("/var/www/snappymail/snappymail/v/[0-9]+\.[0-9]+\.[0-9]+/app/libraries/snappymail/gpg/base.php").allow(); sp.disable_function.function("function_exists").param("function").value("eval").drop(); sp.disable_function.function("function_exists").param("function").value("exec").drop(); sp.disable_function.function("function_exists").param("function").value("system").drop(); sp.disable_function.function("function_exists").param("function").value("shell_exec").drop(); sp.disable_function.function("function_exists").param("function").value("proc_open").drop(); sp.disable_function.function("function_exists").param("function").value("passthru").drop(); +sp.disable_function.function("is_callable").filename_r("/var/www/snappymail/snappymail/v/[0-9]+\.[0-9]+\.[0-9]+/app/libraries/snappymail/gpg/base.php").allow(); sp.disable_function.function("is_callable").param("value").value("eval").drop(); sp.disable_function.function("is_callable").param("value").value("exec").drop(); sp.disable_function.function("is_callable").param("value").value("system").drop(); sp.disable_function.function("is_callable").param("value").value("shell_exec").drop(); -sp.disable_function.function("is_callable").filename_r("^/var/www/snappymail/snappymail/v/[0-9]+\.[0-9]+\.[0-9]+/app/libraries/snappymail/pgp/gpg\.php$").param("value").value("proc_open").allow(); sp.disable_function.function("is_callable").param("value").value("proc_open").drop(); sp.disable_function.function("is_callable").param("value").value("passthru").drop(); From 155a4cce5ebb1dd44380f9210fc7681a56ba4b66 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 6 May 2024 10:41:41 +0200 Subject: [PATCH 43/58] maybe fix tests --- optional/radicale/radicale.conf | 1 - tests/requirements.txt | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/optional/radicale/radicale.conf b/optional/radicale/radicale.conf index 7914e6ea..d9859508 100644 --- a/optional/radicale/radicale.conf +++ b/optional/radicale/radicale.conf @@ -1,5 +1,4 @@ [server] -hosts = :5232 ssl = False [encoding] diff --git a/tests/requirements.txt b/tests/requirements.txt index 013248c9..f63c040e 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,3 +1,3 @@ docker==7.0.0 colorama==0.4.6 -managesieve==0.8 +managesieve==0.7.1 From dbd32cabf9db2ecd3736434bf486dc8154b17634 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 6 May 2024 11:01:54 +0200 Subject: [PATCH 44/58] retry with cargo --- docs/Dockerfile | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/Dockerfile b/docs/Dockerfile index a9dbb109..7a82e49b 100644 --- a/docs/Dockerfile +++ b/docs/Dockerfile @@ -7,12 +7,19 @@ ENV VERSION=$version COPY requirements.txt /requirements.txt COPY . /docs -RUN apk add --no-cache --virtual .build-deps \ - gcc musl-dev \ - && pip3 install -r /requirements.txt \ - && mkdir -p /build/$VERSION \ - && sphinx-build -W /docs /build/$VERSION \ - && apk del .build-deps +RUN set -euxo pipefail \ + ; deps="gcc musl-dev" \ + ; [[ "${machine}" != x86_64 ]] && \ + deps="${deps} cargo" \ + ; apk add --no-cache --virtual .build-deps ${deps} \ + ; [[ "${machine}" == armv7* ]] && \ + mkdir -p /root/.cargo/registry/index && \ + git clone --bare https://github.com/rust-lang/crates.io-index.git /root/.cargo/registry/index/github.com-1285ae84e5963aae \ + ; pip3 install -r /requirements.txt \ + ; mkdir -p /build/$VERSION \ + ; sphinx-build -W /docs /build/$VERSION \ + ; apk del .build-deps \ + ; rm -rf /root/.cargo # Build nginx deployment image including generated html From 4c5ad204592fb675d3e25313e1dfd85b5fe268d2 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 6 May 2024 11:05:16 +0200 Subject: [PATCH 45/58] doh --- docs/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Dockerfile b/docs/Dockerfile index 7a82e49b..25ecc496 100644 --- a/docs/Dockerfile +++ b/docs/Dockerfile @@ -8,6 +8,7 @@ COPY requirements.txt /requirements.txt COPY . /docs RUN set -euxo pipefail \ + ; machine="$(uname -m)" \ ; deps="gcc musl-dev" \ ; [[ "${machine}" != x86_64 ]] && \ deps="${deps} cargo" \ From 2bf19ead473d55f956efd08c87c09df8c3b197c3 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Tue, 21 May 2024 10:15:27 +0200 Subject: [PATCH 46/58] Switch to the upstream image of Tika See https://github.com/apache/tika-docker/pull/19 Upgrade to 2.9.2 while at it --- setup/flavors/compose/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/flavors/compose/docker-compose.yml b/setup/flavors/compose/docker-compose.yml index 6f7c3947..df89d327 100644 --- a/setup/flavors/compose/docker-compose.yml +++ b/setup/flavors/compose/docker-compose.yml @@ -158,7 +158,7 @@ services: {% if tika_enabled %} fts_attachments: - image: ghcr.io/paperless-ngx/tika:2.9.1-full + image: apache/tika:2.9.2-alpha-multi-arch-full hostname: tika logging: driver: journald From 3c9f825e4f3a9c6b5f6f62a0e66cecd1a5c80920 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Tue, 21 May 2024 10:36:53 +0200 Subject: [PATCH 47/58] Pin a version of requests that works See https://github.com/psf/requests/issues/6707 --- tests/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/requirements.txt b/tests/requirements.txt index f63c040e..8d06bd70 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,3 +1,4 @@ docker==7.0.0 colorama==0.4.6 managesieve==0.7.1 +requests==2.31.0 From 8c92bd5b4ff8cf06e92297252d8c72862e88b955 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Tue, 21 May 2024 21:14:13 +0200 Subject: [PATCH 48/58] Use the new image name --- setup/flavors/compose/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/flavors/compose/docker-compose.yml b/setup/flavors/compose/docker-compose.yml index df89d327..8cd374cd 100644 --- a/setup/flavors/compose/docker-compose.yml +++ b/setup/flavors/compose/docker-compose.yml @@ -158,7 +158,7 @@ services: {% if tika_enabled %} fts_attachments: - image: apache/tika:2.9.2-alpha-multi-arch-full + image: apache/tika:2.9.2.1-full hostname: tika logging: driver: journald From eddcedf5cfbd6ed0cf6633784a8196a8ba330ae4 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 27 May 2024 15:34:53 +0200 Subject: [PATCH 49/58] Alpine 3.20 --- core/base/Dockerfile | 2 +- core/dovecot/Dockerfile | 1 - towncrier/newsfragments/3277.bugfix | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 towncrier/newsfragments/3277.bugfix diff --git a/core/base/Dockerfile b/core/base/Dockerfile index 3a5e0b41..4317fad6 100644 --- a/core/base/Dockerfile +++ b/core/base/Dockerfile @@ -3,7 +3,7 @@ # base system image (intermediate) # Note when updating the alpine tag, first manually run the workflow .github/workflows/mirror.yml. # Just run the workflow with the tag that must be synchronised. -ARG DISTRO=ghcr.io/mailu/alpine:3.19.1 +ARG DISTRO=ghcr.io/mailu/alpine:3.20 FROM $DISTRO as system ENV TZ=Etc/UTC LANG=C.UTF-8 diff --git a/core/dovecot/Dockerfile b/core/dovecot/Dockerfile index 25eb9263..72753e1e 100644 --- a/core/dovecot/Dockerfile +++ b/core/dovecot/Dockerfile @@ -7,7 +7,6 @@ ARG VERSION LABEL version=$VERSION RUN set -euxo pipefail \ - ; 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 diff --git a/towncrier/newsfragments/3277.bugfix b/towncrier/newsfragments/3277.bugfix new file mode 100644 index 00000000..0120a32c --- /dev/null +++ b/towncrier/newsfragments/3277.bugfix @@ -0,0 +1 @@ +Switch to alpine 3.20, remove a dependency on edge for dovecot From 47c53a429c3c95ef903935beaec7822188475139 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 27 May 2024 15:42:42 +0200 Subject: [PATCH 50/58] towncrier --- towncrier/newsfragments/{3277.bugfix => 3279.misc} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename towncrier/newsfragments/{3277.bugfix => 3279.misc} (100%) diff --git a/towncrier/newsfragments/3277.bugfix b/towncrier/newsfragments/3279.misc similarity index 100% rename from towncrier/newsfragments/3277.bugfix rename to towncrier/newsfragments/3279.misc From 6dbceeeefa771fc7f29cf49f640e00699bf3e52e Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 27 May 2024 15:43:50 +0200 Subject: [PATCH 51/58] doh --- core/base/Dockerfile | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/base/Dockerfile b/core/base/Dockerfile index 4317fad6..39d9f29c 100644 --- a/core/base/Dockerfile +++ b/core/base/Dockerfile @@ -66,8 +66,6 @@ RUN set -euxo pipefail \ ; rm -rf src/tests/*php7*/ src/tests/*session*/ src/tests/broken_configuration/ src/tests/*cookie* src/tests/upload_validation/ \ ; apk add --virtual .build-deps php83-dev php83-cgi php83-simplexml php83-xml pcre-dev build-base php83-pear php83-openssl re2c \ ; pecl83 install vld-beta \ - ; ln -s /usr/bin/phpize83 /usr/bin/phpize \ - ; ln -s /usr/bin/php-config83 /usr/bin/php-config \ ; make -j $(grep -c processor /proc/cpuinfo) release \ ; cp src/.libs/snuffleupagus.so /app \ ; rm -rf /root/.cargo /tmp/*.pem /root/.cache From 72bf53105cea7be5094c7f917f01d7e5725fca06 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 27 May 2024 16:03:03 +0200 Subject: [PATCH 52/58] doh2 --- webmails/Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/webmails/Dockerfile b/webmails/Dockerfile index 8477126a..950c3f3c 100644 --- a/webmails/Dockerfile +++ b/webmails/Dockerfile @@ -20,7 +20,6 @@ RUN set -euxo pipefail \ aspell-uk aspell-ru aspell-fr aspell-de aspell-en \ ; rm /etc/nginx/http.d/default.conf \ ; rm /etc/php83/php-fpm.d/www.conf \ - ; ln -s /usr/bin/php83 /usr/bin/php \ ; mkdir -m 700 /root/.gnupg/ \ ; gpg --import /tmp/snappymail.asc \ ; gpg --import /tmp/roundcube.asc \ From 7df8cdbe017db19eb7220dddf1248726becb0a23 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 27 May 2024 18:33:25 +0200 Subject: [PATCH 53/58] Ensure we normalize for all languages --- core/dovecot/conf/dovecot.conf | 6 +++--- towncrier/newsfragments/3279.misc | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/core/dovecot/conf/dovecot.conf b/core/dovecot/conf/dovecot.conf index 4ea4fc43..be0035c7 100644 --- a/core/dovecot/conf/dovecot.conf +++ b/core/dovecot/conf/dovecot.conf @@ -69,9 +69,9 @@ plugin { 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_filters = normalizer-icu lowercase snowball stopwords + fts_filters_en = normalizer-icu lowercase snowball english-possessive stopwords + fts_filters_fr = normalizer-icu 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/ diff --git a/towncrier/newsfragments/3279.misc b/towncrier/newsfragments/3279.misc index 0120a32c..c7a3c655 100644 --- a/towncrier/newsfragments/3279.misc +++ b/towncrier/newsfragments/3279.misc @@ -1 +1,2 @@ Switch to alpine 3.20, remove a dependency on edge for dovecot +Ensure we user normalizer-icu in all languages From 12ccdebd20b81efb6c48cfb565a839520983b4e8 Mon Sep 17 00:00:00 2001 From: Dimitri Huisman Date: Sun, 9 Jun 2024 09:44:42 +0000 Subject: [PATCH 54/58] Update documentation with new length requirement for API_TOKEN --- docs/api.rst | 2 +- docs/configuration.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index f1c01b85..c483b798 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -12,7 +12,7 @@ It can also be manually configured via mailu.env: * ``API`` - Expose the API interface (value: true, false) * ``WEB_API`` - Path to the API interface -* ``API_TOKEN`` - API token for authentication +* ``API_TOKEN`` - API token for authentication (with minimum length of 3 characters) For more information refer to the detailed descriptions in the :ref:`configuration reference `. diff --git a/docs/configuration.rst b/docs/configuration.rst index 50a576fd..f0eb6c96 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -218,6 +218,7 @@ Advanced settings 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. +The minimum length is 3 characters. This token must be passed as request header to the API as authentication token. This is a mandatory setting for using the RESTful API. From c63bd0ce38832bea9e12894d9fca1f567506286b Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sun, 9 Jun 2024 11:59:05 +0200 Subject: [PATCH 55/58] Update core/nginx/conf/nginx.conf Co-authored-by: Dimitri Huisman <52963853+Diman0@users.noreply.github.com> --- core/nginx/conf/nginx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/nginx/conf/nginx.conf b/core/nginx/conf/nginx.conf index bbd86fdc..e7d4885e 100644 --- a/core/nginx/conf/nginx.conf +++ b/core/nginx/conf/nginx.conf @@ -96,7 +96,7 @@ http { # Listen on HTTP only in kubernetes or behind reverse proxy {% if TLS_FLAVOR in [ 'mail-letsencrypt', 'notls', 'mail' ] %} - listen 80{% if PROXY_HTTPPROTOCOL_80 %} proxy_protocol{% endif %}; + listen 80{% if PROXY_PROTOCOL_80 %} proxy_protocol{% endif %}; {% if SUBNET6 %} listen [::]:80{% if PROXY_PROTOCOL_80 %} proxy_protocol{% endif %}; {% endif %} From 52e02d4c565edcce288ab3adcad9a2567c4aaa7e Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sun, 9 Jun 2024 11:59:12 +0200 Subject: [PATCH 56/58] Update core/nginx/dovecot/proxy.conf Co-authored-by: Dimitri Huisman <52963853+Diman0@users.noreply.github.com> --- core/nginx/dovecot/proxy.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/nginx/dovecot/proxy.conf b/core/nginx/dovecot/proxy.conf index 95972547..a6e64719 100644 --- a/core/nginx/dovecot/proxy.conf +++ b/core/nginx/dovecot/proxy.conf @@ -162,7 +162,7 @@ service submission-login { inet_listener submissions { port = 465 ssl = yes -{%- if PROXY_PROTOCOL_645 %} +{%- if PROXY_PROTOCOL_465 %} haproxy = yes {% endif %} } From 712f5f48a871dd76daad979cf101012d6b413a32 Mon Sep 17 00:00:00 2001 From: Dimitri Huisman Date: Sun, 9 Jun 2024 10:13:10 +0000 Subject: [PATCH 57/58] Add missing translation for German --- core/admin/mailu/translations/de/LC_MESSAGES/messages.po | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/admin/mailu/translations/de/LC_MESSAGES/messages.po b/core/admin/mailu/translations/de/LC_MESSAGES/messages.po index afd9abda..0f77e563 100644 --- a/core/admin/mailu/translations/de/LC_MESSAGES/messages.po +++ b/core/admin/mailu/translations/de/LC_MESSAGES/messages.po @@ -307,6 +307,14 @@ msgstr "Antispam" msgid "RSPAMD status page" msgstr "RSPAMD Statusseite" +#: mailu/ui/templates/client.html:62 +msgid "If you use an Apple device," +msgstr "Falls es sich um ein Apple-Gerät handelt," + +#: mailu/ui/templates/client.html:63 +msgid "click here to auto-configure it." +msgstr "kann dieses hier automatisch eingerichtet werden." + #: mailu/ui/templates/client.html:8 msgid "configure your email client" msgstr "Informationen zur Einrichtung Ihres Email-Clients" From e5d2fc5de028ca4d9290409956c4890734e9c885 Mon Sep 17 00:00:00 2001 From: Dimitri Huisman Date: Sun, 9 Jun 2024 10:18:43 +0000 Subject: [PATCH 58/58] Update newsfragment --- towncrier/newsfragments/3029.bugfix | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/towncrier/newsfragments/3029.bugfix b/towncrier/newsfragments/3029.bugfix index a3602429..a502affe 100644 --- a/towncrier/newsfragments/3029.bugfix +++ b/towncrier/newsfragments/3029.bugfix @@ -1,5 +1,5 @@ Added missing translations for Dutch, German and French. -4 new strings were introduced after 2.0. These must be translated for all languages. +6 new strings were introduced after 2.0. These must be translated for all languages. If this translation is missing for your native language, please submit a PR with the translation, or open a new issue where you mention the translated strings. @@ -18,4 +18,12 @@ msgstr "translation of password change at next login" #: mailu/ui/forms.py:98 msgid "Allow the user to spoof the sender (send email as anyone)" -msgstr "translation of Allow the user to spoof the sender (send email as anyone)" \ No newline at end of file +msgstr "translation of Allow the user to spoof the sender (send email as anyone)" + +#: mailu/ui/templates/client.html:62 +msgid "If you use an Apple device," +msgstr "translation of If you use an Apple device," + +#: mailu/ui/templates/client.html:63 +msgid "click here to auto-configure it." +msgstr "translation of click here to auto-configure it." \ No newline at end of file