Merge branch 'master' of https://github.com/Mailu/Mailu into HEAD

main
Florent Daigniere 2 years ago
commit 3721a6aa02

@ -126,7 +126,7 @@ jobs:
password: ${{ secrets.Docker_Password }}
- name: Helper to convert docker org to lowercase
id: string
uses: ASzc/change-string-case-action@v2
uses: ASzc/change-string-case-action@v5
with:
string: ${{ github.repository_owner }}
- name: Build all docker images
@ -182,7 +182,7 @@ jobs:
password: ${{ secrets.Docker_Password }}
- name: Helper to convert docker org to lowercase
id: string
uses: ASzc/change-string-case-action@v2
uses: ASzc/change-string-case-action@v5
with:
string: ${{ github.repository_owner }}
- name: Build all docker images
@ -244,7 +244,7 @@ jobs:
password: ${{ secrets.Docker_Password }}
- name: Helper to convert docker org to lowercase
id: string
uses: ASzc/change-string-case-action@v2
uses: ASzc/change-string-case-action@v5
with:
string: ${{ github.repository_owner }}
- name: Build all docker images
@ -307,7 +307,7 @@ jobs:
password: ${{ secrets.Docker_Password }}
- name: Helper to convert docker org to lowercase
id: string
uses: ASzc/change-string-case-action@v2
uses: ASzc/change-string-case-action@v5
with:
string: ${{ github.repository_owner }}
- name: Build all docker images
@ -340,7 +340,7 @@ jobs:
strategy:
fail-fast: false
matrix:
target: ["core", "fetchmail", "filters", "snappymail", "roundcube", "webdav"]
target: ["core", "fetchmail", "filters", "webmail", "webdav"]
time: ["2"]
include:
- target: "filters"
@ -370,7 +370,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Helper to convert docker org to lowercase
id: string
uses: ASzc/change-string-case-action@v2
uses: ASzc/change-string-case-action@v5
with:
string: ${{ github.repository_owner }}
- name: Install python packages
@ -394,7 +394,7 @@ jobs:
strategy:
fail-fast: false
matrix:
target: ["setup", "docs", "fetchmail", "roundcube", "admin", "traefik-certdumper", "radicale", "clamav", "rspamd", "postfix", "dovecot", "unbound", "nginx", "snappymail"]
target: ["setup", "docs", "fetchmail", "webmail", "admin", "traefik-certdumper", "radicale", "clamav", "rspamd", "postfix", "dovecot", "unbound", "nginx"]
steps:
- uses: actions/checkout@v3
- name: Retrieve global variables
@ -416,7 +416,7 @@ jobs:
password: ${{ secrets.Docker_Password }}
- name: Helper to convert docker org to lowercase
id: string
uses: ASzc/change-string-case-action@v2
uses: ASzc/change-string-case-action@v5
with:
string: ${{ github.repository_owner }}
- name: Push image to Docker
@ -439,7 +439,7 @@ jobs:
strategy:
fail-fast: false
matrix:
target: ["setup", "docs", "fetchmail", "roundcube", "admin", "traefik-certdumper", "radicale", "clamav", "rspamd", "postfix", "dovecot", "unbound", "nginx", "snappymail"]
target: ["setup", "docs", "fetchmail", "webmail", "admin", "traefik-certdumper", "radicale", "clamav", "rspamd", "postfix", "dovecot", "unbound", "nginx"]
steps:
- uses: actions/checkout@v3
- name: Retrieve global variables
@ -461,7 +461,7 @@ jobs:
password: ${{ secrets.Docker_Password }}
- name: Helper to convert docker org to lowercase
id: string
uses: ASzc/change-string-case-action@v2
uses: ASzc/change-string-case-action@v5
with:
string: ${{ github.repository_owner }}
- name: Push image to Docker

@ -22,7 +22,7 @@ Main features include:
- **Web access**, multiple Webmails and administration interface
- **User features**, aliases, auto-reply, auto-forward, fetched accounts
- **Admin features**, global admins, announcements, per-domain delegation, quotas
- **Security**, enforced TLS, DANE, MTA-STS, Letsencrypt!, outgoing DKIM, anti-virus scanner
- **Security**, enforced TLS, DANE, MTA-STS, Letsencrypt!, outgoing DKIM, anti-virus scanner, [Snuffleupagus](https://github.com/jvoisin/snuffleupagus/)
- **Antispam**, auto-learn, greylisting, DMARC and SPF, anti-spoofing
- **Freedom**, all FOSS components, no tracker included

@ -12,10 +12,12 @@ def fetch_list():
"id": fetch.id,
"tls": fetch.tls,
"keep": fetch.keep,
"scan": fetch.scan,
"user_email": fetch.user_email,
"protocol": fetch.protocol,
"host": fetch.host,
"port": fetch.port,
"folders": fetch.folders,
"username": fetch.username,
"password": fetch.password
} for fetch in models.Fetch.query.all()

