APIを通じてメールを取得する処理が必要に。そしてメール取得で必ずぶつかるのがページネーションの壁だ。
この記事では、Microsoft Graph APIとGmail APIのページネーションを、TypeScriptの実装コード付きで解説する。両APIを並べて比較することで、それぞれの設計思想の違いも見えてくるはずだ。
Microsoft Graph API
そもそもMicrosoft Graph APIを使うには認証が必須
APIを使用するためには「どのアプリが、どの権限で、誰のデータにアクセスするのか」をMicrosoftに申告する必要がある
そのための場所が Azure Portal(https://portal.azure.com) です。
個人アカウント、組織アカウントの違い
| 項目 | 組織アカウント(Microsoft 365) | 個人アカウント(outlook.com等) |
|---|---|---|
| エンドポイント | /users/{userId}/messages | /me/messages |
| 認証方式 | アプリケーション権限(Client Credentials)が使える | 委任権限(Authorization Code)のみ |
| ユーザー操作 | 不要(サーバー間で完結) | ブラウザでログイン・許可が必要 |
| 管理者同意 | 必要(Azure AD管理者が許可) | 不要(自分で許可するだけ) |
| 他人のメールボックス | アクセス可能(権限があれば) | 自分のメールボックスのみ |
| 使えるAPI | ほぼ全て | 一部制限あり(共有メールボックス、メールフォルダの一部操作など) |
| テナントID | 組織固有の値 | consumers という固定値を使う |
| MSALの設定 | authorityに組織のテナントIDを指定 | authorityに https://login.microsoftonline.com/consumers を指定 |
個人で開発環境を用意する
M365開発者プログラムに参加する方法もある
Microsoft 365の機能を使ったサービスを開発するときのテスト環境として利用ができる
https://developer.microsoft.com/en-us/microsoft-365/dev-program にアクセス無料で90日間のMicrosoft 365開発用テナント(組織アカウント)がもらえるGraph APIの全機能を試せる
開発者プログラムへの参加自体はできましたが、E5サブスクリプション(組織テナント)はもらえない状態でサンドボックスは取得できません。
Azure無料アカウント作成
https://signup.azure.com/signup
https://azure.microsoft.com/ja-jp

検索バーで「Microsoft Entra ID」と検索
Entra IDにアプリを登録 → 「アプリの認証・認可を管理」
アプリ登録画面で
- 名前
graph-mail-test(任意) - サポートされているアカウントの種類「任意の組織ディレクトリ内のアカウントと、個人用のMicrosoftアカウント」に変更してください。「シングルテナントのみ」のままだと個人アカウントでのログインができません
- リダイレクトURI プラットフォームを「Web」にして、
http://localhost:3000/auth/callbackと入力
登録完了したら、概要画面に以下の情報が表示
- アプリケーション(クライアント)ID
- ディレクトリ(テナント)ID
APIのアクセス許可を設定する
- アプリ登録の概要画面の左メニューから「APIのアクセス許可」をクリック
- 「アクセス許可の追加」をクリック
- 「Microsoft Graph」を選択
- 「委任されたアクセス許可」を選択
- 検索バーで以下を検索してチェックを入れる
Mail.ReadMail.ReadWriteMail.Send
- 「アクセス許可の追加」をクリック
クライアントシークレットを作成する
- 左メニューから「証明書とシークレット」をクリック
- 「新しいクライアントシークレット」をクリック
- 説明(例
test-secret) - 有効期限「6か月」でOK(学習用なので短くて問題ない)
- 「追加」をクリック
Microsoft Graph API のページネーション
基本の仕組み — @odata.nextLink
Microsoft Graph APIでは、レスポンスに次ページが存在する場合、@odata.nextLinkというプロパティに次ページの完全なURLが返ってくる。クライアントはこのURLに対してそのままGETリクエストを送ることで、次のページを取得する。最終ページに到達すると@odata.nextLinkは含まれなくなる。
レスポンスの構造はこうなっている。
// レスポンス例
{
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#me/messages",
"@odata.nextLink": "https://graph.microsoft.com/v1.0/me/messages?$skip=10",
"value": [
{ "id": "AAMk...", "subject": "会議のお知らせ", ... },
{ "id": "AAMk...", "subject": "請求書送付", ... },
// ...
]
}
ここで重要なのは、@odata.nextLinkのURLをそのまま使うという点だ。URLの中には$skiptokenや$skipといったパラメータが埋め込まれているが、これを分解して別のリクエストに組み込んではいけない。Graph APIの公式ドキュメントでも、nextLinkのURL全体をそのままGETに渡すよう明記されている。
$top と $skip の違い
Graph APIのページネーションで混乱しやすいのが、$topと$skipの使い分けだ。
$topは1ページあたりの取得件数を指定するパラメータだ。メールの場合、デフォルトは10件で最大999件まで指定できる。
エンドポイントによって違う。統一されていない。
messagesの場合 — 1〜1000の範囲でカスタマイズ可能 Microsoft Learnという記述がある。SDKのページングドキュメントでは「999まで」と書かれている。
usersの場合 —
$top=1000を指定するとエラーになり、1〜999の範囲でなければならないと明示されている Microsoft Learn。一般論 — APIによってデフォルトと最大ページサイズが異なる Microsoft Learn。一律の上限値はない。
// 1ページ50件でメールを取得
GET https://graph.microsoft.com/v1.0/me/messages?$top=50
$skipは先頭から指定件数をスキップするオフセット型のパラメータだ。「$skip=20」なら21件目から取得を開始する。ただし、メールのようにリアルタイムでデータが増減するリソースに$skipを使うと、ページ遷移中に新着メールが届いた場合にデータの重複や欠落が発生する。
結論として、メール取得では$skipを手動で操作せず、@odata.nextLinkによるサーバー駆動型ページネーションを使うべきだ。$topで件数だけ指定し、あとはnextLinkに任せるのが安全なパターンになる。
Graph APIメール取得の注意事項をまとめ
$searchと$orderbyは併用できない 結果は送信日時順で返るが、明示的なソート指定は不可。 https://learn.microsoft.com/en-us/graph/search-query-parameter
https://learn.microsoft.com/en-us/answers/questions/656200/graph-api-to-filter-results-on-from-and-subject-an
$searchと$skipは併用できない ページネーションはnextLinkのみ。 https://learn.microsoft.com/en-us/answers/questions/656200/graph-api-to-filter-results-on-from-and-subject-an
$searchの結果は最大1,000件 https://learn.microsoft.com/en-us/graph/search-query-parameter
$filterと$searchは併用できない https://learn.microsoft.com/en-us/graph/query-parameters
$filterと$orderbyの併用にはプロパティ順序の制約がある $orderbyのプロパティは$filterにも同じ順序で先に含める必要がある。違反すると"The restriction or sort order is too complex for this operation."エラー。 https://learn.microsoft.com/en-us/graph/api/user-list-messages?view=graph-rest-1.0
toRecipients / ccRecipients / bccRecipientsは$filterできない fromは可。to/cc/bccで絞りたい場合は$searchを使うしかない。https://learn.microsoft.com/en-us/answers/questions/1665637/how-to-query-emails-by-recipients-email-address
https://learn.microsoft.com/en-us/graph/search-query-parameter?tabs=http
$filterでドメイン部分一致(endsWith)はメッセージのemailAddressで使えない containsは使えるがNOTとの組み合わせ不可。ドメイン除外は$filterでは実現困難。 https://learn.microsoft.com/en-us/answers/questions/1168280/office-365-mail-graph-api-)-is-there-a-way-to-filt
from/emailAddress/address eqが一部の送信者で正しく動かないケースがある startsWithなら動く場合がある。 https://learn.microsoft.com/en-us/answers/questions/2153305/when-filtering-messages-on-ms-graph-rest-api-using
@odata.nextLinkにカスタムHTTPヘッダーは引き継がれない Preferヘッダーなどは毎回明示的に付与する必要がある。 https://learn.microsoft.com/en-us/graph/sdks/paging
$skipはリアルタイムに変化するデータで重複・欠落を起こす nextLinkの$skiptoken(サーバー管理カーソル)を使うべき。 https://learn.microsoft.com/en-us/graph/paging
Graph Explorerで$searchのNOT挙動を検証する手順。
1. Graph Explorerにアクセス
https://developer.microsoft.com/en-us/graph/graph-explorer
ブラウザで開く。
2. サインイン
左上の「Sign in to Graph Explorer」からMicrosoftアカウント(自分のテストアカウントか業務アカウント)でログイン。初回は権限の同意画面が出る。Mail.Read権限が必要なので、出てきたら同意する。権限が足りない場合は左ペインの「Modify permissions」タブからMail.Readを追加する。
3. ベースラインのクエリを試す
まず除外なしで、対象ドメインのメールが何件ヒットするかを確認しておく。GETメソッドで以下を実行。
https://graph.microsoft.com/v1.0/me/messages?$search="participants:example.com"&$select=id,subject,from,receivedDateTime&$top=10
example.comの部分は自分のメールボックスに実在するドメインに置き換える(テスト結果が0件だと検証にならないため)。レスポンスのvalue配列に何件か入っていることを確認する。
4. NOT付きクエリを試す
次に同じドメインを除外する形で実行。
https://graph.microsoft.com/v1.0/me/messages?$search="NOT participants:example.com"&$select=id,subject,from,receivedDateTime&$top=10
このときの結果を3パターンに分類して観察する。
conversations と messages の違い
Graph APIでメールのスレッドを取得したい場合、/me/messagesとは別に/me/mailFolders/{id}/messagesや、Outlook固有のconversationIdフィールドを利用する方法がある。
/me/messagesはメールボックス内の全メッセージをフラットに返す。スレッド単位でまとめたい場合は、取得したメッセージのconversationIdでグルーピングする。
import { Message } from "@microsoft/microsoft-graph-types";
function groupByThread(messages: Message[]): Map<string, Message[]> {
const threads = new Map<string, Message[]>();
for (const msg of messages) {
const convId = msg.conversationId ?? "unknown";
const existing = threads.get(convId) ?? [];
existing.push(msg);
threads.set(convId, existing);
}
return threads;
}
Graph APIにはスレッド一覧を直接返すエンドポイントがないため、クライアント側でのグルーピングが必要になる。Gmail APIのthreadsエンドポイントとは対照的な設計だ。
Graph APIのメールはフォルダ階層構造で管理されている
/me/mailFolders/inbox/messages — 受信トレイ
/me/mailFolders/drafts/messages — 下書き
/me/mailFolders/sentitems/messages — 送信済み
/me/mailFolders/deleteditems/messages — ゴミ箱(削除済みアイテム)
/me/mailFolders/junkemail/messages — 迷惑メール
/me/mailFolders/archive/messages — アーカイブ
/me/mailFolders/outbox/messages — 送信トレイ共有メールボックス
Outlook(およびExchange Online)では、共有メールボックスと委任メールボックスは既定で既読/未読の状態を共有します。
https://learn.microsoft.com/en-us/answers/questions/4736926/read-unread-issue-in-same-mailbox
Gmail API のページネーション
googleapis
googleapisはGoogleが公式提供しているNode.js向けクライアントライブラリで、google.gmail()はその中のGmail APIクライアント生成関数。
主要なメソッド階層
google.gmail()はクライアントインスタンスを返す関数で、引数にversionとauthを渡す。versionは現状"v1"一択。authは認証済みのOAuth2Clientや、サービスアカウントの認証情報など。
返ってくるgmailオブジェクトに、Gmail APIの全エンドポイントがメソッドチェーンで生えている。
REST APIのパス構造(users.messages.listなど)がそのままメソッド名として表現されている。
userIdは必須で、"me"を渡すと認証中のユーザー自身を指す。
gmail.users.getProfile({ userId: "me" })
gmail.users.messages.list({ userId: "me", q: "...", maxResults: 100 })
gmail.users.messages.get({ userId: "me", id: "..." })
gmail.users.messages.send({ userId: "me", requestBody: { ... } })
gmail.users.messages.modify({ userId: "me", id: "...", requestBody: { ... } })
gmail.users.messages.trash({ userId: "me", id: "..." })
gmail.users.messages.delete({ userId: "me", id: "..." })
gmail.users.threads.list({ userId: "me", q: "..." })
gmail.users.threads.get({ userId: "me", id: "..." })
gmail.users.labels.list({ userId: "me" })
gmail.users.labels.create({ userId: "me", requestBody: { ... } })
gmail.users.history.list({ userId: "me", startHistoryId: "..." })
gmail.users.drafts.list({ userId: "me" })
gmail.users.settings.filters.list({ userId: "me" })users.messages
┣ list ........... メッセージ一覧を取得(ID と threadId だけ)
┣ get ............ 1通の詳細を取得(format で粒度を指定)
┣ send ........... 送信
┣ insert ......... 既存メールを直接挿入(インポート用途)
┣ import ......... SPF等チェック付きでインポート
┣ trash .......... ゴミ箱へ
┣ untrash ........ ゴミ箱から戻す
┣ delete ......... 完全削除
┣ batchDelete .... 複数まとめて削除
┣ batchModify .... 複数のラベルをまとめて変更
�apply ┗ modify ........... ラベル付与/削除gmail.users.messages.list()はGmailのメッセージ一覧を取得するメソッド
パラメータ
| パラメータ | 型 | 説明 |
|---|---|---|
userId | string(必須) | ユーザーのメールアドレス。"me"で認証中ユーザー |
q | string | Gmail検索構文によるフィルタクエリ |
maxResults | number | 1ページあたりの最大件数(デフォルト100、最大500) |
pageToken | string | 次ページ取得用のトークン |
labelIds | string[] | ラベルIDで絞り込み(複数指定はAND) |
includeSpamTrash | boolean | SPAMとTRASHを含めるか(デフォルトfalse) |
list レスポンス
┣ messages[]
┃ ┣ {id, threadId}
┃ ┣ {id, threadId}
┃ ┗ ...
┣ nextPageToken ...... ページング用
┗ resultSizeEstimate重要な点として、レスポンスに含まれるのはメッセージIDとスレッドIDだけ。
件名や送信者などの内容は含まれない。詳細が必要ならmessages.getを別途呼ぶ必要がある。これがGraph APIの/me/messagesとの大きな違い。
list は ID しか返さないので、本文やヘッダーが欲しければ list → 各IDで get という2段構えになります。
users.messages.get が返すのは 1通分の Message オブジェクト です。format パラメータで中身の詳しさが変わります。
format 別の返却内容
minimal
┣ id
┣ threadId
┣ labelIds[]
┣ snippet
┣ historyId
┣ internalDate
┗ sizeEstimate
(payload なし/ヘッダーも本文もなし)
metadata
┣ minimal の全部
┗ payload
┣ mimeType
┣ headers[] ← ここが入る(metadataHeadersで絞れる)
┗ ※ body.data は入らない(本文は取れない)
full ★デフォルト
┣ minimal の全部
┗ payload(MIMEツリー丸ごと)
┣ mimeType
┣ filename
┣ headers[]
┣ body
┃ ┣ data ........ Base64urlの本文
┃ ┗ attachmentId 添付の場合
┗ parts[] ........ 子パート(再帰)
raw
┣ minimal の全部
┗ raw ............... RFC822生データ(Base64url文字列1本)
(payload は基本入らない/自分でMIMEパースする)Googleグループについて
Google グループでは、スレッドの既読 / 未読ステータスはユーザーごとに個別に保存されます。
https://developers.google.com/workspace/admin/groups-migration/v1/guides/manage-email-migrations?hl=ja
Gmail API 基本の仕組み — pageToken と nextPageToken
Gmail APIでは、users.messages.listのレスポンスにnextPageTokenが含まれていれば次ページが存在する。
次のリクエストでpageTokenパラメータにこの値を渡す。Graph APIの@odata.nextLinkが完全なURLを返すのに対して、Gmail APIは不透明なトークン文字列だけを返す点が異なる。
// レスポンス例
{
"messages": [
{ "id": "18f3a...", "threadId": "18f3a..." },
{ "id": "18f2b...", "threadId": "18f2b..." },
// ...
],
"nextPageToken": "09876543210987654321",
"resultSizeEstimate": 342
}
maxResults の使い方
maxResultsは1ページあたりの最大取得件数を指定するパラメータで、Graph APIの$topに相当する。デフォルトは100件、最大500件まで指定可能だ。
GET https://gmail.googleapis.com/gmail/v1/users/me/messages?maxResults=200
ただし重要な注意点がある。users.messages.listが返すのはメッセージIDとスレッドIDだけだ。件名や本文、送信者などの詳細情報は含まれない。個別のメッセージ内容を取得するには、別途users.messages.getを呼ぶ必要がある。
これはGraph APIとの大きな違いだ。Graph APIの/me/messagesはデフォルトで件名や送信者情報を含む。一方Gmail APIは一覧取得と詳細取得を明確に分離する設計になっている。
q パラメータによる検索フィルタ
Gmail APIのqパラメータはGmailのWeb UIの検索バーと同じ構文が使える。メールのフィルタリングに強力な機能だ。よく使うパターンをいくつか紹介する。
// 送信者で絞り込み
const query1 = "from:notifications@example.com";
// 日付範囲で絞り込み
const query2 = "after:2025/01/01 before:2025/04/01";
// 特定ドメインからのメールを除外
const query3 = "-from:@spam-domain.com";
// 複数ドメインを除外
const query4 = "-from:@marketing.example.com -from:@newsletter.example.com";
// ラベルと未読を組み合わせ
const query5 = "label:work is:unread";
// 件名に特定のキーワードを含む
const query6 = "subject:請求書";
// 添付ファイル付き + 特定サイズ以上
const query7 = "has:attachment larger:5M";
// 組み合わせた実践的な例:
// 今年の未読メールで、社内ドメインを除外し、添付ファイル付き
const practicalQuery =
"is:unread has:attachment after:2025/01/01 -from:@mycompany.com";
https://developers.google.com/workspace/gmail/api/guides/filtering?hl=ja
ドメイン除外は-from:@domain.comの形式で書く。ハイフン(-)がNOT演算子として機能する。複数ドメインを除外したい場合はスペース区切りで-from:を並べればよい。
このパラメータは、Gmail ウェブ インターフェースと同じ高度な検索構文のほとんどをサポートしています。
https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.messages/list?hl=ja
注意点として、Gmail APIのqパラメータはWeb UIと完全に同じではない。公式ドキュメントにも記載されているが、エイリアスの展開がAPI側では行われないケースがある。Google Workspaceでアカウントのエイリアスを設定している場合、Web UIでは検索にヒットするがAPIでは返らないという差異が生じうる。
Gmail API のスレッド取得
Gmail APIにはスレッド単位で取得する専用エンドポイントusers.threads.listが用意されている。Graph APIのように手動でグルーピングする必要がない。
ページネーションの仕組みはmessages.listと同じで、nextPageToken / pageTokenのパターンで全スレッドを順次取得する。threads.getを呼ぶと、そのスレッドに属する全メッセージがmessages配列に含まれて返ってくる。
実装で気をつけるべきこと
レートリミット対策
どちらのAPIもレートリミットが設定されている。大量のメールを取得する場合、リトライ処理は必須だ。
async function withRetry<T>(
fn: () => Promise<T>,
maxRetries = 3,
baseDelay = 1000
): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error: unknown) {
if (attempt === maxRetries) throw error;
// 429 Too Many Requests の場合はリトライ
const status = (error as { code?: number }).code;
if (status !== 429) throw error;
const delay = baseDelay * Math.pow(2, attempt);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw new Error("unreachable");
}
指数バックオフ(リトライ間隔を倍々に伸ばしていく方式)は、API連携における基本パターンだ。Graph APIもGmail APIも429ステータスを返した場合はRetry-Afterヘッダーを確認するのがベターだが、指数バックオフで十分対応できることが多い。
Graph API で $skip を避ける理由
繰り返しになるが、メールのような動的に変化するデータに$skipを使うのは危険だ。ページ1を取得した直後に新着メールが届くと、ページ2の先頭が本来ページ1に含まれるはずだったメッセージにずれる。結果として同じメールが重複して取得されたり、一部のメールが抜け落ちたりする。
@odata.nextLink内部で使われる$skiptokenはサーバー側が管理するカーソルであり、$skipのようなオフセットとは仕組みが異なる。データの挿入・削除があっても正しく次のページを指し示す。だから手動で$skipを計算するのではなく、@odata.nextLinkをそのまま使うべきなのだ。
Gmail API で messages.list と messages.get を分離する理由
Gmail APIが一覧取得でIDしか返さない設計は、最初は不便に感じるかもしれない。しかしこの分離にはメリットがある。
一覧取得の段階ではレスポンスが非常に軽い。数千件のメッセージIDを取得しても通信量はわずかだ。そして詳細取得の段階でformatパラメータによって必要な粒度を選べる。ヘッダーだけでいいならmetadata、本文のテキストだけならminimal、完全なデータが要るならfull。用途に応じて最適なリクエストが組める。
また、一覧取得のレスポンスが軽いことでmaxResults: 500を指定しても高速にページネーションが進む。IDだけの取得なら全メールボックスのスキャンも現実的な時間で完了する。

