diff --git a/cmd/hydroxide/main.go b/cmd/hydroxide/main.go index 1414446..d1dd841 100644 --- a/cmd/hydroxide/main.go +++ b/cmd/hydroxide/main.go @@ -12,6 +12,7 @@ import ( imapmove "github.com/emersion/go-imap-move" imapspacialuse "github.com/emersion/go-imap-specialuse" imapserver "github.com/emersion/go-imap/server" + "github.com/emersion/go-mbox" "github.com/emersion/go-smtp" "github.com/howeyc/gopass" "golang.org/x/crypto/openpgp" @@ -20,6 +21,7 @@ import ( "github.com/emersion/hydroxide/auth" "github.com/emersion/hydroxide/carddav" "github.com/emersion/hydroxide/events" + "github.com/emersion/hydroxide/exports" imapbackend "github.com/emersion/hydroxide/imap" "github.com/emersion/hydroxide/imports" "github.com/emersion/hydroxide/protonmail" @@ -116,6 +118,7 @@ Commands: export-secret-keys Export secret keys imap Run hydroxide as an IMAP server import-messages Import messages + export-messages [options...] Export messages serve Run all servers smtp Run hydroxide as an SMTP server status View hydroxide status @@ -151,6 +154,7 @@ func main() { authCmd := flag.NewFlagSet("auth", flag.ExitOnError) exportSecretKeysCmd := flag.NewFlagSet("export-secret-keys", flag.ExitOnError) importMessagesCmd := flag.NewFlagSet("import-messages", flag.ExitOnError) + exportMessagesCmd := flag.NewFlagSet("export-messages", flag.ExitOnError) flag.Parse() @@ -335,6 +339,38 @@ func main() { if err := imports.ImportMessage(c, f); err != nil { log.Fatal(err) } + case "export-messages": + var convID string + exportMessagesCmd.StringVar(&convID, "conversation-id", "", "conversation ID") + exportMessagesCmd.Parse(flag.Args()[1:]) + username := exportMessagesCmd.Arg(0) + log.Println(convID, username) + if convID == "" || username == "" { + log.Fatal("usage: hydroxide export-messages -conversation-id ") + } + + var bridgePassword string + fmt.Fprintf(os.Stderr, "Bridge password: ") + if pass, err := gopass.GetPasswd(); err != nil { + log.Fatal(err) + } else { + bridgePassword = string(pass) + } + + c, privateKeys, err := auth.NewManager(newClient).Auth(username, bridgePassword) + if err != nil { + log.Fatal(err) + } + + mboxWriter := mbox.NewWriter(os.Stdout) + + if err := exports.ExportConversation(c, privateKeys, mboxWriter, convID); err != nil { + log.Fatal(err) + } + + if err := mboxWriter.Close(); err != nil { + log.Fatal(err) + } case "smtp": addr := *smtpHost + ":" + *smtpPort authManager := auth.NewManager(newClient) diff --git a/exports/messages.go b/exports/messages.go new file mode 100644 index 0000000..6b0f086 --- /dev/null +++ b/exports/messages.go @@ -0,0 +1,76 @@ +package exports + +import ( + "fmt" + "io" + "bufio" + "strings" + + "github.com/emersion/go-mbox" + "github.com/emersion/go-message" + "github.com/emersion/go-message/mail" + "github.com/emersion/go-message/textproto" + "golang.org/x/crypto/openpgp" + + "github.com/emersion/hydroxide/protonmail" +) + +func ExportMessage(c *protonmail.Client, privateKeys openpgp.KeyRing, w io.Writer, id string) error { + msg, err := c.GetMessage(id) + if err != nil { + return fmt.Errorf("failed to fetch message: %v", err) + } + + mimeType := msg.MIMEType + if mimeType == "" { + mimeType = "text/html" + } + + br := bufio.NewReader(strings.NewReader(msg.Header)) + th, err := textproto.ReadHeader(br) + if err != nil { + return fmt.Errorf("failed to read message header: %v", err) + } + + mh := mail.Header{message.Header{th}} + mh.SetContentType(mimeType, nil) + mh.Set("Content-Transfer-Encoding", "quoted-printable") + + // TODO: add support for attachments + mw, err := mail.CreateSingleInlineWriter(w, mh) + if err != nil { + return fmt.Errorf("failed to create message writer: %v", err) + } + + md, err := msg.Read(privateKeys, nil) + if err != nil { + return err + } + + // TODO: check signature + if _, err := io.Copy(mw, md.UnverifiedBody); err != nil { + return err + } + + return mw.Close() +} + +func ExportConversation(c *protonmail.Client, privateKeys openpgp.KeyRing, mbox *mbox.Writer, id string) error { + _, msgs, err := c.GetConversation(id, "") + if err != nil { + return fmt.Errorf("failed to fetch conversation: %v", err) + } + + for _, msg := range msgs { + w, err := mbox.CreateMessage(msg.Sender.Address, msg.Time.Time()) + if err != nil { + return fmt.Errorf("failed to create mbox message: %v", err) + } + + if err := ExportMessage(c, privateKeys, w, msg.ID); err != nil { + return fmt.Errorf("failed to export conversation message: %v", err) + } + } + + return nil +} diff --git a/go.mod b/go.mod index c3e16cd..bdf0210 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/emersion/go-imap v1.0.4 github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62 + github.com/emersion/go-mbox v1.0.0 github.com/emersion/go-message v0.11.1 github.com/emersion/go-smtp v0.12.1 github.com/emersion/go-vcard v0.0.0-20191221110513-5f81fa0d3cc7 diff --git a/go.sum b/go.sum index 35c0221..593744e 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 h1:5p1t3e1Po github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w= github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62 h1:4ZAfwfc8aDlj26kkEap1UDSwwDnJp9Ie8Uj1MSXAkPk= github.com/emersion/go-imap-specialuse v0.0.0-20161227184202-ba031ced6a62/go.mod h1:/nybxhI8kXom8Tw6BrHMl42usALvka6meORflnnYwe4= +github.com/emersion/go-mbox v1.0.0 h1:HN6aKbyqmgIfK9fS/gen+NRr2wXLSxZXWfdAIAnzQPc= +github.com/emersion/go-mbox v1.0.0/go.mod h1:Yp9IVuuOYLEuMv4yjgDHvhb5mHOcYH6x92Oas3QqEZI= github.com/emersion/go-message v0.11.1 h1:0C/S4JIXDTSfXB1vpqdimAYyK4+79fgEAMQ0dSL+Kac= github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY= github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e h1:ba7YsgX5OV8FjGi5ZWml8Jng6oBrJAb3ahqWMJ5Ce8Q=