Merged conflicts
This commit is contained in:
10
.mergify.yml
Normal file
10
.mergify.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
rules:
|
||||||
|
default: null
|
||||||
|
branches:
|
||||||
|
master:
|
||||||
|
protection:
|
||||||
|
required_status_checks:
|
||||||
|
contexts:
|
||||||
|
- continuous-integration/travis-ci
|
||||||
|
required_pull_request_reviews:
|
||||||
|
required_approving_review_count: 2
|
||||||
28
.travis.yml
28
.travis.yml
@@ -1,8 +1,22 @@
|
|||||||
language: python
|
sudo: required
|
||||||
python:
|
services: docker
|
||||||
- "3.6"
|
addons:
|
||||||
install:
|
apt:
|
||||||
- pip install -r docs/requirements.txt
|
packages:
|
||||||
|
- docker-ce
|
||||||
|
env:
|
||||||
|
- VERSION=$TRAVIS_BRANCH
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- sphinx-versioning build -b -B 1.5 -r 1.5 -w '^[0-9.]*$' -w master -W '^$' docs/ build/
|
# Default to mailu for DOCKER_ORG
|
||||||
- python "docs/conf.py" "build" "$DEPLOY_HOST" "$DEPLOY_USERNAME" "$DEPLOY_PASSWORD" "$DEPLOY_REMOTEDIR"
|
- if [ -z "$DOCKER_ORG" ]; then export DOCKER_ORG="mailu"; fi
|
||||||
|
- docker-compose -f tests/build.yml build
|
||||||
|
- tests/compose/test-script.sh
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
provider: script
|
||||||
|
script: bash tests/deploy.sh
|
||||||
|
on:
|
||||||
|
all_branches: true
|
||||||
|
condition: -n $DOCKER_UN
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ COPY requirements-prod.txt requirements.txt
|
|||||||
RUN apk add --no-cache openssl \
|
RUN apk add --no-cache openssl \
|
||||||
&& apk add --no-cache --virtual build-dep openssl-dev libffi-dev python-dev build-base \
|
&& apk add --no-cache --virtual build-dep openssl-dev libffi-dev python-dev build-base \
|
||||||
&& pip install -r requirements.txt \
|
&& pip install -r requirements.txt \
|
||||||
&& apk del build-dep
|
&& apk del --no-cache build-dep
|
||||||
|
|
||||||
COPY mailu ./mailu
|
COPY mailu ./mailu
|
||||||
COPY migrations ./migrations
|
COPY migrations ./migrations
|
||||||
@@ -17,5 +17,6 @@ COPY start.sh /start.sh
|
|||||||
RUN pybabel compile -d mailu/translations
|
RUN pybabel compile -d mailu/translations
|
||||||
|
|
||||||
EXPOSE 80/tcp
|
EXPOSE 80/tcp
|
||||||
|
VOLUME ["/data"]
|
||||||
|
|
||||||
CMD ["/start.sh"]
|
CMD ["/start.sh"]
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import os
|
|||||||
import docker
|
import docker
|
||||||
import socket
|
import socket
|
||||||
import uuid
|
import uuid
|
||||||
import redis
|
|
||||||
|
|
||||||
from werkzeug.contrib import fixers
|
from werkzeug.contrib import fixers
|
||||||
|
|
||||||
@@ -58,7 +57,7 @@ default_config = {
|
|||||||
'RECAPTCHA_PUBLIC_KEY': '',
|
'RECAPTCHA_PUBLIC_KEY': '',
|
||||||
'RECAPTCHA_PRIVATE_KEY': '',
|
'RECAPTCHA_PRIVATE_KEY': '',
|
||||||
# Advanced settings
|
# Advanced settings
|
||||||
'PASSWORD_SCHEME': 'SHA512-CRYPT',
|
'PASSWORD_SCHEME': 'BLF-CRYPT',
|
||||||
# Host settings
|
# Host settings
|
||||||
'HOST_IMAP': 'imap',
|
'HOST_IMAP': 'imap',
|
||||||
'HOST_POP3': 'imap',
|
'HOST_POP3': 'imap',
|
||||||
@@ -89,9 +88,6 @@ manager.add_command('db', flask_migrate.MigrateCommand)
|
|||||||
babel = flask_babel.Babel(app)
|
babel = flask_babel.Babel(app)
|
||||||
translations = list(map(str, babel.list_translations()))
|
translations = list(map(str, babel.list_translations()))
|
||||||
|
|
||||||
# Quota manager
|
|
||||||
quota = redis.Redis.from_url(app.config.get("QUOTA_STORAGE_URL"))
|
|
||||||
|
|
||||||
@babel.localeselector
|
@babel.localeselector
|
||||||
def get_locale():
|
def get_locale():
|
||||||
return flask.request.accept_languages.best_match(translations)
|
return flask.request.accept_languages.best_match(translations)
|
||||||
|
|||||||
@@ -1,10 +1,22 @@
|
|||||||
|
from flask_limiter import RateLimitExceeded
|
||||||
|
|
||||||
from mailu import limiter
|
from mailu import limiter
|
||||||
|
|
||||||
import socket
|
import socket
|
||||||
import flask
|
import flask
|
||||||
|
|
||||||
|
|
||||||
internal = flask.Blueprint('internal', __name__)
|
internal = flask.Blueprint('internal', __name__, template_folder='templates')
|
||||||
|
|
||||||
|
|
||||||
|
@internal.app_errorhandler(RateLimitExceeded)
|
||||||
|
def rate_limit_handler(e):
|
||||||
|
response = flask.Response()
|
||||||
|
response.headers['Auth-Status'] = 'Authentication rate limit from one source exceeded'
|
||||||
|
response.headers['Auth-Error-Code'] = '451 4.3.2'
|
||||||
|
if int(flask.request.headers['Auth-Login-Attempt']) < 10:
|
||||||
|
response.headers['Auth-Wait'] = '3'
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@limiter.request_filter
|
@limiter.request_filter
|
||||||
@@ -16,4 +28,4 @@ def whitelist_webmail():
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
from mailu.internal import views
|
from mailu.internal.views import *
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ require "regex";
|
|||||||
require "relational";
|
require "relational";
|
||||||
require "date";
|
require "date";
|
||||||
require "comparator-i;ascii-numeric";
|
require "comparator-i;ascii-numeric";
|
||||||
require "vnd.dovecot.extdata";
|
|
||||||
require "vnd.dovecot.execute";
|
|
||||||
require "spamtestplus";
|
require "spamtestplus";
|
||||||
require "editheader";
|
require "editheader";
|
||||||
require "index";
|
require "index";
|
||||||
@@ -20,21 +18,23 @@ if header :index 2 :matches "Received" "from * by * for <*>; *"
|
|||||||
addheader "Delivered-To" "<${3}>";
|
addheader "Delivered-To" "<${3}>";
|
||||||
}
|
}
|
||||||
|
|
||||||
if allof (string :is "${extdata.spam_enabled}" "1",
|
{% if user.spam_enabled %}
|
||||||
spamtest :percent :value "gt" :comparator "i;ascii-numeric" "${extdata.spam_threshold}")
|
if spamtest :percent :value "gt" :comparator "i;ascii-numeric" "{{ user.spam_threshold }}"
|
||||||
{
|
{
|
||||||
setflag "\\seen";
|
setflag "\\seen";
|
||||||
fileinto :create "Junk";
|
fileinto :create "Junk";
|
||||||
stop;
|
stop;
|
||||||
}
|
}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
if exists "X-Virus" {
|
if exists "X-Virus" {
|
||||||
discard;
|
discard;
|
||||||
stop;
|
stop;
|
||||||
}
|
}
|
||||||
|
|
||||||
if allof (string :is "${extdata.reply_enabled}" "1",
|
{% if user.reply_enabled %}
|
||||||
currentdate :value "le" "date" "${extdata.reply_enddate}")
|
if currentdate :value "le" "date" "{{ user.reply_enddate }}"
|
||||||
{
|
{
|
||||||
vacation :days 1 :subject "${extdata.reply_subject}" "${extdata.reply_body}";
|
vacation :days 1 :subject "{{ user.reply_subject }}" "{{ user.reply_body }}";
|
||||||
}
|
}
|
||||||
|
{% endif %}
|
||||||
3
core/admin/mailu/internal/views/__init__.py
Normal file
3
core/admin/mailu/internal/views/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
__all__ = [
|
||||||
|
'auth', 'postfix', 'dovecot', 'fetch'
|
||||||
|
]
|
||||||
@@ -4,7 +4,6 @@ from mailu.internal import internal, nginx
|
|||||||
import flask
|
import flask
|
||||||
import flask_login
|
import flask_login
|
||||||
import base64
|
import base64
|
||||||
import urllib
|
|
||||||
|
|
||||||
|
|
||||||
@internal.route("/auth/email")
|
@internal.route("/auth/email")
|
||||||
40
core/admin/mailu/internal/views/dovecot.py
Normal file
40
core/admin/mailu/internal/views/dovecot.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
from mailu import db, models
|
||||||
|
from mailu.internal import internal
|
||||||
|
|
||||||
|
import flask
|
||||||
|
|
||||||
|
|
||||||
|
@internal.route("/dovecot/passdb/<user_email>")
|
||||||
|
def dovecot_passdb_dict(user_email):
|
||||||
|
user = models.User.query.get(user_email) or flask.abort(404)
|
||||||
|
return flask.jsonify({
|
||||||
|
"password": user.password,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@internal.route("/dovecot/userdb/<user_email>")
|
||||||
|
def dovecot_userdb_dict(user_email):
|
||||||
|
user = models.User.query.get(user_email) or flask.abort(404)
|
||||||
|
return flask.jsonify({
|
||||||
|
"quota_rule": "*:bytes={}".format(user.quota_bytes)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@internal.route("/dovecot/quota/<ns>/<user_email>", methods=["POST"])
|
||||||
|
def dovecot_quota(ns, user_email):
|
||||||
|
user = models.User.query.get(user_email) or flask.abort(404)
|
||||||
|
if ns == "storage":
|
||||||
|
user.quota_bytes_used = flask.request.get_json()
|
||||||
|
db.session.commit()
|
||||||
|
return flask.jsonify(None)
|
||||||
|
|
||||||
|
|
||||||
|
@internal.route("/dovecot/sieve/name/<script>/<user_email>")
|
||||||
|
def dovecot_sieve_name(script, user_email):
|
||||||
|
return flask.jsonify(script)
|
||||||
|
|
||||||
|
|
||||||
|
@internal.route("/dovecot/sieve/data/default/<user_email>")
|
||||||
|
def dovecot_sieve_data(user_email):
|
||||||
|
user = models.User.query.get(user_email) or flask.abort(404)
|
||||||
|
return flask.jsonify(flask.render_template("default.sieve", user=user))
|
||||||
32
core/admin/mailu/internal/views/fetch.py
Normal file
32
core/admin/mailu/internal/views/fetch.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from mailu import db, models
|
||||||
|
from mailu.internal import internal
|
||||||
|
|
||||||
|
import flask
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
|
@internal.route("/fetch")
|
||||||
|
def fetch_list():
|
||||||
|
return flask.jsonify([
|
||||||
|
{
|
||||||
|
"id": fetch.id,
|
||||||
|
"tls": fetch.tls,
|
||||||
|
"keep": fetch.keep,
|
||||||
|
"user_email": fetch.user_email,
|
||||||
|
"protocol": fetch.protocol,
|
||||||
|
"host": fetch.host,
|
||||||
|
"port": fetch.port,
|
||||||
|
"username": fetch.username,
|
||||||
|
"password": fetch.password
|
||||||
|
} for fetch in models.Fetch.query.all()
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
@internal.route("/fetch/<fetch_id>", methods=["POST"])
|
||||||
|
def fetch_done(fetch_id):
|
||||||
|
fetch = models.Fetch.query.get(fetch_id) or flask.abort(404)
|
||||||
|
fetch.last_check = datetime.datetime.now()
|
||||||
|
fetch.error_message = str(flask.request.get_json())
|
||||||
|
db.session.add(fetch)
|
||||||
|
db.session.commit()
|
||||||
|
return ""
|
||||||
54
core/admin/mailu/internal/views/postfix.py
Normal file
54
core/admin/mailu/internal/views/postfix.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
from mailu import db, models
|
||||||
|
from mailu.internal import internal
|
||||||
|
|
||||||
|
import flask
|
||||||
|
|
||||||
|
|
||||||
|
@internal.route("/postfix/domain/<domain_name>")
|
||||||
|
def postfix_mailbox_domain(domain_name):
|
||||||
|
domain = models.Domain.query.get(domain_name) or flask.abort(404)
|
||||||
|
return flask.jsonify(domain.name)
|
||||||
|
|
||||||
|
|
||||||
|
@internal.route("/postfix/mailbox/<email>")
|
||||||
|
def postfix_mailbox_map(email):
|
||||||
|
user = models.User.query.get(email) or flask.abort(404)
|
||||||
|
return flask.jsonify(user.email)
|
||||||
|
|
||||||
|
|
||||||
|
@internal.route("/postfix/alias/<alias>")
|
||||||
|
def postfix_alias_map(alias):
|
||||||
|
localpart, domain = alias.split('@', 1) if '@' in alias else (None, alias)
|
||||||
|
alternative = models.Alternative.query.get(domain)
|
||||||
|
if alternative:
|
||||||
|
domain = alternative.domain_name
|
||||||
|
email = '{}@{}'.format(localpart, domain)
|
||||||
|
if localpart is None:
|
||||||
|
return flask.jsonify(domain)
|
||||||
|
else:
|
||||||
|
alias_obj = models.Alias.resolve(localpart, domain)
|
||||||
|
if alias_obj:
|
||||||
|
return flask.jsonify(",".join(alias_obj.destination))
|
||||||
|
user_obj = models.User.query.get(email)
|
||||||
|
if user_obj:
|
||||||
|
return flask.jsonify(user_obj.destination)
|
||||||
|
return flask.abort(404)
|
||||||
|
|
||||||
|
|
||||||
|
@internal.route("/postfix/transport/<email>")
|
||||||
|
def postfix_transport(email):
|
||||||
|
localpart, domain = email.split('@', 1) if '@' in email else (None, email)
|
||||||
|
relay = models.Relay.query.get(domain) or flask.abort(404)
|
||||||
|
return flask.jsonify("smtp:[{}]".format(relay.smtp))
|
||||||
|
|
||||||
|
|
||||||
|
@internal.route("/postfix/sender/<sender>")
|
||||||
|
def postfix_sender(sender):
|
||||||
|
""" Simply reject any sender that pretends to be from a local domain
|
||||||
|
"""
|
||||||
|
localpart, domain_name = sender.split('@', 1) if '@' in sender else (None, sender)
|
||||||
|
domain = models.Domain.query.get(domain_name)
|
||||||
|
alternative = models.Alternative.query.get(domain_name)
|
||||||
|
if domain or alternative:
|
||||||
|
return flask.jsonify("REJECT")
|
||||||
|
return flask.abort(404)
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
from mailu import app, db, dkim, login_manager, quota
|
from mailu import app, db, dkim, login_manager
|
||||||
|
|
||||||
from sqlalchemy.ext import declarative
|
from sqlalchemy.ext import declarative
|
||||||
from passlib import context, hash
|
from passlib import context, hash
|
||||||
from datetime import datetime, date
|
from datetime import datetime, date
|
||||||
from email.mime import text
|
from email.mime import text
|
||||||
|
|
||||||
|
import sqlalchemy
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
@@ -235,6 +235,7 @@ class User(Base, Email):
|
|||||||
backref=db.backref('users', cascade='all, delete-orphan'))
|
backref=db.backref('users', cascade='all, delete-orphan'))
|
||||||
password = db.Column(db.String(255), nullable=False)
|
password = db.Column(db.String(255), nullable=False)
|
||||||
quota_bytes = db.Column(db.Integer(), nullable=False, default=10**9)
|
quota_bytes = db.Column(db.Integer(), nullable=False, default=10**9)
|
||||||
|
quota_bytes_used = db.Column(db.Integer(), nullable=False, default=0)
|
||||||
global_admin = db.Column(db.Boolean(), nullable=False, default=False)
|
global_admin = db.Column(db.Boolean(), nullable=False, default=False)
|
||||||
enabled = db.Column(db.Boolean(), nullable=False, default=True)
|
enabled = db.Column(db.Boolean(), nullable=False, default=True)
|
||||||
|
|
||||||
@@ -266,10 +267,17 @@ class User(Base, Email):
|
|||||||
return self.email
|
return self.email
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def quota_bytes_used(self):
|
def destination(self):
|
||||||
return quota.get(self.email + "/quota/storage") or 0
|
if self.forward_enabled:
|
||||||
|
result = self.self.forward_destination
|
||||||
|
if self.forward_keep:
|
||||||
|
result += ',' + self.email
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
return self.email
|
||||||
|
|
||||||
scheme_dict = {'SHA512-CRYPT': "sha512_crypt",
|
scheme_dict = {'BLF-CRYPT': "bcrypt",
|
||||||
|
'SHA512-CRYPT': "sha512_crypt",
|
||||||
'SHA256-CRYPT': "sha256_crypt",
|
'SHA256-CRYPT': "sha256_crypt",
|
||||||
'MD5-CRYPT': "md5_crypt",
|
'MD5-CRYPT': "md5_crypt",
|
||||||
'CRYPT': "des_crypt"}
|
'CRYPT': "des_crypt"}
|
||||||
@@ -329,6 +337,22 @@ class Alias(Base, Email):
|
|||||||
wildcard = db.Column(db.Boolean(), nullable=False, default=False)
|
wildcard = db.Column(db.Boolean(), nullable=False, default=False)
|
||||||
destination = db.Column(CommaSeparatedList, nullable=False, default=[])
|
destination = db.Column(CommaSeparatedList, nullable=False, default=[])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def resolve(cls, localpart, domain_name):
|
||||||
|
return cls.query.filter(
|
||||||
|
sqlalchemy.and_(cls.domain_name == domain_name,
|
||||||
|
sqlalchemy.or_(
|
||||||
|
sqlalchemy.and_(
|
||||||
|
cls.wildcard == False,
|
||||||
|
cls.localpart == localpart
|
||||||
|
), sqlalchemy.and_(
|
||||||
|
cls.wildcard == True,
|
||||||
|
sqlalchemy.bindparam("l", localpart).like(cls.localpart)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).first()
|
||||||
|
|
||||||
|
|
||||||
class Token(Base):
|
class Token(Base):
|
||||||
""" A token is an application password for a given user.
|
""" A token is an application password for a given user.
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class DomainForm(flask_wtf.FlaskForm):
|
|||||||
max_quota_bytes = fields_.IntegerSliderField(_('Maximum user quota'), default=0)
|
max_quota_bytes = fields_.IntegerSliderField(_('Maximum user quota'), default=0)
|
||||||
signup_enabled = fields.BooleanField(_('Enable sign-up'), default=False)
|
signup_enabled = fields.BooleanField(_('Enable sign-up'), default=False)
|
||||||
comment = fields.StringField(_('Comment'))
|
comment = fields.StringField(_('Comment'))
|
||||||
submit = fields.SubmitField(_('Create'))
|
submit = fields.SubmitField(_('Save'))
|
||||||
|
|
||||||
|
|
||||||
class DomainSignupForm(flask_wtf.FlaskForm):
|
class DomainSignupForm(flask_wtf.FlaskForm):
|
||||||
@@ -64,14 +64,14 @@ class DomainSignupForm(flask_wtf.FlaskForm):
|
|||||||
|
|
||||||
class AlternativeForm(flask_wtf.FlaskForm):
|
class AlternativeForm(flask_wtf.FlaskForm):
|
||||||
name = fields.StringField(_('Alternative name'), [validators.DataRequired()])
|
name = fields.StringField(_('Alternative name'), [validators.DataRequired()])
|
||||||
submit = fields.SubmitField(_('Create'))
|
submit = fields.SubmitField(_('Save'))
|
||||||
|
|
||||||
|
|
||||||
class RelayForm(flask_wtf.FlaskForm):
|
class RelayForm(flask_wtf.FlaskForm):
|
||||||
name = fields.StringField(_('Relayed domain name'), [validators.DataRequired()])
|
name = fields.StringField(_('Relayed domain name'), [validators.DataRequired()])
|
||||||
smtp = fields.StringField(_('Remote host'))
|
smtp = fields.StringField(_('Remote host'))
|
||||||
comment = fields.StringField(_('Comment'))
|
comment = fields.StringField(_('Comment'))
|
||||||
submit = fields.SubmitField(_('Create'))
|
submit = fields.SubmitField(_('Save'))
|
||||||
|
|
||||||
|
|
||||||
class UserForm(flask_wtf.FlaskForm):
|
class UserForm(flask_wtf.FlaskForm):
|
||||||
@@ -130,7 +130,7 @@ class TokenForm(flask_wtf.FlaskForm):
|
|||||||
ip = fields.StringField(
|
ip = fields.StringField(
|
||||||
_('Authorized IP'), [validators.Optional(), validators.IPAddress()]
|
_('Authorized IP'), [validators.Optional(), validators.IPAddress()]
|
||||||
)
|
)
|
||||||
submit = fields.SubmitField(_('Create'))
|
submit = fields.SubmitField(_('Save'))
|
||||||
|
|
||||||
|
|
||||||
class AliasForm(flask_wtf.FlaskForm):
|
class AliasForm(flask_wtf.FlaskForm):
|
||||||
@@ -139,7 +139,7 @@ class AliasForm(flask_wtf.FlaskForm):
|
|||||||
_('Use SQL LIKE Syntax (e.g. for catch-all aliases)'))
|
_('Use SQL LIKE Syntax (e.g. for catch-all aliases)'))
|
||||||
destination = DestinationField(_('Destination'))
|
destination = DestinationField(_('Destination'))
|
||||||
comment = fields.StringField(_('Comment'))
|
comment = fields.StringField(_('Comment'))
|
||||||
submit = fields.SubmitField(_('Create'))
|
submit = fields.SubmitField(_('Save'))
|
||||||
|
|
||||||
|
|
||||||
class AdminForm(flask_wtf.FlaskForm):
|
class AdminForm(flask_wtf.FlaskForm):
|
||||||
|
|||||||
28
core/admin/migrations/versions/25fd6c7bcb4a_.py
Normal file
28
core/admin/migrations/versions/25fd6c7bcb4a_.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
""" Add a column for used quota
|
||||||
|
|
||||||
|
Revision ID: 25fd6c7bcb4a
|
||||||
|
Revises: 049fed905da7
|
||||||
|
Create Date: 2018-07-25 21:56:09.729153
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '25fd6c7bcb4a'
|
||||||
|
down_revision = '049fed905da7'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
with op.batch_alter_table('user') as batch:
|
||||||
|
batch.add_column(sa.Column('quota_bytes_used', sa.Integer(), nullable=False, server_default='0'))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
with op.batch_alter_table('user') as batch:
|
||||||
|
batch.drop_column('user', 'quota_bytes_used')
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
alembic==0.9.9
|
alembic==0.9.9
|
||||||
asn1crypto==0.24.0
|
asn1crypto==0.24.0
|
||||||
Babel==2.5.3
|
Babel==2.5.3
|
||||||
|
bcrypt==3.1.4
|
||||||
blinker==1.4
|
blinker==1.4
|
||||||
certifi==2018.4.16
|
certifi==2018.4.16
|
||||||
cffi==1.11.5
|
cffi==1.11.5
|
||||||
|
|||||||
@@ -17,3 +17,4 @@ tabulate
|
|||||||
PyYAML
|
PyYAML
|
||||||
PyOpenSSL
|
PyOpenSSL
|
||||||
dnspython
|
dnspython
|
||||||
|
bcrypt
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
python manage.py advertise
|
python manage.py advertise
|
||||||
python manage.py db upgrade
|
python manage.py db upgrade
|
||||||
gunicorn -w 4 -b 0.0.0.0:80 -b [::]:80 --access-logfile - --error-logfile - --preload mailu:app
|
gunicorn -w 4 -b :80 --access-logfile - --error-logfile - --preload mailu:app
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
FROM alpine:3.7
|
FROM alpine:3.8
|
||||||
|
|
||||||
RUN echo "@testing http://nl.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories \
|
RUN apk add --no-cache \
|
||||||
&& apk add --no-cache \
|
dovecot dovecot-pigeonhole-plugin dovecot-fts-lucene rspamd-client \
|
||||||
dovecot dovecot-sqlite dovecot-pigeonhole-plugin dovecot-pigeonhole-plugin-extdata \
|
python3 py3-pip \
|
||||||
rspamd-client@testing python py-jinja2
|
&& pip3 install --upgrade pip \
|
||||||
|
&& pip3 install jinja2 podop tenacity
|
||||||
|
|
||||||
COPY conf /conf
|
COPY conf /conf
|
||||||
COPY sieve /var/lib/dovecot
|
|
||||||
COPY start.py /start.py
|
COPY start.py /start.py
|
||||||
|
|
||||||
EXPOSE 110/tcp 143/tcp 993/tcp 4190/tcp 2525/tcp
|
EXPOSE 110/tcp 143/tcp 993/tcp 4190/tcp 2525/tcp
|
||||||
|
VOLUME ["/data", "/mail"]
|
||||||
|
|
||||||
CMD /start.py
|
CMD /start.py
|
||||||
5
core/dovecot/conf/auth.conf
Normal file
5
core/dovecot/conf/auth.conf
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
uri = proxy:/tmp/podop.socket:auth
|
||||||
|
iterate_disable = yes
|
||||||
|
default_pass_scheme = plain
|
||||||
|
password_key = passdb/%u
|
||||||
|
user_key = userdb/%u
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
driver = sqlite
|
|
||||||
connect = /data/main.db
|
|
||||||
|
|
||||||
# Return the user hashed password
|
|
||||||
password_query = \
|
|
||||||
SELECT NULL as password, 'Y' as nopassword, '{% if POD_ADDRESS_RANGE %}{{ POD_ADDRESS_RANGE }}{% else %}{{ FRONT_ADDRESS }}{% if WEBMAIL_ADDRESS %},{{ WEBMAIL_ADDRESS }}{% endif %}{% endif %}' as allow_nets \
|
|
||||||
FROM user \
|
|
||||||
WHERE user.email = '%u'
|
|
||||||
|
|
||||||
# Mostly get the user quota
|
|
||||||
user_query = \
|
|
||||||
SELECT '*:bytes=' || user.quota_bytes AS quota_rule \
|
|
||||||
FROM user \
|
|
||||||
WHERE user.email = '%u'
|
|
||||||
|
|
||||||
# For using doveadm -A:
|
|
||||||
iterate_query = \
|
|
||||||
SELECT user.email AS user FROM user
|
|
||||||
@@ -7,15 +7,18 @@ postmaster_address = {{ POSTMASTER }}@{{ DOMAIN }}
|
|||||||
hostname = {{ HOSTNAMES.split(",")[0] }}
|
hostname = {{ HOSTNAMES.split(",")[0] }}
|
||||||
submission_host = {{ FRONT_ADDRESS }}
|
submission_host = {{ FRONT_ADDRESS }}
|
||||||
|
|
||||||
service dict {
|
###############
|
||||||
unix_listener dict {
|
# Full-text search
|
||||||
group = mail
|
###############
|
||||||
mode = 0660
|
mail_plugins = $mail_plugins fts fts_lucene
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dict {
|
plugin {
|
||||||
sieve = sqlite:/etc/dovecot/pigeonhole-sieve.dict
|
fts = lucene
|
||||||
|
|
||||||
|
fts_autoindex = yes
|
||||||
|
fts_autoindex_exclude = \Junk
|
||||||
|
|
||||||
|
fts_lucene = whitespace_chars=@.
|
||||||
}
|
}
|
||||||
|
|
||||||
###############
|
###############
|
||||||
@@ -32,32 +35,30 @@ mail_access_groups = mail
|
|||||||
maildir_stat_dirs = yes
|
maildir_stat_dirs = yes
|
||||||
mailbox_list_index = yes
|
mailbox_list_index = yes
|
||||||
mail_vsize_bg_after_count = 100
|
mail_vsize_bg_after_count = 100
|
||||||
mail_plugins = $mail_plugins quota quota_clone
|
mail_plugins = $mail_plugins quota quota_clone zlib
|
||||||
|
|
||||||
namespace inbox {
|
namespace inbox {
|
||||||
inbox = yes
|
inbox = yes
|
||||||
mailbox Trash {
|
{% for mailbox in ("Trash", "Drafts", "Sent", "Junk") %}
|
||||||
|
mailbox {{ mailbox }} {
|
||||||
auto = subscribe
|
auto = subscribe
|
||||||
special_use = \Trash
|
special_use = \{{ mailbox }}
|
||||||
}
|
|
||||||
mailbox Drafts {
|
|
||||||
auto = subscribe
|
|
||||||
special_use = \Drafts
|
|
||||||
}
|
|
||||||
mailbox Sent {
|
|
||||||
auto = subscribe
|
|
||||||
special_use = \Sent
|
|
||||||
}
|
|
||||||
mailbox Junk {
|
|
||||||
auto = subscribe
|
|
||||||
special_use = \Junk
|
|
||||||
}
|
}
|
||||||
|
{% endfor %}
|
||||||
}
|
}
|
||||||
|
|
||||||
plugin {
|
plugin {
|
||||||
quota = count:User quota
|
quota = count:User quota
|
||||||
quota_vsizes = yes
|
quota_vsizes = yes
|
||||||
quota_clone_dict = redis:host={{ REDIS_ADDRESS }}:port=6379:db=1
|
quota_clone_dict = proxy:/tmp/podop.socket:quota
|
||||||
|
|
||||||
|
{% if COMPRESSION in [ 'gz', 'bz2' ] %}
|
||||||
|
zlib_save = {{ COMPRESSION }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if COMPRESSION_LEVEL %}
|
||||||
|
zlib_save_level = {{ COMPRESSION_LEVEL }}
|
||||||
|
{% endif %}
|
||||||
}
|
}
|
||||||
|
|
||||||
###############
|
###############
|
||||||
@@ -65,16 +66,15 @@ plugin {
|
|||||||
###############
|
###############
|
||||||
auth_mechanisms = plain login
|
auth_mechanisms = plain login
|
||||||
disable_plaintext_auth = no
|
disable_plaintext_auth = no
|
||||||
ssl_protocols = !SSLv3
|
|
||||||
|
|
||||||
passdb {
|
passdb {
|
||||||
driver = sql
|
driver = dict
|
||||||
args = /etc/dovecot/dovecot-sql.conf.ext
|
args = /etc/dovecot/auth.conf
|
||||||
}
|
}
|
||||||
|
|
||||||
userdb {
|
userdb {
|
||||||
driver = sql
|
driver = dict
|
||||||
args = /etc/dovecot/dovecot-sql.conf.ext
|
args = /etc/dovecot/auth.conf
|
||||||
}
|
}
|
||||||
|
|
||||||
service auth {
|
service auth {
|
||||||
@@ -95,7 +95,6 @@ service auth-worker {
|
|||||||
###############
|
###############
|
||||||
# IMAP & POP
|
# IMAP & POP
|
||||||
###############
|
###############
|
||||||
|
|
||||||
protocol imap {
|
protocol imap {
|
||||||
mail_plugins = $mail_plugins imap_quota imap_sieve
|
mail_plugins = $mail_plugins imap_quota imap_sieve
|
||||||
}
|
}
|
||||||
@@ -113,7 +112,6 @@ service imap-login {
|
|||||||
###############
|
###############
|
||||||
# Delivery
|
# Delivery
|
||||||
###############
|
###############
|
||||||
|
|
||||||
protocol lmtp {
|
protocol lmtp {
|
||||||
mail_plugins = $mail_plugins sieve
|
mail_plugins = $mail_plugins sieve
|
||||||
recipient_delimiter = {{ RECIPIENT_DELIMITER }}
|
recipient_delimiter = {{ RECIPIENT_DELIMITER }}
|
||||||
@@ -125,11 +123,9 @@ service lmtp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
###############
|
###############
|
||||||
# Filtering
|
# Filtering
|
||||||
###############
|
###############
|
||||||
|
|
||||||
service managesieve-login {
|
service managesieve-login {
|
||||||
inet_listener sieve {
|
inet_listener sieve {
|
||||||
port = 4190
|
port = 4190
|
||||||
@@ -140,16 +136,13 @@ service managesieve {
|
|||||||
}
|
}
|
||||||
|
|
||||||
plugin {
|
plugin {
|
||||||
sieve = file:~/sieve;active=~/.dovecot.sieve
|
sieve = dict:proxy:/tmp/podop.socket:sieve
|
||||||
sieve_plugins = sieve_extdata sieve_imapsieve sieve_extprograms
|
sieve_plugins = sieve_imapsieve sieve_extprograms
|
||||||
sieve_global_extensions = +vnd.dovecot.extdata +spamtest +spamtestplus +vnd.dovecot.execute +editheader
|
sieve_extensions = +spamtest +spamtestplus +editheader
|
||||||
sieve_before = /var/lib/dovecot/before.sieve
|
sieve_global_extensions = +vnd.dovecot.execute
|
||||||
sieve_default = /var/lib/dovecot/default.sieve
|
|
||||||
sieve_after = /var/lib/dovecot/after.sieve
|
|
||||||
sieve_extdata_dict_uri = proxy::sieve
|
|
||||||
|
|
||||||
# Sieve execute
|
# Sieve execute
|
||||||
sieve_execute_bin_dir = /var/lib/dovecot/bin
|
sieve_execute_bin_dir = /conf/bin
|
||||||
|
|
||||||
# Send vacation replies even for aliases
|
# Send vacation replies even for aliases
|
||||||
# See the Pigeonhole documentation about warnings: http://wiki2.dovecot.org/Pigeonhole/Sieve/Extensions/Vacation
|
# See the Pigeonhole documentation about warnings: http://wiki2.dovecot.org/Pigeonhole/Sieve/Extensions/Vacation
|
||||||
@@ -168,11 +161,11 @@ plugin {
|
|||||||
# Learn from spam
|
# Learn from spam
|
||||||
imapsieve_mailbox1_name = Junk
|
imapsieve_mailbox1_name = Junk
|
||||||
imapsieve_mailbox1_causes = COPY
|
imapsieve_mailbox1_causes = COPY
|
||||||
imapsieve_mailbox1_before = file:/var/lib/dovecot/report-spam.sieve
|
imapsieve_mailbox1_before = file:/conf/report-spam.sieve
|
||||||
imapsieve_mailbox2_name = *
|
imapsieve_mailbox2_name = *
|
||||||
imapsieve_mailbox2_from = Junk
|
imapsieve_mailbox2_from = Junk
|
||||||
imapsieve_mailbox2_causes = COPY
|
imapsieve_mailbox2_causes = COPY
|
||||||
imapsieve_mailbox2_before = file:/var/lib/dovecot/report-ham.sieve
|
imapsieve_mailbox2_before = file:/conf/report-ham.sieve
|
||||||
}
|
}
|
||||||
|
|
||||||
###############
|
###############
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
connect = /data/main.db
|
|
||||||
|
|
||||||
map {
|
|
||||||
pattern = priv/spam_enabled
|
|
||||||
table = user
|
|
||||||
username_field = email
|
|
||||||
value_field = spam_enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
map {
|
|
||||||
pattern = priv/spam_threshold
|
|
||||||
table = user
|
|
||||||
username_field = email
|
|
||||||
value_field = spam_threshold
|
|
||||||
}
|
|
||||||
|
|
||||||
map {
|
|
||||||
pattern = priv/reply_enabled
|
|
||||||
table = user
|
|
||||||
username_field = email
|
|
||||||
value_field = reply_enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
map {
|
|
||||||
pattern = priv/reply_subject
|
|
||||||
table = user
|
|
||||||
username_field = email
|
|
||||||
value_field = reply_subject
|
|
||||||
}
|
|
||||||
|
|
||||||
map {
|
|
||||||
pattern = priv/reply_body
|
|
||||||
table = user
|
|
||||||
username_field = email
|
|
||||||
value_field = reply_body
|
|
||||||
}
|
|
||||||
|
|
||||||
map {
|
|
||||||
pattern = priv/reply_enddate
|
|
||||||
table = user
|
|
||||||
username_field = email
|
|
||||||
value_field = reply_enddate
|
|
||||||
}
|
|
||||||
11
core/dovecot/conf/report-ham.sieve
Normal file
11
core/dovecot/conf/report-ham.sieve
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
require ["vnd.dovecot.execute", "copy", "imapsieve", "environment", "variables"];
|
||||||
|
|
||||||
|
if environment :matches "imap.mailbox" "*" {
|
||||||
|
set "mailbox" "${1}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if string "${mailbox}" "Trash" {
|
||||||
|
stop;
|
||||||
|
}
|
||||||
|
|
||||||
|
execute :pipe "mailtrain" "ham";
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
require "vnd.dovecot.execute";
|
|
||||||
|
|
||||||
execute :pipe "mailtrain" "ham";
|
|
||||||
@@ -1,21 +1,40 @@
|
|||||||
#!/usr/bin/python
|
#!/usr/bin/python3
|
||||||
|
|
||||||
import jinja2
|
import jinja2
|
||||||
import os
|
import os
|
||||||
import socket
|
import socket
|
||||||
import glob
|
import glob
|
||||||
|
import multiprocessing
|
||||||
|
import tenacity
|
||||||
|
|
||||||
|
from tenacity import retry
|
||||||
|
from podop import run_server
|
||||||
|
|
||||||
|
|
||||||
|
def start_podop():
|
||||||
|
os.setuid(8)
|
||||||
|
run_server(3 if "DEBUG" in os.environ else 0, "dovecot", "/tmp/podop.socket", [
|
||||||
|
("quota", "url", "http://admin/internal/dovecot/§"),
|
||||||
|
("auth", "url", "http://admin/internal/dovecot/§"),
|
||||||
|
("sieve", "url", "http://admin/internal/dovecot/§"),
|
||||||
|
])
|
||||||
|
|
||||||
convert = lambda src, dst: open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ))
|
convert = lambda src, dst: open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ))
|
||||||
|
|
||||||
# Actual startup script
|
@retry(stop=tenacity.stop_after_attempt(100), wait=tenacity.wait_random(min=2, max=5))
|
||||||
os.environ["FRONT_ADDRESS"] = socket.gethostbyname(os.environ.get("FRONT_ADDRESS", "front"))
|
def resolve():
|
||||||
os.environ["REDIS_ADDRESS"] = socket.gethostbyname(os.environ.get("REDIS_ADDRESS", "redis"))
|
os.environ["FRONT_ADDRESS"] = socket.gethostbyname(os.environ.get("FRONT_ADDRESS", "front"))
|
||||||
if os.environ["WEBMAIL"] != "none":
|
os.environ["REDIS_ADDRESS"] = socket.gethostbyname(os.environ.get("REDIS_ADDRESS", "redis"))
|
||||||
|
if os.environ["WEBMAIL"] != "none":
|
||||||
os.environ["WEBMAIL_ADDRESS"] = socket.gethostbyname(os.environ.get("WEBMAIL_ADDRESS", "webmail"))
|
os.environ["WEBMAIL_ADDRESS"] = socket.gethostbyname(os.environ.get("WEBMAIL_ADDRESS", "webmail"))
|
||||||
|
|
||||||
for dovecot_file in glob.glob("/conf/*"):
|
# Actual startup script
|
||||||
|
resolve()
|
||||||
|
|
||||||
|
for dovecot_file in glob.glob("/conf/*.conf"):
|
||||||
convert(dovecot_file, os.path.join("/etc/dovecot", os.path.basename(dovecot_file)))
|
convert(dovecot_file, os.path.join("/etc/dovecot", os.path.basename(dovecot_file)))
|
||||||
|
|
||||||
# Run postfix
|
# Run Podop, then postfix
|
||||||
|
multiprocessing.Process(target=start_podop).start()
|
||||||
os.system("chown -R mail:mail /mail /var/lib/dovecot")
|
os.system("chown -R mail:mail /mail /var/lib/dovecot")
|
||||||
os.execv("/usr/sbin/dovecot", ["dovecot", "-c", "/etc/dovecot/dovecot.conf", "-F"])
|
os.execv("/usr/sbin/dovecot", ["dovecot", "-c", "/etc/dovecot/dovecot.conf", "-F"])
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
FROM alpine:3.7
|
FROM alpine:3.8
|
||||||
|
|
||||||
RUN apk add --no-cache nginx nginx-mod-mail python py-jinja2 certbot openssl
|
RUN apk add --no-cache certbot nginx nginx-mod-mail openssl \
|
||||||
|
python py-jinja2 py-requests-toolbelt py-pip \
|
||||||
|
&& pip install --upgrade pip \
|
||||||
|
&& pip install idna
|
||||||
|
|
||||||
COPY conf /conf
|
COPY conf /conf
|
||||||
COPY *.py /
|
COPY *.py /
|
||||||
|
|
||||||
EXPOSE 80/tcp 443/tcp 110/tcp 143/tcp 465/tcp 587/tcp 993/tcp 995/tcp 25/tcp 10025/tcp 10143/tcp
|
EXPOSE 80/tcp 443/tcp 110/tcp 143/tcp 465/tcp 587/tcp 993/tcp 995/tcp 25/tcp 10025/tcp 10143/tcp
|
||||||
|
VOLUME ["/certs"]
|
||||||
|
|
||||||
CMD /start.py
|
CMD /start.py
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# This is an idle image to dynamically replace any component if disabled.
|
# This is an idle image to dynamically replace any component if disabled.
|
||||||
|
|
||||||
FROM alpine
|
FROM alpine:3.8
|
||||||
|
|
||||||
CMD sleep 1000000d
|
CMD sleep 1000000d
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
FROM alpine:3.7
|
FROM alpine:3.8
|
||||||
|
|
||||||
RUN apk add --no-cache postfix postfix-sqlite postfix-pcre rsyslog python py-jinja2
|
RUN apk add --no-cache postfix postfix-pcre rsyslog \
|
||||||
|
python3 py3-pip \
|
||||||
|
&& pip3 install --upgrade pip \
|
||||||
|
&& pip3 install jinja2 podop tenacity
|
||||||
|
|
||||||
COPY conf /conf
|
COPY conf /conf
|
||||||
COPY start.py /start.py
|
COPY start.py /start.py
|
||||||
|
|
||||||
EXPOSE 25/tcp 10025/tcp
|
EXPOSE 25/tcp 10025/tcp
|
||||||
|
VOLUME ["/data"]
|
||||||
|
|
||||||
CMD /start.py
|
CMD /start.py
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ mynetworks = 127.0.0.1/32 [::1]/128 {{ RELAYNETS }}
|
|||||||
# Empty alias list to override the configuration variable and disable NIS
|
# Empty alias list to override the configuration variable and disable NIS
|
||||||
alias_maps =
|
alias_maps =
|
||||||
|
|
||||||
# SQLite configuration
|
# Podop configuration
|
||||||
sql = sqlite:${config_directory}/
|
podop = socketmap:unix:/tmp/podop.socket:
|
||||||
|
|
||||||
# Only accept virtual emails
|
# Only accept virtual emails
|
||||||
mydestination =
|
mydestination =
|
||||||
@@ -56,13 +56,14 @@ smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
|
|||||||
|
|
||||||
# The alias map actually returns both aliases and local mailboxes, which is
|
# The alias map actually returns both aliases and local mailboxes, which is
|
||||||
# required for reject_unlisted_sender to work properly
|
# required for reject_unlisted_sender to work properly
|
||||||
virtual_alias_maps = ${sql}sqlite-virtual_alias_maps.cf
|
virtual_alias_domains =
|
||||||
virtual_mailbox_domains = ${sql}sqlite-virtual_mailbox_domains.cf
|
virtual_alias_maps = ${podop}alias
|
||||||
virtual_mailbox_maps = $virtual_alias_maps
|
virtual_mailbox_domains = ${podop}domain
|
||||||
|
virtual_mailbox_maps = ${podop}mailbox
|
||||||
|
|
||||||
# Mails are transported if required, then forwarded to Dovecot for delivery
|
# Mails are transported if required, then forwarded to Dovecot for delivery
|
||||||
relay_domains = ${sql}sqlite-transport.cf
|
relay_domains = ${podop}transport
|
||||||
transport_maps = ${sql}sqlite-transport.cf
|
transport_maps = ${podop}transport
|
||||||
virtual_transport = lmtp:inet:{{ HOST_LMTP }}
|
virtual_transport = lmtp:inet:{{ HOST_LMTP }}
|
||||||
|
|
||||||
# In order to prevent Postfix from running DNS query, enforce the use of the
|
# In order to prevent Postfix from running DNS query, enforce the use of the
|
||||||
@@ -82,15 +83,20 @@ smtpd_sender_login_maps = $virtual_alias_maps
|
|||||||
# Restrictions for incoming SMTP, other restrictions are applied in master.cf
|
# Restrictions for incoming SMTP, other restrictions are applied in master.cf
|
||||||
smtpd_helo_required = yes
|
smtpd_helo_required = yes
|
||||||
|
|
||||||
smtpd_recipient_restrictions =
|
smtpd_client_restrictions =
|
||||||
permit_mynetworks,
|
permit_mynetworks,
|
||||||
check_sender_access ${sql}sqlite-reject-spoofed.cf,
|
check_sender_access ${podop}sender,
|
||||||
reject_non_fqdn_sender,
|
reject_non_fqdn_sender,
|
||||||
reject_unknown_sender_domain,
|
reject_unknown_sender_domain,
|
||||||
reject_unknown_recipient_domain,
|
reject_unknown_recipient_domain,
|
||||||
reject_unverified_recipient,
|
reject_unverified_recipient,
|
||||||
permit
|
permit
|
||||||
|
|
||||||
|
smtpd_relay_restrictions =
|
||||||
|
permit_mynetworks,
|
||||||
|
permit_sasl_authenticated,
|
||||||
|
reject_unauth_destination
|
||||||
|
|
||||||
unverified_recipient_reject_reason = Address lookup failure
|
unverified_recipient_reject_reason = Address lookup failure
|
||||||
|
|
||||||
###############
|
###############
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ smtp inet n - n - - smtpd
|
|||||||
# Internal SMTP service
|
# Internal SMTP service
|
||||||
10025 inet n - n - - smtpd
|
10025 inet n - n - - smtpd
|
||||||
-o smtpd_sasl_auth_enable=yes
|
-o smtpd_sasl_auth_enable=yes
|
||||||
-o smtpd_recipient_restrictions=reject_unlisted_sender,reject_authenticated_sender_login_mismatch,permit
|
-o smtpd_client_restrictions=reject_unlisted_sender,reject_authenticated_sender_login_mismatch,permit
|
||||||
|
-o smtpd_reject_unlisted_recipient={% if REJECT_UNLISTED_RECIPIENT %}{{ REJECT_UNLISTED_RECIPIENT }}{% else %}no{% endif %}
|
||||||
-o cleanup_service_name=outclean
|
-o cleanup_service_name=outclean
|
||||||
outclean unix n - n - 0 cleanup
|
outclean unix n - n - 0 cleanup
|
||||||
-o header_checks=pcre:/etc/postfix/outclean_header_filter.cf
|
-o header_checks=pcre:/etc/postfix/outclean_header_filter.cf
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
dbpath = /data/main.db
|
|
||||||
query =
|
|
||||||
SELECT 'REJECT' FROM domain WHERE name='%s'
|
|
||||||
UNION
|
|
||||||
SELECT 'REJECT' FROM alternative WHERE name='%s'
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
dbpath = /data/main.db
|
|
||||||
query =
|
|
||||||
SELECT 'smtp:['||smtp||']' FROM relay WHERE name='%s'
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
dbpath = /data/main.db
|
|
||||||
query =
|
|
||||||
SELECT destination
|
|
||||||
FROM
|
|
||||||
(SELECT destination, email, wildcard, localpart, localpart||'@'||alternative.name AS alt_email FROM alias LEFT JOIN alternative ON alias.domain_name = alternative.domain_name
|
|
||||||
UNION
|
|
||||||
SELECT (CASE WHEN forward_enabled=1 THEN (CASE WHEN forward_keep=1 THEN email||',' ELSE '' END)||forward_destination ELSE email END) AS destination, email, 0 as wildcard, localpart, localpart||'@'||alternative.name as alt_email FROM user LEFT JOIN alternative ON user.domain_name = alternative.domain_name
|
|
||||||
UNION
|
|
||||||
SELECT '@'||domain_name as destination, '@'||name as email, 0 as wildcard, '' as localpart, NULL AS alt_email FROM alternative)
|
|
||||||
WHERE
|
|
||||||
(
|
|
||||||
wildcard = 0
|
|
||||||
AND
|
|
||||||
(email = '%s' OR alt_email = '%s')
|
|
||||||
) OR (
|
|
||||||
wildcard = 1
|
|
||||||
AND
|
|
||||||
'%s' LIKE email
|
|
||||||
)
|
|
||||||
ORDER BY
|
|
||||||
wildcard ASC,
|
|
||||||
length(localpart) DESC
|
|
||||||
LIMIT 1
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
dbpath = /data/main.db
|
|
||||||
query =
|
|
||||||
SELECT name FROM domain WHERE name='%s'
|
|
||||||
UNION
|
|
||||||
SELECT name FROM alternative WHERE name='%s'
|
|
||||||
@@ -1,15 +1,35 @@
|
|||||||
#!/usr/bin/python
|
#!/usr/bin/python3
|
||||||
|
|
||||||
import jinja2
|
import jinja2
|
||||||
import os
|
import os
|
||||||
import socket
|
import socket
|
||||||
import glob
|
import glob
|
||||||
import shutil
|
import shutil
|
||||||
|
import tenacity
|
||||||
|
import multiprocessing
|
||||||
|
|
||||||
|
from tenacity import retry
|
||||||
|
from podop import run_server
|
||||||
|
|
||||||
|
|
||||||
|
def start_podop():
|
||||||
|
os.setuid(100)
|
||||||
|
run_server(3 if "DEBUG" in os.environ else 0, "postfix", "/tmp/podop.socket", [
|
||||||
|
("transport", "url", "http://admin/internal/postfix/transport/§"),
|
||||||
|
("alias", "url", "http://admin/internal/postfix/alias/§"),
|
||||||
|
("domain", "url", "http://admin/internal/postfix/domain/§"),
|
||||||
|
("mailbox", "url", "http://admin/internal/postfix/mailbox/§"),
|
||||||
|
("sender", "url", "http://admin/internal/postfix/sender/§")
|
||||||
|
])
|
||||||
|
|
||||||
convert = lambda src, dst: open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ))
|
convert = lambda src, dst: open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ))
|
||||||
|
|
||||||
|
@retry(stop=tenacity.stop_after_attempt(100), wait=tenacity.wait_random(min=2, max=5))
|
||||||
|
def resolve():
|
||||||
|
os.environ["FRONT_ADDRESS"] = socket.gethostbyname(os.environ.get("FRONT_ADDRESS", "front"))
|
||||||
|
|
||||||
# Actual startup script
|
# Actual startup script
|
||||||
os.environ["FRONT_ADDRESS"] = socket.gethostbyname(os.environ.get("FRONT_ADDRESS", "front"))
|
resolve()
|
||||||
os.environ["HOST_ANTISPAM"] = os.environ.get("HOST_ANTISPAM", "antispam:11332")
|
os.environ["HOST_ANTISPAM"] = os.environ.get("HOST_ANTISPAM", "antispam:11332")
|
||||||
os.environ["HOST_LMTP"] = os.environ.get("HOST_LMTP", "imap:2525")
|
os.environ["HOST_LMTP"] = os.environ.get("HOST_LMTP", "imap:2525")
|
||||||
|
|
||||||
@@ -32,7 +52,8 @@ for map_file in glob.glob("/overrides/*.map"):
|
|||||||
|
|
||||||
convert("/conf/rsyslog.conf", "/etc/rsyslog.conf")
|
convert("/conf/rsyslog.conf", "/etc/rsyslog.conf")
|
||||||
|
|
||||||
# Run postfix
|
# Run Podop and Postfix
|
||||||
|
multiprocessing.Process(target=start_podop).start()
|
||||||
if os.path.exists("/var/run/rsyslogd.pid"):
|
if os.path.exists("/var/run/rsyslogd.pid"):
|
||||||
os.remove("/var/run/rsyslogd.pid")
|
os.remove("/var/run/rsyslogd.pid")
|
||||||
os.system("/usr/lib/postfix/post-install meta_directory=/etc/postfix create-missing")
|
os.system("/usr/lib/postfix/post-install meta_directory=/etc/postfix create-missing")
|
||||||
|
|||||||
14
docs/Dockerfile
Normal file
14
docs/Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
FROM python:3-alpine
|
||||||
|
|
||||||
|
COPY requirements.txt /requirements.txt
|
||||||
|
|
||||||
|
RUN pip install -r /requirements.txt \
|
||||||
|
&& apk add --no-cache nginx \
|
||||||
|
&& mkdir /run/nginx
|
||||||
|
|
||||||
|
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY . /docs
|
||||||
|
|
||||||
|
RUN sphinx-build /docs /build
|
||||||
|
|
||||||
|
CMD nginx -g "daemon off;"
|
||||||
@@ -90,6 +90,12 @@ WELCOME=false
|
|||||||
WELCOME_SUBJECT=Welcome to your new email account
|
WELCOME_SUBJECT=Welcome to your new email account
|
||||||
WELCOME_BODY=Welcome to your new email account, if you can read this, then it is configured properly!
|
WELCOME_BODY=Welcome to your new email account, if you can read this, then it is configured properly!
|
||||||
|
|
||||||
|
# Maildir Compression
|
||||||
|
# choose compression-method, default: none (value: bz2, gz)
|
||||||
|
COMPRESSION=
|
||||||
|
# change compression-level, default: 6 (value: 1-9)
|
||||||
|
COMPRESSION_LEVEL=
|
||||||
|
|
||||||
###################################
|
###################################
|
||||||
# Web settings
|
# Web settings
|
||||||
###################################
|
###################################
|
||||||
@@ -117,15 +123,24 @@ WEBSITE=https://mailu.io
|
|||||||
# Advanced settings
|
# Advanced settings
|
||||||
###################################
|
###################################
|
||||||
|
|
||||||
|
# Log driver for front service. Possible values:
|
||||||
|
# json-file (default)
|
||||||
|
# journald (On systemd platforms, useful for Fail2Ban integration)
|
||||||
|
# syslog (Non systemd platforms, Fail2Ban integration. Disables `docker-compose log` for front!)
|
||||||
|
LOG_DRIVER=json-file
|
||||||
|
|
||||||
# Docker-compose project name, this will prepended to containers names.
|
# Docker-compose project name, this will prepended to containers names.
|
||||||
COMPOSE_PROJECT_NAME=mailu
|
COMPOSE_PROJECT_NAME=mailu
|
||||||
|
|
||||||
# Default password scheme used for newly created accounts and changed passwords
|
# Default password scheme used for newly created accounts and changed passwords
|
||||||
# (value: SHA512-CRYPT, SHA256-CRYPT, MD5-CRYPT, CRYPT)
|
# (value: BLF-CRYPT, SHA512-CRYPT, SHA256-CRYPT, MD5-CRYPT, CRYPT)
|
||||||
PASSWORD_SCHEME=SHA512-CRYPT
|
PASSWORD_SCHEME=BLF-CRYPT
|
||||||
|
|
||||||
# Header to take the real ip from
|
# Header to take the real ip from
|
||||||
REAL_IP_HEADER=
|
REAL_IP_HEADER=
|
||||||
|
|
||||||
# IPs for nginx set_real_ip_from (CIDR list separated by commas)
|
# IPs for nginx set_real_ip_from (CIDR list separated by commas)
|
||||||
REAL_IP_FROM=
|
REAL_IP_FROM=
|
||||||
|
|
||||||
|
# choose wether mailu bounces (no) or rejects (yes) mail when recipient is unknown (value: yes, no)
|
||||||
|
REJECT_UNLISTED_RECIPIENT=
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ services:
|
|||||||
image: mailu/nginx:$VERSION
|
image: mailu/nginx:$VERSION
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env
|
env_file: .env
|
||||||
|
logging:
|
||||||
|
driver: $LOG_DRIVER
|
||||||
ports:
|
ports:
|
||||||
- "$BIND_ADDRESS4:80:80"
|
- "$BIND_ADDRESS4:80:80"
|
||||||
- "$BIND_ADDRESS4:443:443"
|
- "$BIND_ADDRESS4:443:443"
|
||||||
@@ -39,7 +41,6 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
env_file: .env
|
env_file: .env
|
||||||
volumes:
|
volumes:
|
||||||
- "$ROOT/data:/data"
|
|
||||||
- "$ROOT/mail:/mail"
|
- "$ROOT/mail:/mail"
|
||||||
- "$ROOT/overrides:/overrides"
|
- "$ROOT/overrides:/overrides"
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -50,7 +51,6 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
env_file: .env
|
env_file: .env
|
||||||
volumes:
|
volumes:
|
||||||
- "$ROOT/data:/data"
|
|
||||||
- "$ROOT/overrides:/overrides"
|
- "$ROOT/overrides:/overrides"
|
||||||
depends_on:
|
depends_on:
|
||||||
- front
|
- front
|
||||||
@@ -104,5 +104,3 @@ services:
|
|||||||
image: mailu/fetchmail:$VERSION
|
image: mailu/fetchmail:$VERSION
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env
|
env_file: .env
|
||||||
volumes:
|
|
||||||
- "$ROOT/data:/data"
|
|
||||||
|
|||||||
@@ -26,36 +26,61 @@ for the ``VERSION_TAG`` branch, use:
|
|||||||
wget https://mailu.io/VERSION_TAG/_downloads/docker-compose.yml
|
wget https://mailu.io/VERSION_TAG/_downloads/docker-compose.yml
|
||||||
wget https://mailu.io/VERSION_TAG/_downloads/.env
|
wget https://mailu.io/VERSION_TAG/_downloads/.env
|
||||||
|
|
||||||
Then open the ``.env`` file to setup the mail server. Modify the ``ROOT`` setting
|
Important configuration variables
|
||||||
to match your setup directory if different from ``/mailu``.
|
---------------------------------
|
||||||
|
|
||||||
Modify the ``VERSION`` configuration in the ``.env`` file to reflect the version you picked.
|
Open the ``.env`` file and review the following variable settings:
|
||||||
|
|
||||||
Set the common configuration values
|
- Change ``ROOT`` if you have your setup directory in a different location then ``/mailu``.
|
||||||
-----------------------------------
|
- Check ``VERSION`` to reflect the version you picked. (``master`` or ``1.5``).
|
||||||
|
|
||||||
Open the ``.env`` file and set configuration settings after reading the configuration
|
Make sure to read the comments in the file and instructions from the :ref:`common_cfg` section.
|
||||||
documentation. Some settings are specific to the Docker Compose setup.
|
|
||||||
|
|
||||||
Modify ``BIND_ADDRESS4`` to match the public IP address assigned to your server.
|
TLS certificates
|
||||||
This address should be configured on one of the network interfaces of the server.
|
````````````````
|
||||||
If the address is not configured directly (NAT) on any of the network interfaces or if
|
|
||||||
you would simply like the server to listen on all interfaces, use ``0.0.0.0``.
|
|
||||||
|
|
||||||
Modify ``BIND_ADDRESS6`` to match the public IPv6 address assigned to your server.
|
|
||||||
The behavior is identical to ``BIND_ADDRESS4``.
|
|
||||||
|
|
||||||
Set the ``TLS_FLAVOR`` to one of the following
|
Set the ``TLS_FLAVOR`` to one of the following
|
||||||
values:
|
values:
|
||||||
|
|
||||||
- ``cert`` is the default and requires certificates to be setup manually;
|
- ``cert`` is the default and requires certificates to be setup manually;
|
||||||
- ``letsencrypt`` will use the Letsencrypt! CA to generate automatic ceriticates;
|
- ``letsencrypt`` will use the *Letsencrypt!* CA to generate automatic ceriticates;
|
||||||
- ``mail`` is similar to ``cert`` except that TLS will only be served for
|
- ``mail`` is similar to ``cert`` except that TLS will only be served for
|
||||||
emails (IMAP and SMTP), not HTTP (use it behind reverse proxies);
|
emails (IMAP and SMTP), not HTTP (use it behind reverse proxies);
|
||||||
- ``mail-letsencrypt`` is similar to ``letsencrypt`` except that TLS will only be served for
|
- ``mail-letsencrypt`` is similar to ``letsencrypt`` except that TLS will only be served for
|
||||||
emails (IMAP and SMTP), not HTTP (use it behind reverse proxies);
|
emails (IMAP and SMTP), not HTTP (use it behind reverse proxies);
|
||||||
- ``notls`` will disable TLS, this is not recommended except for testing.
|
- ``notls`` will disable TLS, this is not recommended except for testing.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
When using *Letsencrypt!* you have to make sure that the DNS ``A`` and ``AAAA`` records for the
|
||||||
|
all hostnames mentioned in the ``HOSTNAMES`` variable match with the ip adresses of you server.
|
||||||
|
Or else certificate generation will fail! See also: :ref:`dns_setup`.
|
||||||
|
|
||||||
|
Bind address
|
||||||
|
````````````
|
||||||
|
|
||||||
|
Modify ``BIND_ADDRESS4`` and ``BIND_ADDRESS6`` to match the public IP addresses assigned to your server. For IPv6 you will need the ``<global>`` scope address.
|
||||||
|
|
||||||
|
You can find those addresses by running the following:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
[root@mailu ~]$ ifconfig eth0
|
||||||
|
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
|
||||||
|
inet 125.189.138.127 netmask 255.255.255.0 broadcast 5.189.138.255
|
||||||
|
inet6 fd21:aab2:717c:cc5a::1 prefixlen 64 scopeid 0x0<global>
|
||||||
|
inet6 fe2f:2a73:43a8:7a1b::1 prefixlen 64 scopeid 0x20<link>
|
||||||
|
ether 00:50:56:3c:b2:23 txqueuelen 1000 (Ethernet)
|
||||||
|
RX packets 174866612 bytes 127773819607 (118.9 GiB)
|
||||||
|
RX errors 0 dropped 0 overruns 0 frame 0
|
||||||
|
TX packets 19905110 bytes 2191519656 (2.0 GiB)
|
||||||
|
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
|
||||||
|
|
||||||
|
If the address is not configured directly (NAT) on any of the network interfaces or if
|
||||||
|
you would simply like the server to listen on all interfaces, use ``0.0.0.0`` and ``::``. Note that running is this mode is not supported and can lead to `issues`_.
|
||||||
|
|
||||||
|
.. _issues: https://github.com/Mailu/Mailu/issues/641
|
||||||
|
|
||||||
Enable optional features
|
Enable optional features
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
@@ -92,7 +117,7 @@ setting. The configuration option must be one of the following:
|
|||||||
- ``none`` disables antivirus checks;
|
- ``none`` disables antivirus checks;
|
||||||
- ``clamav`` is the default values, the popular ClamAV antivirus is enabled.
|
- ``clamav`` is the default values, the popular ClamAV antivirus is enabled.
|
||||||
|
|
||||||
Make sure that you have at least 1GB or memory for ClamAV to load its signature
|
Make sure that you have at least 1GB of memory for ClamAV to load its signature
|
||||||
database.
|
database.
|
||||||
|
|
||||||
If you run Mailu behind a reverse proxy you can use ``REAL_IP_HEADER`` and
|
If you run Mailu behind a reverse proxy you can use ``REAL_IP_HEADER`` and
|
||||||
|
|||||||
25
docs/conf.py
25
docs/conf.py
@@ -7,7 +7,7 @@ templates_path = ['_templates']
|
|||||||
source_suffix = '.rst'
|
source_suffix = '.rst'
|
||||||
master_doc = 'index'
|
master_doc = 'index'
|
||||||
project = 'Mailu'
|
project = 'Mailu'
|
||||||
copyright = '2017, Mailu authors'
|
copyright = '2018, Mailu authors'
|
||||||
author = 'Mailu authors'
|
author = 'Mailu authors'
|
||||||
version = release = 'latest'
|
version = release = 'latest'
|
||||||
language = None
|
language = None
|
||||||
@@ -23,7 +23,7 @@ htmlhelp_basename = 'Mailudoc'
|
|||||||
# to template names.
|
# to template names.
|
||||||
html_sidebars = {
|
html_sidebars = {
|
||||||
'**': [
|
'**': [
|
||||||
'relations.html', # needs 'show_related': True theme option to display
|
'relations.html',
|
||||||
'searchbox.html',
|
'searchbox.html',
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -36,24 +36,3 @@ html_context = {
|
|||||||
'github_version': 'master',
|
'github_version': 'master',
|
||||||
'conf_py_path': '/docs/'
|
'conf_py_path': '/docs/'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Upload function when the script is called directly
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import os, sys, paramiko
|
|
||||||
build_dir, hostname, username, password, dest_dir = sys.argv[1:]
|
|
||||||
transport = paramiko.Transport((hostname, 22))
|
|
||||||
transport.connect(username=username, password=password)
|
|
||||||
sftp = paramiko.SFTPClient.from_transport(transport)
|
|
||||||
os.chdir(build_dir)
|
|
||||||
for dirpath, dirnames, filenames in os.walk("."):
|
|
||||||
remote_path = os.path.join(dest_dir, dirpath)
|
|
||||||
try:
|
|
||||||
sftp.mkdir(remote_path)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
for filename in filenames:
|
|
||||||
sftp.put(
|
|
||||||
os.path.join(dirpath, filename),
|
|
||||||
os.path.join(remote_path, filename)
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
Mailu configuration settings
|
Mailu configuration settings
|
||||||
============================
|
============================
|
||||||
|
|
||||||
|
.. _common_cfg:
|
||||||
|
|
||||||
Common configuration
|
Common configuration
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
The ``SECRET_KEY`` **must** be changed for every setup and set to a 16 bytes
|
The ``SECRET_KEY`` **must** be changed for every setup and set to a 16 bytes
|
||||||
randomly generated value. It is intended to secure authentication cookies
|
randomly generated value. It is intended to secure authentication cookies
|
||||||
among other critical uses.
|
among other critical uses. This can be generated with a utility such as *pwgen*,
|
||||||
|
which can be installed on most Linux systems:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
apt-get install pwgen
|
||||||
|
pwgen 16 1
|
||||||
|
|
||||||
The ``DOMAIN`` holds the main e-mail domain for the server. This email domain
|
The ``DOMAIN`` holds the main e-mail domain for the server. This email domain
|
||||||
is used for bounce emails, for generating the postmaster email and other
|
is used for bounce emails, for generating the postmaster email and other
|
||||||
|
|||||||
@@ -5,39 +5,51 @@ Docker containers
|
|||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
The development environment is quite similar to the production one. You should always use
|
The development environment is quite similar to the production one. You should always use
|
||||||
the ``master`` version when developing. Simply add a build directive to the images
|
the ``master`` version when developing.
|
||||||
you are working on in the ``docker-compose.yml``:
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
Building images
|
||||||
|
```````````````
|
||||||
|
|
||||||
webdav:
|
We supply a separate ``test/build.yml`` file for
|
||||||
build: ./optional/radicale
|
convenience. To build all Mailu containers:
|
||||||
image: mailu/$WEBDAV:$VERSION
|
|
||||||
restart: always
|
|
||||||
env_file: .env
|
|
||||||
volumes:
|
|
||||||
- "$ROOT/dav:/data"
|
|
||||||
|
|
||||||
admin:
|
|
||||||
build: ./core/admin
|
|
||||||
image: mailu/admin:$VERSION
|
|
||||||
restart: always
|
|
||||||
env_file: .env
|
|
||||||
volumes:
|
|
||||||
- "$ROOT/data:/data"
|
|
||||||
- "$ROOT/dkim:/dkim"
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
|
||||||
depends_on:
|
|
||||||
- redis
|
|
||||||
|
|
||||||
|
|
||||||
The build these containers.
|
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
docker-compose build admin webdav
|
docker-compose -f tests/build.yml build
|
||||||
|
|
||||||
Then you can simply start the stack as normal, newly-built images will be used.
|
The ``build.yml`` file has two variables:
|
||||||
|
|
||||||
|
#. ``$DOCKER_ORG``: First part of the image tag. Defaults to *mailu* and needs to be changed
|
||||||
|
only when pushing to your own Docker hub account.
|
||||||
|
#. ``$VERSION``: Last part of the image tag. Defaults to *local* to differentiate from pulled
|
||||||
|
images.
|
||||||
|
|
||||||
|
To re-build only specific containers at a later time.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
docker-compose -f tests/build.yml build admin webdav
|
||||||
|
|
||||||
|
If you have to push the images to Docker Hub for testing in Docker Swarm or a remote
|
||||||
|
host, you have to define ``DOCKER_ORG`` (usually your Docker user-name) and login to
|
||||||
|
the hub.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
docker login
|
||||||
|
Username: Foo
|
||||||
|
Password: Bar
|
||||||
|
export DOCKER_ORG="Foo"
|
||||||
|
export VERSION="feat-extra-app"
|
||||||
|
docker-compose -f tests/build.yml build
|
||||||
|
docker-compose -f tests/build.yml push
|
||||||
|
|
||||||
|
Running containers
|
||||||
|
``````````````````
|
||||||
|
|
||||||
|
To run the newly created images: ``cd`` to your project directory. Edit ``.env`` to set
|
||||||
|
``VERSION`` to the same value as used during the build, which defaults to ``local``.
|
||||||
|
After that you can run:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
@@ -89,3 +101,20 @@ Any change to the files will automatically restart the Web server and reload the
|
|||||||
|
|
||||||
When using the development environment, a debugging toolbar is displayed on the right side
|
When using the development environment, a debugging toolbar is displayed on the right side
|
||||||
of the screen, that you can open to access query details, internal variables, etc.
|
of the screen, that you can open to access query details, internal variables, etc.
|
||||||
|
|
||||||
|
Documentation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Documentation is maintained in the ``docs`` directory and are maintained as `reStructuredText`_ files. It is possible to run a local documentation server for reviewing purposes, using Docker:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
cd <Mailu repo>
|
||||||
|
docker build -t docs docs
|
||||||
|
docker run -p 127.0.0.1:8080:80 docs
|
||||||
|
|
||||||
|
You can now read the local documentation by navigating to http://localhost:8080.
|
||||||
|
|
||||||
|
.. note:: After modifying the documentation, the image needs to be rebuild and the container restarted for the changes to become visible.
|
||||||
|
|
||||||
|
.. _`reStructuredText`: http://docutils.sourceforge.net/rst.html
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
.. _dns_setup:
|
||||||
|
|
||||||
Setting up your DNS
|
Setting up your DNS
|
||||||
===================
|
===================
|
||||||
|
|
||||||
|
|||||||
157
docs/kubernetes/1.6/README.md
Normal file
157
docs/kubernetes/1.6/README.md
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
# Install Mailu master on kubernetes
|
||||||
|
|
||||||
|
## Prequisites
|
||||||
|
|
||||||
|
### Structure
|
||||||
|
|
||||||
|
There's chosen to have a double NGINX stack for Mailu, this way the main ingress can still be used to access other websites/domains on your cluster. This is the current structure:
|
||||||
|
|
||||||
|
- `NGINX Ingress controller`: Listens to the nodes ports 80 & 443 and directly forwards all TCP traffic on the E-amail ports (993,143,25,587,...). This is because this `DaemonSet` already consumes ports 80 & 443 and uses `hostNetwork: true`
|
||||||
|
- `Cert manager`: Creates automatic Lets Encrypt certificates based on an `Ingress`-objects domain name.
|
||||||
|
- `Mailu NGINX Front container`: This container receives all the mail traffic forwarded from the ingress controller. The web traffic is also forwarded based on an ingress
|
||||||
|
- `Mailu components`: All Mailu components are split into separate files to make them more
|
||||||
|
|
||||||
|
### What you need
|
||||||
|
- A working Kubernetes cluster (tested with 1.10.5)
|
||||||
|
- A working [cert-manager](https://github.com/jetstack/cert-manager) installation
|
||||||
|
- A working nginx-ingress controller needed for the lets-encrypt certificates. You can find those files in the `nginx` subfolder
|
||||||
|
|
||||||
|
#### Cert manager
|
||||||
|
|
||||||
|
The `Cert-manager` is quite easy to deploy using Helm when reading the [docs](https://cert-manager.readthedocs.io/en/latest/getting-started/2-installing.html).
|
||||||
|
After booting the `Cert-manager` you'll need a `ClusterIssuer` which takes care of all required certificates through `Ingress` items. An example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: certmanager.k8s.io/v1alpha1
|
||||||
|
kind: ClusterIssuer
|
||||||
|
metadata:
|
||||||
|
name: letsencrypt-prod
|
||||||
|
spec:
|
||||||
|
acme:
|
||||||
|
email: something@example.com
|
||||||
|
http01: {}
|
||||||
|
privateKeySecretRef:
|
||||||
|
key: ""
|
||||||
|
name: letsencrypt-stage
|
||||||
|
server: https://acme-v02.api.letsencrypt.org/directory
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deploying Mailu
|
||||||
|
|
||||||
|
All manifests can be found in the `mailu` subdirectory. All commands below need to be run from this subdirectory
|
||||||
|
|
||||||
|
### Personalization
|
||||||
|
- All services run in the same namespace, currently `mailu-mailserver`. So if you want to use a different one, change the `namespace` value in **every** file
|
||||||
|
- Check the `storage-class` field in the `pvc.yaml` file, you can also change the sizes to your liking. Note that you need `RWX` (read-write-many) and `RWO` (read-write-once) storageclasses.
|
||||||
|
- Check the `configmap.yaml` and adapt it to your needs. Be sure to check the kubernetes DNS values at the end (if you use a different namespace)
|
||||||
|
- Check the `ingress-ssl.yaml` and change it to the domain you want (this is for the kubernetes ingress controller, it will forward to `mailu/nginx` a.k.a. the `front` pod)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
First run the command to start Mailu:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl create -f rbac.yaml
|
||||||
|
kubectl create -f configmap.yaml
|
||||||
|
kubectl create -f pvc.yaml
|
||||||
|
kubectl create -f ingress-ssl.yaml
|
||||||
|
kubectl create -f redis.yaml
|
||||||
|
kubectl create -f front.yaml
|
||||||
|
kubectl create -f webmail.yaml
|
||||||
|
kubectl create -f imap.yaml
|
||||||
|
kubectl create -f security.yaml
|
||||||
|
kubectl create -f smtp.yaml
|
||||||
|
kubectl create -f fetchmail.yaml
|
||||||
|
kubectl create -f admin.yaml
|
||||||
|
kubectl create -f webdav.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Create the first admin account
|
||||||
|
|
||||||
|
When the cluster is online you need to create you master user to access `https://mail.example.com/admin`.
|
||||||
|
Enter the main `admin` pod to create the root account:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n mailu-mailserver get po
|
||||||
|
kubectl -n mailu-mailserver exec -it mailu-admin-.... /bin/sh
|
||||||
|
```
|
||||||
|
|
||||||
|
And in the pod run the following command. The command uses following entries:
|
||||||
|
- `admin` Make it an admin user
|
||||||
|
- `root` The first part of the e-mail adres (ROOT@example.com)
|
||||||
|
- `example.com` the domain appendix
|
||||||
|
- `password` the chosen password for the user
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python manage.py admin root example.com password
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you should be able to login on the mail account: `https://mail.example.com/admin`
|
||||||
|
|
||||||
|
## Adaptations
|
||||||
|
|
||||||
|
### Postfix
|
||||||
|
I noticed you need an override for the `postfix` server in order to be able to send mail. I noticed Google wasn't able to deliver mail to my account and it had to do with the `smtpd_authorized_xclient_hosts` value in the config file. The config can be read [here](https://github.com/hacor/Mailu/blob/master/core/postfix/conf/main.cf#L35) and is pointing to a single IP of the service. But the requests come from the host IPs (the NGINX Ingress proxy) and they don't use the service specific IP.
|
||||||
|
|
||||||
|
Enter the `postfix` pod:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n mailu-mailserver get po
|
||||||
|
kubectl -n mailu-mailserver exec -it mailu-smtp-.... /bin/sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you're in the pod, create an override file like so:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
vi /overrides/postfix.cf
|
||||||
|
```
|
||||||
|
|
||||||
|
And give it the following contents, off course replacing `10.2.0.0/16` with the CIDR of your pod range. This way the NGINX pods can also restart and your mail server will still operate
|
||||||
|
|
||||||
|
```bash
|
||||||
|
not_needed = true
|
||||||
|
smtpd_authorized_xclient_hosts = 10.2.0.0/16
|
||||||
|
```
|
||||||
|
|
||||||
|
The first line seems stupid, but is needed because its pasted after a #, so from the second line we're really in action.
|
||||||
|
Save and close the file and exit. Now you need to delete the pod in order to recreate the config file.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n mailu-mailserver delete po/mailu-smtp-....
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dovecot
|
||||||
|
- If you are using Dovecot on a shared file system (Glusterfs, NFS,...), you need to create a special override otherwise a lot of indexing errors will occur on your Dovecot pod.
|
||||||
|
- I also higher the number of max connections per IP. Now it's limited to 10.
|
||||||
|
Enter the dovecot pod:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n mailu-mailserver get po
|
||||||
|
kubectl -n mailu-mailserver exec -it mailu-imap-.... /bin/sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Create the file `/overrides/dovecot.conf`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
vi /overrides/dovecot.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
And enter following contents:
|
||||||
|
```bash
|
||||||
|
mail_nfs_index = yes
|
||||||
|
mail_nfs_storage = yes
|
||||||
|
mail_fsync = always
|
||||||
|
mmap_disable = yes
|
||||||
|
mail_max_userip_connections=100
|
||||||
|
```
|
||||||
|
|
||||||
|
Save and close the file and delete the imap pod to get it recreated.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n mailu-mailserver delete po/mailu-imap-....
|
||||||
|
```
|
||||||
|
|
||||||
|
Wait for the pod to recreate and you're online!
|
||||||
|
Happy mailing!
|
||||||
|
|
||||||
|
Wait for the pod to recreate and you're online!
|
||||||
|
Happy mailing!
|
||||||
64
docs/kubernetes/1.6/mailu/admin.yaml
Normal file
64
docs/kubernetes/1.6/mailu/admin.yaml
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
|
||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: mailu-admin
|
||||||
|
namespace: mailu-mailserver
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: mailu-admin
|
||||||
|
role: mail
|
||||||
|
tier: backend
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: admin
|
||||||
|
image: mailu/admin:master
|
||||||
|
imagePullPolicy: Always
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: mailu-config
|
||||||
|
volumeMounts:
|
||||||
|
- name: maildata
|
||||||
|
mountPath: /data
|
||||||
|
subPath: maildata
|
||||||
|
- name: maildata
|
||||||
|
mountPath: /dkim
|
||||||
|
subPath: dkim
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 80
|
||||||
|
protocol: TCP
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: 500Mi
|
||||||
|
cpu: 500m
|
||||||
|
limits:
|
||||||
|
memory: 500Mi
|
||||||
|
cpu: 500m
|
||||||
|
volumes:
|
||||||
|
- name: maildata
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: mail-storage
|
||||||
|
---
|
||||||
|
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: admin
|
||||||
|
namespace: mailu-mailserver
|
||||||
|
labels:
|
||||||
|
app: mailu-admin
|
||||||
|
role: mail
|
||||||
|
tier: backend
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: mailu-admin
|
||||||
|
role: mail
|
||||||
|
tier: backend
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
protocol: TCP
|
||||||
153
docs/kubernetes/1.6/mailu/configmap.yaml
Normal file
153
docs/kubernetes/1.6/mailu/configmap.yaml
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: mailu-config
|
||||||
|
namespace: mailu-mailserver
|
||||||
|
data:
|
||||||
|
# Mailu main configuration file
|
||||||
|
#
|
||||||
|
# Most configuration variables can be modified through the Web interface,
|
||||||
|
# these few settings must however be configured before starting the mail
|
||||||
|
# server and require a restart upon change.
|
||||||
|
|
||||||
|
###################################
|
||||||
|
# Common configuration variables
|
||||||
|
###################################
|
||||||
|
|
||||||
|
# Set this to the path where Mailu data and configuration is stored
|
||||||
|
ROOT: "/mailu"
|
||||||
|
|
||||||
|
# Mailu version to run (1.0, 1.1, etc. or master)
|
||||||
|
VERSION: "master"
|
||||||
|
|
||||||
|
# Set to a randomly generated 16 bytes string
|
||||||
|
SECRET_KEY: "YourKeyHere"
|
||||||
|
|
||||||
|
# Address where listening ports should bind
|
||||||
|
BIND_ADDRESS4: "127.0.0.1"
|
||||||
|
#BIND_ADDRESS6: "::1"
|
||||||
|
|
||||||
|
# Main mail domain
|
||||||
|
DOMAIN: "example.com"
|
||||||
|
|
||||||
|
# Hostnames for this server, separated with comas
|
||||||
|
HOSTNAMES: "mail.example.com"
|
||||||
|
|
||||||
|
# Postmaster local part (will append the main mail domain)
|
||||||
|
POSTMASTER: "admin"
|
||||||
|
|
||||||
|
# Choose how secure connections will behave (value: letsencrypt, cert, notls, mail, mail-letsencrypt)
|
||||||
|
TLS_FLAVOR: "cert"
|
||||||
|
|
||||||
|
# Authentication rate limit (per source IP address)
|
||||||
|
AUTH_RATELIMIT: "10/minute;1000/hour"
|
||||||
|
|
||||||
|
# Opt-out of statistics, replace with "True" to opt out
|
||||||
|
DISABLE_STATISTICS: "False"
|
||||||
|
|
||||||
|
###################################
|
||||||
|
# Optional features
|
||||||
|
###################################
|
||||||
|
|
||||||
|
# Expose the admin interface (value: true, false)
|
||||||
|
ADMIN: "true"
|
||||||
|
# Run the admin interface in debug mode
|
||||||
|
#DEBUG: "True"
|
||||||
|
|
||||||
|
# Choose which webmail to run if any (values: roundcube, rainloop, none)
|
||||||
|
WEBMAIL: "roundcube"
|
||||||
|
|
||||||
|
# Dav server implementation (value: radicale, none)
|
||||||
|
WEBDAV: "radicale"
|
||||||
|
|
||||||
|
# Antivirus solution (value: clamav, none)
|
||||||
|
ANTIVIRUS: "clamav"
|
||||||
|
|
||||||
|
###################################
|
||||||
|
# Mail settings
|
||||||
|
###################################
|
||||||
|
|
||||||
|
# Message size limit in bytes
|
||||||
|
# Default: accept messages up to 50MB
|
||||||
|
MESSAGE_SIZE_LIMIT: "50000000"
|
||||||
|
|
||||||
|
# Networks granted relay permissions, make sure that you include your Docker
|
||||||
|
# internal network (default to 172.17.0.0/16)
|
||||||
|
# For kubernetes this is the CIDR of the pod network
|
||||||
|
RELAYNETS: "10.2.0.0/16"
|
||||||
|
POD_ADDRESS_RANGE: "10.2.0.0/16"
|
||||||
|
|
||||||
|
|
||||||
|
# Will relay all outgoing mails if configured
|
||||||
|
#RELAYHOST=
|
||||||
|
|
||||||
|
# This part is needed for the XCLIENT login for postfix. This should be the POD ADDRESS range
|
||||||
|
FRONT_ADDRESS: "front.mailu-mailserver.svc.cluster.local"
|
||||||
|
|
||||||
|
# Fetchmail delay
|
||||||
|
FETCHMAIL_DELAY: "600"
|
||||||
|
|
||||||
|
# Recipient delimiter, character used to delimiter localpart from custom address part
|
||||||
|
# e.g. localpart+custom@domain;tld
|
||||||
|
RECIPIENT_DELIMITER: "+"
|
||||||
|
|
||||||
|
# DMARC rua and ruf email
|
||||||
|
DMARC_RUA: "root"
|
||||||
|
DMARC_RUF: "root"
|
||||||
|
|
||||||
|
# Welcome email, enable and set a topic and body if you wish to send welcome
|
||||||
|
# emails to all users.
|
||||||
|
WELCOME: "false"
|
||||||
|
WELCOME_SUBJECT: "Welcome to your new email account"
|
||||||
|
WELCOME_BODY: "Welcome to your new email account, if you can read this, then it is configured properly!"
|
||||||
|
|
||||||
|
###################################
|
||||||
|
# Web settings
|
||||||
|
###################################
|
||||||
|
|
||||||
|
# Path to the admin interface if enabled
|
||||||
|
WEB_ADMIN: "/admin"
|
||||||
|
|
||||||
|
# Path to the webmail if enabled
|
||||||
|
WEB_WEBMAIL: "/webmail"
|
||||||
|
|
||||||
|
# Website name
|
||||||
|
SITENAME: "AppSynth"
|
||||||
|
|
||||||
|
# Linked Website URL
|
||||||
|
WEBSITE: "https://example.com"
|
||||||
|
|
||||||
|
# Registration reCaptcha settings (warning, this has some privacy impact)
|
||||||
|
# RECAPTCHA_PUBLIC_KEY=
|
||||||
|
# RECAPTCHA_PRIVATE_KEY=
|
||||||
|
|
||||||
|
# Domain registration, uncomment to enable
|
||||||
|
# DOMAIN_REGISTRATION=true
|
||||||
|
|
||||||
|
###################################
|
||||||
|
# Advanced settings
|
||||||
|
###################################
|
||||||
|
|
||||||
|
# Docker-compose project name, this will prepended to containers names.
|
||||||
|
COMPOSE_PROJECT_NAME: "mailu"
|
||||||
|
|
||||||
|
# Default password scheme used for newly created accounts and changed passwords
|
||||||
|
# (value: SHA512-CRYPT, SHA256-CRYPT, MD5-CRYPT, CRYPT)
|
||||||
|
PASSWORD_SCHEME: "SHA512-CRYPT"
|
||||||
|
|
||||||
|
# Header to take the real ip from
|
||||||
|
#REAL_IP_HEADER:
|
||||||
|
|
||||||
|
# IPs for nginx set_real_ip_from (CIDR list separated by commas)
|
||||||
|
#REAL_IP_FROM:
|
||||||
|
|
||||||
|
# Host settings
|
||||||
|
HOST_IMAP: "imap.mailu-mailserver.svc.cluster.local"
|
||||||
|
HOST_POP3: "imap.mailu-mailserver.svc.cluster.local"
|
||||||
|
HOST_SMTP: "smtp.mailu-mailserver.svc.cluster.local"
|
||||||
|
HOST_AUTHSMTP: "smtp.mailu-mailserver.svc.cluster.local"
|
||||||
|
HOST_WEBMAIL: "webmail.mailu-mailserver.svc.cluster.local"
|
||||||
|
HOST_ADMIN: "admin.mailu-mailserver.svc.cluster.local"
|
||||||
|
HOST_WEBDAV: "webdav.mailu-mailserver.svc.cluster.local:5232"
|
||||||
|
HOST_ANTISPAM: "antispam.mailu-mailserver.svc.cluster.local:11332"
|
||||||
|
HOST_REDIS: "redis.mailu-mailserver.svc.cluster.local"
|
||||||
39
docs/kubernetes/1.6/mailu/fetchmail.yaml
Normal file
39
docs/kubernetes/1.6/mailu/fetchmail.yaml
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: mailu-fetchmail
|
||||||
|
namespace: mailu-mailserver
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: mailu-fetchmail
|
||||||
|
role: mail
|
||||||
|
tier: backend
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: fetchmail
|
||||||
|
image: mailu/fetchmail:master
|
||||||
|
imagePullPolicy: Always
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: mailu-config
|
||||||
|
volumeMounts:
|
||||||
|
- name: maildata
|
||||||
|
mountPath: /data
|
||||||
|
subPath: maildata
|
||||||
|
ports:
|
||||||
|
- containerPort: 5232
|
||||||
|
- containerPort: 80
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: 100Mi
|
||||||
|
cpu: 100m
|
||||||
|
limits:
|
||||||
|
memory: 100Mi
|
||||||
|
cpu: 100m
|
||||||
|
volumes:
|
||||||
|
- name: maildata
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: mail-storage
|
||||||
129
docs/kubernetes/1.6/mailu/front.yaml
Normal file
129
docs/kubernetes/1.6/mailu/front.yaml
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
|
||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: mailu-front
|
||||||
|
namespace: mailu-mailserver
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: mailu-front
|
||||||
|
role: mail
|
||||||
|
tier: backend
|
||||||
|
spec:
|
||||||
|
restartPolicy: Always
|
||||||
|
terminationGracePeriodSeconds: 60
|
||||||
|
containers:
|
||||||
|
- name: front
|
||||||
|
image: mailu/nginx:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: mailu-config
|
||||||
|
volumeMounts:
|
||||||
|
- name: certs
|
||||||
|
mountPath: /certs
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 80
|
||||||
|
protocol: TCP
|
||||||
|
- name: https
|
||||||
|
containerPort: 443
|
||||||
|
protocol: TCP
|
||||||
|
- name: pop3
|
||||||
|
containerPort: 110
|
||||||
|
protocol: TCP
|
||||||
|
- name: pop3s
|
||||||
|
containerPort: 995
|
||||||
|
protocol: TCP
|
||||||
|
- name: imap
|
||||||
|
containerPort: 143
|
||||||
|
protocol: TCP
|
||||||
|
- name: imaps
|
||||||
|
containerPort: 993
|
||||||
|
protocol: TCP
|
||||||
|
- name: smtp
|
||||||
|
containerPort: 25
|
||||||
|
protocol: TCP
|
||||||
|
- name: smtp-auth
|
||||||
|
containerPort: 10025
|
||||||
|
protocol: TCP
|
||||||
|
- name: imap-auth
|
||||||
|
containerPort: 10143
|
||||||
|
protocol: TCP
|
||||||
|
- name: smtps
|
||||||
|
containerPort: 465
|
||||||
|
protocol: TCP
|
||||||
|
- name: smtpd
|
||||||
|
containerPort: 587
|
||||||
|
protocol: TCP
|
||||||
|
- name: auth
|
||||||
|
containerPort: 8000
|
||||||
|
protocol: TCP
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: 100Mi
|
||||||
|
cpu: 100m
|
||||||
|
limits:
|
||||||
|
memory: 200Mi
|
||||||
|
cpu: 200m
|
||||||
|
volumes:
|
||||||
|
- name: certs
|
||||||
|
secret:
|
||||||
|
items:
|
||||||
|
- key: tls.crt
|
||||||
|
path: cert.pem
|
||||||
|
- key: tls.key
|
||||||
|
path: key.pem
|
||||||
|
secretName: letsencrypt-certs-all
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: front
|
||||||
|
namespace: mailu-mailserver
|
||||||
|
labels:
|
||||||
|
app: mailu-admin
|
||||||
|
role: mail
|
||||||
|
tier: backend
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: mailu-front
|
||||||
|
role: mail
|
||||||
|
tier: backend
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
protocol: TCP
|
||||||
|
- name: https
|
||||||
|
port: 443
|
||||||
|
protocol: TCP
|
||||||
|
- name: pop3
|
||||||
|
port: 110
|
||||||
|
protocol: TCP
|
||||||
|
- name: pop3s
|
||||||
|
port: 995
|
||||||
|
protocol: TCP
|
||||||
|
- name: imap
|
||||||
|
port: 143
|
||||||
|
protocol: TCP
|
||||||
|
- name: imaps
|
||||||
|
port: 993
|
||||||
|
protocol: TCP
|
||||||
|
- name: smtp
|
||||||
|
port: 25
|
||||||
|
protocol: TCP
|
||||||
|
- name: smtps
|
||||||
|
port: 465
|
||||||
|
protocol: TCP
|
||||||
|
- name: smtpd
|
||||||
|
port: 587
|
||||||
|
protocol: TCP
|
||||||
|
- name: smtp-auth
|
||||||
|
port: 10025
|
||||||
|
protocol: TCP
|
||||||
|
- name: imap-auth
|
||||||
|
port: 10143
|
||||||
|
protocol: TCP
|
||||||
80
docs/kubernetes/1.6/mailu/imap.yaml
Normal file
80
docs/kubernetes/1.6/mailu/imap.yaml
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: mailu-imap
|
||||||
|
namespace: mailu-mailserver
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: mailu-imap
|
||||||
|
role: mail
|
||||||
|
tier: backend
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: imap
|
||||||
|
image: mailu/dovecot:master
|
||||||
|
imagePullPolicy: Always
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: mailu-config
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /data
|
||||||
|
name: maildata
|
||||||
|
subPath: maildata
|
||||||
|
- mountPath: /mail
|
||||||
|
name: maildata
|
||||||
|
subPath: mailstate
|
||||||
|
- mountPath: /overrides
|
||||||
|
name: maildata
|
||||||
|
subPath: overrides
|
||||||
|
ports:
|
||||||
|
- containerPort: 2102
|
||||||
|
- containerPort: 2525
|
||||||
|
- containerPort: 143
|
||||||
|
- containerPort: 993
|
||||||
|
- containerPort: 4190
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: 500Mi
|
||||||
|
cpu: 500m
|
||||||
|
limits:
|
||||||
|
memory: 1Gi
|
||||||
|
cpu: 1000m
|
||||||
|
volumes:
|
||||||
|
- name: maildata
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: mail-storage
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: imap
|
||||||
|
namespace: mailu-mailserver
|
||||||
|
labels:
|
||||||
|
app: mailu
|
||||||
|
role: mail
|
||||||
|
tier: backend
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: mailu-imap
|
||||||
|
role: mail
|
||||||
|
tier: backend
|
||||||
|
ports:
|
||||||
|
ports:
|
||||||
|
- name: imap-auth
|
||||||
|
port: 2102
|
||||||
|
protocol: TCP
|
||||||
|
- name: imap-transport
|
||||||
|
port: 2525
|
||||||
|
protocol: TCP
|
||||||
|
- name: imap-default
|
||||||
|
port: 143
|
||||||
|
protocol: TCP
|
||||||
|
- name: imap-ssl
|
||||||
|
port: 993
|
||||||
|
protocol: TCP
|
||||||
|
- name: sieve
|
||||||
|
port: 4190
|
||||||
|
protocol: TCP
|
||||||
32
docs/kubernetes/1.6/mailu/ingress-ssl.yaml
Normal file
32
docs/kubernetes/1.6/mailu/ingress-ssl.yaml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: mailu-ssl-ingress
|
||||||
|
namespace: mailu-mailserver
|
||||||
|
annotations:
|
||||||
|
kubernetes.io/ingress.class: tectonic
|
||||||
|
kubernetes.io/tls-acme: "true"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-body-size: "0"
|
||||||
|
ingress.kubernetes.io/ssl-redirect: "true"
|
||||||
|
# Replace letsencrypt-prod with the name of the certificate issuer
|
||||||
|
certmanager.k8s.io/cluster-issuer: letsencrypt-prod
|
||||||
|
#ingress.kubernetes.io/rewrite-target: "/"
|
||||||
|
#ingress.kubernetes.io/app-root: "/ui"
|
||||||
|
#ingress.kubernetes.io/follow-redirects: "true"
|
||||||
|
labels:
|
||||||
|
app: mailu
|
||||||
|
role: mail
|
||||||
|
tier: backend
|
||||||
|
spec:
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- "mail.example.com"
|
||||||
|
secretName: letsencrypt-certs-all # If unsure how to generate these, check out https://github.com/ployst/docker-letsencrypt
|
||||||
|
rules:
|
||||||
|
- host: "mail.example.com"
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: "/"
|
||||||
|
backend:
|
||||||
|
serviceName: front
|
||||||
|
servicePort: 80
|
||||||
27
docs/kubernetes/1.6/mailu/pvc.yaml
Normal file
27
docs/kubernetes/1.6/mailu/pvc.yaml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
kind: PersistentVolumeClaim
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: redis-hdd
|
||||||
|
namespace: mailu-mailserver
|
||||||
|
annotations:
|
||||||
|
volume.beta.kubernetes.io/storage-class: "glusterblock-hdd"
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 1Gi
|
||||||
|
---
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: mail-storage
|
||||||
|
namespace: mailu-mailserver
|
||||||
|
annotations:
|
||||||
|
volume.beta.kubernetes.io/storage-class: "gluster-heketi-hdd"
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteMany
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 100Gi
|
||||||
4
docs/kubernetes/1.6/mailu/rbac.yaml
Normal file
4
docs/kubernetes/1.6/mailu/rbac.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: mailu-mailserver
|
||||||
56
docs/kubernetes/1.6/mailu/redis.yaml
Normal file
56
docs/kubernetes/1.6/mailu/redis.yaml
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: mailu-redis
|
||||||
|
namespace: mailu-mailserver
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: mailu-redis
|
||||||
|
role: mail
|
||||||
|
tier: backend
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: redis
|
||||||
|
image: redis:4.0-alpine
|
||||||
|
imagePullPolicy: Always
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /data
|
||||||
|
name: redisdata
|
||||||
|
ports:
|
||||||
|
- containerPort: 6379
|
||||||
|
name: redis
|
||||||
|
protocol: TCP
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: 200Mi
|
||||||
|
cpu: 100m
|
||||||
|
limits:
|
||||||
|
memory: 300Mi
|
||||||
|
cpu: 200m
|
||||||
|
volumes:
|
||||||
|
- name: redisdata
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: redis-hdd
|
||||||
|
---
|
||||||
|
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: redis
|
||||||
|
namespace: mailu-mailserver
|
||||||
|
labels:
|
||||||
|
app: mailu-redis
|
||||||
|
role: mail
|
||||||
|
tier: backend
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: mailu-redis
|
||||||
|
role: mail
|
||||||
|
tier: backend
|
||||||
|
ports:
|
||||||
|
- name: redis
|
||||||
|
port: 6379
|
||||||
|
protocol: TCP
|
||||||
110
docs/kubernetes/1.6/mailu/security.yaml
Normal file
110
docs/kubernetes/1.6/mailu/security.yaml
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
|
||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: mailu-security
|
||||||
|
namespace: mailu-mailserver
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: mailu-security
|
||||||
|
role: mail
|
||||||
|
tier: backend
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: antispam
|
||||||
|
image: mailu/rspamd:master
|
||||||
|
imagePullPolicy: Always
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: mailu-config
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: 100Mi
|
||||||
|
cpu: 100m
|
||||||
|
limits:
|
||||||
|
memory: 200Mi
|
||||||
|
cpu: 200m
|
||||||
|
ports:
|
||||||
|
- name: antispam
|
||||||
|
containerPort: 11332
|
||||||
|
protocol: TCP
|
||||||
|
volumeMounts:
|
||||||
|
- name: filter
|
||||||
|
subPath: filter
|
||||||
|
mountPath: /var/lib/rspamd
|
||||||
|
- name: filter
|
||||||
|
mountPath: /dkim
|
||||||
|
subPath: dkim
|
||||||
|
- name: filter
|
||||||
|
mountPath: /etc/rspamd/override.d
|
||||||
|
subPath: rspamd-overrides
|
||||||
|
- name: antivirus
|
||||||
|
image: mailu/clamav:master
|
||||||
|
imagePullPolicy: Always
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: 1Gi
|
||||||
|
cpu: 1000m
|
||||||
|
limits:
|
||||||
|
memory: 2Gi
|
||||||
|
cpu: 1000m
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: mailu-config
|
||||||
|
ports:
|
||||||
|
- name: antivirus
|
||||||
|
containerPort: 3310
|
||||||
|
protocol: TCP
|
||||||
|
volumeMounts:
|
||||||
|
- name: filter
|
||||||
|
subPath: filter
|
||||||
|
mountPath: /data
|
||||||
|
volumes:
|
||||||
|
- name: filter
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: mail-storage
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: antispam
|
||||||
|
namespace: mailu-mailserver
|
||||||
|
labels:
|
||||||
|
app: mailu-antispam
|
||||||
|
role: mail
|
||||||
|
tier: backend
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: mailu-security
|
||||||
|
role: mail
|
||||||
|
tier: backend
|
||||||
|
ports:
|
||||||
|
- name: antispam
|
||||||
|
port: 11332
|
||||||
|
protocol: TCP
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: antivirus
|
||||||
|
namespace: mailu-mailserver
|
||||||
|
labels:
|
||||||
|
app: mailu-antivirus
|
||||||
|
role: mail
|
||||||
|
tier: backend
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: mailu-security
|
||||||
|
role: mail
|
||||||
|
tier: backend
|
||||||
|
ports:
|
||||||
|
- name: antivirus
|
||||||
|
port: 3310
|
||||||
|
protocol: TCP
|
||||||
80
docs/kubernetes/1.6/mailu/smtp.yaml
Normal file
80
docs/kubernetes/1.6/mailu/smtp.yaml
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: mailu-smtp
|
||||||
|
namespace: mailu-mailserver
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: mailu-smtp
|
||||||
|
role: mail
|
||||||
|
tier: backend
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: smtp
|
||||||
|
image: mailu/postfix:master
|
||||||
|
imagePullPolicy: Always
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: mailu-config
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: 500Mi
|
||||||
|
cpu: 200m
|
||||||
|
limits:
|
||||||
|
memory: 1Gi
|
||||||
|
cpu: 500m
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /data
|
||||||
|
name: maildata
|
||||||
|
subPath: maildata
|
||||||
|
- mountPath: /overrides
|
||||||
|
name: maildata
|
||||||
|
subPath: overrides
|
||||||
|
ports:
|
||||||
|
- name: smtp
|
||||||
|
containerPort: 25
|
||||||
|
protocol: TCP
|
||||||
|
- name: smtp-ssl
|
||||||
|
containerPort: 465
|
||||||
|
protocol: TCP
|
||||||
|
- name: smtp-starttls
|
||||||
|
containerPort: 587
|
||||||
|
protocol: TCP
|
||||||
|
- name: smtp-auth
|
||||||
|
containerPort: 10025
|
||||||
|
protocol: TCP
|
||||||
|
volumes:
|
||||||
|
- name: maildata
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: mail-storage
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: smtp
|
||||||
|
namespace: mailu-mailserver
|
||||||
|
labels:
|
||||||
|
app: mailu
|
||||||
|
role: mail
|
||||||
|
tier: backend
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: mailu-smtp
|
||||||
|
role: mail
|
||||||
|
tier: backend
|
||||||
|
ports:
|
||||||
|
- name: smtp
|
||||||
|
port: 25
|
||||||
|
protocol: TCP
|
||||||
|
- name: smtp-ssl
|
||||||
|
port: 465
|
||||||
|
protocol: TCP
|
||||||
|
- name: smtp-starttls
|
||||||
|
port: 587
|
||||||
|
protocol: TCP
|
||||||
|
- name: smtp-auth
|
||||||
|
port: 10025
|
||||||
|
protocol: TCP
|
||||||
63
docs/kubernetes/1.6/mailu/webdav.yaml
Normal file
63
docs/kubernetes/1.6/mailu/webdav.yaml
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: mailu-webdav
|
||||||
|
namespace: mailu-mailserver
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: mailu-webdav
|
||||||
|
role: mail
|
||||||
|
tier: backend
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: radicale
|
||||||
|
image: mailu/radicale:master
|
||||||
|
imagePullPolicy: Always
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: mailu-config
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /data
|
||||||
|
name: maildata
|
||||||
|
subPath: dav
|
||||||
|
ports:
|
||||||
|
- containerPort: 5232
|
||||||
|
- containerPort: 80
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: 100Mi
|
||||||
|
cpu: 100m
|
||||||
|
limits:
|
||||||
|
memory: 100Mi
|
||||||
|
cpu: 100m
|
||||||
|
volumes:
|
||||||
|
- name: maildata
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: mail-storage
|
||||||
|
---
|
||||||
|
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: webdav
|
||||||
|
namespace: mailu-mailserver
|
||||||
|
labels:
|
||||||
|
app: mailu-webdav
|
||||||
|
role: mail
|
||||||
|
tier: backend
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: mailu-webdav
|
||||||
|
role: mail
|
||||||
|
tier: backend
|
||||||
|
ports:
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
protocol: TCP
|
||||||
|
- name: http-ui
|
||||||
|
port: 5232
|
||||||
|
protocol: TCP
|
||||||
59
docs/kubernetes/1.6/mailu/webmail.yaml
Normal file
59
docs/kubernetes/1.6/mailu/webmail.yaml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
|
||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: mailu-roundcube
|
||||||
|
namespace: mailu-mailserver
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: mailu-roundcube
|
||||||
|
role: mail
|
||||||
|
tier: frontend
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: roundcube
|
||||||
|
image: mailu/roundcube:1.5
|
||||||
|
imagePullPolicy: Always
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: mailu-config
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: 100Mi
|
||||||
|
cpu: 100m
|
||||||
|
limits:
|
||||||
|
memory: 200Mi
|
||||||
|
cpu: 200m
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /data
|
||||||
|
name: maildata
|
||||||
|
subPath: webmail
|
||||||
|
ports:
|
||||||
|
- containerPort: 80
|
||||||
|
volumes:
|
||||||
|
- name: maildata
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: mail-storage
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: webmail
|
||||||
|
namespace: mailu-mailserver
|
||||||
|
labels:
|
||||||
|
app: mailu-roundcube
|
||||||
|
role: mail
|
||||||
|
tier: frontend
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: mailu-roundcube
|
||||||
|
role: mail
|
||||||
|
tier: frontend
|
||||||
|
ports:
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
protocol: TCP
|
||||||
55
docs/kubernetes/1.6/nginx/default-http-backend.yaml
Normal file
55
docs/kubernetes/1.6/nginx/default-http-backend.yaml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: default-http-backend
|
||||||
|
labels:
|
||||||
|
app: default-http-backend
|
||||||
|
namespace: kube-ingress
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: default-http-backend
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: default-http-backend
|
||||||
|
spec:
|
||||||
|
terminationGracePeriodSeconds: 60
|
||||||
|
containers:
|
||||||
|
- name: default-http-backend
|
||||||
|
# Any image is permissible as long as:
|
||||||
|
# 1. It serves a 404 page at /
|
||||||
|
# 2. It serves 200 on a /healthz endpoint
|
||||||
|
image: gcr.io/google_containers/defaultbackend:1.4
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /healthz
|
||||||
|
port: 8080
|
||||||
|
scheme: HTTP
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
timeoutSeconds: 5
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 10m
|
||||||
|
memory: 20Mi
|
||||||
|
requests:
|
||||||
|
cpu: 10m
|
||||||
|
memory: 20Mi
|
||||||
|
---
|
||||||
|
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: default-http-backend
|
||||||
|
namespace: kube-ingress
|
||||||
|
labels:
|
||||||
|
app: default-http-backend
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 8080
|
||||||
|
selector:
|
||||||
|
app: default-http-backend
|
||||||
139
docs/kubernetes/1.6/nginx/nginx-ingress.yaml
Normal file
139
docs/kubernetes/1.6/nginx/nginx-ingress.yaml
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
# keep it under 24 chars
|
||||||
|
name: appsynth-lb
|
||||||
|
namespace: kube-ingress
|
||||||
|
labels:
|
||||||
|
k8s-app: appsynth-lb
|
||||||
|
component: ingress-controller
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
k8s-app: appsynth-lb
|
||||||
|
component: ingress-controller
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
protocol: TCP
|
||||||
|
port: 80
|
||||||
|
targetPort: 80
|
||||||
|
- name: https
|
||||||
|
protocol: TCP
|
||||||
|
port: 443
|
||||||
|
targetPort: 443
|
||||||
|
---
|
||||||
|
kind: ConfigMap
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: udp-services
|
||||||
|
namespace: kube-ingress
|
||||||
|
|
||||||
|
---
|
||||||
|
kind: ConfigMap
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: tcp-services
|
||||||
|
namespace: kube-ingress
|
||||||
|
data:
|
||||||
|
25: "mailu-mailserver/front:25"
|
||||||
|
110: "mailu-mailserver/front:110"
|
||||||
|
465: "mailu-mailserver/front:465"
|
||||||
|
587: "mailu-mailserver/front:587"
|
||||||
|
143: "mailu-mailserver/front:143"
|
||||||
|
993: "mailu-mailserver/front:993"
|
||||||
|
995: "mailu-mailserver/front:995"
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
data:
|
||||||
|
enable-vts-status: "true"
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: nginx-ingress-lb-conf
|
||||||
|
namespace: kube-ingress
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1beta2
|
||||||
|
kind: DaemonSet
|
||||||
|
metadata:
|
||||||
|
name: ingress-controller
|
||||||
|
namespace: kube-ingress
|
||||||
|
annotations:
|
||||||
|
prometheus.io/port: "10254"
|
||||||
|
prometheus.io/scrape: "true"
|
||||||
|
labels:
|
||||||
|
k8s-app: appsynth-lb
|
||||||
|
component: ingress-controller
|
||||||
|
type: nginx
|
||||||
|
spec:
|
||||||
|
updateStrategy:
|
||||||
|
rollingUpdate:
|
||||||
|
maxUnavailable: 1
|
||||||
|
type: RollingUpdate
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
k8s-app: appsynth-lb
|
||||||
|
component: ingress-controller
|
||||||
|
type: nginx
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
k8s-app: appsynth-lb
|
||||||
|
component: ingress-controller
|
||||||
|
type: nginx
|
||||||
|
spec:
|
||||||
|
serviceAccount: kube-nginx-ingress
|
||||||
|
affinity:
|
||||||
|
nodeAffinity:
|
||||||
|
requiredDuringSchedulingIgnoredDuringExecution:
|
||||||
|
nodeSelectorTerms:
|
||||||
|
- matchExpressions:
|
||||||
|
- key: node-role.kubernetes.io/master
|
||||||
|
operator: DoesNotExist
|
||||||
|
containers:
|
||||||
|
- name: nginx-ingress-lb
|
||||||
|
image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.16.2
|
||||||
|
args:
|
||||||
|
- /nginx-ingress-controller
|
||||||
|
- --configmap=$(POD_NAMESPACE)/tectonic-custom-error
|
||||||
|
- --default-backend-service=$(POD_NAMESPACE)/default-http-backend
|
||||||
|
#- --default-ssl-certificate=tectonic-system/tectonic-ingress-tls-secret
|
||||||
|
- --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services
|
||||||
|
- --udp-services-configmap=$(POD_NAMESPACE)/udp-services
|
||||||
|
- --annotations-prefix=ingress.kubernetes.io
|
||||||
|
- --enable-ssl-passthrough
|
||||||
|
- --ingress-class=tectonic
|
||||||
|
# use downward API
|
||||||
|
env:
|
||||||
|
- name: POD_NAME
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.name
|
||||||
|
- name: POD_NAMESPACE
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.namespace
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 80
|
||||||
|
hostPort: 80
|
||||||
|
- name: https
|
||||||
|
containerPort: 443
|
||||||
|
hostPort: 443
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /healthz
|
||||||
|
port: 10254
|
||||||
|
scheme: HTTP
|
||||||
|
livenessProbe:
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
timeoutSeconds: 1
|
||||||
|
httpGet:
|
||||||
|
path: /healthz
|
||||||
|
port: 10254
|
||||||
|
scheme: HTTP
|
||||||
|
hostNetwork: true
|
||||||
|
nodeSelector:
|
||||||
|
node-role.kubernetes.io/node: ""
|
||||||
|
dnsPolicy: ClusterFirst
|
||||||
|
restartPolicy: Always
|
||||||
|
terminationGracePeriodSeconds: 60
|
||||||
129
docs/kubernetes/1.6/nginx/rbac.yaml
Normal file
129
docs/kubernetes/1.6/nginx/rbac.yaml
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: kube-ingress
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: kube-nginx-ingress
|
||||||
|
namespace: kube-ingress
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: kube-nginx-ingress
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- configmaps
|
||||||
|
- endpoints
|
||||||
|
- nodes
|
||||||
|
- pods
|
||||||
|
- secrets
|
||||||
|
verbs:
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- update
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- nodes
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- services
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- "extensions"
|
||||||
|
resources:
|
||||||
|
- ingresses
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- events
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- patch
|
||||||
|
- apiGroups:
|
||||||
|
- "extensions"
|
||||||
|
resources:
|
||||||
|
- ingresses/status
|
||||||
|
verbs:
|
||||||
|
- update
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
name: kube-nginx-ingress
|
||||||
|
namespace: kube-ingress
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- configmaps
|
||||||
|
- pods
|
||||||
|
- secrets
|
||||||
|
- namespaces
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- configmaps
|
||||||
|
resourceNames:
|
||||||
|
- "ingress-controller-leader-nginx"
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- update
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- configmaps
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- endpoints
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- create
|
||||||
|
- update
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: kube-nginx-ingress
|
||||||
|
namespace: kube-ingress
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: Role
|
||||||
|
name: kube-nginx-ingress
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: kube-nginx-ingress
|
||||||
|
namespace: kube-ingress
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
name: kube-nginx-ingress
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: kube-nginx-ingress
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: kube-nginx-ingress
|
||||||
|
namespace: kube-ingress
|
||||||
5
docs/nginx.conf
Normal file
5
docs/nginx.conf
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
root /build;
|
||||||
|
}
|
||||||
@@ -2,5 +2,3 @@ recommonmark
|
|||||||
Sphinx
|
Sphinx
|
||||||
sphinx-autobuild
|
sphinx-autobuild
|
||||||
sphinx-rtd-theme
|
sphinx-rtd-theme
|
||||||
sphinxcontrib-versioning
|
|
||||||
paramiko
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ One of Mailu use cases is as part of a larger services platform, where maybe oth
|
|||||||
In such a configuration, one would usually run a frontend reverse proxy to serve all Web contents based on criteria like the requested hostname (virtual hosts) and/or the requested path. Mailu Web frontend is disabled in the default setup for security reasons, it is however expected that most users will enable it at some point. Also, due to Docker Compose configuration structure, it is impossible for us to make disabling the Web frontend completely available through a configuration variable. This guide was written to help users setup such an architecture.
|
In such a configuration, one would usually run a frontend reverse proxy to serve all Web contents based on criteria like the requested hostname (virtual hosts) and/or the requested path. Mailu Web frontend is disabled in the default setup for security reasons, it is however expected that most users will enable it at some point. Also, due to Docker Compose configuration structure, it is impossible for us to make disabling the Web frontend completely available through a configuration variable. This guide was written to help users setup such an architecture.
|
||||||
|
|
||||||
There are basically three options, from the most to the least recommended one:
|
There are basically three options, from the most to the least recommended one:
|
||||||
|
|
||||||
- have Mailu Web frontend listen locally and use your own Web frontend on top of it
|
- have Mailu Web frontend listen locally and use your own Web frontend on top of it
|
||||||
- override Mailu Web frontend configuration
|
- override Mailu Web frontend configuration
|
||||||
- disable Mailu Web frontend completely and use your own
|
- disable Mailu Web frontend completely and use your own
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ user. Make sure you complete the requirements for the flavor you chose.
|
|||||||
|
|
||||||
You should also have at least a DNS hostname and a DNS name for receiving
|
You should also have at least a DNS hostname and a DNS name for receiving
|
||||||
emails. Some instructions are provided on the matter in the article
|
emails. Some instructions are provided on the matter in the article
|
||||||
[Setup your DNS](dns).
|
:ref:`dns_setup`.
|
||||||
|
|
||||||
.. _`MFAshby's fork`: https://github.com/MFAshby/Mailu
|
.. _`MFAshby's fork`: https://github.com/MFAshby/Mailu
|
||||||
|
|
||||||
@@ -68,10 +68,9 @@ Make sure that you test properly before going live!
|
|||||||
- Try to receive an email from an external service
|
- Try to receive an email from an external service
|
||||||
- Check the logs (``docker-compose logs -f servicenamehere``) to look for
|
- Check the logs (``docker-compose logs -f servicenamehere``) to look for
|
||||||
warnings or errors
|
warnings or errors
|
||||||
- Use an open relay checker like `mailradar`_
|
- Use an open relay checker like `mxtoolbox`_
|
||||||
to ensure you're not contributing to the spam problem on the internet.
|
to ensure you're not contributing to the spam problem on the internet.
|
||||||
All tests there should result in "Relay denied".
|
|
||||||
- If using DMARC, be sure to check the reports you get to verify that legitimate
|
- If using DMARC, be sure to check the reports you get to verify that legitimate
|
||||||
email is getting through and forgeries are being properly blocked.
|
email is getting through and forgeries are being properly blocked.
|
||||||
|
|
||||||
.. _mailradar: http://www.mailradar.com/openrelay/
|
.. _mxtoolbox: https://mxtoolbox.com/diagnostic.aspx
|
||||||
|
|||||||
364
docs/swarm/1.5/README.md
Normal file
364
docs/swarm/1.5/README.md
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
# Install Mailu on a docker swarm
|
||||||
|
|
||||||
|
## Prequisites
|
||||||
|
|
||||||
|
### Swarm
|
||||||
|
|
||||||
|
In order to deploy Mailu on a swarm, you will first need to initialize the swarm:
|
||||||
|
|
||||||
|
The main command will be:
|
||||||
|
```bash
|
||||||
|
docker swarm init --advertise-addr <IP_ADDR>
|
||||||
|
```
|
||||||
|
See https://docs.docker.com/engine/swarm/swarm-tutorial/create-swarm/
|
||||||
|
|
||||||
|
If you want to add other managers or workers, please use:
|
||||||
|
```bash
|
||||||
|
docker swarm join --token xxxxx
|
||||||
|
```
|
||||||
|
See https://docs.docker.com/engine/swarm/join-nodes/
|
||||||
|
|
||||||
|
You have now a working swarm, and you can check its status with:
|
||||||
|
```bash
|
||||||
|
core@coreos-01 ~/git/Mailu/docs/swarm/1.5 $ docker node ls
|
||||||
|
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
|
||||||
|
xhgeekkrlttpmtgmapt5hyxrb black-pearl Ready Active 18.06.0-ce
|
||||||
|
sczlqjgfhehsfdjhfhhph1nvb * coreos-01 Ready Active Leader 18.03.1-ce
|
||||||
|
mzrm9nbdggsfz4sgq6dhs5i6n flying-dutchman Ready Active 18.06.0-ce
|
||||||
|
```
|
||||||
|
|
||||||
|
### Volume definition
|
||||||
|
For data persistance (the Mailu services might be launched/relaunched on any of the swarm nodes), we need to have Mailu data stored in a manner accessible by every manager or worker in the swarm.
|
||||||
|
Hereafter we will use a NFS share:
|
||||||
|
```bash
|
||||||
|
core@coreos-01 ~ $ showmount -e 192.168.0.30
|
||||||
|
Export list for 192.168.0.30:
|
||||||
|
/mnt/Pool1/pv 192.168.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
on the nfs server, I am using the following /etc/exports
|
||||||
|
```bash
|
||||||
|
$more /etc/exports
|
||||||
|
/mnt/Pool1/pv -alldirs -mapall=root -network 192.168.0.0 -mask 255.255.255.0
|
||||||
|
```
|
||||||
|
on the nfs server, I created the Mailu directory (in fact I copied a working Mailu set-up)
|
||||||
|
```bash
|
||||||
|
$mkdir /mnt/Pool1/pv/mailu
|
||||||
|
```
|
||||||
|
|
||||||
|
On your manager node, mount the nfs share to check that the share is available:
|
||||||
|
```bash
|
||||||
|
core@coreos-01 ~ $ sudo mount -t nfs 192.168.0.30:/mnt/Pool1/pv/mailu /mnt/local/
|
||||||
|
```
|
||||||
|
If this is ok, you can umount it:
|
||||||
|
```bash
|
||||||
|
core@coreos-01 ~ $ sudo umount /mnt/local/
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Networking mode
|
||||||
|
On a swarm, the services are available (default mode) through a routing mesh managed by docker itself. With this mode, each service is given a virtual IP adress and docker manages the routing between this virtual IP and the container(s) provinding this service.
|
||||||
|
With this default networking mode, I cannot get login working properly... As found in https://github.com/Mailu/Mailu/issues/375 , a workaround is to use the dnsrr networking mode at least for the front services.
|
||||||
|
|
||||||
|
The main consequence/limitation will be that the front services will *not* be available on every node, but only on the node where it will be deployed. In my case, I have only one manager and I choose to deploy the front service to the manager node, so I know on wich IP the front service will be available (aka the IP adress of my manager node).
|
||||||
|
|
||||||
|
### Variable substitution and docker-compose.yml
|
||||||
|
The docker stack deploy command doesn't support variable substitution in the .yml file itself (but we still can use .env file to pass variables to the services). As a consequence we need to adjust the docker-compose file in order to :
|
||||||
|
- remove all variables : $VERSION , $BIND_ADDRESS4 , $BIND_ADDRESS6 , $ANTIVIRUS , $WEBMAIL , etc
|
||||||
|
- change the way we define the volumes (nfs share in our case)
|
||||||
|
- add a deploy section for every service
|
||||||
|
|
||||||
|
### Docker compose
|
||||||
|
An example of docker-compose-stack.yml file is available here:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
|
||||||
|
version: '3.2'
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
front:
|
||||||
|
image: mailu/nginx:1.5
|
||||||
|
env_file: .env
|
||||||
|
ports:
|
||||||
|
- target: 80
|
||||||
|
published: 80
|
||||||
|
mode: host
|
||||||
|
- target: 443
|
||||||
|
published: 443
|
||||||
|
mode: host
|
||||||
|
- target: 110
|
||||||
|
published: 110
|
||||||
|
mode: host
|
||||||
|
- target: 143
|
||||||
|
published: 143
|
||||||
|
mode: host
|
||||||
|
- target: 993
|
||||||
|
published: 993
|
||||||
|
mode: host
|
||||||
|
- target: 995
|
||||||
|
published: 995
|
||||||
|
mode: host
|
||||||
|
- target: 25
|
||||||
|
published: 25
|
||||||
|
mode: host
|
||||||
|
- target: 465
|
||||||
|
published: 465
|
||||||
|
mode: host
|
||||||
|
- target: 587
|
||||||
|
published: 587
|
||||||
|
mode: host
|
||||||
|
volumes:
|
||||||
|
# - "$ROOT/certs:/certs"
|
||||||
|
- type: volume
|
||||||
|
source: mailu_certs
|
||||||
|
target: /certs
|
||||||
|
deploy:
|
||||||
|
endpoint_mode: dnsrr
|
||||||
|
replicas: 1
|
||||||
|
placement:
|
||||||
|
constraints: [node.role == manager]
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:alpine
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
# - "$ROOT/redis:/data"
|
||||||
|
- type: volume
|
||||||
|
source: mailu_redis
|
||||||
|
target: /data
|
||||||
|
deploy:
|
||||||
|
endpoint_mode: dnsrr
|
||||||
|
replicas: 1
|
||||||
|
placement:
|
||||||
|
constraints: [node.role == manager]
|
||||||
|
|
||||||
|
imap:
|
||||||
|
image: mailu/dovecot:1.5
|
||||||
|
restart: always
|
||||||
|
env_file: .env
|
||||||
|
volumes:
|
||||||
|
# - "$ROOT/data:/data"
|
||||||
|
- type: volume
|
||||||
|
source: mailu_data
|
||||||
|
target: /data
|
||||||
|
# - "$ROOT/mail:/mail"
|
||||||
|
- type: volume
|
||||||
|
source: mailu_mail
|
||||||
|
target: /mail
|
||||||
|
# - "$ROOT/overrides:/overrides"
|
||||||
|
- type: volume
|
||||||
|
source: mailu_overrides
|
||||||
|
target: /overrides
|
||||||
|
depends_on:
|
||||||
|
- front
|
||||||
|
deploy:
|
||||||
|
endpoint_mode: dnsrr
|
||||||
|
replicas: 1
|
||||||
|
placement:
|
||||||
|
constraints: [node.role == manager]
|
||||||
|
|
||||||
|
smtp:
|
||||||
|
image: mailu/postfix:1.5
|
||||||
|
restart: always
|
||||||
|
env_file: .env
|
||||||
|
volumes:
|
||||||
|
# - "$ROOT/data:/data"
|
||||||
|
- type: volume
|
||||||
|
source: mailu_data
|
||||||
|
target: /data
|
||||||
|
# - "$ROOT/overrides:/overrides"
|
||||||
|
- type: volume
|
||||||
|
source: mailu_overrides
|
||||||
|
target: /overrides
|
||||||
|
depends_on:
|
||||||
|
- front
|
||||||
|
deploy:
|
||||||
|
endpoint_mode: dnsrr
|
||||||
|
replicas: 1
|
||||||
|
placement:
|
||||||
|
constraints: [node.role == manager]
|
||||||
|
|
||||||
|
antispam:
|
||||||
|
image: mailu/rspamd:1.5
|
||||||
|
restart: always
|
||||||
|
env_file: .env
|
||||||
|
depends_on:
|
||||||
|
- front
|
||||||
|
volumes:
|
||||||
|
# - "$ROOT/filter:/var/lib/rspamd"
|
||||||
|
- type: volume
|
||||||
|
source: mailu_filter
|
||||||
|
target: /var/lib/rspamd
|
||||||
|
# - "$ROOT/dkim:/dkim"
|
||||||
|
- type: volume
|
||||||
|
source: mailu_dkim
|
||||||
|
target: /dkim
|
||||||
|
# - "$ROOT/overrides/rspamd:/etc/rspamd/override.d"
|
||||||
|
- type: volume
|
||||||
|
source: mailu_overrides_rspamd
|
||||||
|
target: /etc/rspamd/override.d
|
||||||
|
deploy:
|
||||||
|
endpoint_mode: dnsrr
|
||||||
|
replicas: 1
|
||||||
|
placement:
|
||||||
|
constraints: [node.role == manager]
|
||||||
|
|
||||||
|
antivirus:
|
||||||
|
image: mailu/none:1.5
|
||||||
|
restart: always
|
||||||
|
env_file: .env
|
||||||
|
volumes:
|
||||||
|
# - "$ROOT/filter:/data"
|
||||||
|
- type: volume
|
||||||
|
source: mailu_filter
|
||||||
|
target: /data
|
||||||
|
deploy:
|
||||||
|
endpoint_mode: dnsrr
|
||||||
|
replicas: 1
|
||||||
|
placement:
|
||||||
|
constraints: [node.role == manager]
|
||||||
|
|
||||||
|
webdav:
|
||||||
|
image: mailu/none:1.5
|
||||||
|
restart: always
|
||||||
|
env_file: .env
|
||||||
|
volumes:
|
||||||
|
# - "$ROOT/dav:/data"
|
||||||
|
- type: volume
|
||||||
|
source: mailu_dav
|
||||||
|
target: /data
|
||||||
|
deploy:
|
||||||
|
endpoint_mode: dnsrr
|
||||||
|
replicas: 1
|
||||||
|
placement:
|
||||||
|
constraints: [node.role == manager]
|
||||||
|
|
||||||
|
admin:
|
||||||
|
image: mailu/admin:1.5
|
||||||
|
restart: always
|
||||||
|
env_file: .env
|
||||||
|
volumes:
|
||||||
|
# - "$ROOT/data:/data"
|
||||||
|
- type: volume
|
||||||
|
source: mailu_data
|
||||||
|
target: /data
|
||||||
|
# - "$ROOT/dkim:/dkim"
|
||||||
|
- type: volume
|
||||||
|
source: mailu_dkim
|
||||||
|
target: /dkim
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
deploy:
|
||||||
|
endpoint_mode: dnsrr
|
||||||
|
replicas: 1
|
||||||
|
placement:
|
||||||
|
constraints: [node.role == manager]
|
||||||
|
|
||||||
|
webmail:
|
||||||
|
image: "mailu/roundcube:1.5"
|
||||||
|
restart: always
|
||||||
|
env_file: .env
|
||||||
|
volumes:
|
||||||
|
# - "$ROOT/webmail:/data"
|
||||||
|
- type: volume
|
||||||
|
source: mailu_data
|
||||||
|
target: /data
|
||||||
|
depends_on:
|
||||||
|
- imap
|
||||||
|
deploy:
|
||||||
|
endpoint_mode: dnsrr
|
||||||
|
replicas: 1
|
||||||
|
placement:
|
||||||
|
constraints: [node.role == manager]
|
||||||
|
|
||||||
|
fetchmail:
|
||||||
|
image: mailu/fetchmail:1.5
|
||||||
|
restart: always
|
||||||
|
env_file: .env
|
||||||
|
volumes:
|
||||||
|
# - "$ROOT/data:/data"
|
||||||
|
- type: volume
|
||||||
|
source: mailu_data
|
||||||
|
target: /data
|
||||||
|
logging:
|
||||||
|
driver: none
|
||||||
|
deploy:
|
||||||
|
endpoint_mode: dnsrr
|
||||||
|
replicas: 1
|
||||||
|
placement:
|
||||||
|
constraints: [node.role == manager]
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mailu_filter:
|
||||||
|
driver_opts:
|
||||||
|
type: "nfs"
|
||||||
|
o: "addr=192.168.0.30,nolock,soft,rw"
|
||||||
|
device: ":/mnt/Pool1/pv/mailu/filter"
|
||||||
|
mailu_dkim:
|
||||||
|
driver_opts:
|
||||||
|
type: "nfs"
|
||||||
|
o: "addr=192.168.0.30,nolock,soft,rw"
|
||||||
|
device: ":/mnt/Pool1/pv/mailu/dkim"
|
||||||
|
mailu_overrides_rspamd:
|
||||||
|
driver_opts:
|
||||||
|
type: "nfs"
|
||||||
|
o: "addr=192.168.0.30,nolock,soft,rw"
|
||||||
|
device: ":/mnt/Pool1/pv/mailu/overrides/rspamd"
|
||||||
|
mailu_data:
|
||||||
|
driver_opts:
|
||||||
|
type: "nfs"
|
||||||
|
o: "addr=192.168.0.30,nolock,soft,rw"
|
||||||
|
device: ":/mnt/Pool1/pv/mailu/data"
|
||||||
|
mailu_mail:
|
||||||
|
driver_opts:
|
||||||
|
type: "nfs"
|
||||||
|
o: "addr=192.168.0.30,nolock,soft,rw"
|
||||||
|
device: ":/mnt/Pool1/pv/mailu/mail"
|
||||||
|
mailu_overrides:
|
||||||
|
driver_opts:
|
||||||
|
type: "nfs"
|
||||||
|
o: "addr=192.168.0.30,nolock,soft,rw"
|
||||||
|
device: ":/mnt/Pool1/pv/mailu/overrides"
|
||||||
|
mailu_dav:
|
||||||
|
driver_opts:
|
||||||
|
type: "nfs"
|
||||||
|
o: "addr=192.168.0.30,nolock,soft,rw"
|
||||||
|
device: ":/mnt/Pool1/pv/mailu/dav"
|
||||||
|
mailu_certs:
|
||||||
|
driver_opts:
|
||||||
|
type: "nfs"
|
||||||
|
o: "addr=192.168.0.30,nolock,soft,rw"
|
||||||
|
device: ":/mnt/Pool1/pv/mailu/certs"
|
||||||
|
mailu_redis:
|
||||||
|
driver_opts:
|
||||||
|
type: "nfs"
|
||||||
|
o: "addr=192.168.0.30,nolock,soft,rw"
|
||||||
|
device: ":/mnt/Pool1/pv/mailu/redis"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deploy Mailu on the docker swarm
|
||||||
|
Run the following command:
|
||||||
|
```bash
|
||||||
|
docker stack deploy -c docker-compose-stack.yml mailu
|
||||||
|
```
|
||||||
|
See how the services are being deployed:
|
||||||
|
```bash
|
||||||
|
core@coreos-01 ~ $ docker service ls
|
||||||
|
ID NAME MODE REPLICAS IMAGE PORTS
|
||||||
|
ywnsetmtkb1l mailu_antivirus replicated 1/1 mailu/none:1.5
|
||||||
|
pqokiaz0q128 mailu_fetchmail replicated 1/1 mailu/fetchmail:1.5
|
||||||
|
```
|
||||||
|
check a specific service:
|
||||||
|
```bash
|
||||||
|
core@coreos-01 ~ $ docker service ps mailu_fetchmail
|
||||||
|
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
|
||||||
|
tbu8ppgsdffj mailu_fetchmail.1 mailu/fetchmail:1.5 coreos-01 Running Running 11 days ago
|
||||||
|
```
|
||||||
|
|
||||||
|
### Remove the stack
|
||||||
|
Run the follwoing command:
|
||||||
|
```bash
|
||||||
|
core@coreos-01 ~ $ docker stack rm mailu
|
||||||
|
```
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM alpine:edge
|
FROM alpine:3.8
|
||||||
|
|
||||||
RUN apk add --no-cache clamav rsyslog wget clamav-libunrar
|
RUN apk add --no-cache clamav rsyslog wget clamav-libunrar
|
||||||
|
|
||||||
@@ -6,5 +6,6 @@ COPY conf /etc/clamav
|
|||||||
COPY start.sh /start.sh
|
COPY start.sh /start.sh
|
||||||
|
|
||||||
EXPOSE 3310/tcp
|
EXPOSE 3310/tcp
|
||||||
|
VOLUME ["/data"]
|
||||||
|
|
||||||
CMD ["/start.sh"]
|
CMD ["/start.sh"]
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ RUN echo "@testing http://nl.alpinelinux.org/alpine/edge/testing" >> /etc/apk/re
|
|||||||
COPY radicale.conf /radicale.conf
|
COPY radicale.conf /radicale.conf
|
||||||
|
|
||||||
EXPOSE 5232/tcp
|
EXPOSE 5232/tcp
|
||||||
|
VOLUME ["/data"]
|
||||||
|
|
||||||
CMD radicale -f -S -C /radicale.conf
|
CMD radicale -f -S -C /radicale.conf
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
FROM python:alpine
|
FROM python:3-alpine
|
||||||
|
|
||||||
RUN apk add --no-cache fetchmail ca-certificates
|
RUN apk add --no-cache fetchmail ca-certificates \
|
||||||
|
&& pip install requests
|
||||||
|
|
||||||
COPY fetchmail.py /fetchmail.py
|
COPY fetchmail.py /fetchmail.py
|
||||||
|
USER fetchmail
|
||||||
|
|
||||||
CMD ["/fetchmail.py"]
|
CMD ["/fetchmail.py"]
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
import sqlite3
|
|
||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
import re
|
import re
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
FETCHMAIL = """
|
FETCHMAIL = """
|
||||||
@@ -15,6 +15,7 @@ fetchmail -N \
|
|||||||
-f {}
|
-f {}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
RC_LINE = """
|
RC_LINE = """
|
||||||
poll "{host}" proto {protocol} port {port}
|
poll "{host}" proto {protocol} port {port}
|
||||||
user "{username}" password "{password}"
|
user "{username}" password "{password}"
|
||||||
@@ -24,10 +25,12 @@ poll "{host}" proto {protocol} port {port}
|
|||||||
sslproto 'AUTO'
|
sslproto 'AUTO'
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def extract_host_port(host_and_port, default_port):
|
def extract_host_port(host_and_port, default_port):
|
||||||
host, _, port = re.match('^(.*)(:([0-9]*))?$', host_and_port).groups()
|
host, _, port = re.match('^(.*)(:([0-9]*))?$', host_and_port).groups()
|
||||||
return host, int(port) if port else default_port
|
return host, int(port) if port else default_port
|
||||||
|
|
||||||
|
|
||||||
def escape_rc_string(arg):
|
def escape_rc_string(arg):
|
||||||
return arg.replace("\\", "\\\\").replace('"', '\\"')
|
return arg.replace("\\", "\\\\").replace('"', '\\"')
|
||||||
|
|
||||||
@@ -41,30 +44,26 @@ def fetchmail(fetchmailrc):
|
|||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
||||||
def run(connection, cursor, debug):
|
def run(debug):
|
||||||
cursor.execute("""
|
fetches = requests.get("http://admin/internal/fetch").json()
|
||||||
SELECT user_email, protocol, host, port, tls, username, password, keep
|
|
||||||
FROM fetch
|
|
||||||
""")
|
|
||||||
smtphost, smtpport = extract_host_port(os.environ.get("HOST_SMTP", "smtp"), None)
|
smtphost, smtpport = extract_host_port(os.environ.get("HOST_SMTP", "smtp"), None)
|
||||||
if smtpport is None:
|
if smtpport is None:
|
||||||
smtphostport = smtphost
|
smtphostport = smtphost
|
||||||
else:
|
else:
|
||||||
smtphostport = "%s/%d" % (smtphost, smtpport)
|
smtphostport = "%s/%d" % (smtphost, smtpport)
|
||||||
for line in cursor.fetchall():
|
for fetch in fetches:
|
||||||
fetchmailrc = ""
|
fetchmailrc = ""
|
||||||
user_email, protocol, host, port, tls, username, password, keep = line
|
|
||||||
options = "options antispam 501, 504, 550, 553, 554"
|
options = "options antispam 501, 504, 550, 553, 554"
|
||||||
options += " ssl" if tls else ""
|
options += " ssl" if fetch["tls"] else ""
|
||||||
options += " keep" if keep else " fetchall"
|
options += " keep" if fetch["keep"] else " fetchall"
|
||||||
fetchmailrc += RC_LINE.format(
|
fetchmailrc += RC_LINE.format(
|
||||||
user_email=escape_rc_string(user_email),
|
user_email=escape_rc_string(fetch["user_email"]),
|
||||||
protocol=protocol,
|
protocol=fetch["protocol"],
|
||||||
host=escape_rc_string(host),
|
host=escape_rc_string(fetch["host"]),
|
||||||
port=port,
|
port=fetch["port"],
|
||||||
smtphost=smtphostport,
|
smtphost=smtphostport,
|
||||||
username=escape_rc_string(username),
|
username=escape_rc_string(fetch["username"]),
|
||||||
password=escape_rc_string(password),
|
password=escape_rc_string(fetch["password"]),
|
||||||
options=options
|
options=options
|
||||||
)
|
)
|
||||||
if debug:
|
if debug:
|
||||||
@@ -77,26 +76,20 @@ def run(connection, cursor, debug):
|
|||||||
# No mail is not an error
|
# No mail is not an error
|
||||||
if not error_message.startswith("fetchmail: No mail"):
|
if not error_message.startswith("fetchmail: No mail"):
|
||||||
print(error_message)
|
print(error_message)
|
||||||
user_info = "for %s at %s" % (user_email, host)
|
user_info = "for %s at %s" % (fetch["user_email"], fetch["host"])
|
||||||
# Number of messages seen is not a error as well
|
# Number of messages seen is not a error as well
|
||||||
if ("messages" in error_message and
|
if ("messages" in error_message and
|
||||||
"(seen " in error_message and
|
"(seen " in error_message and
|
||||||
user_info in error_message):
|
user_info in error_message):
|
||||||
print(error_message)
|
print(error_message)
|
||||||
finally:
|
finally:
|
||||||
cursor.execute("""
|
requests.post("http://admin/internal/fetch/{}".format(fetch["id"]),
|
||||||
UPDATE fetch SET error=?, last_check=datetime('now')
|
json=error_message.split("\n")[0]
|
||||||
WHERE user_email=?
|
)
|
||||||
""", (error_message.split("\n")[0], user_email))
|
|
||||||
connection.commit()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
debug = os.environ.get("DEBUG", None) == "True"
|
|
||||||
db_path = os.environ.get("DB_PATH", "/data/main.db")
|
|
||||||
connection = sqlite3.connect(db_path)
|
|
||||||
while True:
|
while True:
|
||||||
cursor = connection.cursor()
|
|
||||||
run(connection, cursor, debug)
|
|
||||||
cursor.close()
|
|
||||||
time.sleep(int(os.environ.get("FETCHMAIL_DELAY", 60)))
|
time.sleep(int(os.environ.get("FETCHMAIL_DELAY", 60)))
|
||||||
|
run(os.environ.get("DEBUG", None) == "True")
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
FROM alpine:edge
|
FROM alpine:3.8
|
||||||
|
|
||||||
RUN apk add --no-cache python py-jinja2 rspamd rspamd-controller rspamd-proxy ca-certificates
|
RUN apk add --no-cache python py-jinja2 rspamd rspamd-controller rspamd-proxy ca-certificates py-pip \
|
||||||
|
&& pip install --upgrade pip \
|
||||||
|
&& pip install tenacity
|
||||||
|
|
||||||
RUN mkdir /run/rspamd
|
RUN mkdir /run/rspamd
|
||||||
|
|
||||||
@@ -12,4 +14,6 @@ RUN sed -i '/fuzzy/,$d' /etc/rspamd/rspamd.conf
|
|||||||
|
|
||||||
EXPOSE 11332/tcp 11334/tcp
|
EXPOSE 11332/tcp 11334/tcp
|
||||||
|
|
||||||
|
VOLUME ["/var/lib/rspamd"]
|
||||||
|
|
||||||
CMD /start.py
|
CMD /start.py
|
||||||
|
|||||||
4
services/rspamd/conf/arc.conf
Normal file
4
services/rspamd/conf/arc.conf
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
try_fallback = true;
|
||||||
|
path = "/dkim/$domain.$selector.key";
|
||||||
|
selector = "dkim"
|
||||||
|
use_esld = false;
|
||||||
@@ -4,11 +4,17 @@ import jinja2
|
|||||||
import os
|
import os
|
||||||
import socket
|
import socket
|
||||||
import glob
|
import glob
|
||||||
|
import tenacity
|
||||||
|
from tenacity import retry
|
||||||
|
|
||||||
convert = lambda src, dst: open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ))
|
convert = lambda src, dst: open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ))
|
||||||
|
|
||||||
|
@retry(stop=tenacity.stop_after_attempt(100), wait=tenacity.wait_random(min=2, max=5))
|
||||||
|
def resolve():
|
||||||
|
os.environ["FRONT_ADDRESS"] = socket.gethostbyname(os.environ.get("FRONT_ADDRESS", "front"))
|
||||||
|
|
||||||
# Actual startup script
|
# Actual startup script
|
||||||
os.environ["FRONT_ADDRESS"] = socket.gethostbyname(os.environ.get("FRONT_ADDRESS", "front"))
|
resolve()
|
||||||
if "HOST_REDIS" not in os.environ: os.environ["HOST_REDIS"] = "redis"
|
if "HOST_REDIS" not in os.environ: os.environ["HOST_REDIS"] = "redis"
|
||||||
|
|
||||||
for rspamd_file in glob.glob("/conf/*"):
|
for rspamd_file in glob.glob("/conf/*"):
|
||||||
|
|||||||
@@ -15,4 +15,4 @@ RUN python setup.py https://github.com/mailu/mailu /data
|
|||||||
|
|
||||||
EXPOSE 80/tcp
|
EXPOSE 80/tcp
|
||||||
|
|
||||||
CMD gunicorn -w 4 -b 0.0.0.0:80 -b [::]:80 --access-logfile - --error-logfile - --preload main:app
|
CMD gunicorn -w 4 -b :80 --access-logfile - --error-logfile - --preload main:app
|
||||||
|
|||||||
13
setup/docker-compose.yml
Normal file
13
setup/docker-compose.yml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# This file is used to run the mailu/setup utility
|
||||||
|
|
||||||
|
version: '2'
|
||||||
|
|
||||||
|
services:
|
||||||
|
redis:
|
||||||
|
image: redis:alpine
|
||||||
|
|
||||||
|
setup:
|
||||||
|
image: mailu/setup
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
|
||||||
56
tests/build.yml
Normal file
56
tests/build.yml
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
front:
|
||||||
|
image: ${DOCKER_ORG:-mailu}/nginx:${VERSION:-local}
|
||||||
|
build: ../core/nginx
|
||||||
|
|
||||||
|
imap:
|
||||||
|
image: ${DOCKER_ORG:-mailu}/dovecot:${VERSION:-local}
|
||||||
|
build: ../core/dovecot
|
||||||
|
|
||||||
|
smtp:
|
||||||
|
image: ${DOCKER_ORG:-mailu}/postfix:${VERSION:-local}
|
||||||
|
build: ../core/postfix
|
||||||
|
|
||||||
|
antispam:
|
||||||
|
image: ${DOCKER_ORG:-mailu}/rspamd:${VERSION:-local}
|
||||||
|
build: ../services/rspamd
|
||||||
|
|
||||||
|
antivirus:
|
||||||
|
image: ${DOCKER_ORG:-mailu}/clamav:${VERSION:-local}
|
||||||
|
build: ../optional/clamav
|
||||||
|
|
||||||
|
webdav:
|
||||||
|
image: ${DOCKER_ORG:-mailu}/radicale:${VERSION:-local}
|
||||||
|
build: ../optional/radicale
|
||||||
|
|
||||||
|
admin:
|
||||||
|
image: ${DOCKER_ORG:-mailu}/admin:${VERSION:-local}
|
||||||
|
build: ../core/admin
|
||||||
|
|
||||||
|
roundcube:
|
||||||
|
image: ${DOCKER_ORG:-mailu}/roundcube:${VERSION:-local}
|
||||||
|
build: ../webmails/roundcube
|
||||||
|
|
||||||
|
rainloop:
|
||||||
|
image: ${DOCKER_ORG:-mailu}/rainloop:${VERSION:-local}
|
||||||
|
build: ../webmails/rainloop
|
||||||
|
|
||||||
|
fetchmail:
|
||||||
|
image: ${DOCKER_ORG:-mailu}/fetchmail:${VERSION:-local}
|
||||||
|
build: ../services/fetchmail
|
||||||
|
|
||||||
|
none:
|
||||||
|
image: ${DOCKER_ORG:-mailu}/none:${VERSION:-local}
|
||||||
|
build: ../core/none
|
||||||
|
|
||||||
|
docs:
|
||||||
|
image: ${DOCKER_ORG:-mailu}/docs:${VERSION:-local}
|
||||||
|
build: ../docs
|
||||||
|
|
||||||
|
setup:
|
||||||
|
image: ${DOCKER_ORG:-mailu}/setup:${VERSION:-local}
|
||||||
|
build: ../setup
|
||||||
|
|
||||||
140
tests/compose/core.env
Normal file
140
tests/compose/core.env
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
# Mailu main configuration file
|
||||||
|
#
|
||||||
|
# Most configuration variables can be modified through the Web interface,
|
||||||
|
# these few settings must however be configured before starting the mail
|
||||||
|
# server and require a restart upon change.
|
||||||
|
|
||||||
|
###################################
|
||||||
|
# Common configuration variables
|
||||||
|
###################################
|
||||||
|
|
||||||
|
# Set this to the path where Mailu data and configuration is stored
|
||||||
|
ROOT=/mailu
|
||||||
|
|
||||||
|
# Mailu version to run (1.0, 1.1, etc. or master)
|
||||||
|
#VERSION=master
|
||||||
|
|
||||||
|
# Set to a randomly generated 16 bytes string
|
||||||
|
SECRET_KEY=ChangeMeChangeMe
|
||||||
|
|
||||||
|
# Address where listening ports should bind
|
||||||
|
BIND_ADDRESS4=127.0.0.1
|
||||||
|
#BIND_ADDRESS6=::1
|
||||||
|
|
||||||
|
# Main mail domain
|
||||||
|
DOMAIN=mailu.io
|
||||||
|
|
||||||
|
# Hostnames for this server, separated with comas
|
||||||
|
HOSTNAMES=mail.mailu.io,alternative.mailu.io,yetanother.mailu.io
|
||||||
|
|
||||||
|
# Postmaster local part (will append the main mail domain)
|
||||||
|
POSTMASTER=admin
|
||||||
|
|
||||||
|
# Choose how secure connections will behave (value: letsencrypt, cert, notls, mail, mail-letsencrypt)
|
||||||
|
TLS_FLAVOR=cert
|
||||||
|
|
||||||
|
# Authentication rate limit (per source IP address)
|
||||||
|
AUTH_RATELIMIT=10/minute;1000/hour
|
||||||
|
|
||||||
|
# Opt-out of statistics, replace with "True" to opt out
|
||||||
|
DISABLE_STATISTICS=False
|
||||||
|
|
||||||
|
###################################
|
||||||
|
# Optional features
|
||||||
|
###################################
|
||||||
|
|
||||||
|
# Expose the admin interface (value: true, false)
|
||||||
|
ADMIN=false
|
||||||
|
|
||||||
|
# Choose which webmail to run if any (values: roundcube, rainloop, none)
|
||||||
|
WEBMAIL=none
|
||||||
|
|
||||||
|
# Dav server implementation (value: radicale, none)
|
||||||
|
WEBDAV=none
|
||||||
|
|
||||||
|
# Antivirus solution (value: clamav, none)
|
||||||
|
ANTIVIRUS=none
|
||||||
|
|
||||||
|
###################################
|
||||||
|
# Mail settings
|
||||||
|
###################################
|
||||||
|
|
||||||
|
# Message size limit in bytes
|
||||||
|
# Default: accept messages up to 50MB
|
||||||
|
MESSAGE_SIZE_LIMIT=50000000
|
||||||
|
|
||||||
|
# Networks granted relay permissions, make sure that you include your Docker
|
||||||
|
# internal network (default to 172.17.0.0/16)
|
||||||
|
RELAYNETS=172.16.0.0/12
|
||||||
|
|
||||||
|
# Will relay all outgoing mails if configured
|
||||||
|
RELAYHOST=
|
||||||
|
|
||||||
|
# Fetchmail delay
|
||||||
|
FETCHMAIL_DELAY=600
|
||||||
|
|
||||||
|
# Recipient delimiter, character used to delimiter localpart from custom address part
|
||||||
|
# e.g. localpart+custom@domain;tld
|
||||||
|
RECIPIENT_DELIMITER=+
|
||||||
|
|
||||||
|
# DMARC rua and ruf email
|
||||||
|
DMARC_RUA=admin
|
||||||
|
DMARC_RUF=admin
|
||||||
|
|
||||||
|
# Welcome email, enable and set a topic and body if you wish to send welcome
|
||||||
|
# emails to all users.
|
||||||
|
WELCOME=false
|
||||||
|
WELCOME_SUBJECT=Welcome to your new email account
|
||||||
|
WELCOME_BODY=Welcome to your new email account, if you can read this, then it is configured properly!
|
||||||
|
|
||||||
|
# Maildir Compression
|
||||||
|
# choose compression-method, default: none (value: bz2, gz)
|
||||||
|
COMPRESSION=
|
||||||
|
# change compression-level, default: 6 (value: 1-9)
|
||||||
|
COMPRESSION_LEVEL=
|
||||||
|
|
||||||
|
###################################
|
||||||
|
# Web settings
|
||||||
|
###################################
|
||||||
|
|
||||||
|
# Path to the admin interface if enabled
|
||||||
|
WEB_ADMIN=/admin
|
||||||
|
|
||||||
|
# Path to the webmail if enabled
|
||||||
|
WEB_WEBMAIL=/webmail
|
||||||
|
|
||||||
|
# Website name
|
||||||
|
SITENAME=Mailu
|
||||||
|
|
||||||
|
# Linked Website URL
|
||||||
|
WEBSITE=https://mailu.io
|
||||||
|
|
||||||
|
# Registration reCaptcha settings (warning, this has some privacy impact)
|
||||||
|
# RECAPTCHA_PUBLIC_KEY=
|
||||||
|
# RECAPTCHA_PRIVATE_KEY=
|
||||||
|
|
||||||
|
# Domain registration, uncomment to enable
|
||||||
|
# DOMAIN_REGISTRATION=true
|
||||||
|
|
||||||
|
###################################
|
||||||
|
# Advanced settings
|
||||||
|
###################################
|
||||||
|
|
||||||
|
# Log driver for front service. Possible values:
|
||||||
|
# json-file (default)
|
||||||
|
# journald (On systemd platforms, useful for Fail2Ban integration)
|
||||||
|
# syslog (Non systemd platforms, Fail2Ban integration. Disables `docker-compose log` for front!)
|
||||||
|
LOG_DRIVER=json-file
|
||||||
|
|
||||||
|
# Docker-compose project name, this will prepended to containers names.
|
||||||
|
#COMPOSE_PROJECT_NAME=mailu
|
||||||
|
|
||||||
|
# Default password scheme used for newly created accounts and changed passwords
|
||||||
|
# (value: SHA512-CRYPT, SHA256-CRYPT, MD5-CRYPT, CRYPT)
|
||||||
|
PASSWORD_SCHEME=SHA512-CRYPT
|
||||||
|
|
||||||
|
# Header to take the real ip from
|
||||||
|
REAL_IP_HEADER=
|
||||||
|
|
||||||
|
# IPs for nginx set_real_ip_from (CIDR list separated by commas)
|
||||||
|
REAL_IP_FROM=
|
||||||
101
tests/compose/run.yml
Normal file
101
tests/compose/run.yml
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
version: '2'
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
front:
|
||||||
|
image: $DOCKER_ORG/nginx:$VERSION
|
||||||
|
restart: 'no'
|
||||||
|
env_file: $PWD/.env
|
||||||
|
logging:
|
||||||
|
driver: $LOG_DRIVER
|
||||||
|
ports:
|
||||||
|
- "$BIND_ADDRESS4:80:80"
|
||||||
|
- "$BIND_ADDRESS4:443:443"
|
||||||
|
- "$BIND_ADDRESS4:110:110"
|
||||||
|
- "$BIND_ADDRESS4:143:143"
|
||||||
|
- "$BIND_ADDRESS4:993:993"
|
||||||
|
- "$BIND_ADDRESS4:995:995"
|
||||||
|
- "$BIND_ADDRESS4:25:25"
|
||||||
|
- "$BIND_ADDRESS4:465:465"
|
||||||
|
- "$BIND_ADDRESS4:587:587"
|
||||||
|
volumes:
|
||||||
|
- "$ROOT/certs:/certs"
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:alpine
|
||||||
|
restart: 'no'
|
||||||
|
volumes:
|
||||||
|
- "$ROOT/redis:/data"
|
||||||
|
|
||||||
|
imap:
|
||||||
|
image: $DOCKER_ORG/dovecot:$VERSION
|
||||||
|
restart: 'no'
|
||||||
|
env_file: $PWD/.env
|
||||||
|
volumes:
|
||||||
|
- "$ROOT/data:/data"
|
||||||
|
- "$ROOT/mail:/mail"
|
||||||
|
- "$ROOT/overrides:/overrides"
|
||||||
|
depends_on:
|
||||||
|
- front
|
||||||
|
|
||||||
|
smtp:
|
||||||
|
image: $DOCKER_ORG/postfix:$VERSION
|
||||||
|
restart: 'no'
|
||||||
|
env_file: $PWD/.env
|
||||||
|
volumes:
|
||||||
|
- "$ROOT/data:/data"
|
||||||
|
- "$ROOT/overrides:/overrides"
|
||||||
|
depends_on:
|
||||||
|
- front
|
||||||
|
|
||||||
|
antispam:
|
||||||
|
image: $DOCKER_ORG/rspamd:$VERSION
|
||||||
|
restart: 'no'
|
||||||
|
env_file: $PWD/.env
|
||||||
|
volumes:
|
||||||
|
- "$ROOT/filter:/var/lib/rspamd"
|
||||||
|
- "$ROOT/dkim:/dkim"
|
||||||
|
- "$ROOT/overrides/rspamd:/etc/rspamd/override.d"
|
||||||
|
depends_on:
|
||||||
|
- front
|
||||||
|
|
||||||
|
antivirus:
|
||||||
|
image: $DOCKER_ORG/$ANTIVIRUS:$VERSION
|
||||||
|
restart: 'no'
|
||||||
|
env_file: $PWD/.env
|
||||||
|
volumes:
|
||||||
|
- "$ROOT/filter:/data"
|
||||||
|
|
||||||
|
webdav:
|
||||||
|
image: $DOCKER_ORG/$WEBDAV:$VERSION
|
||||||
|
restart: 'no'
|
||||||
|
env_file: $PWD/.env
|
||||||
|
volumes:
|
||||||
|
- "$ROOT/dav:/data"
|
||||||
|
|
||||||
|
admin:
|
||||||
|
image: $DOCKER_ORG/admin:$VERSION
|
||||||
|
restart: 'no'
|
||||||
|
env_file: $PWD/.env
|
||||||
|
volumes:
|
||||||
|
- "$ROOT/data:/data"
|
||||||
|
- "$ROOT/dkim:/dkim"
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
|
||||||
|
webmail:
|
||||||
|
image: "$DOCKER_ORG/$WEBMAIL:$VERSION"
|
||||||
|
restart: 'no'
|
||||||
|
env_file: $PWD/.env
|
||||||
|
volumes:
|
||||||
|
- "$ROOT/webmail:/data"
|
||||||
|
depends_on:
|
||||||
|
- imap
|
||||||
|
|
||||||
|
fetchmail:
|
||||||
|
image: $DOCKER_ORG/fetchmail:$VERSION
|
||||||
|
restart: 'no'
|
||||||
|
env_file: $PWD/.env
|
||||||
|
volumes:
|
||||||
|
- "$ROOT/data:/data"
|
||||||
57
tests/compose/test-script.sh
Executable file
57
tests/compose/test-script.sh
Executable file
@@ -0,0 +1,57 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
containers=(
|
||||||
|
webmail
|
||||||
|
imap
|
||||||
|
smtp
|
||||||
|
antispam
|
||||||
|
admin
|
||||||
|
redis
|
||||||
|
antivirus
|
||||||
|
webdav
|
||||||
|
# fetchmail
|
||||||
|
front
|
||||||
|
)
|
||||||
|
|
||||||
|
# Time to sleep in minutes after starting the containers
|
||||||
|
WAIT=1
|
||||||
|
|
||||||
|
containers_check() {
|
||||||
|
status=0
|
||||||
|
for container in "${containers[@]}"; do
|
||||||
|
name="${DOCKER_ORG}_${container}_1"
|
||||||
|
echo "Checking $name"
|
||||||
|
docker inspect "$name" | grep '"Status": "running"' || status=1
|
||||||
|
done
|
||||||
|
docker ps -a
|
||||||
|
return $status
|
||||||
|
}
|
||||||
|
|
||||||
|
container_logs() {
|
||||||
|
for container in "${containers[@]}"; do
|
||||||
|
name="${DOCKER_ORG}_${container}_1"
|
||||||
|
echo "Showing logs for $name"
|
||||||
|
docker container logs "$name"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
clean() {
|
||||||
|
docker-compose -f tests/compose/run.yml -p $DOCKER_ORG down || exit 1
|
||||||
|
rm -fv .env
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cleanup before callig exit
|
||||||
|
die() {
|
||||||
|
clean
|
||||||
|
exit $1
|
||||||
|
}
|
||||||
|
|
||||||
|
for file in tests/compose/*.env ; do
|
||||||
|
cp $file .env
|
||||||
|
docker-compose -f tests/compose/run.yml -p $DOCKER_ORG up -d
|
||||||
|
echo -e "\nSleeping for ${WAIT} minutes" # Clean terminal distortion from docker-compose in travis
|
||||||
|
travis_wait sleep ${WAIT}m || sleep ${WAIT}m #Fallback sleep for local run
|
||||||
|
container_logs
|
||||||
|
containers_check || die 1
|
||||||
|
clean
|
||||||
|
done
|
||||||
|
|
||||||
4
tests/deploy.sh
Executable file
4
tests/deploy.sh
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
docker login -u $DOCKER_UN -p $DOCKER_PW
|
||||||
|
docker-compose -f tests/build.yml push
|
||||||
17
tests/smtp.py
Normal file
17
tests/smtp.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import smtplib
|
||||||
|
import sys
|
||||||
|
from email import mime
|
||||||
|
|
||||||
|
from email.mime.image import MIMEImage
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
|
||||||
|
msg = mime.multipart.MIMEMultipart()
|
||||||
|
msg['Subject'] = 'Test email'
|
||||||
|
msg['From'] = sys.argv[1]
|
||||||
|
msg['To'] = sys.argv[2]
|
||||||
|
msg.preamble = 'Test email'
|
||||||
|
|
||||||
|
s = smtplib.SMTP('localhost')
|
||||||
|
s.set_debuglevel(1)
|
||||||
|
s.send_message(msg)
|
||||||
|
s.quit()
|
||||||
@@ -1,20 +1,21 @@
|
|||||||
FROM php:5-apache
|
FROM php:7.2-apache
|
||||||
|
|
||||||
|
ENV RAINLOOP_URL https://github.com/RainLoop/rainloop-webmail/releases/download/v1.12.1/rainloop-community-1.12.1.zip
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
unzip python3 python3-jinja2
|
unzip python3 python3-jinja2 \
|
||||||
|
&& rm -rf /var/www/html/ \
|
||||||
ENV RAINLOOP_URL https://github.com/RainLoop/rainloop-webmail/releases/download/v1.12.0/rainloop-community-1.12.0.zip
|
|
||||||
|
|
||||||
RUN rm -rf /var/www/html/ \
|
|
||||||
&& mkdir /var/www/html \
|
&& mkdir /var/www/html \
|
||||||
&& cd /var/www/html \
|
&& cd /var/www/html \
|
||||||
&& curl -L -O ${RAINLOOP_URL} \
|
&& curl -L -O ${RAINLOOP_URL} \
|
||||||
&& unzip *.zip \
|
&& unzip -q *.zip \
|
||||||
&& rm -f *.zip \
|
&& rm -f *.zip \
|
||||||
&& rm -rf data/ \
|
&& rm -rf data/ \
|
||||||
&& find . -type d -exec chmod 755 {} \; \
|
&& find . -type d -exec chmod 755 {} \; \
|
||||||
&& find . -type f -exec chmod 644 {} \; \
|
&& find . -type f -exec chmod 644 {} \; \
|
||||||
&& chown -R www-data: *
|
&& chown -R www-data: * \
|
||||||
|
&& apt-get purge -y unzip \
|
||||||
|
&& rm -rf /var/lib/apt/lists
|
||||||
|
|
||||||
COPY include.php /var/www/html/include.php
|
COPY include.php /var/www/html/include.php
|
||||||
COPY php.ini /usr/local/etc/php/conf.d/rainloop.ini
|
COPY php.ini /usr/local/etc/php/conf.d/rainloop.ini
|
||||||
@@ -24,4 +25,7 @@ COPY default.ini /default.ini
|
|||||||
|
|
||||||
COPY start.py /start.py
|
COPY start.py /start.py
|
||||||
|
|
||||||
|
EXPOSE 80/tcp
|
||||||
|
VOLUME ["/data"]
|
||||||
|
|
||||||
CMD /start.py
|
CMD /start.py
|
||||||
|
|||||||
@@ -18,4 +18,7 @@ os.makedirs(base + "configs", exist_ok=True)
|
|||||||
convert("/default.ini", "/data/_data_/_default_/domains/default.ini")
|
convert("/default.ini", "/data/_data_/_default_/domains/default.ini")
|
||||||
convert("/config.ini", "/data/_data_/_default_/configs/config.ini")
|
convert("/config.ini", "/data/_data_/_default_/configs/config.ini")
|
||||||
|
|
||||||
|
os.system("chown -R www-data:www-data /data")
|
||||||
|
|
||||||
os.execv("/usr/local/bin/apache2-foreground", ["apache2-foreground"])
|
os.execv("/usr/local/bin/apache2-foreground", ["apache2-foreground"])
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
FROM php:7.0-apache
|
FROM php:7.2-apache
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
libfreetype6-dev \
|
|
||||||
libjpeg62-turbo-dev \
|
|
||||||
libmcrypt-dev \
|
|
||||||
libpng-dev \
|
|
||||||
&& docker-php-ext-install pdo_mysql mcrypt zip
|
|
||||||
|
|
||||||
ENV ROUNDCUBE_URL https://github.com/roundcube/roundcubemail/releases/download/1.3.7/roundcubemail-1.3.7-complete.tar.gz
|
ENV ROUNDCUBE_URL https://github.com/roundcube/roundcubemail/releases/download/1.3.7/roundcubemail-1.3.7-complete.tar.gz
|
||||||
|
|
||||||
RUN echo date.timezone=UTC > /usr/local/etc/php/conf.d/timezone.ini
|
RUN apt-get update && apt-get install -y \
|
||||||
|
zlib1g-dev \
|
||||||
RUN rm -rf /var/www/html/ \
|
&& docker-php-ext-install zip \
|
||||||
|
&& echo date.timezone=UTC > /usr/local/etc/php/conf.d/timezone.ini \
|
||||||
|
&& rm -rf /var/www/html/ \
|
||||||
&& cd /var/www \
|
&& cd /var/www \
|
||||||
&& curl -L -O ${ROUNDCUBE_URL} \
|
&& curl -L -O ${ROUNDCUBE_URL} \
|
||||||
&& tar -xf *.tar.gz \
|
&& tar -xf *.tar.gz \
|
||||||
@@ -20,7 +15,8 @@ RUN rm -rf /var/www/html/ \
|
|||||||
&& cd html \
|
&& cd html \
|
||||||
&& rm -rf CHANGELOG INSTALL LICENSE README.md UPGRADING composer.json-dist installer \
|
&& rm -rf CHANGELOG INSTALL LICENSE README.md UPGRADING composer.json-dist installer \
|
||||||
&& sed -i 's,mod_php5.c,mod_php7.c,g' .htaccess \
|
&& sed -i 's,mod_php5.c,mod_php7.c,g' .htaccess \
|
||||||
&& chown -R www-data: logs temp
|
&& chown -R www-data: logs temp \
|
||||||
|
&& rm -rf /var/lib/apt/lists
|
||||||
|
|
||||||
COPY php.ini /usr/local/etc/php/conf.d/roundcube.ini
|
COPY php.ini /usr/local/etc/php/conf.d/roundcube.ini
|
||||||
|
|
||||||
@@ -28,4 +24,7 @@ COPY config.inc.php /var/www/html/config/
|
|||||||
|
|
||||||
COPY start.sh /start.sh
|
COPY start.sh /start.sh
|
||||||
|
|
||||||
|
EXPOSE 80/tcp
|
||||||
|
VOLUME ["/data"]
|
||||||
|
|
||||||
CMD ["/start.sh"]
|
CMD ["/start.sh"]
|
||||||
@@ -6,6 +6,7 @@ $config = array();
|
|||||||
$config['db_dsnw'] = 'sqlite:////data/roundcube.db';
|
$config['db_dsnw'] = 'sqlite:////data/roundcube.db';
|
||||||
$config['temp_dir'] = '/tmp/';
|
$config['temp_dir'] = '/tmp/';
|
||||||
$config['des_key'] = getenv('SECRET_KEY');
|
$config['des_key'] = getenv('SECRET_KEY');
|
||||||
|
$config['cipher_method'] = 'AES-256-CBC';
|
||||||
$config['identities_level'] = 3;
|
$config['identities_level'] = 3;
|
||||||
$config['reply_all_mode'] = 1;
|
$config['reply_all_mode'] = 1;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user