postbox

package module
v0.2.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 21, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

README

ujex-postbox-go

Go SDK for Ujex Postbox — email for AI agents.

go get github.com/axysar/ujex-go

30-second demo

package main

import (
	"context"
	"fmt"
	"os"

	"github.com/axysar/ujex-go"
)

func main() {
	client := postbox.NewClient(postbox.Options{
		DeviceKey: os.Getenv("UJEX_DEVICE_KEY"),
		AgentID:   os.Getenv("UJEX_AGENT_ID"),
	})

	resp, err := client.Send(context.Background(), postbox.SendParams{
		To:           []string{"alice@vendor.com"},
		Subject:      "Re: invoice",
		Body:         "On it — sending by Friday.",
		SessionID:    "sess_run_20260618_001",
		RequireHuman: true,
	})
	if err != nil {
		panic(err)
	}
	fmt.Println(resp.ID, resp.Status)

	// Plus-address a per-task inbox for free
	addr, _ := postbox.PlusAddress("agent-hello@in.ujex.dev", "proj-42-abc")
	fmt.Println(addr) // agent-hello+proj-42-abc@in.ujex.dev
}

API

Method Purpose
client.Send(ctx, SendParams) Send an email. RequireHuman: true returns waiting_approval; dispatch happens after app/dashboard approval.
client.ListMessages(ctx, ListMessagesParams) List inbound / outbound messages.
client.ReadMessage(ctx, agentID, messageID) Full message body + auth verdicts + pi_score.
client.ListInboxes(ctx, agentID) Provisioned mailboxes.
postbox.PlusAddress(baseInbox, taskID) Build base+task@domain.
postbox.ParsePlusAddress(addr) Split into (base, taskID, domain, ok).
postbox.VerifySignature(rawBody, signature, secret) Timing-safe HMAC-SHA256 check on inbound webhooks.

Generate one stable SessionID per agent run and pass it to every outbound send. Ujex uses it for prompt-injection poisoning, exfiltration checks, and budget preflight.

Error classification

if err != nil {
	switch {
	case postbox.IsQuotaExceeded(err):
		log.Println("out of budget for the month")
	case postbox.IsRateLimited(err):
		log.Println("slow down — backoff")
	case postbox.IsAuth(err):
		log.Println("device key invalid")
	default:
		log.Println(err)
	}
}

Retries

Automatic retries on 429 and 5xx with exponential backoff. Respects Retry-After. Tune via Options{MaxRetries: 5}. Cancellable via the context you pass.

Webhook verification

ok := postbox.VerifySignature(rawBody, r.Header.Get("x-ujex-signature"), os.Getenv("POSTBOX_INGEST_SECRET"))
if !ok {
	http.Error(w, "unauthorized", 401)
	return
}

Core loop

Use Go for the Postbox edge of the golden path: send/receive agent mail, request human approval with RequireHuman: true, and inspect auth or prompt-injection risk signals on inbound messages. Memory and owner-scoped Audit are available through the dashboard, CLI, and TypeScript client while the Go SDK remains Postbox-focused.

Free tier

Free: 100 sends/month plus approvals, audit, and Memory, no card. ujex.dev.

License

Apache-2.0

Documentation

Overview

Package postbox is the Go SDK for Ujex Postbox — email for AI agents.

Quickstart:

client := postbox.NewClient(postbox.Options{
    DeviceKey: os.Getenv("UJEX_DEVICE_KEY"),
    AgentID:   os.Getenv("UJEX_AGENT_ID"),
})
resp, err := client.Send(ctx, postbox.SendParams{
    To:      []string{"alice@vendor.com"},
    Subject: "Re: quote",
    Body:    "on it.",
    SessionID: "sess_run_20260618_001",
})

Index

Constants

View Source
const (
	// DefaultBaseURL is the production Cloud Functions root.
	DefaultBaseURL = "https://us-central1-axy-ujex.cloudfunctions.net"

	// DefaultTimeout is the per-request timeout.
	DefaultTimeout = 30 * time.Second

	// DefaultMaxRetries is the number of automatic retries on 429 / 5xx.
	DefaultMaxRetries = 3

	// DefaultDomain is the inbound mail domain used by plus-address helpers.
	DefaultDomain = "in.ujex.dev"

	// DefaultFirebaseAPIKey exchanges session custom tokens for Firebase ID tokens.
	DefaultFirebaseAPIKey = "AIzaSyCWJzLBaXapTm9TAqQMmtNWT4GxcPO5H9M"
)

Variables

This section is empty.

Functions

func IsAuth

func IsAuth(err error) bool

IsAuth reports auth failures (401 / 403).

func IsNotFound

func IsNotFound(err error) bool

