From c4f203336803a8f3e65be08728c7897a928bbb2b Mon Sep 17 00:00:00 2001
From: the-djmaze <3752035+the-djmaze@users.noreply.github.com>
Date: Tue, 3 Nov 2026 00:58:35 +0100
Subject: [PATCH] More Nextcloud changes #2055
---
.../nextcloud/snappymail/app/include.php | 4 +-
.../nextcloud/snappymail/appinfo/info.xml | 2 +
.../snappymail/lib/AppInfo/Application.php | 2 +-
.../lib/Controller/FetchController.php | 9 +-
.../lib/Controller/PageController.php | 127 +++++++++++++++++-
.../snappymail/lib/Settings/AdminSettings.php | 3 +-
.../lib/Settings/PersonalSettings.php | 4 +-
.../snappymail/lib/Settings/Section.php | 38 ++++++
.../snappymail/lib/Util/RainLoop.php | 5 +-
.../snappymail/lib/Util/SnappyMailHelper.php | 64 +++++++--
10 files changed, 228 insertions(+), 30 deletions(-)
create mode 100644 integrations/nextcloud/snappymail/lib/Settings/Section.php
diff --git a/integrations/nextcloud/snappymail/app/include.php b/integrations/nextcloud/snappymail/app/include.php
index 7aa451923..fcb485a78 100644
--- a/integrations/nextcloud/snappymail/app/include.php
+++ b/integrations/nextcloud/snappymail/app/include.php
@@ -28,13 +28,15 @@
* Custom 'data' folder path
*/
if (class_exists('OC')) {
+ define('APP_DATA_FOLDER_PATH', \OCA\SnappyMail\Util\SnappyMailHelper::getAppDataFolder());
+/*
define('APP_DATA_FOLDER_PATH', \rtrim(\trim(
-// \OCA\SnappyMail\Util\SnappyMailHelper::getSystemConfigValue('datadirectory', '')
\method_exists(\OC::$server, 'getSystemConfig')
? \OC::$server->getSystemConfig()->getValue('datadirectory', '')
// Nextcloud 34+
: \OC::$server->get(\OCP\IConfig::class)->getSystemValue('datadirectory', '')
), '\\/') . '/appdata_snappymail/');
+*/
} else {
http_response_code(400);
header($_SERVER['SERVER_PROTOCOL'].' 400 Bad Request', true, 400);
diff --git a/integrations/nextcloud/snappymail/appinfo/info.xml b/integrations/nextcloud/snappymail/appinfo/info.xml
index 5f25f2e54..48f24eb42 100644
--- a/integrations/nextcloud/snappymail/appinfo/info.xml
+++ b/integrations/nextcloud/snappymail/appinfo/info.xml
@@ -68,7 +68,9 @@ There, click on the link to go to the SnappyMail admin panel.
+ OCA\SnappyMail\Settings\Section
OCA\SnappyMail\Settings\AdminSettings
+ OCA\SnappyMail\Settings\Section
OCA\SnappyMail\Settings\PersonalSettings
diff --git a/integrations/nextcloud/snappymail/lib/AppInfo/Application.php b/integrations/nextcloud/snappymail/lib/AppInfo/Application.php
index ce417562a..ceb584c96 100644
--- a/integrations/nextcloud/snappymail/lib/AppInfo/Application.php
+++ b/integrations/nextcloud/snappymail/lib/AppInfo/Application.php
@@ -75,7 +75,7 @@ class Application extends App implements IBootstrap
{
// SnappyMailHelper::$server = $context->getServerContainer();
- if (!\is_dir(\rtrim(\trim(SnappyMailHelper::getSystemConfigValue('datadirectory', '')), '\\/') . '/appdata_snappymail')) {
+ if (!\is_dir(SnappyMailHelper::getAppDataFolder())) {
return;
}
diff --git a/integrations/nextcloud/snappymail/lib/Controller/FetchController.php b/integrations/nextcloud/snappymail/lib/Controller/FetchController.php
index 66d3724e9..4c1a73b78 100644
--- a/integrations/nextcloud/snappymail/lib/Controller/FetchController.php
+++ b/integrations/nextcloud/snappymail/lib/Controller/FetchController.php
@@ -10,17 +10,20 @@ use OCP\AppFramework\Http\JSONResponse;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
+//use OCP\IUserSession;
class FetchController extends Controller
{
private IConfig $config;
private IL10N $l10n;
+// private IUserSession $userSession;
public function __construct(string $appName, IRequest $request, \Psr\Container\ContainerInterface $container)
{
parent::__construct($appName, $request);
$this->config = $container->query(IConfig::class);
$this->l10n = $container->query(IL10N::class);
+// $this->userSession = $container->query(IUserSession::class);
}
public function upgrade(): JSONResponse
@@ -34,7 +37,7 @@ class FetchController extends Controller
'Message' => $this->l10n->t('Upgraded successfully')
]);
}
- } catch (Exception $e) {
+ } catch (\Throwable $e) {
$error .= ': ' . $e->getMessage();
}
return new JSONResponse([
@@ -91,7 +94,7 @@ class FetchController extends Controller
'status' => 'success',
'Message' => $this->l10n->t('Saved successfully')
]);
- } catch (Exception $e) {
+ } catch (\Throwable $e) {
return new JSONResponse([
'status' => 'error',
'Message' => $e->getMessage()
@@ -136,7 +139,7 @@ class FetchController extends Controller
'Message' => $this->l10n->t('Saved successfully'),
'Email' => $sEmail
]);
- } catch (Exception $e) {
+ } catch (\Throwable $e) {
// Logout as the credentials might have changed, as exception could be in one attribute
// TODO: Handle both exceptions separately?
SnappyMailHelper::loadApp();
diff --git a/integrations/nextcloud/snappymail/lib/Controller/PageController.php b/integrations/nextcloud/snappymail/lib/Controller/PageController.php
index e78b3d5fc..058cb45ee 100644
--- a/integrations/nextcloud/snappymail/lib/Controller/PageController.php
+++ b/integrations/nextcloud/snappymail/lib/Controller/PageController.php
@@ -33,13 +33,18 @@ class PageController extends Controller
$bAdmin = false;
if (!empty($_SERVER['QUERY_STRING'])) {
SnappyMailHelper::loadApp();
- $bAdmin = \RainLoop\Api::Config()->Get('security', 'admin_panel_key', 'admin') == $_SERVER['QUERY_STRING'];
+ $bAdmin = $this->getAdminPanelKey() == $_SERVER['QUERY_STRING'];
+ if ($this->isAdminServiceRequest()) {
+ $this->ensureNextcloudAdminSession();
+ }
if (!$bAdmin) {
return SnappyMailHelper::startApp(true);
}
}
- if (!$bAdmin && SnappyMailHelper::getAppConfigValue('snappymail-no-embed')) {
+ if (!$bAdmin && $this->config->getAppValue('snappymail', 'snappymail-no-embed')) {
+// if (!$bAdmin && SnappyMailHelper::getAppConfigValue('snappymail-no-embed')) {
+// SnappyMailHelper::getNavigationManager()->setActiveEntry('snappymail');
$this->navigationManager->setActiveEntry('snappymail');
\OCP\Util::addScript('snappymail', 'snappymail');
\OCP\Util::addStyle('snappymail', 'style');
@@ -51,10 +56,119 @@ class PageController extends Controller
$csp = new ContentSecurityPolicy();
$csp->addAllowedFrameDomain("'self'");
// $csp->addAllowedFrameAncestorDomain("'self'");
- $response->setContentSecurityPolicy($csp);
+ $response->setContentSecurityPolicy($csp->getPolicy());
return $response;
}
+ return $this->renderEmbeddedApp($bAdmin);
+ }
+
+ /**
+ * Integrated administration entry point.
+ *
+ * Unlike the legacy ?admin URL this route is protected by Nextcloud's
+ * default admin middleware. No @NoAdminRequired annotation must be added.
+ *
+ * @NoCSRFRequired
+ */
+ public function admin()
+ {
+ $this->ensureNextcloudAdminSession();
+
+ if (!empty($_SERVER['QUERY_STRING'])) {
+ return $this->handleIntegratedAdminRequest();
+ }
+
+ return $this->renderEmbeddedApp(true, true);
+ }
+
+ /**
+ * SnappyMail uses its own X-SM-Token for the legacy admin JSON API. The
+ * Nextcloud CSRF middleware cannot validate that token, therefore this
+ * endpoint opts out of the Nextcloud request token check. It remains admin
+ * protected and SnappyMail validates X-SM-Token for every modifying call.
+ *
+ * @NoCSRFRequired
+ */
+ public function adminPost()
+ {
+ $this->ensureNextcloudAdminSession();
+ return $this->handleIntegratedAdminRequest();
+ }
+
+ private function ensureNextcloudAdminSession(): void
+ {
+ $user = \OC::$server->get(\OCP\IUserSession::class)->getUser();
+ if (!$user || !\OC_User::isAdminUser($user->getUID())) {
+ throw new \RuntimeException('SnappyMail administration requires a Nextcloud administrator session.');
+ }
+
+ SnappyMailHelper::loadApp();
+ $config = \RainLoop\Api::Config();
+ if (!$config->Get('security', 'allow_admin_panel', true)) {
+ throw new \RuntimeException('SnappyMail administration is disabled.');
+ }
+
+ $actions = \RainLoop\Api::Actions();
+ if (!$actions->IsAdminLoggined(false)) {
+ $random = \MailSo\Base\Utils::Sha1Rand();
+ if (!$actions->Cacher(null, true)->Set(\RainLoop\KeyPathHelper::SessionAdminKey($random), \time())) {
+ throw new \RuntimeException('Could not create the SnappyMail administrator session.');
+ }
+
+ $token = \RainLoop\Utils::EncodeKeyValuesQ(['token', $random]);
+ if (!$token) {
+ throw new \RuntimeException('Could not encode the SnappyMail administrator session.');
+ }
+ \SnappyMail\Cookies::set('smadmin', $token);
+ }
+ }
+
+ private function handleIntegratedAdminRequest(): void
+ {
+ $adminKey = $this->getAdminPanelKey();
+ $query = \trim((string) ($_SERVER['QUERY_STRING'] ?? ''), " \t\n\r\0\x0B/");
+ // The SnappyMail admin client already includes its configured admin
+ // path in JSON and asset requests. Prefix only requests that do not
+ // contain it yet; otherwise "admin/admin/Json" is produced and domain
+ // as well as extension requests are routed to a non-existing service.
+ if ($query !== $adminKey && 0 !== \strpos($query, $adminKey . '/')) {
+ $query = $adminKey . ($query ? '/' . $query : '');
+ }
+ $_SERVER['QUERY_STRING'] = $query;
+ $_GET[$adminKey] = '1';
+
+ SnappyMailHelper::startApp(true);
+ }
+
+ private function getAdminPanelKey(): string
+ {
+ $key = (string) \RainLoop\Api::Config()->Get('admin_panel', 'key', '');
+ return '' !== $key ? $key : 'admin';
+ }
+
+ private function isAdminServiceRequest(): bool
+ {
+ $query = \trim((string) ($_SERVER['QUERY_STRING'] ?? ''), " \t\n\r\0\x0B/");
+ $ampersand = \strpos($query, '&');
+ if (false !== $ampersand) {
+ $query = \substr($query, 0, $ampersand);
+ }
+ $adminKey = $this->getAdminPanelKey();
+ return $query === $adminKey || 0 === \strpos($query, $adminKey . '/');
+ }
+
+ private function protectLegacyAdminRequest(): void
+ {
+ SnappyMailHelper::loadApp();
+ if ($this->isAdminServiceRequest()) {
+ $this->ensureNextcloudAdminSession();
+ }
+ }
+
+ private function renderEmbeddedApp(bool $bAdmin, bool $blank = false)
+ {
+// SnappyMailHelper::getNavigationManager()->setActiveEntry('snappymail');
$this->navigationManager->setActiveEntry('snappymail');
\OCP\Util::addStyle('snappymail', 'embed');
@@ -93,9 +207,9 @@ class PageController extends Controller
\OCP\Util::addHeader('link', ['type'=>'text/css','rel'=>'stylesheet','href'=>\RainLoop\Utils::WebStaticPath('css/'.($bAdmin?'admin':'app').$sAppCssMin.'.css')], '');
// \OCP\Util::addHeader('style', ['id'=>'app-theme-style','data-href'=>$params['BaseAppThemeCssLink']], $params['BaseAppThemeCss']);
- $response = new TemplateResponse('snappymail', 'index_embed', $params);
+ $response = new TemplateResponse('snappymail', 'index_embed', $params, $blank ? 'blank' : 'user');
- $response->setContentSecurityPolicy($csp);
+ $response->setContentSecurityPolicy($csp->getPolicy());
return $response;
}
@@ -106,6 +220,7 @@ class PageController extends Controller
*/
public function appGet()
{
+ $this->protectLegacyAdminRequest();
return SnappyMailHelper::startApp(true);
}
@@ -115,6 +230,7 @@ class PageController extends Controller
*/
public function appPost()
{
+ $this->protectLegacyAdminRequest();
return SnappyMailHelper::startApp(true);
}
@@ -124,6 +240,7 @@ class PageController extends Controller
*/
public function indexPost()
{
+ $this->protectLegacyAdminRequest();
return SnappyMailHelper::startApp(true);
}
}
diff --git a/integrations/nextcloud/snappymail/lib/Settings/AdminSettings.php b/integrations/nextcloud/snappymail/lib/Settings/AdminSettings.php
index eea9f49a8..d05f6dbe2 100644
--- a/integrations/nextcloud/snappymail/lib/Settings/AdminSettings.php
+++ b/integrations/nextcloud/snappymail/lib/Settings/AdminSettings.php
@@ -68,6 +68,7 @@ class AdminSettings implements ISettings
$parameters['snappymail-admin-password'] = $sPassword;
$parameters['can-import-rainloop'] = $sPassword && \is_dir(
+// SnappyMailHelper::getAppDataFolder()
\rtrim(\trim($this->config->getSystemValue('datadirectory', '')), '\\/')
. '/rainloop-storage'
);
@@ -98,7 +99,7 @@ class AdminSettings implements ISettings
public function getSection()
{
- return 'additional';
+ return 'snappymail';
}
public function getPriority()
diff --git a/integrations/nextcloud/snappymail/lib/Settings/PersonalSettings.php b/integrations/nextcloud/snappymail/lib/Settings/PersonalSettings.php
index 7a3e7ef56..3b3e8aadb 100644
--- a/integrations/nextcloud/snappymail/lib/Settings/PersonalSettings.php
+++ b/integrations/nextcloud/snappymail/lib/Settings/PersonalSettings.php
@@ -8,7 +8,7 @@ use OCP\Settings\ISettings;
class PersonalSettings implements ISettings
{
- private $config;
+ private IConfig $config;
public function __construct(IConfig $config)
{
@@ -33,7 +33,7 @@ class PersonalSettings implements ISettings
public function getSection()
{
- return 'additional';
+ return 'snappymail';
}
public function getPriority()
diff --git a/integrations/nextcloud/snappymail/lib/Settings/Section.php b/integrations/nextcloud/snappymail/lib/Settings/Section.php
new file mode 100644
index 000000000..3df65d201
--- /dev/null
+++ b/integrations/nextcloud/snappymail/lib/Settings/Section.php
@@ -0,0 +1,38 @@
+l = $l;
+ $this->urlGenerator = $urlGenerator;
+ }
+
+ public function getID(): string
+ {
+ return 'snappymail';
+ }
+
+ public function getName(): string
+ {
+ return $this->l->t('SnappyMail');
+ }
+
+ public function getPriority(): int
+ {
+ return 50;
+ }
+
+ public function getIcon(): string
+ {
+ return $this->urlGenerator->imagePath('snappymail', 'favicon-mask.svg');
+ }
+}
diff --git a/integrations/nextcloud/snappymail/lib/Util/RainLoop.php b/integrations/nextcloud/snappymail/lib/Util/RainLoop.php
index 737f73ded..3454e47cc 100644
--- a/integrations/nextcloud/snappymail/lib/Util/RainLoop.php
+++ b/integrations/nextcloud/snappymail/lib/Util/RainLoop.php
@@ -15,9 +15,8 @@ class RainLoop
*/
public static function import(): array
{
- $dir = \rtrim(\trim(SnappyMailHelper::getSystemConfigValue('datadirectory', '')), '\\/');
- $dir_snappy = $dir . '/appdata_snappymail/';
- $dir_rainloop = $dir . '/rainloop-storage';
+ $dir_snappy = SnappyMailHelper::getAppDataFolder();
+ $dir_rainloop = SnappyMailHelper::getDataDirectory() . '/rainloop-storage';
$result = [];
$rainloop_plugins = [];
if (\is_dir($dir_rainloop)) {
diff --git a/integrations/nextcloud/snappymail/lib/Util/SnappyMailHelper.php b/integrations/nextcloud/snappymail/lib/Util/SnappyMailHelper.php
index 8d165466e..d7e3e8848 100644
--- a/integrations/nextcloud/snappymail/lib/Util/SnappyMailHelper.php
+++ b/integrations/nextcloud/snappymail/lib/Util/SnappyMailHelper.php
@@ -34,6 +34,16 @@ class SnappyMailHelper
return static::$server || \OC::$server;
}
+ public static function getAppDataFolder(): string
+ {
+ return static::getDataDirectory() . '/appdata_snappymail/';
+ }
+
+ public static function getDataDirectory(): string
+ {
+ return \rtrim(\trim(static::getSystemConfigValue('datadirectory', '')), '\\/');
+ }
+
public static function getConfig(): object
{
return \method_exists(\OC::$server, 'getConfig')
@@ -110,10 +120,26 @@ class SnappyMailHelper
}
}
+ public static function linkToRoute(string $value): string
+ {
+ return (\method_exists(\OC::$server, 'getURLGenerator')
+ ? \OC::$server->getURLGenerator()
+ // Nextcloud 34+
+ : \OCP\Server::get(\OCP\IURLGenerator::class)
+ )->linkToRoute($value);
+ }
+
public static function loadApp(): void
{
+ $app_dir = \dirname(\dirname(__DIR__)) . '/app';
+
if (\class_exists('RainLoop\\Api')) {
- return;
+ $expected = \realpath($app_dir);
+ $actual = \defined('APP_INDEX_ROOT_PATH') ? \realpath(APP_INDEX_ROOT_PATH) : false;
+ if ($expected && $actual && $expected === $actual) {
+ return;
+ }
+ throw new \RuntimeException('SnappyMail cannot start because another SnappyMail-compatible app is already loaded in the same request.');
}
// Nextcloud the default spl_autoload_register() not working
@@ -129,9 +155,8 @@ class SnappyMailHelper
// define('APP_VERSION', '0.0.0');
// define('APP_INDEX_ROOT_PATH', __DIR__ . DIRECTORY_SEPARATOR);
// include APP_INDEX_ROOT_PATH.'snappymail/v/'.APP_VERSION.'/include.php';
-// define('APP_DATA_FOLDER_PATH', \rtrim(\trim(static::getSystemConfigValue('datadirectory', '')), '\\/').'/appdata_snappymail/');
+// define('APP_DATA_FOLDER_PATH', static::getAppDataFolder());
- $app_dir = \dirname(\dirname(__DIR__)) . '/app';
require_once $app_dir . '/index.php';
}
@@ -173,7 +198,7 @@ class SnappyMailHelper
if (24 < $OC_Version[0]) {
$ocSession = static::getSession();
$ocSession->reopen();
- if (!$doLogin && $ocSession['snappymail-uid'] && $ocSession['snappymail-uid'] != $aCredentials[0]) {
+ if (!$doLogin && $ocSession->get('snappymail-uid') && $ocSession->get('snappymail-uid') != $aCredentials[0]) {
// UID changed, Impersonate plugin probably active
$oActions->Logout(true);
$doLogin = true;
@@ -193,9 +218,20 @@ class SnappyMailHelper
} catch (\Throwable $e) {
// Login failure, reset password to prevent more attempts
if (!$isOIDC) {
- $sUID = static::getUID();
- static::setSessionValue('snappymail-passphrase', '');
- static::getConfig()->setUserValue($sUID, 'snappymail', 'passphrase', '');
+ // Credentials saved in the user's personal settings must never be
+ // deleted as a side effect of a failed login. A temporary network,
+ // DNS, TLS, or mail server outage is not a password change.
+ //
+ // Only discard the password captured from the current Nextcloud
+ // session when the IMAP server explicitly rejected authentication.
+ // Persisted credentials remain available for the user to inspect or
+ // replace in their personal settings.
+ if ($e instanceof \RainLoop\Exceptions\ClientException
+ && \RainLoop\Notifications::AuthError === $e->getCode()
+ ) {
+ static::setSessionValue('snappymail-passphrase', '');
+// static::getConfig()->setUserValue(static::getUID(), 'snappymail', 'passphrase', '');
+ }
\SnappyMail\Log::error('Nextcloud', $e->getMessage());
}
}
@@ -261,7 +297,7 @@ class SnappyMailHelper
// If the current user ID is identical to login ID (not valid when using account switching),
// this has the second priority.
- if ($ocSession['snappymail-nc-uid'] == $sUID) {
+ if ($ocSession->get('snappymail-nc-uid') == $sUID) {
// If OpenID Connect (OIDC) is enabled and used for login, use this.
if (static::isOIDCLogin()) {
@@ -276,10 +312,10 @@ class SnappyMailHelper
$sPassword = '';
if ($config->getAppValue('snappymail', 'snappymail-autologin', false)) {
$sEmail = $sUID;
- $sPassword = $ocSession['snappymail-passphrase'];
+ $sPassword = $ocSession->get('snappymail-passphrase');
} else if ($config->getAppValue('snappymail', 'snappymail-autologin-with-email', false)) {
$sEmail = $config->getUserValue($sUID, 'settings', 'email');
- $sPassword = $ocSession['snappymail-passphrase'];
+ $sPassword = $ocSession->get('snappymail-passphrase');
} else {
\SnappyMail\Log::debug('Nextcloud', 'snappymail-autologin is off');
}
@@ -287,7 +323,7 @@ class SnappyMailHelper
return [$sUID, $sEmail, static::decodePassword($sPassword, $sUID)];
}
} else {
- \SnappyMail\Log::debug('Nextcloud', "snappymail-nc-uid mismatch '{$ocSession['snappymail-nc-uid']}' != '{$sUID}'");
+ \SnappyMail\Log::debug('Nextcloud', "snappymail-nc-uid mismatch '{$ocSession->get('snappymail-nc-uid')}' != '{$sUID}'");
}
return [$sUID, '', ''];
@@ -295,7 +331,7 @@ class SnappyMailHelper
public static function getAppUrl(): string
{
- return \OC::$server->getURLGenerator()->linkToRoute('snappymail.page.appGet');
+ return static::linkToRoute('snappymail.page.appGet');
}
public static function normalizeUrl(string $sUrl): string
@@ -308,13 +344,13 @@ class SnappyMailHelper
return $sUrl;
}
- public static function encodePassword(string $sPassword, string $sSalt): string
+ public static function encodePassword(#[\SensitiveParameter] string $sPassword, string $sSalt): string
{
static::loadApp();
return \SnappyMail\Crypt::EncryptUrlSafe($sPassword, $sSalt);
}
- public static function decodePassword(string $sPassword, string $sSalt): ?\SnappyMail\SensitiveString
+ public static function decodePassword(#[\SensitiveParameter] string $sPassword, string $sSalt): ?\SnappyMail\SensitiveString
{
static::loadApp();
$result = \SnappyMail\Crypt::DecryptUrlSafe($sPassword, $sSalt);