Google Colab上でGmailを送信してみる(サービスアカウント認証・パスワード認証・OAuth 2.0 クライアントID)
はじめに
本記事ではGoogle Colaboratory上でGmailを送る方法を3種類扱います。
最終的に至った結論はサービスアカウント認証かOAuth 2.0 クライアントIDが有力そうではあるがいずれも問題点あり、です。3種類の方法ともメール送信はできましたが、運用・保守まで考えると「これがベスト」という結論には達せずでした。
「試行錯誤したけどダメだった系記事」であることを、最初におことわりしておきます。
要約
- 本記事はGoogle WorkspaceでのGmail送付を扱います。
- 下記の3つでGoogle ColaboratoryからGmailを送ります。
- サービスアカウント認証
- パスワード認証
- OAuth 2.0 クライアントID認証
- いずれの方法も課題があり、本記事では最適解に至っていません。
- サービスアカウント認証
- 特定のユーザーではなく、ドメイン全体を委譲するので、サービスアカウントに強力な権限を渡す
- パスワード認証
- 非推奨。将来的に使えなくなる
- OAuth 2.0 クライアントID(デスクトップアプリ)
- Google Colaboratory内で認証フローが完結しない
- サービスアカウント認証
環境・前提
- 2023年1月初旬時点
- Google Workspaceアカウント上のGoogle Colaboratory
- ローカル環境:MacBook Air(2020, M1)
- pyenv(Python 3.10.10)
- Gmail APIが有効化されたGoogle Cloud Platformプロジェクト
1. サービスアカウント認証
サービスアカウント作成
GCPの「認証情報」からサービスアカウントでの認証を試します。

作成の際のオプションは不要です。

作成後、「新しい鍵を作成」からキーをダウンロードします。


Gmail APIの場合、もう一つ設定が必要です。
ドメイン全体権限の委任(Domain Wide Delegation)
Workspaceのadmin権限のあるアカウントで下記にアクセスし、作成したサービスアカウントに「ドメイン全体権限の委任」を行います。
クライアントIDには先ほどダウンロードしたjsonデータに記載のものをセットします。

これを行わないと権限不足でGmail APIを叩けませんでした。
An error occurred: ('unauthorized_client: Client is unauthorized to retrieve access tokens using this method, or client not authorized for any of the scopes requested.', {'error': 'unauthorized_client', 'error_description': 'Client is unauthorized to retrieve access tokens using this method, or client not authorized for any of the scopes requested.'})
Colabのコード作成
ダウンロードしたキーをColabでアクセスできる場所に置き、KEY_PATHにパスを指定します。ここでは、一例としてservice_account_secrets.jsonという名前でGoogle Driveに置いています。
from google.oauth2 import service_account
from googleapiclient.discovery import build
from email.mime.text import MIMEText
import base64
KEY_PATH = "/content/drive/MyDrive/gcp/service_account_secrets.json"
SENDER_ADDRESS = "Your Sender Email"
RECEIVER_ADDRESS = "Your Receiver Email"
def create_gmail_service():
creds = service_account.Credentials.from_service_account_file(KEY_PATH)
scoped_credentials = creds.with_scopes(
["https://www.googleapis.com/auth/gmail.compose"]
).with_subject(SENDER_ADDRESS)
return build("gmail", "v1", credentials=scoped_credentials)
def create_message(sender, to, subject, message_text):
"""Create a message for an email."""
message = MIMEText(message_text)
message["to"] = to
message["from"] = sender
message["subject"] = subject
return {"raw": base64.urlsafe_b64encode(message.as_bytes()).decode()}
def send_message(service, user_id, message):
"""Send an email message."""
try:
message = (
service.users().messages().send(userId=user_id, body=message).execute()
)
print(f'Message Id: {message["id"]}')
return message
except Exception as error:
print(f"An error occurred: {error}")
return None
def main():
service = create_gmail_service()
subject = "test from colab"
message_text = "hello from google colaboratory"
message = create_message(SENDER_ADDRESS, RECEIVER_ADDRESS, subject, message_text)
send_message(service, "me", message)
if __name__ == "__main__":
main()
以下、ポイントです。
Gmail関連のscope
Gmail関連のscopeは下記にまとめられています。送信したいので、scopeはhttps://www.googleapis.com/auth/gmail.composeとしました。
with_subject
create_gmail_service()内のwith_subjectが「ドメイン全体権限の委任(Domain Wide Delegation)」のために指定が必要になる部分で、どのアドレスとして振る舞うか指定します。
SENDER_ADDRESSとRECEIVER_ADDRESSを埋めて実行するとメールが送信されると思います。
問題点
Domain Wide(ドメイン全体)の名の通り、ドメイン内の任意のアドレスから送信できてしまいます。試しにSENDER_ADDRESSを同一ドメインの別アドレスにすると、送信元もその通り変わります。
「ドメイン全体の委譲」は、
Domain-wide delegation is a powerful feature that allows apps to access users' data across your organization's Google Workspace environment.
https://support.google.com/a/answer/162106?hl=en
とある通り、だいぶ強い権限です。
単一ユーザーだけに絞れないか、調べてみましたが、同じ悩みを抱えたstackoverflowはあれど、解決策はわかりませんでした。
ちなみにGoogle Groups APIsに関して、Domain Wide Delegationが不要になった記事が2020年にありました。
Previously, you had to use domain-wide delegation and admin impersonation to provide service accounts with sufficient data access. This was a cumbersome process, which could result in overly broad privileges for the service account and audit logs that were hard to interpret.
ここにもoverly broad privileges for the service accountと書かれている通り、ColabからGmailを送るためにしては権限が強すぎる感は否めません。
2. ユーザー/パスワード認証
パスワード認証でGmailを送付することもできました。が、結論から言うと、セキュリティ上の理由や将来の仕様変更リスクが大きく、使わないべきです。
- アカウント設定から「安全性の低いアプリのアクセス」をOn