@ -2,7 +2,6 @@
"""
import os
import smtplib
import json
from datetime import date
@ -420,14 +419,19 @@ class Email(object):
def sendmail(self, subject, body):
""" send an email to the address """
f_addr = f'{app.config["POSTMASTER"]}@{idna.encode(app.config["DOMAIN"]).decode("ascii")}'
with smtplib.SMTP(app.config['HOST_AUTHSMTP'], port=10025) as smtp:
to_address = f'{self.localpart}@{idna.encode(self.domain_name).decode("ascii")}'
msg = text.MIMEText(body)
msg['Subject'] = subject
msg['From'] = f_addr
msg['To'] = to_address
smtp.sendmail(f_addr, [to_address], msg.as_string())
try:
f_addr = f'{app.config["POSTMASTER"]}@{idna.encode(app.config["DOMAIN"]).decode("ascii")}'
ip, port = app.config['HOST_LMTP'].rsplit(':')
with smtplib.LMTP(ip, port=port) as lmtp:
to_address = f'{self.localpart}@{idna.encode(self.domain_name).decode("ascii")}'
msg = text.MIMEText(body)
msg['Subject'] = subject
msg['From'] = f_addr
msg['To'] = to_address
lmtp.sendmail(f_addr, [to_address], msg.as_string())
return True
except smtplib.SMTPException:
return False
@classmethod
def resolve_domain(cls, email):
@ -772,6 +776,8 @@ class Fetch(Base):
username = db.Column(db.String(255), nullable=False)
password = db.Column(db.String(255), nullable=False)
keep = db.Column(db.Boolean, nullable=False, default=False)
scan = db.Column(db.Boolean, nullable=False, default=False)
folders = db.Column(CommaSeparatedList, nullable=True, default=list)
last_check = db.Column(db.DateTime, nullable=True)
error = db.Column(db.String(1023), nullable=True)

@ -1,7 +1,7 @@
from mailu.sso import sso
import flask
@sso.route('/language/<language>', methods=['POST'])
@sso.route('/language/<language>', methods=['GET','POST'])
def set_language(language=None):
if language:
flask.session['language'] = language

@ -41,6 +41,16 @@ class MultipleEmailAddressesVerify(object):
if not pattern.match(field.data.replace(" ", "")):
raise validators.ValidationError(self.message)
class MultipleFoldersVerify(object):
""" Ensure that we have CSV formated data """
def __init__(self,message=_('Invalid list of folders.')):
self.message = message
def __call__(self, form, field):
pattern = re.compile(r'^\w+(\s*,\s*\w+)*$')
if not pattern.match(field.data.replace(" ", "")):
raise validators.ValidationError(self.message)
class ConfirmationForm(flask_wtf.FlaskForm):
submit = fields.SubmitField(_('Confirm'))
@ -165,11 +175,13 @@ class FetchForm(flask_wtf.FlaskForm):
('imap', 'IMAP'), ('pop3', 'POP3')
])
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'))
port = fields.IntegerField(_('TCP port'), [validators.DataRequired(), validators.NumberRange(min=0, max=65535)], default=993)
tls = fields.BooleanField(_('Enable TLS'), default=True)
username = fields.StringField(_('Username'), [validators.DataRequired()])
password = fields.PasswordField(_('Password'))
keep = fields.BooleanField(_('Keep emails on the server'))
scan = fields.BooleanField(_('Rescan emails locally'))
folders = fields.StringField(_('Folders to fetch on the server'), [validators.Optional(), MultipleFoldersVerify()], default='INBOX,Junk')
submit = fields.SubmitField(_('Submit'))

@ -24,6 +24,8 @@
{%- call macros.card(title="Settings") %}
{{ macros.form_field(form.keep) }}
{{ macros.form_field(form.scan) }}
{{ macros.form_field(form.folders) }}
{%- endcall %}
{{ macros.form_field(form.submit) }}

@ -20,6 +20,8 @@
<th>{% trans %}Endpoint{% endtrans %}</th>
<th>{% trans %}Username{% endtrans %}</th>
<th>{% trans %}Keep emails{% endtrans %}</th>
<th>{% trans %}Rescan emails{% endtrans %}</th>
<th>{% trans %}Folders{% endtrans %}</th>
<th>{% trans %}Last check{% endtrans %}</th>
<th>{% trans %}Status{% endtrans %}</th>
<th>{% trans %}Created{% endtrans %}</th>
@ -36,6 +38,8 @@
<td>{{ fetch.protocol }}{{ 's' if fetch.tls else '' }}://{{ fetch.host }}:{{ fetch.port }}</td>
<td>{{ fetch.username }}</td>
<td data-sort="{{ fetch.keep }}">{% if fetch.keep %}{% trans %}yes{% endtrans %}{% else %}{% trans %}no{% endtrans %}{% endif %}</td>
<td data-sort="{{ fetch.scan }}">{% if fetch.scan %}{% trans %}yes{% endtrans %}{% else %}{% trans %}no{% endtrans %}{% endif %}</td>
<td>{{ fetch.folders | join(',') }}</td>
<td>{{ fetch.last_check | format_datetime or '-' }}</td>
<td>{{ fetch.error or '-' }}</td>
<td data-sort="{{ fetch.created_at or '0000-00-00' }}">{{ fetch.created_at | format_date }}</td>

@ -21,8 +21,9 @@ def announcement():
form = forms.AnnouncementForm()
if form.validate_on_submit():
for user in models.User.query.all():
user.sendmail(form.announcement_subject.data,
form.announcement_body.data)
if not user.sendmail(form.announcement_subject.data,
form.announcement_body.data):
flask.flash('Failed to send to %s' % user.email, 'error')
# Force-empty the form
form.announcement_subject.data = ''
form.announcement_body.data = ''

@ -1,4 +1,4 @@
from mailu import models
from mailu import models, utils
from mailu.ui import ui, forms, access
from flask import current_app as app
@ -28,9 +28,12 @@ def fetch_create(user_email):
user = models.User.query.get(user_email) or flask.abort(404)
form = forms.FetchForm()
form.password.validators = [wtforms.validators.DataRequired()]
utils.formatCSVField(form.folders)
if form.validate_on_submit():
fetch = models.Fetch(user=user)
form.populate_obj(fetch)
if form.folders.data:
fetch.folders = form.folders.data.replace(' ','').split(',')
models.db.session.add(fetch)
models.db.session.commit()
flask.flash('Fetch configuration created')
@ -46,10 +49,13 @@ def fetch_edit(fetch_id):
flask.abort(404)
fetch = models.Fetch.query.get(fetch_id) or flask.abort(404)
form = forms.FetchForm(obj=fetch)
utils.formatCSVField(form.folders)
if form.validate_on_submit():
if not form.password.data:
form.password.data = fetch.password
form.populate_obj(fetch)
if form.folders.data:
fetch.folders = form.folders.data.replace(' ','').split(',')
models.db.session.commit()
flask.flash('Fetch configuration updated')
return flask.redirect(

@ -64,10 +64,11 @@ def user_edit(user_email):
form.quota_bytes.validators = [
wtforms.validators.NumberRange(max=max_quota_bytes)]
if form.validate_on_submit():
if msg := utils.isBadOrPwned(form):
flask.flash(msg, "error")
return flask.render_template('user/edit.html', form=form, user=user,
domain=user.domain, max_quota_bytes=max_quota_bytes)
if form.pw.data:
if msg := utils.isBadOrPwned(form):
flask.flash(msg, "error")
return flask.render_template('user/edit.html', form=form, user=user,
domain=user.domain, max_quota_bytes=max_quota_bytes)
form.populate_obj(user)
if form.pw.data:
user.set_password(form.pw.data)
@ -99,11 +100,7 @@ def user_settings(user_email):
user_email_or_current = user_email or flask_login.current_user.email
user = models.User.query.get(user_email_or_current) or flask.abort(404)
form = forms.UserSettingsForm(obj=user)
if isinstance(form.forward_destination.data,str):
data = form.forward_destination.data.replace(" ","").split(",")
else:
data = form.forward_destination.data
form.forward_destination.data = ", ".join(data)
utils.formatCSVField(form.forward_destination)
if form.validate_on_submit():
form.forward_destination.data = form.forward_destination.data.replace(" ","").split(",")
form.populate_obj(user)

@ -518,3 +518,10 @@ def isBadOrPwned(form):
if breaches > 0:
return f"This password appears in {breaches} data breaches! It is not unique; please change it."
return None
def formatCSVField(field):
if isinstance(field.data,str):
data = field.data.replace(" ","").split(",")
else:
data = field.data
field.data = ", ".join(data)

@ -0,0 +1,25 @@
""" Add fetch.scan and fetch.folders
Revision ID: f4f0f89e0047
Revises: 8f9ea78776f4
Create Date: 2022-11-13 16:29:01.246509
"""
# revision identifiers, used by Alembic.
revision = 'f4f0f89e0047'
down_revision = '8f9ea78776f4'
from alembic import op
import sqlalchemy as sa
import mailu
def upgrade():
with op.batch_alter_table('fetch') as batch:
batch.add_column(sa.Column('scan', sa.Boolean(), nullable=False, server_default=sa.sql.expression.false()))
batch.add_column(sa.Column('folders', mailu.models.CommaSeparatedList(), nullable=True))
def downgrade():
with op.batch_alter_table('fetch') as batch:
batch.drop_column('fetch', 'folders')
batch.drop_column('fetch', 'scan')

@ -2,8 +2,15 @@
import os
import logging as log
from pwd import getpwnam
import sys
os.system("chown mailu:mailu -R /dkim")
os.system("find /data | grep -v /fetchmail | xargs -n1 chown mailu:mailu")
mailu_id = getpwnam('mailu')
os.setgid(mailu_id.pw_gid)
os.setuid(mailu_id.pw_uid)
log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "INFO"))
os.system("flask mailu advertise")

@ -1,22 +1,21 @@
# syntax=docker/dockerfile-upstream:1.4.3
# base system image (intermediate)
ARG DISTRO=alpine:3.16.2
ARG DISTRO=alpine:3.16.3
FROM $DISTRO as system
ENV TZ=Etc/UTC LANG=C.UTF-8
ARG MAILU_UID=1000
ARG MAILU_GID=1000
ARG TARGETPLATFORM
RUN set -euxo pipefail \
; addgroup -Sg ${MAILU_GID} mailu \
; adduser -Sg ${MAILU_UID} -G mailu -h /app -g "mailu app" -s /bin/bash mailu \
; apk add --no-cache bash ca-certificates curl python3 tzdata \
; apk add --no-cache bash ca-certificates curl python3 tzdata libcap \
; machine="$(uname -m)" \
; ! [[ "${TARGETPLATFORM}" != linux/arm/v7 && \( "${machine}" == x86_64 || "${machine}" == armv8* || "${machine}" == aarch64 \) ]] \
|| apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing hardened-malloc
; ! [[ "${machine}" == x86_64 ]] \
|| apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing hardened-malloc==11-r0
ENV LD_PRELOAD=/usr/lib/libhardened_malloc.so
ENV CXXFLAGS="-g -O2 -fdebug-prefix-map=/app=. -fstack-protector-strong -Wformat -Werror=format-security -fstack-clash-protection -fexceptions"
@ -50,28 +49,38 @@ ENV PATH="${VIRTUAL_ENV}/bin:${PATH}"
COPY requirements-${MAILU_DEPS}.txt ./
COPY libs/ libs/
RUN set -euxo pipefail \
; pip install -r requirements-${MAILU_DEPS}.txt || \
{ \
machine="$(uname -m)" \
; deps="build-base gcc libffi-dev python3-dev" \
; [[ "${machine}" != x86_64 ]] && \
deps="${deps} cargo git libressl-dev mariadb-connector-c-dev postgresql-dev" \
; apk add --virtual .build-deps ${deps} \
; [[ "${machine}" == armv7* ]] && \
mkdir -p /root/.cargo/registry/index && \
git clone --bare https://github.com/rust-lang/crates.io-index.git /root/.cargo/registry/index/github.com-1285ae84e5963aae \
; pip install -r requirements-${MAILU_DEPS}.txt \
; apk del -r .build-deps \
; rm -rf /root/.cargo /tmp/*.pem \
; } \
; rm -rf /root/.cache
ARG SNUFFLEUPAGUS_VERSION=0.8.3
ENV SNUFFLEUPAGUS_URL https://github.com/jvoisin/snuffleupagus/archive/refs/tags/v$SNUFFLEUPAGUS_VERSION.tar.gz
RUN set -euxo pipefail \
; machine="$(uname -m)" \
; deps="build-base gcc libffi-dev python3-dev" \
; [[ "${machine}" != x86_64 ]] && \
deps="${deps} cargo git libressl-dev mariadb-connector-c-dev postgresql-dev" \
; apk add --virtual .build-deps ${deps} \
; [[ "${machine}" == armv7* ]] && \
mkdir -p /root/.cargo/registry/index && \
git clone --bare https://github.com/rust-lang/crates.io-index.git /root/.cargo/registry/index/github.com-1285ae84e5963aae \
; pip install -r requirements-${MAILU_DEPS}.txt \
; curl -sL ${SNUFFLEUPAGUS_URL} | tar xz \
; cd snuffleupagus-$SNUFFLEUPAGUS_VERSION \
; rm -rf src/tests/*php7*/ src/tests/*session*/ src/tests/broken_configuration/ src/tests/*cookie* src/tests/upload_validation/ \
; apk add --virtual .build-deps php81-dev php81-cgi php81-simplexml php81-xml pcre-dev build-base php81-pear php81-openssl re2c \
; ln -s /usr/bin/phpize81 /usr/bin/phpize \
; ln -s /usr/bin/pecl81 /usr/bin/pecl \
; ln -s /usr/bin/php-config81 /usr/bin/php-config \
; ln -s /usr/bin/php81 /usr/bin/php \
; pecl install vld-beta \
; make -j $(grep -c processor /proc/cpuinfo) release \
; cp src/.libs/snuffleupagus.so /app \
; rm -rf /root/.cargo /tmp/*.pem /root/.cache
# base mailu image
FROM system
COPY --from=build /app/venv/ /app/venv/
COPY --chown=root:root --from=build /app/snuffleupagus.so /usr/lib/php81/modules/
RUN setcap 'cap_net_bind_service=+ep' /app/venv/bin/gunicorn
ENV VIRTUAL_ENV=/app/venv
ENV PATH="${VIRTUAL_ENV}/bin:${PATH}"

@ -23,7 +23,7 @@ itsdangerous
limits
marshmallow
marshmallow-sqlalchemy
mysql-connector-python
mysql-connector-python==8.0.29
passlib
psycopg2-binary
Pygments

@ -41,7 +41,7 @@ MarkupSafe==2.1.1
marshmallow==3.18.0
marshmallow-sqlalchemy==0.28.1
multidict==6.0.2
mysql-connector-python==8.0.31
mysql-connector-python==8.0.29
packaging==21.3
passlib==1.7.4
podop @ file:///app/libs/podop

@ -10,5 +10,5 @@ RUN echo $VERSION >/version
HEALTHCHECK CMD true
USER app
USER mailu
CMD ["/bin/bash", "-c", "sleep infinity"]

@ -33,4 +33,7 @@ while True:
log.warning("Admin is not up just yet, retrying in 1 second")
# Run rspamd
os.execv("/usr/sbin/rspamd", ["rspamd", "-i", "-f"])
os.system("mkdir -m 755 -p /run/rspamd")
os.system("chown rspamd:rspamd /run/rspamd")
os.system("find /var/lib/rspamd | grep -v /filter | xargs -n1 chown rspamd:rspamd")
os.execv("/usr/sbin/rspamd", ["rspamd", "-f", "-u", "rspamd", "-g", "rspamd"])

@ -28,7 +28,7 @@ Main features include:
- **Web access**, multiple Webmails and administration interface
- **User features**, aliases, auto-reply, auto-forward, fetched accounts
- **Admin features**, global admins, announcements, per-domain delegation, quotas
- **Security**, enforced TLS, DANE, MTA-STS, Letsencrypt!, outgoing DKIM, anti-virus scanner
- **Security**, enforced TLS, DANE, MTA-STS, Letsencrypt!, outgoing DKIM, anti-virus scanner, Snuffleupagus
- **Antispam**, auto-learn, greylisting, DMARC and SPF, anti-spoofing
- **Freedom**, all FOSS components, no tracker included

@ -157,7 +157,11 @@ You can add a fetched account by clicking on the `Add an account` button on the
* Keep emails on the server. When ticked, retains the email message in the email account after retrieving it.
Click the submit button to apply settings. With the default polling interval, fetchmail will start polling the email account after 10 minutes.
* Scan emails. When ticked, all the fetched emails will go through the local filters (rspamd, clamav, ...).
* Folders. A comma separated list of folders to fetch from the server. This is optional, by default only the INBOX will be pulled.
Click the submit button to apply settings. With the default polling interval, fetchmail will start polling the email account after ``FETCHMAIL_DELAY``.
Authentication tokens

@ -2,11 +2,14 @@
import time
import os
from pathlib import Path
from pwd import getpwnam
import tempfile
import shlex
import subprocess
import re
import requests
from socrate import system
import sys
import traceback
@ -14,6 +17,7 @@ import traceback
FETCHMAIL = """
fetchmail -N \
--idfile /data/fetchids --uidl \
--pidfile /dev/shm/fetchmail.pid \
--sslcertck --sslcertpath /etc/ssl/certs \
-f {}
"""
@ -24,7 +28,9 @@ poll "{host}" proto {protocol} port {port}
user "{username}" password "{password}"
is "{user_email}"
smtphost "{smtphost}"
{folders}
{options}
{lmtp}
"""
@ -48,26 +54,37 @@ def fetchmail(fetchmailrc):
def run(debug):
try:
fetches = requests.get("http://" + os.environ.get("HOST_ADMIN", "admin") + "/internal/fetch").json()
smtphost, smtpport = extract_host_port(os.environ.get("HOST_SMTP", "smtp"), None)
os.environ["SMTP_ADDRESS"] = system.get_host_address_from_environment("SMTP", "smtp")
os.environ["ADMIN_ADDRESS"] = system.get_host_address_from_environment("ADMIN", "admin")
fetches = requests.get(f"http://{os.environ['ADMIN_ADDRESS']}/internal/fetch").json()
smtphost, smtpport = extract_host_port(os.environ["SMTP_ADDRESS"], None)
if smtpport is None:
smtphostport = smtphost
else:
smtphostport = "%s/%d" % (smtphost, smtpport)
os.environ["LMTP_ADDRESS"] = system.get_host_address_from_environment("LMTP", "imap:2525")
lmtphost, lmtpport = extract_host_port(os.environ["LMTP_ADDRESS"], None)
if lmtpport is None:
lmtphostport = lmtphost
else:
lmtphostport = "%s/%d" % (lmtphost, lmtpport)
for fetch in fetches:
fetchmailrc = ""
options = "options antispam 501, 504, 550, 553, 554"
options += " ssl" if fetch["tls"] else ""
options += " keep" if fetch["keep"] else " fetchall"
folders = "folders %s" % ((','.join('"' + item + '"' for item in fetch['folders'])) if fetch['folders'] else '"INBOX"')
fetchmailrc += RC_LINE.format(
user_email=escape_rc_string(fetch["user_email"]),
protocol=fetch["protocol"],
host=escape_rc_string(fetch["host"]),
port=fetch["port"],
smtphost=smtphostport,
smtphost=smtphostport if fetch['scan'] else lmtphostport,
username=escape_rc_string(fetch["username"]),
password=escape_rc_string(fetch["password"]),
options=options
options=options,
folders=folders,
lmtp='' if fetch['scan'] else 'lmtp',
)
if debug:
print(fetchmailrc)
@ -86,14 +103,21 @@ def run(debug):
user_info in error_message):
print(error_message)
finally:
requests.post("http://" + os.environ.get("HOST_ADMIN", "admin") + "/internal/fetch/{}".format(fetch["id"]),
json=error_message.split("\n")[0]
requests.post("http://{}/internal/fetch/{}".format(os.environ['ADMIN_ADDRESS'],fetch['id']),
json=error_message.split('\n')[0]
)
except Exception:
traceback.print_exc()
if __name__ == "__main__":
id_fetchmail = getpwnam('fetchmail')
Path('/data/fetchids').touch()
os.chown("/data/fetchids", id_fetchmail.pw_uid, id_fetchmail.pw_gid)
os.chown("/data/", id_fetchmail.pw_uid, id_fetchmail.pw_gid)
os.chmod("/data/fetchids", 0o700)
os.setgid(id_fetchmail.pw_gid)
os.setuid(id_fetchmail.pw_uid)
while True:
delay = int(os.environ.get("FETCHMAIL_DELAY", 60))
print("Sleeping for {} seconds".format(delay))

@ -1,24 +1,21 @@
ARG DISTRO=alpine:3.14.5
FROM $DISTRO
ARG VERSION
ENV TZ Etc/UTC
# syntax=docker/dockerfile-upstream:1.4.3
# setup image
FROM base
ARG VERSION=local
LABEL version=$VERSION
RUN mkdir -p /app
WORKDIR /app
COPY requirements.txt requirements.txt
RUN apk add --no-cache curl python3 py3-pip \
&& pip3 install -r requirements.txt
COPY server.py ./server.py
COPY main.py ./main.py
COPY flavors /data/flavors
COPY templates /data/templates
COPY static ./static
COPY server.py ./server.py
COPY main.py ./main.py
RUN echo $VERSION >> /version
EXPOSE 80/tcp
HEALTHCHECK --start-period=350s CMD curl -skfLo /dev/null http://localhost/
USER mailu
CMD gunicorn -w 4 -b :80 --access-logfile - --error-logfile - --preload main:app
RUN echo $VERSION >> /version

@ -157,8 +157,11 @@ services:
env_file: {{ env }}
volumes:
- "{{ root }}/data/fetchmail:/data"
{% if resolver_enabled %}
depends_on:
- admin
- smtp
- imap
{% if resolver_enabled %}
- resolver
dns:
- {{ dns }}
@ -168,7 +171,7 @@ services:
# Webmail
{% if webmail_type != 'none' %}
webmail:
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}{{ webmail_type }}:${MAILU_VERSION:-{{ version }}}
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}webmail:${MAILU_VERSION:-{{ version }}}
restart: always
env_file: {{ env }}
volumes:

