Gmail API と Microsoft Graph API の npm ライブラリ(googleapis, @microsoft/microsoft-graph-client)

業務システムやSaaSで、ユーザーが利用しているメールプロバイダに応じてGoogle Workspace と Microsoft 365 の両方に対応する必要が出てくる。

本記事では、Node.js環境で googleapis / google-auth-library@microsoft/microsoft-graph-client / @azure/msal-node を使い、両プロバイダを実装する方法を解説する。

目次

使用パッケージと役割

今回扱うパッケージは以下の5つ。

パッケージ役割
google-auth-libraryGoogle OAuth2 認証クライアント
googleapisGmail API を含むGoogle APIラッパー
@azure/msal-nodeMicrosoft Identity Platform(OAuth2)認証
@microsoft/microsoft-graph-clientMicrosoft Graph API クライアント
@microsoft/microsoft-graph-typesGraph API のTypeScript型定義

Google側は google-auth-library で認証トークンを取得し、googleapis 経由でGmail APIを呼ぶ

Microsoft側は @azure/msal-node(MSAL)でトークンを取得し、@microsoft/microsoft-graph-client でGraph APIを呼ぶ。

認証ライブラリとAPIクライアントが分離している構造は両者に共通している。

以下の図は、両プロバイダのメール送信における処理フローを示している。

Google側 OAuth2 認証のセットアップ

Google Cloud Consoleでプロジェクトを作成し、Gmail APIを有効化したあと、OAuth 2.0クライアントIDを発行する。取得した client_idclient_secretredirect_uri を使って認証クライアントを初期化する。

import { google } from "googleapis";
import { OAuth2Client } from "google-auth-library";

const oauth2Client = new OAuth2Client({
  clientId: process.env.GOOGLE_CLIENT_ID,
  clientSecret: process.env.GOOGLE_CLIENT_SECRET,
  redirectUri: process.env.GOOGLE_REDIRECT_URI,
});

// 認可URLの生成
const authUrl = oauth2Client.generateAuthUrl({
  access_type: "offline",
  scope: ["https://www.googleapis.com/auth/gmail.send"],
});

// コールバックでトークン取得
async function handleCallback(code: string) {
  const { tokens } = await oauth2Client.getToken(code);
  oauth2Client.setCredentials(tokens);
  return tokens;
}

access_type: "offline" を指定するとリフレッシュトークンが返る。これをDBに保存しておけば、ユーザーが再認可しなくてもトークンの自動更新ができる。スコープは gmail.send のみに絞ることで、メールの読み取り権限を不要にできる。最小権限の原則として重要なポイントだ。

googleapisは Google APIを利用するためのクライアントライブラリ

Google 側も最終的には直接 fetch ではなく gaxios という Google 製HTTPクライアントを経由します。

このプロジェクトの実体はこれです。

googleapis
  ↓
googleapis-common
  ↓
google-auth-library
  ↓
gaxios
  ↓
node-fetch

Node環境では、gaxios は内部で node-fetch を使っています。

該当コードはここです。

// node_modules/gaxios/build/cjs/src/gaxios.js
static async #getFetch() {
  const hasWindow = typeof window !== 'undefined' && !!window;
  this.#fetch ||= hasWindow
    ? window.fetch
    : (await import('node-fetch')).default;
  return this.#fetch;
}

gaxiosとは

Google開発のHTTPクライアントライブラリ

Axiosに似た使い勝手らしい、

Microsoft側 MSAL で認証

Microsoft側の認証にはMSAL(Microsoft Authentication Library)を使う。

Azure AD(Entra ID)でアプリ登録を行い、クライアントIDとシークレットを取得する。

import * as msal from "@azure/msal-node";

const msalConfig: msal.Configuration = {
  auth: {
    clientId: process.env.AZURE_CLIENT_ID!,
    authority: `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}`,
    clientSecret: process.env.AZURE_CLIENT_SECRET,
  },
};

const cca = new msal.ConfidentialClientApplication(msalConfig);

// 認可コードからトークン取得
async function handleMsCallback(code: string): Promise<msal.AuthenticationResult> {
  const result = await cca.acquireTokenByCode({
    code,
    scopes: ["https://graph.microsoft.com/Mail.Send"],
    redirectUri: process.env.AZURE_REDIRECT_URI!,
  });
  return result;
}

MSALには ConfidentialClientApplication(サーバーサイド向け)と PublicClientApplication(SPAやデスクトップ向け)がある。

