mox/export.go

267 lines
6.8 KiB
Go
Raw Normal View History

2023-01-30 16:27:06 +03:00
package main
import (
"bufio"
"bytes"
"fmt"
"io"
"log"
"os"
"path/filepath"
"sort"
"time"
"github.com/mjl-/bstore"
"github.com/mjl-/mox/store"
)
func cmdExportMaildir(c *cmd) {
c.params = "dst-path account-path [mailbox]"
c.help = `Export one or all mailboxes from an account in maildir format.
Export bypasses a running mox instance. It opens the account mailbox/message
database file directly. This may block if a running mox instance also has the
database open, e.g. for IMAP connections. To export from a running instance, use
the accounts web page.
2023-01-30 16:27:06 +03:00
`
args := c.Parse()
xcmdExport(false, args, c)
}
func cmdExportMbox(c *cmd) {
c.params = "dst-path account-path [mailbox]"
c.help = `Export messages from one or all mailboxes in an account in mbox format.
Using mbox is not recommended. Maildir is a better format.
Export bypasses a running mox instance. It opens the account mailbox/message
database file directly. This may block if a running mox instance also has the
database open, e.g. for IMAP connections. To export from a running instance, use
the accounts web page.
2023-01-30 16:27:06 +03:00
For mbox export, we use "mboxrd" where message lines starting with the magic
"From " string are escaped by prepending a >. We escape all ">*From ",
otherwise reconstructing the original could lose a ">".
`
args := c.Parse()
xcmdExport(true, args, c)
}
func xcmdExport(mbox bool, args []string, c *cmd) {
if len(args) != 2 && len(args) != 3 {
c.Usage()
}
dst := args[0]
accountDir := args[1]
var mailbox string
if len(args) == 3 {
mailbox = args[2]
}
dbpath := filepath.Join(accountDir, "index.db")
db, err := bstore.Open(dbpath, &bstore.Options{Timeout: 5 * time.Second, Perm: 0660}, store.Message{}, store.Recipient{}, store.Mailbox{})
xcheckf(err, "open database %q", dbpath)
err = db.Read(func(tx *bstore.Tx) error {
exporttx(tx, mbox, dst, accountDir, mailbox)
return nil
})
xcheckf(err, "transaction")
}
func exporttx(tx *bstore.Tx, mbox bool, dst, accountDir, mailbox string) {
id2name := map[int64]string{}
name2id := map[string]int64{}
mailboxes, err := bstore.QueryTx[store.Mailbox](tx).List()
xcheckf(err, "query mailboxes")
for _, mb := range mailboxes {
id2name[mb.ID] = mb.Name
name2id[mb.Name] = mb.ID
}
var mailboxID int64
if mailbox != "" {
var ok bool
mailboxID, ok = name2id[mailbox]
if !ok {
log.Fatalf("mailbox %q not found", mailbox)
}
}
mboxes := map[string]*os.File{}
// Open mbox files or create dirs.
var names []string
for _, name := range id2name {
if mailbox != "" && name != mailbox {
continue
}
names = append(names, name)
}
// We need to sort the names because maildirs can create subdirs. Ranging over
// id2name directly would randomize the directory names, we would create a sub
// maildir before the parent, and fail with "dir exists" when creating the parent
// dir.
sort.Slice(names, func(i, j int) bool {
return names[i] < names[j]
})
for _, name := range names {
p := dst
if mailbox == "" {
p = filepath.Join(p, name)
}
os.MkdirAll(filepath.Dir(p), 0770)
if mbox {
mbp := p
if mailbox == "" {
mbp += ".mbox"
}
f, err := os.OpenFile(mbp, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0660)
xcheckf(err, "creating mbox file")
log.Printf("creating mbox file %s", mbp)
mboxes[name] = f
} else {
err = os.Mkdir(p, 0770)
xcheckf(err, "making maildir")
log.Printf("creating maildir %s", p)
subdirs := []string{"new", "cur", "tmp"}
for _, subdir := range subdirs {
err = os.Mkdir(filepath.Join(p, subdir), 0770)
xcheckf(err, "making maildir subdir")
}
}
}
q := bstore.QueryTx[store.Message](tx)
if mailboxID > 0 {
q.FilterNonzero(store.Message{MailboxID: mailboxID})
}
defer q.Close()
for {
m, err := q.Next()
if err == bstore.ErrAbsent {
break
}
xcheckf(err, "next message")
mbname := id2name[m.MailboxID]
p := dst
if mailbox == "" {
p = filepath.Join(p, mbname)
}
mp := filepath.Join(accountDir, "msg", store.MessagePath(m.ID))
var mr io.ReadCloser
if m.Size == int64(len(m.MsgPrefix)) {
log.Printf("message size is prefix size for m id %d", m.ID)
mr = io.NopCloser(bytes.NewReader(m.MsgPrefix))
} else {
mpf, err := os.Open(mp)
xcheckf(err, "open message file")
st, err := mpf.Stat()
xcheckf(err, "stat message file")
size := st.Size() + int64(len(m.MsgPrefix))
if size != m.Size {
log.Fatalf("message size mismatch, database has %d, size is %d+%d=%d", m.Size, len(m.MsgPrefix), st.Size(), size)
}
mr = store.FileMsgReader(m.MsgPrefix, mpf)
}
if mbox {
// todo: should we put status flags in Status or X-Status header inside the message?
// todo: should we do anything with Content-Length headers? changing the escaping could invalidate those. is anything checking that field?
f := mboxes[mbname]
mailfrom := "mox"
if m.MailFrom != "" {
mailfrom = m.MailFrom
}
_, err := fmt.Fprintf(f, "From %s %s\n", mailfrom, m.Received.Format(time.ANSIC))
xcheckf(err, "writing from header")
r := bufio.NewReader(mr)
for {
line, rerr := r.ReadBytes('\n')
if rerr != io.EOF {
xcheckf(rerr, "reading from message")
}
if len(line) > 0 {
if bytes.HasSuffix(line, []byte("\r\n")) {
line = line[:len(line)-1]
line[len(line)-1] = '\n'
}
if bytes.HasPrefix(bytes.TrimLeft(line, ">"), []byte("From ")) {
_, err = fmt.Fprint(f, ">")
xcheckf(err, "writing escaping >")
}
_, err = f.Write(line)
xcheckf(err, "writing line")
}
if rerr == io.EOF {
break
}
}
_, err = fmt.Fprint(f, "\n")
xcheckf(err, "writing end of message newline")
} else {
if m.Flags.Seen {
p = filepath.Join(p, "cur")
} else {
p = filepath.Join(p, "new")
}
name := fmt.Sprintf("%d.%d.mox:2,", m.Received.Unix(), m.ID)
// todo: more flags? forwarded, (non)junk, phishing, mdnsent would be nice. but what is the convention. dovecot-keywords sounds non-standard.
if m.Flags.Seen {
name += "S"
}
if m.Flags.Answered {
name += "R"
}
if m.Flags.Flagged {
name += "F"
}
if m.Flags.Draft {
name += "D"
}
p = filepath.Join(p, name)
f, err := os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0660)
xcheckf(err, "creating message file in maildir")
r := bufio.NewReader(mr)
for {
line, rerr := r.ReadBytes('\n')
if rerr != io.EOF {
xcheckf(rerr, "reading from message")
}
if len(line) > 0 {
if bytes.HasSuffix(line, []byte("\r\n")) {
line = line[:len(line)-1]
line[len(line)-1] = '\n'
}
_, err = f.Write(line)
xcheckf(err, "writing line")
}
if rerr == io.EOF {
break
}
}
mr.Close()
err = f.Close()
xcheckf(err, "closing new file in maildir")
}
mr.Close()
}
if mbox {
for _, f := range mboxes {
err = f.Close()
xcheckf(err, "closing mbox file")
}
}
}