@ -119,7 +119,7 @@ services:
{% if webmail_type != 'none' %}
webmail:
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}{{ webmail_type }}:${MAILU_VERSION:-{{ version }}}
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}webmail:${MAILU_VERSION:-{{ version }}}
env_file: {{ env }}
volumes:
- "{{ root }}/webmail:/data"

@ -1,12 +0,0 @@
Flask==1.0.2
Flask-Bootstrap==3.3.7.1
gunicorn==19.9.0
redis==3.2.1
Jinja2==3.0.3
MarkupSafe==2.1.0
Werkzeug==2.0.3
click==8.0.3
dominate==2.6.0
itsdangerous==2.0.1
redis==3.2.1
visitor==0.1.3

@ -36,8 +36,7 @@ group "default" {
"imap",
"smtp",
"snappymail",
"roundcube",
"webmail",
"antivirus",
"fetchmail",
@ -107,6 +106,9 @@ target "docs" {
target "setup" {
inherits = ["defaults"]
context = "setup/"
contexts = {
base = "target:base"
}
tags = tag("setup")
}
@ -169,24 +171,15 @@ target "smtp" {
}
# -----------------------------------------------------------------------------------------
# Webmail images
# Webmail image
# -----------------------------------------------------------------------------------------
target "snappymail" {
target "webmail" {
inherits = ["defaults"]
context = "webmails/snappymail/"
context = "webmails/"
contexts = {
base = "target:base"
}
tags = tag("snappymail")
}
target "roundcube" {
inherits = ["defaults"]
context = "webmails/roundcube/"
contexts = {
base = "target:base"
}
tags = tag("roundcube")
tags = tag("webmail")
}
# -----------------------------------------------------------------------------------------

@ -0,0 +1,7 @@
# GTUBE should be blocked, see https://rspamd.com/doc/gtube_patterns.html
python3 tests/email_test.py "XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X"
if [ $? -eq 25 ]; then
exit 0
else
exit 1
fi

@ -1,106 +0,0 @@
# This file is auto-generated by the Mailu configuration wizard.
# Please read the documentation before attempting any change.
# Generated for compose flavor
version: '3.6'
services:
# External dependencies
redis:
image: redis:alpine
restart: always
volumes:
- "/mailu/redis:/data"
# Core services
front:
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}nginx:${MAILU_VERSION:-local}
restart: always
env_file: mailu.env
logging:
driver: json-file
ports:
- "127.0.0.1:80:80"
- "127.0.0.1:443:443"
- "127.0.0.1:25:25"
- "127.0.0.1:465:465"
- "127.0.0.1:587:587"
- "127.0.0.1:110:110"
- "127.0.0.1:995:995"
- "127.0.0.1:143:143"
- "127.0.0.1:993:993"
volumes:
- "/mailu/certs:/certs"
admin:
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}admin:${MAILU_VERSION:-local}
restart: always
env_file: mailu.env
volumes:
- "/mailu/data:/data"
- "/mailu/dkim:/dkim"
depends_on:
- redis
- resolver
dns:
- 192.168.203.254
imap:
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}dovecot:${MAILU_VERSION:-local}
restart: always
env_file: mailu.env
volumes:
- "/mailu/mail:/mail"
- "/mailu/overrides:/overrides"
depends_on:
- front
smtp:
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}postfix:${MAILU_VERSION:-local}
restart: always
env_file: mailu.env
volumes:
- "/mailu/overrides:/overrides"
depends_on:
- front
antispam:
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}rspamd:${MAILU_VERSION:-local}
restart: always
env_file: mailu.env
volumes:
- "/mailu/filter:/var/lib/rspamd"
- "/mailu/dkim:/dkim"
- "/mailu/overrides/rspamd:/etc/rspamd/override.d"
depends_on:
- front
# Optional services
resolver:
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}unbound:${MAILU_VERSION:-local}
env_file: mailu.env
restart: always
networks:
default:
ipv4_address: 192.168.203.254
# Webmail
webmail:
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}snappymail:${MAILU_VERSION:-local}
restart: always
env_file: mailu.env
volumes:
- "/mailu/webmail:/data"
depends_on:
- imap
networks:
default:
driver: bridge
ipam:
driver: default
config:
- subnet: 192.168.203.0/24