ConfidentialClientApplication内部的には以下の処理を担っている。

まず、OAuth2の認可コードフローにおいて、認可コードをトークンエンドポイント(https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token)に送信し、アクセストークンとリフレッシュトークンを取得する。

このとき、リクエストにはクライアントIDだけでなくクライアントシークレットも含まれる。

これが「Confidential(秘密を保持できる)」の意味であり、シークレットを安全に保管できないブラウザやモバイルアプリでは使えない。

認可コードフロー(ConfidentialClientApplication) ユーザー バックエンド Azure AD 1 ログインボタンクリック 2 Azure AD 認可URLへリダイレクト 3 ユーザーがログイン・同意 4 認可コードをコールバックURLへ 5 acquireTokenByCode() 認可コード + clientSecret を送信 → トークンエンドポイント /oauth2/v2.0/token 6 アクセストークン + リフレッシュトークン 7 トークンをキャッシュ / DBに保存 以降 acquireTokenSilent で自動更新 8 Graph API /me/sendMail Authorization: Bearer {accessToken} ステップ5でclientSecretを送るのがConfidentialの核心(PKCEとの違い)

Google側との大きな違いとして、MSALはトークンキャッシュを内蔵しており、acquireTokenSilent を呼ぶだけで期限切れトークンの自動更新を処理してくれる。

Google側では oauth2Client.setCredentials でリフレッシュトークンをセットしたうえで、ライブラリ側の自動更新に任せる形になるため、挙動は似ているがAPIの設計思想に差がある。

@microsoft/microsoft-graph-clientでAPIアクセスの簡略化

import { Client } from '@microsoft/microsoft-graph-client'

const getGraphClient = (): Client => {
  return Client.initWithMiddleware({
    authProvider: {
      getAccessToken,
    },
  })
}

ClientがSDK(認証処理やリクエスト構築を抽象化してくれるツール一式)

直接HTTPリクエストを送るのではなく、MicrosoftがGraph API用に用意したライブラリを通してリクエストしています。

内部的には 最終的に fetch を使っています

実装上も node_modules/@microsoft/microsoft-graph-client/src/middleware/HTTPMessageHandler.ts でこうなっています。

context.response = await fetch(context.request, context.options);

googleapis microsoft graph types 型のサポートの違い

Google側(googleapis)の型補完

Google側では、APIの呼び出しパラメータやレスポンスには型が効くが、メール本文の構造(宛先、件名、本文、添付ファイル)は自前で組み立てるため型の保護が及ばない。

googleapis パッケージは内部に gmail_v1 名前空間を持っており、APIレスポンスやリクエストの型が定義されている。

ただし、メール送信で渡す raw フィールドはBase64urlエンコード済みの文字列なので、メール本文の構造に対して型が効くわけではない。

Microsoft Graph Types の型補完

@microsoft/microsoft-graph-types をインストールすると、Graph APIのリソースに対応する型が一通り使える。

型名役割主なプロパティ
Messageメールメッセージ全体subject, body, toRecipients, ccRecipients, attachments, importance など
ItemBodyメール本文contentType ("text" / "html"), content
Recipient宛先1件emailAddress: { name?, address }
EmailAddressメールアドレスname, address
FileAttachment添付ファイルname, contentType, contentBytes
BodyType本文の形式"text" / "html"

エディタ上では、たとえば message. と入力した時点で subjectbodytoRecipientsimportancehasAttachments などのプロパティ候補が表示される。body の中に入れば contentTypecontent が補完され、contentType にはリテラル型 "text" | "html" が効く。

Message 型には50以上のプロパティが定義されており、APIドキュメントを都度参照しなくても、エディタの補完でどんなフィールドが使えるか把握できる。

エラーハンドリングの実装パターン

メール送信は外部API呼び出しのため、ネットワークエラーやレートリミットへの対応が不可欠だ。

Gmail APIは1日あたりの送信数上限(無料アカウントで100通、Google Workspaceで2,000通)、Graph APIはメールボックスあたり10,000通/10分のリミットがある。

Graph APIにはRetry-Afterというヘッダーを付けてきます。このヘッダーの値は「何秒後なら再リクエストしていいか」を示す数値です。

例えばRetry-After: 5なら「5秒後にもう一度送ってくれ」という意味です。

