Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 13332136c2 | |||
| 947e3ad248 |
@@ -427,6 +427,9 @@ func (a *App) migrate(ctx context.Context) error {
|
||||
if err := a.migrateMessageAuthentication(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.rebuildHTMLOnlyMessageSnippets(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.migrateUsersForTwoFactor(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -492,6 +495,47 @@ func (a *App) migrateMessageAuthentication(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) rebuildHTMLOnlyMessageSnippets(ctx context.Context) error {
|
||||
rows, err := a.db.QueryContext(ctx, `SELECT id,body_html,snippet FROM messages WHERE trim(body_text)='' AND body_html<>''`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
type update struct {
|
||||
id string
|
||||
snippet string
|
||||
}
|
||||
updates := []update{}
|
||||
for rows.Next() {
|
||||
var id, bodyHTML, current string
|
||||
if err := rows.Scan(&id, &bodyHTML, ¤t); err != nil {
|
||||
return err
|
||||
}
|
||||
next := snippetFrom("", bodyHTML)
|
||||
if next != current {
|
||||
updates = append(updates, update{id: id, snippet: next})
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(updates) == 0 {
|
||||
return nil
|
||||
}
|
||||
now := a.now().UTC().Format(time.RFC3339Nano)
|
||||
tx, err := a.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
for _, item := range updates {
|
||||
if _, err := tx.ExecContext(ctx, `UPDATE messages SET snippet=?,updated_at=? WHERE id=?`, item.snippet, now, item.id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (a *App) migrateSendQueueMessageID(ctx context.Context) error {
|
||||
rows, err := a.db.QueryContext(ctx, `PRAGMA table_info(send_queue)`)
|
||||
if err != nil {
|
||||
|
||||
@@ -442,6 +442,46 @@ func TestParseMailAuthenticationResults(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSnippetFromHTMLIgnoresStyleContent(t *testing.T) {
|
||||
html := `<html><head><style>body { margin: 0; padding: 24px; background: #f4f4f5; }</style><title>Hidden title</title></head><body><p>蓝钦AI 余额充值成功</p></body></html>`
|
||||
got := snippetFrom("", html)
|
||||
if strings.Contains(got, "body {") || strings.Contains(got, "margin:") || strings.Contains(got, "Hidden title") {
|
||||
t.Fatalf("snippet kept non-content html text: %q", got)
|
||||
}
|
||||
if !strings.Contains(got, "蓝钦AI 余额充值成功") {
|
||||
t.Fatalf("snippet missing body text: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRebuildHTMLOnlyMessageSnippetsDropsStyleContent(t *testing.T) {
|
||||
a := newTestApp(t)
|
||||
ctx := context.Background()
|
||||
_, mb := defaultAdminUserAndMailbox(t, a)
|
||||
folderID, err := a.ensureFolder(ctx, mb.ID, "Sent")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
now := a.now().UTC().Format(time.RFC3339Nano)
|
||||
bodyHTML := `<html><head><style>body { margin: 0; padding: 24px; background: #f4f4f5; }</style></head><body><p>hello readable body</p></body></html>`
|
||||
if _, err := a.db.ExecContext(ctx, `INSERT INTO messages(id,mailbox_id,folder_id,recipient_addr,message_uid,message_id,subject,from_addr,from_name,to_addrs,cc_addrs,bcc_addrs,sent_at,received_at,snippet,body_text,body_html,is_read,is_starred,has_attachments,size_bytes,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, "msg_css_snippet", mb.ID, folderID, "", newID("uid"), "<css-snippet@example.test>", "css", mb.Address, "", "[]", "[]", "[]", now, now, "body { margin: 0; padding: 24px; }", "", bodyHTML, 1, 0, 0, int64(len(bodyHTML)), now, now); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := a.rebuildHTMLOnlyMessageSnippets(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var snippet string
|
||||
if err := a.db.QueryRowContext(ctx, `SELECT snippet FROM messages WHERE id='msg_css_snippet'`).Scan(&snippet); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(snippet, "body {") || strings.Contains(snippet, "margin:") {
|
||||
t.Fatalf("snippet was not rebuilt: %q", snippet)
|
||||
}
|
||||
if snippet != "hello readable body" {
|
||||
t.Fatalf("snippet=%q, want body text", snippet)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMailRulesConditionGroupsAndActions(t *testing.T) {
|
||||
a := newTestApp(t)
|
||||
ts := httptest.NewServer(a.Router())
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build !unix
|
||||
|
||||
package app
|
||||
|
||||
func applyMaildirOwnership(path string) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
//go:build unix
|
||||
|
||||
package app
|
||||
|
||||
import "os"
|
||||
|
||||
const (
|
||||
maildirOwnerUID = 5000
|
||||
maildirOwnerGID = 5000
|
||||
)
|
||||
|
||||
func applyMaildirOwnership(path string) error {
|
||||
if os.Geteuid() != 0 {
|
||||
return nil
|
||||
}
|
||||
return os.Chown(path, maildirOwnerUID, maildirOwnerGID)
|
||||
}
|
||||
@@ -120,7 +120,7 @@ func (a *App) writeRawMessageToMaildirFolder(ctx context.Context, messageID, fol
|
||||
if strings.EqualFold(folderName, "Inbox") && !state.IsRead {
|
||||
subdir = "new"
|
||||
}
|
||||
if err := ensureMaildirFolderDirs(folderBase); err != nil {
|
||||
if err := ensureMaildirFolderDirs(base, folderBase); err != nil {
|
||||
return err
|
||||
}
|
||||
filename := maildirFilename(messageID, state.MessageID)
|
||||
@@ -130,6 +130,10 @@ func (a *App) writeRawMessageToMaildirFolder(ctx context.Context, messageID, fol
|
||||
if err := os.WriteFile(tmpPath, raw, 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := applyMaildirOwnership(tmpPath); err != nil {
|
||||
_ = os.Remove(tmpPath)
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(tmpPath, finalPath); err != nil {
|
||||
_ = os.Remove(tmpPath)
|
||||
return err
|
||||
@@ -213,7 +217,7 @@ func (a *App) moveMessageMaildir(ctx context.Context, messageID, targetFolderID
|
||||
}
|
||||
base := filepath.Join(strings.TrimSpace(a.cfg.MaildirRoot), mb.Domain, mb.LocalPart, "Maildir")
|
||||
folderBase := maildirFolderPath(base, folderName)
|
||||
if err := ensureMaildirFolderDirs(folderBase); err != nil {
|
||||
if err := ensureMaildirFolderDirs(base, folderBase); err != nil {
|
||||
return err
|
||||
}
|
||||
subdir := "cur"
|
||||
@@ -225,6 +229,9 @@ func (a *App) moveMessageMaildir(ctx context.Context, messageID, targetFolderID
|
||||
if err := os.Rename(state.RawPath, targetPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := applyMaildirOwnership(targetPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if state.FolderID != "" && state.FolderID != targetFolderID {
|
||||
if _, err := a.bumpFolderModSeq(ctx, state.FolderID); err != nil {
|
||||
@@ -318,6 +325,9 @@ func (a *App) updateMessageMaildirFlags(ctx context.Context, messageID string, r
|
||||
if err := os.Rename(state.RawPath, targetPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := applyMaildirOwnership(targetPath); err != nil {
|
||||
return err
|
||||
}
|
||||
modSeq, err := a.bumpFolderModSeq(ctx, state.FolderID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -481,15 +491,33 @@ func (a *App) pathIsUnderMaildirRoot(path string) (bool, error) {
|
||||
return rel != "." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)) && rel != "..", nil
|
||||
}
|
||||
|
||||
func ensureMaildirFolderDirs(folderBase string) error {
|
||||
func ensureMaildirFolderDirs(base, folderBase string) error {
|
||||
for _, sub := range []string{"tmp", "new", "cur"} {
|
||||
if err := os.MkdirAll(filepath.Join(folderBase, sub), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, dir := range maildirOwnershipDirs(base, folderBase) {
|
||||
if err := applyMaildirOwnership(dir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func maildirOwnershipDirs(base, folderBase string) []string {
|
||||
dirs := []string{
|
||||
filepath.Dir(filepath.Dir(base)),
|
||||
filepath.Dir(base),
|
||||
base,
|
||||
}
|
||||
if filepath.Clean(folderBase) != filepath.Clean(base) {
|
||||
dirs = append(dirs, folderBase)
|
||||
}
|
||||
dirs = append(dirs, filepath.Join(folderBase, "tmp"), filepath.Join(folderBase, "new"), filepath.Join(folderBase, "cur"))
|
||||
return dirs
|
||||
}
|
||||
|
||||
func maildirFilename(messageID, headerMessageID string) string {
|
||||
base := strings.TrimSpace(headerMessageID)
|
||||
if base == "" {
|
||||
|
||||
@@ -53,6 +53,7 @@ func (p *HTMLPolicy) Sanitize(s string) string {
|
||||
}
|
||||
|
||||
var emailStyleTagRe = regexp.MustCompile(`(?is)<style\b([^>]*)>(.*?)</style>`)
|
||||
var htmlNonContentTagRe = regexp.MustCompile(`(?is)<(style|script|head|title|noscript)\b[^>]*>.*?</\s*(style|script|head|title|noscript)\s*>`)
|
||||
|
||||
func extractSafeEmailStyles(value string) ([]string, string) {
|
||||
styles := []string{}
|
||||
@@ -241,6 +242,7 @@ func snippetFrom(text, html string) string {
|
||||
}
|
||||
|
||||
func stripTags(s string) string {
|
||||
s = htmlNonContentTagRe.ReplaceAllString(s, " ")
|
||||
var b strings.Builder
|
||||
inTag := false
|
||||
for _, r := range s {
|
||||
|
||||
Reference in New Issue
Block a user