GmailのthreadIdもMicrosoft Graph APIのconversationIdも、「同一スレッドに属するメッセージをグルーピングするID」という同じ意味的役割を持っている。共通の型にまとめるのは妥当。
値の形式が異なる。 GmailのthreadIdは18e5a...のような16進文字列、MicrosoftのconversationIdはAAQkAD...のようなBase64風文字列。共通型では string にしておけば問題ないが、バリデーション(max長やregex)をプロバイダごとに変える必要がある場合、共通型のレイヤーではバリデーションを持たせず、Zodスキーマ側ですのが自然か?
https://developers.google.com/gmail/api/reference/rest/v1/users.messages
https://developers.google.com/workspace/gmail/api/guides/threads
https://learn.microsoft.com/en-us/graph/api/resources/message?view=graph-rest-1.0
https://learn.microsoft.com/en-us/graph/api/message-get?view=graph-rest-1.0
統一可 変換して統一 個別管理
識別子
| 概念 | Gmail | MS Graph | 判定 |
|---|---|---|---|
| メッセージID | id : string16進 例: “18e5a3b…” |
id : stringBase64風 例: “AAMkAG…” |
統一可 |
| スレッドID | threadId : string先頭メッセージのidと同値 |
conversationId : string自動生成・不変 |
統一可 |
宛先情報
| 概念 | Gmail | MS Graph | 判定 |
|---|---|---|---|
| From | payload.headers[]RFC2822文字列をパース要 |
from.emailAddress{ name, address } オブジェクト |
個別 |
| To / CC / BCC | payload.headers[]カンマ区切り文字列をパース要 |
toRecipients[] 等{ emailAddress: { name, address } }[] |
個別 |
本文・件名
| 概念 | Gmail | MS Graph | 判定 |
|---|---|---|---|
| 件名 | payload.headers[]name:”Subject” の値 |
subject : string |
変換統一 |
| 本文 | payload.parts[] 再帰構造base64urlデコード要 |
body.content : stringデコード済み |
個別 |
| プレビュー | snippet : string |
bodyPreview : string |
統一可 |
日時
| 概念 | Gmail | MS Graph | 判定 |
|---|---|---|---|
| 受信日時 | internalDate : stringepoch ms |
receivedDateTime : stringISO 8601 |
変換統一 |
| 送信日時 | payload.headers[]name:”Date” の値 |
sentDateTime : stringISO 8601 |
変換統一 |
メッセージ状態
| 概念 | Gmail | MS Graph | 判定 |
|---|---|---|---|
| 既読 | labelIds[]“UNREAD”の有無で判定 |
isRead : boolean |
変換統一 |
| 下書き | labelIds[]“DRAFT”の有無で判定 |
isDraft : boolean |
変換統一 |
| ラベル/カテゴリ | labelIds : string[]INBOX, SENT, STARRED 等 |
categories : string[]ユーザー定義カテゴリ |
個別 |
添付ファイル・ページネーション
| 概念 | Gmail | MS Graph | 判定 |
|---|---|---|---|
| 添付有無 | payload.parts[]filenameの有無で判定 |
hasAttachments : boolean |
変換統一 |
| ページネーション | nextPageToken : stringpageTokenパラメータに渡す |
@odata.nextLink : string完全URL、そのままGET |
統一可 |
プロバイダ固有フィールド
| API | フィールド | 説明 |
|---|---|---|
| Gmail固有 | sizeEstimate |
RFC822全体のバイト数概算 |
raw |
RFC2822全文 (base64url, format=RAW時) | |
historyId |
変更履歴の同期ポイント | |
| MS Graph固有 | importance |
“low” | “normal” | “high” |
webLink |
Outlook Web上のメールURL | |
parentFolderId |
所属フォルダID | |
flag |
フォローアップフラグ |
参照
Gmail API – users.messages /
Gmail API – threads /
MS Graph – message resource
Gmail API vs Microsoft Graph API ― メッセージ構造比較
============================================================
■ エビデンス
├── Gmail : https://developers.google.com/gmail/api/reference/rest/v1/users.messages
├── Gmail : https://developers.google.com/workspace/gmail/api/guides/threads
├── MS Graph : https://learn.microsoft.com/en-us/graph/api/resources/message?view=graph-rest-1.0
└── MS Graph : https://learn.microsoft.com/en-us/graph/api/message-get?view=graph-rest-1.0
■ メッセージ全体構造
│
├── Gmail API (users.messages)
│ ├── id : string (16進, 例: "18e5a3b...")
│ ├── threadId : string (16進, スレッドの先頭メッセージIDと同値)
│ ├── labelIds : string[]
│ ├── snippet : string
│ ├── historyId : string
│ ├── internalDate : string (epoch ms, 例: "1704067200000")
│ ├── sizeEstimate : number
│ ├── raw : string (RFC2822 base64url, format=RAW時のみ)
│ └── payload : MessagePart (後述)
│
└── Microsoft Graph API (message resource)
├── id : string (Base64風, 例: "AAMkAG...")
├── conversationId : string (Base64風, 例: "AAQkAD...")
├── conversationIndex : string (binary)
├── subject : string
├── bodyPreview : string
├── body : { contentType: string, content: string }
├── from : { emailAddress: { name: string, address: string } }
├── toRecipients : { emailAddress: { name, address } }[]
├── ccRecipients : { emailAddress: { name, address } }[]
├── bccRecipients : { emailAddress: { name, address } }[]
├── receivedDateTime : string (ISO 8601, 例: "2026-01-01T00:00:00Z")
├── sentDateTime : string (ISO 8601)
├── createdDateTime : string (ISO 8601)
├── lastModifiedDateTime : string (ISO 8601)
├── isDraft : boolean
├── isRead : boolean
├── hasAttachments : boolean
├── importance : string ("low" | "normal" | "high")
├── categories : string[]
├── webLink : string
├── parentFolderId : string
└── flag : { flagStatus: string }
■ スレッドID
│
├── Gmail
│ ├── フィールド名 : threadId
│ ├── 型 : string (16進)
│ ├── 意味 : 同一会話のメッセージをグルーピング
│ └── 特徴 : スレッド先頭メッセージのidと同一値
│ ユーザーごとに固有(送信者と受信者で異なる)
│
└── MS Graph
├── フィールド名 : conversationId
├── 型 : string (Base64風)
├── 意味 : 同一会話のメッセージをグルーピング
└── 特徴 : 自動生成・不変
ユーザーごとに固有
→ 役割は同一。共通型では threadId: string に統一可能
■ 宛先情報の取得方法
│
├── Gmail
│ └── payload.headers[] から文字列パース
│ ├── { name: "From", value: "John Doe <john@example.com>" }
│ ├── { name: "To", value: "alice@example.com, Bob <bob@example.com>" }
│ └── { name: "Cc", value: "..." }
│ → RFC 2822 形式の文字列をMIMEパーサーで分解する必要あり
│
└── MS Graph
└── トップレベルにオブジェクト構造で提供
├── from: { emailAddress: { name: "John Doe", address: "john@example.com" } }
├── toRecipients: [{ emailAddress: { name, address } }, ...]
└── ccRecipients: [{ emailAddress: { name, address } }, ...]
→ パース不要、そのまま使える
■ 本文の取得方法
│
├── Gmail
│ └── payload (MessagePart) の再帰構造
│ ├── mimeType : string (例: "multipart/alternative", "text/plain", "text/html")
│ ├── body : { data: string(base64url), size: number }
│ ├── parts : MessagePart[] (再帰)
│ └── 取得手順:
│ 1. payload.parts を再帰的に走査
│ 2. mimeType が "text/plain" or "text/html" の part を探す
│ 3. body.data を base64url デコード
│
└── MS Graph
└── トップレベルにフラット構造で提供
├── body.contentType : string ("text" | "html")
├── body.content : string (デコード済み本文)
└── bodyPreview : string (プレビュー)
→ デコード不要、そのまま使える
■ 日時
│
├── Gmail
│ └── internalDate : string (epoch ms)
│ 例: "1704067200000"
│ → new Date(Number(internalDate)).toISOString() で変換
│
└── MS Graph
└── receivedDateTime : string (ISO 8601)
例: "2026-01-01T00:00:00Z"
→ そのまま使える
■ 既読・下書き状態
│
├── Gmail
│ └── labelIds で判定
│ ├── "UNREAD" が含まれる → 未読
│ ├── "DRAFT" が含まれる → 下書き
│ └── 専用フィールドなし
│
└── MS Graph
└── 専用booleanフィールド
├── isRead : boolean
└── isDraft : boolean
■ 添付ファイル
│
├── Gmail
│ └── payload.parts[] 内で判定
│ ├── filename が空でない part → 添付ファイル
│ └── body.attachmentId → messages.attachments.get で取得
│
└── MS Graph
└── 専用フィールド + 別エンドポイント
├── hasAttachments : boolean
└── /messages/{id}/attachments で一覧取得
■ ページネーション
│
├── Gmail
│ ├── レスポンス : nextPageToken: string
│ └── リクエスト : pageToken パラメータに渡す
│
└── MS Graph
├── レスポンス : @odata.nextLink: string (完全URL)
└── リクエスト : そのURLをそのままGETするMicrosoft Graph の @odata.nextLink vs Google の nextPageToken
| 観点 | Microsoft Graph | Google API |
|---|---|---|
| 位置づけ | OData プロトコルのアノテーション(@ 付きメタ情報) | レスポンス本体のフィールド |
| 中身 | 完全な URL | トークン文字列のみ |
| 使い方 | URL をそのまま GET | 元のクエリに pageToken= を付与して再送 |
| 終端判定 | プロパティが消えたら終了 | 同左(ただし空で返ることもある) |
| 差分取得 | @odata.deltaLink で統一 | historyId + history API で別建て |