先ほど貼ってもらったMicrosoftのドキュメントにあるように、Graph APIにはサービスごとに細かいレート制限があり、それを超えると429が返ります。Outlookのメール関連だと「10分間に10,000リクエスト」「同時4リクエスト」といった制限があるので、大量のメールを取得するこのコードでは当たる可能性があります。

Microsoft Graph SDK の組み込みリトライ機構

前節ではアプリケーション層でのリトライ実装を示したが、Microsoft Graph SDK(@microsoft/microsoft-graph-client)にはミドルウェアとしてリトライ処理が組み込まれている。

自前で429ハンドリングを書く前に、まずSDKのデフォルト動作を把握しておくべきだ。

デフォルトのミドルウェアチェーン

Client.initWithMiddleware でミドルウェアを明示指定しなかった場合、SDKは内部の MiddlewareFactory.getDefaultMiddlewareChain で以下のチェーンを自動構築する。

AuthenticationHandler → RetryHandler → RedirectHandler(Node.js環境のみ) → TelemetryHandler → HTTPMessageHandler

リクエストはこの順に処理され、最終的に HTTPMessageHandlerfetch を実行する。つまり、以下のコードだけで認証とリトライの両方が有効になる。

const client = Client.initWithMiddleware({
  authProvider: {
    getAccessToken: async () => getAccessToken(),
  },
})

RetryHandler の挙動

RetryHandler はレスポンスのステータスコードが 429(Too Many Requests)、503(Service Unavailable)、504(Gateway Timeout)のいずれかだった場合に自動でリトライを行う。

待機時間の決定ロジックは以下の通り。

  1. レスポンスに Retry-After ヘッダーがあれば、その値を待機秒数として採用する。値が数値ならそのまま秒数、HTTP日付形式なら現在時刻との差分を算出する。
  2. Retry-After ヘッダーがなければ、指数バックオフにランダムなジッターを加えた値を使う。
  3. いずれの場合も、最大待機時間(デフォルト180秒)で頭打ちにする。

デフォルトのオプション値

RetryHandlerOptions を引数なしで生成した場合のデフォルト値は以下の通り。

項目デフォルト値
delay(初回待機秒数)3秒
maxRetries(最大リトライ回数)3回
maxDelay(最大待機秒数)180秒

オプションのカスタマイズ

デフォルト値を変更したい場合は、ミドルウェアチェーン全体を自分で組み立てる必要がある。initWithMiddlewaremiddleware プロパティと authProvider プロパティは排他で、middleware を指定すると authProvider は無視される。

import {
  Client,
  AuthenticationHandler,
  RetryHandler,
  RetryHandlerOptions,
  TelemetryHandler,
  HTTPMessageHandler,
} from "@microsoft/microsoft-graph-client"

const authHandler = new AuthenticationHandler({
  getAccessToken: async () => getAccessToken(),
})
const retryHandler = new RetryHandler(new RetryHandlerOptions(5, 5)) // 5秒間隔、最大5回
const httpHandler = new HTTPMessageHandler()

authHandler.setNext(retryHandler)
retryHandler.setNext(httpHandler)

const client = Client.initWithMiddleware({
  middleware: authHandler,
})

認証・リトライ・HTTP実行の3つが最小構成で、TelemetryHandler は省略可能だ。

Retry-After ヘッダーの注意点

Graph APIのすべてのサービスが429レスポンスで Retry-After ヘッダーを返すわけではない。Microsoftのドキュメントでは、OneNoteやID保護・条件付きアクセスなど一部のリソースについて「Retry-After ヘッダーを返さない」と明記されている。

Outlookサービスについてはそのような注記がないため返ってくることが期待できるが、明示的な保証もない。SDKの RetryHandlerRetry-After がない場合に指数バックオフで待機するため、ヘッダーの有無に関わらず適切に動作する。

TypeScriptで開発する場合、型定義の充実度がコーディング体験に直結する。両者の型サポートには明確な差がある。

Microsoft Graph SDK の組み込みリトライ機構

前節ではアプリケーション層でのリトライ実装を示したが、Microsoft Graph SDK(@microsoft/microsoft-graph-client)にはミドルウェアとしてリトライ処理が組み込まれている。自前で429ハンドリングを書く前に、まずSDKのデフォルト動作を把握しておくべきだ。

デフォルトのミドルウェアチェーン

Client.initWithMiddleware でミドルウェアを明示指定しなかった場合、SDKは内部の MiddlewareFactory.getDefaultMiddlewareChain で以下のチェーンを自動構築する。

