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/carddav"
"github.com/emersion/hydroxide/events" "github.com/emersion/hydroxide/events"
imapbackend "github.com/emersion/hydroxide/imap" imapbackend "github.com/emersion/hydroxide/imap"
"github.com/emersion/hydroxide/imports"
"github.com/emersion/hydroxide/protonmail" "github.com/emersion/hydroxide/protonmail"
smtpbackend "github.com/emersion/hydroxide/smtp" smtpbackend "github.com/emersion/hydroxide/smtp"
) )
@ -104,7 +105,6 @@ func listenAndServeCardDAV(addr string, authManager *auth.Manager, eventsManager
} }
func main() { 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") 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") 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") 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") carddavPort := flag.String("carddav-port", "8080", "CardDAV port on which hydroxide listens, defaults to 8080")
// Register arguments
authCmd := flag.NewFlagSet("auth", flag.ExitOnError) authCmd := flag.NewFlagSet("auth", flag.ExitOnError)
exportSecretKeysCmd := flag.NewFlagSet("export-secret-keys", flag.ExitOnError) exportSecretKeysCmd := flag.NewFlagSet("export-secret-keys", flag.ExitOnError)
importMessagesCmd := flag.NewFlagSet("import-messages", flag.ExitOnError)
flag.Parse() flag.Parse()
@ -124,7 +124,7 @@ func main() {
switch cmd { switch cmd {
case "auth": case "auth":
authCmd.Parse(os.Args[2:]) authCmd.Parse(os.Args[2:])
username := authCmd.Arg(1) username := authCmd.Arg(0)
if username == "" { if username == "" {
log.Fatal("usage: hydroxide auth <username>") log.Fatal("usage: hydroxide auth <username>")
} }
@ -222,7 +222,7 @@ func main() {
} }
case "export-secret-keys": case "export-secret-keys":
exportSecretKeysCmd.Parse(os.Args[2:]) exportSecretKeysCmd.Parse(os.Args[2:])
username := exportSecretKeysCmd.Arg(1) username := exportSecretKeysCmd.Arg(0)
if username == "" { if username == "" {
log.Fatal("usage: hydroxide export-secret-keys <username>") log.Fatal("usage: hydroxide export-secret-keys <username>")
} }
@ -254,6 +254,38 @@ func main() {
if err := wc.Close(); err != nil { if err := wc.Close(); err != nil {
log.Fatal(err) 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": case "smtp":
addr := *smtpHost + ":" + *smtpPort addr := *smtpHost + ":" + *smtpPort
authManager := auth.NewManager(newClient) authManager := auth.NewManager(newClient)
@ -292,8 +324,9 @@ func main() {
Commands: Commands:
auth <username> Login to ProtonMail via hydroxide auth <username> Login to ProtonMail via hydroxide
carddav Run hydroxide as a CardDAV server 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 imap Run hydroxide as an IMAP server
import-messages <username> <file> Import messages
serve Run all servers serve Run all servers
smtp Run hydroxide as an SMTP server smtp Run hydroxide as an SMTP server
status View hydroxide status 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 ( const (
PrivateKeyVerify PrivateKeyFlags = 1 PrivateKeyVerify PrivateKeyFlags = 1
PrivateKeyEncrypt = 2 PrivateKeyEncrypt PrivateKeyFlags = 2
) )
type PrivateKey struct { type PrivateKey struct {