Decouple archive mailboxes from accounts
This commit is contained in:
parent
0bb49019b6
commit
8d2838aea7
4 changed files with 130 additions and 46 deletions
|
|
@ -160,6 +160,11 @@ func accountsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
archives, err := ListArchiveMailboxes()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
editName := strings.TrimSpace(r.URL.Query().Get("edit"))
|
editName := strings.TrimSpace(r.URL.Query().Get("edit"))
|
||||||
var edit Account
|
var edit Account
|
||||||
if editName != "" {
|
if editName != "" {
|
||||||
|
|
@ -169,7 +174,7 @@ func accountsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
renderAccountsPage(w, r, accounts, edit, r.URL.Query().Get("msg"), r.URL.Query().Get("err"))
|
renderAccountsPage(w, r, accounts, archives, edit, r.URL.Query().Get("msg"), r.URL.Query().Get("err"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func accountSaveHandler(w http.ResponseWriter, r *http.Request) {
|
func accountSaveHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
@ -253,7 +258,12 @@ func archivesHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
renderArchivesPage(w, r, archiveMailboxes(accounts), accounts, r.URL.Query().Get("msg"), r.URL.Query().Get("err"))
|
archives, err := ListArchiveMailboxes()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
renderArchivesPage(w, r, archives, accounts, r.URL.Query().Get("msg"), r.URL.Query().Get("err"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func transferHandler(w http.ResponseWriter, r *http.Request) {
|
func transferHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
@ -270,7 +280,12 @@ func transferHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
renderTransferPage(w, r, transferModeBySlug(mode), accounts, archiveMailboxes(accounts))
|
archives, err := ListArchiveMailboxes()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
renderTransferPage(w, r, transferModeBySlug(mode), accounts, archives)
|
||||||
}
|
}
|
||||||
|
|
||||||
func emailBoxHandler(w http.ResponseWriter, r *http.Request) {
|
func emailBoxHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
@ -307,6 +322,10 @@ func archiveCreateHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
redirectArchives(w, r, err.Error())
|
redirectArchives(w, r, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err := SaveArchiveMailbox(name); err != nil {
|
||||||
|
redirectArchives(w, r, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
redirectArchives(w, r, "Archiv-Mailbox angelegt.")
|
redirectArchives(w, r, "Archiv-Mailbox angelegt.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -330,7 +349,11 @@ func archiveDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
path := filepath.Join(Cfg.MboxRoot, name)
|
path := filepath.Join(Cfg.MboxRoot, name)
|
||||||
entries, err := os.ReadDir(path)
|
entries, err := os.ReadDir(path)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
redirectArchives(w, r, "Archiv-Mailbox existiert nicht.")
|
if err := DeleteArchiveMailbox(name); err != nil {
|
||||||
|
redirectArchives(w, r, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
redirectArchives(w, r, "Archiv-Mailbox entfernt.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -345,6 +368,10 @@ func archiveDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
redirectArchives(w, r, err.Error())
|
redirectArchives(w, r, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err := DeleteArchiveMailbox(name); err != nil {
|
||||||
|
redirectArchives(w, r, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
redirectArchives(w, r, "Leere Archiv-Mailbox entfernt.")
|
redirectArchives(w, r, "Leere Archiv-Mailbox entfernt.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -542,12 +569,11 @@ func userToggleActiveHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
redirectUsers(w, r, username, "Benutzer aktiviert.")
|
redirectUsers(w, r, username, "Benutzer aktiviert.")
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderAccountsPage(w http.ResponseWriter, r *http.Request, accounts []Account, edit Account, msg, errMsg string) {
|
func renderAccountsPage(w http.ResponseWriter, r *http.Request, accounts []Account, archives []string, edit Account, msg, errMsg string) {
|
||||||
user := CurrentUser(r)
|
user := CurrentUser(r)
|
||||||
if edit.Name == "" {
|
if edit.Name == "" {
|
||||||
edit = Account{SrcPort: 993, SrcSecurity: "tls", SrcProto: "imap", DstPort: 993, DstSecurity: "tls", Active: true}
|
edit = Account{SrcPort: 993, SrcSecurity: "tls", SrcProto: "imap", DstPort: 993, DstSecurity: "tls", Active: true}
|
||||||
}
|
}
|
||||||
archives := archiveMailboxes(accounts)
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
|
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
|
@ -1183,8 +1209,9 @@ func renderArchiveForm(w http.ResponseWriter) {
|
||||||
<button class="btn" type="submit">Erstellen</button>
|
<button class="btn" type="submit">Erstellen</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="archive-help">
|
<div class="archive-help">
|
||||||
<div class="archive-help-title">Schutzlogik</div>
|
<div class="archive-help-title">Archivlogik</div>
|
||||||
<p>Archiv-mboxen werden als lokale Ordner unter dem Backup-Root angelegt.</p>
|
<p>Archiv-mboxen sind eigenstaendige Mailboxen und nicht von einem Quellkonto abhaengig.</p>
|
||||||
|
<p>Sie koennen leer angelegt oder spaeter per Import mit mbox/PST-Inhalten gefuellt werden.</p>
|
||||||
<p>Entfernen ist nur moeglich, wenn die Mailbox keinem Konto zugeordnet und leer ist.</p>
|
<p>Entfernen ist nur moeglich, wenn die Mailbox keinem Konto zugeordnet und leer ist.</p>
|
||||||
</div>`)
|
</div>`)
|
||||||
}
|
}
|
||||||
|
|
@ -1282,7 +1309,7 @@ func archiveSelect(current string, archives []string) string {
|
||||||
values = append([]string{current}, values...)
|
values = append([]string{current}, values...)
|
||||||
}
|
}
|
||||||
if len(values) == 0 {
|
if len(values) == 0 {
|
||||||
return `<input class="input" name="mbox_dir" placeholder="erst Archiv-Mailbox erstellen">`
|
return `<select name="mbox_dir" disabled><option>erst Archiv-Mailbox erstellen</option></select>`
|
||||||
}
|
}
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.WriteString(`<select name="mbox_dir">`)
|
b.WriteString(`<select name="mbox_dir">`)
|
||||||
|
|
@ -1323,30 +1350,6 @@ func parsePort(value string) int {
|
||||||
return port
|
return port
|
||||||
}
|
}
|
||||||
|
|
||||||
func archiveMailboxes(accounts []Account) []string {
|
|
||||||
seen := map[string]bool{}
|
|
||||||
for _, a := range accounts {
|
|
||||||
name := accountMboxDir(a)
|
|
||||||
if name != "" {
|
|
||||||
seen[name] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
entries, err := os.ReadDir(Cfg.MboxRoot)
|
|
||||||
if err == nil {
|
|
||||||
for _, entry := range entries {
|
|
||||||
if entry.IsDir() {
|
|
||||||
seen[entry.Name()] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out := make([]string, 0, len(seen))
|
|
||||||
for name := range seen {
|
|
||||||
out = append(out, name)
|
|
||||||
}
|
|
||||||
sortStrings(out)
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func archiveUses(archive string, accounts []Account) string {
|
func archiveUses(archive string, accounts []Account) string {
|
||||||
var uses []string
|
var uses []string
|
||||||
for _, a := range accounts {
|
for _, a := range accounts {
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ type Account struct {
|
||||||
DstInsecure bool `json:"dst_insecure"`
|
DstInsecure bool `json:"dst_insecure"`
|
||||||
DstUser string `json:"dst_user"` // archiv-hans-peter@dr-gold.com
|
DstUser string `json:"dst_user"` // archiv-hans-peter@dr-gold.com
|
||||||
DstPass string `json:"dst_pass"`
|
DstPass string `json:"dst_pass"`
|
||||||
MboxDir string `json:"mbox_dir"` // Unterordner unter Cfg.MboxRoot; leer = Cfg.MboxRoot/Name
|
MboxDir string `json:"mbox_dir"` // optionaler Name aus archive_mailboxes unter Cfg.MboxRoot
|
||||||
Active bool `json:"active"`
|
Active bool `json:"active"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,6 +54,8 @@ type AppUser struct {
|
||||||
//
|
//
|
||||||
// accounts(...) -- die Umzugs-Zeilen (oben), inkl.
|
// accounts(...) -- die Umzugs-Zeilen (oben), inkl.
|
||||||
// src_security/src_insecure etc.
|
// src_security/src_insecure etc.
|
||||||
|
// archive_mailboxes(name) -- lokale/importierte Archiv-mboxen,
|
||||||
|
// unabhaengig von Quell/Ziel-Konten.
|
||||||
// folder_map(account_id, src, dst) -- optionales manuelles Umbenennen
|
// folder_map(account_id, src, dst) -- optionales manuelles Umbenennen
|
||||||
// (Vorrang vor der Rollen-Automatik
|
// (Vorrang vor der Rollen-Automatik
|
||||||
// aus 11-folders.go)
|
// aus 11-folders.go)
|
||||||
|
|
@ -94,6 +96,11 @@ func ConnectDB() error {
|
||||||
mbox_dir TEXT NOT NULL DEFAULT '',
|
mbox_dir TEXT NOT NULL DEFAULT '',
|
||||||
active INTEGER NOT NULL DEFAULT 1
|
active INTEGER NOT NULL DEFAULT 1
|
||||||
)`,
|
)`,
|
||||||
|
`CREATE TABLE IF NOT EXISTS archive_mailboxes(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)`,
|
||||||
`CREATE TABLE IF NOT EXISTS folder_map(
|
`CREATE TABLE IF NOT EXISTS folder_map(
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
||||||
|
|
@ -153,6 +160,10 @@ func ConnectDB() error {
|
||||||
_ = db.Close()
|
_ = db.Close()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := seedArchiveMailboxesFromFS(); err != nil {
|
||||||
|
_ = db.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -393,6 +404,58 @@ func DeleteAccount(name string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ListArchiveMailboxes() ([]string, error) {
|
||||||
|
rows, err := DB.Query(`SELECT name FROM archive_mailboxes ORDER BY lower(name), name`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var out []string
|
||||||
|
for rows.Next() {
|
||||||
|
var name string
|
||||||
|
if err := rows.Scan(&name); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, name)
|
||||||
|
}
|
||||||
|
return out, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveArchiveMailbox(name string) error {
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("Name der Archiv-Mailbox fehlt.")
|
||||||
|
}
|
||||||
|
_, err := DB.Exec(`INSERT OR IGNORE INTO archive_mailboxes(name) VALUES(?)`, name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteArchiveMailbox(name string) error {
|
||||||
|
_, err := DB.Exec(`DELETE FROM archive_mailboxes WHERE name=?`, strings.TrimSpace(name))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func seedArchiveMailboxesFromFS() error {
|
||||||
|
if Cfg.MboxRoot == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
entries, err := os.ReadDir(Cfg.MboxRoot)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
if err := SaveArchiveMailbox(entry.Name()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func AlreadyCopied(accountID int64, folder, messageID string) (bool, error) {
|
func AlreadyCopied(accountID int64, folder, messageID string) (bool, error) {
|
||||||
if messageID == "" {
|
if messageID == "" {
|
||||||
return false, nil
|
return false, nil
|
||||||
|
|
@ -468,9 +531,7 @@ func normalizeAccount(a *Account) {
|
||||||
a.DstPort = 143
|
a.DstPort = 143
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if a.MboxDir == "" {
|
a.MboxDir = strings.TrimSpace(a.MboxDir)
|
||||||
a.MboxDir = a.Name
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func scanAppUser(s appUserScanner) (AppUser, error) {
|
func scanAppUser(s appUserScanner) (AppUser, error) {
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,11 @@ func migrateAccount(a Account) error {
|
||||||
return fmt.Errorf("open target %s: %w", a.Name, err)
|
return fmt.Errorf("open target %s: %w", a.Name, err)
|
||||||
}
|
}
|
||||||
defer dst.Close()
|
defer dst.Close()
|
||||||
mboxDir := filepath.Join(Cfg.MboxRoot, a.MboxDir)
|
archiveName := accountMboxDir(a)
|
||||||
|
if archiveName == "" {
|
||||||
|
return fmt.Errorf("keine Archiv-Mailbox fuer %s ausgewaehlt", a.Name)
|
||||||
|
}
|
||||||
|
mboxDir := filepath.Join(Cfg.MboxRoot, archiveName)
|
||||||
mbox, err := NewMboxWriter(mboxDir)
|
mbox, err := NewMboxWriter(mboxDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -685,6 +685,20 @@ type targetAccountOption struct {
|
||||||
|
|
||||||
func viewerAccounts() []viewerAccount {
|
func viewerAccounts() []viewerAccount {
|
||||||
labels := accountLabels()
|
labels := accountLabels()
|
||||||
|
if DB != nil {
|
||||||
|
archives, err := ListArchiveMailboxes()
|
||||||
|
if err == nil {
|
||||||
|
out := make([]viewerAccount, 0, len(archives))
|
||||||
|
for _, archive := range archives {
|
||||||
|
a := labels[archive]
|
||||||
|
if a.Name == "" {
|
||||||
|
a = viewerAccount{Name: archive, SourceLabel: archive, DisplayLabel: "Archiv-mbox: " + archive}
|
||||||
|
}
|
||||||
|
out = append(out, a)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
}
|
||||||
entries, err := os.ReadDir(Cfg.MboxRoot)
|
entries, err := os.ReadDir(Cfg.MboxRoot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -700,7 +714,7 @@ func viewerAccounts() []viewerAccount {
|
||||||
}
|
}
|
||||||
a := labels[entry.Name()]
|
a := labels[entry.Name()]
|
||||||
if a.Name == "" {
|
if a.Name == "" {
|
||||||
a = viewerAccount{Name: entry.Name(), SourceLabel: entry.Name(), DisplayLabel: "Original"}
|
a = viewerAccount{Name: entry.Name(), SourceLabel: entry.Name(), DisplayLabel: "Archiv-mbox: " + entry.Name()}
|
||||||
}
|
}
|
||||||
out = append(out, a)
|
out = append(out, a)
|
||||||
}
|
}
|
||||||
|
|
@ -711,7 +725,7 @@ func sourceDisplayLabel(mboxDir string) string {
|
||||||
if a := accountLabels()[mboxDir]; a.DisplayLabel != "" {
|
if a := accountLabels()[mboxDir]; a.DisplayLabel != "" {
|
||||||
return a.DisplayLabel
|
return a.DisplayLabel
|
||||||
}
|
}
|
||||||
return "Original"
|
return "Archiv-mbox: " + mboxDir
|
||||||
}
|
}
|
||||||
|
|
||||||
func sourceAccountLabel(mboxDir string) string {
|
func sourceAccountLabel(mboxDir string) string {
|
||||||
|
|
@ -749,6 +763,9 @@ func accountLabels() map[string]viewerAccount {
|
||||||
n := 0
|
n := 0
|
||||||
for _, a := range accounts {
|
for _, a := range accounts {
|
||||||
dir := accountMboxDir(a)
|
dir := accountMboxDir(a)
|
||||||
|
if dir == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
source := a.SrcUser
|
source := a.SrcUser
|
||||||
if source == "" {
|
if source == "" {
|
||||||
source = a.Name
|
source = a.Name
|
||||||
|
|
@ -768,7 +785,7 @@ func accountByMboxDir(mboxDir string) (Account, bool) {
|
||||||
return Account{}, false
|
return Account{}, false
|
||||||
}
|
}
|
||||||
for _, a := range accounts {
|
for _, a := range accounts {
|
||||||
if accountMboxDir(a) == mboxDir {
|
if dir := accountMboxDir(a); dir != "" && dir == mboxDir {
|
||||||
return a, true
|
return a, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -841,15 +858,14 @@ func targetMboxDirs(targetUser string) map[string]bool {
|
||||||
}
|
}
|
||||||
for _, a := range accounts {
|
for _, a := range accounts {
|
||||||
if strings.EqualFold(strings.TrimSpace(a.DstUser), targetUser) {
|
if strings.EqualFold(strings.TrimSpace(a.DstUser), targetUser) {
|
||||||
out[accountMboxDir(a)] = true
|
if dir := accountMboxDir(a); dir != "" {
|
||||||
|
out[dir] = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func accountMboxDir(a Account) string {
|
func accountMboxDir(a Account) string {
|
||||||
if strings.TrimSpace(a.MboxDir) != "" {
|
return strings.TrimSpace(a.MboxDir)
|
||||||
return a.MboxDir
|
|
||||||
}
|
|
||||||
return a.Name
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue