From 3afbdc4d4a66373065a93ae525eda821c145cb3d Mon Sep 17 00:00:00 2001 From: LanQin_ Date: Mon, 29 Jun 2026 16:52:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(open=5Fapi):=20=E7=BB=9F=E4=B8=80=E5=BC=80?= =?UTF-8?q?=E6=94=BE=E6=8E=A5=E5=8F=A3=E5=91=BD=E5=90=8D=E5=B9=B6=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E6=9D=A5=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将公共 API 相关处理器、路由与测试重命名为 Open API,统一接口语义。 - 新增发送来源 open_api,并在发送审计与队列中保存该来源。 - 前端发送队列来源标签新增 Open API 显示。 --- apps/api/internal/app/app_test.go | 63 ++++++++++++------- apps/api/internal/app/mail_handlers.go | 12 +++- ...c_api_handlers.go => open_api_handlers.go} | 60 +++++++++--------- apps/api/internal/app/router_auth.go | 26 ++++---- apps/api/internal/app/send_queue.go | 1 + apps/web/src/pages/mail.tsx | 1 + 6 files changed, 94 insertions(+), 69 deletions(-) rename apps/api/internal/app/{public_api_handlers.go => open_api_handlers.go} (90%) diff --git a/apps/api/internal/app/app_test.go b/apps/api/internal/app/app_test.go index 3c845a2..f4a9df1 100644 --- a/apps/api/internal/app/app_test.go +++ b/apps/api/internal/app/app_test.go @@ -1744,7 +1744,7 @@ func TestAPITokenManagementStoresHashAndRevokes(t *testing.T) { } } -func TestPublicAPIDomainAndMailboxCRUD(t *testing.T) { +func TestOpenAPIDomainAndMailboxCRUD(t *testing.T) { a := newTestApp(t) ts := httptest.NewServer(a.Router()) defer ts.Close() @@ -1764,7 +1764,7 @@ func TestPublicAPIDomainAndMailboxCRUD(t *testing.T) { var domain Domain 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 == "" { t.Fatalf("domain=%+v", domain) @@ -1773,20 +1773,20 @@ func TestPublicAPIDomainAndMailboxCRUD(t *testing.T) { Items []Domain `json:"items"` } 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 { t.Fatalf("domains=%+v", domains.Items) } var disabled Domain 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" { 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 { - 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 @@ -1797,7 +1797,7 @@ func TestPublicAPIDomainAndMailboxCRUD(t *testing.T) { "password": "Password123!", "quotaMb": 256, }, &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 { t.Fatalf("mailbox=%+v", mailbox) @@ -1806,30 +1806,32 @@ func TestPublicAPIDomainAndMailboxCRUD(t *testing.T) { Items []Mailbox `json:"items"` } 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 { t.Fatalf("mailboxes=%+v", mailboxes.Items) } 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 { - 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" { t.Fatalf("updated mailbox=%+v", updated) } var ok map[string]any 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 { - 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) stopTestWorkers(a) + a.cfg.SMTPHost = "127.0.0.1" + a.cfg.SMTPPort = "25" ts := httptest.NewServer(a.Router()) defer ts.Close() 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) } domainID := mustDefaultDomainID(t, a) - sender := createTestMailbox(t, admin, domainID, "public-sender", "Public Sender", "Password123!", nil) - recipient := createTestMailbox(t, admin, domainID, "public-recipient", "Public Recipient", "Password123!", nil) - other := createTestMailbox(t, admin, domainID, "public-other", "Public Other", "Password123!", nil) + sender := createTestMailbox(t, admin, domainID, "open-sender", "Open API Sender", "Password123!", nil) + recipient := createTestMailbox(t, admin, domainID, "open-recipient", "Open API Recipient", "Password123!", nil) + other := createTestMailbox(t, admin, domainID, "open-other", "Open API Other", "Password123!", nil) 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 { @@ -1852,10 +1854,10 @@ func TestPublicAPISendStatusAndMailboxMessages(t *testing.T) { if code := senderClient.do("POST", "/api/open/send", map[string]any{ "mailboxId": sender.ID, "to": []string{recipient.Address}, - "subject": "cookie-only public api send", + "subject": "cookie-only open api send", "text": "this should not authenticate", }, &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 { ID string `json:"id"` @@ -1871,14 +1873,27 @@ func TestPublicAPISendStatusAndMailboxMessages(t *testing.T) { if code := senderOpen.do("POST", "/api/open/send", map[string]any{ "mailboxId": sender.ID, "to": []string{recipient.Address}, - "subject": "public api send", - "text": "hello from public api", + "subject": "open api send", + "text": "hello from open api", }, &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) } + 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 { ID string `json:"id"` @@ -1891,9 +1906,9 @@ func TestPublicAPISendStatusAndMailboxMessages(t *testing.T) { Subject string `json:"subject"` } 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) } @@ -1908,9 +1923,9 @@ func TestPublicAPISendStatusAndMailboxMessages(t *testing.T) { NextCursor string `json:"nextCursor"` } 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) } if code := recipientOpen.do("GET", "/api/open/mailboxes/"+other.ID+"/messages?folder=Inbox", nil, &map[string]any{}); code != http.StatusNotFound { diff --git a/apps/api/internal/app/mail_handlers.go b/apps/api/internal/app/mail_handlers.go index 8a76dcb..661ed55 100644 --- a/apps/api/internal/app/mail_handlers.go +++ b/apps/api/internal/app/mail_handlers.go @@ -691,6 +691,14 @@ var errSenderNotAuthorized = errors.New("sender address is not authorized") var errMailboxQuotaExceeded = errors.New("mailbox quota exceeded") 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 { return nil, err } @@ -742,8 +750,8 @@ func (a *App) sendMailNow(ctx context.Context, user *User, mb *Mailbox, req mail a.deleteMessage(ctx, sentID) 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}) - 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 { + 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: source, MailFrom: fromAddress, HeaderFrom: fromAddress, Recipients: allRecipients, MIMEBytes: mimeBytes, Now: now}); err != nil { a.deleteMessage(ctx, sentID) return nil, fmt.Errorf("failed to enqueue delivery: %w", err) } diff --git a/apps/api/internal/app/public_api_handlers.go b/apps/api/internal/app/open_api_handlers.go similarity index 90% rename from apps/api/internal/app/public_api_handlers.go rename to apps/api/internal/app/open_api_handlers.go index bef1e57..0eebf77 100644 --- a/apps/api/internal/app/public_api_handlers.go +++ b/apps/api/internal/app/open_api_handlers.go @@ -13,7 +13,7 @@ import ( "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`) if err != nil { 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}) } -func (a *App) handlePublicAPICreateDomain(w http.ResponseWriter, r *http.Request) { +func (a *App) handleOpenAPICreateDomain(w http.ResponseWriter, r *http.Request) { var req struct { Name string `json:"name"` } @@ -57,7 +57,7 @@ func (a *App) handlePublicAPICreateDomain(w http.ResponseWriter, r *http.Request 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")) if err != nil { 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) } -func (a *App) handlePublicAPIUpdateDomain(w http.ResponseWriter, r *http.Request) { +func (a *App) handleOpenAPIUpdateDomain(w http.ResponseWriter, r *http.Request) { var req struct { Status string `json:"status"` } @@ -97,7 +97,7 @@ func (a *App) handlePublicAPIUpdateDomain(w http.ResponseWriter, r *http.Request 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") var count int 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}) } -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 FROM mailboxes mb JOIN users u ON u.id=mb.user_id ORDER BY mb.address`) 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}) } -func (a *App) handlePublicAPICreateMailbox(w http.ResponseWriter, r *http.Request) { +func (a *App) handleOpenAPICreateMailbox(w http.ResponseWriter, r *http.Request) { var req struct { DomainID string `json:"domainId"` LocalPart string `json:"localPart"` @@ -203,7 +203,7 @@ func (a *App) handlePublicAPICreateMailbox(w http.ResponseWriter, r *http.Reques 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")) if err != nil { 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) } -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") current, err := a.mailboxByID(r.Context(), id) if err != nil { @@ -271,7 +271,7 @@ func (a *App) handlePublicAPIUpdateMailbox(w http.ResponseWriter, r *http.Reques 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") var owner string 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}) } -func (a *App) handlePublicAPISendMail(w http.ResponseWriter, r *http.Request) { +func (a *App) handleOpenAPISendMail(w http.ResponseWriter, r *http.Request) { var req mailComposeInput if err := decodeJSON(r, &req); err != nil { badRequest(w, err) @@ -329,26 +329,26 @@ func (a *App) handlePublicAPISendMail(w http.ResponseWriter, r *http.Request) { respondError(w, http.StatusNotFound, "mailbox not found") 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 { respondSendError(w, err) return } - status := publicAPISendStatusFromMessage(msg, mb.Address) + status := openAPISendStatusFromMessage(msg, mb.Address) if msg.SendQueueID != "" { if item, err := a.loadSendQueueEntryForUser(r.Context(), msg.SendQueueID, mb.UserID); err == nil { - status = publicAPISendStatusFromQueue(item, mb.Address) + status = openAPISendStatusFromQueue(item, mb.Address) } } else { item, err := a.loadLatestSendQueueForMailboxMessage(r.Context(), msg.ID, mb.ID) if err == nil { - status = publicAPISendStatusFromQueue(item, mb.Address) + status = openAPISendStatusFromQueue(item, mb.Address) } } 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) id := strings.TrimSpace(chi.URLParam(r, "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 { mailboxAddress = mb.Address } - respondJSON(w, http.StatusOK, publicAPISendStatusFromQueue(item, mailboxAddress)) + respondJSON(w, http.StatusOK, openAPISendStatusFromQueue(item, mailboxAddress)) return } - msg, err := a.loadPublicAPISentMessageForUser(r.Context(), id, user.ID) + msg, err := a.loadOpenAPISentMessageForUser(r.Context(), id, user.ID) if err != nil { respondError(w, http.StatusNotFound, "send item not found") 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 { 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) mailboxID := strings.TrimSpace(chi.URLParam(r, "id")) if _, err := a.mailboxForUserByID(r.Context(), user.ID, mailboxID); err != nil { respondError(w, http.StatusNotFound, "mailbox not found") return } - limit := parsePublicAPILimit(r, 30, 100) - offset := parsePublicAPIOffset(r) + limit := parseOpenAPILimit(r, 30, 100) + offset := parseOpenAPIOffset(r) folder := strings.TrimSpace(r.URL.Query().Get("folder")) if folder == "" { folder = "Inbox" @@ -458,7 +458,7 @@ func scanMailbox(row mailboxScanner) (Mailbox, error) { return item, nil } -type publicAPISendStatus struct { +type openAPISendStatus struct { ID string `json:"id"` QueueID string `json:"queueId,omitempty"` Status string `json:"status"` @@ -477,8 +477,8 @@ type publicAPISendStatus struct { DeliveredAt *time.Time `json:"deliveredAt,omitempty"` } -func publicAPISendStatusFromQueue(item SendQueueEntry, mailboxAddress string) publicAPISendStatus { - return publicAPISendStatus{ +func openAPISendStatusFromQueue(item SendQueueEntry, mailboxAddress string) openAPISendStatus { + return openAPISendStatus{ ID: item.ID, QueueID: item.ID, 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(recipients, msg.BCC...) - return publicAPISendStatus{ + return openAPISendStatus{ ID: msg.ID, Status: sendAuditAccepted, MessageID: msg.ID, @@ -623,7 +623,7 @@ func (a *App) loadSendQueueEntryForSentMessage(ctx context.Context, sentMessageI 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 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 @@ -635,7 +635,7 @@ func (a *App) loadPublicAPISentMessageForUser(ctx context.Context, id, userID st 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")) if err != nil || limit <= 0 { return defaultLimit @@ -646,7 +646,7 @@ func parsePublicAPILimit(r *http.Request, defaultLimit, maxLimit int) int { return limit } -func parsePublicAPIOffset(r *http.Request) int { +func parseOpenAPIOffset(r *http.Request) int { cursor := strings.TrimSpace(r.URL.Query().Get("cursor")) if cursor == "" { return 0 diff --git a/apps/api/internal/app/router_auth.go b/apps/api/internal/app/router_auth.go index 30038b8..1ccf9f4 100644 --- a/apps/api/internal/app/router_auth.go +++ b/apps/api/internal/app/router_auth.go @@ -77,19 +77,19 @@ func (a *App) Router() http.Handler { r.Group(func(r chi.Router) { 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.requirePermission(PermissionDomainsCreate)).Post("/open/domains", a.handlePublicAPICreateDomain) - r.With(a.requireAdminAccess, a.requireAnyPermission(PermissionDomainsView, PermissionDNSView, PermissionMailboxesView, PermissionAliasesView, PermissionSettingsView, PermissionTemplatesView)).Get("/open/domains/{id}", a.handlePublicAPIGetDomain) - r.With(a.requireAdminAccess, a.requirePermission(PermissionDomainsUpdate)).Post("/open/domains/{id}", a.handlePublicAPIUpdateDomain) - r.With(a.requireAdminAccess, a.requirePermission(PermissionDomainsDelete)).Delete("/open/domains/{id}", a.handlePublicAPIDeleteDomain) - r.With(a.requireAdminAccess, a.requireAnyPermission(PermissionMailboxesView, PermissionMessagesView)).Get("/open/mailboxes", a.handlePublicAPIListMailboxes) - r.With(a.requireAdminAccess, a.requirePermission(PermissionMailboxesCreate)).Post("/open/mailboxes", a.handlePublicAPICreateMailbox) - r.With(a.requireAdminAccess, a.requireAnyPermission(PermissionMailboxesView, PermissionMessagesView)).Get("/open/mailboxes/{id}", a.handlePublicAPIGetMailbox) - r.With(a.requireAdminAccess, a.requirePermission(PermissionMailboxesUpdate)).Post("/open/mailboxes/{id}", a.handlePublicAPIUpdateMailbox) - r.With(a.requireAdminAccess, a.requirePermission(PermissionMailboxesDelete)).Delete("/open/mailboxes/{id}", a.handlePublicAPIDeleteMailbox) - r.With(a.requirePermission(PermissionMailSend)).Post("/open/send", a.handlePublicAPISendMail) - r.With(a.requirePermission(PermissionMailRead)).Get("/open/send/{id}", a.handlePublicAPISendStatus) - r.With(a.requirePermission(PermissionMailRead)).Get("/open/mailboxes/{id}/messages", a.handlePublicAPIMailboxMessages) + 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.handleOpenAPICreateDomain) + 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.handleOpenAPIUpdateDomain) + 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.handleOpenAPIListMailboxes) + 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.handleOpenAPIGetMailbox) + 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.handleOpenAPIDeleteMailbox) + r.With(a.requirePermission(PermissionMailSend)).Post("/open/send", a.handleOpenAPISendMail) + r.With(a.requirePermission(PermissionMailRead)).Get("/open/send/{id}", a.handleOpenAPISendStatus) + r.With(a.requirePermission(PermissionMailRead)).Get("/open/mailboxes/{id}/messages", a.handleOpenAPIMailboxMessages) }) r.Group(func(r chi.Router) { diff --git a/apps/api/internal/app/send_queue.go b/apps/api/internal/app/send_queue.go index a8038ca..c0dd55a 100644 --- a/apps/api/internal/app/send_queue.go +++ b/apps/api/internal/app/send_queue.go @@ -27,6 +27,7 @@ const ( sendSourceWebmail = "webmail" sendSourceSubmission = "submission" + sendSourceOpenAPI = "open_api" sendQueueStaleAfter = 15 * time.Minute sendQueueConcurrency = 4 diff --git a/apps/web/src/pages/mail.tsx b/apps/web/src/pages/mail.tsx index c568215..c9fec0e 100644 --- a/apps/web/src/pages/mail.tsx +++ b/apps/web/src/pages/mail.tsx @@ -1921,6 +1921,7 @@ function sendQueueSourceLabel(source: string) { const normalized = source.toLowerCase() if (normalized === "submission") return "SMTP Submission" if (normalized === "webmail") return "Webmail" + if (normalized === "open_api") return "Open API" if (normalized === "scheduled") return "定时发送" return source || "未知" }