"""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/20-lmtp.conf", "conf.d/30-dict-server.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", ] 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 )