This commit is contained in:
Nextgen-Networks 2026-02-20 13:27:32 +00:00 committed by GitHub
commit 61373faa97
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 714 additions and 549 deletions

View file

@ -31,7 +31,7 @@ If applicable, add screenshots to help explain your problem.
- Mode: [e.g. standalone, nextcloud, cyberpanel, docker]
**Debug/logging information**
[Read here how to log](https://github.com/the-djmaze/snappymail/wiki/FAQ#how-do-i-enable-logging)
[Read here how to log](https://github.com/nextgen-networks/snappymail/wiki/FAQ#how-do-i-enable-logging)
- [ ] I've placed them here (few lines) or as attachments (many lines)
**Additional context**

View file

@ -21,7 +21,7 @@ jobs:
DOCKER_METADATA_PR_HEAD_SHA: 'true'
with:
images: |
djmaze/snappymail
nextgennetworks/snappymail
ghcr.io/${{ github.repository }}
# type=ref,event=pr generates tag(s) on PRs only. E.g. 'pr-123', 'pr-123-abc0123'
tags: |

View file

@ -26,7 +26,7 @@ jobs:
DOCKER_METADATA_PR_HEAD_SHA: 'true'
with:
images: |
djmaze/snappymail
nextgennetworks/snappymail
ghcr.io/${{ github.repository }}
# type=ref,event=branch generates tag(s) on branch only. E.g. 'master', 'master-abc0123'
# type=ref,event=tag generates tag(s) on tags only. E.g. 'v0.0.0', 'v0.0.0-abc0123', and 'latest'

View file

@ -72,7 +72,7 @@
### Fixed
- mXSS exploit found by SonarSource
[CVE-2024-45800](https://github.com/the-djmaze/snappymail/security/advisories/GHSA-2rq7-79vp-ffxm)
[CVE-2024-45800](https://github.com/nextgen-networks/snappymail/security/advisories/GHSA-2rq7-79vp-ffxm)
- Call to a member function Email() on null
[#1706](https://github.com/the-djmaze/snappymail/issues/1706)
- IMAP capabilities via IMAP Proxy
@ -202,7 +202,7 @@
### Added
- Customize private key passphrase expiration interval
[#1545](https://github.com/the-djmaze/snappymail/discussions/1545)
[#1545](https://github.com/nextgen-networks/snappymail/discussions/1545)
- AdvancedSearch support for filtering mails before a given date
[#1606](https://github.com/the-djmaze/snappymail/pull/1606) by @codiflow
- Control valid spam and virus headers
@ -411,9 +411,9 @@
[#1458](https://github.com/the-djmaze/snappymail/issues/1458)
- Automatically verify PGP and S/MIME signed messages
- TNEFDecoder for
[#1012](https://github.com/the-djmaze/snappymail/discussions/1012)
[#1012](https://github.com/nextgen-networks/snappymail/discussions/1012)
- RTF to HTML converter for
[#1012](https://github.com/the-djmaze/snappymail/discussions/1012)
[#1012](https://github.com/nextgen-networks/snappymail/discussions/1012)
- Polyfill for PHP ctype
[#1250](https://github.com/the-djmaze/snappymail/issues/1250)

View file

@ -11,7 +11,7 @@
2. Install node.js - `https://nodejs.org/download/`
3. Install yarn - `https://yarnpkg.com/en/docs/install`
4. Install gulp - `npm install gulp -g`
5. Fork snappymail from https://github.com/the-djmaze/snappymail
5. Fork snappymail from https://github.com/nextgen-networks/snappymail/
6. Clone snappymail - `git clone git@github.com:USERNAME/snappymail.git snappymail`
7. `cd snappymail`
8. Install all dependencies - `yarn install`

View file

@ -6,7 +6,7 @@ pkgrel=1
pkgdesc="modern PHP webmail client"
arch=('any')
license=('AGPL3')
url="https://github.com/the-djmaze/snappymail"
url="https://github.com/nextgen-networks/snappymail/"
depends=('php-fpm')
makedepends=('php', 'nodejs' 'yarn' 'gulp', 'rollup')
optdepends=('mariadb: storage backend for contacts'

View file

@ -6,8 +6,8 @@ Recommends: php-intl, php-sodium, php-uuid
Suggests: php-sqlite3 | php-mysql | php-pgsql, php-curl, php-exif, php-gnupg, php-gd | php-gmagick | php-imagick, php-openssl, php-zip
Architecture: all
Homepage: https://snappymail.eu
Vcs-Browser: https://github.com/the-djmaze/snappymail
Vcs-Git: https://github.com/the-djmaze/snappymail.git
Vcs-Browser: https://github.com/nextgen-networks/snappymail/
Vcs-Git: https://github.com/nextgen-networks/snappymail.git
Description: SnappyMail is a PHP-based simple, modern, lightweight & fast web-based email client with no database requirements.
It supports IMAP, SMTP and Sieve protocols, multiple accounts and identities, an admin panel for configuration.
Plugins can be installed to further extend functionality.

View file

@ -207,7 +207,7 @@ if ($options['aur']) {
pkgdesc = modern PHP webmail client
pkgver = '.$package->version.'
pkgrel = 1
url = https://github.com/the-djmaze/snappymail
url = https://github.com/nextgen-networks/snappymail/
arch = any
license = AGPL3
makedepends = php
@ -218,7 +218,7 @@ if ($options['aur']) {
optdepends = mariadb: storage backend for contacts
optdepends = php-pgsql: storage backend for contacts
optdepends = php-sqlite: storage backend for contacts
source = snappymail-'.$package->version.'.tar.gz::https://github.com/the-djmaze/snappymail/archive/v'.$package->version.'.tar.gz
source = snappymail-'.$package->version.'.tar.gz::https://github.com/nextgen-networks/snappymail/archive/v'.$package->version.'.tar.gz
source = snappymail.sysusers
source = snappymail.tmpfiles
b2sums = '.implode("\n b2sums = ", $b2sums).'
@ -244,7 +244,7 @@ else if ($options['docker']) {
$zip_filename = "snappymail-{$package->version}.zip";
copy($zip_destination, "./.docker/release/{$zip_filename}");
if ($docker) {
passthru("{$docker} build --pull " . ROOT_DIR . "/.docker/release/ --build-arg FILES_ZIP={$zip_filename} -t snappymail:{$package->version}");
passthru("{$docker} build --pull -f " . ROOT_DIR . '/.docker/release/Dockerfile ' . ROOT_DIR . " --build-arg FILES_ZIP={$zip_filename} -t snappymail:{$package->version}");
} else {
echo "Docker not installed!\n";
}

View file

@ -10,7 +10,7 @@ Content-Type: text/plain; charset=us-ascii; format=flowed
Test message body with example TNEF part from
https://github.com/Yeraze/ytnef
https://github.com/the-djmaze/snappymail/discussions/1012
https://github.com/nextgen-networks/snappymail/discussions/1012
--=-2UbcDuksJBAwUNBNW7UD
Content-Type: application/vnd.ms-tnef

View file

@ -5,7 +5,7 @@ WORKDIR /app/code
# If you change the extraction below, be sure to test on scaleway
VERSION=2.38.2
RUN wget https://github.com/the-djmaze/snappymail/releases/download/v${VERSION}/snappymail-${VERSION}.zip -O /tmp/snappymail.zip && \
RUN wget https://github.com/nextgen-networks/snappymail/releases/download/v${VERSION}/snappymail-${VERSION}.zip -O /tmp/snappymail.zip && \
unzip /tmp/snappymail.zip -d /app/code && \
rm /tmp/snappymail.zip && \
find /app/code/snappymail -type d -exec chmod 755 {} \; && \

View file

@ -25,19 +25,19 @@ There, click on the link to go to the SnappyMail admin panel.
]]></description>
<namespace>SnappyMail</namespace>
<documentation>
<user>https://github.com/the-djmaze/snappymail/blob/master/integrations/nextcloud/snappymail/README.md</user>
<admin>https://github.com/the-djmaze/snappymail/wiki/Admin-Manual</admin>
<developer>https://github.com/the-djmaze/snappymail/wiki/Developer-Documentation</developer>
<website>https://github.com/the-djmaze/snappymail/wiki</website>
<user>https://github.com/nextgen-networks/snappymail/blob/master/integrations/nextcloud/snappymail/README.md</user>
<admin>https://github.com/nextgen-networks/snappymail/wiki/Admin-Manual</admin>
<developer>https://github.com/nextgen-networks/snappymail/wiki/Developer-Documentation</developer>
<website>https://github.com/nextgen-networks/snappymail/wiki</website>
</documentation>
<category>integration</category>
<category>office</category>
<category>search</category>
<category>social</category>
<screenshot>https://raw.githubusercontent.com/the-djmaze/snappymail/master/integrations/nextcloud/screenshots/inbox.jpg</screenshot>
<screenshot>https://raw.githubusercontent.com/nextgen-networks/snappymail/master/integrations/nextcloud/screenshots/inbox.jpg</screenshot>
<website>https://snappymail.eu</website>
<discussion>https://github.com/the-djmaze/snappymail/discussions</discussion>
<repository>https://github.com/the-djmaze/snappymail/tree/master/integrations/nextcloud</repository>
<discussion>https://github.com/nextgen-networks/snappymail/discussions</discussion>
<repository>https://github.com/nextgen-networks/snappymail/tree/master/integrations/nextcloud</repository>
<bugs>https://github.com/the-djmaze/snappymail/issues</bugs>
<dependencies>
<php min-version="7.4" min-int-size="32" />
@ -64,7 +64,7 @@ There, click on the link to go to the SnappyMail admin panel.
<lib>xxtea</lib>
<lib>zip</lib>
-->
<nextcloud min-version="20" max-version="32" />
<nextcloud min-version="25" max-version="33" />
</dependencies>
<settings>
<admin>OCA\SnappyMail\Settings\AdminSettings</admin>

View file

@ -5,7 +5,6 @@ namespace OCA\SnappyMail\AppInfo;
use OCA\SnappyMail\Util\SnappyMailHelper;
use OCA\SnappyMail\Controller\FetchController;
use OCA\SnappyMail\Controller\PageController;
use OCA\SnappyMail\Dashboard\UnreadMailWidget;
use OCA\SnappyMail\Search\Provider;
use OCA\SnappyMail\Listeners\AccessTokenUpdatedListener;
@ -18,6 +17,10 @@ use OCP\IUser;
use OCP\User\Events\PostLoginEvent;
use OCP\User\Events\BeforeUserLoggedOutEvent;
use OCA\OIDCLogin\Events\AccessTokenUpdatedEvent;
use OCP\IConfig;
use OCP\ISession;
use OCP\IUserSession;
use OCP\INavigationManager;
class Application extends App implements IBootstrap
{
@ -37,7 +40,9 @@ class Application extends App implements IBootstrap
'PageController', function($c) {
return new PageController(
$c->query('AppName'),
$c->query('Request')
$c->query('Request'),
$c->query(IConfig::class),
$c->query(INavigationManager::class)
);
}
);
@ -47,9 +52,10 @@ class Application extends App implements IBootstrap
return new FetchController(
$c->query('AppName'),
$c->query('Request'),
$c->getServer()->getAppManager(),
$c->query('ServerContainer')->getAppManager(),
$c->query('ServerContainer')->getConfig(),
$c->query(IL10N::class)
$c->query(IL10N::class),
$c->query(IUserSession::class)
);
}
);
@ -72,22 +78,24 @@ class Application extends App implements IBootstrap
public function boot(IBootContext $context): void
{
if (!\is_dir(\rtrim(\trim(\OC::$server->getSystemConfig()->getValue('datadirectory', '')), '\\/') . '/appdata_snappymail')) {
$config = $context->getServerContainer()->getConfig();
if (!\is_dir(\rtrim(\trim($config->getSystemValue('datadirectory', '')), '\\/') . '/appdata_snappymail')) {
return;
}
$dispatcher = $context->getAppContainer()->query('OCP\EventDispatcher\IEventDispatcher');
$dispatcher->addListener(PostLoginEvent::class, function (PostLoginEvent $Event) {
$dispatcher->addListener(PostLoginEvent::class, function (PostLoginEvent $Event) use ($context) {
/*
$config = \OC::$server->getConfig();
$config = $context->getServerContainer()->getConfig();
// Only store the user's password in the current session if they have
// enabled auto-login using Nextcloud username or email address.
if ($config->getAppValue('snappymail', 'snappymail-autologin', false)
|| $config->getAppValue('snappymail', 'snappymail-autologin-with-email', false)) {
*/
$sUID = $Event->getUser()->getUID();
\OC::$server->getSession()['snappymail-nc-uid'] = $sUID;
\OC::$server->getSession()['snappymail-passphrase'] = SnappyMailHelper::encodePassword($Event->getPassword(), $sUID);
$session = $context->getServerContainer()->getSession();
$session['snappymail-nc-uid'] = $sUID;
$session['snappymail-passphrase'] = SnappyMailHelper::encodePassword($Event->getPassword(), $sUID);
/*
}
*/
@ -105,13 +113,13 @@ class Application extends App implements IBootstrap
// https://github.com/nextcloud/impersonate/pull/180
$class = 'OCA\Impersonate\Events\BeginImpersonateEvent';
if (\class_exists($class)) {
$dispatcher->addListener($class, function ($Event) {
\OC::$server->getSession()['snappymail-passphrase'] = '';
$dispatcher->addListener($class, function ($Event) use ($context) {
$context->getServerContainer()->getSession()['snappymail-passphrase'] = '';
SnappyMailHelper::loadApp();
\RainLoop\Api::Actions()->Logout(true);
});
$dispatcher->addListener('OCA\Impersonate\Events\EndImpersonateEvent', function ($Event) {
\OC::$server->getSession()['snappymail-passphrase'] = '';
$dispatcher->addListener('OCA\Impersonate\Events\EndImpersonateEvent', function ($Event) use ($context) {
$context->getServerContainer()->getSession()['snappymail-passphrase'] = '';
SnappyMailHelper::loadApp();
\RainLoop\Api::Actions()->Logout(true);
});

View file

@ -3,14 +3,14 @@
namespace OCA\SnappyMail\Command;
use OCA\SnappyMail\Util\SnappyMailHelper;
use OC\Core\Command\Base;
use OCP\IConfig;
use OCP\IUserManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class Settings extends Base
class Settings extends Command
{
protected IUserManager $userManager;
protected IConfig $config;

View file

@ -2,6 +2,9 @@
namespace OCA\SnappyMail;
use OCP\Server;
use OCP\Security\IContentSecurityPolicyNonceManager;
class ContentSecurityPolicy extends \OCP\AppFramework\Http\ContentSecurityPolicy {
/** @var bool Whether inline JS snippets are allowed */
@ -37,7 +40,7 @@ class ContentSecurityPolicy extends \OCP\AppFramework\Http\ContentSecurityPolicy
public function getSnappyMailNonce() {
static $sNonce;
if (!$sNonce) {
$cspManager = \OC::$server->getContentSecurityPolicyNonceManager();
$cspManager = Server::get(IContentSecurityPolicyNonceManager::class);
$sNonce = $cspManager->getNonce() ?: \SnappyMail\UUID::generate();
if (\method_exists($cspManager, 'browserSupportsCspV3') && !$cspManager->browserSupportsCspV3()) {
$this->addAllowedScriptDomain("'nonce-{$sNonce}'");

View file

@ -10,17 +10,21 @@ use OCP\AppFramework\Http\JSONResponse;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IUserSession;
use Exception;
class FetchController extends Controller {
private IConfig $config;
private IAppManager $appManager;
private IL10N $l;
private IUserSession $userSession;
public function __construct(string $appName, IRequest $request, IAppManager $appManager, IConfig $config, IL10N $l) {
public function __construct(string $appName, IRequest $request, IAppManager $appManager, IConfig $config, IL10N $l, IUserSession $userSession) {
parent::__construct($appName, $request);
$this->config = $config;
$this->appManager = $appManager;
$this->l = $l;
$this->userSession = $userSession;
}
public function upgrade(): JSONResponse {
@ -104,15 +108,18 @@ class FetchController extends Controller {
try {
$sEmail = '';
if (isset($_POST['appname'], $_POST['snappymail-password'], $_POST['snappymail-email']) && 'snappymail' === $_POST['appname']) {
$sUser = \OC::$server->getUserSession()->getUser()->getUID();
$user = $this->userSession->getUser();
$sUser = $user ? $user->getUID() : '';
$sEmail = $_POST['snappymail-email'];
$this->config->setUserValue($sUser, 'snappymail', 'snappymail-email', $sEmail);
if ($sUser) {
$sEmail = $_POST['snappymail-email'];
$this->config->setUserValue($sUser, 'snappymail', 'snappymail-email', $sEmail);
$sPass = $_POST['snappymail-password'];
if ('******' !== $sPass) {
$this->config->setUserValue($sUser, 'snappymail', 'passphrase',
$sPass ? SnappyMailHelper::encodePassword($sPass, \md5($sEmail)) : '');
$sPass = $_POST['snappymail-password'];
if ('******' !== $sPass) {
$this->config->setUserValue($sUser, 'snappymail', 'passphrase',
$sPass ? SnappyMailHelper::encodePassword($sPass, \md5($sEmail)) : '');
}
}
} else {
return new JSONResponse([
@ -144,4 +151,3 @@ class FetchController extends Controller {
}
}
}

View file

@ -7,17 +7,27 @@ use OCA\SnappyMail\ContentSecurityPolicy;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\INavigationManager;
use OCP\IConfig;
use OCP\IRequest;
class PageController extends Controller
{
private IConfig $config;
private INavigationManager $navigationManager;
public function __construct($appName, IRequest $request, IConfig $config, INavigationManager $navigationManager) {
parent::__construct($appName, $request);
$this->config = $config;
$this->navigationManager = $navigationManager;
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*/
public function index()
{
$config = \OC::$server->getConfig();
$bAdmin = false;
if (!empty($_SERVER['QUERY_STRING'])) {
SnappyMailHelper::loadApp();
@ -27,10 +37,8 @@ class PageController extends Controller
}
}
if (!$bAdmin && $config->getAppValue('snappymail', 'snappymail-no-embed')) {
\OC::$server->getNavigationManager()->setActiveEntry('snappymail');
\OCP\Util::addScript('snappymail', 'snappymail');
\OCP\Util::addStyle('snappymail', 'style');
if (!$bAdmin && $this->config->getAppValue('snappymail', 'snappymail-no-embed')) {
$this->navigationManager->setActiveEntry('snappymail');
SnappyMailHelper::startApp();
$response = new TemplateResponse('snappymail', 'index', [
'snappymail-iframe-url' => SnappyMailHelper::normalizeUrl(SnappyMailHelper::getAppUrl())
@ -43,9 +51,7 @@ class PageController extends Controller
return $response;
}
\OC::$server->getNavigationManager()->setActiveEntry('snappymail');
\OCP\Util::addStyle('snappymail', 'embed');
$this->navigationManager->setActiveEntry('snappymail');
SnappyMailHelper::startApp();
$oConfig = \RainLoop\Api::Config();
@ -59,6 +65,8 @@ class PageController extends Controller
$csp = new ContentSecurityPolicy();
$sNonce = $csp->getSnappyMailNonce();
$cssLink = \RainLoop\Utils::WebStaticPath('css/'.($bAdmin?'admin':'app').$sAppCssMin.'.css');
$params = [
'Admin' => $bAdmin ? 1 : 0,
'LoadingDescriptionEsc' => \htmlspecialchars($oConfig->Get('webmail', 'loading_description', 'SnappyMail'), ENT_QUOTES|ENT_IGNORE, 'UTF-8'),
@ -71,16 +79,10 @@ class PageController extends Controller
'/\\s*([:;{},]+)\\s*/s',
'$1',
$oActions->compileCss($oActions->GetTheme($bAdmin), $bAdmin)
)
),
'CssLink' => $cssLink
];
// \OCP\Util::addScript('snappymail', '../app/snappymail/v/'.APP_VERSION.'/static/js'.($sAppJsMin ? '/min' : '').'/boot'.$sAppJsMin);
// Nextcloud html encodes, so addHeader('style') is not possible
// \OCP\Util::addHeader('style', ['id'=>'app-boot-css'], \file_get_contents(APP_VERSION_ROOT_PATH.'static/css/boot'.$sAppCssMin.'.css'));
\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->setContentSecurityPolicy($csp);

View file

@ -6,7 +6,8 @@ use OCA\SnappyMail\AppInfo\Application;
use OCP\IConfig;
use OCP\Migration\IOutput;
use OCP\Migration\IRepairStep;
use OCP\ILogger;
use Psr\Log\LoggerInterface;
use OCP\App\IAppManager;
/**
* Note: this is not run at install, but when:
@ -15,6 +16,16 @@ use OCP\ILogger;
*/
class InstallStep implements IRepairStep
{
private IAppManager $appManager;
private IConfig $config;
private LoggerInterface $logger;
public function __construct(IAppManager $appManager, IConfig $config, LoggerInterface $logger) {
$this->appManager = $appManager;
$this->config = $config;
$this->logger = $logger;
}
public function getName() {
return 'Setup SnappyMail';
}
@ -53,7 +64,7 @@ class InstallStep implements IRepairStep
if (!$oConfig->Get('webmail', 'app_path')) {
$output->info('Set config [webmail]app_path');
$oConfig->Set('webmail', 'app_path', \OC::$server->getAppManager()->getAppWebPath('snappymail') . '/app/');
$oConfig->Set('webmail', 'app_path', $this->appManager->getAppWebPath('snappymail') . '/app/');
$oConfig->Set('webmail', 'allow_languages_on_settings', false);
$oConfig->Set('login', 'allow_languages_on_login', false);
$bSave = true;
@ -116,9 +127,7 @@ class InstallStep implements IRepairStep
// ex: php occ config:app:set snappymail custom_config_file --value="/path/to/config.php"
// https://github.com/the-djmaze/snappymail/pull/1197
try {
/** @var IConfig $ncConfig */
$ncConfig = \OC::$server->get(IConfig::class);
$customConfigFile = $ncConfig->getAppValue(Application::APP_ID, 'custom_config_file');
$customConfigFile = $this->config->getAppValue(Application::APP_ID, 'custom_config_file');
if ($customConfigFile) {
$output->info("Load custom config: {$customConfigFile}");
if (!\str_contains($customConfigFile, ':') && \is_readable($customConfigFile)) {
@ -129,9 +138,7 @@ class InstallStep implements IRepairStep
}
} catch (\Throwable $e) {
$output->warning("custom config error: " . $e->getMessage());
/** @var \Psr\Log\LoggerInterface $logger */
$logger = \OC::$server->get(\Psr\Log\LoggerInterface::class);
$logger->error("custom config error: " . $e->getMessage());
$this->logger->error("custom config error: " . $e->getMessage());
}
}
}

View file

@ -5,14 +5,32 @@ use OCA\SnappyMail\Util\SnappyMailHelper;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IConfig;
use OCP\Settings\ISettings;
use OCP\IUserSession;
use OCP\IGroupManager;
use OCP\IURLGenerator;
use OCP\App\IAppManager;
use OCP\Util;
class AdminSettings implements ISettings
{
private $config;
private IConfig $config;
private IUserSession $userSession;
private IGroupManager $groupManager;
private IURLGenerator $urlGenerator;
private IAppManager $appManager;
public function __construct(IConfig $config)
{
public function __construct(
IConfig $config,
IUserSession $userSession,
IGroupManager $groupManager,
IURLGenerator $urlGenerator,
IAppManager $appManager
) {
$this->config = $config;
$this->userSession = $userSession;
$this->groupManager = $groupManager;
$this->urlGenerator = $urlGenerator;
$this->appManager = $appManager;
}
public function getForm()
@ -30,12 +48,15 @@ class AdminSettings implements ISettings
$v = $this->config->getAppValue('snappymail', $k);
$parameters[$k] = $v;
}
$uid = \OC::$server->getUserSession()->getUser()->getUID();
if (\OC_User::isAdminUser($uid)) {
$user = $this->userSession->getUser();
$uid = $user ? $user->getUID() : null;
if ($uid && $this->groupManager->isAdmin($uid)) {
// $parameters['snappymail-admin-panel-link'] = SnappyMailHelper::getAppUrl().'?admin';
SnappyMailHelper::loadApp();
$parameters['snappymail-admin-panel-link'] =
\OC::$server->getURLGenerator()->linkToRoute('snappymail.page.index')
$this->urlGenerator->linkToRoute('snappymail.page.index')
. '?' . \RainLoop\Api::Config()->Get('security', 'admin_panel_key', 'admin');
}
@ -49,7 +70,7 @@ class AdminSettings implements ISettings
$parameters['snappymail-admin-password'] = $sPassword;
$parameters['can-import-rainloop'] = $sPassword && \is_dir(
\rtrim(\trim(\OC::$server->getSystemConfig()->getValue('datadirectory', '')), '\\/')
\rtrim(\trim($this->config->getSystemValue('datadirectory', '')), '\\/')
. '/rainloop-storage'
);
@ -65,7 +86,7 @@ class AdminSettings implements ISettings
// Prevent "Failed loading /nextcloud/snappymail/v/2.N.N/static/js/min/libs.min.js"
$app_path = $oConfig->Get('webmail', 'app_path');
if (!$app_path) {
$app_path = \OC::$server->getAppManager()->getAppWebPath('snappymail') . '/app/';
$app_path = $this->appManager->getAppWebPath('snappymail') . '/app/';
$oConfig->Set('webmail', 'app_path', $app_path);
$oConfig->Set('webmail', 'theme', 'NextcloudV25+');
$oConfig->Save();
@ -73,7 +94,6 @@ class AdminSettings implements ISettings
$parameters['snappymail-app_path'] = $oConfig->Get('webmail', 'app_path', false);
$parameters['snappymail-nc-lang'] = !$oConfig->Get('webmail', 'allow_languages_on_settings', true);
\OCP\Util::addScript('snappymail', 'snappymail');
return new TemplateResponse('snappymail', 'admin-local', $parameters);
}

View file

@ -1,232 +1,252 @@
<?php
namespace OCA\SnappyMail\Util;
class SnappyMailResponse extends \OCP\AppFramework\Http\Response
{
public function render(): string
{
$data = '';
$i = \ob_get_level();
while ($i--) {
$data .= \ob_get_clean();
}
return $data;
}
}
class SnappyMailHelper
{
public static function loadApp() : void
{
if (\class_exists('RainLoop\\Api')) {
return;
}
// Nextcloud the default spl_autoload_register() not working
\spl_autoload_register(function($sClassName){
$file = SNAPPYMAIL_LIBRARIES_PATH . \strtolower(\strtr($sClassName, '\\', DIRECTORY_SEPARATOR)) . '.php';
if (\is_file($file)) {
include_once $file;
}
});
$_ENV['SNAPPYMAIL_INCLUDE_AS_API'] = true;
// 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(\OC::$server->getSystemConfig()->getValue('datadirectory', '')), '\\/').'/appdata_snappymail/');
$app_dir = \dirname(\dirname(__DIR__)) . '/app';
require_once $app_dir . '/index.php';
}
public static function startApp(bool $handle = false)
{
static::loadApp();
$oConfig = \RainLoop\Api::Config();
if (false !== \stripos(\php_sapi_name(), 'cli')) {
return;
}
try {
$oActions = \RainLoop\Api::Actions();
if (isset($_GET[$oConfig->Get('security', 'admin_panel_key', 'admin')])) {
if ($oConfig->Get('security', 'allow_admin_panel', true)
&& \OC_User::isAdminUser(\OC::$server->getUserSession()->getUser()->getUID())
&& !$oActions->IsAdminLoggined(false)
) {
$sRand = \MailSo\Base\Utils::Sha1Rand();
if ($oActions->Cacher(null, true)->Set(\RainLoop\KeyPathHelper::SessionAdminKey($sRand), \time())) {
$sToken = \RainLoop\Utils::EncodeKeyValuesQ(array('token', $sRand));
// $oActions->setAdminAuthToken($sToken);
\SnappyMail\Cookies::set('smadmin', $sToken);
}
}
} else {
$doLogin = !$oActions->getMainAccountFromToken(false);
$aCredentials = static::getLoginCredentials();
/*
// NC25+ workaround for Impersonate plugin
// https://github.com/the-djmaze/snappymail/issues/561#issuecomment-1301317723
// https://github.com/nextcloud/server/issues/34935#issuecomment-1302145157
require \OC::$SERVERROOT . '/version.php';
// \OC\SystemConfig
// file_get_contents(\OC::$SERVERROOT . 'config/config.php');
// $CONFIG['version']
if (24 < $OC_Version[0]) {
$ocSession = \OC::$server->getSession();
$ocSession->reopen();
if (!$doLogin && $ocSession['snappymail-uid'] && $ocSession['snappymail-uid'] != $aCredentials[0]) {
// UID changed, Impersonate plugin probably active
$oActions->Logout(true);
$doLogin = true;
}
$ocSession->set('snappymail-uid', $aCredentials[0]);
}
*/
if ($doLogin && $aCredentials[1] && $aCredentials[2]) {
$isOIDC = \str_starts_with($aCredentials[2], 'oidc_login|');
try {
$ocSession = \OC::$server->getSession();
$oAccount = $oActions->LoginProcess($aCredentials[1], $aCredentials[2]);
if (!$isOIDC && $oAccount
&& $oConfig->Get('login', 'sign_me_auto', \RainLoop\Enumerations\SignMeType::DefaultOff) === \RainLoop\Enumerations\SignMeType::DefaultOn
) {
$oActions->SetSignMeToken($oAccount);
}
} catch (\Throwable $e) {
// Login failure, reset password to prevent more attempts
if (!$isOIDC) {
$sUID = \OC::$server->getUserSession()->getUser()->getUID();
\OC::$server->getSession()['snappymail-passphrase'] = '';
\OC::$server->getConfig()->setUserValue($sUID, 'snappymail', 'passphrase', '');
\SnappyMail\Log::error('Nextcloud', $e->getMessage());
}
}
}
}
if ($handle) {
\header_remove('Content-Security-Policy');
\RainLoop\Service::Handle();
// https://github.com/the-djmaze/snappymail/issues/1069
exit;
// return new SnappyMailResponse();
}
} catch (\Throwable $e) {
// Ignore login failure
}
}
// Check if OpenID Connect (OIDC) is enabled and used for login
// https://apps.nextcloud.com/apps/oidc_login
public static function isOIDCLogin() : bool
{
$config = \OC::$server->getConfig();
if ($config->getAppValue('snappymail', 'snappymail-autologin-oidc', false)) {
// Check if the OIDC Login app is enabled
if (\OC::$server->getAppManager()->isEnabledForUser('oidc_login')) {
// Check if session is an OIDC Login
$ocSession = \OC::$server->getSession();
if ($ocSession->get('is_oidc')) {
// IToken->getPassword() ???
if ($ocSession->get('oidc_access_token')) {
return true;
}
\SnappyMail\Log::debug('Nextcloud', 'OIDC access_token missing');
} else {
\SnappyMail\Log::debug('Nextcloud', 'No OIDC login');
}
} else {
\SnappyMail\Log::debug('Nextcloud', 'OIDC login disabled');
}
}
return false;
}
private static function getLoginCredentials() : array
{
$sUID = \OC::$server->getUserSession()->getUser()->getUID();
$config = \OC::$server->getConfig();
$ocSession = \OC::$server->getSession();
// If the user has set credentials for SnappyMail in their personal settings,
// this has the first priority.
$sEmail = $config->getUserValue($sUID, 'snappymail', 'snappymail-email');
$sPassword = $config->getUserValue($sUID, 'snappymail', 'passphrase')
?: $config->getUserValue($sUID, 'snappymail', 'snappymail-password');
if ($sEmail && $sPassword) {
$sPassword = static::decodePassword($sPassword, \md5($sEmail));
if ($sPassword) {
return [$sUID, $sEmail, $sPassword];
} else {
\SnappyMail\Log::debug('Nextcloud', 'decodePassword failed for getUserValue');
}
}
// 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 OpenID Connect (OIDC) is enabled and used for login, use this.
if (static::isOIDCLogin()) {
$sEmail = $config->getUserValue($sUID, 'settings', 'email');
return [$sUID, $sEmail, "oidc_login|{$sUID}"];
}
// Only use the user's password in the current session if they have
// enabled auto-login using Nextcloud username or email address.
$sEmail = '';
$sPassword = '';
if ($config->getAppValue('snappymail', 'snappymail-autologin', false)) {
$sEmail = $sUID;
$sPassword = $ocSession['snappymail-passphrase'];
} else if ($config->getAppValue('snappymail', 'snappymail-autologin-with-email', false)) {
$sEmail = $config->getUserValue($sUID, 'settings', 'email');
$sPassword = $ocSession['snappymail-passphrase'];
} else {
\SnappyMail\Log::debug('Nextcloud', 'snappymail-autologin is off');
}
if ($sPassword) {
return [$sUID, $sEmail, static::decodePassword($sPassword, $sUID)];
}
} else {
\SnappyMail\Log::debug('Nextcloud', "snappymail-nc-uid mismatch '{$ocSession['snappymail-nc-uid']}' != '{$sUID}'");
}
return [$sUID, '', ''];
}
public static function getAppUrl() : string
{
return \OC::$server->getURLGenerator()->linkToRoute('snappymail.page.appGet');
}
public static function normalizeUrl(string $sUrl) : string
{
$sUrl = \rtrim(\trim($sUrl), '/\\');
if ('.php' !== \strtolower(\substr($sUrl, -4))) {
$sUrl .= '/';
}
return $sUrl;
}
public static function encodePassword(string $sPassword, string $sSalt) : string
{
static::loadApp();
return \SnappyMail\Crypt::EncryptUrlSafe($sPassword, $sSalt);
}
public static function decodePassword(string $sPassword, string $sSalt) : ?\SnappyMail\SensitiveString
{
static::loadApp();
$result = \SnappyMail\Crypt::DecryptUrlSafe($sPassword, $sSalt);
return $result ? new \SnappyMail\SensitiveString($result) : null;
}
}
<?php
namespace OCA\SnappyMail\Util;
use OCP\Server;
use OCP\IUserSession;
use OCP\IConfig;
use OCP\IURLGenerator;
use OCP\App\IAppManager;
use OCP\ISession;
use OCP\IGroupManager;
class SnappyMailResponse extends \OCP\AppFramework\Http\Response
{
public function render(): string
{
$data = '';
$i = \ob_get_level();
while ($i--) {
$data .= \ob_get_clean();
}
return $data;
}
}
class SnappyMailHelper
{
public static function loadApp() : void
{
if (\class_exists('RainLoop\\Api')) {
return;
}
// Nextcloud the default spl_autoload_register() not working
\spl_autoload_register(function($sClassName){
$file = SNAPPYMAIL_LIBRARIES_PATH . \strtolower(\strtr($sClassName, '\\', DIRECTORY_SEPARATOR)) . '.php';
if (\is_file($file)) {
include_once $file;
}
});
$_ENV['SNAPPYMAIL_INCLUDE_AS_API'] = true;
// 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(\OC::$server->getSystemConfig()->getValue('datadirectory', '')), '\\/').'/appdata_snappymail/');
$app_dir = \dirname(\dirname(__DIR__)) . '/app';
require_once $app_dir . '/index.php';
}
public static function startApp(bool $handle = false)
{
static::loadApp();
$oConfig = \RainLoop\Api::Config();
if (false !== \stripos(\php_sapi_name(), 'cli')) {
return;
}
try {
$oActions = \RainLoop\Api::Actions();
if (isset($_GET[$oConfig->Get('security', 'admin_panel_key', 'admin')])) {
$user = Server::get(IUserSession::class)->getUser();
if ($oConfig->Get('security', 'allow_admin_panel', true)
&& $user
&& Server::get(IGroupManager::class)->isAdmin($user->getUID())
&& !$oActions->IsAdminLoggined(false)
) {
$sRand = \MailSo\Base\Utils::Sha1Rand();
if ($oActions->Cacher(null, true)->Set(\RainLoop\KeyPathHelper::SessionAdminKey($sRand), \time())) {
$sToken = \RainLoop\Utils::EncodeKeyValuesQ(array('token', $sRand));
// $oActions->setAdminAuthToken($sToken);
\SnappyMail\Cookies::set('smadmin', $sToken);
}
}
} else {
$doLogin = !$oActions->getMainAccountFromToken(false);
$aCredentials = static::getLoginCredentials();
/*
// NC25+ workaround for Impersonate plugin
// https://github.com/the-djmaze/snappymail/issues/561#issuecomment-1301317723
// https://github.com/nextcloud/server/issues/34935#issuecomment-1302145157
require \OC::$SERVERROOT . '/version.php';
// \OC\SystemConfig
// file_get_contents(\OC::$SERVERROOT . 'config/config.php');
// $CONFIG['version']
if (24 < $OC_Version[0]) {
$ocSession = Server::get(ISession::class);
$ocSession->reopen();
if (!$doLogin && $ocSession['snappymail-uid'] && $ocSession['snappymail-uid'] != $aCredentials[0]) {
// UID changed, Impersonate plugin probably active
$oActions->Logout(true);
$doLogin = true;
}
$ocSession->set('snappymail-uid', $aCredentials[0]);
}
*/
if ($doLogin && $aCredentials[1] && $aCredentials[2]) {
$isOIDC = \str_starts_with($aCredentials[2], 'oidc_login|');
try {
$ocSession = Server::get(ISession::class);
$oAccount = $oActions->LoginProcess($aCredentials[1], $aCredentials[2]);
if (!$isOIDC && $oAccount
&& $oConfig->Get('login', 'sign_me_auto', \RainLoop\Enumerations\SignMeType::DefaultOff) === \RainLoop\Enumerations\SignMeType::DefaultOn
) {
$oActions->SetSignMeToken($oAccount);
}
} catch (\Throwable $e) {
// Login failure, reset password to prevent more attempts
if (!$isOIDC) {
$user = Server::get(IUserSession::class)->getUser();
$sUID = $user ? $user->getUID() : null;
if ($sUID) {
$session = Server::get(ISession::class);
$session['snappymail-passphrase'] = '';
Server::get(IConfig::class)->setUserValue($sUID, 'snappymail', 'passphrase', '');
\SnappyMail\Log::error('Nextcloud', $e->getMessage());
}
}
}
}
}
if ($handle) {
\header_remove('Content-Security-Policy');
\RainLoop\Service::Handle();
// https://github.com/the-djmaze/snappymail/issues/1069
exit;
// return new SnappyMailResponse();
}
} catch (\Throwable $e) {
// Ignore login failure
}
}
// Check if OpenID Connect (OIDC) is enabled and used for login
// https://apps.nextcloud.com/apps/oidc_login
public static function isOIDCLogin() : bool
{
$config = Server::get(IConfig::class);
if ($config->getAppValue('snappymail', 'snappymail-autologin-oidc', false)) {
// Check if the OIDC Login app is enabled
if (Server::get(IAppManager::class)->isEnabledForUser('oidc_login')) {
// Check if session is an OIDC Login
$ocSession = Server::get(ISession::class);
if ($ocSession->get('is_oidc')) {
// IToken->getPassword() ???
if ($ocSession->get('oidc_access_token')) {
return true;
}
\SnappyMail\Log::debug('Nextcloud', 'OIDC access_token missing');
} else {
\SnappyMail\Log::debug('Nextcloud', 'No OIDC login');
}
} else {
\SnappyMail\Log::debug('Nextcloud', 'OIDC login disabled');
}
}
return false;
}
private static function getLoginCredentials() : array
{
$user = Server::get(IUserSession::class)->getUser();
$sUID = $user ? $user->getUID() : '';
$config = Server::get(IConfig::class);
$ocSession = Server::get(ISession::class);
if (!$sUID) {
return ['', '', ''];
}
// If the user has set credentials for SnappyMail in their personal settings,
// this has the first priority.
$sEmail = $config->getUserValue($sUID, 'snappymail', 'snappymail-email');
$sPassword = $config->getUserValue($sUID, 'snappymail', 'passphrase')
?: $config->getUserValue($sUID, 'snappymail', 'snappymail-password');
if ($sEmail && $sPassword) {
$sPassword = static::decodePassword($sPassword, \md5($sEmail));
if ($sPassword) {
return [$sUID, $sEmail, $sPassword];
} else {
\SnappyMail\Log::debug('Nextcloud', 'decodePassword failed for getUserValue');
}
}
// If the current user ID is identical to login ID (not valid when using account switching),
// this has the second priority.
// Note: $ocSession array access is supported but deprecated? ISession implements ArrayAccess.
if ($ocSession['snappymail-nc-uid'] == $sUID) {
// If OpenID Connect (OIDC) is enabled and used for login, use this.
if (static::isOIDCLogin()) {
$sEmail = $config->getUserValue($sUID, 'settings', 'email');
return [$sUID, $sEmail, "oidc_login|{$sUID}"];
}
// Only use the user's password in the current session if they have
// enabled auto-login using Nextcloud username or email address.
$sEmail = '';
$sPassword = '';
if ($config->getAppValue('snappymail', 'snappymail-autologin', false)) {
$sEmail = $sUID;
$sPassword = $ocSession['snappymail-passphrase'];
} else if ($config->getAppValue('snappymail', 'snappymail-autologin-with-email', false)) {
$sEmail = $config->getUserValue($sUID, 'settings', 'email');
$sPassword = $ocSession['snappymail-passphrase'];
} else {
\SnappyMail\Log::debug('Nextcloud', 'snappymail-autologin is off');
}
if ($sPassword) {
return [$sUID, $sEmail, static::decodePassword($sPassword, $sUID)];
}
} else {
\SnappyMail\Log::debug('Nextcloud', "snappymail-nc-uid mismatch '{$ocSession['snappymail-nc-uid']}' != '{$sUID}'");
}
return [$sUID, '', ''];
}
public static function getAppUrl() : string
{
return Server::get(IURLGenerator::class)->linkToRoute('snappymail.page.appGet');
}
public static function normalizeUrl(string $sUrl) : string
{
$sUrl = \rtrim(\trim($sUrl), '/\\');
if ('.php' !== \strtolower(\substr($sUrl, -4))) {
$sUrl .= '/';
}
return $sUrl;
}
public static function encodePassword(string $sPassword, string $sSalt) : string
{
static::loadApp();
return \SnappyMail\Crypt::EncryptUrlSafe($sPassword, $sSalt);
}
public static function decodePassword(string $sPassword, string $sSalt) : ?\SnappyMail\SensitiveString
{
static::loadApp();
$result = \SnappyMail\Crypt::DecryptUrlSafe($sPassword, $sSalt);
return $result ? new \SnappyMail\SensitiveString($result) : null;
}
}

View file

@ -1,3 +1,4 @@
<?php script('snappymail', 'snappymail'); ?>
<div class="section">
<form class="snappymail" action="admin.php" method="post">
<input type="hidden" name="requesttoken" value="<?php echo $_['requesttoken'] ?>" id="requesttoken">

View file

@ -1,3 +1,5 @@
<iframe id="rliframe" style="border: none; width: 100%; min-height: 100%; position: relative;" tabindex="-1" frameborder="0" src="<?php echo $_['snappymail-iframe-url']; ?>"></iframe>
<?php
// OCP\Util::addScript('snappymail', 'resize');
script('snappymail', 'snappymail');
style('snappymail', 'style');
?>
<iframe id="rliframe" style="border: none; width: 100%; min-height: 100%; position: relative;" tabindex="-1" frameborder="0" src="<?php echo $_['snappymail-iframe-url']; ?>"></iframe>

View file

@ -1,3 +1,5 @@
<?php style('snappymail', 'embed'); ?>
<link type="text/css" rel="stylesheet" href="<?php echo $_['CssLink']; ?>" />
<style id="app-boot-css"><?php echo $_['BaseAppBootCss']; ?></style>
<style id="app-theme-style"><?php echo $_['BaseAppThemeCss']; ?></style>
<div id="rl-app" data-admin="<?php echo $_['Admin']; ?>" spellcheck="false">

View file

@ -22,17 +22,17 @@ There, click on the link to go to the SnappyMail admin panel.
]]></description>
<namespace>SnappyMail</namespace>
<documentation>
<user>https://github.com/the-djmaze/snappymail/blob/master/integrations/owncloud/snappymail/README.md</user>
<website>https://github.com/the-djmaze/snappymail/wiki</website>
<user>https://github.com/nextgen-networks/snappymail/blob/master/integrations/owncloud/snappymail/README.md</user>
<website>https://github.com/nextgen-networks/snappymail/wiki</website>
</documentation>
<category>integration</category>
<category>office</category>
<category>search</category>
<category>social</category>
<screenshot>https://raw.githubusercontent.com/the-djmaze/snappymail/master/integrations/owncloud/screenshots/inbox.jpg</screenshot>
<screenshot>https://raw.githubusercontent.com/nextgen-networks/snappymail/master/integrations/owncloud/screenshots/inbox.jpg</screenshot>
<website>https://snappymail.eu</website>
<discussion>https://github.com/the-djmaze/snappymail/discussions</discussion>
<repository>https://github.com/the-djmaze/snappymail/tree/master/integrations/owncloud</repository>
<discussion>https://github.com/nextgen-networks/snappymail/discussions</discussion>
<repository>https://github.com/nextgen-networks/snappymail/tree/master/integrations/owncloud</repository>
<bugs>https://github.com/the-djmaze/snappymail/issues</bugs>
<dependencies>
<php min-version="7.4" min-int-size="64" />

View file

@ -153,7 +153,7 @@ sub script_snappymail_files
local ($d, $ver, $opts, $upgrade) = @_;
local @files = ( { 'name' => "source",
'file' => "snappymail-$ver.tar.gz",
'url' => "https://github.com/the-djmaze/snappymail/releases/download/v${ver}/snappymail-${ver}.tar.gz" },
'url' => "https://github.com/nextgen-networks/snappymail/releases/download/v${ver}/snappymail-${ver}.tar.gz" },
);
return @files;
}

View file

@ -12,7 +12,7 @@
},
"repository": {
"type": "git",
"url": "git://github.com/the-djmaze/snappymail.git"
"url": "git://github.com/nextgen-networks/snappymail.git"
},
"scripts": {
"watch-css": "gulp watchCss"

View file

@ -7,7 +7,7 @@
* be set to to 'example.com'.
*
* Based on:
* https://github.com/the-djmaze/snappymail/blob/master/plugins/override-smtp-credentials/index.php
* https://github.com/nextgen-networks/snappymail/blob/master/plugins/override-smtp-credentials/index.php
*
*/

View file

@ -148,7 +148,7 @@ class LoginGMailPlugin extends \RainLoop\Plugins\AbstractPlugin
->SetLabel('Client ID')
->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING)
->SetAllowedInJs()
->SetDescription('https://github.com/the-djmaze/snappymail/wiki/FAQ#gmail'),
->SetDescription('https://github.com/nextgen-networks/snappymail/wiki/FAQ#gmail'),
\RainLoop\Plugins\Property::NewInstance('client_secret')
->SetLabel('Client Secret')
->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING)

View file

@ -166,7 +166,7 @@ class LoginO365Plugin extends \RainLoop\Plugins\AbstractPlugin
->SetLabel('Client ID')
->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING)
->SetAllowedInJs()
->SetDescription('https://github.com/the-djmaze/snappymail/wiki/FAQ#o365'),
->SetDescription('https://github.com/nextgen-networks/snappymail/wiki/FAQ#o365'),
\RainLoop\Plugins\Property::NewInstance('client_secret')
->SetLabel('Client Secret')
->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING)

View file

@ -4,8 +4,9 @@ class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Nextcloud',
VERSION = '2.38.1',
RELEASE = '2024-10-08',
// Keep upstream metadata if you prefer; this is not functional.
VERSION = '2.38.2',
RELEASE = '2026-02-06',
CATEGORY = 'Integrations',
DESCRIPTION = 'Integrate with Nextcloud v20+',
REQUIRED = '2.38.0';
@ -36,14 +37,13 @@ class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin
$this->addTemplate('templates/PopupsNextcloudFiles.html');
$this->addTemplate('templates/PopupsNextcloudCalendars.html');
// $this->addHook('login.credentials.step-2', 'loginCredentials2');
// $this->addHook('login.credentials', 'loginCredentials');
// $this->addHook('login.credentials.step-2', 'loginCredentials2');
// $this->addHook('login.credentials', 'loginCredentials');
$this->addHook('imap.before-login', 'beforeLogin');
$this->addHook('smtp.before-login', 'beforeLogin');
$this->addHook('sieve.before-login', 'beforeLogin');
} else {
\SnappyMail\Log::debug('Nextcloud', 'NOT integrated');
// \OC::$server->getConfig()->getAppValue('snappymail', 'snappymail-no-embed');
$this->addHook('main.content-security-policy', 'ContentSecurityPolicy');
}
}
@ -72,13 +72,7 @@ class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin
public function loginCredentials(string &$sEmail, string &$sLogin, ?string &$sPassword = null) : void
{
/**
* This has an issue.
* When user changes email address, all settings are gone as the new
* _data_/_default_/storage/{domain}/{local-part} is used
*/
// $ocUser = \OC::$server->getUserSession()->getUser();
// $sEmail = $ocUser->getEMailAddress() ?: $ocUser->getPrimaryEMailAddress() ?: $sEmail;
// left intentionally as upstream (commented in upstream)
}
public function loginCredentials2(string &$sEmail, ?string &$sPassword = null) : void
@ -89,83 +83,145 @@ class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin
public function beforeLogin(\RainLoop\Model\Account $oAccount, \MailSo\Net\NetClient $oClient, \MailSo\Net\ConnectSettings $oSettings) : void
{
// Only login with OIDC access token if
// it is enabled in config, the user is currently logged in with OIDC,
// the current snappymail account is the OIDC account and no account defined explicitly
if ($oAccount instanceof \RainLoop\Model\MainAccount
&& \OCA\SnappyMail\Util\SnappyMailHelper::isOIDCLogin()
// && $oClient->supportsAuthType('OAUTHBEARER') // v2.28
&& \str_starts_with($oSettings->passphrase, 'oidc_login|')
&& \OCA\SnappyMail\Util\SnappyMailHelper::isOIDCLogin()
&& \str_starts_with($oSettings->passphrase, 'oidc_login|')
) {
// $oSettings->passphrase = \OC::$server->getSession()->get('snappymail-passphrase');
$oSettings->passphrase = \OC::$server->getSession()->get('oidc_access_token');
$oSettings->passphrase = (string) \OC::$server->getSession()->get('oidc_access_token');
\array_unshift($oSettings->SASLMechanisms, 'OAUTHBEARER');
}
}
/*
\OC::$server->getCalendarManager();
\OC::$server->getLDAPProvider();
*/
/**
* Attach a Nextcloud file into SnappyMail temp storage (for composing).
*/
public function NextcloudAttachFile() : array
{
$aResult = [
'success' => false,
'tempName' => ''
];
$sFile = $this->jsonParam('file', '');
$oFiles = \OCP\Files::getStorage('files');
if ($oFiles && $oFiles->is_file($sFile) && $fp = $oFiles->fopen($sFile, 'rb')) {
$sFile = (string) $this->jsonParam('file', '');
try {
$oActions = \RainLoop\Api::Actions();
$oAccount = $oActions->getAccountFromToken();
if ($oAccount) {
$sSavedName = 'nextcloud-file-' . \sha1($sFile . \microtime());
if (!$oActions->FilesProvider()->PutFile($oAccount, $sSavedName, $fp)) {
$aResult['error'] = 'failed';
} else {
$aResult['tempName'] = $sSavedName;
$aResult['success'] = true;
}
if (!$oAccount) {
$aResult['error'] = 'no-account';
return $this->jsonResponse(__FUNCTION__, $aResult);
}
$user = \OC::$server->getUserSession()->getUser();
if (!$user) {
$aResult['error'] = 'no-user';
return $this->jsonResponse(__FUNCTION__, $aResult);
}
/** @var \OCP\Files\IRootFolder $root */
$root = \OC::$server->get(\OCP\Files\IRootFolder::class);
$userFolder = $root->getUserFolder($user->getUID());
// SnappyMail sends "/Documents/app.svg" style paths; userFolder expects relative.
$relPath = \ltrim($sFile, '/');
if ($relPath === '') {
$aResult['error'] = 'empty-path';
return $this->jsonResponse(__FUNCTION__, $aResult);
}
$node = $userFolder->get($relPath);
if (!($node instanceof \OCP\Files\File)) {
$aResult['error'] = 'not-a-file';
return $this->jsonResponse(__FUNCTION__, $aResult);
}
$fp = $node->fopen('rb');
if (!$fp) {
$aResult['error'] = 'open-failed';
return $this->jsonResponse(__FUNCTION__, $aResult);
}
$sSavedName = 'nextcloud-file-' . \sha1($sFile . \microtime(true));
$ok = $oActions->FilesProvider()->PutFile($oAccount, $sSavedName, $fp);
@\fclose($fp);
if (!$ok) {
$aResult['error'] = 'failed';
} else {
$aResult['tempName'] = $sSavedName;
$aResult['success'] = true;
}
} catch (\Throwable $e) {
$aResult['error'] = 'exception';
$aResult['errorMessage'] = $e->getMessage();
}
return $this->jsonResponse(__FUNCTION__, $aResult);
}
/**
* Save an .eml message from SnappyMail into Nextcloud Files.
*/
public function NextcloudSaveMsg() : array
{
$sSaveFolder = \ltrim($this->jsonParam('folder', ''), '/');
// $aValues = \RainLoop\Api::Actions()->decodeRawKey($this->jsonParam('msgHash', ''));
$msgHash = $this->jsonParam('msgHash', '');
$sSaveFolder = \ltrim((string) $this->jsonParam('folder', ''), '/');
$msgHash = (string) $this->jsonParam('msgHash', '');
$aValues = \json_decode(\MailSo\Base\Utils::UrlSafeBase64Decode($msgHash), true);
$aResult = [
'folder' => '',
'filename' => '',
'success' => false
'success' => false,
];
if ($sSaveFolder && !empty($aValues['folder']) && !empty($aValues['uid'])) {
if (!empty($aValues['folder']) && !empty($aValues['uid'])) {
$oActions = \RainLoop\Api::Actions();
$oMailClient = $oActions->MailClient();
if (!$oMailClient->IsLoggined()) {
$oAccount = $oActions->getAccountFromToken();
$oAccount->ImapConnectAndLogin($oActions->Plugins(), $oMailClient->ImapClient(), $oActions->Config());
}
// Default folder
$sSaveFolder = $sSaveFolder ?: 'Emails';
$oFiles = \OCP\Files::getStorage('files');
if ($oFiles) {
$oFiles->is_dir($sSaveFolder) || $oFiles->mkdir($sSaveFolder);
}
$aResult['folder'] = $sSaveFolder;
$aResult['filename'] = \MailSo\Base\Utils::SecureFileName(
\mb_substr($this->jsonParam('filename', '') ?: \date('YmdHis'), 0, 100)
) . '.' . \md5($msgHash) . '.eml';
$sSaveFolder = \trim($sSaveFolder, '/');
// Use Nextcloud Files API (per-user)
$userFolder = \OC::$server->getUserFolder();
// Create folder path (supports nested folders)
$folderNode = $this->ensureFolderPath($userFolder, $sSaveFolder);
$filenameBase = (string) ($this->jsonParam('filename', '') ?: \date('YmdHis'));
$safeBase = \MailSo\Base\Utils::SecureFileName(\mb_substr($filenameBase, 0, 100));
$filename = $safeBase . '.' . \md5($msgHash) . '.eml';
$aResult['folder'] = $sSaveFolder;
$aResult['filename'] = $filename;
$oMailClient->MessageMimeStream(
function ($rResource) use ($oFiles, $aResult) {
if (\is_resource($rResource)) {
$aResult['success'] = $oFiles->file_put_contents("{$aResult['folder']}/{$aResult['filename']}", $rResource);
function ($rResource) use (&$aResult, $folderNode, $filename) {
if (!\is_resource($rResource)) {
return;
}
// If exists, overwrite by deleting and recreating (keeps behavior deterministic)
if ($folderNode->nodeExists($filename)) {
$existing = $folderNode->get($filename);
if ($existing instanceof \OCP\Files\File) {
$existing->delete();
}
}
$fileNode = $folderNode->newFile($filename);
$out = $fileNode->fopen('w');
if (\is_resource($out)) {
\stream_copy_to_stream($rResource, $out);
\fclose($out);
$aResult['success'] = true;
}
},
(string) $aValues['folder'],
@ -177,118 +233,148 @@ class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin
return $this->jsonResponse(__FUNCTION__, $aResult);
}
/**
* Save message attachments from SnappyMail into Nextcloud Files.
* (Used by SnappyMail "save attachments to nextcloud" actions.)
*/
public function DoAttachmentsActions(\SnappyMail\AttachmentsAction $data)
{
if (static::isLoggedIn() && 'nextcloud' === $data->action) {
$oFiles = \OCP\Files::getStorage('files');
if ($oFiles && \method_exists($oFiles, 'file_put_contents')) {
$sSaveFolder = \ltrim($this->jsonParam('NcFolder', ''), '/');
$sSaveFolder = $sSaveFolder ?: 'Attachments';
$oFiles->is_dir($sSaveFolder) || $oFiles->mkdir($sSaveFolder);
$data->result = true;
foreach ($data->items as $aItem) {
$sSavedFileName = empty($aItem['fileName']) ? 'file.dat' : $aItem['fileName'];
if (!empty($aItem['data'])) {
$sSavedFileNameFull = static::SmartFileExists($sSaveFolder.'/'.$sSavedFileName, $oFiles);
if (!$oFiles->file_put_contents($sSavedFileNameFull, $aItem['data'])) {
$data->result = false;
}
} else if (!empty($aItem['fileHash'])) {
$fFile = $data->filesProvider->GetFile($data->account, $aItem['fileHash'], 'rb');
if (\is_resource($fFile)) {
$sSavedFileNameFull = static::SmartFileExists($sSaveFolder.'/'.$sSavedFileName, $oFiles);
if (!$oFiles->file_put_contents($sSavedFileNameFull, $fFile)) {
$data->result = false;
}
if (\is_resource($fFile)) {
\fclose($fFile);
}
}
if (!static::isLoggedIn() || 'nextcloud' !== $data->action) {
return;
}
try {
// Target folder in Nextcloud
$sSaveFolder = \ltrim((string) $this->jsonParam('NcFolder', ''), '/');
$sSaveFolder = $sSaveFolder ?: 'Attachments';
$sSaveFolder = \trim($sSaveFolder, '/');
$userFolder = \OC::$server->getUserFolder();
$folderNode = $this->ensureFolderPath($userFolder, $sSaveFolder);
$data->result = true;
foreach ($data->items as $aItem) {
$sSavedFileName = empty($aItem['fileName']) ? 'file.dat' : (string) $aItem['fileName'];
$sSavedFileName = \MailSo\Base\Utils::SecureFileName(\mb_substr($sSavedFileName, 0, 180));
$finalName = $this->uniqueNameInFolder($folderNode, $sSavedFileName);
// Create file node
$fileNode = $folderNode->newFile($finalName);
$out = $fileNode->fopen('w');
if (!\is_resource($out)) {
$data->result = false;
continue;
}
$wrote = false;
if (!empty($aItem['data'])) {
// Raw data string
$bytes = \fwrite($out, (string) $aItem['data']);
$wrote = ($bytes !== false);
} else if (!empty($aItem['fileHash'])) {
// Stream from SnappyMail temp storage
$fFile = $data->filesProvider->GetFile($data->account, (string) $aItem['fileHash'], 'rb');
if (\is_resource($fFile)) {
\stream_copy_to_stream($fFile, $out);
\fclose($fFile);
$wrote = true;
}
}
\fclose($out);
if (!$wrote) {
// Clean up the empty file
try { $fileNode->delete(); } catch (\Throwable $e) {}
$data->result = false;
}
}
} catch (\Throwable $e) {
$data->result = false;
}
}
public function FilterAppData($bAdmin, &$aResult) : void
{
if (!$bAdmin && \is_array($aResult)) {
$ocUser = \OC::$server->getUserSession()->getUser();
$sUID = $ocUser->getUID();
$oUrlGen = \OC::$server->getURLGenerator();
$sWebDAV = $oUrlGen->getAbsoluteURL($oUrlGen->linkTo('', 'remote.php') . '/dav');
// $sWebDAV = \OCP\Util::linkToRemote('dav');
$aResult['Nextcloud'] = [
'UID' => $sUID,
'WebDAV' => $sWebDAV,
'CalDAV' => $this->Config()->Get('plugin', 'calendar', false)
// 'WebDAV_files' => $sWebDAV . '/files/' . $sUID
];
if (empty($aResult['Auth'])) {
$config = \OC::$server->getConfig();
$sEmail = '';
// Only store the user's password in the current session if they have
// enabled auto-login using Nextcloud username or email address.
if ($config->getAppValue('snappymail', 'snappymail-autologin', false)) {
$sEmail = $sUID;
} else if ($config->getAppValue('snappymail', 'snappymail-autologin-with-email', false)) {
$sEmail = $config->getUserValue($sUID, 'settings', 'email', '');
} else {
\SnappyMail\Log::debug('Nextcloud', 'snappymail-autologin is off');
}
// If the user has set credentials for SnappyMail in their personal
// settings, override everything before and use those instead.
$sCustomEmail = $config->getUserValue($sUID, 'snappymail', 'snappymail-email', '');
if ($sCustomEmail) {
$sEmail = $sCustomEmail;
}
if (!$sEmail) {
$sEmail = $ocUser->getEMailAddress();
// ?: $ocUser->getPrimaryEMailAddress();
}
/*
if ($config->getAppValue('snappymail', 'snappymail-autologin-oidc', false)) {
if (\OC::$server->getSession()->get('is_oidc')) {
$sEmail = "{$sUID}@nextcloud";
$aResult['DevPassword'] = \OC::$server->getSession()->get('oidc_access_token');
} else {
\SnappyMail\Log::debug('Nextcloud', 'Not an OIDC login');
}
} else {
\SnappyMail\Log::debug('Nextcloud', 'OIDC is off');
}
*/
$aResult['DevEmail'] = $sEmail ?: '';
} else if (!empty($aResult['ContactsSync'])) {
$bSave = false;
if (empty($aResult['ContactsSync']['Url'])) {
$aResult['ContactsSync']['Url'] = "{$sWebDAV}/addressbooks/users/{$sUID}/contacts/";
if ($bAdmin || !\is_array($aResult)) {
return;
}
$ocUser = \OC::$server->getUserSession()->getUser();
if (!$ocUser) {
return;
}
$sUID = $ocUser->getUID();
$oUrlGen = \OC::$server->getURLGenerator();
$sWebDAV = $oUrlGen->getAbsoluteURL($oUrlGen->linkTo('', 'remote.php') . '/dav');
$aResult['Nextcloud'] = [
'UID' => $sUID,
'WebDAV' => $sWebDAV,
'CalDAV' => $this->Config()->Get('plugin', 'calendar', false)
];
if (empty($aResult['Auth'])) {
$config = \OC::$server->getConfig();
$sEmail = '';
if ($config->getAppValue('snappymail', 'snappymail-autologin', false)) {
$sEmail = $sUID;
} else if ($config->getAppValue('snappymail', 'snappymail-autologin-with-email', false)) {
$sEmail = $config->getUserValue($sUID, 'settings', 'email', '');
} else {
\SnappyMail\Log::debug('Nextcloud', 'snappymail-autologin is off');
}
// User-set SnappyMail credentials override
$sCustomEmail = $config->getUserValue($sUID, 'snappymail', 'snappymail-email', '');
if ($sCustomEmail) {
$sEmail = $sCustomEmail;
}
if (!$sEmail) {
$sEmail = $ocUser->getEMailAddress();
}
$aResult['DevEmail'] = $sEmail ?: '';
} else if (!empty($aResult['ContactsSync'])) {
$bSave = false;
if (empty($aResult['ContactsSync']['Url'])) {
$aResult['ContactsSync']['Url'] = "{$sWebDAV}/addressbooks/users/{$sUID}/contacts/";
$bSave = true;
}
if (empty($aResult['ContactsSync']['User'])) {
$aResult['ContactsSync']['User'] = $sUID;
$bSave = true;
}
// FIX: do not array-access the session; use ->get()
$pass = (string) \OC::$server->getSession()->get('snappymail-passphrase');
if ($pass) {
$pass = \SnappyMail\Crypt::DecryptUrlSafe($pass, $sUID);
if ($pass) {
$aResult['ContactsSync']['Password'] = $pass;
$bSave = true;
}
if (empty($aResult['ContactsSync']['User'])) {
$aResult['ContactsSync']['User'] = $sUID;
$bSave = true;
}
$pass = \OC::$server->getSession()['snappymail-passphrase'];
if ($pass/* && empty($aResult['ContactsSync']['Password'])*/) {
$pass = \SnappyMail\Crypt::DecryptUrlSafe($pass, $sUID);
if ($pass) {
$aResult['ContactsSync']['Password'] = $pass;
$bSave = true;
}
}
if ($bSave) {
$oActions = \RainLoop\Api::Actions();
$oActions->setContactsSyncData(
$oActions->getAccountFromToken(),
array(
'Mode' => $aResult['ContactsSync']['Mode'],
'User' => $aResult['ContactsSync']['User'],
'Password' => $aResult['ContactsSync']['Password'],
'Url' => $aResult['ContactsSync']['Url']
)
);
}
}
if ($bSave) {
$oActions = \RainLoop\Api::Actions();
$oActions->setContactsSyncData(
$oActions->getAccountFromToken(),
[
'Mode' => $aResult['ContactsSync']['Mode'],
'User' => $aResult['ContactsSync']['User'],
'Password' => $aResult['ContactsSync']['Password'],
'Url' => $aResult['ContactsSync']['Url']
]
);
}
}
}
@ -301,116 +387,124 @@ class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin
$userLang = \OC::$server->getConfig()->getUserValue($userId, 'core', 'lang', 'en');
$userLang = \strtr($userLang, '_', '-');
$sLanguage = $this->determineLocale($userLang, $aResultLang);
// Check if $sLanguage is null
if (!$sLanguage) {
$sLanguage = 'en'; // Assign 'en' if $sLanguage is null
$sLanguage = 'en';
}
}
}
/**
* Determine locale from user language.
*
* @param string $langCode The name of the input.
* @param array $languagesArray The value of the array.
*
* @return string return locale
*/
private function determineLocale(string $langCode, array $languagesArray) : ?string
{
// Direct check for the language code
if (\in_array($langCode, $languagesArray)) {
if (\in_array($langCode, $languagesArray, true)) {
return $langCode;
}
// Check without country code
if (\str_contains($langCode, '-')) {
$langCode = \explode('-', $langCode)[0];
if (\in_array($langCode, $languagesArray)) {
if (\in_array($langCode, $languagesArray, true)) {
return $langCode;
}
}
// Check with uppercase country code
$langCodeWithUpperCase = $langCode . '-' . \strtoupper($langCode);
if (\in_array($langCodeWithUpperCase, $languagesArray)) {
if (\in_array($langCodeWithUpperCase, $languagesArray, true)) {
return $langCodeWithUpperCase;
}
// If no match is found
return null;
}
/**
* @param mixed $mResult
*/
public function MainFabrica(string $sName, &$mResult)
{
if (static::isLoggedIn()) {
if ('suggestions' === $sName && $this->Config()->Get('plugin', 'suggestions', true)) {
if (!\is_array($mResult)) {
$mResult = array();
}
include_once __DIR__ . '/NextcloudContactsSuggestions.php';
$mResult[] = new NextcloudContactsSuggestions(
$this->Config()->Get('plugin', 'ignoreSystemAddressbook', true)
);
}
/*
if ($this->Config()->Get('plugin', 'storage', false) && ('storage' === $sName || 'storage-local' === $sName)) {
require_once __DIR__ . '/storage.php';
$oDriver = new \NextcloudStorage(APP_PRIVATE_DATA.'storage', $sName === 'storage-local');
}
*/
if (!static::isLoggedIn()) {
return;
}
if ('suggestions' === $sName && $this->Config()->Get('plugin', 'suggestions', true)) {
if (!\is_array($mResult)) {
$mResult = [];
}
include_once __DIR__ . '/NextcloudContactsSuggestions.php';
$mResult[] = new NextcloudContactsSuggestions(
$this->Config()->Get('plugin', 'ignoreSystemAddressbook', true)
);
}
// storage hook left as upstream (commented)
}
protected function configMapping() : array
{
return array(
return [
\RainLoop\Plugins\Property::NewInstance('suggestions')->SetLabel('Suggestions')
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
->SetDefaultValue(true),
\RainLoop\Plugins\Property::NewInstance('ignoreSystemAddressbook')->SetLabel('Ignore system addressbook')
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
->SetDefaultValue(true),
/*
\RainLoop\Plugins\Property::NewInstance('storage')->SetLabel('Use Nextcloud user ID in config storage path')
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
->SetDefaultValue(false)
*/
\RainLoop\Plugins\Property::NewInstance('calendar')->SetLabel('Enable "Put ICS in calendar"')
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
->SetDefaultValue(false)
);
];
}
private static function SmartFileExists(string $sFilePath, $oFiles) : string
/**
* Ensure a (possibly nested) folder path exists under the given base folder.
*/
private function ensureFolderPath(\OCP\Files\Folder $baseFolder, string $path) : \OCP\Files\Folder
{
$sFilePath = \str_replace('\\', '/', \trim($sFilePath));
if (!$oFiles->file_exists($sFilePath)) {
return $sFilePath;
$path = \trim($path, '/');
if ($path === '') {
return $baseFolder;
}
$aFileInfo = \pathinfo($sFilePath);
$iIndex = 0;
while (true) {
++$iIndex;
$sFilePathNew = $aFileInfo['dirname'].'/'.
\preg_replace('/\(\d{1,2}\)$/', '', $aFileInfo['filename']).
' ('.$iIndex.')'.
(empty($aFileInfo['extension']) ? '' : '.'.$aFileInfo['extension'])
;
if (!$oFiles->file_exists($sFilePathNew)) {
return $sFilePathNew;
}
if (10 < $iIndex) {
break;
$folderNode = $baseFolder;
foreach (\array_filter(\explode('/', $path), 'strlen') as $part) {
$part = (string) $part;
if ($folderNode->nodeExists($part)) {
$node = $folderNode->get($part);
if ($node instanceof \OCP\Files\Folder) {
$folderNode = $node;
} else {
// name collision: file exists where folder needed; create a suffixed folder
$folderNode = $folderNode->newFolder($part . '-folder');
}
} else {
$folderNode = $folderNode->newFolder($part);
}
}
return $sFilePath;
return $folderNode;
}
/**
* Return a unique filename inside a Nextcloud folder (keeps original extension).
*/
private function uniqueNameInFolder(\OCP\Files\Folder $folderNode, string $fileName) : string
{
$fileName = \trim($fileName);
if ($fileName === '') {
$fileName = 'file.dat';
}
if (!$folderNode->nodeExists($fileName)) {
return $fileName;
}
$info = \pathinfo($fileName);
$base = $info['filename'] ?? 'file';
$ext = isset($info['extension']) && $info['extension'] !== '' ? ('.' . $info['extension']) : '';
for ($i = 1; $i <= 50; $i++) {
$try = $base . ' (' . $i . ')' . $ext;
if (!$folderNode->nodeExists($try)) {
return $try;
}
}
// fallback
return $fileName;
}
}

View file

@ -40,9 +40,9 @@
<br />
</span>
<br />
<a class="btn" href="https://github.com/the-djmaze/snappymail/releases" target="_blank" data-i18n="TAB_ABOUT/BUTTON_RELEASES"></a>
<a class="btn" href="https://github.com/nextgen-networks/snappymail/releases" target="_blank" data-i18n="TAB_ABOUT/BUTTON_RELEASES"></a>
<a class="btn" data-bind="visible: coreUpdatable, click: updateCoreData" data-i18n="GLOBAL/UPDATE"></a>
<a class="btn" href="https://github.com/the-djmaze/snappymail/releases/latest" data-bind="visible: !coreUpdatable()" data-i18n="GLOBAL/UPDATE"></a>
<a class="btn" href="https://github.com/nextgen-networks/snappymail/releases/latest" data-bind="visible: !coreUpdatable()" data-i18n="GLOBAL/UPDATE"></a>
</div>
<div data-bind="visible: 'up-to-date' === statusType()">
<i style="color: green"></i>

View file

@ -9,8 +9,8 @@
<div class="alert alert-error span8" style="margin-top: 10px;">
<h4 data-i18n="TAB_GENERAL/ALERT_WARNING"></h4>
<div data-i18n="TAB_GENERAL/ALERT_DATA_ACCESS"></div>
<a href="https://github.com/the-djmaze/snappymail/wiki/Installation-instructions#secure-data-folder"
target="_blank"><strong>https://github.com/the-djmaze/snappymail/wiki/Installation-instructions</strong></a>
<a href="https://github.com/nextgen-networks/snappymail/wiki/Installation-instructions#secure-data-folder"
target="_blank"><strong>https://github.com/nextgen-networks/snappymail/wiki/Installation-instructions</strong></a>
</div>
</div>
<div class="form-horizontal">

View file

@ -93,7 +93,7 @@
<h4>
<span data-i18n="POPUPS_DOMAIN/DISABLE_CAPABILITIES"></span>
<a style="margin-left:0.5em" href="https://github.com/the-djmaze/snappymail/wiki/IMAP-capabilities" target="_blank"></a>
<a style="margin-left:0.5em" href="https://github.com/nextgen-networks/snappymail/wiki/IMAP-capabilities" target="_blank"></a>
</h4>
<div data-bind="foreach: imapCapabilities" style="columns: 16em auto">