Laravel-exchange EWS郵件服務

gyp719發表於2022-10-01

背景:最新開發一個讀取郵件內容,傳送郵件的功能,然而是內網配置 POP、IMAP、SMTP 都不可用,僅支援 Exchange 的通訊。

PHP Exchange Web服務庫(PHP-ews)旨在使用Exchange Web服務簡化與Microsoft Exchange伺服器的通訊。它處理使用SOAP服務所需的NTLM身份驗證,併為形成請求所需的複雜型別提供物件導向的介面。

github:github.com/jamesiarmes/php-ews

安裝

composer require php-ews/php-ews "~1.0" // 需開啟 soap 擴充套件

使用
.env

# Laravel-exchange EWS郵件服務
# 郵件傳送服務地址
EWS_HOST=smtp.office365.com
# 傳送郵件郵箱
EWS_USERNAME=xxxx@outlook.com
# 傳送郵件密碼
EWS_PASSWORD=xxxxx
# 回覆郵件的地址
MAIL_TO_ADDRESS=xxxx@tencent.com
# 回覆郵件者姓名
MAIL_FROM_NAME=xxx

建立EWS類
App\Handlers\EWS.php

<?php

namespace App\Handlers;

use Illuminate\Support\Facades\Storage;
use jamesiarmes\PhpEws\ArrayType\NonEmptyArrayOfBaseFolderIdsType;
use jamesiarmes\PhpEws\ArrayType\NonEmptyArrayOfRequestAttachmentIdsType;
use jamesiarmes\PhpEws\Client;
use App\Exceptions\EWSException;
use jamesiarmes\PhpEws\Enumeration\DefaultShapeNamesType;
use jamesiarmes\PhpEws\Enumeration\IndexBasePointType;
use jamesiarmes\PhpEws\Enumeration\ItemQueryTraversalType;
use jamesiarmes\PhpEws\Request\FindItemType;
use jamesiarmes\PhpEws\Request\GetAttachmentType;
use jamesiarmes\PhpEws\Request\GetItemType;
use jamesiarmes\PhpEws\Type\BodyType;
use jamesiarmes\PhpEws\Type\IndexedPageViewType;
use jamesiarmes\PhpEws\Type\ItemIdType;
use jamesiarmes\PhpEws\Type\ItemResponseShapeType;
use jamesiarmes\PhpEws\Type\MessageType;
use jamesiarmes\PhpEws\Request\SendItemType;
use jamesiarmes\PhpEws\Type\EmailAddressType;
use jamesiarmes\PhpEws\Request\CreateItemType;
use jamesiarmes\PhpEws\Type\RequestAttachmentIdType;
use jamesiarmes\PhpEws\Type\TargetFolderIdType;
use jamesiarmes\PhpEws\Type\FileAttachmentType;
use jamesiarmes\PhpEws\Enumeration\BodyTypeType;
use jamesiarmes\PhpEws\Type\SingleRecipientType;
use jamesiarmes\PhpEws\Request\CreateAttachmentType;
use jamesiarmes\PhpEws\Enumeration\ResponseClassType;
use jamesiarmes\PhpEws\Type\DistinguishedFolderIdType;
use jamesiarmes\PhpEws\ArrayType\ArrayOfRecipientsType;
use jamesiarmes\PhpEws\Enumeration\MessageDispositionType;
use jamesiarmes\PhpEws\ArrayType\NonEmptyArrayOfAllItemsType;
use jamesiarmes\PhpEws\ArrayType\NonEmptyArrayOfAttachmentsType;
use jamesiarmes\PhpEws\ArrayType\NonEmptyArrayOfBaseItemIdsType;
use jamesiarmes\PhpEws\Enumeration\DistinguishedFolderIdNameType;

class EWS
{
    /**
     * [EWS client]
     *
     * @var  [Object]
     */
    public $client;

    /**
     * [郵箱地址]
     *
     * @var  [String]
     */
    public $emails;

    /**
     * [郵件標題]
     *
     * @var  [String]
     */
    public $subject;

    /**
     * [郵件內容]
     *
     * @var  [String]
     */
    public $body;

    /**
     * [附件的絕對路徑]
     *
     * @var  [String]
     */
    public $attachment;

    /**
     * [構造方法]
     *
     * @return  [Object]
     */
    public function __construct()
    {
        $host         = env('EWS_HOST', null);
        $username     = env('EWS_USERNAME', null);
        $password     = env('EWS_PASSWORD', null);
        $version      = Client::VERSION_2016;
        $this->client = new Client($host, $username, $password, $version);
    }

    /**
     * 傳送郵件
     *
     * @param   [Array]  $data   [資料]
     *
     * @return  [Json]           [結果]
     */
    public function send($data)
    {
        if(empty($data['emails'])){
            throw new EWSException('The email is invalid.', 422);
        }

        if(!empty($data['attachments'])){
            foreach ($data['attachments'] as $path) {
                if(!file_exists($path)){
                    throw new EWSException("The file:{$path} is not exists.", 404);
                }
            }
        }
        //初始化
        $this->emails      = $data['emails'] ?? '';
        $this->subject     = $data['subject'] ?? '';
        $this->body        = $data['body'] ?? '';
        $this->attachments = $data['attachments'] ?? '';
        //建立所需要引數
        $message = $this->createMessage();
        $request  = $this->createRequest($message);
        //建立草稿並返回
        $item = $this->createItem($request);

        $send = $this->doSend($item);

        return $send;
    }

    /**
     * [建立草稿並且返回]
     *
     * @param   [Request]  $request  [message]
     *
     * @return  [Array]              [返回建立資訊]
     */
    protected function createItem($request)
    {
        $response  = $this->client->CreateItem($request);
        //分析結果
        $response_messages = $response->ResponseMessages->CreateItemResponseMessage;
        $data = [];
        foreach ($response_messages as $response_message) {
            // Make sure the request succeeded.
            if ($response_message->ResponseClass != ResponseClassType::SUCCESS) {
                throw new EWSException($response_message->MessageText, $response_message->ResponseCode);
            }
            // Iterate over the created messages, printing the id for each.
            foreach ($response_message->Items->Message as $item) {
                $data['code']       = 0;
                $data['item_id']    = $item->ItemId->Id;
                $data['change_key'] = $item->ItemId->ChangeKey;
            }
        }

        return $data;
    }

    /**
     * [執行傳送]
     *
     * @param   [Array]  $item  [草稿]
     *
     * @return  [Array]         [結果]
     */
    protected function doSend($item)
    {
        //判斷是否有附件新增
        if(!empty($this->attachments) && $item['code'] == 0){
            $item = $this->addAttachment($item);
        }

        $message_id = $item['item_id'];
        $change_key = $item['change_key'];

        // Build the request.
        $request = new SendItemType();
        $request->SaveItemToFolder = true;
        $request->ItemIds = new NonEmptyArrayOfBaseItemIdsType();

        // Add the message to the request.
        $item = new ItemIdType();
        $item->Id = $message_id;
        $item->ChangeKey = $change_key;
        $request->ItemIds->ItemId[] = $item;

        // Configure the folder to save the sent message to.
        $send_folder = new TargetFolderIdType();
        $send_folder->DistinguishedFolderId = new DistinguishedFolderIdType();
        $send_folder->DistinguishedFolderId->Id = DistinguishedFolderIdNameType::SENT;
        $request->SavedItemFolderId = $send_folder;

        $response = $this->client->SendItem($request);

        // Iterate over the results, printing any error messages.
        $response_messages = $response->ResponseMessages->SendItemResponseMessage;
        $data = [];
        foreach ($response_messages as $response_message) {
            // Make sure the request succeeded.
            if ($response_message->ResponseClass != ResponseClassType::SUCCESS) {
                throw new EWSException($response_message->MessageText, $response_message->ResponseCode);
            }
            $data['code']    = 0;
            $data['message'] = 'Message sent successfully.';
        }

        return $data;
    }

    /**
     * [新增附件]
     *
     * @param   [Item]  $item  [草稿]
     *
     * @return  [Array]        [附件結果]
     */
    protected function addAttachment($item)
    {


        // Build the request,
        $request = new CreateAttachmentType();
        $request->ParentItemId = new ItemIdType();
        $request->ParentItemId->Id = $item['item_id'];
        $request->Attachments = new NonEmptyArrayOfAttachmentsType();

        // Build the file attachment.
        foreach ($this->attachments as $path) {
            // Open file handlers.
            $file = new \SplFileObject($path);
            $finfo = finfo_open();
            $attachment = new FileAttachmentType();
            $attachment->Content = $file->openFile()->fread($file->getSize());
            $attachment->Name = $file->getBasename();
            $attachment->ContentType = finfo_file($finfo, $path);

            $request->Attachments->FileAttachment[] = $attachment;
        }
        $response = $this->client->CreateAttachment($request);

        $response_messages = $response->ResponseMessages->CreateAttachmentResponseMessage;
        $item = [];
        foreach ($response_messages as $response_message) {
            // Make sure the request succeeded.
            if ($response_message->ResponseClass != ResponseClassType::SUCCESS) {
                throw new EWSException($response_message->MessageText, $response_message->ResponseCode);
            }
            // Iterate over the created attachments, printing the id of each.
            foreach ($response_message->Attachments->FileAttachment as $attachment) {
                $item['code']          = 0;
                $item['item_id']       = $attachment->AttachmentId->RootItemId;
                $item['change_key']    = $attachment->AttachmentId->RootItemChangeKey;
                $item['attachment_id'] = $attachment->AttachmentId->Id;
            }
        }

        return $item;
    }

    /**
     * 建立草稿箱
     *
     * @param   [Message]  $message  [要傳送的文字資訊]
     *
     * @return  [request]            []
     */
    protected function createRequest($message)
    {
        // Build the request,
        $request = new CreateItemType();
        $request->Items = new NonEmptyArrayOfAllItemsType();
        // Add the message to the request.
        $request->Items->Message[] = $message;
        // Save the message, but do not send it.
        $request->MessageDisposition = MessageDispositionType::SAVE_ONLY;

        return $request;
    }

    /**
     * 建立 發件人 標題 內容
     *
     * @param   [Client]            $client   [EWS客戶端]
     * @param   [EmailAddress]      $email    [郵箱地址]
     * @param   [Subject]           $subject  [標題]
     * @param   [Body]              $body     [內容]
     *
     * @return  [Message]             [message]
     */
    protected function createMessage()
    {
        // Create the message.
        $message = new MessageType();
        $message->Subject = $this->subject;
        $message->ToRecipients = new ArrayOfRecipientsType();

        // Set the sender.
        $message->From = new SingleRecipientType();
        $message->From->Mailbox = new EmailAddressType();
        $message->From->Mailbox->EmailAddress = env('EWS_USERNAME', null);

        foreach ($this->emails as $key => $email) {
            // Set the recipient.
            $to        = $email['to'] ?? '';
            $name      = $email['name'] ?? $to;
            $recipient = new EmailAddressType();
            $recipient->Name = $name;
            $recipient->EmailAddress = $to;
            $message->ToRecipients->Mailbox[] = $recipient;
        }

        // Set the message body.
        $message->Body = new BodyType();
        $message->Body->BodyType = BodyTypeType::HTML;
        $message->Body->_ = $this->body;

        return $message;
    }


    /**
     * 獲取郵件ID
     * @param  int  $page_size
     * @return array
     * @throws EWSException
     */
    public function getEmailIds(int $page_size = 10): array
    {
        // Build the request.
        $request                  = new FindItemType();
        $request->ParentFolderIds = new NonEmptyArrayOfBaseFolderIdsType();
        $request->Traversal       = ItemQueryTraversalType::SHALLOW;

        // Return all message properties.
        $request->ItemShape            = new ItemResponseShapeType();
        $request->ItemShape->BaseShape = DefaultShapeNamesType::ALL_PROPERTIES;

        // Search in the user's inbox.
        $folder_id                                         = new DistinguishedFolderIdType();
        $folder_id->Id                                     = DistinguishedFolderIdNameType::INBOX;
        $request->ParentFolderIds->DistinguishedFolderId[] = $folder_id;

        // Limits the number of items retrieved
        $request->IndexedPageItemView                     = new IndexedPageViewType();
        $request->IndexedPageItemView->BasePoint          = IndexBasePointType::BEGINNING;
        $request->IndexedPageItemView->Offset             = 0;
        $request->IndexedPageItemView->MaxEntriesReturned = $page_size;

        $response = $this->client->FindItem($request);

        $email_ids = [];
        // Iterate over the results, printing any error messages or message subjects.
        $response_messages = $response->ResponseMessages->FindItemResponseMessage;
        foreach ($response_messages as $response_message) {
            // Make sure the request succeeded.
            if ($response_message->ResponseClass != ResponseClassType::SUCCESS) {
                throw new EWSException("Failed to search for messages with :".$response_message->MessageText, $response_message->ResponseCode);
            }
            foreach ($response_message->RootFolder->Items->Message as $message) {
                $email_ids[] = $message->ItemId->Id;
            }
        }

        return $email_ids;
    }