IsNotFound reports HTTP 404.

func IsQuotaExceeded

func IsQuotaExceeded(err error) bool

IsQuotaExceeded reports HTTP 402.

func IsRateLimited

func IsRateLimited(err error) bool

IsRateLimited reports HTTP 429.

func ParsePlusAddress

func ParsePlusAddress(address string) (base, taskID, domain string, ok bool)

ParsePlusAddress returns (base, taskID, domain). taskID is empty when the address is not plus-tagged. ok=false on malformed input.

func PlusAddress

func PlusAddress(baseInbox, taskID string) (string, error)

PlusAddress builds "base+taskID@domain". If baseInbox has no domain, the default inbound domain is used.

func VerifySignature

func VerifySignature(rawBody []byte, signature, secret string) bool

VerifySignature returns true when `signature` is a valid hex-encoded HMAC-SHA256 of rawBody using secret. Timing-safe.

Accepts the signature with or without a "sha256=" prefix.

Types

type AcmeStartResult

type AcmeStartResult struct {
	Challenge   string `json:"challenge"`
	RecordName  string `json:"recordName"`
	RecordValue string `json:"recordValue"`
	OrderID     string `json:"orderId"`
}

AcmeStartResult is the challenge detail returned for custom domains. Publish the TXT record at RecordName with value RecordValue, then call AcmeFinish with OrderID.

type AuthVerdict

type AuthVerdict struct {
	DKIM  bool `json:"dkim"`
	SPF   bool `json:"spf"`
	DMARC bool `json:"dmarc"`
}

AuthVerdict captures DKIM / SPF / DMARC booleans on an inbound.

type Certificate

type Certificate struct {
	CertPEM  string `json:"certPem"`
	KeyPEM   string `json:"keyPem"`
	NotAfter string `json:"notAfter"`
}

Certificate bundles the issued PEMs + expiry returned by GetCert.

type ClaimHostnameParams

type ClaimHostnameParams struct {
	FQDN string       `json:"fqdn"`
	Mode HostnameMode `json:"mode,omitempty"`
}

ClaimHostnameParams creates the Gateway-side hostname record.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is the blocking Postbox client. Safe for concurrent use.

func NewClient

func NewClient(opts Options) *Client

NewClient returns a configured client.

func (*Client) ListInboxes

func (c *Client) ListInboxes(ctx context.Context, agentID string) ([]Inbox, error)

ListInboxes returns the mailboxes provisioned for the caller (optionally filtered to a specific agent).

func (*Client) ListMessages

func (c *Client) ListMessages(ctx context.Context, p ListMessagesParams) ([]Message, error)

ListMessages returns recent inbound / outbound messages for an agent.

func (*Client) ReadMessage

func (c *Client) ReadMessage(ctx context.Context, agentID, messageID string) (*Message, error)

ReadMessage fetches a full message by ID, body and auth verdicts included.

func (*Client) Send

func (c *Client) Send(ctx context.Context, p SendParams) (*SendResult, error)

Send enqueues an outbound message.

When RequireHuman is true, the send returns waiting_approval; dispatch happens after app/dashboard approval.

type ClientOption

type ClientOption interface {
	// contains filtered or unexported methods
}

ClientOption configures a callable-using client. Postbox-style options are orthogonal.

func WithCallableBaseURL

func WithCallableBaseURL(u string) ClientOption

WithCallableBaseURL overrides the Cloud Functions root.

func WithCallableHTTPClient

func WithCallableHTTPClient(h *http.Client) ClientOption

WithCallableHTTPClient injects a pre-built http.Client (for pooling or tests).

func WithCallableTimeout

func WithCallableTimeout(d time.Duration) ClientOption

WithCallableTimeout sets the per-request timeout on an auto-created http.Client. Ignored if the caller supplied one via WithCallableHTTPClient.

type CreateJobParams

type CreateJobParams struct {
	Name        string `json:"name"`
	Cron        string `json:"cron"`
	WebhookURL  string `json:"webhookUrl"`
	Payload     any    `json:"payload,omitempty"`
	MaxAttempts int    `json:"maxAttempts,omitempty"`
}

CreateJobParams registers a new cron job under the calling agent.

type DlqReplayParams

type DlqReplayParams struct {
	AgentID string `json:"agentId"`
	JobName string `json:"jobName"`
	RunID   string `json:"runId"`
}

DlqReplayParams re-queues a specific failed run for the owner's agent.

type Error

type Error struct {
	Status  int
	Message string
	Kind    string // auth | quota | not_found | rate_limited | other
	Body    string
}

Error is returned for any non-2xx HTTP response from Postbox.

func (*Error) Error

func (e *Error) Error() string

type GatewayClient

