Add preliminary support for imports

This commit is contained in:
Simon Ser 2019-07-30 20:51:46 +03:00
parent 6fe47ba219
commit fbf85fecaa
No known key found for this signature in database
GPG Key ID: 0FDE7BE0E88F5E48
4 changed files with 315 additions and 6 deletions

View File

@ -21,6 +21,7 @@ import (
"github.com/emersion/hydroxide/carddav"
"github.com/emersion/hydroxide/events"
imapbackend "github.com/emersion/hydroxide/imap"
"github.com/emersion/hydroxide/imports"
"github.com/emersion/hydroxide/protonmail"
smtpbackend "github.com/emersion/hydroxide/smtp"
)
@ -104,7 +105,6 @@ func listenAndServeCardDAV(addr string, authManager *auth.Manager, eventsManager
}
func main() {
// Register flags
smtpHost := flag.String("smtp-host", "127.0.0.1", "Allowed SMTP email hostname on which hydroxide listens, defaults to 127.0.0.1")
smtpPort := flag.String("smtp-port", "1025", "SMTP port on which hydroxide listens, defaults to 1025")
@ -114,9 +114,9 @@ func main() {
carddavHost := flag.String("carddav-host", "127.0.0.1", "Allowed CardDAV email hostname on which hydroxide listens, defaults to 127.0.0.1")
carddavPort := flag.String("carddav-port", "8080", "CardDAV port on which hydroxide listens, defaults to 8080")
// Register arguments
authCmd := flag.NewFlagSet("auth", flag.ExitOnError)
exportSecretKeysCmd := flag.NewFlagSet("export-secret-keys", flag.ExitOnError)
importMessagesCmd := flag.NewFlagSet("import-messages", flag.ExitOnError)
flag.Parse()
@ -124,7 +124,7 @@ func main() {
switch cmd {
case "auth":
authCmd.Parse(os.Args[2:])
username := authCmd.Arg(1)
username := authCmd.Arg(0)
if username == "" {
log.Fatal("usage: hydroxide auth <username>")
}
@ -222,7 +222,7 @@ func main() {
}
case "export-secret-keys":
exportSecretKeysCmd.Parse(os.Args[2:])
username := exportSecretKeysCmd.Arg(1)
username := exportSecretKeysCmd.Arg(0)
if username == "" {
log.Fatal("usage: hydroxide export-secret-keys <username>")
}
@ -254,6 +254,38 @@ func main() {
if err := wc.Close(); err != nil {
log.Fatal(err)
}
case "import-messages":
// TODO: support for mbox
importMessagesCmd.Parse(os.Args[2:])
username := importMessagesCmd.Arg(0)
archivePath := importMessagesCmd.Arg(1)
if username == "" || archivePath == "" {
log.Fatal("usage: hydroxide import-messages <username> <file>")
}
f, err := os.Open(archivePath)
if err != nil {
log.Fatal(err)
}
defer f.Close()
var bridgePassword string
fmt.Printf("Bridge password: ")
if pass, err := gopass.GetPasswd(); err != nil {
log.Fatal(err)
} else {
bridgePassword = string(pass)
}
c, _, err := auth.NewManager(newClient).Auth(username, bridgePassword)
if err != nil {
log.Fatal(err)
}
if err := imports.ImportMessage(c, f); err != nil {
log.Fatal(err)
}
case "smtp":
addr := *smtpHost + ":" + *smtpPort
authManager := auth.NewManager(newClient)
@ -292,8 +324,9 @@ func main() {
Commands:
auth <username> Login to ProtonMail via hydroxide
carddav Run hydroxide as a CardDAV server
export-secret-keys Export keys
export-secret-keys <username> Export secret keys
imap Run hydroxide as an IMAP server
import-messages <username> <file> Import messages
serve Run all servers
smtp Run hydroxide as an SMTP server
status View hydroxide status

117
imports/messages.go Normal file
View File

@ -0,0 +1,117 @@
package imports
import (
"fmt"
"io"
"github.com/emersion/go-message/mail"
"golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/armor"
"github.com/emersion/hydroxide/protonmail"
)
func ImportMessage(c *protonmail.Client, r io.Reader) error {
mr, err := mail.CreateReader(r)
if err != nil {
return err
}
defer mr.Close()
// TODO: support attachments
hdr := mr.Header
var body io.Reader
for {
p, err := mr.NextPart()
if err == io.EOF {
break
} else if err != nil {
return err
}
if _, ok := p.Header.(*mail.InlineHeader); ok {
hdr.Set("Content-Type", p.Header.Get("Content-Type"))
body = p.Body
break
}
}
if body == nil {
return fmt.Errorf("message has no body")
}
addrs, err := c.ListAddresses()
if err != nil {
return err
}
// TODO: choose address depending on message header
var importAddr *protonmail.Address
for _, addr := range addrs {
if addr.Send == protonmail.AddressSendPrimary {
importAddr = addr
break
}
}
if importAddr == nil {
return fmt.Errorf("no primary address found")
}
publicKey, err := importAddr.Keys[0].Entity()
if err != nil {
return err
}
key := "0"
metadata := map[string]*protonmail.Message{
key: {
Unread: 1,
LabelIDs: []string{protonmail.LabelInbox},
Type: protonmail.MessageInbox,
AddressID: importAddr.ID,
},
}
importer, err := c.Import(metadata)
if err != nil {
return err
}
w, err := importer.ImportMessage(key)
if err != nil {
return err
}
mwc, err := mail.CreateSingleInlineWriter(w, hdr)
if err != nil {
return err
}
defer mwc.Close()
awc, err := armor.Encode(mwc, "PGP MESSAGE", nil)
if err != nil {
return err
}
defer awc.Close()
ewc, err := openpgp.Encrypt(awc, []*openpgp.Entity{publicKey}, nil, nil, nil)
if err != nil {
return err
}
defer ewc.Close()
if _, err := io.Copy(w, body); err != nil {
return err
}
if err := ewc.Close(); err != nil {
return err
}
if err := awc.Close(); err != nil {
return err
}
if err := mwc.Close(); err != nil {
return err
}
if _, err := importer.Commit(); err != nil {
return err
}
return nil
}

159
protonmail/import.go Normal file
View File

@ -0,0 +1,159 @@
package protonmail
import (
"encoding/json"
"fmt"
"io"
"mime"
"mime/multipart"
"net/http"
"net/textproto"
)
type ImportResult map[string]ImportMessageResult
func (res ImportResult) Err() error {
for _, msgRes := range res {
if msgRes.Err != nil {
return msgRes.Err
}
}
return nil
}
type ImportMessageResult struct {
Err error
MessageID string
}
type Importer struct {
pw *io.PipeWriter
mw *multipart.Writer
uploaded map[string]bool
closed bool
done <-chan error
result <-chan ImportResult
}
func (imp *Importer) ImportMessage(key string) (io.Writer, error) {
if uploaded, ok := imp.uploaded[key]; !ok {
return nil, fmt.Errorf("protonmail: unknown import message %q", key)
} else if uploaded {
return nil, fmt.Errorf("protonmail: message %q already imported", key)
}
imp.uploaded[key] = true
hdr := make(textproto.MIMEHeader)
params := map[string]string{
"name": key,
"filename": key + ".eml",
}
hdr.Set("Content-Disposition", mime.FormatMediaType("form-data", params))
hdr.Set("Content-Type", "message/rfc822")
return imp.mw.CreatePart(hdr)
}
func (imp *Importer) close() error {
if imp.closed {
return fmt.Errorf("protonmail: importer already closed")
}
imp.closed = true
if err := imp.mw.Close(); err != nil {
return err
}
return imp.pw.Close()
}
func (imp *Importer) Commit() (ImportResult, error) {
if err := imp.close(); err != nil {
return nil, err
}
for key, ok := range imp.uploaded {
if !ok {
return nil, fmt.Errorf("protonmail: message %q has not been imported", key)
}
}
if err := <-imp.done; err != nil {
return nil, err
}
return <-imp.result, nil
}
func (c *Client) Import(metadata map[string]*Message) (*Importer, error) {
pr, pw := io.Pipe()
mw := multipart.NewWriter(pw)
done := make(chan error, 1)
result := make(chan ImportResult, 1)
go func() {
defer close(done)
defer close(result)
req, err := c.newRequest(http.MethodPost, "/import", pr)
if err != nil {
done <- err
return
}
req.Header.Set("Content-Type", mw.FormDataContentType())
type messageResp struct {
Name string
Response struct {
resp
MessageID string
}
}
var respData struct {
resp
Responses []messageResp
}
err = c.doJSON(req, &respData)
done <- err
if err != nil {
return
}
res := make(ImportResult, len(respData.Responses))
for _, msgData := range respData.Responses {
res[msgData.Name] = ImportMessageResult{
Err: msgData.Response.Err(),
MessageID: msgData.Response.MessageID,
}
}
result <- res
}()
// Send metadata
hdr := make(textproto.MIMEHeader)
params := map[string]string{"name": "Metadata"}
hdr.Set("Content-Disposition", mime.FormatMediaType("form-data", params))
hdr.Set("Content-Type", "application/json")
metadataWriter, err := mw.CreatePart(hdr)
if err != nil {
pw.CloseWithError(fmt.Errorf("protonmail: failed to write metadata"))
return nil, err
}
if err := json.NewEncoder(metadataWriter).Encode(metadata); err != nil {
pw.CloseWithError(fmt.Errorf("protonmail: failed to write metadata"))
return nil, err
}
uploaded := make(map[string]bool, len(metadata))
for key := range metadata {
uploaded[key] = false
}
return &Importer{
pw: pw,
mw: mw,
uploaded: uploaded,
done: done,
result: result,
}, nil
}

View File

@ -14,7 +14,7 @@ type PrivateKeyFlags int
const (
PrivateKeyVerify PrivateKeyFlags = 1
PrivateKeyEncrypt = 2
PrivateKeyEncrypt PrivateKeyFlags = 2
)
type PrivateKey struct {