Microsoft Graph 実務入門 #4 メールとカレンダーの自動化 — 読み取り、送信、予定の作成
第 3 回の骨格に最初の実務シナリオを載せます。メールとカレンダーは Graph 自動化で最も需要の大きい領域です。毎朝レポートを配信する、特定のメールボックスを見張って添付を集める、会議を自動で入れる、といった仕事です。今回の例は無人の自動化を前提に application 権限のクライアントを使いますが、delegated に替えるなら users.by_user_id(...) を me に置き換えればよい、という第 3 回の対応ルールがそのまま適用されます。
先に権限を整理しておきます。今回登場するのは、メールの読み取り Mail.Read、送信 Mail.Send、カレンダー Calendars.ReadWrite、空き状況の照会 Calendars.Read(getSchedule は Schedule.Read.All でも可)です。application 権限なら全部が管理者の同意の対象で、第 2 回で触れた ApplicationAccessPolicy でアクセスできるメールボックスを自動化アカウント数個に絞っておくのが安全な構成です。
受信箱の読み取り — フィルタとともに #
メール照会の基本形です。「直近 24 時間に届いた、件名に [日報] が付いたメール」を取り出してみます。
from datetime import datetime, timedelta, timezone
from msgraph.generated.users.item.messages.messages_request_builder import (
MessagesRequestBuilder,
)
async def recent_reports(client, user_id: str):
since = (datetime.now(timezone.utc) - timedelta(days=1)).strftime(
"%Y-%m-%dT%H:%M:%SZ"
)
params = MessagesRequestBuilder.MessagesRequestBuilderGetQueryParameters(
filter=f"receivedDateTime ge {since} and startsWith(subject, '[日報]')",
select=["subject", "from", "receivedDateTime", "hasAttachments"],
orderby=["receivedDateTime desc"],
top=50,
)
config = MessagesRequestBuilder.MessagesRequestBuilderGetRequestConfiguration(
query_parameters=params,
)
result = await client.users.by_user_id(user_id).messages.get(
request_configuration=config
)
return result.value or []実務の要領を二つ足します。第一に、デフォルトの照会対象はメールボックス全体なので、受信箱だけ見るなら mail_folders.by_mail_folder_id("inbox").messages のようにフォルダを経由します。第二に、本文(body)は大きいので一覧の段階では $select から外し、選別したメールだけ個別照会で本文を取る 2 段構えが、スロットリングと速度の両方に有利です。
添付の収集も一歩足すだけです。hasAttachments が真のメールに対して messages/{id}/attachments を照会すると添付の一覧が来て、通常のファイル添付(fileAttachment)なら contentBytes に Base64 で内容が入っているので、デコードして保存すれば終わりです。
メールの送信 — sendMail 一回で済みます #
送信は下書きの作成 → 送信の 2 段階もできますが、実務の自動化の 99% は一度に送る sendMail で十分です。
from msgraph.generated.models.message import Message
from msgraph.generated.models.item_body import ItemBody
from msgraph.generated.models.body_type import BodyType
from msgraph.generated.models.recipient import Recipient
from msgraph.generated.models.email_address import EmailAddress
from msgraph.generated.users.item.send_mail.send_mail_post_request_body import (
SendMailPostRequestBody,
)
def to_recipient(addr: str) -> Recipient:
return Recipient(email_address=EmailAddress(address=addr))
async def send_report(client, sender_id: str, to: list[str], html: str):
body = SendMailPostRequestBody(
message=Message(
subject="[自動送信] 週次デプロイ状況",
body=ItemBody(content_type=BodyType.Html, content=html),
to_recipients=[to_recipient(a) for a in to],
),
save_to_sent_items=True,
)
await client.users.by_user_id(sender_id).send_mail.post(body)送信者(sender_id)は application 権限では任意のユーザーを指定できますが、実務では noreply@... のような専用の自動化アカウント(または共有メールボックス)を作り、そのアカウントからだけ送るのが定石です。人のアカウントから送ると返信や不在通知がその人のメールボックスに届いてしまい、監査(audit)の観点でも自動送信と人の送信が混ざるのは避けるべきです。添付が必要なら message.attachments に fileAttachment(名前 + Base64 の内容)を足せばよく、3MB を超える大きい添付は第 5 回で扱うアップロードセッションと同じ方式の別手順が必要だ、ということだけ覚えておきます。
大量送信には注意が必要です。Exchange 側にはアカウントあたりの送信量の制限(おおよそ一日 1 万通程度)と受信者数の制限があり、数百人へループで送るコードは第 7 回で扱うスロットリング(429)に最初にぶつかる場面でもあります。ニュースレター級の大量メールは Graph ではなく専門の配信サービスの領域だ、という境界線も引いておきます。
予定の作成 — 出席者の招待まで #
カレンダーの書き込みの基本形はイベントの作成です。出席者を入れると、招待メールまで Exchange が送ってくれます。
from msgraph.generated.models.event import Event
from msgraph.generated.models.date_time_time_zone import DateTimeTimeZone
from msgraph.generated.models.attendee import Attendee
from msgraph.generated.models.attendee_type import AttendeeType
async def create_meeting(client, organizer_id: str):
event = Event(
subject="デプロイのふりかえり",
start=DateTimeTimeZone(
date_time="2026-08-20T10:00:00", time_zone="Asia/Tokyo"
),
end=DateTimeTimeZone(
date_time="2026-08-20T11:00:00", time_zone="Asia/Tokyo"
),
attendees=[
Attendee(
email_address=EmailAddress(address="dev-team@contoso.com"),
type=AttendeeType.Required,
),
],
is_online_meeting=True, # Teams 会議リンクを自動生成
online_meeting_provider="teamsForBusiness",
)
return await client.users.by_user_id(organizer_id).events.post(event)is_online_meeting=True の 1 行で Teams 会議のリンクが付くこと、繰り返しの予定は recurrence プロパティで表現することあたりが、よく使う拡張です。
タイムゾーン — カレンダー自動化の定番の罠 #
カレンダーのコードで最も多いバグはタイムゾーンから出ます。ルールを二つ立てておけば大半は予防できます。
- 書くときは上の例のように
dateTime + timeZoneを常に対で明示します。「ローカル時間だろう」という仮定は、サーバー(UTC 基準)で動いた瞬間に 9 時間ずれます。 - 読むときは、Graph が基本的に UTC で返すことを前提にするか、リクエストヘッダー
Prefer: outlook.timezone="Asia/Tokyo"を付けて希望のタイムゾーンに変換された値を受け取ります。照会の結果をそのまま人に見せる自動化(毎日の予定の要約など)なら後者が楽です。
期間で予定を取り出すときは events ではなく calendarView を使うことも一緒に覚えておきます。calendarView?startDateTime=...&endDateTime=... は繰り返しの予定を実際の発生回に展開して返すので、「今週の予定」の類いの照会は必ずこちらです。events は繰り返しの予定を定義 1 件でしか返さず、週次の要約から定例会議が丸ごと抜ける事故になります。
空き時間を探す — getSchedule #
「出席者全員が空いている時間」を探す自動化の材料が getSchedule です。複数のユーザー(会議室もメールアドレスを持つリソースなので同じ方法で)の空き/多忙の情報を一度に照会します。
from msgraph.generated.users.item.calendar.get_schedule.get_schedule_post_request_body import (
GetSchedulePostRequestBody,
)
async def check_availability(client, user_id: str, emails: list[str]):
body = GetSchedulePostRequestBody(
schedules=emails,
start_time=DateTimeTimeZone(
date_time="2026-08-20T09:00:00", time_zone="Asia/Tokyo"
),
end_time=DateTimeTimeZone(
date_time="2026-08-20T18:00:00", time_zone="Asia/Tokyo"
),
availability_view_interval=30, # 30 分単位
)
result = await client.users.by_user_id(user_id).calendar.get_schedule.post(body)
for sched in result.value:
# availability_view: "0"=空き "1"=未定 "2"=多忙 "3"=不在 の文字列
print(sched.schedule_id, sched.availability_view)レスポンスの availabilityView は、指定した間隔(上では 30 分)単位の状態を数字の文字列で返します。"002200..." のように来るので、出席者全員の文字列で同じ位置がすべて 0 の区間を探せば、それが共通の空き時間です。文字列処理数行で会議時間のレコメンダーが作れる、手間のわりに効果の大きい API です。
まとめ #
- 権限は Mail.Read / Mail.Send / Calendars.ReadWrite が基本セットで、application なら ApplicationAccessPolicy でアクセス範囲を自動化アカウントに絞っておきます。
- メールの一覧はフォルダ経由 +
$selectから本文を除外 + 2 段照会が基本です。送信は sendMail 一つで終わり、送信元は専用の自動化アカウントに統一します。 - 大量送信は Exchange の送信上限とスロットリングの地雷原です。ニュースレター級は Graph の領域ではありません。
- 予定は dateTime+timeZone を常に対で、照会は
Prefer: outlook.timezoneで、期間の照会は events ではなく calendarView で行います。繰り返しの予定が絡む事故の大半はこの三つから出ます。 - getSchedule の availabilityView の文字列を重ねると「全員が空いている時間」が出ます。会議室も同じ方法で照会できます。
次回はファイルです。OneDrive と SharePoint の構造(drive と site)、大容量のアップロードセッション、共有リンクの生成を扱います。