mirror of
https://github.com/profullstack/agentbbs.git
synced 2026-08-13 22:37:28 +00:00
* feat(mail): give every verified member a free @bbs.profullstack.com mailbox Email was built but paid-only (Founding Lifetime gate) and never wired to a running backend. Make it a free benefit of membership and split the address domain from the mail-server host. - internal/mailu: Mailu admin-API client; EnsureUser idempotently provisions a mailbox via the loopback admin REST API (token = mailu.env API_TOKEN). - main.go: auto-provision <name>@<mailDomain> at join@ verification and on first Mail open; un-gate the Mail hub entry + mail@ (membership/email-verified, not Premium); address domain (AGENTBBS_MAIL_ADDR_DOMAIN, default the BBS host) is now distinct from the mail server host (AGENTBBS_MAIL_DOMAIN) and the webmail URL. Drop the forwardemail alias path (Mailu now owns delivery for everyone). - mailbox: gate on membership (a registered handle) instead of Paid; ErrNotPaid -> ErrNotMember. - join@ copy: list email under free membership; premium now pitches custom domains + Tor only. - setup.sh / docs/mail.md / deploy/mailu: address-domain vs server-host split, Mailu API token, MX for the address domain, local-relay SMTP for verify codes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(mailu): pin Docker network subnet to match SUBNET; ignore runtime state The base compose declares no network, so Docker assigns the default bridge an arbitrary subnet that won't match mailu.env SUBNET — breaking Mailu's internal service auth/relay. Add a docker-compose.override.yml.example that pins the default network to 192.168.203.0/24, and gitignore the live override + Mailu runtime state (mailu.env, certs/, data/). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(mail): plaintext loopback IMAP so the gateway bypasses Mailu's front Mailu's front (nginx mail proxy) pre-authenticates against Mailu's user DB before proxying to Dovecot, which rejects the Dovecot master-user login <addr>*gateway. The gateway must reach Dovecot directly. The imap container has no TLS cert (only the front does), so the bypass is plaintext over loopback — the master password never leaves the host. - mailbox: IMAPConfig.Plaintext dials with DialInsecure (loopback only). - main.go: mailClientFor sets Plaintext from AGENTBBS_MAIL_IMAP_PLAINTEXT. - override.example: add the unbound resolver (admin needs DNSSEC), webmail image fix (2024.06 uses mailu/webmail), and publish Dovecot 143 on 127.0.0.1:14143. - docs/mail.md: document the front-bypass, the dovecot.conf master passdb (Mailu includes that exact filename), and the 644 master-users perms (640 = temp_fail). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * deploy(mailu): wire gateway IMAP to the loopback Dovecot path in setup.sh setup.sh §9e set AGENTBBS_MAIL_IMAP_ADDR to the front's :993, which the front's auth proxy rejects for the master-user login (and would clobber the working loopback wiring on every self-update). Point it at 127.0.0.1:14143 + AGENTBBS_MAIL_IMAP_PLAINTEXT=1 instead, matching the override + docs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(mail): give free members a webmail password at join@ The gateway opens mailboxes via the Dovecot master user (no member password), but webmail (Roundcube) needs the member to have a password. join@ now sets a fresh, readable webmail password via the Mailu API and shows it with the webmail URL + login, so free members can use webmail at mail.profullstack.com. - mailu: SetPassword (PATCH /user/<email> raw_password) + test. - main.go: setWebmailPassword + readablePassword; join@ displays url/login/password. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
100 lines
2.9 KiB
Go
100 lines
2.9 KiB
Go
// 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.
|
|
package mailbox
|
|
|
|
import "time"
|
|
|
|
// Address is a parsed mail address.
|
|
type Address struct {
|
|
Name string `json:"name,omitempty"`
|
|
Address string `json:"address"`
|
|
}
|
|
|
|
// Mailbox is an IMAP folder with unread/total counts.
|
|
type Mailbox struct {
|
|
Name string `json:"name"`
|
|
Path string `json:"path"`
|
|
Unseen int `json:"unseen"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
// MessageSummary is a lightweight row for list/search views.
|
|
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"`
|
|
}
|
|
|
|
// Attachment is attachment metadata (bytes fetched separately).
|
|
type Attachment struct {
|
|
Filename string `json:"filename"`
|
|
ContentType string `json:"contentType"`
|
|
Size int `json:"size"`
|
|
}
|
|
|
|
// Message is a fully fetched 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"`
|
|
}
|
|
|
|
// Draft is an outgoing message.
|
|
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"`
|
|
}
|
|
|
|
// ListOptions controls a mailbox listing.
|
|
type ListOptions struct {
|
|
Mailbox string
|
|
Limit int
|
|
}
|
|
|
|
// SearchOptions controls a search.
|
|
type SearchOptions struct {
|
|
Mailbox string // empty = all mailboxes
|
|
Query string
|
|
Limit int
|
|
}
|
|
|
|
// FlagChange is a partial flag update; nil fields are left unchanged.
|
|
type FlagChange struct {
|
|
Seen *bool
|
|
Flagged *bool
|
|
}
|
|
|
|
// SendResult reports a sent message's id.
|
|
type SendResult struct {
|
|
MessageID string `json:"messageId"`
|
|
}
|
|
|
|
const (
|
|
// Inbox is the default mailbox.
|
|
Inbox = "INBOX"
|
|
// Sent is where sent mail is stored.
|
|
Sent = "Sent"
|
|
)
|