From be4675b3745e0fa9620d1f4831f081415e15b763 Mon Sep 17 00:00:00 2001 From: James Hillyerd Date: Sun, 5 Feb 2017 14:04:34 -0800 Subject: [PATCH] Add powerful `match` subcommand to cmdline client - Multiple output formats - Signals matches via exit status for shell scripts - Match against To, From, Subject via regular expressions - Can optionally delete matched messages --- CHANGELOG.md | 2 + cmd/client/main.go | 34 +++++++++++ cmd/client/match.go | 141 ++++++++++++++++++++++++++++++++++++++++++++ cmd/client/mbox.go | 36 +++++++---- 4 files changed, 201 insertions(+), 12 deletions(-) create mode 100644 cmd/client/match.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a7a9ab..5817b71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added - `rest/client` types `MessageHeader` and `Message` with convenience methods; provides a more natural API +- Powerful command line REST + [client](https://github.com/jhillyerd/inbucket/wiki/cmd-client) ### Changed - `rest/client.NewV1` renamed to `New` diff --git a/cmd/client/main.go b/cmd/client/main.go index b56b63b..48828fe 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -6,6 +6,7 @@ import ( "flag" "fmt" "os" + "regexp" "github.com/google/subcommands" ) @@ -13,6 +14,38 @@ import ( var host = flag.String("host", "localhost", "host/IP of Inbucket server") var port = flag.Uint("port", 9000, "HTTP port of Inbucket server") +// Allow subcommands to accept regular expressions as flags +type regexFlag struct { + *regexp.Regexp +} + +func (r *regexFlag) Defined() bool { + return r.Regexp != nil +} + +func (r *regexFlag) Set(pattern string) error { + if pattern == "" { + r.Regexp = nil + return nil + } + re, err := regexp.Compile(pattern) + if err != nil { + return err + } + r.Regexp = re + return nil +} + +func (r *regexFlag) String() string { + if r.Regexp == nil { + return "" + } + return r.Regexp.String() +} + +// regexFlag must implement flag.Value +var _ flag.Value = ®exFlag{} + func main() { // Important top-level flags subcommands.ImportantFlag("host") @@ -23,6 +56,7 @@ func main() { subcommands.Register(subcommands.CommandsCommand(), "") // Setup my commands subcommands.Register(&listCmd{}, "") + subcommands.Register(&matchCmd{}, "") subcommands.Register(&mboxCmd{}, "") // Parse and execute flag.Parse() diff --git a/cmd/client/match.go b/cmd/client/match.go new file mode 100644 index 0000000..f6ab45d --- /dev/null +++ b/cmd/client/match.go @@ -0,0 +1,141 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "os" + + "github.com/google/subcommands" + "github.com/jhillyerd/inbucket/rest/client" +) + +type matchCmd struct { + mailbox string + output string + outFunc func(headers []*client.MessageHeader) error + delete bool + from regexFlag + subject regexFlag + to regexFlag +} + +func (*matchCmd) Name() string { + return "match" +} + +func (*matchCmd) Synopsis() string { + return "output messages matching criteria" +} + +func (*matchCmd) Usage() string { + return `match [options] : + output messages matching all specified criteria + exit status will be 1 if no matches were found, otherwise 0 +` +} + +func (m *matchCmd) SetFlags(f *flag.FlagSet) { + f.StringVar(&m.output, "output", "id", "output format: id, json, or mbox") + f.BoolVar(&m.delete, "delete", false, "delete matched messages after output") + f.Var(&m.from, "from", "From header matching regexp") + f.Var(&m.subject, "subject", "Subject header matching regexp") + f.Var(&m.to, "to", "To header matching regexp (must match one)") +} + +func (m *matchCmd) Execute( + _ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { + mailbox := f.Arg(0) + if mailbox == "" { + return usage("mailbox required") + } + // Select output function + switch m.output { + case "id": + m.outFunc = outputID + case "json": + m.outFunc = outputJSON + case "mbox": + m.outFunc = outputMbox + default: + return usage("unknown output type: " + m.output) + } + // Setup REST client + c, err := client.New(baseURL()) + if err != nil { + return fatal("Couldn't build client", err) + } + // Get list + headers, err := c.ListMailbox(mailbox) + if err != nil { + return fatal("List REST call failed", err) + } + // Find matches + matches := make([]*client.MessageHeader, 0, len(headers)) + for _, h := range headers { + if m.match(h) { + matches = append(matches, h) + } + } + // Return error status if no matches + if len(matches) == 0 { + return subcommands.ExitFailure + } + // Output matches + err = m.outFunc(matches) + if err != nil { + return fatal("Error", err) + } + if m.delete { + // Delete matches + for _, h := range matches { + err = h.Delete() + if err != nil { + return fatal("Delete REST call failed", err) + } + } + } + return subcommands.ExitSuccess +} + +// match returns true if header matches all defined criteria +func (m *matchCmd) match(header *client.MessageHeader) bool { + if m.subject.Defined() { + if !m.subject.MatchString(header.Subject) { + return false + } + } + if m.from.Defined() { + if !m.from.MatchString(header.From) { + return false + } + } + if m.to.Defined() { + match := false + for _, to := range header.To { + if m.to.MatchString(to) { + match = true + break + } + } + if !match { + return false + } + } + return true +} + +func outputID(headers []*client.MessageHeader) error { + for _, h := range headers { + fmt.Println(h.ID) + } + return nil +} + +func outputJSON(headers []*client.MessageHeader) error { + jsonEncoder := json.NewEncoder(os.Stdout) + jsonEncoder.SetEscapeHTML(false) + jsonEncoder.SetIndent("", " ") + return jsonEncoder.Encode(headers) +} diff --git a/cmd/client/mbox.go b/cmd/client/mbox.go index 6943796..71e7aaf 100644 --- a/cmd/client/mbox.go +++ b/cmd/client/mbox.go @@ -39,7 +39,7 @@ func (m *mboxCmd) Execute( if mailbox == "" { return usage("mailbox required") } - // Setup rest client + // Setup REST client c, err := client.New(baseURL()) if err != nil { return fatal("Couldn't build client", err) @@ -49,22 +49,34 @@ func (m *mboxCmd) Execute( if err != nil { return fatal("List REST call failed", err) } - for _, h := range headers { - source, err := h.GetSource() - if err != nil { - return fatal("Source REST call failed", err) - } - fmt.Printf("From %s\n", h.From) - // TODO Escape "From " in message bodies with > - source.WriteTo(os.Stdout) - fmt.Println() - if m.delete { + err = outputMbox(headers) + if err != nil { + return fatal("Error", err) + } + if m.delete { + // Delete matches + for _, h := range headers { err = h.Delete() if err != nil { return fatal("Delete REST call failed", err) } } } - return subcommands.ExitSuccess } + +// outputMbox renders messages in mbox format +// also used by match subcommand +func outputMbox(headers []*client.MessageHeader) error { + for _, h := range headers { + source, err := h.GetSource() + if err != nil { + return fmt.Errorf("Get source REST failed: %v", err) + } + fmt.Printf("From %s\n", h.From) + // TODO Escape "From " in message bodies with > + source.WriteTo(os.Stdout) + fmt.Println() + } + return nil +}