diff --git a/.github/workflows/build_test_deploy.yml b/.github/workflows/build_test_deploy.yml index d1395bec..4d10e4ca 100644 --- a/.github/workflows/build_test_deploy.yml +++ b/.github/workflows/build_test_deploy.yml @@ -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 diff --git a/README.md b/README.md index 0fd737b6..b6ed040b 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/core/admin/mailu/internal/views/fetch.py b/core/admin/mailu/internal/views/fetch.py index 1945b9c7..e813c33b 100644 --- a/core/admin/mailu/internal/views/fetch.py +++ b/core/admin/mailu/internal/views/fetch.py @@ -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() diff --git a/core/admin/mailu/models.py b/core/admin/mailu/models.py index 1c57c8be..b33a0776 100644 --- a/core/admin/mailu/models.py +++ b/core/admin/mailu/models.py @@ -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) diff --git a/core/admin/mailu/sso/views/languages.py b/core/admin/mailu/sso/views/languages.py index ff65af45..19764519 100644 --- a/core/admin/mailu/sso/views/languages.py +++ b/core/admin/mailu/sso/views/languages.py @@ -1,7 +1,7 @@ from mailu.sso import sso import flask -@sso.route('/language/', methods=['POST']) +@sso.route('/language/', methods=['GET','POST']) def set_language(language=None): if language: flask.session['language'] = language diff --git a/core/admin/mailu/ui/forms.py b/core/admin/mailu/ui/forms.py index 3882064d..959f46b2 100644 --- a/core/admin/mailu/ui/forms.py +++ b/core/admin/mailu/ui/forms.py @@ -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')) diff --git a/core/admin/mailu/ui/templates/fetch/create.html b/core/admin/mailu/ui/templates/fetch/create.html index 00698329..69584d15 100644 --- a/core/admin/mailu/ui/templates/fetch/create.html +++ b/core/admin/mailu/ui/templates/fetch/create.html @@ -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) }} diff --git a/core/admin/mailu/ui/templates/fetch/list.html b/core/admin/mailu/ui/templates/fetch/list.html index 7a527ce8..74d3a02f 100644 --- a/core/admin/mailu/ui/templates/fetch/list.html +++ b/core/admin/mailu/ui/templates/fetch/list.html @@ -20,6 +20,8 @@ {% trans %}Endpoint{% endtrans %} {% trans %}Username{% endtrans %} {% trans %}Keep emails{% endtrans %} + {% trans %}Rescan emails{% endtrans %} + {% trans %}Folders{% endtrans %} {% trans %}Last check{% endtrans %} {% trans %}Status{% endtrans %} {% trans %}Created{% endtrans %} @@ -36,6 +38,8 @@ {{ fetch.protocol }}{{ 's' if fetch.tls else '' }}://{{ fetch.host }}:{{ fetch.port }} {{ fetch.username }} {% if fetch.keep %}{% trans %}yes{% endtrans %}{% else %}{% trans %}no{% endtrans %}{% endif %} + {% if fetch.scan %}{% trans %}yes{% endtrans %}{% else %}{% trans %}no{% endtrans %}{% endif %} + {{ fetch.folders | join(',') }} {{ fetch.last_check | format_datetime or '-' }} {{ fetch.error or '-' }} {{ fetch.created_at | format_date }} diff --git a/core/admin/mailu/ui/views/base.py b/core/admin/mailu/ui/views/base.py index 9b7614e1..1d06464a 100644 --- a/core/admin/mailu/ui/views/base.py +++ b/core/admin/mailu/ui/views/base.py @@ -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 = '' diff --git a/core/admin/mailu/ui/views/fetches.py b/core/admin/mailu/ui/views/fetches.py index ca837a8e..3c4d629d 100644 --- a/core/admin/mailu/ui/views/fetches.py +++ b/core/admin/mailu/ui/views/fetches.py @@ -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( diff --git a/core/admin/mailu/ui/views/users.py b/core/admin/mailu/ui/views/users.py index 85a5c2db..c7d252a9 100644 --- a/core/admin/mailu/ui/views/users.py +++ b/core/admin/mailu/ui/views/users.py @@ -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) diff --git a/core/admin/mailu/utils.py b/core/admin/mailu/utils.py index f160fe3f..b432192d 100644 --- a/core/admin/mailu/utils.py +++ b/core/admin/mailu/utils.py @@ -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) diff --git a/core/admin/migrations/versions/f4f0f89e0047_.py b/core/admin/migrations/versions/f4f0f89e0047_.py new file mode 100644 index 00000000..8d20063c --- /dev/null +++ b/core/admin/migrations/versions/f4f0f89e0047_.py @@ -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') diff --git a/core/admin/start.py b/core/admin/start.py index 3cb5c422..e2163398 100755 --- a/core/admin/start.py +++ b/core/admin/start.py @@ -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") diff --git a/core/base/Dockerfile b/core/base/Dockerfile index 6f31e21c..65e8be94 100644 --- a/core/base/Dockerfile +++ b/core/base/Dockerfile @@ -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}" diff --git a/core/base/requirements-dev.txt b/core/base/requirements-dev.txt index 92d34fdb..ebcdde92 100644 --- a/core/base/requirements-dev.txt +++ b/core/base/requirements-dev.txt @@ -23,7 +23,7 @@ itsdangerous limits marshmallow marshmallow-sqlalchemy -mysql-connector-python +mysql-connector-python==8.0.29 passlib psycopg2-binary Pygments diff --git a/core/base/requirements-prod.txt b/core/base/requirements-prod.txt index 716f848e..4cf70cd0 100644 --- a/core/base/requirements-prod.txt +++ b/core/base/requirements-prod.txt @@ -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 diff --git a/core/none/Dockerfile b/core/none/Dockerfile index f06cc31c..d605cb07 100644 --- a/core/none/Dockerfile +++ b/core/none/Dockerfile @@ -10,5 +10,5 @@ RUN echo $VERSION >/version HEALTHCHECK CMD true -USER app +USER mailu CMD ["/bin/bash", "-c", "sleep infinity"] diff --git a/core/rspamd/start.py b/core/rspamd/start.py index 537d996d..37de1df9 100755 --- a/core/rspamd/start.py +++ b/core/rspamd/start.py @@ -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"]) diff --git a/docs/index.rst b/docs/index.rst index 5c004dc1..0b37cf43 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -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 diff --git a/docs/webadministration.rst b/docs/webadministration.rst index 2e0de745..7e2a5728 100644 --- a/docs/webadministration.rst +++ b/docs/webadministration.rst @@ -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 diff --git a/optional/fetchmail/fetchmail.py b/optional/fetchmail/fetchmail.py index 3a82a124..62bd7124 100755 --- a/optional/fetchmail/fetchmail.py +++ b/optional/fetchmail/fetchmail.py @@ -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)) diff --git a/setup/Dockerfile b/setup/Dockerfile index 85e5f55b..a410871d 100644 --- a/setup/Dockerfile +++ b/setup/Dockerfile @@ -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 diff --git a/setup/flavors/compose/docker-compose.yml b/setup/flavors/compose/docker-compose.yml index 6dac166b..b6c99ca5 100644 --- a/setup/flavors/compose/docker-compose.yml +++ b/setup/flavors/compose/docker-compose.yml @@ -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: diff --git a/setup/flavors/stack/docker-compose.yml b/setup/flavors/stack/docker-compose.yml index 89da923c..809362df 100644 --- a/setup/flavors/stack/docker-compose.yml +++ b/setup/flavors/stack/docker-compose.yml @@ -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" diff --git a/setup/requirements.txt b/setup/requirements.txt deleted file mode 100644 index b6f9f713..00000000 --- a/setup/requirements.txt +++ /dev/null @@ -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 diff --git a/tests/build.hcl b/tests/build.hcl index 34955270..d657cbb7 100644 --- a/tests/build.hcl +++ b/tests/build.hcl @@ -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") } # ----------------------------------------------------------------------------------------- diff --git a/tests/compose/filters/02_email_antispam.sh b/tests/compose/filters/02_email_antispam.sh new file mode 100755 index 00000000..ac2653a3 --- /dev/null +++ b/tests/compose/filters/02_email_antispam.sh @@ -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 diff --git a/tests/compose/snappymail/docker-compose.yml b/tests/compose/snappymail/docker-compose.yml deleted file mode 100644 index b9df7332..00000000 --- a/tests/compose/snappymail/docker-compose.yml +++ /dev/null @@ -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 diff --git a/tests/compose/snappymail/mailu.env b/tests/compose/snappymail/mailu.env deleted file mode 100644 index 50271fc7..00000000 --- a/tests/compose/snappymail/mailu.env +++ /dev/null @@ -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= diff --git a/tests/compose/webmail/01_ensure_admin_unreachable.sh b/tests/compose/webmail/01_ensure_admin_unreachable.sh new file mode 100755 index 00000000..4fd78a1b --- /dev/null +++ b/tests/compose/webmail/01_ensure_admin_unreachable.sh @@ -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 diff --git a/tests/compose/roundcube/docker-compose.yml b/tests/compose/webmail/docker-compose.yml similarity index 96% rename from tests/compose/roundcube/docker-compose.yml rename to tests/compose/webmail/docker-compose.yml index f2c43686..14d1dae9 100644 --- a/tests/compose/roundcube/docker-compose.yml +++ b/tests/compose/webmail/docker-compose.yml @@ -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: diff --git a/tests/compose/roundcube/mailu.env b/tests/compose/webmail/mailu.env similarity index 99% rename from tests/compose/roundcube/mailu.env rename to tests/compose/webmail/mailu.env index 7f000f2c..f87f3262 100644 --- a/tests/compose/roundcube/mailu.env +++ b/tests/compose/webmail/mailu.env @@ -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 diff --git a/towncrier/newsfragments/1231.bugfix b/towncrier/newsfragments/1231.bugfix new file mode 100644 index 00000000..333ae35f --- /dev/null +++ b/towncrier/newsfragments/1231.bugfix @@ -0,0 +1 @@ +Add an option so that emails fetched with fetchmail don't go through the filters (closes #1231) diff --git a/towncrier/newsfragments/2231.bugfix b/towncrier/newsfragments/2231.bugfix new file mode 100644 index 00000000..e710ea6d --- /dev/null +++ b/towncrier/newsfragments/2231.bugfix @@ -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. diff --git a/towncrier/newsfragments/2246.bugfix b/towncrier/newsfragments/2246.bugfix new file mode 100644 index 00000000..92e90ac6 --- /dev/null +++ b/towncrier/newsfragments/2246.bugfix @@ -0,0 +1 @@ +Fetchmail: Missing support for '*_ADDRESS' env vars diff --git a/towncrier/newsfragments/2526.misc b/towncrier/newsfragments/2526.misc new file mode 100644 index 00000000..9425e88a --- /dev/null +++ b/towncrier/newsfragments/2526.misc @@ -0,0 +1 @@ +Upgrade Snappymail to 2.21 and merge the webmail containers diff --git a/towncrier/newsfragments/2539.misc b/towncrier/newsfragments/2539.misc new file mode 100644 index 00000000..10e3954e --- /dev/null +++ b/towncrier/newsfragments/2539.misc @@ -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 diff --git a/towncrier/newsfragments/2550.misc b/towncrier/newsfragments/2550.misc new file mode 100644 index 00000000..fcd5dacf --- /dev/null +++ b/towncrier/newsfragments/2550.misc @@ -0,0 +1 @@ +Add Snuffleupagus to protect webmails (a Suhosin replacement) diff --git a/towncrier/newsfragments/711.feature b/towncrier/newsfragments/711.feature new file mode 100644 index 00000000..aa605aa2 --- /dev/null +++ b/towncrier/newsfragments/711.feature @@ -0,0 +1 @@ +Allow other folders to be synced by fetchmail diff --git a/webmails/Dockerfile b/webmails/Dockerfile new file mode 100644 index 00000000..77bc65f3 --- /dev/null +++ b/webmails/Dockerfile @@ -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 diff --git a/webmails/snappymail/config/nginx-snappymail.conf b/webmails/nginx-webmail.conf similarity index 76% rename from webmails/snappymail/config/nginx-snappymail.conf rename to webmails/nginx-webmail.conf index 80268340..1794a635 100644 --- a/webmails/snappymail/config/nginx-snappymail.conf +++ b/webmails/nginx-webmail.conf @@ -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; } diff --git a/webmails/roundcube/config/php-roundcube.conf b/webmails/php-webmail.conf similarity index 98% rename from webmails/roundcube/config/php-roundcube.conf rename to webmails/php-webmail.conf index ac0c3375..18a1f66e 100644 --- a/webmails/roundcube/config/php-roundcube.conf +++ b/webmails/php-webmail.conf @@ -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: diff --git a/webmails/php.ini b/webmails/php.ini new file mode 100644 index 00000000..87660288 --- /dev/null +++ b/webmails/php.ini @@ -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 diff --git a/webmails/roundcube/Dockerfile b/webmails/roundcube/Dockerfile deleted file mode 100644 index 8db6f984..00000000 --- a/webmails/roundcube/Dockerfile +++ /dev/null @@ -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 diff --git a/webmails/roundcube/config/config.inc.php b/webmails/roundcube/config/config.inc.php index d5213b32..e8aedeff 100644 --- a/webmails/roundcube/config/config.inc.php +++ b/webmails/roundcube/config/config.inc.php @@ -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; diff --git a/webmails/roundcube/config/nginx-roundcube.conf b/webmails/roundcube/config/nginx-roundcube.conf deleted file mode 100644 index 80268340..00000000 --- a/webmails/roundcube/config/nginx-roundcube.conf +++ /dev/null @@ -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), , off - access_log off; - - # /dev/stderr (Default), , 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; - } -} diff --git a/webmails/roundcube/config/php.ini b/webmails/roundcube/config/php.ini deleted file mode 100644 index 9f45dc80..00000000 --- a/webmails/roundcube/config/php.ini +++ /dev/null @@ -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 diff --git a/webmails/roundcube/login/mailu.php b/webmails/roundcube/login/mailu.php index 0596ca9d..86de6562 100644 --- a/webmails/roundcube/login/mailu.php +++ b/webmails/roundcube/login/mailu.php @@ -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(); } diff --git a/webmails/roundcube/pubkey.asc b/webmails/roundcube/pubkey.asc new file mode 100644 index 00000000..3d4449c9 --- /dev/null +++ b/webmails/roundcube/pubkey.asc @@ -0,0 +1,102 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +0dXJnhEe7eXkwBbfEzk= +=lBKd +-----END PGP PUBLIC KEY BLOCK----- diff --git a/webmails/snappymail/Dockerfile b/webmails/snappymail/Dockerfile deleted file mode 100644 index 3bc4ef53..00000000 --- a/webmails/snappymail/Dockerfile +++ /dev/null @@ -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 diff --git a/webmails/snappymail/config.py b/webmails/snappymail/config.py deleted file mode 100755 index f9fa363c..00000000 --- a/webmails/snappymail/config.py +++ /dev/null @@ -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") diff --git a/webmails/snappymail/config/php-snappymail.conf b/webmails/snappymail/config/php-snappymail.conf deleted file mode 100644 index 74b1889f..00000000 --- a/webmails/snappymail/config/php-snappymail.conf +++ /dev/null @@ -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 diff --git a/webmails/snappymail/defaults/application.ini b/webmails/snappymail/defaults/application.ini index 71a19f35..bcf544c5 100644 --- a/webmails/snappymail/defaults/application.ini +++ b/webmails/snappymail/defaults/application.ini @@ -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 diff --git a/webmails/snappymail/defaults/default.ini b/webmails/snappymail/defaults/default.ini deleted file mode 100644 index be9a0969..00000000 --- a/webmails/snappymail/defaults/default.ini +++ /dev/null @@ -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 diff --git a/webmails/snappymail/defaults/default.json b/webmails/snappymail/defaults/default.json new file mode 100644 index 00000000..ecbf116c --- /dev/null +++ b/webmails/snappymail/defaults/default.json @@ -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": "" +} diff --git a/webmails/snappymail/defaults/php.ini b/webmails/snappymail/defaults/php.ini deleted file mode 100644 index d3d4d9f1..00000000 --- a/webmails/snappymail/defaults/php.ini +++ /dev/null @@ -1,5 +0,0 @@ -expose_php=Off -date.timezone={{ TZ }} -upload_max_filesize = {{ MAX_FILESIZE }}M -post_max_size = {{ MAX_FILESIZE }}M - diff --git a/webmails/snappymail/login/sso.php b/webmails/snappymail/login/sso.php index e3d04824..254bb151 100644 --- a/webmails/snappymail/login/sso.php +++ b/webmails/snappymail/login/sso.php @@ -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); } -?> \ No newline at end of file +?> diff --git a/webmails/snappymail/pubkey.asc b/webmails/snappymail/pubkey.asc new file mode 100644 index 00000000..9f295b79 --- /dev/null +++ b/webmails/snappymail/pubkey.asc @@ -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----- diff --git a/webmails/snappymail/start.py b/webmails/snappymail/start.py deleted file mode 100755 index 5307f23b..00000000 --- a/webmails/snappymail/start.py +++ /dev/null @@ -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;"]) diff --git a/webmails/snuffleupagus.rules b/webmails/snuffleupagus.rules new file mode 100644 index 00000000..ec7bee13 --- /dev/null +++ b/webmails/snuffleupagus.rules @@ -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(); diff --git a/webmails/roundcube/start.py b/webmails/start.py similarity index 65% rename from webmails/roundcube/start.py rename to webmails/start.py index b5a4dca5..f87ac55f 100755 --- a/webmails/roundcube/start.py +++ b/webmails/start.py @@ -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")