type GatewayClient struct {
	// contains filtered or unexported fields
}

GatewayClient is the blocking Go client for the Ujex Gateway subsystem.

func NewGatewayClient

func NewGatewayClient(idToken string, opts ...ClientOption) *GatewayClient

NewGatewayClient constructs a GatewayClient.

func (*GatewayClient) AcmeFinish

func (c *GatewayClient) AcmeFinish(ctx context.Context, orderID string) (*IssueResult, error)

AcmeFinish completes the custom-domain ACME flow after the TXT record has propagated. Returns the certificate id + expiry.

func (*GatewayClient) AcmeStart

func (c *GatewayClient) AcmeStart(ctx context.Context, fqdn string, staging bool) (*AcmeStartResult, error)

AcmeStart begins a custom-domain ACME flow. The caller must publish the returned TXT record on their own DNS before calling AcmeFinish.

func (*GatewayClient) ClaimHostname

func (c *GatewayClient) ClaimHostname(ctx context.Context, p ClaimHostnameParams) (HostnameMode, error)

ClaimHostname must precede Issue or AcmeStart. Mode is inferred from the FQDN suffix when omitted.

func (*GatewayClient) GetCert

func (c *GatewayClient) GetCert(ctx context.Context, fqdn, id string) (*Certificate, error)

GetCert retrieves the latest cert for an FQDN, or a specific id when set.

func (*GatewayClient) Issue

func (c *GatewayClient) Issue(ctx context.Context, fqdn string, staging bool) (*IssueResult, error)

Issue runs the full ACME flow for a *.duckdns.org hostname. Requires SetDuckDNSToken and ClaimHostname first.

func (*GatewayClient) SetDuckDNSToken

func (c *GatewayClient) SetDuckDNSToken(ctx context.Context, token string) error

SetDuckDNSToken is human-only: KMS-encrypts and stores your DuckDNS token so Issue() can drive the DuckDNS update + TXT challenge APIs.

func (*GatewayClient) UpdateIP

func (c *GatewayClient) UpdateIP(ctx context.Context, fqdn, ip string) (*UpdateIPResult, error)

UpdateIP records the current IP for an FQDN. For .duckdns.org hostnames, the DuckDNS update API is called and the result is in Propagated.

type HostnameMode

type HostnameMode string

HostnameMode is "duckdns" or "custom".

const (
	HostnameDuckDNS HostnameMode = "duckdns"
	HostnameCustom  HostnameMode = "custom"
)

type Inbox

type Inbox struct {
	LocalPart string    `json:"localPart"`
	Domain    string    `json:"domain"`
	AgentID   string    `json:"agentId"`
	CreatedAt time.Time `json:"createdAt,omitempty"`
}

Inbox is a provisioned per-agent mailbox.

func (Inbox) FullAddress

func (i Inbox) FullAddress() string

FullAddress returns "localPart@domain".

type InvokeResult

type InvokeResult struct {
	Status     int             `json:"status,omitempty"`
	Result     json.RawMessage `json:"result,omitempty"`
	ApprovalID string          `json:"approvalId,omitempty"`
	Reason     string          `json:"reason,omitempty"`
}

InvokeResult is either a tool HTTP response or a waiting approval.

func (*InvokeResult) UnmarshalJSON added in v0.2.0

func (r *InvokeResult) UnmarshalJSON(b []byte) error

func (InvokeResult) WaitingApproval added in v0.2.0

func (r InvokeResult) WaitingApproval() bool

type InvokeToolParams

type InvokeToolParams struct {
	Name       string `json:"name"`
	SessionID  string `json:"sessionId"`
	Args       any    `json:"args,omitempty"`
	TimeoutMS  int    `json:"timeoutMs,omitempty"`
	ApprovalID string `json:"approvalId,omitempty"`
}

InvokeToolParams configures a single tool invocation.

type IssueResult

type IssueResult struct {
	Issued   bool   `json:"issued"`
	ID       string `json:"id"`
	NotAfter string `json:"notAfter"`
}

IssueResult is returned by Issue and AcmeFinish once a cert lands.

type ListMessagesParams

type ListMessagesParams struct {
	AgentID   string
	Direction string
	Limit     int
	Since     *time.Time
}

ListMessagesParams is the query shape for Client.ListMessages.

type Message

type Message struct {
	ID         string      `json:"id"`
	Direction  string      `json:"direction"`
	From       string      `json:"from"`
	To         []string    `json:"to"`
	Subject    string      `json:"subject"`
	ThreadKey  string      `json:"threadKey,omitempty"`
	BodyText   string      `json:"bodyText,omitempty"`
	BodyHTML   string      `json:"bodyHtml,omitempty"`
	PIScore    float64     `json:"piScore"`
	PIReasons  []string    `json:"piReasons,omitempty"`
	DKIMPass   bool        `json:"dkimPass"`
	SPFPass    bool        `json:"spfPass"`
	DMARCPass  bool        `json:"dmarcPass"`
	ReceivedAt time.Time   `json:"receivedAt,omitempty"`
	Raw        interface{} `json:"-"`
}

