changeset 8:8caacf702c0d

Add edit subcommand for interactive editing of a script
author Guido Berhoerster <guido+sievemgr@berhoerster.name>
date Fri, 13 Nov 2020 10:49:08 +0100
parents 3abc8be485c0
children 7ce77ceeaccc
files cmd/sievemgr/edit.go cmd/sievemgr/main.go
diffstat 2 files changed, 228 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /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 <guido+sievemgr@berhoerster.name>
+//
+// 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
+}
--- 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() {