Compare commits

..

11 Commits

Author SHA1 Message Date
b86ecbfc99 Add support for SMTP
Signed-off-by: Shin'ya Minazuki <shinyoukai@laidback.moe>
2026-01-11 20:29:57 -03:00
tflidd
51feb76b47 Merge pull request #272 from hschletz/fix-270
Set up logger instance in base constructor
2025-09-23 14:56:12 +02:00
Andy Scherzinger
21b7ad5b3a Merge pull request #277 from nextcloud/remove-dead-link
Update README.md
2025-08-12 15:28:03 +02:00
Anna
65b7b2b5e6 Update README.md
Signed-off-by: Anna <anna@nextcloud.com>
2025-08-12 15:00:06 +02:00
hschletz
21fa01c4ba Set up logger instance in base constructor. Fixes #270
Signed-off-by: Holger Schletz <holger.schletz@web.de>
2025-04-22 18:22:22 +02:00
tflidd
79ae3e9235 Merge pull request #261 from ervee/patch-1
Make NC30 compatible
2025-03-10 17:43:58 +01:00
dependabot[bot]
6d4405ecff Merge pull request #262 from nextcloud/dependabot/composer/symfony/process-5.4.46 2024-11-06 20:51:23 +00:00
dependabot[bot]
2884c6e749 Bump symfony/process from 5.4.7 to 5.4.46
Bumps [symfony/process](https://github.com/symfony/process) from 5.4.7 to 5.4.46.
- [Release notes](https://github.com/symfony/process/releases)
- [Changelog](https://github.com/symfony/process/blob/7.1/CHANGELOG.md)
- [Commits](https://github.com/symfony/process/compare/v5.4.7...v5.4.46)

---
updated-dependencies:
- dependency-name: symfony/process
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-06 20:47:58 +00:00
Ralf
47e8099502 Make NC30 compatible
Make the app compatible with Nextcloud 30

Signed-off-by: Ralf <ervee@moskovic.org>
2024-10-28 20:18:45 +01:00
Joas Schilling
412e397069 Merge pull request #258 from nextcloud/ci/noid/update-workflow-109
ci: Update workflows
2024-09-10 11:48:12 +02:00
Joas Schilling
8b18c65014 ci: Update workflows
[skip-ci]

Signed-off-by: Joas Schilling <coding@schilljs.com>
2024-09-10 11:47:58 +02:00
12 changed files with 180 additions and 85 deletions

View File

@@ -1,51 +0,0 @@
# This workflow is provided via the organization template repository
#
# https://github.com/nextcloud/.github
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
name: Rebase command
on:
issue_comment:
types: created
permissions:
contents: read
jobs:
rebase:
runs-on: ubuntu-latest
permissions:
contents: none
# On pull requests and if the comment starts with `/rebase`
if: github.event.issue.pull_request != '' && startsWith(github.event.comment.body, '/rebase')
steps:
- name: Add reaction on start
uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1
with:
token: ${{ secrets.COMMAND_BOT_PAT }}
repository: ${{ github.event.repository.full_name }}
comment-id: ${{ github.event.comment.id }}
reaction-type: "+1"
- name: Checkout the latest code
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
with:
fetch-depth: 0
token: ${{ secrets.COMMAND_BOT_PAT }}
- name: Automatic Rebase
uses: cirrus-actions/rebase@b87d48154a87a85666003575337e27b8cd65f691 # 1.8
env:
GITHUB_TOKEN: ${{ secrets.COMMAND_BOT_PAT }}
- name: Add reaction on failure
uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1
if: failure()
with:
token: ${{ secrets.COMMAND_BOT_PAT }}
repository: ${{ github.event.repository.full_name }}
comment-id: ${{ github.event.comment.id }}
reaction-type: "-1"

View File

@@ -220,9 +220,11 @@ Add the following to your `config.php`:
**⚠⚠ Warning:** If you need to set *5 (Hashed Password in Database)* to false, your Prosody Instance is storing passwords in plaintext. This is insecure and not recommended. We highly recommend that you change your Prosody configuration to protect the passwords of your Prosody users. ⚠⚠
## SMTP
Works the same way as the IMAP section above, but authenticates against an SMTP server.
Alternatives
------------
Other extensions allow connecting to external user databases directly via SQL, which may be faster:
* [user_sql](https://github.com/nextcloud/user_sql)
* [user_backend_sql_raw](https://github.com/PanCakeConnaisseur/user_backend_sql_raw)

View File

@@ -11,12 +11,13 @@
* FTP
* WebDAV
* HTTP BasicAuth
* SMTP
* SSH
* XMPP
Read the [documentation](https://github.com/nextcloud/user_external#readme) to learn how to configure it!
]]></description>
<version>3.4.0</version>
<version>3.5.0</version>
<licence>agpl</licence>
<author>Robin Appelman</author>
<namespace>UserExternal</namespace>
@@ -33,6 +34,6 @@ Read the [documentation](https://github.com/nextcloud/user_external#readme) to l
<bugs>https://github.com/nextcloud/user_external/issues</bugs>
<repository type="git">https://github.com/nextcloud/user_external.git</repository>
<dependencies>
<nextcloud min-version="25" max-version="29" />
<nextcloud min-version="25" max-version="32" />
</dependencies>
</info>

31
composer.lock generated
View File

@@ -3707,26 +3707,23 @@
},
{
"name": "symfony/polyfill-php80",
"version": "v1.25.0",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c"
"reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/4407588e0d3f1f52efb65fbe92babe41f37fe50c",
"reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
"reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
"shasum": ""
},
"require": {
"php": ">=7.1"
"php": ">=7.2"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
@@ -3770,7 +3767,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.25.0"
"source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0"
},
"funding": [
{
@@ -3786,7 +3783,7 @@
"type": "tidelift"
}
],
"time": "2022-03-04T08:16:47+00:00"
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-php81",
@@ -3869,16 +3866,16 @@
},
{
"name": "symfony/process",
"version": "v5.4.7",
"version": "v5.4.46",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "38a44b2517b470a436e1c944bf9b9ba3961137fb"
"reference": "01906871cb9b5e3cf872863b91aba4ec9767daf4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/38a44b2517b470a436e1c944bf9b9ba3961137fb",
"reference": "38a44b2517b470a436e1c944bf9b9ba3961137fb",
"url": "https://api.github.com/repos/symfony/process/zipball/01906871cb9b5e3cf872863b91aba4ec9767daf4",
"reference": "01906871cb9b5e3cf872863b91aba4ec9767daf4",
"shasum": ""
},
"require": {
@@ -3911,7 +3908,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/v5.4.7"
"source": "https://github.com/symfony/process/tree/v5.4.46"
},
"funding": [
{
@@ -3927,7 +3924,7 @@
"type": "tidelift"
}
],
"time": "2022-03-18T16:18:52+00:00"
"time": "2024-11-06T09:18:28+00:00"
},
{
"name": "symfony/service-contracts",
@@ -4279,5 +4276,5 @@
"platform-overrides": {
"php": "7.3"
},
"plugin-api-version": "2.3.0"
"plugin-api-version": "2.6.0"
}

View File

@@ -9,6 +9,9 @@
*/
namespace OCA\UserExternal;
use OCP\Server;
use Psr\Log\LoggerInterface;
/**
* Base class for external auth implementations that stores users
* on their first login in a local table.
@@ -23,6 +26,7 @@ namespace OCA\UserExternal;
*/
abstract class Base extends \OC\User\Backend {
protected $backend = '';
protected readonly LoggerInterface $logger;
/**
* Create new instance, set backend name
@@ -31,6 +35,7 @@ abstract class Base extends \OC\User\Backend {
*/
public function __construct($backend) {
$this->backend = $backend;
$this->logger = Server::get(LoggerInterface::class);
}
/**

View File

@@ -37,14 +37,14 @@ class BasicAuth extends Base {
);
$canary = get_headers($this->authUrl, 1, $context);
if (!$canary) {
\OC::$server->getLogger()->error(
$this->logger->error(
'ERROR: Not possible to connect to BasicAuth Url: '.$this->authUrl,
['app' => 'user_external']
);
return false;
}
if (!isset(array_change_key_case($canary, CASE_LOWER)['www-authenticate'])) {
\OC::$server->getLogger()->error(
$this->logger->error(
'ERROR: Mis-configured BasicAuth Url: '.$this->authUrl.', provided URL does not do authentication!',
['app' => 'user_external']
);
@@ -61,7 +61,7 @@ class BasicAuth extends Base {
$headers = get_headers($this->authUrl, 1, $context);
if (!$headers) {
\OC::$server->getLogger()->error(
$this->logger->error(
'ERROR: Not possible to connect to BasicAuth Url: '.$this->authUrl,
['app' => 'user_external']
);
@@ -82,7 +82,7 @@ class BasicAuth extends Base {
$this->storeUser($uid);
return $uid;
case "3":
\OC::$server->getLogger()->error(
$this->logger->error(
'ERROR: Too many redirects from BasicAuth Url: '.$this->authUrl,
['app' => 'user_external']
);

View File

@@ -48,7 +48,7 @@ class FTP extends Base {
*/
public function checkPassword($uid, $password) {
if (false === array_search($this->protocol, stream_get_wrappers())) {
\OC::$server->getLogger()->error(
$this->logger->error(
'ERROR: Stream wrapper not available: ' . $this->protocol,
['app' => 'user_external']
);

View File

@@ -71,7 +71,7 @@ class IMAP extends Base {
$uid = $pieces[0];
}
} else {
\OC::$server->getLogger()->error(
$this->logger->error(
'ERROR: User has a wrong domain! Expecting: '.$this->domain,
['app' => 'user_external']
);
@@ -111,7 +111,7 @@ class IMAP extends Base {
$errorcode === 28) {
# This is not defined in PHP-8.x
# 28: CURLE_OPERATION_TIMEDOUT
\OC::$server->getLogger()->error(
$this->logger->error(
'ERROR: Could not connect to imap server via curl: ' . curl_strerror($errorcode),
['app' => 'user_external']
);
@@ -122,12 +122,12 @@ class IMAP extends Base {
# 9: CURLE_REMOTE_ACCESS_DENIED
# 67: CURLE_LOGIN_DENIED
# 94: CURLE_AUTH_ERROR)
\OC::$server->getLogger()->error(
$this->logger->error(
'ERROR: IMAP Login failed via curl: ' . curl_strerror($errorcode),
['app' => 'user_external']
);
} else {
\OC::$server->getLogger()->error(
$this->logger->error(
'ERROR: IMAP server returned an error: ' . $errorcode . ' / ' . curl_strerror($errorcode),
['app' => 'user_external']
);

View File

@@ -43,7 +43,7 @@ class SMB extends Base {
$command = self::SMBCLIENT.' '.escapeshellarg('//' . $this->host . '/dummy').' -U '.$uidEscaped.'%'.$password;
$lastline = exec($command, $output, $retval);
if ($retval === 127) {
\OC::$server->getLogger()->error(
$this->logger->error(
'ERROR: smbclient executable missing',
['app' => 'user_external']
);
@@ -56,7 +56,7 @@ class SMB extends Base {
goto login;
} elseif ($retval !== 0) {
//some other error
\OC::$server->getLogger()->error(
$this->logger->error(
'ERROR: smbclient error: ' . trim($lastline),
['app' => 'user_external']
);

141
lib/SMTP.php Normal file
View File

@@ -0,0 +1,141 @@
<?php
/**
* @author Robin Appelman <icewind@owncloud.com>
* @author Jonas Sulzer <jonas@violoncello.ch>
* @copyright (c) 2012 Robin Appelman <icewind@owncloud.com>
* @copyright (c) 2026 Yakumo Laboratories -- https://yakumolabs.privatedns.org --
* This file is licensed under the Affero General Public License version 3 or
* later.
* See the COPYING-README file.
*/
namespace OCA\UserExternal;
/**
* User authentication against an SMTP mail server
* (modified from IMAP.php)
*
* @category Apps
* @package UserExternal
* @author Robin Appelman <icewind@owncloud.com>
* @license http://www.gnu.org/licenses/agpl AGPL
* @link http://github.com/owncloud/apps
*/
class SMTP extends Base {
private $mailbox;
private $port;
private $sslmode;
private $domain;
private $stripeDomain;
private $groupDomain;
/**
* Create new SMTP authentication provider
*
* @param string $mailbox SMTP server domain/IP
* @param int $port SMTP server $port
* @param string $sslmode
* @param string $domain If provided, loging will be restricted to this domain
* @param boolean $stripeDomain (whether to stripe the domain part from the username or not)
* @param boolean $groupDomain (whether to add the usere to a group corresponding to the domain of the address)
*/
public function __construct($mailbox, $port = null, $sslmode = null, $domain = null, $stripeDomain = true, $groupDomain = false) {
parent::__construct($mailbox);
$this->mailbox = $mailbox;
$this->port = $port === null ? 25 : $port;
$this->sslmode = $sslmode;
$this->domain = $domain === null ? '' : $domain;
$this->stripeDomain = $stripeDomain;
$this->groupDomain = $groupDomain;
}
/**
* Check if the password is correct without logging in the user
*
* @param string $uid The username
* @param string $password The password
*
* @return true/false
*/
public function checkPassword($uid, $password) {
// Replace escaped @ symbol in uid (which is a mail address)
// but only if there is no @ symbol and if there is a %40 inside the uid
if (!(strpos($uid, '@') !== false) && (strpos($uid, '%40') !== false)) {
$uid = str_replace("%40", "@", $uid);
}
$pieces = explode('@', $uid);
if ($this->domain !== '') {
if (count($pieces) === 1) {
$username = $uid . '@' . $this->domain;
} elseif (count($pieces) === 2 && $pieces[1] === $this->domain) {
$username = $uid;
if ($this->stripeDomain) {
$uid = $pieces[0];
}
} else {
$this->logger->error(
'ERROR: User has a wrong domain! Expecting: '.$this->domain,
['app' => 'user_external']
);
return false;
}
} else {
$username = $uid;
}
$groups = [];
if ((count($pieces) > 1) && $this->groupDomain && $pieces[1]) {
$groups[] = $pieces[1];
}
$protocol = ($this->sslmode === "ssl") ? "smtps" : "smtp";
$url = "{$protocol}://{$this->mailbox}:{$this->port}";
$ch = curl_init();
if ($this->sslmode === 'tls') {
curl_setopt($ch, CURLOPT_USE_SSL, CURLUSESSL_ALL);
}
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_USERPWD, $username.":".$password);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_exec($ch);
$errorcode = curl_errno($ch);
if ($errorcode === 0) {
curl_close($ch);
$uid = mb_strtolower($uid);
$this->storeUser($uid, $groups);
return $uid;
} elseif ($errorcode === CURLE_COULDNT_CONNECT ||
$errorcode === CURLE_SSL_CONNECT_ERROR ||
$errorcode === 28) {
# This is not defined in PHP-8.x
# 28: CURLE_OPERATION_TIMEDOUT
$this->logger->error(
'ERROR: Could not connect to smtp server via curl: ' . curl_strerror($errorcode),
['app' => 'user_external']
);
} elseif ($errorcode === 9 ||
$errorcode === 67 ||
$errorcode === 94) {
# These are not defined in PHP-8.x
# 9: CURLE_REMOTE_ACCESS_DENIED
# 67: CURLE_LOGIN_DENIED
# 94: CURLE_AUTH_ERROR)
$this->logger->error(
'ERROR: SMTP Login failed via curl: ' . curl_strerror($errorcode),
['app' => 'user_external']
);
} else {
$this->logger->error(
'ERROR: SMTP server returned an error: ' . $errorcode . ' / ' . curl_strerror($errorcode),
['app' => 'user_external']
);
}
curl_close($ch);
return false;
}
}

View File

@@ -44,7 +44,7 @@ class SSH extends Base {
*/
public function checkPassword($uid, $password) {
if (!extension_loaded('ssh2')) {
\OC::$server->getLogger()->error(
$this->logger->error(
'ERROR: php-ssh2 PECL module missing',
['app' => 'user_external']
);

View File

@@ -27,14 +27,14 @@ class WebDavAuth extends Base {
public function checkPassword($uid, $password) {
$arr = explode('://', $this->webDavAuthUrl, 2);
if (! isset($arr) or count($arr) !== 2) {
\OC::$server->getLogger()->error('ERROR: Invalid WebdavUrl: "'.$this->webDavAuthUrl.'" ', ['app' => 'user_external']);
$this->logger->error('ERROR: Invalid WebdavUrl: "'.$this->webDavAuthUrl.'" ', ['app' => 'user_external']);
return false;
}
list($protocol, $path) = $arr;
$url = $protocol.'://'.urlencode($uid).':'.urlencode($password).'@'.$path;
$headers = get_headers($url);
if ($headers === false) {
\OC::$server->getLogger()->error('ERROR: Not possible to connect to WebDAV Url: "'.$protocol.'://'.$path.'" ', ['app' => 'user_external']);
$this->logger->error('ERROR: Not possible to connect to WebDAV Url: "'.$protocol.'://'.$path.'" ', ['app' => 'user_external']);
return false;
}
$returnCode = substr($headers[0], 9, 3);