Compare commits
36 Commits
dovecot-24
...
fix/oauth2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a38a535f8 | ||
|
|
2121cfe267 | ||
|
|
36c8352223 | ||
|
|
01ec9b406f | ||
|
|
f2c7423296 | ||
|
|
9276953f87 | ||
|
|
11f6f646d1 | ||
|
|
46d7c6acca | ||
|
|
0e64b92199 | ||
|
|
1d701353d9 | ||
|
|
3acec87ab5 | ||
|
|
dd9c7457c8 | ||
|
|
73feb967fe | ||
|
|
ad3f9f8cef | ||
|
|
765b56d48a | ||
|
|
a9a8f8888c | ||
|
|
355e8d9b98 | ||
|
|
212fa9b5c1 | ||
|
|
65961209c8 | ||
|
|
5a6f5e7d71 | ||
|
|
eed7603ba1 | ||
|
|
731df935e5 | ||
|
|
6f1e717fc4 | ||
|
|
c7f24218aa | ||
|
|
3d80bc3131 | ||
|
|
67971ee981 | ||
|
|
34bf98452d | ||
|
|
6e9fc2e7c2 | ||
|
|
90d4aac001 | ||
|
|
22cbc2e278 | ||
|
|
2dbec9d2a5 | ||
|
|
9d91a25293 | ||
|
|
246419407a | ||
|
|
fc81c04220 | ||
|
|
ff485f0d25 | ||
|
|
56be1be372 |
@@ -3,11 +3,11 @@
|
||||
|
||||
|workflow| |codecov|
|
||||
|
||||
An installer which deploy a complete mail server based on Modoboa.
|
||||
An installer which deploys a complete mail server based on Modoboa.
|
||||
|
||||
.. warning::
|
||||
|
||||
This tool is still in beta stage, it has been tested on:
|
||||
This tool is still in beta, it has been tested on:
|
||||
|
||||
* Debian 12 and upper
|
||||
* Ubuntu Focal Fossa (20.04) and upper
|
||||
@@ -44,7 +44,6 @@ The following components are installed by the installer:
|
||||
* Postfix
|
||||
* Dovecot
|
||||
* Amavis (with SpamAssassin and ClamAV) or Rspamd
|
||||
* automx (autoconfiguration service)
|
||||
* OpenDKIM
|
||||
* Radicale (CalDAV and CardDAV server)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import random
|
||||
import string
|
||||
import uuid
|
||||
|
||||
from .constants import DEFAULT_BACKUP_DIRECTORY
|
||||
|
||||
@@ -11,6 +12,10 @@ def make_password(length=16):
|
||||
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
|
||||
def is_email(user_input):
|
||||
"""Return True in input is a valid email"""
|
||||
@@ -222,35 +227,6 @@ ConfigDictTemplate = [
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "automx",
|
||||
"values": [
|
||||
{
|
||||
"option": "enabled",
|
||||
"default": "true",
|
||||
},
|
||||
{
|
||||
"option": "user",
|
||||
"default": "automx",
|
||||
},
|
||||
{
|
||||
"option": "config_dir",
|
||||
"default": "/etc",
|
||||
},
|
||||
{
|
||||
"option": "home_dir",
|
||||
"default": "/srv/automx",
|
||||
},
|
||||
{
|
||||
"option": "venv_path",
|
||||
"default": "%(home_dir)s/env",
|
||||
},
|
||||
{
|
||||
"option": "instance_path",
|
||||
"default": "%(home_dir)s/instance",
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "rspamd",
|
||||
"if": ["antispam.enabled=true", "antispam.type=rspamd"],
|
||||
@@ -380,6 +356,10 @@ ConfigDictTemplate = [
|
||||
"option": "move_spam_to_junk",
|
||||
"default": "true",
|
||||
},
|
||||
{
|
||||
"option": "oauth2_client_secret",
|
||||
"default": make_client_secret
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -509,7 +489,11 @@ ConfigDictTemplate = [
|
||||
{
|
||||
"option": "venv_path",
|
||||
"default": "%(home_dir)s/env",
|
||||
}
|
||||
},
|
||||
{
|
||||
"option": "oauth2_client_secret",
|
||||
"default": make_client_secret
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
import os
|
||||
import pwd
|
||||
import stat
|
||||
from typing import Optional
|
||||
|
||||
from . import package
|
||||
from . import system
|
||||
from . import utils
|
||||
|
||||
|
||||
class Database(object):
|
||||
|
||||
class Database:
|
||||
"""Common database backend."""
|
||||
|
||||
default_port = None
|
||||
packages = None
|
||||
service = None
|
||||
default_port: Optional[int] = None
|
||||
packages: Optional[dict[str, list[str]]] = None
|
||||
service: Optional[str] = None
|
||||
|
||||
def __init__(self, config):
|
||||
"""Install if necessary."""
|
||||
@@ -36,7 +36,6 @@ class Database(object):
|
||||
|
||||
|
||||
class PostgreSQL(Database):
|
||||
|
||||
"""Postgres."""
|
||||
|
||||
default_port = 5432
|
||||
@@ -157,7 +156,6 @@ class PostgreSQL(Database):
|
||||
|
||||
|
||||
class MySQL(Database):
|
||||
|
||||
"""MySQL backend."""
|
||||
|
||||
default_port = 3306
|
||||
@@ -178,7 +176,7 @@ class MySQL(Database):
|
||||
if name.startswith("debian"):
|
||||
if version.startswith("8"):
|
||||
self.packages["deb"].append("libmysqlclient-dev")
|
||||
elif version.startswith("11") or version.startswith("12"):
|
||||
elif int(version[:2]) >= 11:
|
||||
self.packages["deb"].append("libmariadb-dev")
|
||||
else:
|
||||
self.packages["deb"].append("libmariadbclient-dev")
|
||||
@@ -188,7 +186,7 @@ class MySQL(Database):
|
||||
self.packages["deb"].append("libmariadb-dev")
|
||||
else:
|
||||
self.packages["deb"].append("libmysqlclient-dev")
|
||||
super(MySQL, self).install_package()
|
||||
super().install_package()
|
||||
queries = []
|
||||
if name.startswith("debian"):
|
||||
if version.startswith("8"):
|
||||
@@ -200,7 +198,7 @@ class MySQL(Database):
|
||||
self.dbpassword)
|
||||
return
|
||||
if (
|
||||
(name.startswith("debian") and (version.startswith("11") or version.startswith("12"))) or
|
||||
(name.startswith("debian") and int(version[:2]) >= 11) or
|
||||
(name.startswith("ubuntu") and int(version[:2]) >= 22)
|
||||
):
|
||||
queries = [
|
||||
|
||||
@@ -52,7 +52,17 @@ class Amavis(base.Installer):
|
||||
packages = super(Amavis, self).get_packages()
|
||||
if package.backend.FORMAT == "deb":
|
||||
db_driver = "pg" if self.db_driver == "pgsql" else self.db_driver
|
||||
return packages + ["libdbd-{}-perl".format(db_driver)]
|
||||
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":
|
||||
db_driver = "Pg"
|
||||
elif self.db_driver == "mysql":
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
"""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)
|
||||
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")
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
from .. import database
|
||||
from .. import package
|
||||
@@ -13,15 +14,15 @@ from .. import utils
|
||||
class Installer:
|
||||
"""Simple installer for one application."""
|
||||
|
||||
appname = None
|
||||
no_daemon = False
|
||||
daemon_name = None
|
||||
packages = {}
|
||||
with_user = False
|
||||
with_db = False
|
||||
config_files = []
|
||||
appname: str
|
||||
no_daemon: bool = False
|
||||
daemon_name: Optional[str] = None
|
||||
packages: dict[str, list[str]] = {}
|
||||
with_user: bool = False
|
||||
with_db: bool = False
|
||||
config_files: list[str] = []
|
||||
|
||||
def __init__(self, config, upgrade: bool, archive_path: str):
|
||||
def __init__(self, config, upgrade: bool, archive_path: str) -> None:
|
||||
"""Get configuration."""
|
||||
self.config = config
|
||||
self.upgrade = upgrade
|
||||
@@ -44,7 +45,7 @@ class Installer:
|
||||
self.dbpasswd = self.config.get(self.appname, "dbpassword")
|
||||
|
||||
@property
|
||||
def modoboa_2_2_or_greater(self):
|
||||
def modoboa_2_2_or_greater(self) -> bool:
|
||||
# Check if modoboa version > 2.2
|
||||
modoboa_version = python.get_package_version(
|
||||
"modoboa",
|
||||
|
||||
@@ -23,10 +23,9 @@ class Dovecot(base.Installer):
|
||||
"dovecot-imapd",
|
||||
"dovecot-lmtpd",
|
||||
"dovecot-managesieved",
|
||||
"dovecot-sieve"
|
||||
"dovecot-sieve",
|
||||
],
|
||||
"rpm": [
|
||||
"dovecot", "dovecot-pigeonhole"]
|
||||
"rpm": ["dovecot", "dovecot-pigeonhole"],
|
||||
}
|
||||
per_version_config_files = {
|
||||
"2.3": [
|
||||
@@ -44,10 +43,10 @@ class Dovecot(base.Installer):
|
||||
"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/30-dict-server.conf",
|
||||
"conf.d/auth-oauth2.conf.ext",
|
||||
]
|
||||
],
|
||||
}
|
||||
with_user = True
|
||||
|
||||
@@ -69,11 +68,12 @@ class Dovecot(base.Installer):
|
||||
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"
|
||||
f"dovecot-sql-master-{self.dbengine}.conf.ext=dovecot-sql-master.conf.ext",
|
||||
]
|
||||
result = []
|
||||
for path in files:
|
||||
@@ -107,7 +107,9 @@ class Dovecot(base.Installer):
|
||||
packages += super().get_packages()
|
||||
backports_codename = getattr(self, "backports_codename", None)
|
||||
if backports_codename:
|
||||
packages = [f"{package}/{backports_codename}-backports" for package in packages]
|
||||
packages = [
|
||||
f"{package}/{backports_codename}-backports" for package in packages
|
||||
]
|
||||
return packages
|
||||
|
||||
def install_packages(self):
|
||||
@@ -118,7 +120,8 @@ class Dovecot(base.Installer):
|
||||
package.backend.enable_backports("bookworm")
|
||||
self.backports_codename = "bookworm"
|
||||
package.backend.preconfigure(
|
||||
"dovecot-core", "create-ssl-cert", "boolean", "false")
|
||||
"dovecot-core", "create-ssl-cert", "boolean", "false"
|
||||
)
|
||||
super().install_packages()
|
||||
|
||||
def get_template_context(self):
|
||||
@@ -127,11 +130,17 @@ class Dovecot(base.Installer):
|
||||
pw_mailbox = pwd.getpwnam(self.mailboxes_owner)
|
||||
dovecot_package = {"deb": "dovecot-core", "rpm": "dovecot"}
|
||||
ssl_protocol_parameter = "ssl_protocols"
|
||||
if package.backend.get_installed_version(dovecot_package[package.backend.FORMAT]) > "2.3":
|
||||
if (
|
||||
package.backend.get_installed_version(
|
||||
dovecot_package[package.backend.FORMAT]
|
||||
)
|
||||
> "2.3"
|
||||
):
|
||||
ssl_protocol_parameter = "ssl_min_protocol"
|
||||
ssl_protocols = "!SSLv2 !SSLv3"
|
||||
if package.backend.get_installed_version("openssl").startswith("1.1") \
|
||||
or package.backend.get_installed_version("openssl").startswith("3"):
|
||||
if package.backend.get_installed_version("openssl").startswith(
|
||||
"1.1"
|
||||
) or package.backend.get_installed_version("openssl").startswith("3"):
|
||||
ssl_protocols = "!SSLv3"
|
||||
if ssl_protocol_parameter == "ssl_min_protocol":
|
||||
ssl_protocols = "TLSv1.2"
|
||||
@@ -145,31 +154,41 @@ class Dovecot(base.Installer):
|
||||
protocols = ""
|
||||
|
||||
oauth2_client_id, oauth2_client_secret = utils.create_oauth2_app(
|
||||
"Dovecot", "dovecot", self.config)
|
||||
"Dovecot",
|
||||
"dovecot",
|
||||
self.config.get("dovecot", "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({
|
||||
"db_driver": self.db_driver,
|
||||
"mailboxes_owner_uid": pw_mailbox[2],
|
||||
"mailboxes_owner_gid": pw_mailbox[3],
|
||||
"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"),
|
||||
})
|
||||
context.update(
|
||||
{
|
||||
"db_driver": self.db_driver,
|
||||
"mailboxes_owner_uid": pw_mailbox[2],
|
||||
"mailboxes_owner_gid": pw_mailbox[3],
|
||||
"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
|
||||
|
||||
def install_config_files(self):
|
||||
@@ -177,10 +196,14 @@ class Dovecot(base.Installer):
|
||||
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
|
||||
)
|
||||
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):
|
||||
@@ -191,14 +214,18 @@ class Dovecot(base.Installer):
|
||||
dbpassword = self.config.get("modoboa", "dbpassword")
|
||||
backend = database.get_backend(self.config)
|
||||
backend.load_sql_file(
|
||||
dbname, dbuser, dbpassword,
|
||||
self.get_file_path("install_modoboa_postgres_trigger.sql")
|
||||
dbname,
|
||||
dbuser,
|
||||
dbpassword,
|
||||
self.get_file_path("install_modoboa_postgres_trigger.sql"),
|
||||
)
|
||||
backend.load_sql_file(
|
||||
dbname, dbuser, dbpassword,
|
||||
self.get_file_path("fix_modoboa_postgres_schema.sql")
|
||||
dbname,
|
||||
dbuser,
|
||||
dbpassword,
|
||||
self.get_file_path("fix_modoboa_postgres_schema.sql"),
|
||||
)
|
||||
for f in glob.glob(f"{self.version}/{self.get_file_path('conf.d')}/*"):
|
||||
for f in glob.glob(f"{self.get_file_path(f'{self.version}/conf.d')}/*"):
|
||||
if os.path.isfile(f):
|
||||
utils.copy_file(f, "{}/conf.d".format(self.config_dir))
|
||||
# Make postlogin script executable
|
||||
@@ -210,9 +237,11 @@ class Dovecot(base.Installer):
|
||||
# See https://github.com/modoboa/modoboa/issues/2157.
|
||||
if self.app_config["move_spam_to_junk"]:
|
||||
# Compile sieve script
|
||||
sieve_file = f"{self.config_dir}/conf.d/custom_after_sieve/spam-to-junk.sieve"
|
||||
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')
|
||||
system.add_user_to_group(self.mailboxes_owner, "dovecot")
|
||||
|
||||
def restart_daemon(self):
|
||||
"""Restart daemon process.
|
||||
@@ -226,7 +255,8 @@ class Dovecot(base.Installer):
|
||||
action = "start" if code else "restart"
|
||||
utils.exec_cmd(
|
||||
"service {} {} > /dev/null 2>&1".format(self.appname, action),
|
||||
capture_output=False)
|
||||
capture_output=False,
|
||||
)
|
||||
system.enable_service(self.get_daemon_name())
|
||||
|
||||
def backup(self, path):
|
||||
@@ -234,8 +264,10 @@ class Dovecot(base.Installer):
|
||||
home_dir = self.config.get("dovecot", "home_dir")
|
||||
utils.printcolor("Backing up mails", utils.MAGENTA)
|
||||
if not os.path.exists(home_dir) or os.path.isfile(home_dir):
|
||||
utils.error("Error backing up emails, provided path "
|
||||
f" ({home_dir}) seems not right...")
|
||||
utils.error(
|
||||
"Error backing up emails, provided path "
|
||||
f" ({home_dir}) seems not right..."
|
||||
)
|
||||
return
|
||||
|
||||
dst = os.path.join(path, "mails/")
|
||||
@@ -257,10 +289,13 @@ class Dovecot(base.Installer):
|
||||
for dirpath, dirnames, filenames in os.walk(home_dir):
|
||||
shutil.chown(dirpath, self.mailboxes_owner, self.mailboxes_owner)
|
||||
for filename in filenames:
|
||||
shutil.chown(os.path.join(dirpath, filename),
|
||||
self.mailboxes_owner, self.mailboxes_owner)
|
||||
shutil.chown(
|
||||
os.path.join(dirpath, filename),
|
||||
self.mailboxes_owner,
|
||||
self.mailboxes_owner,
|
||||
)
|
||||
else:
|
||||
utils.printcolor(
|
||||
"It seems that emails were not backed up, skipping restoration.",
|
||||
utils.MAGENTA
|
||||
utils.MAGENTA,
|
||||
)
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
[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
|
||||
@@ -0,0 +1,90 @@
|
||||
##
|
||||
## 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
|
||||
#}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
##
|
||||
## 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
|
||||
}
|
||||
}
|
||||
@@ -104,21 +104,21 @@ mysql %dbhost {
|
||||
#
|
||||
# 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
|
||||
# %%{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.
|
||||
# difficult to have eg. usernames containing '%%' characters.
|
||||
#
|
||||
# Example:
|
||||
# query = SELECT userid AS user, pw AS password \
|
||||
# FROM users WHERE userid = '%u' AND active = 'Y'
|
||||
# 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}'
|
||||
# FROM users WHERE userid = '%%{user|username}' AND domain = '%%{user|domain}'
|
||||
#}
|
||||
|
||||
passdb sql {
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
upstream automx {
|
||||
server unix:%uwsgi_socket_path fail_timeout=0;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name %hostname;
|
||||
root /srv/automx/instance;
|
||||
root %app_instance_path;
|
||||
|
||||
access_log /var/log/nginx/%{hostname}-access.log;
|
||||
error_log /var/log/nginx/%{hostname}-error.log;
|
||||
|
||||
location /mail/config-v1.1.xml {
|
||||
location ~ ^/(mail/config-v1.1.xml|mobileconfig) {
|
||||
include uwsgi_params;
|
||||
uwsgi_pass automx;
|
||||
uwsgi_param UWSGI_SCRIPT instance.wsgi:application;
|
||||
uwsgi_pass modoboa;
|
||||
}
|
||||
}
|
||||
@@ -102,8 +102,6 @@ file = %{config_dir}/rights
|
||||
|
||||
# Storage backend
|
||||
# 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
|
||||
filesystem_folder = %{home_dir}/collections
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
[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
|
||||
@@ -19,14 +19,15 @@ class Nginx(base.Installer):
|
||||
"rpm": ["nginx"]
|
||||
}
|
||||
|
||||
def get_template_context(self, app):
|
||||
def get_template_context(self):
|
||||
"""Additionnal variables."""
|
||||
context = super().get_template_context()
|
||||
context.update({
|
||||
"app_instance_path": (
|
||||
self.config.get(app, "instance_path")),
|
||||
self.config.get("modoboa", "instance_path")),
|
||||
"uwsgi_socket_path": (
|
||||
Uwsgi(self.config, self.upgrade, self.restore).get_socket_path(app))
|
||||
Uwsgi(self.config, self.upgrade, self.restore).get_socket_path("modoboa")
|
||||
)
|
||||
})
|
||||
return context
|
||||
|
||||
@@ -34,9 +35,10 @@ class Nginx(base.Installer):
|
||||
"""Custom app configuration."""
|
||||
if hostname is None:
|
||||
hostname = self.config.get("general", "hostname")
|
||||
context = self.get_template_context(app)
|
||||
context = self.get_template_context()
|
||||
context.update({"hostname": hostname, "extra_config": extra_config})
|
||||
src = self.get_file_path("{}.conf.tpl".format(app))
|
||||
group = None
|
||||
if package.backend.FORMAT == "deb":
|
||||
dst = os.path.join(
|
||||
self.config_dir, "sites-available", "{}.conf".format(hostname))
|
||||
@@ -46,7 +48,8 @@ class Nginx(base.Installer):
|
||||
if os.path.exists(link):
|
||||
return
|
||||
os.symlink(dst, link)
|
||||
group = self.config.get(app, "user")
|
||||
if self.config.has_section(app):
|
||||
group = self.config.get(app, "user")
|
||||
user = "www-data"
|
||||
else:
|
||||
dst = os.path.join(
|
||||
@@ -54,25 +57,17 @@ class Nginx(base.Installer):
|
||||
utils.copy_from_template(src, dst, context)
|
||||
group = "uwsgi"
|
||||
user = "nginx"
|
||||
system.add_user_to_group(user, group)
|
||||
if user and group:
|
||||
system.add_user_to_group(user, group)
|
||||
|
||||
def post_run(self):
|
||||
"""Additionnal tasks."""
|
||||
extra_modoboa_config = ""
|
||||
if self.config.getboolean("automx", "enabled"):
|
||||
hostname = "autoconfig.{}".format(
|
||||
self.config.get("general", "domain"))
|
||||
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;
|
||||
}
|
||||
"""
|
||||
|
||||
hostname = "autoconfig.{}".format(
|
||||
self.config.get("general", "domain"))
|
||||
self._setup_config("autoconfig", hostname)
|
||||
|
||||
if self.config.get("radicale", "enabled"):
|
||||
extra_modoboa_config += """
|
||||
location /radicale/ {
|
||||
|
||||
@@ -20,8 +20,8 @@ class Postwhite(base.Installer):
|
||||
]
|
||||
no_daemon = True
|
||||
packages = {
|
||||
"deb": ["bind9-host"],
|
||||
"rpm": ["bind-utils"]
|
||||
"deb": ["bind9-host", "unzip"],
|
||||
"rpm": ["bind-utils", "unzip"]
|
||||
}
|
||||
|
||||
def install_from_archive(self, repository, target_dir):
|
||||
|
||||
@@ -36,16 +36,16 @@ class Radicale(base.Installer):
|
||||
"Radicale", "pytz", "radicale-modoboa-auth-oauth2"
|
||||
]
|
||||
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):
|
||||
"""Additional variables."""
|
||||
context = super().get_template_context()
|
||||
oauth2_client_id, oauth2_client_secret = utils.create_oauth2_app(
|
||||
"Radicale", "radicale", self.config)
|
||||
"Radicale",
|
||||
"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}"
|
||||
|
||||
@@ -83,24 +83,9 @@ class Uwsgi(base.Installer):
|
||||
utils.exec_cmd(
|
||||
"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):
|
||||
"""Additionnal tasks."""
|
||||
self._setup_modoboa_config()
|
||||
if self.config.getboolean("automx", "enabled"):
|
||||
self._setup_automx_config()
|
||||
|
||||
def restart_daemon(self):
|
||||
"""Restart daemon process."""
|
||||
|
||||
@@ -13,7 +13,6 @@ import stat
|
||||
import string
|
||||
import subprocess
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
from . import config_dict_template
|
||||
from .compatibility_matrix import APP_INCOMPATIBILITY
|
||||
@@ -515,14 +514,13 @@ def validate_backup_path(path: str, silent_mode: bool):
|
||||
return backup_path
|
||||
|
||||
|
||||
def create_oauth2_app(app_name: str, client_id: str, config) -> tuple[str, str]:
|
||||
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")
|
||||
client_secret = str(uuid.uuid4())
|
||||
cmd = (
|
||||
f"{python_path} {script_path} createapplication "
|
||||
f"--name={app_name} --skip-authorization "
|
||||
|
||||
1
run.py
1
run.py
@@ -22,7 +22,6 @@ from modoboa_installer import disclaimers
|
||||
PRIMARY_APPS = [
|
||||
"fail2ban",
|
||||
"modoboa",
|
||||
"automx",
|
||||
"radicale",
|
||||
"uwsgi",
|
||||
"nginx",
|
||||
|
||||
2
tests.py
2
tests.py
@@ -126,7 +126,7 @@ class ConfigFileTestCase(unittest.TestCase):
|
||||
"example.test"])
|
||||
self.assertTrue(os.path.exists(self.cfgfile))
|
||||
self.assertIn(
|
||||
"fail2ban modoboa automx amavis clamav dovecot nginx "
|
||||
"fail2ban modoboa amavis clamav dovecot nginx "
|
||||
"postfix postwhite spamassassin uwsgi radicale opendkim",
|
||||
out.getvalue()
|
||||
)
|
||||
|
||||
@@ -1 +1 @@
|
||||
506b59cda748708242cadaa831aa84aa320e1cfc
|
||||
36c8352223481a87316367f76cfc8c5772286e7a
|
||||
Reference in New Issue
Block a user