webmail: add export functionality

per mailbox, or for all mailboxes, in maildir/mbox format, in tar/tgz/zip
archive or without archive format for single mbox, single or recursive. the
webaccount already had an option to export all mailboxes, it now looks similar
to the webmail version.
This commit is contained in:
Mechiel Lukkien
2024-04-22 13:41:40 +02:00
parent a3f5fd26a6
commit bf5cfca6b9
14 changed files with 483 additions and 289 deletions

96
webops/export.go Normal file
View File

@ -0,0 +1,96 @@
package webops
import (
"archive/tar"
"archive/zip"
"compress/gzip"
"fmt"
"mime"
"net/http"
"strings"
"time"
"github.com/mjl-/mox/mlog"
"github.com/mjl-/mox/store"
)
// Export is used by webmail and webaccount to export messages of one or
// multiple mailboxes, in maildir or mbox format, in a tar/tgz/zip archive or
// direct mbox.
func Export(log mlog.Log, accName string, w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "405 - method not allowed - use post", http.StatusMethodNotAllowed)
return
}
mailbox := r.FormValue("mailbox") // Empty means all.
format := r.FormValue("format")
archive := r.FormValue("archive")
recursive := r.FormValue("recursive") != ""
switch format {
case "maildir", "mbox":
default:
http.Error(w, "400 - bad request - unknown format", http.StatusBadRequest)
return
}
switch archive {
case "none", "tar", "tgz", "zip":
default:
http.Error(w, "400 - bad request - unknown archive", http.StatusBadRequest)
return
}
if archive == "none" && (format != "mbox" || recursive) {
http.Error(w, "400 - bad request - archive none can only be used with non-recursive mbox", http.StatusBadRequest)
return
}
acc, err := store.OpenAccount(log, accName)
if err != nil {
log.Errorx("open account for export", err)
http.Error(w, "500 - internal server error", http.StatusInternalServerError)
return
}
defer func() {
err := acc.Close()
log.Check(err, "closing account")
}()
name := strings.ReplaceAll(mailbox, "/", "-")
if name == "" {
name = "all"
}
filename := fmt.Sprintf("mailexport-%s-%s", name, time.Now().Format("20060102-150405"))
filename += "." + format
var archiver store.Archiver
if archive == "none" {
w.Header().Set("Content-Type", "application/mbox")
archiver = &store.MboxArchiver{Writer: w}
} else if archive == "tar" {
// Don't tempt browsers to "helpfully" decompress.
w.Header().Set("Content-Type", "application/x-tar")
archiver = store.TarArchiver{Writer: tar.NewWriter(w)}
filename += ".tar"
} else if archive == "tgz" {
// Don't tempt browsers to "helpfully" decompress.
w.Header().Set("Content-Type", "application/octet-stream")
gzw := gzip.NewWriter(w)
defer func() {
_ = gzw.Close()
}()
archiver = store.TarArchiver{Writer: tar.NewWriter(gzw)}
filename += ".tgz"
} else {
w.Header().Set("Content-Type", "application/zip")
archiver = store.ZipArchiver{Writer: zip.NewWriter(w)}
filename += ".zip"
}
defer func() {
err := archiver.Close()
log.Check(err, "exporting mail close")
}()
w.Header().Set("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": filename}))
if err := store.ExportMessages(r.Context(), log, acc.DB, acc.Dir, archiver, format == "maildir", mailbox, recursive); err != nil {
log.Errorx("exporting mail", err)
}
}