Compare commits
2 Commits
a67b0d24b5
...
7d23e9c777
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d23e9c777 | ||
|
|
2c5e85884e |
@@ -4,7 +4,8 @@ namespace app\api\controller;
|
||||
|
||||
use think\Db;
|
||||
use think\Env;
|
||||
use PHPMailer;
|
||||
use PHPMailer\PHPMailer;
|
||||
use think\Validate;
|
||||
|
||||
class EmailClient extends Base
|
||||
{
|
||||
@@ -63,6 +64,9 @@ class EmailClient extends Base
|
||||
'smtp_from_name' => trim($data['smtp_from_name'] ?? ''),
|
||||
'daily_limit' => intval($data['daily_limit'] ?? 100),
|
||||
'today_sent' => 0,
|
||||
'imap_host' => trim($data['imap_host'] ?? ''),
|
||||
'imap_port' => intval($data['imap_port'] ?? 993),
|
||||
'last_uid' => 0,
|
||||
'state' => 0,
|
||||
];
|
||||
|
||||
@@ -89,7 +93,7 @@ class EmailClient extends Base
|
||||
}
|
||||
|
||||
$update = [];
|
||||
$fields = ['smtp_host', 'smtp_port', 'smtp_user', 'smtp_password', 'smtp_encryption', 'smtp_from_name', 'daily_limit', 'state'];
|
||||
$fields = ['smtp_host', 'smtp_port', 'smtp_user', 'smtp_password', 'smtp_encryption', 'smtp_from_name', 'daily_limit', 'imap_host', 'imap_port', 'state'];
|
||||
foreach ($fields as $field) {
|
||||
if (isset($data[$field])) {
|
||||
$update[$field] = $data[$field];
|
||||
@@ -99,6 +103,9 @@ class EmailClient extends Base
|
||||
if (isset($update['smtp_port'])) {
|
||||
$update['smtp_port'] = intval($update['smtp_port']);
|
||||
}
|
||||
if (isset($update['imap_port'])) {
|
||||
$update['imap_port'] = intval($update['imap_port']);
|
||||
}
|
||||
if (isset($update['daily_limit'])) {
|
||||
$update['daily_limit'] = intval($update['daily_limit']);
|
||||
}
|
||||
@@ -338,6 +345,684 @@ class EmailClient extends Base
|
||||
return jsonSuccess(['reset_count' => $count]);
|
||||
}
|
||||
|
||||
// ==================== Inbox - Sync ====================
|
||||
|
||||
/**
|
||||
* Sync emails from IMAP to local database
|
||||
* Params: j_email_id (single account) or journal_id (all accounts)
|
||||
*/
|
||||
public function syncInbox()
|
||||
{
|
||||
$accountId = intval($this->request->param('j_email_id', 0));
|
||||
$journalId = intval($this->request->param('journal_id', 0));
|
||||
|
||||
if (!$accountId && !$journalId) {
|
||||
return jsonError('j_email_id or journal_id is required');
|
||||
}
|
||||
|
||||
set_time_limit(300);
|
||||
|
||||
if ($accountId) {
|
||||
$accounts = Db::name('journal_email')->where('j_email_id', $accountId)->where('state', 0)->select();
|
||||
} else {
|
||||
$accounts = Db::name('journal_email')->where('journal_id', $journalId)->where('state', 0)->select();
|
||||
}
|
||||
|
||||
$report = [];
|
||||
foreach ($accounts as $account) {
|
||||
$report[] = $this->doSyncAccount($account);
|
||||
}
|
||||
|
||||
return jsonSuccess(['report' => $report]);
|
||||
}
|
||||
|
||||
private function doSyncAccount($account)
|
||||
{
|
||||
if (empty($account['imap_host'])) {
|
||||
return ['j_email_id' => $account['j_email_id'], 'error' => 'IMAP not configured'];
|
||||
}
|
||||
|
||||
$imap = $this->connectImap($account);
|
||||
if (!$imap) {
|
||||
return ['j_email_id' => $account['j_email_id'], 'error' => 'IMAP connection failed'];
|
||||
}
|
||||
|
||||
$lastUid = intval($account['last_uid']);
|
||||
$synced = 0;
|
||||
$bounced = 0;
|
||||
$maxUid = $lastUid;
|
||||
|
||||
try {
|
||||
if ($lastUid > 0) {
|
||||
$searchRange = ($lastUid + 1) . ':*';
|
||||
$emails = imap_fetch_overview($imap, $searchRange, FT_UID);
|
||||
} else {
|
||||
$emails = imap_fetch_overview($imap, '1:*', FT_UID);
|
||||
}
|
||||
|
||||
if (!$emails) {
|
||||
$emails = [];
|
||||
}
|
||||
|
||||
foreach ($emails as $overview) {
|
||||
$uid = intval($overview->uid);
|
||||
if ($uid <= $lastUid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$messageId = isset($overview->message_id) ? trim($overview->message_id) : '';
|
||||
if ($messageId && Db::name('email_inbox')->where('message_id', $messageId)->find()) {
|
||||
if ($uid > $maxUid) $maxUid = $uid;
|
||||
continue;
|
||||
}
|
||||
|
||||
$header = imap_fetchheader($imap, $uid, FT_UID);
|
||||
$struct = imap_fetchstructure($imap, $uid, FT_UID);
|
||||
$body = $this->getEmailBody($imap, $uid, $struct);
|
||||
|
||||
$fromParts = isset($overview->from) ? $this->parseAddress($overview->from) : ['email' => '', 'name' => ''];
|
||||
$subject = isset($overview->subject) ? $this->decodeMimeStr($overview->subject) : '';
|
||||
$toEmail = isset($overview->to) ? $this->parseAddress($overview->to)['email'] : $account['smtp_user'];
|
||||
$emailDate = isset($overview->date) ? strtotime($overview->date) : time();
|
||||
if (!$emailDate) $emailDate = time();
|
||||
|
||||
$hasAttachment = $this->checkAttachment($struct);
|
||||
$isBounce = $this->detectBounce($fromParts['email'], $subject, $header);
|
||||
$bounceEmail = '';
|
||||
|
||||
if ($isBounce) {
|
||||
$bounceEmail = $this->extractBounceRecipient($body['text'], $header);
|
||||
if ($bounceEmail) {
|
||||
Db::name('expert')->where('email', strtolower($bounceEmail))->update(['state' => 4]);
|
||||
$bounced++;
|
||||
}
|
||||
}
|
||||
|
||||
$insert = [
|
||||
'j_email_id' => $account['j_email_id'],
|
||||
'journal_id' => $account['journal_id'],
|
||||
'message_id' => mb_substr($messageId, 0, 255),
|
||||
'uid' => $uid,
|
||||
'from_email' => mb_substr($fromParts['email'], 0, 255),
|
||||
'from_name' => mb_substr($fromParts['name'], 0, 255),
|
||||
'to_email' => mb_substr($toEmail, 0, 255),
|
||||
'subject' => mb_substr($subject, 0, 512),
|
||||
'content_html' => $body['html'],
|
||||
'content_text' => $body['text'],
|
||||
'email_date' => $emailDate,
|
||||
'has_attachment' => $hasAttachment ? 1 : 0,
|
||||
'is_read' => 0,
|
||||
'is_starred' => 0,
|
||||
'is_bounce' => $isBounce ? 1 : 0,
|
||||
'bounce_email' => mb_substr($bounceEmail, 0, 128),
|
||||
'folder' => 'INBOX',
|
||||
'ctime' => time(),
|
||||
'state' => 0,
|
||||
];
|
||||
|
||||
try {
|
||||
Db::name('email_inbox')->insert($insert);
|
||||
$synced++;
|
||||
} catch (\Exception $e) {
|
||||
// skip duplicate
|
||||
}
|
||||
|
||||
if ($uid > $maxUid) {
|
||||
$maxUid = $uid;
|
||||
}
|
||||
}
|
||||
|
||||
if ($maxUid > $lastUid) {
|
||||
Db::name('journal_email')->where('j_email_id', $account['j_email_id'])->update(['last_uid' => $maxUid]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
imap_close($imap);
|
||||
return ['j_email_id' => $account['j_email_id'], 'error' => $e->getMessage()];
|
||||
}
|
||||
|
||||
imap_close($imap);
|
||||
|
||||
return [
|
||||
'j_email_id' => $account['j_email_id'],
|
||||
'smtp_user' => $account['smtp_user'],
|
||||
'synced' => $synced,
|
||||
'bounced' => $bounced,
|
||||
'last_uid' => $maxUid,
|
||||
];
|
||||
}
|
||||
|
||||
// ==================== Inbox - Read ====================
|
||||
|
||||
/**
|
||||
* Get inbox list with pagination and filtering
|
||||
*/
|
||||
public function getInboxList()
|
||||
{
|
||||
$journalId = intval($this->request->param('journal_id', 0));
|
||||
$accountId = intval($this->request->param('j_email_id', 0));
|
||||
$isRead = $this->request->param('is_read', '-1');
|
||||
$isBounce = $this->request->param('is_bounce', '-1');
|
||||
$keyword = trim($this->request->param('keyword', ''));
|
||||
$folder = trim($this->request->param('folder', 'INBOX'));
|
||||
$page = max(1, intval($this->request->param('page', 1)));
|
||||
$perPage = max(1, min(intval($this->request->param('per_page', 20)), 100));
|
||||
|
||||
$where = ['state' => 0, 'folder' => $folder];
|
||||
|
||||
if ($accountId) {
|
||||
$where['j_email_id'] = $accountId;
|
||||
} elseif ($journalId) {
|
||||
$where['journal_id'] = $journalId;
|
||||
} else {
|
||||
return jsonError('j_email_id or journal_id is required');
|
||||
}
|
||||
|
||||
if ($isRead !== '-1' && $isRead !== '') {
|
||||
$where['is_read'] = intval($isRead);
|
||||
}
|
||||
if ($isBounce !== '-1' && $isBounce !== '') {
|
||||
$where['is_bounce'] = intval($isBounce);
|
||||
}
|
||||
|
||||
$query = Db::name('email_inbox')->where($where);
|
||||
if ($keyword !== '') {
|
||||
$query->where('from_email|from_name|subject', 'like', '%' . $keyword . '%');
|
||||
}
|
||||
|
||||
$total = Db::name('email_inbox')->where($where)->count();
|
||||
$list = $query
|
||||
->field('inbox_id,j_email_id,from_email,from_name,to_email,subject,email_date,has_attachment,is_read,is_starred,is_bounce,bounce_email,ctime')
|
||||
->order('email_date desc')
|
||||
->page($page, $perPage)
|
||||
->select();
|
||||
|
||||
return jsonSuccess([
|
||||
'list' => $list,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'per_page' => $perPage,
|
||||
'total_pages' => $total > 0 ? ceil($total / $perPage) : 0,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getOneEmail(){
|
||||
$data = $this->request->post();
|
||||
$rule = new Validate([
|
||||
"j_email_id"
|
||||
]);
|
||||
if(!$rule->check($data)){
|
||||
return jsonError($rule->getError());
|
||||
}
|
||||
$email_info = DB::name("journal_email")->where("j_email_id",$data['j_email_id'])->find();
|
||||
$re['email'] = $email_info;
|
||||
return jsonSuccess($re);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get single email detail
|
||||
*/
|
||||
public function getEmailDetail()
|
||||
{
|
||||
$inboxId = intval($this->request->param('inbox_id', 0));
|
||||
if (!$inboxId) {
|
||||
return jsonError('inbox_id is required');
|
||||
}
|
||||
|
||||
$email = Db::name('email_inbox')->where('inbox_id', $inboxId)->where('state', 0)->find();
|
||||
if (!$email) {
|
||||
return jsonError('Email not found');
|
||||
}
|
||||
|
||||
if (!$email['is_read']) {
|
||||
Db::name('email_inbox')->where('inbox_id', $inboxId)->update(['is_read' => 1]);
|
||||
$email['is_read'] = 1;
|
||||
}
|
||||
|
||||
return jsonSuccess($email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unread count
|
||||
*/
|
||||
public function getUnreadCount()
|
||||
{
|
||||
$journalId = intval($this->request->param('journal_id', 0));
|
||||
$accountId = intval($this->request->param('j_email_id', 0));
|
||||
|
||||
$where = ['state' => 0, 'is_read' => 0, 'folder' => 'INBOX'];
|
||||
|
||||
if ($accountId) {
|
||||
$where['j_email_id'] = $accountId;
|
||||
} elseif ($journalId) {
|
||||
$where['journal_id'] = $journalId;
|
||||
} else {
|
||||
return jsonError('j_email_id or journal_id is required');
|
||||
}
|
||||
|
||||
$total = Db::name('email_inbox')->where($where)->count();
|
||||
$bounce = Db::name('email_inbox')->where($where)->where('is_bounce', 1)->count();
|
||||
|
||||
return jsonSuccess([
|
||||
'unread' => $total,
|
||||
'unread_bounce' => $bounce,
|
||||
]);
|
||||
}
|
||||
|
||||
// ==================== Inbox - Actions ====================
|
||||
|
||||
/**
|
||||
* Mark emails as read/unread
|
||||
* Params: inbox_ids (comma separated), is_read (0 or 1)
|
||||
*/
|
||||
public function markRead()
|
||||
{
|
||||
$ids = trim($this->request->param('inbox_ids', ''));
|
||||
$isRead = intval($this->request->param('is_read', 1));
|
||||
|
||||
if (empty($ids)) {
|
||||
return jsonError('inbox_ids is required');
|
||||
}
|
||||
|
||||
$idArr = array_map('intval', explode(',', $ids));
|
||||
$count = Db::name('email_inbox')->where('inbox_id', 'in', $idArr)->update(['is_read' => $isRead ? 1 : 0]);
|
||||
|
||||
return jsonSuccess(['updated' => $count]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Star/unstar emails
|
||||
*/
|
||||
public function markStar()
|
||||
{
|
||||
$ids = trim($this->request->param('inbox_ids', ''));
|
||||
$isStarred = intval($this->request->param('is_starred', 1));
|
||||
|
||||
if (empty($ids)) {
|
||||
return jsonError('inbox_ids is required');
|
||||
}
|
||||
|
||||
$idArr = array_map('intval', explode(',', $ids));
|
||||
$count = Db::name('email_inbox')->where('inbox_id', 'in', $idArr)->update(['is_starred' => $isStarred ? 1 : 0]);
|
||||
|
||||
return jsonSuccess(['updated' => $count]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete emails (soft delete)
|
||||
*/
|
||||
public function deleteEmail()
|
||||
{
|
||||
$ids = trim($this->request->param('inbox_ids', ''));
|
||||
if (empty($ids)) {
|
||||
return jsonError('inbox_ids is required');
|
||||
}
|
||||
|
||||
$idArr = array_map('intval', explode(',', $ids));
|
||||
$count = Db::name('email_inbox')->where('inbox_id', 'in', $idArr)->update(['state' => 1]);
|
||||
|
||||
return jsonSuccess(['deleted' => $count]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download attachment from IMAP in real time
|
||||
* Params: inbox_id, part_index (attachment index, starting from 0)
|
||||
*/
|
||||
public function getAttachment()
|
||||
{
|
||||
$inboxId = intval($this->request->param('inbox_id', 0));
|
||||
$partIndex = intval($this->request->param('part_index', 0));
|
||||
|
||||
if (!$inboxId) {
|
||||
return jsonError('inbox_id is required');
|
||||
}
|
||||
|
||||
$email = Db::name('email_inbox')->where('inbox_id', $inboxId)->where('state', 0)->find();
|
||||
if (!$email) {
|
||||
return jsonError('Email not found');
|
||||
}
|
||||
|
||||
$account = Db::name('journal_email')->where('j_email_id', $email['j_email_id'])->find();
|
||||
if (!$account || empty($account['imap_host'])) {
|
||||
return jsonError('IMAP not configured for this account');
|
||||
}
|
||||
|
||||
$imap = $this->connectImap($account);
|
||||
if (!$imap) {
|
||||
return jsonError('IMAP connection failed');
|
||||
}
|
||||
|
||||
$struct = imap_fetchstructure($imap, $email['uid'], FT_UID);
|
||||
$attachments = $this->listAttachments($struct);
|
||||
|
||||
if (!isset($attachments[$partIndex])) {
|
||||
imap_close($imap);
|
||||
return jsonError('Attachment not found at index ' . $partIndex);
|
||||
}
|
||||
|
||||
$att = $attachments[$partIndex];
|
||||
$partNum = $att['part_number'];
|
||||
$filename = $att['filename'];
|
||||
$encoding = $att['encoding'];
|
||||
|
||||
$body = imap_fetchbody($imap, $email['uid'], $partNum, FT_UID);
|
||||
imap_close($imap);
|
||||
|
||||
if ($encoding == 3) {
|
||||
$body = base64_decode($body);
|
||||
} elseif ($encoding == 4) {
|
||||
$body = quoted_printable_decode($body);
|
||||
}
|
||||
|
||||
$dir = ROOT_PATH . 'public' . DS . 'attachments';
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0777, true);
|
||||
}
|
||||
$safeName = time() . '_' . preg_replace('/[^a-zA-Z0-9._\-]/', '_', $filename);
|
||||
$filepath = $dir . DS . $safeName;
|
||||
file_put_contents($filepath, $body);
|
||||
|
||||
return jsonSuccess([
|
||||
'filename' => $filename,
|
||||
'file_url' => '/attachments/' . $safeName,
|
||||
'file_size' => strlen($body),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* List attachments for an email (without downloading)
|
||||
*/
|
||||
public function listEmailAttachments()
|
||||
{
|
||||
$inboxId = intval($this->request->param('inbox_id', 0));
|
||||
if (!$inboxId) {
|
||||
return jsonError('inbox_id is required');
|
||||
}
|
||||
|
||||
$email = Db::name('email_inbox')->where('inbox_id', $inboxId)->where('state', 0)->find();
|
||||
if (!$email) {
|
||||
return jsonError('Email not found');
|
||||
}
|
||||
|
||||
$account = Db::name('journal_email')->where('j_email_id', $email['j_email_id'])->find();
|
||||
if (!$account || empty($account['imap_host'])) {
|
||||
return jsonError('IMAP not configured');
|
||||
}
|
||||
|
||||
$imap = $this->connectImap($account);
|
||||
if (!$imap) {
|
||||
return jsonError('IMAP connection failed');
|
||||
}
|
||||
|
||||
$struct = imap_fetchstructure($imap, $email['uid'], FT_UID);
|
||||
$attachments = $this->listAttachments($struct);
|
||||
imap_close($imap);
|
||||
|
||||
$result = [];
|
||||
foreach ($attachments as $i => $att) {
|
||||
$result[] = [
|
||||
'index' => $i,
|
||||
'filename' => $att['filename'],
|
||||
'size' => $att['size'] ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
return jsonSuccess($result);
|
||||
}
|
||||
|
||||
// ==================== IMAP Helpers ====================
|
||||
|
||||
private function connectImap($account)
|
||||
{
|
||||
$host = $account['imap_host'];
|
||||
$port = intval($account['imap_port'] ?: 993);
|
||||
$user = $account['smtp_user'];
|
||||
$password = $account['smtp_password'];
|
||||
$encryption = ($port == 993) ? 'ssl' : '';
|
||||
|
||||
$flags = '/imap';
|
||||
if ($encryption === 'ssl') {
|
||||
$flags .= '/ssl';
|
||||
}
|
||||
$flags .= '/novalidate-cert';
|
||||
|
||||
$mailbox = '{' . $host . ':' . $port . $flags . '}INBOX';
|
||||
|
||||
$imap = @imap_open($mailbox, $user, $password, 0, 1);
|
||||
if (!$imap) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $imap;
|
||||
}
|
||||
|
||||
private function getEmailBody($imap, $uid, $struct)
|
||||
{
|
||||
$html = '';
|
||||
$text = '';
|
||||
|
||||
if (!isset($struct->parts) || empty($struct->parts)) {
|
||||
$body = imap_fetchbody($imap, $uid, '1', FT_UID);
|
||||
$body = $this->decodeBody($body, $struct->encoding ?? 0);
|
||||
$charset = $this->getCharset($struct);
|
||||
$body = $this->convertToUtf8($body, $charset);
|
||||
|
||||
if (isset($struct->subtype) && strtoupper($struct->subtype) === 'HTML') {
|
||||
$html = $body;
|
||||
} else {
|
||||
$text = $body;
|
||||
}
|
||||
} else {
|
||||
foreach ($struct->parts as $index => $part) {
|
||||
$partNum = ($index + 1);
|
||||
if (isset($part->subtype)) {
|
||||
$subtype = strtoupper($part->subtype);
|
||||
if ($subtype === 'PLAIN' && empty($text)) {
|
||||
$body = imap_fetchbody($imap, $uid, (string) $partNum, FT_UID);
|
||||
$text = $this->decodeBody($body, $part->encoding ?? 0);
|
||||
$text = $this->convertToUtf8($text, $this->getCharset($part));
|
||||
} elseif ($subtype === 'HTML' && empty($html)) {
|
||||
$body = imap_fetchbody($imap, $uid, (string) $partNum, FT_UID);
|
||||
$html = $this->decodeBody($body, $part->encoding ?? 0);
|
||||
$html = $this->convertToUtf8($html, $this->getCharset($part));
|
||||
} elseif ($subtype === 'ALTERNATIVE' || $subtype === 'MIXED' || $subtype === 'RELATED') {
|
||||
if (isset($part->parts)) {
|
||||
foreach ($part->parts as $subIndex => $subPart) {
|
||||
$subPartNum = $partNum . '.' . ($subIndex + 1);
|
||||
$sub = strtoupper($subPart->subtype ?? '');
|
||||
if ($sub === 'PLAIN' && empty($text)) {
|
||||
$body = imap_fetchbody($imap, $uid, $subPartNum, FT_UID);
|
||||
$text = $this->decodeBody($body, $subPart->encoding ?? 0);
|
||||
$text = $this->convertToUtf8($text, $this->getCharset($subPart));
|
||||
} elseif ($sub === 'HTML' && empty($html)) {
|
||||
$body = imap_fetchbody($imap, $uid, $subPartNum, FT_UID);
|
||||
$html = $this->decodeBody($body, $subPart->encoding ?? 0);
|
||||
$html = $this->convertToUtf8($html, $this->getCharset($subPart));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($html) && !empty($text)) {
|
||||
$html = '<pre>' . htmlspecialchars($text) . '</pre>';
|
||||
}
|
||||
|
||||
return ['html' => $html, 'text' => $text];
|
||||
}
|
||||
|
||||
private function decodeBody($body, $encoding)
|
||||
{
|
||||
switch ($encoding) {
|
||||
case 3: return base64_decode($body);
|
||||
case 4: return quoted_printable_decode($body);
|
||||
default: return $body;
|
||||
}
|
||||
}
|
||||
|
||||
private function getCharset($struct)
|
||||
{
|
||||
if (isset($struct->parameters)) {
|
||||
foreach ($struct->parameters as $param) {
|
||||
if (strtolower($param->attribute) === 'charset') {
|
||||
return $param->value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 'UTF-8';
|
||||
}
|
||||
|
||||
private function convertToUtf8($str, $charset)
|
||||
{
|
||||
$charset = strtoupper(trim($charset));
|
||||
if ($charset === 'UTF-8' || $charset === 'UTF8' || empty($charset)) {
|
||||
return $str;
|
||||
}
|
||||
$converted = @iconv($charset, 'UTF-8//IGNORE', $str);
|
||||
return $converted !== false ? $converted : $str;
|
||||
}
|
||||
|
||||
private function decodeMimeStr($str)
|
||||
{
|
||||
$elements = imap_mime_header_decode($str);
|
||||
$decoded = '';
|
||||
foreach ($elements as $el) {
|
||||
$charset = ($el->charset === 'default') ? 'ASCII' : $el->charset;
|
||||
$decoded .= $this->convertToUtf8($el->text, $charset);
|
||||
}
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
private function parseAddress($addressStr)
|
||||
{
|
||||
$decoded = $this->decodeMimeStr($addressStr);
|
||||
$email = '';
|
||||
$name = '';
|
||||
|
||||
if (preg_match('/<?([a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,})>?/', $decoded, $m)) {
|
||||
$email = strtolower($m[1]);
|
||||
}
|
||||
if (preg_match('/^"?([^"<]+)"?\s*</', $decoded, $m)) {
|
||||
$name = trim($m[1]);
|
||||
}
|
||||
|
||||
return ['email' => $email, 'name' => $name];
|
||||
}
|
||||
|
||||
private function checkAttachment($struct)
|
||||
{
|
||||
if (!isset($struct->parts)) {
|
||||
return false;
|
||||
}
|
||||
foreach ($struct->parts as $part) {
|
||||
$disposition = '';
|
||||
if (isset($part->disposition)) {
|
||||
$disposition = strtolower($part->disposition);
|
||||
}
|
||||
if ($disposition === 'attachment') {
|
||||
return true;
|
||||
}
|
||||
if (isset($part->ifdparameters) && $part->ifdparameters) {
|
||||
foreach ($part->dparameters as $dp) {
|
||||
if (strtolower($dp->attribute) === 'filename') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function listAttachments($struct, $prefix = '')
|
||||
{
|
||||
$attachments = [];
|
||||
if (!isset($struct->parts)) {
|
||||
return $attachments;
|
||||
}
|
||||
|
||||
foreach ($struct->parts as $index => $part) {
|
||||
$partNum = $prefix ? ($prefix . '.' . ($index + 1)) : (string) ($index + 1);
|
||||
$filename = '';
|
||||
|
||||
if (isset($part->dparameters)) {
|
||||
foreach ($part->dparameters as $dp) {
|
||||
if (strtolower($dp->attribute) === 'filename') {
|
||||
$filename = $this->decodeMimeStr($dp->value);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (empty($filename) && isset($part->parameters)) {
|
||||
foreach ($part->parameters as $p) {
|
||||
if (strtolower($p->attribute) === 'name') {
|
||||
$filename = $this->decodeMimeStr($p->value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($filename)) {
|
||||
$attachments[] = [
|
||||
'filename' => $filename,
|
||||
'part_number' => $partNum,
|
||||
'encoding' => $part->encoding ?? 0,
|
||||
'size' => $part->bytes ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
if (isset($part->parts)) {
|
||||
$sub = $this->listAttachments($part, $partNum);
|
||||
$attachments = array_merge($attachments, $sub);
|
||||
}
|
||||
}
|
||||
|
||||
return $attachments;
|
||||
}
|
||||
|
||||
// ==================== Bounce Detection ====================
|
||||
|
||||
private function detectBounce($fromEmail, $subject, $header)
|
||||
{
|
||||
$fromEmail = strtolower($fromEmail);
|
||||
if (strpos($fromEmail, 'mailer-daemon') !== false || strpos($fromEmail, 'postmaster') !== false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$subjectLower = strtolower($subject);
|
||||
$bounceKeywords = ['undelivered', 'delivery status', 'delivery failed', 'mail delivery failed', 'returned mail', 'undeliverable', 'failure notice'];
|
||||
foreach ($bounceKeywords as $kw) {
|
||||
if (strpos($subjectLower, $kw) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (stripos($header, 'report-type=delivery-status') !== false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function extractBounceRecipient($bodyText, $header)
|
||||
{
|
||||
if (preg_match('/Final-Recipient:.*?;\s*<?([a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,})>?/i', $header . "\n" . $bodyText, $m)) {
|
||||
return strtolower($m[1]);
|
||||
}
|
||||
|
||||
if (preg_match('/Original-Recipient:.*?;\s*<?([a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,})>?/i', $header . "\n" . $bodyText, $m)) {
|
||||
return strtolower($m[1]);
|
||||
}
|
||||
|
||||
if (preg_match('/<?([a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,})>?\s+was not delivered/i', $bodyText, $m)) {
|
||||
return strtolower($m[1]);
|
||||
}
|
||||
|
||||
if (preg_match('/delivery to.*?<?([a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,})>?.*?failed/i', $bodyText, $m)) {
|
||||
return strtolower($m[1]);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
// ==================== Internal Methods ====================
|
||||
|
||||
/**
|
||||
@@ -394,7 +1079,7 @@ class EmailClient extends Base
|
||||
private function doSendEmail($account, $toEmail, $subject, $htmlContent)
|
||||
{
|
||||
try {
|
||||
$mail = new PHPMailer(true);
|
||||
$mail = new PHPMailer\PHPMailer(true);
|
||||
$mail->isSMTP();
|
||||
$mail->SMTPDebug = 0;
|
||||
$mail->CharSet = 'UTF-8';
|
||||
|
||||
Reference in New Issue
Block a user