Hauptmenü

Werkzeuge

Kategorien

Archiv

Einhell

Mails mit PHP über OAuth2 und Office365 versenden

Mit meiner Klasse phpoauth2mail, kann man E-Mails ohne SMTP über die Microsoft Schnittstelle versenden. Im ersten Schritt wird ein Token über OAuth2 erzeugt, mit dem dann die API angesprochen wird.

Die Klasse unterstützt HTML/Text Mails und Anhänge. Die Authentifizierung kann mittels geheimen Clientschlüssel oder Zertifikat erfolgen.

Voraussetzungen

  • Anwendungs-ID (Client) und Verzeichnis-ID (Mandanten ID) notieren
  • Berechtigungen an der APP setzen (Mail.send ist hier wichtig) und die Administratoreinwilligung muss durchgeführt werden.

1. Verwendung mit geheimen Clientschlüssel

Bei Microsoft muss der geheime Clientschlüssel angelegt werden, dieser ist maximal 2 Jahre gültig.

Wir brauchen dann den „Wert“, dieser wird dann mit setAuthByClientKey an die Klasse übergeben.

include implode(DIRECTORY_SEPARATOR, [dirname(__FILE__), 'phpoauth2mail.class.php']);

$phpoauth2mail = new \DS\phpoauth2mail(
  client_id: '..',
  mandant_id: '..'
);

$phpoauth2mail->setAuthByClientKey('..');

$phpoauth2mail->sendMail(
  user_from: 'service@menuemobil.net',
  arTo: ['empfänger1@daschmi.de', 'empfänger2@daschmi.de'],
  subject: 'TEST',
  body: '<h1>TEST</h1>OK',
  arAttachments: [
    implode(DIRECTORY_SEPARATOR, array: [dirname(__FILE__), 'attachment1.zip']),
    implode(DIRECTORY_SEPARATOR, [dirname(__FILE__), 'attachment2.zip']),
  ],
  content_type: \DS\phpoauth2mail::CONTENT_TYPE_HTML,
  saveToSentItems: false
);

2. Verwendung mit einem privaten Schlüssel und Zertifikat

Zuerst musst du ein Zertifikat und einen privaten Schlüssel für den Zugriff erstellen. Unter UNIX/MAC kannst du das wie folgt:

openssl req -newkey rsa:2048 -nodes -keyout private.pem -x509 -days 3650 -out public.pem

Jetzt hast du zwei Dateien:

  • public.pem
    Das ist das Zertifikat, was ins Azure Portal hochgeladen werden muss (App-Registrierung → Zertifikate)
  • private.pem
    Das ist der geheime Schlüssel, den dein Script zur Authorisierung braucht.

Die Dateien sollten so abgelegt werden, dass sie nicht öffentlich zugänglich sind.

Das Script musst du jetzt wie folgt nutzen:

include implode(DIRECTORY_SEPARATOR, [dirname(__FILE__), 'phpoauth2mail.class.php']);

$phpoauth2mail = new \DS\phpoauth2mail(
  client_id: '..',
  mandant_id: '..'
);

$phpoauth2mail->setAuthByKeyAndCertificateFile(
  implode(DIRECTORY_SEPARATOR, [dirname(__FILE__), '..', 'private.pem']),
  implode(DIRECTORY_SEPARATOR, [dirname(__FILE__), '..', 'public.pem'])
);

$phpoauth2mail->sendMail(
  user_from: 'service@menuemobil.net',
  arTo: ['empfänger1@daschmi.de', 'empfänger2@daschmi.de'],
  subject: 'TEST',
  body: '<h1>TEST</h1>OK',
  arAttachments: [
    implode(DIRECTORY_SEPARATOR, array: [dirname(__FILE__), 'attachment1.zip']),
    implode(DIRECTORY_SEPARATOR, [dirname(__FILE__), 'attachment2.zip']),
  ],
  content_type: \DS\phpoauth2mail::CONTENT_TYPE_HTML,
  saveToSentItems: false
);

Und hier der die Klasse

