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:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
@@ -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
54
cmd/client/list.go
Normal 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
79
cmd/client/main.go
Normal 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 = ®exFlag{}
|
||||||
|
|
||||||
|
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
164
cmd/client/match.go
Normal 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
82
cmd/client/mbox.go
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user