Documentation
¶
Overview ¶
Package bridge implements a bidirectional Nostr-Email bridge using the Marmot protocol (MLS-based E2E encrypted messaging) for all Nostr-side communication. It receives inbound email via SMTP and delivers it as Marmot DMs, and receives outbound email as Marmot DMs to send via SMTP.
Index ¶
- Constants
- func ComposeHandler() http.HandlerFunc
- func DecryptAttachment(ciphertext []byte, keyHex string) ([]byte, error)
- func DecryptHandler() http.HandlerFunc
- func EncryptAttachment(plaintext []byte) (ciphertext []byte, keyHex string, err error)
- func GenerateReplyLink(baseURL, replyTo, subject string) string
- func IsOutboundEmail(content string) bool
- func ZipParts(htmlBody string, attachments []bridgesmtp.Attachment) ([]byte, error)
- type BlossomUploader
- type Bridge
- type ClassifyDMResult
- type Config
- type DMCommand
- type FileSubscriptionStore
- func (s *FileSubscriptionStore) Delete(pubkeyHex string) error
- func (s *FileSubscriptionStore) Get(pubkeyHex string) (*Subscription, error)
- func (s *FileSubscriptionStore) List() ([]*Subscription, error)
- func (s *FileSubscriptionStore) Save(sub *Subscription) error
- func (s *FileSubscriptionStore) Stop()
- type IdentitySource
- type InboundProcessor
- type Invoice
- type InvoiceStatus
- type MemorySubscriptionStore
- func (s *MemorySubscriptionStore) Delete(pubkeyHex string) error
- func (s *MemorySubscriptionStore) Get(pubkeyHex string) (*Subscription, error)
- func (s *MemorySubscriptionStore) List() ([]*Subscription, error)
- func (s *MemorySubscriptionStore) Save(sub *Subscription) error
- func (s *MemorySubscriptionStore) Stop()
- type NWCRequester
- type OutboundProcessor
- type ParsedDM
- type PaymentProcessor
- func (pp *PaymentProcessor) CreateInvoice(ctx context.Context, amountSats int64) (*Invoice, error)
- func (pp *PaymentProcessor) CreateSubscriptionInvoice(ctx context.Context) (*Invoice, error)
- func (pp *PaymentProcessor) LookupInvoice(ctx context.Context, paymentHash string) (*InvoiceStatus, error)
- func (pp *PaymentProcessor) WaitForPayment(ctx context.Context, paymentHash string, pollInterval time.Duration) (*InvoiceStatus, error)
- type RateLimitConfig
- type RateLimiter
- type RelayConn
- func (rc *RelayConn) Close()
- func (rc *RelayConn) Connect(ctx context.Context) error
- func (rc *RelayConn) FetchKind0(ctx context.Context, pubkey []byte) *event.E
- func (rc *RelayConn) Publish(ctx context.Context, ev *event.E) error
- func (rc *RelayConn) Reconnect() error
- func (rc *RelayConn) Subscribe(ctx context.Context, ff *filter.S) (*WsEventStream, error)
- type Router
- type Subscription
- type SubscriptionHandler
- type SubscriptionStore
- type WsEventStream
Constants ¶
FreeInterval is the minimum time between sends for unsubscribed users.
Variables ¶
This section is empty.
Functions ¶
func ComposeHandler ¶
func ComposeHandler() http.HandlerFunc
ComposeHandler returns an HTTP handler that serves the compose form.
func DecryptAttachment ¶
DecryptAttachment decrypts data that was encrypted by EncryptAttachment. keyHex is the hex-encoded key from the fragment URL.
func DecryptHandler ¶
func DecryptHandler() http.HandlerFunc
DecryptHandler returns an HTTP handler that serves the decrypt page.
func EncryptAttachment ¶
EncryptAttachment encrypts data with a random ChaCha20-Poly1305 key. Returns (ciphertext, keyHex). The key is hex-encoded for use in fragment URLs.
func GenerateReplyLink ¶
GenerateReplyLink creates a compose form URL pre-populated with reply fields.
func IsOutboundEmail ¶
IsOutboundEmail returns true if the DM content looks like an outbound email (starts with recognized headers like To:, Subject:, etc.)
func ZipParts ¶
func ZipParts(htmlBody string, attachments []bridgesmtp.Attachment) ([]byte, error)
ZipParts bundles non-plaintext MIME parts (HTML + attachments) into a flat zip archive. The text/plain body is excluded — it goes directly into the DM content.
Types ¶
type BlossomUploader ¶
BlossomUploader uploads encrypted data to a Blossom server and returns the URL.
type Bridge ¶
type Bridge struct {
// contains filtered or unexported fields
}
Bridge is the Nostr-Email bridge. It manages identity, relay connection, Marmot DM handling, and SMTP transport.
func New ¶
New creates a new Bridge. The dbGetter is optional - pass nil for standalone mode (the bridge will fall back to file-based identity).
func (*Bridge) IdentitySource ¶
func (b *Bridge) IdentitySource() IdentitySource
IdentitySource returns how the identity was resolved.
func (*Bridge) Signer ¶
Signer returns the bridge's identity signer.
func (*Bridge) Start ¶
Start initializes the bridge: resolves identity, connects to relay, and begins listening for events.
type ClassifyDMResult ¶
type ClassifyDMResult struct {
Command DMCommand
Alias string // Optional alias from "subscribe <alias>"
}
ClassifyDMResult holds the classified command and any extracted alias.
func ClassifyDMFull ¶
func ClassifyDMFull(content string) ClassifyDMResult
ClassifyDMFull returns the classified command along with any extracted alias.
type Config ¶
type Config struct {
// Domain is the email domain (e.g., "relay.example.com").
// Users get addresses like npub1abc...@relay.example.com.
Domain string
// NSEC is the bridge identity secret key in nsec or hex format.
// If empty, the bridge attempts to read it from the database
// (monolithic mode) or from a file (standalone mode).
NSEC string
// RelayURL is the WebSocket URL of the relay to connect to.
// Used in standalone mode. In monolithic mode this can be empty
// (events are routed in-process).
RelayURL string
// PublicRelayURL is the public wss:// URL advertised in kind 10002
// and kind 10050 relay list events. Separate from RelayURL because
// the bridge may connect via ws://localhost internally.
PublicRelayURL string
// SMTPPort is the port the SMTP server listens on.
SMTPPort int
// SMTPHost is the address the SMTP server binds to.
SMTPHost string
// DataDir is where the bridge persists its state (group state,
// subscriptions, identity file for standalone mode).
DataDir string
// DKIMKeyPath is the path to the DKIM private key PEM file.
DKIMKeyPath string
// DKIMSelector is the DKIM selector for DNS TXT lookup.
DKIMSelector string
// NWCURI is the NWC connection string for subscription payments.
NWCURI string
// MonthlyPriceSats is the price in sats for one month subscription.
MonthlyPriceSats int64
// ComposeURL is the public URL of the compose form page.
ComposeURL string
// SMTPRelayHost is the smarthost for outbound email delivery
// (e.g., "smtp.migadu.com"). If empty, direct MX delivery is used.
SMTPRelayHost string
// SMTPRelayPort is the smarthost port (typically 587 for STARTTLS).
SMTPRelayPort int
// SMTPRelayUsername is the SMTP AUTH username for the smarthost.
SMTPRelayUsername string
// SMTPRelayPassword is the SMTP AUTH password for the smarthost.
SMTPRelayPassword string
// ACLGRPCServer is the gRPC address of the ACL server.
// When set, the bridge uses ACL-backed subscriptions instead of file store.
ACLGRPCServer string
// AliasPriceSats is the monthly price in sats for an alias email address.
// Must be >= MonthlyPriceSats. If zero, defaults to 2x MonthlyPriceSats.
AliasPriceSats int64
// ProfilePath is the path to a profile template file (email-header format).
// If the file exists, the bridge publishes a kind 0 metadata event on startup.
// Default: $DataDir/profile.txt
ProfilePath string
// SMTPMXPort overrides the port for direct MX delivery.
// 0 (default) tries 2525 then falls back to 25.
SMTPMXPort int
}
Config holds the bridge configuration. Constructed by callers from environment values (see app/config/config.go GetBridgeConfigValues).
type FileSubscriptionStore ¶
type FileSubscriptionStore struct {
// contains filtered or unexported fields
}
FileSubscriptionStore persists subscriptions as a JSON file.
func NewFileSubscriptionStore ¶
func NewFileSubscriptionStore(dataDir string) (*FileSubscriptionStore, error)
NewFileSubscriptionStore creates a subscription store backed by a JSON file.
func (*FileSubscriptionStore) Delete ¶
func (s *FileSubscriptionStore) Delete(pubkeyHex string) error
func (*FileSubscriptionStore) Get ¶
func (s *FileSubscriptionStore) Get(pubkeyHex string) (*Subscription, error)
func (*FileSubscriptionStore) List ¶
func (s *FileSubscriptionStore) List() ([]*Subscription, error)
func (*FileSubscriptionStore) Save ¶
func (s *FileSubscriptionStore) Save(sub *Subscription) error
func (*FileSubscriptionStore) Stop ¶
func (s *FileSubscriptionStore) Stop()
Stop shuts down the actor goroutine and waits for it to exit.
type IdentitySource ¶
type IdentitySource int
IdentitySource describes where the bridge identity came from.
const ( IdentityFromConfig IdentitySource = iota // NSEC provided via env/config IdentityFromDB // Read from relay database IdentityFromFile // Read from or generated to file )
func ResolveIdentity ¶
func ResolveIdentity(nsecConfig string, dbGetter func() ([]byte, error), dataDir string) (signer.I, IdentitySource, error)
ResolveIdentity resolves the bridge signer using a three-tier strategy:
- Config NSEC (env var ORLY_BRIDGE_NSEC) — highest priority
- Database getter (monolithic mode — reads relay identity)
- File fallback (standalone mode — reads or generates bridge.nsec)
The dbGetter parameter is a function that returns the relay identity secret key from the database. Pass nil in standalone mode.
type InboundProcessor ¶
type InboundProcessor struct {
// contains filtered or unexported fields
}
InboundProcessor handles converting inbound emails to DMs.
func NewInboundProcessor ¶
func NewInboundProcessor( blossom BlossomUploader, composeURL string, sendDM func(pubkeyHex string, content string) error, ) *InboundProcessor
NewInboundProcessor creates an inbound email processor.
func (*InboundProcessor) ProcessInbound ¶
func (ip *InboundProcessor) ProcessInbound(email *bridgesmtp.InboundEmail, recipientPubkeyHex string) error
ProcessInbound converts an inbound email to a DM and sends it to the Nostr recipient. The recipientPubkeyHex is resolved from the local part of the recipient email address (npub or hex).
type Invoice ¶
type Invoice struct {
Bolt11 string `json:"invoice"`
PaymentHash string `json:"payment_hash"`
Amount int64 `json:"amount"` // millisatoshis
Description string `json:"description"`
CreatedAt int64 `json:"created_at"`
ExpiresAt int64 `json:"expires_at"`
}
Invoice represents a Lightning invoice returned by the wallet.
type InvoiceStatus ¶
type InvoiceStatus struct {
Bolt11 string `json:"invoice"`
PaymentHash string `json:"payment_hash"`
Amount int64 `json:"amount"`
Preimage string `json:"preimage,omitempty"`
SettledAt int64 `json:"settled_at,omitempty"`
IsPaid bool `json:"-"`
}
InvoiceStatus represents the status of a looked-up invoice.
type MemorySubscriptionStore ¶
type MemorySubscriptionStore struct {
// contains filtered or unexported fields
}
MemorySubscriptionStore is an in-memory subscription store for testing.
func NewMemorySubscriptionStore ¶
func NewMemorySubscriptionStore() *MemorySubscriptionStore
NewMemorySubscriptionStore creates a new in-memory subscription store.
func (*MemorySubscriptionStore) Delete ¶
func (s *MemorySubscriptionStore) Delete(pubkeyHex string) error
func (*MemorySubscriptionStore) Get ¶
func (s *MemorySubscriptionStore) Get(pubkeyHex string) (*Subscription, error)
func (*MemorySubscriptionStore) List ¶
func (s *MemorySubscriptionStore) List() ([]*Subscription, error)
func (*MemorySubscriptionStore) Save ¶
func (s *MemorySubscriptionStore) Save(sub *Subscription) error
func (*MemorySubscriptionStore) Stop ¶
func (s *MemorySubscriptionStore) Stop()
Stop shuts down the actor goroutine and waits for it to exit.
type NWCRequester ¶
type NWCRequester interface {
Request(ctx context.Context, method string, params, result any) error
}
NWCRequester abstracts the NWC client's Request method for testing.
type OutboundProcessor ¶
type OutboundProcessor struct {
// contains filtered or unexported fields
}
OutboundProcessor handles converting DMs to outbound emails.
func NewOutboundProcessor ¶
func NewOutboundProcessor( smtpClient *bridgesmtp.Client, rateLimiter *RateLimiter, subHandler *SubscriptionHandler, domain string, sendDM func(pubkeyHex string, content string) error, aclClient *aclgrpc.Client, ) *OutboundProcessor
NewOutboundProcessor creates an outbound DM-to-email processor.
func (*OutboundProcessor) ProcessOutbound ¶
func (op *OutboundProcessor) ProcessOutbound(senderPubkeyHex, content string) error
ProcessOutbound parses a DM as an outbound email and sends it. senderPubkeyHex is the Nostr pubkey of the DM author.
type ParsedDM ¶
type ParsedDM struct {
// To is the list of recipient email addresses.
To []string
// Cc is the list of CC email addresses.
Cc []string
// Bcc is the list of BCC email addresses.
Bcc []string
// Subject is the email subject line.
Subject string
// Attachments is the list of Blossom fragment-key URLs.
Attachments []string
// Body is the email body text (everything after the blank line separator).
Body string
}
ParsedDM represents a parsed outbound email DM from a user.
func ParseDMContent ¶
ParseDMContent parses a DM message in RFC 822-style format into structured email fields. The format is:
To: alice@example.com, bob@example.com Cc: carol@example.com Subject: Hello from Nostr Attachment: https://blossom.example/abc123#key Message body starts here.
Headers are terminated by the first blank line. Everything after is the body. No From: header — the bridge derives the sender from the Nostr event pubkey.
type PaymentProcessor ¶
type PaymentProcessor struct {
// contains filtered or unexported fields
}
PaymentProcessor wraps a NWC client for bridge subscription payments.
func NewPaymentProcessor ¶
func NewPaymentProcessor(nwcURI string, monthlyPriceSats int64) (*PaymentProcessor, error)
NewPaymentProcessor creates a payment processor with the given NWC URI and monthly price in satoshis.
func NewPaymentProcessorWithClient ¶
func NewPaymentProcessorWithClient(client NWCRequester, monthlyPriceSats int64) *PaymentProcessor
NewPaymentProcessorWithClient creates a payment processor with a provided NWCRequester. This is useful for testing or custom NWC implementations.
func (*PaymentProcessor) CreateInvoice ¶
CreateInvoice creates a Lightning invoice for the given amount in satoshis.
func (*PaymentProcessor) CreateSubscriptionInvoice ¶
func (pp *PaymentProcessor) CreateSubscriptionInvoice(ctx context.Context) (*Invoice, error)
CreateSubscriptionInvoice creates a Lightning invoice for a one-month bridge subscription at the default price.
func (*PaymentProcessor) LookupInvoice ¶
func (pp *PaymentProcessor) LookupInvoice(ctx context.Context, paymentHash string) (*InvoiceStatus, error)
LookupInvoice checks the payment status of an invoice by its payment hash.
func (*PaymentProcessor) WaitForPayment ¶
func (pp *PaymentProcessor) WaitForPayment(ctx context.Context, paymentHash string, pollInterval time.Duration) (*InvoiceStatus, error)
WaitForPayment polls the invoice until paid or the context is cancelled. Returns the settled invoice status on success.
type RateLimitConfig ¶
type RateLimitConfig struct {
PerUserPerHour int // Max emails per user per hour (default: 10)
PerUserPerDay int // Max emails per user per day (default: 50)
GlobalPerHour int // Max emails globally per hour (default: 100)
GlobalPerDay int // Max emails globally per day (default: 500)
MinInterval time.Duration // Min time between sends per user (default: 30s)
}
RateLimitConfig holds rate limit configuration values.
func DefaultRateLimitConfig ¶
func DefaultRateLimitConfig() RateLimitConfig
DefaultRateLimitConfig returns sensible defaults per the spec.
type RateLimiter ¶
RateLimiter tracks outbound email sending rates using sliding windows. All mutable state is owned by the actor goroutine.
func NewRateLimiter ¶
func NewRateLimiter(cfg RateLimitConfig) *RateLimiter
NewRateLimiter creates a rate limiter with the given config.
func (*RateLimiter) Check ¶
func (rl *RateLimiter) Check(pubkeyHex string) error
Check returns nil if the user is allowed to send, or an error describing when they can retry.
func (*RateLimiter) CheckFree ¶
func (rl *RateLimiter) CheckFree(pubkeyHex string) error
CheckFree applies the free-tier rate limit: 1 email per 5 minutes.
func (*RateLimiter) Record ¶
func (rl *RateLimiter) Record(pubkeyHex string)
Record records a send event for rate limiting purposes. Call this after a successful send.
func (*RateLimiter) Shutdown ¶ added in v0.65.60
func (rl *RateLimiter) Shutdown()
Shutdown stops the actor goroutine and waits for it to exit.
type RelayConn ¶
type RelayConn struct {
// contains filtered or unexported fields
}
RelayConn wraps a WebSocket relay connection with auto-reconnect. It satisfies the RelayConnection interface used by the bridge's Marmot client for standalone mode (connecting to an external relay).
func NewRelayConn ¶
NewRelayConn creates a new relay connection wrapper. The signer is used for NIP-42 authentication when the relay requires it.
func (*RelayConn) Connect ¶
Connect establishes the WebSocket connection to the relay and pre-authenticates via NIP-42 so that subscriptions have proper access. In monolithic mode, the relay may not be listening yet, so Connect retries with exponential backoff for up to 30 seconds.
func (*RelayConn) FetchKind0 ¶
FetchKind0 fetches the latest kind 0 profile event for a pubkey. Returns nil if not found or on error.
func (*RelayConn) Publish ¶
Publish sends an event to the relay. If the relay responds with auth-required, the bridge authenticates via NIP-42 and retries once.
func (*RelayConn) Reconnect ¶
Reconnect attempts to reconnect with exponential backoff.
type Router ¶
type Router struct {
// contains filtered or unexported fields
}
Router dispatches incoming DMs to the appropriate handler.
func NewRouter ¶
func NewRouter( subHandler *SubscriptionHandler, outbound *OutboundProcessor, sendDM func(pubkeyHex string, content string) error, ) *Router
NewRouter creates a DM router.
func (*Router) RouteDM ¶
RouteDM processes an incoming DM and routes it to the right handler.
type Subscription ¶
type Subscription struct {
// PubkeyHex is the subscriber's 32-byte pubkey in hex.
PubkeyHex string `json:"pubkey"`
// ExpiresAt is the subscription expiration time.
ExpiresAt time.Time `json:"expires_at"`
// CreatedAt is when the subscription was created.
CreatedAt time.Time `json:"created_at"`
// InvoiceHash is the payment hash of the last paid invoice.
InvoiceHash string `json:"invoice_hash,omitempty"`
}
Subscription represents a user's bridge subscription.
func (*Subscription) IsActive ¶
func (s *Subscription) IsActive() bool
IsActive returns true if the subscription has not expired.
type SubscriptionHandler ¶
type SubscriptionHandler struct {
// contains filtered or unexported fields
}
SubscriptionHandler manages the subscription flow: user sends "subscribe" → create invoice → poll for payment → activate → confirm.
func NewSubscriptionHandler ¶
func NewSubscriptionHandler( store SubscriptionStore, payments *PaymentProcessor, sendDM func(pubkeyHex string, content string) error, priceSats int64, aclClient *aclgrpc.Client, aliasPriceSats int64, domain string, ) *SubscriptionHandler
NewSubscriptionHandler creates a handler for subscription DM commands. sendDM is a callback that sends a DM reply to the user.
func (*SubscriptionHandler) HandleStatus ¶
func (sh *SubscriptionHandler) HandleStatus(pubkeyHex string)
HandleStatus replies with the user's subscription info.
func (*SubscriptionHandler) HandleSubscribe ¶
func (sh *SubscriptionHandler) HandleSubscribe(ctx context.Context, pubkeyHex, alias string)
HandleSubscribe processes a "subscribe" or "subscribe <alias>" command. Plain subscribe is free and instant. Alias subscribe requires payment.
func (*SubscriptionHandler) IsSubscribed ¶
func (sh *SubscriptionHandler) IsSubscribed(pubkeyHex string) bool
IsSubscribed checks whether a user has an active subscription.
type SubscriptionStore ¶
type SubscriptionStore interface {
Save(sub *Subscription) error
Get(pubkeyHex string) (*Subscription, error)
List() ([]*Subscription, error)
Delete(pubkeyHex string) error
}
SubscriptionStore persists and queries subscriptions.
type WsEventStream ¶
type WsEventStream struct {
// contains filtered or unexported fields
}
WsEventStream wraps a ws.Subscription to deliver events.
func (*WsEventStream) Events ¶
func (s *WsEventStream) Events() <-chan *event.E
Events returns the channel of events.
Source Files
¶
- attachments.go
- bridge.go
- config.go
- identity.go
- inbound.go
- mls.go
- outbound.go
- parser.go
- payment.go
- profile.go
- ratelimit.go
- relay.go
- router.go
- serve.go
- subscription.go
- subscription_handler.go
- zip.go