コンテンツにスキップ
コンソール →
公式サイト →
URL を貼り付けて AI に質問 https://keelson.dev/ja/llms.txt

メールを受け取る

アプリに専用のメールアドレスを持たせることができます。届いたメールは、Keelson がアプリのデータベースの表に 1 通 1 行で保存し、添付ファイルを Files に置きます。アプリは SQL で読むだけで、メールを受け取るコードは要りません。保存したメールを 1 通ずつ処理したい場合は、on_receive にバックグラウンドタスクの名前を書きます。

主な用途は、問い合わせの共有受信箱や、注文・請求のメールの読み取りです。独自ドメインのメールを Keelson で直接受けることはできません。Gmail・Google Workspace・Microsoft 365 から、アプリのアドレスへ転送します(アプリへ転送する)。

通常は AI エージェントに、届いたメールをどう扱うかを伝えると、keelson.yaml の設定と処理のタスクを書きます。

営業グループ(sales@)から転送される注文メールを保存して、1 通ごとに注文として登録してください。同じメールを 2 回処理しても、注文が重複しないようにしてください。

keelson.yaml に email.inbound を書いてデプロイします。受信には Managed SQLite(db.mode: libsql)が必要です。

slug: order-inbox
runtime: python-slim
command: "uvicorn main:app --host 0.0.0.0 --port $PORT"
db:
mode: libsql # 必須
email:
inbound:
enabled: true
on_receive: process-mail # 任意。tasks に宣言したタスク
tasks:
- name: process-mail
command: "python process_mail.py"
項目意味
email.inbound.enabledtrue で受信を有効にする。デプロイで受信アドレスを発行し、表 keelson_inbound_emails を作る
email.inbound.on_receive任意。tasks に宣言したタスクの名前。メールを保存するたびに、このタスクを起動する

次の場合はデプロイが拒否されます。

エラー原因
email_inbound_requires_libsqldb.mode: libsql でないのに enabled: true にした
email_on_receive_requires_enabledon_receive があるのに enabled が true でない
email_on_receive_unknown_taskon_receive の名前が tasks[].name のどれとも一致しない

on_receive を書いたアプリは、tasks があっても command と crons が無くて構いません(受信がタスクを起動するため)。添付ファイルの置き場は固定で、設定できません。

受信アドレスは、コンソールのアプリの Email 設定に表示されます。形は in+<ランダムな文字列>@inbound.keelson.run です。ランダムな部分はアプリの slug と関係が無く、アプリ名を変えてもアドレスは変わりません。

  • 作り直す:コンソールから新しいアドレスを発行できます。古いアドレスはその場で退役し、以後そのアドレス宛てのメールは拒否されます。
  • 受信をやめる:email.inbound を外した(または enabled: false にした)デプロイで、アドレスは退役し、二度と使えなくなります。まだ保存していないメールと、保存できずに保留しているメールは破棄します。保存済みの行と添付ファイルはそのまま残ります。
  • もう一度有効にする:新しいアドレスを発行します。転送の設定を新しいアドレスに直してください。

退役したアドレスは、どのアプリでも再利用しません。

1 通のメールが keelson_inbound_emails の 1 行になります。表はアプリ自身のデータベースにあるので、他の表と同じ接続で読めます。

列内容
email_id主キー。受け取ったときに決まる
schema_version行を書いたときの列の版。今は 1
received_at受け取った時刻(ISO 8601、UTC)
envelope_to配送されたアプリの受信アドレス
envelope_from配送に使われた送信元アドレス(エンベロープの送信元)
from_address差出人のアドレス。X-Original-From があればそれ、無ければ From。認証済みの値ではない(後述)
from_name差出人の表示名(同じヘッダーから)
header_from生の From ヘッダー(転送グループが書き換えた後の値)
reply_toReply-To ヘッダー
to_addressesTo のアドレスの JSON 配列(転送されたメールでは共有受信箱のアドレス)
cc_addressesCc のアドレスの JSON 配列
subject件名
message_idMessage-ID ヘッダー
in_reply_toIn-Reply-To ヘッダー
references_headerReferences ヘッダー
list_idList-Id ヘッダー
text_bodyテキストの本文(UTF-8 に変換済み)
html_bodyHTML の本文(UTF-8 に変換済み)
body_truncatedどちらかの本文が 1 MiB を超えて切られたら 1
attachments添付ファイルの JSON 配列(無ければ [])
spam_score簡易的な spam の採点。印だけ(spam)
auth_spf / auth_dkim / auth_dmarc受信サーバーが記録した SPF / DKIM / DMARC の結果

無いヘッダーは NULL です。符号化された件名やファイル名(日本語を含む)は復号して保存します。

to_addresses と cc_addresses はアドレスの配列です(表示名が無ければ name は null)。

[{"address": "[email protected]", "name": "サポート窓口"}]

attachments の各要素は次の形です。

[{"attachment_id": "att_01", "filename": "注文書.pdf", "content_type": "application/pdf",
"content_id": null, "size_bytes": 182344, "files_key": "inbound/<email_id>/att_01",
"sha256": "<hex>"}]

content_id は、HTML の本文から参照されるインライン画像に入ります。

-- 新しい順に 20 通
SELECT email_id, received_at, from_address, subject
FROM keelson_inbound_emails
ORDER BY received_at DESC
LIMIT 20;
-- 1 通の添付ファイル
SELECT json_extract(a.value, '$.filename') AS filename,
json_extract(a.value, '$.files_key') AS files_key
FROM keelson_inbound_emails e, json_each(e.attachments) a
WHERE e.email_id = ?;

添付ファイルは Files のキー inbound/<email_id>/<attachment_id> に置かれます。attachments の files_key がこのキーです。Files SDK で読みます。

import json
from keelson import files
(attachments,) = conn.execute(
"SELECT attachments FROM keelson_inbound_emails WHERE email_id = ?", (email_id,)
).fetchone()
for att in json.loads(attachments):
data = files.read(att["files_key"]) # bytes | None
import * as files from "@keelsonhq/files";
const { rows } = await db.execute({
sql: "SELECT attachments FROM keelson_inbound_emails WHERE email_id = ?",
args: [emailId],
});
for (const att of JSON.parse(rows[0].attachments)) {
const data = await files.read(att.files_key); // Uint8Array | null
}

添付ファイルは Files の書き込み上限(10 MiB)を超えることがありますが、読み取りはできます。添付ファイルはワークスペースのストレージに数えます。表の行は数えません。

  • keelson_inbound_emails は、受信を有効にしたデプロイで Keelson が作ります。移行やアプリのコードで自分で作らないでください。Keelson が作っていない同名の表が既にあると、デプロイが止まります。
  • 表は利用者のものです。アプリから読み書き・削除できます。「対応済み」「担当者」などの状態は、email_id をキーにした別の表に持ちます。
  • Keelson は行を足すだけで、既存の行の書き換えや表の作り直しはしません。
  • email_id の主キーを外す、列を消す、トリガーを付ける、Files のキー inbound/ の下に自分のファイルを置く、のいずれかをすると、新しいメールを保存できず保留になります(保存できなかったメール)。
  • データベースをバックアップから復元すると、この表もその時点の中身に戻ります。

on_receive にタスクを書くと、メールを保存するたびに Keelson がそのタスクを起動します。タスクの payload はメールの ID だけです。

{"attempt_no":1,"payload":{"email_id":"…"},"schema_version":1,"task_id":"…","task_name":"process-mail"}

タスクは email_id で keelson_inbound_emails の行を読みます。起動のされ方以外は、通常のバックグラウンドタスクと同じです(バックグラウンドタスク)。

タスクは少なくとも 1 回起動します。 同じ email_id が 2 回以上渡ることがあり、失敗した試行は再試行されます。処理済みのメールを記録し、その記録と処理の結果を 1 つのトランザクションで書きます。

