コード及びアーキテクチャの動作無保証(Disclaimer)
本ドキュメントに掲載されているすべてのソースコード、設定手順、構成ファイルは、教育および実験目的として提供されるものであり、完全性、安全性、可用性、商用利用への適合性を含め、一切の動作保証を行いません。 実装・デプロイ・運用の結果生じたいかなる障害、データ消失、第三者サービス利用料の発生等について開発者は責任を負いません。すべてご自身の判断と検証の上でご利用ください。
パターン 1 (推奨・ドメイン代0円)
Resend Inbound + Deno Deploy 構築手順
Resendが無料提供する自動サブドメインと、Deno Deployの無料ステートレス中継サーバーを組み合わせる構成です。
1
中継ファイル(
中継ファイル(main.ts)の入手
以下のボタンから main.ts を直接PCにダウンロードするか、コードブロックからコピーしてください。
// Generic Inbound Webhook Gateway (Deno Deploy)
// (c) 2026 Hikaproj. All Rights Reserved.
import { finalizeEvent, generateSecretKey } from "https://esm.sh/nostr-tools@2.10.4/pure";
import * as nip04 from "https://esm.sh/nostr-tools@2.10.4/nip04";
import { Relay } from "https://esm.sh/nostr-tools@2.10.4/relay";
const RELAYS = ["wss://relay.damus.io", "wss://nos.lol", "wss://relay.nostr.band"];
const RESEND_API_KEY = Deno.env.get("RESEND_API_KEY") || "";
Deno.serve(async (req: Request) => {
// CORSプリフライト
if (req.method === "OPTIONS") {
return new Response(null, {
status: 204,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, GET, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
},
});
}
// フロントエンドからの死活監視(GET)対応
if (req.method === "GET") {
return new Response(JSON.stringify({ status: "ok", gateway: "Deno Deploy Relay" }), {
status: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*"
}
});
}
if (req.method !== "POST") return new Response("Not Found", { status: 404 });
try {
const raw = await req.json();
const data = raw.data || raw;
const toRaw = Array.isArray(data.to) ? data.to[0] : (data.to || "");
const match = toRaw.match(/([a-fA-F0-9]{64})/);
if (!match) return new Response("Invalid Pubkey", { status: 400 });
const recipientPubkey = match[1].toLowerCase();
let emailText = data.text || "";
let emailHtml = data.html || "";
const emailId = data.email_id || data.id;
if (emailId && RESEND_API_KEY && (!emailText && !emailHtml)) {
try {
const res = await fetch(`https://api.resend.com/emails/receiving/${emailId}`, {
headers: { "Authorization": `Bearer ${RESEND_API_KEY}` }
});
if (res.ok) {
const detail = await res.json();
emailText = detail.text || "";
emailHtml = detail.html || "";
}
} catch (_) {}
}
const payload = {
from: data.from || "unknown",
to: toRaw,
subject: data.subject || "(件名なし)",
text: emailText || "(本文なし)",
html: emailHtml || "",
receivedAt: Date.now()
};
const senderPrivKey = generateSecretKey();
const ciphertext = await nip04.encrypt(senderPrivKey, recipientPubkey, JSON.stringify(payload));
const signedEvent = finalizeEvent({
kind: 4,
created_at: Math.floor(Date.now() / 1000),
tags: [["p", recipientPubkey]],
content: ciphertext,
}, senderPrivKey);
const promises = RELAYS.map(async url => {
try {
const relay = await Relay.connect(url);
await relay.publish(signedEvent);
relay.close();
} catch (_) {}
});
await Promise.allSettled(promises);
return new Response(JSON.stringify({ success: true }), {
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*"
}
});
} catch (err: any) {
return new Response(err.message, { status: 500 });
}
});
直リンク:
./deno-relay/main.ts
2
Resend側の設定
- Resend.com の「Emails」→「Receiving」でサブドメイン(
team-xxx.resend.app)を確認。 - 「API Keys」でキー(
re_...)を発行し、Deno Deployの環境変数RESEND_API_KEYに登録。 - Resendの「Webhooks」で、Denoの公開URLをイベント
email.receivedとして登録。
パターン 2 (独自ドメイン保有者向け)
Cloudflare Email Routing + Workers
独自ドメイン宛てのメールをCloudflare Email Workerで直接捕捉し、Nostrへ暗号化中継します。
// Cloudflare Email Worker - Aurimbox Relay
// (c) 2026 Hikaproj. All Rights Reserved.
import PostalMime from "postal-mime";
import { finalizeEvent, generateSecretKey } from "nostr-tools/pure";
import * as nip04 from "nostr-tools/nip04";
import { Relay } from "nostr-tools/relay";
const RELAYS = ["wss://relay.damus.io", "wss://nos.lol", "wss://relay.nostr.band"];
export default {
async fetch(request, env, ctx) {
if (request.method === "OPTIONS") {
return new Response(null, {
status: 204,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
},
});
}
return new Response(JSON.stringify({ status: "ok", gateway: "Cloudflare Email Workers" }), {
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" }
});
},
async email(message, env, ctx) {
const recipientRaw = message.to;
const match = recipientRaw.match(/([a-fA-F0-9]{64})/);
if (!match) return;
const recipientPubkey = match[1].toLowerCase();
const rawEmail = await new Response(message.raw).arrayBuffer();
const parsed = await PostalMime.parse(rawEmail);
const payload = {
from: message.from || parsed.from?.address || "unknown",
to: recipientRaw,
subject: parsed.subject || "(No Subject)",
text: parsed.text || "",
html: parsed.html || "",
receivedAt: Date.now()
};
const senderPrivKey = generateSecretKey();
const ciphertext = await nip04.encrypt(senderPrivKey, recipientPubkey, JSON.stringify(payload));
const signedEvent = finalizeEvent({
kind: 4,
created_at: Math.floor(Date.now() / 1000),
tags: [["p", recipientPubkey]],
content: ciphertext
}, senderPrivKey);
const promises = RELAYS.map(async url => {
try {
const relay = await Relay.connect(url);
await relay.publish(signedEvent);
relay.close();
} catch (_) {}
});
ctx.waitUntil(Promise.allSettled(promises));
}
};
直リンク:
./cf-workers/worker.js
パターン 3 (完全自立・VPS運用)
VPS + Docker 自己ホスト構成ファイル
Linux VPS(ポート25開放)で直接メールを受け入れ、Nostrへ暗号化中継する独立サーバーの全構成ファイルです。
自己ホスト用ファイル一式
以下の3ファイルを同一フォルダに保存して
docker compose up -d を実行します。1. docker-compose.yml
version: '3.8'
services:
smtp-aurimbox:
build: .
restart: always
ports:
- "25:25" # メール受付用SMTPポート
- "8080:8080" # フロントエンド死活監視用HTTPポート
environment:
- NODE_ENV=production
2. Dockerfile
FROM node:20-slim
WORKDIR /app
RUN npm install smtp-server mailparser nostr-tools ws
COPY server.js ./
EXPOSE 25 8080
CMD ["node", "server.js"]
3. server.js
// Standalone SMTP Gateway & Health Check Server (Node.js)
// (c) 2026 Hikaproj. All Rights Reserved.
import http from "http";
import { SMTPServer } from "smtp-server";
import { simpleParser } from "mailparser";
import { finalizeEvent, generateSecretKey } from "nostr-tools/pure";
import * as nip04 from "nostr-tools/nip04";
import { Relay } from "nostr-tools/relay";
import WebSocket from "ws";
global.WebSocket = WebSocket;
const RELAYS = ["wss://relay.damus.io", "wss://nos.lol", "wss://relay.nostr.band"];
// 1. ヘルスチェック用 HTTP (8080)
const healthServer = http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
res.end(JSON.stringify({ status: "ok", gateway: "Self-Host SMTP Node" }));
});
healthServer.listen(8080, "0.0.0.0", () => {
console.log("Health check HTTP server is running on port 8080");
});
// 2. メール受付用 SMTP (25)
const smtpServer = new SMTPServer({
secure: false,
disabledCommands: ['AUTH'],
onData(stream, session, callback) {
simpleParser(stream, async (err, parsed) => {
if (err) return callback(err);
try {
const toAddress = session.envelope.rcptTo[0].address;
const match = toAddress.match(/([a-fA-F0-9]{64})/);
if (!match) return callback();
const recipientPubkey = match[1].toLowerCase();
const payload = {
from: session.envelope.mailFrom.address || parsed.from?.text || "unknown",
to: toAddress,
subject: parsed.subject || "(No Subject)",
text: parsed.text || "",
html: parsed.html || "",
receivedAt: Date.now()
};
const ephemeralSk = generateSecretKey();
const ciphertext = await nip04.encrypt(ephemeralSk, recipientPubkey, JSON.stringify(payload));
const signedEvent = finalizeEvent({
kind: 4,
created_at: Math.floor(Date.now() / 1000),
tags: [["p", recipientPubkey]],
content: ciphertext
}, ephemeralSk);
for (const relayUrl of RELAYS) {
try {
const relay = await Relay.connect(relayUrl);
await relay.publish(signedEvent);
relay.close();
} catch (_) {}
}
console.log(`[OK] 中継成功 -> ${recipientPubkey.slice(0, 8)}...`);
callback();
} catch (e) {
callback(e);
}
});
}
});
smtpServer.listen(25, "0.0.0.0", () => {
console.log("SMTP receiver is listening on port 25");
});