From 58ce783fcb90005f5d5bfc6572612eba8c9fcdfd Mon Sep 17 00:00:00 2001 From: Jarno Rankinen Date: Wed, 27 Mar 2024 05:20:03 +0000 Subject: [PATCH] docs: Update readme, delete cmd/hydroxide/ --- README.md | 2 +- cmd/hydroxide/main.go | 572 ------------------------------------------ 2 files changed, 1 insertion(+), 573 deletions(-) delete mode 100644 cmd/hydroxide/main.go diff --git a/README.md b/README.md index 37cbc63..4ce68d6 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # hydroxide-push ### *Forked from [Hydroxide](https://github.com/emersion/hydroxide)* -## Push notifications for Proton Mail mobile via a UP provider +## Push notifications for Proton Mail mobile via a UP provider [![.github/workflows/build.yaml](https://github.com/0ranki/hydroxide-push/actions/workflows/build.yaml/badge.svg)](https://github.com/0ranki/hydroxide-push/actions/workflows/build.yaml) Protonmail depends on Google services to deliver push notifications, This is a stripped down version of [Hydroxide](https://github.com/emersion/hydroxide) diff --git a/cmd/hydroxide/main.go b/cmd/hydroxide/main.go deleted file mode 100644 index c1456c4..0000000 --- a/cmd/hydroxide/main.go +++ /dev/null @@ -1,572 +0,0 @@ -package main - -import ( - "bufio" - "bytes" - "crypto/tls" - "flag" - "fmt" - "io" - "log" - "net/http" - "os" - - "github.com/ProtonMail/go-crypto/openpgp" - "github.com/ProtonMail/go-crypto/openpgp/armor" - imapserver "github.com/emersion/go-imap/server" - "github.com/emersion/go-mbox" - "github.com/emersion/go-smtp" - "golang.org/x/term" - - "github.com/0ranki/hydroxide-push/auth" - "github.com/0ranki/hydroxide-push/carddav" - "github.com/0ranki/hydroxide-push/config" - "github.com/0ranki/hydroxide-push/events" - "github.com/0ranki/hydroxide-push/exports" - imapbackend "github.com/0ranki/hydroxide-push/imap" - "github.com/0ranki/hydroxide-push/imports" - "github.com/0ranki/hydroxide-push/protonmail" - smtpbackend "github.com/0ranki/hydroxide-push/smtp" -) - -const ( - defaultAPIEndpoint = "https://mail.proton.me/api" - defaultAppVersion = "Other" -) - -var ( - debug bool - apiEndpoint string - appVersion string -) - -func newClient() *protonmail.Client { - return &protonmail.Client{ - RootURL: apiEndpoint, - AppVersion: appVersion, - Debug: debug, - } -} - -func askPass(prompt string) ([]byte, error) { - f := os.Stdin - if !term.IsTerminal(int(f.Fd())) { - // This can happen if stdin is used for piping data - // TODO: the following assumes Unix - var err error - if f, err = os.Open("/dev/tty"); err != nil { - return nil, err - } - defer f.Close() - } - fmt.Fprintf(os.Stderr, "%v: ", prompt) - b, err := term.ReadPassword(int(f.Fd())) - if err == nil { - fmt.Fprintf(os.Stderr, "\n") - } - return b, err -} - -func askBridgePass() (string, error) { - if v := os.Getenv("HYDROXIDE_BRIDGE_PASS"); v != "" { - return v, nil - } - b, err := askPass("Bridge password") - return string(b), err -} - -func listenAndServeSMTP(addr string, debug bool, authManager *auth.Manager, tlsConfig *tls.Config) error { - be := smtpbackend.New(authManager) - s := smtp.NewServer(be) - s.Addr = addr - s.Domain = "localhost" // TODO: make this configurable - s.AllowInsecureAuth = tlsConfig == nil - s.TLSConfig = tlsConfig - if debug { - s.Debug = os.Stdout - } - - if s.TLSConfig != nil { - log.Println("SMTP server listening with TLS on", s.Addr) - return s.ListenAndServeTLS() - } - - log.Println("SMTP server listening on", s.Addr) - return s.ListenAndServe() -} - -func listenAndServeIMAP(addr string, debug bool, authManager *auth.Manager, eventsManager *events.Manager, tlsConfig *tls.Config) error { - be := imapbackend.New(authManager, eventsManager) - s := imapserver.New(be) - s.Addr = addr - s.AllowInsecureAuth = tlsConfig == nil - s.TLSConfig = tlsConfig - if debug { - s.Debug = os.Stdout - } - - if s.TLSConfig != nil { - log.Println("IMAP server listening with TLS on", s.Addr) - return s.ListenAndServeTLS() - } - - log.Println("IMAP server listening on", s.Addr) - return s.ListenAndServe() -} - -func listenAndServeCardDAV(addr string, authManager *auth.Manager, eventsManager *events.Manager, tlsConfig *tls.Config) error { - handlers := make(map[string]http.Handler) - - s := &http.Server{ - Addr: addr, - TLSConfig: tlsConfig, - Handler: http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - resp.Header().Set("WWW-Authenticate", "Basic") - - username, password, ok := req.BasicAuth() - if !ok { - resp.WriteHeader(http.StatusUnauthorized) - io.WriteString(resp, "Credentials are required") - return - } - - c, privateKeys, err := authManager.Auth(username, password) - if err != nil { - if err == auth.ErrUnauthorized { - resp.WriteHeader(http.StatusUnauthorized) - } else { - resp.WriteHeader(http.StatusInternalServerError) - } - io.WriteString(resp, err.Error()) - return - } - - h, ok := handlers[username] - if !ok { - ch := make(chan *protonmail.Event) - eventsManager.Register(c, username, ch, nil) - h = carddav.NewHandler(c, privateKeys, ch) - - handlers[username] = h - } - - h.ServeHTTP(resp, req) - }), - } - - if s.TLSConfig != nil { - log.Println("CardDAV server listening with TLS on", s.Addr) - return s.ListenAndServeTLS("", "") - } - - log.Println("CardDAV server listening on", s.Addr) - return s.ListenAndServe() -} - -func isMbox(br *bufio.Reader) (bool, error) { - prefix := []byte("From ") - b, err := br.Peek(len(prefix)) - if err != nil { - return false, err - } - return bytes.Equal(b, prefix), nil -} - -const usage = `usage: hydroxide [options...] -Commands: - auth Login to ProtonMail via hydroxide - carddav Run hydroxide as a CardDAV server - export-secret-keys Export secret keys - imap Run hydroxide as an IMAP server - import-messages [file] Import messages - export-messages [options...] Export messages - sendmail -- sendmail(1) interface - serve Run all servers - smtp Run hydroxide as an SMTP server - status View hydroxide status - -Global options: - -debug - Enable debug logs - -api-endpoint - ProtonMail API endpoint - -app-version - ProtonMail application version - -smtp-host example.com - Allowed SMTP email hostname on which hydroxide listens, defaults to 127.0.0.1 - -imap-host example.com - Allowed IMAP email hostname on which hydroxide listens, defaults to 127.0.0.1 - -carddav-host example.com - Allowed SMTP email hostname on which hydroxide listens, defaults to 127.0.0.1 - -smtp-port example.com - SMTP port on which hydroxide listens, defaults to 1025 - -imap-port example.com - IMAP port on which hydroxide listens, defaults to 1143 - -carddav-port example.com - CardDAV port on which hydroxide listens, defaults to 8080 - -disable-imap - Disable IMAP for hydroxide serve - -disable-smtp - Disable SMTP for hydroxide serve - -disable-carddav - Disable CardDAV for hydroxide serve - -tls-cert /path/to/cert.pem - Path to the certificate to use for incoming connections (Optional) - -tls-key /path/to/key.pem - Path to the certificate key to use for incoming connections (Optional) - -tls-client-ca /path/to/ca.pem - If set, clients must provide a certificate signed by the given CA (Optional) - -Environment variables: - HYDROXIDE_BRIDGE_PASS Don't prompt for the bridge password, use this variable instead -` - -func main() { - flag.BoolVar(&debug, "debug", false, "Enable debug logs") - flag.StringVar(&apiEndpoint, "api-endpoint", defaultAPIEndpoint, "ProtonMail API endpoint") - flag.StringVar(&appVersion, "app-version", defaultAppVersion, "ProtonMail app version") - - 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") - disableSMTP := flag.Bool("disable-smtp", false, "Disable SMTP for hydroxide serve") - - imapHost := flag.String("imap-host", "127.0.0.1", "Allowed IMAP email hostname on which hydroxide listens, defaults to 127.0.0.1") - imapPort := flag.String("imap-port", "1143", "IMAP port on which hydroxide listens, defaults to 1143") - disableIMAP := flag.Bool("disable-imap", false, "Disable IMAP for hydroxide serve") - - 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") - disableCardDAV := flag.Bool("disable-carddav", false, "Disable CardDAV for hydroxide serve") - - tlsCert := flag.String("tls-cert", "", "Path to the certificate to use for incoming connections") - tlsCertKey := flag.String("tls-key", "", "Path to the certificate key to use for incoming connections") - tlsClientCA := flag.String("tls-client-ca", "", "If set, clients must provide a certificate signed by the given CA") - - 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) - sendmailCmd := flag.NewFlagSet("sendmail", flag.ExitOnError) - - flag.Usage = func() { - fmt.Print(usage) - } - - flag.Parse() - - tlsConfig, err := config.TLS(*tlsCert, *tlsCertKey, *tlsClientCA) - if err != nil { - log.Fatal(err) - } - - cmd := flag.Arg(0) - switch cmd { - case "auth": - authCmd.Parse(flag.Args()[1:]) - username := authCmd.Arg(0) - if username == "" { - log.Fatal("usage: hydroxide auth ") - } - - c := newClient() - - var a *protonmail.Auth - /*if cachedAuth, ok := auths[username]; ok { - var err error - a, err = c.AuthRefresh(a) - if err != nil { - // TODO: handle expired token error - log.Fatal(err) - } - }*/ - - var loginPassword string - if a == nil { - if pass, err := askPass("Password"); err != nil { - log.Fatal(err) - } else { - loginPassword = string(pass) - } - - authInfo, err := c.AuthInfo(username) - if err != nil { - log.Fatal(err) - } - - a, err = c.Auth(username, loginPassword, authInfo) - if err != nil { - log.Fatal(err) - } - - if a.TwoFactor.Enabled != 0 { - if a.TwoFactor.TOTP != 1 { - log.Fatal("Only TOTP is supported as a 2FA method") - } - - scanner := bufio.NewScanner(os.Stdin) - fmt.Printf("2FA TOTP code: ") - scanner.Scan() - code := scanner.Text() - - scope, err := c.AuthTOTP(code) - if err != nil { - log.Fatal(err) - } - a.Scope = scope - } - } - - var mailboxPassword string - if a.PasswordMode == protonmail.PasswordSingle { - mailboxPassword = loginPassword - } - if mailboxPassword == "" { - prompt := "Password" - if a.PasswordMode == protonmail.PasswordTwo { - prompt = "Mailbox password" - } - if pass, err := askPass(prompt); err != nil { - log.Fatal(err) - } else { - mailboxPassword = string(pass) - } - } - - keySalts, err := c.ListKeySalts() - if err != nil { - log.Fatal(err) - } - - _, err = c.Unlock(a, keySalts, mailboxPassword) - if err != nil { - log.Fatal(err) - } - - secretKey, bridgePassword, err := auth.GeneratePassword() - if err != nil { - log.Fatal(err) - } - - err = auth.EncryptAndSave(&auth.CachedAuth{ - Auth: *a, - LoginPassword: loginPassword, - MailboxPassword: mailboxPassword, - KeySalts: keySalts, - }, username, secretKey) - if err != nil { - log.Fatal(err) - } - - fmt.Println("Bridge password:", bridgePassword) - case "status": - usernames, err := auth.ListUsernames() - if err != nil { - log.Fatal(err) - } - - if len(usernames) == 0 { - fmt.Printf("No logged in user.\n") - } else { - fmt.Printf("%v logged in user(s):\n", len(usernames)) - for _, u := range usernames { - fmt.Printf("- %v\n", u) - } - } - case "export-secret-keys": - exportSecretKeysCmd.Parse(flag.Args()[1:]) - username := exportSecretKeysCmd.Arg(0) - if username == "" { - log.Fatal("usage: hydroxide export-secret-keys ") - } - - bridgePassword, err := askBridgePass() - if err != nil { - log.Fatal(err) - } - - _, privateKeys, err := auth.NewManager(newClient).Auth(username, bridgePassword) - if err != nil { - log.Fatal(err) - } - - wc, err := armor.Encode(os.Stdout, openpgp.PrivateKeyType, nil) - if err != nil { - log.Fatal(err) - } - - for _, key := range privateKeys { - if err := key.SerializePrivate(wc, nil); err != nil { - log.Fatal(err) - } - } - - if err := wc.Close(); err != nil { - log.Fatal(err) - } - case "import-messages": - importMessagesCmd.Parse(flag.Args()[1:]) - username := importMessagesCmd.Arg(0) - archivePath := importMessagesCmd.Arg(1) - if username == "" { - log.Fatal("usage: hydroxide import-messages [file]") - } - - f := os.Stdin - if archivePath != "" { - f, err = os.Open(archivePath) - if err != nil { - log.Fatal(err) - } - defer f.Close() - } - - bridgePassword, err := askBridgePass() - if err != nil { - log.Fatal(err) - } - - c, _, err := auth.NewManager(newClient).Auth(username, bridgePassword) - if err != nil { - log.Fatal(err) - } - - br := bufio.NewReader(f) - if ok, err := isMbox(br); err != nil { - log.Fatal(err) - } else if ok { - mr := mbox.NewReader(br) - for { - r, err := mr.NextMessage() - if err == io.EOF { - break - } else if err != nil { - log.Fatal(err) - } - if err := imports.ImportMessage(c, r); err != nil { - log.Fatal(err) - } - } - } else { - if err := imports.ImportMessage(c, br); err != nil { - log.Fatal(err) - } - } - case "export-messages": - // TODO: allow specifying multiple IDs - var convID, msgID string - exportMessagesCmd.StringVar(&convID, "conversation-id", "", "conversation ID") - exportMessagesCmd.StringVar(&msgID, "message-id", "", "message ID") - exportMessagesCmd.Parse(flag.Args()[1:]) - username := exportMessagesCmd.Arg(0) - if (convID == "" && msgID == "") || username == "" { - log.Fatal("usage: hydroxide export-messages [-conversation-id ] [-message-id ] ") - } - - bridgePassword, err := askBridgePass() - if err != nil { - log.Fatal(err) - } - - c, privateKeys, err := auth.NewManager(newClient).Auth(username, bridgePassword) - if err != nil { - log.Fatal(err) - } - - mboxWriter := mbox.NewWriter(os.Stdout) - - if convID != "" { - if err := exports.ExportConversationMbox(c, privateKeys, mboxWriter, convID); err != nil { - log.Fatal(err) - } - } - if msgID != "" { - if err := exports.ExportMessageMbox(c, privateKeys, mboxWriter, msgID); err != nil { - log.Fatal(err) - } - } - - if err := mboxWriter.Close(); err != nil { - log.Fatal(err) - } - case "smtp": - addr := *smtpHost + ":" + *smtpPort - authManager := auth.NewManager(newClient) - log.Fatal(listenAndServeSMTP(addr, debug, authManager, tlsConfig)) - case "imap": - addr := *imapHost + ":" + *imapPort - authManager := auth.NewManager(newClient) - eventsManager := events.NewManager() - log.Fatal(listenAndServeIMAP(addr, debug, authManager, eventsManager, tlsConfig)) - case "carddav": - addr := *carddavHost + ":" + *carddavPort - authManager := auth.NewManager(newClient) - eventsManager := events.NewManager() - log.Fatal(listenAndServeCardDAV(addr, authManager, eventsManager, tlsConfig)) - case "serve": - smtpAddr := *smtpHost + ":" + *smtpPort - imapAddr := *imapHost + ":" + *imapPort - carddavAddr := *carddavHost + ":" + *carddavPort - - authManager := auth.NewManager(newClient) - eventsManager := events.NewManager() - - done := make(chan error, 3) - if !*disableSMTP { - go func() { - done <- listenAndServeSMTP(smtpAddr, debug, authManager, tlsConfig) - }() - } - if !*disableIMAP { - go func() { - done <- listenAndServeIMAP(imapAddr, debug, authManager, eventsManager, tlsConfig) - }() - } - if !*disableCardDAV { - go func() { - done <- listenAndServeCardDAV(carddavAddr, authManager, eventsManager, tlsConfig) - }() - } - log.Fatal(<-done) - case "sendmail": - username := flag.Arg(1) - if username == "" || flag.Arg(2) != "--" { - log.Fatal("usage: hydroxide sendmail -- ") - } - - // TODO: other sendmail flags - var dotEOF bool - sendmailCmd.BoolVar(&dotEOF, "i", false, "don't treat a line with only a . character as the end of input") - sendmailCmd.Parse(flag.Args()[3:]) - rcpt := sendmailCmd.Args() - - bridgePassword, err := askBridgePass() - if err != nil { - log.Fatal(err) - } - - c, privateKeys, err := auth.NewManager(newClient).Auth(username, bridgePassword) - if err != nil { - log.Fatal(err) - } - - u, err := c.GetCurrentUser() - if err != nil { - log.Fatal(err) - } - - addrs, err := c.ListAddresses() - if err != nil { - log.Fatal(err) - } - - err = smtpbackend.SendMail(c, u, privateKeys, addrs, rcpt, os.Stdin) - if err != nil { - log.Fatal(err) - } - default: - fmt.Print(usage) - if cmd != "help" { - log.Fatal("Unrecognized command") - } - } -}