mailbox

package
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Oct 1, 2026 License: MIT Imports: 23 Imported by: 0

Documentation

Overview

Package mailbox is the BBS-side mail client for members (a free benefit of membership): a transport-agnostic core (read, search, compose, send, flag, delete) with a Bubble Tea TUI for humans and a line-oriented JSON mode for agents/bots. Addresses are <name>@bbs.profullstack.com; it talks to the self-hosted Mailu stack (Dovecot IMAP + Postfix submission) hosted on mail.profullstack.com.

The TS counterpart is @logicsrc/plugin-agentmail; the domain shapes here are deliberately the same so tooling can move between them.

Index

Constants

View Source
const (
	// Inbox is the default mailbox.
	Inbox = "INBOX"
	// Sent is where sent mail is stored.
	Sent = "Sent"
)
View Source
const BotUsage = `` /* 685-byte string literal not displayed */

BotUsage is printed when a bot/agent invokes mail with no/unknown command.

Variables

View Source
var ErrNotFound = errors.New("mailbox: message not found")

ErrNotFound is returned when a uid/mailbox cannot be resolved.

View Source
var ErrNotMember = errors.New("AgentMail is a member feature — register first: ssh join@bbs.profullstack.com")

ErrNotMember is returned when a caller without a registered handle attempts a mail action. AgentMail is open to every verified member.

Functions

func FormatAddress

func FormatAddress(a Address) string

FormatAddress renders an Address back to "Name <addr>" (or just the address).

func RunBot

func RunBot(ctx context.Context, c *Client, args []string, in io.Reader, out io.Writer) error

RunBot executes one non-interactive command for agents/bots, writing JSON to out. It returns an error (also emitted as {"error":...}) on failure.

func RunReader

func RunReader(s ssh.Session, c *Client) error

RunReader runs the interactive mail TUI for a member on the session. Used by the hub "Mail" entry and the ssh mail@ route (with a PTY).

func Snippet

func Snippet(body string, max int) string

Snippet collapses whitespace and truncates to at most max runes.

func ValidEmail

func ValidEmail(v string) bool

ValidEmail is a loose RFC5322-ish check: one @, a dotted domain, no spaces.

func VerifyLogin

func VerifyLogin(addr, user, pass string, plaintext bool) error

VerifyLogin checks a username/password against the IMAP backend by logging in and immediately logging out. It returns nil only when the credentials are accepted. The web file browser uses this to authenticate members with their webmail (Mailu/Dovecot) password — the same credential Roundcube uses.

Types

type Address

type Address struct {
	Name    string `json:"name,omitempty"`
	Address string `json:"address"`
}

Address is a parsed mail address.

func ParseAddress

func ParseAddress(raw string) Address

ParseAddress parses "Name <a@b>" or a bare "a@b".

type Attachment

type Attachment struct {
	Filename    string `json:"filename"`
	ContentType string `json:"contentType"`
	Size        int    `json:"size"`
}

Attachment is attachment metadata (bytes fetched separately).

type Client

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

Client is the ergonomic, member-gated facade the TUI and bot mode use. Every method returns plain structs, so the same calls serve humans and agents.

func NewClient

func NewClient(t Transport, id Identity, domain string, pageSize int) *Client

NewClient builds a member-gated client. domain is the email address domain (e.g. bbs.profullstack.com); pageSize defaults to 50 when <= 0.

func (*Client) Address

func (c *Client) Address() string

Address is the member's own mailbox address, e.g. alice@bbs.profullstack.com.

func (*Client) Close

func (c *Client) Close() error

Close releases the underlying transport.

func (*Client) Delete

func (c *Client) Delete(ctx context.Context, mailbox string, uid uint32) error

Delete removes a message.

func (*Client) Flag

func (c *Client) Flag(ctx context.Context, mailbox string, uid uint32, flagged bool) error

Flag sets or clears the \Flagged flag.

func (*Client) List

