aboutsummaryrefslogblamecommitdiffstats
path: root/widgets/account-wizard.go
blob: 9ad814b5d5735b95e7c17e99aff071dcbac8a359 (plain) (tree)
1
2
3
4
5
6
7
8
9


               

                
             
                 
            
                 
              
                 
                 
              
              
 
                                             
                                     

                               
 

                                       
                                    





















                                 
                       



                            

























                                     














                                                                            
                                                  
                                 
                                                                          
                                   
                                    
                                   



                                                                                         
                                                                                                               


                                                                                         
                                                                                                               
                                                                          















                                                                         





                                                                                           







                                                                     
                          
                                



                                                                     



                                                                                    
                                                        





                                                                                              











                                                            









                                                                                      
                                 
                                                              

                        


                                                                                       
                                                       

                                                                                                 
                                                                  
                        
                                                                                
                                                                  


                                            
                                                             

                        
                                                                              
                                                                  


                                         
                                                             

                        
                                                                           
                                                                  


                                      
                                                                













                                                                                          
                                                                                    







                                                                       
                                          
                                         
                                                                            
         

                                                    














                                                                                        
                                 
                                                              
          
                                                                        
                                                          
                          
                                      
                                                                  


                                               
                                                               

                          
                                      
                                                                  


                                               
                                                               


                                             
                                                                               
                                                                  


                                             
                                                               

                          
                                             
                                                                  
                         
                                         


                                     
                                                                     










                                                       
                                                                           
                                        
                                                                        
                                                   
                                             

                                                                            
                                   
         

                                                    

















                                                                                        
                                 
                                                              
          
                                                                        
                                                          
                          
                                      
                                                                  


                                               
                                                               

                          
                                      
                                                                  


                                               
                                                               


                                             
                                                                               
                                                                  


                                             
                                                               

                          
                                             
                                                                  
                         
                                         


                                     
                                                                     










                                                       
                                                                           
                                        
                                                                        
                                                   
                                                                        
                          
                                                                  

                                                                           








                                                            
                                             

                                                                            
                                             
         

                                                    

                                                                                                            
                                 
                                                              

                                     



                                                                                                  
                                                          
                                        
                           
                                         
                         
                                                       



                                                  
                                           
                              
                                            

                 

                                                    




                                                                       



                                                                        

                                                                    
                                                  













                                                           
                                                                  





































                                                                            
                       
                                  








                                                                              



                                                                                                                                                 
                                                                                                                                                 

                                                                 
                                                                                                                                       

         
                              
                                                                                   







                                                         

         





                                                                

                                        
                                  
                               
                                                     
                                                                                   
         


                                       
                                                           
 
                                                                            
                       
                                                                          

                      



                                                 











                                                                                
                                                          
                                       
                                                                  

                         

         
                                            

 














































                                                                                   
                                
                           
                               
                           
                                        




























                                                                                   
                       








































                                                                       
                                                     


                                    

                                      

































                                                                           















                                                                         
package widgets

import (
	"errors"
	"fmt"
	"net"
	"net/url"
	"os"
	"os/exec"
	"path"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/emersion/go-message/mail"
	"github.com/gdamore/tcell/v2"
	"github.com/go-ini/ini"
	"github.com/kyoh86/xdg"

	"git.sr.ht/~rjarry/aerc/config"
	"git.sr.ht/~rjarry/aerc/lib/ui"
	"git.sr.ht/~rjarry/aerc/log"
)

const (
	CONFIGURE_BASICS   = iota
	CONFIGURE_INCOMING = iota
	CONFIGURE_OUTGOING = iota
	CONFIGURE_COMPLETE = iota
)

const (
	IMAP_OVER_TLS = iota
	IMAP_STARTTLS = iota
	IMAP_INSECURE = iota
)

const (
	SMTP_OVER_TLS = iota
	SMTP_STARTTLS = iota
	SMTP_INSECURE = iota
)

