add: full multi-tenancy control

This commit is contained in:
Cauê Faleiros
2026-02-02 15:31:15 -03:00
commit c6ec92802b
1711 changed files with 258106 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
<?php
namespace Webkul\Email\InboundEmailProcessor\Contracts;
interface InboundEmailProcessor
{
/**
* Process messages from all folders.
*
* @return mixed
*/
public function processMessagesFromAllFolders();
/**
* Process the inbound email.
*
* @param mixed|null $content
*/
public function processMessage($content = null): void;
}

View File

@@ -0,0 +1,125 @@
<?php
namespace Webkul\Email\InboundEmailProcessor;
use Webkul\Email\Helpers\HtmlFilter;
use Webkul\Email\Helpers\Parser;
use Webkul\Email\InboundEmailProcessor\Contracts\InboundEmailProcessor;
use Webkul\Email\Repositories\AttachmentRepository;
use Webkul\Email\Repositories\EmailRepository;
class SendgridEmailProcessor implements InboundEmailProcessor
{
/**
* Create a new repository instance.
*
* @return void
*/
public function __construct(
protected EmailRepository $emailRepository,
protected AttachmentRepository $attachmentRepository,
protected Parser $emailParser,
protected HtmlFilter $htmlFilter
) {}
/**
* Process messages from all folders.
*/
public function processMessagesFromAllFolders()
{
/**
* SendGrid's Inbound Parse is a specialized tool for developers to handle incoming emails in
* their applications, but it doesn't replace the full functionality of IMAP for typical
* email client usage. Thats why we can't process the messages.
*/
throw new \Exception('Currently bulk processing is not supported for Sendgrid.');
}
/**
* Process the inbound email.
*/
public function processMessage($message = null): void
{
$this->emailParser->setText($message);
$email = $this->emailRepository->findOneWhere(['message_id' => $messageID = $this->emailParser->getHeader('message-id')]);
if ($email) {
return;
}
$headers = [
'from' => $this->emailParser->parseEmailAddress('from'),
'sender' => $this->emailParser->parseEmailAddress('sender'),
'reply_to' => $this->emailParser->parseEmailAddress('to'),
'cc' => $this->emailParser->parseEmailAddress('cc'),
'bcc' => $this->emailParser->parseEmailAddress('bcc'),
'subject' => $this->emailParser->getHeader('subject'),
'name' => $this->emailParser->parseSenderName(),
'source' => 'email',
'user_type' => 'person',
'message_id' => $messageID ?? time().'@'.config('mail.domain'),
'reference_ids' => htmlspecialchars_decode($this->emailParser->getHeader('references')),
'in_reply_to' => htmlspecialchars_decode($this->emailParser->getHeader('in-reply-to')),
];
foreach ($headers['reply_to'] as $to) {
if ($email = $this->emailRepository->findOneWhere(['message_id' => $to])) {
break;
}
}
if (! isset($email) && $headers['in_reply_to']) {
$email = $this->emailRepository->findOneWhere(['message_id' => $headers['in_reply_to']]);
if (! $email) {
$email = $this->emailRepository->findOneWhere([['reference_ids', 'like', '%'.$headers['in_reply_to'].'%']]);
}
}
if (! isset($email) && $headers['reference_ids']) {
$referenceIds = explode(' ', $headers['reference_ids']);
foreach ($referenceIds as $referenceId) {
if ($email = $this->emailRepository->findOneWhere([['reference_ids', 'like', '%'.$referenceId.'%']])) {
break;
}
}
}
if (! $reply = $this->emailParser->getMessageBody('text')) {
$reply = $this->emailParser->getTextMessageBody();
}
if (! isset($email)) {
$email = $this->emailRepository->create(array_merge($headers, [
'folders' => ['inbox'],
'reply' => $reply,
'unique_id' => time().'@'.config('mail.domain'),
'reference_ids' => [$headers['message_id']],
'user_type' => 'person',
]));
$this->attachmentRepository->uploadAttachments($email, [
'source' => 'email',
'attachments' => $this->emailParser->getAttachments(),
]);
} else {
$parentEmail = $this->emailRepository->update([
'folders' => array_unique(array_merge($email->folders, ['inbox'])),
'reference_ids' => array_merge($email->reference_ids ?? [], [$headers['message_id']]),
], $email->id);
$email = $this->emailRepository->create(array_merge($headers, [
'reply' => $this->htmlFilter->process($reply, ''),
'parent_id' => $parentEmail->id,
'user_type' => 'person',
]));
$this->attachmentRepository->uploadAttachments($email, [
'source' => 'email',
'attachments' => $this->emailParser->getAttachments(),
]);
}
}
}

View File

