diff --git a/dd-apps/docker/nextcloud/Dockerfile b/dd-apps/docker/nextcloud/Dockerfile index aecb664..acdd39b 100644 --- a/dd-apps/docker/nextcloud/Dockerfile +++ b/dd-apps/docker/nextcloud/Dockerfile @@ -69,6 +69,7 @@ COPY supervisord.conf / # Temporary replacement for a real queue RUN echo '*/1 * * * * /nc-queue.sh' >> /etc/crontabs/www-data COPY nc-queue.sh / +COPY nc-mail-update.sh / COPY saml.sh / ENV NEXTCLOUD_UPDATE=1 diff --git a/dd-apps/docker/nextcloud/nc-mail-update.sh b/dd-apps/docker/nextcloud/nc-mail-update.sh new file mode 100755 index 0000000..090d6a5 --- /dev/null +++ b/dd-apps/docker/nextcloud/nc-mail-update.sh @@ -0,0 +1,50 @@ +#!/bin/sh -eu + +OCC="${OCC:-/var/www/html/occ}" +# +# For this user, obtain lines formatted as: EmailAccountId:Email +# +get_mail_accounts() { + "$OCC" mail:account:export "$1" | \ + grep -E '^([^-]|- E-Mail)' | tr -d '\n' | \ + sed -Ee 's!(Account|- E-Mail: )!!g' | tr -d ' ' '\n' || true +} + +# User-specific +user_id="$1" +account_name="$2" +email="$3" +email_password="$4" +# Server settings +inbound_host="$5" +inbound_port="$6" +inbound_ssl_mode="$7" +outbound_host="$8" +outbound_port="$9" +outbound_ssl_mode="${10}" + +existing_mail_accounts="$(get_mail_accounts "$user_id")" + +if [ -n "${existing_mail_accounts:-}" ]; then + # Use the first one, it was likely created by DD + account_id="$(echo "${existing_mail_accounts}" | head -n 1 | cut -d ':' -f 1)" +fi + +if [ -z "${account_id:-}" ]; then + # Create account + "$OCC" mail:account:create \ + "$user_id" "$account_name" "$email" \ + "$inbound_host" "$inbound_port" "$inbound_ssl_mode" \ + "$email" "$email_password" \ + "$outbound_host" "$outbound_port" "$outbound_ssl_mode" \ + "$email" "$email_password" +else + # Update account + "$OCC" mail:account:update \ + --imap-host "$inbound_host" --imap-port "$inbound_port" --imap-ssl-mode "$inbound_ssl_mode" \ + --imap-user "$email" --imap-password "$email_password" \ + --smtp-host "$outbound_host" --smtp-port "$outbound_port" --smtp-ssl-mode "$outbound_ssl_mode" \ + --smtp-user "$email" --smtp-password "$email_password" \ + --name "$account_name" --email "$email" \ + -- "$account_id" +fi diff --git a/dd-apps/docker/nextcloud/nc-queue.sh b/dd-apps/docker/nextcloud/nc-queue.sh index 70e5fc4..6bbc7f7 100755 --- a/dd-apps/docker/nextcloud/nc-queue.sh +++ b/dd-apps/docker/nextcloud/nc-queue.sh @@ -1,5 +1,5 @@ -#/bin/sh +#!/bin/sh find "${NC_MAIL_QUEUE_FOLDER:-/nc-mail-queue}" -name '*.sh' -exec sh -c \ - 'cd /var/www/html && {} && rm {}' \ + 'i="$1"; "$i" && rm "$i"' shell {} \ ';' diff --git a/dd-apps/docker/nextcloud/nc_mail/lib/Command/UpdateAccount.php b/dd-apps/docker/nextcloud/nc_mail/lib/Command/UpdateAccount.php index e6f6324..45cf1f7 100644 --- a/dd-apps/docker/nextcloud/nc_mail/lib/Command/UpdateAccount.php +++ b/dd-apps/docker/nextcloud/nc_mail/lib/Command/UpdateAccount.php @@ -24,6 +24,7 @@ namespace OCA\Mail\Command; use OCA\Mail\Db\MailAccountMapper; use OCP\Security\ICrypto; +use OCP\AppFramework\Db\DoesNotExistException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; @@ -31,8 +32,10 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class UpdateAccount extends Command { - public const ARGUMENT_USER_ID = 'user-id'; + public const ARGUMENT_ACCOUNT_ID = 'account-id'; + public const ARGUMENT_NAME = 'name'; public const ARGUMENT_EMAIL = 'email'; + public const ARGUMENT_AUTH_METHOD = 'auth-method'; public const ARGUMENT_IMAP_HOST = 'imap-host'; public const ARGUMENT_IMAP_PORT = 'imap-port'; public const ARGUMENT_IMAP_SSL_MODE = 'imap-ssl-mode'; @@ -44,6 +47,7 @@ class UpdateAccount extends Command { public const ARGUMENT_SMTP_USER = 'smtp-user'; public const ARGUMENT_SMTP_PASSWORD = 'smtp-password'; + /** @var mapper */ private $mapper; @@ -63,8 +67,10 @@ class UpdateAccount extends Command { protected function configure() { $this->setName('mail:account:update'); $this->setDescription('Update a user\'s IMAP account'); - $this->addArgument(self::ARGUMENT_USER_ID, InputArgument::REQUIRED); - $this->addArgument(self::ARGUMENT_EMAIL, InputArgument::REQUIRED); + $this->addArgument(self::ARGUMENT_ACCOUNT_ID, InputArgument::REQUIRED); + + $this->addOption(self::ARGUMENT_NAME, '', InputOption::VALUE_OPTIONAL); + $this->addOption(self::ARGUMENT_EMAIL, '', InputOption::VALUE_OPTIONAL); $this->addOption(self::ARGUMENT_IMAP_HOST, '', InputOption::VALUE_OPTIONAL); $this->addOption(self::ARGUMENT_IMAP_PORT, '', InputOption::VALUE_OPTIONAL); @@ -77,11 +83,15 @@ class UpdateAccount extends Command { $this->addOption(self::ARGUMENT_SMTP_SSL_MODE, '', InputOption::VALUE_OPTIONAL); $this->addOption(self::ARGUMENT_SMTP_USER, '', InputOption::VALUE_OPTIONAL); $this->addOption(self::ARGUMENT_SMTP_PASSWORD, '', InputOption::VALUE_OPTIONAL); + + $this->addOption(self::ARGUMENT_AUTH_METHOD, '', InputOption::VALUE_OPTIONAL); } protected function execute(InputInterface $input, OutputInterface $output): int { - $userId = $input->getArgument(self::ARGUMENT_USER_ID); - $email = $input->getArgument(self::ARGUMENT_EMAIL); + $accountId = (int)$input->getArgument(self::ARGUMENT_ACCOUNT_ID); + + $name = $input->getOption(self::ARGUMENT_NAME); + $email = $input->getOption(self::ARGUMENT_EMAIL); $imapHost = $input->getOption(self::ARGUMENT_IMAP_HOST); $imapPort = $input->getOption(self::ARGUMENT_IMAP_PORT); @@ -94,61 +104,76 @@ class UpdateAccount extends Command { $smtpSslMode = $input->getOption(self::ARGUMENT_SMTP_SSL_MODE); $smtpUser = $input->getOption(self::ARGUMENT_SMTP_USER); $smtpPassword = $input->getOption(self::ARGUMENT_SMTP_PASSWORD); + $authMethod = $input->getOption(self::ARGUMENT_AUTH_METHOD); - $mailAccount = $this->mapper->findByUserIdAndEmail($userId, $email); - - if ($mailAccount) { - //INBOUND - if ($input->getOption(self::ARGUMENT_IMAP_HOST)) { - $mailAccount->setInboundHost($imapHost); - } - - if ($input->getOption(self::ARGUMENT_IMAP_PORT)) { - $mailAccount->setInboundPort((int) $imapPort); - } - - if ($input->getOption(self::ARGUMENT_IMAP_SSL_MODE)) { - $mailAccount->setInboundSslMode($imapSslMode); - } - - if ($input->getOption(self::ARGUMENT_IMAP_PASSWORD)) { - $mailAccount->setInboundPassword($this->crypto->encrypt($imapPassword)); - } - - if ($input->getOption(self::ARGUMENT_SMTP_USER)) { - $mailAccount->setInboundUser($imapUser); - } - - // OUTBOUND - - if ($input->getOption(self::ARGUMENT_SMTP_HOST)) { - $mailAccount->setOutboundHost($smtpHost); - } - - if ($input->getOption(self::ARGUMENT_SMTP_PORT)) { - $mailAccount->setOutboundPort((int) $smtpPort); - } - - if ($input->getOption(self::ARGUMENT_SMTP_SSL_MODE)) { - $mailAccount->setOutboundSslMode($smtpSslMode); - } - - if ($input->getOption(self::ARGUMENT_SMTP_PASSWORD)) { - $mailAccount->setOutboundPassword($this->crypto->encrypt($smtpPassword)); - } - - if ($input->getOption(self::ARGUMENT_SMTP_USER)) { - $mailAccount->setOutboundUser($smtpUser); - } - - $this->mapper->save($mailAccount); - - $output->writeln("Account $email for user $userId succesfully updated "); + try { + $mailAccount = $this->mapper->findById($accountId); + } catch (DoesNotExistException $e) { + $output->writeln("No Email Account found with ID $accountId "); return 1; - } else { - $output->writeln("No Email Account $email found for user $userId "); } + $output->writeLn("Found account with email: " . $mailAccount->getEmail() . ""); + + //ACCOUNT OPTIONS + if ($input->getOption(self::ARGUMENT_NAME)) { + $mailAccount->setName($name); + } + if ($input->getOption(self::ARGUMENT_EMAIL)) { + $mailAccount->setEmail($email); + } + + //AUTH METHOD + if ($input->getOption(self::ARGUMENT_AUTH_METHOD)) { + $mailAccount->setAuthMethod($authMethod); + } + + //INBOUND + if ($input->getOption(self::ARGUMENT_IMAP_HOST)) { + $mailAccount->setInboundHost($imapHost); + } + + if ($input->getOption(self::ARGUMENT_IMAP_PORT)) { + $mailAccount->setInboundPort((int) $imapPort); + } + + if ($input->getOption(self::ARGUMENT_IMAP_SSL_MODE)) { + $mailAccount->setInboundSslMode($imapSslMode); + } + + if ($input->getOption(self::ARGUMENT_IMAP_PASSWORD)) { + $mailAccount->setInboundPassword($this->crypto->encrypt($imapPassword)); + } + + if ($input->getOption(self::ARGUMENT_SMTP_USER)) { + $mailAccount->setInboundUser($imapUser); + } + + // OUTBOUND + + if ($input->getOption(self::ARGUMENT_SMTP_HOST)) { + $mailAccount->setOutboundHost($smtpHost); + } + + if ($input->getOption(self::ARGUMENT_SMTP_PORT)) { + $mailAccount->setOutboundPort((int) $smtpPort); + } + + if ($input->getOption(self::ARGUMENT_SMTP_SSL_MODE)) { + $mailAccount->setOutboundSslMode($smtpSslMode); + } + + if ($input->getOption(self::ARGUMENT_SMTP_PASSWORD)) { + $mailAccount->setOutboundPassword($this->crypto->encrypt($smtpPassword)); + } + + if ($input->getOption(self::ARGUMENT_SMTP_USER)) { + $mailAccount->setOutboundUser($smtpUser); + } + + $this->mapper->save($mailAccount); + + $output->writeln("Account " . $mailAccount->getEmail() . " with ID $accountId succesfully updated "); return 0; } } diff --git a/dd-sso/admin/src/admin/lib/admin.py b/dd-sso/admin/src/admin/lib/admin.py index c495b9d..a7c6fee 100644 --- a/dd-sso/admin/src/admin/lib/admin.py +++ b/dd-sso/admin/src/admin/lib/admin.py @@ -139,23 +139,32 @@ class Admin: res = res and tp.delete_user(user_id) return res - def _nextcloud_mail_set_cmd(self, user : DDUser, kw : Dict) -> Tuple[str, str]: - account_name = 'DD' # Treating this as a constant - update_cmd = f"""mail:account:update \ - --imap-host '{ kw['inbound_host'] }' --imap-port '{ kw['inbound_port'] }' --imap-ssl-mode '{ kw['inbound_ssl_mode'] }' \\ - --imap-user '{ user['email'] }' --imap-password '{ user['password'] }' \\ - --smtp-host '{ kw['outbound_host'] }' --smtp-port '{ kw['outbound_port'] }' --smtp-ssl-mode '{ kw['outbound_ssl_mode'] }' \\ - --smtp-user '{ user['email'] }' --smtp-password '{ user['password'] }' \\ - -- '{ user['user_id'] }' '{ user['email']}'""" - create_cmd = f"""mail:account:create '{ user['user_id'] }' '{ account_name }' '{ user['email'] }' \\ - '{ kw['inbound_host'] }' '{ kw['inbound_port'] }' '{ kw['inbound_ssl_mode'] }' \\ - '{ user['email'] }' '{ user['password'] }' \\ - '{ kw['outbound_host'] }' '{ kw['outbound_port'] }' '{ kw['outbound_ssl_mode'] }' \\ - '{ user['email'] }' '{ user['password'] }'""" - return (update_cmd, create_cmd) + def _nextcloud_mail_set_cmd(self, user: DDUser, kw: Dict) -> str: + from shlex import quote as q - def _nextcloud_mail_set_sh(self, users : List[DDUser], extra_data : Dict) -> str: - cmds = '\n'.join((f"./occ {u} || ./occ {c}" for u, c in (self._nextcloud_mail_set_cmd(u, extra_data) for u in users))) + account_name = user.get("name", "DD User") + + nc_mail_update = "/nc-mail-update.sh" + # As defined in nc-mail-update.sh + unquoted_args = [ + # User-specific + user["user_id"], + account_name, + user["email"], + user["password"], + # Server settings + kw.get("inbound_host", ""), + kw.get("inbound_port", ""), + kw.get("inbound_ssl_mode", ""), + kw.get("outbound_host", ""), + kw.get("outbound_port", ""), + kw.get("outbound_ssl_mode", ""), + ] + args = [q(str(a) if a else '') for a in unquoted_args] + return " ".join([nc_mail_update] + args) + + def _nextcloud_mail_set_sh(self, users: List[DDUser], extra_data: Dict) -> str: + cmds = "\n".join((self._nextcloud_mail_set_cmd(u, extra_data) for u in users)) return f"""#!/bin/sh -eu {cmds} """ @@ -170,10 +179,15 @@ class Admin: tmp = d.joinpath(fn + '.tmp') # Create executable file tmp.touch(mode=0o750) - # Write script - tmp.write_text(self._nextcloud_mail_set_sh(users, extra_data)) - # Put it in-place - tmp.rename(sh) + try: + # Write script + tmp.write_text(self._nextcloud_mail_set_sh(users, extra_data)) + # Put it in-place + tmp.rename(sh) + except: + log.error(traceback.format_exc()) + log.error("Issue writing mail changes...") + raise return {} def check_connections(self, app : "AdminFlaskApp") -> None: