motze92 / office365-mail

Office365 transport for Laravel
MIT License
62 stars 21 forks source link

Calculation needs to be done with the Content-Body, not with the sum of attachments #8

Closed matrad closed 4 years ago

matrad commented 4 years ago

First issue: Currently we only calculate the sum of attachments to decide how we upload the attachments. We need to add the Message-Body in this calculation to be sure that the sum of attachments + body does not exceeds the POST message body (Request entity too large)

Second issue:

Attachments smaller than 3MB cannot be added with the createUploadSession , then we receive a ErrorAttachmentSizeShouldNotBeLessThanMinimumSize error from the api - see here. In this case we need to send a POST-Request to the attachments-endpoint.

These fixes should be applied to the 1.x versions, but also to the 2.x versions:

Updated Office365MailTransport.php:

<?php

namespace Office365Mail\Transport;

use Illuminate\Mail\Transport\Transport;
use Swift_Mime_SimpleMessage;
use Microsoft\Graph\Graph;
use Microsoft\Graph\Model\UploadSession;

class Office365MailTransport extends Transport
{

    public function __construct()
    {

    }

    public function send(Swift_Mime_SimpleMessage $message, &$failedRecipients = null)
    {

        $this->beforeSendPerformed($message);

        $graph = new Graph();

        $graph->setAccessToken($this->getAccessToken());

        //special treatment if the message has too large attachments
        $messageBody = $this->getBody($message, true);
        $messageBodySizeMb = json_encode($messageBodySizeMb);
        $messageBodySizeMb = strlen($messageBodySizeMb);
        $messageBodySizeMb = $messageBodySizeMb / 1048576; //byte -> mb
        if ($attachmentCount > 0 && $messageBodySizeMb >= 4) {
            unset($messageBody);
            $graphMessage = $graph->createRequest("POST", "/users/".key($message->getFrom())."/messages")
                ->attachBody($this->getBody($message))
                ->setReturnType(\Microsoft\Graph\Model\Message::class)
                ->execute();

            foreach($message->getChildren() as $attachment) {
                if($attachment instanceof \Swift_Attachment) {
                    $fileName = $attachment->getHeaders()->get('Content-Disposition')->getParameter('filename');
                    $content = $attachment->getBody();
                    $fileSize = strlen($content);
                    $size = $fileSize / 1048576; //byte -> mb
                    $attachmentMessage = [
                        'AttachmentItem' => [
                            'attachmentType' => 'file',
                            'name' => $fileName,
                            'size' => strlen($content)
                        ]
                    ];

                    if($size<=3) { //ErrorAttachmentSizeShouldNotBeLessThanMinimumSize if attachment <= 3mb, then we need to add this
                        $attachmentBody = [
                            "@odata.type" => "#microsoft.graph.fileAttachment",
                            "name" => $attachment->getHeaders()->get('Content-Disposition')->getParameter('filename'),
                            "contentType" => $attachment->getBodyContentType(),
                            "contentBytes" => base64_encode($attachment->getBody())
                        ];

                        $addAttachment = $graph->createRequest("POST", "/users/".key($message->getFrom())."/messages/".$graphMessage->getId()."/attachments")
                            ->attachBody($attachmentBody)
                            ->setReturnType(UploadSession::class)
                            ->execute();
                    } else {
                        //upload the files in chunks of 4mb....
                        $uploadSession = $graph->createRequest("POST", "/users/".key($message->getFrom())."/messages/".$graphMessage->getId()."/attachments/createUploadSession")
                            ->attachBody($attachmentMessage)
                            ->setReturnType(UploadSession::class)
                            ->execute();

                        $fragSize =  1024 * 1024 * 4; //4mb at once...
                        $numFragments = ceil($fileSize / $fragSize);
                        $contentChunked = str_split($content, $fragSize);
                        $bytesRemaining = $fileSize;
                        $i = 0;
                        while ($i < $numFragments) {
                            $chunkSize = $numBytes = $fragSize;
                            $start = $i * $fragSize;
                            $end = $i * $fragSize + $chunkSize - 1;
                            if ($bytesRemaining < $chunkSize) {
                                $chunkSize = $numBytes = $bytesRemaining;
                                $end = $fileSize - 1;
                            }
                            $data = $contentChunked[$i];
                            $content_range = "bytes " . $start . "-" . $end . "/" . $fileSize;
                            $headers = [
                                "Content-Length"=> $numBytes,
                                "Content-Range"=> $content_range
                            ];
                            $client = new \GuzzleHttp\Client();
                            $tmp = $client->put($uploadSession->getUploadUrl(), [
                                'headers'         => $headers,
                                'body'            => $data,
                                'allow_redirects' => false,
                                'timeout'         => 1000
                            ]);
                            $result = $tmp->getBody().'';
                            $result = json_decode($result); //if body == empty, then the file was successfully uploaded
                            $bytesRemaining = $bytesRemaining - $chunkSize;
                            $i++;
                        }
                    }
                }
            }

            //definetly send the message
            $graph->createRequest("POST", "/users/".key($message->getFrom())."/messages/".$graphMessage->getId()."/send")->execute();
        } else {
            $graphMessage = $graph->createRequest("POST", "/users/".key($message->getFrom())."/sendmail")
                ->attachBody($messageBody)
                ->setReturnType(\Microsoft\Graph\Model\Message::class)
                ->execute();
        }

        $this->sendPerformed($message);

        return $this->numberOfRecipients($message);
    }

    /**
     * Get body for the message.
     *
     * @param \Swift_Mime_SimpleMessage $message
     * @param bool $withAttachments
     * @return array
     */

    protected function getBody(Swift_Mime_SimpleMessage $message, $withAttachments = false)
    {
        $messageData = [
            'from' => [
                'emailAddress' => [
                    'address' => key($message->getFrom()),
                    'name' => current($message->getFrom())
                ]
            ],
            'toRecipients' => $this->getTo($message),
            'subject' => $message->getSubject(),
            'body' => [
                'contentType' => $message->getBodyContentType() == "text/html" ? 'html' : 'text',
                'content' => $message->getBody()
            ]
        ];

        if ($withAttachments) {
            $messageData = ['message' => $messageData];
            //add attachments if any
            $attachments = [];
            foreach($message->getChildren() as $attachment) {
                if($attachment instanceof \Swift_Attachment) {
                    $attachments[] = [
                        "@odata.type" => "#microsoft.graph.fileAttachment",
                        "name" => $attachment->getHeaders()->get('Content-Disposition')->getParameter('filename'),
                        "contentType" => $attachment->getBodyContentType(),
                        "contentBytes" => base64_encode($attachment->getBody())
                    ];
                }
            }
            if(count($attachments)>0) {
                $messageData['message']['attachments'] = $attachments;
            }
        }
        return $messageData;
    }

    /**
     * Get the "to" payload field for the API request.
     *
     * @param \Swift_Mime_SimpleMessage $message
     * @return string
     */
    protected function getTo(Swift_Mime_SimpleMessage $message)
    {
        return collect($this->allContacts($message))->map(function ($display, $address) {
            return $display ? [
                'emailAddress' => [
                    'address' => $address,
                    'name' => $display
                ]
            ] : [
                'emailAddress' => [
                    'address' => $address
                ]
            ];
        })->values()->toArray();
    }

    /**
     * Get all of the contacts for the message.
     *
     * @param \Swift_Mime_SimpleMessage $message
     * @return array
     */
    protected function allContacts(Swift_Mime_SimpleMessage $message)
    {
        return array_merge(
            (array) $message->getTo(),
            (array) $message->getCc(),
            (array) $message->getBcc()
        );
    }

    protected function getAccessToken()
    {
        $guzzle = new \GuzzleHttp\Client();
        $url = 'https://login.microsoftonline.com/' . config('office365mail.tenant') . '/oauth2/v2.0/token';
        $token = json_decode($guzzle->post($url, [
            'form_params' => [
                'client_id' => config('office365mail.client_id'),
                'client_secret' => config('office365mail.client_secret'),
                'scope' => 'https://graph.microsoft.com/.default',
                'grant_type' => 'client_credentials',
            ],
        ])->getBody()->getContents());

        return $token->access_token;
    }

}