アプリに戻る | Aurimbox Infrastructure Docs
© 2026 Hikaproj. All Rights Reserved.
コード及びアーキテクチャの動作無保証(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 });
  }
});
2

Resend側の設定

  1. Resend.com の「Emails」→「Receiving」でサブドメイン(team-xxx.resend.app)を確認。
  2. 「API Keys」でキー(re_...)を発行し、Deno Deployの環境変数 RESEND_API_KEY に登録。
  3. 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));
  }
};
パターン 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");
});