    /**
     * 根據郵件ID 獲取文字內容和下載附件
     * @param  array  $email_ids
     * @return array
     * @throws EWSException
     */
    public function getBodyContent(array $email_ids): array
    {
        // Build the request.
        $request = new GetItemType();

        $request->ItemShape            = new ItemResponseShapeType();
        $request->ItemShape->BaseShape = DefaultShapeNamesType::ALL_PROPERTIES;
        $request->ItemIds              = new NonEmptyArrayOfBaseItemIdsType();

        // Iterate over the message ids, setting each one on the request.
        foreach ($email_ids as $email_id) {
            $item                       = new ItemIdType();
            $item->Id                   = $email_id;
            $request->ItemIds->ItemId[] = $item;
        }

        $response = $this->client->GetItem($request);

        $bodyContent = [];
        // Iterate over the results, printing any error messages or message subjects.
        $response_messages = $response->ResponseMessages->GetItemResponseMessage;
        foreach ($response_messages as  $key => $response_message) {
            // Make sure the request succeeded.
            if ($response_message->ResponseClass != ResponseClassType::SUCCESS) {
                throw new EWSException("Failed to get message with :".$response_message->MessageText, $response_message->ResponseCode);
            }

            $attachments = array();
            // Iterate over the messages, printing the subject for each.
            foreach ($response_message->Items->Message as $item) {
                $body = str_replace(array("\r\n", "\r", "\n"), "", strip_tags($item->Body->_));
                preg_match("/:(\d+)]/", $body, $passwordMatches);

                $bodyContent[$key] = [
                    'message_id' => $item->InternetMessageId,
                    'subject'    => $item->Subject,
                    'email_id'   => $item->ItemId->Id,
                    'body'       => $body,
                    'password'   => last($passwordMatches),
                ];

                // If there are no attachments for the item, move on to the next
                // message.
                if (empty($item->Attachments)) {
                    continue;
                }
                // Iterate over the attachments for the message.
                foreach ($item->Attachments->FileAttachment as $attachment) {
                    $attachments[] = $attachment->AttachmentId->Id;
                }

                // Build the request to get the attachments.
                $request = new GetAttachmentType();
                $request->AttachmentIds = new NonEmptyArrayOfRequestAttachmentIdsType();

                // Iterate over the attachments for the message.
                foreach ($attachments as $attachment_id) {
                    $id = new RequestAttachmentIdType();
                    $id->Id = $attachment_id;
                    $request->AttachmentIds->AttachmentId[] = $id;
                }

                $response = $this->client->GetAttachment($request);

                // Iterate over the response messages, printing any error messages or
                // saving the attachments.
                $attachment_response_messages = $response->ResponseMessages->GetAttachmentResponseMessage;
                foreach ($attachment_response_messages as $attachment_response_message) {
                    // Make sure the request succeeded.
                    if ($attachment_response_message->ResponseClass != ResponseClassType::SUCCESS) {
                        throw new EWSException("Failed to get attachment with :".$response_message->MessageText, $response_message->ResponseCode);
                    }

                    // Iterate over the file attachments, saving each one.
                    $attachments = $attachment_response_message->Attachments->FileAttachment;
                    foreach ($attachments as $attachment) {
                        $bodyContent[$key]['attachments'] = [
                            'AttachmentId'   => $attachment->AttachmentId->Id,
                            'AttachmentName' => $attachment->Name,
                        ];
                        Storage::disk('public')->put($attachment->Name, $attachment->Content);
                    }
                }
            }
        }

        return $bodyContent;
    }
}

獲取郵件內容及附件:

        $exchange = new EWS();
        try {
            $emailIds = $exchange->getEmailIds(5);
        } catch (EWSException $e) {
            throw new EWSException("獲取郵件ID失敗 :".$e->getMessage());
        }

        try {
            $bodyContents = $exchange->getBodyContent($emailIds);
        } catch (EWSException $e) {
            throw new EWSException("獲取文字內容和下載附件失敗 :".$e->getMessage());
        }

