add: full multi-tenancy control
This commit is contained in:
28
packages/Webkul/Email/composer.json
Executable file
28
packages/Webkul/Email/composer.json
Executable file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "krayin/laravel-email",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jitendra Singh",
|
||||
"email": "jitendra@webkul.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"krayin/laravel-contact": "^1.0",
|
||||
"krayin/laravel-core": "^1.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Webkul\\Email\\": "src/"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Webkul\\Email\\Providers\\EmailServiceProvider"
|
||||
],
|
||||
"aliases": {}
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev"
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Email\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Webkul\Email\InboundEmailProcessor\Contracts\InboundEmailProcessor;
|
||||
|
||||
class ProcessInboundEmails extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'inbound-emails:process';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'This command will process the incoming emails from the mail server.';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
protected InboundEmailProcessor $inboundEmailProcessor
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('Processing the incoming emails.');
|
||||
|
||||
$this->inboundEmailProcessor->processMessagesFromAllFolders();
|
||||
|
||||
$this->info('Incoming emails processed successfully.');
|
||||
}
|
||||
}
|
||||
5
packages/Webkul/Email/src/Contracts/Attachment.php
Normal file
5
packages/Webkul/Email/src/Contracts/Attachment.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Email\Contracts;
|
||||
|
||||
interface Attachment {}
|
||||
5
packages/Webkul/Email/src/Contracts/Email.php
Normal file
5
packages/Webkul/Email/src/Contracts/Email.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Email\Contracts;
|
||||
|
||||
interface Email {}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('emails', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->string('subject')->nullable();
|
||||
$table->string('source');
|
||||
$table->string('user_type');
|
||||
$table->string('name')->nullable();
|
||||
$table->text('reply')->nullable();
|
||||
$table->boolean('is_read')->default(0);
|
||||
$table->json('folders')->nullable();
|
||||
$table->json('from')->nullable();
|
||||
$table->json('sender')->nullable();
|
||||
$table->json('reply_to')->nullable();
|
||||
$table->json('cc')->nullable();
|
||||
$table->json('bcc')->nullable();
|
||||
$table->string('unique_id')->nullable()->unique();
|
||||
$table->string('message_id')->unique();
|
||||
$table->json('reference_ids')->nullable();
|
||||
|
||||
$table->integer('person_id')->unsigned()->nullable();
|
||||
$table->foreign('person_id')->references('id')->on('persons')->onDelete('set null');
|
||||
|
||||
$table->integer('lead_id')->unsigned()->nullable();
|
||||
$table->foreign('lead_id')->references('id')->on('leads')->onDelete('set null');
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::table('emails', function (Blueprint $table) {
|
||||
$table->integer('parent_id')->unsigned()->nullable();
|
||||
$table->foreign('parent_id')->references('id')->on('emails')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('emails');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('email_attachments', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->string('name')->nullable();
|
||||
$table->string('path');
|
||||
$table->integer('size')->nullable();
|
||||
$table->string('content_type')->nullable();
|
||||
$table->string('content_id')->nullable();
|
||||
|
||||
$table->integer('email_id')->unsigned();
|
||||
$table->foreign('email_id')->references('id')->on('emails')->onDelete('cascade');
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('email_attachments');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('email_tags', function (Blueprint $table) {
|
||||
$table->integer('tag_id')->unsigned();
|
||||
$table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
|
||||
|
||||
$table->integer('email_id')->unsigned();
|
||||
$table->foreign('email_id')->references('id')->on('emails')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('email_tags');
|
||||
}
|
||||
};
|
||||
46
packages/Webkul/Email/src/Enums/SupportedFolderEnum.php
Normal file
46
packages/Webkul/Email/src/Enums/SupportedFolderEnum.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Email\Enums;
|
||||
|
||||
enum SupportedFolderEnum: string
|
||||
{
|
||||
/**
|
||||
* Inbox.
|
||||
*/
|
||||
case INBOX = 'inbox';
|
||||
|
||||
/**
|
||||
* Important.
|
||||
*/
|
||||
case IMPORTANT = 'important';
|
||||
|
||||
/**
|
||||
* Starred.
|
||||
*/
|
||||
case STARRED = 'starred';
|
||||
|
||||
/**
|
||||
* Draft.
|
||||
*/
|
||||
case DRAFT = 'draft';
|
||||
|
||||
/**
|
||||
* Outbox.
|
||||
*/
|
||||
case OUTBOX = 'outbox';
|
||||
|
||||
/**
|
||||
* Sent.
|
||||
*/
|
||||
case SENT = 'sent';
|
||||
|
||||
/**
|
||||
* Spam.
|
||||
*/
|
||||
case SPAM = 'spam';
|
||||
|
||||
/**
|
||||
* Trash.
|
||||
*/
|
||||
case TRASH = 'trash';
|
||||
}
|
||||
108
packages/Webkul/Email/src/Helpers/Attachment.php
Normal file
108
packages/Webkul/Email/src/Helpers/Attachment.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Email\Helpers;
|
||||
|
||||
class Attachment
|
||||
{
|
||||
/**
|
||||
* Content.
|
||||
*
|
||||
* @var File Content
|
||||
*/
|
||||
private $content = null;
|
||||
|
||||
/**
|
||||
* Create an helper instance
|
||||
*/
|
||||
public function __construct(
|
||||
public $filename,
|
||||
public $contentType,
|
||||
public $stream,
|
||||
public $contentDisposition = 'attachment',
|
||||
public $contentId = '',
|
||||
public $headers = []
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve the attachment filename.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFilename()
|
||||
{
|
||||
return $this->filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the attachment content type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getContentType()
|
||||
{
|
||||
return $this->contentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the attachment content disposition.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getContentDisposition()
|
||||
{
|
||||
return $this->contentDisposition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the attachment content ID.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getContentID()
|
||||
{
|
||||
return $this->contentId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the attachment headers.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getHeaders()
|
||||
{
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the contents a few bytes at a time until completed.
|
||||
*
|
||||
* Once read to completion, it always returns false.
|
||||
*
|
||||
* @param int $bytes
|
||||
* @return string
|
||||
*/
|
||||
public function read($bytes = 2082)
|
||||
{
|
||||
return feof($this->stream) ? false : fread($this->stream, $bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the file content in one go.
|
||||
*
|
||||
* Once you retrieve the content you cannot use MimeMailParser_attachment::read().
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getContent()
|
||||
{
|
||||
if ($this->content === null) {
|
||||
fseek($this->stream, 0);
|
||||
|
||||
while (($buf = $this->read()) !== false) {
|
||||
$this->content .= $buf;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->content;
|
||||
}
|
||||
}
|
||||
353
packages/Webkul/Email/src/Helpers/Charset.php
Normal file
353
packages/Webkul/Email/src/Helpers/Charset.php
Normal file
@@ -0,0 +1,353 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Email\Helpers;
|
||||
|
||||
use Webkul\Email\Helpers\Contracts\CharsetManager;
|
||||
|
||||
class Charset implements CharsetManager
|
||||
{
|
||||
/**
|
||||
* Charset aliases.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $charsetAlias = [
|
||||
'ascii' => 'us-ascii',
|
||||
'us-ascii' => 'us-ascii',
|
||||
'ansi_x3.4-1968' => 'us-ascii',
|
||||
'646' => 'us-ascii',
|
||||
'iso-8859-1' => 'ISO-8859-1',
|
||||
'iso-8859-2' => 'ISO-8859-2',
|
||||
'iso-8859-3' => 'ISO-8859-3',
|
||||
'iso-8859-4' => 'ISO-8859-4',
|
||||
'iso-8859-5' => 'ISO-8859-5',
|
||||
'iso-8859-6' => 'ISO-8859-6',
|
||||
'iso-8859-6-i' => 'ISO-8859-6-I',
|
||||
'iso-8859-6-e' => 'ISO-8859-6-E',
|
||||
'iso-8859-7' => 'ISO-8859-7',
|
||||
'iso-8859-8' => 'ISO-8859-8',
|
||||
'iso-8859-8-i' => 'ISO-8859-8-I',
|
||||
'iso-8859-8-e' => 'ISO-8859-8-E',
|
||||
'iso-8859-9' => 'ISO-8859-9',
|
||||
'iso-8859-10' => 'ISO-8859-10',
|
||||
'iso-8859-11' => 'ISO-8859-11',
|
||||
'iso-8859-13' => 'ISO-8859-13',
|
||||
'iso-8859-14' => 'ISO-8859-14',
|
||||
'iso-8859-15' => 'ISO-8859-15',
|
||||
'iso-8859-16' => 'ISO-8859-16',
|
||||
'iso-ir-111' => 'ISO-IR-111',
|
||||
'iso-2022-cn' => 'ISO-2022-CN',
|
||||
'iso-2022-cn-ext' => 'ISO-2022-CN',
|
||||
'iso-2022-kr' => 'ISO-2022-KR',
|
||||
'iso-2022-jp' => 'ISO-2022-JP',
|
||||
'utf-16be' => 'UTF-16BE',
|
||||
'utf-16le' => 'UTF-16LE',
|
||||
'utf-16' => 'UTF-16',
|
||||
'windows-1250' => 'windows-1250',
|
||||
'windows-1251' => 'windows-1251',
|
||||
'windows-1252' => 'windows-1252',
|
||||
'windows-1253' => 'windows-1253',
|
||||
'windows-1254' => 'windows-1254',
|
||||
'windows-1255' => 'windows-1255',
|
||||
'windows-1256' => 'windows-1256',
|
||||
'windows-1257' => 'windows-1257',
|
||||
'windows-1258' => 'windows-1258',
|
||||
'ibm866' => 'IBM866',
|
||||
'ibm850' => 'IBM850',
|
||||
'ibm852' => 'IBM852',
|
||||
'ibm855' => 'IBM855',
|
||||
'ibm857' => 'IBM857',
|
||||
'ibm862' => 'IBM862',
|
||||
'ibm864' => 'IBM864',
|
||||
'utf-8' => 'UTF-8',
|
||||
'utf-7' => 'UTF-7',
|
||||
'shift_jis' => 'Shift_JIS',
|
||||
'big5' => 'Big5',
|
||||
'euc-jp' => 'EUC-JP',
|
||||
'euc-kr' => 'EUC-KR',
|
||||
'gb2312' => 'GB2312',
|
||||
'gb18030' => 'gb18030',
|
||||
'viscii' => 'VISCII',
|
||||
'koi8-r' => 'KOI8-R',
|
||||
'koi8_r' => 'KOI8-R',
|
||||
'cskoi8r' => 'KOI8-R',
|
||||
'koi' => 'KOI8-R',
|
||||
'koi8' => 'KOI8-R',
|
||||
'koi8-u' => 'KOI8-U',
|
||||
'tis-620' => 'TIS-620',
|
||||
't.61-8bit' => 'T.61-8bit',
|
||||
'hz-gb-2312' => 'HZ-GB-2312',
|
||||
'big5-hkscs' => 'Big5-HKSCS',
|
||||
'gbk' => 'gbk',
|
||||
'cns11643' => 'x-euc-tw',
|
||||
'x-imap4-modified-utf7' => 'x-imap4-modified-utf7',
|
||||
'x-euc-tw' => 'x-euc-tw',
|
||||
'x-mac-ce' => 'x-mac-ce',
|
||||
'x-mac-turkish' => 'x-mac-turkish',
|
||||
'x-mac-greek' => 'x-mac-greek',
|
||||
'x-mac-icelandic' => 'x-mac-icelandic',
|
||||
'x-mac-croatian' => 'x-mac-croatian',
|
||||
'x-mac-romanian' => 'x-mac-romanian',
|
||||
'x-mac-cyrillic' => 'x-mac-cyrillic',
|
||||
'x-mac-ukrainian' => 'x-mac-cyrillic',
|
||||
'x-mac-hebrew' => 'x-mac-hebrew',
|
||||
'x-mac-arabic' => 'x-mac-arabic',
|
||||
'x-mac-farsi' => 'x-mac-farsi',
|
||||
'x-mac-devanagari' => 'x-mac-devanagari',
|
||||
'x-mac-gujarati' => 'x-mac-gujarati',
|
||||
'x-mac-gurmukhi' => 'x-mac-gurmukhi',
|
||||
'armscii-8' => 'armscii-8',
|
||||
'x-viet-tcvn5712' => 'x-viet-tcvn5712',
|
||||
'x-viet-vps' => 'x-viet-vps',
|
||||
'iso-10646-ucs-2' => 'UTF-16BE',
|
||||
'x-iso-10646-ucs-2-be' => 'UTF-16BE',
|
||||
'x-iso-10646-ucs-2-le' => 'UTF-16LE',
|
||||
'x-user-defined' => 'x-user-defined',
|
||||
'x-johab' => 'x-johab',
|
||||
'latin1' => 'ISO-8859-1',
|
||||
'iso_8859-1' => 'ISO-8859-1',
|
||||
'iso8859-1' => 'ISO-8859-1',
|
||||
'iso8859-2' => 'ISO-8859-2',
|
||||
'iso8859-3' => 'ISO-8859-3',
|
||||
'iso8859-4' => 'ISO-8859-4',
|
||||
'iso8859-5' => 'ISO-8859-5',
|
||||
'iso8859-6' => 'ISO-8859-6',
|
||||
'iso8859-7' => 'ISO-8859-7',
|
||||
'iso8859-8' => 'ISO-8859-8',
|
||||
'iso8859-9' => 'ISO-8859-9',
|
||||
'iso8859-10' => 'ISO-8859-10',
|
||||
'iso8859-11' => 'ISO-8859-11',
|
||||
'iso8859-13' => 'ISO-8859-13',
|
||||
'iso8859-14' => 'ISO-8859-14',
|
||||
'iso8859-15' => 'ISO-8859-15',
|
||||
'iso_8859-1:1987' => 'ISO-8859-1',
|
||||
'iso-ir-100' => 'ISO-8859-1',
|
||||
'l1' => 'ISO-8859-1',
|
||||
'ibm819' => 'ISO-8859-1',
|
||||
'cp819' => 'ISO-8859-1',
|
||||
'csisolatin1' => 'ISO-8859-1',
|
||||
'latin2' => 'ISO-8859-2',
|
||||
'iso_8859-2' => 'ISO-8859-2',
|
||||
'iso_8859-2:1987' => 'ISO-8859-2',
|
||||
'iso-ir-101' => 'ISO-8859-2',
|
||||
'l2' => 'ISO-8859-2',
|
||||
'csisolatin2' => 'ISO-8859-2',
|
||||
'latin3' => 'ISO-8859-3',
|
||||
'iso_8859-3' => 'ISO-8859-3',
|
||||
'iso_8859-3:1988' => 'ISO-8859-3',
|
||||
'iso-ir-109' => 'ISO-8859-3',
|
||||
'l3' => 'ISO-8859-3',
|
||||
'csisolatin3' => 'ISO-8859-3',
|
||||
'latin4' => 'ISO-8859-4',
|
||||
'iso_8859-4' => 'ISO-8859-4',
|
||||
'iso_8859-4:1988' => 'ISO-8859-4',
|
||||
'iso-ir-110' => 'ISO-8859-4',
|
||||
'l4' => 'ISO-8859-4',
|
||||
'csisolatin4' => 'ISO-8859-4',
|
||||
'cyrillic' => 'ISO-8859-5',
|
||||
'iso_8859-5' => 'ISO-8859-5',
|
||||
'iso_8859-5:1988' => 'ISO-8859-5',
|
||||
'iso-ir-144' => 'ISO-8859-5',
|
||||
'csisolatincyrillic' => 'ISO-8859-5',
|
||||
'arabic' => 'ISO-8859-6',
|
||||
'iso_8859-6' => 'ISO-8859-6',
|
||||
'iso_8859-6:1987' => 'ISO-8859-6',
|
||||
'iso-ir-127' => 'ISO-8859-6',
|
||||
'ecma-114' => 'ISO-8859-6',
|
||||
'asmo-708' => 'ISO-8859-6',
|
||||
'csisolatinarabic' => 'ISO-8859-6',
|
||||
'csiso88596i' => 'ISO-8859-6-I',
|
||||
'csiso88596e' => 'ISO-8859-6-E',
|
||||
'greek' => 'ISO-8859-7',
|
||||
'greek8' => 'ISO-8859-7',
|
||||
'sun_eu_greek' => 'ISO-8859-7',
|
||||
'iso_8859-7' => 'ISO-8859-7',
|
||||
'iso_8859-7:1987' => 'ISO-8859-7',
|
||||
'iso-ir-126' => 'ISO-8859-7',
|
||||
'elot_928' => 'ISO-8859-7',
|
||||
'ecma-118' => 'ISO-8859-7',
|
||||
'csisolatingreek' => 'ISO-8859-7',
|
||||
'hebrew' => 'ISO-8859-8',
|
||||
'iso_8859-8' => 'ISO-8859-8',
|
||||
'visual' => 'ISO-8859-8',
|
||||
'iso_8859-8:1988' => 'ISO-8859-8',
|
||||
'iso-ir-138' => 'ISO-8859-8',
|
||||
'csisolatinhebrew' => 'ISO-8859-8',
|
||||
'csiso88598i' => 'ISO-8859-8-I',
|
||||
'iso-8859-8i' => 'ISO-8859-8-I',
|
||||
'logical' => 'ISO-8859-8-I',
|
||||
'csiso88598e' => 'ISO-8859-8-E',
|
||||
'latin5' => 'ISO-8859-9',
|
||||
'iso_8859-9' => 'ISO-8859-9',
|
||||
'iso_8859-9:1989' => 'ISO-8859-9',
|
||||
'iso-ir-148' => 'ISO-8859-9',
|
||||
'l5' => 'ISO-8859-9',
|
||||
'csisolatin5' => 'ISO-8859-9',
|
||||
'unicode-1-1-utf-8' => 'UTF-8',
|
||||
'utf8' => 'UTF-8',
|
||||
'x-sjis' => 'Shift_JIS',
|
||||
'shift-jis' => 'Shift_JIS',
|
||||
'ms_kanji' => 'Shift_JIS',
|
||||
'csshiftjis' => 'Shift_JIS',
|
||||
'windows-31j' => 'Shift_JIS',
|
||||
'cp932' => 'Shift_JIS',
|
||||
'sjis' => 'Shift_JIS',
|
||||
'cseucpkdfmtjapanese' => 'EUC-JP',
|
||||
'x-euc-jp' => 'EUC-JP',
|
||||
'csiso2022jp' => 'ISO-2022-JP',
|
||||
'iso-2022-jp-2' => 'ISO-2022-JP',
|
||||
'csiso2022jp2' => 'ISO-2022-JP',
|
||||
'csbig5' => 'Big5',
|
||||
'cn-big5' => 'Big5',
|
||||
'x-x-big5' => 'Big5',
|
||||
'zh_tw-big5' => 'Big5',
|
||||
'cseuckr' => 'EUC-KR',
|
||||
'ks_c_5601-1987' => 'EUC-KR',
|
||||
'iso-ir-149' => 'EUC-KR',
|
||||
'ks_c_5601-1989' => 'EUC-KR',
|
||||
'ksc_5601' => 'EUC-KR',
|
||||
'ksc5601' => 'EUC-KR',
|
||||
'korean' => 'EUC-KR',
|
||||
'csksc56011987' => 'EUC-KR',
|
||||
'5601' => 'EUC-KR',
|
||||
'windows-949' => 'EUC-KR',
|
||||
'gb_2312-80' => 'GB2312',
|
||||
'iso-ir-58' => 'GB2312',
|
||||
'chinese' => 'GB2312',
|
||||
'csiso58gb231280' => 'GB2312',
|
||||
'csgb2312' => 'GB2312',
|
||||
'zh_cn.euc' => 'GB2312',
|
||||
'gb_2312' => 'GB2312',
|
||||
'x-cp1250' => 'windows-1250',
|
||||
'x-cp1251' => 'windows-1251',
|
||||
'x-cp1252' => 'windows-1252',
|
||||
'x-cp1253' => 'windows-1253',
|
||||
'x-cp1254' => 'windows-1254',
|
||||
'x-cp1255' => 'windows-1255',
|
||||
'x-cp1256' => 'windows-1256',
|
||||
'x-cp1257' => 'windows-1257',
|
||||
'x-cp1258' => 'windows-1258',
|
||||
'windows-874' => 'windows-874',
|
||||
'ibm874' => 'windows-874',
|
||||
'dos-874' => 'windows-874',
|
||||
'macintosh' => 'macintosh',
|
||||
'x-mac-roman' => 'macintosh',
|
||||
'mac' => 'macintosh',
|
||||
'csmacintosh' => 'macintosh',
|
||||
'cp866' => 'IBM866',
|
||||
'cp-866' => 'IBM866',
|
||||
'866' => 'IBM866',
|
||||
'csibm866' => 'IBM866',
|
||||
'cp850' => 'IBM850',
|
||||
'850' => 'IBM850',
|
||||
'csibm850' => 'IBM850',
|
||||
'cp852' => 'IBM852',
|
||||
'852' => 'IBM852',
|
||||
'csibm852' => 'IBM852',
|
||||
'cp855' => 'IBM855',
|
||||
'855' => 'IBM855',
|
||||
'csibm855' => 'IBM855',
|
||||
'cp857' => 'IBM857',
|
||||
'857' => 'IBM857',
|
||||
'csibm857' => 'IBM857',
|
||||
'cp862' => 'IBM862',
|
||||
'862' => 'IBM862',
|
||||
'csibm862' => 'IBM862',
|
||||
'cp864' => 'IBM864',
|
||||
'864' => 'IBM864',
|
||||
'csibm864' => 'IBM864',
|
||||
'ibm-864' => 'IBM864',
|
||||
't.61' => 'T.61-8bit',
|
||||
'iso-ir-103' => 'T.61-8bit',
|
||||
'csiso103t618bit' => 'T.61-8bit',
|
||||
'x-unicode-2-0-utf-7' => 'UTF-7',
|
||||
'unicode-2-0-utf-7' => 'UTF-7',
|
||||
'unicode-1-1-utf-7' => 'UTF-7',
|
||||
'csunicode11utf7' => 'UTF-7',
|
||||
'csunicode' => 'UTF-16BE',
|
||||
'csunicode11' => 'UTF-16BE',
|
||||
'iso-10646-ucs-basic' => 'UTF-16BE',
|
||||
'csunicodeascii' => 'UTF-16BE',
|
||||
'iso-10646-unicode-latin1' => 'UTF-16BE',
|
||||
'csunicodelatin1' => 'UTF-16BE',
|
||||
'iso-10646' => 'UTF-16BE',
|
||||
'iso-10646-j-1' => 'UTF-16BE',
|
||||
'latin6' => 'ISO-8859-10',
|
||||
'iso-ir-157' => 'ISO-8859-10',
|
||||
'l6' => 'ISO-8859-10',
|
||||
'csisolatin6' => 'ISO-8859-10',
|
||||
'iso_8859-15' => 'ISO-8859-15',
|
||||
'csisolatin9' => 'ISO-8859-15',
|
||||
'l9' => 'ISO-8859-15',
|
||||
'ecma-cyrillic' => 'ISO-IR-111',
|
||||
'csiso111ecmacyrillic' => 'ISO-IR-111',
|
||||
'csiso2022kr' => 'ISO-2022-KR',
|
||||
'csviscii' => 'VISCII',
|
||||
'zh_tw-euc' => 'x-euc-tw',
|
||||
'iso88591' => 'ISO-8859-1',
|
||||
'iso88592' => 'ISO-8859-2',
|
||||
'iso88593' => 'ISO-8859-3',
|
||||
'iso88594' => 'ISO-8859-4',
|
||||
'iso88595' => 'ISO-8859-5',
|
||||
'iso88596' => 'ISO-8859-6',
|
||||
'iso88597' => 'ISO-8859-7',
|
||||
'iso88598' => 'ISO-8859-8',
|
||||
'iso88599' => 'ISO-8859-9',
|
||||
'iso885910' => 'ISO-8859-10',
|
||||
'iso885911' => 'ISO-8859-11',
|
||||
'iso885912' => 'ISO-8859-12',
|
||||
'iso885913' => 'ISO-8859-13',
|
||||
'iso885914' => 'ISO-8859-14',
|
||||
'iso885915' => 'ISO-8859-15',
|
||||
'tis620' => 'TIS-620',
|
||||
'cp1250' => 'windows-1250',
|
||||
'cp1251' => 'windows-1251',
|
||||
'cp1252' => 'windows-1252',
|
||||
'cp1253' => 'windows-1253',
|
||||
'cp1254' => 'windows-1254',
|
||||
'cp1255' => 'windows-1255',
|
||||
'cp1256' => 'windows-1256',
|
||||
'cp1257' => 'windows-1257',
|
||||
'cp1258' => 'windows-1258',
|
||||
'x-gbk' => 'gbk',
|
||||
'windows-936' => 'gbk',
|
||||
'ansi-1251' => 'windows-1251',
|
||||
];
|
||||
|
||||
/**
|
||||
* Decode the string from charset.
|
||||
*
|
||||
* @param string $encodedString
|
||||
* @param string $charset
|
||||
* @return string
|
||||
*/
|
||||
public function decodeCharset($encodedString, $charset)
|
||||
{
|
||||
if (strtolower($charset) == 'utf-8' || strtolower($charset) == 'us-ascii') {
|
||||
return $encodedString;
|
||||
}
|
||||
|
||||
try {
|
||||
return iconv($this->getCharsetAlias($charset), 'UTF-8//TRANSLIT', $encodedString);
|
||||
} catch (\Exception $e) {
|
||||
return iconv($this->getCharsetAlias($charset), 'UTF-8//IGNORE', $encodedString);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get charset alias.
|
||||
*
|
||||
* @param string $charset.
|
||||
* @return string
|
||||
*/
|
||||
public function getCharsetAlias($charset)
|
||||
{
|
||||
$charset = strtolower($charset);
|
||||
|
||||
if (array_key_exists($charset, $this->charsetAlias)) {
|
||||
return $this->charsetAlias[$charset];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Email\Helpers\Contracts;
|
||||
|
||||
interface CharsetManager
|
||||
{
|
||||
/**
|
||||
* Decode the string from Charset.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function decodeCharset($encodedString, $charset);
|
||||
|
||||
/**
|
||||
* Get charset alias.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCharsetAlias($charset);
|
||||
}
|
||||
1207
packages/Webkul/Email/src/Helpers/HtmlFilter.php
Normal file
1207
packages/Webkul/Email/src/Helpers/HtmlFilter.php
Normal file
File diff suppressed because it is too large
Load Diff
888
packages/Webkul/Email/src/Helpers/Parser.php
Normal file
888
packages/Webkul/Email/src/Helpers/Parser.php
Normal file
@@ -0,0 +1,888 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Email\Helpers;
|
||||
|
||||
use Webkul\Email\Helpers\Contracts\CharsetManager;
|
||||
|
||||
class Parser
|
||||
{
|
||||
/**
|
||||
* Resource.
|
||||
*/
|
||||
public $resource;
|
||||
|
||||
/**
|
||||
* A file pointer to email.
|
||||
*/
|
||||
public $stream;
|
||||
|
||||
/**
|
||||
* Data.
|
||||
*/
|
||||
public $data;
|
||||
|
||||
/**
|
||||
* Container.
|
||||
*/
|
||||
public $container;
|
||||
|
||||
/**
|
||||
* Entity.
|
||||
*/
|
||||
public $entity;
|
||||
|
||||
/**
|
||||
* Files.
|
||||
*/
|
||||
public $files;
|
||||
|
||||
/**
|
||||
* Parts of an email.
|
||||
*/
|
||||
public $parts;
|
||||
|
||||
/**
|
||||
* Charset manager object.
|
||||
*/
|
||||
public $charset;
|
||||
|
||||
/**
|
||||
* Create a new instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(?CharsetManager $charset = null)
|
||||
{
|
||||
if (is_null($charset)) {
|
||||
$charset = new Charset;
|
||||
}
|
||||
|
||||
$this->charset = $charset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Free the held resouces.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
// clear the email file resource
|
||||
if (is_resource($this->stream)) {
|
||||
fclose($this->stream);
|
||||
}
|
||||
|
||||
// clear the mail parse resource
|
||||
if (is_resource($this->resource)) {
|
||||
mailparse_msg_free($this->resource);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the file path we use to get the email text.
|
||||
*
|
||||
* @param string $path
|
||||
* @return object
|
||||
*/
|
||||
public function setPath($path)
|
||||
{
|
||||
$this->resource = mailparse_msg_parse_file($path);
|
||||
|
||||
$this->stream = fopen($path, 'r');
|
||||
|
||||
$this->parse();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the stream resource we use to get the email text.
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function setStream($stream)
|
||||
{
|
||||
// streams have to be cached to file first
|
||||
$meta = @stream_get_meta_data($stream);
|
||||
|
||||
if (
|
||||
! $meta
|
||||
|| ! $meta['mode']
|
||||
|| $meta['mode'][0] != 'r'
|
||||
|| $meta['eof']
|
||||
) {
|
||||
throw new \Exception(
|
||||
'setStream() expects parameter stream to be readable stream resource.'
|
||||
);
|
||||
}
|
||||
|
||||
$tmp_fp = tmpfile();
|
||||
|
||||
if ($tmp_fp) {
|
||||
while (! feof($stream)) {
|
||||
fwrite($tmp_fp, fread($stream, 2028));
|
||||
}
|
||||
|
||||
fseek($tmp_fp, 0);
|
||||
|
||||
$this->stream = &$tmp_fp;
|
||||
} else {
|
||||
throw new \Exception(
|
||||
'Could not create temporary files for attachments. Your tmp directory may be un-writable by PHP.'
|
||||
);
|
||||
}
|
||||
|
||||
fclose($stream);
|
||||
|
||||
$this->resource = mailparse_msg_create();
|
||||
|
||||
// parses the message incrementally (low memory usage but slower)
|
||||
while (! feof($this->stream)) {
|
||||
mailparse_msg_parse($this->resource, fread($this->stream, 2082));
|
||||
}
|
||||
|
||||
$this->parse();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the email text.
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function setText($data)
|
||||
{
|
||||
$this->resource = \mailparse_msg_create();
|
||||
|
||||
// does not parse incrementally, fast memory hog might explode
|
||||
mailparse_msg_parse($this->resource, $data);
|
||||
|
||||
$this->data = $data;
|
||||
|
||||
$this->parse();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the message into parts.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function parse()
|
||||
{
|
||||
$structure = mailparse_msg_get_structure($this->resource);
|
||||
|
||||
$headerType = (stripos($this->data, 'Content-Language:') !== false) ? 'Content-Language:' : 'Content-Type:';
|
||||
|
||||
if (count($structure) == 1) {
|
||||
$tempParts = explode(PHP_EOL, $this->data);
|
||||
|
||||
foreach ($tempParts as $key => $part) {
|
||||
if (stripos($part, $headerType) !== false) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (trim($part) == '') {
|
||||
unset($tempParts[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
$data = implode(PHP_EOL, $tempParts);
|
||||
|
||||
$this->resource = \mailparse_msg_create();
|
||||
|
||||
mailparse_msg_parse($this->resource, $data);
|
||||
|
||||
$this->data = $data;
|
||||
|
||||
$structure = mailparse_msg_get_structure($this->resource);
|
||||
}
|
||||
|
||||
$this->parts = [];
|
||||
|
||||
foreach ($structure as $part_id) {
|
||||
$part = mailparse_msg_get_part($this->resource, $part_id);
|
||||
|
||||
$this->parts[$part_id] = mailparse_msg_get_part_data($part);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse sender name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function parseSenderName()
|
||||
{
|
||||
if (! $fromNameParts = mailparse_rfc822_parse_addresses($this->getHeader('from'))) {
|
||||
$fromNameParts = mailparse_rfc822_parse_addresses($this->getHeader('sender'));
|
||||
}
|
||||
|
||||
return $fromNameParts[0]['display'] == $fromNameParts[0]['address']
|
||||
? current(explode('@', $fromNameParts[0]['display']))
|
||||
: $fromNameParts[0]['display'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse email address.
|
||||
*
|
||||
* @param string $type
|
||||
* @return array
|
||||
*/
|
||||
public function parseEmailAddress($type)
|
||||
{
|
||||
$emails = [];
|
||||
|
||||
$addresses = mailparse_rfc822_parse_addresses($this->getHeader($type));
|
||||
|
||||
if (count($addresses) > 1) {
|
||||
foreach ($addresses as $address) {
|
||||
if (filter_var($address['address'], FILTER_VALIDATE_EMAIL)) {
|
||||
$emails[] = $address['address'];
|
||||
}
|
||||
}
|
||||
} elseif ($addresses) {
|
||||
$emails[] = $addresses[0]['address'];
|
||||
}
|
||||
|
||||
return $emails;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a specific email header, without charset conversion.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRawHeader($name)
|
||||
{
|
||||
if (isset($this->parts[1])) {
|
||||
$headers = $this->getPart('headers', $this->parts[1]);
|
||||
|
||||
return (isset($headers[$name])) ? $headers[$name] : false;
|
||||
} else {
|
||||
throw new \Exception(
|
||||
'setPath() or setText() or setStream() must be called before retrieving email headers.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a specific email header.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getHeader($name)
|
||||
{
|
||||
$rawHeader = $this->getRawHeader($name);
|
||||
|
||||
if ($rawHeader === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->decodeHeader($rawHeader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all mail headers.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getHeaders()
|
||||
{
|
||||
if (isset($this->parts[1])) {
|
||||
$headers = $this->getPart('headers', $this->parts[1]);
|
||||
|
||||
foreach ($headers as $name => &$value) {
|
||||
if (is_array($value)) {
|
||||
foreach ($value as &$v) {
|
||||
$v = $this->decodeSingleHeader($v);
|
||||
}
|
||||
} else {
|
||||
$value = $this->decodeSingleHeader($value);
|
||||
}
|
||||
}
|
||||
|
||||
return $headers;
|
||||
} else {
|
||||
throw new \Exception(
|
||||
'setPath() or setText() or setStream() must be called before retrieving email headers.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get from name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFromName()
|
||||
{
|
||||
$headers = $this->getHeaders();
|
||||
|
||||
return $headers['from'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract multipart MIME text.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function extractMultipartMIMEText($part, $source, $encodingType)
|
||||
{
|
||||
$boundary = trim($part['content-boundary']);
|
||||
$boundary = substr($boundary, strpos($boundary, '----=') + strlen('----='));
|
||||
|
||||
preg_match_all('/------=(3D_.*)\sContent-Type:\s(.*)\s*boundary=3D"----=(3D_.*)"/', $source, $matches);
|
||||
|
||||
$delimeter = array_shift($matches);
|
||||
$content_delimeter = end($delimeter);
|
||||
|
||||
[$relations, $content_types, $boundaries] = $matches;
|
||||
|
||||
$messageToProcess = substr($source, stripos($source, (string) $content_delimeter) + strlen($content_delimeter));
|
||||
|
||||
array_unshift($boundaries, $boundary);
|
||||
|
||||
// Extract the text
|
||||
foreach (array_reverse($boundaries) as $index => $boundary) {
|
||||
$processedEmailSegments = [];
|
||||
$emailSegments = explode('------='.$boundary, $messageToProcess);
|
||||
|
||||
// Remove empty parts
|
||||
foreach ($emailSegments as $emailSegment) {
|
||||
if (! empty(trim($emailSegment))) {
|
||||
$processedEmailSegments[] = trim($emailSegment);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove unrelated parts
|
||||
array_pop($processedEmailSegments);
|
||||
|
||||
for ($i = 0; $i < $index; $i++) {
|
||||
if (count($processedEmailSegments) > 1) {
|
||||
array_shift($processedEmailSegments);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse each parts for text|html content
|
||||
foreach ($processedEmailSegments as $emailSegment) {
|
||||
$emailSegment = quoted_printable_decode(quoted_printable_decode($emailSegment));
|
||||
|
||||
if (stripos($emailSegment, 'content-type: text/plain;') !== false
|
||||
|| stripos($emailSegment, 'content-type: text/html;') !== false
|
||||
) {
|
||||
$search = 'content-transfer-encoding: '.$encodingType;
|
||||
|
||||
return substr($emailSegment, stripos($emailSegment, $search) + strlen($search));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the email message body in the specified format.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getMessageBody($type = 'text')
|
||||
{
|
||||
$textBody = $htmlBody = $body = false;
|
||||
|
||||
$mime_types = [
|
||||
'text'=> 'text/plain',
|
||||
'text'=> 'text/plain; (error)',
|
||||
'html'=> 'text/html',
|
||||
];
|
||||
|
||||
if (in_array($type, array_keys($mime_types))) {
|
||||
foreach ($this->parts as $key => $part) {
|
||||
if (in_array($this->getPart('content-type', $part), $mime_types)
|
||||
&& $this->getPart('content-disposition', $part) != 'attachment'
|
||||
) {
|
||||
$headers = $this->getPart('headers', $part);
|
||||
$encodingType = array_key_exists('content-transfer-encoding', $headers) ? $headers['content-transfer-encoding'] : '';
|
||||
|
||||
if ($this->getPart('content-type', $part) == 'text/plain') {
|
||||
$textBody .= $this->decodeContentTransfer($this->getPartBody($part), $encodingType);
|
||||
$textBody = nl2br($this->charset->decodeCharset($textBody, $this->getPartCharset($part)));
|
||||
} elseif ($this->getPart('content-type', $part) == 'text/plain; (error)') {
|
||||
if (empty($part['headers']) || ! isset($part['headers']['from'])) {
|
||||
$parentKey = explode('.', $key)[0];
|
||||
if (isset($this->parts[$parentKey]) && isset($this->parts[$parentKey]['headers']['from'])) {
|
||||
$part_from_sender = is_array($this->parts[$parentKey]['headers']['from'])
|
||||
? $this->parts[$parentKey]['headers']['from'][0]
|
||||
: $this->parts[$parentKey]['headers']['from'];
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
$part_from_sender = is_array($part['headers']['from'])
|
||||
? $part['headers']['from'][0]
|
||||
: $part['headers']['from'];
|
||||
}
|
||||
$mail_part_addresses = mailparse_rfc822_parse_addresses($part_from_sender);
|
||||
|
||||
if (! empty($mail_part_addresses[0]['address'])
|
||||
&& strrpos($mail_part_addresses[0]['address'], 'pcsms.com') !== false
|
||||
) {
|
||||
$last_header = end($headers);
|
||||
$partMessage = substr($this->data, strrpos($this->data, $last_header) + strlen($last_header), $part['ending-pos-body']);
|
||||
$textBody .= $this->decodeContentTransfer($partMessage, $encodingType);
|
||||
$textBody = nl2br($this->charset->decodeCharset($textBody, $this->getPartCharset($part)));
|
||||
}
|
||||
} elseif ($this->getPart('content-type', $part) == 'multipart/mixed'
|
||||
|| $this->getPart('content-type', $part) == 'multipart/related'
|
||||
) {
|
||||
$emailContent = $this->extractMultipartMIMEText($part, $this->data, $encodingType);
|
||||
|
||||
$textBody .= $this->decodeContentTransfer($emailContent, $encodingType);
|
||||
$textBody = nl2br($this->charset->decodeCharset($textBody, $this->getPartCharset($part)));
|
||||
} else {
|
||||
$htmlBody .= $this->decodeContentTransfer($this->getPartBody($part), $encodingType);
|
||||
$htmlBody = $this->charset->decodeCharset($htmlBody, $this->getPartCharset($part));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$body = $htmlBody ?: $textBody;
|
||||
|
||||
if (is_array($this->files)) {
|
||||
foreach ($this->files as $file) {
|
||||
if ($file['contentId']) {
|
||||
$body = str_replace('cid:'.preg_replace('/[<>]/', '', $file['contentId']), $file['path'], $body);
|
||||
$path = $file['path'];
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new \Exception('Invalid type specified for getMessageBody(). "type" can either be text or html.');
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get text message body.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTextMessageBody()
|
||||
{
|
||||
$textBody = null;
|
||||
|
||||
foreach ($this->parts as $key => $part) {
|
||||
if ($this->getPart('content-disposition', $part) != 'attachment') {
|
||||
$headers = $this->getPart('headers', $part);
|
||||
$encodingType = array_key_exists('content-transfer-encoding', $headers) ? $headers['content-transfer-encoding'] : '';
|
||||
|
||||
if ($this->getPart('content-type', $part) == 'text/plain') {
|
||||
$textBody .= $this->decodeContentTransfer($this->getPartBody($part), $encodingType);
|
||||
$textBody = nl2br($this->charset->decodeCharset($textBody, $this->getPartCharset($part)));
|
||||
} elseif ($this->getPart('content-type', $part) == 'text/plain; (error)') {
|
||||
$part_from_sender = is_array($part['headers']['from']) ? $part['headers']['from'][0] : $part['headers']['from'];
|
||||
$mail_part_addresses = mailparse_rfc822_parse_addresses($part_from_sender);
|
||||
|
||||
if (! empty($mail_part_addresses[0]['address'])
|
||||
&& strrpos($mail_part_addresses[0]['address'], 'pcsms.com') !== false
|
||||
) {
|
||||
$last_header = end($headers);
|
||||
$partMessage = substr($this->data, strrpos($this->data, $last_header) + strlen($last_header), $part['ending-pos-body']);
|
||||
$textBody .= $this->decodeContentTransfer($partMessage, $encodingType);
|
||||
$textBody = nl2br($this->charset->decodeCharset($textBody, $this->getPartCharset($part)));
|
||||
}
|
||||
} elseif ($this->getPart('content-type', $part) == 'multipart/mixed'
|
||||
|| $this->getPart('content-type', $part) == 'multipart/related'
|
||||
) {
|
||||
$emailContent = $this->extractMultipartMIMEText($part, $this->data, $encodingType);
|
||||
|
||||
$textBody .= $this->decodeContentTransfer($emailContent, $encodingType);
|
||||
$textBody = nl2br($this->charset->decodeCharset($textBody, $this->getPartCharset($part)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $textBody;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the attachments contents in order of appearance.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAttachments()
|
||||
{
|
||||
$attachments = [];
|
||||
$dispositions = ['attachment', 'inline'];
|
||||
$non_attachment_types = ['text/plain', 'text/html', 'text/plain; (error)'];
|
||||
$nonameIter = 0;
|
||||
|
||||
foreach ($this->parts as $part) {
|
||||
$disposition = $this->getPart('content-disposition', $part);
|
||||
$filename = 'noname';
|
||||
|
||||
if (isset($part['disposition-filename'])) {
|
||||
$filename = $this->decodeHeader($part['disposition-filename']);
|
||||
} elseif (isset($part['content-name'])) {
|
||||
// if we have no disposition but we have a content-name, it's a valid attachment.
|
||||
// we simulate the presence of an attachment disposition with a disposition filename
|
||||
$filename = $this->decodeHeader($part['content-name']);
|
||||
$disposition = 'attachment';
|
||||
} elseif (! in_array($part['content-type'], $non_attachment_types, true)
|
||||
&& substr($part['content-type'], 0, 10) !== 'multipart/'
|
||||
) {
|
||||
// if we cannot get it by getMessageBody(), we assume it is an attachment
|
||||
$disposition = 'attachment';
|
||||
}
|
||||
|
||||
if (in_array($disposition, $dispositions) === true && isset($filename) === true) {
|
||||
if ($filename == 'noname') {
|
||||
$nonameIter++;
|
||||
$filename = 'noname'.$nonameIter;
|
||||
}
|
||||
|
||||
$headersAttachments = $this->getPart('headers', $part);
|
||||
$contentidAttachments = $this->getPart('content-id', $part);
|
||||
|
||||
if (! $contentidAttachments
|
||||
&& $disposition == 'inline'
|
||||
&& ! strpos($this->getPart('content-type', $part), 'image/')
|
||||
&& ! stripos($filename, 'noname') == false
|
||||
) {
|
||||
// skip
|
||||
} else {
|
||||
$attachments[] = new Attachment(
|
||||
$filename,
|
||||
$this->getPart('content-type', $part),
|
||||
$this->getAttachmentStream($part),
|
||||
$disposition,
|
||||
$contentidAttachments,
|
||||
$headersAttachments
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ! empty($attachments) ? $attachments : $this->extractMultipartMIMEAttachments();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract attachments from multipart MIME.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function extractMultipartMIMEAttachments()
|
||||
{
|
||||
$attachmentCollection = $processedAttachmentCollection = [];
|
||||
|
||||
foreach ($this->parts as $part) {
|
||||
$boundary = isset($part['content-boundary']) ? trim($part['content-boundary']) : '';
|
||||
$boundary = substr($boundary, strpos($boundary, '----=') + strlen('----='));
|
||||
|
||||
preg_match_all('/------=(3D_.*)\sContent-Type:\s(.*)\s*boundary=3D"----=(3D_.*)"/', $this->data, $matches);
|
||||
|
||||
$delimeter = array_shift($matches);
|
||||
$content_delimeter = end($delimeter);
|
||||
|
||||
[$relations, $content_types, $boundaries] = $matches;
|
||||
$messageToProcess = substr($this->data, stripos($this->data, (string) $content_delimeter) + strlen($content_delimeter));
|
||||
|
||||
array_unshift($boundaries, $boundary);
|
||||
|
||||
// Extract the text
|
||||
foreach (array_reverse($boundaries) as $index => $boundary) {
|
||||
$processedEmailSegments = [];
|
||||
$emailSegments = explode('------='.$boundary, $messageToProcess);
|
||||
|
||||
// Remove empty parts
|
||||
foreach ($emailSegments as $emailSegment) {
|
||||
if (! empty(trim($emailSegment))) {
|
||||
$processedEmailSegments[] = trim($emailSegment);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove unrelated parts
|
||||
array_pop($processedEmailSegments);
|
||||
|
||||
for ($i = 0; $i < $index; $i++) {
|
||||
if (count($processedEmailSegments) > 1) {
|
||||
array_shift($processedEmailSegments);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse each parts for text|html content
|
||||
foreach ($processedEmailSegments as $emailSegment) {
|
||||
$emailSegment = quoted_printable_decode(quoted_printable_decode($emailSegment));
|
||||
|
||||
if (stripos($emailSegment, 'content-type: text/plain;') === false
|
||||
&& stripos($emailSegment, 'content-type: text/html;') === false
|
||||
) {
|
||||
$attachmentParts = explode("\n\n", $emailSegment);
|
||||
|
||||
if (! empty($attachmentParts) && count($attachmentParts) == 2) {
|
||||
$attachmentDetails = explode("\n", $attachmentParts[0]);
|
||||
|
||||
$attachmentDetails = array_map(function ($item) {
|
||||
return trim($item);
|
||||
}, $attachmentDetails);
|
||||
|
||||
$attachmentData = trim($attachmentParts[1]);
|
||||
|
||||
$attachmentCollection[] = [
|
||||
'details' => $attachmentDetails,
|
||||
'data' => $attachmentData,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($attachmentCollection as $attachmentDetails) {
|
||||
$stream = '';
|
||||
|
||||
$resourceDetails = [
|
||||
'name' => '',
|
||||
'fileName' => '',
|
||||
'contentType' => '',
|
||||
'encodingType' => 'base64',
|
||||
'contentDisposition' => 'inline',
|
||||
'contentId' => '',
|
||||
];
|
||||
|
||||
foreach ($attachmentDetails['details'] as $attachmentDetail) {
|
||||
if (stripos($attachmentDetail, 'Content-Type: ') === 0) {
|
||||
$resourceDetails['contentType'] = substr($attachmentDetail, strlen('Content-Type: '));
|
||||
} elseif (stripos($attachmentDetail, 'name="') === 0) {
|
||||
$resourceDetails['name'] = substr($attachmentDetail, strlen('name="'), -1);
|
||||
} elseif (stripos($attachmentDetail, 'Content-Transfer-Encoding: ') === 0) {
|
||||
$resourceDetails['encodingType'] = substr($attachmentDetail, strlen('Content-Transfer-Encoding: '));
|
||||
} elseif (stripos($attachmentDetail, 'Content-ID: ') === 0) {
|
||||
$resourceDetails['contentId'] = substr($attachmentDetail, strlen('Content-ID: '));
|
||||
} elseif (stripos($attachmentDetail, 'filename="') === 0) {
|
||||
$resourceDetails['fileName'] = substr($attachmentDetail, strlen('filename="'), -1);
|
||||
} elseif (stripos($attachmentDetail, 'Content-Disposition: ') === 0) {
|
||||
$resourceDetails['contentDisposition'] = substr($attachmentDetail, strlen('Content-Disposition: '), -1);
|
||||
}
|
||||
}
|
||||
|
||||
$resourceDetails['name'] = empty($resourceDetails['name']) ? $resourceDetails['fileName'] : $resourceDetails['name'];
|
||||
$resourceDetails['fileName'] = empty($resourceDetails['fileName']) ? $resourceDetails['name'] : $resourceDetails['fileName'];
|
||||
|
||||
$temp_fp = tmpfile();
|
||||
|
||||
fwrite($temp_fp, base64_decode($attachmentDetails['data']), strlen($attachmentDetails['data']));
|
||||
fseek($temp_fp, 0, SEEK_SET);
|
||||
|
||||
$processedAttachmentCollection[] = new Attachment(
|
||||
$resourceDetails['fileName'],
|
||||
$resourceDetails['contentType'],
|
||||
$temp_fp,
|
||||
$resourceDetails['contentDisposition'],
|
||||
$resourceDetails['contentId'], []
|
||||
);
|
||||
}
|
||||
|
||||
return $processedAttachmentCollection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the attachment body and save temporary file resource.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getAttachmentStream(&$part)
|
||||
{
|
||||
$temp_fp = tmpfile();
|
||||
|
||||
$headers = $this->getPart('headers', $part);
|
||||
$encodingType = array_key_exists('content-transfer-encoding', $headers)
|
||||
? $headers['content-transfer-encoding']
|
||||
: '';
|
||||
|
||||
if ($temp_fp) {
|
||||
if ($this->stream) {
|
||||
$start = $part['starting-pos-body'];
|
||||
$end = $part['ending-pos-body'];
|
||||
|
||||
fseek($this->stream, $start, SEEK_SET);
|
||||
|
||||
$len = $end - $start;
|
||||
$written = 0;
|
||||
|
||||
while ($written < $len) {
|
||||
$write = $len;
|
||||
$part = fread($this->stream, $write);
|
||||
|
||||
fwrite($temp_fp, $this->decodeContentTransfer($part, $encodingType));
|
||||
|
||||
$written += $write;
|
||||
}
|
||||
} elseif ($this->data) {
|
||||
$attachment = $this->decodeContentTransfer($this->getPartBodyFromText($part), $encodingType);
|
||||
|
||||
fwrite($temp_fp, $attachment, strlen($attachment));
|
||||
}
|
||||
|
||||
fseek($temp_fp, 0, SEEK_SET);
|
||||
} else {
|
||||
throw new \Exception(
|
||||
'Could not create temporary files for attachments. Your tmp directory may be unwritable by PHP.'
|
||||
);
|
||||
}
|
||||
|
||||
return $temp_fp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the string from Content-Transfer-Encoding.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function decodeContentTransfer($encodedString, $encodingType)
|
||||
{
|
||||
$encodingType = strtolower($encodingType);
|
||||
|
||||
if ($encodingType == 'base64') {
|
||||
return base64_decode($encodedString);
|
||||
} elseif ($encodingType == 'quoted-printable') {
|
||||
return quoted_printable_decode($encodedString);
|
||||
} else {
|
||||
return $encodedString; // 8bit, 7bit, binary
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode header.
|
||||
*
|
||||
* @param string|array $input
|
||||
* @return string
|
||||
*/
|
||||
private function decodeHeader($input)
|
||||
{
|
||||
// sometimes we have 2 label From so we take only the first
|
||||
if (is_array($input)) {
|
||||
return $this->decodeSingleHeader($input[0]);
|
||||
}
|
||||
|
||||
return $this->decodeSingleHeader($input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a single header (= string).
|
||||
*
|
||||
* @param string
|
||||
* @return string
|
||||
*/
|
||||
private function decodeSingleHeader($input)
|
||||
{
|
||||
// Remove white space between encoded-words
|
||||
$input = preg_replace('/(=\?[^?]+\?(q|b)\?[^?]*\?=)(\s)+=\?/i', '\1=?', $input);
|
||||
|
||||
// For each encoded-word...
|
||||
while (preg_match('/(=\?([^?]+)\?(q|b)\?([^?]*)\?=)/i', $input, $matches)) {
|
||||
$encoded = $matches[1];
|
||||
$charset = $matches[2];
|
||||
$encoding = $matches[3];
|
||||
$text = $matches[4];
|
||||
|
||||
switch (strtolower($encoding)) {
|
||||
case 'b':
|
||||
$text = $this->decodeContentTransfer($text, 'base64');
|
||||
break;
|
||||
|
||||
case 'q':
|
||||
$text = str_replace('_', ' ', $text);
|
||||
|
||||
preg_match_all('/=([a-f0-9]{2})/i', $text, $matches);
|
||||
|
||||
foreach ($matches[1] as $value) {
|
||||
$text = str_replace('='.$value, chr(hexdec($value)), $text);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$text = $this->charset->decodeCharset($text, $this->charset->getCharsetAlias($charset));
|
||||
|
||||
$input = str_replace($encoded, $text, $input);
|
||||
}
|
||||
|
||||
return $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the charset of the MIME part.
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
private function getPartCharset($part)
|
||||
{
|
||||
if (isset($part['charset'])) {
|
||||
return $charset = $this->charset->getCharsetAlias($part['charset']);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a specified MIME part.
|
||||
*
|
||||
* @return string|array
|
||||
*/
|
||||
private function getPart($type, $parts)
|
||||
{
|
||||
return (isset($parts[$type])) ? $parts[$type] : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the Body of a MIME part.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getPartBody(&$part)
|
||||
{
|
||||
$body = '';
|
||||
|
||||
if ($this->stream) {
|
||||
$body = $this->getPartBodyFromFile($part);
|
||||
} elseif ($this->data) {
|
||||
$body = $this->getPartBodyFromText($part);
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the Body from a MIME part from file.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getPartBodyFromFile(&$part)
|
||||
{
|
||||
$start = $part['starting-pos-body'];
|
||||
|
||||
$end = $part['ending-pos-body'];
|
||||
|
||||
fseek($this->stream, $start, SEEK_SET);
|
||||
|
||||
return fread($this->stream, $end - $start);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the Body from a MIME part from text.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getPartBodyFromText(&$part)
|
||||
{
|
||||
$start = $part['starting-pos-body'];
|
||||
|
||||
$end = $part['ending-pos-body'];
|
||||
|
||||
return substr($this->data, $start, $end - $start);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
51
packages/Webkul/Email/src/Mails/Email.php
Normal file
51
packages/Webkul/Email/src/Mails/Email.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Email\Mails;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Symfony\Component\Mime\Email as MimeEmail;
|
||||
|
||||
class Email extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new email instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(public $email) {}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
$this->from($this->email->from)
|
||||
->to($this->email->reply_to)
|
||||
->replyTo($this->email->parent_id ? $this->email->parent->unique_id : $this->email->unique_id)
|
||||
->cc($this->email->cc ?? [])
|
||||
->bcc($this->email->bcc ?? [])
|
||||
->subject($this->email->parent_id ? $this->email->parent->subject : $this->email->subject)
|
||||
->html($this->email->reply);
|
||||
|
||||
$this->withSymfonyMessage(function (MimeEmail $message) {
|
||||
$message->getHeaders()->addIdHeader('Message-ID', $this->email->message_id);
|
||||
|
||||
$message->getHeaders()->addTextHeader('References', $this->email->parent_id
|
||||
? implode(' ', $this->email->parent->reference_ids)
|
||||
: implode(' ', $this->email->reference_ids)
|
||||
);
|
||||
});
|
||||
|
||||
foreach ($this->email->attachments as $attachment) {
|
||||
$this->attachFromStorage($attachment->path);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
62
packages/Webkul/Email/src/Models/Attachment.php
Normal file
62
packages/Webkul/Email/src/Models/Attachment.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Email\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Webkul\Email\Contracts\Attachment as AttachmentContract;
|
||||
|
||||
class Attachment extends Model implements AttachmentContract
|
||||
{
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'email_attachments';
|
||||
|
||||
/**
|
||||
* The attributes that are appended.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $appends = ['url'];
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'path',
|
||||
'size',
|
||||
'content_type',
|
||||
'content_id',
|
||||
'email_id',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the email.
|
||||
*/
|
||||
public function email()
|
||||
{
|
||||
return $this->belongsTo(EmailProxy::modelClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image url for the product image.
|
||||
*/
|
||||
public function url()
|
||||
{
|
||||
return Storage::url($this->path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor for the 'url' attribute.
|
||||
*/
|
||||
public function getUrlAttribute()
|
||||
{
|
||||
return $this->url();
|
||||
}
|
||||
}
|
||||
7
packages/Webkul/Email/src/Models/AttachmentProxy.php
Normal file
7
packages/Webkul/Email/src/Models/AttachmentProxy.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Email\Models;
|
||||
|
||||
use Konekt\Concord\Proxies\ModelProxy;
|
||||
|
||||
class AttachmentProxy extends ModelProxy {}
|
||||
127
packages/Webkul/Email/src/Models/Email.php
Normal file
127
packages/Webkul/Email/src/Models/Email.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Email\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Webkul\Contact\Models\PersonProxy;
|
||||
use Webkul\Email\Contracts\Email as EmailContract;
|
||||
use Webkul\Lead\Models\LeadProxy;
|
||||
use Webkul\Tag\Models\TagProxy;
|
||||
|
||||
class Email extends Model implements EmailContract
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'emails';
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'folders' => 'array',
|
||||
'sender' => 'array',
|
||||
'from' => 'array',
|
||||
'reply_to' => 'array',
|
||||
'cc' => 'array',
|
||||
'bcc' => 'array',
|
||||
'reference_ids' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that are appended.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $appends = [
|
||||
'time_ago',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'subject',
|
||||
'source',
|
||||
'name',
|
||||
'user_type',
|
||||
'is_read',
|
||||
'folders',
|
||||
'from',
|
||||
'sender',
|
||||
'reply_to',
|
||||
'cc',
|
||||
'bcc',
|
||||
'unique_id',
|
||||
'message_id',
|
||||
'reference_ids',
|
||||
'reply',
|
||||
'person_id',
|
||||
'parent_id',
|
||||
'lead_id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the parent email.
|
||||
*/
|
||||
public function parent()
|
||||
{
|
||||
return $this->belongsTo(EmailProxy::modelClass(), 'parent_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the lead.
|
||||
*/
|
||||
public function lead()
|
||||
{
|
||||
return $this->belongsTo(LeadProxy::modelClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the emails.
|
||||
*/
|
||||
public function emails()
|
||||
{
|
||||
return $this->hasMany(EmailProxy::modelClass(), 'parent_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the person that owns the thread.
|
||||
*/
|
||||
public function person()
|
||||
{
|
||||
return $this->belongsTo(PersonProxy::modelClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* The tags that belong to the lead.
|
||||
*/
|
||||
public function tags()
|
||||
{
|
||||
return $this->belongsToMany(TagProxy::modelClass(), 'email_tags');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachments.
|
||||
*/
|
||||
public function attachments()
|
||||
{
|
||||
return $this->hasMany(AttachmentProxy::modelClass(), 'email_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the time ago.
|
||||
*/
|
||||
public function getTimeAgoAttribute(): string
|
||||
{
|
||||
return $this->created_at->diffForHumans();
|
||||
}
|
||||
}
|
||||
7
packages/Webkul/Email/src/Models/EmailProxy.php
Normal file
7
packages/Webkul/Email/src/Models/EmailProxy.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Email\Models;
|
||||
|
||||
use Konekt\Concord\Proxies\ModelProxy;
|
||||
|
||||
class EmailProxy extends ModelProxy {}
|
||||
58
packages/Webkul/Email/src/Providers/EmailServiceProvider.php
Normal file
58
packages/Webkul/Email/src/Providers/EmailServiceProvider.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Email\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Webkul\Email\Console\Commands\ProcessInboundEmails;
|
||||
use Webkul\Email\InboundEmailProcessor\Contracts\InboundEmailProcessor;
|
||||
use Webkul\Email\InboundEmailProcessor\SendgridEmailProcessor;
|
||||
use Webkul\Email\InboundEmailProcessor\WebklexImapEmailProcessor;
|
||||
|
||||
class EmailServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
$this->loadMigrationsFrom(__DIR__.'/../Database/Migrations');
|
||||
|
||||
$this->app->bind(InboundEmailProcessor::class, function ($app) {
|
||||
$driver = config('mail-receiver.default');
|
||||
|
||||
if ($driver === 'sendgrid') {
|
||||
return $app->make(SendgridEmailProcessor::class);
|
||||
}
|
||||
|
||||
if ($driver === 'webklex-imap') {
|
||||
return $app->make(WebklexImapEmailProcessor::class);
|
||||
}
|
||||
|
||||
throw new \Exception("Unsupported mail receiver driver [{$driver}].");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->registerCommands();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the console commands of this package.
|
||||
*/
|
||||
protected function registerCommands(): void
|
||||
{
|
||||
if ($this->app->runningInConsole()) {
|
||||
$this->commands([
|
||||
ProcessInboundEmails::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Email\Providers;
|
||||
|
||||
use Webkul\Core\Providers\BaseModuleServiceProvider;
|
||||
|
||||
class ModuleServiceProvider extends BaseModuleServiceProvider
|
||||
{
|
||||
protected $models = [
|
||||
\Webkul\Email\Models\Email::class,
|
||||
\Webkul\Email\Models\Attachment::class,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Email\Repositories;
|
||||
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Webklex\PHPIMAP\Attachment as ImapAttachment;
|
||||
use Webkul\Core\Eloquent\Repository;
|
||||
use Webkul\Email\Contracts\Attachment;
|
||||
use Webkul\Email\Contracts\Email;
|
||||
|
||||
class AttachmentRepository extends Repository
|
||||
{
|
||||
/**
|
||||
* Specify model class name.
|
||||
*/
|
||||
public function model(): string
|
||||
{
|
||||
return Attachment::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload attachments.
|
||||
*/
|
||||
public function uploadAttachments(Email $email, array $data): void
|
||||
{
|
||||
if (
|
||||
empty($data['attachments'])
|
||||
|| empty($data['source'])
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($data['attachments'] as $attachment) {
|
||||
$attributes = $this->prepareData($email, $attachment);
|
||||
|
||||
if (
|
||||
! empty($attachment->contentId)
|
||||
&& $data['source'] === 'email'
|
||||
) {
|
||||
$attributes['content_id'] = $attachment->contentId;
|
||||
}
|
||||
|
||||
$this->create($attributes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path for the attachment.
|
||||
*/
|
||||
private function prepareData(Email $email, UploadedFile|ImapAttachment $attachment): array
|
||||
{
|
||||
if ($attachment instanceof UploadedFile) {
|
||||
$name = $attachment->getClientOriginalName();
|
||||
|
||||
$content = file_get_contents($attachment->getRealPath());
|
||||
|
||||
$mimeType = $attachment->getMimeType();
|
||||
} else {
|
||||
$name = $attachment->name;
|
||||
|
||||
$content = $attachment->content;
|
||||
|
||||
$mimeType = $attachment->mime;
|
||||
}
|
||||
|
||||
$path = 'emails/'.$email->id.'/'.$name;
|
||||
|
||||
Storage::put($path, $content);
|
||||
|
||||
$attributes = [
|
||||
'path' => $path,
|
||||
'name' => $name,
|
||||
'content_type' => $mimeType,
|
||||
'size' => Storage::size($path),
|
||||
'email_id' => $email->id,
|
||||
];
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
}
|
||||
95
packages/Webkul/Email/src/Repositories/EmailRepository.php
Normal file
95
packages/Webkul/Email/src/Repositories/EmailRepository.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Email\Repositories;
|
||||
|
||||
use Illuminate\Container\Container;
|
||||
use Webkul\Core\Eloquent\Repository;
|
||||
use Webkul\Email\Contracts\Email;
|
||||
|
||||
class EmailRepository extends Repository
|
||||
{
|
||||
public function __construct(
|
||||
protected AttachmentRepository $attachmentRepository,
|
||||
Container $container
|
||||
) {
|
||||
parent::__construct($container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify model class name.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function model()
|
||||
{
|
||||
return Email::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create.
|
||||
*
|
||||
* @return \Webkul\Email\Contracts\Email
|
||||
*/
|
||||
public function create(array $data)
|
||||
{
|
||||
$uniqueId = time().'@'.config('mail.domain');
|
||||
|
||||
$referenceIds = [];
|
||||
|
||||
if (isset($data['parent_id'])) {
|
||||
$parent = parent::findOrFail($data['parent_id']);
|
||||
|
||||
$referenceIds = $parent->reference_ids ?? [];
|
||||
}
|
||||
|
||||
$data = $this->sanitizeEmails(array_merge([
|
||||
'source' => 'web',
|
||||
'from' => config('mail.from.address'),
|
||||
'user_type' => 'admin',
|
||||
'folders' => isset($data['is_draft']) ? ['draft'] : ['outbox'],
|
||||
'unique_id' => $uniqueId,
|
||||
'message_id' => $uniqueId,
|
||||
'reference_ids' => array_merge($referenceIds, [$uniqueId]),
|
||||
], $data));
|
||||
|
||||
$email = parent::create($data);
|
||||
|
||||
$this->attachmentRepository->uploadAttachments($email, $data);
|
||||
|
||||
return $email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update.
|
||||
*
|
||||
* @param int $id
|
||||
* @param string $attribute
|
||||
* @return \Webkul\Email\Contracts\Email
|
||||
*/
|
||||
public function update(array $data, $id, $attribute = 'id')
|
||||
{
|
||||
return parent::update($this->sanitizeEmails($data), $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize emails.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function sanitizeEmails(array $data)
|
||||
{
|
||||
if (isset($data['reply_to'])) {
|
||||
$data['reply_to'] = array_values(array_filter($data['reply_to']));
|
||||
}
|
||||
|
||||
if (isset($data['cc'])) {
|
||||
$data['cc'] = array_values(array_filter($data['cc']));
|
||||
}
|
||||
|
||||
if (isset($data['bcc'])) {
|
||||
$data['bcc'] = array_values(array_filter($data['bcc']));
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user