# HG changeset patch # User Guido Berhoerster # Date 1605260948 -3600 # Node ID 8caacf702c0dc7d6c92888cea24728d4a92e2e66 # Parent 3abc8be485c06ebee7ba9e885adda2c3f185da43 Add edit subcommand for interactive editing of a script diff -r 3abc8be485c0 -r 8caacf702c0d cmd/sievemgr/edit.go --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cmd/sievemgr/edit.go Fri Nov 13 10:49:08 2020 +0100 @@ -0,0 +1,227 @@ +// Copyright (C) 2020 Guido Berhoerster +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package main + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + + "go.guido-berhoerster.org/managesieve" + "go.guido-berhoerster.org/sievemgr/cmd/sievemgr/internal/config" + "golang.org/x/crypto/ssh/terminal" +) + +func init() { + cmdEdit.Flag.StringVar(&acctName, "a", "", "Select the account") +} + +var cmdEdit = &command{ + UsageLine: "edit [options] name", + Run: runEdit, +} + +func promptYesNo(prompt string) (yesNo bool, err error) { + var tty *os.File + var r io.Reader + var w io.Writer + if runtime.GOOS == "windows" { + r = os.Stdin + w = os.Stdout + } else { + tty, err = os.OpenFile("/dev/tty", os.O_RDWR, 0666) + if err != nil { + return + } + r = tty + w = tty + defer tty.Close() + } + + for { + io.WriteString(w, prompt) + + var response string + _, err = fmt.Fscanln(r, &response) + if err != nil { + err = fmt.Errorf("failed to read response: %s", err) + return + } + + switch strings.ToLower(response) { + case "y", "yes": + yesNo = true + return + case "n", "no": + return + } + } +} + +func getScript(acct *config.Account, scriptName string) (string, error) { + c, err := dialPlainAuth(acct) + if err != nil { + return "", err + } + defer c.Logout() + + return c.GetScript(scriptName) +} + +func readScript(filename string) (string, error) { + f, err := os.Open(filename) + if err != nil { + return "", err + } + defer f.Close() + + return readLimitedString(f, managesieve.ReadLimit) +} + +func putScript(acct *config.Account, scriptName, script string) (string, error) { + c, err := dialPlainAuth(acct) + if err != nil { + return "", err + } + defer c.Logout() + + return c.PutScript(scriptName, script) +} + +func runEdit(cmd *command, args []string) error { + if len(args) != 1 { + return usageError("invalid number of arguments") + } + + scriptName := args[0] + + if !terminal.IsTerminal(int(os.Stdin.Fd())) || + !terminal.IsTerminal(int(os.Stdout.Fd())) { + return fmt.Errorf("the edit subcommand can only be used interactively\n") + } + + editor := os.Getenv("EDITOR") + if editor == "" { + return fmt.Errorf("EDITOR environment variable not set") + } + + acct, err := getAccount(&conf, acctName) + if err != nil { + return err + } + + if err := lookupHostPort(acct); err != nil { + return err + } + + if err := readPassword(acct); err != nil { + return err + } + + c, err := dialPlainAuth(acct) + if err != nil { + return err + } + defer c.Logout() + + script, err := getScript(acct, scriptName) + if err != nil { + return err + } + + tmpDir, err := ioutil.TempDir(os.TempDir(), "sievemgr*") + if err != nil { + return fmt.Errorf("failed to create temporary directory: %s", err) + } + + tmpFile := filepath.Join(tmpDir, scriptName) + if err = ioutil.WriteFile(tmpFile, []byte(script), 0640); err != nil { + return fmt.Errorf("failed to create script file: %s", err) + } + defer func() { + // show filename if an error has occured and the file is + // preserved + if tmpFile != "" { + fmt.Fprintf(os.Stderr, + "the script has been preserved as %s\n", + tmpFile) + } + }() + + // modification time is used to detect changes + info, err := os.Stat(tmpFile) + if err != nil { + return fmt.Errorf("failed to stat file: %s", err) + } + origModTime := info.ModTime() + + for { + cmd := exec.Command(editor, tmpFile) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to run editor: %s", err) + } + + // quit if the script has not been changed + if info, err := os.Stat(tmpFile); err != nil { + return fmt.Errorf("failed to stat file: %s", err) + } else if info.ModTime() == origModTime { + fmt.Fprintln(os.Stderr, "aborting, script was not modified") + break + } + + script, err = readScript(tmpFile) + if err != nil { + return fmt.Errorf("failed to read script: %s", err) + } + + warnings, err := putScript(acct, scriptName, script) + if err != nil { + // show error and try again if the script was rejected + // by the server + fmt.Fprintln(os.Stderr, err) + yesNo, err := promptYesNo("edit again [y/n]? ") + if err != nil { + return err + } else if !yesNo { + return fmt.Errorf("script not saved") + } + } else if warnings != "" { + fmt.Fprintln(os.Stderr, warnings) + break + } else { + break + } + } + + os.RemoveAll(tmpDir) + tmpFile = "" + + return nil +} diff -r 3abc8be485c0 -r 8caacf702c0d cmd/sievemgr/main.go --- a/cmd/sievemgr/main.go Sat Nov 07 16:48:55 2020 +0100 +++ b/cmd/sievemgr/main.go Fri Nov 13 10:49:08 2020 +0100 @@ -70,6 +70,7 @@ cmdCheck, cmdCheckSpace, cmdRename, + cmdEdit, } func usage() {