package cli import ( "bufio" "context" "fmt" "io" "os" "strings" "syscall" "unsafe" "email-mcp/internal/secretstore" ) type SetupPrompter interface { PromptSetup(ctx context.Context) (secretstore.Credential, error) } type PasswordReader interface { ReadPassword(prompt string) (string, error) } type InteractiveSetupPrompter struct { input *bufio.Reader rawInput io.Reader output io.Writer passwordReader PasswordReader } type terminalPasswordReader struct { input io.Reader output io.Writer isTerminal func(io.Reader) bool readHiddenPassword func(*os.File) ([]byte, error) readFallbackPassword func(string) (string, error) } func NewInteractiveSetupPrompter(input io.Reader, output io.Writer) *InteractiveSetupPrompter { return NewInteractiveSetupPrompterWithPasswordReader(input, output, nil) } func NewInteractiveSetupPrompterWithPasswordReader(input io.Reader, output io.Writer, passwordReader PasswordReader) *InteractiveSetupPrompter { if input == nil { input = strings.NewReader("") } if output == nil { output = io.Discard } prompter := &InteractiveSetupPrompter{ input: bufio.NewReader(input), rawInput: input, output: output, } if passwordReader == nil { passwordReader = newTerminalPasswordReader(input, output, prompter.prompt) } prompter.passwordReader = passwordReader return prompter } func (p *InteractiveSetupPrompter) PromptSetup(context.Context) (secretstore.Credential, error) { host, err := p.prompt("IMAP host: ") if err != nil { return secretstore.Credential{}, err } username, err := p.prompt("Username: ") if err != nil { return secretstore.Credential{}, err } password, err := p.passwordReader.ReadPassword("Password: ") if err != nil { return secretstore.Credential{}, err } cred := secretstore.Credential{ Host: host, Username: username, Password: password, } if err := cred.Validate(); err != nil { return secretstore.Credential{}, err } return cred, nil } func (p *InteractiveSetupPrompter) prompt(label string) (string, error) { if _, err := fmt.Fprint(p.output, label); err != nil { return "", err } value, err := p.input.ReadString('\n') if err != nil && err != io.EOF { return "", err } return strings.TrimSpace(value), nil } func newTerminalPasswordReader(input io.Reader, output io.Writer, fallback func(string) (string, error)) PasswordReader { return terminalPasswordReader{ input: input, output: output, isTerminal: isTerminalReader, readHiddenPassword: readHiddenPassword, readFallbackPassword: fallback, } } func (r terminalPasswordReader) ReadPassword(prompt string) (string, error) { if r.isTerminal != nil && r.isTerminal(r.input) { file, ok := r.input.(*os.File) if ok { if _, err := fmt.Fprint(r.output, prompt); err != nil { return "", err } password, err := r.readHiddenPassword(file) if _, printErr := fmt.Fprintln(r.output); err == nil && printErr != nil { return "", printErr } if err != nil { return "", err } return strings.TrimSpace(string(password)), nil } } return r.readFallbackPassword(prompt) } func isTerminalReader(reader io.Reader) bool { file, ok := reader.(*os.File) if !ok { return false } return isTerminalFile(file) } func isTerminalFile(file *os.File) bool { _, err := getTermios(file.Fd()) return err == nil } func readHiddenPassword(file *os.File) ([]byte, error) { state, err := getTermios(file.Fd()) if err != nil { return nil, err } next := *state next.Lflag &^= syscall.ECHO if err := setTermios(file.Fd(), &next); err != nil { return nil, err } defer func() { _ = setTermios(file.Fd(), state) }() reader := bufio.NewReader(file) value, err := reader.ReadBytes('\n') if err != nil && err != io.EOF { return nil, err } return []byte(strings.TrimRight(string(value), "\r\n")), nil } func getTermios(fd uintptr) (*syscall.Termios, error) { state := &syscall.Termios{} _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(syscall.TCGETS), uintptr(unsafe.Pointer(state))) if errno != 0 { return nil, errno } return state, nil } func setTermios(fd uintptr, state *syscall.Termios) error { _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(syscall.TCSETS), uintptr(unsafe.Pointer(state))) if errno != 0 { return errno } return nil }