Skip to content

Commit

Permalink
Merge pull request #218 from Slamdunk/microsoft_pop3_xoauth2
Browse files Browse the repository at this point in the history
Add support for Microsoft POP3 XOAUTH2
  • Loading branch information
Slamdunk authored Nov 18, 2022
2 parents 430e034 + 64b2059 commit 0516586
Show file tree
Hide file tree
Showing 7 changed files with 293 additions and 15 deletions.
46 changes: 31 additions & 15 deletions src/Protocol/Pop3.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Laminas\Mail\Protocol;

use Laminas\Mail\Protocol\Pop3\Response;
use Laminas\Stdlib\ErrorHandler;

use function explode;
Expand Down Expand Up @@ -151,25 +152,14 @@ public function sendRequest($request)
*/
public function readResponse($multiline = false)
{
ErrorHandler::start();
$result = fgets($this->socket);
$error = ErrorHandler::stop();
if (! is_string($result)) {
throw new Exception\RuntimeException('read failed - connection closed?', 0, $error);
}
$response = $this->readRemoteResponse();

$result = trim($result);
if (strpos($result, ' ')) {
[$status, $message] = explode(' ', $result, 2);
} else {
$status = $result;
$message = '';
}

if ($status != '+OK') {
if ($response->status() != '+OK') {
throw new Exception\RuntimeException('last request failed');
}

$message = $response->message();

if ($multiline) {
$message = '';
$line = fgets($this->socket);
Expand All @@ -185,6 +175,32 @@ public function readResponse($multiline = false)
return $message;
}

/**
* read a response
* return extracted status / message from response
* @throws Exception\RuntimeException
*/
protected function readRemoteResponse(): Response
{
ErrorHandler::start();
$result = fgets($this->socket);
$error = ErrorHandler::stop();
if (! is_string($result)) {
throw new Exception\RuntimeException('read failed - connection closed?', 0, $error);
}

$result = trim($result);
if (strpos($result, ' ')) {
[$status, $message] = explode(' ', $result, 2);
} else {
$status = $result;
$message = '';
}

return new Response($status, $message);
}

/**
* Send request and get response
*
Expand Down
35 changes: 35 additions & 0 deletions src/Protocol/Pop3/Response.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace Laminas\Mail\Protocol\Pop3;

/**
* POP3 response value object
*
* @internal
*/
final class Response
{
/** @var string $status */
private $status;

/** @var string $message */
private $message;

public function __construct(string $status, string $message)
{
$this->status = $status;
$this->message = $message;
}

public function status(): string
{
return $this->status;
}

public function message(): string
{
return $this->message;
}
}
34 changes: 34 additions & 0 deletions src/Protocol/Pop3/Xoauth2/Microsoft.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace Laminas\Mail\Protocol\Pop3\Xoauth2;

use Laminas\Mail\Protocol\Exception\RuntimeException;
use Laminas\Mail\Protocol\Pop3;
use Laminas\Mail\Protocol\Xoauth2\Xoauth2;

/**
* @final
*/
class Microsoft extends Pop3
{
protected const AUTH_INITIALIZE_REQUEST = 'AUTH XOAUTH2';
protected const AUTH_RESPONSE_INITIALIZED_OK = '+';

/**
* @param string $user the target mailbox to access
* @param string $password OAUTH2 accessToken
* @param bool $tryApop obsolete parameter not used here
*/
public function login($user, $password, $tryApop = true): void
{
$this->sendRequest(self::AUTH_INITIALIZE_REQUEST);

$response = $this->readRemoteResponse();

if ($response->status() != self::AUTH_RESPONSE_INITIALIZED_OK) {
throw new RuntimeException($response->message());
}

$this->request(Xoauth2::encodeXoauth2Sasl($user, $password));
}
}
32 changes: 32 additions & 0 deletions src/Protocol/Xoauth2/Xoauth2.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace Laminas\Mail\Protocol\Xoauth2;

use function base64_encode;
use function chr;
use function sprintf;

/**
* @internal
*/
final class Xoauth2
{
/**
* encodes accessToken and target mailbox to Xoauth2 SASL base64 encoded string
*/
public static function encodeXoauth2Sasl(string $targetMailbox, string $accessToken): string
{
return base64_encode(
sprintf(
"user=%s%sauth=Bearer %s%s%s",
$targetMailbox,
chr(0x01),
$accessToken,
chr(0x01),
chr(0x01)
)
);
}
}
27 changes: 27 additions & 0 deletions test/Protocol/Pop3/ResponseTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace LaminasTest\Mail\Protocol\Pop3;

use Laminas\Mail\Protocol\Pop3\Response;
use PHPUnit\Framework\TestCase;

/**
* @covers Laminas\Mail\Protocol\Pop3\Response
*/
class ResponseTest extends TestCase
{
/** @psalm-suppress InternalClass */
public function testIntegration(): void
{
/** @psalm-suppress InternalMethod */
$response = new Response('+OK', 'Auth');

/** @psalm-suppress InternalMethod */
$this->assertEquals('+OK', $response->status());

/** @psalm-suppress InternalMethod */
$this->assertEquals('Auth', $response->message());
}
}
102 changes: 102 additions & 0 deletions test/Protocol/Pop3/Xoauth2/MicrosoftTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

declare(strict_types=1);

namespace LaminasTest\Mail\Protocol\Pop3\Xoauth2;

use Laminas\Mail\Exception\RuntimeException;
use Laminas\Mail\Protocol\Pop3\Response;
use Laminas\Mail\Protocol\Pop3\Xoauth2\Microsoft;
use Laminas\Mail\Protocol\Xoauth2\Xoauth2;
use PHPUnit\Framework\TestCase;

use function fopen;
use function rewind;
use function str_replace;
use function stream_get_contents;

/**
* @covers Laminas\Mail\Protocol\Pop3\Xoauth2\Microsoft
*/
class MicrosoftTest extends TestCase
{
/** @psalm-suppress InternalClass */
public function testIntegration(): void
{
/**
* @psalm-suppress PropertyNotSetInConstructor
* @psalm-suppress InvalidExtendClass
*/
$protocol = new class () extends Microsoft {
private string $step;

/** @psalm-suppress InternalClass */
public function readRemoteResponse(): Response
{
if ($this->step === self::AUTH_INITIALIZE_REQUEST) {
/** @psalm-suppress InternalMethod */
return new Response(self::AUTH_RESPONSE_INITIALIZED_OK, 'Auth initialized');
}

/** @psalm-suppress InternalMethod */
return new Response('+OK', 'Authenticated');
}

/**
* Send a request
*
* @param string $request your request without newline
* @throws RuntimeException
*/
public function sendRequest($request): void
{
$this->step = $request;
parent::sendRequest($request);
}

/**
* Open connection to POP3 server
*
* @param string $host hostname or IP address of POP3 server
* @param int|null $port of POP3 server, default is 110 (995 for ssl)
* @param string|bool $ssl use 'SSL', 'TLS' or false
* @throws RuntimeException
* @return string welcome message
*/
public function connect($host, $port = null, $ssl = false)
{
$this->socket = fopen("php://memory", 'rw+');
return '';
}

/**
* @return null|resource
*/
public function getSocket()
{
return $this->socket;
}
};

$protocol->connect('localhost', 0, false);

$protocol->login('[email protected]', '123');

$this->assertInstanceOf(Microsoft::class, $protocol);

$streamContents = '';
if ($socket = $protocol->getSocket()) {
rewind($socket);
$streamContents = stream_get_contents($socket);
$streamContents = str_replace("\r\n", "\n", $streamContents);
}

/** @psalm-suppress InternalMethod */
$xoauth2Sasl = Xoauth2::encodeXoauth2Sasl('[email protected]', '123');

$this->assertEquals(
'AUTH XOAUTH2' . "\n" . $xoauth2Sasl . "\n",
$streamContents
);
}
}
32 changes: 32 additions & 0 deletions test/Protocol/Xoauth2/Xoauth2Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace LaminasTest\Mail\Protocol\Xoauth2;

use Laminas\Mail\Protocol\Xoauth2\Xoauth2;
use PHPUnit\Framework\TestCase;

/**
* @covers Laminas\Mail\Protocol\Xoauth2\Xoauth2
*/
class Xoauth2Test extends TestCase
{
/** @psalm-suppress InternalClass */
public function testEncodeXoauth2Sasl(): void
{
$accessToken = 'dXNlcj10ZXN0QGNvbnRvc28ub25taWNyb3NvZnQuY29tAWF1dGg9QmVhcmVyIEV3QkFBbDNCQUFVRkZwVUFvN';
$accessToken .= '0ozVmUwYmpMQldaV0NjbFJDM0VvQUEBAQ==';

/**
* @psalm-suppress InternalMethod
*/
$this->assertEquals(
$accessToken,
Xoauth2::encodeXoauth2Sasl(
'[email protected]',
'EwBAAl3BAAUFFpUAo7J3Ve0bjLBWZWCclRC3EoAA'
)
);
}
}

0 comments on commit 0516586

Please sign in to comment.