Merge remote-tracking branch 'upstream/master' into feat-psql-support

master
Ionut Filip 6 years ago
commit 50343f354e

@ -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))
@ -78,6 +77,8 @@ v1.6.0 - unreleased
- Enhancement: Added regex validation for alias username ([#764](https://github.com/Mailu/Mailu/issues/764))
- 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 +111,9 @@ v1.6.0 - unreleased
- Bug: Error when trying to log in with an account without domain ([#585](https://github.com/Mailu/Mailu/issues/585))
- 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 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
-------------------

@ -267,10 +267,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'))

@ -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 = [

@ -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"])

@ -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()

@ -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

@ -91,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
-----------------------

@ -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"])

@ -161,6 +161,9 @@ 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
###################################
# Database settings
###################################

@ -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))

Loading…
Cancel
Save