Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: trusted senders #9685

Closed
wants to merge 20 commits into from
Closed
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
18 changes: 18 additions & 0 deletions lib/Address.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,27 @@ public function getLabel(): ?string {
// 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
*/
Expand Down
27 changes: 25 additions & 2 deletions lib/IMAP/ImapMessageFetcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
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;
Expand All @@ -53,8 +54,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;

Expand All @@ -71,6 +74,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;
Expand All @@ -81,13 +85,16 @@ public function __construct(int $uid,
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;
}


Expand All @@ -102,6 +109,17 @@ public function withBody(bool $value): 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.
Expand Down Expand Up @@ -255,6 +273,7 @@ public function fetchMessage(?Horde_Imap_Client_Data_Fetch $fetch = null): IMAPM
$this->rawReferences,
$this->dispositionNotificationTo,
$this->hasDkimSignature,
$this->phishingDetails,
$this->unsubscribeUrl,
$this->isOneClickUnsubscribe,
$this->unsubscribeMailto,
Expand Down Expand Up @@ -495,7 +514,7 @@ private function decodeSubject(Horde_Imap_Client_Data_Envelope $envelope): strin

private function parseHeaders(Horde_Imap_Client_Data_Fetch $fetch): void {
/** @var resource $headersStream */
$headersStream = $fetch->getHeaderText('0', Horde_Imap_Client_Data_Fetch::HEADER_STREAM);
$headersStream = $fetch->getHeaderText('0', 10);
$parsedHeaders = Horde_Mime_Headers::parseHeaders($headersStream);
fclose($headersStream);

Expand All @@ -512,6 +531,10 @@ private function parseHeaders(Horde_Imap_Client_Data_Fetch $fetch): void {
$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();
Expand Down
7 changes: 6 additions & 1 deletion lib/IMAP/ImapMessageFetcherFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,23 @@
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,
Expand All @@ -56,6 +60,7 @@ public function build(int $uid,
$this->htmlService,
$this->smimeService,
$this->charsetConverter,
$this->phishingDetectionService,
);
}
}
8 changes: 5 additions & 3 deletions lib/IMAP/MessageMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public function find(Horde_Imap_Client_Base $client,
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");
Expand Down Expand Up @@ -264,7 +264,8 @@ public function findByIds(Horde_Imap_Client_Base $client,
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();
Expand Down Expand Up @@ -309,7 +310,7 @@ public function findByIds(Horde_Imap_Client_Base $client,
$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(),
Expand All @@ -318,6 +319,7 @@ public function findByIds(Horde_Imap_Client_Base $client,
$userId,
)
->withBody($loadBody)
->withPhishingCheck($runPhishingCheck)
->fetchMessage($fetchResult);
}, $fetchResults);
}
Expand Down
4 changes: 4 additions & 0 deletions lib/Model/IMAPMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,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;
Expand Down Expand Up @@ -105,6 +106,7 @@ public function __construct(int $uid,
string $rawReferences,
string $dispositionNotificationTo,
bool $hasDkimSignature,
array $phishingDetails,
?string $unsubscribeUrl,
bool $isOneClickUnsubscribe,
?string $unsubscribeMailto,
Expand Down Expand Up @@ -133,6 +135,7 @@ public function __construct(int $uid,
$this->rawReferences = $rawReferences;
$this->dispositionNotificationTo = $dispositionNotificationTo;
$this->hasDkimSignature = $hasDkimSignature;
$this->phishingDetails = $phishingDetails;
$this->unsubscribeUrl = $unsubscribeUrl;
$this->isOneClickUnsubscribe = $isOneClickUnsubscribe;
$this->unsubscribeMailto = $unsubscribeMailto;
Expand Down Expand Up @@ -319,6 +322,7 @@ public function jsonSerialize() {
'hasHtmlBody' => $this->hasHtmlMessage,
'dispositionNotificationTo' => $this->getDispositionNotificationTo(),
'hasDkimSignature' => $this->hasDkimSignature,
'phishingDetails' => $this->phishingDetails,
'unsubscribeUrl' => $this->unsubscribeUrl,
'isOneClickUnsubscribe' => $this->isOneClickUnsubscribe,
'unsubscribeMailto' => $this->unsubscribeMailto,
Expand Down
70 changes: 70 additions & 0 deletions lib/PhishingDetectionList.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

declare(strict_types=1);

/**
* @copyright 2024 Hamza Mahjoubi <hamza.mahjoubi221@proton.me>
*
* @author 2024 Hamza Mahjoubi <hamza.mahjoubi221@proton.me>
*
* @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;

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(),
];
}

}
75 changes: 75 additions & 0 deletions lib/PhishingDetectionResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);

/**
* @copyright 2024 Hamza Mahjoubi <hamza.mahjoubi221@proton.me>
*
* @author 2024 Hamza Mahjoubi <hamza.mahjoubi221@proton.me>
*
* @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;

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,
];
}

}
Loading
Loading