Signed-off-by: Hamza Mahjoubi <hamzamahjoubi221@gmail.com>
This commit is contained in:
Hamza Mahjoubi 2024-04-18 14:30:48 +02:00
Родитель e63f1e0722
Коммит 9b87b23bc2
26 изменённых файлов: 860 добавлений и 9 удалений

Просмотреть файл

@ -51,9 +51,27 @@ class Address implements JsonSerializable {
// Fallback
return $this->getEmail();
}
$personal = trim(explode('<', $personal)[0]); // Remove the email part if present
return $personal;
}
/**
* @return string|null
*/
public function getCustomEmail(): ?string {
$personal = $this->wrapped->personal;
if ($personal === null) {
// Fallback
return null;
}
$parts = explode('<', $personal);
if (count($parts) === 1) {
return null;
}
$customEmail = trim($parts[1], '>');
return $customEmail;
}
/**
* @return string|null
*/

Просмотреть файл

@ -25,6 +25,7 @@ use OCA\Mail\Exception\ServiceException;
use OCA\Mail\IMAP\Charset\Converter;
use OCA\Mail\Model\IMAPMessage;
use OCA\Mail\Service\Html;
use OCA\Mail\Service\PhishingDetection\PhishingDetectionService;
use OCA\Mail\Service\SmimeService;
use OCP\AppFramework\Db\DoesNotExistException;
use function str_starts_with;
@ -36,8 +37,10 @@ class ImapMessageFetcher {
private Html $htmlService;
private SmimeService $smimeService;
private PhishingDetectionService $phishingDetectionService;
private string $userId;
private bool $runPhishingCheck = false;
// Conditional fetching/parsing
private bool $loadBody = false;
@ -54,6 +57,7 @@ class ImapMessageFetcher {
private string $rawReferences = '';
private string $dispositionNotificationTo = '';
private bool $hasDkimSignature = false;
private array $phishingDetails = [];
private ?string $unsubscribeUrl = null;
private bool $isOneClickUnsubscribe = false;
private ?string $unsubscribeMailto = null;
@ -64,13 +68,16 @@ class ImapMessageFetcher {
string $userId,
Html $htmlService,
SmimeService $smimeService,
private Converter $converter) {
private Converter $converter,
PhishingDetectionService $phishingDetectionService,
) {
$this->uid = $uid;
$this->mailbox = $mailbox;
$this->client = $client;
$this->userId = $userId;
$this->htmlService = $htmlService;
$this->smimeService = $smimeService;
$this->phishingDetectionService = $phishingDetectionService;
}
@ -85,6 +92,17 @@ class ImapMessageFetcher {
return $this;
}
/**
* Configure the fetcher to check for phishing.
*
* @param bool $value
* @return $this
*/
public function withPhishingCheck(bool $value): ImapMessageFetcher {
$this->runPhishingCheck = $value;
return $this;
}
/**
* @param Horde_Imap_Client_Data_Fetch|null $fetch
* Will be reused if no body is requested.
@ -238,6 +256,7 @@ class ImapMessageFetcher {
$this->rawReferences,
$this->dispositionNotificationTo,
$this->hasDkimSignature,
$this->phishingDetails,
$this->unsubscribeUrl,
$this->isOneClickUnsubscribe,
$this->unsubscribeMailto,
@ -495,6 +514,10 @@ class ImapMessageFetcher {
$dkimSignatureHeader = $parsedHeaders->getHeader('dkim-signature');
$this->hasDkimSignature = $dkimSignatureHeader !== null;
if ($this->runPhishingCheck) {
$this->phishingDetails = $this->phishingDetectionService->checkHeadersForPhishing($parsedHeaders, $this->hasHtmlMessage, $this->htmlMessage);
}
$listUnsubscribeHeader = $parsedHeaders->getHeader('list-unsubscribe');
if ($listUnsubscribeHeader !== null) {
$listHeaders = new Horde_ListHeaders();

Просмотреть файл

@ -12,19 +12,23 @@ namespace OCA\Mail\IMAP;
use Horde_Imap_Client_Base;
use OCA\Mail\IMAP\Charset\Converter;
use OCA\Mail\Service\Html;
use OCA\Mail\Service\PhishingDetection\PhishingDetectionService;
use OCA\Mail\Service\SmimeService;
class ImapMessageFetcherFactory {
private Html $htmlService;
private SmimeService $smimeService;
private Converter $charsetConverter;
private PhishingDetectionService $phishingDetectionService;
public function __construct(Html $htmlService,
SmimeService $smimeService,
Converter $charsetConverter) {
Converter $charsetConverter,
PhishingDetectionService $phishingDetectionService) {
$this->htmlService = $htmlService;
$this->smimeService = $smimeService;
$this->charsetConverter = $charsetConverter;
$this->phishingDetectionService = $phishingDetectionService;
}
public function build(int $uid,
@ -39,6 +43,7 @@ class ImapMessageFetcherFactory {
$this->htmlService,
$this->smimeService,
$this->charsetConverter,
$this->phishingDetectionService,
);
}
}

Просмотреть файл

@ -68,7 +68,7 @@ class MessageMapper {
int $id,
string $userId,
bool $loadBody = false): IMAPMessage {
$result = $this->findByIds($client, $mailbox, new Horde_Imap_Client_Ids([$id]), $userId, $loadBody);
$result = $this->findByIds($client, $mailbox, new Horde_Imap_Client_Ids([$id]), $userId, $loadBody, true);
if (count($result) === 0) {
throw new DoesNotExistException("Message does not exist");
@ -249,7 +249,8 @@ class MessageMapper {
string $mailbox,
$ids,
string $userId,
bool $loadBody = false): array {
bool $loadBody = false,
bool $runPhishingCheck = false): array {
$query = new Horde_Imap_Client_Fetch_Query();
$query->envelope();
$query->flags();
@ -294,7 +295,7 @@ class MessageMapper {
$this->logger->debug("findByIds in $mailbox got " . count($ids) . " UIDs ($range) and found " . count($fetchResults) . ". minFetched=$minFetched maxFetched=$maxFetched");
}
return array_map(function (Horde_Imap_Client_Data_Fetch $fetchResult) use ($client, $mailbox, $loadBody, $userId) {
return array_map(function (Horde_Imap_Client_Data_Fetch $fetchResult) use ($client, $mailbox, $loadBody, $userId, $runPhishingCheck) {
return $this->imapMessageFactory
->build(
$fetchResult->getUid(),
@ -303,6 +304,7 @@ class MessageMapper {
$userId,
)
->withBody($loadBody)
->withPhishingCheck($runPhishingCheck)
->fetchMessage($fetchResult);
}, $fetchResults);
}

Просмотреть файл

@ -57,6 +57,7 @@ class IMAPMessage implements IMessage, JsonSerializable {
private string $rawReferences;
private string $dispositionNotificationTo;
private bool $hasDkimSignature;
private array $phishingDetails;
private ?string $unsubscribeUrl;
private bool $isOneClickUnsubscribe;
private ?string $unsubscribeMailto;
@ -85,6 +86,7 @@ class IMAPMessage implements IMessage, JsonSerializable {
string $rawReferences,
string $dispositionNotificationTo,
bool $hasDkimSignature,
array $phishingDetails,
?string $unsubscribeUrl,
bool $isOneClickUnsubscribe,
?string $unsubscribeMailto,
@ -113,6 +115,7 @@ class IMAPMessage implements IMessage, JsonSerializable {
$this->rawReferences = $rawReferences;
$this->dispositionNotificationTo = $dispositionNotificationTo;
$this->hasDkimSignature = $hasDkimSignature;
$this->phishingDetails = $phishingDetails;
$this->unsubscribeUrl = $unsubscribeUrl;
$this->isOneClickUnsubscribe = $isOneClickUnsubscribe;
$this->unsubscribeMailto = $unsubscribeMailto;
@ -299,6 +302,7 @@ class IMAPMessage implements IMessage, JsonSerializable {
'hasHtmlBody' => $this->hasHtmlMessage,
'dispositionNotificationTo' => $this->getDispositionNotificationTo(),
'hasDkimSignature' => $this->hasDkimSignature,
'phishingDetails' => $this->phishingDetails,
'unsubscribeUrl' => $this->unsubscribeUrl,
'isOneClickUnsubscribe' => $this->isOneClickUnsubscribe,
'unsubscribeMailto' => $this->unsubscribeMailto,

Просмотреть файл

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Mail;
use JsonSerializable;
use ReturnTypeWillChange;
class PhishingDetectionList implements JsonSerializable {
/** @var PhishingDetectionResult[] */
private array $checks;
private bool $warning = false;
/**
* @param PhishingDetectionResult[] $checks
*/
public function __construct(array $checks = []) {
$this->checks = $checks;
}
public function addCheck(PhishingDetectionResult $check) {
$this->checks[] = $check;
}
private function isWarning() {
foreach ($this->checks as $check) {
if (in_array($check->getType(), [PhishingDetectionResult::DATE_CHECK, PhishingDetectionResult::LINK_CHECK, PhishingDetectionResult::CUSTOM_EMAIL_CHECK, PhishingDetectionResult::CONTACTS_CHECK]) && $check->isPhishing()) {
return true;
}
}
return false;
}
#[ReturnTypeWillChange]
public function jsonSerialize() {
$result = array_map(static function (PhishingDetectionResult $check) {
return $check->jsonSerialize();
}, $this->checks);
return [
'checks' => $result,
'warning' => $this->isWarning(),
];
}
}

Просмотреть файл

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Mail;
use JsonSerializable;
use ReturnTypeWillChange;
/**
* @psalm-immutable
*/
class PhishingDetectionResult implements JsonSerializable {
public const DATE_CHECK = "Date";
public const LINK_CHECK = "Link";
public const REPLYTO_CHECK = "Reply-To";
public const CUSTOM_EMAIL_CHECK = "Custom Email";
public const CONTACTS_CHECK = "Contacts";
public const TRUSTED_CHECK = "Trusted";
private string $message = "";
private bool $isPhishing;
private array $additionalData = [];
private string $type;
public function __construct(string $type, bool $isPhishing, string $message = "", array $additionalData = []) {
$this->type = $type;
$this->message = $message;
$this->isPhishing = $isPhishing;
$this->additionalData = $additionalData;
}
public function getType(): string {
return $this->type;
}
public function isPhishing(): bool {
return $this->isPhishing;
}
#[ReturnTypeWillChange]
public function jsonSerialize() {
return [
'type' => $this->type,
'isPhishing' => $this->isPhishing,
'message' => $this->message,
'additionalData' => $this->additionalData,
];
}
}

Просмотреть файл

@ -223,7 +223,7 @@ class ContactsIntegration {
/**
* @param string[] $fields
*/
private function doSearch(string $term, array $fields, bool $strictSearch): array {
private function doSearch(string $term, array $fields, bool $strictSearch, bool $forceSAB = false) : array {
$allowSystemUsers = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'no') === 'yes';
$result = $this->contactsManager->search($term, $fields, [
@ -231,14 +231,16 @@ class ContactsIntegration {
]);
$matches = [];
foreach ($result as $r) {
if (!$allowSystemUsers && isset($r['isLocalSystemBook']) && $r['isLocalSystemBook']) {
if ((!$allowSystemUsers && !$forceSAB) && isset($r['isLocalSystemBook']) && $r['isLocalSystemBook']) {
continue;
}
$id = $r['UID'];
$fn = $r['FN'];
$email = $r['EMAIL'];
$matches[] = [
'id' => $id,
'label' => $fn,
'email' => $email,
];
}
return $matches;
@ -257,7 +259,7 @@ class ContactsIntegration {
/**
* Extracts all Contacts with the specified name
*/
public function getContactsWithName(string $name): array {
return $this->doSearch($name, ['FN'], false);
public function getContactsWithName(string $name, bool $forceSAB = false): array {
return $this->doSearch($name, ['FN'], false, $forceSAB);
}
}

Просмотреть файл

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Mail\Service\PhishingDetection;
use OCA\Mail\PhishingDetectionResult;
use OCA\Mail\Service\ContactsIntegration;
use OCP\IL10N;
class ContactCheck {
public function __construct(private ContactsIntegration $contactIntegration, private IL10N $l10n) {
$this->l10n = $l10n;
$this->contactIntegration = $contactIntegration;
}
public function run(string $fn, string $email): PhishingDetectionResult {
$emailInContacts = false;
$emails = "";
$contacts = $this->contactIntegration->getContactsWithName($fn, true);
foreach ($contacts as $contact) {
foreach ($contact['email'] as $contactEmail) {
$emailInContacts = true;
$emails .= $contactEmail.",";
if ($contactEmail === $email) {
return new PhishingDetectionResult(PhishingDetectionResult::CONTACTS_CHECK, false);
}
}
}
if ($emailInContacts) {
return new PhishingDetectionResult(PhishingDetectionResult::CONTACTS_CHECK, true, $this->l10n->t('Sender email: %1$s is not in the address book, but the sender name: %2$s is in the address book with the following emails: %3$s', [$email, $fn, $emails]));
}
return new PhishingDetectionResult(PhishingDetectionResult::CONTACTS_CHECK, false);
}
}

Просмотреть файл

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Mail\Service\PhishingDetection;
use OCA\Mail\PhishingDetectionResult;
use OCP\IL10N;
class CustomEmailCheck {
protected IL10N $l10n;
public function __construct(IL10N $l10n) {
$this->l10n = $l10n;
}
public function run(string $fromEmail, ?string $customEmail): PhishingDetectionResult {
if(!(isset($customEmail))) {
return new PhishingDetectionResult(PhishingDetectionResult::CUSTOM_EMAIL_CHECK, false);
}
if($fromEmail === $customEmail) {
return new PhishingDetectionResult(PhishingDetectionResult::CUSTOM_EMAIL_CHECK, false);
}
return new PhishingDetectionResult(PhishingDetectionResult::CUSTOM_EMAIL_CHECK, true, $this->l10n->t('Sender is using a custom email: %1$s instead of the sender email: %2$s', [$customEmail, $fromEmail]));
}
}

Просмотреть файл

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Mail\Service\PhishingDetection;
use OCA\Mail\PhishingDetectionResult;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IL10N;
class DateCheck {
protected IL10N $l10n;
protected ITimeFactory $timeFactory;
public function __construct(IL10N $l10n, ITimeFactory $timeFactory) {
$this->l10n = $l10n;
$this->timeFactory = $timeFactory;
}
public function run(string $date): PhishingDetectionResult {
$now = $this->timeFactory->getDateTime('now');
$dt = $this->timeFactory->getDateTime($date);
if ($dt > $now) {
return new PhishingDetectionResult(PhishingDetectionResult::DATE_CHECK, true, $this->l10n->t("Sent date is in the future"));
}
return new PhishingDetectionResult(PhishingDetectionResult::DATE_CHECK, false);
}
}

Просмотреть файл

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Mail\Service\PhishingDetection;
use Horde_Mime_Headers;
use OCA\Mail\AddressList;
use OCA\Mail\PhishingDetectionList;
class PhishingDetectionService {
public function __construct(private ContactCheck $contactCheck, private CustomEmailCheck $customEmailCheck, private DateCheck $dateCheck, private ReplyToCheck $replyToCheck) {
$this->contactCheck = $contactCheck;
$this->customEmailCheck = $customEmailCheck;
$this->dateCheck = $dateCheck;
$this->replyToCheck = $replyToCheck;
}
public function checkHeadersForPhishing(Horde_Mime_Headers $headers, bool $hasHtmlMessage, string $htmlMessage = ''): array {
$list = new PhishingDetectionList();
/** @psalm-suppress UndefinedMethod */
$fromFN = AddressList::fromHorde($headers->getHeader('From')->getAddressList(true))->first()->getLabel();
/** @psalm-suppress UndefinedMethod */
$fromEmail = AddressList::fromHorde($headers->getHeader('From')->getAddressList(true))->first()->getEmail();
/** @psalm-suppress UndefinedMethod */
$replyToEmailHeader = $headers->getHeader('Reply-To')?->getAddressList(true);
$replyToEmail = isset($replyToEmailHeader)? AddressList::fromHorde($replyToEmailHeader)->first()->getEmail() : null ;
$date = $headers->getHeader('Date')->__get('value');
/** @psalm-suppress UndefinedMethod */
$customEmail = AddressList::fromHorde($headers->getHeader('From')->getAddressList(true))->first()->getCustomEmail();
$list->addCheck($this->replyToCheck->run($fromEmail, $replyToEmail));
$list->addCheck($this->contactCheck->run($fromFN, $fromEmail));
$list->addCheck($this->dateCheck->run($date));
$list->addCheck($this->customEmailCheck->run($fromEmail, $customEmail));
return $list->jsonSerialize();
}
}

Просмотреть файл

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Mail\Service\PhishingDetection;
use OCA\Mail\PhishingDetectionResult;
use OCP\IL10N;
class ReplyToCheck {
protected IL10N $l10n;
public function __construct(IL10N $l10n) {
$this->l10n = $l10n;
}
public function run(string $fromEmail, ?string $replyToEmail) :PhishingDetectionResult {
if ($replyToEmail === null) {
return new PhishingDetectionResult(PhishingDetectionResult::REPLYTO_CHECK, false);
}
if ($fromEmail === $replyToEmail) {
return new PhishingDetectionResult(PhishingDetectionResult::REPLYTO_CHECK, false);
}
return new PhishingDetectionResult(PhishingDetectionResult::REPLYTO_CHECK, true, $this->l10n->t('Reply-To email: %1$s is different from the sender email: %2$s', [$replyToEmail, $fromEmail]));
}
}

Просмотреть файл

@ -7,6 +7,7 @@
<div :class="[message.hasHtmlBody ? 'mail-message-body mail-message-body-html' : 'mail-message-body']"
role="region"
:aria-label="t('mail','Message body')">
<PhishingWarning v-if="message.phishingDetails.warning" :phishing-data="message.phishingDetails.checks" />
<div v-if="message.smime.isSigned && !message.smime.signatureIsValid"
class="invalid-signature-warning">
<LockOffIcon :size="20"
@ -68,6 +69,7 @@ import { html, plain } from '../util/text.js'
import { isPgpgMessage } from '../crypto/pgp.js'
import Itinerary from './Itinerary.vue'
import MessageAttachments from './MessageAttachments.vue'
import PhishingWarning from './PhishingWarning.vue'
import MessageEncryptedBody from './MessageEncryptedBody.vue'
import MessageHTMLBody from './MessageHTMLBody.vue'
import MessagePlainTextBody from './MessagePlainTextBody.vue'
@ -83,6 +85,7 @@ export default {
MessageEncryptedBody,
MessageHTMLBody,
MessagePlainTextBody,
PhishingWarning,
Imip,
LockOffIcon,
ReplyIcon,

Просмотреть файл

@ -0,0 +1,67 @@
<!--
- SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<div class="warning">
<div class="warning__title">
<IconAlertOutline :size="20" :title="t('mail', 'Phishing email')" />
This email might be a phishing attempt
</div>
<ul v-for="(warning,index) in warnings" :key="index" class="warning__list">
<li>{{ warning.message }}</li>
</ul>
</div>
</template>
<script>
import IconAlertOutline from 'vue-material-design-icons/AlertOutline.vue'
export default {
name: 'PhishingWarning',
components: {
IconAlertOutline,
},
props: {
phishingData: {
required: true,
type: Object,
},
},
data() {
return {
showMore: false,
}
},
computed: {
warnings() {
return this.phishingData.filter(check => check.isPhishing)
},
},
}
</script>
<style lang="scss" scoped>
.warning {
background-color:var(--ck-color-base-error);
border-radius: var(--border-radius-rounded);
width: 100%;
text-align: left;
padding: 15px;
margin-bottom: 10px;
&__title {
display: flex;
}
&__list {
list-style-position: inside;
list-style-type: disc;
}
&__links {
margin-top: 10px;
&__button{
margin-bottom: 10px;
}
}
}
</style>

Просмотреть файл

@ -0,0 +1,73 @@
<?php
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Mail\Tests\Integration\Service\Phishing;
use ChristophWurst\Nextcloud\Testing\TestCase;
use Horde_Mime_Headers;
use OCA\Mail\Service\ContactsIntegration;
use OCA\Mail\Service\PhishingDetection\ContactCheck;
use OCA\Mail\Service\PhishingDetection\CustomEmailCheck;
use OCA\Mail\Service\PhishingDetection\DateCheck;
use OCA\Mail\Service\PhishingDetection\PhishingDetectionService;
use OCA\Mail\Service\PhishingDetection\ReplyToCheck;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IL10N;
use PHPUnit\Framework\MockObject\MockObject;
class PhishingDetectionServiceIntegrationTest extends TestCase {
private ContactsIntegration|MockObject $contactsIntegration;
private IL10N|MockObject $l10n;
private ITimeFactory $timeFactory;
private ContactCheck $contactCheck;
private CustomEmailCheck $customEmailCheck;
private DateCheck $dateCheck;
private ReplyToCheck $replyToCheck;
private PhishingDetectionService $service;
protected function setUp(): void {
parent::setUp();
$this->contactsIntegration = $this->createMock(ContactsIntegration::class);
$this->l10n = $this->createMock(IL10N::class);
$this->contactCheck = new ContactCheck($this->contactsIntegration, $this->l10n);
$this->customEmailCheck = new CustomEmailCheck($this->l10n);
$this->dateCheck = new DateCheck($this->l10n, \OC::$server->get(ITimeFactory::class));
$this->replyToCheck = new ReplyToCheck($this->l10n);
$this->service = new PhishingDetectionService($this->contactCheck, $this->customEmailCheck, $this->dateCheck, $this->replyToCheck);
}
public function testContactCheck(): void {
$this->contactsIntegration->expects($this->once())
->method('getContactsWithName')
->willReturn([["id" => 1, "fn" => "John Doe", "email" => ["jhon@example.org","Doe@example.org"]]]);
$result = $this->contactCheck->run("John Doe", "jhon.doe@example.org");
$this->assertTrue($result->isPhishing());
}
public function testCustomEmailCheck(): void {
$result = $this->customEmailCheck->run("jhon@example.org", "jhon.doe@example.org");
$this->assertTrue($result->isPhishing());
}
public function testReplyToCheck(): void {
$result = $this->replyToCheck->run("jhon@example.org", "jhon.doe@example.org");
$this->assertTrue($result->isPhishing());
}
public function testCheckHeadersForPhishing(): void {
$headerStream = fopen(__DIR__ . '/../../../data/phishing-mail-headers.txt', 'r');
$parsedHeaders = Horde_Mime_Headers::parseHeaders($headerStream);
fclose($headerStream);
$result = $this->service->checkHeadersForPhishing($parsedHeaders, false);
$this->assertTrue($result["warning"]);
}
}

Просмотреть файл

@ -134,6 +134,7 @@ class ListControllerTest extends TestCase {
'',
'',
false,
[],
null,
false,
'',
@ -196,6 +197,7 @@ class ListControllerTest extends TestCase {
'',
'',
false,
[],
'https://un.sub.scribe/me',
true,
'',

Просмотреть файл

@ -61,6 +61,7 @@ class MessageMapperTest extends TestCase {
$ids = [1, 3];
$userId = 'user';
$loadBody = false;
$runPhishingCheck = false;
$imapMessageFetcher1 = $this->createMock(ImapMessageFetcher::class);
$imapMessageFetcher2 = $this->createMock(ImapMessageFetcher::class);
@ -98,6 +99,10 @@ class MessageMapperTest extends TestCase {
->method('withBody')
->with($loadBody)
->willReturnSelf();
$imapMessageFetcher1->expects(self::once())
->method('withPhishingCheck')
->with($runPhishingCheck)
->willReturnSelf();
$imapMessageFetcher1->expects(self::once())
->method('fetchMessage')
->with($fetchResult1)
@ -106,6 +111,10 @@ class MessageMapperTest extends TestCase {
->method('withBody')
->with($loadBody)
->willReturnSelf();
$imapMessageFetcher2->expects(self::once())
->method('withPhishingCheck')
->with($runPhishingCheck)
->willReturnSelf();
$imapMessageFetcher2->expects(self::once())
->method('fetchMessage')
->with($fetchResult2)
@ -167,6 +176,7 @@ class MessageMapperTest extends TestCase {
$ids = [1, 3];
$userId = 'user';
$loadBody = false;
$runPhishingCheck = false;
$imapMessageFetcher1 = $this->createMock(ImapMessageFetcher::class);
$message1 = $this->createMock(IMAPMessage::class);
@ -201,6 +211,10 @@ class MessageMapperTest extends TestCase {
->method('withBody')
->with($loadBody)
->willReturnSelf();
$imapMessageFetcher1->expects(self::once())
->method('withPhishingCheck')
->with($runPhishingCheck)
->willReturnSelf();
$imapMessageFetcher1->expects(self::once())
->method('fetchMessage')
->with($fetchResult1)

Просмотреть файл

@ -72,6 +72,7 @@ class IMAPMessageTest extends TestCase {
'',
'disposition',
false,
[],
null,
false,
'',
@ -113,6 +114,7 @@ class IMAPMessageTest extends TestCase {
'',
'disposition',
false,
[],
null,
false,
null,
@ -152,6 +154,7 @@ class IMAPMessageTest extends TestCase {
'hasHtmlBody' => true,
'dispositionNotificationTo' => 'disposition',
'hasDkimSignature' => false,
'phishingDetails' => [],
'scheduling' => [],
], $json);
$this->assertEquals(1234, $json['uid']);

Просмотреть файл

@ -0,0 +1,72 @@
<?php
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Mail\Tests\Unit\Service\Phishing;
use ChristophWurst\Nextcloud\Testing\TestCase;
use OCA\Mail\Service\ContactsIntegration;
use OCA\Mail\Service\PhishingDetection\ContactCheck;
use OCP\IL10N;
use PHPUnit\Framework\MockObject\MockObject;
class ContactCheckTest extends TestCase {
private IL10N|MockObject $l10n;
private ContactsIntegration|MockObject $contactsIntegration;
private ContactCheck|MockObject $service;
protected function setUp(): void {
parent::setUp();
$this->l10n = $this->createMock(IL10N::class);
$this->contactsIntegration = $this->createMock(ContactsIntegration::class);
$this->service = new ContactCheck($this->contactsIntegration, $this->l10n);
}
public function testContactInABCorrectEmail(): void {
$fn = "John Doe";
$email = "jhon@example.com" ;
$contacts = [
[
"email" => ["jhon@example.com"]
]
];
$this->contactsIntegration->method('getContactsWithName')->willReturn($contacts);
$result = $this->service->run($fn, $email);
$this->assertFalse($result->isPhishing());
}
public function testContactInABWrongEmail(): void {
$fn = "John Doe";
$email = "jhon@example.com" ;
$contacts = [
[
"email" => ["jhonDoe@example.com"]
]
];
$this->contactsIntegration->method('getContactsWithName')->willReturn($contacts);
$result = $this->service->run($fn, $email);
$this->assertTrue($result->isPhishing());
}
public function testContactNotInAB(): void {
$fn = "John Doe";
$email = "jhon@example.com" ;
$contacts = [];
$this->contactsIntegration->method('getContactsWithName')->willReturn($contacts);
$result = $this->service->run($fn, $email);
$this->assertFalse($result->isPhishing());
}
}

Просмотреть файл

@ -0,0 +1,49 @@
<?php
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Mail\Tests\Unit\Service\Phishing;
use ChristophWurst\Nextcloud\Testing\TestCase;
use OCA\Mail\Service\PhishingDetection\CustomEmailCheck;
use OCP\IL10N;
use PHPUnit\Framework\MockObject\MockObject;
class CustomEmailCheckTest extends TestCase {
private IL10N|MockObject $l10n;
private CustomEmailCheck|MockObject $service;
protected function setUp(): void {
parent::setUp();
$this->l10n = $this->createMock(IL10N::class);
$this->service = new CustomEmailCheck($this->l10n);
}
public function testNoEmail(): void {
$email = "jhon@example.com";
$result = $this->service->run($email, null);
$this->assertFalse($result->isPhishing());
}
public function testSameEmail(): void {
$email = "jhon@example.com";
$result = $this->service->run($email, $email);
$this->assertFalse($result->isPhishing());
}
public function testDifferentEmail(): void {
$email = "jhon@example.com";
$customEmail = "jhondoe@example.com";
$result = $this->service->run($email, $customEmail);
$this->assertTrue($result->isPhishing());
}
}

Просмотреть файл

@ -0,0 +1,68 @@
<?php
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Mail\Tests\Unit\Service\Phishing;
use ChristophWurst\Nextcloud\Testing\TestCase;
use DateTime;
use OCA\Mail\Service\PhishingDetection\DateCheck;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IL10N;
use PHPUnit\Framework\MockObject\MockObject;
class DateCheckTest extends TestCase {
private IL10N|MockObject $l10n;
private DateCheck|MockObject $service;
private ITimeFactory|MockObject $time;
protected function setUp(): void {
parent::setUp();
$this->l10n = $this->createMock(IL10N::class);
$this->time = $this->createMock(ITimeFactory::class);
$this->service = new DateCheck($this->l10n, $this->time);
}
public function testInThePast(): void {
$this->time->expects($this->exactly(2))
->method('getDateTime')
->withConsecutive(
['now'],
["26 June 2024 22:45:34 +0000"]
)
->willReturnOnConsecutiveCalls(
new DateTime('now'),
new DateTime('26 June 2024 22:45:34 +0000')
);
$result = $this->service->run("26 June 2024 22:45:34 +0000");
$this->assertFalse($result->isPhishing());
}
public function testInTheFuture(): void {
$this->time->expects($this->exactly(2))
->method('getDateTime')
->withConsecutive(
['now'],
["17 June 3000 22:45:34 +0000"]
)
->willReturnOnConsecutiveCalls(
new DateTime('now'),
new DateTime('17 June 3000 22:45:34 +0000')
);
$result = $this->service->run("17 June 3000 22:45:34 +0000");
$this->assertTrue($result->isPhishing());
}
}

Просмотреть файл

@ -0,0 +1,63 @@
<?php
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Mail\Tests\Unit\Service\Phishing;
use ChristophWurst\Nextcloud\Testing\TestCase;
use Horde_Mime_Headers;
use OCA\Mail\PhishingDetectionResult;
use OCA\Mail\Service\PhishingDetection\ContactCheck;
use OCA\Mail\Service\PhishingDetection\CustomEmailCheck;
use OCA\Mail\Service\PhishingDetection\DateCheck;
use OCA\Mail\Service\PhishingDetection\PhishingDetectionService;
use OCA\Mail\Service\PhishingDetection\ReplyToCheck;
use PHPUnit\Framework\MockObject\MockObject;
class PhishingDetectionServiceTest extends TestCase {
private ContactCheck|MockObject $contactCheck;
private CustomEmailCheck|MockObject $customEmailCheck;
private DateCheck|MockObject $dateCheck;
private ReplyToCheck|MockObject $replyToCheck;
private PhishingDetectionService $service;
protected function setUp(): void {
parent::setUp();
$this->contactCheck = $this->createMock(ContactCheck::class);
$this->customEmailCheck = $this->createMock(customEmailCheck::class);
$this->dateCheck = $this->createMock(DateCheck::class);
$this->replyToCheck = $this->createMock(ReplyToCheck::class);
$this->service = new PhishingDetectionService($this->contactCheck, $this->customEmailCheck, $this->dateCheck, $this->replyToCheck);
}
public function testCheckHeadersForPhishing(): void {
$headerStream = fopen(__DIR__ . '/../../../data/phishing-mail-headers.txt', 'r');
$parsedHeaders = Horde_Mime_Headers::parseHeaders($headerStream);
fclose($headerStream);
$this->replyToCheck->expects($this->once())
->method('run')
->with('jhondoe@example.com', 'batman@example.com')
->willReturn(new PhishingDetectionResult(PhishingDetectionResult::REPLYTO_CHECK, false));
$this->contactCheck->expects($this->once())
->method('run')
->with('Jhon Doe', 'jhondoe@example.com')
->willReturn(new PhishingDetectionResult(PhishingDetectionResult::CONTACTS_CHECK, false));
$this->dateCheck->expects($this->once())
->method('run')
->with('Tue, 28 May 3024 13:02:15 +0200')
->willReturn(new PhishingDetectionResult(PhishingDetectionResult::DATE_CHECK, false));
$this->customEmailCheck->expects($this->once())
->method('run')
->willReturn(new PhishingDetectionResult(PhishingDetectionResult::CUSTOM_EMAIL_CHECK, false));
$result = $this->service->checkHeadersForPhishing($parsedHeaders, false);
$this->assertFalse($result["warning"]);
}
}

Просмотреть файл

@ -0,0 +1,50 @@
<?php
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Mail\Tests\Integration\Service\Phishing;
use ChristophWurst\Nextcloud\Testing\TestCase;
use OCA\Mail\Service\PhishingDetection\ReplyToCheck;
use OCP\IL10N;
use PHPUnit\Framework\MockObject\MockObject;
class ReplyToCheckTest extends TestCase {
private IL10N|MockObject $l10n;
private ReplyToCheck|MockObject $service;
protected function setUp(): void {
parent::setUp();
$this->l10n = $this->createMock(IL10N::class);
$this->service = new ReplyToCheck($this->l10n);
}
public function testNoEmail(): void {
$email = "jhon@example.com";
$result = $this->service->run($email, null);
$this->assertFalse($result->isPhishing());
}
public function testSameEmail(): void {
$email = "jhon@example.com";
$result = $this->service->run($email, $email);
$this->assertFalse($result->isPhishing());
}
public function testDifferentEmail(): void {
$email = "jhon@example.com";
$customEmail = "jhondoe@example.com";
$result = $this->service->run($email, $customEmail);
$this->assertTrue($result->isPhishing());
}
}

Просмотреть файл

@ -0,0 +1,36 @@
MIME-Version: 1.0
Received: from DB3P189MB2377.EURP189.PROD.OUTLOOK.COM (2603:10a6:10:434::18)
by HE1P189MB0380.EURP189.PROD.OUTLOOK.COM with HTTPS; Tue, 28 May 2024
11:02:30 +0000
Received: from BYAPR03CA0019.namprd03.prod.outlook.com (2603:10b6:a02:a8::32)
by DB3P189MB2377.EURP189.PROD.OUTLOOK.COM (2603:10a6:10:434::18) with
Microsoft SMTP Server (version=TLS1_2,
cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.7611.29; Tue, 28 May
2024 11:02:29 +0000
Received: from MWH0EPF000971E4.namprd02.prod.outlook.com
(2603:10b6:a02:a8:cafe::b6) by BYAPR03CA0019.outlook.office365.com
(2603:10b6:a02:a8::32) with Microsoft SMTP Server (version=TLS1_2,
cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.7633.17 via Frontend
Transport; Tue, 28 May 2024 11:02:27 +0000
Authentication-Results: spf=pass (sender IP is 209.85.218.45)
smtp.mailfrom=example.com; dkim=pass (signature was verified)
header.d=example.com;dmarc=pass action=none header.from=example.com;compauth=pass
reason=100
Received-SPF: Pass (protection.outlook.com: domain of example.com designates
209.85.218.45 as permitted sender) receiver=protection.outlook.com;
client-ip=209.85.218.45; helo=mail-ej1-f45.google.com; pr=C
Received: from mail-ej1-f45.google.com (209.85.218.45) by
MWH0EPF000971E4.mail.protection.outlook.com (10.167.243.72) with Microsoft
SMTP Server (version=TLS1_3, cipher=TLS_AES_256_GCM_SHA384) id 15.20.7633.15
via Frontend Transport; Tue, 28 May 2024 11:02:27 +0000
Received: by mail-ej1-f45.google.com with SMTP id a640c23a62f3a-a626777f74eso85431166b.3
for <bob@example.com>; Tue, 28 May 2024 04:02:27 -0700 (PDT)
From: Jhon Doe <jhondoe@example.com>
Date: Tue, 28 May 3024 13:02:15 +0200
Message-ID: <CAOFMF9F1cMTJz=cSEk13i=3xDQ4fbrb5G-z1GWntYFrg+A__Vg@mail.example.com>
Subject: Smells Phishy 🎣
To: bobby bob <bob@example.com>
Content-Type: multipart/alternative; boundary="000000000000b4c1d1061981915d"
X-IncomingHeaderCount: 13
Return-Path: jhondoe@example.com
Reply-To: Jhon Doe <batman@example.com>

Просмотреть файл

@ -0,0 +1,2 @@
SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
SPDX-License-Identifier: AGPL-3.0-or-later