@@ -0,0 +1,228 @@
<?php
namespace Webkul\Email\InboundEmailProcessor;
use Webklex\IMAP\Facades\Client;
use Webkul\Email\Enums\SupportedFolderEnum;
use Webkul\Email\InboundEmailProcessor\Contracts\InboundEmailProcessor;
use Webkul\Email\Repositories\AttachmentRepository;
use Webkul\Email\Repositories\EmailRepository;
class WebklexImapEmailProcessor implements InboundEmailProcessor
{
/**
* The IMAP client instance.
*/
protected $client;
/**
* Create a new repository instance.
*
* @return void
*/
public function __construct(
protected EmailRepository $emailRepository,
protected AttachmentRepository $attachmentRepository
) {
$this->client = Client::make($this->getDefaultConfigs());
$this->client->connect();
if (! $this->client->isConnected()) {
throw new \Exception('Failed to connect to the mail server.');
}
}
/**
* Close the connection.
*/
public function __destruct()
{
$this->client->disconnect();
}
/**
* Process messages from all folders.
*/
public function processMessagesFromAllFolders()
{
try {
$rootFolders = $this->client->getFolders();
$this->processMessagesFromLeafFolders($rootFolders);
} catch (\Exception $e) {
throw new \Exception($e->getMessage());
}
}
/**
* Process the inbound email.
*
* @param ?\Webklex\PHPIMAP\Message $message
*/
public function processMessage($message = null): void
{
$attributes = $message->getAttributes();
$messageId = $attributes['message_id']->first();
$email = $this->emailRepository->findOneByField('message_id', $messageId);
if ($email) {
return;
}
$replyToEmails = $this->getEmailsByAttributeCode($attributes, 'to');
foreach ($replyToEmails as $to) {
if ($email = $this->emailRepository->findOneWhere(['message_id' => $to])) {
break;
}
}
if (! isset($email) && isset($attributes['in_reply_to'])) {
$inReplyTo = $attributes['in_reply_to']->first();
$email = $this->emailRepository->findOneWhere(['message_id' => $inReplyTo]);
if (! $email) {
$email = $this->emailRepository->findOneWhere([['reference_ids', 'like', '%'.$inReplyTo.'%']]);
}
}
$references = [$messageId];
if (! isset($email) && isset($attributes['references'])) {
array_push($references, ...$attributes['references']->all());
foreach ($references as $reference) {
if ($email = $this->emailRepository->findOneWhere([['reference_ids', 'like', '%'.$reference.'%']])) {
break;
}
}
}
/**
* Maps the folder name to the supported folder in our application.
*
* To Do: Review this.
*/
$folderName = match ($message->getFolder()->name) {
'INBOX' => SupportedFolderEnum::INBOX->value,
'Important' => SupportedFolderEnum::IMPORTANT->value,
'Starred' => SupportedFolderEnum::STARRED->value,
'Drafts' => SupportedFolderEnum::DRAFT->value,
'Sent Mail' => SupportedFolderEnum::SENT->value,
'Trash' => SupportedFolderEnum::TRASH->value,
default => '',
};
$parentEmail = null;
if ($email) {
$parentEmail = $this->emailRepository->update([
'folders' => array_unique(array_merge($email->folders, [$folderName])),
'reference_ids' => array_merge($email->reference_ids ?? [], [$references]),
], $email->id);
}
$email = $this->emailRepository->create([
'from' => $attributes['from']->first()->mail,
'subject' => $attributes['subject']->first(),
'name' => $attributes['from']->first()->personal,
'reply' => $message->bodies['html'] ?? $message->bodies['text'],
'is_read' => (int) $message->flags()->has('seen'),
'folders' => [$folderName],
'reply_to' => $this->getEmailsByAttributeCode($attributes, 'to'),
'cc' => $this->getEmailsByAttributeCode($attributes, 'cc'),
'bcc' => $this->getEmailsByAttributeCode($attributes, 'bcc'),
'source' => 'email',
'user_type' => 'person',
'unique_id' => $messageId,
'message_id' => $messageId,
'reference_ids' => $references,
'created_at' => $this->convertToDesiredTimezone($message->date->toDate()),
'parent_id' => $parentEmail?->id,
]);
if ($message->hasAttachments()) {
$this->attachmentRepository->uploadAttachments($email, [
'source' => 'email',
'attachments' => $message->getAttachments(),
]);
}
}
/**
* Process the messages from all folders.
*
* @param \Webklex\IMAP\Support\FolderCollection $rootFoldersCollection
*/
protected function processMessagesFromLeafFolders($rootFoldersCollection = null): void
{
$rootFoldersCollection->each(function ($folder) {
if (! $folder->children->isEmpty()) {
$this->processMessagesFromLeafFolders($folder->children);
return;
}
if (in_array($folder->name, ['All Mail'])) {
return;
}
return $folder->query()->since(now()->subDays(10))->get()->each(function ($message) {
$this->processMessage($message);
});
});
}
/**
* Get the emails by the attribute code.
*/
protected function getEmailsByAttributeCode(array $attributes, string $attributeCode): array
{
$emails = [];
if (isset($attributes[$attributeCode])) {
$emails = collect($attributes[$attributeCode]->all())->map(fn ($attribute) => $attribute->mail)->toArray();
}
return $emails;
}
/**
* Convert the date to the desired timezone.
*
* @param \Carbon\Carbon $carbonDate
* @param ?string $targetTimezone
*/
protected function convertToDesiredTimezone($carbonDate, $targetTimezone = null)
{
$targetTimezone = $targetTimezone ?: config('app.timezone');
return $carbonDate->clone()->setTimezone($targetTimezone);
}
/**
* Get the default configurations.
*/
protected function getDefaultConfigs(): array
{
$defaultConfig = config('imap.accounts.default');
$defaultConfig['host'] = core()->getConfigData('email.imap.account.host') ?: $defaultConfig['host'];
$defaultConfig['port'] = core()->getConfigData('email.imap.account.port') ?: $defaultConfig['port'];
$defaultConfig['encryption'] = core()->getConfigData('email.imap.account.encryption') ?: $defaultConfig['encryption'];
$defaultConfig['validate_cert'] = (bool) core()->getConfigData('email.imap.account.validate_cert');
$defaultConfig['username'] = core()->getConfigData('email.imap.account.username') ?: $defaultConfig['username'];
$defaultConfig['password'] = core()->getConfigData('email.imap.account.password') ?: $defaultConfig['password'];
return $defaultConfig;
}
}