Add preliminary support for imports
This commit is contained in:
parent
6fe47ba219
commit
fbf85fecaa
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -14,7 +14,7 @@ type PrivateKeyFlags int
|
|||
|
||||
const (
|
||||
PrivateKeyVerify PrivateKeyFlags = 1
|
||||
PrivateKeyEncrypt = 2
|
||||
PrivateKeyEncrypt PrivateKeyFlags = 2
|
||||
)
|
||||
|
||||
type PrivateKey struct {
|
||||
|
|
Loading…
Reference in New Issue