feat: refine admin dashboard and mail experience
This commit is contained in:
@@ -24,23 +24,36 @@ func (a *App) handleAdminOverview(w http.ResponseWriter, r *http.Request) {
|
||||
Messages int64 `json:"messages"`
|
||||
UnreadMessages int64 `json:"unreadMessages"`
|
||||
StorageBytes int64 `json:"storageBytes"`
|
||||
TodaySent int64 `json:"todaySent"`
|
||||
TodayReceived int64 `json:"todayReceived"`
|
||||
SendDelivered int64 `json:"sendDelivered"`
|
||||
SendFailed int64 `json:"sendFailed"`
|
||||
QueueMessages int64 `json:"queueMessages"`
|
||||
}
|
||||
now := a.now().UTC()
|
||||
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC).Format(time.RFC3339Nano)
|
||||
queries := []struct {
|
||||
q string
|
||||
dest *int64
|
||||
args []any
|
||||
}{
|
||||
{`SELECT COUNT(*) FROM users`, &out.Users},
|
||||
{`SELECT COUNT(*) FROM users WHERE disabled=0`, &out.ActiveUsers},
|
||||
{`SELECT COUNT(*) FROM domains`, &out.Domains},
|
||||
{`SELECT COUNT(*) FROM mailboxes`, &out.Mailboxes},
|
||||
{`SELECT COUNT(*) FROM mailboxes WHERE status='active'`, &out.ActiveMailboxes},
|
||||
{`SELECT COUNT(*) FROM aliases`, &out.Aliases},
|
||||
{`SELECT COUNT(*) FROM messages`, &out.Messages},
|
||||
{`SELECT COUNT(*) FROM messages WHERE is_read=0`, &out.UnreadMessages},
|
||||
{`SELECT COALESCE(SUM(size_bytes),0) FROM messages`, &out.StorageBytes},
|
||||
{q: `SELECT COUNT(*) FROM users`, dest: &out.Users},
|
||||
{q: `SELECT COUNT(*) FROM users WHERE disabled=0`, dest: &out.ActiveUsers},
|
||||
{q: `SELECT COUNT(*) FROM domains`, dest: &out.Domains},
|
||||
{q: `SELECT COUNT(*) FROM mailboxes`, dest: &out.Mailboxes},
|
||||
{q: `SELECT COUNT(*) FROM mailboxes WHERE status='active'`, dest: &out.ActiveMailboxes},
|
||||
{q: `SELECT COUNT(*) FROM aliases`, dest: &out.Aliases},
|
||||
{q: `SELECT COUNT(*) FROM messages`, dest: &out.Messages},
|
||||
{q: `SELECT COUNT(*) FROM messages WHERE is_read=0`, dest: &out.UnreadMessages},
|
||||
{q: `SELECT COALESCE(SUM(size_bytes),0) FROM messages`, dest: &out.StorageBytes},
|
||||
{q: `SELECT COUNT(m.id) FROM messages m JOIN folders f ON f.id=m.folder_id WHERE f.role='sent' AND m.sent_at>=?`, dest: &out.TodaySent, args: []any{todayStart}},
|
||||
{q: `SELECT COUNT(m.id) FROM messages m JOIN folders f ON f.id=m.folder_id WHERE f.role NOT IN ('sent','drafts') AND m.received_at>=?`, dest: &out.TodayReceived, args: []any{todayStart}},
|
||||
{q: `SELECT COUNT(*) FROM send_queue WHERE status=? AND created_at>=?`, dest: &out.SendDelivered, args: []any{sendQueueStatusDelivered, todayStart}},
|
||||
{q: `SELECT COUNT(*) FROM send_queue WHERE status=? AND created_at>=?`, dest: &out.SendFailed, args: []any{sendQueueStatusFailed, todayStart}},
|
||||
{q: `SELECT COUNT(*) FROM send_queue WHERE status IN (?,?)`, dest: &out.QueueMessages, args: []any{sendQueueStatusQueued, sendQueueStatusSending}},
|
||||
}
|
||||
for _, item := range queries {
|
||||
if err := a.db.QueryRowContext(r.Context(), item.q).Scan(item.dest); err != nil {
|
||||
if err := a.db.QueryRowContext(r.Context(), item.q, item.args...).Scan(item.dest); err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "failed to load overview")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
|
||||
"golang.org/x/text/encoding"
|
||||
"golang.org/x/text/encoding/ianaindex"
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
)
|
||||
|
||||
type maildirMailbox struct {
|
||||
@@ -822,6 +823,14 @@ func charsetReader(charset string, input io.Reader) (io.Reader, error) {
|
||||
if charset == "utf-8" || charset == "us-ascii" {
|
||||
return input, nil
|
||||
}
|
||||
// GB2312 is commonly used as a label for GBK-compatible mail content.
|
||||
// ianaindex does not consistently resolve these real-world aliases.
|
||||
switch charset {
|
||||
case "gb2312", "gb_2312-80", "x-gbk", "euc-cn", "cp936", "ms936", "windows-936":
|
||||
return simplifiedchinese.GBK.NewDecoder().Reader(input), nil
|
||||
case "gb18030":
|
||||
return simplifiedchinese.GB18030.NewDecoder().Reader(input), nil
|
||||
}
|
||||
enc, err := ianaindex.IANA.Encoding(charset)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unsupported charset %q: %w", charset, err)
|
||||
|
||||
@@ -2,6 +2,7 @@ package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -355,6 +356,37 @@ func TestTelegramMailboxScopeAndOriginalRecipient(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMaildirMessageDecodesAppleGB2312(t *testing.T) {
|
||||
subject := "验证 Apple 账户电子邮件地址"
|
||||
body := "你的 Apple 验证码是 978534"
|
||||
encodedSubject, err := simplifiedchinese.GBK.NewEncoder().Bytes([]byte(subject))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
encodedBody, err := simplifiedchinese.GBK.NewEncoder().Bytes([]byte(body))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw := []byte("From: Apple <appleid@id.apple.com>\r\n" +
|
||||
"To: admin@example.com\r\n" +
|
||||
"Subject: =?gb2312?B?" + base64.StdEncoding.EncodeToString(encodedSubject) + "?=\r\n" +
|
||||
"Content-Type: text/plain; charset=gb2312\r\n" +
|
||||
"Content-Transfer-Encoding: base64\r\n\r\n" +
|
||||
base64.StdEncoding.EncodeToString(encodedBody))
|
||||
a := newTestApp(t)
|
||||
stopTestWorkers(a)
|
||||
msg, _, err := a.parseMaildirMessage(raw, "admin@example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if msg.Subject != subject {
|
||||
t.Fatalf("GB2312 subject was not decoded: %q", msg.Subject)
|
||||
}
|
||||
if msg.BodyText != body {
|
||||
t.Fatalf("GB2312 body was not decoded: %q", msg.BodyText)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTelegramBadRequestFallsBackToPlainText(t *testing.T) {
|
||||
var calls atomic.Int32
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user