Files
proton-bridge/internal/frontend/cli/accounts.go

415 lines
11 KiB
Go

// Copyright (c) 2024 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package cli
import (
"context"
"fmt"
"strings"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/hv"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/abiosoft/ishell"
)
func (f *frontendCLI) listAccounts(_ *ishell.Context) {
spacing := "%-2d: %-20s (%-15s, %-15s)\n"
f.Printf(bold(strings.ReplaceAll(spacing, "d", "s")), "#", "account", "status", "address mode")
for idx, userID := range f.bridge.GetUserIDs() {
user, err := f.bridge.GetUserInfo(userID)
if err != nil {
panic(err)
}
var state string
switch user.State {
case bridge.SignedOut:
state = "signed out"
case bridge.Locked:
state = "locked"
case bridge.Connected:
state = "connected"
default:
panic("Unknown user state")
}
f.Printf(spacing, idx, user.Username, state, user.AddressMode)
}
f.Println()
}
func (f *frontendCLI) showAccountInfo(c *ishell.Context) {
user := f.askUserByIndexOrName(c)
if user.UserID == "" {
return
}
switch user.State {
case bridge.SignedOut:
f.Printf("Please login to %s to get email client configuration.\n", bold(user.Username))
return
case bridge.Locked:
f.Printf("User %s is currently locked. Please wait and try again.\n", bold(user.Username))
return
case bridge.Connected:
default:
}
if user.AddressMode == vault.CombinedMode {
f.showAccountAddressInfo(user, user.Addresses[0])
} else {
for _, address := range user.Addresses {
f.showAccountAddressInfo(user, address)
}
}
}
func (f *frontendCLI) showAccountAddressInfo(user bridge.UserInfo, address string) {
const (
StartTLS = "STARTTLS"
SSL = "SSL"
)
imapSecurity := StartTLS
if f.bridge.GetIMAPSSL() {
imapSecurity = SSL
}
smtpSecurity := StartTLS
if f.bridge.GetSMTPSSL() {
smtpSecurity = SSL
}
f.Println(bold("Configuration for " + address))
f.Printf("IMAP Settings\nAddress: %s\nIMAP port: %d\nUsername: %s\nPassword: %s\nSecurity: %s\n",
constants.Host,
f.bridge.GetIMAPPort(),
address,
user.BridgePass,
imapSecurity,
)
f.Println("")
f.Printf("SMTP Settings\nAddress: %s\nSMTP port: %d\nUsername: %s\nPassword: %s\nSecurity: %s\n",
constants.Host,
f.bridge.GetSMTPPort(),
address,
user.BridgePass,
smtpSecurity,
)
f.Println("")
}
func (f *frontendCLI) promptHvURL(details *proton.APIHVDetails) {
hvURL := hv.FormatHvURL(details)
fmt.Print("\nHuman Verification requested. Please open the URL below in a browser and press ENTER when the challenge has been completed.\n\n", hvURL+"\n")
f.ReadLine()
fmt.Println("Authenticating ...")
}
func (f *frontendCLI) loginAccount(c *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
loginName := ""
if len(c.Args) > 0 {
user := f.getUserByIndexOrName(c.Args[0])
if user.UserID != "" {
loginName = user.Addresses[0]
}
}
if loginName == "" {
loginName = f.readStringInAttempts("Username", c.ReadLine, isNotEmpty)
if loginName == "" {
return
}
} else {
f.Println("Username:", loginName)
}
password := f.readStringInAttempts("Password", c.ReadPassword, isNotEmpty)
if password == "" {
return
}
f.Println("Authenticating ... ")
client, auth, err := f.bridge.LoginAuth(context.Background(), loginName, []byte(password), nil)
var hvDetails *proton.APIHVDetails
hvDetails, hvErr := hv.VerifyAndExtractHvRequest(err)
if hvErr != nil || hvDetails != nil {
if hvErr != nil {
f.printAndLogError("Cannot login", hvErr)
return
}
f.promptHvURL(hvDetails)
client, auth, err = f.bridge.LoginAuth(context.Background(), loginName, []byte(password), hvDetails)
}
if err != nil {
f.printAndLogError("Cannot login: ", err)
return
}
if auth.TwoFA.Enabled&proton.HasTOTP != 0 {
code := f.readStringInAttempts("Two factor code", c.ReadLine, isNotEmpty)
if code == "" {
f.printAndLogError("Cannot login: need two factor code")
return
}
if err := client.Auth2FA(context.Background(), proton.Auth2FAReq{TwoFactorCode: code}); err != nil {
f.printAndLogError("Cannot login: ", err)
return
}
}
var keyPass []byte
if auth.PasswordMode == proton.TwoPasswordMode {
keyPass = []byte(f.readStringInAttempts("Mailbox password", c.ReadPassword, isNotEmpty))
if len(keyPass) == 0 {
f.printAndLogError("Cannot login: need mailbox password")
return
}
} else {
keyPass = []byte(password)
}
userID, err := f.bridge.LoginUser(context.Background(), client, auth, keyPass, hvDetails)
hvDetails, hvErr = hv.VerifyAndExtractHvRequest(err)
if hvDetails != nil || hvErr != nil {
if hvErr != nil {
f.printAndLogError("Cannot login: ", hvErr)
return
}
f.loginAccountHv(c, loginName, password, keyPass, hvDetails)
return
}
if err != nil {
f.processAPIError(err)
return
}
user, err := f.bridge.GetUserInfo(userID)
if err != nil {
panic(err)
}
f.Printf("Account %s was added successfully.\n", bold(user.Username))
}
func (f *frontendCLI) loginAccountHv(c *ishell.Context, loginName string, password string, keyPass []byte, hvDetails *proton.APIHVDetails) {
f.promptHvURL(hvDetails)
client, auth, err := f.bridge.LoginAuth(context.Background(), loginName, []byte(password), hvDetails)
if err != nil {
f.printAndLogError("Cannot login: ", err)
return
}
if auth.TwoFA.Enabled&proton.HasTOTP != 0 {
code := f.readStringInAttempts("Two factor code", c.ReadLine, isNotEmpty)
if code == "" {
f.printAndLogError("Cannot login: need two factor code")
return
}
if err := client.Auth2FA(context.Background(), proton.Auth2FAReq{TwoFactorCode: code}); err != nil {
f.printAndLogError("Cannot login: ", err)
return
}
}
userID, err := f.bridge.LoginUser(context.Background(), client, auth, keyPass, hvDetails)
if err != nil {
f.processAPIError(err)
return
}
user, err := f.bridge.GetUserInfo(userID)
if err != nil {
panic(err)
}
f.Printf("Account %s was added successfully.\n", bold(user.Username))
}
func (f *frontendCLI) logoutAccount(c *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
user := f.askUserByIndexOrName(c)
if user.UserID == "" {
return
}
if f.yesNoQuestion("Are you sure you want to logout account " + bold(user.Username)) {
if err := f.bridge.LogoutUser(context.Background(), user.UserID); err != nil {
f.printAndLogError("Logging out failed: ", err)
}
}
}
func (f *frontendCLI) deleteAccount(c *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
user := f.askUserByIndexOrName(c)
if user.UserID == "" {
return
}
if f.yesNoQuestion("Are you sure you want to " + bold("remove account "+user.Username)) {
if err := f.bridge.DeleteUser(context.Background(), user.UserID); err != nil {
f.printAndLogError("Cannot delete account: ", err)
return
}
}
}
func (f *frontendCLI) deleteAccounts(c *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
if !f.yesNoQuestion("Do you really want remove all accounts") {
return
}
for _, userID := range f.bridge.GetUserIDs() {
user, err := f.bridge.GetUserInfo(userID)
if err != nil {
f.printAndLogError("Cannot get user info: ", err)
return
}
if err := f.bridge.DeleteUser(context.Background(), user.UserID); err != nil {
f.printAndLogError("Cannot delete account ", user.Username, ": ", err)
return
}
}
c.Println("Keychain cleared")
}
func (f *frontendCLI) deleteEverything(c *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
if !f.yesNoQuestion("Do you really want remove everything") {
return
}
f.bridge.FactoryReset(context.Background())
c.Println("Everything cleared")
f.restarter.Set(true, false)
f.Stop()
}
func (f *frontendCLI) changeMode(c *ishell.Context) {
user := f.askUserByIndexOrName(c)
if user.UserID == "" {
return
}
var targetMode vault.AddressMode
if user.AddressMode == vault.CombinedMode {
targetMode = vault.SplitMode
} else {
targetMode = vault.CombinedMode
}
if !f.yesNoQuestion("Are you sure you want to change the mode for account " + bold(user.Username) + " to " + bold(targetMode.String())) {
return
}
if err := f.bridge.SetAddressMode(context.Background(), user.UserID, targetMode); err != nil {
f.printAndLogError("Cannot switch address mode:", err)
}
f.Printf("Address mode for account %s changed to %s\n", user.Username, targetMode)
}
func (f *frontendCLI) configureAppleMail(c *ishell.Context) {
user := f.askUserByIndexOrName(c)
if user.UserID == "" {
return
}
if !f.yesNoQuestion("Are you sure you want to configure Apple Mail for " + bold(user.Username) + " with address " + bold(user.Addresses[0])) {
return
}
cert, _ := f.bridge.GetBridgeTLSCert()
installer := certs.NewInstaller()
if !installer.IsCertInstalled(cert) {
f.Println("Apple Mail requires that a TLS certificate for bridge IMAP and SMTP server is installed in your system keychain.")
f.Println("Please provide your credentials in the system popup dialog in order to continue.")
if err := installer.InstallCert(cert); err != nil {
f.printAndLogError(err)
return
}
}
if err := f.bridge.ConfigureAppleMail(context.Background(), user.UserID, user.Addresses[0]); err != nil {
f.printAndLogError(err)
return
}
f.Printf("Apple Mail configured for %v with address %v\n", user.Username, user.Addresses[0])
}
func (f *frontendCLI) badEventSynchronize(_ *ishell.Context) {
f.badEventFeedback(true)
}
func (f *frontendCLI) badEventLogout(_ *ishell.Context) {
f.badEventFeedback(false)
}
func (f *frontendCLI) badEventFeedback(doResync bool) {
if f.badUserID == "" {
f.Printf("Error: There was no unresolved bad event.")
return
}
action := "synchronize"
if !doResync {
action = "logout"
}
if err := f.bridge.SendBadEventUserFeedback(context.Background(), f.badUserID, doResync); err != nil {
f.Printf("Error: failed to send %s feedback: %w", action, err)
return
}
f.badUserID = ""
}