import json
import os
import sys
import libsql
email_id = json.loads(sys.stdin.readline())["payload"]["email_id"]
conn = libsql.connect(
database=os.environ.get("KEELSON_DB_URL", "file:local.db"),
auth_token=os.environ.get("KEELSON_DB_AUTH_TOKEN", ""),
)
conn.execute(
"CREATE TABLE IF NOT EXISTS handled_emails (email_id TEXT PRIMARY KEY, handled_at TEXT NOT NULL)"
)
cur = conn.execute(
"INSERT INTO handled_emails (email_id, handled_at) VALUES (?, datetime('now')) "
"ON CONFLICT (email_id) DO NOTHING",
(email_id,),
)
if cur.rowcount == 0:
sys.exit(0) # 処理済み
(subject,) = conn.execute(
"SELECT subject FROM keelson_inbound_emails WHERE email_id = ?",
(email_id,),
).fetchone()
conn.execute("INSERT INTO orders (email_id, title) VALUES (?, ?)", (email_id, subject))
conn.commit() # 処理済みの記録と注文を一緒に保存する

commit の前にタスクが失敗すると何も保存されず、再試行で最初からやり直します。外部 API には email_id を冪等キーとして渡します。

  • 保存済みのメールの on_receive は、コンソールから起動し直せます。数え方は自動の起動と同じです。
  • on_receive で起動したタスクは、バックグラウンド実行の回数に数えません。実行時間は月間の合計実行時間に合算します。合計実行時間の枠を使い切っているとタスクは起動せず、コンソールにその旨が出ます(自動では再試行しません)。
  • 処理が重いと合計実行時間を使い切り、定期実行ジョブも止まります。コンソールで月間の使用量を確認してください。

from_address は X-Original-From か From のヘッダーから取ります。このヘッダーには誰でも好きなアドレスを書けます。from_address を、利用者の本人確認や、そのメールに何を許すかの判断に使わないでください。(例:「社長から来たメールなら支払いを承認する」)。表示と振り分けにだけ使います。

auth_spf・auth_dkim・auth_dmarc は受信サーバーの検査の記録です。転送で崩れることが多く、これも差出人の証明にはなりません。

独自ドメインのメールを Keelson で直接受けることはできません。今メールを受けている所で受けたまま、アプリのアドレスへ転送します。

  1. Gmail の 設定 → すべての設定を表示 → メール転送と POP/IMAP で、アプリの受信アドレスを転送先に追加します。
  2. Gmail が [email protected] から確認メールをそのアドレスへ送ります。コンソールのアプリの Email 設定の 最近届いたメール で確認コードを読みます。アプリが動いていなくても読めます。
  3. Gmail に確認コードを入力し、転送を有効にします。一部のメールだけを転送する場合は、Gmail のフィルタで転送します。

確認メールも、他のメールと同じように表に保存されます。on_receive を使っている場合は、確認メールでもタスクが起動します。

メールを受けている Google グループ(例:support@)のメンバーに、アプリの受信アドレスを追加します。グループに届いたメールの写しがアプリにも届きます。

  • アドレスを追加できない場合は、Google Workspace の管理者が、組織外のメンバーをグループに追加できるように設定する必要があります。
  • 元の差出人のドメインの DMARC ポリシーが厳しいと、Google グループは From をグループのアドレスに書き換え、元の差出人を X-Original-From に残します。from_address は X-Original-From を使うので、元の差出人が入ります。header_from には書き換え後の From、to_addresses にはグループのアドレスが入ります。

Microsoft 365 は、外部のアドレスへの自動転送を既定で止めています(送信の迷惑メールフィルターのポリシー)。転送する前に、管理者が許可する必要があります。許可していないと、転送したメールは 550 5.7.520 Access denied, Your organization does not allow external forwarding で戻ってきます。

  1. Microsoft Defender ポータルで、送信の迷惑メール対策ポリシーの自動転送をオンにします。転送するメールボックスだけに適用する独自のポリシーを作るのが安全です。
  2. そのメールボックスで、アプリのアドレスへの転送(Outlook の転送設定か受信トレイのルール)を設定します。メールフローのルールでも転送できます。

Microsoft 365 からの転送は Keelson では試していません。手順は Microsoft の文書に基づきます。

項目上限
1 通の大きさ添付ファイルを含めて 25 MiB。超えるメールは拒否される
本文text_body・html_body はそれぞれ 1 MiB で切る(body_truncated = 1)
添付ファイル25 MiB の範囲で、1 つあたりの上限は無い
月間の受信数ワークスペース単位で、全アプリの合計。下の表

受信の月間上限(ワークスペース単位・UTC の暦月)

StarterPlusTeamBusiness
受信数 / 月500 通3,000 通10,000 通30,000 通

