mirror of
				https://github.com/optim-enterprises-bv/Mailu.git
				synced 2025-11-03 19:47:52 +00:00 
			
		
		
		
	Merge branch 'apiv1' of https://github.com/ghostwheel42/Mailu into feature-445-restful-api-ghostwheel
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -10,6 +10,7 @@ pip-selfcheck.json
 | 
				
			|||||||
/docs/include
 | 
					/docs/include
 | 
				
			||||||
/docs/_build
 | 
					/docs/_build
 | 
				
			||||||
/.env
 | 
					/.env
 | 
				
			||||||
 | 
					/.venv
 | 
				
			||||||
/docker-compose.yml
 | 
					/docker-compose.yml
 | 
				
			||||||
/.idea
 | 
					/.idea
 | 
				
			||||||
/.vscode
 | 
					/.vscode
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -70,10 +70,12 @@ def create_app_from_config(config):
 | 
				
			|||||||
        return utils.flask_babel.format_datetime(value) if value else ''
 | 
					        return utils.flask_babel.format_datetime(value) if value else ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Import views
 | 
					    # Import views
 | 
				
			||||||
    from mailu import ui, internal, sso
 | 
					    from mailu import ui, internal, sso, api
 | 
				
			||||||
    app.register_blueprint(ui.ui, url_prefix=app.config['WEB_ADMIN'])
 | 
					    app.register_blueprint(ui.ui, url_prefix=app.config['WEB_ADMIN'])
 | 
				
			||||||
    app.register_blueprint(internal.internal, url_prefix='/internal')
 | 
					    app.register_blueprint(internal.internal, url_prefix='/internal')
 | 
				
			||||||
    app.register_blueprint(sso.sso, url_prefix='/sso')
 | 
					    app.register_blueprint(sso.sso, url_prefix='/sso')
 | 
				
			||||||
 | 
					    if app.config.get('API_TOKEN'):
 | 
				
			||||||
 | 
					        api.register(app, web_api_root=app.config.get('WEB_API'))
 | 
				
			||||||
    return app
 | 
					    return app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										24
									
								
								core/admin/mailu/api/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								core/admin/mailu/api/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					from flask import redirect, url_for, Blueprint
 | 
				
			||||||
 | 
					from flask_restx import apidoc
 | 
				
			||||||
 | 
					from . import v1 as APIv1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def register(app, web_api_root):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    APIv1.app = app
 | 
				
			||||||
 | 
					    # register api bluprint(s)
 | 
				
			||||||
 | 
					    apidoc.apidoc.url_prefix = f'{web_api_root}/v{int(APIv1.VERSION)}'
 | 
				
			||||||
 | 
					    APIv1.api_token = app.config['API_TOKEN']
 | 
				
			||||||
 | 
					    app.register_blueprint(APIv1.blueprint, url_prefix=f'{web_api_root}/v{int(APIv1.VERSION)}')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # add redirect to current api version
 | 
				
			||||||
 | 
					    redirect_api = Blueprint('redirect_api', __name__)
 | 
				
			||||||
 | 
					    @redirect_api.route('/')
 | 
				
			||||||
 | 
					    def redir():
 | 
				
			||||||
 | 
					        return redirect(url_for(f'{APIv1.blueprint.name}.root'))
 | 
				
			||||||
 | 
					    app.register_blueprint(redirect_api, url_prefix=f'{web_api_root}')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # swagger ui config
 | 
				
			||||||
 | 
					    app.config.SWAGGER_UI_DOC_EXPANSION = 'list'
 | 
				
			||||||
 | 
					    app.config.SWAGGER_UI_OPERATION_ID = True
 | 
				
			||||||
 | 
					    app.config.SWAGGER_UI_REQUEST_DURATION = True
 | 
				
			||||||
 | 
					    app.config.RESTX_MASK_SWAGGER = False
 | 
				
			||||||
							
								
								
									
										42
									
								
								core/admin/mailu/api/common.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								core/admin/mailu/api/common.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					from .. import models, utils
 | 
				
			||||||
 | 
					from . import v1
 | 
				
			||||||
 | 
					from flask import request
 | 
				
			||||||
 | 
					import flask
 | 
				
			||||||
 | 
					import hmac
 | 
				
			||||||
 | 
					from functools import wraps
 | 
				
			||||||
 | 
					from flask_restx import abort
 | 
				
			||||||
 | 
					from sqlalchemy.sql.expression import label
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def fqdn_in_use(name):
 | 
				
			||||||
 | 
					    d = models.db.session.query(label('name', models.Domain.name))
 | 
				
			||||||
 | 
					    a = models.db.session.query(label('name', models.Alternative.name))
 | 
				
			||||||
 | 
					    r = models.db.session.query(label('name', models.Relay.name))
 | 
				
			||||||
 | 
					    u = d.union_all(a).union_all(r).filter_by(name=name)
 | 
				
			||||||
 | 
					    if models.db.session.query(u.exists()).scalar():
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					    return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					""" Decorator for validating api token for authentication """
 | 
				
			||||||
 | 
					def api_token_authorization(func):
 | 
				
			||||||
 | 
					    @wraps(func)
 | 
				
			||||||
 | 
					    def decorated_function(*args, **kwds):
 | 
				
			||||||
 | 
					        client_ip = flask.request.headers.get('X-Real-IP', flask.request.remote_addr)
 | 
				
			||||||
 | 
					        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 <token>'
 | 
				
			||||||
 | 
					        if (' ' in request.headers.get('Authorization')
 | 
				
			||||||
 | 
					                and not hmac.compare_digest(request.headers.get('Authorization'), '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: <token>'
 | 
				
			||||||
 | 
					        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')
 | 
				
			||||||
 | 
					        flask.current_app.logger.info(f'Valid API token provided by {client_ip}.')
 | 
				
			||||||
 | 
					        return func(*args, **kwds)
 | 
				
			||||||
 | 
					    return decorated_function
 | 
				
			||||||
							
								
								
									
										43
									
								
								core/admin/mailu/api/v1/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								core/admin/mailu/api/v1/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					from flask import Blueprint
 | 
				
			||||||
 | 
					from flask_restx import Api, fields
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					VERSION = 1.0
 | 
				
			||||||
 | 
					api_token = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					blueprint = Blueprint(f'api_v{int(VERSION)}', __name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					authorization = {
 | 
				
			||||||
 | 
					    'Bearer': {
 | 
				
			||||||
 | 
					        'type': 'apiKey',
 | 
				
			||||||
 | 
					        'in': 'header',
 | 
				
			||||||
 | 
					        'name': 'Authorization'
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					api = Api(
 | 
				
			||||||
 | 
					    blueprint, version=f'{VERSION:.1f}',
 | 
				
			||||||
 | 
					    title='Mailu API', default_label='Mailu',
 | 
				
			||||||
 | 
					    validate=True,
 | 
				
			||||||
 | 
					    authorizations=authorization,
 | 
				
			||||||
 | 
					    security='Bearer',
 | 
				
			||||||
 | 
					    doc='/'
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					response_fields = api.model('Response', {
 | 
				
			||||||
 | 
					    'code': fields.Integer,
 | 
				
			||||||
 | 
					    'message': fields.String,
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					error_fields = api.model('Error', {
 | 
				
			||||||
 | 
					    'errors': fields.Nested(api.model('Error_Key', {
 | 
				
			||||||
 | 
					        'key': fields.String,
 | 
				
			||||||
 | 
					        'message':fields.String
 | 
				
			||||||
 | 
					    })),
 | 
				
			||||||
 | 
					    'message': fields.String,
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from . import domains
 | 
				
			||||||
 | 
					from . import alias
 | 
				
			||||||
 | 
					from . import relay
 | 
				
			||||||
 | 
					from . import user
 | 
				
			||||||
							
								
								
									
										127
									
								
								core/admin/mailu/api/v1/alias.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								core/admin/mailu/api/v1/alias.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,127 @@
 | 
				
			|||||||
 | 
					from flask_restx import Resource, fields, marshal
 | 
				
			||||||
 | 
					from . import api, response_fields
 | 
				
			||||||
 | 
					from .. import common
 | 
				
			||||||
 | 
					from ... import models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					db = models.db
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					alias = api.namespace('alias', description='Alias operations')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					alias_fields_update = alias.model('AliasUpdate', {
 | 
				
			||||||
 | 
					    'comment': fields.String(description='a comment'),
 | 
				
			||||||
 | 
					    'destination': fields.List(fields.String(description='alias email address', example='user@example.com')),
 | 
				
			||||||
 | 
					    'wildcard': fields.Boolean(description='enable SQL Like wildcard syntax')
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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)),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@alias.route('')
 | 
				
			||||||
 | 
					class Aliases(Resource):
 | 
				
			||||||
 | 
					    @alias.doc('list_alias')
 | 
				
			||||||
 | 
					    @alias.marshal_with(alias_fields, as_list=True, skip_none=True, mask=None)
 | 
				
			||||||
 | 
					    @alias.doc(security='Bearer')
 | 
				
			||||||
 | 
					    @common.api_token_authorization
 | 
				
			||||||
 | 
					    def get(self):
 | 
				
			||||||
 | 
					        """ List aliases """
 | 
				
			||||||
 | 
					        return models.Alias.query.all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @alias.doc('create_alias')
 | 
				
			||||||
 | 
					    @alias.expect(alias_fields)
 | 
				
			||||||
 | 
					    @alias.response(200, 'Success', response_fields)
 | 
				
			||||||
 | 
					    @alias.response(400, 'Input validation exception', response_fields)
 | 
				
			||||||
 | 
					    @alias.response(409, 'Duplicate alias', response_fields)
 | 
				
			||||||
 | 
					    @alias.doc(security='Bearer')
 | 
				
			||||||
 | 
					    @common.api_token_authorization
 | 
				
			||||||
 | 
					    def post(self):
 | 
				
			||||||
 | 
					        """ Create a new alias """
 | 
				
			||||||
 | 
					        data = api.payload
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        alias_found = models.Alias.query.filter_by(email = data['email']).first()
 | 
				
			||||||
 | 
					        if alias_found:
 | 
				
			||||||
 | 
					          return { 'code': 409, 'message': f'Duplicate alias {data["email"]}'}, 409
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        alias_model = models.Alias(email=data["email"],destination=data['destination'])
 | 
				
			||||||
 | 
					        if 'comment' in data:
 | 
				
			||||||
 | 
					          alias_model.comment = data['comment']
 | 
				
			||||||
 | 
					        if 'wildcard' in data:
 | 
				
			||||||
 | 
					          alias_model.wildcard = data['wildcard']
 | 
				
			||||||
 | 
					        db.session.add(alias_model)
 | 
				
			||||||
 | 
					        db.session.commit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return {'code': 200, 'message': f'Alias {data["email"]} to destination {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(404, 'Alias not found', response_fields)
 | 
				
			||||||
 | 
					    @alias.doc(security='Bearer')
 | 
				
			||||||
 | 
					    @common.api_token_authorization
 | 
				
			||||||
 | 
					    def get(self, alias):
 | 
				
			||||||
 | 
					        """ Find 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
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					          return marshal(alias_found,alias_fields), 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @alias.doc('update_alias')
 | 
				
			||||||
 | 
					    @alias.expect(alias_fields_update)
 | 
				
			||||||
 | 
					    @alias.response(200, 'Success', response_fields)
 | 
				
			||||||
 | 
					    @alias.response(404, 'Alias not found', response_fields)
 | 
				
			||||||
 | 
					    @alias.response(400, 'Input validation exception', response_fields)
 | 
				
			||||||
 | 
					    @alias.doc(security='Bearer')
 | 
				
			||||||
 | 
					    @common.api_token_authorization
 | 
				
			||||||
 | 
					    def put(self, alias):
 | 
				
			||||||
 | 
					      """ Update alias """
 | 
				
			||||||
 | 
					      data = api.payload
 | 
				
			||||||
 | 
					      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
 | 
				
			||||||
 | 
					      if 'comment' in data:
 | 
				
			||||||
 | 
					        alias_found.comment = data['comment']
 | 
				
			||||||
 | 
					      if 'destination' in data:
 | 
				
			||||||
 | 
					        destination_csl = ",".join(data['destination'])
 | 
				
			||||||
 | 
					        alias_found.destination = destination_csl
 | 
				
			||||||
 | 
					      if 'wildcard' in data:
 | 
				
			||||||
 | 
					        alias_found.wildcard = data['wildcard']
 | 
				
			||||||
 | 
					      db.session.add(alias_found)
 | 
				
			||||||
 | 
					      db.session.commit()
 | 
				
			||||||
 | 
					      return {'code': 200, 'message': f'Alias {alias} has been updated'}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @alias.doc('delete_alias')
 | 
				
			||||||
 | 
					    @alias.response(200, 'Success', response_fields)
 | 
				
			||||||
 | 
					    @alias.response(404, 'Alias not found', response_fields)
 | 
				
			||||||
 | 
					    @alias.doc(security='Bearer')
 | 
				
			||||||
 | 
					    @common.api_token_authorization
 | 
				
			||||||
 | 
					    def delete(self, alias):
 | 
				
			||||||
 | 
					      """ Delete 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
 | 
				
			||||||
 | 
					      db.session.delete(alias_found)
 | 
				
			||||||
 | 
					      db.session.commit()
 | 
				
			||||||
 | 
					      return {'code': 200, 'message': f'Alias {alias} has been deleted'}, 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@alias.route('/destination/<string:domain>')
 | 
				
			||||||
 | 
					class AliasWithDest(Resource):
 | 
				
			||||||
 | 
					    @alias.doc('find_alias_filter_domain')
 | 
				
			||||||
 | 
					    @alias.response(200, 'Success', alias_fields)
 | 
				
			||||||
 | 
					    @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 """
 | 
				
			||||||
 | 
					        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
 | 
				
			||||||
 | 
					        aliases_found = domain_found.aliases
 | 
				
			||||||
 | 
					        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
 | 
				
			||||||
							
								
								
									
										410
									
								
								core/admin/mailu/api/v1/domains.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										410
									
								
								core/admin/mailu/api/v1/domains.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,410 @@
 | 
				
			|||||||
 | 
					import validators
 | 
				
			||||||
 | 
					from flask_restx import Resource, fields, marshal
 | 
				
			||||||
 | 
					from . import api, response_fields, user
 | 
				
			||||||
 | 
					from .. import common
 | 
				
			||||||
 | 
					from ... import models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					db = models.db
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					dom = api.namespace('domain', description='Domain operations')
 | 
				
			||||||
 | 
					alt = api.namespace('alternative', description='Alternative operations')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					domain_fields = api.model('Domain', {
 | 
				
			||||||
 | 
					    'name': fields.String(description='FQDN (e.g. example.com)', example='example.com', required=True),
 | 
				
			||||||
 | 
					    'comment': fields.String(description='a comment'),
 | 
				
			||||||
 | 
					    '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')),
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					domain_fields_update = api.model('DomainUpdate', {
 | 
				
			||||||
 | 
					    'comment': fields.String(description='a comment'),
 | 
				
			||||||
 | 
					    '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')),
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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'),
 | 
				
			||||||
 | 
					    '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')),
 | 
				
			||||||
 | 
					    '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'),
 | 
				
			||||||
 | 
					    'dns_dkim': fields.String(Description='DKIM record for domain'),
 | 
				
			||||||
 | 
					    'dns_dmarc': fields.String(Description='DMARC record for domain'),
 | 
				
			||||||
 | 
					    'dns_dmarc_report': fields.String(Description='DMARC report record for domain'),
 | 
				
			||||||
 | 
					    'dns_tlsa': fields.String(Description='TLSA record for domain'),
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					domain_fields_dns = api.model('DomainDNS', {
 | 
				
			||||||
 | 
					    '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'),
 | 
				
			||||||
 | 
					    'dns_dkim': fields.String(Description='DKIM record for domain'),
 | 
				
			||||||
 | 
					    'dns_dmarc': fields.String(Description='DMARC record for domain'),
 | 
				
			||||||
 | 
					    'dns_dmarc_report': fields.String(Description='DMARC report record for domain'),
 | 
				
			||||||
 | 
					    'dns_tlsa': fields.String(Description='TLSA record for domain'),
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					manager_fields = api.model('Manager', {
 | 
				
			||||||
 | 
					    'domain_name': fields.String(description='domain managed by manager'),
 | 
				
			||||||
 | 
					    'user_email': fields.String(description='email address of manager'),
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					manager_fields_create = api.model('ManagerCreate', {
 | 
				
			||||||
 | 
					    'user_email': fields.String(description='email address of manager', required=True),
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					alternative_fields_update = api.model('AlternativeDomainUpdate', {
 | 
				
			||||||
 | 
					    'domain': fields.String(description='domain FQDN', example='example.com', required=False),
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					alternative_fields = api.model('AlternativeDomain', {
 | 
				
			||||||
 | 
					    'name': fields.String(description='alternative FQDN', example='example2.com', required=True),
 | 
				
			||||||
 | 
					    'domain': fields.String(description='domain FQDN', example='example.com', required=True),
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@dom.route('')
 | 
				
			||||||
 | 
					class Domains(Resource):
 | 
				
			||||||
 | 
					    @dom.doc('list_domain')
 | 
				
			||||||
 | 
					    @dom.marshal_with(domain_fields_get, as_list=True, skip_none=True, mask=None)
 | 
				
			||||||
 | 
					    @dom.doc(security='Bearer')
 | 
				
			||||||
 | 
					    @common.api_token_authorization
 | 
				
			||||||
 | 
					    def get(self):
 | 
				
			||||||
 | 
					        """ List domains """
 | 
				
			||||||
 | 
					        return models.Domain.query.all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @dom.doc('create_domain')
 | 
				
			||||||
 | 
					    @dom.expect(domain_fields)
 | 
				
			||||||
 | 
					    @dom.response(200, 'Success', response_fields)
 | 
				
			||||||
 | 
					    @dom.response(400, 'Input validation exception', response_fields)
 | 
				
			||||||
 | 
					    @dom.response(409, 'Duplicate domain/alternative name', response_fields)
 | 
				
			||||||
 | 
					    @dom.doc(security='Bearer')
 | 
				
			||||||
 | 
					    @common.api_token_authorization
 | 
				
			||||||
 | 
					    def post(self):
 | 
				
			||||||
 | 
					        """ Create a new domain """
 | 
				
			||||||
 | 
					        data = api.payload
 | 
				
			||||||
 | 
					        if not validators.domain(data['name']):
 | 
				
			||||||
 | 
					            return { 'code': 400, 'message': f'Domain {data["name"]} is not a valid domain'}, 400
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if common.fqdn_in_use(data['name']):
 | 
				
			||||||
 | 
					            return { 'code': 409, 'message': f'Duplicate domain name {data["name"]}'}, 409
 | 
				
			||||||
 | 
					        if 'alternatives' in data:
 | 
				
			||||||
 | 
					            #check if duplicate alternatives are supplied
 | 
				
			||||||
 | 
					            if [x for x in data['alternatives'] if data['alternatives'].count(x) >= 2]:
 | 
				
			||||||
 | 
					                return { 'code': 409, 'message': f'Duplicate alternative domain names in request' }, 409
 | 
				
			||||||
 | 
					            for item in data['alternatives']:
 | 
				
			||||||
 | 
					                if common.fqdn_in_use(item):
 | 
				
			||||||
 | 
					                    return { 'code': 409, 'message': f'Duplicate alternative domain name {item}' }, 409
 | 
				
			||||||
 | 
					                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'])
 | 
				
			||||||
 | 
					                models.db.session.add(alternative)
 | 
				
			||||||
 | 
					        domain_new = models.Domain(name=data['name'])
 | 
				
			||||||
 | 
					        if 'comment' in data:
 | 
				
			||||||
 | 
					            domain_new.comment = data['comment']
 | 
				
			||||||
 | 
					        if 'max_users' in data:
 | 
				
			||||||
 | 
					            domain_new.comment = data['max_users']
 | 
				
			||||||
 | 
					        if 'max_aliases' in data:
 | 
				
			||||||
 | 
					            domain_new.comment = data['max_aliases']
 | 
				
			||||||
 | 
					        if 'max_quota_bytes' in data:
 | 
				
			||||||
 | 
					            domain_new.comment = data['max_quota_bytes']
 | 
				
			||||||
 | 
					        if 'signup_enabled' in data:
 | 
				
			||||||
 | 
					            domain_new.comment = data['signup_enabled']
 | 
				
			||||||
 | 
					        models.db.session.add(domain_new)
 | 
				
			||||||
 | 
					        #apply the changes
 | 
				
			||||||
 | 
					        db.session.commit()
 | 
				
			||||||
 | 
					        return  {'code': 200, 'message': f'Domain {data["name"]} has been created'}, 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@dom.route('/<domain>')
 | 
				
			||||||
 | 
					class Domain(Resource):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @dom.doc('find_domain')
 | 
				
			||||||
 | 
					    @dom.response(200, 'Success', domain_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 """
 | 
				
			||||||
 | 
					        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_found:
 | 
				
			||||||
 | 
					            return { 'code': 404, 'message': f'Domain {domain} does not exist'}, 404
 | 
				
			||||||
 | 
					        return marshal(domain_found, domain_fields_get), 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @dom.doc('update_domain')
 | 
				
			||||||
 | 
					    @dom.expect(domain_fields_update)
 | 
				
			||||||
 | 
					    @dom.response(200, 'Success', response_fields)
 | 
				
			||||||
 | 
					    @dom.response(400, 'Input validation exception', response_fields)
 | 
				
			||||||
 | 
					    @dom.response(404, 'Domain not found', response_fields)
 | 
				
			||||||
 | 
					    @dom.response(409, 'Duplicate domain/alternative name', response_fields)
 | 
				
			||||||
 | 
					    @dom.doc(security='Bearer')
 | 
				
			||||||
 | 
					    @common.api_token_authorization
 | 
				
			||||||
 | 
					    def put(self, domain):
 | 
				
			||||||
 | 
					        """ Update an existing 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)
 | 
				
			||||||
 | 
					        if not domain:
 | 
				
			||||||
 | 
					            return { 'code': 404, 'message': f'Domain {data["name"]} does not exist'}, 404
 | 
				
			||||||
 | 
					        data = api.payload
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if 'alternatives' in data:
 | 
				
			||||||
 | 
					            #check if duplicate alternatives are supplied
 | 
				
			||||||
 | 
					            if [x for x in data['alternatives'] if data['alternatives'].count(x) >= 2]:
 | 
				
			||||||
 | 
					                return { 'code': 409, 'message': f'Duplicate alternative domain names in request' }, 409
 | 
				
			||||||
 | 
					            for item in data['alternatives']:
 | 
				
			||||||
 | 
					                if common.fqdn_in_use(item):
 | 
				
			||||||
 | 
					                    return { 'code': 409, 'message': f'Duplicate alternative domain name {item}' }, 409
 | 
				
			||||||
 | 
					                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'])
 | 
				
			||||||
 | 
					                models.db.session.add(alternative)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if 'comment' in data:
 | 
				
			||||||
 | 
					            domain_found.comment = data['comment']
 | 
				
			||||||
 | 
					        if 'max_users' in data:
 | 
				
			||||||
 | 
					            domain_found.comment = data['max_users']
 | 
				
			||||||
 | 
					        if 'max_aliases' in data:
 | 
				
			||||||
 | 
					            domain_found.comment = data['max_aliases']
 | 
				
			||||||
 | 
					        if 'max_quota_bytes' in data:
 | 
				
			||||||
 | 
					            domain_found.comment = data['max_quota_bytes']
 | 
				
			||||||
 | 
					        if 'signup_enabled' in data:
 | 
				
			||||||
 | 
					            domain_found.comment = data['signup_enabled']
 | 
				
			||||||
 | 
					        models.db.session.add(domain_found)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        #apply the changes
 | 
				
			||||||
 | 
					        db.session.commit()
 | 
				
			||||||
 | 
					        return  {'code': 200, 'message': f'Domain {domain} has been updated'}, 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @dom.doc('delete_domain')
 | 
				
			||||||
 | 
					    @dom.response(200, 'Success', response_fields)
 | 
				
			||||||
 | 
					    @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 delete(self, domain):
 | 
				
			||||||
 | 
					        """ Delete 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)
 | 
				
			||||||
 | 
					        if not domain:
 | 
				
			||||||
 | 
					            return { 'code': 404, 'message': f'Domain {domain} does not exist'}, 404
 | 
				
			||||||
 | 
					        db.session.delete(domain_found)
 | 
				
			||||||
 | 
					        db.session.commit()
 | 
				
			||||||
 | 
					        return {'code': 200, 'message': f'Domain {domain} has been deleted'}, 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@dom.route('/<domain>/dkim')
 | 
				
			||||||
 | 
					class Domain(Resource):
 | 
				
			||||||
 | 
					    @dom.doc('generate_dkim')
 | 
				
			||||||
 | 
					    @dom.response(200, 'Success', response_fields)
 | 
				
			||||||
 | 
					    @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 post(self, domain):
 | 
				
			||||||
 | 
					        """ Generate new DKIM/DMARC keys for 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)
 | 
				
			||||||
 | 
					        if not domain_found:
 | 
				
			||||||
 | 
					            return { 'code': 404, 'message': f'Domain {domain} does not exist'}, 404
 | 
				
			||||||
 | 
					        domain_found.generate_dkim_key()
 | 
				
			||||||
 | 
					        domain_found.save_dkim_key()
 | 
				
			||||||
 | 
					        return {'code': 200, 'message': f'DKIM/DMARC keys have been generated for domain {domain}'}, 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@dom.route('/<domain>/manager')
 | 
				
			||||||
 | 
					class Manager(Resource):
 | 
				
			||||||
 | 
					    @dom.doc('list_managers')
 | 
				
			||||||
 | 
					    @dom.marshal_with(manager_fields, 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 """
 | 
				
			||||||
 | 
					        if not validators.domain(domain):
 | 
				
			||||||
 | 
					            return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400
 | 
				
			||||||
 | 
					        if not domain:
 | 
				
			||||||
 | 
					            return { 'code': 404, 'message': f'Domain {domain} does not exist'}, 404
 | 
				
			||||||
 | 
					        domain = models.Domain.query.filter_by(name=domain)
 | 
				
			||||||
 | 
					        return domain.managers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @dom.doc('create_manager')
 | 
				
			||||||
 | 
					    @dom.expect(manager_fields_create)
 | 
				
			||||||
 | 
					    @dom.response(200, 'Success', response_fields)
 | 
				
			||||||
 | 
					    @dom.response(400, 'Input validation exception', response_fields)
 | 
				
			||||||
 | 
					    @dom.response(404, 'User or domain not found', response_fields)
 | 
				
			||||||
 | 
					    @dom.response(409, 'Duplicate domain manager', response_fields)
 | 
				
			||||||
 | 
					    @dom.doc(security='Bearer')
 | 
				
			||||||
 | 
					    @common.api_token_authorization
 | 
				
			||||||
 | 
					    def post(self, domain):
 | 
				
			||||||
 | 
					        """ Create a new domain manager """
 | 
				
			||||||
 | 
					        data = api.payload
 | 
				
			||||||
 | 
					        if not validators.email(data['user_email']):
 | 
				
			||||||
 | 
					            return {'code': 400, 'message': f'Invalid email address {data["user_email"]}'}, 400
 | 
				
			||||||
 | 
					        if not validators.domain(domain):
 | 
				
			||||||
 | 
					            return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400
 | 
				
			||||||
 | 
					        domain = models.Domain.query.get(domain)
 | 
				
			||||||
 | 
					        if not domain:
 | 
				
			||||||
 | 
					            return { 'code': 404, 'message': f'Domain {domain} does not exist'}, 404
 | 
				
			||||||
 | 
					        user = models.User.query.get(data['user_email'])
 | 
				
			||||||
 | 
					        if not user:
 | 
				
			||||||
 | 
					            return { 'code': 404, 'message': f'User {data["user_email"]} does not exist'}, 404
 | 
				
			||||||
 | 
					        if user in domain.managers:
 | 
				
			||||||
 | 
					            return {'code': 409, 'message': f'User {data["user_email"]} is already a manager of the domain {domain} '}, 409
 | 
				
			||||||
 | 
					        domain.managers.append(user)
 | 
				
			||||||
 | 
					        models.db.session.commit()
 | 
				
			||||||
 | 
					        return {'code': 200, 'message': f'User {data["user_email"]} has been added as manager of the domain {domain} '},200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@dom.route('/<domain>/manager/<email>')
 | 
				
			||||||
 | 
					class Domain(Resource):
 | 
				
			||||||
 | 
					    @dom.doc('find_manager')
 | 
				
			||||||
 | 
					    @dom.response(200, 'Success', manager_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 """
 | 
				
			||||||
 | 
					        if not validators.email(email):
 | 
				
			||||||
 | 
					            return {'code': 400, 'message': f'Invalid email address {email}'}, 400
 | 
				
			||||||
 | 
					        if not validators.domain(domain):
 | 
				
			||||||
 | 
					            return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400
 | 
				
			||||||
 | 
					        domain = models.Domain.query.get(domain)
 | 
				
			||||||
 | 
					        if not domain:
 | 
				
			||||||
 | 
					            return { 'code': 404, 'message': f'Domain {domain} does not exist'}, 404
 | 
				
			||||||
 | 
					        user = models.User.query.get(email)
 | 
				
			||||||
 | 
					        if not user:
 | 
				
			||||||
 | 
					            return { 'code': 404, 'message': f'User {email} does not exist'}, 404
 | 
				
			||||||
 | 
					        if user in domain.managers:
 | 
				
			||||||
 | 
					            for manager in domain.managers:
 | 
				
			||||||
 | 
					                if manager.email == email:
 | 
				
			||||||
 | 
					                    return marshal(manager, manager_fields),200
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return { 'code': 404, 'message': f'User {email} is not a manager of the domain {domain}'}, 404
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @dom.doc('delete_manager')
 | 
				
			||||||
 | 
					    @dom.response(200, 'Success', response_fields)
 | 
				
			||||||
 | 
					    @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 delete(self, domain, email):
 | 
				
			||||||
 | 
					        if not validators.email(email):
 | 
				
			||||||
 | 
					            return {'code': 400, 'message': f'Invalid email address {email}'}, 400
 | 
				
			||||||
 | 
					        if not validators.domain(domain):
 | 
				
			||||||
 | 
					            return { 'code': 400, 'message': f'Domain {domain} is not a valid domain'}, 400
 | 
				
			||||||
 | 
					        domain = models.Domain.query.get(domain)
 | 
				
			||||||
 | 
					        if not domain:
 | 
				
			||||||
 | 
					            return { 'code': 404, 'message': f'Domain {domain} does not exist'}, 404
 | 
				
			||||||
 | 
					        user = models.User.query.get(email)
 | 
				
			||||||
 | 
					        if not user:
 | 
				
			||||||
 | 
					            return { 'code': 404, 'message': f'User {email} does not exist'}, 404
 | 
				
			||||||
 | 
					        if user in domain.managers:
 | 
				
			||||||
 | 
					            domain.managers.remove(user)
 | 
				
			||||||
 | 
					            models.db.session.commit()
 | 
				
			||||||
 | 
					            return {'code': 200, 'message': f'User {email} has been removed as a manager of the domain {domain} '},200
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return { 'code': 404, 'message': f'User {email} is not a manager of the domain {domain}'}, 404
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@dom.route('/<domain>/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.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 """
 | 
				
			||||||
 | 
					        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_found:
 | 
				
			||||||
 | 
					            return { 'code': 404, 'message': f'Domain {domain} does not exist'}, 404
 | 
				
			||||||
 | 
					        return  models.User.query.filter_by(domain=domain_found).all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@alt.route('')
 | 
				
			||||||
 | 
					class Alternatives(Resource):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @alt.doc('list_alternative')
 | 
				
			||||||
 | 
					    @alt.marshal_with(alternative_fields, as_list=True, skip_none=True, mask=None)
 | 
				
			||||||
 | 
					    @alt.doc(security='Bearer')
 | 
				
			||||||
 | 
					    @common.api_token_authorization
 | 
				
			||||||
 | 
					    def get(self):
 | 
				
			||||||
 | 
					      """ List alternatives """
 | 
				
			||||||
 | 
					      return models.Alternative.query.all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @alt.doc('create_alternative')
 | 
				
			||||||
 | 
					    @alt.expect(alternative_fields)
 | 
				
			||||||
 | 
					    @alt.response(200, 'Success', response_fields)
 | 
				
			||||||
 | 
					    @alt.response(400, 'Input validation exception', response_fields)
 | 
				
			||||||
 | 
					    @alt.response(404, 'Domain not found or missing', response_fields)
 | 
				
			||||||
 | 
					    @alt.response(409, 'Duplicate alternative domain name', response_fields)
 | 
				
			||||||
 | 
					    @alt.doc(security='Bearer')
 | 
				
			||||||
 | 
					    @common.api_token_authorization
 | 
				
			||||||
 | 
					    def post(self):
 | 
				
			||||||
 | 
					        """ Create 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
 | 
				
			||||||
 | 
					        if not validators.domain(data['domain']):
 | 
				
			||||||
 | 
					            return { 'code': 400, 'message': f'Domain {data["domain"]} is not a valid domain'}, 400
 | 
				
			||||||
 | 
					        domain = models.Domain.query.get(data['domain'])
 | 
				
			||||||
 | 
					        if not domain:
 | 
				
			||||||
 | 
					            return { 'code': 404, 'message': f'Domain {data["domain"]} does not exist'}, 404
 | 
				
			||||||
 | 
					        if common.fqdn_in_use(data['name']):
 | 
				
			||||||
 | 
					            return { 'code': 409, 'message': f'Duplicate alternative domain name {data["name"]}'}, 409
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        alternative = models.Alternative(name=data['name'], domain_name=data['domain'])
 | 
				
			||||||
 | 
					        models.db.session.add(alternative)
 | 
				
			||||||
 | 
					        db.session.commit()
 | 
				
			||||||
 | 
					        return {'code': 200, 'message': f'Alternative {data["name"]} for domain {data["domain"]} has been created'}, 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@alt.route('/<string:alt>')
 | 
				
			||||||
 | 
					class Alternative(Resource):
 | 
				
			||||||
 | 
					    @alt.doc('find_alternative')
 | 
				
			||||||
 | 
					    @alt.doc(security='Bearer')
 | 
				
			||||||
 | 
					    @common.api_token_authorization
 | 
				
			||||||
 | 
					    def get(self, alt):
 | 
				
			||||||
 | 
					        """ Find 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()
 | 
				
			||||||
 | 
					        if not alternative:
 | 
				
			||||||
 | 
					            return{ 'code': 404, 'message': f'Alternative domain {alt} does not exist'}, 404
 | 
				
			||||||
 | 
					        return marshal(alternative, alternative_fields), 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @alt.doc('delete_alternative')
 | 
				
			||||||
 | 
					    @alt.response(200, 'Success', response_fields)
 | 
				
			||||||
 | 
					    @alt.response(400, 'Input validation exception', response_fields)
 | 
				
			||||||
 | 
					    @alt.response(404, 'Alternative/Domain not found or missing', response_fields)
 | 
				
			||||||
 | 
					    @alt.response(409, 'Duplicate domain name', response_fields)
 | 
				
			||||||
 | 
					    @alt.doc(security='Bearer')
 | 
				
			||||||
 | 
					    @common.api_token_authorization
 | 
				
			||||||
 | 
					    def delete(self, alt):
 | 
				
			||||||
 | 
					        """ Delete 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()
 | 
				
			||||||
 | 
					        if not alternative:
 | 
				
			||||||
 | 
					            return { 'code': 404, 'message': f'Alternative domain {alt} does not exist'}, 404
 | 
				
			||||||
 | 
					        domain = alternative.domain_name
 | 
				
			||||||
 | 
					        db.session.delete(alternative)
 | 
				
			||||||
 | 
					        db.session.commit()
 | 
				
			||||||
 | 
					        return {'code': 200, 'message': f'Alternative {alt} for domain {domain} has been deleted'}, 200
 | 
				
			||||||
							
								
								
									
										118
									
								
								core/admin/mailu/api/v1/relay.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								core/admin/mailu/api/v1/relay.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,118 @@
 | 
				
			|||||||
 | 
					from flask_restx import Resource, fields, marshal
 | 
				
			||||||
 | 
					import validators
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from . import api, response_fields
 | 
				
			||||||
 | 
					from .. import common
 | 
				
			||||||
 | 
					from ... import models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					db = models.db
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					relay = api.namespace('relay', description='Relay operations')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					relay_fields = api.model('Relay', {
 | 
				
			||||||
 | 
					    'name': fields.String(description='relayed domain name', example='example.com', required=True),
 | 
				
			||||||
 | 
					    'smtp': fields.String(description='remote host', example='example.com', required=False),
 | 
				
			||||||
 | 
					    'comment': fields.String(description='a comment', required=False)
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					relay_fields_update = api.model('RelayUpdate', {
 | 
				
			||||||
 | 
					    'smtp': fields.String(description='remote host', example='example.com', required=False),
 | 
				
			||||||
 | 
					    'comment': fields.String(description='a comment', required=False)
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@relay.route('')
 | 
				
			||||||
 | 
					class Relays(Resource):
 | 
				
			||||||
 | 
					    @relay.doc('list_relays')
 | 
				
			||||||
 | 
					    @relay.marshal_with(relay_fields, as_list=True, skip_none=True, mask=None)
 | 
				
			||||||
 | 
					    @relay.doc(security='Bearer')
 | 
				
			||||||
 | 
					    @common.api_token_authorization
 | 
				
			||||||
 | 
					    def get(self):
 | 
				
			||||||
 | 
					        "List 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(409, 'Duplicate relay', response_fields)
 | 
				
			||||||
 | 
					    @relay.doc(security='Bearer')
 | 
				
			||||||
 | 
					    @common.api_token_authorization
 | 
				
			||||||
 | 
					    def post(self):
 | 
				
			||||||
 | 
					        """ Create relay """
 | 
				
			||||||
 | 
					        data = api.payload
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not validators.domain(name):
 | 
				
			||||||
 | 
					            return { 'code': 400, 'message': f'Relayed domain {name} is not a valid domain'}, 400
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if common.fqdn_in_use(data['name']):
 | 
				
			||||||
 | 
					            return { 'code': 409, 'message': f'Duplicate domain {data["name"]}'}, 409
 | 
				
			||||||
 | 
					        relay_model = models.Relay(name=data['name'])
 | 
				
			||||||
 | 
					        if 'smtp' in data:
 | 
				
			||||||
 | 
					            relay_model.smtp = data['smtp']
 | 
				
			||||||
 | 
					        if 'comment' in data:
 | 
				
			||||||
 | 
					            relay_model.comment = data['comment']
 | 
				
			||||||
 | 
					        db.session.add(relay_model)
 | 
				
			||||||
 | 
					        db.session.commit()
 | 
				
			||||||
 | 
					        return {'code': 200, 'message': f'Relayed domain {data["name"]} has been created'}, 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@relay.route('/<string:name>')
 | 
				
			||||||
 | 
					class Relay(Resource):
 | 
				
			||||||
 | 
					    @relay.doc('find_relay')
 | 
				
			||||||
 | 
					    @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 """
 | 
				
			||||||
 | 
					        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()
 | 
				
			||||||
 | 
					        if relay_found is None:
 | 
				
			||||||
 | 
					            return { 'code': 404, 'message': f'Relayed domain {name} cannot be found'}, 404
 | 
				
			||||||
 | 
					        return  marshal(relay_found, relay_fields), 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @relay.doc('update_relay')
 | 
				
			||||||
 | 
					    @relay.expect(relay_fields_update)
 | 
				
			||||||
 | 
					    @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 put(self, name):
 | 
				
			||||||
 | 
					        """ Update relay """
 | 
				
			||||||
 | 
					        data = api.payload
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        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()
 | 
				
			||||||
 | 
					        if relay_found is None:
 | 
				
			||||||
 | 
					            return { 'code': 404, 'message': f'Relayed domain {name} cannot be found'}, 404
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if 'smtp' in data:
 | 
				
			||||||
 | 
					            relay_found.smtp = data['smtp']
 | 
				
			||||||
 | 
					        if 'comment' in data:
 | 
				
			||||||
 | 
					            relay_found.comment = data['comment']
 | 
				
			||||||
 | 
					        db.session.add(relay_found)
 | 
				
			||||||
 | 
					        db.session.commit()
 | 
				
			||||||
 | 
					        return { 'code': 200, 'message': f'Relayed domain {name} has been updated'}, 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @relay.doc('delete_relay')
 | 
				
			||||||
 | 
					    @relay.response(200, 'Success', response_fields)
 | 
				
			||||||
 | 
					    @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 delete(self, name):
 | 
				
			||||||
 | 
					        """ Delete 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()
 | 
				
			||||||
 | 
					        if relay_found is None:
 | 
				
			||||||
 | 
					            return { 'code': 404, 'message': f'Relayed domain {name} cannot be found'}, 404
 | 
				
			||||||
 | 
					        db.session.delete(relay_found)
 | 
				
			||||||
 | 
					        db.session.commit()
 | 
				
			||||||
 | 
					        return { 'code': 200, 'message': f'Relayed domain {name} has been deleted'}, 200
 | 
				
			||||||
							
								
								
									
										262
									
								
								core/admin/mailu/api/v1/user.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										262
									
								
								core/admin/mailu/api/v1/user.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,262 @@
 | 
				
			|||||||
 | 
					from flask_restx import Resource, fields, marshal
 | 
				
			||||||
 | 
					import validators, datetime
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from . import api, response_fields
 | 
				
			||||||
 | 
					from .. import common
 | 
				
			||||||
 | 
					from ... import models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					db = models.db
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					user = api.namespace('user', description='User operations')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					user_fields_get = api.model('UserGet', {
 | 
				
			||||||
 | 
					    'email': fields.String(description='The email address of the user', example='John.Doe@example.com', attribute='_email'),
 | 
				
			||||||
 | 
					    'password': fields.String(description="Hash of the user's password; Example='$bcrypt-sha256$v=2,t=2b,r=12$fmsAdJbYAD1gGQIE5nfJq.$zLkQUEs2XZfTpAEpcix/1k5UTNPm0jO'"),
 | 
				
			||||||
 | 
					    'comment': fields.String(description='A description for the user. This description is shown on the Users page', example='my comment'),
 | 
				
			||||||
 | 
					    'quota_bytes': fields.Integer(description='The maximum quota for the user’s email box in bytes', example='1000000000'),
 | 
				
			||||||
 | 
					    'global_admin': fields.Boolean(description='Make the user a global administrator'),
 | 
				
			||||||
 | 
					    'enabled': fields.Boolean(description='Enable the user. When an user is disabled, the user is unable to login to the Admin GUI or webmail or access his email via IMAP/POP3 or send mail'),
 | 
				
			||||||
 | 
					    'enable_imap': fields.Boolean(description='Allow email retrieval via IMAP'),
 | 
				
			||||||
 | 
					    '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_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'),
 | 
				
			||||||
 | 
					    'reply_body': fields.String(description='The body of the automatic reply email', example='Hello, I am out of office. I will respond when I am back.'),
 | 
				
			||||||
 | 
					    'reply_startdate': fields.Date(description='Start date for automatic replies in YYYY-MM-DD format.', example='2022-02-10'),
 | 
				
			||||||
 | 
					    'reply_enddate': fields.Date(description='End date for automatic replies in YYYY-MM-DD format.', example='2022-02-22'),
 | 
				
			||||||
 | 
					    'displayed_name': fields.String(description='The display name of the user within the Admin GUI', example='John Doe'),
 | 
				
			||||||
 | 
					    'spam_enabled': fields.Boolean(description='Enable the spam filter'),
 | 
				
			||||||
 | 
					    'spam_mark_as_read': fields.Boolean(description='Enable marking spam mails as read'),
 | 
				
			||||||
 | 
					    'spam_threshold': fields.Integer(description='The user defined spam filter tolerance', example='80'),
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					user_fields_post = api.model('UserCreate', {
 | 
				
			||||||
 | 
					    'email': fields.String(description='The email address of the user', example='John.Doe@example.com', attribute='_email', required=True),
 | 
				
			||||||
 | 
					    'raw_password': fields.String(description='The raw (plain text) password of the user. Mailu will hash the password using BCRYPT-SHA256', example='secret', required=True),
 | 
				
			||||||
 | 
					    'comment': fields.String(description='A description for the user. This description is shown on the Users page', example='my comment'),
 | 
				
			||||||
 | 
					    'quota_bytes': fields.Integer(description='The maximum quota for the user’s email box in bytes', example='1000000000'),
 | 
				
			||||||
 | 
					    'global_admin': fields.Boolean(description='Make the user a global administrator'),
 | 
				
			||||||
 | 
					    'enabled': fields.Boolean(description='Enable the user. When an user is disabled, the user is unable to login to the Admin GUI or webmail or access his email via IMAP/POP3 or send mail'),
 | 
				
			||||||
 | 
					    'enable_imap': fields.Boolean(description='Allow email retrieval via IMAP'),
 | 
				
			||||||
 | 
					    '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_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'),
 | 
				
			||||||
 | 
					    'reply_body': fields.String(description='The body of the automatic reply email', example='Hello, I am out of office. I will respond when I am back.'),
 | 
				
			||||||
 | 
					    'reply_startdate': fields.Date(description='Start date for automatic replies in YYYY-MM-DD format.', example='2022-02-10'),
 | 
				
			||||||
 | 
					    'reply_enddate': fields.Date(description='End date for automatic replies in YYYY-MM-DD format.', example='2022-02-22'),
 | 
				
			||||||
 | 
					    'displayed_name': fields.String(description='The display name of the user within the Admin GUI', example='John Doe'),
 | 
				
			||||||
 | 
					    'spam_enabled': fields.Boolean(description='Enable the spam filter'),
 | 
				
			||||||
 | 
					    'spam_mark_as_read': fields.Boolean(description='Enable marking spam mails as read'),
 | 
				
			||||||
 | 
					    'spam_threshold': fields.Integer(description='The user defined spam filter tolerance', example='80'),
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					user_fields_put = api.model('UserUpdate', {
 | 
				
			||||||
 | 
					    'raw_password': fields.String(description='The raw (plain text) password of the user. Mailu will hash the password using BCRYPT-SHA256', example='secret'),
 | 
				
			||||||
 | 
					    'comment': fields.String(description='A description for the user. This description is shown on the Users page', example='my comment'),
 | 
				
			||||||
 | 
					    'quota_bytes': fields.Integer(description='The maximum quota for the user’s email box in bytes', example='1000000000'),
 | 
				
			||||||
 | 
					    'global_admin': fields.Boolean(description='Make the user a global administrator'),
 | 
				
			||||||
 | 
					    'enabled': fields.Boolean(description='Enable the user. When an user is disabled, the user is unable to login to the Admin GUI or webmail or access his email via IMAP/POP3 or send mail'),
 | 
				
			||||||
 | 
					    'enable_imap': fields.Boolean(description='Allow email retrieval via IMAP'),
 | 
				
			||||||
 | 
					    '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_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'),
 | 
				
			||||||
 | 
					    'reply_body': fields.String(description='The body of the automatic reply email', example='Hello, I am out of office. I will respond when I am back.'),
 | 
				
			||||||
 | 
					    'reply_startdate': fields.Date(description='Start date for automatic replies in YYYY-MM-DD format.', example='2022-02-10'),
 | 
				
			||||||
 | 
					    'reply_enddate': fields.Date(description='End date for automatic replies in YYYY-MM-DD format.', example='2022-02-22'),
 | 
				
			||||||
 | 
					    'displayed_name': fields.String(description='The display name of the user within the Admin GUI', example='John Doe'),
 | 
				
			||||||
 | 
					    'spam_enabled': fields.Boolean(description='Enable the spam filter'),
 | 
				
			||||||
 | 
					    'spam_mark_as_read': fields.Boolean(description='Enable marking spam mails as read'),
 | 
				
			||||||
 | 
					    'spam_threshold': fields.Integer(description='The user defined spam filter tolerance', example='80'),
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@user.route('')
 | 
				
			||||||
 | 
					class Users(Resource):
 | 
				
			||||||
 | 
					    @user.doc('list_users')
 | 
				
			||||||
 | 
					    @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"
 | 
				
			||||||
 | 
					        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(409, 'Duplicate user', response_fields)
 | 
				
			||||||
 | 
					    @user.doc(security='Bearer')
 | 
				
			||||||
 | 
					    @common.api_token_authorization
 | 
				
			||||||
 | 
					    def post(self):
 | 
				
			||||||
 | 
					        """ Create 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
 | 
				
			||||||
 | 
					        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'}, 404
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        user_new = models.User(email=data['email'])
 | 
				
			||||||
 | 
					        if 'raw_password' in data:
 | 
				
			||||||
 | 
					            user_new.set_password(data['raw_password'])
 | 
				
			||||||
 | 
					        if 'comment' in data:
 | 
				
			||||||
 | 
					            user_new.comment = data['comment']
 | 
				
			||||||
 | 
					        if 'quota_bytes' in data:
 | 
				
			||||||
 | 
					            user_new.quota_bytes = data['quota_bytes']
 | 
				
			||||||
 | 
					        if 'global_admin' in data:
 | 
				
			||||||
 | 
					            user_new.global_admin = data['global_admin']
 | 
				
			||||||
 | 
					        if 'enabled' in data:
 | 
				
			||||||
 | 
					            user_new.enabled = data['enabled']
 | 
				
			||||||
 | 
					        if 'enable_imap' in data:
 | 
				
			||||||
 | 
					            user_new.enable_imap = data['enable_imap']
 | 
				
			||||||
 | 
					        if 'enable_pop' in data:
 | 
				
			||||||
 | 
					            user_new.enable_pop = data['enable_pop']
 | 
				
			||||||
 | 
					        if 'allow_spoofing' in data:
 | 
				
			||||||
 | 
					            user_new.allow_spoofing = data['allow_spoofing']
 | 
				
			||||||
 | 
					        if 'forward_enabled' in data:
 | 
				
			||||||
 | 
					            user_new.forward_enabled = data['forward_enabled']
 | 
				
			||||||
 | 
					        if 'forward_destination' in data:
 | 
				
			||||||
 | 
					            user_new.forward_destination = data['forward_destination']
 | 
				
			||||||
 | 
					        if 'forward_keep' in data:
 | 
				
			||||||
 | 
					            user_new.forward_keep = data['forward_keep']
 | 
				
			||||||
 | 
					        if 'reply_enabled' in data:
 | 
				
			||||||
 | 
					            user_new.reply_enabled = data['reply_enabled']
 | 
				
			||||||
 | 
					        if 'reply_subject' in data:
 | 
				
			||||||
 | 
					            user_new.reply_subject = data['reply_subject']
 | 
				
			||||||
 | 
					        if 'reply_body' in data:
 | 
				
			||||||
 | 
					            user_new.reply_body = data['reply_body']
 | 
				
			||||||
 | 
					        if 'reply_startdate' in data:
 | 
				
			||||||
 | 
					            year, month, day = data['reply_startdate'].split('-')
 | 
				
			||||||
 | 
					            date = datetime.datetime(int(year), int(month), int(day))
 | 
				
			||||||
 | 
					            user_new.reply_startdate = date
 | 
				
			||||||
 | 
					        if 'reply_enddate' in data:
 | 
				
			||||||
 | 
					            year, month, day = data['reply_enddate'].split('-')
 | 
				
			||||||
 | 
					            date = datetime.datetime(int(year), int(month), int(day))
 | 
				
			||||||
 | 
					            user_new.reply_enddate = date
 | 
				
			||||||
 | 
					        if 'displayed_name' in data:
 | 
				
			||||||
 | 
					            user_new.displayed_name = data['displayed_name']
 | 
				
			||||||
 | 
					        if 'spam_enabled' in data:
 | 
				
			||||||
 | 
					            user_new.spam_enabled = data['spam_enabled']
 | 
				
			||||||
 | 
					        if 'spam_mark_as_read' in data:
 | 
				
			||||||
 | 
					            user_new.spam_mark_as_read = data['spam_mark_as_read']
 | 
				
			||||||
 | 
					        if 'spam_threshold' in data:
 | 
				
			||||||
 | 
					            user_new.spam_threshold = data['spam_threshold']
 | 
				
			||||||
 | 
					        db.session.add(user_new)
 | 
				
			||||||
 | 
					        db.session.commit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return {'code': 200,'message': f'User {data["email"]} has been created'}, 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@user.route('/<string:email>')
 | 
				
			||||||
 | 
					class User(Resource):
 | 
				
			||||||
 | 
					    @user.doc('find_user')
 | 
				
			||||||
 | 
					    @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 """
 | 
				
			||||||
 | 
					        if not validators.email(email):
 | 
				
			||||||
 | 
					            return { 'code': 400, 'message': f'Provided email address {email} is not a valid email address'}, 400
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        email_found = models.User.query.filter_by(email=email).first()
 | 
				
			||||||
 | 
					        if email_found is None:
 | 
				
			||||||
 | 
					            return { 'code': 404, 'message': f'User {email} cannot be found'}, 404
 | 
				
			||||||
 | 
					        return  marshal(email_found, user_fields_get), 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @user.doc('update_user')
 | 
				
			||||||
 | 
					    @user.expect(user_fields_put)
 | 
				
			||||||
 | 
					    @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 put(self, email):
 | 
				
			||||||
 | 
					        """ Update 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
 | 
				
			||||||
 | 
					        user_found = models.User.query.get(email)
 | 
				
			||||||
 | 
					        if not user_found:
 | 
				
			||||||
 | 
					            return {'code': 404, 'message': f'User {email} cannot be found'}, 404
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if 'raw_password' in data:
 | 
				
			||||||
 | 
					            user_found.set_password(data['raw_password'])
 | 
				
			||||||
 | 
					        if 'comment' in data:
 | 
				
			||||||
 | 
					            user_found.comment = data['comment']
 | 
				
			||||||
 | 
					        if 'quota_bytes' in data:
 | 
				
			||||||
 | 
					            user_found.quota_bytes = data['quota_bytes']
 | 
				
			||||||
 | 
					        if 'global_admin' in data:
 | 
				
			||||||
 | 
					            user_found.global_admin = data['global_admin']
 | 
				
			||||||
 | 
					        if 'enabled' in data:
 | 
				
			||||||
 | 
					            user_found.enabled = data['enabled']
 | 
				
			||||||
 | 
					        if 'enable_imap' in data:
 | 
				
			||||||
 | 
					            user_found.enable_imap = data['enable_imap']
 | 
				
			||||||
 | 
					        if 'enable_pop' in data:
 | 
				
			||||||
 | 
					            user_found.enable_pop = data['enable_pop']
 | 
				
			||||||
 | 
					        if 'allow_spoofing' in data:
 | 
				
			||||||
 | 
					            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_keep' in data:
 | 
				
			||||||
 | 
					            user_found.forward_keep = data['forward_keep']
 | 
				
			||||||
 | 
					        if 'reply_enabled' in data:
 | 
				
			||||||
 | 
					            user_found.reply_enabled = data['reply_enabled']
 | 
				
			||||||
 | 
					        if 'reply_subject' in data:
 | 
				
			||||||
 | 
					            user_found.reply_subject = data['reply_subject']
 | 
				
			||||||
 | 
					        if 'reply_body' in data:
 | 
				
			||||||
 | 
					            user_found.reply_body = data['reply_body']
 | 
				
			||||||
 | 
					        if 'reply_startdate' in data:
 | 
				
			||||||
 | 
					            year, month, day = data['reply_startdate'].split('-')
 | 
				
			||||||
 | 
					            date = datetime.datetime(int(year), int(month), int(day))
 | 
				
			||||||
 | 
					            user_found.reply_startdate = date
 | 
				
			||||||
 | 
					        if 'reply_enddate' in data:
 | 
				
			||||||
 | 
					            year, month, day = data['reply_enddate'].split('-')
 | 
				
			||||||
 | 
					            date = datetime.datetime(int(year), int(month), int(day))
 | 
				
			||||||
 | 
					            user_found.reply_enddate = date
 | 
				
			||||||
 | 
					        if 'displayed_name' in data:
 | 
				
			||||||
 | 
					            user_found.displayed_name = data['displayed_name']
 | 
				
			||||||
 | 
					        if 'spam_enabled' in data:
 | 
				
			||||||
 | 
					            user_found.spam_enabled = data['spam_enabled']
 | 
				
			||||||
 | 
					        if 'spam_mark_as_read' in data:
 | 
				
			||||||
 | 
					            user_found.spam_mark_as_read = data['spam_mark_as_read']
 | 
				
			||||||
 | 
					        if 'spam_threshold' in data:
 | 
				
			||||||
 | 
					            user_found.spam_threshold = data['spam_threshold']
 | 
				
			||||||
 | 
					        db.session.add(user_found)
 | 
				
			||||||
 | 
					        db.session.commit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return {'code': 200,'message': f'User {email} has been updated'}, 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @user.doc('delete_user')
 | 
				
			||||||
 | 
					    @user.response(200, 'Success', response_fields)
 | 
				
			||||||
 | 
					    @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 delete(self, email):
 | 
				
			||||||
 | 
					        """ Delete user """
 | 
				
			||||||
 | 
					        if not validators.email(email):
 | 
				
			||||||
 | 
					            return { 'code': 400, 'message': f'Provided email address {email} is not a valid email address'}, 400
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        email_found = models.User.query.filter_by(email=email).first()
 | 
				
			||||||
 | 
					        if email_found is None:
 | 
				
			||||||
 | 
					            return { 'code': 404, 'message': f'User {email} cannot be found'}, 404
 | 
				
			||||||
 | 
					        db.session.delete(email_found)
 | 
				
			||||||
 | 
					        db.session.commit()
 | 
				
			||||||
 | 
					        return { 'code': 200, 'message': f'User {email} has been deleted'}, 200
 | 
				
			||||||
@@ -70,6 +70,9 @@ DEFAULT_CONFIG = {
 | 
				
			|||||||
    'LOGO_URL': None,
 | 
					    'LOGO_URL': None,
 | 
				
			||||||
    'LOGO_BACKGROUND': None,
 | 
					    'LOGO_BACKGROUND': None,
 | 
				
			||||||
    # Advanced settings
 | 
					    # Advanced settings
 | 
				
			||||||
 | 
					    'API': False,
 | 
				
			||||||
 | 
					    'WEB_API': '/api',
 | 
				
			||||||
 | 
					    'API_TOKEN': None,
 | 
				
			||||||
    'LOG_LEVEL': 'WARNING',
 | 
					    'LOG_LEVEL': 'WARNING',
 | 
				
			||||||
    'SESSION_KEY_BITS': 128,
 | 
					    'SESSION_KEY_BITS': 128,
 | 
				
			||||||
    'SESSION_TIMEOUT': 3600,
 | 
					    'SESSION_TIMEOUT': 3600,
 | 
				
			||||||
@@ -157,4 +160,3 @@ class ConfigManager:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        # update the app config
 | 
					        # update the app config
 | 
				
			||||||
        app.config.update(self.config)
 | 
					        app.config.update(self.config)
 | 
				
			||||||
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,6 +15,7 @@ Flask-DebugToolbar
 | 
				
			|||||||
Flask-Login
 | 
					Flask-Login
 | 
				
			||||||
flask-marshmallow
 | 
					flask-marshmallow
 | 
				
			||||||
Flask-Migrate
 | 
					Flask-Migrate
 | 
				
			||||||
 | 
					Flask-RESTX
 | 
				
			||||||
Flask-SQLAlchemy<3
 | 
					Flask-SQLAlchemy<3
 | 
				
			||||||
Flask-WTF
 | 
					Flask-WTF
 | 
				
			||||||
gunicorn
 | 
					gunicorn
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -27,6 +27,7 @@ Flask-DebugToolbar==0.13.1
 | 
				
			|||||||
Flask-Login==0.6.2
 | 
					Flask-Login==0.6.2
 | 
				
			||||||
flask-marshmallow==0.14.0
 | 
					flask-marshmallow==0.14.0
 | 
				
			||||||
Flask-Migrate==3.1.0
 | 
					Flask-Migrate==3.1.0
 | 
				
			||||||
 | 
					Flask-RESTX==1.0.3
 | 
				
			||||||
Flask-SQLAlchemy==2.5.1
 | 
					Flask-SQLAlchemy==2.5.1
 | 
				
			||||||
Flask-WTF==1.0.1
 | 
					Flask-WTF==1.0.1
 | 
				
			||||||
frozenlist==1.3.1
 | 
					frozenlist==1.3.1
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -244,6 +244,13 @@ http {
 | 
				
			|||||||
      {% endif %}
 | 
					      {% endif %}
 | 
				
			||||||
      {% endif %}
 | 
					      {% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {% if API %}
 | 
				
			||||||
 | 
					      location ~ {{ WEB_API or '/api' }} {
 | 
				
			||||||
 | 
					        include /etc/nginx/proxy.conf;
 | 
				
			||||||
 | 
					        proxy_pass http://$admin;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      {% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      location /internal {
 | 
					      location /internal {
 | 
				
			||||||
        internal;
 | 
					        internal;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										31
									
								
								docs/api.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								docs/api.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					Mailu RESTful API
 | 
				
			||||||
 | 
					=================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Mailu offers a RESTful API for changing the Mailu configuration.
 | 
				
			||||||
 | 
					Anything that can be configured via the Mailu web administration interface,
 | 
				
			||||||
 | 
					can also be configured via the API.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The Mailu API is disabled by default. It can be enabled and configured via
 | 
				
			||||||
 | 
					the settings:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* ``API``
 | 
				
			||||||
 | 
					* ``WEB_API``
 | 
				
			||||||
 | 
					* ``API_TOKEN``
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					For more information see the section :ref:`Advanced configuration <advanced_settings>`
 | 
				
			||||||
 | 
					in the configuration reference.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Swagger.json
 | 
				
			||||||
 | 
					------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The swagger.json file can be retrieved via: https://myserver/api/v1/swagger.json.
 | 
				
			||||||
 | 
					The swagger.json file can be consumed in programs such as Postman for generating all API calls.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					In-built SwaggerUI
 | 
				
			||||||
 | 
					------------------
 | 
				
			||||||
 | 
					The Mailu API comes with an in-built SwaggerUI. It is a web client that allows
 | 
				
			||||||
 | 
					anyone to visualize and interact with the Mailu API.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					It is accessible via the URL: https://myserver/api/v1/swaggerui
 | 
				
			||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
Mailu command line
 | 
					Mailu command line
 | 
				
			||||||
==================
 | 
					==================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Managing users and aliases can be done from CLI using commands:
 | 
					Managing domains, users and aliases can be done from CLI using the commands:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* alias
 | 
					* alias
 | 
				
			||||||
* alias-delete
 | 
					* alias-delete
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -198,11 +198,19 @@ An example:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Depending on your particular deployment you most probably will want to change the default.
 | 
					Depending on your particular deployment you most probably will want to change the default.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. _advanced_cfg:
 | 
					.. _advanced_settings:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Advanced settings
 | 
					Advanced settings
 | 
				
			||||||
-----------------
 | 
					-----------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The ``API`` (default: False) setting controls if the API endpoint is reachable.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The ``WEB_API`` (default: /api) setting configures the endpoint that the API
 | 
				
			||||||
 | 
					listens on publicly&interally. The path must always start with a leading slash.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The ``API_TOKEN`` (default: None) enables the API endpoint. This token must be
 | 
				
			||||||
 | 
					passed as request header to the API as authentication token.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The ``CREDENTIAL_ROUNDS`` (default: 12) setting is the number of rounds used by the
 | 
					The ``CREDENTIAL_ROUNDS`` (default: 12) setting is the number of rounds used by the
 | 
				
			||||||
password hashing scheme. The number of rounds can be reduced in case faster
 | 
					password hashing scheme. The number of rounds can be reduced in case faster
 | 
				
			||||||
authentication is needed or increased when additional protection is desired.
 | 
					authentication is needed or increased when additional protection is desired.
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										33
									
								
								docs/faq.rst
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								docs/faq.rst
									
									
									
									
									
								
							@@ -591,7 +591,7 @@ follow these steps:
 | 
				
			|||||||
  maxretry = 10
 | 
					  maxretry = 10
 | 
				
			||||||
  action = docker-action
 | 
					  action = docker-action
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The above will block flagged IPs for a week, you can of course change it to you needs.
 | 
					The above will block flagged IPs for a week, you can of course change it to your needs.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
4. In the mailu docker-compose set the logging driver of the Admin container to journald; and set the tag to mailu-admin
 | 
					4. In the mailu docker-compose set the logging driver of the Admin container to journald; and set the tag to mailu-admin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -625,9 +625,34 @@ The above will block flagged IPs for a week, you can of course change it to you
 | 
				
			|||||||
  maxretry = 10
 | 
					  maxretry = 10
 | 
				
			||||||
  action = docker-action
 | 
					  action = docker-action
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The above will block flagged IPs for a week, you can of course change it to you needs.
 | 
					The above will block flagged IPs for a week, you can of course change it to your needs.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
7. Add the /etc/fail2ban/action.d/docker-action.conf
 | 
					7. Add the /etc/fail2ban/filter.d/bad-auth-api.conf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. code-block:: bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Fail2Ban configuration file
 | 
				
			||||||
 | 
					  [Definition]
 | 
				
			||||||
 | 
					  failregex = .* Invalid API token provided by <HOST>.
 | 
				
			||||||
 | 
					  ignoreregex =
 | 
				
			||||||
 | 
					  journalmatch = CONTAINER_TAG=mailu-admin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					8. Add the /etc/fail2ban/jail.d/bad-auth-api.conf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. code-block:: bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  [bad-auth-api]
 | 
				
			||||||
 | 
					  enabled = true
 | 
				
			||||||
 | 
					  backend = systemd
 | 
				
			||||||
 | 
					  filter = bad-auth-api
 | 
				
			||||||
 | 
					  bantime = 604800
 | 
				
			||||||
 | 
					  findtime = 300
 | 
				
			||||||
 | 
					  maxretry = 10
 | 
				
			||||||
 | 
					  action = docker-action
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The above will block flagged IPs for a week, you can of course change it to your needs.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					9.  Add the /etc/fail2ban/action.d/docker-action.conf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Option 1: Use plain iptables
 | 
					Option 1: Use plain iptables
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -678,7 +703,7 @@ Using iptables with ipset might reduce the system load in such attacks significa
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Using DOCKER-USER chain ensures that the blocked IPs are processed in the correct order with Docker. See more in: https://docs.docker.com/network/iptables/
 | 
					Using DOCKER-USER chain ensures that the blocked IPs are processed in the correct order with Docker. See more in: https://docs.docker.com/network/iptables/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
1. Configure and restart the Fail2Ban service
 | 
					10. Configure and restart the Fail2Ban service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Make sure Fail2Ban is started after the Docker service by adding a partial override which appends this to the existing configuration.
 | 
					Make sure Fail2Ban is started after the Docker service by adding a partial override which appends this to the existing configuration.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -70,6 +70,7 @@ the version of Mailu that you are running.
 | 
				
			|||||||
    webadministration
 | 
					    webadministration
 | 
				
			||||||
    antispam
 | 
					    antispam
 | 
				
			||||||
    cli
 | 
					    cli
 | 
				
			||||||
 | 
					    api
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. toctree::
 | 
					.. toctree::
 | 
				
			||||||
    :maxdepth: 2
 | 
					    :maxdepth: 2
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
recommonmark
 | 
					recommonmark==0.7.1
 | 
				
			||||||
Sphinx
 | 
					Sphinx==5.2.0
 | 
				
			||||||
sphinx-autobuild
 | 
					sphinx-autobuild==2021.3.14
 | 
				
			||||||
sphinx-rtd-theme
 | 
					sphinx-rtd-theme==1.0.0
 | 
				
			||||||
docutils==0.16
 | 
					docutils==0.16
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								towncrier/newsfragments/445.feature
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								towncrier/newsfragments/445.feature
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					Introduction of the Mailu RESTful API. The full Mailu config can be changed via the Mailu API.
 | 
				
			||||||
 | 
					See the section Mailu RESTful API & the section configuration reference in the documentation for more information.
 | 
				
			||||||
		Reference in New Issue
	
	Block a user