5 Commits

Author SHA1 Message Date
Spitap
4e2e9b6ab9 Revert "Make pip quiet"
This reverts commit 7ccc871da7.
2024-02-05 12:27:17 +01:00
Spitap
7ccc871da7 Make pip quiet 2024-02-05 12:23:06 +01:00
Spitap
5f4817736f Increased verbosity 2024-02-05 11:52:26 +01:00
Spitfireap
804c20a18d fixed output not decoded 2024-02-01 13:13:06 +01:00
Spitfireap
dd32b21ce9 Improved version checking 2024-02-01 13:07:11 +01:00
96 changed files with 642 additions and 3186 deletions

View File

@@ -11,48 +11,47 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: [3.9, '3.10', '3.11', '3.12'] python-version: [3.7, 3.8, 3.9]
fail-fast: false fail-fast: false
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5 uses: actions/setup-python@v2
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install dependencies - name: Install dependencies
run: | run: |
pip install -r test-requirements.txt pip install -r test-requirements.txt
- name: Run tests - name: Run tests
if: ${{ matrix.python-version != '3.12' }} if: ${{ matrix.python-version != '3.9' }}
run: | run: |
python tests.py python tests.py
- name: Run tests and coverage - name: Run tests and coverage
if: ${{ matrix.python-version == '3.12' }} if: ${{ matrix.python-version == '3.9' }}
run: | run: |
coverage run tests.py coverage run tests.py
- name: Upload coverage result - name: Upload coverage result
if: ${{ matrix.python-version == '3.12' }} if: ${{ matrix.python-version == '3.9' }}
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v2
with: with:
name: coverage-results name: coverage-results
path: .coverage path: .coverage
include-hidden-files: true
coverage: coverage:
needs: test needs: test
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v2
with: with:
python-version: '3.12' python-version: '3.9'
- name: Install dependencies - name: Install dependencies
run: | run: |
pip install codecov pip install codecov
- name: Download coverage results - name: Download coverage results
uses: actions/download-artifact@v4 uses: actions/download-artifact@v2
with: with:
name: coverage-results name: coverage-results
- name: Report coverage - name: Report coverage

View File

@@ -1,32 +0,0 @@
name: Update version file
on:
workflow_run:
branches: [ master ]
workflows: [Modoboa installer]
types:
- completed
jobs:
update-version:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository.
ref: ${{ github.head_ref }}
- name: Overwrite file
uses: "DamianReeves/write-file-action@master"
with:
path: version.txt
write-mode: overwrite
contents: ${{ github.sha }}
- name: Commit & Push
uses: Andro999b/push@v1.3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ github.ref_name }}
force: true
message: '[GitHub Action] Updated version file'

5
.gitignore vendored
View File

@@ -58,8 +58,3 @@ target/
# PyCharm # PyCharm
.idea/ .idea/
#KDE
*.kdev4
installer.cfg

View File

@@ -1,19 +1,20 @@
**modoboa-installer** modoboa-installer
===================== =================
|workflow| |codecov| |workflow| |codecov|
An installer which deploys a complete mail server based on Modoboa. An installer which deploy a complete mail server based on Modoboa.
.. warning:: .. warning::
This tool is still in beta, it has been tested on: This tool is still in beta stage, it has been tested on:
* Debian 12 and upper * Debian Buster (10) / Bullseye (11)
* Ubuntu Focal Fossa (20.04) and upper * Ubuntu Bionic Beaver (18.04) and upper
* CentOS 7
.. warning:: .. warning::
``/tmp`` partition must be mounted without the ``noexec`` option. ``/tmp`` partition must be mounted without the ``noexec`` option.
.. note:: .. note::
@@ -43,7 +44,8 @@ The following components are installed by the installer:
* Nginx and uWSGI * Nginx and uWSGI
* Postfix * Postfix
* Dovecot * Dovecot
* Amavis (with SpamAssassin and ClamAV) or Rspamd * Amavis (with SpamAssassin and ClamAV)
* automx (autoconfiguration service)
* OpenDKIM * OpenDKIM
* Radicale (CalDAV and CardDAV server) * Radicale (CalDAV and CardDAV server)
@@ -75,7 +77,7 @@ If you want more information about the installation process, add the
``--debug`` option to your command line. ``--debug`` option to your command line.
Upgrade mode Upgrade mode
============ ------------
An experimental upgrade mode is available. An experimental upgrade mode is available.
@@ -90,8 +92,8 @@ You can activate it as follows::
It will automatically install latest versions of modoboa and its plugins. It will automatically install latest versions of modoboa and its plugins.
Backup mode Backup mode
=========== ------------
An experimental backup mode is available. An experimental backup mode is available.
@@ -106,19 +108,7 @@ You can start the process as follows::
Then follow the step on the console. Then follow the step on the console.
There is also a non-interactive mode:: There is also a non-interactive mode:
$ sudo ./run.py --silent-backup <your domain>
You can also add a path, else it will be saved in ./modoboa_backup/Backup_M_Y_d_H_M::
$ sudo ./run.py --silent-backup --backup-path "/My_Backup_Path" <your domain>
if you want to disable mail backup::
$ sudo ./run.py --backup --no-mail <your domain>
This can be useful for larger instance
1. Silent mode 1. Silent mode
@@ -140,7 +130,7 @@ configuration file (set enabled to False).
This can be useful for larger instance. This can be useful for larger instance.
Restore mode Restore mode
============ ------------
An experimental restore mode is available. An experimental restore mode is available.
@@ -151,7 +141,7 @@ You can start the process as follows::
Then wait for the process to finish. Then wait for the process to finish.
Change the generated hostname Change the generated hostname
============================= -----------------------------
By default, the installer will setup your email server using the By default, the installer will setup your email server using the
following hostname: ``mail.<your domain>``. If you want a different following hostname: ``mail.<your domain>``. If you want a different
@@ -170,26 +160,14 @@ modifications.
Finally, run the installer without the Finally, run the installer without the
``--stop-after-configfile-check`` option. ``--stop-after-configfile-check`` option.
Certificate Let's Encrypt certificate
=========== -------------------------
Self-signed
-----------
It is the default type of certificate the installer will generate, it
is however not recommended for production use.
Letsencrypt
-----------
.. warning:: .. warning::
Please note that by using this option, you agree to the `ToS Please note this option requires the hostname you're using to be
<https://community.letsencrypt.org/tos>`_ of valid (ie. it can be resolved with a DNS query) and to match the
letsencrypt and that your IP will be logged (see ToS). server you're installing Modoboa on.
Please also note this option requires the hostname you're using to be
valid (ie. it can be resolved with a DNS query) and to match the
server you're installing Modoboa on.
If you want to generate a valid certificate using `Let's Encrypt If you want to generate a valid certificate using `Let's Encrypt
<https://letsencrypt.org/>`_, edit the ``installer.cfg`` file and <https://letsencrypt.org/>`_, edit the ``installer.cfg`` file and
@@ -198,8 +176,6 @@ modify the following settings::
[certificate] [certificate]
generate = true generate = true
type = letsencrypt type = letsencrypt
tls_cert_file_path =
tls_key_file_path =
[letsencrypt] [letsencrypt]
email = admin@example.com email = admin@example.com
@@ -207,43 +183,6 @@ modify the following settings::
Change the ``email`` setting to a valid value since it will be used Change the ``email`` setting to a valid value since it will be used
for account recovery. for account recovery.
Manual
------
.. warning::
It is not possible to configure manual certs interactively, so
you'll have to do it in 2 steps. Please run ``run.py`` with
`--stop-after-configfile-check` first, configure your file as
desired and apply the configuration as written bellow. Then run
``run.py`` again but without `--stop-after-configfile-check` or
`--interactive`.
If you want to use already generated certs, simply edit the
``installer.cfg`` file and modify the following settings::
[certificate]
generate = true
type = manual
tls_cert_file_path = *path to tls fullchain file*
tls_key_file_path = *path to tls key file*
Antispam
========
You have 3 options regarding antispam : disabled, Amavis, Rspamd
Amavis
------
Amavis
Rspamd
------
Rspamd
.. |workflow| image:: https://github.com/modoboa/modoboa-installer/workflows/Modoboa%20installer/badge.svg .. |workflow| image:: https://github.com/modoboa/modoboa-installer/workflows/Modoboa%20installer/badge.svg
.. |codecov| image:: https://codecov.io/gh/modoboa/modoboa-installer/graph/badge.svg?token=Fo2o1GdHZq .. |codecov| image:: http://codecov.io/github/modoboa/modoboa-installer/coverage.svg?branch=master
:target: https://codecov.io/gh/modoboa/modoboa-installer :target: http://codecov.io/github/modoboa/modoboa-installer?branch=master

View File

@@ -1,37 +0,0 @@
"""Checks to be performed before any install or upgrade"""
import sys
from urllib.request import urlopen
from modoboa_installer import utils
def check_version():
local_version = ""
with open("version.txt", "r") as version:
local_version = version.readline()
remote_version = ""
with urlopen("https://raw.githubusercontent.com/modoboa/modoboa-installer/master/version.txt") as r_version:
remote_version = r_version.read().decode()
if local_version == "" or remote_version == "":
utils.printcolor(
"Could not check that your installer is up-to-date: "
f"local version: {local_version}, "
f"remote version: {remote_version}",
utils.YELLOW
)
if remote_version != local_version:
utils.error(
"Your installer seems outdated.\n"
"Check README file for instructions about how to update.\n"
"No support will be provided without an up-to-date installer!"
)
answer = utils.user_input("Continue anyway? (y/N) ")
if not answer.lower().startswith("y"):
sys.exit(0)
else:
utils.success("Installer seems up to date!")
def handle():
check_version()

View File

@@ -21,26 +21,13 @@ COMPATIBILITY_MATRIX = {
"modoboa-sievefilters": ">=1.1.1", "modoboa-sievefilters": ">=1.1.1",
"modoboa-webmail": ">=1.2.0", "modoboa-webmail": ">=1.2.0",
}, },
"2.1.0": {
"modoboa-pdfcredentials": None,
"modoboa-dmarc": None,
"modoboa-imap-migration": None,
},
} }
EXTENSIONS_AVAILABILITY = { EXTENSIONS_AVAILABILITY = {
"modoboa-contacts": "1.7.4", "modoboa-contacts": "1.7.4",
} }
REMOVED_EXTENSIONS = {
"modoboa-pdfcredentials": "2.1.0",
"modoboa-dmarc": "2.1.0",
"modoboa-imap-migration": "2.1.0",
"modoboa-sievefilters": "2.3.0",
"modoboa-postfix-autoreply": "2.3.0",
"modoboa-contacts": "2.4.0",
"modoboa-radicale": "2.4.0",
"modoboa-webmail": "2.4.0",
}
APP_INCOMPATIBILITY = {
"opendkim": ["rspamd"],
"amavis": ["rspamd"],
"postwhite": ["rspamd"],
"spamassassin": ["rspamd"]
}

View File

