Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 54 additions & 1 deletion src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* @author Damian Fernandez Sosa <damlists@cnba.uba.ar>
* @author Anish Mistry <amistry@am-productions.biz>
* @author Jan Schneider <jan@horde.org>
* @author Jean Charles Delépine <delepine@u-picardie.fr>
* @license http://www.horde.org/licenses/bsd BSD
*/

Expand Down Expand Up @@ -82,6 +83,11 @@ class Client
*/
const AUTH_EXTERNAL = 'EXTERNAL';

/**
* XOAUTH2 authentication.
*/
const AUTH_XOAUTH2 = 'XOAUTH2';

/**
* The authentication methods this class supports.
*
Expand All @@ -95,6 +101,7 @@ class Client
self::AUTH_EXTERNAL,
self::AUTH_PLAIN,
self::AUTH_LOGIN,
self::AUTH_XOAUTH2,
);

/**
Expand Down Expand Up @@ -161,6 +168,9 @@ class Client
* - port: Port of server (DEFAULT: 4190).
* - user: Login username (optional).
* - password: Login password (optional).
* - xoauth2_token: (mixed) If set, will authenticate via the XOAUTH2
* mechanism (if available) with this token. Either a
* string or a Horde\ManageSieve\Password object.
* - authmethod: Type of login to perform (see $supportedAuthMethods)
* (DEFAULT: AUTH_AUTOMATIC).
* - euser: Effective user. If authenticating as an administrator, login
Expand Down Expand Up @@ -196,6 +206,7 @@ public function __construct($params = array())
'secure' => true,
'timeout' => 5,
'user' => '',
'xoauth2_token' => null,
),
$params
);
Expand All @@ -216,11 +227,34 @@ public function __construct($params = array())
}

if (strlen($this->_params['user']) &&
strlen($this->_params['password'])) {
(strlen((string)$this->_params['password']) || $this->getParam('xoauth2_token'))) {
$this->_handleConnectAndLogin();
}
}

/**
* Get a connection parameter.
*
* @param string $key The parameter key.
*
* @return mixed The parameter value, or null if it doesn't exist.
*/
public function getParam($key)
{
switch ($key) {
case 'xoauth2_token':
if (isset($this->_params[$key]) &&
($this->_params[$key] instanceof Password)) {
return $this->_params[$key]->getPassword();
}
break;
}

return isset($this->_params[$key])
? $this->_params[$key]
: null;
}

/**
* Passes a logger for debug logging.
*
Expand Down Expand Up @@ -612,6 +646,9 @@ protected function _cmdAuthenticate(
case self::AUTH_EXTERNAL:
$this->_authEXTERNAL($uid, $pwd, $euser);
break;
case self::AUTH_XOAUTH2:
$this->_authXOAUTH2($uid, $this->getParam('xoauth2_token'), $euser);
break;
default :
throw new Exception(
$method . ' is not a supported authentication method'
Expand Down Expand Up @@ -737,6 +774,22 @@ protected function _authEXTERNAL($user, $pass, $euser)
return $this->_sendCmd($cmd);
}

/**
* Authenticates the user using the XOAUTH2 method.
*
* @param string $user The userid to authenticate as.
* @param string $token The XOAUTH2 token (already formatted).
* @param string $euser The effective uid to authenticate as. Not used.
*
* @throws \Horde\ManageSieve\Exception
*/
protected function _authXOAUTH2($user, $token, $euser)
{
return $this->_sendCmd(
sprintf('AUTHENTICATE "XOAUTH2" "%s"', $token)
);
}

/**
* Removes a script from the server.
*
Expand Down
34 changes: 34 additions & 0 deletions src/Password.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php
/**
* Copyright 2025 Horde LLC (http://www.horde.org/)
*
* See the enclosed file LICENSE for license information (BSD). If you
* did not receive this file, see http://www.horde.org/licenses/bsd.
*
* @category Horde
* @copyright 2025 Horde LLC
* @license http://www.horde.org/licenses/bsd BSD
* @package ManageSieve
*/

