We are using Microsoft Graph API (version 1.110.0) to connect to our customers' Office365 accounts (> 100k messages each day. It is a professional property management CRM which uses Office365 over an MS application). We are using the ms graph api from our backend which is hosted on an aws EC2.
We perform the following actions:
None of the addressed functions throw an exception in our scenario (only HTTP Code 2xx). In 99,8% of all deliveries there is no problem: email is added to draft folder and it is moved from draft so send items after that. In 0.2% of all cases (and especially if there are more deliveries with attachments at the same time for one customer) the email is not moved to send folder and stays in drafts. The weird fact about this: the message was send and the receiver gets it. Only the "move action" was not performed. So the message do not appear in send folder but in drafts what should be impossible at all.
I found a few people via google who reported this bug as well in boards outside stackoverflow. Is there anybody who has an advice what we can do here? We think, we cannot refactor our code to send a message directly (https://learn.microsoft.com/en-us/graph/api/user-sendmail?view=graph-rest-1.0&tabs=http) as there is no possibility to get the message eml file here (we need the messageId to perform the GET-action).
Thank you very much for any help.
As requested, some code (there are no syntax errors, but copied it together, so there might be are some now). We are using microsoft/microsoft-graph version 1.110.0 by the way.
$message = array(
"subject" => "Test",
"body" => array(
"contentType" => "html",
"content" => "<strong>message</strong>"
),
"from" => array(
"emailAddress" => array("name" => "sender", "address" => "mail@mail.de")
),
"toRecipients" => array(
emailAddress" => array("name" => "receiver", "address" => "mail@mail.de")
),
"replyTo" => [...] (optional),
"bccRecipients" => [...] (optional),
"ccRecipients" => [...] (optional)
);
$saveResult = $microsoftGraph->saveMail($message);
$messageID = $saveResult->getId();
foreach($attachments as $attachment) {
if($attachment["fileSize"] / 1000 <= 3000) {
$attachmentRequest = array(
"@odata.type" => "#microsoft.graph.fileAttachment",
"name" => "file.pdf",
"contentBytes" => base64_encode("raw")
);
$microsoftGraph->addMailAttachment($messageID, $attachmentRequest);
}
else {
$attachmentRequest = array(
"AttachmentItem" => array(
"attachmentType" => "file",
"isInline" => false,
"name" => "file.pdf",
"size" => 1233
)
);
$uploadResult = $microsoftGraph->addMailAttachmentUploadSession($messageID, $attachmentRequest);
$microsoftGraph->uploadFile($uploadResult->getUploadUrl(), "/path/to/file", 1233);
}
}
$messageEML = $microsoftGraph->getMailFile($messageID);
$this->saveMail((string)$messageEML->getRawBody());
$sendResult = $microsoftGraph->sendMail($messageID);
and microsoftGraph class:
use Microsoft\Graph\Graph;
use Microsoft\Graph\Model;
class microsoftGraph {
public function setTokens($accessToken, $accessTokenExpire, $refreshToken) {
$this->accessToken = $accessToken;
$this->accessTokenExpire = $accessTokenExpire;
$this->refreshToken = $refreshToken;
$this->setAccessToken();
}
private function setAccessToken() {
$this->graph = new Graph();
$this->graph->setAccessToken($this->accessToken);
}
public function saveMail($mailBody) {
return $this->graph->createRequest("POST", "/me/messages")
->attachBody($mailBody)
->setReturnType(Model\Message::class)
->execute();
}
public function addMailAttachment($itemID, $attachmentBody) {
return $this->graph->createRequest("POST", "/me/messages/" . $itemID . "/attachments")
->attachBody($attachmentBody)
->execute();
}
public function addMailAttachmentUploadSession($itemID, $attachmentBody) {
return $this->graph->createRequest("POST", "/me/messages/" . $itemID . "/attachments/createUploadSession")
->attachBody($attachmentBody)
->setReturnType(Model\UploadSession::class)
->execute();
}
public function uploadFile($uploadUrl, $file, $fileSize) {
$maxPerUpload = 1000 * 3000;
$splitCount = ceil($fileSize / $maxPerUpload);
$start = 0;
for($i = 0; $i < $splitCount; $i++) {
$start = $i * $maxPerUpload;
$size = ((($fileSize - $start) > $maxPerUpload) ? $maxPerUpload : ($fileSize - $start));
$end = $start + $size - 1;
$headers = [
'Content-Length' => $size,
'Content-Range' => 'bytes ' . $start . '-' . $end . '/' . $fileSize,
'Content-Type' => "application/octet-stream"
];
$guzzle = new \GuzzleHttp\Client();
$guzzle->put(
$uploadUrl,
[
'headers' => $headers,
'body' => file_get_contents($file, false, null, $start, $size)
]
);
}
}
public function getMailFile($itemID) {
return $this->graph->createRequest("GET", "/me/messages/" . $itemID . "/\$value")
->execute();
}
public function sendMail($itemID) {
return $this->graph->createRequest("POST", "/me/messages/" . $itemID . "/send")
->execute();
}
}
I was able to resolve the issue in one of my projects as follows: there must never be two processes using /messages/[ID]/attachments/createUploadSession under the same accessToken. This function is related to the accessToken, even though a messageID is specified, and the request should therefore be tied to the messageID instead of the accessToken.
The assumption that attachments don’t seem to move from Drafts -> Sent Items is incorrect. Attachments are added while the email is in the Drafts folder. After the email is sent, a second process running simultaneously can attach another file to the already sent email (even though this attachment belongs to Email 2), thereby moving it back from the Sent folder to Drafts. This is where the race condition occurs. The upload of this incorrectly placed attachment then fails internally at Microsoft (though it doesn’t throw an exception because the send request was already successfully confirmed with an HTTP 200 response). The email itself is placed back in the Drafts folder (presumably because this action is performed before the upload of each attachment). As a result, the attachment is missing in the second email because it was incorrectly attempted to attach it to the first email.
Solution: Never execute two processes under the same accessToken, and always complete one action before starting the next. Note: Microsoft should definitely mention this in the documentation, as it may give the impression that the requests are tied to the messageID and can be parallelized.