重複を除いた後に 1 通 1 回数えます。80%・100%・150% に達すると、ワークスペースの Owner と Admin に通知します。

  • 100% を超えたら:受信と保存は続けますが、超えた分のメールでは on_receive を起動しません。コンソールでそのメールに印が付きます。プランを上げた後に、コンソールから on_receive を起動できます。
  • 150% に達したら:ワークスペースのアドレスがメールを拒否し、送信元にエラーが返ります。翌月 1 日 0 時(UTC)か、プランを変更すると受信を再開します。切り替わるまでの間に届いた数通は保存されることがあります。

表を消した、主キーを外した、トリガーで行を弾いた、inbound/ に別のファイルが既にある、などで表に書けなかったメールは、Keelson が保留します。コンソールのメールの履歴に、理由と直し方が出ます。表を直した後、コンソールから保存し直してください。

  • 保留は 7 日、アプリあたり 1,000 通までです。保留が 1,000 通に達している間は、そのアプリのアドレスへの新しいメールを拒否します。
  • 7 日を過ぎた保留のメールは「期限切れ」と記録し、メールと添付ファイルを消します。

コンソールのメールの履歴には、保存待ちのメール、メールとして読み取れなかったもの、受信をやめたために破棄したメールも出ます。

spam_score は簡易的な採点です。Keelson はこれで隔離や削除をせず、すべてのメールを保存します。転送したメールは SPF に落ちやすく、普通のメールにも点が付くことがあります。点が高いメールをどう扱うかは、アプリで決めてください。

  1. コンソールを見る:アプリの Email 設定のメールの履歴を開きます。そこにあれば Keelson は受け取っています。状態(保留とその理由)を確かめます。
  2. アドレスを確かめる:作り直す前のアドレスや、受信をやめたアプリのアドレスへのメールは拒否されます。
  3. 上限を確かめる:月間上限の 150% に達している、または保留が 1,000 通に達していると、新しいメールは拒否されます。25 MiB を超えるメールも拒否されます。
  4. 転送の設定を確かめる:Gmail の転送は、確認コードを入力するまで始まりません。Microsoft 365 は、管理者が許可しないと外部への転送を止めます。
  5. Keelson より手前の認証検査:SPF と DKIM の両方に落ちたメールや、送信元ドメインの DMARC ポリシーに落ちたメールは、Keelson に届く前に受信サービスが拒否します。このメールは Keelson に記録が残らず、送信元には通常エラーが返ります。メールの中身を変える転送でこうなることがあります。Google グループ経由の転送なら、こうした送信元のメールも届きます。

keelson dev email は、保存したメールのファイル(.eml)を、Keelson が受け取ったときと同じ形で手元のデータベースと Files に保存します。

Terminal window
export KEELSON_DB_URL=file:local.db
keelson dev email ./samples/order.eml
keelson dev email <file.eml> [--to <address>]
  • メールを Keelson と同じ規則で読み取り、KEELSON_DB_URL のデータベースの keelson_inbound_emails に 1 行 INSERT します。表が無ければ作ります(手元だけ)。
  • 添付ファイルは KEELSON_FILES_DIR(既定 ./.keelson/files)の inbound/<email_id>/<attachment_id> に書きます。手元の開発では Files SDK がここを読みます。
  • KEELSON_DB_URL は手元を指す必要があります。file: の URL か、ホストが 127.0.0.1 か localhost の http://・libsql://・ws:// の URL です。それ以外や未設定のときは、何も書かずに終了コード 2 で終わります。
  • --to は受信アドレス(envelope_to)です。既定は [email protected] です。
  • 出力には email_id・件名・添付ファイルの数と置き場が出ます。
  • email_id はメールの中身と --to から決まります。同じファイルをもう一度流すと Already stored と出て、終了コード 0 で終わります。
  • メールとして読み取れないファイルは終了コード 1 です。
  • on_receive は起動しません。keelson.yaml に宣言があれば、タスクを動かすコマンドを表示します。
Terminal window
keelson dev task run process-mail --payload '{"email_id":"…"}'

.eml ファイルは、メールソフトで元のメッセージをダウンロードして作ります(Gmail では メッセージのソースを表示 → 元のメッセージをダウンロード)。