feat(open_api): 统一开放接口命名并记录来源
- 将公共 API 相关处理器、路由与测试重命名为 Open API,统一接口语义。 - 新增发送来源 open_api,并在发送审计与队列中保存该来源。 - 前端发送队列来源标签新增 Open API 显示。
This commit is contained in:
@@ -1744,7 +1744,7 @@ func TestAPITokenManagementStoresHashAndRevokes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPublicAPIDomainAndMailboxCRUD(t *testing.T) {
|
func TestOpenAPIDomainAndMailboxCRUD(t *testing.T) {
|
||||||
a := newTestApp(t)
|
a := newTestApp(t)
|
||||||
ts := httptest.NewServer(a.Router())
|
ts := httptest.NewServer(a.Router())
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
@@ -1764,7 +1764,7 @@ func TestPublicAPIDomainAndMailboxCRUD(t *testing.T) {
|
|||||||
|
|
||||||
var domain Domain
|
var domain Domain
|
||||||
if code := openAdmin.do("POST", "/api/open/domains", map[string]string{"name": "api.example.test"}, &domain); code != http.StatusCreated {
|
if code := openAdmin.do("POST", "/api/open/domains", map[string]string{"name": "api.example.test"}, &domain); code != http.StatusCreated {
|
||||||
t.Fatalf("create public api domain code=%d domain=%+v", code, domain)
|
t.Fatalf("create open api domain code=%d domain=%+v", code, domain)
|
||||||
}
|
}
|
||||||
if domain.Name != "api.example.test" || domain.DKIMPublicKey == "" {
|
if domain.Name != "api.example.test" || domain.DKIMPublicKey == "" {
|
||||||
t.Fatalf("domain=%+v", domain)
|
t.Fatalf("domain=%+v", domain)
|
||||||
@@ -1773,20 +1773,20 @@ func TestPublicAPIDomainAndMailboxCRUD(t *testing.T) {
|
|||||||
Items []Domain `json:"items"`
|
Items []Domain `json:"items"`
|
||||||
}
|
}
|
||||||
if code := openAdmin.do("GET", "/api/open/domains", nil, &domains); code != http.StatusOK {
|
if code := openAdmin.do("GET", "/api/open/domains", nil, &domains); code != http.StatusOK {
|
||||||
t.Fatalf("list public api domains code=%d", code)
|
t.Fatalf("list open api domains code=%d", code)
|
||||||
}
|
}
|
||||||
if len(domains.Items) < 2 {
|
if len(domains.Items) < 2 {
|
||||||
t.Fatalf("domains=%+v", domains.Items)
|
t.Fatalf("domains=%+v", domains.Items)
|
||||||
}
|
}
|
||||||
var disabled Domain
|
var disabled Domain
|
||||||
if code := openAdmin.do("POST", "/api/open/domains/"+domain.ID, map[string]string{"status": "disabled"}, &disabled); code != http.StatusOK {
|
if code := openAdmin.do("POST", "/api/open/domains/"+domain.ID, map[string]string{"status": "disabled"}, &disabled); code != http.StatusOK {
|
||||||
t.Fatalf("update public api domain code=%d domain=%+v", code, disabled)
|
t.Fatalf("update open api domain code=%d domain=%+v", code, disabled)
|
||||||
}
|
}
|
||||||
if disabled.Status != "disabled" {
|
if disabled.Status != "disabled" {
|
||||||
t.Fatalf("domain status=%q", disabled.Status)
|
t.Fatalf("domain status=%q", disabled.Status)
|
||||||
}
|
}
|
||||||
if code := openAdmin.do("POST", "/api/open/domains/"+domain.ID, map[string]string{"status": "active"}, &domain); code != http.StatusOK {
|
if code := openAdmin.do("POST", "/api/open/domains/"+domain.ID, map[string]string{"status": "active"}, &domain); code != http.StatusOK {
|
||||||
t.Fatalf("reactivate public api domain code=%d domain=%+v", code, domain)
|
t.Fatalf("reactivate open api domain code=%d domain=%+v", code, domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
var mailbox Mailbox
|
var mailbox Mailbox
|
||||||
@@ -1797,7 +1797,7 @@ func TestPublicAPIDomainAndMailboxCRUD(t *testing.T) {
|
|||||||
"password": "Password123!",
|
"password": "Password123!",
|
||||||
"quotaMb": 256,
|
"quotaMb": 256,
|
||||||
}, &mailbox); code != http.StatusCreated {
|
}, &mailbox); code != http.StatusCreated {
|
||||||
t.Fatalf("create public api mailbox code=%d mailbox=%+v", code, mailbox)
|
t.Fatalf("create open api mailbox code=%d mailbox=%+v", code, mailbox)
|
||||||
}
|
}
|
||||||
if mailbox.Address != "api-user@api.example.test" || mailbox.QuotaMB != 256 {
|
if mailbox.Address != "api-user@api.example.test" || mailbox.QuotaMB != 256 {
|
||||||
t.Fatalf("mailbox=%+v", mailbox)
|
t.Fatalf("mailbox=%+v", mailbox)
|
||||||
@@ -1806,30 +1806,32 @@ func TestPublicAPIDomainAndMailboxCRUD(t *testing.T) {
|
|||||||
Items []Mailbox `json:"items"`
|
Items []Mailbox `json:"items"`
|
||||||
}
|
}
|
||||||
if code := openAdmin.do("GET", "/api/open/mailboxes", nil, &mailboxes); code != http.StatusOK {
|
if code := openAdmin.do("GET", "/api/open/mailboxes", nil, &mailboxes); code != http.StatusOK {
|
||||||
t.Fatalf("list public api mailboxes code=%d", code)
|
t.Fatalf("list open api mailboxes code=%d", code)
|
||||||
}
|
}
|
||||||
if len(mailboxes.Items) < 2 {
|
if len(mailboxes.Items) < 2 {
|
||||||
t.Fatalf("mailboxes=%+v", mailboxes.Items)
|
t.Fatalf("mailboxes=%+v", mailboxes.Items)
|
||||||
}
|
}
|
||||||
var updated Mailbox
|
var updated Mailbox
|
||||||
if code := openAdmin.do("POST", "/api/open/mailboxes/"+mailbox.ID, map[string]any{"displayName": "Renamed API User", "quotaMb": 512, "status": "disabled"}, &updated); code != http.StatusOK {
|
if code := openAdmin.do("POST", "/api/open/mailboxes/"+mailbox.ID, map[string]any{"displayName": "Renamed API User", "quotaMb": 512, "status": "disabled"}, &updated); code != http.StatusOK {
|
||||||
t.Fatalf("update public api mailbox code=%d mailbox=%+v", code, updated)
|
t.Fatalf("update open api mailbox code=%d mailbox=%+v", code, updated)
|
||||||
}
|
}
|
||||||
if updated.DisplayName != "Renamed API User" || updated.QuotaMB != 512 || updated.Status != "disabled" {
|
if updated.DisplayName != "Renamed API User" || updated.QuotaMB != 512 || updated.Status != "disabled" {
|
||||||
t.Fatalf("updated mailbox=%+v", updated)
|
t.Fatalf("updated mailbox=%+v", updated)
|
||||||
}
|
}
|
||||||
var ok map[string]any
|
var ok map[string]any
|
||||||
if code := openAdmin.do("DELETE", "/api/open/mailboxes/"+mailbox.ID, nil, &ok); code != http.StatusOK {
|
if code := openAdmin.do("DELETE", "/api/open/mailboxes/"+mailbox.ID, nil, &ok); code != http.StatusOK {
|
||||||
t.Fatalf("delete public api mailbox code=%d body=%v", code, ok)
|
t.Fatalf("delete open api mailbox code=%d body=%v", code, ok)
|
||||||
}
|
}
|
||||||
if code := openAdmin.do("DELETE", "/api/open/domains/"+domain.ID, nil, &ok); code != http.StatusOK {
|
if code := openAdmin.do("DELETE", "/api/open/domains/"+domain.ID, nil, &ok); code != http.StatusOK {
|
||||||
t.Fatalf("delete public api domain code=%d body=%v", code, ok)
|
t.Fatalf("delete open api domain code=%d body=%v", code, ok)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPublicAPISendStatusAndMailboxMessages(t *testing.T) {
|
func TestOpenAPISendStatusAndMailboxMessages(t *testing.T) {
|
||||||
a := newTestApp(t)
|
a := newTestApp(t)
|
||||||
stopTestWorkers(a)
|
stopTestWorkers(a)
|
||||||
|
a.cfg.SMTPHost = "127.0.0.1"
|
||||||
|
a.cfg.SMTPPort = "25"
|
||||||
ts := httptest.NewServer(a.Router())
|
ts := httptest.NewServer(a.Router())
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
admin := &testClient{t: t, server: ts}
|
admin := &testClient{t: t, server: ts}
|
||||||
@@ -1839,9 +1841,9 @@ func TestPublicAPISendStatusAndMailboxMessages(t *testing.T) {
|
|||||||
t.Fatalf("admin login code=%d body=%v", code, login)
|
t.Fatalf("admin login code=%d body=%v", code, login)
|
||||||
}
|
}
|
||||||
domainID := mustDefaultDomainID(t, a)
|
domainID := mustDefaultDomainID(t, a)
|
||||||
sender := createTestMailbox(t, admin, domainID, "public-sender", "Public Sender", "Password123!", nil)
|
sender := createTestMailbox(t, admin, domainID, "open-sender", "Open API Sender", "Password123!", nil)
|
||||||
recipient := createTestMailbox(t, admin, domainID, "public-recipient", "Public Recipient", "Password123!", nil)
|
recipient := createTestMailbox(t, admin, domainID, "open-recipient", "Open API Recipient", "Password123!", nil)
|
||||||
other := createTestMailbox(t, admin, domainID, "public-other", "Public Other", "Password123!", nil)
|
other := createTestMailbox(t, admin, domainID, "open-other", "Open API Other", "Password123!", nil)
|
||||||
|
|
||||||
senderClient := &testClient{t: t, server: ts}
|
senderClient := &testClient{t: t, server: ts}
|
||||||
if code := senderClient.do("POST", "/api/auth/login", map[string]string{"email": sender.Address, "password": "Password123!"}, &login); code != http.StatusOK {
|
if code := senderClient.do("POST", "/api/auth/login", map[string]string{"email": sender.Address, "password": "Password123!"}, &login); code != http.StatusOK {
|
||||||
@@ -1852,10 +1854,10 @@ func TestPublicAPISendStatusAndMailboxMessages(t *testing.T) {
|
|||||||
if code := senderClient.do("POST", "/api/open/send", map[string]any{
|
if code := senderClient.do("POST", "/api/open/send", map[string]any{
|
||||||
"mailboxId": sender.ID,
|
"mailboxId": sender.ID,
|
||||||
"to": []string{recipient.Address},
|
"to": []string{recipient.Address},
|
||||||
"subject": "cookie-only public api send",
|
"subject": "cookie-only open api send",
|
||||||
"text": "this should not authenticate",
|
"text": "this should not authenticate",
|
||||||
}, &map[string]any{}); code != http.StatusUnauthorized {
|
}, &map[string]any{}); code != http.StatusUnauthorized {
|
||||||
t.Fatalf("cookie-only public api send code=%d", code)
|
t.Fatalf("cookie-only open api send code=%d", code)
|
||||||
}
|
}
|
||||||
var sent struct {
|
var sent struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
@@ -1871,14 +1873,27 @@ func TestPublicAPISendStatusAndMailboxMessages(t *testing.T) {
|
|||||||
if code := senderOpen.do("POST", "/api/open/send", map[string]any{
|
if code := senderOpen.do("POST", "/api/open/send", map[string]any{
|
||||||
"mailboxId": sender.ID,
|
"mailboxId": sender.ID,
|
||||||
"to": []string{recipient.Address},
|
"to": []string{recipient.Address},
|
||||||
"subject": "public api send",
|
"subject": "open api send",
|
||||||
"text": "hello from public api",
|
"text": "hello from open api",
|
||||||
}, &sent); code != http.StatusCreated {
|
}, &sent); code != http.StatusCreated {
|
||||||
t.Fatalf("public api send code=%d body=%+v", code, sent)
|
t.Fatalf("open api send code=%d body=%+v", code, sent)
|
||||||
}
|
}
|
||||||
if sent.ID == "" || sent.Status != sendAuditAccepted || sent.MessageID == "" || sent.MailboxAddress != sender.Address {
|
if sent.ID == "" || sent.QueueID == "" || sent.Status != sendQueueStatusQueued || sent.MessageID == "" || sent.MailboxAddress != sender.Address {
|
||||||
t.Fatalf("sent response=%+v", sent)
|
t.Fatalf("sent response=%+v", sent)
|
||||||
}
|
}
|
||||||
|
var sendSource string
|
||||||
|
if err := a.db.QueryRow(`SELECT source FROM send_audit_events WHERE sent_message_id=? AND event=?`, sent.MessageID, sendAuditAccepted).Scan(&sendSource); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if sendSource != sendSourceOpenAPI {
|
||||||
|
t.Fatalf("open api send audit source=%q, want %q", sendSource, sendSourceOpenAPI)
|
||||||
|
}
|
||||||
|
if err := a.db.QueryRow(`SELECT source FROM send_queue WHERE id=?`, sent.QueueID).Scan(&sendSource); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if sendSource != sendSourceOpenAPI {
|
||||||
|
t.Fatalf("open api send queue source=%q, want %q", sendSource, sendSourceOpenAPI)
|
||||||
|
}
|
||||||
|
|
||||||
var status struct {
|
var status struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
@@ -1891,9 +1906,9 @@ func TestPublicAPISendStatusAndMailboxMessages(t *testing.T) {
|
|||||||
Subject string `json:"subject"`
|
Subject string `json:"subject"`
|
||||||
}
|
}
|
||||||
if code := senderOpen.do("GET", "/api/open/send/"+sent.ID, nil, &status); code != http.StatusOK {
|
if code := senderOpen.do("GET", "/api/open/send/"+sent.ID, nil, &status); code != http.StatusOK {
|
||||||
t.Fatalf("public api send status code=%d status=%+v", code, status)
|
t.Fatalf("open api send status code=%d status=%+v", code, status)
|
||||||
}
|
}
|
||||||
if status.ID != sent.ID || status.MessageID != sent.MessageID || status.Status != sendAuditAccepted {
|
if status.ID != sent.QueueID || status.MessageID != sent.MessageID || status.Status != sendQueueStatusQueued {
|
||||||
t.Fatalf("status=%+v sent=%+v", status, sent)
|
t.Fatalf("status=%+v sent=%+v", status, sent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1908,9 +1923,9 @@ func TestPublicAPISendStatusAndMailboxMessages(t *testing.T) {
|
|||||||
NextCursor string `json:"nextCursor"`
|
NextCursor string `json:"nextCursor"`
|
||||||
}
|
}
|
||||||
if code := recipientOpen.do("GET", "/api/open/mailboxes/"+recipient.ID+"/messages?folder=Inbox", nil, &inbox); code != http.StatusOK {
|
if code := recipientOpen.do("GET", "/api/open/mailboxes/"+recipient.ID+"/messages?folder=Inbox", nil, &inbox); code != http.StatusOK {
|
||||||
t.Fatalf("public api mailbox messages code=%d inbox=%+v", code, inbox)
|
t.Fatalf("open api mailbox messages code=%d inbox=%+v", code, inbox)
|
||||||
}
|
}
|
||||||
if len(inbox.Items) != 1 || inbox.Items[0].Subject != "public api send" || inbox.Items[0].From != sender.Address {
|
if len(inbox.Items) != 1 || inbox.Items[0].Subject != "open api send" || inbox.Items[0].From != sender.Address {
|
||||||
t.Fatalf("inbox=%+v", inbox.Items)
|
t.Fatalf("inbox=%+v", inbox.Items)
|
||||||
}
|
}
|
||||||
if code := recipientOpen.do("GET", "/api/open/mailboxes/"+other.ID+"/messages?folder=Inbox", nil, &map[string]any{}); code != http.StatusNotFound {
|
if code := recipientOpen.do("GET", "/api/open/mailboxes/"+other.ID+"/messages?folder=Inbox", nil, &map[string]any{}); code != http.StatusNotFound {
|
||||||
|
|||||||
@@ -691,6 +691,14 @@ var errSenderNotAuthorized = errors.New("sender address is not authorized")
|
|||||||
var errMailboxQuotaExceeded = errors.New("mailbox quota exceeded")
|
var errMailboxQuotaExceeded = errors.New("mailbox quota exceeded")
|
||||||
|
|
||||||
func (a *App) sendMailNow(ctx context.Context, user *User, mb *Mailbox, req mailComposeInput) (*MailMessage, error) {
|
func (a *App) sendMailNow(ctx context.Context, user *User, mb *Mailbox, req mailComposeInput) (*MailMessage, error) {
|
||||||
|
return a.sendMailWithSource(ctx, user, mb, req, sendSourceWebmail)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) sendMailWithSource(ctx context.Context, user *User, mb *Mailbox, req mailComposeInput, source string) (*MailMessage, error) {
|
||||||
|
source = strings.TrimSpace(source)
|
||||||
|
if source == "" {
|
||||||
|
source = sendSourceWebmail
|
||||||
|
}
|
||||||
if err := validateAttachmentLimit(req.Attachments, userLimits(user)); err != nil {
|
if err := validateAttachmentLimit(req.Attachments, userLimits(user)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -742,8 +750,8 @@ func (a *App) sendMailNow(ctx context.Context, user *User, mb *Mailbox, req mail
|
|||||||
a.deleteMessage(ctx, sentID)
|
a.deleteMessage(ctx, sentID)
|
||||||
return nil, fmt.Errorf("failed to store sent message in maildir: %w", err)
|
return nil, fmt.Errorf("failed to store sent message in maildir: %w", err)
|
||||||
}
|
}
|
||||||
a.recordSendAudit(ctx, sendAuditAccepted, sendQueueStatusQueued, sendAuditInput{UserID: user.ID, MailboxID: mb.ID, SentMessageID: sentID, Source: sendSourceWebmail, MailFrom: fromAddress, HeaderFrom: fromAddress, Recipients: allRecipients})
|
a.recordSendAudit(ctx, sendAuditAccepted, sendQueueStatusQueued, sendAuditInput{UserID: user.ID, MailboxID: mb.ID, SentMessageID: sentID, Source: source, MailFrom: fromAddress, HeaderFrom: fromAddress, Recipients: allRecipients})
|
||||||
if _, err := a.enqueueSend(ctx, sendQueueInput{UserID: user.ID, MailboxID: mb.ID, SentMessageID: sentID, MessageID: messageID, Source: sendSourceWebmail, MailFrom: fromAddress, HeaderFrom: fromAddress, Recipients: allRecipients, MIMEBytes: mimeBytes, Now: now}); err != nil {
|
if _, err := a.enqueueSend(ctx, sendQueueInput{UserID: user.ID, MailboxID: mb.ID, SentMessageID: sentID, MessageID: messageID, Source: source, MailFrom: fromAddress, HeaderFrom: fromAddress, Recipients: allRecipients, MIMEBytes: mimeBytes, Now: now}); err != nil {
|
||||||
a.deleteMessage(ctx, sentID)
|
a.deleteMessage(ctx, sentID)
|
||||||
return nil, fmt.Errorf("failed to enqueue delivery: %w", err)
|
return nil, fmt.Errorf("failed to enqueue delivery: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-30
@@ -13,7 +13,7 @@ import (
|
|||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *App) handlePublicAPIListDomains(w http.ResponseWriter, r *http.Request) {
|
func (a *App) handleOpenAPIListDomains(w http.ResponseWriter, r *http.Request) {
|
||||||
rows, err := a.db.QueryContext(r.Context(), `SELECT id,name,status,dkim_selector,dkim_public_key,dns_status,dns_checked_at,created_at FROM domains ORDER BY name`)
|
rows, err := a.db.QueryContext(r.Context(), `SELECT id,name,status,dkim_selector,dkim_public_key,dns_status,dns_checked_at,created_at FROM domains ORDER BY name`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, http.StatusInternalServerError, "failed to list domains")
|
respondError(w, http.StatusInternalServerError, "failed to list domains")
|
||||||
@@ -36,7 +36,7 @@ func (a *App) handlePublicAPIListDomains(w http.ResponseWriter, r *http.Request)
|
|||||||
respondJSON(w, http.StatusOK, map[string]any{"items": items})
|
respondJSON(w, http.StatusOK, map[string]any{"items": items})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) handlePublicAPICreateDomain(w http.ResponseWriter, r *http.Request) {
|
func (a *App) handleOpenAPICreateDomain(w http.ResponseWriter, r *http.Request) {
|
||||||
var req struct {
|
var req struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
@@ -57,7 +57,7 @@ func (a *App) handlePublicAPICreateDomain(w http.ResponseWriter, r *http.Request
|
|||||||
respondJSON(w, http.StatusCreated, domain)
|
respondJSON(w, http.StatusCreated, domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) handlePublicAPIGetDomain(w http.ResponseWriter, r *http.Request) {
|
func (a *App) handleOpenAPIGetDomain(w http.ResponseWriter, r *http.Request) {
|
||||||
domain, err := a.domainByID(r.Context(), chi.URLParam(r, "id"))
|
domain, err := a.domainByID(r.Context(), chi.URLParam(r, "id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, http.StatusNotFound, "domain not found")
|
respondError(w, http.StatusNotFound, "domain not found")
|
||||||
@@ -66,7 +66,7 @@ func (a *App) handlePublicAPIGetDomain(w http.ResponseWriter, r *http.Request) {
|
|||||||
respondJSON(w, http.StatusOK, domain)
|
respondJSON(w, http.StatusOK, domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) handlePublicAPIUpdateDomain(w http.ResponseWriter, r *http.Request) {
|
func (a *App) handleOpenAPIUpdateDomain(w http.ResponseWriter, r *http.Request) {
|
||||||
var req struct {
|
var req struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ func (a *App) handlePublicAPIUpdateDomain(w http.ResponseWriter, r *http.Request
|
|||||||
respondJSON(w, http.StatusOK, domain)
|
respondJSON(w, http.StatusOK, domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) handlePublicAPIDeleteDomain(w http.ResponseWriter, r *http.Request) {
|
func (a *App) handleOpenAPIDeleteDomain(w http.ResponseWriter, r *http.Request) {
|
||||||
id := chi.URLParam(r, "id")
|
id := chi.URLParam(r, "id")
|
||||||
var count int
|
var count int
|
||||||
if err := a.db.QueryRowContext(r.Context(), `SELECT COUNT(*) FROM mailboxes WHERE domain_id=?`, id).Scan(&count); err != nil {
|
if err := a.db.QueryRowContext(r.Context(), `SELECT COUNT(*) FROM mailboxes WHERE domain_id=?`, id).Scan(&count); err != nil {
|
||||||
@@ -120,7 +120,7 @@ func (a *App) handlePublicAPIDeleteDomain(w http.ResponseWriter, r *http.Request
|
|||||||
respondJSON(w, http.StatusOK, map[string]any{"ok": true})
|
respondJSON(w, http.StatusOK, map[string]any{"ok": true})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) handlePublicAPIListMailboxes(w http.ResponseWriter, r *http.Request) {
|
func (a *App) handleOpenAPIListMailboxes(w http.ResponseWriter, r *http.Request) {
|
||||||
rows, err := a.db.QueryContext(r.Context(), `SELECT mb.id,mb.user_id,u.email,mb.domain_id,mb.local_part,mb.address,mb.display_name,mb.quota_mb,mb.status,mb.created_at
|
rows, err := a.db.QueryContext(r.Context(), `SELECT mb.id,mb.user_id,u.email,mb.domain_id,mb.local_part,mb.address,mb.display_name,mb.quota_mb,mb.status,mb.created_at
|
||||||
FROM mailboxes mb JOIN users u ON u.id=mb.user_id ORDER BY mb.address`)
|
FROM mailboxes mb JOIN users u ON u.id=mb.user_id ORDER BY mb.address`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -144,7 +144,7 @@ func (a *App) handlePublicAPIListMailboxes(w http.ResponseWriter, r *http.Reques
|
|||||||
respondJSON(w, http.StatusOK, map[string]any{"items": items})
|
respondJSON(w, http.StatusOK, map[string]any{"items": items})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) handlePublicAPICreateMailbox(w http.ResponseWriter, r *http.Request) {
|
func (a *App) handleOpenAPICreateMailbox(w http.ResponseWriter, r *http.Request) {
|
||||||
var req struct {
|
var req struct {
|
||||||
DomainID string `json:"domainId"`
|
DomainID string `json:"domainId"`
|
||||||
LocalPart string `json:"localPart"`
|
LocalPart string `json:"localPart"`
|
||||||
@@ -203,7 +203,7 @@ func (a *App) handlePublicAPICreateMailbox(w http.ResponseWriter, r *http.Reques
|
|||||||
respondJSON(w, http.StatusCreated, mailbox)
|
respondJSON(w, http.StatusCreated, mailbox)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) handlePublicAPIGetMailbox(w http.ResponseWriter, r *http.Request) {
|
func (a *App) handleOpenAPIGetMailbox(w http.ResponseWriter, r *http.Request) {
|
||||||
mailbox, err := a.mailboxByID(r.Context(), chi.URLParam(r, "id"))
|
mailbox, err := a.mailboxByID(r.Context(), chi.URLParam(r, "id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, http.StatusNotFound, "mailbox not found")
|
respondError(w, http.StatusNotFound, "mailbox not found")
|
||||||
@@ -212,7 +212,7 @@ func (a *App) handlePublicAPIGetMailbox(w http.ResponseWriter, r *http.Request)
|
|||||||
respondJSON(w, http.StatusOK, mailbox)
|
respondJSON(w, http.StatusOK, mailbox)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) handlePublicAPIUpdateMailbox(w http.ResponseWriter, r *http.Request) {
|
func (a *App) handleOpenAPIUpdateMailbox(w http.ResponseWriter, r *http.Request) {
|
||||||
id := chi.URLParam(r, "id")
|
id := chi.URLParam(r, "id")
|
||||||
current, err := a.mailboxByID(r.Context(), id)
|
current, err := a.mailboxByID(r.Context(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -271,7 +271,7 @@ func (a *App) handlePublicAPIUpdateMailbox(w http.ResponseWriter, r *http.Reques
|
|||||||
respondJSON(w, http.StatusOK, mailbox)
|
respondJSON(w, http.StatusOK, mailbox)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) handlePublicAPIDeleteMailbox(w http.ResponseWriter, r *http.Request) {
|
func (a *App) handleOpenAPIDeleteMailbox(w http.ResponseWriter, r *http.Request) {
|
||||||
id := chi.URLParam(r, "id")
|
id := chi.URLParam(r, "id")
|
||||||
var owner string
|
var owner string
|
||||||
if err := a.db.QueryRowContext(r.Context(), `SELECT user_id FROM mailboxes WHERE id=?`, id).Scan(&owner); err != nil {
|
if err := a.db.QueryRowContext(r.Context(), `SELECT user_id FROM mailboxes WHERE id=?`, id).Scan(&owner); err != nil {
|
||||||
@@ -318,7 +318,7 @@ func (a *App) handlePublicAPIDeleteMailbox(w http.ResponseWriter, r *http.Reques
|
|||||||
respondJSON(w, http.StatusOK, map[string]any{"ok": true})
|
respondJSON(w, http.StatusOK, map[string]any{"ok": true})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) handlePublicAPISendMail(w http.ResponseWriter, r *http.Request) {
|
func (a *App) handleOpenAPISendMail(w http.ResponseWriter, r *http.Request) {
|
||||||
var req mailComposeInput
|
var req mailComposeInput
|
||||||
if err := decodeJSON(r, &req); err != nil {
|
if err := decodeJSON(r, &req); err != nil {
|
||||||
badRequest(w, err)
|
badRequest(w, err)
|
||||||
@@ -329,26 +329,26 @@ func (a *App) handlePublicAPISendMail(w http.ResponseWriter, r *http.Request) {
|
|||||||
respondError(w, http.StatusNotFound, "mailbox not found")
|
respondError(w, http.StatusNotFound, "mailbox not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
msg, err := a.sendMailNow(r.Context(), currentUser(r), mb, req)
|
msg, err := a.sendMailWithSource(r.Context(), currentUser(r), mb, req, sendSourceOpenAPI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondSendError(w, err)
|
respondSendError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
status := publicAPISendStatusFromMessage(msg, mb.Address)
|
status := openAPISendStatusFromMessage(msg, mb.Address)
|
||||||
if msg.SendQueueID != "" {
|
if msg.SendQueueID != "" {
|
||||||
if item, err := a.loadSendQueueEntryForUser(r.Context(), msg.SendQueueID, mb.UserID); err == nil {
|
if item, err := a.loadSendQueueEntryForUser(r.Context(), msg.SendQueueID, mb.UserID); err == nil {
|
||||||
status = publicAPISendStatusFromQueue(item, mb.Address)
|
status = openAPISendStatusFromQueue(item, mb.Address)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
item, err := a.loadLatestSendQueueForMailboxMessage(r.Context(), msg.ID, mb.ID)
|
item, err := a.loadLatestSendQueueForMailboxMessage(r.Context(), msg.ID, mb.ID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
status = publicAPISendStatusFromQueue(item, mb.Address)
|
status = openAPISendStatusFromQueue(item, mb.Address)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
respondJSON(w, http.StatusCreated, status)
|
respondJSON(w, http.StatusCreated, status)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) handlePublicAPISendStatus(w http.ResponseWriter, r *http.Request) {
|
func (a *App) handleOpenAPISendStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
user := currentUser(r)
|
user := currentUser(r)
|
||||||
id := strings.TrimSpace(chi.URLParam(r, "id"))
|
id := strings.TrimSpace(chi.URLParam(r, "id"))
|
||||||
item, err := a.loadSendQueueEntryForUser(r.Context(), id, user.ID)
|
item, err := a.loadSendQueueEntryForUser(r.Context(), id, user.ID)
|
||||||
@@ -360,10 +360,10 @@ func (a *App) handlePublicAPISendStatus(w http.ResponseWriter, r *http.Request)
|
|||||||
if mb, mbErr := a.mailboxByID(r.Context(), item.MailboxID); mbErr == nil {
|
if mb, mbErr := a.mailboxByID(r.Context(), item.MailboxID); mbErr == nil {
|
||||||
mailboxAddress = mb.Address
|
mailboxAddress = mb.Address
|
||||||
}
|
}
|
||||||
respondJSON(w, http.StatusOK, publicAPISendStatusFromQueue(item, mailboxAddress))
|
respondJSON(w, http.StatusOK, openAPISendStatusFromQueue(item, mailboxAddress))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
msg, err := a.loadPublicAPISentMessageForUser(r.Context(), id, user.ID)
|
msg, err := a.loadOpenAPISentMessageForUser(r.Context(), id, user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, http.StatusNotFound, "send item not found")
|
respondError(w, http.StatusNotFound, "send item not found")
|
||||||
return
|
return
|
||||||
@@ -372,18 +372,18 @@ func (a *App) handlePublicAPISendStatus(w http.ResponseWriter, r *http.Request)
|
|||||||
if mb, mbErr := a.mailboxByID(r.Context(), msg.MailboxID); mbErr == nil {
|
if mb, mbErr := a.mailboxByID(r.Context(), msg.MailboxID); mbErr == nil {
|
||||||
mailboxAddress = mb.Address
|
mailboxAddress = mb.Address
|
||||||
}
|
}
|
||||||
respondJSON(w, http.StatusOK, publicAPISendStatusFromMessage(msg, mailboxAddress))
|
respondJSON(w, http.StatusOK, openAPISendStatusFromMessage(msg, mailboxAddress))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) handlePublicAPIMailboxMessages(w http.ResponseWriter, r *http.Request) {
|
func (a *App) handleOpenAPIMailboxMessages(w http.ResponseWriter, r *http.Request) {
|
||||||
user := currentUser(r)
|
user := currentUser(r)
|
||||||
mailboxID := strings.TrimSpace(chi.URLParam(r, "id"))
|
mailboxID := strings.TrimSpace(chi.URLParam(r, "id"))
|
||||||
if _, err := a.mailboxForUserByID(r.Context(), user.ID, mailboxID); err != nil {
|
if _, err := a.mailboxForUserByID(r.Context(), user.ID, mailboxID); err != nil {
|
||||||
respondError(w, http.StatusNotFound, "mailbox not found")
|
respondError(w, http.StatusNotFound, "mailbox not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
limit := parsePublicAPILimit(r, 30, 100)
|
limit := parseOpenAPILimit(r, 30, 100)
|
||||||
offset := parsePublicAPIOffset(r)
|
offset := parseOpenAPIOffset(r)
|
||||||
folder := strings.TrimSpace(r.URL.Query().Get("folder"))
|
folder := strings.TrimSpace(r.URL.Query().Get("folder"))
|
||||||
if folder == "" {
|
if folder == "" {
|
||||||
folder = "Inbox"
|
folder = "Inbox"
|
||||||
@@ -458,7 +458,7 @@ func scanMailbox(row mailboxScanner) (Mailbox, error) {
|
|||||||
return item, nil
|
return item, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type publicAPISendStatus struct {
|
type openAPISendStatus struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
QueueID string `json:"queueId,omitempty"`
|
QueueID string `json:"queueId,omitempty"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
@@ -477,8 +477,8 @@ type publicAPISendStatus struct {
|
|||||||
DeliveredAt *time.Time `json:"deliveredAt,omitempty"`
|
DeliveredAt *time.Time `json:"deliveredAt,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func publicAPISendStatusFromQueue(item SendQueueEntry, mailboxAddress string) publicAPISendStatus {
|
func openAPISendStatusFromQueue(item SendQueueEntry, mailboxAddress string) openAPISendStatus {
|
||||||
return publicAPISendStatus{
|
return openAPISendStatus{
|
||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
QueueID: item.ID,
|
QueueID: item.ID,
|
||||||
Status: item.Status,
|
Status: item.Status,
|
||||||
@@ -498,10 +498,10 @@ func publicAPISendStatusFromQueue(item SendQueueEntry, mailboxAddress string) pu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func publicAPISendStatusFromMessage(msg *MailMessage, mailboxAddress string) publicAPISendStatus {
|
func openAPISendStatusFromMessage(msg *MailMessage, mailboxAddress string) openAPISendStatus {
|
||||||
recipients := append(append([]string{}, msg.To...), msg.CC...)
|
recipients := append(append([]string{}, msg.To...), msg.CC...)
|
||||||
recipients = append(recipients, msg.BCC...)
|
recipients = append(recipients, msg.BCC...)
|
||||||
return publicAPISendStatus{
|
return openAPISendStatus{
|
||||||
ID: msg.ID,
|
ID: msg.ID,
|
||||||
Status: sendAuditAccepted,
|
Status: sendAuditAccepted,
|
||||||
MessageID: msg.ID,
|
MessageID: msg.ID,
|
||||||
@@ -623,7 +623,7 @@ func (a *App) loadSendQueueEntryForSentMessage(ctx context.Context, sentMessageI
|
|||||||
return a.loadLatestSendQueueForMessage(ctx, sentMessageID, userID)
|
return a.loadLatestSendQueueForMessage(ctx, sentMessageID, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) loadPublicAPISentMessageForUser(ctx context.Context, id, userID string) (*MailMessage, error) {
|
func (a *App) loadOpenAPISentMessageForUser(ctx context.Context, id, userID string) (*MailMessage, error) {
|
||||||
var messageID string
|
var messageID string
|
||||||
err := a.db.QueryRowContext(ctx, `SELECT m.id
|
err := a.db.QueryRowContext(ctx, `SELECT m.id
|
||||||
FROM messages m JOIN mailboxes mb ON mb.id=m.mailbox_id JOIN folders f ON f.id=m.folder_id
|
FROM messages m JOIN mailboxes mb ON mb.id=m.mailbox_id JOIN folders f ON f.id=m.folder_id
|
||||||
@@ -635,7 +635,7 @@ func (a *App) loadPublicAPISentMessageForUser(ctx context.Context, id, userID st
|
|||||||
return a.messageByID(ctx, messageID, false)
|
return a.messageByID(ctx, messageID, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parsePublicAPILimit(r *http.Request, defaultLimit, maxLimit int) int {
|
func parseOpenAPILimit(r *http.Request, defaultLimit, maxLimit int) int {
|
||||||
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
|
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||||
if err != nil || limit <= 0 {
|
if err != nil || limit <= 0 {
|
||||||
return defaultLimit
|
return defaultLimit
|
||||||
@@ -646,7 +646,7 @@ func parsePublicAPILimit(r *http.Request, defaultLimit, maxLimit int) int {
|
|||||||
return limit
|
return limit
|
||||||
}
|
}
|
||||||
|
|
||||||
func parsePublicAPIOffset(r *http.Request) int {
|
func parseOpenAPIOffset(r *http.Request) int {
|
||||||
cursor := strings.TrimSpace(r.URL.Query().Get("cursor"))
|
cursor := strings.TrimSpace(r.URL.Query().Get("cursor"))
|
||||||
if cursor == "" {
|
if cursor == "" {
|
||||||
return 0
|
return 0
|
||||||
@@ -77,19 +77,19 @@ func (a *App) Router() http.Handler {
|
|||||||
|
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(a.requireAPIToken)
|
r.Use(a.requireAPIToken)
|
||||||
r.With(a.requireAdminAccess, a.requireAnyPermission(PermissionDomainsView, PermissionDNSView, PermissionMailboxesView, PermissionAliasesView, PermissionSettingsView, PermissionTemplatesView)).Get("/open/domains", a.handlePublicAPIListDomains)
|
r.With(a.requireAdminAccess, a.requireAnyPermission(PermissionDomainsView, PermissionDNSView, PermissionMailboxesView, PermissionAliasesView, PermissionSettingsView, PermissionTemplatesView)).Get("/open/domains", a.handleOpenAPIListDomains)
|
||||||
r.With(a.requireAdminAccess, a.requirePermission(PermissionDomainsCreate)).Post("/open/domains", a.handlePublicAPICreateDomain)
|
r.With(a.requireAdminAccess, a.requirePermission(PermissionDomainsCreate)).Post("/open/domains", a.handleOpenAPICreateDomain)
|
||||||
r.With(a.requireAdminAccess, a.requireAnyPermission(PermissionDomainsView, PermissionDNSView, PermissionMailboxesView, PermissionAliasesView, PermissionSettingsView, PermissionTemplatesView)).Get("/open/domains/{id}", a.handlePublicAPIGetDomain)
|
r.With(a.requireAdminAccess, a.requireAnyPermission(PermissionDomainsView, PermissionDNSView, PermissionMailboxesView, PermissionAliasesView, PermissionSettingsView, PermissionTemplatesView)).Get("/open/domains/{id}", a.handleOpenAPIGetDomain)
|
||||||
r.With(a.requireAdminAccess, a.requirePermission(PermissionDomainsUpdate)).Post("/open/domains/{id}", a.handlePublicAPIUpdateDomain)
|
r.With(a.requireAdminAccess, a.requirePermission(PermissionDomainsUpdate)).Post("/open/domains/{id}", a.handleOpenAPIUpdateDomain)
|
||||||
r.With(a.requireAdminAccess, a.requirePermission(PermissionDomainsDelete)).Delete("/open/domains/{id}", a.handlePublicAPIDeleteDomain)
|
r.With(a.requireAdminAccess, a.requirePermission(PermissionDomainsDelete)).Delete("/open/domains/{id}", a.handleOpenAPIDeleteDomain)
|
||||||
r.With(a.requireAdminAccess, a.requireAnyPermission(PermissionMailboxesView, PermissionMessagesView)).Get("/open/mailboxes", a.handlePublicAPIListMailboxes)
|
r.With(a.requireAdminAccess, a.requireAnyPermission(PermissionMailboxesView, PermissionMessagesView)).Get("/open/mailboxes", a.handleOpenAPIListMailboxes)
|
||||||
r.With(a.requireAdminAccess, a.requirePermission(PermissionMailboxesCreate)).Post("/open/mailboxes", a.handlePublicAPICreateMailbox)
|
r.With(a.requireAdminAccess, a.requirePermission(PermissionMailboxesCreate)).Post("/open/mailboxes", a.handleOpenAPICreateMailbox)
|
||||||
r.With(a.requireAdminAccess, a.requireAnyPermission(PermissionMailboxesView, PermissionMessagesView)).Get("/open/mailboxes/{id}", a.handlePublicAPIGetMailbox)
|
r.With(a.requireAdminAccess, a.requireAnyPermission(PermissionMailboxesView, PermissionMessagesView)).Get("/open/mailboxes/{id}", a.handleOpenAPIGetMailbox)
|
||||||
r.With(a.requireAdminAccess, a.requirePermission(PermissionMailboxesUpdate)).Post("/open/mailboxes/{id}", a.handlePublicAPIUpdateMailbox)
|
r.With(a.requireAdminAccess, a.requirePermission(PermissionMailboxesUpdate)).Post("/open/mailboxes/{id}", a.handleOpenAPIUpdateMailbox)
|
||||||
r.With(a.requireAdminAccess, a.requirePermission(PermissionMailboxesDelete)).Delete("/open/mailboxes/{id}", a.handlePublicAPIDeleteMailbox)
|
r.With(a.requireAdminAccess, a.requirePermission(PermissionMailboxesDelete)).Delete("/open/mailboxes/{id}", a.handleOpenAPIDeleteMailbox)
|
||||||
r.With(a.requirePermission(PermissionMailSend)).Post("/open/send", a.handlePublicAPISendMail)
|
r.With(a.requirePermission(PermissionMailSend)).Post("/open/send", a.handleOpenAPISendMail)
|
||||||
r.With(a.requirePermission(PermissionMailRead)).Get("/open/send/{id}", a.handlePublicAPISendStatus)
|
r.With(a.requirePermission(PermissionMailRead)).Get("/open/send/{id}", a.handleOpenAPISendStatus)
|
||||||
r.With(a.requirePermission(PermissionMailRead)).Get("/open/mailboxes/{id}/messages", a.handlePublicAPIMailboxMessages)
|
r.With(a.requirePermission(PermissionMailRead)).Get("/open/mailboxes/{id}/messages", a.handleOpenAPIMailboxMessages)
|
||||||
})
|
})
|
||||||
|
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const (
|
|||||||
|
|
||||||
sendSourceWebmail = "webmail"
|
sendSourceWebmail = "webmail"
|
||||||
sendSourceSubmission = "submission"
|
sendSourceSubmission = "submission"
|
||||||
|
sendSourceOpenAPI = "open_api"
|
||||||
|
|
||||||
sendQueueStaleAfter = 15 * time.Minute
|
sendQueueStaleAfter = 15 * time.Minute
|
||||||
sendQueueConcurrency = 4
|
sendQueueConcurrency = 4
|
||||||
|
|||||||
@@ -1921,6 +1921,7 @@ function sendQueueSourceLabel(source: string) {
|
|||||||
const normalized = source.toLowerCase()
|
const normalized = source.toLowerCase()
|
||||||
if (normalized === "submission") return "SMTP Submission"
|
if (normalized === "submission") return "SMTP Submission"
|
||||||
if (normalized === "webmail") return "Webmail"
|
if (normalized === "webmail") return "Webmail"
|
||||||
|
if (normalized === "open_api") return "Open API"
|
||||||
if (normalized === "scheduled") return "定时发送"
|
if (normalized === "scheduled") return "定时发送"
|
||||||
return source || "未知"
|
return source || "未知"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user