当店HPでは、メールサーバにExchange Onlineを使用しています。
以前は、XServerのメールを普通に使っていたのですが
- 送ったメールが迷惑メールに入ってしまう
- iCloudメールに至っては受信すらされない
といった出来事が続き、Exchange Onlineに切り替えたのです。
その際、PHPで送信してみた記事を2018年に公開しました。
SMTP-AUTHでは認証が通らない事態が多発
ところがその後、マイクロソフトのセキュリティポリシーの変化とともに、単純にID/パスワードで認証する方法(SMTP-AUTH)では、認証が通らないことが度々起こりました。
そのたびにメールが止まり、システム全体を止めて、マイクロソフトサポートに相談してきました。
その都度、マイクロソフトサポートの案内で、Microsoft 365 管理センターでどっかのスイッチをオンにしてみたり、Azure AD(現 Microsoft Entra)のどっかを無効にしてみたり、いろいろと延命してきたのですが
今回ついに、その延命も尽きたようなのです。
マイクロソフトサポートから回答がなかなか来なくなった
以前のトラブルでは、マイクロソフトサポートからも比較的すぐに回答がきていました。
初回トラブル時は「SMTP-AUTHの無効化は誤って適用されたもので、今後もサポートされる」と案内されたため、特にコードは変更しなかったのです。
ところが、その後も認証が通らないことがたびたび起こります。当方側の操作も必要になってきました。
- 365管理画面→ユーザー→各ユーザー→メール→メールアプリを管理する→認証済みSMTPがオフになっていればオンに戻す
- AzureAD(現Entra)→プロパティから、「セキュリティの既定値群」をオフにする(※非推奨)
(※現在は、SMTP-AUTHを使用していないため、どちらも上記指定とは逆の設定になっています)
そして今回は、トラブル発生からほぼ一日たっても、マイクロソフトサポートからこれといった解決策が来ないという事態にいたりました。
おそらく、サポートでも予期しない何かの変更があったのだと思うのですが…
そんなわけで、今回は観念して、本腰を入れて解決策を探したところ、「Microsoft Graph」が有望そうだ…ということが見えてきたのです。
こんなことなら、最初の時にサポートからこれを教えてくれていれば…とつい、思ってしまいますが、まあ仕方ありません。
とにかく、なんとかこれを実装して、トラブルにさよならしよう、というのが今回の作業です。
“Microsoft Graph”を使うと、メールが送信できそう
今回は、こちらの記事を見つけ、Microsoft Graphでの実装をやってみよう、と考えました。
実際にメールを送信するアクションも見つかりました。
これなら、引数さえ正しく渡せば、メールが送れそうです。
Microsoft Graphを使った自動メール送信・全体像をつかむ
実際の作業は、こちらのチュートリアルに沿ってテストを行い、それを拡張する方法で進めていきました。
とはいえ、実際にやった作業を順番にここに書いても、なんだかよく分からなくなりそうです。
そこで、全部終わった時点で得られた知識をもとに、まずは、「Microsoft Graphでメールを送信する」という動作の全体イメージをご紹介していきたいと思います。
全体像をつかむ
今回実装した自動メール送信の全体像を、図解にしてみました。こんな構造です。
この図解の各要素をひとつずつ設定していく作業になります。
前提条件
作業の前提として、次の項目が満たされていることが必要です。
- Microsoft 365 for Businessに契約しており、組織のアカウントがある。
- Microsoft 365/ Microsoft Entraの管理者権限を持っている
- Webサーバを契約しており、PHPが使える。SSH接続ができる。composerがインストールされている
- 送信元アドレスのドメインは、Microsoft365に設定されている
- 送信元アドレスは、Exchange Onlineのライセンスが割り当てられ、メールボックスの設定が完了している
(Exchage Onlineを含むプラン、またはExchange Online単体ライセンス)
【STEP1】Microsoft Entraにアプリを作成
どこから始めてもいいんですけれども、まずMicrosoft Entraにアプリを作成していきたいと思います。
チュートリアルのStep1を参考にしていきますが、現時点でスクショがAzure ADのままなので、うちの管理画面のスクショをモザイク入りで載せていこうかと思います。
管理センターにログイン、アプリ登録画面へ
まずはMicrosoft Entra管理センターにログイン。
「アプリの登録」をみつけます。
※スクリーンショットがスマホでは小さくて見づらい場合、指で広げる操作をすると大きくなります。
アプリを新規登録
「アプリの登録」に入ったら、新規登録
通常は、次の画面は既定値のまま、名前だけ決めて登録でOKです。
次の画面の、赤く囲ったあたりに出ているクライアントIDやテナントIDが、Webアプリに実装するときに必要なものです。あとでコピーします。
この画面の下にチュートリアルへのリンクがあります
いまの画面の下にリンクがあり、さまざまな言語に対応したリソースにつながっています。
例えば本記事で使ったチュートリアルは、次のようにたどって見つけたものです。
アプリにMail.Sendの権限を付与する
できたてのアプリで「APIのアクセス許可」を見ると、次のようになっています。
今回は、メールを送信しますので、Mail.Sendの権限を付与していきます。
アクセス許可の追加を選ぶと、2つ選択肢があり
【委任されたアクセス許可】 【アプリケーションの許可】の2種類があります。
これが結構重要なところです。
「委任されたアクセス許可」…アプリに認証要求すると、実際にMicrosoft365ユーザーのMFA(多要素認証)画面にリダイレクトされ、認証操作をすることによってはじめて、アプリがその権限を取得する(メールが送れるようになる)
「アプリケーションの許可」…アプリにシークレットキーを設定すれば、サインイン操作不要で、アプリは権限を行使できる(メールを送信できる)
今回は、ユーザーが関与することなく、自動的にメールを送信するのが目的ですので、「アプリケーションの許可」の方を選択します。この方法だと、SDKを噛ませてはいますが、実際に行う処理は、単純なSMTP-AUTHの場合と大差ない実装が可能になります。
※Wordpressのプラグインを使う方法の場合は、「委任されたアクセス許可」の方を選択します。詳しくは後ほど。
そこで、参考にするチュートリアルも、実は、最初にご紹介したチュートリアルの最後の方のリンクからたどった、「アプリ専用認証を使用」というチュートリアルを参考にしていきます。
「アクセス許可の追加」をクリックすると、次のような画面になります。矢印の箇所に、黄色い警告が出てしまいます。これは、「管理者の同意が必要」が「はい」な権限なのに、まだ同意を付与していないためです。
赤枠の箇所をクリックして、管理者の同意を与えます。
すると次のように、同意付与済みの表示に変わります。
最初から付与されている「User.Read」のアクセス許可は、いらないかなと思ったんですが、削除しようとすると「アプリケーションが正常に機能するために必要です」と警告が出るので、残しました。
クライアントシークレットの新規作成
次に、クライアントシークレットを作成します。SMTP-AUTHでいうと、パスワードに相当するものです。
名前と有効期限をきめるよう指示されます。何に使ったシークレットなのか分かるような名前をつけます。有効期限は、推奨の6か月を使用しました。もっと長くも短くもできます。
有効期限がくる前に、クライアントシークレットを新たに作って差し替える必要があるので、カレンダーなどに入力してしっかりと管理をします。
シークレットが作成されたら、赤枠の箇所を確実にメモします。
「値」がクライアントシークレット本体です。ここを逃すと二度と表示されませんので、確実にメモしてください。
WordPressサイトの場合は、ここまでやったらあとはプラグインで。
もし、自動送信したいウェブサイトがWordPressで作成されている場合は、作業はここまででOKです。
Microsoft Graphでメールを送信できるプラグインがありました。当ブログの自動送信も、このプラグインで行っています。
プラグインの場合、APIのアクセス許可を「アプリケーションの許可」にしていると、動くんですが警告が表示されます。「委任されたアクセス許可」の方にします。
インストール後、設定画面で、ここまで作った各種IDの入力欄があります。正しく入力して、最下部送信テストボタンで成功すれば設定完了です。
【STEP2】サーバにMicrosoft Graph SDKをインストール
次に、使用しているサーバにMicrosoft Graph PHP SDKをインストールします。
SSH接続して、Composerを使ってインストールしていきます。
この項目に必要だった知識まとめ
細かく書くと本記事の趣旨から逸脱するため、詳しい方法が書かれたリンクをまとめます。
SDKのインストール
チュートリアルを参照します。
composer require microsoft/microsoft-graph
うちの場合は、環境変数は他の方法で読み込ませるのでphpdotenvは入れませんでした。入れる場合はチュートリアル通り「vlucas/phpdotenv」を追加。
また、すでにいろいろとcomposer使用中だったので、チュートリアルにある「composer init」はなしで。
Composer 1系で実行すると、非対応ですと言われインストールできません。Composer 2にバージョンアップしてから実行する必要があります。
各種ID・キーは、今回は.htaccessで設定
各種キーは.htaccess に入れ込みました。(.htaccessは外部からアクセス不可を確認のうえです。)
SetEnv GRAPH_CLIENT_ID '[クライアントID]'
SetEnv GRAPH_CLIENT_SECRET '[クライアントシークレット]'
SetEnv GRAPH_TENANT_ID '[テナントID]'
チュートリアルのように.envを使ってもよいが、くれぐれも.envを外部から読みだされないよう細心の注意を払う必要あり。
それぞれの’GRAPH_~’という名前は任意の名前で。同じ名前をコード側で参照できればOK。
クライアントID、テナントIDはここです。(さきほどと同じスクリーンショット)
クライアントシークレットは、さきほど新規作成して保存しておいたもの。「値」に書いてあるものを入れます。
新規作成の一度きりしか表示されませんので、今から見に行っても見れません。控え忘れていたら、いちど削除して作り直しです。
【STEP3】PHPコードを書く
チュートリアルに掲載のコードを、今回の目的と手段にあわせて書き換えたものを用意します。
いらないものが残っていたり、もっと良い書き方があったりするかもしれませんが、ひとまずこういうコードで送信は成功しました。
チュートリアル掲載コードのコメントがそのまま残ってたりしますが、ああそこを使ったのかという参考程度に、比較してごらんください。
GraphHelper.inc
<?php
use Microsoft\Graph\Graph;
use Microsoft\Graph\Http;
use Microsoft\Graph\Model;
use GuzzleHttp\Client;
class GraphHelper {
private static Client $tokenClient;
private static string $clientId = '';
private static string $clientSecret = '';
private static string $tenantId = '';
private static Graph $appClient;
private static string $appToken;
public static function initializeGraphForAppOnlyAuth(): void {
GraphHelper::$tokenClient = new Client();
GraphHelper::$clientId = getenv('GRAPH_CLIENT_ID');
GraphHelper::$clientSecret = getenv('GRAPH_CLIENT_SECRET');
GraphHelper::$tenantId = getenv('GRAPH_TENANT_ID');
GraphHelper::$appClient = new Graph();
}
public static function getAppOnlyToken(): string {
if (isset(GraphHelper::$appToken)) {
return GraphHelper::$appToken;
}
$tokenRequestUrl = 'https://login.microsoftonline.com/'.GraphHelper::$tenantId.'/oauth2/v2.0/token';
// POST to the /token endpoint
$tokenResponse = GraphHelper::$tokenClient->post($tokenRequestUrl, [
'form_params' => [
'client_id' => GraphHelper::$clientId,
'client_secret' => GraphHelper::$clientSecret,
'grant_type' => 'client_credentials',
'scope' => 'https://graph.microsoft.com/.default'
],
// These options are needed to enable getting
// the response body from a 4xx response
'http_errors' => false,
'curl' => [
CURLOPT_FAILONERROR => false
]
]);
$responseBody = json_decode($tokenResponse->getBody()->getContents());
if ($tokenResponse->getStatusCode() == 200) {
// Return the access token
GraphHelper::$appToken = $responseBody->access_token;
return $responseBody->access_token;
} else {
$error = isset($responseBody->error) ? $responseBody->error : $tokenResponse->getStatusCode();
throw new Exception('Token endpoint returned '.$error, 100);
}
}
public static function sendMail(string $subject, string $body, string $recipient,string $mime): void {
$token = GraphHelper::getAppOnlyToken();
GraphHelper::$appClient->setAccessToken($token);
$sendMailBody = array(
'message' => array (
'subject' => $subject,
'body' => array (
'content' => $body,
'contentType' => $mime
),
'toRecipients' => array (
array (
'emailAddress' => array (
'address' => $recipient
)
)
)
)
);
GraphHelper::$appClient->createRequest('POST', '/users/[送信元となるMS365ユーザーアドレス]/sendMail')
->attachBody($sendMailBody)
->execute();
}
}
?>
うちの場合は、メール送信元アドレスは一つだけで固定なので、ハードコーディングしてしまいました。複数アドレスから送信する場合は、ここを引数で渡す必要がありそうです。
「アプリケーションの許可」で認証していますので、ユーザーのメールアドレスさえ指定すれば、テナント内の任意のユーザーとしてメールが送信できてしまいます。
また、チュートリアル通りでは、テキスト形式のメールしか送信できません。引数$mimeを追加し、HTMLメールも送信できるようにしています。
メール送信用の関数モジュール
いまのGraphHelper.incを使って、こんなふうに。
<?php
require_once 'vendor/autoload.php';
require_once __DIR__.'/GraphHelper.inc';
function sendGraphmail_html($to_email, $subject, $body): void {
initializeGraph();
try {
GraphHelper::sendMail($subject, $body, $to_email,'HTML');
} catch (Exception $e) {
die('メール送信エラーです。繰り返し発生する場合は、お手数ですが当店までご連絡ください。'.PHP_EOL.PHP_EOL);
}
}
function sendGraphmail($to_email,$subject, $body): void {
initializeGraph();
try {
GraphHelper::sendMail($subject, $body, $to_email,'text');
} catch (Exception $e) {
die('メール送信エラーです。繰り返し発生する場合は、お手数ですが当店までご連絡ください。'.PHP_EOL.PHP_EOL);
}
}
function initializeGraph(): void {
GraphHelper::initializeGraphForAppOnlyAuth();
}
?>
既存のモジュールが、テキストメールとHTMLメールで呼び出す関数を分けていたため、便宜上このように2つにしました。最後の引数を「text」にすればテキストメール、「HTML」とすればHTMLメールが送信できました。
これで完成
あとは、できた送信モジュールを既存のコードに組み込めば完成。
最初に載せた全体図の全要素がそろいました。
以上で作業完了。メールの送信テストをおこない、SMTP-AUTHを残すために設定していた項目も、はれて推奨設定に戻して終了です。
おわりに
記事を書くことで、細かいところの再点検になりました。
トラブルが発生してから対処すると、大急ぎになるので、細かいところが抜けがちです。
そして、マイクロソフトサポートに相談しても、SMTP-AUTHを延命する話しか出てこなく、対応が遅れてしまいました。こういう話をMicrosoft 365のサポートから聞こうというのが、そもそも無理な相談なのかもしれません。
そんなわけで、本職の方が見たら笑われるかな、と思いつつ、次の方が同じ苦労をしなくてすめば、と思い、記事にしてみた次第です。
コメント