獲取附件解壓後的內容(有密碼):

public function getZipBodyContent($bodyContents): array
    {
        foreach ($bodyContents as $bodyContent) {
            // 判斷主題是否包含 Tencentflex 或 文字是否有密碼
            if (!Str::contains($bodyContent['subject'], 'Tencentflex') || !$bodyContent['password']) {
                continue;
            }

            $zip = new ZipArchive();

            // 開啟壓縮包
            if (!$zip->open(config('filesystems.disks.public.root').'/'.$bodyContent['attachments']['AttachmentName'])) {
                logger()->error("開啟檔案失敗。檔名: ".$bodyContent['attachments']['AttachmentName']);
                return [];
            }
            // 解壓密碼
            $zip->setPassword($bodyContent['password']);
            // 設定解壓到新的目錄
            if (!$zip->extractTo(config('filesystems.disks.public.root'))) {
                logger()->error("密碼錯誤, 解壓失敗。檔名: ".$bodyContent['attachments']['AttachmentName']);
                return [];
            }

            $rows = [];
            for ($i = 0; $i < $zip->numFiles; $i++) {
                $filename = $zip->getNameIndex($i);
                $rows     += Excel::toArray(new VolunteerServiceInfosImport(), config('filesystems.disks.public.root').'/'.$filename)[0];
                Storage::disk('public')->delete($filename); // 刪除ZIP附件解壓後的檔案
                Storage::disk('public')->delete($bodyContent['attachments']['AttachmentName']); // 刪除ZIP附件
            }
            $zip->close();

            logger()->info("[下載附件成功, No: ".$bodyContent['email_id'].'][檔案內容]['.json_encode($rows, JSON_UNESCAPED_UNICODE)."]");
            return [
                'rows'        => $rows,
                'subject'     => $bodyContent['subject'], // 郵件主題
                'zipFileName' => $bodyContent['attachments']['AttachmentName'], // 附件壓縮包
                'message_id'  => $bodyContent['message_id'], // 郵件ID
            ];
        }

        return [];
    }

傳送郵件:

            // 要傳送給的人的姓名和郵箱地址
            $emails = [
                [
                    'to'   => env("MAIL_TO_ADDRESS"),
                    'name' => env("MAIL_FROM_NAME")
                ],
            ];
            // 附件路徑
            $attachments = [
               storage_path('1.pdf'),
               storage_path('2.pdf')
            ];
            // 傳送
            $exchange   = new EWS();
            $send =  $exchange->send([
                'emails'      => $emails,
                'subject'     => 'Laravel-exchange EWS服務庫',
                'body'        => response(view('emails.pdf', ['name' => '郵箱測試']))->getContent(), //這裡可以使用blade模板自定義你的模板樣式
                'attachments' => $attachments
            ]);

            return response()->json($send);

自定義異常
App\Exceptions\EWSException.php

<?php

namespace App\Exceptions;

use Exception;

class EWSException extends Exception
{

}

自定義異常處理:
App\Exceptions\Handler.php

<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;

class Handler extends ExceptionHandler
{
    /**
     * A list of the exception types that are not reported.
     *
     * @var array
     */
    protected $dontReport = [
        //
    ];

    /**
     * A list of the inputs that are never flashed for validation exceptions.
     *
     * @var array
     */
    protected $dontFlash = [
        'password',
        'password_confirmation',
    ];

    /**
     * Report or log an exception.
     *
     * This is a great spot to send exceptions to Sentry, Bugsnag, etc.
     *
     * @param  \Exception  $exception
     * @return void
     */
    public function report(Exception $exception)
    {
        if ($exception instanceof EWSException) {
            //如果是郵件傳送異常,這裡做一些記錄
        }

        parent::report($exception);
    }

    /**
     * Render an exception into an HTTP response.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Exception  $exception
     * @return \Illuminate\Http\Response
     */
    public function render($request, Exception $exception)
    {
        if($exception instanceof EWSException) {
            $result = [
                "code"   => $exception->getCode(),
                "message" => $exception->getMessage()
            ];

            return response()->json($result, $exception->getCode());
        }

        return parent::render($request, $exception);
    }
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章