1
0
mirror of https://github.com/Mailu/Mailu.git synced 2025-02-07 13:08:22 +02:00

Add automatic tests for RESTful API. Fix all remaining issues that I could find with the API.

This commit is contained in:
Dimitri Huisman 2024-03-22 15:01:37 +00:00
parent c8e3270724
commit 2558ae3bc9
No known key found for this signature in database
14 changed files with 1065 additions and 46 deletions

View File

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

View File

@ -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('/<string:alias>')
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/<string:domain>')
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

View File

@ -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('/<domain>/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('/<domain>/manager/<email>')
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('/<domain>/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')

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

151
tests/compose/api/mailu.env Normal file
View File

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