type AccountWizard struct {
	aerc      *Aerc
	step      int
	steps     []*ui.Grid
	focus     int
	temporary bool
	// CONFIGURE_BASICS
	accountName *ui.TextInput
	email       *ui.TextInput
	fullName    *ui.TextInput
	basics      []ui.Interactive
	// CONFIGURE_INCOMING
	imapUsername *ui.TextInput
	imapPassword *ui.TextInput
	imapServer   *ui.TextInput
	imapMode     int
	imapStr      *ui.Text
	imapUrl      url.URL
	incoming     []ui.Interactive
	// CONFIGURE_OUTGOING
	smtpUsername *ui.TextInput
	smtpPassword *ui.TextInput
	smtpServer   *ui.TextInput
	smtpMode     int
	smtpStr      *ui.Text
	smtpUrl      url.URL
	copySent     bool
	outgoing     []ui.Interactive
	// CONFIGURE_COMPLETE
	complete []ui.Interactive
}

func showPasswordWarning(aerc *Aerc) {
	title := "The Wizard will store your passwords in plaintext"
	text := "It is recommended to remove the plaintext passwords " +
		"and use your personal password store with " +
		"'source-cred-cmd' and 'outgoing-cred-cmd' after the setup."
	warning := NewSelectorDialog(
		title, text, []string{"OK"}, 0,
		aerc.SelectedAccountUiConfig(),
		func(_ string, _ error) {
			aerc.CloseDialog()
		},
	)
	aerc.AddDialog(warning)
}

func NewAccountWizard(aerc *Aerc) *AccountWizard {
	wizard := &AccountWizard{
		accountName:  ui.NewTextInput("", config.Ui).Prompt("> "),
		aerc:         aerc,
		temporary:    false,
		copySent:     true,
		email:        ui.NewTextInput("", config.Ui).Prompt("> "),
		fullName:     ui.NewTextInput("", config.Ui).Prompt("> "),
		imapPassword: ui.NewTextInput("", config.Ui).Prompt("] ").Password(true),
		imapServer:   ui.NewTextInput("", config.Ui).Prompt("> "),
		imapStr:      ui.NewText("Connection URL: imaps://", config.Ui.GetStyle(config.STYLE_DEFAULT)),
		imapUsername: ui.NewTextInput("", config.Ui).Prompt("> "),
		smtpPassword: ui.NewTextInput("", config.Ui).Prompt("] ").Password(true),
		smtpServer:   ui.NewTextInput("", config.Ui).Prompt("> "),
		smtpStr:      ui.NewText("Connection URL: smtps://", config.Ui.GetStyle(config.STYLE_DEFAULT)),
		smtpUsername: ui.NewTextInput("", config.Ui).Prompt("> "),
	}

	// Autofill some stuff for the user
	wizard.email.OnChange(func(_ *ui.TextInput) {
		value := wizard.email.String()
		wizard.imapUsername.Set(value)
		wizard.smtpUsername.Set(value)
		if strings.ContainsRune(value, '@') {
			server := value[strings.IndexRune(value, '@')+1:]
			wizard.imapServer.Set(server)
			wizard.smtpServer.Set(server)
		}
		wizard.imapUri()
		wizard.smtpUri()
	})
	wizard.imapServer.OnChange(func(_ *ui.TextInput) {
		imapServerURI := wizard.imapServer.String()
		smtpServerURI := imapServerURI
		if strings.HasPrefix(imapServerURI, "imap.") {
			smtpServerURI = strings.Replace(imapServerURI, "imap.", "smtp.", 1)
		}
		wizard.smtpServer.Set(smtpServerURI)
		wizard.imapUri()
		wizard.smtpUri()
	})
	wizard.imapUsername.OnChange(func(_ *ui.TextInput) {
		wizard.smtpUsername.Set(wizard.imapUsername.String())
		wizard.imapUri()
		wizard.smtpUri()
	})
	var once sync.Once
	var lastChange time.Time
	wizard.imapPassword.OnChange(func(_ *ui.TextInput) {
		wizard.smtpPassword.Set(wizard.imapPassword.String())
		wizard.imapUri()
		wizard.smtpUri()
		lastChange = time.Now()
		once.Do(func() {
			// debounce to ensure pasted passwords are pasted completely
			go func() {
				defer log.PanicHandler()
				for !time.Now().After(lastChange.Add(10 * time.Millisecond)) {
					<-time.After(10 * time.Millisecond)
				}
				showPasswordWarning(aerc)
			}()
		})
	})
	wizard.smtpServer.OnChange(func(_ *ui.TextInput) {
		wizard.smtpUri()
	})
	wizard.smtpUsername.OnChange(func(_ *ui.TextInput) {
		wizard.smtpUri()
	})
	wizard.smtpPassword.OnChange(func(_ *ui.TextInput) {
		wizard.smtpUri()
	})

	basics := ui.NewGrid().Rows([]ui.GridSpec{
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(8)}, // Introduction
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // Account name (label)
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // (input)
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // Padding
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // Full name (label)
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // (input)
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // Padding
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // Email address (label)
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // (input)
		{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
	}).Columns([]ui.GridSpec{
		{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
	})
	basics.AddChild(
		ui.NewText("\nWelcome to aerc! Let's configure your account.\n\n"+
			"This wizard supports basic IMAP & SMTP configuration.\n"+
			"For other configurations, use <Ctrl+q> to exit and read the "+
			"aerc-accounts(5) man page.\n"+
			"Press <Tab> and <Shift+Tab> to cycle between each field in this form, "+
			"or <Ctrl+j> and <Ctrl+k>.",
			config.Ui.GetStyle(config.STYLE_DEFAULT)))
	basics.AddChild(
		ui.NewText("Name for this account? (e.g. 'Personal' or 'Work')",
			config.Ui.GetStyle(config.STYLE_HEADER))).
		At(1, 0)
	basics.AddChild(wizard.accountName).
		At(2, 0)
	basics.AddChild(ui.NewFill(' ', tcell.StyleDefault)).
		At(3, 0)
	basics.AddChild(
		ui.NewText("Full name for outgoing emails? (e.g. 'John Doe')",
			config.Ui.GetStyle(config.STYLE_HEADER))).
		At(4, 0)
	basics.AddChild(wizard.fullName).
		At(5, 0)
	basics.AddChild(ui.NewFill(' ', tcell.StyleDefault)).
		At(6, 0)
	basics.AddChild(
		ui.NewText("Your email address? (e.g. 'john@example.org')",
			config.Ui.GetStyle(config.STYLE_HEADER))).
		At(7, 0)
	basics.AddChild(wizard.email).
		At(8, 0)
	selector := NewSelector([]string{"Next"}, 0, config.Ui).
		OnChoose(func(option string) {
			email := wizard.email.String()
			if strings.ContainsRune(email, '@') {
				server := email[strings.IndexRune(email, '@')+1:]
				hostport, srv := getSRV(server, []string{"imaps", "imap"})
				if hostport != "" {
					wizard.imapServer.Set(hostport)
					if srv == "imaps" {
						wizard.imapMode = IMAP_OVER_TLS
					} else {
						wizard.imapMode = IMAP_STARTTLS
					}
					wizard.imapUri()
				}
				hostport, _ = getSRV(server, []string{"submission"})
				if hostport != "" {
					wizard.smtpServer.Set(hostport)
					wizard.smtpMode = SMTP_STARTTLS
					wizard.smtpUri()
				}
			}
			wizard.advance(option)
		})
	basics.AddChild(selector).At(9, 0)
	wizard.basics = []ui.Interactive{
		wizard.accountName, wizard.fullName, wizard.email, selector,
	}

	incoming := ui.NewGrid().Rows([]ui.GridSpec{
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(3)}, // Introduction
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // Username (label)
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // (input)
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // Padding
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // Password (label)
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // (input)
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // Padding
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // Server (label)
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // (input)
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // Padding
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // Connection mode (label)
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(2)}, // (input)
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // Padding
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // Connection string
		{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
	}).Columns([]ui.GridSpec{
		{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
	})
	incoming.AddChild(ui.NewText("\nConfigure incoming mail (IMAP)",
		config.Ui.GetStyle(config.STYLE_DEFAULT)))
	incoming.AddChild(
		ui.NewText("Username",
			config.Ui.GetStyle(config.STYLE_HEADER))).
		At(1, 0)
	incoming.AddChild(wizard.imapUsername).
		At(2, 0)
	incoming.AddChild(ui.NewFill(' ', tcell.StyleDefault)).
		At(3, 0)
	incoming.AddChild(
		ui.NewText("Password",
			config.Ui.GetStyle(config.STYLE_HEADER))).
		At(4, 0)
	incoming.AddChild(wizard.imapPassword).
		At(5, 0)
	incoming.AddChild(ui.NewFill(' ', tcell.StyleDefault)).
		At(6, 0)
	incoming.AddChild(
		ui.NewText("Server address "+
			"(e.g. 'mail.example.org' or 'mail.example.org:1313')",
			config.Ui.GetStyle(config.STYLE_HEADER))).
		At(7, 0)
	incoming.AddChild(wizard.imapServer).
		At(8, 0)
	incoming.AddChild(ui.NewFill(' ', tcell.StyleDefault)).
		At(9, 0)
	incoming.AddChild(
		ui.NewText("Connection mode",
			config.Ui.GetStyle(config.STYLE_HEADER))).
		At(10, 0)
	imapMode := NewSelector([]string{
		"IMAP over SSL/TLS",
		"IMAP with STARTTLS",
		"Insecure IMAP",
	}, 0, config.Ui).Chooser(true).OnSelect(func(option string) {
		switch option {
		case "IMAP over SSL/TLS":
			wizard.imapMode = IMAP_OVER_TLS
		case "IMAP with STARTTLS":
			wizard.imapMode = IMAP_STARTTLS
		case "Insecure IMAP":
			wizard.imapMode = IMAP_INSECURE
		}
		wizard.imapUri()
	})
	incoming.AddChild(imapMode).At(11, 0)
	selector = NewSelector([]string{"Previous", "Next"}, 1, config.Ui).
		OnChoose(wizard.advance)
	incoming.AddChild(ui.NewFill(' ', tcell.StyleDefault)).At(12, 0)
	incoming.AddChild(wizard.imapStr).At(13, 0)
	incoming.AddChild(selector).At(14, 0)
	wizard.incoming = []ui.Interactive{
		wizard.imapUsername, wizard.imapPassword, wizard.imapServer,
		imapMode, selector,
	}

	outgoing := ui.NewGrid().Rows([]ui.GridSpec{
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(3)}, // Introduction
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // Username (label)
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // (input)
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // Padding
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // Password (label)
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // (input)
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // Padding
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // Server (label)
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // (input)
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // Padding
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // Connection mode (label)
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(2)}, // (input)
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // Padding
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // Connection string
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // Padding
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(1)}, // Copy to sent (label)
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(2)}, // (input)
		{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
	}).Columns([]ui.GridSpec{
		{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
	})
	outgoing.AddChild(ui.NewText("\nConfigure outgoing mail (SMTP)",
		config.Ui.GetStyle(config.STYLE_DEFAULT)))
	outgoing.AddChild(
		ui.NewText("Username",
			config.Ui.GetStyle(config.STYLE_HEADER))).
		At(1, 0)
	outgoing.AddChild(wizard.smtpUsername).
		At(2, 0)
	outgoing.AddChild(ui.NewFill(' ', tcell.StyleDefault)).
		At(3, 0)
	outgoing.AddChild(
		ui.NewText("Password",
			config.Ui.GetStyle(config.STYLE_HEADER))).
		At(4, 0)
	outgoing.AddChild(wizard.smtpPassword).
		At(5, 0)
	outgoing.AddChild(ui.NewFill(' ', tcell.StyleDefault)).
		At(6, 0)
	outgoing.AddChild(
		ui.NewText("Server address "+
			"(e.g. 'mail.example.org' or 'mail.example.org:1313')",
			config.Ui.GetStyle(config.STYLE_HEADER))).
		At(7, 0)
	outgoing.AddChild(wizard.smtpServer).
		At(8, 0)
	outgoing.AddChild(ui.NewFill(' ', tcell.StyleDefault)).
		At(9, 0)
	outgoing.AddChild(
		ui.NewText("Connection mode",
			config.Ui.GetStyle(config.STYLE_HEADER))).
		At(10, 0)
	smtpMode := NewSelector([]string{
		"SMTP over SSL/TLS",
		"SMTP with STARTTLS",
		"Insecure SMTP",
	}, 0, config.Ui).Chooser(true).OnSelect(func(option string) {
		switch option {
		case "SMTP over SSL/TLS":
			wizard.smtpMode = SMTP_OVER_TLS
		case "SMTP with STARTTLS":
			wizard.smtpMode = SMTP_STARTTLS
		case "Insecure SMTP":
			wizard.smtpMode = SMTP_INSECURE
		}
		wizard.smtpUri()
	})
	outgoing.AddChild(smtpMode).At(11, 0)
	selector = NewSelector([]string{"Previous", "Next"}, 1, config.Ui).
		OnChoose(wizard.advance)
	outgoing.AddChild(ui.NewFill(' ', tcell.StyleDefault)).At(12, 0)
	outgoing.AddChild(wizard.smtpStr).At(13, 0)
	outgoing.AddChild(ui.NewFill(' ', tcell.StyleDefault)).At(14, 0)
	outgoing.AddChild(
		ui.NewText("Copy sent messages to 'Sent' folder?",
			config.Ui.GetStyle(config.STYLE_HEADER))).At(15, 0)
	copySent := NewSelector([]string{"Yes", "No"}, 0, config.Ui).
		Chooser(true).OnChoose(func(option string) {
		switch option {
		case "Yes":
			wizard.copySent = true
		case "No":
			wizard.copySent = false
		}
	})
	outgoing.AddChild(copySent).At(16, 0)
	outgoing.AddChild(selector).At(17, 0)
	wizard.outgoing = []ui.Interactive{
		wizard.smtpUsername, wizard.smtpPassword, wizard.smtpServer,
		smtpMode, copySent, selector,
	}

	complete := ui.NewGrid().Rows([]ui.GridSpec{
		{Strategy: ui.SIZE_EXACT, Size: ui.Const(7)},  // Introduction
		{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)}, // Previous / Finish / Finish & open tutorial
	}).Columns([]ui.GridSpec{
		{Strategy: ui.SIZE_WEIGHT, Size: ui.Const(1)},
	})
	complete.AddChild(ui.NewText(
		"\nConfiguration complete!\n\n"+
			"You can go back and double check your settings, or choose 'Finish' to\n"+
			"save your settings to accounts.conf.\n\n"+
			"To add another account in the future, run ':new-account'.",
		config.Ui.GetStyle(config.STYLE_DEFAULT)))
	selector = NewSelector([]string{
		"Previous",
		"Finish & open tutorial",
		"Finish",
	}, 1, config.Ui).OnChoose(func(option string) {
		switch option {
		case "Previous":
			wizard.advance("Previous")
		case "Finish & open tutorial":
			wizard.finish(true)
		case "Finish":
			wizard.finish(false)
		}
	})
	complete.AddChild(selector).At(1, 0)
	wizard.complete = []ui.Interactive{selector}

	wizard.steps = []*ui.Grid{basics, incoming, outgoing, complete}
	return wizard
}

func (wizard *AccountWizard) ConfigureTemporaryAccount(temporary bool) {
	wizard.temporary = temporary
}

func (wizard *AccountWizard) errorFor(d ui.Interactive, err error) {
	if d == nil {
		wizard.aerc.PushError(err.Error())
		wizard.Invalidate()
		return
	}
	for step, interactives := range [][]ui.Interactive{
		wizard.basics,
		wizard.incoming,
		wizard.outgoing,
	} {
		for focus, item := range interactives {
			if item == d {
				wizard.Focus(false)
				wizard.step = step
				wizard.focus = focus
				wizard.Focus(true)
				wizard.aerc.PushError(err.Error())
				wizard.Invalidate()
				return
			}
		}
	}
}

func (wizard *AccountWizard) finish(tutorial bool) {
	accountsConf := path.Join(xdg.ConfigHome(), "aerc", "accounts.conf")

	// Validation
	if wizard.accountName.String() == "" {
		wizard.errorFor(wizard.accountName,
			errors.New("Account name is required"))
		return
	}
	if wizard.email.String() == "" {
		wizard.errorFor(wizard.email,
			errors.New("Email address is required"))
		return
	}
	if wizard.fullName.String() == "" {
		wizard.errorFor(wizard.fullName,
			errors.New("Full name is required"))
		return
	}
	if wizard.imapServer.String() == "" {
		wizard.errorFor(wizard.imapServer,
			errors.New("IMAP server is required"))
		return
	}
	if wizard.imapServer.String() == "" {
		wizard.errorFor(wizard.smtpServer,
			errors.New("SMTP server is required"))
		return
	}

	file, err := ini.Load(accountsConf)
	if err != nil {
		file = ini.Empty()
	}

	var sec *ini.Section
	if sec, _ = file.GetSection(wizard.accountName.String()); sec != nil {
		wizard.errorFor(wizard.accountName,
			errors.New("An account by this name already exists"))
		return
	}
	sec, _ = file.NewSection(wizard.accountName.String())
	// these can't really fail
	sec.NewKey("source", wizard.imapUrl.String())   //nolint:errcheck // can't fail. option shadowing is not enabled and the key is not empty
	sec.NewKey("outgoing", wizard.smtpUrl.String()) //nolint:errcheck // can't fail. option shadowing is not enabled and the key is not empty
	sec.NewKey("default", "INBOX")                  //nolint:errcheck // can't fail. option shadowing is not enabled and the key is not empty
	sec.NewKey("from", fmt.Sprintf("%s <%s>",       //nolint:errcheck // can't fail. option shadowing is not enabled and the key is not empty
		wizard.fullName.String(), wizard.email.String()))
	if wizard.copySent {
		sec.NewKey("copy-to", "Sent") //nolint:errcheck // can't fail. option shadowing is not enabled and the key is not empty
	}

	if !wizard.temporary {
		f, err := os.OpenFile(accountsConf, os.O_WRONLY|os.O_CREATE, 0o600)
		if err != nil {
			wizard.errorFor(nil, err)
			return
		}
		if _, err = file.WriteTo(f); err != nil {
			wizard.errorFor(nil, err)
			return
		}
	}

	from, err := mail.ParseAddress(sec.Key("from").String())
	if err != nil {
		wizard.errorFor(nil, err)
		return
	}

	account := config.AccountConfig{
		Name:     sec.Name(),
		Default:  "INBOX",
		From:     from,
		Source:   sec.Key("source").String(),
		Outgoing: config.RemoteConfig{Value: sec.Key("outgoing").String()},
	}
	if wizard.copySent {
		account.CopyTo = "Sent"
	}
	config.Accounts = append(config.Accounts, &account)

	view, err := NewAccountView(wizard.aerc, &account, wizard.aerc, nil)
	if err != nil {
		wizard.aerc.NewTab(errorScreen(err.Error()), account.Name)
		return
	}
	wizard.aerc.accounts[account.Name] = view
	wizard.aerc.NewTab(view, account.Name)

	if tutorial {
		name := "aerc-tutorial"
		if _, err := os.Stat("./aerc-tutorial.7"); !os.IsNotExist(err) {
			// For development
			name = "./aerc-tutorial.7"
		}
		term, err := NewTerminal(exec.Command("man", name))
		if err != nil {
			wizard.errorFor(nil, err)
			return
		}
		wizard.aerc.NewTab(term, "Tutorial")
		term.OnClose = func(err error) {
			wizard.aerc.RemoveTab(term, false)
			if err != nil {
				wizard.aerc.PushError(err.Error())
			}
		}
	}

	wizard.aerc.RemoveTab(wizard, false)
}

func (wizard *AccountWizard) imapUri() url.URL {
	host := wizard.imapServer.String()
	user := wizard.imapUsername.String()
	pass := wizard.imapPassword.String()
	var scheme string
	switch wizard.imapMode {
	case IMAP_OVER_TLS:
		scheme = "imaps"
	case IMAP_STARTTLS:
		scheme = "imap"
	case IMAP_INSECURE:
		scheme = "imap+insecure"
	}
	var (
		userpass   *url.Userinfo
		userwopass *url.Userinfo
	)
	if pass == "" {
		userpass = url.User(user)
		userwopass = userpass
	} else {
		userpass = url.UserPassword(user, pass)
		userwopass = url.UserPassword(user, strings.Repeat("*", len(pass)))
	}
	uri := url.URL{
		Scheme: scheme,
		Host:   host,
		User:   userpass,
	}
	clean := url.URL{
		Scheme: scheme,
		Host:   host,
		User:   userwopass,
	}
	wizard.imapStr.Text("Connection URL: " +
		strings.ReplaceAll(clean.String(), "%2A", "*"))
	wizard.imapUrl = uri
	return uri
}

func (wizard *AccountWizard) smtpUri() url.URL {
	host := wizard.smtpServer.String()
	user := wizard.smtpUsername.String()
	pass := wizard.smtpPassword.String()
	var scheme string
	switch wizard.smtpMode {
	case SMTP_OVER_TLS:
		scheme = "smtps"
	case SMTP_STARTTLS:
		scheme = "smtp"
	case SMTP_INSECURE:
		scheme = "smtp+insecure"
	}
	var (
		userpass   *url.Userinfo
		userwopass *url.Userinfo
	)
	if pass == "" {
		userpass = url.User(user)
		userwopass = userpass
	} else {
		userpass = url.UserPassword(user, pass)
		userwopass = url.UserPassword(user, strings.Repeat("*", len(pass)))
	}
	uri := url.URL{
		Scheme: scheme,
		Host:   host,
		User:   userpass,
	}
	clean := url.URL{
		Scheme: scheme,
		Host:   host,
		User:   userwopass,
	}
	wizard.smtpStr.Text("Connection URL: " +
		strings.ReplaceAll(clean.String(), "%2A", "*"))
	wizard.smtpUrl = uri
	return uri
}

func (wizard *AccountWizard) Invalidate() {
	ui.Invalidate()
}

func (wizard *AccountWizard) Draw(ctx *ui.Context) {
	wizard.steps[wizard.step].Draw(ctx)
}

func (wizard *AccountWizard) getInteractive() []ui.Interactive {
	switch wizard.step {
	case CONFIGURE_BASICS:
		return wizard.basics
	case CONFIGURE_INCOMING:
		return wizard.incoming
	case CONFIGURE_OUTGOING:
		return wizard.outgoing
	case CONFIGURE_COMPLETE:
		return wizard.complete
	}
	return nil
}

func (wizard *AccountWizard) advance(direction string) {
	wizard.Focus(false)
	if direction == "Next" && wizard.step < len(wizard.steps)-1 {
		wizard.step++
	}
	if direction == "Previous" && wizard.step > 0 {
		wizard.step--
	}
	wizard.focus = 0
	wizard.Focus(true)
	wizard.Invalidate()
}

func (wizard *AccountWizard) Focus(focus bool) {
	if interactive := wizard.getInteractive(); interactive != nil {
		interactive[wizard.focus].Focus(focus)
	}
}

func (wizard *AccountWizard) Event(event tcell.Event) bool {
	interactive := wizard.getInteractive()
	if event, ok := event.(*tcell.EventKey); ok {
		switch event.Key() {
		case tcell.KeyUp:
			fallthrough
		case tcell.KeyBacktab:
			fallthrough
		case tcell.KeyCtrlK:
			if interactive != nil {
				interactive[wizard.focus].Focus(false)
				wizard.focus--
				if wizard.focus < 0 {
					wizard.focus = len(interactive) - 1
				}
				interactive[wizard.focus].Focus(true)
			}
			wizard.Invalidate()
			return true
		case tcell.KeyDown:
			fallthrough
		case tcell.KeyTab:
			fallthrough
		case tcell.KeyCtrlJ:
			if interactive != nil {
				interactive[wizard.focus].Focus(false)
				wizard.focus++
				if wizard.focus >= len(interactive) {
					wizard.focus = 0
				}
				interactive[wizard.focus].Focus(true)
			}
			wizard.Invalidate()
			return true
		}
	}
	if interactive != nil {
		return interactive[wizard.focus].Event(event)
	}
	return false
}

func getSRV(host string, services []string) (string, string) {
	var hostport, srv string
	for _, srv = range services {
		_, addrs, err := net.LookupSRV(srv, "tcp", host)
		if err != nil {
			continue
		}
		if addrs[0].Target != "" && addrs[0].Port > 0 {
			hostport = net.JoinHostPort(
				strings.TrimSuffix(addrs[0].Target, "."),
				strconv.Itoa(int(addrs[0].Port)))
			break
		}
	}
	return hostport, srv
}