namespace Horde\ManageSieve;

/**
* Interface representing a ManageSieve password object.
*
* @author Jean Charles Delepine <delepine@u-picardie.fr>
* @category Horde
* @copyright 2026 Horde LLC
* @license http://www.horde.org/licenses/bsd BSD
* @package ManageSieve
* @since 2.0.0
*/
interface Password
{
/**
* Return the password to use for the server connection.
*
* @return string The password.
*/
public function getPassword();
}
71 changes: 71 additions & 0 deletions src/Password/Xoauth2.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php
/**
* Copyright 2025 Horde LLC (http://www.horde.org/)
*
* See the enclosed file LICENSE for license information (BSD). If you
* did not receive this file, see http://www.horde.org/licenses/bsd.
*
* @category Horde
* @copyright 2025 Horde LLC
* @license http://www.horde.org/licenses/bsd BSD
* @package ManageSieve
*/

namespace Horde\ManageSieve\Password;

/**
* Generates an OAuth 2.0 authentication token as used in the XOAUTH2
* authentication mechanism.
*
* See: https://developers.google.com/gmail/xoauth2_protocol
*
* @author Jean Charles Delepine <delepine@u-picardie.fr>
* @category Horde
* @copyright 2026 Horde LLC
* @license http://www.horde.org/licenses/bsd BSD
* @package ManageSieve
* @since 2.0.0
*/
class Xoauth2 implements \Horde\ManageSieve\Password
{
/**
* Access token.
*
* @var string
*/
public $access_token;

/**
* Username.
*
* @var string
*/
public $username;

/**
* Constructor.
*
* @param string $username The username.
* @param string $access_token The access token.
*/
public function __construct($username, $access_token)
{
$this->username = $username;
$this->access_token = $access_token;
}

/**
* Return the password to use for the server connection.
*
* @return string The password.
*/
public function getPassword()
{
// base64("user=" {User} "^Aauth=Bearer " {Access Token} "^A^A")
// ^A represents a Control+A (\001)
return base64_encode(
'user=' . $this->username . "\1" .
'auth=Bearer ' . $this->access_token . "\1\1"
);
}
}
55 changes: 55 additions & 0 deletions test/Unit/Xoauth2Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
/**
* Copyright 2026 Horde LLC (http://www.horde.org/)
*
* See the enclosed file LICENSE for license information (BSD). If you
* did not receive this file, see http://www.horde.org/licenses/bsd.
*
* @category Horde
* @copyright 2026 Horde LLC
* @license http://www.horde.org/licenses/bsd BSD
* @package ManageSieve
*/

namespace Horde\ManageSieve\Test\Unit;

use Horde\ManageSieve\Password;
use Horde\ManageSieve\Password\Xoauth2;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;

#[CoversClass(Xoauth2::class)]
class Xoauth2Test extends TestCase
{
public function testTokenGeneration(): void
{
// Example from https://developers.google.com/gmail/xoauth2_protocol
$xoauth2 = new Xoauth2(
'someuser@example.com',
'vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==',
);
$this->assertEquals(
'dXNlcj1zb21ldXNlckBleGFtcGxlLmNvbQFhdXRoPUJlYXJlciB2RjlkZnQ0cW1UYzJOdmIzUmxja0JoZEhSaGRtbHpkR0V1WTI5dENnPT0BAQ==',
$xoauth2->getPassword(),
);
}

public function testImplementsPasswordInterface(): void
{
$xoauth2 = new Xoauth2('user@example.com', 'token');
$this->assertInstanceOf(Password::class, $xoauth2);
}

public function testUsernameIsAccessible(): void
{
$xoauth2 = new Xoauth2('user@example.com', 'token');
$this->assertSame('user@example.com', $xoauth2->username);
}

public function testAccessTokenIsAccessible(): void
{
$xoauth2 = new Xoauth2('user@example.com', 'mytoken');
$this->assertSame('mytoken', $xoauth2->access_token);
}
}
Loading