<?php

  declare(strict_types=1);

  /**
   * @author: Daniel Schmitzer
   * @date: 05.10.25
   * @time: 09:49
   */

  namespace DS;

  class phpoauth2mail {

    public const int AUTH_MODE_CLIENT_KEY = 1;
    public const int AUTH_MODE_CERTIFICATE = 2;

    public const int CONTENT_TYPE_TEXT = 1;
    public const int CONTENT_TYPE_HTML = 2;

    private ?int $auth_mode = null;

    private ?string $client_id = null;
    private ?string $mandant_id = null;
    private ?string $client_secret = null;
    private ?string $private_key = null;
    private ?string $certificate = null;

    /**
     * @param string $client_id Anwendungs-ID
     * @param string $mandant_id (tenant_id) / Verzeichnis-ID
     */
    public function __construct(string $client_id, string $mandant_id) {

      $this->client_id = $client_id;
      $this->mandant_id = $mandant_id;

    }

    /**
     * @param string $client_secret / Geheimer Clientschlüssel
     * @return void
     */
    public function setAuthByClientKey(string $client_secret): void {

      $this->auth_mode = self::AUTH_MODE_CLIENT_KEY;
      $this->client_secret = $client_secret;

    }

    public function setAuthByKeyAndCertificateFile(
      string $path_to_key,
      string $path_to_cert
    ): void {

      if (!file_exists($path_to_key)) throw new \Exception('Private key not found');
      if (!file_exists($path_to_cert)) throw new \Exception('Certificate not found');

      $this->auth_mode = self::AUTH_MODE_CERTIFICATE;
      $this->private_key = file_get_contents($path_to_key);
      $this->certificate = file_get_contents($path_to_cert);

    }

    public function setAuthByKeyAndCertificate(
      string $private_key,
      string $certificate
    ) {

      $this->auth_mode = self::AUTH_MODE_CERTIFICATE;
      $this->private_key = $private_key;
      $this->certificate = $certificate;

    }

    private function getToken(): string {

      if ($this->auth_mode === self::AUTH_MODE_CLIENT_KEY) {

        $url = 'https://login.microsoftonline.com/'.$this->mandant_id.'/oauth2/v2.0/token';

        $data = [
          'client_id' => $this->client_id,
          'client_secret' => $this->client_secret,
          'scope' => 'https://graph.microsoft.com/.default',
          'grant_type' => 'client_credentials'
        ];

        $ch = curl_init($url);

        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

        $response = curl_exec($ch);

        curl_close($ch);

        $response = json_decode($response, true);

        if (!isset($response['access_token'])) throw new \Exception($response['error_description']??$response['error']??'Unknown Error');

        return $response['access_token'];

      }
      else if ($this->auth_mode === self::AUTH_MODE_CERTIFICATE) {

        $url = 'https://login.microsoftonline.com/'.$this->mandant_id.'/oauth2/v2.0/token';

        if (preg_match('/-----BEGIN CERTIFICATE-----(.*?)-----END CERTIFICATE-----/s', $this->certificate, $m)) $der = base64_decode(trim($m[1])); else  throw new \Exception("Invalid PEM format");
        $hash = hash('sha256', $der, true);
        $x5tS256 = rtrim(strtr(base64_encode($hash), '+/', '-_'), '=');

        $header = json_encode([
          "alg" => "RS256",
          "typ" => "JWT",
          "x5t#S256" => $x5tS256
        ]);

        $payload = json_encode([
          "aud" => $url, "iss" => $this->client_id, "sub" => $this->client_id, "jti" => bin2hex(random_bytes(16)), "exp" => time() + 600
        ]);

        $base64url_encode = function($data) { return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); };

        $header_b64 = $base64url_encode($header);
        $payload_b64 = $base64url_encode($payload);
        $unsignedToken = "$header_b64.$payload_b64";

        openssl_sign($unsignedToken, $signature, $this->private_key, OPENSSL_ALGO_SHA256);
        $signature_b64 = $base64url_encode($signature);

        $jwt = "$unsignedToken.$signature_b64";

        $postFields = http_build_query([
          "client_id" => $this->client_id,
          "scope" => "https://graph.microsoft.com/.default",
          "grant_type" => "client_credentials",
          "client_assertion_type" => "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
          "client_assertion" => $jwt
        ]);

        $ch = curl_init($url);

        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/x-www-form-urlencoded"]);

        $response = curl_exec($ch);

        curl_close($ch);

        $response = json_decode($response, true);

        if (!isset($response['access_token'])) throw new \Exception($response['error_description']??$response['error']??'Unknown Error');

        return $response['access_token'];

      }
      else {

        throw new \Exception('No Auth Mode set, please run setAuthByClientKey or setAuthByCertificate');

      }

    }

    /**
     * @param string $user_from Postfach, was zum versenden verwendet wird
     * @param array|string $arTo Array von Empfänger E-Mail Adressen
     * @param string $subject Betreff
     * @param string $body Body
     * @param array $arAttachments Array von Dateien, die als Anhang angehängt werden
     * @param int $content_type ContentType der E-Mail 1 = TEXT, 2 = HTML
     * @param bool $saveToSentItems true, wenn die E-Mail in den gesendet Ordner kopiert werden soll
     * @return bool
     * @throws \Exception
     */
    public function sendMail(
      string $user_from,
      array|string $arTo,
      string $subject,
      string $body,
      array $arAttachments = [],
      int $content_type = self::CONTENT_TYPE_TEXT,
      bool $saveToSentItems = true
    ): bool {

      $auth_token = $this->getToken();

      $mail = [
        'message' => [
          'subject' => $subject,
          'body' => [
            'contentType' => match($content_type) {
              self::CONTENT_TYPE_TEXT => 'Text',
              self::CONTENT_TYPE_HTML => 'HTML',
              default => throw new \Exception('Unknown Content Type')
            },
            'content' => $body
          ],
          'toRecipients' => (function() use ($arTo): array {

            if (is_string($arTo)) return $arTo = [$arTo];

            $arReturn = [];

            foreach ($arTo as $to) $arReturn[] = [ 'emailAddress' => [ 'address' => $to] ];

            return $arReturn;

          })(),
          'attachments' => (function() use ($arAttachments): array {

            $arReturn = [];

            foreach ($arAttachments as $attachment) {

              if (file_exists($attachment)) $arReturn[] = [
                "@odata.type" => "#microsoft.graph.fileAttachment",
                "name" => basename($attachment),
                "contentType" => "text/plain",
                "contentBytes" => base64_encode(file_get_contents($attachment))
              ];

            }

            return $arReturn;

          })(),
        ],
        'saveToSentItems' => $saveToSentItems
      ];

      $ch = curl_init('https://graph.microsoft.com/v1.0/users/'.$user_from.'/sendMail');

      curl_setopt($ch, CURLOPT_HTTPHEADER, [
         "Authorization: Bearer $auth_token",
         "Content-Type: application/json"
      ]);
      curl_setopt($ch, CURLOPT_POST, true);
      curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($mail));
      curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

      $result = curl_exec($ch);

      if ($result === '') return true;
      else {

        var_dump($result);
        throw new \Exception('Error');

      }

    }

  }