email-mcp/internal/cli/setup.go
2026-04-10 10:17:38 +02:00

183 lines
4.3 KiB
Go

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
}