メールを受け取る
アプリに専用のメールアドレスを持たせることができます。届いたメールは、Keelson がアプリのデータベースの表に 1 通 1 行で保存し、添付ファイルを Files に置きます。アプリは SQL で読むだけで、メールを受け取るコードは要りません。保存したメールを 1 通ずつ処理したい場合は、on_receive にバックグラウンドタスクの名前を書きます。
主な用途は、問い合わせの共有受信箱や、注文・請求のメールの読み取りです。独自ドメインのメールを Keelson で直接受けることはできません。Gmail・Google Workspace・Microsoft 365 から、アプリのアドレスへ転送します(アプリへ転送する)。
通常は AI エージェントに、届いたメールをどう扱うかを伝えると、keelson.yaml の設定と処理のタスクを書きます。
営業グループ(sales@)から転送される注文メールを保存して、1 通ごとに注文として登録してください。同じメールを 2 回処理しても、注文が重複しないようにしてください。
受信を有効にする
Section titled “受信を有効にする”keelson.yaml に email.inbound を書いてデプロイします。受信には Managed SQLite(db.mode: libsql)が必要です。
slug: order-inboxruntime: python-slimcommand: "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.enabled | true で受信を有効にする。デプロイで受信アドレスを発行し、表 keelson_inbound_emails を作る |
email.inbound.on_receive | 任意。tasks に宣言したタスクの名前。メールを保存するたびに、このタスクを起動する |
次の場合はデプロイが拒否されます。
| エラー | 原因 |
|---|---|
email_inbound_requires_libsql | db.mode: libsql でないのに enabled: true にした |
email_on_receive_requires_enabled | on_receive があるのに enabled が true でない |
email_on_receive_unknown_task | on_receive の名前が tasks[].name のどれとも一致しない |
on_receive を書いたアプリは、tasks があっても command と crons が無くて構いません(受信がタスクを起動するため)。添付ファイルの置き場は固定で、設定できません。
受信アドレス
Section titled “受信アドレス”受信アドレスは、コンソールのアプリの Email 設定に表示されます。形は in+<ランダムな文字列>@inbound.keelson.run です。ランダムな部分はアプリの slug と関係が無く、アプリ名を変えてもアドレスは変わりません。
- 作り直す:コンソールから新しいアドレスを発行できます。古いアドレスはその場で退役し、以後そのアドレス宛てのメールは拒否されます。
- 受信をやめる:
email.inboundを外した(またはenabled: falseにした)デプロイで、アドレスは退役し、二度と使えなくなります。まだ保存していないメールと、保存できずに保留しているメールは破棄します。保存済みの行と添付ファイルはそのまま残ります。 - もう一度有効にする:新しいアドレスを発行します。転送の設定を新しいアドレスに直してください。
退役したアドレスは、どのアプリでも再利用しません。
保存のされ方
Section titled “保存のされ方”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_to | Reply-To ヘッダー |
to_addresses | To のアドレスの JSON 配列(転送されたメールでは共有受信箱のアドレス) |
cc_addresses | Cc のアドレスの JSON 配列 |
subject | 件名 |
message_id | Message-ID ヘッダー |
in_reply_to | In-Reply-To ヘッダー |
references_header | References ヘッダー |
list_id | List-Id ヘッダー |
text_body | テキストの本文(UTF-8 に変換済み) |
html_body | HTML の本文(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)。
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 の本文から参照されるインライン画像に入ります。
SQL で読む
Section titled “SQL で読む”-- 新しい順に 20 通SELECT email_id, received_at, from_address, subjectFROM keelson_inbound_emailsORDER BY received_at DESCLIMIT 20;
-- 1 通の添付ファイルSELECT json_extract(a.value, '$.filename') AS filename, json_extract(a.value, '$.files_key') AS files_keyFROM keelson_inbound_emails e, json_each(e.attachments) aWHERE e.email_id = ?;添付ファイルを読む
Section titled “添付ファイルを読む”添付ファイルは Files のキー inbound/<email_id>/<attachment_id> に置かれます。attachments の files_key がこのキーです。Files SDK で読みます。
import jsonfrom 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 | Noneimport * 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/の下に自分のファイルを置く、のいずれかをすると、新しいメールを保存できず保留になります(保存できなかったメール)。- データベースをバックアップから復元すると、この表もその時点の中身に戻ります。
1 通ずつ処理する(on_receive)
Section titled “1 通ずつ処理する(on_receive)”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 jsonimport osimport 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で起動したタスクは、バックグラウンド実行の回数に数えません。実行時間は月間の合計実行時間に合算します。合計実行時間の枠を使い切っているとタスクは起動せず、コンソールにその旨が出ます(自動では再試行しません)。- 処理が重いと合計実行時間を使い切り、定期実行ジョブも止まります。コンソールで月間の使用量を確認してください。
差出人は認証されていない
Section titled “差出人は認証されていない”from_address は X-Original-From か From のヘッダーから取ります。このヘッダーには誰でも好きなアドレスを書けます。from_address を、利用者の本人確認や、そのメールに何を許すかの判断に使わないでください。(例:「社長から来たメールなら支払いを承認する」)。表示と振り分けにだけ使います。
auth_spf・auth_dkim・auth_dmarc は受信サーバーの検査の記録です。転送で崩れることが多く、これも差出人の証明にはなりません。
アプリへ転送する
Section titled “アプリへ転送する”独自ドメインのメールを Keelson で直接受けることはできません。今メールを受けている所で受けたまま、アプリのアドレスへ転送します。
- Gmail の 設定 → すべての設定を表示 → メール転送と POP/IMAP で、アプリの受信アドレスを転送先に追加します。
- Gmail が
[email protected]から確認メールをそのアドレスへ送ります。コンソールのアプリの Email 設定の 最近届いたメール で確認コードを読みます。アプリが動いていなくても読めます。 - Gmail に確認コードを入力し、転送を有効にします。一部のメールだけを転送する場合は、Gmail のフィルタで転送します。
確認メールも、他のメールと同じように表に保存されます。on_receive を使っている場合は、確認メールでもタスクが起動します。
Google Workspace(グループ)
Section titled “Google Workspace(グループ)”メールを受けている Google グループ(例:support@)のメンバーに、アプリの受信アドレスを追加します。グループに届いたメールの写しがアプリにも届きます。
- アドレスを追加できない場合は、Google Workspace の管理者が、組織外のメンバーをグループに追加できるように設定する必要があります。
- 元の差出人のドメインの DMARC ポリシーが厳しいと、Google グループは
Fromをグループのアドレスに書き換え、元の差出人をX-Original-Fromに残します。from_addressはX-Original-Fromを使うので、元の差出人が入ります。header_fromには書き換え後のFrom、to_addressesにはグループのアドレスが入ります。
Microsoft 365
Section titled “Microsoft 365”Microsoft 365 は、外部のアドレスへの自動転送を既定で止めています(送信の迷惑メールフィルターのポリシー)。転送する前に、管理者が許可する必要があります。許可していないと、転送したメールは 550 5.7.520 Access denied, Your organization does not allow external forwarding で戻ってきます。
- Microsoft Defender ポータルで、送信の迷惑メール対策ポリシーの自動転送をオンにします。転送するメールボックスだけに適用する独自のポリシーを作るのが安全です。
- そのメールボックスで、アプリのアドレスへの転送(Outlook の転送設定か受信トレイのルール)を設定します。メールフローのルールでも転送できます。
Microsoft 365 からの転送は Keelson では試していません。手順は Microsoft の文書に基づきます。
| 項目 | 上限 |
|---|---|
| 1 通の大きさ | 添付ファイルを含めて 25 MiB。超えるメールは拒否される |
| 本文 | text_body・html_body はそれぞれ 1 MiB で切る(body_truncated = 1) |
| 添付ファイル | 25 MiB の範囲で、1 つあたりの上限は無い |
| 月間の受信数 | ワークスペース単位で、全アプリの合計。下の表 |
受信の月間上限(ワークスペース単位・UTC の暦月)
| Starter | Plus | Team | Business | |
|---|---|---|---|---|
| 受信数 / 月 | 500 通 | 3,000 通 | 10,000 通 | 30,000 通 |
重複を除いた後に 1 通 1 回数えます。80%・100%・150% に達すると、ワークスペースの Owner と Admin に通知します。
- 100% を超えたら:受信と保存は続けますが、超えた分のメールでは
on_receiveを起動しません。コンソールでそのメールに印が付きます。プランを上げた後に、コンソールからon_receiveを起動できます。 - 150% に達したら:ワークスペースのアドレスがメールを拒否し、送信元にエラーが返ります。翌月 1 日 0 時(UTC)か、プランを変更すると受信を再開します。切り替わるまでの間に届いた数通は保存されることがあります。
保存できなかったメール
Section titled “保存できなかったメール”表を消した、主キーを外した、トリガーで行を弾いた、inbound/ に別のファイルが既にある、などで表に書けなかったメールは、Keelson が保留します。コンソールのメールの履歴に、理由と直し方が出ます。表を直した後、コンソールから保存し直してください。
- 保留は 7 日、アプリあたり 1,000 通までです。保留が 1,000 通に達している間は、そのアプリのアドレスへの新しいメールを拒否します。
- 7 日を過ぎた保留のメールは「期限切れ」と記録し、メールと添付ファイルを消します。
コンソールのメールの履歴には、保存待ちのメール、メールとして読み取れなかったもの、受信をやめたために破棄したメールも出ます。
spam_score は簡易的な採点です。Keelson はこれで隔離や削除をせず、すべてのメールを保存します。転送したメールは SPF に落ちやすく、普通のメールにも点が付くことがあります。点が高いメールをどう扱うかは、アプリで決めてください。
メールが届かないとき
Section titled “メールが届かないとき”- コンソールを見る:アプリの Email 設定のメールの履歴を開きます。そこにあれば Keelson は受け取っています。状態(保留とその理由)を確かめます。
- アドレスを確かめる:作り直す前のアドレスや、受信をやめたアプリのアドレスへのメールは拒否されます。
- 上限を確かめる:月間上限の 150% に達している、または保留が 1,000 通に達していると、新しいメールは拒否されます。25 MiB を超えるメールも拒否されます。
- 転送の設定を確かめる:Gmail の転送は、確認コードを入力するまで始まりません。Microsoft 365 は、管理者が許可しないと外部への転送を止めます。
- Keelson より手前の認証検査:SPF と DKIM の両方に落ちたメールや、送信元ドメインの DMARC ポリシーに落ちたメールは、Keelson に届く前に受信サービスが拒否します。このメールは Keelson に記録が残らず、送信元には通常エラーが返ります。メールの中身を変える転送でこうなることがあります。Google グループ経由の転送なら、こうした送信元のメールも届きます。
keelson dev email は、保存したメールのファイル(.eml)を、Keelson が受け取ったときと同じ形で手元のデータベースと Files に保存します。
export KEELSON_DB_URL=file:local.dbkeelson dev email ./samples/order.emlkeelson 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に宣言があれば、タスクを動かすコマンドを表示します。
keelson dev task run process-mail --payload '{"email_id":"…"}'.eml ファイルは、メールソフトで元のメッセージをダウンロードして作ります(Gmail では メッセージのソースを表示 → 元のメッセージをダウンロード)。