func (c *Client) List(ctx context.Context, mailbox string, limit int) ([]MessageSummary, error)

List returns newest-first summaries for a mailbox (INBOX when empty).

func (*Client) Mailboxes

func (c *Client) Mailboxes(ctx context.Context) ([]Mailbox, error)

Mailboxes lists folders with unread/total counts.

func (*Client) MarkSeen

func (c *Client) MarkSeen(ctx context.Context, mailbox string, uid uint32, seen bool) error

MarkSeen sets or clears the \Seen flag.

func (*Client) Read

func (c *Client) Read(ctx context.Context, mailbox string, uid uint32, peek bool) (Message, bool, error)

Read fetches a full message, marking it seen unless peek is true.

func (*Client) Reply

func (c *Client) Reply(ctx context.Context, orig Message, text string, replyAll bool) (SendResult, error)

Reply addresses the original sender (and, when replyAll, the other recipients minus the member), prefixes "Re:", threads via In-Reply-To, and sends.

func (*Client) Search

func (c *Client) Search(ctx context.Context, query, mailbox string, limit int) ([]MessageSummary, error)

Search runs a free-text search across a mailbox (or all when empty).

func (*Client) Send

func (c *Client) Send(ctx context.Context, d Draft) (SendResult, error)

Send validates, stamps From, and sends a draft.

type Draft

type Draft struct {
	To        []Address `json:"to"`
	CC        []Address `json:"cc,omitempty"`
	BCC       []Address `json:"bcc,omitempty"`
	Subject   string    `json:"subject"`
	Text      string    `json:"text"`
	InReplyTo string    `json:"inReplyTo,omitempty"`
}

Draft is an outgoing message.

func NormalizeDraft

func NormalizeDraft(d Draft) (Draft, error)

NormalizeDraft trims the subject, drops empty recipient lists, and rejects a draft with no valid recipient.

type FlagChange

type FlagChange struct {
	Seen    *bool
	Flagged *bool
}

FlagChange is a partial flag update; nil fields are left unchanged.

type IMAPConfig

type IMAPConfig struct {
	IMAPAddr string // host:port, e.g. mail.profullstack.com:993 (implicit TLS)
	SMTPAddr string // host:port, e.g. smtp.profullstack.com:587 (STARTTLS)
	Username string
	Password string
	// SMTPUser/SMTPPass default to Username/Password when empty.
	SMTPUser string
	SMTPPass string
	// SMTPServerName is the TLS server name verified during STARTTLS. Set it when
	// the dial host differs from the certificate name — e.g. dialing the trusted
	// local relay at 127.0.0.1:25 whose cert is mail.<host>. Empty = use the dial
	// host (the net/smtp default).
	SMTPServerName string
	// Plaintext dials IMAP without TLS. Used only for a co-located backend over
	// loopback (the Mailu gateway hitting Dovecot directly on 127.0.0.1, bypassing
	// the front's auth proxy so master-user login works) — the password never
	// leaves the host. Never enable it for a remote server.
	Plaintext bool
}

IMAPConfig connects the adapter to the mail backend (Mailu: Dovecot IMAP + Postfix submission). Username/Password are the resolved login (which may be a Dovecot master-user login like "alice*gateway").

type Identity

type Identity struct {
	Name string // local-part / handle, e.g. "alice"
	Paid bool   // Founding Lifetime Member (informational; does not gate mail)
}

Identity is the acting member. AgentMail is a free benefit of membership, so having a registered handle is the only requirement; Paid is retained for tier-aware features (e.g. quotas) but no longer gates access.

type ListOptions

type ListOptions struct {
	Mailbox string
	Limit   int
}

ListOptions controls a mailbox listing.

type Mailbox

type Mailbox struct {
	Name   string `json:"name"`
	Path   string `json:"path"`
	Unseen int    `json:"unseen"`
	Total  int    `json:"total"`
}

Mailbox is an IMAP folder with unread/total counts.

type MemoryTransport

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

