Merge branch 'master' into fix-domain-negative-values-1
@ -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: 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: 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: 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))
|
||||
@ -77,6 +76,9 @@ v1.6.0 - unreleased
|
||||
- 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: 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 Rainloop
|
||||
- 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 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 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
|
||||
-------------------
|
||||
|
16
PULL_REQUEST_TEMPLATE.md
Normal file
@ -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
|
||||
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)
|
||||
if not alias and localpart_stripped:
|
||||
alias = Alias.resolve(localpart_stripped, domain_name)
|
||||
if alias:
|
||||
return alias.destination
|
||||
|
||||
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.forward_enabled:
|
||||
destination = user.forward_destination
|
||||
|
@ -84,7 +84,7 @@ class RelayForm(flask_wtf.FlaskForm):
|
||||
|
||||
class UserForm(flask_wtf.FlaskForm):
|
||||
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')])
|
||||
quota_bytes = fields_.IntegerSliderField(_('Quota'), default=1000000000)
|
||||
enable_imap = fields.BooleanField(_('Allow IMAP access'), default=True)
|
||||
@ -165,11 +165,11 @@ class FetchForm(flask_wtf.FlaskForm):
|
||||
protocol = fields.SelectField(_('Protocol'), choices=[
|
||||
('imap', 'IMAP'), ('pop3', 'POP3')
|
||||
])
|
||||
host = fields.StringField(_('Hostname or IP'))
|
||||
port = fields.IntegerField(_('TCP port'))
|
||||
host = fields.StringField(_('Hostname or IP'), [validators.DataRequired()])
|
||||
port = fields.IntegerField(_('TCP port'), [validators.DataRequired(), validators.NumberRange(min=0, max=65535)])
|
||||
tls = fields.BooleanField(_('Enable TLS'))
|
||||
username = fields.StringField(_('Username'))
|
||||
password = fields.StringField(_('Password'))
|
||||
username = fields.StringField(_('Username'), [validators.DataRequired()])
|
||||
password = fields.PasswordField(_('Password'))
|
||||
keep = fields.BooleanField(_('Keep emails on the server'))
|
||||
submit = fields.SubmitField(_('Submit'))
|
||||
|
||||
|
@ -13,6 +13,13 @@
|
||||
|
||||
{% block head %}
|
||||
{{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 %}
|
||||
{{super()}}
|
||||
<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_login
|
||||
import wtforms
|
||||
|
||||
|
||||
@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 = models.User.query.get(user_email) or flask.abort(404)
|
||||
form = forms.FetchForm()
|
||||
form.pw.validators = [wtforms.validators.DataRequired()]
|
||||
if form.validate_on_submit():
|
||||
fetch = models.Fetch(user=user)
|
||||
form.populate_obj(fetch)
|
||||
@ -38,6 +40,8 @@ def fetch_edit(fetch_id):
|
||||
fetch = models.Fetch.query.get(fetch_id) or flask.abort(404)
|
||||
form = forms.FetchForm(obj=fetch)
|
||||
if form.validate_on_submit():
|
||||
if not form.password.data:
|
||||
form.password.data = fetch.password
|
||||
form.populate_obj(fetch)
|
||||
models.db.session.commit()
|
||||
flask.flash('Fetch configuration updated')
|
||||
|
@ -23,6 +23,7 @@ def user_create(domain_name):
|
||||
return flask.redirect(
|
||||
flask.url_for('.user_list', domain_name=domain.name))
|
||||
form = forms.UserForm()
|
||||
form.pw.validators = [wtforms.validators.DataRequired()]
|
||||
if domain.max_quota_bytes:
|
||||
form.quota_bytes.validators = [
|
||||
wtforms.validators.NumberRange(max=domain.max_quota_bytes)]
|
||||
@ -54,7 +55,6 @@ def user_edit(user_email):
|
||||
# Create the form
|
||||
form = forms.UserForm(obj=user)
|
||||
wtforms_components.read_only(form.localpart)
|
||||
form.pw.validators = []
|
||||
form.localpart.validators = []
|
||||
if max_quota_bytes:
|
||||
form.quota_bytes.validators = [
|
||||
|
@ -34,7 +34,7 @@ pyOpenSSL==18.0.0
|
||||
python-dateutil==2.7.5
|
||||
python-editor==1.0.3
|
||||
pytz==2018.7
|
||||
PyYAML==3.13
|
||||
PyYAML==4.2b4
|
||||
redis==3.0.1
|
||||
six==1.11.0
|
||||
SQLAlchemy==1.2.13
|
||||
|
@ -9,8 +9,9 @@ RUN pip3 install jinja2
|
||||
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
|
||||
dovecot dovecot-pigeonhole-plugin rspamd-client bash \
|
||||
&& pip3 install podop \
|
||||
&& mkdir /var/lib/dovecot
|
||||
|
||||
COPY conf /conf
|
||||
COPY start.py /start.py
|
||||
|
@ -7,22 +7,6 @@ postmaster_address = {{ POSTMASTER }}@{{ DOMAIN }}
|
||||
hostname = {{ HOSTNAMES.split(",")[0] }}
|
||||
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
|
||||
###############
|
||||
|
@ -1,3 +1,5 @@
|
||||
require "imap4flags";
|
||||
require "vnd.dovecot.execute";
|
||||
|
||||
setflag "\\seen";
|
||||
execute :pipe "spam";
|
||||
|
@ -6,23 +6,40 @@ import socket
|
||||
import glob
|
||||
import multiprocessing
|
||||
import tenacity
|
||||
import logging as log
|
||||
import sys
|
||||
|
||||
from tenacity import retry
|
||||
from podop import run_server
|
||||
|
||||
log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING"))
|
||||
|
||||
def start_podop():
|
||||
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/§"),
|
||||
("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))
|
||||
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
|
||||
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":
|
||||
@ -33,5 +50,6 @@ for dovecot_file in glob.glob("/conf/*.conf"):
|
||||
|
||||
# Run Podop, then postfix
|
||||
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"])
|
||||
|
@ -10,6 +10,7 @@ RUN apk add --no-cache certbot nginx nginx-mod-mail openssl curl \
|
||||
&& pip3 install idna requests watchdog
|
||||
|
||||
COPY conf /conf
|
||||
COPY static /static
|
||||
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
|
||||
|
@ -38,6 +38,8 @@ http {
|
||||
{% if KUBERNETES_INGRESS != 'true' %}
|
||||
# Main HTTP server
|
||||
server {
|
||||
# Favicon stuff
|
||||
root /static;
|
||||
# Variables for proxifying
|
||||
set $admin {{ HOST_ADMIN }};
|
||||
set $antispam {{ HOST_ANTISPAM }};
|
||||
@ -90,9 +92,9 @@ http {
|
||||
{% if WEB_WEBMAIL != '/' %}
|
||||
location / {
|
||||
{% if WEBROOT_REDIRECT %}
|
||||
return 301 {{ WEBROOT_REDIRECT }};
|
||||
try_files $uri {{ WEBROOT_REDIRECT }};
|
||||
{% else %}
|
||||
return 404;
|
||||
try_files $uri =404;
|
||||
{% endif %}
|
||||
}
|
||||
{% endif %}
|
||||
|
@ -2,11 +2,18 @@
|
||||
|
||||
import jinja2
|
||||
import os
|
||||
|
||||
convert = lambda src, dst, args: open(dst, "w").write(jinja2.Template(open(src).read()).render(**args))
|
||||
import logging as log
|
||||
import sys
|
||||
|
||||
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
|
||||
with open("/etc/resolv.conf") as handle:
|
||||
content = handle.read().split()
|
||||
|
BIN
core/nginx/static/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 6.1 KiB |
BIN
core/nginx/static/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
core/nginx/static/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
9
core/nginx/static/browserconfig.xml
Normal file
@ -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>
|
BIN
core/nginx/static/favicon-16x16.png
Normal file
After Width: | Height: | Size: 978 B |
BIN
core/nginx/static/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
core/nginx/static/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
core/nginx/static/mstile-150x150.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
25
core/nginx/static/safari-pinned-tab.svg
Normal file
@ -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 |
19
core/nginx/static/site.webmanifest
Normal file
@ -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 tenacity
|
||||
import multiprocessing
|
||||
import logging as log
|
||||
import sys
|
||||
|
||||
from tenacity import retry
|
||||
from podop import run_server
|
||||
|
||||
log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING"))
|
||||
|
||||
def start_podop():
|
||||
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/§"),
|
||||
("alias", "url", "http://admin/internal/postfix/alias/§"),
|
||||
("domain", "url", "http://admin/internal/postfix/domain/§"),
|
||||
@ -23,11 +27,24 @@ def start_podop():
|
||||
("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
|
||||
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")
|
||||
|
@ -3,10 +3,6 @@
|
||||
# these few settings must however be configured before starting the mail
|
||||
# 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
|
||||
###################################
|
||||
@ -151,3 +147,6 @@ REAL_IP_FROM=
|
||||
|
||||
# choose wether mailu bounces (no) or rejects (yes) mail when recipient is unknown (value: yes, no)
|
||||
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
|
||||
``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
|
||||
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
|
||||
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
|
||||
-----------------------
|
||||
|
||||
|
39
docs/faq.rst
@ -204,6 +204,41 @@ correct syntax. The following file names will be taken as override configuration
|
||||
|
||||
*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 Nextcloud’s 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
|
||||
.. _`Dovecot`: https://wiki.dovecot.org/ConfigFile
|
||||
.. _`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
|
||||
.. _`520`: https://github.com/Mailu/Mailu/issues/520
|
||||
.. _`591`: https://github.com/Mailu/Mailu/issues/591
|
||||
.. _`575`: https://github.com/Mailu/Mailu/issues/575
|
||||
|
||||
Technical issues
|
||||
----------------
|
||||
@ -304,7 +340,7 @@ See also :ref:`external_certs`.
|
||||
*Issue reference:* `426`_, `615`_.
|
||||
|
||||
How do I activate DKIM and DMARC?
|
||||
```````````````````````
|
||||
`````````````````````````````````
|
||||
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).
|
||||
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`_.
|
||||
|
||||
|
||||
.. _`troubleshooting tag`: https://github.com/Mailu/Mailu/issues?utf8=%E2%9C%93&q=label%3Afaq%2Ftroubleshooting
|
||||
.. _`85`: https://github.com/Mailu/Mailu/issues/85
|
||||
.. _`102`: https://github.com/Mailu/Mailu/issues/102
|
||||
|
@ -1,12 +1,21 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
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
|
||||
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
|
||||
logger.info("Starting the update daemon")
|
||||
os.system("freshclam -d -c 6")
|
||||
|
||||
# Run clamav
|
||||
logger.info("Starting clamav")
|
||||
os.system("clamd")
|
||||
|
@ -5,13 +5,31 @@ import os
|
||||
import socket
|
||||
import glob
|
||||
import tenacity
|
||||
import logging as log
|
||||
import sys
|
||||
|
||||
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
|
||||
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"))
|
||||
|
||||
if "HOST_REDIS" not in os.environ: os.environ["HOST_REDIS"] = "redis"
|
||||
|
@ -2,8 +2,16 @@
|
||||
|
||||
import jinja2
|
||||
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")
|
||||
|
||||
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)
|
||||
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 os
|
||||
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
|
||||
os.environ["FRONT_ADDRESS"] = os.environ.get("FRONT_ADDRESS", "front")
|
||||
|
@ -2,8 +2,15 @@
|
||||
|
||||
import os
|
||||
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))
|
||||
|
||||
@ -14,4 +21,4 @@ os.system("mkdir -p /data/gpg")
|
||||
os.system("chown -R www-data:www-data /data")
|
||||
|
||||
# Run apache
|
||||
os.execv("/usr/local/bin/apache2-foreground", ["apache2-foreground"])
|
||||
os.execv("/usr/local/bin/apache2-foreground", ["apache2-foreground"])
|
||||
|