usable()) { throw new TransportException('Microsoft 365 is selected as the mail provider, but no mailbox is connected.'); } try { $token = $this->broker->freshAccessToken($connection); } catch (MailOAuthException $e) { throw new TransportException('Could not get a Microsoft 365 access token: '.$e->getMessage(), 0, $e); } $response = Http::withToken($token) ->withBody(base64_encode($message->toString()), 'text/plain') ->post('https://graph.microsoft.com/v1.0/me/sendMail'); // Graph acknowledges an accepted submission with 202 and an empty // body; anything else is a refusal worth the admin's attention // (SendAsDenied when the From header isn't the connected mailbox, // ErrorMessageSubmissionBlocked, throttling). if ($response->status() !== 202) { $code = $response->json('error.code'); $detail = $response->json('error.message'); // The code carries the diagnosis ("ErrorQuotaExceeded", // "ErrorSendAsDenied"); Graph's message text is often generic // to the point of useless, so both go into the exception. throw new TransportException( 'Microsoft Graph refused the message (HTTP '.$response->status() .(is_string($code) && $code !== '' ? ', '.$code : '').')' .(is_string($detail) && $detail !== '' ? ': '.$detail : '.'), ); } } public function __toString(): string { return 'microsoft-graph'; } }