Merge branch 'master' into fix-domain-negative-values-1

master
Tim Möhlmann 6 years ago committed by GitHub
commit 2567646f47
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -31,7 +31,6 @@ v1.6.0 - unreleased
- Feature: Add posibilty to run webmail on root ([#501](https://github.com/Mailu/Mailu/issues/501)) - Feature: Add posibilty to run webmail on root ([#501](https://github.com/Mailu/Mailu/issues/501))
- Feature: Upgrade docker-compose.yml to version 3 ([#539](https://github.com/Mailu/Mailu/issues/539)) - Feature: Upgrade docker-compose.yml to version 3 ([#539](https://github.com/Mailu/Mailu/issues/539))
- Feature: Documentation to deploy mailu on a docker swarm ([#551](https://github.com/Mailu/Mailu/issues/551)) - Feature: Documentation to deploy mailu on a docker swarm ([#551](https://github.com/Mailu/Mailu/issues/551))
- Feature: Add full-text search support ([#552](https://github.com/Mailu/Mailu/issues/552))
- Feature: Add optional Maildir-Compression ([#553](https://github.com/Mailu/Mailu/issues/553)) - Feature: Add optional Maildir-Compression ([#553](https://github.com/Mailu/Mailu/issues/553))
- Feature: Preserve rspamd history on container restart ([#561](https://github.com/Mailu/Mailu/issues/561)) - Feature: Preserve rspamd history on container restart ([#561](https://github.com/Mailu/Mailu/issues/561))
- Feature: FAQ ([#564](https://github.com/Mailu/Mailu/issues/564), [#677](https://github.com/Mailu/Mailu/issues/677)) - Feature: FAQ ([#564](https://github.com/Mailu/Mailu/issues/564), [#677](https://github.com/Mailu/Mailu/issues/677))
@ -77,6 +76,9 @@ v1.6.0 - unreleased
- Enhancement: Added regex validation for alias username ([#764](https://github.com/Mailu/Mailu/issues/764)) - Enhancement: Added regex validation for alias username ([#764](https://github.com/Mailu/Mailu/issues/764))
- Enhancement: Allow to disable aliases or users for a specific domain ([#799](https://github.com/Mailu/Mailu/issues/799)) - Enhancement: Allow to disable aliases or users for a specific domain ([#799](https://github.com/Mailu/Mailu/issues/799))
- Enhancement: Update documentation - Enhancement: Update documentation
- Enhancement: Include favicon package ([#801](https://github.com/Mailu/Mailu/issues/801), ([#802](https://github.com/Mailu/Mailu/issues/802))
- Enhancement: Add logging at critical places in python start.py scripts. Implement LOG_LEVEL to control verbosity ([#588](https://github.com/Mailu/Mailu/issues/588))
- Enhancement: Mark message as seen when reporting as spam
- Upstream: Update Roundcube - Upstream: Update Roundcube
- Upstream: Update Rainloop - Upstream: Update Rainloop
- Bug: Rainloop fails with "domain not allowed" ([#93](https://github.com/Mailu/Mailu/issues/93)) - Bug: Rainloop fails with "domain not allowed" ([#93](https://github.com/Mailu/Mailu/issues/93))
@ -110,6 +112,9 @@ v1.6.0 - unreleased
- Bug: Fix rainloop permissions ([#637](https://github.com/Mailu/Mailu/issues/637)) - Bug: Fix rainloop permissions ([#637](https://github.com/Mailu/Mailu/issues/637))
- Bug: Fix broken webmail and logo url in admin ([#792](https://github.com/Mailu/Mailu/issues/792)) - Bug: Fix broken webmail and logo url in admin ([#792](https://github.com/Mailu/Mailu/issues/792))
- Bug: Don't allow negative values on domain creation/edit ([#799](https://github.com/Mailu/Mailu/issues/799)) - Bug: Don't allow negative values on domain creation/edit ([#799](https://github.com/Mailu/Mailu/issues/799))
- Bug: Don't recursivly chown on mailboxes ([#776](https://github.com/Mailu/Mailu/issues/776))
- Bug: Fix forced password input for user edit ([#745](https://github.com/Mailu/Mailu/issues/745))
- Bug: Fetched accounts: Password field is of type "text" ([#789](https://github.com/Mailu/Mailu/issues/789))
v1.5.1 - 2017-11-21 v1.5.1 - 2017-11-21
------------------- -------------------

@ -0,0 +1,16 @@
## What type of PR?
(Feature, enhancement, bug-fix, documentation)
## What does this PR do?
### Related issue(s)
- Mention an issue like: #001
- Auto close an issue like: closes #001
## Prerequistes
Before we can consider review and merge, please make sure the following list is done and checked.
If an entry in not applicable, you can check it or remove it from the list.
- [ ] In case of feature or enhancement: documentation updated accordingly
- [ ] Unless it's docs or a minor change: place entry in the [changelog](CHANGELOG.md), under the latest un-released version.

@ -260,10 +260,19 @@ class Email(object):
@classmethod @classmethod
def resolve_destination(cls, localpart, domain_name, ignore_forward_keep=False): def resolve_destination(cls, localpart, domain_name, ignore_forward_keep=False):
localpart_stripped = None
if os.environ.get('RECIPIENT_DELIMITER') in localpart:
localpart_stripped = localpart.rsplit(os.environ.get('RECIPIENT_DELIMITER'), 1)[0]
alias = Alias.resolve(localpart, domain_name) alias = Alias.resolve(localpart, domain_name)
if not alias and localpart_stripped:
alias = Alias.resolve(localpart_stripped, domain_name)
if alias: if alias:
return alias.destination return alias.destination
user = User.query.get('{}@{}'.format(localpart, domain_name)) user = User.query.get('{}@{}'.format(localpart, domain_name))
if not user and localpart_stripped:
user = User.query.get('{}@{}'.format(localpart_stripped, domain_name))
if user: if user:
if user.forward_enabled: if user.forward_enabled:
destination = user.forward_destination destination = user.forward_destination

@ -84,7 +84,7 @@ class RelayForm(flask_wtf.FlaskForm):
class UserForm(flask_wtf.FlaskForm): class UserForm(flask_wtf.FlaskForm):
localpart = fields.StringField(_('E-mail'), [validators.DataRequired(), validators.Regexp(LOCALPART_REGEX)]) localpart = fields.StringField(_('E-mail'), [validators.DataRequired(), validators.Regexp(LOCALPART_REGEX)])
pw = fields.PasswordField(_('Password'), [validators.DataRequired()]) pw = fields.PasswordField(_('Password'))
pw2 = fields.PasswordField(_('Confirm password'), [validators.EqualTo('pw')]) pw2 = fields.PasswordField(_('Confirm password'), [validators.EqualTo('pw')])
quota_bytes = fields_.IntegerSliderField(_('Quota'), default=1000000000) quota_bytes = fields_.IntegerSliderField(_('Quota'), default=1000000000)
enable_imap = fields.BooleanField(_('Allow IMAP access'), default=True) enable_imap = fields.BooleanField(_('Allow IMAP access'), default=True)
@ -165,11 +165,11 @@ class FetchForm(flask_wtf.FlaskForm):
protocol = fields.SelectField(_('Protocol'), choices=[ protocol = fields.SelectField(_('Protocol'), choices=[
('imap', 'IMAP'), ('pop3', 'POP3') ('imap', 'IMAP'), ('pop3', 'POP3')
]) ])
host = fields.StringField(_('Hostname or IP')) host = fields.StringField(_('Hostname or IP'), [validators.DataRequired()])
port = fields.IntegerField(_('TCP port')) port = fields.IntegerField(_('TCP port'), [validators.DataRequired(), validators.NumberRange(min=0, max=65535)])
tls = fields.BooleanField(_('Enable TLS')) tls = fields.BooleanField(_('Enable TLS'))
username = fields.StringField(_('Username')) username = fields.StringField(_('Username'), [validators.DataRequired()])
password = fields.StringField(_('Password')) password = fields.PasswordField(_('Password'))
keep = fields.BooleanField(_('Keep emails on the server')) keep = fields.BooleanField(_('Keep emails on the server'))
submit = fields.SubmitField(_('Submit')) submit = fields.SubmitField(_('Submit'))

@ -13,6 +13,13 @@
{% block head %} {% block head %}
{{super()}} {{super()}}
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#00aba9">
<meta name="theme-color" content="#ffffff">
{% block scripts %} {% block scripts %}
{{super()}} {{super()}}
<script src="{{ url_for('.static', filename='select2/js/select2.min.js') }}"></script> <script src="{{ url_for('.static', filename='select2/js/select2.min.js') }}"></script>

@ -3,6 +3,7 @@ from mailu.ui import ui, forms, access
import flask import flask
import flask_login import flask_login
import wtforms
@ui.route('/fetch/list', methods=['GET', 'POST'], defaults={'user_email': None}) @ui.route('/fetch/list', methods=['GET', 'POST'], defaults={'user_email': None})
@ -21,6 +22,7 @@ def fetch_create(user_email):
user_email = user_email or flask_login.current_user.email user_email = user_email or flask_login.current_user.email
user = models.User.query.get(user_email) or flask.abort(404) user = models.User.query.get(user_email) or flask.abort(404)
form = forms.FetchForm() form = forms.FetchForm()
form.pw.validators = [wtforms.validators.DataRequired()]
if form.validate_on_submit(): if form.validate_on_submit():
fetch = models.Fetch(user=user) fetch = models.Fetch(user=user)
form.populate_obj(fetch) form.populate_obj(fetch)
@ -38,6 +40,8 @@ def fetch_edit(fetch_id):
fetch = models.Fetch.query.get(fetch_id) or flask.abort(404) fetch = models.Fetch.query.get(fetch_id) or flask.abort(404)
form = forms.FetchForm(obj=fetch) form = forms.FetchForm(obj=fetch)
if form.validate_on_submit(): if form.validate_on_submit():
if not form.password.data:
form.password.data = fetch.password
form.populate_obj(fetch) form.populate_obj(fetch)
models.db.session.commit() models.db.session.commit()
flask.flash('Fetch configuration updated') flask.flash('Fetch configuration updated')

@ -23,6 +23,7 @@ def user_create(domain_name):
return flask.redirect( return flask.redirect(
flask.url_for('.user_list', domain_name=domain.name)) flask.url_for('.user_list', domain_name=domain.name))
form = forms.UserForm() form = forms.UserForm()
form.pw.validators = [wtforms.validators.DataRequired()]
if domain.max_quota_bytes: if domain.max_quota_bytes:
form.quota_bytes.validators = [ form.quota_bytes.validators = [
wtforms.validators.NumberRange(max=domain.max_quota_bytes)] wtforms.validators.NumberRange(max=domain.max_quota_bytes)]
@ -54,7 +55,6 @@ def user_edit(user_email):
# Create the form # Create the form
form = forms.UserForm(obj=user) form = forms.UserForm(obj=user)
wtforms_components.read_only(form.localpart) wtforms_components.read_only(form.localpart)
form.pw.validators = []
form.localpart.validators = [] form.localpart.validators = []
if max_quota_bytes: if max_quota_bytes:
form.quota_bytes.validators = [ form.quota_bytes.validators = [

@ -34,7 +34,7 @@ pyOpenSSL==18.0.0
python-dateutil==2.7.5 python-dateutil==2.7.5
python-editor==1.0.3 python-editor==1.0.3
pytz==2018.7 pytz==2018.7
PyYAML==3.13 PyYAML==4.2b4
redis==3.0.1 redis==3.0.1
six==1.11.0 six==1.11.0
SQLAlchemy==1.2.13 SQLAlchemy==1.2.13

@ -9,8 +9,9 @@ RUN pip3 install jinja2
RUN pip3 install tenacity RUN pip3 install tenacity
# Image specific layers under this line # Image specific layers under this line
RUN apk add --no-cache \ RUN apk add --no-cache \
dovecot dovecot-pigeonhole-plugin dovecot-fts-lucene rspamd-client bash \ dovecot dovecot-pigeonhole-plugin rspamd-client bash \
&& pip3 install podop && pip3 install podop \
&& mkdir /var/lib/dovecot
COPY conf /conf COPY conf /conf
COPY start.py /start.py COPY start.py /start.py

@ -7,22 +7,6 @@ postmaster_address = {{ POSTMASTER }}@{{ DOMAIN }}
hostname = {{ HOSTNAMES.split(",")[0] }} hostname = {{ HOSTNAMES.split(",")[0] }}
submission_host = {{ FRONT_ADDRESS }} submission_host = {{ FRONT_ADDRESS }}
{% if DISABLE_FTS_LUCENE != 'true' %}
###############
# Full-text search
###############
mail_plugins = $mail_plugins fts fts_lucene
plugin {
fts = lucene
fts_autoindex = yes
fts_autoindex_exclude = \Junk
fts_lucene = whitespace_chars=@.
}
{% endif %}
############### ###############
# Mailboxes # Mailboxes
############### ###############

@ -1,3 +1,5 @@
require "imap4flags";
require "vnd.dovecot.execute"; require "vnd.dovecot.execute";
setflag "\\seen";
execute :pipe "spam"; execute :pipe "spam";

@ -6,23 +6,40 @@ import socket
import glob import glob
import multiprocessing import multiprocessing
import tenacity import tenacity
import logging as log
import sys
from tenacity import retry from tenacity import retry
from podop import run_server from podop import run_server
log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING"))
def start_podop(): def start_podop():
os.setuid(8) os.setuid(8)
run_server(3 if "DEBUG" in os.environ else 0, "dovecot", "/tmp/podop.socket", [ run_server(0, "dovecot", "/tmp/podop.socket", [
("quota", "url", "http://admin/internal/dovecot/§"), ("quota", "url", "http://admin/internal/dovecot/§"),
("auth", "url", "http://admin/internal/dovecot/§"), ("auth", "url", "http://admin/internal/dovecot/§"),
("sieve", "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)) def convert(src, dst):
logger = log.getLogger("convert()")
logger.debug("Source: %s, Destination: %s", 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),
before=tenacity.before_log(log.getLogger("tenacity.retry"), log.DEBUG),
before_sleep=tenacity.before_sleep_log(log.getLogger("tenacity.retry"), log.INFO),
after=tenacity.after_log(log.getLogger("tenacity.retry"), log.DEBUG)
)
def resolve(hostname):
logger = log.getLogger("resolve()")
logger.info(hostname)
return socket.gethostbyname(hostname)
# Actual startup script # Actual startup script
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["FRONT_ADDRESS"] = resolve(os.environ.get("FRONT_ADDRESS", "front"))
os.environ["REDIS_ADDRESS"] = resolve(os.environ.get("REDIS_ADDRESS", "redis")) os.environ["REDIS_ADDRESS"] = resolve(os.environ.get("REDIS_ADDRESS", "redis"))
if os.environ["WEBMAIL"] != "none": if os.environ["WEBMAIL"] != "none":
@ -33,5 +50,6 @@ for dovecot_file in glob.glob("/conf/*.conf"):
# Run Podop, then postfix # Run Podop, then postfix
multiprocessing.Process(target=start_podop).start() multiprocessing.Process(target=start_podop).start()
os.system("chown -R mail:mail /mail /var/lib/dovecot /conf") os.system("chown mail:mail /mail")
os.system("chown -R mail:mail /var/lib/dovecot /conf")
os.execv("/usr/sbin/dovecot", ["dovecot", "-c", "/etc/dovecot/dovecot.conf", "-F"]) os.execv("/usr/sbin/dovecot", ["dovecot", "-c", "/etc/dovecot/dovecot.conf", "-F"])

@ -10,6 +10,7 @@ RUN apk add --no-cache certbot nginx nginx-mod-mail openssl curl \
&& pip3 install idna requests watchdog && pip3 install idna requests watchdog
COPY conf /conf COPY conf /conf
COPY static /static
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

@ -38,6 +38,8 @@ http {
{% if KUBERNETES_INGRESS != 'true' %} {% if KUBERNETES_INGRESS != 'true' %}
# Main HTTP server # Main HTTP server
server { server {
# Favicon stuff
root /static;
# Variables for proxifying # Variables for proxifying
set $admin {{ HOST_ADMIN }}; set $admin {{ HOST_ADMIN }};
set $antispam {{ HOST_ANTISPAM }}; set $antispam {{ HOST_ANTISPAM }};
@ -90,9 +92,9 @@ http {
{% if WEB_WEBMAIL != '/' %} {% if WEB_WEBMAIL != '/' %}
location / { location / {
{% if WEBROOT_REDIRECT %} {% if WEBROOT_REDIRECT %}
return 301 {{ WEBROOT_REDIRECT }}; try_files $uri {{ WEBROOT_REDIRECT }};
{% else %} {% else %}
return 404; try_files $uri =404;
{% endif %} {% endif %}
} }
{% endif %} {% endif %}

@ -2,11 +2,18 @@
import jinja2 import jinja2
import os import os
import logging as log
convert = lambda src, dst, args: open(dst, "w").write(jinja2.Template(open(src).read()).render(**args)) import sys
args = os.environ.copy() args = os.environ.copy()
log.basicConfig(stream=sys.stderr, level=args.get("LOG_LEVEL", "WARNING"))
def convert(src, dst, args):
logger = log.getLogger("convert()")
logger.debug("Source: %s, Destination: %s", src, dst)
open(dst, "w").write(jinja2.Template(open(src).read()).render(**args))
# Get the first DNS server # Get the first DNS server
with open("/etc/resolv.conf") as handle: with open("/etc/resolv.conf") as handle:
content = handle.read().split() content = handle.read().split()

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#00aba9</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 978 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

@ -0,0 +1,25 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="536.000000pt" height="536.000000pt" viewBox="0 0 536.000000 536.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,536.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M2508 5346 c-1 -2 -38 -6 -80 -10 -120 -10 -319 -44 -418 -71 -196
-53 -336 -107 -526 -200 -128 -63 -346 -197 -419 -257 -11 -9 -54 -44 -95 -78
-429 -353 -738 -840 -880 -1385 -16 -61 -24 -98 -45 -205 -3 -14 -8 -47 -11
-75 -4 -27 -8 -53 -10 -56 -7 -13 -18 -230 -18 -354 2 -261 42 -536 109 -745
8 -25 21 -65 29 -90 30 -96 100 -260 160 -376 255 -489 653 -889 1136 -1141
103 -53 245 -118 285 -129 6 -1 35 -12 65 -24 48 -18 164 -54 210 -65 8 -2 45
-10 81 -19 208 -48 333 -61 599 -61 176 0 328 8 384 20 12 2 35 6 51 9 293 45
623 162 895 319 466 267 862 694 1082 1167 27 58 53 114 58 125 61 127 159
484 176 640 3 28 7 55 9 62 8 25 19 242 18 353 -6 552 -175 1074 -495 1525
-71 101 -82 114 -189 234 -255 286 -568 513 -924 669 -178 78 -422 152 -590
178 -16 3 -41 7 -55 10 -14 2 -47 7 -75 10 -27 3 -61 8 -75 11 -28 5 -436 13
-442 9z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@ -0,0 +1,19 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

@ -7,14 +7,18 @@ import glob
import shutil import shutil
import tenacity import tenacity
import multiprocessing import multiprocessing
import logging as log
import sys
from tenacity import retry from tenacity import retry
from podop import run_server from podop import run_server
log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING"))
def start_podop(): def start_podop():
os.setuid(100) os.setuid(100)
run_server(3 if "DEBUG" in os.environ else 0, "postfix", "/tmp/podop.socket", [ # TODO: Remove verbosity setting from Podop?
run_server(0, "postfix", "/tmp/podop.socket", [
("transport", "url", "http://admin/internal/postfix/transport/§"), ("transport", "url", "http://admin/internal/postfix/transport/§"),
("alias", "url", "http://admin/internal/postfix/alias/§"), ("alias", "url", "http://admin/internal/postfix/alias/§"),
("domain", "url", "http://admin/internal/postfix/domain/§"), ("domain", "url", "http://admin/internal/postfix/domain/§"),
@ -23,11 +27,24 @@ def start_podop():
("senderlogin", "url", "http://admin/internal/postfix/sender/login/§") ("senderlogin", "url", "http://admin/internal/postfix/sender/login/§")
]) ])
convert = lambda src, dst: open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ)) def convert(src, dst):
logger = log.getLogger("convert()")
logger.debug("Source: %s, Destination: %s", 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),
before=tenacity.before_log(log.getLogger("tenacity.retry"), log.DEBUG),
before_sleep=tenacity.before_sleep_log(log.getLogger("tenacity.retry"), log.INFO),
after=tenacity.after_log(log.getLogger("tenacity.retry"), log.DEBUG)
)
def resolve(hostname):
logger = log.getLogger("resolve()")
logger.info(hostname)
return socket.gethostbyname(hostname)
# Actual startup script # Actual startup script
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["FRONT_ADDRESS"] = resolve(os.environ.get("FRONT_ADDRESS", "front"))
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")

@ -3,10 +3,6 @@
# these few settings must however be configured before starting the mail # these few settings must however be configured before starting the mail
# server and require a restart upon change. # server and require a restart upon change.
# Set this to `true` to disable full text search by lucene (value: true, false)
# This is a workaround for the bug in issue #751 (indexer-worker crashes)
DISABLE_FTS_LUCENE=false
################################### ###################################
# Common configuration variables # Common configuration variables
################################### ###################################
@ -151,3 +147,6 @@ REAL_IP_FROM=
# choose wether mailu bounces (no) or rejects (yes) mail when recipient is unknown (value: yes, no) # choose wether mailu bounces (no) or rejects (yes) mail when recipient is unknown (value: yes, no)
REJECT_UNLISTED_RECIPIENT= REJECT_UNLISTED_RECIPIENT=
# Log level threshold in start.py (value: CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET)
LOG_LEVEL=WARNING

@ -71,6 +71,14 @@ Web settings
The ``WEB_ADMIN`` contains the path to the main admin interface, while The ``WEB_ADMIN`` contains the path to the main admin interface, while
``WEB_WEBMAIL`` contains the path to the Web email client. ``WEB_WEBMAIL`` contains the path to the Web email client.
The ``WEBROOT_REDIRECT`` redirects all non-found queries to the set path.
An empty ``WEBROOT_REDIRECT`` value disables redirecting and enables classic
behavior of a 404 result when not found.
All three options need a leading slash (``/``) to work.
.. note:: ``WEBROOT_REDIRECT`` has to point to a valid path on the webserver.
This means it cannot point to any services which are not enabled.
For example, don't point it to ``/webmail`` when ``WEBMAIL=none``
Both ``SITENAME`` and ``WEBSITE`` are customization options for the panel menu Both ``SITENAME`` and ``WEBSITE`` are customization options for the panel menu
in the admin interface, while ``SITENAME`` is a customization option for in the admin interface, while ``SITENAME`` is a customization option for
@ -83,6 +91,13 @@ The ``PASSWORD_SCHEME`` is the password encryption scheme. You should use the
default value, unless you are importing password from a separate system and default value, unless you are importing password from a separate system and
want to keep using the old password encryption scheme. want to keep using the old password encryption scheme.
The ``LOG_LEVEL`` setting is used by the python start-up scripts as a logging threshold.
Log messages equal or higher than this priority will be printed.
Can be one of: CRITICAL, ERROR, WARNING, INFO, DEBUG or NOTSET.
See the `python docs`_ for more information.
.. _`python docs`: https://docs.python.org/3.6/library/logging.html#logging-levels
Infrastructure settings Infrastructure settings
----------------------- -----------------------

@ -204,6 +204,41 @@ correct syntax. The following file names will be taken as override configuration
*Issue reference:* `206`_. *Issue reference:* `206`_.
I want to integrate Nextcloud with Mailu
````````````````````````````````````````
First of all you have to install dependencies required to authenticate users via imap in Nextcloud
.. code-block:: bash
apt-get update \
&& apt-get install -y libc-client-dev libkrb5-dev \
&& rm -rf /var/lib/apt/lists/* \
&& docker-php-ext-configure imap --with-kerberos --with-imap-ssl \
&& docker-php-ext-install imap
Next, you have to enable External user support from Nextcloud Apps interface
In the end you need to configure additional user backends in Nextclouds configuration config/config.php using the following syntax:
.. code-block:: bash
<?php
'user_backends' => array(
array(
'class' => 'OC_User_IMAP',
'arguments' => array(
'{imap.example.com:993/imap/ssl}', 'example.com'
),
),
),
If a domain name (e.g. example.com) is specified, then this makes sure that only users from this domain will be allowed to login.
After successfull login the domain part will be striped and the rest used as username in NextCloud. e.g. 'username@example.com' will be 'username' in NextCloud.
*Issue reference:* `575`_.
.. _`Postfix`: http://www.postfix.org/postconf.5.html .. _`Postfix`: http://www.postfix.org/postconf.5.html
.. _`Dovecot`: https://wiki.dovecot.org/ConfigFile .. _`Dovecot`: https://wiki.dovecot.org/ConfigFile
.. _`NGINX`: https://nginx.org/en/docs/ .. _`NGINX`: https://nginx.org/en/docs/
@ -218,6 +253,7 @@ correct syntax. The following file names will be taken as override configuration
.. _`747`: https://github.com/Mailu/Mailu/issues/747 .. _`747`: https://github.com/Mailu/Mailu/issues/747
.. _`520`: https://github.com/Mailu/Mailu/issues/520 .. _`520`: https://github.com/Mailu/Mailu/issues/520
.. _`591`: https://github.com/Mailu/Mailu/issues/591 .. _`591`: https://github.com/Mailu/Mailu/issues/591
.. _`575`: https://github.com/Mailu/Mailu/issues/575
Technical issues Technical issues
---------------- ----------------
@ -304,7 +340,7 @@ See also :ref:`external_certs`.
*Issue reference:* `426`_, `615`_. *Issue reference:* `426`_, `615`_.
How do I activate DKIM and DMARC? How do I activate DKIM and DMARC?
``````````````````````` `````````````````````````````````
Go into the Domain Panel and choose the Domain you want to enable DKIM for. Go into the Domain Panel and choose the Domain you want to enable DKIM for.
Click the first icon on the left side (domain details). Click the first icon on the left side (domain details).
Now click on the top right on the *"Regenerate Keys"* Button. Now click on the top right on the *"Regenerate Keys"* Button.
@ -367,7 +403,6 @@ We **strongly** advice against downgrading the TLS version and ciphers!
*Issue reference:* `363`_, `698`_. *Issue reference:* `363`_, `698`_.
.. _`troubleshooting tag`: https://github.com/Mailu/Mailu/issues?utf8=%E2%9C%93&q=label%3Afaq%2Ftroubleshooting .. _`troubleshooting tag`: https://github.com/Mailu/Mailu/issues?utf8=%E2%9C%93&q=label%3Afaq%2Ftroubleshooting
.. _`85`: https://github.com/Mailu/Mailu/issues/85 .. _`85`: https://github.com/Mailu/Mailu/issues/85
.. _`102`: https://github.com/Mailu/Mailu/issues/102 .. _`102`: https://github.com/Mailu/Mailu/issues/102

@ -1,12 +1,21 @@
#!/usr/bin/python3 #!/usr/bin/python3
import os import os
import logging as log
import sys
log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING"))
logger=log.getLogger(__name__)
# Bootstrap the database if clamav is running for the first time # Bootstrap the database if clamav is running for the first time
os.system("[ -f /data/main.cvd ] || freshclam") if not os.path.isfile("/data/main.cvd"):
logger.info("Starting primary virus DB download")
os.system("freshclam")
# Run the update daemon # Run the update daemon
logger.info("Starting the update daemon")
os.system("freshclam -d -c 6") os.system("freshclam -d -c 6")
# Run clamav # Run clamav
logger.info("Starting clamav")
os.system("clamd") os.system("clamd")

@ -5,13 +5,31 @@ import os
import socket import socket
import glob import glob
import tenacity import tenacity
import logging as log
import sys
from tenacity import retry from tenacity import retry
convert = lambda src, dst: open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ)) log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING"))
def convert(src, dst):
logger = log.getLogger("convert()")
logger.debug("Source: %s, Destination: %s", 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),
before=tenacity.before_log(log.getLogger("tenacity.retry"), log.DEBUG),
before_sleep=tenacity.before_sleep_log(log.getLogger("tenacity.retry"), log.INFO),
after=tenacity.after_log(log.getLogger("tenacity.retry"), log.DEBUG)
)
def resolve(hostname):
logger = log.getLogger("resolve()")
logger.info(hostname)
return socket.gethostbyname(hostname)
# Actual startup script # Actual startup script
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["FRONT_ADDRESS"] = resolve(os.environ.get("FRONT_ADDRESS", "front"))
if "HOST_REDIS" not in os.environ: os.environ["HOST_REDIS"] = "redis" if "HOST_REDIS" not in os.environ: os.environ["HOST_REDIS"] = "redis"

@ -2,8 +2,16 @@
import jinja2 import jinja2
import os import os
import logging as log
import sys
log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING"))
def convert(src, dst):
logger = log.getLogger("convert()")
logger.debug("Source: %s, Destination: %s", 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))
convert("/unbound.conf", "/etc/unbound/unbound.conf") convert("/unbound.conf", "/etc/unbound/unbound.conf")
os.execv("/usr/sbin/unbound", ["-c /etc/unbound/unbound.conf"]) os.execv("/usr/sbin/unbound", ["-c /etc/unbound/unbound.conf"])

@ -160,3 +160,6 @@ REAL_IP_FROM={{ real_ip_from }}
# choose wether mailu bounces (no) or rejects (yes) mail when recipient is unknown (value: yes, no) # choose wether mailu bounces (no) or rejects (yes) mail when recipient is unknown (value: yes, no)
REJECT_UNLISTED_RECIPIENT={{ reject_unlisted_recipient }} REJECT_UNLISTED_RECIPIENT={{ reject_unlisted_recipient }}
# Log level threshold in start.py (value: CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET)
LOG_LEVEL=WARNING

@ -3,8 +3,15 @@
import jinja2 import jinja2
import os import os
import shutil import shutil
import logging as log
import sys
convert = lambda src, dst: open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ)) log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING"))
def convert(src, dst):
logger = log.getLogger("convert()")
logger.debug("Source: %s, Destination: %s", src, dst)
open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ))
# Actual startup script # Actual startup script
os.environ["FRONT_ADDRESS"] = os.environ.get("FRONT_ADDRESS", "front") os.environ["FRONT_ADDRESS"] = os.environ.get("FRONT_ADDRESS", "front")

@ -2,8 +2,15 @@
import os import os
import jinja2 import jinja2
import logging as log
import sys
convert = lambda src, dst: open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ)) log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING"))
def convert(src, dst):
logger = log.getLogger("convert()")
logger.debug("Source: %s, Destination: %s", src, dst)
open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ))
os.environ["MAX_FILESIZE"] = str(int(int(os.environ.get("MESSAGE_SIZE_LIMIT"))*0.66/1048576)) os.environ["MAX_FILESIZE"] = str(int(int(os.environ.get("MESSAGE_SIZE_LIMIT"))*0.66/1048576))

Loading…
Cancel
Save