@ -1,138 +0,0 @@
# Mailu main configuration file
#
# Generated for compose flavor
#
# This file is autogenerated by the configuration management wizard.
# For a detailed list of configuration variables, see the documentation at
# https://mailu.io
###################################
# Common configuration variables
###################################
# Set this to the path where Mailu data and configuration is stored
# This variable is now set directly in `docker-compose.yml by the setup utility
# ROOT=/mailu
# Mailu version to run (1.0, 1.1, etc. or master)
#VERSION=master
# Set to a randomly generated 16 bytes string
SECRET_KEY=V5J4SHRYVW9PZIQU
# Address where listening ports should bind
# This variables are now set directly in `docker-compose.yml by the setup utility
# PUBLIC_IPV4= 127.0.0.1 (default: 127.0.0.1)
# PUBLIC_IPV6= (default: ::1)
# Subnet of the docker network. This should not conflict with any networks to which your system is connected. (Internal and external!)
SUBNET=192.168.203.0/24
# Main mail domain
DOMAIN=mailu.io
# Hostnames for this server, separated with comas
HOSTNAMES=localhost
# Postmaster local part (will append the main mail domain)
POSTMASTER=admin
# Choose how secure connections will behave (value: letsencrypt, cert, notls, mail, mail-letsencrypt)
TLS_FLAVOR=cert
# Authentication rate limit (per source IP address)
AUTH_RATELIMIT=10/minute;1000/hour
# Opt-out of statistics, replace with "True" to opt out
DISABLE_STATISTICS=False
###################################
# Optional features
###################################
# Expose the admin interface (value: true, false)
ADMIN=false
# Choose which webmail to run if any (values: roundcube, snappymail, none)
WEBMAIL=snappymail
# Dav server implementation (value: radicale, none)
WEBDAV=none
# Antivirus solution (value: clamav, none)
#ANTIVIRUS=none
#Antispam solution
ANTISPAM=none
###################################
# Mail settings
###################################
# Message size limit in bytes
# Default: accept messages up to 50MB
MESSAGE_SIZE_LIMIT=50000000
# Networks granted relay permissions
# Use this with care, all hosts in this networks will be able to send mail without authentication!
RELAYNETS=
# Will relay all outgoing mails if configured
RELAYHOST=
# Fetchmail delay
FETCHMAIL_DELAY=600
# Recipient delimiter, character used to delimiter localpart from custom address part
RECIPIENT_DELIMITER=+
# DMARC rua and ruf email
DMARC_RUA=admin
DMARC_RUF=admin
# Maildir Compression
# choose compression-method, default: none (value: gz, bz2, lz4, zstd)
COMPRESSION=
# change compression-level, default: 6 (value: 1-9)
COMPRESSION_LEVEL=
###################################
# Web settings
###################################
# Path to the admin interface if enabled
WEB_ADMIN=/admin
# Path to the webmail if enabled
WEB_WEBMAIL=/webmail
# Website name
SITENAME=Mailu
# Linked Website URL
WEBSITE=https://mailu.io
###################################
# Advanced settings
###################################
# Log driver for front service. Possible values:
# json-file (default)
# journald (On systemd platforms, useful for Fail2Ban integration)
# syslog (Non systemd platforms, Fail2Ban integration. Disables `docker-compose log` for front!)
# LOG_DRIVER=json-file
# Docker-compose project name, this will prepended to containers names.
COMPOSE_PROJECT_NAME=mailu
# Header to take the real ip from
REAL_IP_HEADER=
# IPs for nginx set_real_ip_from (CIDR list separated by commas)
REAL_IP_FROM=
# choose wether mailu bounces (no) or rejects (yes) mail when recipient is unknown (value: yes, no)
REJECT_UNLISTED_RECIPIENT=