AuthenticationHandler → RetryHandler → RedirectHandler(Node.js環境のみ) → TelemetryHandler → HTTPMessageHandler

リクエストはこの順に処理され、最終的に HTTPMessageHandlerfetch を実行する。つまり、以下のコードだけで認証とリトライの両方が有効になる。

const client = Client.initWithMiddleware({
  authProvider: {
    getAccessToken: async () => getAccessToken(),
  },
})

デフォルトのオプション値

RetryHandlerOptions を引数なしで生成した場合のデフォルト値は以下の通り。

項目デフォルト値
delay(初回待機秒数)3秒
maxRetries(最大リトライ回数)3回
maxDelay(最大待機秒数)180秒

オプションのカスタマイズ

デフォルト値を変更したい場合は、ミドルウェアチェーン全体を自分で組み立てる必要がある。initWithMiddlewaremiddleware プロパティと authProvider プロパティは排他で、middleware を指定すると authProvider は無視される。

import {
  Client,
  AuthenticationHandler,
  RetryHandler,
  RetryHandlerOptions,
  TelemetryHandler,
  HTTPMessageHandler,
} from "@microsoft/microsoft-graph-client"

const authHandler = new AuthenticationHandler({
  getAccessToken: async () => getAccessToken(),
})
const retryHandler = new RetryHandler(new RetryHandlerOptions(5, 5)) // 5秒間隔、最大5回
const httpHandler = new HTTPMessageHandler()

authHandler.setNext(retryHandler)
retryHandler.setNext(httpHandler)

const client = Client.initWithMiddleware({
  middleware: authHandler,
})

認証・リトライ・HTTP実行の3つが最小構成で、TelemetryHandler は省略可能だ。

Retry-After ヘッダーの注意点

Graph APIのすべてのサービスが429レスポンスで Retry-After ヘッダーを返すわけではない。

Microsoftのドキュメントでは、OneNoteやID保護・条件付きアクセスなど一部のリソースについて「Retry-After ヘッダーを返さない」と明記されている。Outlookサービスについてはそのような注記がないため返ってくることが期待できるが、明示的な保証もない。

SDKの RetryHandlerRetry-After がない場合に指数バックオフで待機するため、ヘッダーの有無に関わらず適切に動作する。

テストでの検証

SDKのデフォルト構成に依存する場合、バージョンアップで挙動が変わるリスクがある。以下のようなテストを書いておくと、デフォルト値や構成の変更を検知できる。

import { describe, it, expect } from "vitest"
import {
  MiddlewareFactory,
  RetryHandler,
  RetryHandlerOptions,
} from "@microsoft/microsoft-graph-client"

describe("Graph API ミドルウェア構成", () => {
  it("デフォルトのミドルウェアチェーンにRetryHandlerが含まれること", () => {
    const mockAuthProvider = {
      getAccessToken: async () => "dummy-token",
    }
    const chain = MiddlewareFactory.getDefaultMiddlewareChain(mockAuthProvider)
    const hasRetryHandler = chain.some((m) => m instanceof RetryHandler)
    expect(hasRetryHandler).toBe(true)
  })

  it("RetryHandlerが429, 503, 504をリトライ対象としていること", () => {
    const statusCodes = (RetryHandler as unknown as Record<string, number[]>)[
      "RETRY_STATUS_CODES"
    ]
    expect(statusCodes).toContain(429)
    expect(statusCodes).toContain(503)
    expect(statusCodes).toContain(504)
  })

  it("RetryHandlerOptionsのデフォルト値が適切であること", () => {
    const options = new RetryHandlerOptions()
    expect(options.delay).toBe(3)
    expect(options.maxRetries).toBe(3)
    expect(options.getMaxDelay()).toBe(180)
  })
})

開発・テスト時のスコープ設定

以下に、メール関連で使用頻度の高いスコープをまとめる。

操作Gmail API スコープGraph API パーミッション
送信のみgmail.sendMail.Send
読み取りgmail.readonlyMail.Read
読み書きgmail.modifyMail.ReadWrite
全権限mail.google.com(非推奨)Mail.ReadWrite + Mail.Send

開発中は広めのスコープで動作確認しがちだが、本番リリース前に必要最小限に絞ることを忘れないこと。Googleはスコープが広いとOAuth同意画面の審査が厳しくなり、Microsoftは管理者同意が必要なパーミッション(Application 型)を使うと組織のIT管理者の承認が必要になる。

参考リンク

Google(Gmail API)

Microsoft(Graph API)

目次