Microsoft Graph APIでメールを送るには、Microsoft 365のメールボックスを差出人にして sendMail を呼びます。業務システムの通知メールをSMTPから切り替える場面でよく使われますが、アプリ専用アクセスの Mail.Send は既定で組織の全員を差出人にできてしまう権限です。この記事では、Pythonで1通送る最小の実装から、3MBを超える添付、送信元を特定のメールボックスに限定する設定、2026年12月末に迫るSMTP認証の基本認証の既定無効化への備えまでを、作業の順に説明します。内容は2026年11月7日時点の公式ドキュメントに基づきます。
まとめ:Graph APIのメール送信はsendMailと送信元の限定と202の後の確認で組む
無人のバッチやWebアプリから送るなら、アプリ専用アクセスで POST /users/{送信元}/sendMail を呼ぶ構成が基本です。成功時の 202 Accepted は受付の完了を示すだけで、宛先不明などの失敗は後から送信元の受信トレイに配信不能レポートとして届きます。
権限は必ず絞ります。Entra IDで同意した Mail.Send はテナント内の誰を差出人にしても送れるため、Exchange OnlineのRBAC for Applicationsで送信元のメールボックスだけに範囲を限り、Entra側の同意は外します。SMTP認証をユーザー名とパスワードで使っている機器やアプリは、2026年12月末に既存テナントで既定無効になる予定です。社内宛ての通知ならGraphへ、社外への大量配信なら専用の配信サービスへ移すのが現実的な分け方です。
Graph APIでメールを送る前に決める送信元メールボックスと委任・アプリ権限の選び方
コードを書く前に、誰の名前で送るのかと、ユーザーのサインインを挟むのかを決めます。この2つで、呼ぶURLと必要な権限が変わります。
sendMailが受け付ける2つのURLと委任アクセス・アプリ専用アクセスの使い分け
sendMailの公式リファレンスによると、要求先は /me/sendMail と /users/{id | userPrincipalName}/sendMail の2つで、必要な権限は委任でもアプリケーションでも Mail.Send です。本文はJSONのほか、MIMEをBase64にした文字列でも渡せます。保存先の指定は saveToSentItems で、省略すると送信済みアイテムに残ります。
画面からユーザーがサインインして自分のメールを送るなら委任アクセスで /me を使い、夜間バッチや問い合わせフォームの自動返信のように人が介在しない処理は、アプリ専用アクセスで /users/{送信元} を指定します。委任とアプリ専用の違いや権限設計の全体像は、親記事のMicrosoft Graph APIとはで詳しく扱っています。
共有メールボックスや別ユーザーを差出人にするときに必要な2種類の権限
「noreply」のような共有メールボックスから送りたい場合は、別のユーザーを差出人として送信する方法を確認します。委任アクセスでは、Graph側の Mail.Send.Shared に加えて、Exchange側で「代理人として送信」か「メールボックス所有者として送信」のどちらかを送信元に付けておくことが必要です。どちらも無いと 403 Forbidden と ErrorSendAsDenied が返ります。
前者は受信者に「AさんがBさんの代理で送信」と表示され、後者は代理の表示が出ません。アプリ専用アクセスで Mail.Send に管理者が同意している場合は、メールボックス単位の権限が無くても任意のユーザーとして送れます。便利な反面、この広さが後で説明する絞り込みの理由になります。
Entra IDへのアプリ登録からトークン取得までをPythonとMSALで動かす手順
ここからは、アプリ専用アクセスで共有の送信元メールボックスから1通送るまでを動かします。Entra IDのテナントと、送信元にするライセンス付きのメールボックスか共有メールボックスが前提です。
アプリ登録でMail.Sendを付与しクライアントシークレットを発行する手順
Microsoft Entra管理センターの「アプリの登録」で新しいアプリを作り、「APIのアクセス許可」からMicrosoft Graphのアプリケーションの許可 Mail.Send を追加して、管理者の同意を与えます。続いて「証明書とシークレット」でクライアントシークレットを発行し、テナントID・クライアントID・シークレットの3つを控えてください。Entra IDのテナントやアプリ登録の基礎はMicrosoft Entra ID(旧Azure AD)とはで説明しています。
本番では、シークレットより証明書による認証を選ぶと、期限切れの事故と漏えい時の影響を抑えられます。トークンはOAuth 2.0のクライアント資格情報フローで取得し、スコープには個々の権限名ではなく https://graph.microsoft.com/.default を指定します。
MSALのクライアント資格情報フローで1通送るPythonの最小実装コード
次のコードは pip install msal requests で動きます。接続情報と宛先は環境変数で渡し、コードに直接書かない形にしています。
# graph_sendmail.py : アプリ専用アクセスで送信元メールボックスから1通送る
import os, time
import msal, requests
TENANT_ID = os.environ["TENANT_ID"]
CLIENT_ID = os.environ["CLIENT_ID"]
CLIENT_SECRET = os.environ["CLIENT_SECRET"]
SENDER = os.environ["SENDER_UPN"] # 送信元メールボックスの UPN
TO_ADDRESS = os.environ["TO_ADDRESS"] # 宛先アドレス
def get_token():
app = msal.ConfidentialClientApplication(
CLIENT_ID,
authority=f"https://login.microsoftonline.com/{TENANT_ID}",
client_credential=CLIENT_SECRET,
)
result = app.acquire_token_for_client(scopes=["https://graph.microsoft.com/.default"])
if "access_token" not in result:
raise RuntimeError(result.get("error_description"))
return result["access_token"]
def send_mail(subject, html_body):
url = f"https://graph.microsoft.com/v1.0/users/{SENDER}/sendMail"
payload = {
"message": {
"subject": subject,
"body": {"contentType": "HTML", "content": html_body},
"toRecipients": [{"emailAddress": {"address": TO_ADDRESS}}],
},
"saveToSentItems": True,
}
headers = {"Authorization": f"Bearer {get_token()}"}
for _ in range(5):
r = requests.post(url, json=payload, headers=headers, timeout=30)
if r.status_code == 202: # 受付の完了(配信の完了ではない)
return r.headers.get("request-id")
if r.status_code == 429: # スロットリングは指定秒数だけ待って再送
time.sleep(int(r.headers.get("Retry-After", "10")))
continue
raise RuntimeError(f"{r.status_code} {r.text}")
raise RuntimeError("再送の上限に達しました")
if __name__ == "__main__":
print(send_mail("請求書発行のお知らせ", "<p>今月分の請求書を発行しました。</p>"))
再送するのは429だけにしています。5xxで自動再送すると、Exchange側では受け付け済みだった場合に同じメールが2通届くおそれがあるためです。5xxは応答ヘッダーの request-id と時刻をログに残し、送信済みアイテムで実際に出たかを確かめてから再送を判断します。
202 Acceptedは配信完了ではない:送信処理の流れと配信不能レポートの戻り先
Graphのメール送信プロセスの概要によると、sendMailはまず送信者の下書きフォルダーにメッセージを作り、その時点で202を返します。トランスポートへの受け渡し、送信済みアイテムへの移動、ポリシーの評価、宛先への配信は応答の後に進みます。
そのため、宛先のアドレスが存在しない場合や、管理者が設定したメールフローのルールで止められた場合も、APIの応答は202のままです。失敗は配信不能レポートとして送信元の受信トレイに届きます。共有メールボックスを送信元にするなら、その受信トレイを誰かが見る運用にするか、Graphで定期的に読み取って通知する仕組みを用意しておかないと、送信失敗に気付けません。
添付ファイルとHTML本文の送り方:3MBの境界とアップロードセッションの実装
添付ファイルは大きさで送り方が変わります。帳票のPDF程度ならsendMail1回で済みますが、図面や動画のように数MBを超えるものは手順が増えます。
3MB未満の添付をcontentBytesに埋めてsendMailで一度に送る書き方
3MB未満なら、メッセージの attachments に #microsoft.graph.fileAttachment を並べ、ファイルの中身をBase64にして contentBytes に入れます。公式リファレンスの例と同じ形で、権限も Mail.Send のままです。Base64にすると元のサイズより3割ほど大きくなるため、送る前の判定は元ファイルではなく変換後の長さで行うと安全です。
3MB以上150MBまでの添付を下書きとアップロードセッションで送る手順
大きいファイルを添付する手順では、3MBから150MBまでのファイルは、下書きを作ってアップロードセッションで分割して送る方式と定められています。3MB未満でセッションを作ると ErrorAttachmentSizeShouldNotBeLessThanMinimumSize になります。下書きの作成とセッションの作成には Mail.ReadWrite が必要で、Mail.Send だけでは足りない点に注意してください。
# 3MB 以上の添付: 下書き作成 → アップロードセッション → 分割 PUT → 下書きを送信
def send_with_large_file(token, subject, path):
base = f"https://graph.microsoft.com/v1.0/users/{SENDER}"
h = {"Authorization": f"Bearer {token}"}
draft = requests.post(f"{base}/messages", headers=h, timeout=30, json={
"subject": subject,
"body": {"contentType": "Text", "content": "添付ファイルをご確認ください。"},
"toRecipients": [{"emailAddress": {"address": TO_ADDRESS}}],
})
draft.raise_for_status()
msg_id = draft.json()["id"]
size = os.path.getsize(path)
s = requests.post(f"{base}/messages/{msg_id}/attachments/createUploadSession",
headers=h, timeout=30,
json={"AttachmentItem": {"attachmentType": "file",
"name": os.path.basename(path), "size": size}})
s.raise_for_status()
upload_url = s.json()["uploadUrl"] # 認証済みの URL。Authorization ヘッダーは付けない
chunk = 3 * 1024 * 1024 # 1 回の PUT は 4MB 未満に抑える
with open(path, "rb") as f:
start = 0
while start < size:
data = f.read(chunk)
end = start + len(data) - 1
r = requests.put(upload_url, data=data, timeout=60, headers={
"Content-Type": "application/octet-stream",
"Content-Range": f"bytes {start}-{end}/{size}",
})
r.raise_for_status()
start = end + 1
requests.post(f"{base}/messages/{msg_id}/send", headers=h, timeout=30).raise_for_status()
uploadUrl は outlook.office.com ドメインの事前認証済みURLで、公式の手順どおりAuthorizationヘッダーを付けずにPUTします。最後のPUTが 201 Created を返したら添付の完了です。下書きの送信はmessageのsendで、これも成功時は202を返します。
アプリ権限のMail.Sendを特定メールボックスだけに絞るRBACの設定
アプリ専用アクセスの Mail.Send は、社長のアドレスを差出人にしたメールも送れる権限です。シークレットが漏れたときの被害を考えると、送信元を限定する設定は本番前に必ず入れます。
Application Access PolicyからRBACへの移行経緯
以前は、Exchange OnlineのApplication Access Policyでアプリが触れるメールボックスをグループ単位に制限していました。現在はRBAC for Applications in Exchange Onlineが、Application Access Policyを置き換える仕組みと明記されています。2026年11月7日に確認した時点では、Graphのドキュメントにあった旧方式の解説ページ(auth-limit-mailbox-access)もこのページへ転送されていました。これから作る連携は、RBAC for Applicationsで設定します。
管理スコープとロール割り当てで送信元を限定するPowerShellコマンド
設定にはExchange OnlineのOrganization Managementのロールグループに属するアカウントが必要です。次の例は、カスタム属性に「graph-sender」を入れたメールボックスだけを送信元として許可します。
Connect-ExchangeOnline
# 1. Entra ID のサービスプリンシパルを Exchange に登録する
# ID は「エンタープライズ アプリケーション」の画面の値を使う(アプリの登録の画面とは値が違う)
New-ServicePrincipal -AppId "<アプリケーションID>" -ObjectId "<オブジェクトID>" -DisplayName "graph-mail-sender"
# 2. 送信元にしてよいメールボックスを管理スコープで定義する
New-ManagementScope -Name "Graph送信元" -RecipientRestrictionFilter "CustomAttribute1 -eq 'graph-sender'"
# 3. Application Mail.Send ロールをスコープ付きで割り当てる
New-ManagementRoleAssignment -App "<オブジェクトID>" -Role "Application Mail.Send" -CustomResourceScope "Graph送信元"
# 4. 送信元にするメールボックスと、それ以外で結果を比べる(InScope が True と False になるか)
Test-ServicePrincipalAuthorization -Identity "graph-mail-sender" -Resource "<送信元メールボックス>" | Format-Table
Test-ServicePrincipalAuthorization -Identity "graph-mail-sender" -Resource "<対象外のメールボックス>" | Format-Table
公式ページの制限事項によると、権限の変更はキャッシュの都合で反映まで30分から2時間かかります。テスト用のコマンドはキャッシュを通らないため、コマンドで InScope が期待どおりでも、実際のAPI呼び出しがしばらく古い権限のまま動くことがあります。切り替えの直後に403や想定外の成功が出ても、2時間ほど待ってから再確認してください。
Entra側のMail.Sendを外さないと絞り込みが効かない和集合の落とし穴
見落としやすいのは、RBAC for Applicationsの権限がEntra IDで同意した権限に「追加」される点です。公式のFAQは、両方の権限は和集合として評価されると説明しています。Entra IDに組織全体の Mail.Send が残っていると、Exchange側でスコープを付けても誰の名前でも送れる状態のままになります。
手順としては、RBACの割り当てとテストを済ませてから、アプリの登録の「APIのアクセス許可」で Mail.Send の管理者の同意を取り消します。大きな添付のために Mail.ReadWrite も使う場合は、同じく Application Mail.ReadWrite のロールをスコープ付きで割り当て、Entra側の同意を外します。
SMTP認証の基本認証の既定無効化に備えてGraph送信へ移すときの判断基準
複合機のスキャン送信や古い業務アプリが、Microsoft 365のアカウントのユーザー名とパスワードでSMTP送信している環境は少なくありません。Graphへ移す理由の多くはここにあります。
2026年12月末に既存テナントで既定無効となるSMTP AUTHの日程と影響範囲
Exchangeチームが2026年1月27日に公開したSMTP AUTHの基本認証廃止の改定スケジュール(1月29日更新のVersion 3.0)は、日程を次のように示しています。当初の2026年3月から4月に段階的に拒否する計画は、この改定で延期されました。
| 時期 | SMTP AUTHの基本認証 |
|---|---|
| 2026年12月まで | 現状のまま使える |
| 2026年12月末 | 既存テナントで既定無効 |
| 同上(管理者の操作) | 必要なら再び有効化できる |
| 2026年12月以降の新テナント | 既定で利用できない |
| 2027年下半期 | 最終的な削除日を発表 |
対象はユーザー名とパスワードによるSMTPのクライアント送信だけで、OAuthで認証するSMTP送信はサポートされる認証方式として残ります。再有効化で時間は稼げますが、削除日の発表後は延ばせなくなるため、2026年の年末を移行の区切りにする計画が無難です。EWSでメールを送っている連携は別の期限で止まるので、Microsoft Graph APIは終了する?EWS廃止とAzure AD Graph終了の期限で日程を確認してください。
Graph・SMTPのOAuth化・ACSを送信元と宛先で選ぶ判断基準
移行先は、送信元を自社でコード変更できるか、宛先が社内か社外か、で分けます。表中のGraphはGraph sendMail、ACSはAzure Communication Servicesを指す略記です。送信元の改修可否と、宛先の範囲に合わせて次の表を確認してください。
| 移行先 | 向いている送信元 | 主な宛先 |
|---|---|---|
| Graph sendMail | 自社開発のアプリ | 社内と少数の社外 |
| SMTPのOAuth化 | SMTPしか話せない製品 | 社内と少数の社外 |
| ACS | 大量配信するサービス | 社外の顧客 |
SMTPのOAuth化は、IMAP・POP・SMTPをOAuthで認証する手順に沿って、スコープ https://outlook.office365.com/.default でトークンを取り、SASL XOAUTH2で渡します。クライアント資格情報フローを使う場合は、Exchangeにサービスプリンシパルを登録し、Add-MailboxPermission で送信元へのアクセスを、別の差出人を名乗るなら Add-RecipientPermission で「メールボックス所有者として送信」を与えます。製品側がXOAUTH2に対応していなければこの道は使えないので、まずベンダーに対応状況を確認してください。
Azure Communication Servicesのメールは、独自ドメインからのトランザクションメールや大量配信を従量課金で送るサービスです。概要ページの冒頭には一部サービスの廃止予定に関する警告が掲載されているため、採用する前に対象の機能が含まれていないかを廃止ガイドで確認してください。AWSを使っている組織なら、Amazon SESとはで解説しているSESも同じ位置づけの選択肢です。
送信数の上限:1日1万人・毎分30通とGraphの10分1万要求の両方で見積もる
Graphで送るメールにも、メールボックスとしての送信上限がそのまま掛かります。Exchange Onlineの制限では、1つのメールボックスから送れる受信者は24時間で10,000人、メッセージは1分あたり30通です。1通の宛先に並べられる受信者は、管理者が最大1,000人までの範囲で設定します。
API側では、Graphのサービス固有の調整制限により、アプリとメールボックスの組ごとに10分間で10,000要求、同時4要求、5分間で150MBのアップロードが上限です。毎分30通の制限の方が先に効くため、1つの送信元から月末にまとめて数千通を送る設計は避けて、送信元を分けるか、時間を分散させるキューを置きます。社外の顧客へ数万通を送る用途はExchange Onlineの想定外で、制限のページでも外部への大量送信には別のサービスの利用を勧めています。
Graph APIでのメール送信を自社開発すべき条件と外部配信へ回す場面
Graphで自社開発してよいのは、宛先の多くが社内か取引先で、1つの送信元からの送信が1日数百通程度に収まり、送信済みアイテムをMicrosoft 365側で監査したい場合です。承認依頼や障害通知、帳票の送付のように「社員のメールボックスから出たこと」に意味がある通知は、Graphと相性が良い用途です。
反対に、会員向けのお知らせやキャンペーンのように社外へ数千通単位で送る処理、開封やクリックの計測が必要な処理、配信停止の管理が要る処理は、Graphで作り込まずに外部の配信サービスへ回します。複合機や既製のパッケージのようにコードを直せない送信元は、SMTPのOAuth化に対応した版へ上げるか、ベンダーの対応を待つ間だけ基本認証を再有効化する方法で、移行までの時間を稼ぐことが可能です。どの送信元がどの方式を使っているかの棚卸しから、Graphへの書き換え、RBACでの権限の絞り込みまでを社内で進める余力が無い場合は、Microsoft 365・Copilot導入支援で移行の開発部分だけを切り出して相談する方法もあります。
よくある質問
Graph APIでのメール送信について、実装の前によく聞かれる質問に公式ドキュメントの記述に沿って答えます。
Graph APIでのメール送信に追加の料金はかかりますか?
sendMailは従量課金の対象APIではなく、Microsoft 365のライセンスの範囲で使えます。必要なのは、送信元にするメールボックスのライセンスか、ライセンス不要の共有メールボックスです。Graphで料金が発生する経路は限られており、詳しくはMicrosoft Graph APIの料金で整理しています。
個人のOutlook.comアカウントからもsendMailで送れますか?
送れます。公式リファレンスでは、個人用のMicrosoftアカウントでも委任アクセスの Mail.Send が使えると記載されています。ただしアプリ専用アクセスは職場や学校のアカウントのテナントが前提です。個人アカウントでは、ユーザーのサインインを挟む委任アクセスで /me/sendMail を呼ぶ形になります。
sendMailで送ったメールを送信済みアイテムに残さないことはできますか?
要求本文で saveToSentItems を false にすると残りません。ただし、別のユーザーを差出人にして送る場合に、管理者が「代理で送信したメッセージのコピーを必ず送信済みアイテムに保存する」設定をしていると、指定にかかわらず差出人側の送信済みアイテムに保存されます。
sendMailでBCCや独自のメールヘッダーを指定できますか?
できます。BCCはメッセージの bccRecipients に宛先を並べる形式です。独自のヘッダーは internetMessageHeaders に名前と値の組で指定し、公式の例では「x-custom-header-」で始まる名前を使っています。ヘッダーやS/MIMEまで細かく組み立てたい場合は、MIME形式で本文を作ってBase64にし、Content-Type: text/plain で送る方法もあります。
アプリ専用アクセスで送ったメールの差出人は誰になりますか?
要求のURLに指定した /users/{id} のユーザーが差出人になり、送信済みアイテムもそのメールボックスに保存されます。アプリの名前が差出人に表示されることはありません。そのため、送信元には個人の社員ではなく、用途ごとに用意した共有メールボックスを指定し、RBAC for Applicationsでそのメールボックスだけを許可する構成をおすすめします。
関連記事
- Microsoft Graph APIの料金:無料の範囲と従量課金APIの単価・有効化手順【2026年11月】:sendMail以外でGraphの費用がかかる経路
- SharePoint APIとは?REST APIとGraphの使い分け・Sites.Selected権限とリスト操作の実装例:アプリ権限を特定のサイトだけに絞る同じ考え方
- メール配信システムの自作・構築とは?SMTP基盤設計と到達率確保の実装手順:社外への大量配信を自前で組む場合の設計
- BIMIとは?Exchange Online・Outlookの対応状況とMicrosoft 365から送るメールの設定手順:Microsoft 365から送るメールの送信ドメイン認証