diff --git a/conf/dovecot/dovecot.conf b/conf/dovecot/dovecot.conf index e614c3796..152f4c01c 100644 --- a/conf/dovecot/dovecot.conf +++ b/conf/dovecot/dovecot.conf @@ -38,14 +38,26 @@ ssl_prefer_server_ciphers = no ############################################################################### +# Regular Yunohost accounts passdb { args = /etc/dovecot/dovecot-ldap.conf driver = ldap } +# Internally, allow authentication from apps system user who have "enable_email = true" +passdb { + driver = passwd-file + args = /etc/dovecot/app-senders-passwd +} + userdb { - args = /etc/dovecot/dovecot-ldap.conf driver = ldap + args = /etc/dovecot/dovecot-ldap.conf +} + +userdb { + driver = passwd-file + args = /etc/dovecot/app-senders-passwd } protocol imap { diff --git a/conf/postfix/main.cf b/conf/postfix/main.cf index 19b40aefb..e30ca0874 100644 --- a/conf/postfix/main.cf +++ b/conf/postfix/main.cf @@ -107,7 +107,12 @@ virtual_alias_domains = virtual_minimum_uid = 100 virtual_uid_maps = static:vmail virtual_gid_maps = static:mail -smtpd_sender_login_maps= ldap:/etc/postfix/ldap-accounts.cf +smtpd_sender_login_maps= + # Regular Yunohost accounts + ldap:/etc/postfix/ldap-accounts.cf, + # Extra maps for app system users who need to send emails + hash:/etc/postfix/app_senders_login_maps + # Dovecot LDA virtual_transport = dovecot diff --git a/hooks/conf_regen/19-postfix b/hooks/conf_regen/19-postfix index 3a2aead5d..d6ddcb5ee 100755 --- a/hooks/conf_regen/19-postfix +++ b/hooks/conf_regen/19-postfix @@ -80,6 +80,8 @@ do_post_regen() { postmap -F hash:/etc/postfix/sni + python3 -c 'from yunohost.app import regen_mail_app_user_config_for_dovecot_and_postfix as r; r(only="postfix")' + [[ -z "$regen_conf_files" ]] \ || { systemctl restart postfix && systemctl restart postsrsd; } diff --git a/hooks/conf_regen/25-dovecot b/hooks/conf_regen/25-dovecot index 49ff0c9ba..54b4e5d37 100755 --- a/hooks/conf_regen/25-dovecot +++ b/hooks/conf_regen/25-dovecot @@ -53,6 +53,8 @@ do_post_regen() { chown root:mail /var/mail chmod 1775 /var/mail + python3 -c 'from yunohost.app import regen_mail_app_user_config_for_dovecot_and_postfix as r; r(only="dovecot")' + [ -z "$regen_conf_files" ] && exit 0 # compile sieve script diff --git a/src/app.py b/src/app.py index 6cc21f404..5711a7adb 100644 --- a/src/app.py +++ b/src/app.py @@ -1636,6 +1636,9 @@ def app_setting(app, key, value=None, delete=False): if delete: if key in app_settings: del app_settings[key] + else: + # Don't call _set_app_settings to avoid unecessary writes... + return # SET else: @@ -3234,3 +3237,47 @@ def _ask_confirmation( if not answer: raise YunohostError("aborting") + + +def regen_mail_app_user_config_for_dovecot_and_postfix(only=None): + + dovecot = True if only in [None, "dovecot"] else False + postfix = True if only in [None, "postfix"] else False + + from yunohost.user import _hash_user_password + + postfix_map = [] + dovecot_passwd = [] + for app in _installed_apps(): + + settings = _get_app_settings(app) + + if "domain" not in settings or "mail_pwd" not in settings: + continue + + if dovecot: + hashed_password = _hash_user_password(settings["mail_pwd"]) + dovecot_passwd.append(f"{app}:{hashed_password}::::::allow_nets=127.0.0.1/24") + if postfix: + mail_user = settings.get("mail_user", app) + mail_domain = settings.get("mail_domain", settings["domain"]) + postfix_map.append(f"{mail_user}@{mail_domain} {app}") + + if dovecot: + app_senders_passwd = "/etc/dovecot/app-senders-passwd" + content = "# This file is regenerated automatically.\n# Please DO NOT edit manually ... changes will be overwritten!" + content += '\n' + '\n'.join(dovecot_passwd) + write_to_file(app_senders_passwd, content) + chmod(app_senders_passwd, 0o440) + chown(app_senders_passwd, "root", "dovecot") + + if postfix: + app_senders_map = "/etc/postfix/app_senders_login_maps" + content = "# This file is regenerated automatically.\n# Please DO NOT edit manually ... changes will be overwritten!" + content += '\n' + '\n'.join(postfix_map) + write_to_file(app_senders_map, content) + chmod(app_senders_map, 0o440) + chown(app_senders_map, "postfix", "root") + os.system(f"postmap {app_senders_map} 2>/dev/null") + chmod(app_senders_map + ".db", 0o640) + chown(app_senders_map + ".db", "postfix", "root") diff --git a/src/utils/resources.py b/src/utils/resources.py index 8d33c3bac..be7f9fba5 100644 --- a/src/utils/resources.py +++ b/src/utils/resources.py @@ -25,6 +25,7 @@ import subprocess from typing import Dict, Any, List, Union from moulinette import m18n +from moulinette.utils.text import random_ascii from moulinette.utils.process import check_output from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import mkdir, chown, chmod, write_to_file @@ -679,6 +680,7 @@ class SystemuserAppResource(AppResource): ##### Properties - `allow_ssh`: (default: False) Adds the user to the ssh.app group, allowing SSH connection via this user - `allow_sftp`: (default: False) Adds the user to the sftp.app group, allowing SFTP connection via this user + - `allow_email`: (default: False) Enable authentication on the mail stack for the system user and send mail using `__APP__@__DOMAIN__`. A `mail_pwd` setting is automatically defined (similar to `db_pwd` for databases). You can then configure the app to use `__APP__` and `__MAIL_PWD__` as SMTP credentials (with host 127.0.0.1). You can also tweak the user-part of the domain-part of the email used by manually defining a custom setting `mail_user` or `mail_domain` - `home`: (default: `/var/www/__APP__`) Defines the home property for this user. NB: unfortunately you can't simply use `__INSTALL_DIR__` or `__DATA_DIR__` for now ##### Provision/Update @@ -702,6 +704,7 @@ class SystemuserAppResource(AppResource): default_properties: Dict[str, Any] = { "allow_ssh": False, "allow_sftp": False, + "allow_email": False, "home": "/var/www/__APP__", } @@ -709,9 +712,13 @@ class SystemuserAppResource(AppResource): allow_ssh: bool = False allow_sftp: bool = False + allow_email: bool = False home: str = "" def provision_or_update(self, context: Dict = {}): + + from yunohost.app import regen_mail_app_user_config_for_dovecot_and_postfix + # FIXME : validate that no yunohost user exists with that name? # and/or that no system user exists during install ? @@ -756,7 +763,25 @@ class SystemuserAppResource(AppResource): f"sed -i 's@{raw_user_line_in_etc_passwd}@{new_raw_user_line_in_etc_passwd}@g' /etc/passwd" ) + # Update mail-related stuff + if self.allow_email: + mail_pwd = self.get_setting("mail_pwd") + if not mail_pwd: + mail_pwd = random_ascii(24) + self.set_setting("mail_pwd", mail_pwd) + + regen_mail_app_user_config_for_dovecot_and_postfix() + else: + self.delete_setting("mail_pwd") + if os.system(f"grep --quiet ' {self.app}$' /etc/postfix/app_senders_login_maps") == 0 \ + or os.system(f"grep --quiet '^{self.app}:' /etc/dovecot/app-senders-passwd") == 0: + regen_mail_app_user_config_for_dovecot_and_postfix() + + def deprovision(self, context: Dict = {}): + + from yunohost.app import regen_mail_app_user_config_for_dovecot_and_postfix + if os.system(f"getent passwd {self.app} >/dev/null 2>/dev/null") == 0: os.system(f"deluser {self.app} >/dev/null") if os.system(f"getent passwd {self.app} >/dev/null 2>/dev/null") == 0: @@ -771,6 +796,11 @@ class SystemuserAppResource(AppResource): f"Failed to delete system user for {self.app}", raw_msg=True ) + self.delete_setting("mail_pwd") + if os.system(f"grep --quiet ' {self.app}$' /etc/postfix/app_senders_login_maps") == 0 \ + or os.system(f"grep --quiet '^{self.app}:' /etc/dovecot/app-senders-passwd") == 0: + regen_mail_app_user_config_for_dovecot_and_postfix() + # FIXME : better logging and error handling, add stdout/stderr from the deluser/delgroup commands... @@ -1315,8 +1345,6 @@ class DatabaseAppResource(AppResource): self.set_setting("db_pwd", db_pwd) if not db_pwd: - from moulinette.utils.text import random_ascii - db_pwd = random_ascii(24) self.set_setting("db_pwd", db_pwd)