@@ -1,6 +1,5 @@
import random import random
import string import string
import uuid
from .constants import DEFAULT_BACKUP_DIRECTORY from .constants import DEFAULT_BACKUP_DIRECTORY
@@ -12,10 +11,6 @@ def make_password(length=16):
string.ascii_letters + string.digits) for _ in range(length)) string.ascii_letters + string.digits) for _ in range(length))
def make_client_secret():
return str(uuid.uuid4())
# Validators should return a tuple bool, error message # Validators should return a tuple bool, error message
def is_email(user_input): def is_email(user_input):
"""Return True in input is a valid email""" """Return True in input is a valid email"""
@@ -32,50 +27,25 @@ ConfigDictTemplate = [
} }
] ]
}, },
{
"name": "antispam",
"values": [
{
"option": "enabled",
"default": "true",
"customizable": True,
"values": ["true", "false"],
"question": "Do you want to setup an antispam utility?"
},
{
"option": "type",
"default": "amavis",
"customizable": True,
"question": "Please select your antispam utility",
"values": ["rspamd", "amavis"],
"if": ["antispam.enabled=true"]
}
]
},
{ {
"name": "certificate", "name": "certificate",
"values": [ "values": [
{
"option": "generate",
"default": "true",
},
{ {
"option": "type", "option": "type",
"default": "self-signed", "default": "self-signed",
"customizable": True, "customizable": True,
"question": "Please choose your certificate type", "question": "Please choose your certificate type",
"values": ["self-signed", "letsencrypt", "manual"], "values": ["self-signed", "letsencrypt"],
"non_interactive_values": ["manual"],
},
{
"option": "tls_cert_file_path",
"default": ""
},
{
"option": "tls_key_file_path",
"default": ""
} }
], ],
}, },
{ {
"name": "letsencrypt", "name": "letsencrypt",
"if": ["certificate.type=letsencrypt"], "if": "certificate.type=letsencrypt",
"values": [ "values": [
{ {
"option": "email", "option": "email",
@@ -110,7 +80,7 @@ ConfigDictTemplate = [
}, },
{ {
"name": "postgres", "name": "postgres",
"if": ["database.engine=postgres"], "if": "database.engine=postgres",
"values": [ "values": [
{ {
"option": "user", "option": "user",
@@ -126,7 +96,7 @@ ConfigDictTemplate = [
}, },
{ {
"name": "mysql", "name": "mysql",
"if": ["database.engine=mysql"], "if": "database.engine=mysql",
"values": [ "values": [
{ {
"option": "user", "option": "user",
@@ -210,16 +180,14 @@ ConfigDictTemplate = [
"customizable": True, "customizable": True,
"question": "Please enter Modoboa db password", "question": "Please enter Modoboa db password",
}, },
{
"option": "cron_error_recipient",
"default": "root",
"customizable": True,
"question":
"Please enter a mail recipient for cron error reports"
},
{ {
"option": "extensions", "option": "extensions",
"default": "" "default": (
"modoboa-amavis "
"modoboa-postfix-autoreply modoboa-sievefilters "
"modoboa-webmail modoboa-contacts "
"modoboa-radicale"
),
}, },
{ {
"option": "devmode", "option": "devmode",
@@ -228,59 +196,40 @@ ConfigDictTemplate = [
] ]
}, },
{ {
"name": "rspamd", "name": "automx",
"if": ["antispam.enabled=true", "antispam.type=rspamd"],
"values": [ "values": [
{ {
"option": "enabled", "option": "enabled",
"default": ["antispam.enabled=true", "antispam.type=rspamd"],
},
{
"option": "user",
"default": "_rspamd",
},
{
"option": "password",
"default": make_password,
"customizable": True,
"question": "Please enter Rspamd interface password",
},
{
"option": "dnsbl",
"default": "true", "default": "true",
}, },
{ {
"option": "dkim_keys_storage_dir", "option": "user",
"default": "/var/lib/dkim" "default": "automx",
}, },
{ {
"option": "key_map_path", "option": "config_dir",
"default": "/var/lib/dkim/keys.path.map" "default": "/etc",
}, },
{ {
"option": "selector_map_path", "option": "home_dir",
"default": "/var/lib/dkim/selectors.path.map" "default": "/srv/automx",
}, },
{ {
"option": "greylisting", "option": "venv_path",
"default": "true" "default": "%(home_dir)s/env",
}, },
{ {
"option": "whitelist_auth", "option": "instance_path",
"default": "true" "default": "%(home_dir)s/instance",
}, },
{ ]
"option": "whitelist_auth_weigth",
"default": "-5"
}
],
}, },
{ {
"name": "amavis", "name": "amavis",
"values": [ "values": [
{ {
"option": "enabled", "option": "enabled",
"default": ["antispam.enabled=true", "antispam.type=amavis"], "default": "true",
}, },
{ {
"option": "user", "option": "user",
@@ -301,6 +250,8 @@ ConfigDictTemplate = [
{ {
"option": "dbpassword", "option": "dbpassword",
"default": make_password, "default": make_password,
"customizable": True,
"question": "Please enter amavis db password"
}, },
], ],
}, },
@@ -350,15 +301,7 @@ ConfigDictTemplate = [
}, },
{ {
"option": "radicale_auth_socket_path", "option": "radicale_auth_socket_path",
"default": "/var/run/dovecot/auth-radicale", "default": "/var/run/dovecot/auth-radicale"
},
{
"option": "move_spam_to_junk",
"default": "true",
},
{
"option": "oauth2_client_secret",
"default": make_client_secret
}, },
] ]
}, },
@@ -380,7 +323,7 @@ ConfigDictTemplate = [
"values": [ "values": [
{ {
"option": "enabled", "option": "enabled",
"default": "false", "default": "true",
}, },
{ {
"option": "config_dir", "option": "config_dir",
@@ -414,7 +357,7 @@ ConfigDictTemplate = [
"values": [ "values": [
{ {
"option": "enabled", "option": "enabled",
"default": ["antispam.enabled=true", "antispam.type=amavis"], "default": "true",
}, },
{ {
"option": "config_dir", "option": "config_dir",
@@ -424,11 +367,10 @@ ConfigDictTemplate = [
}, },
{ {
"name": "spamassassin", "name": "spamassassin",
"if": ["antispam.enabled=true", "antispam.type=amavis"],
"values": [ "values": [
{ {
"option": "enabled", "option": "enabled",
"default": ["antispam.enabled=true", "antispam.type=amavis"], "default": "true",
}, },
{ {
"option": "config_dir", "option": "config_dir",
@@ -463,7 +405,7 @@ ConfigDictTemplate = [
}, },
{ {
"option": "nb_processes", "option": "nb_processes",
"default": "4", "default": "2",
}, },
] ]
}, },
@@ -489,20 +431,15 @@ ConfigDictTemplate = [
{ {
"option": "venv_path", "option": "venv_path",
"default": "%(home_dir)s/env", "default": "%(home_dir)s/env",
}, }
{
"option": "oauth2_client_secret",
"default": make_client_secret
},
] ]
}, },
{ {
"name": "opendkim", "name": "opendkim",
"if": ["antispam.enabled=true", "antispam.type=amavis"],
"values": [ "values": [
{ {
"option": "enabled", "option": "enabled",
"default": ["antispam.enabled=true", "antispam.type=amavis"], "default": "true",
}, },
{ {
"option": "user", "option": "user",

View File

@@ -3,19 +3,19 @@
import os import os
import pwd import pwd
import stat import stat
from typing import Optional
from . import package from . import package
from . import system from . import system
from . import utils from . import utils
class Database: class Database(object):
"""Common database backend.""" """Common database backend."""
default_port: Optional[int] = None default_port = None
packages: Optional[dict[str, list[str]]] = None packages = None
service: Optional[str] = None service = None
def __init__(self, config): def __init__(self, config):
"""Install if necessary.""" """Install if necessary."""
@@ -36,6 +36,7 @@ class Database:
class PostgreSQL(Database): class PostgreSQL(Database):
"""Postgres.""" """Postgres."""
default_port = 5432 default_port = 5432
@@ -102,7 +103,7 @@ class PostgreSQL(Database):
def create_database(self, name, owner): def create_database(self, name, owner):
"""Create a database.""" """Create a database."""
code, output = utils.exec_cmd( code, output = utils.exec_cmd(
"psql -lqt | cut -d \\| -f 1 | grep -w {} | wc -l" "psql -lqt | cut -d \| -f 1 | grep -w {} | wc -l"
.format(name), sudo_user=self.dbuser) .format(name), sudo_user=self.dbuser)
if code: if code:
return return
@@ -156,6 +157,7 @@ class PostgreSQL(Database):
class MySQL(Database): class MySQL(Database):
"""MySQL backend.""" """MySQL backend."""
default_port = 3306 default_port = 3306
@@ -176,17 +178,17 @@ class MySQL(Database):
if name.startswith("debian"): if name.startswith("debian"):
if version.startswith("8"): if version.startswith("8"):
self.packages["deb"].append("libmysqlclient-dev") self.packages["deb"].append("libmysqlclient-dev")
elif int(version[:2]) >= 11: elif version.startswith("11") or version.startswith("12"):
self.packages["deb"].append("libmariadb-dev") self.packages["deb"].append("libmariadb-dev")
else: else:
self.packages["deb"].append("libmariadbclient-dev") self.packages["deb"].append("libmariadbclient-dev")
elif name == "ubuntu": elif name == "ubuntu":
if version.startswith("2"): if version.startswith("2"):
# Works for Ubuntu 20, 22, and 24. # Works for Ubuntu 22 and 20
self.packages["deb"].append("libmariadb-dev") self.packages["deb"].append("libmariadb-dev")
else: else:
self.packages["deb"].append("libmysqlclient-dev") self.packages["deb"].append("libmysqlclient-dev")
super().install_package() super(MySQL, self).install_package()
queries = [] queries = []
if name.startswith("debian"): if name.startswith("debian"):
if version.startswith("8"): if version.startswith("8"):
@@ -198,8 +200,8 @@ class MySQL(Database):
self.dbpassword) self.dbpassword)
return return
if ( if (
(name.startswith("debian") and int(version[:2]) >= 11) or (name.startswith("debian") and (version.startswith("11") or version.startswith("12"))) or
(name.startswith("ubuntu") and int(version[:2]) >= 22) (name.startswith("ubuntu") and version.startswith("22"))
): ):
queries = [ queries = [
"SET PASSWORD FOR 'root'@'localhost' = PASSWORD('{}')" "SET PASSWORD FOR 'root'@'localhost' = PASSWORD('{}')"

View File

@@ -1,51 +0,0 @@
from . import utils
def installation_disclaimer(args, config):
"""Display installation disclaimer."""
hostname = config.get("general", "hostname")
utils.printcolor(
"Notice:\n"
"It is recommanded to run this installer on a FRESHLY installed server.\n"
"(ie. with nothing special already installed on it)\n",
utils.CYAN
)
utils.printcolor(
"Warning:\n"
"Before you start the installation, please make sure the following "
"DNS records exist for domain '{}':\n"
" {} IN A <IP ADDRESS OF YOUR SERVER>\n"
" @ IN MX {}.\n".format(
args.domain,
hostname.replace(".{}".format(args.domain), ""),
hostname
),
utils.YELLOW
)
utils.printcolor(
"Your mail server will be installed with the following components:",
utils.BLUE)
def upgrade_disclaimer(config):
"""Display upgrade disclaimer."""
utils.printcolor(
"Your mail server is about to be upgraded and the following components"
" will be impacted:", utils.BLUE
)
def backup_disclaimer():
"""Display backup disclamer. """
utils.printcolor(
"Your mail server will be backed up locally.\n"
" !! You should really transfer the backup somewhere else...\n"
" !! Custom configuration (like for postfix) won't be saved.", utils.BLUE)
def restore_disclaimer():
"""Display restore disclamer. """
utils.printcolor(
"You are about to restore a previous installation of Modoboa.\n"
"If a new version has been released in between, please update your database!",
utils.BLUE)

View File

@@ -2,12 +2,10 @@
import re import re
from os.path import isfile as file_exists
from . import utils from . import utils
class Package: class Package(object):
"""Base classe.""" """Base classe."""
def __init__(self, dist_name): def __init__(self, dist_name):
@@ -31,17 +29,10 @@ class DEBPackage(Package):
FORMAT = "deb" FORMAT = "deb"
def __init__(self, dist_name): def __init__(self, dist_name):
super().__init__(dist_name) super(DEBPackage, self).__init__(dist_name)
self.index_updated = False self.index_updated = False
self.policy_file = "/usr/sbin/policy-rc.d" self.policy_file = "/usr/sbin/policy-rc.d"
def enable_backports(self, codename):
code, output = utils.exec_cmd(f"grep {codename}-backports /etc/apt/sources.list")
if code:
with open(f"/etc/apt/sources.list.d/backports.list", "w") as fp:
fp.write(f"deb http://deb.debian.org/debian {codename}-backports main\n")
self.update(force=True)
def prepare_system(self): def prepare_system(self):
"""Make sure services don't start at installation.""" """Make sure services don't start at installation."""
with open(self.policy_file, "w") as fp: with open(self.policy_file, "w") as fp:
@@ -51,32 +42,9 @@ class DEBPackage(Package):
def restore_system(self): def restore_system(self):
utils.exec_cmd("rm -f {}".format(self.policy_file)) utils.exec_cmd("rm -f {}".format(self.policy_file))
def add_custom_repository(self, def update(self):
name: str,
url: str,
key_url: str,
codename: str,
with_source: bool = True):
key_file = f"/etc/apt/keyrings/{name}.gpg"
utils.exec_cmd(
f"wget -O - {key_url} | gpg --dearmor | tee {key_file} > /dev/null"
)
line_types = ["deb"]
if with_source:
line_types.append("deb-src")
for line_type in line_types:
line = (
f"{line_type} [arch=amd64 signed-by={key_file}] "
f"{url} {codename} main"
)
target_file = f"/etc/apt/sources.list.d/{name}.list"
tee_option = "-a" if file_exists(target_file) else ""
utils.exec_cmd(f'echo "{line}" | tee {tee_option} {target_file}')
self.index_updated = False
def update(self, force=False):
"""Update local cache.""" """Update local cache."""
if self.index_updated and not force: if self.index_updated:
return return
utils.exec_cmd("apt-get -o Dpkg::Progress-Fancy=0 update --quiet") utils.exec_cmd("apt-get -o Dpkg::Progress-Fancy=0 update --quiet")
self.index_updated = True self.index_updated = True
@@ -89,12 +57,12 @@ class DEBPackage(Package):
def install(self, name): def install(self, name):
"""Install a package.""" """Install a package."""
self.update() self.update()
utils.exec_cmd("apt-get -o Dpkg::Progress-Fancy=0 install --quiet --assume-yes -o DPkg::options::=--force-confold {}".format(name)) utils.exec_cmd("apt-get -o Dpkg::Progress-Fancy=0 install --quiet --assume-yes {}".format(name))
def install_many(self, names): def install_many(self, names):
"""Install many packages.""" """Install many packages."""
self.update() self.update()
return utils.exec_cmd("apt-get -o Dpkg::Progress-Fancy=0 install --quiet --assume-yes -o DPkg::options::=--force-confold {}".format( return utils.exec_cmd("apt-get -o Dpkg::Progress-Fancy=0 install --quiet --assume-yes {}".format(
" ".join(names))) " ".join(names)))
def get_installed_version(self, name): def get_installed_version(self, name):
@@ -114,7 +82,7 @@ class RPMPackage(Package):
def __init__(self, dist_name): def __init__(self, dist_name):
"""Initialize backend.""" """Initialize backend."""
super().__init__(dist_name) super(RPMPackage, self).__init__(dist_name)
if "centos" in dist_name: if "centos" in dist_name:
self.install("epel-release") self.install("epel-release")
@@ -140,7 +108,7 @@ def get_backend():
"""Return the appropriate package backend.""" """Return the appropriate package backend."""
distname = utils.dist_name() distname = utils.dist_name()
backend = None backend = None
if distname in ["debian", "debian gnu/linux", "ubuntu", "linuxmint"]: if distname in ["debian", "debian gnu/linux", "ubuntu"]:
backend = DEBPackage backend = DEBPackage
elif "centos" in distname: elif "centos" in distname:
backend = RPMPackage backend = RPMPackage

View File

@@ -1,5 +1,6 @@
"""Python related tools.""" """Python related tools."""
import json
import os import os
import sys import sys
@@ -48,35 +49,29 @@ def install_packages(names, venv=None, upgrade=False, **kwargs):
def get_package_version(name, venv=None, **kwargs): def get_package_version(name, venv=None, **kwargs):
"""Returns the version of an installed package.""" """Returns the version of an installed package."""
cmd = "{} show {}".format( cmd = f"{get_pip_path(venv)} list --format json"
get_pip_path(venv),
name
)
exit_code, output = utils.exec_cmd(cmd, **kwargs) exit_code, output = utils.exec_cmd(cmd, **kwargs)
if exit_code != 0: if exit_code != 0:
utils.error(f"Failed to get version of {name}. " utils.error(f"Failed to get version of {name}. "
f"Output is: {output}") f"Output is: {output}")
sys.exit(1) sys.exit(1)
print(f"name: {name}, venv: {venv}, cmd: {cmd}, exit_code: {exit_code}, output: {output.decode()}")
list_dict = json.loads(output.decode())
version_list = []
for element in list_dict:
if element["name"] == name:
version_list = element["version"].split(".")
break
version_list_clean = [] version_list_clean = []
for line in output.decode().split("\n"): for element in version_list:
if not line.startswith("Version:"): try:
continue version_list_clean.append(int(element))
version_item_list = line.split(":") except ValueError:
version_list = version_item_list[1].split(".") utils.printcolor(
for element in version_list: f"Failed to decode some part of the version of {name}",
try: utils.YELLOW)
version_list_clean.append(int(element)) version_list_clean.append(element)
except ValueError:
utils.printcolor(
f"Failed to decode some part of the version of {name}",
utils.YELLOW)
version_list_clean.append(element)
if len(version_list_clean) == 0:
utils.printcolor(
f"Failed to find the version of {name}",
utils.RED)
sys.exit(1)
return version_list_clean return version_list_clean
@@ -89,17 +84,36 @@ def install_package_from_repository(name, url, vcs="git", venv=None, **kwargs):
utils.exec_cmd(cmd, **kwargs) utils.exec_cmd(cmd, **kwargs)
def setup_virtualenv(path, sudo_user=None): def install_package_from_remote_requirements(url, venv=None, **kwargs):
"""Install a Python package from a file."""
cmd = "{} install {} {}".format(
get_pip_path(venv),
"-r",
url
)
utils.exec_cmd(cmd, **kwargs)
def setup_virtualenv(path, sudo_user=None, python_version=2):
"""Install a virtualenv if needed.""" """Install a virtualenv if needed."""
if os.path.exists(path): if os.path.exists(path):
return return
if utils.dist_name().startswith("centos"): if python_version == 2:
python_binary = "python3" python_binary = "python"
packages = ["python3"] packages = ["python-virtualenv"]
if utils.dist_name() == "debian":
packages.append("virtualenv")
else: else:
python_binary = "python3" if utils.dist_name().startswith("centos"):
packages = ["python3-venv"] python_binary = "python3"
packages = ["python3"]
else:
python_binary = "python3"
packages = ["python3-venv"]
package.backend.install_many(packages) package.backend.install_many(packages)
with utils.settings(sudo_user=sudo_user): with utils.settings(sudo_user=sudo_user):
utils.exec_cmd("{} -m venv {}".format(python_binary, path)) if python_version == 2:
install_packages(["pip", "setuptools"], venv=path, upgrade=True) utils.exec_cmd("virtualenv {}".format(path))
else:
utils.exec_cmd("{} -m venv {}".format(python_binary, path))
install_packages(["pip", "setuptools\<58.0.0"], venv=path, upgrade=True)

View File

@@ -52,17 +52,7 @@ class Amavis(base.Installer):
packages = super(Amavis, self).get_packages() packages = super(Amavis, self).get_packages()
if package.backend.FORMAT == "deb": if package.backend.FORMAT == "deb":
db_driver = "pg" if self.db_driver == "pgsql" else self.db_driver db_driver = "pg" if self.db_driver == "pgsql" else self.db_driver
packages += ["libdbd-{}-perl".format(db_driver)] return packages + ["libdbd-{}-perl".format(db_driver)]
name, version = utils.dist_info()
try:
major_version = int(version.split(".")[0])
except ValueError:
major_version = 0
if major_version >= 13:
packages = [p if p != "liblz4-tool" else "lz4" for p in packages]
return packages
if self.db_driver == "pgsql": if self.db_driver == "pgsql":
db_driver = "Pg" db_driver = "Pg"
elif self.db_driver == "mysql": elif self.db_driver == "mysql":

View File

@@ -0,0 +1,102 @@
"""Automx related tasks."""
import os
import pwd
import shutil
import stat
from .. import python
from .. import system
from .. import utils
from . import base
class Automx(base.Installer):
"""Automx installation."""
appname = "automx"
config_files = ["automx.conf"]
no_daemon = True
packages = {
"deb": ["memcached", "unzip"],
"rpm": ["memcached", "unzip"]
}
with_user = True
def __init__(self, *args, **kwargs):
"""Get configuration."""
super(Automx, self).__init__(*args, **kwargs)
self.venv_path = self.config.get("automx", "venv_path")
self.instance_path = self.config.get("automx", "instance_path")
def get_template_context(self):
"""Additional variables."""
context = super(Automx, self).get_template_context()
sql_dsn = "{}://{}:{}@{}:{}/{}".format(
"postgresql" if self.dbengine == "postgres" else self.dbengine,
self.config.get("modoboa", "dbuser"),
self.config.get("modoboa", "dbpassword"),
self.dbhost,
self.dbport,
self.config.get("modoboa", "dbname"))
if self.db_driver == "pgsql":
sql_query = (
"SELECT first_name || ' ' || last_name AS display_name, email"
", SPLIT_PART(email, '@', 2) AS domain "
"FROM core_user WHERE email='%s' AND is_active;")
else:
sql_query = (
"SELECT concat(first_name, ' ', last_name) AS display_name, "
"email, SUBSTRING_INDEX(email, '@', -1) AS domain "
"FROM core_user WHERE email='%s' AND is_active=1;"
)
context.update({"sql_dsn": sql_dsn, "sql_query": sql_query})
return context
def _setup_venv(self):
"""Prepare a python virtualenv."""
python.setup_virtualenv(
self.venv_path, sudo_user=self.user, python_version=3)
packages = [
"future", "lxml", "ipaddress", "sqlalchemy < 2.0", "python-memcached",
"python-dateutil", "configparser"
]
if self.dbengine == "postgres":
packages.append("psycopg2-binary")
else:
packages.append("mysqlclient")
python.install_packages(packages, self.venv_path, sudo_user=self.user)
target = "{}/master.zip".format(self.home_dir)
if os.path.exists(target):
os.unlink(target)
utils.exec_cmd(
"wget https://github.com/sys4/automx/archive/master.zip",
sudo_user=self.user, cwd=self.home_dir)
self.repo_dir = "{}/automx-master".format(self.home_dir)
if os.path.exists(self.repo_dir):
shutil.rmtree(self.repo_dir)
utils.exec_cmd(
"unzip master.zip", sudo_user=self.user, cwd=self.home_dir)
utils.exec_cmd(
"{} setup.py install".format(
python.get_path("python", self.venv_path)),
cwd=self.repo_dir)
def _deploy_instance(self):
"""Copy files to instance dir."""
if not os.path.exists(self.instance_path):
pw = pwd.getpwnam(self.user)
mode = (
stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP |
stat.S_IROTH | stat.S_IXOTH)
utils.mkdir(self.instance_path, mode, pw[2], pw[3])
path = "{}/src/automx_wsgi.py".format(self.repo_dir)
utils.exec_cmd("cp {} {}".format(path, self.instance_path),
sudo_user=self.user, cwd=self.home_dir)
def post_run(self):
"""Additional tasks."""
self._setup_venv()
self._deploy_instance()
system.enable_and_start_service("memcached")

View File

@@ -142,7 +142,7 @@ class Backup:
""" """
Custom config : Custom config :
- DKIM keys: {{keys_storage_dir}} - DKIM keys: {{keys_storage_dir}}
- Radicale collection (calendars, contacts): {{home_dir}} - Radicale collection (calendat, contacts): {{home_dir}}
- Amavis : /etc/amavis/conf.d/99-custom - Amavis : /etc/amavis/conf.d/99-custom
- Postwhite : /etc/postwhite.conf - Postwhite : /etc/postwhite.conf
Feel free to suggest to add others! Feel free to suggest to add others!

View File

@@ -2,7 +2,6 @@
import os import os
import sys import sys
from typing import Optional
from .. import database from .. import database
from .. import package from .. import package
@@ -14,15 +13,15 @@ from .. import utils
class Installer: class Installer:
"""Simple installer for one application.""" """Simple installer for one application."""
appname: str appname = None
no_daemon: bool = False no_daemon = False
daemon_name: Optional[str] = None daemon_name = None
packages: dict[str, list[str]] = {} packages = {}
with_user: bool = False with_user = False
with_db: bool = False with_db = False
config_files: list[str] = [] config_files = []
def __init__(self, config, upgrade: bool, archive_path: str) -> None: def __init__(self, config, upgrade: bool, archive_path: str):
"""Get configuration.""" """Get configuration."""
self.config = config self.config = config
self.upgrade = upgrade self.upgrade = upgrade
@@ -45,7 +44,7 @@ class Installer:
self.dbpasswd = self.config.get(self.appname, "dbpassword") self.dbpasswd = self.config.get(self.appname, "dbpassword")
@property @property
def modoboa_2_2_or_greater(self) -> bool: def modoboa_2_2_or_greater(self):
# Check if modoboa version > 2.2 # Check if modoboa version > 2.2
modoboa_version = python.get_package_version( modoboa_version = python.get_package_version(
"modoboa", "modoboa",

View File

@@ -42,10 +42,9 @@ class Clamav(base.Installer):
"""Additional tasks.""" """Additional tasks."""
if package.backend.FORMAT == "deb": if package.backend.FORMAT == "deb":
user = self.config.get(self.appname, "user") user = self.config.get(self.appname, "user")
if self.config.getboolean("amavis", "enabled"): system.add_user_to_group(
system.add_user_to_group( user, self.config.get("amavis", "user")
user, self.config.get("amavis", "user") )
)
pattern = ( pattern = (
"s/^AllowSupplementaryGroups false/" "s/^AllowSupplementaryGroups false/"
"AllowSupplementaryGroups true/") "AllowSupplementaryGroups true/")

View File

@@ -4,7 +4,6 @@ import glob
import os import os
import pwd import pwd
import shutil import shutil
import stat
from .. import database from .. import database
from .. import package from .. import package
@@ -15,88 +14,38 @@ from . import base
class Dovecot(base.Installer): class Dovecot(base.Installer):
"""Dovecot installer.""" """Dovecot installer."""
appname = "dovecot" appname = "dovecot"
packages = { packages = {
"deb": [ "deb": [
"dovecot-imapd", "dovecot-imapd", "dovecot-lmtpd", "dovecot-managesieved",
"dovecot-lmtpd", "dovecot-sieve"],
"dovecot-managesieved", "rpm": [
"dovecot-sieve", "dovecot", "dovecot-pigeonhole"]
],
"rpm": ["dovecot", "dovecot-pigeonhole"],
}
per_version_config_files = {
"2.3": [
"dovecot.conf",
"dovecot-dict-sql.conf.ext",
"conf.d/10-ssl.conf",
"conf.d/10-master.conf",
"conf.d/20-lmtp.conf",
"conf.d/10-ssl-keys.try",
"conf.d/dovecot-oauth2.conf.ext",
],
"2.4": [
"dovecot.conf",
"conf.d/10-mail.conf",
"conf.d/10-master.conf",
"conf.d/10-ssl.conf",
"conf.d/10-ssl-keys.try",
"conf.d/15-mailboxes.conf",
"conf.d/20-lmtp.conf",
"conf.d/auth-oauth2.conf.ext",
],
} }
config_files = [
"dovecot.conf", "dovecot-dict-sql.conf.ext", "conf.d/10-ssl.conf",
"conf.d/10-master.conf", "conf.d/20-lmtp.conf", "conf.d/10-ssl-keys.try"]
with_user = True with_user = True
@property
def version(self) -> str:
if not hasattr(self, "_version"):
self._version = package.backend.get_installed_version("dovecot-core")[:3]
return self._version
def setup_user(self): def setup_user(self):
"""Setup mailbox user.""" """Setup mailbox user."""
super().setup_user() super().setup_user()
self.mailboxes_owner = self.app_config["mailboxes_owner"] self.mailboxes_owner = self.app_config["mailboxes_owner"]
system.create_user(self.mailboxes_owner, self.home_dir) system.create_user(self.mailboxes_owner, self.home_dir)
def _get_config_files_for_version(self, version: str) -> list[str]: def get_config_files(self):
files = self.per_version_config_files[version]
if version == "2.4":
files += [
f"conf.d/auth-sql-{self.dbengine}.conf.ext=conf.d/auth-sql.conf.ext",
f"conf.d/auth-master-{self.dbengine}.conf.ext=conf.d/auth-master.conf.ext",
f"conf.d/30-dict-server-{self.dbengine}.conf=conf.d/30-dict-server.conf",
]
else:
files += [
f"dovecot-sql-{self.dbengine}.conf.ext=dovecot-sql.conf.ext",
f"dovecot-sql-master-{self.dbengine}.conf.ext=dovecot-sql-master.conf.ext",
]
result = []
for path in files:
if "=" not in path:
result.append(f"{version}/{path}={path}")
else:
src, dst = path.split("=")
result.append(f"{version}/{src}={dst}")
return result
def get_config_files(self) -> list[str]:
"""Additional config files.""" """Additional config files."""
_config_files = self._get_config_files_for_version(self.version) return self.config_files + [
_config_files.append( "dovecot-sql-{}.conf.ext=dovecot-sql.conf.ext"
f"postlogin-{self.dbengine}.sh=/usr/local/bin/postlogin.sh" .format(self.dbengine),
) "dovecot-sql-master-{}.conf.ext=dovecot-sql-master.conf.ext"
if self.app_config["move_spam_to_junk"]: .format(self.dbengine),
_config_files += [ "postlogin-{}.sh=/usr/local/bin/postlogin.sh"
"custom_after_sieve/spam-to-junk.sieve=conf.d/custom_after_sieve/spam-to-junk.sieve", .format(self.dbengine),
f"{self.version}/conf.d/90-sieve.conf=conf.d/90-sieve.conf", ]
]
return _config_files
def get_packages(self): def get_packages(self):
"""Additional packages.""" """Additional packages."""
@@ -104,46 +53,28 @@ class Dovecot(base.Installer):
if package.backend.FORMAT == "deb": if package.backend.FORMAT == "deb":
if "pop3" in self.config.get("dovecot", "extra_protocols"): if "pop3" in self.config.get("dovecot", "extra_protocols"):
packages += ["dovecot-pop3d"] packages += ["dovecot-pop3d"]
packages += super().get_packages() return super(Dovecot, self).get_packages() + packages
backports_codename = getattr(self, "backports_codename", None)
if backports_codename:
packages = [
f"{package}/{backports_codename}-backports" for package in packages
]
return packages
def install_packages(self): def install_packages(self):
"""Preconfigure Dovecot if needed.""" """Preconfigure Dovecot if needed."""
name, version = utils.dist_info()
name = name.lower()
if name.startswith("debian") and version.startswith("12"):
package.backend.enable_backports("bookworm")
self.backports_codename = "bookworm"
package.backend.preconfigure( package.backend.preconfigure(
"dovecot-core", "create-ssl-cert", "boolean", "false" "dovecot-core", "create-ssl-cert", "boolean", "false")
) super(Dovecot, self).install_packages()
super().install_packages()
def get_template_context(self): def get_template_context(self):
"""Additional variables.""" """Additional variables."""
context = super().get_template_context() context = super(Dovecot, self).get_template_context()
pw_mailbox = pwd.getpwnam(self.mailboxes_owner) pw_mailbox = pwd.getpwnam(self.mailboxes_owner)
dovecot_package = {"deb": "dovecot-core", "rpm": "dovecot"} dovecot_package = {"deb": "dovecot-core", "rpm": "dovecot"}
ssl_protocol_parameter = "ssl_protocols" ssl_protocol_parameter = "ssl_protocols"
if ( if package.backend.get_installed_version(dovecot_package[package.backend.FORMAT]) > "2.3":
package.backend.get_installed_version(
dovecot_package[package.backend.FORMAT]
)
> "2.3"
):
ssl_protocol_parameter = "ssl_min_protocol" ssl_protocol_parameter = "ssl_min_protocol"
ssl_protocols = "!SSLv2 !SSLv3" ssl_protocols = "!SSLv2 !SSLv3"
if package.backend.get_installed_version("openssl").startswith( if package.backend.get_installed_version("openssl").startswith("1.1") \
"1.1" or package.backend.get_installed_version("openssl").startswith("3"):
) or package.backend.get_installed_version("openssl").startswith("3"):
ssl_protocols = "!SSLv3" ssl_protocols = "!SSLv3"
if ssl_protocol_parameter == "ssl_min_protocol": if ssl_protocol_parameter == "ssl_min_protocol":
ssl_protocols = "TLSv1.2" ssl_protocols = "TLSv1"
if "centos" in utils.dist_name(): if "centos" in utils.dist_name():
protocols = "protocols = imap lmtp sieve" protocols = "protocols = imap lmtp sieve"
extra_protocols = self.config.get("dovecot", "extra_protocols") extra_protocols = self.config.get("dovecot", "extra_protocols")
@@ -153,81 +84,43 @@ class Dovecot(base.Installer):
# Protocols are automatically guessed on debian/ubuntu # Protocols are automatically guessed on debian/ubuntu
protocols = "" protocols = ""
oauth2_client_id, oauth2_client_secret = utils.create_oauth2_app( context.update({
"Dovecot", "db_driver": self.db_driver,
"dovecot", "mailboxes_owner_uid": pw_mailbox[2],
self.config.get("dovecot", "oauth2_client_secret"), "mailboxes_owner_gid": pw_mailbox[3],
self.config "mailbox_owner": self.mailboxes_owner,
) "modoboa_user": self.config.get("modoboa", "user"),
hostname = self.config.get("general", "hostname") "modoboa_dbname": self.config.get("modoboa", "dbname"),
oauth2_introspection_url = ( "modoboa_dbuser": self.config.get("modoboa", "dbuser"),
f"https://{oauth2_client_id}:{oauth2_client_secret}" "modoboa_dbpassword": self.config.get("modoboa", "dbpassword"),
f"@{hostname}/api/o/introspect/" "protocols": protocols,
) "ssl_protocols": ssl_protocols,
"ssl_protocol_parameter": ssl_protocol_parameter,
context.update( "radicale_user": self.config.get("radicale", "user"),
{ "radicale_auth_socket_path": os.path.basename(
"db_driver": self.db_driver, self.config.get("dovecot", "radicale_auth_socket_path")),
"mailboxes_owner_uid": pw_mailbox[2], "modoboa_2_2_or_greater": "" if self.modoboa_2_2_or_greater else "#",
"mailboxes_owner_gid": pw_mailbox[3], "not_modoboa_2_2_or_greater": "" if not self.modoboa_2_2_or_greater else "#"
"mailbox_owner": self.mailboxes_owner, })
"modoboa_user": self.config.get("modoboa", "user"),
"modoboa_dbname": self.config.get("modoboa", "dbname"),
"modoboa_dbuser": self.config.get("modoboa", "dbuser"),
"modoboa_dbpassword": self.config.get("modoboa", "dbpassword"),
"protocols": protocols,
"ssl_protocols": ssl_protocols,
"ssl_protocol_parameter": ssl_protocol_parameter,
"modoboa_2_2_or_greater": "" if self.modoboa_2_2_or_greater else "#",
"not_modoboa_2_2_or_greater": (
"" if not self.modoboa_2_2_or_greater else "#"
),
"do_move_spam_to_junk": (
"" if self.app_config["move_spam_to_junk"] else "#"
),
"oauth2_introspection_url": oauth2_introspection_url,
"radicale_user": self.config.get("radicale", "user"),
}
)
return context return context
def install_config_files(self):
"""Create sieve dir if needed."""
if self.app_config["move_spam_to_junk"]:
utils.mkdir_safe(
f"{self.config_dir}/conf.d/custom_after_sieve",
stat.S_IRWXU
| stat.S_IRGRP
| stat.S_IXGRP
| stat.S_IROTH
| stat.S_IXOTH,
0,
0,
)
super().install_config_files()
def post_run(self): def post_run(self):
"""Additional tasks.""" """Additional tasks."""
if self.version == "2.3" and self.dbengine == "postgres": if self.dbengine == "postgres":
dbname = self.config.get("modoboa", "dbname") dbname = self.config.get("modoboa", "dbname")
dbuser = self.config.get("modoboa", "dbuser") dbuser = self.config.get("modoboa", "dbuser")
dbpassword = self.config.get("modoboa", "dbpassword") dbpassword = self.config.get("modoboa", "dbpassword")
backend = database.get_backend(self.config) backend = database.get_backend(self.config)
backend.load_sql_file( backend.load_sql_file(
dbname, dbname, dbuser, dbpassword,
dbuser, self.get_file_path("install_modoboa_postgres_trigger.sql")
dbpassword,
self.get_file_path("install_modoboa_postgres_trigger.sql"),
) )
backend.load_sql_file( backend.load_sql_file(
dbname, dbname, dbuser, dbpassword,
dbuser, self.get_file_path("fix_modoboa_postgres_schema.sql")
dbpassword,
self.get_file_path("fix_modoboa_postgres_schema.sql"),
) )
for f in glob.glob(f"{self.get_file_path(f'{self.version}/conf.d')}/*"): for f in glob.glob("{}/*".format(self.get_file_path("conf.d"))):
if os.path.isfile(f): utils.copy_file(f, "{}/conf.d".format(self.config_dir))
utils.copy_file(f, "{}/conf.d".format(self.config_dir))
# Make postlogin script executable # Make postlogin script executable
utils.exec_cmd("chmod +x /usr/local/bin/postlogin.sh") utils.exec_cmd("chmod +x /usr/local/bin/postlogin.sh")
# Only root should have read access to the 10-ssl-keys.try # Only root should have read access to the 10-ssl-keys.try
@@ -235,13 +128,7 @@ class Dovecot(base.Installer):
utils.exec_cmd("chmod 600 /etc/dovecot/conf.d/10-ssl-keys.try") utils.exec_cmd("chmod 600 /etc/dovecot/conf.d/10-ssl-keys.try")
# Add mailboxes user to dovecot group for modoboa mailbox commands. # Add mailboxes user to dovecot group for modoboa mailbox commands.
# See https://github.com/modoboa/modoboa/issues/2157. # See https://github.com/modoboa/modoboa/issues/2157.
if self.app_config["move_spam_to_junk"]: system.add_user_to_group(self.mailboxes_owner, 'dovecot')
# Compile sieve script
sieve_file = (
f"{self.config_dir}/conf.d/custom_after_sieve/spam-to-junk.sieve"
)
utils.exec_cmd(f"/usr/bin/sievec {sieve_file}")
system.add_user_to_group(self.mailboxes_owner, "dovecot")
def restart_daemon(self): def restart_daemon(self):
"""Restart daemon process. """Restart daemon process.
@@ -255,8 +142,7 @@ class Dovecot(base.Installer):
action = "start" if code else "restart" action = "start" if code else "restart"
utils.exec_cmd( utils.exec_cmd(
"service {} {} > /dev/null 2>&1".format(self.appname, action), "service {} {} > /dev/null 2>&1".format(self.appname, action),
capture_output=False, capture_output=False)
)
system.enable_service(self.get_daemon_name()) system.enable_service(self.get_daemon_name())
def backup(self, path): def backup(self, path):
@@ -264,10 +150,8 @@ class Dovecot(base.Installer):
home_dir = self.config.get("dovecot", "home_dir") home_dir = self.config.get("dovecot", "home_dir")
utils.printcolor("Backing up mails", utils.MAGENTA) utils.printcolor("Backing up mails", utils.MAGENTA)
if not os.path.exists(home_dir) or os.path.isfile(home_dir): if not os.path.exists(home_dir) or os.path.isfile(home_dir):
utils.error( utils.error("Error backing up emails, provided path "
"Error backing up emails, provided path " f" ({home_dir}) seems not right...")
f" ({home_dir}) seems not right..."
)
return return
dst = os.path.join(path, "mails/") dst = os.path.join(path, "mails/")
@@ -289,13 +173,10 @@ class Dovecot(base.Installer):
for dirpath, dirnames, filenames in os.walk(home_dir): for dirpath, dirnames, filenames in os.walk(home_dir):
shutil.chown(dirpath, self.mailboxes_owner, self.mailboxes_owner) shutil.chown(dirpath, self.mailboxes_owner, self.mailboxes_owner)
for filename in filenames: for filename in filenames:
shutil.chown( shutil.chown(os.path.join(dirpath, filename),
os.path.join(dirpath, filename), self.mailboxes_owner, self.mailboxes_owner)
self.mailboxes_owner,
self.mailboxes_owner,
)
else: else:
utils.printcolor( utils.printcolor(
"It seems that emails were not backed up, skipping restoration.", "It seems that emails were not backed up, skipping restoration.",
utils.MAGENTA, utils.MAGENTA
) )

View File

@@ -0,0 +1,39 @@
[automx]
provider = %domain
domains = *
#debug=yes
#logfile = /srv/automx/automx.log
# Protect against DoS
memcache = 127.0.0.1:11211
memcache_ttl = 600
client_error_limit = 20
rate_limit_exception_networks = 127.0.0.0/8, ::1/128
[global]
backend = sql
action = settings
account_type = email
host = %sql_dsn
query = %sql_query
result_attrs = display_name, email
display_name = ${display_name}
smtp = yes
smtp_server = %hostname
smtp_port = 587
smtp_encryption = starttls
smtp_auth = plaintext
smtp_auth_identity = ${email}
smtp_refresh_ttl = 6
smtp_default = yes
imap = yes
imap_server = %hostname
imap_port = 143
imap_encryption = starttls
imap_auth = plaintext
imap_auth_identity = ${email}
imap_refresh_ttl = 6

View File

@@ -1,5 +0,0 @@
passdb {
driver = oauth2
mechanisms = xoauth2 oauthbearer
args = /etc/dovecot/conf.d/dovecot-oauth2.conf.ext
}

View File

@@ -1,6 +0,0 @@
introspection_mode = post
introspection_url = %{oauth2_introspection_url}
username_attribute = username
tls_ca_cert_file = /etc/ssl/certs/ca-certificates.crt
active_attribute = active
active_value = true

View File

@@ -1,122 +0,0 @@
#log_debug=category=auth
#auth_debug_passwords = yes
##
## Authentication processes
##
# Enable LOGIN command and all other plaintext authentications even if
# SSL/TLS is not used (LOGINDISABLED capability). Note that if the remote IP
# matches the local IP (ie. you're connecting from the same computer), the
# connection is considered secure and plaintext authentication is allowed,
# unless ssl = required.
#auth_allow_cleartext = yes
# Authentication cache size (e.g. 10M). 0 means it's disabled. Note that
# bsdauth, PAM and vpopmail require cache_key to be set for caching to be used.
#auth_cache_size = 0
# Time to live for cached data. After TTL expires the cached record is no
# longer used, *except* if the main database lookup returns internal failure.
# We also try to handle password changes automatically: If user's previous
# authentication was successful, but this one wasn't, the cache isn't used.
# For now this works only with plaintext authentication.
#auth_cache_ttl = 1 hour
# TTL for negative hits (user not found, password mismatch).
# 0 disables caching them completely.
#auth_cache_negative_ttl = 1 hour
# Space separated list of realms for SASL authentication mechanisms that need
# them. You can leave it empty if you don't want to support multiple realms.
# Many clients simply use the first one listed here, so keep the default realm
# first.
#auth_realms =
#
# Default realm/domain to use if none was specified. This is used for both
# SASL realms and appending @domain to username in plaintext logins.
#auth_default_domain =
# List of allowed characters in username. If the user-given username contains
# a character not listed in here, the login automatically fails. This is just
# an extra check to make sure user can't exploit any potential quote escaping
# vulnerabilities with SQL/LDAP databases. If you want to allow all characters,
# set this value to empty.
#auth_username_chars = abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-_@
# Username character translations before it's looked up from databases. The
# value contains series of from -> to characters. For example "#@/@" means
# that '#' and '/' characters are translated to '@'.
#auth_username_translation =
# Username formatting before it's looked up from databases.
auth_username_format = %{user|lower}
#auth_username_format = %{user|username|lower}
# If you want to allow master users to log in by specifying the master
# username within the normal username string (ie. not using SASL mechanism's
# support for it), you can specify the separator character here. The format
# is then <username><separator><master username>. UW-IMAP uses "*" as the
# separator, so that could be a good choice.
auth_master_user_separator = *
# Username to use for users logging in with ANONYMOUS SASL mechanism
#auth_anonymous_username = anonymous
# Host name to use in GSSAPI principal names. The default is to use the
# name returned by gethostname(). Use "$ALL" (with quotes) to allow all keytab
# entries.
#auth_gssapi_hostname =
# Kerberos keytab to use for the GSSAPI mechanism. Will use the system
# default (usually /etc/krb5.keytab) if not specified. You may need to change
# the auth service to run as root to be able to read this file.
#auth_krb5_keytab =
# Do NTLM and GSS-SPNEGO authentication using Samba's winbind daemon and
# ntlm_auth helper. <https://doc.dovecot.org/latest/core/config/auth/mechanisms/winbind.html>
#auth_use_winbind = no
# Path for Samba's ntlm_auth helper binary.
#auth_winbind_helper_path = /usr/bin/ntlm_auth
# Time to delay before replying to failed authentications.
#auth_failure_delay = 2 secs
# Require a valid SSL client certificate or the authentication fails.
#auth_ssl_require_client_cert = no
# Take the username from client's SSL certificate, using
# X509_NAME_get_text_by_NID() which returns the subject's DN's
# CommonName.
#auth_ssl_username_from_cert = no
# Space separated list of wanted authentication mechanisms:
# plain login digest-md5 cram-md5 ntlm anonymous gssapi
# gss-spnego xoauth2 oauthbearer
# NOTE: See also auth_allow_cleartext setting.
auth_mechanisms = plain login oauthbearer xoauth2
##
## Password and user databases
##
#
# Password database is used to verify user's password (and nothing more).
# You can have multiple passdbs and userdbs. This is useful if you want to
# allow both system users (/etc/passwd) and virtual users to login without
# duplicating the system users into virtual database.
#
# <https://doc.dovecot.org/latest/core/config/auth/passdb.html>
#
# User database specifies where mails are located and what user/group IDs
# own them. For single-UID configuration use "static" userdb.
#
# <https://doc.dovecot.org/latest/core/config/auth/userdb.html>
#!include auth-deny.conf.ext
!include auth-master.conf.ext
!include auth-oauth2.conf.ext
#!include auth-system.conf.ext
!include auth-sql.conf.ext
#!include auth-ldap.conf.ext
#!include auth-passwdfile.conf.ext
#!include auth-static.conf.ext

View File

@@ -1,417 +0,0 @@
##
## Mailbox locations and namespaces
##
# Location for users' mailboxes. The default is empty, which means that Dovecot
# tries to find the mailboxes automatically. This won't work if the user
# doesn't yet have any mail, so you should explicitly tell Dovecot the full
# location.
#
# If you're using mbox, giving a path to the INBOX file (eg. /var/mail/%%u)
# isn't enough. You'll also need to tell Dovecot where the other mailboxes are
# kept. This is called the "root mail directory", and it must be the first
# path given in the mail_location setting.
#
# There are a few special variables you can use, eg.:
#
# %%{user} - username
# %%{user|username} - user part in user@domain, same as %%u if there's no domain
# %%{user|domain} - domain part in user@domain, empty if there's no domain
# %%{home} - home directory
#
# See https://doc.dovecot.org/latest/core/settings/variables.html for full list
# of variables.
#
# Example:
# mail_driver = maildir
# mail_path = ~/Maildir
# mail_inbox_path = ~/Maildir/.INBOX
#
# Debian defaults
# Note that upstream considers mbox deprecated and strongly recommends
# against its use in production environments. See further information
# at
# https://doc.dovecot.org/2.4.0/core/config/mailbox/formats/mbox.html
# mail_driver = mbox
# mail_home = /home/%%{user|username}
# mail_path = %%{home}/mail
# mail_inbox_path = /var/mail/%%{user}
mail_driver = maildir
mail_home = %{home_dir}/%%{user|domain}/%%{user|username}
mail_path = %%{home}/Maildir
# If you need to set multiple mailbox locations or want to change default
# namespace settings, you can do it by defining namespace sections.
#
# You can have private, shared and public namespaces. Private namespaces
# are for user's personal mails. Shared namespaces are for accessing other
# users' mailboxes that have been shared. Public namespaces are for shared
# mailboxes that are managed by sysadmin. If you create any shared or public
# namespaces you'll typically want to enable ACL plugin also, otherwise all
# users can access all the shared mailboxes, assuming they have permissions
# on filesystem level to do so.
namespace inbox {
# Namespace type: private, shared or public
#type = private
# Hierarchy separator to use. You should use the same separator for all
# namespaces or some clients get confused. '/' is usually a good one.
# The default however depends on the underlying mail storage format.
#separator =
# Prefix required to access this namespace. This needs to be different for
# all namespaces. For example "Public/".
#prefix =
# Physical location of the mailbox. This is in same format as
# mail location, which is also the default for it.
# mail_driver =
# mail_path =
#
# There can be only one INBOX, and this setting defines which namespace
# has it.
inbox = yes
# If namespace is hidden, it's not advertised to clients via NAMESPACE
# extension. You'll most likely also want to set list=no. This is mostly
# useful when converting from another server with different namespaces which
# you want to deprecate but still keep working. For example you can create
# hidden namespaces with prefixes "~/mail/", "~%%u/mail/" and "mail/".
#hidden = no
# Show the mailboxes under this namespace with LIST command. This makes the
# namespace visible for clients that don't support NAMESPACE extension.
# "children" value lists child mailboxes, but hides the namespace prefix.
#list = yes
# Namespace handles its own subscriptions. If set to "no", the parent
# namespace handles them (empty prefix should always have this as "yes")
#subscriptions = yes
# See 15-mailboxes.conf for definitions of special mailboxes.
}
# Example shared namespace configuration
#namespace shared {
#type = shared
#separator = /
# Mailboxes are visible under "shared/user@domain/"
# $user, $domain and $username are expanded to the destination user.
#prefix = shared/$user/
# Mail location for other users' mailboxes. Note that %%{variables} and ~/
# expands to the logged in user's data. %%{owner_user} and %%{owner_home}
# destination user's data.
#mail_driver = maildir
#mail_path = %%{owner_home}/Maildir
#mail_index_path = ~/Maildir/shared/%%{owner_user}
# Use the default namespace for saving subscriptions.
#subscriptions = no
# List the shared/ namespace only if there are visible shared mailboxes.
#list = children
#}
# Should shared INBOX be visible as "shared/user" or "shared/user/INBOX"?
#mail_shared_explicit_inbox = no
# System user and group used to access mails. If you use multiple, userdb
# can override these by returning uid or gid fields. You can use either numbers
# or names. <https://doc.dovecot.org/latest/core/config/system_users.html#uids>
#mail_uid =
#mail_gid =
# Group to enable temporarily for privileged operations. Currently this is
# used only with INBOX when either its initial creation or dotlocking fails.
# Typically this is set to "mail" to give access to /var/mail.
mail_privileged_group = mail
# Grant access to these supplementary groups for mail processes. Typically
# these are used to set up access to shared mailboxes. Note that it may be
# dangerous to set these if users can create symlinks (e.g. if "mail" group is
# set here, ln -s /var/mail ~/mail/var could allow a user to delete others'
# mailboxes, or ln -s /secret/shared/box ~/mail/mybox would allow reading it).
#mail_access_groups =
# Allow full filesystem access to clients. There's no access checks other than
# what the operating system does for the active UID/GID. It works with both
# maildir and mboxes, allowing you to prefix mailboxes names with eg. /path/
# or ~user/.
#mail_full_filesystem_access = no
# Dictionary for key=value mailbox attributes. This is used for example by
# URLAUTH and METADATA extensions.
#mail_attribute {
# dict file {
# path = %%{home}/Maildir/dovecot-attributes
# }
#}
# A comment or note that is associated with the server. This value is
# accessible for authenticated users through the IMAP METADATA server
# entry "/shared/comment".
#mail_server_comment = ""
# Indicates a method for contacting the server administrator. According to
# RFC 5464, this value MUST be a URI (e.g., a mailto: or tel: URL), but that
# is currently not enforced. Use for example mailto:admin@example.com. This
# value is accessible for authenticated users through the IMAP METADATA server
# entry "/shared/admin".
#mail_server_admin =
##
## Mail processes
##
# Don't use mmap() at all. This is required if you store indexes to shared
# filesystems (NFS or clustered filesystem).
#mmap_disable = no
# Rely on O_EXCL to work when creating dotlock files. NFS supports O_EXCL
# since version 3, so this should be safe to use nowadays by default.
#dotlock_use_excl = yes
# When to use fsync() or fdatasync() calls:
# optimized (default): Whenever necessary to avoid losing important data
# always: Useful with e.g. NFS when write()s are delayed
# never: Never use it (best performance, but crashes can lose data)
#mail_fsync = optimized
# Locking method for index files. Alternatives are fcntl, flock and dotlock.
# Dotlocking uses some tricks which may create more disk I/O than other locking
# methods. NFS users: flock doesn't work, remember to change mmap_disable.
#lock_method = fcntl
# Directory where mails can be temporarily stored. Usually it's used only for
# mails larger than >= 128 kB. It's used by various parts of Dovecot, for
# example LDA/LMTP while delivering large mails or zlib plugin for keeping
# uncompressed mails.
#mail_temp_dir = /tmp
# Valid UID range for users, defaults to 500 and above. This is mostly
# to make sure that users can't log in as daemons or other system users.
# Note that denying root logins is hardcoded to dovecot binary and can't
# be done even if first_valid_uid is set to 0.
#first_valid_uid = 500
#last_valid_uid = 0
# Valid GID range for users, defaults to non-root/wheel. Users having
# non-valid GID as primary group ID aren't allowed to log in. If user
# belongs to supplementary groups with non-valid GIDs, those groups are
# not set.
#first_valid_gid = 1
#last_valid_gid = 0
# Maximum allowed length for mail keyword name. It's only forced when trying
# to create new keywords.
#mail_max_keyword_length = 50
# ':' separated list of directories under which chrooting is allowed for mail
# processes (ie. /var/mail will allow chrooting to /var/mail/foo/bar too).
# This setting doesn't affect login_chroot, mail_chroot or auth chroot
# settings. If this setting is empty, "/./" in home dirs are ignored.
# WARNING: Never add directories here which local users can modify, that
# may lead to root exploit. Usually this should be done only if you don't
# allow shell access for users. <doc/wiki/Chrooting.txt>
#valid_chroot_dirs =
# Default chroot directory for mail processes. This can be overridden for
# specific users in user database by giving /./ in user's home directory
# (eg. /home/./user chroots into /home). Note that usually there is no real
# need to do chrooting, Dovecot doesn't allow users to access files outside
# their mail directory anyway. If your home directories are prefixed with
# the chroot directory, append "/." to mail_chroot. <doc/wiki/Chrooting.txt>
#mail_chroot =
# UNIX socket path to master authentication server to find users.
# This is used by imap (for shared users) and lda.
#auth_socket_path = /var/run/dovecot/auth-userdb
# Directory where to look up mail plugins.
#mail_plugin_dir = /usr/lib/dovecot
# Space separated list of plugins to load for all services. Plugins specific to
# IMAP, LDA, etc. are added to this list in their own .conf files.
#mail_plugins =
#
# To add plugins, use
#mail_plugins {
# plugin = yes
#}
mail_plugins {
quota = yes
quota_clone = yes
}
##
## Mailbox handling optimizations
##
# Mailbox list indexes can be used to optimize IMAP STATUS commands. They are
# also required for IMAP NOTIFY extension to be enabled.
#mailbox_list_index = yes
# Trust mailbox list index to be up-to-date. This reduces disk I/O at the cost
# of potentially returning out-of-date results after e.g. server crashes.
# The results will be automatically fixed once the folders are opened.
#mailbox_list_index_very_dirty_syncs = yes
# Should INBOX be kept up-to-date in the mailbox list index? By default it's
# not, because most of the mailbox accesses will open INBOX anyway.
#mailbox_list_index_include_inbox = no
# The minimum number of mails in a mailbox before updates are done to cache
# file. This allows optimizing Dovecot's behavior to do less disk writes at
# the cost of more disk reads.
#mail_cache_min_mail_count = 0
# When IDLE command is running, mailbox is checked once in a while to see if
# there are any new mails or other changes. This setting defines the minimum
# time to wait between those checks. Dovecot can also use inotify and
# kqueue to find out immediately when changes occur.
#mailbox_idle_check_interval = 30 secs
# Save mails with CR+LF instead of plain LF. This makes sending those mails
# take less CPU, especially with sendfile() syscall with Linux and FreeBSD.
# But it also creates a bit more disk I/O which may just make it slower.
# Also note that if other software reads the mboxes/maildirs, they may handle
# the extra CRs wrong and cause problems.
#mail_save_crlf = no
# Max number of mails to keep open and prefetch to memory. This only works with
# some mailbox formats and/or operating systems.
#mail_prefetch_count = 0
# How often to scan for stale temporary files and delete them (0 = never).
# These should exist only after Dovecot dies in the middle of saving mails.
#mail_temp_scan_interval = 1w
# How many slow mail accesses sorting can perform before it returns failure.
# With IMAP the reply is: NO [LIMIT] Requested sort would have taken too long.
# The untagged SORT reply is still returned, but it's likely not correct.
#mail_sort_max_read_count = 0
protocol !indexer-worker {
# If folder vsize calculation requires opening more than this many mails from
# disk (i.e. mail sizes aren't in cache already), return failure and finish
# the calculation via indexer process. Disabled by default. This setting must
# be 0 for indexer-worker processes.
#mail_vsize_bg_after_count = 0
}
##
## Maildir-specific settings
##
# By default LIST command returns all entries in maildir beginning with a dot.
# Enabling this option makes Dovecot return only entries which are directories.
# This is done by stat()ing each entry, so it causes more disk I/O.
# (For systems setting struct dirent->d_type, this check is free and it's
# done always regardless of this setting)
#maildir_stat_dirs = no
# When copying a message, do it with hard links whenever possible. This makes
# the performance much better, and it's unlikely to have any side effects.
#maildir_copy_with_hardlinks = yes
# Assume Dovecot is the only MUA accessing Maildir: Scan cur/ directory only
# when its mtime changes unexpectedly or when we can't find the mail otherwise.
#maildir_very_dirty_syncs = no
# If enabled, Dovecot doesn't use the S=<size> in the Maildir filenames for
# getting the mail's physical size, except when recalculating Maildir++ quota.
# This can be useful in systems where a lot of the Maildir filenames have a
# broken size. The performance hit for enabling this is very small.
#maildir_broken_filename_sizes = no
# Always move mails from new/ directory to cur/, even when the \Recent flags
# aren't being reset.
#maildir_empty_new = no
##
## mbox-specific settings
##
# Which locking methods to use for locking mbox. There are four available:
# dotlock: Create <mailbox>.lock file. This is the oldest and most NFS-safe
# solution. If you want to use /var/mail/ like directory, the users
# will need write access to that directory.
# dotlock_try: Same as dotlock, but if it fails because of permissions or
# because there isn't enough disk space, just skip it.
# fcntl : Use this if possible. Works with NFS too if lockd is used.
# flock : May not exist in all systems. Doesn't work with NFS.
# lockf : May not exist in all systems. Doesn't work with NFS.
#
# You can use multiple locking methods; if you do the order they're declared
# in is important to avoid deadlocks if other MTAs/MUAs are using multiple
# locking methods as well. Some operating systems don't allow using some of
# them simultaneously.
#mbox_read_locks = fcntl
#mbox_write_locks = dotlock fcntl
# Maximum time to wait for lock (all of them) before aborting.
#mbox_lock_timeout = 5 mins
# If dotlock exists but the mailbox isn't modified in any way, override the
# lock file after this much time.
#mbox_dotlock_change_timeout = 2 mins
# When mbox changes unexpectedly we have to fully read it to find out what
# changed. If the mbox is large this can take a long time. Since the change
# is usually just a newly appended mail, it'd be faster to simply read the
# new mails. If this setting is enabled, Dovecot does this but still safely
# fallbacks to re-reading the whole mbox file whenever something in mbox isn't
# how it's expected to be. The only real downside to this setting is that if
# some other MUA changes message flags, Dovecot doesn't notice it immediately.
# Note that a full sync is done with SELECT, EXAMINE, EXPUNGE and CHECK
# commands.
#mbox_dirty_syncs = yes
# Like mbox_dirty_syncs, but don't do full syncs even with SELECT, EXAMINE,
# EXPUNGE or CHECK commands. If this is set, mbox_dirty_syncs is ignored.
#mbox_very_dirty_syncs = no
# Delay writing mbox headers until doing a full write sync (EXPUNGE and CHECK
# commands and when closing the mailbox). This is especially useful for POP3
# where clients often delete all mails. The downside is that our changes
# aren't immediately visible to other MUAs.
#mbox_lazy_writes = yes
# If mbox size is smaller than this (e.g. 100k), don't write index files.
# If an index file already exists it's still read, just not updated.
#mbox_min_index_size = 0
# Mail header selection algorithm to use for MD5 POP3 UIDLs when
# pop3_uidl_format=%%m. For backwards compatibility we use apop3d inspired
# algorithm, but it fails if the first Received: header isn't unique in all
# mails. An alternative algorithm is "all" that selects all headers.
#mbox_md5 = apop3d
##
## mdbox-specific settings
##
# Maximum dbox file size until it's rotated.
#mdbox_rotate_size = 10M
# Maximum dbox file age until it's rotated. Typically in days. Day begins
# from midnight, so 1d = today, 2d = yesterday, etc. 0 = check disabled.
#mdbox_rotate_interval = 0
# When creating new mdbox files, immediately preallocate their size to
# mdbox_rotate_size. This setting currently works only in Linux with some
# filesystems (ext4, xfs).
#mdbox_preallocate_space = no
# Settings to control adding $HasAttachment or $HasNoAttachment keywords.
# By default, all MIME parts with Content-Disposition=attachment, or inlines
# with filename parameter are consired attachments.
# add-flags - Add the keywords when saving new mails or when fetching can
# do it efficiently.
# content-type=type or !type - Include/exclude content type. Excluding will
# never consider the matched MIME part as attachment. Including will only
# negate an exclusion (e.g. content-type=!foo/* content-type=foo/bar).
# exclude-inlined - Exclude any Content-Disposition=inline MIME part.
#mail_attachment_detection_options =

View File

@@ -1,173 +0,0 @@
#default_process_limit = 100
#default_client_limit = 1000
# Default VSZ (virtual memory size) limit for service processes. This is mainly
# intended to catch and kill processes that leak memory before they eat up
# everything.
#default_vsz_limit = 256M
# Login user is internally used by login processes. This is the most untrusted
# user in Dovecot system. It shouldn't have access to anything at all.
#default_login_user = dovenull
# Internal user is used by unprivileged processes. It should be separate from
# login user, so that login processes can't disturb other processes.
#default_internal_user = dovecot
service imap-login {
inet_listener imap {
#port = 143
}
inet_listener imaps {
#port = 993
#ssl = yes
}
# Number of connections to handle before starting a new process. Typically
# the only useful values are 0 (unlimited) or 1. 1 is more secure, but 0
# is faster. <d>
#service_restart_request_count = 1
# Number of processes to always keep waiting for more connections.
#process_min_avail = 0
# If you set service_restart_request_count=0, you probably need to grow this.
#vsz_limit = 256M # default
}
service pop3-login {
inet_listener pop3 {
#port = 110
}
inet_listener pop3s {
#port = 995
#ssl = yes
}
}
service submission-login {
inet_listener submission {
#port = 587
}
inet_listener submissions {
#port = 465
}
}
service lmtp {
unix_listener lmtp {
#mode = 0666
}
# Create inet listener only if you can't use the above UNIX socket
#inet_listener lmtp {
# Avoid making LMTP visible for the entire internet
#listen = 127.0.0.1
#port = 24
#}
unix_listener /var/spool/postfix/private/dovecot-lmtp {
mode = 0600
user = postfix
group = postfix
}
}
service imap {
# Most of the memory goes to mmap()ing files. You may need to increase this
# limit if you have huge mailboxes.
#vsz_limit = 256M # default
# Max. number of IMAP processes (connections)
#process_limit = 1024
executable = imap postlogin
}
service pop3 {
# Max. number of POP3 processes (connections)
#process_limit = 1024
executable = imap postlogin
}
service submission {
# Max. number of SMTP Submission processes (connections)
#process_limit = 1024
}
service postlogin {
executable = script-login /usr/local/bin/postlogin.sh
user = %modoboa_user
unix_listener postlogin {
}
}
service stats {
# To allow modoboa to access available cipher list.
unix_listener stats-reader {
user = %{mailboxes_owner}
group = %{mailboxes_owner}
mode = 0660
}
unix_listener stats-writer {
user = %{mailboxes_owner}
group = %{mailboxes_owner}
mode = 0660
}
}
service auth {
# auth_socket_path points to this userdb socket by default. It's typically
# used by dovecot-lda, doveadm, possibly imap process, etc. Users that have
# full permissions to this socket are able to get a list of all usernames and
# get the results of everyone's userdb lookups.
#
# The default 0666 mode allows anyone to connect to the socket, but the
# userdb lookups will succeed only if the userdb returns an "uid" field that
# matches the caller process's UID. Also if caller's uid or gid matches the
# socket's uid or gid the lookup succeeds. Anything else causes a failure.
#
# To give the caller full permissions to lookup all users, set the mode to
# something else than 0666 and Dovecot lets the kernel enforce the
# permissions (e.g. 0777 allows everyone full permissions).
unix_listener auth-userdb {
#mode = 0666
user = %{mailboxes_owner}
#group =
}
# Postfix smtp-auth
unix_listener /var/spool/postfix/private/auth {
mode = 0666
user = postfix
group = postfix
}
%{radicale_enabled}unix_listener auth-radicale {
%{radicale_enabled} mode = 0666
%{radicale_enabled} user = %{radicale_user}
%{radicale_enabled} group = %{radicale_user}
%{radicale_enabled}}
# Auth process is run as this user.
#user = $SET:default_internal_user
}
service auth-worker {
# Auth worker process is run as root by default, so that it can access
# /etc/shadow. If this isn't necessary, the user should be changed to
# $SET:default_internal_user.
#user = root
}
service dict {
# If dict proxy is used, mail processes should have access to its socket.
# For example: mode=0660, group=vmail and global mail_access_groups=vmail
unix_listener dict {
mode = 0600
user = %{mailboxes_owner}
#group =
}
}

View File

@@ -1,6 +0,0 @@
# PEM encoded X.509 SSL/TLS certificate and private key. They're opened before
# dropping root privileges, so keep the key file unreadable by anyone but
# root. Included doc/mkcert.sh can be used to easily generate self-signed
# certificate, just make sure to update the domains in dovecot-openssl.cnf
ssl_server_cert_file = %tls_cert_file
ssl_server_key_file = %tls_key_file

View File

@@ -1,59 +0,0 @@
##
## SSL settings
##
# SSL/TLS support: yes, no, required. <https://doc.dovecot.org/latest/core/config/ssl.html>
ssl = yes
# PEM encoded X.509 SSL/TLS certificate and private key. By default, Debian
# installs a self-signed certificate. This is useful for testing, but you
# should obtain a real certificate from a recognized certificate authority.
#
# These files are opened before dropping root privileges, so keep the key file
# unreadable by anyone but root. Included /usr/share/dovecot/mkcert.sh can be
# used to easily generate self-signed certificate, just make sure to update the
# domains in dovecot-openssl.cnf
#
# Preferred permissions: root:root 0444
# ssl_server_cert_file = /etc/dovecot/private/dovecot.pem
# Preferred permissions: root:root 0400
# ssl_server_key_file = /etc/dovecot/private/dovecot.key
!include_try /etc/dovecot/conf.d/10-ssl-keys.try
# If key file is password protected, give the password here. Alternatively
# give it when starting dovecot with -p parameter. Since this file is often
# world-readable, you may want to place this setting instead to a different
# root owned 0600 file by using ssl_key_password = <path.
#ssl_server_key_password =
# PEM encoded trusted certificate authority. Set this only if you intend to use
# ssl_request_client_cert=yes. The file should contain the CA certificate(s)
# followed by the matching CRL(s). (e.g. ssl_server_ca_file = /etc/ssl/certs/ca.pem)
#ssl_server_ca_file =
# Require that CRL check succeeds for client certificates.
#ssl_server_require_crl = yes
# Request client to send a certificate. If you also want to require it, set
# auth_ssl_require_client_cert=yes in auth section.
#ssl_server_request_client_cert = no
# Which field from certificate to use for username. commonName and
# x500UniqueIdentifier are the usual choices. You'll also need to set
# auth_ssl_username_from_cert=yes.
#ssl_server_cert_username_field = commonName
# SSL protocols to use. Debian systems specify TLSv1.2 by default, which should
# be reasonbly secure and compatible with existing clients.
%ssl_protocol_parameter = %ssl_protocols
# Diffie-Hellman parameters are no longer required and should be phased out.
# They do not work with ECDH(E) and require DH(E) ciphers.
#ssl_server_dh_file = /etc/dovecot/dh.pem
# SSL ciphers to use
#ssl_cipher_list = ALL:!kRSA:!SRP:!kDHd:!DSS:!aNULL:!eNULL:!EXPORT:!DES:!3DES:!MD5:!PSK:!RC4:!ADH:!LOW@STRENGTH
ssl_cipher_list = EECDH+ECDSA+AESGCM:EECDH+aRSA+AESGCM:EECDH+ECDSA+SHA384:EECDH+ECDSA+SHA256:EECDH+aRSA+SHA384:EECDH+aRSA+SHA256:EECDH+aRSA+RC4:EECDH:EDH+aRSA:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS:!RC4
# SSL crypto device to use, for valid values run "openssl engine"
#ssl_crypto_device = /dev/crypto

View File

@@ -1,90 +0,0 @@
##
## Mailbox definitions
##
# Each mailbox is specified in a separate mailbox section. The section name
# specifies the mailbox name. If it has spaces, you can put the name
# "in quotes". These sections can contain the following mailbox settings:
#
# auto:
# Indicates whether the mailbox with this name is automatically created
# implicitly when it is first accessed. The user can also be automatically
# subscribed to the mailbox after creation. The following values are
# defined for this setting:
#
# no - Never created automatically.
# create - Automatically created, but no automatic subscription.
# subscribe - Automatically created and subscribed.
#
# special_use:
# A space-separated list of SPECIAL-USE flags (RFC 6154) to use for the
# mailbox. There are no validity checks, so you could specify anything
# you want in here, but it's not a good idea to use flags other than the
# standard ones specified in the RFC:
#
# \All - This (virtual) mailbox presents all messages in the
# user's message store.
# \Archive - This mailbox is used to archive messages.
# \Drafts - This mailbox is used to hold draft messages.
# \Flagged - This (virtual) mailbox presents all messages in the
# user's message store marked with the IMAP \Flagged flag.
# \Important - This (virtual) mailbox presents all messages in the
# user's message store deemed important to user.
# \Junk - This mailbox is where messages deemed to be junk mail
# are held.
# \Sent - This mailbox is used to hold copies of messages that
# have been sent.
# \Trash - This mailbox is used to hold messages that have been
# deleted.
#
# comment:
# Defines a default comment or note associated with the mailbox. This
# value is accessible through the IMAP METADATA mailbox entries
# "/shared/comment" and "/private/comment". Users with sufficient
# privileges can override the default value for entries with a custom
# value.
# NOTE: Assumes "namespace inbox" has been defined in 10-mail.conf.
namespace inbox {
# These mailboxes are widely used and could perhaps be created automatically:
mailbox Drafts {
auto = subscribe
special_use = \Drafts
}
mailbox Junk {
auto = subscribe
special_use = \Junk
}
mailbox Trash {
auto = subscribe
special_use = \Trash
}
# For \Sent mailboxes there are two widely used names. We'll mark both of
# them as \Sent. User typically deletes one of them if duplicates are created.
mailbox Sent {
auto = subscribe
special_use = \Sent
}
# mailbox "Sent Messages" {
# special_use = \Sent
# }
# If you have a virtual "All messages" mailbox:
#mailbox virtual/All {
# special_use = \All
# comment = All my messages
#}
# If you have a virtual "Flagged" mailbox:
#mailbox virtual/Flagged {
# special_use = \Flagged
# comment = All my flagged messages
#}
# If you have a virtual "Important" mailbox:
#mailbox virtual/Important {
# special_use = \Important
# comment = All my important messages
#}
}

View File

@@ -1,108 +0,0 @@
##
## IMAP specific settings
##
# If nothing happens for this long while client is IDLEing, move the connection
# to imap-hibernate process and close the old imap process. This saves memory,
# because connections use very little memory in imap-hibernate process. The
# downside is that recreating the imap process back uses some resources.
#imap_hibernate_timeout = 0
# Maximum IMAP command line length. Some clients generate very long command
# lines with huge mailboxes, so you may need to raise this if you get
# "Too long argument" or "IMAP command line too large" errors often.
#imap_max_line_length = 64k
# IMAP logout format string:
# %{input} - total number of bytes read from client
# %{output} - total number of bytes sent to client
# %{fetch_hdr_count} - Number of mails with mail header data sent to client
# %{fetch_hdr_bytes} - Number of bytes with mail header data sent to client
# %{fetch_body_count} - Number of mails with mail body data sent to client
# %{fetch_body_bytes} - Number of bytes with mail body data sent to client
# %{deleted} - Number of mails where client added \Deleted flag
# %{expunged} - Number of mails that client expunged, which does not
# include automatically expunged mails
# %{autoexpunged} - Number of mails that were automatically expunged after
# client disconnected
# %{trashed} - Number of mails that client copied/moved to the
# special_use=\Trash mailbox.
# %{appended} - Number of mails saved during the session
#imap_logout_format = in=%i out=%o deleted=%{deleted} expunged=%{expunged} \
# trashed=%{trashed} hdr_count=%{fetch_hdr_count} \
# hdr_bytes=%{fetch_hdr_bytes} body_count=%{fetch_body_count} \
# body_bytes=%{fetch_body_bytes}
# Amend or override the IMAP capability response. To override, set the value
# with imap_capability =
#
# To amend, you can use a boolean list to specify which capabilities to turn
# on and off
#imap_capability {
# SPECIAL-USE = yes
# "LITERAL+" = no
#}
# How long to wait between "OK Still here" notifications when client is
# IDLEing.
#imap_idle_notify_interval = 2 mins
# ID field names and values to send to clients. Using * as the value makes
# Dovecot use the default value. The following fields have default values
# currently: name, version, os, os-version, support-url, support-email,
# revision.
#imap_id_send =
# Use imap_id_received event to log IMAP id
# Workarounds for various client bugs:
# delay-newmail:
# Send EXISTS/RECENT new mail notifications only when replying to NOOP
# and CHECK commands. Some clients ignore them otherwise, for example OSX
# Mail (<v2.1). Outlook Express breaks more badly though, without this it
# may show user "Message no longer in server" errors. Note that OE6 still
# breaks even with this workaround if synchronization is set to
# "Headers Only".
# tb-extra-mailbox-sep:
# Thunderbird gets somehow confused with LAYOUT=fs (mbox and dbox) and
# adds extra '/' suffixes to mailbox names. This option causes Dovecot to
# ignore the extra '/' instead of treating it as invalid mailbox name.
# tb-lsub-flags:
# Show \Noselect flags for LSUB replies with LAYOUT=fs (e.g. mbox).
# This makes Thunderbird realize they aren't selectable and show them
# greyed out, instead of only later giving "not selectable" popup error.
#
# This is a boolean list
#imap_client_workarounds {
# delay-newmail = yes
#}
# Host allowed in URLAUTH URLs sent by client. "*" allows all.
#imap_urlauth_host =
# Enable IMAP LITERAL- extension (replaces LITERAL+)
#imap_literal_minus = no
# What happens when FETCH fails due to some internal error:
# disconnect-immediately:
# The FETCH is aborted immediately and the IMAP client is disconnected.
# disconnect-after:
# The FETCH runs for all the requested mails returning as much data as
# possible. The client is finally disconnected without a tagged reply.
# no-after:
# Same as disconnect-after, but tagged NO reply is sent instead of
# disconnecting the client. If the client attempts to FETCH the same failed
# mail more than once, the client is disconnected. This is to avoid clients
# from going into infinite loops trying to FETCH a broken mail.
#imap_fetch_failure = disconnect-immediately
protocol imap {
# Space separated list of plugins to load (default is global mail_plugins).
mail_plugins {
quota = yes
}
# Maximum number of IMAP connections allowed for a user from each IP address.
# NOTE: The username is compared case-sensitively.
#mail_max_userip_connections = 10
}

View File

@@ -1,53 +0,0 @@
##
#i
## LMTP specific settings
##
# Support proxying to other LMTP/SMTP servers by performing passdb lookups.
#lmtp_proxy = no
# When recipient address includes the detail (e.g. user+detail), try to save
# the mail to the detail mailbox. See also recipient_delimiter and
# lda_mailbox_autocreate settings.
#lmtp_save_to_detail_mailbox = no
# Verify quota before replying to RCPT TO. This adds a small overhead.
lmtp_rcpt_check_quota = yes
# Add "Received:" header to mails delivered.
#lmtp_add_received_header = yes
# Which recipient address to use for Delivered-To: header and Received:
# header. The default is "final", which is the same as the one given to
# RCPT TO command. "original" uses the address given in RCPT TO's ORCPT
# parameter, "none" uses nothing. Note that "none" is currently always used
# when a mail has multiple recipients.
#lmtp_hdr_delivery_address = final
# Workarounds for various client bugs:
# whitespace-before-path:
# Allow one or more spaces or tabs between `MAIL FROM:' and path and between
# `RCPT TO:' and path.
# mailbox-for-path:
# Allow using bare Mailbox syntax (i.e., without <...>) instead of full path
# syntax.
#
#lmtp_client_workarounds {
# whitespace-before-path = yes
#}
protocol lmtp {
mail_plugins {
quota = yes
sieve = yes
}
postmaster_address = %postmaster_address
# This strips the domain name before delivery, since the default
# userdb in Debian is /etc/passwd, which doesn't include domain
# names in the user. If you're using a different userdb backend
# that does include domain names, you may wish to remove this. See
# https://doc.dovecot.org/2.4.0/howto/lmtp/exim.html and
# https://doc.dovecot.org/2.4.0/core/summaries/settings.html#auth_username_format
# auth_username_format = %%{user | username}
}

View File

@@ -1,45 +0,0 @@
##
## Dictionary server settings
##
# Dictionary can be used to store key=value lists. This is used by several
# plugins. The dictionary can be accessed either directly or though a
# dictionary server. The following dict block maps dictionary names to URIs
# when the server is used. These can then be referenced using URIs in format
# "proxy::<name>".
dict_server {
mysql %dbhost {
port = %dbport
dbname = %modoboa_dbname
user = %modoboa_dbuser
password = %modoboa_dbpassword
}
dict quota {
driver = sql
sql_driver = %db_driver
hostname = %dbhost
dict_map priv/quota/storage {
sql_table = admin_quota
username_field = username
value_field bytes {
type = uint
}
}
dict_map priv/quota/messages {
sql_table = admin_quota
username_field = username
value_field messages {
type = uint
}
}
}
}
quota_clone {
dict proxy {
name = quota
}
}

View File

@@ -1,47 +0,0 @@
##
## Dictionary server settings
##
# Dictionary can be used to store key=value lists. This is used by several
# plugins. The dictionary can be accessed either directly or though a
# dictionary server. The following dict block maps dictionary names to URIs
# when the server is used. These can then be referenced using URIs in format
# "proxy::<name>".
dict_server {
pgsql %dbhost {
parameters {
port = %dbport
dbname = %modoboa_dbname
user = %modoboa_dbuser
password = %modoboa_dbpassword
}
}
dict quota {
driver = sql
sql_driver = %db_driver
hostname = %dbhost
dict_map priv/quota/storage {
sql_table = admin_quota
username_field = username
value_field bytes {
type = uint
}
}
dict_map priv/quota/messages {
sql_table = admin_quota
username_field = username
value_field messages {
type = uint
}
}
}
}
quota_clone {
dict proxy {
name = quota
}
}

View File

@@ -1,79 +0,0 @@
##
## Quota configuration.
##
# Note that you also have to enable quota plugin in mail_plugins setting.
## <https://doc.dovecot.org/latest/core/plugins/quota.html>
##
## Quota limits
##
# Quota limits are set using "quota_rule" parameters. To get per-user quota
# limits, you can set/override them by returning "quota_rule" extra field
# from userdb. It's also possible to give mailbox-specific limits, for example
# to give additional 100 MB when saving to Trash:
#mail_plugins {
# quota = yes
#}
quota "User quota" {
# storage_size = 1G
}
#
#namespace inbox {
# mailbox Trash {
# quota_storage_extra = 100M
# }
#}
##
## Quota warnings
##
# You can execute a given command when user exceeds a specified quota limit.
# Each quota root has separate limits. Only the command for the first
# exceeded limit is excecuted, so put the highest limit first.
# The commands are executed via script service by connecting to the named
# UNIX socket (quota-warning below).
# Note that % needs to be escaped as %%, otherwise "% " expands to empty.
#quota "User quota" {
# warning warn-95 {
# quota_storage_percentage = 95
# execute quota-warning {
# args = 95 %{user}
# }
# }
# warning warn-80 {
# quota_storage_percentage = 80
# execute quota-warning {
# args = 80 %{user}
# }
# }
#}
# Example quota-warning service. The unix listener's permissions should be
# set in a way that mail processes can connect to it. Below example assumes
# that mail processes run as vmail user. If you use mode=0666, all system users
# can generate quota warnings to anyone.
#service quota-warning {
# executable = script /usr/local/bin/quota-warning.sh
# user = dovecot
# unix_listener quota-warning {
# user = vmail
# }
#}
##
## Quota backends
##
# Multiple backends are supported:
# count: Default and recommended, quota driver tracks the quota internally within Dovecot's index files.
# maildir: Maildir++ quota
# fs: Read-only support for filesystem quota
#quota "User quota" {
# driver = count
#}

View File

@@ -1,118 +0,0 @@
##
## Settings for the Sieve interpreter
##
# Do not forget to enable the Sieve plugin in 15-lda.conf and 20-lmtp.conf
# by adding it to the respective mail_plugins { sieve = yes } settings.
# See https://doc.dovecot.org/latest/core/plugins/sieve.html
# Personal sieve script location
#sieve_script personal {
# driver = file
# path = ~/sieve
# active_path = ~/.dovecot.sieve
#}
# Default sieve script location
#sieve_script default {
# type = default
# name = default
# driver = file
# path = /etc/dovecot/sieve/default/
#}
%{do_move_spam_to_junk}sieve_script after {
%{do_move_spam_to_junk} type = after
%{do_move_spam_to_junk} path = /etc/dovecot/conf.d/custom_after_sieve
%{do_move_spam_to_junk}}
# Which Sieve language extensions are available to users. By default, all
# supported extensions are available, except for deprecated extensions or
# those that are still under development. Some system administrators may want
# to disable certain Sieve extensions or enable those that are not available
# by default. This setting can use 'yes' and 'no' to specify differences relative
# to the default. For example `imapflags = yes' will enable the
# deprecated imapflags extension in addition to all extensions were already
# enabled by default.
#sieve_extensions {
# mboxmetadata = yes
# vnd.dovecot.debug = yes
#}
# Which Sieve language extensions are ONLY available in global scripts. This
# can be used to restrict the use of certain Sieve extensions to administrator
# control, for instance when these extensions can cause security concerns.
# This setting has higher precedence than the `sieve_extensions' setting
# (above), meaning that the extensions enabled with this setting are never
# available to the user's personal script no matter what is specified for the
# `sieve_extensions' setting. The syntax of this setting is similar to the
# `sieve_extensions' setting, with the difference that extensions are
# enabled or disabled for exclusive use in global scripts. Currently, no
# extensions are marked as such by default.
#sieve_global_extensions =
# The Pigeonhole Sieve interpreter can have plugins of its own. Using this
# setting, the used plugins can be specified. Check the Dovecot documentation
# https://doc.dovecot.org/latest/core/plugins/sieve.html
#sieve_plugins = sieve_imapsieve sieve_extprograms
#sieve_pipe_bin_dir = /usr/share/dovecot-pigeonhole/sieve
#sieve_execute_bin_dir = /usr/share/dovecot-pigeonhole/sieve
#sieve_global_extensions {
# vnd.dovecot.pipe = yes
# vnd.dovecot.execute = yes
#}
#imapsieve_url =
# The separator that is expected between the :user and :detail
# address parts introduced by the subaddress extension. This may
# also be a sequence of characters (e.g. '--'). The current
# implementation looks for the separator from the left of the
# localpart and uses the first one encountered. The :user part is
# left of the separator and the :detail part is right. This setting
# is also used by Dovecot's LMTP service.
#recipient_delimiter = +-_
# The maximum size of a Sieve script. The compiler will refuse to compile any
# script larger than this limit. If set to 0, no limit on the script size is
# enforced.
#sieve_max_script_size = 1M
# The maximum number of actions that can be performed during a single script
# execution. If set to 0, no limit on the total number of actions is enforced.
#sieve_max_actions = 32
# The maximum number of redirect actions that can be performed during a single
# script execution. If set to 0, no redirect actions are allowed.
#sieve_max_redirects = 4
# The maximum number of personal Sieve scripts a single user can have. If set
# to 0, no limit on the number of scripts is enforced.
# (Currently only relevant for ManageSieve)
#sieve_quota_script_count = 0
# The maximum amount of disk storage a single user's scripts may occupy. If
# set to 0, no limit on the used amount of disk storage is enforced.
# (Currently only relevant for ManageSieve)
#sieve_quota_storage_size = 0
#mailbox Spam {
## From elsewhere to Spam folder
# sieve_script report-spam {
# type = before
# cause = copy
# path = /etc/dovecot/report-spam.sieve
# }
#}
## From Spam folder to elsewhere
#imapsieve_from Spam {
# sieve_script report-ham {
# type = before
# cause = copy
# path = /etc/dovecot/report-ham.sieve
# }
#}

View File

@@ -1,28 +0,0 @@
# Authentication for master users. Included from auth.conf.
# By adding master=yes setting inside a passdb you make the passdb a list
# of "master users", who can log in as anyone else.
# <https://doc.dovecot.org/latest/core/config/auth/master_users.html>
# Example master user passdb using passwd-file. You can use any passdb though.
#passdb master-passwd-file {
# driver = passwd-file
# master = yes
# passwd_file_path = /etc/dovecot/master-users
#}
sql_driver = %db_driver
mysql %dbhost {
port = %dbport
dbname = %modoboa_dbname
user = %modoboa_dbuser
password = %modoboa_dbpassword
}
passdb db1 {
driver = sql
sql_query = SELECT email AS user, password FROM core_user WHERE email='%%{user}' and is_active=1 and master_user=1
master = yes
result_success = continue
}

View File

@@ -1,30 +0,0 @@
# Authentication for master users. Included from auth.conf.
# By adding master=yes setting inside a passdb you make the passdb a list
# of "master users", who can log in as anyone else.
# <https://doc.dovecot.org/latest/core/config/auth/master_users.html>
# Example master user passdb using passwd-file. You can use any passdb though.
#passdb master-passwd-file {
# driver = passwd-file
# master = yes
# passwd_file_path = /etc/dovecot/master-users
#}
sql_driver = %db_driver
pgsql %dbhost {
parameters {
port = %dbport
dbname = %modoboa_dbname
user = %modoboa_dbuser
password = %modoboa_dbpassword
}
}
passdb db1 {
driver = sql
sql_query = SELECT email AS user, password FROM core_user WHERE email='%%{user}' and is_active and master_user
master = yes
result_success = continue
}

View File

@@ -1,24 +0,0 @@
auth_mechanisms {
xoauth2 = yes
oauthbearer = yes
}
oauth2 {
introspection_mode = post
introspection_url = %{oauth2_introspection_url}
#force_introspection = yes
username_attribute = username
}
# with local validation
#oauth2 {
# introspection_mode = local
# username_attribute = email
# oauth2_local_validation {
# dict fs {
# fs posix {
# prefix = /etc/dovecot/oauth2-keys/
# }
# }
# }
#}

View File

@@ -1,195 +0,0 @@
# Authentication for SQL users. Included from auth.conf.
#
# <https://doc.dovecot.org/latest/core/config/auth/databases/sql.html>
# For the sql passdb module, you'll need a database with a table that
# contains fields for at least the username and password. If you want to
# use the user@domain syntax, you might want to have a separate domain
# field as well.
#
# If your users all have the same uig/gid, and have predictable home
# directories, you can use the static userdb module to generate the home
# dir based on the username and domain. In this case, you won't need fields
# for home, uid, or gid in the database.
#
# If you prefer to use the sql userdb module, you'll want to add fields
# for home, uid, and gid. Here is an example table:
#
# CREATE TABLE users (
# username VARCHAR(128) NOT NULL,
# domain VARCHAR(128) NOT NULL,
# password VARCHAR(64) NOT NULL,
# home VARCHAR(255) NOT NULL,
# uid INTEGER NOT NULL,
# gid INTEGER NOT NULL,
# active CHAR(1) DEFAULT 'Y' NOT NULL
# );
# Database driver: mysql, pgsql, sqlite
sql_driver = %db_driver
# Database connection string. This is driver-specific setting.
#
# HA / round-robin load-balancing is supported by giving multiple host
# settings, like: host=sql1.host.org host=sql2.host.org
#
# pgsql:
# For available options, see the PostgreSQL documention for the
# PQconnectdb function of libpq.
# Use maxconns=n (default 5) to change how many connections Dovecot can
# create to pgsql.
#
# mysql:
# Basic options emulate PostgreSQL option names:
# host, port, user, password, dbname
#
# But also adds some new settings:
# client_flags - See MySQL manual
# ssl_ca, ssl_ca_path - Set either one or both to enable SSL
# ssl_cert, ssl_key - For sending client-side certificates to server
# ssl_cipher - Set minimum allowed cipher security (default: HIGH)
# option_file - Read options from the given file instead of
# the default my.cnf location
# option_group - Read options from the given group (default: client)
#
# You can connect to UNIX sockets by using host: host=/var/run/mysql.sock
# Note that currently you can't use spaces in parameters.
#
# sqlite:
# The path to the database file.
#
# Examples:
# mysql 192.168.1.1 {
# dbname = users
# }
# mysql sql.example.com {
# ssl = yes
# user = virtual
# password = blarg
# dbname = virtual
# }
# sqlite /etc/dovecot/authdb.sqlite {
# }
#
#mysql /var/run/mysqld/mysqld.sock {
# user = dovecot
# password = dvmail
# dbname = dovecot
#}
#mysql localhost {
# ...
#}
mysql %dbhost {
port = %dbport
dbname = %modoboa_dbname
user = %modoboa_dbuser
password = %modoboa_dbpassword
}
#passdb sql {
# default_password_scheme = SHA256
# passdb query to retrieve the password. It can return fields:
# password - The user's password. This field must be returned.
# user - user@domain from the database. Needed with case-insensitive lookups.
# username and domain - An alternative way to represent the "user" field.
#
# The "user" field is often necessary with case-insensitive lookups to avoid
# e.g. "name" and "nAme" logins creating two different mail directories. If
# your user and domain names are in separate fields, you can return "username"
# and "domain" fields instead of "user".
#
# The query can also return other fields which have a special meaning, see
# https://doc.dovecot.org/latest/core/config/auth/passdb.html#extra-fields
#
# Commonly used available substitutions (see https://doc.dovecot.org/latest/core/settings/variables.html
# for full list):
# %%{user} = entire user@domain
# %%{user|username} = user part of user@domain
# %%{user|domain} = domain part of user@domain
#
# Note that these can be used only as input to SQL query. If the query outputs
# any of these substitutions, they're not touched. Otherwise it would be
# difficult to have eg. usernames containing '%%' characters.
#
# Example:
# query = SELECT userid AS user, pw AS password \
# FROM users WHERE userid = '%%u' AND active = 'Y'
#
# query = \
# SELECT userid as username, domain, password \
# FROM users WHERE userid = '%%{user|username}' AND domain = '%%{user|domain}'
#}
passdb sql {
query = SELECT email AS user, password FROM core_user u INNER JOIN admin_mailbox mb ON u.id=mb.user_id INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE (mb.is_send_only=0 OR '%%{protocol}' NOT IN ('imap', 'pop3')) AND u.email='%%{user}' AND u.is_active=1 AND dom.enabled=1
}
#userdb sql {
# userdb query to retrieve the user information. It can return fields:
# uid - System UID (overrides mail_uid setting)
# gid - System GID (overrides mail_gid setting)
# home - Home directory
# mail_driver - Mail driver
# mail_path - Mail storage path
#
# None of these are strictly required. If you use a single UID and GID, and
# home or mail directory fits to a template string, you could use userdb static
# instead. For a list of all fields that can be returned, see
# Examples:
# query = SELECT home, uid, gid FROM users WHERE userid = '%%{user}'
# query = SELECT dir AS home, user AS uid, group AS gid FROM users where userid = '%%{user}'
# query = SELECT home, 501 AS uid, 501 AS gid FROM users WHERE userid = '%%{user}'
#
# query = \
# SELECT home, uid, gid \
# FROM users WHERE userid = '%%{user|username}' AND domain = '%%{user|domain}'
# Query to get a list of all usernames.
# iterate_query = SELECT username AS user,domain FROM users
# userdb_ldap {
# iterate_fields {
# home = /var/vmail/%%{home}
# }
# }
#}
userdb sql {
query = SELECT '%{home_dir}/%%{user|domain}/%%{user|username}' AS home, %mailboxes_owner_uid as uid, %mailboxes_owner_gid as gid, CONCAT(mb.quota, 'M') AS quota_storage_size FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN core_user u ON u.id=mb.user_id WHERE (mb.is_send_only=0 OR '%%{protocol}' NOT IN ('imap', 'pop3', 'lmtp')) AND mb.address='%%{user|username}' AND dom.name='%%{user|domain}'
iterate_query = SELECT email AS user FROM core_user
}
#passdb static {
# fields {
# user=%%{user|username|lower}
# noauthenticate=yes
# }
## you can remove next line if you want to always normalize your usernames
# skip = authenticated
#}
# "prefetch" user database means that the passdb already provided the
# needed information and there's no need to do a separate userdb lookup.
# <https://doc.dovecot.org/latest/core/config/auth/databases/prefetch.html>
#userdb prefetch {
#}
#userdb static {
# fields {
# user=%%{user|lower}
# }
# you can remove next line if you want to always normalize your usernames
# skip = found
#}
# If you don't have any user-specific settings, you can avoid the user_query
# by using userdb static instead of userdb sql, for example:
# <https://doc.dovecot.org/latest/core/config/auth/databases/static.html>
#userdb static {
#fields {
# uid = vmail
# gid = vmail
# home = /var/vmail/%%{user}
#}
#}

View File

@@ -1,195 +0,0 @@
# Authentication for SQL users. Included from auth.conf.
#
# <https://doc.dovecot.org/latest/core/config/auth/databases/sql.html>
# For the sql passdb module, you'll need a database with a table that
# contains fields for at least the username and password. If you want to
# use the user@domain syntax, you might want to have a separate domain
# field as well.
#
# If your users all have the same uig/gid, and have predictable home
# directories, you can use the static userdb module to generate the home
# dir based on the username and domain. In this case, you won't need fields
# for home, uid, or gid in the database.
#
# If you prefer to use the sql userdb module, you'll want to add fields
# for home, uid, and gid. Here is an example table:
#
# CREATE TABLE users (
# username VARCHAR(128) NOT NULL,
# domain VARCHAR(128) NOT NULL,
# password VARCHAR(64) NOT NULL,
# home VARCHAR(255) NOT NULL,
# uid INTEGER NOT NULL,
# gid INTEGER NOT NULL,
# active CHAR(1) DEFAULT 'Y' NOT NULL
# );
# Database driver: mysql, pgsql, sqlite
sql_driver = %db_driver
# Database connection string. This is driver-specific setting.
#
# HA / round-robin load-balancing is supported by giving multiple host
# settings, like: host=sql1.host.org host=sql2.host.org
#
# pgsql:
# For available options, see the PostgreSQL documention for the
# PQconnectdb function of libpq.
# Use maxconns=n (default 5) to change how many connections Dovecot can
# create to pgsql.
#
# mysql:
# Basic options emulate PostgreSQL option names:
# host, port, user, password, dbname
#
# But also adds some new settings:
# client_flags - See MySQL manual
# ssl_ca, ssl_ca_path - Set either one or both to enable SSL
# ssl_cert, ssl_key - For sending client-side certificates to server
# ssl_cipher - Set minimum allowed cipher security (default: HIGH)
# option_file - Read options from the given file instead of
# the default my.cnf location
# option_group - Read options from the given group (default: client)
#
# You can connect to UNIX sockets by using host: host=/var/run/mysql.sock
# Note that currently you can't use spaces in parameters.
#
# sqlite:
# The path to the database file.
#
# Examples:
# mysql 192.168.1.1 {
# dbname = users
# }
# mysql sql.example.com {
# ssl = yes
# user = virtual
# password = blarg
# dbname = virtual
# }
# sqlite /etc/dovecot/authdb.sqlite {
# }
#
#mysql /var/run/mysqld/mysqld.sock {
# user = dovecot
# password = dvmail
# dbname = dovecot
#}
#mysql localhost {
# ...
#}
pgsql %dbhost {
parameters {
port = %dbport
dbname = %modoboa_dbname
user = %modoboa_dbuser
password = %modoboa_dbpassword
}
}
#passdb sql {
# default_password_scheme = SHA256
# passdb query to retrieve the password. It can return fields:
# password - The user's password. This field must be returned.
# user - user@domain from the database. Needed with case-insensitive lookups.
# username and domain - An alternative way to represent the "user" field.
#
# The "user" field is often necessary with case-insensitive lookups to avoid
# e.g. "name" and "nAme" logins creating two different mail directories. If
# your user and domain names are in separate fields, you can return "username"
# and "domain" fields instead of "user".
#
# The query can also return other fields which have a special meaning, see
# https://doc.dovecot.org/latest/core/config/auth/passdb.html#extra-fields
#
# Commonly used available substitutions (see https://doc.dovecot.org/latest/core/settings/variables.html
# for full list):
# %%{user} = entire user@domain
# %%{user|username} = user part of user@domain
# %%{user|domain} = domain part of user@domain
#
# Note that these can be used only as input to SQL query. If the query outputs
# any of these substitutions, they're not touched. Otherwise it would be
# difficult to have eg. usernames containing '%%' characters.
#
# Example:
# query = SELECT userid AS user, pw AS password \
# FROM users WHERE userid = '%%u' AND active = 'Y'
#
# query = \
# SELECT userid as username, domain, password \
# FROM users WHERE userid = '%%{user|username}' AND domain = '%%{user|domain}'
#}
passdb sql {
query = SELECT email AS user, password FROM core_user u INNER JOIN admin_mailbox mb ON u.id=mb.user_id INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE (mb.is_send_only IS NOT TRUE OR '%%{protocol}' NOT IN ('imap', 'pop3')) AND email='%%{user}' AND is_active AND dom.enabled
}
#userdb sql {
# userdb query to retrieve the user information. It can return fields:
# uid - System UID (overrides mail_uid setting)
# gid - System GID (overrides mail_gid setting)
# home - Home directory
# mail_driver - Mail driver
# mail_path - Mail storage path
#
# None of these are strictly required. If you use a single UID and GID, and
# home or mail directory fits to a template string, you could use userdb static
# instead. For a list of all fields that can be returned, see
# Examples:
# query = SELECT home, uid, gid FROM users WHERE userid = '%%{user}'
# query = SELECT dir AS home, user AS uid, group AS gid FROM users where userid = '%%{user}'
# query = SELECT home, 501 AS uid, 501 AS gid FROM users WHERE userid = '%%{user}'
#
# query = \
# SELECT home, uid, gid \
# FROM users WHERE userid = '%%{user|username}' AND domain = '%%{user|domain}'
# Query to get a list of all usernames.
# iterate_query = SELECT username AS user,domain FROM users
# userdb_ldap {
# iterate_fields {
# home = /var/vmail/%%{home}
# }
# }
#}
userdb sql {
query = SELECT '%{home_dir}/%%{user|domain}/%%{user|username}' AS home, %mailboxes_owner_uid as uid, %mailboxes_owner_gid as gid, mb.quota || 'M' AS quota_storage_size FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN core_user u ON u.id=mb.user_id WHERE (mb.is_send_only IS NOT TRUE OR '%%{protocol}' NOT IN ('imap', 'pop3', 'lmtp')) AND mb.address='%%{user|username}' AND dom.name='%%{user|domain}'
iterate_query = SELECT email AS user FROM core_user
}
#passdb static {
# fields {
# user=%%{user|username|lower}
# noauthenticate=yes
# }
## you can remove next line if you want to always normalize your usernames
# skip = authenticated
#}
# "prefetch" user database means that the passdb already provided the
# needed information and there's no need to do a separate userdb lookup.
# <https://doc.dovecot.org/latest/core/config/auth/databases/prefetch.html>
#userdb prefetch {
#}
#userdb static {
# fields {
# user=%%{user|lower}
# }
# you can remove next line if you want to always normalize your usernames
# skip = found
#}
# If you don't have any user-specific settings, you can avoid the user_query
# by using userdb static instead of userdb sql, for example:
# <https://doc.dovecot.org/latest/core/config/auth/databases/static.html>
#userdb static {
#fields {
# uid = vmail
# gid = vmail
# home = /var/vmail/%%{user}
#}
#}

View File

@@ -1,87 +0,0 @@
## Dovecot configuration file
# If you're in a hurry, see https://doc.dovecot.org/latest/core/config/guides/quick.html
# "doveconf -n" command gives a clean output of the changed settings. Use it
# instead of copy&pasting files when posting to the Dovecot mailing list.
# '#' character and everything after it is treated as comments. Extra spaces
# and tabs are ignored. If you want to use either of these explicitly, put the
# value inside quotes, eg.: key = "# char and trailing whitespace "
# Default values are shown for each setting, it's not required to uncomment
# those. These are exceptions to this though: No sections (e.g. namespace {})
# or plugin settings are added by default, they're listed only as examples.
# Paths are also just examples with the real defaults being based on configure
# options. The paths listed here are for configure --prefix=/usr/local
# --sysconfdir=/usr/local/etc --localstatedir=/var
dovecot_config_version = 2.4.0
dovecot_storage_version = 2.4.0
# Protocols we want to be serving.
%protocols
!include_try /usr/share/dovecot/protocols.d/*.protocol
# A comma separated list of IPs or hosts where to listen in for connections.
# "*" listens in all IPv4 interfaces, "::" listens in all IPv6 interfaces.
# If you want to specify non-default ports or anything more complex,
# edit conf.d/master.conf.
#listen = *, ::
# Base directory where to store runtime data.
#base_dir = /var/run/dovecot/
# Name of this instance. In multi-instance setup doveadm and other commands
# can use -i <instance_name> to select which instance is used (an alternative
# to -c <config_path>). The instance name is also added to Dovecot processes
# in ps output.
#instance_name = dovecot
# Greeting message for clients.
#login_greeting = Dovecot ready.
# Space separated list of trusted network ranges. Connections from these
# IPs are allowed to override their IP addresses and ports (for logging and
# for authentication checks). disable_plaintext_auth is also ignored for
# these networks, unless ssl=required.
# Typically you'd specify your IMAP proxy servers here.
#login_trusted_networks =
# With proxy_maybe=yes if proxy destination matches any of these IPs, don't do
# proxying. This isn't necessary normally, but may be useful if the destination
# IP is e.g. a load balancer's IP.
#auth_proxy_self =
# Show more verbose process titles (in ps). Currently shows user name and
# IP address. Useful for seeing who are actually using the IMAP processes
# (eg. shared mailboxes or if same uid is used for multiple accounts).
#verbose_proctitle = yes
# Should all processes be killed when Dovecot master process shuts down.
# Setting this to "no" means that Dovecot can be upgraded without
# forcing existing client connections to close (although that could also be
# a problem if the upgrade is e.g. because of a security fix).
#shutdown_clients = yes
# If non-zero, run mail commands via this many connections to doveadm server,
# instead of running them directly in the same process.
#doveadm_worker_count = 0
# UNIX socket or host:port used for connecting to doveadm server
#doveadm_socket_path = doveadm-server
# Space separated list of environment variables that are preserved on Dovecot
# startup and passed down to all of its child processes. You can also give
# key=value pairs to always set specific settings.
#import_environment {
# TZ=%%{env:TZ}
#}
# Most of the actual configuration gets included below. The filenames are
# first sorted by their ASCII value and parsed in that order. The 00-prefixes
# in filenames are intended to make it easier to understand the ordering.
!include conf.d/*.conf
# A config file can also tried to be included without giving an error if
# it's not found:
!include_try local.conf

View File

@@ -96,7 +96,7 @@ auth_master_user_separator = *
# plain login digest-md5 cram-md5 ntlm rpa apop anonymous gssapi otp skey # plain login digest-md5 cram-md5 ntlm rpa apop anonymous gssapi otp skey
# gss-spnego # gss-spnego
# NOTE: See also disable_plaintext_auth setting. # NOTE: See also disable_plaintext_auth setting.
auth_mechanisms = plain login oauthbearer xoauth2 auth_mechanisms = plain login
## ##
## Password and user databases ## Password and user databases
@@ -120,7 +120,6 @@ auth_mechanisms = plain login oauthbearer xoauth2
#!include auth-system.conf.ext #!include auth-system.conf.ext
!include auth-sql.conf.ext !include auth-sql.conf.ext
!include auth-oauth2.conf.ext
#!include auth-ldap.conf.ext #!include auth-ldap.conf.ext
#!include auth-passwdfile.conf.ext #!include auth-passwdfile.conf.ext
#!include auth-checkpassword.conf.ext #!include auth-checkpassword.conf.ext

View File

@@ -131,6 +131,13 @@ service auth {
group = postfix group = postfix
} }
# Radicale auth
%{radicale_enabled}unix_listener %{radicale_auth_socket_path} {
%{radicale_enabled} mode = 0666
%{radicale_enabled} user = %{radicale_user}
%{radicale_enabled} group = %{radicale_user}
%{radicale_enabled}}
# Auth process is run as this user. # Auth process is run as this user.
#user = $default_internal_user #user = $default_internal_user
} }

View File

@@ -38,7 +38,7 @@ plugin {
# Identical to sieve_before, only the specified scripts are executed after the # Identical to sieve_before, only the specified scripts are executed after the
# user's script (only when keep is still in effect!). Multiple script file or # user's script (only when keep is still in effect!). Multiple script file or
# directory paths can be specified by appending an increasing number. # directory paths can be specified by appending an increasing number.
%{do_move_spam_to_junk}sieve_after = /etc/dovecot/conf.d/custom_after_sieve #sieve_after =
#sieve_after2 = #sieve_after2 =
#sieve_after2 = (etc...) #sieve_after2 = (etc...)

View File

@@ -1,4 +0,0 @@
require "fileinto";
if header :contains "X-Spam-Status" "Yes" {
fileinto "Junk";
}

View File

@@ -3,7 +3,6 @@
# #
PYTHON=%{venv_path}/bin/python PYTHON=%{venv_path}/bin/python
INSTANCE=%{instance_path} INSTANCE=%{instance_path}
MAILTO=%{cron_error_recipient}
# Operations on mailboxes # Operations on mailboxes
%{dovecot_enabled}* * * * * %{dovecot_mailboxes_owner} $PYTHON $INSTANCE/manage.py handle_mailbox_operations %{dovecot_enabled}* * * * * %{dovecot_mailboxes_owner} $PYTHON $INSTANCE/manage.py handle_mailbox_operations

View File

@@ -3,7 +3,7 @@ autostart=true
autorestart=true autorestart=true
command=%{venv_path}/bin/python %{home_dir}/instance/manage.py rqworker dkim command=%{venv_path}/bin/python %{home_dir}/instance/manage.py rqworker dkim
directory=%{home_dir} directory=%{home_dir}
user=%{dkim_user} user=%{opendkim_user}
redirect_stderr=true redirect_stderr=true
numprocs=1 numprocs=1
stopsignal=TERM stopsignal=TERM

View File

@@ -1,15 +1,18 @@
upstream automx {
server unix:%uwsgi_socket_path fail_timeout=0;
}
server { server {
listen 80; listen 80;
listen [::]:80; listen [::]:80;
server_name %hostname; server_name %hostname;
root %app_instance_path; root /srv/automx/instance;
access_log /var/log/nginx/%{hostname}-access.log; access_log /var/log/nginx/%{hostname}-access.log;
error_log /var/log/nginx/%{hostname}-error.log; error_log /var/log/nginx/%{hostname}-error.log;
location ~ ^/(mail/config-v1.1.xml|mobileconfig) { location /mail/config-v1.1.xml {
include uwsgi_params; include uwsgi_params;
uwsgi_param UWSGI_SCRIPT instance.wsgi:application; uwsgi_pass automx;
uwsgi_pass modoboa;
} }
} }

View File

@@ -37,20 +37,7 @@ server {
try_files $uri $uri/ =404; try_files $uri $uri/ =404;
} }
%{rspamd_enabled} location /rspamd/ { location ^~ /new-admin {
%{rspamd_enabled} proxy_pass http://localhost:11334/;
%{rspamd_enabled}
%{rspamd_enabled} proxy_set_header Host $host;
%{rspamd_enabled} proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
%{rspamd_enabled} }
location ~ ^/(api|accounts|autodiscover) {
include uwsgi_params;
uwsgi_param UWSGI_SCRIPT instance.wsgi:application;
uwsgi_pass modoboa;
}
location / {
alias %{app_instance_path}/frontend/; alias %{app_instance_path}/frontend/;
index index.html; index index.html;
@@ -61,5 +48,10 @@ server {
try_files $uri $uri/ /index.html = 404; try_files $uri $uri/ /index.html = 404;
} }
location / {
include uwsgi_params;
uwsgi_param UWSGI_SCRIPT instance.wsgi:application;
uwsgi_pass modoboa;
}
%{extra_config} %{extra_config}
} }

View File

@@ -1,11 +0,0 @@
if /^\s*Received:.*Authenticated sender.*\(Postfix\)/
/^Received: from .*? \([\w\-.]* \[.*?\]\)(.*|\n.*)\(Authenticated sender: (.+)\)\s+by.+\(Postfix\) with (.*)/
REPLACE Received: from [127.0.0.1] (localhost [127.0.0.1]) by localhost (Mailerdaemon) with $3
endif
if /^\s*Received: from .*rspamd.localhost .*\(Postfix\)/
/^Received: from.* (.*|\n.*)\((.+) (.+)\)\s+by (.+) \(Postfix\) with (.*)/
REPLACE Received: from rspamd (rspamd $3) by $4 (Postfix) with $5
endif
/^\s*X-Enigmail/ IGNORE
/^\s*X-Originating-IP/ IGNORE
/^\s*X-Forward/ IGNORE

View File

@@ -48,13 +48,11 @@ smtpd_tls_security_level = may
smtpd_tls_received_header = yes smtpd_tls_received_header = yes
# Disallow SSLv2 and SSLv3, only accept secure ciphers # Disallow SSLv2 and SSLv3, only accept secure ciphers
smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 smtpd_tls_protocols = !SSLv2, !SSLv3
smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3
smtpd_tls_mandatory_ciphers = high smtpd_tls_mandatory_ciphers = high
smtpd_tls_mandatory_exclude_ciphers = aNULL, eNULL, EXPORT, DES, RC4, MD5, PSK, aECDH, EDH-DSS-DES-CBC3-SHA, EDH-RSA-DES-CBC3-SHA, KRB5-DES, CBC3-SHA, CAMELLIA, SEED-SHA, AES256-SHA, AES256-SHA256, AES256-GCM-SHA384, AES128-SHA, AES128-SHA256, AES128-GCM-SHA256, DHE-RSA-AES128-GCM-SHA256, DHE-RSA-AES128-SHA, DHE-RSA-AES128-SHA256, DHE-RSA-AES256-GCM-SHA384, DHE-RSA-AES256-SHA, DHE-RSA-AES256-SHA256, DHE-RSA-CHACHA20-POLY1305, ECDHE-RSA-AES128-SHA, ECDHE-RSA-AES256-SHA smtpd_tls_mandatory_exclude_ciphers = aNULL, MD5 , DES, ADH, RC4, PSD, SRP, 3DES, eNULL
smtpd_tls_exclude_ciphers = aNULL, eNULL, EXPORT, DES, RC4, MD5, PSK, aECDH, EDH-DSS-DES-CBC3-SHA, EDH-RSA-DES-CBC3-SHA, KRB5-DES, CBC3-SHA, CAMELLIA, SEED-SHA, AES256-SHA, AES256-SHA256, AES256-GCM-SHA384, AES128-SHA, AES128-SHA256, AES128-GCM-SHA256, DHE-RSA-AES128-GCM-SHA256, DHE-RSA-AES128-SHA, DHE-RSA-AES128-SHA256, DHE-RSA-AES256-GCM-SHA384, DHE-RSA-AES256-SHA, DHE-RSA-AES256-SHA256, DHE-RSA-CHACHA20-POLY1305, ECDHE-RSA-AES128-SHA, ECDHE-RSA-AES256-SHA smtpd_tls_exclude_ciphers = aNULL, MD5 , DES, ADH, RC4, PSD, SRP, 3DES, eNULL
tls_preempt_cipherlist = yes
tls_ssl_options = NO_COMPRESSION
# Enable elliptic curve cryptography # Enable elliptic curve cryptography
smtpd_tls_eecdh_grade = strong smtpd_tls_eecdh_grade = strong
@@ -122,19 +120,10 @@ strict_rfc821_envelopes = yes
%{opendkim_enabled}milter_default_action = accept %{opendkim_enabled}milter_default_action = accept
%{opendkim_enabled}milter_content_timeout = 30s %{opendkim_enabled}milter_content_timeout = 30s
# Rspamd setup
%{rspamd_enabled}smtpd_milters = inet:localhost:11332
%{rspamd_enabled}non_smtpd_milters = inet:localhost:11332
%{rspamd_enabled}milter_default_action = accept
%{rspamd_enabled}milter_protocol = 6
# List of authorized senders # List of authorized senders
smtpd_sender_login_maps = smtpd_sender_login_maps =
proxy:%{db_driver}:/etc/postfix/sql-sender-login-map.cf proxy:%{db_driver}:/etc/postfix/sql-sender-login-map.cf
# Add authenticated header to hide public client IP
smtpd_sasl_authenticated_header = yes
# Recipient restriction rules # Recipient restriction rules
smtpd_recipient_restrictions = smtpd_recipient_restrictions =
check_policy_service inet:127.0.0.1:9999 check_policy_service inet:127.0.0.1:9999
@@ -151,27 +140,28 @@ smtpd_recipient_restrictions =
## Postcreen settings ## Postcreen settings
# #
%{rspamd_disabled}postscreen_access_list = postscreen_access_list =
%{rspamd_disabled} permit_mynetworks permit_mynetworks
%{rspamd_disabled} cidr:/etc/postfix/postscreen_spf_whitelist.cidr cidr:/etc/postfix/postscreen_spf_whitelist.cidr
%{rspamd_disabled}postscreen_blacklist_action = enforce postscreen_blacklist_action = enforce
# Use some DNSBL # Use some DNSBL
%{rspamd_disabled}postscreen_dnsbl_sites = postscreen_dnsbl_sites =
%{rspamd_disabled} zen.spamhaus.org=127.0.0.[2..11]*3 zen.spamhaus.org=127.0.0.[2..11]*3
%{rspamd_disabled} bl.spameatingmonkey.net=127.0.0.2*2 bl.spameatingmonkey.net=127.0.0.2*2
%{rspamd_disabled} bl.spamcop.net=127.0.0.2 bl.spamcop.net=127.0.0.2
%{rspamd_disabled}postscreen_dnsbl_threshold = 3 dnsbl.sorbs.net=127.0.0.[2..15]
%{rspamd_disabled}postscreen_dnsbl_action = enforce postscreen_dnsbl_threshold = 3
postscreen_dnsbl_action = enforce
%{rspamd_disabled}postscreen_greet_banner = Welcome, please wait... postscreen_greet_banner = Welcome, please wait...
%{rspamd_disabled}postscreen_greet_action = enforce postscreen_greet_action = enforce
%{rspamd_disabled}postscreen_pipelining_enable = yes postscreen_pipelining_enable = yes
%{rspamd_disabled}postscreen_pipelining_action = enforce postscreen_pipelining_action = enforce
%{rspamd_disabled}postscreen_non_smtp_command_enable = yes postscreen_non_smtp_command_enable = yes
%{rspamd_disabled}postscreen_non_smtp_command_action = enforce postscreen_non_smtp_command_action = enforce
%{rspamd_disabled}postscreen_bare_newline_enable = yes postscreen_bare_newline_enable = yes
%{rspamd_disabled}postscreen_bare_newline_action = enforce postscreen_bare_newline_action = enforce

View File

@@ -9,8 +9,7 @@
# service type private unpriv chroot wakeup maxproc command + args # service type private unpriv chroot wakeup maxproc command + args
# (yes) (yes) (yes) (never) (100) # (yes) (yes) (yes) (never) (100)
# ========================================================================== # ==========================================================================
%{rspamd_disabled}smtp inet n - - - 1 postscreen smtp inet n - - - 1 postscreen
%{rspamd_enabled}smtp inet n - - - - smtpd
smtpd pass - - - - - smtpd smtpd pass - - - - - smtpd
%{amavis_enabled} -o smtpd_proxy_filter=inet:[127.0.0.1]:10024 %{amavis_enabled} -o smtpd_proxy_filter=inet:[127.0.0.1]:10024
%{amavis_enabled} -o smtpd_proxy_options=speed_adjust %{amavis_enabled} -o smtpd_proxy_options=speed_adjust
@@ -27,7 +26,6 @@ submission inet n - - - - smtpd
-o smtpd_helo_restrictions= -o smtpd_helo_restrictions=
-o smtpd_sender_restrictions=reject_sender_login_mismatch -o smtpd_sender_restrictions=reject_sender_login_mismatch
-o milter_macro_daemon_name=ORIGINATING -o milter_macro_daemon_name=ORIGINATING
-o cleanup_service_name=ascleanup
%{amavis_enabled} -o smtpd_proxy_filter=inet:[127.0.0.1]:10026 %{amavis_enabled} -o smtpd_proxy_filter=inet:[127.0.0.1]:10026
#smtps inet n - - - - smtpd #smtps inet n - - - - smtpd
# -o syslog_name=postfix/smtps # -o syslog_name=postfix/smtps
@@ -43,8 +41,6 @@ submission inet n - - - - smtpd
#628 inet n - - - - qmqpd #628 inet n - - - - qmqpd
pickup unix n - - 60 1 pickup pickup unix n - - 60 1 pickup
cleanup unix n - - - 0 cleanup cleanup unix n - - - 0 cleanup
ascleanup unix n - - - 0 cleanup
-o header_checks=pcre:/etc/postfix/anonymize_headers.pcre
qmgr unix n - n 300 1 qmgr qmgr unix n - n 300 1 qmgr
#qmgr unix n - n 300 1 oqmgr #qmgr unix n - n 300 1 oqmgr
tlsmgr unix - - - 1000? 1 tlsmgr tlsmgr unix - - - 1000? 1 tlsmgr
@@ -128,6 +124,11 @@ mailman unix - n n - - pipe
flags=FR user=list argv=/usr/lib/mailman/bin/postfix-to-mailman.py flags=FR user=list argv=/usr/lib/mailman/bin/postfix-to-mailman.py
${nexthop} ${user} ${nexthop} ${user}
# Modoboa autoreply service
#
autoreply unix - n n - - pipe
flags= user=%{dovecot_mailboxes_owner}:%{dovecot_mailboxes_owner} argv=%{modoboa_venv_path}/bin/python %{modoboa_instance_path}/manage.py autoreply $sender $mailbox
# Amavis return path # Amavis return path
# #
%{amavis_enabled}127.0.0.1:10025 inet n - n - - smtpd %{amavis_enabled}127.0.0.1:10025 inet n - n - - smtpd
@@ -148,4 +149,4 @@ mailman unix - n n - - pipe
%{amavis_enabled} -o smtpd_client_connection_count_limit=0 %{amavis_enabled} -o smtpd_client_connection_count_limit=0
%{amavis_enabled} -o smtpd_client_connection_rate_limit=0 %{amavis_enabled} -o smtpd_client_connection_rate_limit=0
%{amavis_enabled} -o receive_override_options=no_header_body_checks,no_unknown_recipient_checks %{amavis_enabled} -o receive_override_options=no_header_body_checks,no_unknown_recipient_checks
%{amavis_enabled} -o local_header_rewrite_clients=permit_mynetworks,permit_sasl_authenticated %{amavis_enabled} -o local_header_rewrite_clients=

View File

@@ -71,7 +71,7 @@
# Authentication method # Authentication method
# Value: none | htpasswd | remote_user | http_x_remote_user # Value: none | htpasswd | remote_user | http_x_remote_user
type = radicale_modoboa_auth_oauth2 type = radicale_dovecot_auth
# Htpasswd filename # Htpasswd filename
# htpasswd_filename = users # htpasswd_filename = users
@@ -85,14 +85,14 @@ type = radicale_modoboa_auth_oauth2
# Incorrect authentication delay (seconds) # Incorrect authentication delay (seconds)
#delay = 1 #delay = 1
oauth2_introspection_endpoint = %{oauth2_introspection_url} auth_socket = %{auth_socket_path}
[rights] [rights]
# Rights backend # Rights backend
# Value: none | authenticated | owner_only | owner_write | from_file # Value: none | authenticated | owner_only | owner_write | from_file
type = from_file type = from_file
# File for rights management from_file # File for rights management from_file
file = %{config_dir}/rights file = %{config_dir}/rights
@@ -102,6 +102,8 @@ file = %{config_dir}/rights
# Storage backend # Storage backend
# Value: multifilesystem # Value: multifilesystem
type = radicale_storage_by_index
radicale_storage_by_index_fields = dtstart, dtend, uid, summary
# Folder for storing local collections, created if not present # Folder for storing local collections, created if not present
filesystem_folder = %{home_dir}/collections filesystem_folder = %{home_dir}/collections
@@ -132,7 +134,7 @@ filesystem_folder = %{home_dir}/collections
# Web interface backend # Web interface backend
# Value: none | internal # Value: none | internal
type = none type = none
[logging] [logging]

View File

@@ -1,14 +0,0 @@
clamav {
scan_mime_parts = true;
scan_text_mime = true;
scan_image_mime = true;
retransmits = 2;
timeout = 30;
symbol = "CLAM_VIRUS";
type = "clamav";
servers = "127.0.0.1:3310"
patterns {
# symbol_name = "pattern";
JUST_EICAR = "Test.EICAR";
}
}

View File

@@ -1,3 +0,0 @@
try_fallback = false;
selector_map = "%selector_map_path";
path_map = "%key_map_path";

View File

@@ -1,3 +0,0 @@
try_fallback = false;
selector_map = "%selector_map_path";
path_map = "%key_map_path";

View File

@@ -1,21 +0,0 @@
reporting {
# Required attributes
enabled = true; # Enable reports in general
email = 'postmaster@%hostname'; # Source of DMARC reports
domain = '%hostname'; # Domain to serve
org_name = '%hostname'; # Organisation
# Optional parameters
#bcc_addrs = ["postmaster@example.com"]; # additional addresses to copy on reports
report_local_controller = false; # Store reports for local/controller scans (for testing only)
#helo = 'rspamd.localhost'; # Helo used in SMTP dialog
#smtp = '127.0.0.1'; # SMTP server IP
#smtp_port = 25; # SMTP server port
from_name = '%hostname DMARC REPORT'; # SMTP FROM
msgid_from = 'rspamd'; # Msgid format
#max_entries = 1k; # Maxiumum amount of entries per domain
#keys_expire = 2d; # Expire date for Redis keys
#only_domains = '/path/to/map'; # Only store reports from domains or eSLDs listed in this map
# Available from 3.3
#exclude_domains = '/path/to/map'; # Exclude reports from domains or eSLDs listed in this map
#exclude_domains = ["example.com", "another.com"]; # Alternative, use array to exclude reports from domains or eSLDs
}

View File

@@ -1,5 +0,0 @@
rules {
DMARC_POLICY_QUARANTINE {
action = "add header";
}
}

View File

@@ -1,2 +0,0 @@
%{greylisting_disabled}enabled = false;
servers = "127.0.0.1:6379";

View File

@@ -1,5 +0,0 @@
symbols {
"WHITELIST_AUTHENTICATED" {
weight = %whitelist_auth_weigth;
}
}

View File

@@ -1,20 +0,0 @@
actions {
reject = 15; # normal value is 15, 150 so it will never be rejected
add_header = 6; # set to 0.1 for testing, 6 for normal operation.
rewrite_subject = 8; # Default: 8
greylist = 4; # Default: 4
}
group "antivirus" {
symbol "JUST_EICAR" {
weight = 10;
description = "Eicar test signature";
}
symbol "CLAM_VIRUS_FAIL" {
weight = 0;
}
symbol "CLAM_VIRUS" {
weight = 10;
description = "ClamAV found a Virus";
}
}

View File

@@ -1,16 +0,0 @@
use = ["x-spam-status","x-virus","authentication-results" ];
extended_spam_headers = false;
skip_local = false;
skip_authenticated = false;
routines {
x-virus {
header = "X-Virus";
remove = 1;
symbols = ["CLAM_VIRUS", "JUST_EICAR"];
}
}

View File

@@ -1 +0,0 @@
enabled = true;

View File

@@ -1,6 +0,0 @@
# to disable all predefined rules if the user doesn't want dnsbl
url_whitelist = [];
rbls {
}

View File

@@ -1,2 +0,0 @@
write_servers = "localhost";
read_servers = "localhost";

View File

@@ -1,8 +0,0 @@
authenticated {
priority = high;
authenticated = yes;
apply {
groups_disabled = ["rbl", "spf"];
}
%{whitelist_auth_enabled} symbols ["WHITELIST_AUTHENTICATED"];
}

View File

@@ -1,6 +0,0 @@
spf_cache_size = 1k;
spf_cache_expire = 1d;
max_dns_nesting = 10;
max_dns_requests = 30;
min_cache_ttl = 5m;
disable_ipv6 = false;

View File

@@ -1 +0,0 @@
enable_password = %controller_password

View File

@@ -1 +0,0 @@
enabled = false;

View File

@@ -1,3 +0,0 @@
upstream "local" {
self_scan = yes;
}

View File

@@ -0,0 +1,14 @@
[uwsgi]
uid = %app_user
gid = %app_user
plugins = %uwsgi_plugin
home = %app_venv_path
chdir = %app_instance_path
module = automx_wsgi
master = true
vhost = true
harakiri = 60
processes = %nb_processes
socket = %uwsgi_socket_path
chmod-socket = 660
vacuum = true

View File

@@ -13,5 +13,3 @@ socket = %uwsgi_socket_path
chmod-socket = 660 chmod-socket = 660
vacuum = true vacuum = true
single-interpreter = True single-interpreter = True
max-requests = 5000
buffer-size = 8192

View File

@@ -27,7 +27,7 @@ class Modoboa(base.Installer):
"build-essential", "python3-dev", "libxml2-dev", "libxslt-dev", "build-essential", "python3-dev", "libxml2-dev", "libxslt-dev",
"libjpeg-dev", "librrd-dev", "rrdtool", "libffi-dev", "cron", "libjpeg-dev", "librrd-dev", "rrdtool", "libffi-dev", "cron",
"libssl-dev", "redis-server", "supervisor", "pkg-config", "libssl-dev", "redis-server", "supervisor", "pkg-config",
"libcairo2-dev", "libmagic-dev" "libcairo2-dev"
], ],
"rpm": [ "rpm": [
"gcc", "gcc-c++", "python3-devel", "libxml2-devel", "libxslt-devel", "gcc", "gcc-c++", "python3-devel", "libxml2-devel", "libxslt-devel",
@@ -49,47 +49,40 @@ class Modoboa(base.Installer):
self.instance_path = self.config.get("modoboa", "instance_path") self.instance_path = self.config.get("modoboa", "instance_path")
self.extensions = self.config.get("modoboa", "extensions").split() self.extensions = self.config.get("modoboa", "extensions").split()
self.devmode = self.config.getboolean("modoboa", "devmode") self.devmode = self.config.getboolean("modoboa", "devmode")
self.amavis_enabled = self.config.getboolean("amavis", "enabled") # Sanity check for amavis
self.rspamd_enabled = self.config.getboolean("rspamd", "enabled") self.amavis_enabled = False
if "modoboa-amavis" in self.extensions:
if self.config.getboolean("amavis", "enabled"):
self.amavis_enabled = True
else:
self.extensions.remove("modoboa-amavis")
if "modoboa-radicale" in self.extensions:
if not self.config.getboolean("radicale", "enabled"):
self.extensions.remove("modoboa-radicale")
self.dovecot_enabled = self.config.getboolean("dovecot", "enabled") self.dovecot_enabled = self.config.getboolean("dovecot", "enabled")
self.opendkim_enabled = self.config.getboolean("opendkim", "enabled") self.opendkim_enabled = self.config.getboolean("opendkim", "enabled")
self.dkim_cron_enabled = False self.dkim_cron_enabled = False
def is_extension_ok_for_version(self, extension, version): def is_extension_ok_for_version(self, extension, version):
"""Check if extension can be installed with this modo version.""" """Check if extension can be installed with this modo version."""
if extension not in compatibility_matrix.EXTENSIONS_AVAILABILITY:
return True
version = utils.convert_version_to_int(version) version = utils.convert_version_to_int(version)
if extension in compatibility_matrix.EXTENSIONS_AVAILABILITY: min_version = compatibility_matrix.EXTENSIONS_AVAILABILITY[extension]
min_version = compatibility_matrix.EXTENSIONS_AVAILABILITY[extension] min_version = utils.convert_version_to_int(min_version)
min_version = utils.convert_version_to_int(min_version) return version >= min_version
return version >= min_version
if extension in compatibility_matrix.REMOVED_EXTENSIONS:
max_version = compatibility_matrix.REMOVED_EXTENSIONS[extension]
max_version = utils.convert_version_to_int(max_version)
return version < max_version
return True
def _setup_venv(self): def _setup_venv(self):
"""Prepare a dedicated virtualenv.""" """Prepare a dedicated virtualenv."""
python.setup_virtualenv(self.venv_path, sudo_user=self.user) python.setup_virtualenv(
packages = [] self.venv_path, sudo_user=self.user, python_version=3)
packages = ["rrdtool"]
version = self.config.get("modoboa", "version") version = self.config.get("modoboa", "version")
extras = "postgresql"
if self.dbengine != "postgres":
extras = "mysql"
if self.devmode:
extras += ",dev"
if version == "latest": if version == "latest":
packages += [f"modoboa[{extras}]"] + self.extensions packages += ["modoboa"] + self.extensions
for extension in list(self.extensions):
if extension in compatibility_matrix.REMOVED_EXTENSIONS.keys():
self.extensions.remove(extension)
self.extensions = [
extension for extension in self.extensions
if extension not in compatibility_matrix.REMOVED_EXTENSIONS
]
else: else:
matrix = compatibility_matrix.COMPATIBILITY_MATRIX[version] matrix = compatibility_matrix.COMPATIBILITY_MATRIX[version]
packages.append(f"modoboa[{extras}]=={version}") packages.append("modoboa=={}".format(version))
for extension in list(self.extensions): for extension in list(self.extensions):
if not self.is_extension_ok_for_version(extension, version): if not self.is_extension_ok_for_version(extension, version):
self.extensions.remove(extension) self.extensions.remove(extension)
@@ -98,8 +91,8 @@ class Modoboa(base.Installer):
req_version = matrix[extension] req_version = matrix[extension]
if req_version is None: if req_version is None:
continue continue
req_version = req_version.replace("<", "\\<") req_version = req_version.replace("<", "\<")
req_version = req_version.replace(">", "\\>") req_version = req_version.replace(">", "\>")
packages.append("{}{}".format(extension, req_version)) packages.append("{}{}".format(extension, req_version))
else: else:
packages.append(extension) packages.append(extension)
@@ -113,6 +106,25 @@ class Modoboa(base.Installer):
beta=self.config.getboolean("modoboa", "install_beta") beta=self.config.getboolean("modoboa", "install_beta")
) )
# Install version specific modules to the venv
modoboa_version = ".".join(str(i) for i in python.get_package_version(
"modoboa", self.venv_path, sudo_user=self.user
))
# Database:
db_file = "postgresql"
if self.dbengine != "postgres":
db_file = "mysql"
db_file += "-requirements.txt"
python.install_package_from_remote_requirements(
f"https://raw.githubusercontent.com/modoboa/modoboa/{modoboa_version}/{db_file}",
venv=self.venv_path)
# Dev mode:
if self.devmode:
python.install_package_from_remote_requirements(
f"https://raw.githubusercontent.com/modoboa/modoboa/{modoboa_version}/dev-requirements.txt",
venv=self.venv_path)
def _deploy_instance(self): def _deploy_instance(self):
"""Deploy Modoboa.""" """Deploy Modoboa."""
target = os.path.join(self.home_dir, "instance") target = os.path.join(self.home_dir, "instance")
@@ -135,10 +147,6 @@ class Modoboa(base.Installer):
prefix = ". {}; ".format( prefix = ". {}; ".format(
os.path.join(self.venv_path, "bin", "activate")) os.path.join(self.venv_path, "bin", "activate"))
if self.amavis_enabled:
self.extensions += ["modoboa.amavis"]
if self.rspamd_enabled:
self.extensions += ["modoboa.rspamd"]
args = [ args = [
"--collectstatic", "--collectstatic",
"--timezone", self.config.get("modoboa", "timezone"), "--timezone", self.config.get("modoboa", "timezone"),
@@ -239,8 +247,9 @@ class Modoboa(base.Installer):
), ),
"dovecot_mailboxes_owner": ( "dovecot_mailboxes_owner": (
self.config.get("dovecot", "mailboxes_owner")), self.config.get("dovecot", "mailboxes_owner")),
"radicale_enabled": (
"" if "modoboa-radicale" in extensions else "#"),
"opendkim_user": self.config.get("opendkim", "user"), "opendkim_user": self.config.get("opendkim", "user"),
"dkim_user": "_rspamd" if self.rspamd_enabled else self.config.get("opendkim", "user"),
"minutes": random.randint(1, 59), "minutes": random.randint(1, 59),
"hours": f"{random_hour},{random_hour+12}", "hours": f"{random_hour},{random_hour+12}",
"modoboa_2_2_or_greater": "" if self.modoboa_2_2_or_greater else "#", "modoboa_2_2_or_greater": "" if self.modoboa_2_2_or_greater else "#",
@@ -262,13 +271,16 @@ class Modoboa(base.Installer):
"handle_mailboxes": True, "handle_mailboxes": True,
"account_auto_removal": True "account_auto_removal": True
}, },
"modoboa_amavis": {
"am_pdp_mode": "inet",
},
"maillog": { "maillog": {
"rrd_rootdir": rrd_root_dir, "rrd_rootdir": rrd_root_dir,
}, },
"pdfcredentials": { "pdfcredentials": {
"storage_dir": pdf_storage_dir "storage_dir": pdf_storage_dir
}, },
"calendars": { "modoboa_radicale": {
"server_location": "https://{}/radicale/".format( "server_location": "https://{}/radicale/".format(
self.config.get("general", "hostname")), self.config.get("general", "hostname")),
"rights_file_path": "{}/rights".format( "rights_file_path": "{}/rights".format(
@@ -281,19 +293,6 @@ class Modoboa(base.Installer):
if self.config.getboolean("opendkim", "enabled"): if self.config.getboolean("opendkim", "enabled"):
settings["admin"]["dkim_keys_storage_dir"] = ( settings["admin"]["dkim_keys_storage_dir"] = (
self.config.get("opendkim", "keys_storage_dir")) self.config.get("opendkim", "keys_storage_dir"))
if self.rspamd_enabled:
settings["admin"]["dkim_keys_storage_dir"] = (
self.config.get("rspamd", "dkim_keys_storage_dir"))
settings["rspamd"] = {
"key_map_path": self.config.get("rspamd", "key_map_path"),
"selector_map_path": self.config.get("rspamd", "selector_map_path")
}
if self.config.getboolean("amavis", "enabled"):
settings["amavis"] = {
"am_pdp_mode": "inet",
}
settings = json.dumps(settings) settings = json.dumps(settings)
query = ( query = (
"UPDATE core_localconfig SET _parameters='{}'" "UPDATE core_localconfig SET _parameters='{}'"

View File

@@ -19,15 +19,14 @@ class Nginx(base.Installer):
"rpm": ["nginx"] "rpm": ["nginx"]
} }
def get_template_context(self): def get_template_context(self, app):
"""Additionnal variables.""" """Additionnal variables."""
context = super().get_template_context() context = super(Nginx, self).get_template_context()
context.update({ context.update({
"app_instance_path": ( "app_instance_path": (
self.config.get("modoboa", "instance_path")), self.config.get(app, "instance_path")),
"uwsgi_socket_path": ( "uwsgi_socket_path": (
Uwsgi(self.config, self.upgrade, self.restore).get_socket_path("modoboa") Uwsgi(self.config, self.upgrade, self.restore).get_socket_path(app))
)
}) })
return context return context
@@ -35,10 +34,9 @@ class Nginx(base.Installer):
"""Custom app configuration.""" """Custom app configuration."""
if hostname is None: if hostname is None:
hostname = self.config.get("general", "hostname") hostname = self.config.get("general", "hostname")
context = self.get_template_context() context = self.get_template_context(app)
context.update({"hostname": hostname, "extra_config": extra_config}) context.update({"hostname": hostname, "extra_config": extra_config})
src = self.get_file_path("{}.conf.tpl".format(app)) src = self.get_file_path("{}.conf.tpl".format(app))
group = None
if package.backend.FORMAT == "deb": if package.backend.FORMAT == "deb":
dst = os.path.join( dst = os.path.join(
self.config_dir, "sites-available", "{}.conf".format(hostname)) self.config_dir, "sites-available", "{}.conf".format(hostname))
@@ -48,8 +46,7 @@ class Nginx(base.Installer):
if os.path.exists(link): if os.path.exists(link):
return return
os.symlink(dst, link) os.symlink(dst, link)
if self.config.has_section(app): group = self.config.get(app, "user")
group = self.config.get(app, "user")
user = "www-data" user = "www-data"
else: else:
dst = os.path.join( dst = os.path.join(
@@ -57,17 +54,25 @@ class Nginx(base.Installer):
utils.copy_from_template(src, dst, context) utils.copy_from_template(src, dst, context)
group = "uwsgi" group = "uwsgi"
user = "nginx" user = "nginx"
if user and group: system.add_user_to_group(user, group)
system.add_user_to_group(user, group)
def post_run(self): def post_run(self):
"""Additionnal tasks.""" """Additionnal tasks."""
extra_modoboa_config = "" extra_modoboa_config = ""
if self.config.getboolean("automx", "enabled"):
hostname = "autoconfig.{}".format( hostname = "autoconfig.{}".format(
self.config.get("general", "domain")) self.config.get("general", "domain"))
self._setup_config("autoconfig", hostname) self._setup_config("automx", hostname)
extra_modoboa_config = """
location ~* ^/autodiscover/autodiscover.xml {
include uwsgi_params;
uwsgi_pass automx;
}
location /mobileconfig {
include uwsgi_params;
uwsgi_pass automx;
}
"""
if self.config.get("radicale", "enabled"): if self.config.get("radicale", "enabled"):
extra_modoboa_config += """ extra_modoboa_config += """
location /radicale/ { location /radicale/ {

View File

@@ -18,9 +18,10 @@ class Postfix(base.Installer):
appname = "postfix" appname = "postfix"
packages = { packages = {
"deb": ["postfix", "postfix-pcre"], "deb": ["postfix"],
"rpm": ["postfix"],
} }
config_files = ["main.cf", "master.cf", "anonymize_headers.pcre"] config_files = ["main.cf", "master.cf"]
def get_packages(self): def get_packages(self):
"""Additional packages.""" """Additional packages."""
@@ -28,7 +29,7 @@ class Postfix(base.Installer):
packages = ["postfix-{}".format(self.db_driver)] packages = ["postfix-{}".format(self.db_driver)]
else: else:
packages = [] packages = []
return super().get_packages() + packages return super(Postfix, self).get_packages() + packages
def install_packages(self): def install_packages(self):
"""Preconfigure postfix package installation.""" """Preconfigure postfix package installation."""
@@ -45,7 +46,7 @@ class Postfix(base.Installer):
package.backend.preconfigure( package.backend.preconfigure(
"postfix", "main_mailer_type", "select", "No configuration") "postfix", "main_mailer_type", "select", "No configuration")
super().install_packages() super(Postfix, self).install_packages()
def get_template_context(self): def get_template_context(self):
"""Additional variables.""" """Additional variables."""
@@ -59,9 +60,7 @@ class Postfix(base.Installer):
"modoboa_instance_path": self.config.get( "modoboa_instance_path": self.config.get(
"modoboa", "instance_path"), "modoboa", "instance_path"),
"opendkim_port": self.config.get( "opendkim_port": self.config.get(
"opendkim", "port"), "opendkim", "port")
"rspamd_disabled": "" if not self.config.getboolean(
"rspamd", "enabled") else "#"
}) })
return context return context
@@ -102,18 +101,8 @@ class Postfix(base.Installer):
utils.exec_cmd("postalias {}".format(aliases_file)) utils.exec_cmd("postalias {}".format(aliases_file))
# Postwhite # Postwhite
condition = ( install("postwhite", self.config, self.upgrade, self.archive_path)
not self.config.getboolean("rspamd", "enabled") and
self.config.getboolean("postwhite", "enabled")
)
if condition:
install("postwhite", self.config, self.upgrade, self.archive_path)
def backup(self, path): def backup(self, path):
"""Launch postwhite backup.""" """Launch postwhite backup."""
condition = ( backup("postwhite", self.config, path)
not self.config.getboolean("rspamd", "enabled") and
self.config.getboolean("postwhite", "enabled")
)
if condition:
backup("postwhite", self.config, path)

View File

@@ -20,8 +20,8 @@ class Postwhite(base.Installer):
] ]
no_daemon = True no_daemon = True
packages = { packages = {
"deb": ["bind9-host", "unzip"], "deb": ["bind9-host"],
"rpm": ["bind-utils", "unzip"] "rpm": ["bind-utils"]
} }
def install_from_archive(self, repository, target_dir): def install_from_archive(self, repository, target_dir):

View File

@@ -31,34 +31,30 @@ class Radicale(base.Installer):
def _setup_venv(self): def _setup_venv(self):
"""Prepare a dedicated virtualenv.""" """Prepare a dedicated virtualenv."""
python.setup_virtualenv(self.venv_path, sudo_user=self.user) python.setup_virtualenv(
self.venv_path, sudo_user=self.user, python_version=3)
packages = [ packages = [
"Radicale", "pytz", "radicale-modoboa-auth-oauth2" "Radicale", "radicale-dovecot-auth", "pytz"
] ]
python.install_packages(packages, self.venv_path, sudo_user=self.user) python.install_packages(packages, self.venv_path, sudo_user=self.user)
python.install_package_from_repository(
"radicale-storage-by-index",
"https://github.com/tonioo/RadicaleStorageByIndex",
venv=self.venv_path, sudo_user=self.user)
def get_template_context(self): def get_template_context(self):
"""Additional variables.""" """Additional variables."""
context = super().get_template_context() context = super(Radicale, self).get_template_context()
oauth2_client_id, oauth2_client_secret = utils.create_oauth2_app( radicale_auth_socket_path = self.config.get(
"Radicale", "dovecot", "radicale_auth_socket_path")
"radicale",
self.config.get("radicale", "oauth2_client_secret"),
self.config
)
hostname = self.config.get("general", "hostname")
oauth2_introspection_url = (
f"https://{oauth2_client_id}:{oauth2_client_secret}"
f"@{hostname}/api/o/introspect/"
)
context.update({ context.update({
"oauth2_introspection_url": oauth2_introspection_url, "auth_socket_path": radicale_auth_socket_path
}) })
return context return context
def get_config_files(self): def get_config_files(self):
"""Return appropriate path.""" """Return appropriate path."""
config_files = super().get_config_files() config_files = super(Radicale, self).get_config_files()
if package.backend.FORMAT == "deb": if package.backend.FORMAT == "deb":
path = "supervisor=/etc/supervisor/conf.d/radicale.conf" path = "supervisor=/etc/supervisor/conf.d/radicale.conf"
else: else:

View File

@@ -1,154 +0,0 @@
"""Rspamd related functions."""
import os
import pwd
import stat
from .. import package
from .. import utils
from .. import system
from . import base
from . import install
class Rspamd(base.Installer):
"""Rspamd installer."""
appname = "rspamd"
packages = {
"deb": [
"rspamd", "redis"
]
}
config_files = [
"local.d/arc.conf",
"local.d/dkim_signing.conf",
"local.d/dmarc.conf",
"local.d/force_actions.conf",
"local.d/greylist.conf",
"local.d/metrics.conf",
"local.d/milter_headers.conf",
"local.d/mx_check.conf",
"local.d/redis.conf",
"local.d/settings.conf",
"local.d/spf.conf",
"local.d/worker-normal.inc",
"local.d/worker-proxy.inc",
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.generate_password_condition = (
not self.upgrade or utils.user_input(
"Do you want to (re)generate rspamd password ? (y/N)").lower().startswith("y")
)
@property
def config_dir(self):
"""Return appropriate config dir."""
return "/etc/rspamd"
def install_packages(self):
debian_based_dist, codename = utils.is_dist_debian_based()
if debian_based_dist:
utils.mkdir_safe(
"/etc/apt/keyrings",
stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP |
stat.S_IROTH | stat.S_IXOTH,
0, 0
)
package.backend.add_custom_repository(
"rspamd",
"http://rspamd.com/apt-stable/",
"https://rspamd.com/apt-stable/gpg.key",
codename
)
package.backend.update()
return super().install_packages()
def install_config_files(self):
"""Make sure config directory exists."""
user = self.config.get(self.appname, "user")
pw = pwd.getpwnam(user)
targets = [
[self.app_config["dkim_keys_storage_dir"], pw[2], pw[3]]
]
for target in targets:
if not os.path.exists(target[0]):
utils.mkdir(
target[0],
stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP |
stat.S_IROTH | stat.S_IXOTH,
target[1], target[2]
)
super().install_config_files()
def get_config_files(self):
"""Return appropriate config files."""
_config_files = self.config_files
if self.config.getboolean("clamav", "enabled"):
_config_files.append("local.d/antivirus.conf")
if self.app_config["dnsbl"].lower() == "true":
_config_files.append("local.d/rbl.conf")
if self.app_config["whitelist_auth"].lower() == "true":
_config_files.append("local.d/groups.conf")
if self.generate_password_condition:
_config_files.append("local.d/worker-controller.inc")
return _config_files
def get_template_context(self):
_context = super().get_template_context()
_context["greylisting_disabled"] = "" if not self.app_config["greylisting"].lower() == "true" else "#"
_context["whitelist_auth_enabled"] = "" if self.app_config["whitelist_auth"].lower() == "true" else "#"
if self.generate_password_condition:
code, controller_password = utils.exec_cmd(
r"rspamadm pw -p {}".format(self.app_config["password"]))
if code != 0:
utils.error("Error setting rspamd password. "
"Please make sure it is not 'q1' or 'q2'."
"Storing the password in plain. See"
"https://rspamd.com/doc/quickstart.html#setting-the-controller-password")
_context["controller_password"] = self.app_config["password"]
else:
controller_password = controller_password.decode().replace("\n", "")
_context["controller_password"] = controller_password
return _context
def post_run(self):
"""Additional tasks."""
user = self.config.get(self.appname, "user")
system.add_user_to_group(
self.config.get("modoboa", "user"),
user
)
if self.config.getboolean("clamav", "enabled"):
install("clamav", self.config, self.upgrade, self.archive_path)
def custom_backup(self, path):
"""Backup custom configuration if any."""
custom_config_dir = os.path.join(self.config_dir,
"/local.d/")
custom_backup_dir = os.path.join(path, "/rspamd/")
local_files = [f for f in os.listdir(custom_config_dir)
if os.path.isfile(custom_config_dir, f)
]
for file in local_files:
utils.copy_file(file, custom_backup_dir)
if len(local_files) != 0:
utils.success("Rspamd custom configuration saved!")
def restore(self):
"""Restore custom config files."""
custom_config_dir = os.path.join(self.config_dir,
"/local.d/")
custom_backup_dir = os.path.join(self.archive_path, "/rspamd/")
backed_up_files = [
f for f in os.listdir(custom_backup_dir)
if os.path.isfile(custom_backup_dir, f)
]
for f in backed_up_files:
utils.copy_file(f, custom_config_dir)
utils.success("Custom Rspamd configuration restored.")

View File

@@ -83,9 +83,24 @@ class Uwsgi(base.Installer):
utils.exec_cmd( utils.exec_cmd(
"perl -pi -e '{}' /etc/uwsgi.ini".format(pattern)) "perl -pi -e '{}' /etc/uwsgi.ini".format(pattern))
def _setup_automx_config(self):
"""Custom automx configuration."""
dst = self._setup_config("automx")
if package.backend.FORMAT == "deb":
self._enable_config_debian(dst)
else:
system.add_user_to_group(
"uwsgi", self.config.get("automx", "user"))
pattern = (
"s/emperor-tyrant = true/emperor-tyrant = false/")
utils.exec_cmd(
"perl -pi -e '{}' /etc/uwsgi.ini".format(pattern))
def post_run(self): def post_run(self):
"""Additionnal tasks.""" """Additionnal tasks."""
self._setup_modoboa_config() self._setup_modoboa_config()
if self.config.getboolean("automx", "enabled"):
self._setup_automx_config()
def restart_daemon(self): def restart_daemon(self):
"""Restart daemon process.""" """Restart daemon process."""

View File

@@ -7,7 +7,7 @@ from . import package
from . import utils from . import utils
class CertificateBackend: class CertificateBackend(object):
"""Base class.""" """Base class."""
def __init__(self, config): def __init__(self, config):
@@ -24,44 +24,13 @@ class CertificateBackend:
return False return False
return True return True
def generate_cert(self):
"""Create a certificate."""
pass
class ManualCertificate(CertificateBackend):
"""Use certificate provided."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
path_correct = True
self.tls_cert_file_path = self.config.get("certificate",
"tls_cert_file_path")
self.tls_key_file_path = self.config.get("certificate",
"tls_key_file_path")
if not os.path.exists(self.tls_key_file_path):
utils.error("'tls_key_file_path' path is not accessible")
path_correct = False
if not os.path.exists(self.tls_cert_file_path):
utils.error("'tls_cert_file_path' path is not accessible")
path_correct = False
if not path_correct:
sys.exit(1)
self.config.set("general", "tls_key_file",
self.tls_key_file_path)
self.config.set("general", "tls_cert_file",
self.tls_cert_file_path)
class SelfSignedCertificate(CertificateBackend): class SelfSignedCertificate(CertificateBackend):
"""Create a self signed certificate.""" """Create a self signed certificate."""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Sanity checks.""" """Sanity checks."""
super().__init__(*args, **kwargs) super(SelfSignedCertificate, self).__init__(*args, **kwargs)
if self.config.has_option("general", "tls_key_file"): if self.config.has_option("general", "tls_key_file"):
# Compatibility # Compatibility
return return
@@ -96,7 +65,7 @@ class LetsEncryptCertificate(CertificateBackend):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Update config.""" """Update config."""
super().__init__(*args, **kwargs) super(LetsEncryptCertificate, self).__init__(*args, **kwargs)
self.hostname = self.config.get("general", "hostname") self.hostname = self.config.get("general", "hostname")
self.config.set("general", "tls_cert_file", ( self.config.set("general", "tls_cert_file", (
"/etc/letsencrypt/live/{}/fullchain.pem".format(self.hostname))) "/etc/letsencrypt/live/{}/fullchain.pem".format(self.hostname)))
@@ -146,24 +115,12 @@ class LetsEncryptCertificate(CertificateBackend):
cfg_file = "/etc/letsencrypt/renewal/{}.conf".format(self.hostname) cfg_file = "/etc/letsencrypt/renewal/{}.conf".format(self.hostname)
pattern = "s/authenticator = standalone/authenticator = nginx/" pattern = "s/authenticator = standalone/authenticator = nginx/"
utils.exec_cmd("perl -pi -e '{}' {}".format(pattern, cfg_file)) utils.exec_cmd("perl -pi -e '{}' {}".format(pattern, cfg_file))
with open("/etc/letsencrypt/renewal-hooks/deploy/reload-services.sh", "w") as fp:
fp.write(f"""#!/bin/bash
HOSTNAME=$(basename $RENEWED_LINEAGE)
if [ "$HOSTNAME" = "{self.hostname}" ]
then
systemctl reload dovecot
systemctl reload postfix
fi
""")
def get_backend(config): def get_backend(config):
"""Return the appropriate backend.""" """Return the appropriate backend."""
cert_type = config.get("certificate", "type") if not config.getboolean("certificate", "generate"):
if cert_type == "letsencrypt": return None
if config.get("certificate", "type") == "letsencrypt":
return LetsEncryptCertificate(config) return LetsEncryptCertificate(config)
if cert_type == "manual":
return ManualCertificate(config)
return SelfSignedCertificate(config) return SelfSignedCertificate(config)

View File

@@ -1,6 +1,5 @@
"""Utility functions.""" """Utility functions."""
import configparser
import contextlib import contextlib
import datetime import datetime
import getpass import getpass
@@ -13,9 +12,12 @@ import stat
import string import string
import subprocess import subprocess
import sys import sys
try:
import configparser
except ImportError:
import ConfigParser as configparser
from . import config_dict_template from . import config_dict_template
from .compatibility_matrix import APP_INCOMPATIBILITY
ENV = {} ENV = {}
@@ -31,7 +33,12 @@ class FatalError(Exception):
def user_input(message): def user_input(message):
"""Ask something to the user.""" """Ask something to the user."""
answer = input(message) try:
from builtins import input
except ImportError:
answer = raw_input(message)
else:
answer = input(message)
return answer return answer
@@ -94,17 +101,6 @@ def dist_name():
return dist_info()[0].lower() return dist_info()[0].lower()
def is_dist_debian_based() -> (bool, str):
"""Check if current OS is Debian based or not."""
status, codename = exec_cmd("lsb_release -c -s")
codename = codename.decode().strip().lower()
return codename in [
"bionic", "bookworm", "bullseye", "buster",
"focal", "jammy", "jessie", "sid", "stretch",
"trusty", "wheezy", "xenial"
], codename
def mkdir(path, mode, uid, gid): def mkdir(path, mode, uid, gid):
"""Create a directory.""" """Create a directory."""
if not os.path.exists(path): if not os.path.exists(path):
@@ -176,29 +172,25 @@ def copy_from_template(template, dest, context):
fp.write(ConfigFileTemplate(buf).substitute(context)) fp.write(ConfigFileTemplate(buf).substitute(context))
def check_config_file(dest, def check_config_file(dest, interactive=False, upgrade=False, backup=False, restore=False):
interactive=False,
upgrade=False,
backup=False,
restore=False):
"""Create a new installer config file if needed.""" """Create a new installer config file if needed."""
is_present = True is_present = True
if os.path.exists(dest): if os.path.exists(dest):
return is_present, update_config(dest, False) return is_present, update_config(dest, False)
if upgrade: if upgrade:
error( printcolor(
"You cannot upgrade an existing installation without a " "You cannot upgrade an existing installation without a "
"configuration file.") "configuration file.", RED)
sys.exit(1) sys.exit(1)
elif backup: elif backup:
is_present = False is_present = False
error( printcolor(
"Your configuration file hasn't been found. A new one will be generated. " "Your configuration file hasn't been found. A new one will be generated. "
"Please edit it with correct password for the databases !") "Please edit it with correct password for the databases !", RED)
elif restore: elif restore:
error( printcolor(
"You cannot restore an existing installation without a " "You cannot restore an existing installation without a "
f"configuration file. (file : {dest} has not been found...") f"configuration file. (file : {dest} has not been found...", RED)
sys.exit(1) sys.exit(1)
printcolor( printcolor(
@@ -284,16 +276,6 @@ def random_key(l=16):
return key return key
def check_if_condition(config, entry):
"""Check if the "if" directive is present and computes it"""
section_if = True
for condition in entry:
config_key, value = condition.split("=")
section_name, option = config_key.split(".")
section_if = config.get(section_name, option) == value
return section_if
def validate(value, config_entry): def validate(value, config_entry):
if value is None: if value is None:
return False return False
@@ -314,14 +296,11 @@ def validate(value, config_entry):
return True return True
def get_entry_value(entry: dict, interactive: bool, config: configparser.ConfigParser) -> string: def get_entry_value(entry, interactive):
default_entry = entry["default"] if callable(entry["default"]):
if type(default_entry) is type(list()):
default_value = str(check_if_condition(config, default_entry)).lower()
elif callable(default_entry):
default_value = entry["default"]() default_value = entry["default"]()
else: else:
default_value = default_entry default_value = entry["default"]
user_value = None user_value = None
if entry.get("customizable") and interactive: if entry.get("customizable") and interactive:
while (user_value != '' and not validate(user_value, entry)): while (user_value != '' and not validate(user_value, entry)):
@@ -337,17 +316,6 @@ def get_entry_value(entry: dict, interactive: bool, config: configparser.ConfigP
if entry.get("values") and user_value != "": if entry.get("values") and user_value != "":
user_value = values[int(user_value)] user_value = values[int(user_value)]
non_interactive_values = entry.get("non_interactive_values", [])
if user_value in non_interactive_values:
error(
f"{user_value} cannot be set interactively. "
"Please configure installer.cfg manually by running "
"'python3 run.py --stop-after-configfile-check domain'. "
"Check modoboa-installer README for more information."
)
sys.exit(1)
return user_value if user_value else default_value return user_value if user_value else default_value
@@ -357,22 +325,16 @@ def load_config_template(interactive):
config = configparser.ConfigParser() config = configparser.ConfigParser()
# only ask about options we need, else still generate default # only ask about options we need, else still generate default
for section in tpl_dict: for section in tpl_dict:
interactive_section = interactive
if "if" in section: if "if" in section:
condition = check_if_condition(config, section["if"]) config_key, value = section.get("if").split("=")
interactive_section = condition and interactive section_name, option = config_key.split(".")
interactive_section = (
config.get(section_name, option) == value and interactive)
else:
interactive_section = interactive
config.add_section(section["name"]) config.add_section(section["name"])
for config_entry in section["values"]: for config_entry in section["values"]:
if config_entry.get("if") is not None: value = get_entry_value(config_entry, interactive_section)
interactive_section = (interactive_section and
check_if_condition(
config, config_entry["if"]
)
)
value = get_entry_value(config_entry,
interactive_section,
config)
config.set(section["name"], config_entry["option"], value) config.set(section["name"], config_entry["option"], value)
return config return config
@@ -472,7 +434,7 @@ def validate_backup_path(path: str, silent_mode: bool):
if not path_exists: if not path_exists:
if not silent_mode: if not silent_mode:
create_dir = input( create_dir = input(
f"\"{path}\" doesn't exist, would you like to create it? [y/N]\n" f"\"{path}\" doesn't exist, would you like to create it? [Y/n]\n"
).lower() ).lower()
if silent_mode or (not silent_mode and create_dir.startswith("y")): if silent_mode or (not silent_mode and create_dir.startswith("y")):
@@ -487,7 +449,7 @@ def validate_backup_path(path: str, silent_mode: bool):
if len(os.listdir(path)) != 0: if len(os.listdir(path)) != 0:
if not silent_mode: if not silent_mode:
delete_dir = input( delete_dir = input(
"Warning: backup directory is not empty, it will be purged if you continue... [y/N]\n").lower() "Warning: backup directory is not empty, it will be purged if you continue... [Y/n]\n").lower()
if silent_mode or (not silent_mode and delete_dir.startswith("y")): if silent_mode or (not silent_mode and delete_dir.startswith("y")):
try: try:
@@ -512,32 +474,3 @@ def validate_backup_path(path: str, silent_mode: bool):
mkdir_safe(os.path.join(backup_path, dir), mkdir_safe(os.path.join(backup_path, dir),
stat.S_IRWXU | stat.S_IRWXG, pw[2], pw[3]) stat.S_IRWXU | stat.S_IRWXG, pw[2], pw[3])
return backup_path return backup_path
def create_oauth2_app(app_name: str, client_id: str, client_secret: str, config) -> tuple[str, str]:
"""Create a application for Oauth2 authentication."""
# FIXME: how can we check that application already exists ?
venv_path = config.get("modoboa", "venv_path")
python_path = os.path.join(venv_path, "bin", "python")
instance_path = config.get("modoboa", "instance_path")
script_path = os.path.join(instance_path, "manage.py")
cmd = (
f"{python_path} {script_path} createapplication "
f"--name={app_name} --skip-authorization "
f"--client-id={client_id} --client-secret={client_secret} "
f"confidential client-credentials"
)
exec_cmd(cmd)
return client_id, client_secret
def check_app_compatibility(section, config):
"""Check that the app can be installed in regards to other enabled apps."""
incompatible_app = []
if section in APP_INCOMPATIBILITY.keys():
for app in APP_INCOMPATIBILITY[section]:
if config.getboolean(app, "enabled"):
error(f"{section} cannot be installed if {app} is enabled. "
"Please disable one of them.")
incompatible_app.append(app)
return len(incompatible_app) == 0

139
run.py
View File

@@ -3,12 +3,14 @@
"""An installer for Modoboa.""" """An installer for Modoboa."""
import argparse import argparse
import configparser
import datetime import datetime
import os import os
try:
import configparser
except ImportError:
import ConfigParser as configparser
import sys import sys
from modoboa_installer import checks
from modoboa_installer import compatibility_matrix from modoboa_installer import compatibility_matrix
from modoboa_installer import constants from modoboa_installer import constants
from modoboa_installer import package from modoboa_installer import package
@@ -16,23 +18,69 @@ from modoboa_installer import scripts
from modoboa_installer import ssl from modoboa_installer import ssl
from modoboa_installer import system from modoboa_installer import system
from modoboa_installer import utils from modoboa_installer import utils
from modoboa_installer import disclaimers
PRIMARY_APPS = [ PRIMARY_APPS = [
"amavis",
"fail2ban", "fail2ban",
"modoboa", "modoboa",
"automx",
"radicale", "radicale",
"uwsgi", "uwsgi",
"nginx", "nginx",
"opendkim",
"postfix", "postfix",
"dovecot" "dovecot"
] ]
def installation_disclaimer(args, config):
"""Display installation disclaimer."""
hostname = config.get("general", "hostname")
utils.printcolor(
"Warning:\n"
"Before you start the installation, please make sure the following "
"DNS records exist for domain '{}':\n"
" {} IN A <IP ADDRESS OF YOUR SERVER>\n"
" @ IN MX {}.\n".format(
args.domain,
hostname.replace(".{}".format(args.domain), ""),
hostname
),
utils.CYAN
)
utils.printcolor(
"Your mail server will be installed with the following components:",
utils.BLUE)
def upgrade_disclaimer(config):
"""Display upgrade disclaimer."""
utils.printcolor(
"Your mail server is about to be upgraded and the following components"
" will be impacted:", utils.BLUE
)
def backup_disclaimer():
"""Display backup disclamer. """
utils.printcolor(
"Your mail server will be backed up locally.\n"
" !! You should really transfer the backup somewhere else...\n"
" !! Custom configuration (like for postfix) won't be saved.", utils.BLUE)
def restore_disclaimer():
"""Display restore disclamer. """
utils.printcolor(
"You are about to restore a previous installation of Modoboa.\n"
"If a new version has been released in between, please update your database!",
utils.BLUE)
def backup_system(config, args): def backup_system(config, args):
"""Launch backup procedure.""" """Launch backup procedure."""
disclaimers.backup_disclaimer() backup_disclaimer()
backup_path = None backup_path = None
if args.silent_backup: if args.silent_backup:
if not args.backup_path: if not args.backup_path:
@@ -65,9 +113,6 @@ def backup_system(config, args):
utils.copy_file(args.configfile, backup_path) utils.copy_file(args.configfile, backup_path)
# Backup applications # Backup applications
for app in PRIMARY_APPS: for app in PRIMARY_APPS:
if app == "dovecot" and args.no_mail:
utils.printcolor("Skipping mail backup", utils.BLUE)
continue
scripts.backup(app, config, backup_path) scripts.backup(app, config, backup_path)
@@ -80,11 +125,12 @@ def config_file_update_complete(backup_location):
utils.BLUE) utils.BLUE)
def parser_setup(input_args): def main(input_args):
"""Install process."""
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
versions = ( versions = (
["latest"] + list(compatibility_matrix.COMPATIBILITY_MATRIX.keys()) ["latest"] + list(compatibility_matrix.COMPATIBILITY_MATRIX.keys())
) )
parser.add_argument("--debug", action="store_true", default=False, parser.add_argument("--debug", action="store_true", default=False,
help="Enable debug output") help="Enable debug output")
parser.add_argument("--force", action="store_true", default=False, parser.add_argument("--force", action="store_true", default=False,
@@ -112,31 +158,20 @@ def parser_setup(input_args):
parser.add_argument( parser.add_argument(
"--backup", action="store_true", default=False, "--backup", action="store_true", default=False,
help="Backing up interactively previously installed instance" help="Backing up interactively previously installed instance"
) )
parser.add_argument( parser.add_argument(
"--silent-backup", action="store_true", default=False, "--silent-backup", action="store_true", default=False,
help="For script usage, do not require user interaction " help="For script usage, do not require user interaction "
"backup will be saved at ./modoboa_backup/Backup_M_Y_d_H_M " "backup will be saved at ./modoboa_backup/Backup_M_Y_d_H_M "
"if --backup-path is not provided") "if --backup-path is not provided")
parser.add_argument(
"--no-mail", action="store_true", default=False,
help="Disable mail backup (save space)")
parser.add_argument( parser.add_argument(
"--restore", type=str, metavar="path", "--restore", type=str, metavar="path",
help="Restore a previously backup up modoboa instance on a NEW machine. " help="Restore a previously backup up modoboa instance on a NEW machine. "
"You MUST provide backup directory" "You MUST provide backup directory"
) )
parser.add_argument(
"--skip-checks", action="store_true", default=False,
help="Skip the checks the installer performs initially")
parser.add_argument("domain", type=str, parser.add_argument("domain", type=str,
help="The main domain of your future mail server") help="The main domain of your future mail server")
return parser.parse_args(input_args) args = parser.parse_args(input_args)
def main(input_args):
"""Install process."""
args = parser_setup(input_args)
if args.debug: if args.debug:
utils.ENV["debug"] = True utils.ENV["debug"] = True
@@ -154,12 +189,6 @@ def main(input_args):
utils.success("Welcome to Modoboa installer!\n") utils.success("Welcome to Modoboa installer!\n")
# Checks
if not args.skip_checks:
utils.printcolor("Checking the installer...", utils.BLUE)
checks.handle()
utils.success("Checks complete\n")
is_config_file_available, outdate_config = utils.check_config_file( is_config_file_available, outdate_config = utils.check_config_file(
args.configfile, args.interactive, args.upgrade, args.backup, is_restoring) args.configfile, args.interactive, args.upgrade, args.backup, is_restoring)
@@ -172,11 +201,11 @@ def main(input_args):
if is_config_file_available and outdate_config: if is_config_file_available and outdate_config:
answer = utils.user_input("It seems that your config file is outdated. " answer = utils.user_input("It seems that your config file is outdated. "
"Would you like to update it? (Y/n) ") "Would you like to update it? (Y/n) ")
if not answer or answer.lower().startswith("y"): if answer.lower().startswith("y"):
config_file_update_complete(utils.update_config(args.configfile)) config_file_update_complete(utils.update_config(args.configfile))
if not args.stop_after_configfile_check: if not args.stop_after_configfile_check:
answer = utils.user_input("Would you like to stop to review the updated config? (Y/n)") answer = utils.user_input("Would you like to stop to review the updated config? (Y/n)")
if not answer or answer.lower().startswith("y"): if answer.lower().startswith("y"):
return return
else: else:
utils.error("You might encounter unexpected errors ! " utils.error("You might encounter unexpected errors ! "
@@ -195,36 +224,28 @@ def main(input_args):
config.set("modoboa", "version", args.version) config.set("modoboa", "version", args.version)
config.set("modoboa", "install_beta", str(args.beta)) config.set("modoboa", "install_beta", str(args.beta))
if config.get("antispam", "type") == "amavis":
antispam_apps = ["amavis", "opendkim"]
else:
antispam_apps = ["rspamd"]
if args.backup or args.silent_backup: if args.backup or args.silent_backup:
backup_system(config, args) backup_system(config, args)
return return
# Display disclaimer python 3 linux distribution # Display disclaimer python 3 linux distribution
if args.upgrade: if args.upgrade:
disclaimers.upgrade_disclaimer(config) upgrade_disclaimer(config)
elif args.restore: elif args.restore:
disclaimers.restore_disclaimer() restore_disclaimer()
scripts.restore_prep(args.restore) scripts.restore_prep(args.restore)
else: else:
disclaimers.installation_disclaimer(args, config) installation_disclaimer(args, config)
# Show concerned components # Show concerned components
components = [] components = []
for section in config.sections(): for section in config.sections():
if section in ["general", "antispam", "database", "mysql", "postgres", if section in ["general", "database", "mysql", "postgres",
"certificate", "letsencrypt", "backup"]: "certificate", "letsencrypt"]:
continue continue
if (config.has_option(section, "enabled") and if (config.has_option(section, "enabled") and
not config.getboolean(section, "enabled")): not config.getboolean(section, "enabled")):
continue continue
incompatible_app_detected = not utils.check_app_compatibility(section, config)
if incompatible_app_detected:
sys.exit(0)
components.append(section) components.append(section)
utils.printcolor(" ".join(components), utils.YELLOW) utils.printcolor(" ".join(components), utils.YELLOW)
if not args.force: if not args.force:
@@ -241,40 +262,20 @@ def main(input_args):
ssl_backend = ssl.get_backend(config) ssl_backend = ssl.get_backend(config)
if ssl_backend and not args.upgrade: if ssl_backend and not args.upgrade:
ssl_backend.generate_cert() ssl_backend.generate_cert()
for appname in PRIMARY_APPS + antispam_apps: for appname in PRIMARY_APPS:
scripts.install(appname, config, args.upgrade, args.restore) scripts.install(appname, config, args.upgrade, args.restore)
system.restart_service("cron") system.restart_service("cron")
package.backend.restore_system() package.backend.restore_system()
hostname = config.get("general", "hostname")
if not args.restore: if not args.restore:
utils.success( utils.success(
f"Congratulations! You can enjoy Modoboa at https://{hostname} " "Congratulations! You can enjoy Modoboa at https://{} (admin:password)"
"(admin:password)" .format(config.get("general", "hostname"))
) )
else: else:
utils.success( utils.success(
f"Restore complete! You can enjoy Modoboa at https://{hostname} " "Restore complete! You can enjoy Modoboa at https://{} (same credentials as before)"
"(same credentials as before)" .format(config.get("general", "hostname"))
) )
if config.getboolean("rspamd", "enabled"):
rspamd_password = config.get("rspamd", "password")
utils.success(
f"You can also enjoy rspamd at https://{hostname}/rspamd "
f"(password: {rspamd_password})"
)
utils.success(
"\n"
"Modoboa is a free software maintained by volunteers.\n"
"You like the project and want it to be sustainable?\n"
"Then don't wait anymore and go sponsor it here:\n"
)
utils.printcolor(
"https://github.com/sponsors/modoboa\n",
utils.YELLOW
)
utils.success(
"Thank you for your help :-)\n"
)
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -47,7 +47,7 @@ class ConfigFileTestCase(unittest.TestCase):
def test_interactive_mode(self, mock_user_input): def test_interactive_mode(self, mock_user_input):
"""Check interactive mode.""" """Check interactive mode."""
mock_user_input.side_effect = [ mock_user_input.side_effect = [
"0", "0", "", "", "", "", "", "" "0", "0", "", "", "", "", ""
] ]
with open(os.devnull, "w") as fp: with open(os.devnull, "w") as fp:
sys.stdout = fp sys.stdout = fp
@@ -99,7 +99,7 @@ class ConfigFileTestCase(unittest.TestCase):
def test_interactive_mode_letsencrypt(self, mock_user_input): def test_interactive_mode_letsencrypt(self, mock_user_input):
"""Check interactive mode.""" """Check interactive mode."""
mock_user_input.side_effect = [ mock_user_input.side_effect = [
"0", "0", "1", "admin@example.test", "0", "", "", "", "" "1", "admin@example.test", "0", "", "", "", "", ""
] ]
with open(os.devnull, "w") as fp: with open(os.devnull, "w") as fp:
sys.stdout = fp sys.stdout = fp
@@ -126,13 +126,12 @@ class ConfigFileTestCase(unittest.TestCase):
"example.test"]) "example.test"])
self.assertTrue(os.path.exists(self.cfgfile)) self.assertTrue(os.path.exists(self.cfgfile))
self.assertIn( self.assertIn(
"fail2ban modoboa amavis clamav dovecot nginx " "modoboa automx amavis clamav dovecot nginx razor postfix"
"postfix postwhite spamassassin uwsgi radicale opendkim", " postwhite spamassassin uwsgi",
out.getvalue() out.getvalue()
) )
self.assertNotIn( self.assertNotIn("It seems that your config file is outdated.",
"It seems that your config file is outdated.", out.getvalue()
out.getvalue()
) )
@patch("modoboa_installer.utils.user_input") @patch("modoboa_installer.utils.user_input")

View File

@@ -1 +0,0 @@
03b124501ec1a61eaa3063ac9fb839fdbc64f00c