https://myaccount.google.com/security
- 下記コードの設定変数を埋めて実行
import smtplib, ssl
port = 465
smtp_server = "smtp.gmail.com"
sender_email = "<入力してください>"
receiver_email = "<入力してください>"
password = "<入力してください>"
message = "<入力してください>"
context = ssl.create_default_context()
with smtplib.SMTP_SSL(smtp_server, port, context=context) as server:
server.login(sender_email, password)
server.sendmail(sender_email, receiver_email, message)
なお、Google Workspace以外でのパスワード認証は2022年5月30日にサポート終了となっており、Google Workspaceに関しても2024年9月でサポート終了のアナウンスが出ていました。
もう一つ、アプリパスワードというものを設定する方法もあるようです。
しかしながら、
アプリ パスワードは推奨されておらず、ほとんどの場合は不要です。
とドキュメントに明記されているのでこれも避けるべきでしょう。
3. OAuth 2.0 クライアントID(デスクトップアプリ)
Gmail APIを使う際はまずこの方法を採りたいところですが、Colaboratoryで送ろうとすると問題があります。
まず、クライアントタイプのうち、「ウェブアプリケーション」 はユーザー同意の後のcallbackをColab上で受け取れず、頓挫しました。

ちょっと前まではOOB(out-of-band) と言って、callbackフローの代わりにauthorization codeを手動で貼って認証する方法があったようですが、2022年に廃止されていました。
次にデスクトップアプリを選択する方法を試しました。結論から言うとローカルで認証を行い、認証キーをColabから参照する方法が筆者の妥協点となりました。
Colab上で認証する(失敗)
- GCPプロジェクトでOAuth 2.0 クライアントIDを作成(デスクトップアプリ)
- 認証情報をダウンロードし、
credentials.jsonとしてColabの直下(/content/)に設置 - Colab上でコードを実行
コード例です。公式サンプルからそのまま持ってきています。
import os.path
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
# If modifying these scopes, delete the file token.json.
SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"]
def main():
"""Shows basic usage of the Gmail API.
Lists the user's Gmail labels.
"""
creds = None
# The file token.json stores the user's access and refresh tokens, and is
# created automatically when the authorization flow completes for the first
# time.
if os.path.exists("token.json"):
creds = Credentials.from_authorized_user_file("token.json", SCOPES)
# If there are no (valid) credentials available, let the user log in.
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
"credentials.json", SCOPES
)
creds = flow.run_local_server(port=0)
# Save the credentials for the next run
with open("token.json", "w") as token:
token.write(creds.to_json())
try:
# Call the Gmail API
service = build("gmail", "v1", credentials=creds)
results = service.users().labels().list(userId="me").execute()
labels = results.get("labels", [])
if not labels:
print("No labels found.")
return
print("Labels:")
for label in labels:
print(label["name"])
except HttpError as error:
# TODO(developer) - Handle errors from gmail API.
print(f"An error occurred: {error}")
if __name__ == "__main__":
main()
しかし、OAuth2.0フローでブラウザ上で認証しようとするところで、案の定Colab環境にbrowserがなくエラーになります。
Error: could not locate runnable browser
ローカルで認証を作成 & Colabから参照
仕方ないので妥協します。
ローカルで認証し、tokenとrefresh_tokenを含むtoken.jsonを生成、Colabで取得できる場所に置いてみます。
Pythonがインストールされたローカル環境[1]で、以下のコードを実行します。
$pip install google-auth-oauthlib
from google_auth_oauthlib.flow import InstalledAppFlow
SCOPES = ["https://www.googleapis.com/auth/gmail.compose"]
def main():
flow = InstalledAppFlow.from_client_secrets_file("credentials.json", SCOPES)
creds = flow.run_local_server(port=0)
with open("token.json", "w") as token:
token.write(creds.to_json())
if __name__ == "__main__":
main()
ブラウザが開くので、メール送信元にするアカウントを選択/ログインすると、token.jsonが作成されます。

