view cmd/sievemgr/edit.go @ 22:fc5e6970a0d5 default tip

Add support for specifying an authorization identity on the command line
author Guido Berhoerster <guido+sievemgr@berhoerster.name>
date Wed, 17 Feb 2021 07:50:55 +0100
parents 29769b9e2f09
children
line wrap: on
line source

// 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/term"
)

func init() {
	cmdEdit.Flag.StringVar(&acctName, "a", "", "Select the account")
	cmdEdit.Flag.StringVar(&authzID, "A", "", "Specify the authorization identity")
}

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 !term.IsTerminal(int(os.Stdin.Fd())) ||
		!term.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
}