feat: One-click unsubscribe
Co-Authored-By: Daniel Kesselberg <mail@danielkesselberg.de> Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
This commit is contained in:
Родитель
5e67cc9c53
Коммит
f2b61b383b
|
@ -390,6 +390,11 @@ return [
|
|||
'url' => '/integration/microsoft-auth',
|
||||
'verb' => 'GET',
|
||||
],
|
||||
[
|
||||
'name' => 'list#unsubscribe',
|
||||
'url' => '/api/list/unsubscribe/{id}',
|
||||
'verb' => 'POST',
|
||||
],
|
||||
],
|
||||
'resources' => [
|
||||
'accounts' => ['url' => '/api/accounts'],
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace OCA\Mail\Controller;
|
||||
|
||||
use Exception;
|
||||
use OCA\Mail\AppInfo\Application;
|
||||
use OCA\Mail\Contracts\IMailManager;
|
||||
use OCA\Mail\Http\JsonResponse;
|
||||
use OCA\Mail\IMAP\IMAPClientFactory;
|
||||
use OCA\Mail\Service\AccountService;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\Http\Client\IClientService;
|
||||
use OCP\IRequest;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class ListController extends Controller {
|
||||
private IMailManager $mailManager;
|
||||
private AccountService $accountService;
|
||||
private IMAPClientFactory $clientFactory;
|
||||
private IClientService $httpClientService;
|
||||
private LoggerInterface $logger;
|
||||
private ?string $currentUserId;
|
||||
|
||||
public function __construct(IRequest $request,
|
||||
IMailManager $mailManager,
|
||||
AccountService $accountService,
|
||||
IMAPClientFactory $clientFactory,
|
||||
IClientService $httpClientService,
|
||||
LoggerInterface $logger,
|
||||
?string $userId) {
|
||||
parent::__construct(Application::APP_ID, $request);
|
||||
$this->mailManager = $mailManager;
|
||||
$this->accountService = $accountService;
|
||||
$this->clientFactory = $clientFactory;
|
||||
$this->request = $request;
|
||||
$this->httpClientService = $httpClientService;
|
||||
$this->logger = $logger;
|
||||
$this->currentUserId = $userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $messageId
|
||||
* @NoAdminRequired
|
||||
* @UserRateThrottle(limit=10, period=3600)
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function unsubscribe(int $id): JsonResponse {
|
||||
try {
|
||||
$message = $this->mailManager->getMessage($this->currentUserId, $id);
|
||||
$mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId());
|
||||
$account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId());
|
||||
} catch (DoesNotExistException $e) {
|
||||
return JsonResponse::fail(null, Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
|
||||
$client = $this->clientFactory->getClient($account);
|
||||
try {
|
||||
$imapMessage = $this->mailManager->getImapMessage(
|
||||
$client,
|
||||
$account,
|
||||
$mailbox,
|
||||
$message->getUid(),
|
||||
true
|
||||
);
|
||||
$unsubscribeUrl = $imapMessage->getUnsubscribeUrl();
|
||||
if ($unsubscribeUrl === null || !$imapMessage->isOneClickUnsubscribe()) {
|
||||
return JsonResponse::fail(null, Http::STATUS_FORBIDDEN);
|
||||
}
|
||||
|
||||
$httpClient = $this->httpClientService->newClient();
|
||||
$httpClient->post($unsubscribeUrl, [
|
||||
'body' => [
|
||||
'List-Unsubscribe' => 'One-Click'
|
||||
]
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
$this->logger->error('Could not unsubscribe mailing list', [
|
||||
'exception' => $e,
|
||||
]);
|
||||
return JsonResponse::error('Unknown error');
|
||||
} finally {
|
||||
$client->logout();
|
||||
}
|
||||
|
||||
return JsonResponse::success();
|
||||
}
|
||||
}
|
|
@ -45,6 +45,7 @@ use OCA\Mail\Service\Html;
|
|||
use OCA\Mail\Service\SmimeService;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use function str_starts_with;
|
||||
use function strtolower;
|
||||
|
||||
class ImapMessageFetcher {
|
||||
/** @var string[] */
|
||||
|
@ -70,6 +71,7 @@ class ImapMessageFetcher {
|
|||
private string $rawReferences = '';
|
||||
private string $dispositionNotificationTo = '';
|
||||
private ?string $unsubscribeUrl = null;
|
||||
private bool $isOneClickUnsubscribe = false;
|
||||
private ?string $unsubscribeMailto = null;
|
||||
|
||||
public function __construct(int $uid,
|
||||
|
@ -251,6 +253,7 @@ class ImapMessageFetcher {
|
|||
$this->rawReferences,
|
||||
$this->dispositionNotificationTo,
|
||||
$this->unsubscribeUrl,
|
||||
$this->isOneClickUnsubscribe,
|
||||
$this->unsubscribeMailto,
|
||||
$envelope->in_reply_to,
|
||||
$isEncrypted,
|
||||
|
@ -512,19 +515,29 @@ class ImapMessageFetcher {
|
|||
$this->dispositionNotificationTo = $dispositionNotificationTo->value_single;
|
||||
}
|
||||
|
||||
$listUnsubscribeHeader = $parsedHeaders->getHeader('list-unsubscribe');
|
||||
if ($listUnsubscribeHeader !== null) {
|
||||
$listHeaders = new Horde_ListHeaders();
|
||||
/** @var Horde_ListHeaders_Base[] $headers */
|
||||
$headers = $listHeaders->parse($listUnsubscribeHeader->name, $listUnsubscribeHeader->value_single);
|
||||
foreach ($headers as $header) {
|
||||
if (str_starts_with($header->url, 'http')) {
|
||||
$this->unsubscribeUrl = $header->url;
|
||||
break;
|
||||
}
|
||||
if (str_starts_with($header->url, 'mailto')) {
|
||||
$this->unsubscribeMailto = $header->url;
|
||||
break;
|
||||
$dkimSignatureHeader = $parsedHeaders->getHeader('dkim-signature');
|
||||
$hasDkimSignature = $dkimSignatureHeader !== null;
|
||||
|
||||
if ($hasDkimSignature) {
|
||||
$listUnsubscribeHeader = $parsedHeaders->getHeader('list-unsubscribe');
|
||||
if ($listUnsubscribeHeader !== null) {
|
||||
$listHeaders = new Horde_ListHeaders();
|
||||
/** @var Horde_ListHeaders_Base[] $headers */
|
||||
$headers = $listHeaders->parse($listUnsubscribeHeader->name, $listUnsubscribeHeader->value_single);
|
||||
foreach ($headers as $header) {
|
||||
if (str_starts_with($header->url, 'http')) {
|
||||
$this->unsubscribeUrl = $header->url;
|
||||
|
||||
$unsubscribePostHeader = $parsedHeaders->getHeader('List-Unsubscribe-Post');
|
||||
if ($unsubscribePostHeader !== null) {
|
||||
$this->isOneClickUnsubscribe = strtolower($unsubscribePostHeader->value_single) === 'list-unsubscribe=one-click';
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (str_starts_with($header->url, 'mailto')) {
|
||||
$this->unsubscribeMailto = $header->url;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,6 +77,7 @@ class IMAPMessage implements IMessage, JsonSerializable {
|
|||
private string $rawReferences;
|
||||
private string $dispositionNotificationTo;
|
||||
private ?string $unsubscribeUrl;
|
||||
private bool $isOneClickUnsubscribe;
|
||||
private ?string $unsubscribeMailto;
|
||||
private string $rawInReplyTo;
|
||||
private bool $isEncrypted;
|
||||
|
@ -103,6 +104,7 @@ class IMAPMessage implements IMessage, JsonSerializable {
|
|||
string $rawReferences,
|
||||
string $dispositionNotificationTo,
|
||||
?string $unsubscribeUrl,
|
||||
bool $isOneClickUnsubscribe,
|
||||
?string $unsubscribeMailto,
|
||||
string $rawInReplyTo,
|
||||
bool $isEncrypted,
|
||||
|
@ -129,6 +131,7 @@ class IMAPMessage implements IMessage, JsonSerializable {
|
|||
$this->rawReferences = $rawReferences;
|
||||
$this->dispositionNotificationTo = $dispositionNotificationTo;
|
||||
$this->unsubscribeUrl = $unsubscribeUrl;
|
||||
$this->isOneClickUnsubscribe = $isOneClickUnsubscribe;
|
||||
$this->unsubscribeMailto = $unsubscribeMailto;
|
||||
$this->rawInReplyTo = $rawInReplyTo;
|
||||
$this->isEncrypted = $isEncrypted;
|
||||
|
@ -312,6 +315,7 @@ class IMAPMessage implements IMessage, JsonSerializable {
|
|||
'hasHtmlBody' => $this->hasHtmlMessage,
|
||||
'dispositionNotificationTo' => $this->getDispositionNotificationTo(),
|
||||
'unsubscribeUrl' => $this->unsubscribeUrl,
|
||||
'isOneClickUnsubscribe' => $this->isOneClickUnsubscribe,
|
||||
'unsubscribeMailto' => $this->unsubscribeMailto,
|
||||
'scheduling' => $this->scheduling,
|
||||
];
|
||||
|
@ -443,6 +447,14 @@ class IMAPMessage implements IMessage, JsonSerializable {
|
|||
return $this->signatureIsValid;
|
||||
}
|
||||
|
||||
public function getUnsubscribeUrl(): ?string {
|
||||
return $this->unsubscribeUrl;
|
||||
}
|
||||
|
||||
public function isOneClickUnsubscribe(): bool {
|
||||
return $this->isOneClickUnsubscribe;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast all values from an IMAP message into the correct DB format
|
||||
*
|
||||
|
|
|
@ -199,7 +199,14 @@
|
|||
:data="error"
|
||||
:auto-margin="true"
|
||||
role="alert" />
|
||||
<ConfirmModal v-if="message && message.unsubscribeUrl && showListUnsubscribeConfirmation"
|
||||
<ConfirmModal v-if="message && message.unsubscribeUrl && message.isOneClickUnsubscribe && showListUnsubscribeConfirmation"
|
||||
:confirm-text="t('mail', 'Unsubscribe')"
|
||||
:title="t('mail', 'Unsubscribe via link')"
|
||||
@cancel="showListUnsubscribeConfirmation = false"
|
||||
@confirm="unsubscribeViaOneClick">
|
||||
{{ t('mail', 'Unsubscribing will stop all messages from the mailing list {sender}', { sender: from }) }}
|
||||
</ConfirmModal>
|
||||
<ConfirmModal v-else-if="message && message.unsubscribeUrl && showListUnsubscribeConfirmation"
|
||||
:confirm-text="t('mail', 'Unsubscribe')"
|
||||
:confirm-url="message.unsubscribeUrl"
|
||||
:title="t('mail', 'Unsubscribe via link')"
|
||||
|
@ -249,6 +256,7 @@ import NoTrashMailboxConfiguredError from '../errors/NoTrashMailboxConfiguredErr
|
|||
import { isPgpText } from '../crypto/pgp'
|
||||
import NcActions from '@nextcloud/vue/dist/Components/NcActions'
|
||||
import NcActionText from '@nextcloud/vue/dist/Components/NcActionText'
|
||||
import { unsubscribe } from '../service/ListService'
|
||||
|
||||
// Ternary loading state
|
||||
const LOADING_DONE = 0
|
||||
|
@ -612,6 +620,20 @@ export default {
|
|||
return t('mail', 'Could not archive message')
|
||||
}
|
||||
},
|
||||
async unsubscribeViaOneClick() {
|
||||
try {
|
||||
this.unsubscribing = true
|
||||
|
||||
await unsubscribe(this.envelope.databaseId)
|
||||
showSuccess(t('mail', 'Unsubscribe request sent'))
|
||||
} catch (error) {
|
||||
logger.error('Could not one-click unsubscribe', { error })
|
||||
showError(t('mail', 'Could not unsubscribe from mailing list'))
|
||||
} finally {
|
||||
this.unsubscribing = false
|
||||
this.showListUnsubscribeConfirmation = false
|
||||
}
|
||||
},
|
||||
async unsubscribeViaMailto() {
|
||||
const mailto = this.message.unsubscribeMailto
|
||||
const [email, paramString] = mailto.replace(/^mailto:/, '').split('?')
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import axios from '@nextcloud/axios'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
|
||||
export async function unsubscribe(id) {
|
||||
const url = generateUrl('/apps/mail/api/list/unsubscribe/{id}', {
|
||||
id,
|
||||
})
|
||||
|
||||
axios.post(url)
|
||||
}
|
|
@ -0,0 +1,239 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace OCA\Mail\Tests\Unit\Controller;
|
||||
|
||||
use ChristophWurst\Nextcloud\Testing\ServiceMockObject;
|
||||
use ChristophWurst\Nextcloud\Testing\TestCase;
|
||||
use Horde_Imap_Client_DateTime;
|
||||
use OCA\Mail\Account;
|
||||
use OCA\Mail\AddressList;
|
||||
use OCA\Mail\Controller\ListController;
|
||||
use OCA\Mail\Db\MailAccount;
|
||||
use OCA\Mail\Db\Mailbox;
|
||||
use OCA\Mail\Db\Message;
|
||||
use OCA\Mail\Model\IMAPMessage;
|
||||
use OCA\Mail\Service\Html;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\Http\Client\IClient;
|
||||
|
||||
class ListControllerTest extends TestCase {
|
||||
private ServiceMockObject $serviceMock;
|
||||
private ListController $controller;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->serviceMock = $this->createServiceMock(ListController::class, [
|
||||
'userId' => 'user123',
|
||||
]);
|
||||
$this->controller = $this->serviceMock->getService();
|
||||
}
|
||||
|
||||
public function testMessageNotFound(): void {
|
||||
$this->serviceMock->getParameter('mailManager')
|
||||
->expects(self::once())
|
||||
->method('getMessage')
|
||||
->willThrowException(new DoesNotExistException(''));
|
||||
|
||||
$response = $this->controller->unsubscribe(123);
|
||||
|
||||
self::assertEquals(Http::STATUS_NOT_FOUND, $response->getStatus());
|
||||
}
|
||||
|
||||
public function testMailboxNotFound(): void {
|
||||
$message = new Message();
|
||||
$message->setMailboxId(321);
|
||||
$this->serviceMock->getParameter('mailManager')
|
||||
->expects(self::once())
|
||||
->method('getMessage')
|
||||
->willReturn($message);
|
||||
$this->serviceMock->getParameter('mailManager')
|
||||
->expects(self::once())
|
||||
->method('getMailbox')
|
||||
->with('user123', 321)
|
||||
->willThrowException(new DoesNotExistException(''));
|
||||
|
||||
$response = $this->controller->unsubscribe(123);
|
||||
|
||||
self::assertEquals(Http::STATUS_NOT_FOUND, $response->getStatus());
|
||||
}
|
||||
|
||||
public function testAccountNotFound(): void {
|
||||
$message = new Message();
|
||||
$message->setMailboxId(321);
|
||||
$this->serviceMock->getParameter('mailManager')
|
||||
->expects(self::once())
|
||||
->method('getMessage')
|
||||
->willReturn($message);
|
||||
$mailbox = new Mailbox();
|
||||
$mailbox->setAccountId(567);
|
||||
$this->serviceMock->getParameter('mailManager')
|
||||
->expects(self::once())
|
||||
->method('getMailbox')
|
||||
->with('user123', 321)
|
||||
->willReturn($mailbox);
|
||||
$this->serviceMock->getParameter('accountService')
|
||||
->expects(self::once())
|
||||
->method('find')
|
||||
->with('user123', 567)
|
||||
->willThrowException(new DoesNotExistException(''));
|
||||
|
||||
$response = $this->controller->unsubscribe(123);
|
||||
|
||||
self::assertEquals(Http::STATUS_NOT_FOUND, $response->getStatus());
|
||||
}
|
||||
|
||||
public function testUnsupportedMessage(): void {
|
||||
$message = new Message();
|
||||
$message->setUid(987);
|
||||
$message->setMailboxId(321);
|
||||
$this->serviceMock->getParameter('mailManager')
|
||||
->expects(self::once())
|
||||
->method('getMessage')
|
||||
->willReturn($message);
|
||||
$mailbox = new Mailbox();
|
||||
$mailbox->setAccountId(567);
|
||||
$this->serviceMock->getParameter('mailManager')
|
||||
->expects(self::once())
|
||||
->method('getMailbox')
|
||||
->with('user123', 321)
|
||||
->willReturn($mailbox);
|
||||
$mailAccount = new MailAccount([]);
|
||||
$account = new Account($mailAccount);
|
||||
$this->serviceMock->getParameter('accountService')
|
||||
->expects(self::once())
|
||||
->method('find')
|
||||
->with('user123', 567)
|
||||
->willReturn($account);
|
||||
$imapMessage = new IMAPMessage(
|
||||
123,
|
||||
'',
|
||||
[],
|
||||
new AddressList([]),
|
||||
new AddressList([]),
|
||||
new AddressList([]),
|
||||
new AddressList([]),
|
||||
new AddressList([]),
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
false,
|
||||
[],
|
||||
[],
|
||||
false,
|
||||
[],
|
||||
new Horde_Imap_Client_DateTime(),
|
||||
'',
|
||||
'',
|
||||
null,
|
||||
false,
|
||||
'',
|
||||
'',
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
$this->createMock(Html::class),
|
||||
);
|
||||
$this->serviceMock->getParameter('mailManager')
|
||||
->expects(self::once())
|
||||
->method('getImapMessage')
|
||||
->willReturn($imapMessage);
|
||||
|
||||
$response = $this->controller->unsubscribe(123);
|
||||
|
||||
self::assertEquals(Http::STATUS_FORBIDDEN, $response->getStatus());
|
||||
}
|
||||
|
||||
public function testUnsubscribe(): void {
|
||||
$message = new Message();
|
||||
$message->setUid(987);
|
||||
$message->setMailboxId(321);
|
||||
$this->serviceMock->getParameter('mailManager')
|
||||
->expects(self::once())
|
||||
->method('getMessage')
|
||||
->willReturn($message);
|
||||
$mailbox = new Mailbox();
|
||||
$mailbox->setAccountId(567);
|
||||
$this->serviceMock->getParameter('mailManager')
|
||||
->expects(self::once())
|
||||
->method('getMailbox')
|
||||
->with('user123', 321)
|
||||
->willReturn($mailbox);
|
||||
$mailAccount = new MailAccount([]);
|
||||
$account = new Account($mailAccount);
|
||||
$this->serviceMock->getParameter('accountService')
|
||||
->expects(self::once())
|
||||
->method('find')
|
||||
->with('user123', 567)
|
||||
->willReturn($account);
|
||||
$imapMessage = new IMAPMessage(
|
||||
123,
|
||||
'',
|
||||
[],
|
||||
new AddressList([]),
|
||||
new AddressList([]),
|
||||
new AddressList([]),
|
||||
new AddressList([]),
|
||||
new AddressList([]),
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
false,
|
||||
[],
|
||||
[],
|
||||
false,
|
||||
[],
|
||||
new Horde_Imap_Client_DateTime(),
|
||||
'',
|
||||
'',
|
||||
'https://un.sub.scribe/me',
|
||||
true,
|
||||
'',
|
||||
'',
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
$this->createMock(Html::class),
|
||||
);
|
||||
$this->serviceMock->getParameter('mailManager')
|
||||
->expects(self::once())
|
||||
->method('getImapMessage')
|
||||
->willReturn($imapMessage);
|
||||
$httpClient = $this->createMock(IClient::class);
|
||||
$this->serviceMock->getParameter('httpClientService')
|
||||
->expects(self::once())
|
||||
->method('newClient')
|
||||
->willReturn($httpClient);
|
||||
$httpClient->expects(self::once())
|
||||
->method('post')
|
||||
->with('https://un.sub.scribe/me');
|
||||
|
||||
$response = $this->controller->unsubscribe(123);
|
||||
|
||||
self::assertEquals(Http::STATUS_OK, $response->getStatus());
|
||||
}
|
||||
}
|
|
@ -87,7 +87,8 @@ class IMAPMessageTest extends TestCase {
|
|||
'',
|
||||
'disposition',
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
'',
|
||||
'',
|
||||
false,
|
||||
false,
|
||||
|
@ -126,6 +127,7 @@ class IMAPMessageTest extends TestCase {
|
|||
'',
|
||||
'disposition',
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
'',
|
||||
false,
|
||||
|
@ -157,6 +159,7 @@ class IMAPMessageTest extends TestCase {
|
|||
'important' => true,
|
||||
],
|
||||
'unsubscribeUrl' => null,
|
||||
'isOneClickUnsubscribe' => false,
|
||||
'unsubscribeMailto' => null,
|
||||
'hasHtmlBody' => true,
|
||||
'dispositionNotificationTo' => 'disposition',
|
||||
|
|
Загрузка…
Ссылка в новой задаче