mirror of
https://github.com/Mailu/Mailu.git
synced 2025-06-06 23:36:26 +02:00
Merge branch 'master' of https://github.com/Mailu/Mailu into HEAD
This commit is contained in:
commit
3721a6aa02
20
.github/workflows/build_test_deploy.yml
vendored
20
.github/workflows/build_test_deploy.yml
vendored
@ -126,7 +126,7 @@ jobs:
|
|||||||
password: ${{ secrets.Docker_Password }}
|
password: ${{ secrets.Docker_Password }}
|
||||||
- name: Helper to convert docker org to lowercase
|
- name: Helper to convert docker org to lowercase
|
||||||
id: string
|
id: string
|
||||||
uses: ASzc/change-string-case-action@v2
|
uses: ASzc/change-string-case-action@v5
|
||||||
with:
|
with:
|
||||||
string: ${{ github.repository_owner }}
|
string: ${{ github.repository_owner }}
|
||||||
- name: Build all docker images
|
- name: Build all docker images
|
||||||
@ -182,7 +182,7 @@ jobs:
|
|||||||
password: ${{ secrets.Docker_Password }}
|
password: ${{ secrets.Docker_Password }}
|
||||||
- name: Helper to convert docker org to lowercase
|
- name: Helper to convert docker org to lowercase
|
||||||
id: string
|
id: string
|
||||||
uses: ASzc/change-string-case-action@v2
|
uses: ASzc/change-string-case-action@v5
|
||||||
with:
|
with:
|
||||||
string: ${{ github.repository_owner }}
|
string: ${{ github.repository_owner }}
|
||||||
- name: Build all docker images
|
- name: Build all docker images
|
||||||
@ -244,7 +244,7 @@ jobs:
|
|||||||
password: ${{ secrets.Docker_Password }}
|
password: ${{ secrets.Docker_Password }}
|
||||||
- name: Helper to convert docker org to lowercase
|
- name: Helper to convert docker org to lowercase
|
||||||
id: string
|
id: string
|
||||||
uses: ASzc/change-string-case-action@v2
|
uses: ASzc/change-string-case-action@v5
|
||||||
with:
|
with:
|
||||||
string: ${{ github.repository_owner }}
|
string: ${{ github.repository_owner }}
|
||||||
- name: Build all docker images
|
- name: Build all docker images
|
||||||
@ -307,7 +307,7 @@ jobs:
|
|||||||
password: ${{ secrets.Docker_Password }}
|
password: ${{ secrets.Docker_Password }}
|
||||||
- name: Helper to convert docker org to lowercase
|
- name: Helper to convert docker org to lowercase
|
||||||
id: string
|
id: string
|
||||||
uses: ASzc/change-string-case-action@v2
|
uses: ASzc/change-string-case-action@v5
|
||||||
with:
|
with:
|
||||||
string: ${{ github.repository_owner }}
|
string: ${{ github.repository_owner }}
|
||||||
- name: Build all docker images
|
- name: Build all docker images
|
||||||
@ -340,7 +340,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
target: ["core", "fetchmail", "filters", "snappymail", "roundcube", "webdav"]
|
target: ["core", "fetchmail", "filters", "webmail", "webdav"]
|
||||||
time: ["2"]
|
time: ["2"]
|
||||||
include:
|
include:
|
||||||
- target: "filters"
|
- target: "filters"
|
||||||
@ -370,7 +370,7 @@ jobs:
|
|||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Helper to convert docker org to lowercase
|
- name: Helper to convert docker org to lowercase
|
||||||
id: string
|
id: string
|
||||||
uses: ASzc/change-string-case-action@v2
|
uses: ASzc/change-string-case-action@v5
|
||||||
with:
|
with:
|
||||||
string: ${{ github.repository_owner }}
|
string: ${{ github.repository_owner }}
|
||||||
- name: Install python packages
|
- name: Install python packages
|
||||||
@ -394,7 +394,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
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:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Retrieve global variables
|
- name: Retrieve global variables
|
||||||
@ -416,7 +416,7 @@ jobs:
|
|||||||
password: ${{ secrets.Docker_Password }}
|
password: ${{ secrets.Docker_Password }}
|
||||||
- name: Helper to convert docker org to lowercase
|
- name: Helper to convert docker org to lowercase
|
||||||
id: string
|
id: string
|
||||||
uses: ASzc/change-string-case-action@v2
|
uses: ASzc/change-string-case-action@v5
|
||||||
with:
|
with:
|
||||||
string: ${{ github.repository_owner }}
|
string: ${{ github.repository_owner }}
|
||||||
- name: Push image to Docker
|
- name: Push image to Docker
|
||||||
@ -439,7 +439,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
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:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Retrieve global variables
|
- name: Retrieve global variables
|
||||||
@ -461,7 +461,7 @@ jobs:
|
|||||||
password: ${{ secrets.Docker_Password }}
|
password: ${{ secrets.Docker_Password }}
|
||||||
- name: Helper to convert docker org to lowercase
|
- name: Helper to convert docker org to lowercase
|
||||||
id: string
|
id: string
|
||||||
uses: ASzc/change-string-case-action@v2
|
uses: ASzc/change-string-case-action@v5
|
||||||
with:
|
with:
|
||||||
string: ${{ github.repository_owner }}
|
string: ${{ github.repository_owner }}
|
||||||
- name: Push image to Docker
|
- name: Push image to Docker
|
||||||
|
@ -22,7 +22,7 @@ Main features include:
|
|||||||
- **Web access**, multiple Webmails and administration interface
|
- **Web access**, multiple Webmails and administration interface
|
||||||
- **User features**, aliases, auto-reply, auto-forward, fetched accounts
|
- **User features**, aliases, auto-reply, auto-forward, fetched accounts
|
||||||
- **Admin features**, global admins, announcements, per-domain delegation, quotas
|
- **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
|
- **Antispam**, auto-learn, greylisting, DMARC and SPF, anti-spoofing
|
||||||
- **Freedom**, all FOSS components, no tracker included
|
- **Freedom**, all FOSS components, no tracker included
|
||||||
|
|
||||||
|
@ -12,10 +12,12 @@ def fetch_list():
|
|||||||
"id": fetch.id,
|
"id": fetch.id,
|
||||||
"tls": fetch.tls,
|
"tls": fetch.tls,
|
||||||
"keep": fetch.keep,
|
"keep": fetch.keep,
|
||||||
|
"scan": fetch.scan,
|
||||||
"user_email": fetch.user_email,
|
"user_email": fetch.user_email,
|
||||||
"protocol": fetch.protocol,
|
"protocol": fetch.protocol,
|
||||||
"host": fetch.host,
|
"host": fetch.host,
|
||||||
"port": fetch.port,
|
"port": fetch.port,
|
||||||
|
"folders": fetch.folders,
|
||||||
"username": fetch.username,
|
"username": fetch.username,
|
||||||
"password": fetch.password
|
"password": fetch.password
|
||||||
} for fetch in models.Fetch.query.all()
|
} for fetch in models.Fetch.query.all()
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import smtplib
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
@ -420,14 +419,19 @@ class Email(object):
|
|||||||
|
|
||||||
def sendmail(self, subject, body):
|
def sendmail(self, subject, body):
|
||||||
""" send an email to the address """
|
""" send an email to the address """
|
||||||
f_addr = f'{app.config["POSTMASTER"]}@{idna.encode(app.config["DOMAIN"]).decode("ascii")}'
|
try:
|
||||||
with smtplib.SMTP(app.config['HOST_AUTHSMTP'], port=10025) as smtp:
|
f_addr = f'{app.config["POSTMASTER"]}@{idna.encode(app.config["DOMAIN"]).decode("ascii")}'
|
||||||
to_address = f'{self.localpart}@{idna.encode(self.domain_name).decode("ascii")}'
|
ip, port = app.config['HOST_LMTP'].rsplit(':')
|
||||||
msg = text.MIMEText(body)
|
with smtplib.LMTP(ip, port=port) as lmtp:
|
||||||
msg['Subject'] = subject
|
to_address = f'{self.localpart}@{idna.encode(self.domain_name).decode("ascii")}'
|
||||||
msg['From'] = f_addr
|
msg = text.MIMEText(body)
|
||||||
msg['To'] = to_address
|
msg['Subject'] = subject
|
||||||
smtp.sendmail(f_addr, [to_address], msg.as_string())
|
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
|
@classmethod
|
||||||
def resolve_domain(cls, email):
|
def resolve_domain(cls, email):
|
||||||
@ -772,6 +776,8 @@ class Fetch(Base):
|
|||||||
username = db.Column(db.String(255), nullable=False)
|
username = db.Column(db.String(255), nullable=False)
|
||||||
password = db.Column(db.String(255), nullable=False)
|
password = db.Column(db.String(255), nullable=False)
|
||||||
keep = db.Column(db.Boolean, nullable=False, default=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)
|
last_check = db.Column(db.DateTime, nullable=True)
|
||||||
error = db.Column(db.String(1023), nullable=True)
|
error = db.Column(db.String(1023), nullable=True)
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from mailu.sso import sso
|
from mailu.sso import sso
|
||||||
import flask
|
import flask
|
||||||
|
|
||||||
@sso.route('/language/<language>', methods=['POST'])
|
@sso.route('/language/<language>', methods=['GET','POST'])
|
||||||
def set_language(language=None):
|
def set_language(language=None):
|
||||||
if language:
|
if language:
|
||||||
flask.session['language'] = language
|
flask.session['language'] = language
|
||||||
|
@ -41,6 +41,16 @@ class MultipleEmailAddressesVerify(object):
|
|||||||
if not pattern.match(field.data.replace(" ", "")):
|
if not pattern.match(field.data.replace(" ", "")):
|
||||||
raise validators.ValidationError(self.message)
|
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):
|
class ConfirmationForm(flask_wtf.FlaskForm):
|
||||||
submit = fields.SubmitField(_('Confirm'))
|
submit = fields.SubmitField(_('Confirm'))
|
||||||
|
|
||||||
@ -165,11 +175,13 @@ class FetchForm(flask_wtf.FlaskForm):
|
|||||||
('imap', 'IMAP'), ('pop3', 'POP3')
|
('imap', 'IMAP'), ('pop3', 'POP3')
|
||||||
])
|
])
|
||||||
host = fields.StringField(_('Hostname or IP'), [validators.DataRequired()])
|
host = fields.StringField(_('Hostname or IP'), [validators.DataRequired()])
|
||||||
port = fields.IntegerField(_('TCP port'), [validators.DataRequired(), validators.NumberRange(min=0, max=65535)])
|
port = fields.IntegerField(_('TCP port'), [validators.DataRequired(), validators.NumberRange(min=0, max=65535)], default=993)
|
||||||
tls = fields.BooleanField(_('Enable TLS'))
|
tls = fields.BooleanField(_('Enable TLS'), default=True)
|
||||||
username = fields.StringField(_('Username'), [validators.DataRequired()])
|
username = fields.StringField(_('Username'), [validators.DataRequired()])
|
||||||
password = fields.PasswordField(_('Password'))
|
password = fields.PasswordField(_('Password'))
|
||||||
keep = fields.BooleanField(_('Keep emails on the server'))
|
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'))
|
submit = fields.SubmitField(_('Submit'))
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,6 +24,8 @@
|
|||||||
|
|
||||||
{%- call macros.card(title="Settings") %}
|
{%- call macros.card(title="Settings") %}
|
||||||
{{ macros.form_field(form.keep) }}
|
{{ macros.form_field(form.keep) }}
|
||||||
|
{{ macros.form_field(form.scan) }}
|
||||||
|
{{ macros.form_field(form.folders) }}
|
||||||
{%- endcall %}
|
{%- endcall %}
|
||||||
|
|
||||||
{{ macros.form_field(form.submit) }}
|
{{ macros.form_field(form.submit) }}
|
||||||
|
@ -20,6 +20,8 @@
|
|||||||
<th>{% trans %}Endpoint{% endtrans %}</th>
|
<th>{% trans %}Endpoint{% endtrans %}</th>
|
||||||
<th>{% trans %}Username{% endtrans %}</th>
|
<th>{% trans %}Username{% endtrans %}</th>
|
||||||
<th>{% trans %}Keep emails{% 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 %}Last check{% endtrans %}</th>
|
||||||
<th>{% trans %}Status{% endtrans %}</th>
|
<th>{% trans %}Status{% endtrans %}</th>
|
||||||
<th>{% trans %}Created{% 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.protocol }}{{ 's' if fetch.tls else '' }}://{{ fetch.host }}:{{ fetch.port }}</td>
|
||||||
<td>{{ fetch.username }}</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.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.last_check | format_datetime or '-' }}</td>
|
||||||
<td>{{ fetch.error or '-' }}</td>
|
<td>{{ fetch.error or '-' }}</td>
|
||||||
<td data-sort="{{ fetch.created_at or '0000-00-00' }}">{{ fetch.created_at | format_date }}</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()
|
form = forms.AnnouncementForm()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
for user in models.User.query.all():
|
for user in models.User.query.all():
|
||||||
user.sendmail(form.announcement_subject.data,
|
if not user.sendmail(form.announcement_subject.data,
|
||||||
form.announcement_body.data)
|
form.announcement_body.data):
|
||||||
|
flask.flash('Failed to send to %s' % user.email, 'error')
|
||||||
# Force-empty the form
|
# Force-empty the form
|
||||||
form.announcement_subject.data = ''
|
form.announcement_subject.data = ''
|
||||||
form.announcement_body.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 mailu.ui import ui, forms, access
|
||||||
from flask import current_app as app
|
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)
|
user = models.User.query.get(user_email) or flask.abort(404)
|
||||||
form = forms.FetchForm()
|
form = forms.FetchForm()
|
||||||
form.password.validators = [wtforms.validators.DataRequired()]
|
form.password.validators = [wtforms.validators.DataRequired()]
|
||||||
|
utils.formatCSVField(form.folders)
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
fetch = models.Fetch(user=user)
|
fetch = models.Fetch(user=user)
|
||||||
form.populate_obj(fetch)
|
form.populate_obj(fetch)
|
||||||
|
if form.folders.data:
|
||||||
|
fetch.folders = form.folders.data.replace(' ','').split(',')
|
||||||
models.db.session.add(fetch)
|
models.db.session.add(fetch)
|
||||||
models.db.session.commit()
|
models.db.session.commit()
|
||||||
flask.flash('Fetch configuration created')
|
flask.flash('Fetch configuration created')
|
||||||
@ -46,10 +49,13 @@ def fetch_edit(fetch_id):
|
|||||||
flask.abort(404)
|
flask.abort(404)
|
||||||
fetch = models.Fetch.query.get(fetch_id) or flask.abort(404)
|
fetch = models.Fetch.query.get(fetch_id) or flask.abort(404)
|
||||||
form = forms.FetchForm(obj=fetch)
|
form = forms.FetchForm(obj=fetch)
|
||||||
|
utils.formatCSVField(form.folders)
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
if not form.password.data:
|
if not form.password.data:
|
||||||
form.password.data = fetch.password
|
form.password.data = fetch.password
|
||||||
form.populate_obj(fetch)
|
form.populate_obj(fetch)
|
||||||
|
if form.folders.data:
|
||||||
|
fetch.folders = form.folders.data.replace(' ','').split(',')
|
||||||
models.db.session.commit()
|
models.db.session.commit()
|
||||||
flask.flash('Fetch configuration updated')
|
flask.flash('Fetch configuration updated')
|
||||||
return flask.redirect(
|
return flask.redirect(
|
||||||
|
@ -64,10 +64,11 @@ def user_edit(user_email):
|
|||||||
form.quota_bytes.validators = [
|
form.quota_bytes.validators = [
|
||||||
wtforms.validators.NumberRange(max=max_quota_bytes)]
|
wtforms.validators.NumberRange(max=max_quota_bytes)]
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
if msg := utils.isBadOrPwned(form):
|
if form.pw.data:
|
||||||
flask.flash(msg, "error")
|
if msg := utils.isBadOrPwned(form):
|
||||||
return flask.render_template('user/edit.html', form=form, user=user,
|
flask.flash(msg, "error")
|
||||||
domain=user.domain, max_quota_bytes=max_quota_bytes)
|
return flask.render_template('user/edit.html', form=form, user=user,
|
||||||
|
domain=user.domain, max_quota_bytes=max_quota_bytes)
|
||||||
form.populate_obj(user)
|
form.populate_obj(user)
|
||||||
if form.pw.data:
|
if form.pw.data:
|
||||||
user.set_password(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_email_or_current = user_email or flask_login.current_user.email
|
||||||
user = models.User.query.get(user_email_or_current) or flask.abort(404)
|
user = models.User.query.get(user_email_or_current) or flask.abort(404)
|
||||||
form = forms.UserSettingsForm(obj=user)
|
form = forms.UserSettingsForm(obj=user)
|
||||||
if isinstance(form.forward_destination.data,str):
|
utils.formatCSVField(form.forward_destination)
|
||||||
data = form.forward_destination.data.replace(" ","").split(",")
|
|
||||||
else:
|
|
||||||
data = form.forward_destination.data
|
|
||||||
form.forward_destination.data = ", ".join(data)
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
form.forward_destination.data = form.forward_destination.data.replace(" ","").split(",")
|
form.forward_destination.data = form.forward_destination.data.replace(" ","").split(",")
|
||||||
form.populate_obj(user)
|
form.populate_obj(user)
|
||||||
|
@ -518,3 +518,10 @@ def isBadOrPwned(form):
|
|||||||
if breaches > 0:
|
if breaches > 0:
|
||||||
return f"This password appears in {breaches} data breaches! It is not unique; please change it."
|
return f"This password appears in {breaches} data breaches! It is not unique; please change it."
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def formatCSVField(field):
|
||||||
|
if isinstance(field.data,str):
|
||||||
|
data = field.data.replace(" ","").split(",")
|
||||||
|
else:
|
||||||
|
data = field.data
|
||||||
|
field.data = ", ".join(data)
|
||||||
|
25
core/admin/migrations/versions/f4f0f89e0047_.py
Normal file
25
core/admin/migrations/versions/f4f0f89e0047_.py
Normal file
@ -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 os
|
||||||
import logging as log
|
import logging as log
|
||||||
|
from pwd import getpwnam
|
||||||
import sys
|
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"))
|
log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "INFO"))
|
||||||
|
|
||||||
os.system("flask mailu advertise")
|
os.system("flask mailu advertise")
|
||||||
|
@ -1,22 +1,21 @@
|
|||||||
# syntax=docker/dockerfile-upstream:1.4.3
|
# syntax=docker/dockerfile-upstream:1.4.3
|
||||||
|
|
||||||
# base system image (intermediate)
|
# base system image (intermediate)
|
||||||
ARG DISTRO=alpine:3.16.2
|
ARG DISTRO=alpine:3.16.3
|
||||||
FROM $DISTRO as system
|
FROM $DISTRO as system
|
||||||
|
|
||||||
ENV TZ=Etc/UTC LANG=C.UTF-8
|
ENV TZ=Etc/UTC LANG=C.UTF-8
|
||||||
|
|
||||||
ARG MAILU_UID=1000
|
ARG MAILU_UID=1000
|
||||||
ARG MAILU_GID=1000
|
ARG MAILU_GID=1000
|
||||||
ARG TARGETPLATFORM
|
|
||||||
|
|
||||||
RUN set -euxo pipefail \
|
RUN set -euxo pipefail \
|
||||||
; addgroup -Sg ${MAILU_GID} mailu \
|
; addgroup -Sg ${MAILU_GID} mailu \
|
||||||
; adduser -Sg ${MAILU_UID} -G mailu -h /app -g "mailu app" -s /bin/bash 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)" \
|
; machine="$(uname -m)" \
|
||||||
; ! [[ "${TARGETPLATFORM}" != linux/arm/v7 && \( "${machine}" == x86_64 || "${machine}" == armv8* || "${machine}" == aarch64 \) ]] \
|
; ! [[ "${machine}" == x86_64 ]] \
|
||||||
|| apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing hardened-malloc
|
|| 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 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"
|
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 requirements-${MAILU_DEPS}.txt ./
|
||||||
COPY libs/ libs/
|
COPY libs/ libs/
|
||||||
|
|
||||||
RUN set -euxo pipefail \
|
ARG SNUFFLEUPAGUS_VERSION=0.8.3
|
||||||
; pip install -r requirements-${MAILU_DEPS}.txt || \
|
ENV SNUFFLEUPAGUS_URL https://github.com/jvoisin/snuffleupagus/archive/refs/tags/v$SNUFFLEUPAGUS_VERSION.tar.gz
|
||||||
{ \
|
|
||||||
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
|
|
||||||
|
|
||||||
|
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
|
# base mailu image
|
||||||
FROM system
|
FROM system
|
||||||
|
|
||||||
COPY --from=build /app/venv/ /app/venv/
|
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 VIRTUAL_ENV=/app/venv
|
||||||
ENV PATH="${VIRTUAL_ENV}/bin:${PATH}"
|
ENV PATH="${VIRTUAL_ENV}/bin:${PATH}"
|
||||||
|
@ -23,7 +23,7 @@ itsdangerous
|
|||||||
limits
|
limits
|
||||||
marshmallow
|
marshmallow
|
||||||
marshmallow-sqlalchemy
|
marshmallow-sqlalchemy
|
||||||
mysql-connector-python
|
mysql-connector-python==8.0.29
|
||||||
passlib
|
passlib
|
||||||
psycopg2-binary
|
psycopg2-binary
|
||||||
Pygments
|
Pygments
|
||||||
|
@ -41,7 +41,7 @@ MarkupSafe==2.1.1
|
|||||||
marshmallow==3.18.0
|
marshmallow==3.18.0
|
||||||
marshmallow-sqlalchemy==0.28.1
|
marshmallow-sqlalchemy==0.28.1
|
||||||
multidict==6.0.2
|
multidict==6.0.2
|
||||||
mysql-connector-python==8.0.31
|
mysql-connector-python==8.0.29
|
||||||
packaging==21.3
|
packaging==21.3
|
||||||
passlib==1.7.4
|
passlib==1.7.4
|
||||||
podop @ file:///app/libs/podop
|
podop @ file:///app/libs/podop
|
||||||
|
@ -10,5 +10,5 @@ RUN echo $VERSION >/version
|
|||||||
|
|
||||||
HEALTHCHECK CMD true
|
HEALTHCHECK CMD true
|
||||||
|
|
||||||
USER app
|
USER mailu
|
||||||
CMD ["/bin/bash", "-c", "sleep infinity"]
|
CMD ["/bin/bash", "-c", "sleep infinity"]
|
||||||
|
@ -33,4 +33,7 @@ while True:
|
|||||||
log.warning("Admin is not up just yet, retrying in 1 second")
|
log.warning("Admin is not up just yet, retrying in 1 second")
|
||||||
|
|
||||||
# Run rspamd
|
# 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
|
- **Web access**, multiple Webmails and administration interface
|
||||||
- **User features**, aliases, auto-reply, auto-forward, fetched accounts
|
- **User features**, aliases, auto-reply, auto-forward, fetched accounts
|
||||||
- **Admin features**, global admins, announcements, per-domain delegation, quotas
|
- **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
|
- **Antispam**, auto-learn, greylisting, DMARC and SPF, anti-spoofing
|
||||||
- **Freedom**, all FOSS components, no tracker included
|
- **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.
|
* 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
|
Authentication tokens
|
||||||
|
@ -2,11 +2,14 @@
|
|||||||
|
|
||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from pwd import getpwnam
|
||||||
import tempfile
|
import tempfile
|
||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
import re
|
import re
|
||||||
import requests
|
import requests
|
||||||
|
from socrate import system
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
@ -14,6 +17,7 @@ import traceback
|
|||||||
FETCHMAIL = """
|
FETCHMAIL = """
|
||||||
fetchmail -N \
|
fetchmail -N \
|
||||||
--idfile /data/fetchids --uidl \
|
--idfile /data/fetchids --uidl \
|
||||||
|
--pidfile /dev/shm/fetchmail.pid \
|
||||||
--sslcertck --sslcertpath /etc/ssl/certs \
|
--sslcertck --sslcertpath /etc/ssl/certs \
|
||||||
-f {}
|
-f {}
|
||||||
"""
|
"""
|
||||||
@ -24,7 +28,9 @@ poll "{host}" proto {protocol} port {port}
|
|||||||
user "{username}" password "{password}"
|
user "{username}" password "{password}"
|
||||||
is "{user_email}"
|
is "{user_email}"
|
||||||
smtphost "{smtphost}"
|
smtphost "{smtphost}"
|
||||||
|
{folders}
|
||||||
{options}
|
{options}
|
||||||
|
{lmtp}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@ -48,26 +54,37 @@ def fetchmail(fetchmailrc):
|
|||||||
|
|
||||||
def run(debug):
|
def run(debug):
|
||||||
try:
|
try:
|
||||||
fetches = requests.get("http://" + os.environ.get("HOST_ADMIN", "admin") + "/internal/fetch").json()
|
os.environ["SMTP_ADDRESS"] = system.get_host_address_from_environment("SMTP", "smtp")
|
||||||
smtphost, smtpport = extract_host_port(os.environ.get("HOST_SMTP", "smtp"), None)
|
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:
|
if smtpport is None:
|
||||||
smtphostport = smtphost
|
smtphostport = smtphost
|
||||||
else:
|
else:
|
||||||
smtphostport = "%s/%d" % (smtphost, smtpport)
|
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:
|
for fetch in fetches:
|
||||||
fetchmailrc = ""
|
fetchmailrc = ""
|
||||||
options = "options antispam 501, 504, 550, 553, 554"
|
options = "options antispam 501, 504, 550, 553, 554"
|
||||||
options += " ssl" if fetch["tls"] else ""
|
options += " ssl" if fetch["tls"] else ""
|
||||||
options += " keep" if fetch["keep"] else " fetchall"
|
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(
|
fetchmailrc += RC_LINE.format(
|
||||||
user_email=escape_rc_string(fetch["user_email"]),
|
user_email=escape_rc_string(fetch["user_email"]),
|
||||||
protocol=fetch["protocol"],
|
protocol=fetch["protocol"],
|
||||||
host=escape_rc_string(fetch["host"]),
|
host=escape_rc_string(fetch["host"]),
|
||||||
port=fetch["port"],
|
port=fetch["port"],
|
||||||
smtphost=smtphostport,
|
smtphost=smtphostport if fetch['scan'] else lmtphostport,
|
||||||
username=escape_rc_string(fetch["username"]),
|
username=escape_rc_string(fetch["username"]),
|
||||||
password=escape_rc_string(fetch["password"]),
|
password=escape_rc_string(fetch["password"]),
|
||||||
options=options
|
options=options,
|
||||||
|
folders=folders,
|
||||||
|
lmtp='' if fetch['scan'] else 'lmtp',
|
||||||
)
|
)
|
||||||
if debug:
|
if debug:
|
||||||
print(fetchmailrc)
|
print(fetchmailrc)
|
||||||
@ -86,14 +103,21 @@ def run(debug):
|
|||||||
user_info in error_message):
|
user_info in error_message):
|
||||||
print(error_message)
|
print(error_message)
|
||||||
finally:
|
finally:
|
||||||
requests.post("http://" + os.environ.get("HOST_ADMIN", "admin") + "/internal/fetch/{}".format(fetch["id"]),
|
requests.post("http://{}/internal/fetch/{}".format(os.environ['ADMIN_ADDRESS'],fetch['id']),
|
||||||
json=error_message.split("\n")[0]
|
json=error_message.split('\n')[0]
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
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:
|
while True:
|
||||||
delay = int(os.environ.get("FETCHMAIL_DELAY", 60))
|
delay = int(os.environ.get("FETCHMAIL_DELAY", 60))
|
||||||
print("Sleeping for {} seconds".format(delay))
|
print("Sleeping for {} seconds".format(delay))
|
||||||
|
@ -1,24 +1,21 @@
|
|||||||
ARG DISTRO=alpine:3.14.5
|
# syntax=docker/dockerfile-upstream:1.4.3
|
||||||
FROM $DISTRO
|
|
||||||
ARG VERSION
|
# setup image
|
||||||
ENV TZ Etc/UTC
|
FROM base
|
||||||
|
|
||||||
|
ARG VERSION=local
|
||||||
LABEL version=$VERSION
|
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 flavors /data/flavors
|
||||||
COPY templates /data/templates
|
COPY templates /data/templates
|
||||||
COPY static ./static
|
COPY static ./static
|
||||||
|
COPY server.py ./server.py
|
||||||
|
COPY main.py ./main.py
|
||||||
|
|
||||||
|
RUN echo $VERSION >> /version
|
||||||
|
|
||||||
EXPOSE 80/tcp
|
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
|
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 }}
|
env_file: {{ env }}
|
||||||
volumes:
|
volumes:
|
||||||
- "{{ root }}/data/fetchmail:/data"
|
- "{{ root }}/data/fetchmail:/data"
|
||||||
{% if resolver_enabled %}
|
|
||||||
depends_on:
|
depends_on:
|
||||||
|
- admin
|
||||||
|
- smtp
|
||||||
|
- imap
|
||||||
|
{% if resolver_enabled %}
|
||||||
- resolver
|
- resolver
|
||||||
dns:
|
dns:
|
||||||
- {{ dns }}
|
- {{ dns }}
|
||||||
@ -168,7 +171,7 @@ services:
|
|||||||
# Webmail
|
# Webmail
|
||||||
{% if webmail_type != 'none' %}
|
{% if webmail_type != 'none' %}
|
||||||
webmail:
|
webmail:
|
||||||
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}{{ webmail_type }}:${MAILU_VERSION:-{{ version }}}
|
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}webmail:${MAILU_VERSION:-{{ version }}}
|
||||||
restart: always
|
restart: always
|
||||||
env_file: {{ env }}
|
env_file: {{ env }}
|
||||||
volumes:
|
volumes:
|
||||||
|
@ -119,7 +119,7 @@ services:
|
|||||||
|
|
||||||
{% if webmail_type != 'none' %}
|
{% if webmail_type != 'none' %}
|
||||||
webmail:
|
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 }}
|
env_file: {{ env }}
|
||||||
volumes:
|
volumes:
|
||||||
- "{{ root }}/webmail:/data"
|
- "{{ 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",
|
"imap",
|
||||||
"smtp",
|
"smtp",
|
||||||
|
|
||||||
"snappymail",
|
"webmail",
|
||||||
"roundcube",
|
|
||||||
|
|
||||||
"antivirus",
|
"antivirus",
|
||||||
"fetchmail",
|
"fetchmail",
|
||||||
@ -107,6 +106,9 @@ target "docs" {
|
|||||||
target "setup" {
|
target "setup" {
|
||||||
inherits = ["defaults"]
|
inherits = ["defaults"]
|
||||||
context = "setup/"
|
context = "setup/"
|
||||||
|
contexts = {
|
||||||
|
base = "target:base"
|
||||||
|
}
|
||||||
tags = tag("setup")
|
tags = tag("setup")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,24 +171,15 @@ target "smtp" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------------------
|
||||||
# Webmail images
|
# Webmail image
|
||||||
# -----------------------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------------------
|
||||||
target "snappymail" {
|
target "webmail" {
|
||||||
inherits = ["defaults"]
|
inherits = ["defaults"]
|
||||||
context = "webmails/snappymail/"
|
context = "webmails/"
|
||||||
contexts = {
|
contexts = {
|
||||||
base = "target:base"
|
base = "target:base"
|
||||||
}
|
}
|
||||||
tags = tag("snappymail")
|
tags = tag("webmail")
|
||||||
}
|
|
||||||
|
|
||||||
target "roundcube" {
|
|
||||||
inherits = ["defaults"]
|
|
||||||
context = "webmails/roundcube/"
|
|
||||||
contexts = {
|
|
||||||
base = "target:base"
|
|
||||||
}
|
|
||||||
tags = tag("roundcube")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------------------
|
||||||
|
7
tests/compose/filters/02_email_antispam.sh
Executable file
7
tests/compose/filters/02_email_antispam.sh
Executable file
@ -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=
|
|
10
tests/compose/webmail/01_ensure_admin_unreachable.sh
Executable file
10
tests/compose/webmail/01_ensure_admin_unreachable.sh
Executable file
@ -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
|
||||||
webmail:
|
webmail:
|
||||||
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}roundcube:${MAILU_VERSION:-local}
|
image: ${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}webmail:${MAILU_VERSION:-local}
|
||||||
restart: always
|
restart: always
|
||||||
env_file: mailu.env
|
env_file: mailu.env
|
||||||
volumes:
|
volumes:
|
@ -54,7 +54,7 @@ DISABLE_STATISTICS=False
|
|||||||
ADMIN=false
|
ADMIN=false
|
||||||
|
|
||||||
# Choose which webmail to run if any (values: roundcube, snappymail, none)
|
# Choose which webmail to run if any (values: roundcube, snappymail, none)
|
||||||
WEBMAIL=roundcube
|
WEBMAIL=snappymail
|
||||||
|
|
||||||
# Dav server implementation (value: radicale, none)
|
# Dav server implementation (value: radicale, none)
|
||||||
WEBDAV=none
|
WEBDAV=none
|
1
towncrier/newsfragments/1231.bugfix
Normal file
1
towncrier/newsfragments/1231.bugfix
Normal file
@ -0,0 +1 @@
|
|||||||
|
Add an option so that emails fetched with fetchmail don't go through the filters (closes #1231)
|
1
towncrier/newsfragments/2231.bugfix
Normal file
1
towncrier/newsfragments/2231.bugfix
Normal file
@ -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.
|
1
towncrier/newsfragments/2246.bugfix
Normal file
1
towncrier/newsfragments/2246.bugfix
Normal file
@ -0,0 +1 @@
|
|||||||
|
Fetchmail: Missing support for '*_ADDRESS' env vars
|
1
towncrier/newsfragments/2526.misc
Normal file
1
towncrier/newsfragments/2526.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Upgrade Snappymail to 2.21 and merge the webmail containers
|
1
towncrier/newsfragments/2539.misc
Normal file
1
towncrier/newsfragments/2539.misc
Normal file
@ -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
|
1
towncrier/newsfragments/2550.misc
Normal file
1
towncrier/newsfragments/2550.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Add Snuffleupagus to protect webmails (a Suhosin replacement)
|
1
towncrier/newsfragments/711.feature
Normal file
1
towncrier/newsfragments/711.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Allow other folders to be synced by fetchmail
|
96
webmails/Dockerfile
Normal file
96
webmails/Dockerfile
Normal file
@ -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;
|
||||||
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;
|
include /etc/nginx/mime.types;
|
||||||
|
|
||||||
@ -16,6 +20,11 @@ server {
|
|||||||
|
|
||||||
# set maximum body size to configured limit
|
# set maximum body size to configured limit
|
||||||
client_max_body_size {{ MESSAGE_SIZE_LIMIT|int + 8388608 }};
|
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 / {
|
location / {
|
||||||
try_files $uri $uri/ /index.php$args;
|
try_files $uri $uri/ /index.php$args;
|
||||||
@ -42,11 +51,11 @@ server {
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~ /\. {
|
location ~ (^|/)\. {
|
||||||
deny all;
|
deny all;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ^~ /data {
|
location ~* /(config|temp|logs|data) {
|
||||||
deny all;
|
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
|
; the variable $pool can be used in any directive and will be replaced by the
|
||||||
; pool name ('roundcube' here)
|
; pool name ('php' here)
|
||||||
[roundcube]
|
[php]
|
||||||
|
|
||||||
; Redirect worker stdout and stderr into main error log. If not set, stdout and
|
; 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.
|
; stderr will be redirected to /dev/null according to FastCGI specs.
|
||||||
@ -11,8 +11,8 @@ catch_workers_output = 1
|
|||||||
; Unix user/group of processes
|
; Unix user/group of processes
|
||||||
; Note: The user is mandatory. If the group is not set, the default user's group
|
; Note: The user is mandatory. If the group is not set, the default user's group
|
||||||
; will be used.
|
; will be used.
|
||||||
user = nginx
|
user = mailu
|
||||||
group = nginx
|
group = mailu
|
||||||
|
|
||||||
; The address on which to accept FastCGI requests.
|
; The address on which to accept FastCGI requests.
|
||||||
; Valid syntaxes are:
|
; Valid syntaxes are:
|
15
webmails/php.ini
Normal file
15
webmails/php.ini
Normal file
@ -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
|
// Generals
|
||||||
$config['db_dsnw'] = '{{ DB_DSNW }}';
|
$config['db_dsnw'] = '{{ DB_DSNW }}';
|
||||||
$config['temp_dir'] = '/tmp/';
|
$config['temp_dir'] = '/dev/shm/';
|
||||||
$config['des_key'] = '{{ SECRET_KEY }}';
|
$config['des_key'] = '{{ ROUNDCUBE_KEY }}';
|
||||||
$config['cipher_method'] = 'AES-256-CBC';
|
$config['cipher_method'] = 'AES-256-CBC';
|
||||||
$config['identities_level'] = 0;
|
$config['identities_level'] = 0;
|
||||||
$config['reply_all_mode'] = 1;
|
$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';
|
$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;
|
return $args;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,7 +28,7 @@ class mailu extends rcube_plugin
|
|||||||
header('HTTP/1.0 403 Forbidden');
|
header('HTTP/1.0 403 Forbidden');
|
||||||
print('mailu sso failure');
|
print('mailu sso failure');
|
||||||
} else {
|
} else {
|
||||||
header('Location: sso.php');
|
header('Location: sso.php', 302);
|
||||||
}
|
}
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
@ -54,19 +47,19 @@ class mailu extends rcube_plugin
|
|||||||
{
|
{
|
||||||
$this->load_config();
|
$this->load_config();
|
||||||
$sso_logout_url = rcmail::get_instance()->config->get('sso_logout_url');
|
$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();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
function login($args)
|
function login($args)
|
||||||
{
|
{
|
||||||
header('Location: index.php');
|
header('Location: index.php', 302);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
function login_failed($args)
|
function login_failed($args)
|
||||||
{
|
{
|
||||||
header('Location: sso.php');
|
header('Location: sso.php', 302);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
102
webmails/roundcube/pubkey.asc
Normal file
102
webmails/roundcube/pubkey.asc
Normal file
@ -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]
|
[security]
|
||||||
allow_admin_panel = Off
|
allow_admin_panel = Off
|
||||||
|
openpgp = On
|
||||||
|
|
||||||
[labs]
|
[labs]
|
||||||
allow_gravatar = Off
|
allow_gravatar = Off
|
||||||
{% if WEB_WEBMAIL == '/' %}
|
image_exif_auto_rotate = On
|
||||||
custom_login_link='sso.php'
|
try_to_detect_hidden_images = On
|
||||||
{% else %}
|
{% if WEB_WEBMAIL == '/' %}custom_login_link = "sso.php"{% else %}custom_login_link = "{{ WEB_WEBMAIL }}/sso.php"{% endif %}
|
||||||
custom_login_link='{{ WEB_WEBMAIL }}/sso.php'
|
custom_logout_link = "/sso/logout"
|
||||||
{% endif %}
|
|
||||||
custom_logout_link='/sso/logout'
|
|
||||||
|
|
||||||
[contacts]
|
[contacts]
|
||||||
enable = On
|
enable = On
|
||||||
@ -21,3 +20,10 @@ allow_sync = On
|
|||||||
|
|
||||||
[defaults]
|
[defaults]
|
||||||
contacts_autosave = On
|
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
|
|
50
webmails/snappymail/defaults/default.json
Normal file
50
webmails/snappymail/defaults/default.json
Normal file
@ -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);
|
$ssoHash = \RainLoop\Api::CreateUserSsoHash($email, $password);
|
||||||
|
|
||||||
// redirect to webmail sso url
|
// redirect to webmail sso url
|
||||||
header('Location: index.php?sso&hash='.$ssoHash);
|
header('Location: index.php?sso&hash='.$ssoHash, 302);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
header('HTTP/1.0 403 Forbidden');
|
header('HTTP/1.0 403 Forbidden', 403);
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
11
webmails/snappymail/pubkey.asc
Normal file
11
webmails/snappymail/pubkey.asc
Normal file
@ -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;"])
|
|
133
webmails/snuffleupagus.rules
Normal file
133
webmails/snuffleupagus.rules
Normal file
@ -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 logging
|
||||||
import sys
|
import sys
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import shutil
|
||||||
import hmac
|
import hmac
|
||||||
|
|
||||||
from socrate import conf
|
from socrate import conf, system
|
||||||
|
|
||||||
env = os.environ
|
env = os.environ
|
||||||
|
|
||||||
@ -17,6 +18,8 @@ context = {}
|
|||||||
context.update(env)
|
context.update(env)
|
||||||
|
|
||||||
context["MAX_FILESIZE"] = str(int(int(env.get("MESSAGE_SIZE_LIMIT", "50000000")) * 0.66 / 1048576))
|
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")
|
db_flavor = env.get("ROUNDCUBE_DB_FLAVOR", "sqlite")
|
||||||
if 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)
|
print(f"Can't read SECRET_KEY from file: {exc}", file=sys.stderr)
|
||||||
exit(2)
|
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
|
# roundcube plugins
|
||||||
# (using "dict" because it is ordered and "set" is not)
|
# (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:
|
if plugins:
|
||||||
plugins["mailu"] = None
|
plugins["mailu"] = None
|
||||||
else:
|
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)
|
context["SESSION_TIMEOUT_MINUTES"] = max(int(env.get("SESSION_TIMEOUT", "3600")) // 60, 1)
|
||||||
|
|
||||||
# create config files
|
# create config files
|
||||||
conf.jinja("/conf/php.ini", context, "/etc/php81/php.ini")
|
conf.jinja("/conf/config.inc.php", context, "/var/www/roundcube/config/config.inc.php")
|
||||||
conf.jinja("/conf/config.inc.php", context, "/var/www/webmail/config/config.inc.php")
|
|
||||||
|
|
||||||
# create dirs
|
# create dirs
|
||||||
os.system("mkdir -p /data/gpg")
|
os.system("mkdir -p /data/gpg")
|
||||||
|
|
||||||
print("Initializing database")
|
print("Initializing database")
|
||||||
try:
|
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)
|
stderr=subprocess.STDOUT)
|
||||||
print(result.decode())
|
print(result.decode())
|
||||||
except subprocess.CalledProcessError as exc:
|
except subprocess.CalledProcessError as exc:
|
||||||
@ -88,27 +92,35 @@ except subprocess.CalledProcessError as exc:
|
|||||||
|
|
||||||
print("Upgrading database")
|
print("Upgrading database")
|
||||||
try:
|
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:
|
except subprocess.CalledProcessError as exc:
|
||||||
exit(4)
|
exit(4)
|
||||||
else:
|
else:
|
||||||
print("Cleaning database")
|
print("Cleaning database")
|
||||||
try:
|
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:
|
except subprocess.CalledProcessError as exc:
|
||||||
exit(5)
|
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
|
# setup permissions
|
||||||
os.system("chown -R nginx:nginx /data")
|
os.system("chown -R mailu:mailu /data")
|
||||||
os.system("chmod -R a+rX /var/www/webmail/")
|
|
||||||
|
|
||||||
# Configure nginx
|
# 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"):
|
if os.path.exists("/var/run/nginx.pid"):
|
||||||
os.system("nginx -s reload")
|
os.system("nginx -s reload")
|
||||||
|
|
||||||
# clean env
|
# 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
|
# run nginx
|
||||||
os.system("php-fpm81")
|
os.system("php-fpm81")
|
Loading…
x
Reference in New Issue
Block a user