Message is a stored Postbox message (inbound or outbound).

func (Message) AuthVerdict

func (m Message) AuthVerdict() AuthVerdict

AuthVerdict returns DKIM/SPF/DMARC as a single struct.

type Options

type Options struct {
	DeviceKey          string
	AgentID            string
	BaseURL            string
	FirebaseAPIKey     string
	IdentityToolkitURL string
	Timeout            time.Duration
	MaxRetries         int
	HTTPClient         *http.Client
}

Options configures a Client. DeviceKey is required; AgentID can be supplied here or per call where the method accepts it.

type RegisterToolParams

type RegisterToolParams struct {
	AgentID   string   `json:"agentId"`
	Name      string   `json:"name"`
	Kind      ToolKind `json:"kind"`
	URL       string   `json:"url"`
	AuthToken string   `json:"authToken,omitempty"`
	Manifest  any      `json:"manifest,omitempty"`
}

RegisterToolParams configures a new tool registration.

type SchedulerClient

type SchedulerClient struct {
	// contains filtered or unexported fields
}

SchedulerClient is the blocking Go client for the Ujex Scheduler subsystem.

func NewSchedulerClient

func NewSchedulerClient(idToken string, opts ...ClientOption) *SchedulerClient

NewSchedulerClient constructs a SchedulerClient with sensible defaults. idToken must be a Firebase Auth ID token obtained via the session exchange.

func (*SchedulerClient) CreateJob

func (c *SchedulerClient) CreateJob(ctx context.Context, p CreateJobParams) error

CreateJob registers a recurring job for the current agent. Fails with invalid-argument if the cron string is not parseable.

func (*SchedulerClient) DlqReplay

func (c *SchedulerClient) DlqReplay(ctx context.Context, p DlqReplayParams) error

DlqReplay is owner-only: it resubmits a specific failed run to the next dispatch tick. Returns permission-denied if the caller doesn't own the agent.

type SendParams

type SendParams struct {
	AgentID        string
	To             []string
	Subject        string
	Body           string
	SessionID      string
	ThreadKey      string
	RequireHuman   bool
	IdempotencyKey string
}

SendParams is the request shape for Client.Send.

type SendResult

type SendResult struct {
	ID             string `json:"id"`
	Status         string `json:"status"`
	QuotaRemaining *int   `json:"quotaRemaining,omitempty"`
}

SendResult is what Postbox returns when a message is accepted.

type ToolKind

type ToolKind string

ToolKind is "mcp" or "http".

const (
	ToolMCP  ToolKind = "mcp"
	ToolHTTP ToolKind = "http"
)

type ToolSummary

type ToolSummary struct {
	Name     string   `json:"name"`
	Kind     ToolKind `json:"kind"`
	URL      string   `json:"url"`
	Manifest any      `json:"manifest"`
}

ToolSummary is one entry returned by List. Credentials are never included.

type ToolsClient

type ToolsClient struct {
	// contains filtered or unexported fields
}

ToolsClient is the blocking Go client for the Ujex Tools subsystem.

func NewToolsClient

func NewToolsClient(idToken string, opts ...ClientOption) *ToolsClient

NewToolsClient constructs a ToolsClient. idToken must be a Firebase Auth ID token (obtain via session exchange).

func (*ToolsClient) GetCredential

func (c *ToolsClient) GetCredential(ctx context.Context, name string) (string, error)

GetCredential is kept for old callers, but production Ujex denies raw credential reads. Use Invoke so credentials stay server-side.

func (*ToolsClient) Invoke

Invoke is agent-only: dispatches the tool server-side with creds auto-injected and the call audited. Preferred over GetCredential.

func (*ToolsClient) List

func (c *ToolsClient) List(ctx context.Context) ([]ToolSummary, error)

List is agent-only: returns enabled tools.

func (*ToolsClient) Register

func (c *ToolsClient) Register(ctx context.Context, p RegisterToolParams) error

Register is human-only: creates or updates a tool doc. The authToken is KMS-encrypted server-side before hitting Firestore.

type UpdateIPResult

type UpdateIPResult struct {
	Changed    bool  `json:"changed"`
	Propagated *bool `json:"propagated"`
}

UpdateIPResult describes whether the update changed the recorded IP and whether DuckDNS propagation succeeded (nil for non-DuckDNS hostnames).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL