// Copyright (c) 2020 Proton Technologies AG // // This file is part of ProtonMail Bridge. // // ProtonMail 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. // // ProtonMail 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 ProtonMail Bridge. If not, see . // Package cli provides CLI interface of the Bridge. package cli import ( "github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/frontend/types" "github.com/ProtonMail/proton-bridge/internal/preferences" "github.com/ProtonMail/proton-bridge/pkg/config" "github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/abiosoft/ishell" "github.com/sirupsen/logrus" ) var ( log = logrus.WithField("pkg", "frontend/cli") //nolint[gochecknoglobals] ) type frontendCLI struct { *ishell.Shell config *config.Config preferences *config.Preferences eventListener listener.Listener updates types.Updater bridge types.Bridger appRestart bool } // New returns a new CLI frontend configured with the given options. func New( //nolint[funlen] panicHandler types.PanicHandler, config *config.Config, preferences *config.Preferences, eventListener listener.Listener, updates types.Updater, bridge types.Bridger, ) *frontendCLI { //nolint[golint] fe := &frontendCLI{ Shell: ishell.New(), config: config, preferences: preferences, eventListener: eventListener, updates: updates, bridge: bridge, appRestart: false, } // Clear commands. clearCmd := &ishell.Cmd{Name: "clear", Help: "remove stored accounts and preferences. (alias: cl)", Aliases: []string{"cl"}, } clearCmd.AddCmd(&ishell.Cmd{Name: "cache", Help: "remove stored preferences for accounts (aliases: c, prefs, preferences)", Aliases: []string{"c", "prefs", "preferences"}, Func: fe.deleteCache, }) clearCmd.AddCmd(&ishell.Cmd{Name: "accounts", Help: "remove all accounts from keychain. (aliases: k, keychain)", Aliases: []string{"a", "k", "keychain"}, Func: fe.deleteAccounts, }) fe.AddCmd(clearCmd) // Change commands. changeCmd := &ishell.Cmd{Name: "change", Help: "change server or account settings (aliases: ch, switch)", Aliases: []string{"ch", "switch"}, } changeCmd.AddCmd(&ishell.Cmd{Name: "mode", Help: "switch between combined addresses and split addresses mode for account. Use index or account name as parameter. (alias: m)", Aliases: []string{"m"}, Func: fe.changeMode, Completer: fe.completeUsernames, }) changeCmd.AddCmd(&ishell.Cmd{Name: "port", Help: "change port numbers of IMAP and SMTP servers. (alias: p)", Aliases: []string{"p"}, Func: fe.changePort, }) changeCmd.AddCmd(&ishell.Cmd{Name: "proxy", Help: "allow or disallow bridge to securely connect to proton via a third party when it is being blocked", Func: fe.toggleAllowProxy, }) changeCmd.AddCmd(&ishell.Cmd{Name: "smtp-security", Help: "change port numbers of IMAP and SMTP servers.(alias: ssl, starttls)", Aliases: []string{"ssl", "starttls"}, Func: fe.changeSMTPSecurity, }) fe.AddCmd(changeCmd) // Check commands. checkCmd := &ishell.Cmd{Name: "check", Help: "check internet connection or new version."} checkCmd.AddCmd(&ishell.Cmd{Name: "updates", Help: "check for Bridge updates. (aliases: u, v, version)", Aliases: []string{"u", "version", "v"}, Func: fe.checkUpdates, }) checkCmd.AddCmd(&ishell.Cmd{Name: "internet", Help: "check internet connection. (aliases: i, conn, connection)", Aliases: []string{"i", "con", "connection"}, Func: fe.checkInternetConnection, }) fe.AddCmd(checkCmd) // Print info commands. fe.AddCmd(&ishell.Cmd{Name: "log-dir", Help: "print path to directory with logs. (aliases: log, logs)", Aliases: []string{"log", "logs"}, Func: fe.printLogDir, }) fe.AddCmd(&ishell.Cmd{Name: "manual", Help: "print URL with instructions. (alias: man)", Aliases: []string{"man"}, Func: fe.printManual, }) fe.AddCmd(&ishell.Cmd{Name: "release-notes", Help: "print release notes. (aliases: notes, fixed-bugs, bugs, ver, version)", Aliases: []string{"notes", "fixed-bugs", "bugs", "ver", "version"}, Func: fe.printLocalReleaseNotes, }) fe.AddCmd(&ishell.Cmd{Name: "credits", Help: "print used resources.", Func: fe.printCredits, }) // Account commands. fe.AddCmd(&ishell.Cmd{Name: "list", Help: "print the list of accounts. (aliases: l, ls)", Func: fe.noAccountWrapper(fe.listAccounts), Aliases: []string{"l", "ls"}, }) fe.AddCmd(&ishell.Cmd{Name: "info", Help: "print the configuration for account. Use index or account name as parameter. (alias: i)", Func: fe.noAccountWrapper(fe.showAccountInfo), Completer: fe.completeUsernames, Aliases: []string{"i"}, }) fe.AddCmd(&ishell.Cmd{Name: "login", Help: "login procedure to add or connect account. Optionally use index or account as parameter. (aliases: a, add, con, connect)", Func: fe.loginAccount, Aliases: []string{"add", "a", "con", "connect"}, Completer: fe.completeUsernames, }) fe.AddCmd(&ishell.Cmd{Name: "logout", Help: "disconnect the account. Use index or account name as parameter. (aliases: d, disconnect)", Func: fe.noAccountWrapper(fe.logoutAccount), Aliases: []string{"d", "disconnect"}, Completer: fe.completeUsernames, }) fe.AddCmd(&ishell.Cmd{Name: "delete", Help: "remove the account from keychain. Use index or account name as parameter. (aliases: del, rm, remove)", Func: fe.noAccountWrapper(fe.deleteAccount), Aliases: []string{"del", "rm", "remove"}, Completer: fe.completeUsernames, }) // System commands. fe.AddCmd(&ishell.Cmd{Name: "restart", Help: "restart the bridge.", Func: fe.restart, }) go func() { defer panicHandler.HandlePanic() fe.watchEvents() }() fe.eventListener.RetryEmit(events.TLSCertIssue) fe.eventListener.RetryEmit(events.ErrorEvent) return fe } func (f *frontendCLI) watchEvents() { errorCh := f.getEventChannel(events.ErrorEvent) internetOffCh := f.getEventChannel(events.InternetOffEvent) internetOnCh := f.getEventChannel(events.InternetOnEvent) addressChangedCh := f.getEventChannel(events.AddressChangedEvent) addressChangedLogoutCh := f.getEventChannel(events.AddressChangedLogoutEvent) logoutCh := f.getEventChannel(events.LogoutEvent) certIssue := f.getEventChannel(events.TLSCertIssue) for { select { case errorDetails := <-errorCh: f.Println("Bridge failed:", errorDetails) case <-internetOffCh: f.notifyInternetOff() case <-internetOnCh: f.notifyInternetOn() case address := <-addressChangedCh: f.Printf("Address changed for %s. You may need to reconfigure your email client.", address) case address := <-addressChangedLogoutCh: f.notifyLogout(address) case userID := <-logoutCh: user, err := f.bridge.GetUser(userID) if err != nil { return } f.notifyLogout(user.Username()) case <-certIssue: f.notifyCertIssue() } } } func (f *frontendCLI) getEventChannel(event string) <-chan string { ch := make(chan string) f.eventListener.Add(event, ch) return ch } // IsAppRestarting returns whether the app is currently set to restart. func (f *frontendCLI) IsAppRestarting() bool { return f.appRestart } // Loop starts the frontend loop with an interactive shell. func (f *frontendCLI) Loop(credentialsError error) error { if credentialsError != nil { f.notifyCredentialsError() return credentialsError } f.preferences.SetBool(preferences.FirstStartKey, false) f.Print(` Welcome to ProtonMail Bridge interactive shell ___....___ ^^ __..-:'':__:..:__:'':-..__ _.-:__:.-:'': : : :'':-.:__:-._ .':.-: : : : : : : : : :._:'. _ :.': : : : : : : : : : : :'.: _ [ ]: : : : : : : : : : : : : :[ ] [ ]: : : : : : : : : : : : : :[ ] :::::::::[ ]:__:__:__:__:__:__:__:__:__:__:__:__:__:[ ]::::::::::: !!!!!!!!![ ]!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!![ ]!!!!!!!!!!! ^^^^^^^^^[ ]^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^[ ]^^^^^^^^^^^ [ ] [ ] [ ] [ ] jgs [ ] [ ] ~~^_~^~/ \~^-~^~ _~^-~_^~-^~_^~~-^~_~^~-~_~-^~_^/ \~^ ~~_ ^ `) f.Run() return nil }