feat(mail): 增强发送队列筛选与投递追踪能力
- 发送队列支持按 Message-ID、收件人和时间范围筛选,并改为稳定游标分页。 - 邮件详情补充关联的发送队列信息,支持从邮件直接查看投递时间线。 - 前端同步接入新筛选条件,并在发送队列页提供清除筛选入口。
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"math/big"
|
||||
@@ -1541,6 +1542,137 @@ func TestSendQueueAPIPermissionIsolation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendQueueAPIFiltersStableCursorAndMessageDetailLink(t *testing.T) {
|
||||
a := newTestApp(t)
|
||||
a.cfg.SMTPHost = "127.0.0.1"
|
||||
a.cfg.SMTPPort = "25"
|
||||
ts := httptest.NewServer(a.Router())
|
||||
defer ts.Close()
|
||||
client := &testClient{t: t, server: ts}
|
||||
|
||||
var login map[string]any
|
||||
if code := client.do("POST", "/api/auth/login", map[string]string{"email": "admin@lanqin.local", "password": "ChangeMe123!"}, &login); code != http.StatusOK {
|
||||
t.Fatalf("login code=%d", code)
|
||||
}
|
||||
user, mb := defaultAdminUserAndMailbox(t, a)
|
||||
now := a.now().UTC()
|
||||
sentFolderID, err := a.ensureFolder(context.Background(), mb.ID, "Sent")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sentMsg := storedMessage{
|
||||
MailboxID: mb.ID,
|
||||
FolderID: sentFolderID,
|
||||
MessageUID: "uid-queue-detail",
|
||||
MessageID: "<queue-detail@example.test>",
|
||||
Subject: "queue detail",
|
||||
From: mb.Address,
|
||||
To: []string{"detail@example.test"},
|
||||
SentAt: now,
|
||||
ReceivedAt: now,
|
||||
Snippet: "detail",
|
||||
BodyText: "detail",
|
||||
IsRead: true,
|
||||
}
|
||||
sentID, err := a.insertMessage(context.Background(), sentMsg, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
firstID, err := a.enqueueSend(context.Background(), sendQueueInput{
|
||||
UserID: user.ID,
|
||||
MailboxID: mb.ID,
|
||||
SentMessageID: sentID,
|
||||
MessageID: "<queue-detail@example.test>",
|
||||
Source: sendSourceWebmail,
|
||||
MailFrom: mb.Address,
|
||||
HeaderFrom: mb.Address,
|
||||
Recipients: []string{"detail@example.test"},
|
||||
MIMEBytes: []byte("Subject: detail\r\n\r\nbody"),
|
||||
Now: now.Add(-2 * time.Hour),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
secondID, err := a.enqueueSend(context.Background(), sendQueueInput{
|
||||
UserID: user.ID,
|
||||
MailboxID: mb.ID,
|
||||
MessageID: "<queue-other@example.test>",
|
||||
Source: sendSourceWebmail,
|
||||
MailFrom: mb.Address,
|
||||
HeaderFrom: mb.Address,
|
||||
Recipients: []string{"other@example.test"},
|
||||
MIMEBytes: []byte("Subject: other\r\n\r\nbody"),
|
||||
Now: now.Add(-1 * time.Hour),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
thirdID, err := a.enqueueSend(context.Background(), sendQueueInput{
|
||||
UserID: user.ID,
|
||||
MailboxID: mb.ID,
|
||||
MessageID: "<queue-latest@example.test>",
|
||||
Source: sendSourceWebmail,
|
||||
MailFrom: mb.Address,
|
||||
HeaderFrom: mb.Address,
|
||||
Recipients: []string{"latest@example.test"},
|
||||
MIMEBytes: []byte("Subject: latest\r\n\r\nbody"),
|
||||
Now: now,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i := 0; i < 28; i++ {
|
||||
if _, err := a.enqueueSend(context.Background(), sendQueueInput{
|
||||
UserID: user.ID,
|
||||
MailboxID: mb.ID,
|
||||
MessageID: fmt.Sprintf("<queue-extra-%02d@example.test>", i),
|
||||
Source: sendSourceWebmail,
|
||||
MailFrom: mb.Address,
|
||||
HeaderFrom: mb.Address,
|
||||
Recipients: []string{fmt.Sprintf("extra-%02d@example.test", i)},
|
||||
MIMEBytes: []byte("Subject: extra\r\n\r\nbody"),
|
||||
Now: now.Add(time.Duration(-24-i) * time.Hour),
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
var byMessage struct {
|
||||
Items []SendQueueEntry `json:"items"`
|
||||
}
|
||||
if code := client.do("GET", "/api/mail/send-queue?messageId="+url.QueryEscape("<queue-detail@example.test>"), nil, &byMessage); code != http.StatusOK || len(byMessage.Items) != 1 || byMessage.Items[0].ID != firstID {
|
||||
t.Fatalf("message filter code=%d items=%+v", code, byMessage.Items)
|
||||
}
|
||||
var byRecipient struct {
|
||||
Items []SendQueueEntry `json:"items"`
|
||||
}
|
||||
if code := client.do("GET", "/api/mail/send-queue?recipient="+url.QueryEscape("other@example.test"), nil, &byRecipient); code != http.StatusOK || len(byRecipient.Items) != 1 || byRecipient.Items[0].ID != secondID {
|
||||
t.Fatalf("recipient filter code=%d items=%+v", code, byRecipient.Items)
|
||||
}
|
||||
var byTime struct {
|
||||
Items []SendQueueEntry `json:"items"`
|
||||
}
|
||||
from := now.Add(-90 * time.Minute).Format(time.RFC3339Nano)
|
||||
to := now.Add(30 * time.Minute).Format(time.RFC3339Nano)
|
||||
if code := client.do("GET", "/api/mail/send-queue?from="+url.QueryEscape(from)+"&to="+url.QueryEscape(to), nil, &byTime); code != http.StatusOK || len(byTime.Items) != 2 || byTime.Items[0].ID != thirdID || byTime.Items[1].ID != secondID {
|
||||
t.Fatalf("time filter code=%d items=%+v", code, byTime.Items)
|
||||
}
|
||||
var firstPage struct {
|
||||
Items []SendQueueEntry `json:"items"`
|
||||
NextCursor string `json:"nextCursor"`
|
||||
}
|
||||
if code := client.do("GET", "/api/mail/send-queue?cursor=0", nil, &firstPage); code != http.StatusOK || len(firstPage.Items) != 30 || firstPage.NextCursor == "" {
|
||||
t.Fatalf("first page code=%d cursor=%q items=%+v", code, firstPage.NextCursor, firstPage.Items)
|
||||
}
|
||||
if _, _, _, err := parseSendQueueCursor(firstPage.NextCursor); err != nil {
|
||||
t.Fatalf("next cursor is not stable cursor: %q err=%v", firstPage.NextCursor, err)
|
||||
}
|
||||
var detail MailMessage
|
||||
if code := client.do("GET", "/api/mail/messages/"+sentID+"?markRead=0", nil, &detail); code != http.StatusOK || detail.SendQueueID != firstID || detail.SendQueueStatus == "" {
|
||||
t.Fatalf("message detail queue link code=%d detail=%+v", code, detail)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendQueueAPIRetryAndCancel(t *testing.T) {
|
||||
a := newTestApp(t)
|
||||
host, port, received := startCapturingSMTP(t, 1)
|
||||
|
||||
Reference in New Issue
Block a user