hydroxide-push/cmd/hydroxide/main.go

573 lines
15 KiB
Go
Raw Normal View History

2017-08-22 01:04:16 +03:00
package main
import (
"bufio"
"bytes"
"crypto/tls"
2017-09-09 16:37:03 +03:00
"flag"
2017-08-22 01:04:16 +03:00
"fmt"
2017-09-09 16:37:03 +03:00
"io"
2017-08-22 01:04:16 +03:00
"log"
2017-09-03 21:11:01 +03:00
"net/http"
2017-08-22 01:04:16 +03:00
"os"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/armor"
2018-10-21 13:15:20 +03:00
imapserver "github.com/emersion/go-imap/server"
"github.com/emersion/go-mbox"
2017-12-02 17:23:06 +02:00
"github.com/emersion/go-smtp"
2022-08-21 17:48:47 +03:00
"golang.org/x/term"
2017-12-02 17:23:06 +02:00
"github.com/emersion/hydroxide/auth"
2017-09-03 21:11:01 +03:00
"github.com/emersion/hydroxide/carddav"
"github.com/emersion/hydroxide/config"
2018-01-11 19:17:11 +02:00
"github.com/emersion/hydroxide/events"
"github.com/emersion/hydroxide/exports"
2017-12-03 15:58:24 +02:00
imapbackend "github.com/emersion/hydroxide/imap"
2019-07-30 20:51:46 +03:00
"github.com/emersion/hydroxide/imports"
2018-10-21 13:15:20 +03:00
"github.com/emersion/hydroxide/protonmail"
2017-12-02 17:23:06 +02:00
smtpbackend "github.com/emersion/hydroxide/smtp"
2017-08-22 01:04:16 +03:00
)
const (
defaultAPIEndpoint = "https://mail.proton.me/api"
defaultAppVersion = "Other"
)
var (
debug bool
apiEndpoint string
appVersion string
)
2017-09-09 16:37:03 +03:00
func newClient() *protonmail.Client {
return &protonmail.Client{
RootURL: apiEndpoint,
AppVersion: appVersion,
Debug: debug,
2017-08-22 01:04:16 +03:00
}
2017-09-09 16:37:03 +03:00
}
2017-08-22 01:04:16 +03:00
2021-11-10 15:01:49 +02:00
func askPass(prompt string) ([]byte, error) {
f := os.Stdin
2022-08-21 17:48:47 +03:00
if !term.IsTerminal(int(f.Fd())) {
2021-11-10 15:29:54 +02:00
// 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()
}
2021-11-10 15:01:49 +02:00
fmt.Fprintf(os.Stderr, "%v: ", prompt)
2022-08-21 17:48:47 +03:00
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
2020-02-29 13:34:20 +02:00
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
2020-02-29 13:34:20 +02:00
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
}
2020-09-14 12:30:12 +03:00
const usage = `usage: hydroxide [options...] <command>
Commands:
auth <username> Login to ProtonMail via hydroxide
carddav Run hydroxide as a CardDAV server
export-secret-keys <username> Export secret keys
imap Run hydroxide as an IMAP server
import-messages <username> [file] Import messages
export-messages [options...] <username> Export messages
2021-11-10 15:29:54 +02:00
sendmail <username> -- <args...> sendmail(1) interface
serve Run all servers
smtp Run hydroxide as an SMTP server
status View hydroxide status
2020-09-14 12:30:12 +03:00
Global options:
-debug
Enable debug logs
-api-endpoint <url>
ProtonMail API endpoint
-app-version <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
`
2017-09-09 16:37:03 +03:00
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")
2020-02-29 13:34:20 +02:00
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)
2019-07-30 20:51:46 +03:00
importMessagesCmd := flag.NewFlagSet("import-messages", flag.ExitOnError)
exportMessagesCmd := flag.NewFlagSet("export-messages", flag.ExitOnError)
2021-11-10 15:29:54 +02:00
sendmailCmd := flag.NewFlagSet("sendmail", flag.ExitOnError)
flag.Usage = func() {
fmt.Print(usage)
}
2017-09-09 16:37:03 +03:00
flag.Parse()
tlsConfig, err := config.TLS(*tlsCert, *tlsCertKey, *tlsClientCA)
if err != nil {
log.Fatal(err)
}
2019-04-23 00:06:49 +03:00
cmd := flag.Arg(0)
switch cmd {
2017-09-09 16:37:03 +03:00
case "auth":
authCmd.Parse(flag.Args()[1:])
2019-07-30 20:51:46 +03:00
username := authCmd.Arg(0)
2019-04-23 00:03:20 +03:00
if username == "" {
log.Fatal("usage: hydroxide auth <username>")
}
2017-09-09 16:37:03 +03:00
c := newClient()
var a *protonmail.Auth
2017-09-09 16:37:03 +03:00
/*if cachedAuth, ok := auths[username]; ok {
var err error
a, err = c.AuthRefresh(a)
2017-09-09 16:37:03 +03:00
if err != nil {
// TODO: handle expired token error
log.Fatal(err)
}
}*/
var loginPassword string
if a == nil {
2021-11-10 15:01:49 +02:00
if pass, err := askPass("Password"); err != nil {
log.Fatal(err)
} else {
loginPassword = string(pass)
}
2017-09-09 16:37:03 +03:00
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: ")
2017-09-09 16:37:03 +03:00
scanner.Scan()
code := scanner.Text()
2017-09-09 16:37:03 +03:00
scope, err := c.AuthTOTP(code)
if err != nil {
log.Fatal(err)
}
a.Scope = scope
2017-09-09 16:37:03 +03:00
}
2017-08-22 13:07:31 +03:00
}
2017-08-22 11:51:48 +03:00
2017-09-09 16:37:03 +03:00
var mailboxPassword string
if a.PasswordMode == protonmail.PasswordSingle {
2017-09-09 16:37:03 +03:00
mailboxPassword = loginPassword
}
if mailboxPassword == "" {
2021-11-10 15:01:49 +02:00
prompt := "Password"
if a.PasswordMode == protonmail.PasswordTwo {
2021-11-10 15:01:49 +02:00
prompt = "Mailbox password"
2017-09-09 16:37:03 +03:00
}
2021-11-10 15:01:49 +02:00
if pass, err := askPass(prompt); err != nil {
log.Fatal(err)
} else {
mailboxPassword = string(pass)
}
2017-09-09 16:37:03 +03:00
}
2017-08-22 01:04:16 +03:00
keySalts, err := c.ListKeySalts()
if err != nil {
log.Fatal(err)
}
_, err = c.Unlock(a, keySalts, mailboxPassword)
2017-08-22 13:07:31 +03:00
if err != nil {
log.Fatal(err)
}
secretKey, bridgePassword, err := auth.GeneratePassword()
if err != nil {
2017-09-09 16:37:03 +03:00
log.Fatal(err)
2017-08-22 13:07:31 +03:00
}
err = auth.EncryptAndSave(&auth.CachedAuth{
2019-12-09 13:27:16 +02:00
Auth: *a,
LoginPassword: loginPassword,
MailboxPassword: mailboxPassword,
2019-12-09 13:27:16 +02:00
KeySalts: keySalts,
}, username, secretKey)
2017-08-22 13:07:31 +03:00
if err != nil {
log.Fatal(err)
}
2017-08-22 10:41:47 +03:00
2017-09-09 16:37:03 +03:00
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)
}
}
2019-04-23 00:03:20 +03:00
case "export-secret-keys":
exportSecretKeysCmd.Parse(flag.Args()[1:])
2019-07-30 20:51:46 +03:00
username := exportSecretKeysCmd.Arg(0)
2019-04-23 00:03:20 +03:00
if username == "" {
log.Fatal("usage: hydroxide export-secret-keys <username>")
}
bridgePassword, err := askBridgePass()
if err != nil {
2019-04-23 00:03:20 +03:00
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)
}
2019-07-30 20:51:46 +03:00
case "import-messages":
importMessagesCmd.Parse(flag.Args()[1:])
2019-07-30 20:51:46 +03:00
username := importMessagesCmd.Arg(0)
archivePath := importMessagesCmd.Arg(1)
if username == "" {
log.Fatal("usage: hydroxide import-messages <username> [file]")
2019-07-30 20:51:46 +03:00
}
f := os.Stdin
if archivePath != "" {
f, err = os.Open(archivePath)
if err != nil {
log.Fatal(err)
}
defer f.Close()
2019-07-30 20:51:46 +03:00
}
bridgePassword, err := askBridgePass()
if err != nil {
2019-07-30 20:51:46 +03:00
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 {
2019-07-30 20:51:46 +03:00
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)
}
2019-07-30 20:51:46 +03:00
}
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 <id>] [-message-id <id>] <username>")
}
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)
}
2017-12-02 17:23:06 +02:00
case "smtp":
addr := *smtpHost + ":" + *smtpPort
authManager := auth.NewManager(newClient)
log.Fatal(listenAndServeSMTP(addr, debug, authManager, tlsConfig))
2017-12-03 15:58:24 +02:00
case "imap":
addr := *imapHost + ":" + *imapPort
authManager := auth.NewManager(newClient)
2018-01-12 14:20:17 +02:00
eventsManager := events.NewManager()
log.Fatal(listenAndServeIMAP(addr, debug, authManager, eventsManager, tlsConfig))
2017-12-02 17:23:06 +02:00
case "carddav":
addr := *carddavHost + ":" + *carddavPort
authManager := auth.NewManager(newClient)
2018-01-12 14:20:17 +02:00
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)
2021-11-10 15:29:54 +02:00
case "sendmail":
username := flag.Arg(1)
if username == "" || flag.Arg(2) != "--" {
log.Fatal("usage: hydroxide sendmail <username> -- <args...>")
}
// 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 {
2021-11-10 15:29:54 +02:00
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)
}
2017-09-09 16:37:03 +03:00
default:
fmt.Print(usage)
2019-04-23 00:06:49 +03:00
if cmd != "help" {
log.Fatal("Unrecognized command")
}
2017-09-09 16:37:03 +03:00
}
2017-08-22 01:04:16 +03:00
}