Merge branch 'master' into refactor-config
This commit is contained in:
@@ -1,17 +1,21 @@
|
||||
FROM python:3-alpine
|
||||
|
||||
FROM alpine:3.8
|
||||
# python3 shared with most images
|
||||
RUN apk add --no-cache \
|
||||
python3 py3-pip \
|
||||
&& pip3 install --upgrade pip
|
||||
# Image specific layers under this line
|
||||
RUN mkdir -p /app
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements-prod.txt requirements.txt
|
||||
RUN apk add --no-cache openssl \
|
||||
&& apk add --no-cache --virtual build-dep openssl-dev libffi-dev python-dev build-base \
|
||||
&& pip install -r requirements.txt \
|
||||
RUN apk add --no-cache openssl curl \
|
||||
&& apk add --no-cache --virtual build-dep openssl-dev libffi-dev python3-dev build-base \
|
||||
&& pip3 install -r requirements.txt \
|
||||
&& apk del --no-cache build-dep
|
||||
|
||||
COPY mailu ./mailu
|
||||
COPY migrations ./migrations
|
||||
COPY start.sh /start.sh
|
||||
COPY start.py /start.py
|
||||
|
||||
RUN pybabel compile -d mailu/translations
|
||||
|
||||
@@ -19,4 +23,6 @@ EXPOSE 80/tcp
|
||||
VOLUME ["/data"]
|
||||
ENV FLASK_APP mailu
|
||||
|
||||
CMD ["/start.sh"]
|
||||
CMD /start.py
|
||||
|
||||
HEALTHCHECK CMD curl -f -L http://localhost/ui || exit 1
|
||||
|
||||
@@ -54,3 +54,4 @@ def create_app():
|
||||
"""
|
||||
config = configuration.ConfigManager()
|
||||
return create_app_from_config(config)
|
||||
|
||||
|
||||
@@ -32,9 +32,6 @@ if exists "X-Virus" {
|
||||
stop;
|
||||
}
|
||||
|
||||
{% if user.reply_enabled %}
|
||||
if currentdate :value "le" "date" "{{ user.reply_enddate }}"
|
||||
{
|
||||
vacation :days 1 :subject "{{ user.reply_subject }}" "{{ user.reply_body }}";
|
||||
}
|
||||
{% if user.reply_active %}
|
||||
vacation :days 1 :subject "{{ user.reply_subject }}" "{{ user.reply_body }}";
|
||||
{% endif %}
|
||||
|
||||
@@ -3,13 +3,24 @@ from mailu.internal import internal
|
||||
from flask import current_app as app
|
||||
|
||||
import flask
|
||||
|
||||
import socket
|
||||
import os
|
||||
|
||||
@internal.route("/dovecot/passdb/<user_email>")
|
||||
def dovecot_passdb_dict(user_email):
|
||||
user = models.User.query.get(user_email) or flask.abort(404)
|
||||
allow_nets = []
|
||||
allow_nets.append(
|
||||
app.config.get("POD_ADDRESS_RANGE") or
|
||||
socket.gethostbyname(app.config["HOST_FRONT"])
|
||||
)
|
||||
if os.environ["WEBMAIL"] != "none":
|
||||
allow_nets.append(socket.gethostbyname(app.config["HOST_WEBMAIL"]))
|
||||
print(allow_nets)
|
||||
return flask.jsonify({
|
||||
"password": user.password,
|
||||
"password": None,
|
||||
"nopassword": "Y",
|
||||
"allow_nets": ",".join(allow_nets)
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -40,11 +40,14 @@ class IdnaEmail(db.TypeDecorator):
|
||||
impl = db.String(255, collation="NOCASE")
|
||||
|
||||
def process_bind_param(self, value, dialect):
|
||||
localpart, domain_name = value.split('@')
|
||||
return "{0}@{1}".format(
|
||||
localpart,
|
||||
idna.encode(domain_name).decode('ascii'),
|
||||
)
|
||||
try:
|
||||
localpart, domain_name = value.split('@')
|
||||
return "{0}@{1}".format(
|
||||
localpart,
|
||||
idna.encode(domain_name).decode('ascii'),
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def process_result_value(self, value, dialect):
|
||||
localpart, domain_name = value.split('@')
|
||||
@@ -276,6 +279,8 @@ class User(Base, Email):
|
||||
reply_enabled = db.Column(db.Boolean(), nullable=False, default=False)
|
||||
reply_subject = db.Column(db.String(255), nullable=True, default=None)
|
||||
reply_body = db.Column(db.Text(), nullable=True, default=None)
|
||||
reply_startdate = db.Column(db.Date, nullable=False,
|
||||
default=date(1900, 1, 1))
|
||||
reply_enddate = db.Column(db.Date, nullable=False,
|
||||
default=date(2999, 12, 31))
|
||||
|
||||
@@ -295,14 +300,24 @@ class User(Base, Email):
|
||||
@property
|
||||
def destination(self):
|
||||
if self.forward_enabled:
|
||||
result = self.self.forward_destination
|
||||
result = self.forward_destination
|
||||
if self.forward_keep:
|
||||
result += ',' + self.email
|
||||
return result
|
||||
else:
|
||||
return self.email
|
||||
|
||||
scheme_dict = {'BLF-CRYPT': "bcrypt",
|
||||
@property
|
||||
def reply_active(self):
|
||||
now = date.today()
|
||||
return (
|
||||
self.reply_enabled and
|
||||
self.reply_startdate < now and
|
||||
self.reply_enddate > now
|
||||
)
|
||||
|
||||
scheme_dict = {'PBKDF2': "pbkdf2_sha512",
|
||||
'BLF-CRYPT': "bcrypt",
|
||||
'SHA512-CRYPT': "sha512_crypt",
|
||||
'SHA256-CRYPT': "sha256_crypt",
|
||||
'MD5-CRYPT': "md5_crypt",
|
||||
@@ -315,8 +330,14 @@ class User(Base, Email):
|
||||
)
|
||||
|
||||
def check_password(self, password):
|
||||
context = User.pw_context
|
||||
reference = re.match('({[^}]+})?(.*)', self.password).group(2)
|
||||
return self.get_password_context().verify(password, reference)
|
||||
result = context.verify(password, reference)
|
||||
if result and context.identify(reference) != context.default_scheme():
|
||||
self.set_password(password)
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
return result
|
||||
|
||||
def set_password(self, password, hash_scheme=None, raw=False):
|
||||
"""Set password for user with specified encryption scheme
|
||||
|
||||
@@ -117,6 +117,7 @@ class UserReplyForm(flask_wtf.FlaskForm):
|
||||
reply_subject = fields.StringField(_('Reply subject'))
|
||||
reply_body = fields.StringField(_('Reply body'),
|
||||
widget=widgets.TextArea())
|
||||
reply_startdate = fields.html5.DateField(_('Start of vacation'))
|
||||
reply_enddate = fields.html5.DateField(_('End of vacation'))
|
||||
submit = fields.SubmitField(_('Update'))
|
||||
|
||||
|
||||
@@ -13,14 +13,17 @@
|
||||
<form class="form" method="post" role="form">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ macros.form_field(form.reply_enabled,
|
||||
onchange="if(this.checked){$('#reply_subject,#reply_body,#reply_enddate').removeAttr('readonly')}
|
||||
onchange="if(this.checked){$('#reply_subject,#reply_body,#reply_enddate,#reply_startdate').removeAttr('readonly')}
|
||||
else{$('#reply_subject,#reply_body,#reply_enddate').attr('readonly', '')}") }}
|
||||
{{ macros.form_field(form.reply_subject,
|
||||
**{("rw" if user.reply_enabled else "readonly"): ""}) }}
|
||||
{{ macros.form_field(form.reply_body, rows=10,
|
||||
**{("rw" if user.reply_enabled else "readonly"): ""}) }}
|
||||
{{ macros.form_field(form.reply_enddate,
|
||||
**{("rw" if user.reply_enabled else "readonly"): ""}) }}
|
||||
**{("rw" if user.reply_enabled else "readonly"): ""}) }}
|
||||
{{ macros.form_field(form.reply_startdate,
|
||||
**{("rw" if user.reply_enabled else "readonly"): ""}) }}
|
||||
|
||||
{{ macros.form_field(form.submit) }}
|
||||
</form>
|
||||
{% endcall %}
|
||||
|
||||
24
core/admin/migrations/versions/3b281286c7bd_.py
Normal file
24
core/admin/migrations/versions/3b281286c7bd_.py
Normal file
@@ -0,0 +1,24 @@
|
||||
""" Add a start day for vacations
|
||||
|
||||
Revision ID: 3b281286c7bd
|
||||
Revises: 25fd6c7bcb4a
|
||||
Create Date: 2018-09-27 22:20:08.158553
|
||||
|
||||
"""
|
||||
|
||||
revision = '3b281286c7bd'
|
||||
down_revision = '25fd6c7bcb4a'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
with op.batch_alter_table('user') as batch:
|
||||
batch.add_column(sa.Column('reply_startdate', sa.Date(), nullable=False,
|
||||
server_default="1900-01-01"))
|
||||
|
||||
|
||||
def downgrade():
|
||||
with op.batch_alter_table('user') as batch:
|
||||
batch.drop_column('reply_startdate')
|
||||
7
core/admin/start.py
Executable file
7
core/admin/start.py
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import os
|
||||
|
||||
os.system("flask mailu advertise")
|
||||
os.system("flask db upgrade")
|
||||
os.system("gunicorn -w 4 -b :80 --access-logfile - --error-logfile - --preload 'mailu:create_app()'")
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
flask mailu advertise
|
||||
flask db upgrade
|
||||
|
||||
gunicorn -w 4 -b :80 --access-logfile - --error-logfile - --preload "$FLASK_APP:create_app()"
|
||||
@@ -1,10 +1,16 @@
|
||||
FROM alpine:3.8
|
||||
|
||||
# python3 shared with most images
|
||||
RUN apk add --no-cache \
|
||||
dovecot dovecot-pigeonhole-plugin dovecot-fts-lucene rspamd-client \
|
||||
python3 py3-pip \
|
||||
&& pip3 install --upgrade pip \
|
||||
&& pip3 install jinja2 podop tenacity
|
||||
python3 py3-pip \
|
||||
&& pip3 install --upgrade pip
|
||||
# Shared layer between rspamd, postfix, dovecot, unbound and nginx
|
||||
RUN pip3 install jinja2
|
||||
# Shared layer between rspamd, postfix, dovecot
|
||||
RUN pip3 install tenacity
|
||||
# Image specific layers under this line
|
||||
RUN apk add --no-cache \
|
||||
dovecot dovecot-pigeonhole-plugin dovecot-fts-lucene rspamd-client bash \
|
||||
&& pip3 install podop
|
||||
|
||||
COPY conf /conf
|
||||
COPY start.py /start.py
|
||||
@@ -13,3 +19,5 @@ EXPOSE 110/tcp 143/tcp 993/tcp 4190/tcp 2525/tcp
|
||||
VOLUME ["/data", "/mail"]
|
||||
|
||||
CMD /start.py
|
||||
|
||||
HEALTHCHECK --start-period=350s CMD echo QUIT|nc localhost 110|grep "Dovecot ready."
|
||||
|
||||
4
core/dovecot/conf/bin/ham
Executable file
4
core/dovecot/conf/bin/ham
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
tee >(rspamc -h antispam:11334 -P mailu learn_ham /dev/stdin) \
|
||||
| rspamc -h antispam:11334 -P mailu -f 13 fuzzy_add /dev/stdin
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
rspamc -h antispam:11334 -P mailu "learn_$1" /dev/stdin <&0
|
||||
4
core/dovecot/conf/bin/spam
Executable file
4
core/dovecot/conf/bin/spam
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
tee >(rspamc -h antispam:11334 -P mailu learn_spam /dev/stdin) \
|
||||
>(rspamc -h antispam:11334 -P mailu -f 11 fuzzy_add /dev/stdin)
|
||||
@@ -136,7 +136,8 @@ service managesieve {
|
||||
}
|
||||
|
||||
plugin {
|
||||
sieve = dict:proxy:/tmp/podop.socket:sieve
|
||||
sieve = file:~/sieve;active=~/.dovecot.sieve
|
||||
sieve_before = dict:proxy:/tmp/podop.socket:sieve
|
||||
sieve_plugins = sieve_imapsieve sieve_extprograms
|
||||
sieve_extensions = +spamtest +spamtestplus +editheader
|
||||
sieve_global_extensions = +vnd.dovecot.execute
|
||||
|
||||
@@ -8,4 +8,4 @@ if string "${mailbox}" "Trash" {
|
||||
stop;
|
||||
}
|
||||
|
||||
execute :pipe "mailtrain" "ham";
|
||||
execute :pipe "ham";
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
require "vnd.dovecot.execute";
|
||||
|
||||
execute :pipe "mailtrain" "spam";
|
||||
execute :pipe "spam";
|
||||
|
||||
@@ -21,20 +21,17 @@ def start_podop():
|
||||
|
||||
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"))
|
||||
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"))
|
||||
|
||||
# Actual startup script
|
||||
resolve()
|
||||
resolve = retry(socket.gethostbyname, stop=tenacity.stop_after_attempt(100), wait=tenacity.wait_random(min=2, max=5))
|
||||
os.environ["FRONT_ADDRESS"] = resolve(os.environ.get("FRONT_ADDRESS", "front"))
|
||||
os.environ["REDIS_ADDRESS"] = resolve(os.environ.get("REDIS_ADDRESS", "redis"))
|
||||
if os.environ["WEBMAIL"] != "none":
|
||||
os.environ["WEBMAIL_ADDRESS"] = resolve(os.environ.get("WEBMAIL_ADDRESS", "webmail"))
|
||||
|
||||
for dovecot_file in glob.glob("/conf/*.conf"):
|
||||
convert(dovecot_file, os.path.join("/etc/dovecot", os.path.basename(dovecot_file)))
|
||||
|
||||
# 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 /conf")
|
||||
os.execv("/usr/sbin/dovecot", ["dovecot", "-c", "/etc/dovecot/dovecot.conf", "-F"])
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
FROM alpine:3.8
|
||||
|
||||
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
|
||||
# python3 shared with most images
|
||||
RUN apk add --no-cache \
|
||||
python3 py3-pip \
|
||||
&& pip3 install --upgrade pip
|
||||
# Shared layer between rspamd, postfix, dovecot, unbound and nginx
|
||||
RUN pip3 install jinja2
|
||||
# Image specific layers under this line
|
||||
RUN apk add --no-cache certbot nginx nginx-mod-mail openssl curl \
|
||||
&& pip3 install idna requests
|
||||
|
||||
COPY conf /conf
|
||||
COPY *.py /
|
||||
@@ -12,3 +16,5 @@ EXPOSE 80/tcp 443/tcp 110/tcp 143/tcp 465/tcp 587/tcp 993/tcp 995/tcp 25/tcp 100
|
||||
VOLUME ["/certs"]
|
||||
|
||||
CMD /start.py
|
||||
|
||||
HEALTHCHECK CMD curl -k -f -L http://localhost/health || exit 1
|
||||
|
||||
@@ -34,6 +34,8 @@ http {
|
||||
'' $scheme;
|
||||
}
|
||||
|
||||
# Disable the main http server when on kubernetes (port 80 and 443)
|
||||
{% if KUBERNETES_INGRESS != 'true' %}
|
||||
# Main HTTP server
|
||||
server {
|
||||
# Variables for proxifying
|
||||
@@ -48,8 +50,8 @@ http {
|
||||
|
||||
# Only enable HTTPS if TLS is enabled with no error
|
||||
{% if TLS and not TLS_ERROR %}
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
|
||||
include /etc/nginx/tls.conf;
|
||||
ssl_session_cache shared:SSLHTTP:50m;
|
||||
@@ -91,8 +93,10 @@ http {
|
||||
{% endif %}
|
||||
|
||||
location {{ WEB_WEBMAIL }} {
|
||||
{% if WEB_WEBMAIL != '/' %}
|
||||
rewrite ^({{ WEB_WEBMAIL }})$ $1/ permanent;
|
||||
rewrite ^{{ WEB_WEBMAIL }}/(.*) /$1 break;
|
||||
{% endif %}
|
||||
include /etc/nginx/proxy.conf;
|
||||
client_max_body_size {{ MESSAGE_SIZE_LIMIT|int + 8388608 }};
|
||||
proxy_pass http://$webmail;
|
||||
@@ -146,7 +150,12 @@ http {
|
||||
proxy_pass_request_body off;
|
||||
proxy_set_header Content-Length "";
|
||||
}
|
||||
|
||||
location /health {
|
||||
return 204;
|
||||
}
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
# Forwarding authentication server
|
||||
server {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python
|
||||
#!/usr/bin/python3
|
||||
|
||||
import jinja2
|
||||
import os
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python
|
||||
#!/usr/bin/python3
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python
|
||||
#!/usr/bin/python3
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
FROM alpine:3.8
|
||||
# python3 shared with most images
|
||||
RUN apk add --no-cache \
|
||||
python3 py3-pip \
|
||||
&& pip3 install --upgrade pip
|
||||
# Shared layer between rspamd, postfix, dovecot, unbound and nginx
|
||||
RUN pip3 install jinja2
|
||||
# Shared layer between rspamd, postfix, dovecot
|
||||
RUN pip3 install tenacity
|
||||
# Image specific layers under this line
|
||||
|
||||
RUN apk add --no-cache postfix postfix-pcre rsyslog \
|
||||
python3 py3-pip \
|
||||
&& pip3 install --upgrade pip \
|
||||
&& pip3 install jinja2 podop tenacity
|
||||
&& pip3 install podop
|
||||
|
||||
COPY conf /conf
|
||||
COPY start.py /start.py
|
||||
@@ -12,3 +19,5 @@ EXPOSE 25/tcp 10025/tcp
|
||||
VOLUME ["/data"]
|
||||
|
||||
CMD /start.py
|
||||
|
||||
HEALTHCHECK --start-period=350s CMD echo QUIT|nc localhost 25|grep "220 .* ESMTP Postfix"
|
||||
|
||||
@@ -32,7 +32,7 @@ relayhost = {{ RELAYHOST }}
|
||||
recipient_delimiter = {{ RECIPIENT_DELIMITER }}
|
||||
|
||||
# Only the front server is allowed to perform xclient
|
||||
smtpd_authorized_xclient_hosts={{ FRONT_ADDRESS }}
|
||||
smtpd_authorized_xclient_hosts={{ FRONT_ADDRESS }} {{ POD_ADDRESS_RANGE }}
|
||||
|
||||
###############
|
||||
# TLS
|
||||
|
||||
@@ -24,12 +24,10 @@ def start_podop():
|
||||
|
||||
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
|
||||
resolve()
|
||||
resolve = retry(socket.gethostbyname, stop=tenacity.stop_after_attempt(100), wait=tenacity.wait_random(min=2, max=5))
|
||||
|
||||
os.environ["FRONT_ADDRESS"] = resolve(os.environ.get("FRONT_ADDRESS", "front"))
|
||||
os.environ["HOST_ANTISPAM"] = os.environ.get("HOST_ANTISPAM", "antispam:11332")
|
||||
os.environ["HOST_LMTP"] = os.environ.get("HOST_LMTP", "imap:2525")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user