454 lines
16 KiB
Go
454 lines
16 KiB
Go
|
|
package app
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"errors"
|
||
|
|
"net/http"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/go-chi/chi/v5"
|
||
|
|
)
|
||
|
|
|
||
|
|
func (a *App) handleListContacts(w http.ResponseWriter, r *http.Request) {
|
||
|
|
user := currentUser(r)
|
||
|
|
rows, err := a.db.QueryContext(r.Context(), `SELECT id,user_id,name,email,note,created_at FROM contacts WHERE user_id=? ORDER BY name,email`, user.ID)
|
||
|
|
if err != nil {
|
||
|
|
respondError(w, http.StatusInternalServerError, "failed to load contacts")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
defer rows.Close()
|
||
|
|
items := []Contact{}
|
||
|
|
for rows.Next() {
|
||
|
|
item, err := scanContact(rows)
|
||
|
|
if err != nil {
|
||
|
|
respondError(w, http.StatusInternalServerError, "failed to scan contacts")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
items = append(items, item)
|
||
|
|
}
|
||
|
|
respondJSON(w, http.StatusOK, map[string]any{"items": items})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (a *App) handleCreateContact(w http.ResponseWriter, r *http.Request) {
|
||
|
|
user := currentUser(r)
|
||
|
|
var req struct {
|
||
|
|
Name string `json:"name"`
|
||
|
|
Email string `json:"email"`
|
||
|
|
Note string `json:"note"`
|
||
|
|
}
|
||
|
|
if err := decodeJSON(r, &req); err != nil {
|
||
|
|
badRequest(w, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
email := normalizeEmail(req.Email)
|
||
|
|
if email == "" || !strings.Contains(email, "@") {
|
||
|
|
badRequest(w, errors.New("invalid email"))
|
||
|
|
return
|
||
|
|
}
|
||
|
|
name := strings.TrimSpace(req.Name)
|
||
|
|
if name == "" {
|
||
|
|
name = email
|
||
|
|
}
|
||
|
|
id := newID("ctc")
|
||
|
|
now := a.now().UTC().Format(time.RFC3339Nano)
|
||
|
|
_, err := a.db.ExecContext(r.Context(), `INSERT INTO contacts(id,user_id,name,email,note,created_at,updated_at)
|
||
|
|
VALUES(?,?,?,?,?,?,?)
|
||
|
|
ON CONFLICT(user_id,email) DO UPDATE SET name=excluded.name,note=excluded.note,updated_at=excluded.updated_at`,
|
||
|
|
id, user.ID, name, email, strings.TrimSpace(req.Note), now, now)
|
||
|
|
if err != nil {
|
||
|
|
badRequest(w, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
row := a.db.QueryRowContext(r.Context(), `SELECT id,user_id,name,email,note,created_at FROM contacts WHERE user_id=? AND email=?`, user.ID, email)
|
||
|
|
item, err := scanContact(row)
|
||
|
|
if err != nil {
|
||
|
|
respondError(w, http.StatusInternalServerError, "failed to load contact")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
respondJSON(w, http.StatusCreated, item)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (a *App) handleDeleteContact(w http.ResponseWriter, r *http.Request) {
|
||
|
|
user := currentUser(r)
|
||
|
|
res, err := a.db.ExecContext(r.Context(), `DELETE FROM contacts WHERE id=? AND user_id=?`, chi.URLParam(r, "id"), user.ID)
|
||
|
|
if err != nil {
|
||
|
|
respondError(w, http.StatusInternalServerError, "failed to delete contact")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if n, _ := res.RowsAffected(); n == 0 {
|
||
|
|
respondError(w, http.StatusNotFound, "contact not found")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
respondJSON(w, http.StatusOK, map[string]any{"ok": true})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (a *App) handleListRules(w http.ResponseWriter, r *http.Request) {
|
||
|
|
user := currentUser(r)
|
||
|
|
rows, err := a.db.QueryContext(r.Context(), `SELECT id,user_id,mailbox_id,name,from_contains,subject_contains,action,enabled,created_at FROM mail_rules WHERE user_id=? ORDER BY created_at DESC`, user.ID)
|
||
|
|
if err != nil {
|
||
|
|
respondError(w, http.StatusInternalServerError, "failed to load rules")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
defer rows.Close()
|
||
|
|
items := []MailRule{}
|
||
|
|
for rows.Next() {
|
||
|
|
item, err := scanRule(rows)
|
||
|
|
if err != nil {
|
||
|
|
respondError(w, http.StatusInternalServerError, "failed to scan rules")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
items = append(items, item)
|
||
|
|
}
|
||
|
|
respondJSON(w, http.StatusOK, map[string]any{"items": items})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (a *App) handleCreateRule(w http.ResponseWriter, r *http.Request) {
|
||
|
|
user := currentUser(r)
|
||
|
|
var req struct {
|
||
|
|
MailboxID string `json:"mailboxId"`
|
||
|
|
Name string `json:"name"`
|
||
|
|
FromContains string `json:"fromContains"`
|
||
|
|
SubjectContains string `json:"subjectContains"`
|
||
|
|
Action string `json:"action"`
|
||
|
|
Enabled *bool `json:"enabled"`
|
||
|
|
}
|
||
|
|
if err := decodeJSON(r, &req); err != nil {
|
||
|
|
badRequest(w, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
mailboxID, ok := a.optionalMailboxIDForUser(r, req.MailboxID)
|
||
|
|
if !ok {
|
||
|
|
respondError(w, http.StatusNotFound, "mailbox not found")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
action := strings.TrimSpace(req.Action)
|
||
|
|
if action != "archive" && action != "trash" && action != "star" && action != "mark-read" {
|
||
|
|
badRequest(w, errors.New("invalid rule action"))
|
||
|
|
return
|
||
|
|
}
|
||
|
|
fromContains := strings.TrimSpace(req.FromContains)
|
||
|
|
subjectContains := strings.TrimSpace(req.SubjectContains)
|
||
|
|
if fromContains == "" && subjectContains == "" {
|
||
|
|
badRequest(w, errors.New("rule condition is required"))
|
||
|
|
return
|
||
|
|
}
|
||
|
|
name := strings.TrimSpace(req.Name)
|
||
|
|
if name == "" {
|
||
|
|
name = "收件规则"
|
||
|
|
}
|
||
|
|
enabled := true
|
||
|
|
if req.Enabled != nil {
|
||
|
|
enabled = *req.Enabled
|
||
|
|
}
|
||
|
|
id := newID("rule")
|
||
|
|
now := a.now().UTC().Format(time.RFC3339Nano)
|
||
|
|
_, err := a.db.ExecContext(r.Context(), `INSERT INTO mail_rules(id,user_id,mailbox_id,name,from_contains,subject_contains,action,enabled,created_at,updated_at)
|
||
|
|
VALUES(?,?,?,?,?,?,?,?,?,?)`, id, user.ID, mailboxID, name, fromContains, subjectContains, action, boolInt(enabled), now, now)
|
||
|
|
if err != nil {
|
||
|
|
badRequest(w, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
row := a.db.QueryRowContext(r.Context(), `SELECT id,user_id,mailbox_id,name,from_contains,subject_contains,action,enabled,created_at FROM mail_rules WHERE id=?`, id)
|
||
|
|
item, err := scanRule(row)
|
||
|
|
if err != nil {
|
||
|
|
respondError(w, http.StatusInternalServerError, "failed to load rule")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
respondJSON(w, http.StatusCreated, item)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (a *App) handleDeleteRule(w http.ResponseWriter, r *http.Request) {
|
||
|
|
user := currentUser(r)
|
||
|
|
res, err := a.db.ExecContext(r.Context(), `DELETE FROM mail_rules WHERE id=? AND user_id=?`, chi.URLParam(r, "id"), user.ID)
|
||
|
|
if err != nil {
|
||
|
|
respondError(w, http.StatusInternalServerError, "failed to delete rule")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if n, _ := res.RowsAffected(); n == 0 {
|
||
|
|
respondError(w, http.StatusNotFound, "rule not found")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
respondJSON(w, http.StatusOK, map[string]any{"ok": true})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (a *App) handleListBlockedSenders(w http.ResponseWriter, r *http.Request) {
|
||
|
|
user := currentUser(r)
|
||
|
|
rows, err := a.db.QueryContext(r.Context(), `SELECT id,user_id,mailbox_id,email,reason,created_at FROM blocked_senders WHERE user_id=? ORDER BY created_at DESC`, user.ID)
|
||
|
|
if err != nil {
|
||
|
|
respondError(w, http.StatusInternalServerError, "failed to load blocked senders")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
defer rows.Close()
|
||
|
|
items := []BlockedSender{}
|
||
|
|
for rows.Next() {
|
||
|
|
item, err := scanBlockedSender(rows)
|
||
|
|
if err != nil {
|
||
|
|
respondError(w, http.StatusInternalServerError, "failed to scan blocked senders")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
items = append(items, item)
|
||
|
|
}
|
||
|
|
respondJSON(w, http.StatusOK, map[string]any{"items": items})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (a *App) handleCreateBlockedSender(w http.ResponseWriter, r *http.Request) {
|
||
|
|
user := currentUser(r)
|
||
|
|
var req struct {
|
||
|
|
MailboxID string `json:"mailboxId"`
|
||
|
|
Email string `json:"email"`
|
||
|
|
Reason string `json:"reason"`
|
||
|
|
}
|
||
|
|
if err := decodeJSON(r, &req); err != nil {
|
||
|
|
badRequest(w, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
mailboxID, ok := a.optionalMailboxIDForUser(r, req.MailboxID)
|
||
|
|
if !ok {
|
||
|
|
respondError(w, http.StatusNotFound, "mailbox not found")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
email := normalizeEmail(req.Email)
|
||
|
|
if email == "" || !strings.Contains(email, "@") {
|
||
|
|
badRequest(w, errors.New("invalid email"))
|
||
|
|
return
|
||
|
|
}
|
||
|
|
id := newID("blk")
|
||
|
|
now := a.now().UTC().Format(time.RFC3339Nano)
|
||
|
|
_, err := a.db.ExecContext(r.Context(), `INSERT INTO blocked_senders(id,user_id,mailbox_id,email,reason,created_at,updated_at)
|
||
|
|
VALUES(?,?,?,?,?,?,?)
|
||
|
|
ON CONFLICT(user_id,mailbox_id,email) DO UPDATE SET reason=excluded.reason,updated_at=excluded.updated_at`,
|
||
|
|
id, user.ID, mailboxID, email, strings.TrimSpace(req.Reason), now, now)
|
||
|
|
if err != nil {
|
||
|
|
badRequest(w, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
row := a.db.QueryRowContext(r.Context(), `SELECT id,user_id,mailbox_id,email,reason,created_at FROM blocked_senders WHERE user_id=? AND mailbox_id=? AND email=?`, user.ID, mailboxID, email)
|
||
|
|
item, err := scanBlockedSender(row)
|
||
|
|
if err != nil {
|
||
|
|
respondError(w, http.StatusInternalServerError, "failed to load blocked sender")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
respondJSON(w, http.StatusCreated, item)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (a *App) handleDeleteBlockedSender(w http.ResponseWriter, r *http.Request) {
|
||
|
|
user := currentUser(r)
|
||
|
|
res, err := a.db.ExecContext(r.Context(), `DELETE FROM blocked_senders WHERE id=? AND user_id=?`, chi.URLParam(r, "id"), user.ID)
|
||
|
|
if err != nil {
|
||
|
|
respondError(w, http.StatusInternalServerError, "failed to delete blocked sender")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if n, _ := res.RowsAffected(); n == 0 {
|
||
|
|
respondError(w, http.StatusNotFound, "blocked sender not found")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
respondJSON(w, http.StatusOK, map[string]any{"ok": true})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (a *App) handleMailStats(w http.ResponseWriter, r *http.Request) {
|
||
|
|
user := currentUser(r)
|
||
|
|
mailboxID := strings.TrimSpace(r.URL.Query().Get("mailboxId"))
|
||
|
|
args := []any{user.ID}
|
||
|
|
where := `mb.user_id=?`
|
||
|
|
if mailboxID != "" {
|
||
|
|
if _, err := a.mailboxForCurrentUserWithID(r, mailboxID); err != nil {
|
||
|
|
respondError(w, http.StatusNotFound, "mailbox not found")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
where += ` AND mb.id=?`
|
||
|
|
args = append(args, mailboxID)
|
||
|
|
}
|
||
|
|
stats := MailStats{ByFolder: []MailStatsFolderCount{}}
|
||
|
|
row := a.db.QueryRowContext(r.Context(), `SELECT COUNT(m.id),COALESCE(SUM(CASE WHEN m.is_read=0 THEN 1 ELSE 0 END),0),COALESCE(SUM(CASE WHEN m.is_starred=1 THEN 1 ELSE 0 END),0),COALESCE(SUM(m.size_bytes),0)
|
||
|
|
FROM mailboxes mb LEFT JOIN messages m ON m.mailbox_id=mb.id WHERE `+where, args...)
|
||
|
|
if err := row.Scan(&stats.TotalMessages, &stats.UnreadMessages, &stats.StarredMessages, &stats.StorageBytes); err != nil {
|
||
|
|
respondError(w, http.StatusInternalServerError, "failed to load stats")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if err := a.db.QueryRowContext(r.Context(), `SELECT COUNT(a.id) FROM attachments a JOIN messages m ON m.id=a.message_id JOIN mailboxes mb ON mb.id=m.mailbox_id WHERE `+where, args...).Scan(&stats.AttachmentCount); err != nil {
|
||
|
|
respondError(w, http.StatusInternalServerError, "failed to load attachment stats")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
rows, err := a.db.QueryContext(r.Context(), `SELECT f.name,f.role,COUNT(m.id),COALESCE(SUM(CASE WHEN m.is_read=0 THEN 1 ELSE 0 END),0),COALESCE(SUM(m.size_bytes),0)
|
||
|
|
FROM mailboxes mb JOIN folders f ON f.mailbox_id=mb.id LEFT JOIN messages m ON m.folder_id=f.id
|
||
|
|
WHERE `+where+` GROUP BY f.id,f.name,f.role ORDER BY CASE f.role WHEN 'inbox' THEN 1 WHEN 'sent' THEN 2 WHEN 'drafts' THEN 3 WHEN 'archive' THEN 4 WHEN 'spam' THEN 5 WHEN 'trash' THEN 6 ELSE 99 END`, args...)
|
||
|
|
if err != nil {
|
||
|
|
respondError(w, http.StatusInternalServerError, "failed to load folder stats")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
defer rows.Close()
|
||
|
|
for rows.Next() {
|
||
|
|
var item MailStatsFolderCount
|
||
|
|
if err := rows.Scan(&item.Folder, &item.Role, &item.Count, &item.Unread, &item.Bytes); err != nil {
|
||
|
|
respondError(w, http.StatusInternalServerError, "failed to scan folder stats")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
stats.ByFolder = append(stats.ByFolder, item)
|
||
|
|
}
|
||
|
|
respondJSON(w, http.StatusOK, stats)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (a *App) handleMailCleanup(w http.ResponseWriter, r *http.Request) {
|
||
|
|
var req struct {
|
||
|
|
MailboxID string `json:"mailboxId"`
|
||
|
|
Target string `json:"target"`
|
||
|
|
}
|
||
|
|
if err := decodeJSON(r, &req); err != nil {
|
||
|
|
badRequest(w, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
mb, err := a.mailboxForCurrentUserWithID(r, req.MailboxID)
|
||
|
|
if err != nil {
|
||
|
|
respondError(w, http.StatusNotFound, "mailbox not found")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
target := strings.TrimSpace(req.Target)
|
||
|
|
affected := int64(0)
|
||
|
|
switch target {
|
||
|
|
case "empty-trash":
|
||
|
|
affected, err = a.deleteMessagesInFolder(r.Context(), mb.ID, "Trash")
|
||
|
|
case "empty-spam":
|
||
|
|
affected, err = a.deleteMessagesInFolder(r.Context(), mb.ID, "Spam")
|
||
|
|
case "archive-read-inbox":
|
||
|
|
affected, err = a.archiveReadInbox(r.Context(), mb.ID)
|
||
|
|
default:
|
||
|
|
badRequest(w, errors.New("invalid cleanup target"))
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if err != nil {
|
||
|
|
respondError(w, http.StatusInternalServerError, "failed to cleanup messages")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
respondJSON(w, http.StatusOK, map[string]any{"ok": true, "affected": affected})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (a *App) optionalMailboxIDForUser(r *http.Request, mailboxID string) (string, bool) {
|
||
|
|
mailboxID = strings.TrimSpace(mailboxID)
|
||
|
|
if mailboxID == "" || mailboxID == "all" {
|
||
|
|
return "", true
|
||
|
|
}
|
||
|
|
_, err := a.mailboxForCurrentUserWithID(r, mailboxID)
|
||
|
|
return mailboxID, err == nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (a *App) deleteMessagesInFolder(ctx context.Context, mailboxID, folder string) (int64, error) {
|
||
|
|
folderID, err := a.ensureFolder(ctx, mailboxID, folder)
|
||
|
|
if err != nil {
|
||
|
|
return 0, err
|
||
|
|
}
|
||
|
|
rows, err := a.db.QueryContext(ctx, `SELECT id FROM messages WHERE mailbox_id=? AND folder_id=?`, mailboxID, folderID)
|
||
|
|
if err != nil {
|
||
|
|
return 0, err
|
||
|
|
}
|
||
|
|
defer rows.Close()
|
||
|
|
ids := []string{}
|
||
|
|
for rows.Next() {
|
||
|
|
var id string
|
||
|
|
if err := rows.Scan(&id); err != nil {
|
||
|
|
return 0, err
|
||
|
|
}
|
||
|
|
ids = append(ids, id)
|
||
|
|
}
|
||
|
|
for _, id := range ids {
|
||
|
|
a.deleteMessageFiles(ctx, id)
|
||
|
|
if _, err := a.db.ExecContext(ctx, `DELETE FROM messages WHERE id=?`, id); err != nil {
|
||
|
|
return 0, err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return int64(len(ids)), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (a *App) archiveReadInbox(ctx context.Context, mailboxID string) (int64, error) {
|
||
|
|
inboxID, err := a.ensureFolder(ctx, mailboxID, "Inbox")
|
||
|
|
if err != nil {
|
||
|
|
return 0, err
|
||
|
|
}
|
||
|
|
archiveID, err := a.ensureFolder(ctx, mailboxID, "Archive")
|
||
|
|
if err != nil {
|
||
|
|
return 0, err
|
||
|
|
}
|
||
|
|
res, err := a.db.ExecContext(ctx, `UPDATE messages SET folder_id=?, updated_at=? WHERE mailbox_id=? AND folder_id=? AND is_read=1`,
|
||
|
|
archiveID, a.now().UTC().Format(time.RFC3339Nano), mailboxID, inboxID)
|
||
|
|
if err != nil {
|
||
|
|
return 0, err
|
||
|
|
}
|
||
|
|
n, _ := res.RowsAffected()
|
||
|
|
return n, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func scanContact(row messageSummaryScanner) (Contact, error) {
|
||
|
|
var item Contact
|
||
|
|
var created string
|
||
|
|
err := row.Scan(&item.ID, &item.UserID, &item.Name, &item.Email, &item.Note, &created)
|
||
|
|
item.CreatedAt = parseTime(created)
|
||
|
|
return item, err
|
||
|
|
}
|
||
|
|
|
||
|
|
func scanRule(row messageSummaryScanner) (MailRule, error) {
|
||
|
|
var item MailRule
|
||
|
|
var enabled int
|
||
|
|
var created string
|
||
|
|
err := row.Scan(&item.ID, &item.UserID, &item.MailboxID, &item.Name, &item.FromContains, &item.SubjectContains, &item.Action, &enabled, &created)
|
||
|
|
item.Enabled = intBool(enabled)
|
||
|
|
item.CreatedAt = parseTime(created)
|
||
|
|
return item, err
|
||
|
|
}
|
||
|
|
|
||
|
|
func scanBlockedSender(row messageSummaryScanner) (BlockedSender, error) {
|
||
|
|
var item BlockedSender
|
||
|
|
var created string
|
||
|
|
err := row.Scan(&item.ID, &item.UserID, &item.MailboxID, &item.Email, &item.Reason, &created)
|
||
|
|
item.CreatedAt = parseTime(created)
|
||
|
|
return item, err
|
||
|
|
}
|
||
|
|
|
||
|
|
func (a *App) applyInboundControls(ctx context.Context, messageID, mailboxID, from, subject string) {
|
||
|
|
var userID string
|
||
|
|
if err := a.db.QueryRowContext(ctx, `SELECT user_id FROM mailboxes WHERE id=?`, mailboxID).Scan(&userID); err != nil {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
from = normalizeEmail(from)
|
||
|
|
var blocked int
|
||
|
|
_ = a.db.QueryRowContext(ctx, `SELECT COUNT(1) FROM blocked_senders WHERE user_id=? AND (mailbox_id='' OR mailbox_id=?) AND email=?`, userID, mailboxID, from).Scan(&blocked)
|
||
|
|
if blocked > 0 {
|
||
|
|
if spamID, err := a.ensureFolder(ctx, mailboxID, "Spam"); err == nil {
|
||
|
|
_, _ = a.db.ExecContext(ctx, `UPDATE messages SET folder_id=?, updated_at=? WHERE id=?`, spamID, a.now().UTC().Format(time.RFC3339Nano), messageID)
|
||
|
|
}
|
||
|
|
return
|
||
|
|
}
|
||
|
|
rows, err := a.db.QueryContext(ctx, `SELECT from_contains,subject_contains,action FROM mail_rules WHERE user_id=? AND (mailbox_id='' OR mailbox_id=?) AND enabled=1 ORDER BY created_at`, userID, mailboxID)
|
||
|
|
if err != nil {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
defer rows.Close()
|
||
|
|
lowerFrom := strings.ToLower(from)
|
||
|
|
lowerSubject := strings.ToLower(subject)
|
||
|
|
for rows.Next() {
|
||
|
|
var fromContains, subjectContains, action string
|
||
|
|
if rows.Scan(&fromContains, &subjectContains, &action) != nil {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if fromContains != "" && !strings.Contains(lowerFrom, strings.ToLower(fromContains)) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if subjectContains != "" && !strings.Contains(lowerSubject, strings.ToLower(subjectContains)) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
switch action {
|
||
|
|
case "archive":
|
||
|
|
if folderID, err := a.ensureFolder(ctx, mailboxID, "Archive"); err == nil {
|
||
|
|
_, _ = a.db.ExecContext(ctx, `UPDATE messages SET folder_id=?, updated_at=? WHERE id=?`, folderID, a.now().UTC().Format(time.RFC3339Nano), messageID)
|
||
|
|
}
|
||
|
|
case "trash":
|
||
|
|
if folderID, err := a.ensureFolder(ctx, mailboxID, "Trash"); err == nil {
|
||
|
|
_, _ = a.db.ExecContext(ctx, `UPDATE messages SET folder_id=?, updated_at=? WHERE id=?`, folderID, a.now().UTC().Format(time.RFC3339Nano), messageID)
|
||
|
|
}
|
||
|
|
case "star":
|
||
|
|
_, _ = a.db.ExecContext(ctx, `UPDATE messages SET is_starred=1, updated_at=? WHERE id=?`, a.now().UTC().Format(time.RFC3339Nano), messageID)
|
||
|
|
case "mark-read":
|
||
|
|
_, _ = a.db.ExecContext(ctx, `UPDATE messages SET is_read=1, updated_at=? WHERE id=?`, a.now().UTC().Format(time.RFC3339Nano), messageID)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|