299 lines
11 KiB
Python
299 lines
11 KiB
Python
"""Dovecot related tools."""
|
|
|
|
import glob
|
|
import os
|
|
import pwd
|
|
import shutil
|
|
import stat
|
|
|
|
from .. import database
|
|
from .. import package
|
|
from .. import system
|
|
from .. import utils
|
|
|
|
from . import base
|
|
|
|
|
|
class Dovecot(base.Installer):
|
|
"""Dovecot installer."""
|
|
|
|
appname = "dovecot"
|
|
packages = {
|
|
"deb": [
|
|
"dovecot-imapd",
|
|
"dovecot-lmtpd",
|
|
"dovecot-managesieved",
|
|
"dovecot-sieve",
|
|
],
|
|
"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",
|
|
],
|
|
}
|
|
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):
|
|
"""Setup mailbox user."""
|
|
super().setup_user()
|
|
self.mailboxes_owner = self.app_config["mailboxes_owner"]
|
|
system.create_user(self.mailboxes_owner, self.home_dir)
|
|
|
|
def _get_config_files_for_version(self, version: str) -> list[str]:
|
|
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."""
|
|
_config_files = self._get_config_files_for_version(self.version)
|
|
_config_files.append(
|
|
f"postlogin-{self.dbengine}.sh=/usr/local/bin/postlogin.sh"
|
|
)
|
|
if self.app_config["move_spam_to_junk"]:
|
|
_config_files += [
|
|
"custom_after_sieve/spam-to-junk.sieve=conf.d/custom_after_sieve/spam-to-junk.sieve",
|
|
f"{self.version}/conf.d/90-sieve.conf=conf.d/90-sieve.conf",
|
|
]
|
|
|
|
return _config_files
|
|
|
|
def get_packages(self):
|
|
"""Additional packages."""
|
|
packages = ["dovecot-{}".format(self.db_driver)]
|
|
if package.backend.FORMAT == "deb":
|
|
if "pop3" in self.config.get("dovecot", "extra_protocols"):
|
|
packages += ["dovecot-pop3d"]
|
|
packages += super().get_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):
|
|
"""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(
|
|
"dovecot-core", "create-ssl-cert", "boolean", "false"
|
|
)
|
|
super().install_packages()
|
|
|
|
def get_template_context(self):
|
|
"""Additional variables."""
|
|
context = super().get_template_context()
|
|
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"
|
|
):
|
|
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"):
|
|
ssl_protocols = "!SSLv3"
|
|
if ssl_protocol_parameter == "ssl_min_protocol":
|
|
ssl_protocols = "TLSv1.2"
|
|
if "centos" in utils.dist_name():
|
|
protocols = "protocols = imap lmtp sieve"
|
|
extra_protocols = self.config.get("dovecot", "extra_protocols")
|
|
if extra_protocols:
|
|
protocols += " {}".format(extra_protocols)
|
|
else:
|
|
# Protocols are automatically guessed on debian/ubuntu
|
|
protocols = ""
|
|
|
|
oauth2_client_id, oauth2_client_secret = utils.create_oauth2_app(
|
|
"Dovecot", "dovecot", 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"),
|
|
}
|
|
)
|
|
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):
|
|
"""Additional tasks."""
|
|
if self.version == "2.3" and self.dbengine == "postgres":
|
|
dbname = self.config.get("modoboa", "dbname")
|
|
dbuser = self.config.get("modoboa", "dbuser")
|
|
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"),
|
|
)
|
|
backend.load_sql_file(
|
|
dbname,
|
|
dbuser,
|
|
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')}/*"):
|
|
if os.path.isfile(f):
|
|
utils.copy_file(f, "{}/conf.d".format(self.config_dir))
|
|
# Make postlogin script executable
|
|
utils.exec_cmd("chmod +x /usr/local/bin/postlogin.sh")
|
|
# Only root should have read access to the 10-ssl-keys.try
|
|
# See https://github.com/modoboa/modoboa/issues/2570
|
|
utils.exec_cmd("chmod 600 /etc/dovecot/conf.d/10-ssl-keys.try")
|
|
# Add mailboxes user to dovecot group for modoboa mailbox commands.
|
|
# 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"
|
|
)
|
|
utils.exec_cmd(f"/usr/bin/sievec {sieve_file}")
|
|
system.add_user_to_group(self.mailboxes_owner, "dovecot")
|
|
|
|
def restart_daemon(self):
|
|
"""Restart daemon process.
|
|
|
|
Note: we don't capture output and manually redirect stdout to
|
|
/dev/null since this command may hang depending on the process
|
|
being restarted (dovecot for example)...
|
|
|
|
"""
|
|
code, output = utils.exec_cmd("service dovecot status")
|
|
action = "start" if code else "restart"
|
|
utils.exec_cmd(
|
|
"service {} {} > /dev/null 2>&1".format(self.appname, action),
|
|
capture_output=False,
|
|
)
|
|
system.enable_service(self.get_daemon_name())
|
|
|
|
def backup(self, path):
|
|
"""Backup emails."""
|
|
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..."
|
|
)
|
|
return
|
|
|
|
dst = os.path.join(path, "mails/")
|
|
if os.path.exists(dst):
|
|
shutil.rmtree(dst)
|
|
shutil.copytree(home_dir, dst)
|
|
utils.success("Mail backup complete!")
|
|
|
|
def restore(self):
|
|
"""Restore emails."""
|
|
home_dir = self.config.get("dovecot", "home_dir")
|
|
mail_dir = os.path.join(self.archive_path, "mails/")
|
|
if len(os.listdir(mail_dir)) > 0:
|
|
utils.success("Copying mail backup over dovecot directory.")
|
|
if os.path.exists(home_dir):
|
|
shutil.rmtree(home_dir)
|
|
shutil.copytree(mail_dir, home_dir)
|
|
# Resetting permission for vmail
|
|
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,
|
|
)
|
|
else:
|
|
utils.printcolor(
|
|
"It seems that emails were not backed up, skipping restoration.",
|
|
utils.MAGENTA,
|
|
)
|