@ -0,0 +1,10 @@
#!/bin/bash
IP="$(docker inspect webmail_webmail_1|jq -r '.[0].NetworkSettings.Networks.webmail_default.IPAddress')"
MAIN_RETURN_CODE=$(curl -I -so /dev/null -w "%{http_code}" http://$IP/)
[[ $MAIN_RETURN_CODE -ne 200 && $MAIN_RETURN_CODE -ne 302 ]] && echo "The default page of snappymail hasn't returned 200 but $MAIN_RETURN_CODE!" >>/dev/stderr && exit 1
[[ $(curl -I -so /dev/null -w "%{http_code}" http://$IP/?admin) -ne 403 ]] && echo "The admin of snappymail is not disabled!" >>/dev/stderr && exit 1
echo "Everything OK" >/dev/stderr
exit 0

@ -88,7 +88,7 @@ services:
# Webmail
webmail:
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}roundcube:${MAILU_VERSION:-local}
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}webmail:${MAILU_VERSION:-local}
restart: always
env_file: mailu.env
volumes:

@ -54,7 +54,7 @@ DISABLE_STATISTICS=False
ADMIN=false
# Choose which webmail to run if any (values: roundcube, snappymail, none)
WEBMAIL=roundcube
WEBMAIL=snappymail
# Dav server implementation (value: radicale, none)
WEBDAV=none

@ -0,0 +1 @@
Add an option so that emails fetched with fetchmail don't go through the filters (closes #1231)

@ -0,0 +1 @@
Make public announcement bypass the filters. They may still time-out before being sent if there is a large number of users.

@ -0,0 +1 @@
Fetchmail: Missing support for '*_ADDRESS' env vars

@ -0,0 +1 @@
Upgrade Snappymail to 2.21 and merge the webmail containers

@ -0,0 +1 @@
Upgrade to Alpine 3.16.3; Make setup, admin and rspamd run without root privs. Please ensure that your folder overrides/rspamd is owned by 1000:1000

@ -0,0 +1 @@
Add Snuffleupagus to protect webmails (a Suhosin replacement)

@ -0,0 +1 @@
Allow other folders to be synced by fetchmail

@ -0,0 +1,96 @@
# syntax=docker/dockerfile-upstream:1.4.3
FROM base
ARG VERSION
LABEL version=$VERSION
COPY snappymail/pubkey.asc /tmp/snappymail.asc
COPY roundcube/pubkey.asc /tmp/roundcube.asc
RUN set -euxo pipefail \
; apk add --no-cache \
nginx gpg gpg-agent \
php81 php81-fpm php81-mbstring php81-zip php81-xml php81-simplexml php81-pecl-apcu \
php81-dom php81-curl php81-exif gd php81-gd php81-iconv php81-intl php81-openssl \
php81-pdo_sqlite php81-pdo_mysql php81-pdo_pgsql php81-pdo php81-sodium libsodium php81-tidy php81-pecl-uuid \
php81-pspell php81-pecl-imagick php81-opcache php81-session php81-sockets php81-fileinfo \
aspell-uk aspell-ru aspell-fr aspell-de aspell-en \
; rm /etc/nginx/http.d/default.conf \
; rm /etc/php81/php-fpm.d/www.conf \
; ln -s /usr/bin/php81 /usr/bin/php \
; gpg --import /tmp/snappymail.asc \
; gpg --import /tmp/roundcube.asc \
; echo extension=snuffleupagus > /etc/php81/conf.d/snuffleupagus.ini \
; rm -f /tmp/roundcube.asc /tmp/snappymail.asc \
; mkdir -p /run/nginx /conf
# roundcube
ENV ROUNDCUBE_URL https://github.com/roundcube/roundcubemail/releases/download/1.5.3/roundcubemail-1.5.3-complete.tar.gz
ENV CARDDAV_URL https://github.com/mstilkerich/rcmcarddav/releases/download/v4.4.3/carddav-v4.4.3.tar.gz
RUN set -euxo pipefail \
; cd /var/www \
; curl -sLo /dev/shm/roundcube.tgz ${ROUNDCUBE_URL} \
; curl -sLo /dev/shm/roundcube.tgz.asc ${ROUNDCUBE_URL}.asc \
; gpg --status-fd 1 --verify /dev/shm/roundcube.tgz.asc \
; tar xzf /dev/shm/roundcube.tgz \
; curl -sL ${CARDDAV_URL} | tar xz \
; mv roundcubemail-* roundcube \
; mkdir -p /var/www/roundcube/config \
; mv carddav roundcube/plugins/ \
; cd roundcube \
; rm -rf CHANGELOG.md SECURITY.md INSTALL LICENSE README.md UPGRADING composer.json-dist installer composer.* \
; ln -sf index.php /var/www/roundcube/public_html/sso.php \
; rm -rf plugins/{autologon,example_addressbook,http_authentication,krb_authentication,new_user_identity,password,redundant_attachments,squirrelmail_usercopy,userinfo,virtuser_file,virtuser_query} \
; sed -i '/suhosin.session.encrypt/d;/mbstring\.func_overload/d' program/lib/Roundcube/bootstrap.php
COPY roundcube/config/config.inc.php /conf/
COPY roundcube/login/mailu.php /var/www/roundcube/plugins/mailu/
COPY roundcube/config/config.inc.carddav.php /var/www/roundcube/plugins/carddav/config.inc.php
# snappymail
ENV SNAPPYMAIL_URL https://github.com/the-djmaze/snappymail/releases/download/v2.21.3/snappymail-2.21.3.tar.gz
RUN set -euxo pipefail \
; mkdir /var/www/snappymail \
; cd /var/www/snappymail \
; curl -sLo /dev/shm/snappymail.tgz ${SNAPPYMAIL_URL} \
; curl -sLo /dev/shm/snappymail.tgz.asc ${SNAPPYMAIL_URL}.asc \
; gpg --status-fd 1 --verify /dev/shm/snappymail.tgz.asc \
; tar xzf /dev/shm/snappymail.tgz
# SnappyMail login
COPY snappymail/login/include.php /var/www/snappymail/
COPY snappymail/login/sso.php /var/www/snappymail/
# Parsed and moved at startup
COPY snappymail/defaults/application.ini /defaults/
COPY snappymail/defaults/default.json /defaults/
# set perms
RUN set -euxo pipefail \
; chmod -R a+rX /var/www/snappymail \
; chown -R root:root /var/www/snappymail \
; chown -R mailu:mailu /var/www/snappymail/data \
; chown -R root:root /var/www/roundcube/ \
; chown -R mailu:mailu /var/www/roundcube/temp /var/www/roundcube/logs \
; chmod -R a+rX /var/www/roundcube
# common
COPY start.py /
COPY php.ini /defaults/
COPY php-webmail.conf /etc/php81/php-fpm.d/
COPY nginx-webmail.conf /conf/
COPY snuffleupagus.rules /etc/snuffleupagus.rules.tpl
EXPOSE 80/tcp
VOLUME /data
VOLUME /overrides
CMD /start.py
HEALTHCHECK CMD curl -f -L http://localhost/ping || exit 1
RUN echo $VERSION >> /version

@ -2,7 +2,11 @@ server {
listen 80 default_server;
listen [::]:80 default_server;
root /var/www/webmail;
{% if WEBMAIL == 'roundcube' %}
root /var/www/{{ WEBMAIL }}/public_html;
{% else %}
root /var/www/{{ WEBMAIL }};
{% endif %}
include /etc/nginx/mime.types;
@ -16,6 +20,11 @@ server {
# set maximum body size to configured limit
client_max_body_size {{ MESSAGE_SIZE_LIMIT|int + 8388608 }};
fastcgi_hide_header X-Powered-By;
add_header X-Download-Options "noopen" always;
add_header X-Robots-Tag "none" always;
add_header X-Permitted-Cross-Domain-Policies "none" always;
add_header Referrer-Policy "no-referrer" always;
location / {
try_files $uri $uri/ /index.php$args;
@ -42,11 +51,11 @@ server {
{% endif %}
}
location ~ /\. {
location ~ (^|/)\. {
deny all;
}
location ^~ /data {
location ~* /(config|temp|logs|data) {
deny all;
}

@ -1,7 +1,7 @@
; Start a new pool named 'roundcube'.
; Start a new pool named 'php'.
; the variable $pool can be used in any directive and will be replaced by the
; pool name ('roundcube' here)
[roundcube]
; pool name ('php' here)
[php]
; Redirect worker stdout and stderr into main error log. If not set, stdout and
; stderr will be redirected to /dev/null according to FastCGI specs.
@ -11,8 +11,8 @@ catch_workers_output = 1
; Unix user/group of processes
; Note: The user is mandatory. If the group is not set, the default user's group
; will be used.
user = nginx
group = nginx
user = mailu
group = mailu
; The address on which to accept FastCGI requests.
; Valid syntaxes are:

@ -0,0 +1,15 @@
expose_php=Off
date.timezone={{ TZ }}
upload_max_filesize = {{ MAX_FILESIZE }}M
post_max_size = {{ MAX_FILESIZE }}M
session.auto_start=Off
mbstring.func_overload=Off
file_uploads=On
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT & ~E_NOTICE
display_errors=Off
log_errors=On
zlib.output_compression=Off
access.log = /dev/fd/2
error_log = /dev/fd/2
module=snuffleupagus.so
sp.configuration_file=/etc/snuffleupagus.rules

@ -1,58 +0,0 @@
# syntax=docker/dockerfile-upstream:1.4.3
#roundcube image
FROM base
ARG VERSION
LABEL version=$VERSION
RUN set -euxo pipefail \
; apk add --no-cache \
nginx gpg gpg-agent \
php81 php81-fpm php81-mbstring php81-zip php81-xml php81-simplexml \
php81-dom php81-curl php81-exif gd php81-gd php81-iconv php81-intl php81-openssl \
php81-pdo_sqlite php81-pdo_mysql php81-pdo_pgsql php81-pdo php81-sodium libsodium php81-tidy php81-pecl-uuid \
php81-pspell php81-pecl-imagick php81-opcache php81-session php81-sockets php81-fileinfo \
; rm /etc/nginx/http.d/default.conf \
; rm /etc/php81/php-fpm.d/www.conf \
; ln -s /usr/bin/php81 /usr/bin/php \
; mkdir -p /run/nginx \
; mkdir -p /conf
ENV ROUNDCUBE_URL https://github.com/roundcube/roundcubemail/releases/download/1.5.3/roundcubemail-1.5.3-complete.tar.gz
ENV CARDDAV_URL https://github.com/mstilkerich/rcmcarddav/releases/download/v4.4.3/carddav-v4.4.3.tar.gz
RUN set -euxo pipefail \
; cd /var/www \
; curl -sL ${ROUNDCUBE_URL} | tar xz \
; curl -sL ${CARDDAV_URL} | tar xz \
; mv roundcubemail-* webmail \
; mkdir -p /var/www/webmail/config \
; mv carddav webmail/plugins/ \
; cd webmail \
; rm -rf CHANGELOG.md SECURITY.md INSTALL LICENSE README.md UPGRADING composer.json-dist installer composer.* \
; ln -sf index.php /var/www/webmail/sso.php \
; chmod -R u+w,a+rX /var/www/webmail \
; chown -R nginx:nginx /var/www/webmail \
; rm -rf plugins/{autologon,example_addressbook,http_authentication,krb_authentication,new_user_identity,password,redundant_attachments,squirrelmail_usercopy,userinfo,virtuser_file,virtuser_query}
# nginx / PHP config files
COPY config/nginx-roundcube.conf /conf/
COPY config/php-roundcube.conf /etc/php81/php-fpm.d/roundcube.conf
COPY config/php.ini /conf/
COPY config/config.inc.php /conf/
COPY login/mailu.php /var/www/webmail/plugins/mailu/
COPY config/config.inc.carddav.php /var/www/webmail/plugins/carddav/config.inc.php
COPY start.py /
EXPOSE 80/tcp
VOLUME /data
VOLUME /overrides
CMD /start.py
HEALTHCHECK CMD curl -f -L http://localhost/ping || exit 1
RUN echo $VERSION >> /version

@ -4,8 +4,8 @@ $config = array();
// Generals
$config['db_dsnw'] = '{{ DB_DSNW }}';
$config['temp_dir'] = '/tmp/';
$config['des_key'] = '{{ SECRET_KEY }}';
$config['temp_dir'] = '/dev/shm/';
$config['des_key'] = '{{ ROUNDCUBE_KEY }}';
$config['cipher_method'] = 'AES-256-CBC';
$config['identities_level'] = 0;
$config['reply_all_mode'] = 1;

@ -1,63 +0,0 @@
server {
listen 80 default_server;
listen [::]:80 default_server;
root /var/www/webmail;
include /etc/nginx/mime.types;
# /dev/stdout (Default), <path>, off
access_log off;
# /dev/stderr (Default), <path>, debug, info, notice, warn, error, crit, alert, emerg
error_log /dev/stderr notice;
index index.php;
# set maximum body size to configured limit
client_max_body_size {{ MESSAGE_SIZE_LIMIT|int + 8388608 }};
location / {
try_files $uri $uri/ /index.php$args;
}
location ~ \.php$ {
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
if (!-f $document_root$fastcgi_script_name) {
return 404;
}
include /etc/nginx/fastcgi_params;
fastcgi_intercept_errors on;
fastcgi_index index.php;
fastcgi_keep_conn on;
fastcgi_pass unix:/var/run/php8-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
{% if WEB_WEBMAIL == '/' %}
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
{% else %}
fastcgi_param SCRIPT_NAME {{WEB_WEBMAIL}}/$fastcgi_script_name;
{% endif %}
}
location ~ /\. {
deny all;
}
location ^~ /data {
deny all;
}
location = /ping {
allow 127.0.0.1;
allow ::1;
deny all;
include /etc/nginx/fastcgi_params;
fastcgi_index index.php;
fastcgi_pass unix:/var/run/php8-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}

@ -1,8 +0,0 @@
expose_php=Off
date.timezone={{ TZ }}
upload_max_filesize = {{ MAX_FILESIZE }}M
post_max_size = {{ MAX_FILESIZE }}M
suhosin.session.encrypt=Off
session.auto_start=Off
mbstring.func_overload=Off
file_uploads=On

@ -18,13 +18,6 @@ class mailu extends rcube_plugin
$args['action'] = 'login';
}
$ua = $_SERVER['HTTP_USER_AGENT'];
$ra = $_SERVER['REMOTE_ADDR'];
if ($ua == 'health' and ($ra == '127.0.0.1' or $ra == '::1')) {
print('OK');
exit();
}
return $args;
}
@ -35,7 +28,7 @@ class mailu extends rcube_plugin
header('HTTP/1.0 403 Forbidden');
print('mailu sso failure');
} else {
header('Location: sso.php');
header('Location: sso.php', 302);
}
exit();
}
@ -54,19 +47,19 @@ class mailu extends rcube_plugin
{
$this->load_config();
$sso_logout_url = rcmail::get_instance()->config->get('sso_logout_url');
header('Location: ' . $sso_logout_url, true);
header('Location: ' . $sso_logout_url, true, 302);
exit();
}
function login($args)
{
header('Location: index.php');
header('Location: index.php', 302);
exit();
}
function login_failed($args)
{
header('Location: sso.php');
header('Location: sso.php', 302);
exit();
}

@ -0,0 +1,102 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
0dXJnhEe7eXkwBbfEzk=
=lBKd
-----END PGP PUBLIC KEY BLOCK-----

@ -1,54 +0,0 @@
# syntax=docker/dockerfile-upstream:1.4.3
#snappymail image
FROM base
ARG VERSION
LABEL version=$VERSION
RUN set -euxo pipefail \
; apk add --no-cache \
nginx curl \
php81 php81-fpm php81-mbstring php81-zip php81-xml php81-simplexml \
php81-dom php81-curl php81-exif gd php81-gd php81-iconv php81-intl php81-openssl \
php81-pdo_sqlite php81-pdo php81-sodium libsodium php81-tidy php81-pecl-uuid \
; ln -s /usr/bin/php81 /usr/bin/php \
; rm /etc/nginx/http.d/default.conf \
; rm /etc/php81/php-fpm.d/www.conf \
; mkdir -p /run/nginx \
; mkdir -p /var/www/webmail \
; mkdir -p /config
# nginx / PHP config files
COPY config/nginx-snappymail.conf /config/
COPY config/php-snappymail.conf /etc/php81/php-fpm.d/snappymail.conf
# Parsed and moved at startup
COPY defaults/php.ini /defaults/
COPY defaults/application.ini /defaults/
COPY defaults/default.ini /defaults/
# Install Snappymail from source
ENV SNAPPYMAIL_URL https://github.com/the-djmaze/snappymail/releases/download/v2.19.4/snappymail-2.19.4.tar.gz
# Note. This is the last working snappymail version. 2.19.6 up to 2.20.6 do not work.
RUN set -euxo pipefail \
; cd /var/www/webmail \
; curl -sL ${SNAPPYMAIL_URL} | tar xz \
; chmod -R u+w,a+rX /var/www/webmail \
; chown -R nginx:nginx /var/www/webmail
# SnappyMail login
COPY login/include.php /var/www/webmail/
COPY login/sso.php /var/www/webmail/
COPY start.py /
COPY config.py /
EXPOSE 80/tcp
VOLUME ["/data"]
CMD /start.py
HEALTHCHECK CMD curl -f -L http://localhost/ping || exit 1
RUN echo $VERSION >> /version

@ -1,16 +0,0 @@
#!/usr/bin/env python3
import os
import logging as log
import sys
from socrate import system, conf
args = os.environ.copy()
log.basicConfig(stream=sys.stderr, level=args.get("LOG_LEVEL", "WARNING"))
# Build final configuration paths
conf.jinja("/config/nginx-snappymail.conf", args, "/etc/nginx/http.d/snappymail.conf")
if os.path.exists("/var/run/nginx.pid"):
os.system("nginx -s reload")

@ -1,118 +0,0 @@
; Start a new pool named 'snappymail'.
; the variable $pool can be used in any directive and will be replaced by the
; pool name ('snappymail' here)
[snappymail]
; Redirect worker stdout and stderr into main error log. If not set, stdout and
; stderr will be redirected to /dev/null according to FastCGI specs.
; Default value: no.
catch_workers_output = 1
; Unix user/group of processes
; Note: The user is mandatory. If the group is not set, the default user's group
; will be used.
user = nginx
group = nginx
; The address on which to accept FastCGI requests.
; Valid syntaxes are:
; 'ip.add.re.ss:port' - to listen on a TCP socket to a specific IPv4 address on
; a specific port;
; '[ip:6:addr:ess]:port' - to listen on a TCP socket to a specific IPv6 address on
; a specific port;
; 'port' - to listen on a TCP socket to all addresses
; (IPv6 and IPv4-mapped) on a specific port;
; '/path/to/unix/socket' - to listen on a unix socket.
; Note: This value is mandatory.
listen = /var/run/php8-fpm.sock
; Set permissions for unix socket, if one is used. In Linux, read/write
; permissions must be set in order to allow connections from a web server. Many
; BSD-derived systems allow connections regardless of permissions.
; Default Values: user and group are set as the running user
; mode is set to 0660
listen.owner = nginx
listen.group = nginx
listen.mode = 0660
; Choose how the process manager will control the number of child processes.
; Possible Values:
; static - a fixed number (pm.max_children) of child processes;
; dynamic - the number of child processes are set dynamically based on the
; following directives. With this process management, there will be
; always at least 1 children.
; pm.max_children - the maximum number of children that can
; be alive at the same time.
; pm.start_servers - the number of children created on startup.
; pm.min_spare_servers - the minimum number of children in 'idle'
; state (waiting to process). If the number
; of 'idle' processes is less than this
; number then some children will be created.
; pm.max_spare_servers - the maximum number of children in 'idle'
; state (waiting to process). If the number
; of 'idle' processes is greater than this
; number then some children will be killed.
; ondemand - no children are created at startup. Children will be forked when
; new requests will connect. The following parameter are used:
; pm.max_children - the maximum number of children that
; can be alive at the same time.
; pm.process_idle_timeout - The number of seconds after which
; an idle process will be killed.
; Note: This value is mandatory.
pm = ondemand
; The number of child processes to be created when pm is set to 'static' and the
; maximum number of child processes when pm is set to 'dynamic' or 'ondemand'.
; This value sets the limit on the number of simultaneous requests that will be
; served. Equivalent to the ApacheMaxClients directive with mpm_prefork.
; Equivalent to the PHP_FCGI_CHILDREN environment variable in the original PHP
; CGI. The below defaults are based on a server without much resources. Don't
; forget to tweak pm.* to fit your needs.
; Note: Used when pm is set to 'static', 'dynamic' or 'ondemand'
; Note: This value is mandatory.
pm.max_children = 5
; The number of child processes created on startup.
; Note: Used only when pm is set to 'dynamic'
; Default Value: min_spare_servers + (max_spare_servers - min_spare_servers) / 2
; pm.start_servers = 2
; The desired minimum number of idle server processes.
; Note: Used only when pm is set to 'dynamic'
; Note: Mandatory when pm is set to 'dynamic'
; pm.min_spare_servers = 1
; The desired maximum number of idle server processes.
; Note: Used only when pm is set to 'dynamic'
; Note: Mandatory when pm is set to 'dynamic'
; pm.max_spare_servers = 3
; This sets the maximum time in seconds a script is allowed to run before it is
; terminated by the parser. This helps prevent poorly written scripts from tying up
; the server. The default setting is 30s.
; Note: Used only when pm is set to 'ondemand'
pm.process_idle_timeout = 10s
; The number of requests each child process should execute before respawning.
; This can be useful to work around memory leaks in 3rd party libraries. For endless
; request processing specify '0'.
; Equivalent to PHP_FCGI_MAX_REQUESTS. Default value: 0.
; Noted: Used only when pm is set to 'ondemand'
pm.max_requests = 200
; The ping URI to call the monitoring page of FPM. If this value is not set, no
; URI will be recognized as a ping page. This could be used to test from outside
; that FPM is alive and responding, or to
; - create a graph of FPM availability (rrd or such);
; - remove a server from a group if it is not responding (load balancing);
; - trigger alerts for the operating team (24/7).
; Note: The value must start with a leading slash (/). The value can be
; anything, but it may not be a good idea to use the .php extension or it
; may conflict with a real PHP file.
; Default Value: not set
ping.path = /ping
; This directive may be used to customize the response of a ping request. The
; response is formatted as text/plain with a 200 response code.
; Default Value: pong
;ping.response = pong

@ -5,15 +5,14 @@ attachment_size_limit = {{ MAX_FILESIZE }}
[security]
allow_admin_panel = Off
openpgp = On
[labs]
allow_gravatar = Off
{% if WEB_WEBMAIL == '/' %}
custom_login_link='sso.php'
{% else %}
custom_login_link='{{ WEB_WEBMAIL }}/sso.php'
{% endif %}
custom_logout_link='/sso/logout'
image_exif_auto_rotate = On
try_to_detect_hidden_images = On
{% if WEB_WEBMAIL == '/' %}custom_login_link = "sso.php"{% else %}custom_login_link = "{{ WEB_WEBMAIL }}/sso.php"{% endif %}
custom_logout_link = "/sso/logout"
[contacts]
enable = On
@ -21,3 +20,10 @@ allow_sync = On
[defaults]
contacts_autosave = On
[cache]
enable = On
fast_cache_driver = "APCU"
[imap]
use_move = On

@ -1,15 +0,0 @@
imap_host = "{{ FRONT_ADDRESS }}"
imap_port = 10143
imap_secure = "None"
imap_short_login = Off
sieve_use = On
sieve_allow_raw = Off
sieve_host = "{{ IMAP_ADDRESS }}"
sieve_port = 4190
sieve_secure = "None"
smtp_host = "{{ FRONT_ADDRESS }}"
smtp_port = 10025
smtp_secure = "None"
smtp_short_login = Off
smtp_auth = On
smtp_php_mail = Off

@ -0,0 +1,50 @@
{
"name": "*",
"IMAP": {
"host": "{{ FRONT_ADDRESS }}",
"port": 10143,
"secure": 0,
"shortLogin": false,
"ssl": {
"verify_peer": false,
"verify_peer_name": false,
"allow_self_signed": false,
"SNI_enabled": true,
"disable_compression": true,
"security_level": 1
}
},
"SMTP": {
"host": "{{ FRONT_ADDRESS }}",
"port": 10025,
"secure": 0,
"shortLogin": false,
"ssl": {
"verify_peer": false,
"verify_peer_name": false,
"allow_self_signed": false,
"SNI_enabled": true,
"disable_compression": true,
"security_level": 1
},
"useAuth": true,
"setSender": false,
"usePhpMail": false
},
"Sieve": {
"host": "{{ IMAP_ADDRESS }}",
"port": 4190,
"secure": 0,
"shortLogin": false,
"ssl": {
"verify_peer": false,
"verify_peer_name": false,
"allow_self_signed": false,
"SNI_enabled": true,
"disable_compression": true,
"security_level": 1
},
"enabled": true
},
"whiteList": ""
}

@ -1,5 +0,0 @@
expose_php=Off
date.timezone={{ TZ }}
upload_max_filesize = {{ MAX_FILESIZE }}M
post_max_size = {{ MAX_FILESIZE }}M

@ -9,9 +9,9 @@ if (isset($_SERVER['HTTP_X_REMOTE_USER']) && isset($_SERVER['HTTP_X_REMOTE_USER_
$ssoHash = \RainLoop\Api::CreateUserSsoHash($email, $password);
// redirect to webmail sso url
header('Location: index.php?sso&hash='.$ssoHash);
header('Location: index.php?sso&hash='.$ssoHash, 302);
}
else {
header('HTTP/1.0 403 Forbidden');
header('HTTP/1.0 403 Forbidden', 403);
}
?>
?>

@ -0,0 +1,11 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: Hostname:
Version: Hockeypuck 2.1.0-184-g50f1108
xjMEYg0atBYJKwYBBAHaRw8BAQdA2S2tvGavChACjtBastsKRThD3rsBW1LUZLmN
Zbs4uaHNI1NuYXBweU1haWwgPHJlbGVhc2VzQHNuYXBweW1haWwuZXU+wpQEExYK
ADwWIQQQFuRweRRVQvi6EzVIIIuhMpDz6wUCYg0atAIbAwULCQgHAgMiAgEGFQoJ
CAsCBBYCAwECHgcCF4AACgkQSCCLoTKQ8+u9SAD/Q/IoAwjUkKDJBPq0RGwCFnl6
FG/VHB97CvBSpGOxtIsBAMCwMhWlsaBHAEqbzxiN+cdlMYwV23+SWLUJ/XMFgukE
=vC/h
-----END PGP PUBLIC KEY BLOCK-----

@ -1,34 +0,0 @@
#!/usr/bin/env python3
import os
import shutil
import logging as log
import sys
import subprocess
from socrate import system, conf
log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING"))
# Actual startup script
os.environ["FRONT_ADDRESS"] = system.resolve_address(os.environ.get("HOST_FRONT", "front"))
os.environ["IMAP_ADDRESS"] = system.resolve_address(os.environ.get("HOST_IMAP", "imap"))
os.environ["MAX_FILESIZE"] = str(int(int(os.environ.get("MESSAGE_SIZE_LIMIT"))*0.66/1048576))
base = "/data/_data_/_default_/"
shutil.rmtree(base + "domains/", ignore_errors=True)
os.makedirs(base + "domains", exist_ok=True)
os.makedirs(base + "configs", exist_ok=True)
conf.jinja("/defaults/default.ini", os.environ, "/data/_data_/_default_/domains/default.ini")
conf.jinja("/defaults/application.ini", os.environ, "/data/_data_/_default_/configs/application.ini")
conf.jinja("/defaults/php.ini", os.environ, "/etc/php81/php.ini")
# Start the fastcgi process manager now that config files have been adjusted
os.system("php-fpm81")
os.system("chown -R nginx:nginx /data")
os.system("chmod -R a+rX /var/www/webmail/")
subprocess.call(["/config.py"])
os.execv("/usr/sbin/nginx", ["nginx", "-g", "daemon off;"])

@ -0,0 +1,133 @@
# This is based on default configuration file for Snuffleupagus (https://snuffleupagus.rtfd.io),
# for php8.
# It contains "reasonable" defaults that won't break your websites,
# and a lot of commented directives that you can enable if you want to
# have a better protection.
# Harden the PRNG
sp.harden_random.enable();
# Disabled XXE
sp.xxe_protection.enable();
# Global configuration variables
sp.global.secret_key("{{ SNUFFLEUPAGUS_KEY }}");
# Globally activate strict mode
# https://www.php.net/manual/en/language.types.declarations.php#language.types.declarations.strict
sp.global_strict.enable();
# Prevent unserialize-related exploits
# sp.unserialize_hmac.enable();
# Only allow execution of read-only files. This is a low-hanging fruit that you should enable.
sp.readonly_exec.enable();
# PHP has a lot of wrappers, most of them aren't usually useful, you should
# only enable the ones you're using.
sp.wrappers_whitelist.list("file,php,phar,mailsosubstreams");
# Prevent sloppy comparisons.
sp.sloppy_comparison.enable();
# Use SameSite on session cookie
# https://snuffleupagus.readthedocs.io/features.html#protection-against-cross-site-request-forgery
sp.cookie.name("PHPSESSID").samesite("lax");
# Harden the `chmod` function (0777 (oct = 511, 0666 = 438)
sp.disable_function.function("chmod").param("permissions").value("438").drop();
sp.disable_function.function("chmod").param("permissions").value("511").drop();
# Prevent various `mail`-related vulnerabilities
sp.disable_function.function("mail").param("additional_parameters").value_r("\\-").drop();
# Since it's now burned, me might as well mitigate it publicly
sp.disable_function.function("putenv").param("assignment").value_r("LD_").drop()
# This one was burned in Nov 2019 - https://gist.github.com/LoadLow/90b60bd5535d6c3927bb24d5f9955b80
sp.disable_function.function("putenv").param("assignment").value_r("GCONV_").drop()
# Since people are stupid enough to use `extract` on things like $_GET or $_POST, we might as well mitigate this vector
sp.disable_function.function("extract").param("array").value_r("^_").drop()
sp.disable_function.function("extract").param("flags").value("0").drop()
# This is also burned:
# ini_set('open_basedir','..');chdir('..');…;chdir('..');ini_set('open_basedir','/');echo(file_get_contents('/etc/passwd'));
# Since we have no way of matching on two parameters at the same time, we're
# blocking calls to open_basedir altogether: nobody is using it via ini_set anyway.
# Moreover, there are non-public bypasses that are also using this vector ;)
sp.disable_function.function("ini_set").param("option").value_r("open_basedir").drop()
# Prevent various `include`-related vulnerabilities
sp.disable_function.function("require_once").value_r("\.(inc|phtml|php)$").allow();
sp.disable_function.function("include_once").value_r("\.(inc|phtml|php)$").allow();
sp.disable_function.function("require").value_r("\.(inc|phtml|php)$").allow();
sp.disable_function.function("include").value_r("\.(inc|phtml|php)$").allow();
sp.disable_function.function("require_once").drop()
sp.disable_function.function("include_once").drop()
sp.disable_function.function("require").drop()
sp.disable_function.function("include").drop()
# Prevent `system`-related injections
sp.disable_function.function("system").param("command").value_r("[$|;&`\\n\\(\\)\\\\]").drop();
sp.disable_function.function("shell_exec").param("command").value_r("[$|;&`\\n\\(\\)\\\\]").drop();
sp.disable_function.function("exec").param("command").value_r("[$|;&`\\n\\(\\)\\\\]").drop();
# This is **very** broad but doing better is non-straightforward
sp.disable_function.function("proc_open").param("command").value_r("^gpg ").allow();
sp.disable_function.function("proc_open").param("command").value_r("[$|;&`\\n\\(\\)\\\\]").drop();
# Prevent runtime modification of interesting things
sp.disable_function.function("ini_set").param("option").value("assert.active").drop();
sp.disable_function.function("ini_set").param("option").value("zend.assertions").drop();
sp.disable_function.function("ini_set").param("option").value("memory_limit").drop();
sp.disable_function.function("ini_set").param("option").value("include_path").drop();
sp.disable_function.function("ini_set").param("option").value("open_basedir").drop();
# Detect some backdoors via environment recon
sp.disable_function.function("ini_get").param("option").value("allow_url_fopen").drop();
sp.disable_function.function("ini_get").param("option").value("open_basedir").drop();
sp.disable_function.function("ini_get").param("option").value_r("suhosin").drop();
sp.disable_function.function("function_exists").param("function").value("eval").drop();
sp.disable_function.function("function_exists").param("function").value("exec").drop();
sp.disable_function.function("function_exists").param("function").value("system").drop();
sp.disable_function.function("function_exists").param("function").value("shell_exec").drop();
sp.disable_function.function("function_exists").param("function").value("proc_open").drop();
sp.disable_function.function("function_exists").param("function").value("passthru").drop();
sp.disable_function.function("is_callable").param("value").value("eval").drop();
sp.disable_function.function("is_callable").param("value").value("exec").drop();
sp.disable_function.function("is_callable").param("value").value("system").drop();
sp.disable_function.function("is_callable").param("value").value("shell_exec").drop();
sp.disable_function.function("is_callable").filename_r("/app/libraries/snappymail/pgp/gpg\.php$").param("value").value("proc_open").allow();
sp.disable_function.function("is_callable").param("value").value("proc_open").drop();
sp.disable_function.function("is_callable").param("value").value("passthru").drop();
# Ghetto error-based sqli detection
#sp.disable_function.function("mysql_query").ret("FALSE").drop();
#sp.disable_function.function("mysqli_query").ret("FALSE").drop();
#sp.disable_function.function("PDO::query").ret("FALSE").drop();
# Ensure that certificates are properly verified
sp.disable_function.function("curl_setopt").param("value").value("1").allow();
sp.disable_function.function("curl_setopt").param("value").value("2").allow();
# `81` is SSL_VERIFYHOST and `64` SSL_VERIFYPEER
sp.disable_function.function("curl_setopt").param("option").value("64").drop().alias("Please don't turn CURLOPT_SSL_VERIFYCLIENT off.");
sp.disable_function.function("curl_setopt").param("option").value("81").drop().alias("Please don't turn CURLOPT_SSL_VERIFYHOST off.");
# File upload
sp.disable_function.function("move_uploaded_file").param("to").value_r("\\.ph").drop();
sp.disable_function.function("move_uploaded_file").param("to").value_r("\\.ht").drop();
# Logging lockdown
sp.disable_function.function("ini_set").param("option").value_r("error_log").drop()
sp.disable_function.function("ini_set").param("option").value_r("display_errors").drop()
sp.auto_cookie_secure.enable();
# TODO: consider encrypting the cookies?
# TODO: ensure this is up to date
sp.cookie.name("roundcube_sessauth").samesite("strict");
sp.cookie.name("roundcube_sessid").samesite("strict");
sp.ini_protection.policy_silent_fail();
# roundcube uses unserialize() everywhere.
# This should do the job until https://github.com/jvoisin/snuffleupagus/issues/438 is implemented.
sp.disable_function.function("unserialize").param("data").value_r("[cCoO]:\d+:[\"{]").drop();

@ -4,9 +4,10 @@ import os
import logging
import sys
import subprocess
import shutil
import hmac
from socrate import conf
from socrate import conf, system
env = os.environ
@ -17,6 +18,8 @@ context = {}
context.update(env)
context["MAX_FILESIZE"] = str(int(int(env.get("MESSAGE_SIZE_LIMIT", "50000000")) * 0.66 / 1048576))
context["FRONT_ADDRESS"] = system.get_host_address_from_environment("FRONT", "front")
context["IMAP_ADDRESS"] = system.get_host_address_from_environment("IMAP", "imap")
db_flavor = env.get("ROUNDCUBE_DB_FLAVOR", "sqlite")
if db_flavor == "sqlite":
@ -48,11 +51,13 @@ if not secret_key:
print(f"Can't read SECRET_KEY from file: {exc}", file=sys.stderr)
exit(2)
context['SECRET_KEY'] = hmac.new(bytearray(secret_key, 'utf-8'), bytearray('ROUNDCUBE_KEY', 'utf-8'), 'sha256').hexdigest()
context['ROUNDCUBE_KEY'] = hmac.new(bytearray(secret_key, 'utf-8'), bytearray('ROUNDCUBE_KEY', 'utf-8'), 'sha256').hexdigest()
context['SNUFFLEUPAGUS_KEY'] = hmac.new(bytearray(secret_key, 'utf-8'), bytearray('SNUFFLEUPAGUS_KEY', 'utf-8'), 'sha256').hexdigest()
conf.jinja("/etc/snuffleupagus.rules.tpl", context, "/etc/snuffleupagus.rules")
# roundcube plugins
# (using "dict" because it is ordered and "set" is not)
plugins = dict((p, None) for p in env.get("ROUNDCUBE_PLUGINS", "").replace(" ", "").split(",") if p and os.path.isdir(os.path.join("/var/www/webmail/plugins", p)))
plugins = dict((p, None) for p in env.get("ROUNDCUBE_PLUGINS", "").replace(" ", "").split(",") if p and os.path.isdir(os.path.join("/var/www/roundcube/plugins", p)))
if plugins:
plugins["mailu"] = None
else:
@ -67,15 +72,14 @@ context["INCLUDES"] = sorted(inc for inc in os.listdir("/overrides") if inc.ends
context["SESSION_TIMEOUT_MINUTES"] = max(int(env.get("SESSION_TIMEOUT", "3600")) // 60, 1)
# create config files
conf.jinja("/conf/php.ini", context, "/etc/php81/php.ini")
conf.jinja("/conf/config.inc.php", context, "/var/www/webmail/config/config.inc.php")
conf.jinja("/conf/config.inc.php", context, "/var/www/roundcube/config/config.inc.php")
# create dirs
os.system("mkdir -p /data/gpg")
print("Initializing database")
try:
result = subprocess.check_output(["/var/www/webmail/bin/initdb.sh", "--dir", "/var/www/webmail/SQL"],
result = subprocess.check_output(["/var/www/roundcube/bin/initdb.sh", "--dir", "/var/www/roundcube/SQL"],
stderr=subprocess.STDOUT)
print(result.decode())
except subprocess.CalledProcessError as exc:
@ -88,27 +92,35 @@ except subprocess.CalledProcessError as exc:
print("Upgrading database")
try:
subprocess.check_call(["/var/www/webmail/bin/update.sh", "--version=?", "-y"], stderr=subprocess.STDOUT)
subprocess.check_call(["/var/www/roundcube/bin/update.sh", "--version=?", "-y"], stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as exc:
exit(4)
else:
print("Cleaning database")
try:
subprocess.check_call(["/var/www/webmail/bin/cleandb.sh"], stderr=subprocess.STDOUT)
subprocess.check_call(["/var/www/roundcube/bin/cleandb.sh"], stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as exc:
exit(5)
base = "/data/_data_/_default_/"
shutil.rmtree(base + "domains/", ignore_errors=True)
os.makedirs(base + "domains", exist_ok=True)
os.makedirs(base + "configs", exist_ok=True)
conf.jinja("/defaults/default.json", context, "/data/_data_/_default_/domains/default.json")
conf.jinja("/defaults/application.ini", context, "/data/_data_/_default_/configs/application.ini")
conf.jinja("/defaults/php.ini", context, "/etc/php81/php.ini")
# setup permissions
os.system("chown -R nginx:nginx /data")
os.system("chmod -R a+rX /var/www/webmail/")
os.system("chown -R mailu:mailu /data")
# Configure nginx
conf.jinja("/conf/nginx-roundcube.conf", context, "/etc/nginx/http.d/roundcube.conf")
conf.jinja("/conf/nginx-webmail.conf", context, "/etc/nginx/http.d/webmail.conf")
if os.path.exists("/var/run/nginx.pid"):
os.system("nginx -s reload")
# clean env
[env.pop(key, None) for key in env.keys() if key == "SECRET_KEY" or key.startswith("ROUNDCUBE_")]
[env.pop(key, None) for key in env.keys() if key == "SECRET_KEY" or key.endswith("_KEY")]
# run nginx
os.system("php-fpm81")
Loading…
Cancel
Save