Add CLI INBOX migration breakthrough
This commit is contained in:
parent
8a6160817b
commit
27206930f0
6 changed files with 713 additions and 43 deletions
|
|
@ -2,6 +2,9 @@ package backend
|
|||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
|
@ -11,27 +14,27 @@ var DB *sql.DB
|
|||
// Account = eine Umzugs-Zeile: Quelle (alt) -> Ziel (neu), frei gemapptes
|
||||
// Zieladress-Postfach. Wird im Browser gepflegt (Tabelle accounts).
|
||||
type Account struct {
|
||||
ID int64
|
||||
Name string // z.B. "hans-peter"
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"` // z.B. "hans-peter"
|
||||
// Quelle (alter Provider). Security: "tls" (implizit, 993/995),
|
||||
// "starttls" (Klartext-Port 143/110 + Upgrade), "none" (reiner Klartext).
|
||||
// SrcInsecure = ungueltige/selbstsignierte Zerts akzeptieren (alte Hoster).
|
||||
SrcHost string
|
||||
SrcPort int
|
||||
SrcSecurity string
|
||||
SrcInsecure bool
|
||||
SrcUser string // hans-peter@dr-gold.de
|
||||
SrcPass string
|
||||
SrcProto string // "imap" (default) oder "pop3" (Fallback-Quelle)
|
||||
SrcHost string `json:"src_host"`
|
||||
SrcPort int `json:"src_port"`
|
||||
SrcSecurity string `json:"src_security"`
|
||||
SrcInsecure bool `json:"src_insecure"`
|
||||
SrcUser string `json:"src_user"` // hans-peter@dr-gold.de
|
||||
SrcPass string `json:"src_pass"`
|
||||
SrcProto string `json:"src_proto"` // "imap" (default) oder "pop3" (Fallback-Quelle)
|
||||
// Ziel (neuer Provider). Meist "tls"; "none"/"starttls" ebenso moeglich.
|
||||
DstHost string
|
||||
DstPort int
|
||||
DstSecurity string
|
||||
DstInsecure bool
|
||||
DstUser string // archiv-hans-peter@dr-gold.com
|
||||
DstPass string
|
||||
MboxDir string // Unterordner unter Cfg.MboxRoot; leer = Cfg.MboxRoot/Name
|
||||
Active bool
|
||||
DstHost string `json:"dst_host"`
|
||||
DstPort int `json:"dst_port"`
|
||||
DstSecurity string `json:"dst_security"`
|
||||
DstInsecure bool `json:"dst_insecure"`
|
||||
DstUser string `json:"dst_user"` // archiv-hans-peter@dr-gold.com
|
||||
DstPass string `json:"dst_pass"`
|
||||
MboxDir string `json:"mbox_dir"` // Unterordner unter Cfg.MboxRoot; leer = Cfg.MboxRoot/Name
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
// ConnectDB oeffnet die SQLite-DB (modernc, kein cgo) und legt das Schema an.
|
||||
|
|
@ -49,11 +52,202 @@ type Account struct {
|
|||
// jobs(account_id, started, finished, -- Lauf-Historie fuer den Fortschritt
|
||||
// total, done, errors, state) im Browser.
|
||||
func ConnectDB() error {
|
||||
// TODO Codex: sql.Open("sqlite", Cfg.DBPath), PRAGMA journal_mode=WAL,
|
||||
// CREATE TABLE IF NOT EXISTS ... siehe Schema oben.
|
||||
if Cfg.DBPath == "" {
|
||||
Cfg.DBPath = "emailforwarder.db"
|
||||
}
|
||||
db, err := sql.Open("sqlite", Cfg.DBPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := db.Exec(`PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;`); err != nil {
|
||||
_ = db.Close()
|
||||
return err
|
||||
}
|
||||
schema := []string{
|
||||
`CREATE TABLE IF NOT EXISTS accounts(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
src_host TEXT NOT NULL,
|
||||
src_port INTEGER NOT NULL,
|
||||
src_security TEXT NOT NULL DEFAULT 'tls',
|
||||
src_insecure INTEGER NOT NULL DEFAULT 0,
|
||||
src_user TEXT NOT NULL,
|
||||
src_pass TEXT NOT NULL,
|
||||
src_proto TEXT NOT NULL DEFAULT 'imap',
|
||||
dst_host TEXT NOT NULL,
|
||||
dst_port INTEGER NOT NULL,
|
||||
dst_security TEXT NOT NULL DEFAULT 'tls',
|
||||
dst_insecure INTEGER NOT NULL DEFAULT 0,
|
||||
dst_user TEXT NOT NULL,
|
||||
dst_pass TEXT NOT NULL,
|
||||
mbox_dir TEXT NOT NULL DEFAULT '',
|
||||
active INTEGER NOT NULL DEFAULT 1
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS folder_map(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
||||
src_folder TEXT NOT NULL,
|
||||
dst_folder TEXT NOT NULL,
|
||||
UNIQUE(account_id, src_folder)
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS copied(
|
||||
account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
||||
folder TEXT NOT NULL,
|
||||
message_id TEXT NOT NULL,
|
||||
copied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(account_id, folder, message_id)
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS jobs(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
||||
started TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
finished TEXT,
|
||||
total INTEGER NOT NULL DEFAULT 0,
|
||||
done INTEGER NOT NULL DEFAULT 0,
|
||||
errors INTEGER NOT NULL DEFAULT 0,
|
||||
state TEXT NOT NULL DEFAULT 'running'
|
||||
)`,
|
||||
}
|
||||
for _, stmt := range schema {
|
||||
if _, err := db.Exec(stmt); err != nil {
|
||||
_ = db.Close()
|
||||
return err
|
||||
}
|
||||
}
|
||||
DB = db
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO Codex: ListAccounts / SaveAccount / DeleteAccount / GetAccount,
|
||||
// AlreadyCopied(accountID, folder, messageID) bool,
|
||||
// MarkCopied(accountID, folder, messageID).
|
||||
func ListAccounts() ([]Account, error) {
|
||||
rows, err := DB.Query(`SELECT id,name,src_host,src_port,src_security,src_insecure,src_user,src_pass,src_proto,
|
||||
dst_host,dst_port,dst_security,dst_insecure,dst_user,dst_pass,mbox_dir,active
|
||||
FROM accounts ORDER BY name`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []Account
|
||||
for rows.Next() {
|
||||
a, err := scanAccount(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, a)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func GetAccount(name string) (Account, error) {
|
||||
row := DB.QueryRow(`SELECT id,name,src_host,src_port,src_security,src_insecure,src_user,src_pass,src_proto,
|
||||
dst_host,dst_port,dst_security,dst_insecure,dst_user,dst_pass,mbox_dir,active
|
||||
FROM accounts WHERE name=?`, name)
|
||||
return scanAccount(row)
|
||||
}
|
||||
|
||||
func SaveAccount(a Account) error {
|
||||
normalizeAccount(&a)
|
||||
_, err := DB.Exec(`INSERT INTO accounts(name,src_host,src_port,src_security,src_insecure,src_user,src_pass,src_proto,
|
||||
dst_host,dst_port,dst_security,dst_insecure,dst_user,dst_pass,mbox_dir,active)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
ON CONFLICT(name) DO UPDATE SET
|
||||
src_host=excluded.src_host, src_port=excluded.src_port, src_security=excluded.src_security,
|
||||
src_insecure=excluded.src_insecure, src_user=excluded.src_user, src_pass=excluded.src_pass,
|
||||
src_proto=excluded.src_proto, dst_host=excluded.dst_host, dst_port=excluded.dst_port,
|
||||
dst_security=excluded.dst_security, dst_insecure=excluded.dst_insecure,
|
||||
dst_user=excluded.dst_user, dst_pass=excluded.dst_pass, mbox_dir=excluded.mbox_dir,
|
||||
active=excluded.active`,
|
||||
a.Name, a.SrcHost, a.SrcPort, a.SrcSecurity, boolInt(a.SrcInsecure), a.SrcUser, a.SrcPass, a.SrcProto,
|
||||
a.DstHost, a.DstPort, a.DstSecurity, boolInt(a.DstInsecure), a.DstUser, a.DstPass, a.MboxDir, boolInt(a.Active))
|
||||
return err
|
||||
}
|
||||
|
||||
func DeleteAccount(name string) error {
|
||||
_, err := DB.Exec(`DELETE FROM accounts WHERE name=?`, name)
|
||||
return err
|
||||
}
|
||||
|
||||
func AlreadyCopied(accountID int64, folder, messageID string) (bool, error) {
|
||||
if messageID == "" {
|
||||
return false, nil
|
||||
}
|
||||
var x int
|
||||
err := DB.QueryRow(`SELECT 1 FROM copied WHERE account_id=? AND folder=? AND message_id=?`, accountID, folder, messageID).Scan(&x)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return false, nil
|
||||
}
|
||||
return err == nil, err
|
||||
}
|
||||
|
||||
func MarkCopied(accountID int64, folder, messageID string) error {
|
||||
if messageID == "" {
|
||||
return nil
|
||||
}
|
||||
_, err := DB.Exec(`INSERT OR IGNORE INTO copied(account_id, folder, message_id) VALUES(?,?,?)`, accountID, folder, messageID)
|
||||
return err
|
||||
}
|
||||
|
||||
func SeedAccountFromFile(path string) error {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var a Account
|
||||
if err := json.Unmarshal(b, &a); err != nil {
|
||||
return err
|
||||
}
|
||||
return SaveAccount(a)
|
||||
}
|
||||
|
||||
type accountScanner interface {
|
||||
Scan(dest ...any) error
|
||||
}
|
||||
|
||||
func scanAccount(s accountScanner) (Account, error) {
|
||||
var a Account
|
||||
var srcInsecure, dstInsecure, active int
|
||||
err := s.Scan(&a.ID, &a.Name, &a.SrcHost, &a.SrcPort, &a.SrcSecurity, &srcInsecure, &a.SrcUser, &a.SrcPass, &a.SrcProto,
|
||||
&a.DstHost, &a.DstPort, &a.DstSecurity, &dstInsecure, &a.DstUser, &a.DstPass, &a.MboxDir, &active)
|
||||
a.SrcInsecure = srcInsecure != 0
|
||||
a.DstInsecure = dstInsecure != 0
|
||||
a.Active = active != 0
|
||||
return a, err
|
||||
}
|
||||
|
||||
func normalizeAccount(a *Account) {
|
||||
if a.SrcProto == "" {
|
||||
a.SrcProto = "imap"
|
||||
}
|
||||
if a.SrcSecurity == "" {
|
||||
a.SrcSecurity = "tls"
|
||||
}
|
||||
if a.DstSecurity == "" {
|
||||
a.DstSecurity = "tls"
|
||||
}
|
||||
if a.SrcPort == 0 {
|
||||
if a.SrcSecurity == "tls" {
|
||||
a.SrcPort = 993
|
||||
} else {
|
||||
a.SrcPort = 143
|
||||
}
|
||||
}
|
||||
if a.DstPort == 0 {
|
||||
if a.DstSecurity == "tls" {
|
||||
a.DstPort = 993
|
||||
} else {
|
||||
a.DstPort = 143
|
||||
}
|
||||
}
|
||||
if a.MboxDir == "" {
|
||||
a.MboxDir = a.Name
|
||||
}
|
||||
if !a.Active {
|
||||
a.Active = true
|
||||
}
|
||||
}
|
||||
|
||||
func boolInt(v bool) int {
|
||||
if v {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue