1
0
mirror of https://github.com/jhillyerd/inbucket.git synced 2025-12-17 17:47:03 +00:00

Merge branch 'feature/cmdline' into develop

This commit is contained in:
James Hillyerd
2017-02-05 15:47:40 -08:00
6 changed files with 384 additions and 1 deletions

4
.gitignore vendored
View File

@@ -25,10 +25,12 @@ _testmain.go
*.swp *.swp
*.swo *.swo
# our binary # our binaries
/inbucket /inbucket
/inbucket.exe /inbucket.exe
/target/** /target/**
/cmd/client/client
/cmd/client/client.exe
# local goxc config # local goxc config
.goxc.local.json .goxc.local.json

View File

@@ -10,6 +10,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Added ### Added
- `rest/client` types `MessageHeader` and `Message` with convenience methods; - `rest/client` types `MessageHeader` and `Message` with convenience methods;
provides a more natural API provides a more natural API
- Powerful command line REST
[client](https://github.com/jhillyerd/inbucket/wiki/cmd-client)
### Changed ### Changed
- `rest/client.NewV1` renamed to `New` - `rest/client.NewV1` renamed to `New`

54
cmd/client/list.go Normal file
View File

@@ -0,0 +1,54 @@
package main
import (
"context"
"flag"
"fmt"
"github.com/google/subcommands"
"github.com/jhillyerd/inbucket/rest/client"
)
type listCmd struct {
mailbox string
}
func (*listCmd) Name() string {
return "list"
}
func (*listCmd) Synopsis() string {
return "list contents of mailbox"
}
func (*listCmd) Usage() string {
return `list <mailbox>:
list message IDs in mailbox
`
}
func (l *listCmd) SetFlags(f *flag.FlagSet) {
}
func (l *listCmd) Execute(
_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
mailbox := f.Arg(0)
if mailbox == "" {
return usage("mailbox required")
}
// 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("REST call failed", err)
}
for _, h := range headers {
fmt.Println(h.ID)
}
return subcommands.ExitSuccess
}

79
cmd/client/main.go Normal file
View File

@@ -0,0 +1,79 @@
// Package main implements a command line client for the Inbucket REST API
package main
import (
"context"
"flag"
"fmt"
"os"
"regexp"
"github.com/google/subcommands"
)
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 = &regexFlag{}
func main() {
// Important top-level flags
subcommands.ImportantFlag("host")
subcommands.ImportantFlag("port")
// Setup standard helpers
subcommands.Register(subcommands.HelpCommand(), "")
subcommands.Register(subcommands.FlagsCommand(), "")
subcommands.Register(subcommands.CommandsCommand(), "")
// Setup my commands
subcommands.Register(&listCmd{}, "")
subcommands.Register(&matchCmd{}, "")
subcommands.Register(&mboxCmd{}, "")
// Parse and execute
flag.Parse()
ctx := context.Background()
os.Exit(int(subcommands.Execute(ctx)))
}
func baseURL() string {
return fmt.Sprintf("http://%s:%v", *host, *port)
}
func fatal(msg string, err error) subcommands.ExitStatus {
fmt.Fprintf(os.Stderr, "%s: %v\n", msg, err)
return subcommands.ExitFailure
}
func usage(msg string) subcommands.ExitStatus {
fmt.Fprintln(os.Stderr, msg)
return subcommands.ExitUsageError
}

164
cmd/client/match.go Normal file
View File

@@ -0,0 +1,164 @@
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"net/mail"
"os"
"time"
"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
// match criteria
from regexFlag
subject regexFlag
to regexFlag
maxAge time.Duration
}
func (*matchCmd) Name() string {
return "match"
}
func (*matchCmd) Synopsis() string {
return "output messages matching criteria"
}
func (*matchCmd) Usage() string {
return `match [flags] <mailbox>:
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 (address, not name)")
f.Var(&m.subject, "subject", "Subject header matching regexp")
f.Var(&m.to, "to", "To header matching regexp (must match 1+ to address)")
f.DurationVar(
&m.maxAge, "maxage", 0,
"Matches must have been received in this time frame (ex: \"10s\", \"5m\")")
}
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.maxAge > 0 {
if time.Since(header.Date) > m.maxAge {
return false
}
}
if m.subject.Defined() {
if !m.subject.MatchString(header.Subject) {
return false
}
}
if m.from.Defined() {
from := header.From
addr, err := mail.ParseAddress(from)
if err == nil {
// Parsed successfully
from = addr.Address
}
if !m.from.MatchString(from) {
return false
}
}
if m.to.Defined() {
match := false
for _, to := range header.To {
addr, err := mail.ParseAddress(to)
if err == nil {
// Parsed successfully
to = addr.Address
}
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)
}

82
cmd/client/mbox.go Normal file
View File

@@ -0,0 +1,82 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"github.com/google/subcommands"
"github.com/jhillyerd/inbucket/rest/client"
)
type mboxCmd struct {
mailbox string
delete bool
}
func (*mboxCmd) Name() string {
return "mbox"
}
func (*mboxCmd) Synopsis() string {
return "output mailbox in mbox format"
}
func (*mboxCmd) Usage() string {
return `mbox [flags] <mailbox>:
output mailbox in mbox format
`
}
func (m *mboxCmd) SetFlags(f *flag.FlagSet) {
f.BoolVar(&m.delete, "delete", false, "delete messages after output")
}
func (m *mboxCmd) Execute(
_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
mailbox := f.Arg(0)
if mailbox == "" {
return usage("mailbox required")
}
// 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)
}
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
}