Tabelle imap_account_mails, Importer schreibt die Zuordnung auch bei Duplikaten, Zugriffsregel und Index-Tokens nutzen sie, archivmail imap-link befüllt den Bestand (Trockenlauf Standard, --apply schreibt nur die Zuordnung). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
153 lines
3.7 KiB
Go
153 lines
3.7 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
const imapAccountTokenPrefix = "imapacct"
|
|
|
|
type MailSource struct {
|
|
Type string
|
|
ID *int64
|
|
// PROJ-94: accounts from imap_account_mails, loaded separately because
|
|
// a failed link lookup must not drop the source-based rules.
|
|
Linked []int64
|
|
}
|
|
|
|
func ImapAccountToken(accountID int64) string {
|
|
if accountID <= 0 {
|
|
return ""
|
|
}
|
|
return imapAccountTokenPrefix + strconv.FormatInt(accountID, 10)
|
|
}
|
|
|
|
func (m MailSource) ImapAccountID() (int64, bool) {
|
|
if m.Type != "imap" || m.ID == nil || *m.ID <= 0 {
|
|
return 0, false
|
|
}
|
|
return *m.ID, true
|
|
}
|
|
|
|
func (m MailSource) AccountToken() string {
|
|
id, ok := m.ImapAccountID()
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return ImapAccountToken(id)
|
|
}
|
|
|
|
func (m MailSource) AccountIDs() []int64 {
|
|
var ids []int64
|
|
if id, ok := m.ImapAccountID(); ok {
|
|
ids = append(ids, id)
|
|
}
|
|
for _, id := range m.Linked {
|
|
if id > 0 {
|
|
ids = append(ids, id)
|
|
}
|
|
}
|
|
slices.Sort(ids)
|
|
return slices.Compact(ids)
|
|
}
|
|
|
|
func (m MailSource) AccountTokens() []string {
|
|
ids := m.AccountIDs()
|
|
out := make([]string, 0, len(ids))
|
|
for _, id := range ids {
|
|
out = append(out, ImapAccountToken(id))
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Unknown IDs and legacy mails yield an empty MailSource without error.
|
|
func (s *Store) GetMailSource(ctx context.Context, id string) (MailSource, error) {
|
|
if s.db == nil {
|
|
return MailSource{}, nil
|
|
}
|
|
var st *string
|
|
var sid *int64
|
|
err := s.db.QueryRow(ctx, `SELECT source_type, source_id FROM emails WHERE id = $1`, id).Scan(&st, &sid)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return MailSource{}, nil
|
|
}
|
|
if err != nil {
|
|
return MailSource{}, fmt.Errorf("storage: get mail source: %w", err)
|
|
}
|
|
out := MailSource{ID: sid}
|
|
if st != nil {
|
|
out.Type = *st
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *Store) GetMailSources(ctx context.Context, ids []string) (map[string]MailSource, error) {
|
|
out := make(map[string]MailSource, len(ids))
|
|
if s.db == nil || len(ids) == 0 {
|
|
return out, nil
|
|
}
|
|
rows, err := s.db.Query(ctx,
|
|
`SELECT id, source_type, source_id FROM emails WHERE id = ANY($1) AND source_type IS NOT NULL`, ids)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("storage: get mail sources: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var id, st string
|
|
var sid *int64
|
|
if err := rows.Scan(&id, &st, &sid); err != nil {
|
|
return nil, fmt.Errorf("storage: get mail sources scan: %w", err)
|
|
}
|
|
out[id] = MailSource{Type: st, ID: sid}
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("storage: get mail sources rows: %w", err)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func ownerIdentities(username, email string) []string {
|
|
var out []string
|
|
for _, v := range []string{username, email} {
|
|
v = strings.ToLower(strings.TrimSpace(v))
|
|
if v != "" && !slices.Contains(out, v) {
|
|
out = append(out, v)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// IS NOT DISTINCT FROM: a user without tenant only owns accounts without tenant.
|
|
func (s *Store) ImapAccountIDsForOwner(ctx context.Context, username, email string, tenantID *int64) ([]int64, error) {
|
|
idents := ownerIdentities(username, email)
|
|
if s.db == nil || len(idents) == 0 {
|
|
return nil, nil
|
|
}
|
|
rows, err := s.db.Query(ctx, `
|
|
SELECT id FROM imap_accounts
|
|
WHERE lower(owner) = ANY($1) AND tenant_id IS NOT DISTINCT FROM $2
|
|
ORDER BY id`, idents, tenantID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("storage: imap accounts for owner: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
var ids []int64
|
|
for rows.Next() {
|
|
var id int64
|
|
if err := rows.Scan(&id); err != nil {
|
|
return nil, fmt.Errorf("storage: imap accounts for owner scan: %w", err)
|
|
}
|
|
ids = append(ids, id)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("storage: imap accounts for owner rows: %w", err)
|
|
}
|
|
return ids, nil
|
|
}
|