続いて作成したtoken.jsonをColabからアクセスできる場所(たとえばGoogle Driveの/content/drive/MyDrive/gcp/token.json)に置き、Colabで下記を実行します。
(TO_ADDRESS = "Your Receiver Email"を置き換えてください。)
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
from email.mime.text import MIMEText
import base64
SCOPES = ["https://www.googleapis.com/auth/gmail.compose"]
TOKEN_PATH = "/content/drive/MyDrive/gcp/token.json"
TO_ADDRESS = "Your Receiver Email"
def create_gmail_service():
creds = Credentials.from_authorized_user_file(TOKEN_PATH, SCOPES)
return build("gmail", "v1", credentials=creds)
def create_message(to, subject, message_text):
"""Create a message for an email."""
message = MIMEText(message_text)
message["to"] = to
message["subject"] = subject
return {"raw": base64.urlsafe_b64encode(message.as_bytes()).decode()}
def send_message(service, user_id, message):
"""Send an email message."""
try:
message = (
service.users().messages().send(userId=user_id, body=message).execute()
)
print(f'Message Id: {message["id"]}')
return message
except Exception as error:
print(f"An error occurred: {error}")
return None
def main():
try:
service = create_gmail_service()
except Exception as e:
print(e)
return
receiver_email = TO_ADDRESS
subject = "test from colab"
message_text = "hello from google colaboratory"
message = create_message(receiver_email, subject, message_text)
send_message(service, "me", message)
if __name__ == "__main__":
main()
これでメール送信ができ、かつ送信元をOAuth2.0で代理させたアカウントのみに縛れます。
ただ、ローカルのPython環境が必要になるのは、環境構築不要なColabの良さを消してしまっていて、残念な感じがします。
おわりに
Google Colab上でGmail APIを用いるための認証として3つの方法を試しました。
まとめると、
| 認証方法 | 😄 | 🤔 |
|---|---|---|
| サービスアカウント | Google Colaboratoryで完結して動作する | 特定のユーザーではなく、ドメイン全体を委譲するので、サービスアカウントに強力な権限が渡る |
| パスワード | 実装が容易 | 非推奨。将来的に使えなくなる |
| OAuth 2.0 クライアントID(デスクトップアプリ) | 特定のユーザーのみの権限を与えることができる | Google Colaboratoryで完結しない |
とどれも一長一短という具合でした。
理想としては、「Google Colaboratoryで完結して動作し、特定のユーザーのみの権限を与えることができる」認証方法です。これができるとすればOAuth 2.0 クライアントIDによるものだと思われます。
もうひとつ、colabtoolsのauthモジュールでもできないか模索しましたが、権限不足で上手くいきませんでした。
def create_gmail_service():
auth.authenticate_user() # auth.authenticate_user(project_id='your_project')
creds, _ = default()
scoped_creds = creds.with_scopes(SCOPES)
print(scoped_creds.scopes)
return build("gmail", "v1", credentials=scoped_creds)
['https://www.googleapis.com/auth/gmail.compose']
WARNING:googleapiclient.http:Encountered 403 Forbidden with reason "insufficientPermissions"
An error occurred: <HttpError 403 when requesting https://gmail.googleapis.com/gmail/v1/users/me/messages/send?alt=json returned "Request had insufficient authentication scopes.". Details: "[{'message': 'Insufficient Permission', 'domain': 'global', 'reason': 'insufficientPermissions'}]">
もし最適解をご存じの方がいらっしゃれば、ご教示ください...
-
厳密には認証すれば良いので、必ずしもPythonでなくても構いませんが。 ↩︎
Discussion