MemoryTransport is a complete, dependency-free Transport for tests, local development, and as the reference for what the IMAP/SMTP adapter must do.

func NewMemoryTransport

func NewMemoryTransport() *MemoryTransport

NewMemoryTransport returns an empty in-memory transport.

func (*MemoryTransport) Add

func (m *MemoryTransport) Add(msg Message) Message

Add inserts a message, filling defaults, and returns the stored copy.

func (*MemoryTransport) Close

func (m *MemoryTransport) Close() error

func (*MemoryTransport) DeleteMessage

func (m *MemoryTransport) DeleteMessage(_ context.Context, mailbox string, uid uint32) error

func (*MemoryTransport) ListMailboxes

func (m *MemoryTransport) ListMailboxes(_ context.Context) ([]Mailbox, error)

func (*MemoryTransport) ListMessages

func (m *MemoryTransport) ListMessages(_ context.Context, opts ListOptions) ([]MessageSummary, error)

func (*MemoryTransport) ReadMessage

func (m *MemoryTransport) ReadMessage(_ context.Context, mailbox string, uid uint32) (Message, bool, error)

func (*MemoryTransport) Search

func (*MemoryTransport) Send

func (m *MemoryTransport) Send(_ context.Context, from string, d Draft) (SendResult, error)

func (*MemoryTransport) SetFlags

func (m *MemoryTransport) SetFlags(_ context.Context, mailbox string, uid uint32, fc FlagChange) error

type Message

type Message struct {
	MessageSummary
	CC          []Address    `json:"cc,omitempty"`
	ReplyTo     *Address     `json:"replyTo,omitempty"`
	MessageID   string       `json:"messageId"`
	References  []string     `json:"references,omitempty"`
	Text        string       `json:"text"`
	HTML        string       `json:"html,omitempty"`
	Attachments []Attachment `json:"attachments,omitempty"`
}

Message is a fully fetched message.

type MessageSummary

type MessageSummary struct {
	UID            uint32    `json:"uid"`
	Mailbox        string    `json:"mailbox"`
	From           Address   `json:"from"`
	To             []Address `json:"to"`
	Subject        string    `json:"subject"`
	Date           time.Time `json:"date"`
	Seen           bool      `json:"seen"`
	Flagged        bool      `json:"flagged"`
	HasAttachments bool      `json:"hasAttachments"`
	Snippet        string    `json:"snippet"`
}

MessageSummary is a lightweight row for list/search views.

type SearchOptions

type SearchOptions struct {
	Mailbox string // empty = all mailboxes
	Query   string
	Limit   int
}

SearchOptions controls a search.

type SendResult

type SendResult struct {
	MessageID string `json:"messageId"`
}

SendResult reports a sent message's id.

type Transport

type Transport interface {
	ListMailboxes(ctx context.Context) ([]Mailbox, error)
	ListMessages(ctx context.Context, opts ListOptions) ([]MessageSummary, error)
	// ReadMessage returns (Message, false, nil) when the uid is unknown.
	ReadMessage(ctx context.Context, mailbox string, uid uint32) (Message, bool, error)
	Search(ctx context.Context, opts SearchOptions) ([]MessageSummary, error)
	// Send delivers an already-validated draft, stamped from `from`.
	Send(ctx context.Context, from string, d Draft) (SendResult, error)
	SetFlags(ctx context.Context, mailbox string, uid uint32, fc FlagChange) error
	DeleteMessage(ctx context.Context, mailbox string, uid uint32) error
	Close() error
}

Transport is the low-level mailbox backend. The TUI, the bot mode, and the Client all talk to this interface, so an in-memory fake (tests/dev) and the real IMAP/SMTP adapter are interchangeable.

func NewIMAPTransport

func NewIMAPTransport(cfg IMAPConfig) (Transport, error)

NewIMAPTransport dials the IMAP server, logs in, and returns a Transport.

Jump to

Keyboard shortcuts

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