🤬
  • ■ ■ ■ ■ ■ ■
    apiv1_client.go
     1 +// Package client provides a basic REST client for crash.fyi
     2 +package cspam
     3 + 
     4 +import (
     5 + "bytes"
     6 + "fmt"
     7 + "net/http"
     8 + "net/url"
     9 + "time"
     10 + 
     11 + "crash.software/crash.fyi/cspam.go/rest/model"
     12 +)
     13 + 
     14 +// Client accesses crash.fyi REST API v1
     15 +type Client struct {
     16 + restClient
     17 +}
     18 + 
     19 +// New creates a new v1 REST API client given the base URL of the server, ex:
     20 +// "https://crash.fyi"
     21 +func New(baseURL string) (*Client, error) {
     22 + parsedURL, err := url.Parse(baseURL)
     23 + if err != nil {
     24 + return nil, err
     25 + }
     26 + c := &Client{
     27 + restClient{
     28 + client: &http.Client{
     29 + Timeout: 30 * time.Second,
     30 + },
     31 + baseURL: parsedURL,
     32 + },
     33 + }
     34 + return c, nil
     35 +}
     36 + 
     37 +// ListMailbox returns a list of messages for the requested mailbox
     38 +func (c *Client) ListMailbox(name string) (headers []*MessageHeader, err error) {
     39 + uri := "/api/v1/mailbox/" + url.QueryEscape(name)
     40 + err = c.doJSON("GET", uri, &headers)
     41 + if err != nil {
     42 + return nil, err
     43 + }
     44 + for _, h := range headers {
     45 + h.client = c
     46 + }
     47 + return
     48 +}
     49 + 
     50 +// GetMessage returns the message details given a mailbox name and message ID.
     51 +func (c *Client) GetMessage(name, id string) (message *Message, err error) {
     52 + uri := "/api/v1/mailbox/" + url.QueryEscape(name) + "/" + id
     53 + err = c.doJSON("GET", uri, &message)
     54 + if err != nil {
     55 + return nil, err
     56 + }
     57 + message.client = c
     58 + return
     59 +}
     60 + 
     61 +// MarkSeen marks the specified message as having been read.
     62 +func (c *Client) MarkSeen(name, id string) error {
     63 + uri := "/api/v1/mailbox/" + url.QueryEscape(name) + "/" + id
     64 + err := c.doJSON("PATCH", uri, nil)
     65 + if err != nil {
     66 + return err
     67 + }
     68 + return nil
     69 +}
     70 + 
     71 +// GetMessageSource returns the message source given a mailbox name and message ID.
     72 +func (c *Client) GetMessageSource(name, id string) (*bytes.Buffer, error) {
     73 + uri := "/api/v1/mailbox/" + url.QueryEscape(name) + "/" + id + "/source"
     74 + resp, err := c.do("GET", uri, nil)
     75 + if err != nil {
     76 + return nil, err
     77 + }
     78 + 
     79 + defer func() {
     80 + _ = resp.Body.Close()
     81 + }()
     82 + if resp.StatusCode != http.StatusOK {
     83 + return nil,
     84 + fmt.Errorf("Unexpected HTTP response status %v: %s", resp.StatusCode, resp.Status)
     85 + }
     86 + buf := new(bytes.Buffer)
     87 + _, err = buf.ReadFrom(resp.Body)
     88 + return buf, err
     89 +}
     90 + 
     91 +// DeleteMessage deletes a single message given the mailbox name and message ID.
     92 +func (c *Client) DeleteMessage(name, id string) error {
     93 + uri := "/api/v1/mailbox/" + url.QueryEscape(name) + "/" + id
     94 + resp, err := c.do("DELETE", uri, nil)
     95 + if err != nil {
     96 + return err
     97 + }
     98 + _ = resp.Body.Close()
     99 + if resp.StatusCode != http.StatusOK {
     100 + return fmt.Errorf("Unexpected HTTP response status %v: %s", resp.StatusCode, resp.Status)
     101 + }
     102 + return nil
     103 +}
     104 + 
     105 +// PurgeMailbox deletes all messages in the given mailbox
     106 +func (c *Client) PurgeMailbox(name string) error {
     107 + uri := "/api/v1/mailbox/" + url.QueryEscape(name)
     108 + resp, err := c.do("DELETE", uri, nil)
     109 + if err != nil {
     110 + return err
     111 + }
     112 + _ = resp.Body.Close()
     113 + if resp.StatusCode != http.StatusOK {
     114 + return fmt.Errorf("Unexpected HTTP response status %v: %s", resp.StatusCode, resp.Status)
     115 + }
     116 + return nil
     117 +}
     118 + 
     119 +// MessageHeader represents a crash.fyi message sans content
     120 +type MessageHeader struct {
     121 + *model.JSONMessageHeaderV1
     122 + client *Client
     123 +}
     124 + 
     125 +// GetMessage returns this message with content
     126 +func (h *MessageHeader) GetMessage() (message *Message, err error) {
     127 + return h.client.GetMessage(h.Mailbox, h.ID)
     128 +}
     129 + 
     130 +// GetSource returns the source for this message
     131 +func (h *MessageHeader) GetSource() (*bytes.Buffer, error) {
     132 + return h.client.GetMessageSource(h.Mailbox, h.ID)
     133 +}
     134 + 
     135 +// Delete deletes this message from the mailbox
     136 +func (h *MessageHeader) Delete() error {
     137 + return h.client.DeleteMessage(h.Mailbox, h.ID)
     138 +}
     139 + 
     140 +// Message represents a crash.fyi message including content
     141 +type Message struct {
     142 + *model.JSONMessageV1
     143 + client *Client
     144 +}
     145 + 
     146 +// GetSource returns the source for this message
     147 +func (m *Message) GetSource() (*bytes.Buffer, error) {
     148 + return m.client.GetMessageSource(m.Mailbox, m.ID)
     149 +}
     150 + 
     151 +// Delete deletes this message from the mailbox
     152 +func (m *Message) Delete() error {
     153 + return m.client.DeleteMessage(m.Mailbox, m.ID)
     154 +}
     155 + 
  • ■ ■ ■ ■ ■ ■
    apiv1_client_test.go
     1 +package client_test
     2 + 
     3 +import (
     4 + "github.com/gorilla/mux"
     5 + "net/http"
     6 + "net/http/httptest"
     7 + "testing"
     8 + "time"
     9 + 
     10 + "crash.software/crash.fyi/cspam.go"
     11 +)
     12 + 
     13 +func TestClientV1ListMailbox(t *testing.T) {
     14 + // Setup.
     15 + c, router, teardown := setup()
     16 + defer teardown()
     17 + 
     18 + listHandler := &jsonHandler{json: `[
     19 + {
     20 + "mailbox": "testbox",
     21 + "id": "1",
     22 + "from": "fromuser",
     23 + "subject": "test subject",
     24 + "date": "2013-10-15T16:12:02.231532239-07:00",
     25 + "size": 264,
     26 + "seen": true
     27 + }
     28 + ]`}
     29 + 
     30 + router.Path("/api/v1/mailbox/testbox").Methods("GET").Handler(listHandler)
     31 + 
     32 + // Method under test.
     33 + headers, err := c.ListMailbox("testbox")
     34 + if err != nil {
     35 + t.Fatal(err)
     36 + }
     37 + 
     38 + if len(headers) != 1 {
     39 + t.Fatalf("Got %v headers, want 1", len(headers))
     40 + }
     41 + h := headers[0]
     42 + 
     43 + got := h.Mailbox
     44 + want := "testbox"
     45 + if got != want {
     46 + t.Errorf("Mailbox got %q, want %q", got, want)
     47 + }
     48 + 
     49 + got = h.ID
     50 + want = "1"
     51 + if got != want {
     52 + t.Errorf("ID got %q, want %q", got, want)
     53 + }
     54 + 
     55 + got = h.From
     56 + want = "fromuser"
     57 + if got != want {
     58 + t.Errorf("From got %q, want %q", got, want)
     59 + }
     60 + 
     61 + got = h.Subject
     62 + want = "test subject"
     63 + if got != want {
     64 + t.Errorf("Subject got %q, want %q", got, want)
     65 + }
     66 + 
     67 + gotTime := h.Date
     68 + wantTime := time.Date(2013, 10, 15, 16, 12, 02, 231532239, time.FixedZone("UTC-7", -7*60*60))
     69 + if !wantTime.Equal(gotTime) {
     70 + t.Errorf("Date got %v, want %v", gotTime, wantTime)
     71 + }
     72 + 
     73 + gotInt := h.Size
     74 + wantInt := int64(264)
     75 + if gotInt != wantInt {
     76 + t.Errorf("Size got %v, want %v", gotInt, wantInt)
     77 + }
     78 + 
     79 + wantBool := true
     80 + gotBool := h.Seen
     81 + if gotBool != wantBool {
     82 + t.Errorf("Seen got %v, want %v", gotBool, wantBool)
     83 + }
     84 +}
     85 + 
     86 +func TestClientV1GetMessage(t *testing.T) {
     87 + // Setup.
     88 + c, router, teardown := setup()
     89 + defer teardown()
     90 + 
     91 + messageHandler := &jsonHandler{json: `{
     92 + "mailbox": "testbox",
     93 + "id": "20170107T224128-0000",
     94 + "from": "fromuser",
     95 + "subject": "test subject",
     96 + "date": "2013-10-15T16:12:02.231532239-07:00",
     97 + "size": 264,
     98 + "seen": true,
     99 + "body": {
     100 + "text": "Plain text",
     101 + "html": "<html>"
     102 + }
     103 + }`}
     104 + 
     105 + router.Path("/api/v1/mailbox/testbox/20170107T224128-0000").Methods("GET").Handler(messageHandler)
     106 + 
     107 + // Method under test.
     108 + m, err := c.GetMessage("testbox", "20170107T224128-0000")
     109 + if err != nil {
     110 + t.Fatal(err)
     111 + }
     112 + if m == nil {
     113 + t.Fatalf("message was nil, wanted a value")
     114 + }
     115 + 
     116 + got := m.Mailbox
     117 + want := "testbox"
     118 + if got != want {
     119 + t.Errorf("Mailbox got %q, want %q", got, want)
     120 + }
     121 + 
     122 + got = m.ID
     123 + want = "20170107T224128-0000"
     124 + if got != want {
     125 + t.Errorf("ID got %q, want %q", got, want)
     126 + }
     127 + 
     128 + got = m.From
     129 + want = "fromuser"
     130 + if got != want {
     131 + t.Errorf("From got %q, want %q", got, want)
     132 + }
     133 + 
     134 + got = m.Subject
     135 + want = "test subject"
     136 + if got != want {
     137 + t.Errorf("Subject got %q, want %q", got, want)
     138 + }
     139 + 
     140 + gotTime := m.Date
     141 + wantTime := time.Date(2013, 10, 15, 16, 12, 02, 231532239, time.FixedZone("UTC-7", -7*60*60))
     142 + if !wantTime.Equal(gotTime) {
     143 + t.Errorf("Date got %v, want %v", gotTime, wantTime)
     144 + }
     145 + 
     146 + gotInt := m.Size
     147 + wantInt := int64(264)
     148 + if gotInt != wantInt {
     149 + t.Errorf("Size got %v, want %v", gotInt, wantInt)
     150 + }
     151 + 
     152 + gotBool := m.Seen
     153 + wantBool := true
     154 + if gotBool != wantBool {
     155 + t.Errorf("Seen got %v, want %v", gotBool, wantBool)
     156 + }
     157 + 
     158 + got = m.Body.Text
     159 + want = "Plain text"
     160 + if got != want {
     161 + t.Errorf("Body Text got %q, want %q", got, want)
     162 + }
     163 + 
     164 + got = m.Body.HTML
     165 + want = "<html>"
     166 + if got != want {
     167 + t.Errorf("Body HTML got %q, want %q", got, want)
     168 + }
     169 +}
     170 + 
     171 +func TestClientV1MarkSeen(t *testing.T) {
     172 + // Setup.
     173 + c, router, teardown := setup()
     174 + defer teardown()
     175 + 
     176 + handler := &jsonHandler{}
     177 + router.Path("/api/v1/mailbox/testbox/20170107T224128-0000").Methods("PATCH").
     178 + Handler(handler)
     179 + 
     180 + // Method under test.
     181 + err := c.MarkSeen("testbox", "20170107T224128-0000")
     182 + if err != nil {
     183 + t.Fatal(err)
     184 + }
     185 + 
     186 + if !handler.called {
     187 + t.Error("Wanted HTTP handler to be called, but it was not")
     188 + }
     189 +}
     190 + 
     191 +func TestClientV1GetMessageSource(t *testing.T) {
     192 + // Setup.
     193 + c, router, teardown := setup()
     194 + defer teardown()
     195 + 
     196 + router.Path("/api/v1/mailbox/testbox/20170107T224128-0000/source").Methods("GET").
     197 + Handler(&jsonHandler{json: `message source`})
     198 + 
     199 + // Method under test.
     200 + source, err := c.GetMessageSource("testbox", "20170107T224128-0000")
     201 + if err != nil {
     202 + t.Fatal(err)
     203 + }
     204 + 
     205 + want := "message source"
     206 + got := source.String()
     207 + if got != want {
     208 + t.Errorf("Source got %q, want %q", got, want)
     209 + }
     210 +}
     211 + 
     212 +func TestClientV1DeleteMessage(t *testing.T) {
     213 + // Setup.
     214 + c, router, teardown := setup()
     215 + defer teardown()
     216 + 
     217 + handler := &jsonHandler{}
     218 + router.Path("/api/v1/mailbox/testbox/20170107T224128-0000").Methods("DELETE").
     219 + Handler(handler)
     220 + 
     221 + // Method under test.
     222 + err := c.DeleteMessage("testbox", "20170107T224128-0000")
     223 + if err != nil {
     224 + t.Fatal(err)
     225 + }
     226 + 
     227 + if !handler.called {
     228 + t.Error("Wanted HTTP handler to be called, but it was not")
     229 + }
     230 +}
     231 + 
     232 +func TestClientV1PurgeMailbox(t *testing.T) {
     233 + // Setup.
     234 + c, router, teardown := setup()
     235 + defer teardown()
     236 + 
     237 + handler := &jsonHandler{}
     238 + router.Path("/api/v1/mailbox/testbox").Methods("DELETE").Handler(handler)
     239 + 
     240 + // Method under test.
     241 + err := c.PurgeMailbox("testbox")
     242 + if err != nil {
     243 + t.Fatal(err)
     244 + }
     245 + 
     246 + if !handler.called {
     247 + t.Error("Wanted HTTP handler to be called, but it was not")
     248 + }
     249 +}
     250 + 
     251 +func TestClientV1MessageHeader(t *testing.T) {
     252 + // Setup.
     253 + c, router, teardown := setup()
     254 + defer teardown()
     255 + 
     256 + listHandler := &jsonHandler{json: `[
     257 + {
     258 + "mailbox":"mailbox1",
     259 + "id":"id1",
     260 + "from":"from1",
     261 + "subject":"subject1",
     262 + "date":"2017-01-01T00:00:00.000-07:00",
     263 + "size":100,
     264 + "seen":true
     265 + }
     266 + ]`}
     267 + router.Path("/api/v1/mailbox/testbox").Methods("GET").Handler(listHandler)
     268 + 
     269 + // Method under test.
     270 + headers, err := c.ListMailbox("testbox")
     271 + if err != nil {
     272 + t.Fatal(err)
     273 + }
     274 + 
     275 + if len(headers) != 1 {
     276 + t.Fatalf("len(headers) == %v, want 1", len(headers))
     277 + }
     278 + header := headers[0]
     279 + 
     280 + // Test MessageHeader.Delete().
     281 + handler := &jsonHandler{}
     282 + router.Path("/api/v1/mailbox/mailbox1/id1").Methods("DELETE").Handler(handler)
     283 + err = header.Delete()
     284 + if err != nil {
     285 + t.Fatal(err)
     286 + }
     287 + 
     288 + // Test MessageHeader.GetSource().
     289 + router.Path("/api/v1/mailbox/mailbox1/id1/source").Methods("GET").
     290 + Handler(&jsonHandler{json: `source1`})
     291 + buf, err := header.GetSource()
     292 + if err != nil {
     293 + t.Fatal(err)
     294 + }
     295 + 
     296 + want := "source1"
     297 + got := buf.String()
     298 + if got != want {
     299 + t.Errorf("Got source %q, want %q", got, want)
     300 + }
     301 + 
     302 + // Test MessageHeader.GetMessage().
     303 + messageHandler := &jsonHandler{json: `{
     304 + "mailbox":"mailbox1",
     305 + "id":"id1",
     306 + "from":"from1",
     307 + "subject":"subject1",
     308 + "date":"2017-01-01T00:00:00.000-07:00",
     309 + "size":100
     310 + }`}
     311 + router.Path("/api/v1/mailbox/mailbox1/id1").Methods("GET").Handler(messageHandler)
     312 + message, err := header.GetMessage()
     313 + if err != nil {
     314 + t.Fatal(err)
     315 + }
     316 + if message == nil {
     317 + t.Fatalf("message was nil, wanted a value")
     318 + }
     319 + 
     320 + // Test Message.Delete().
     321 + err = message.Delete()
     322 + if err != nil {
     323 + t.Fatal(err)
     324 + }
     325 + 
     326 + // Test Message.GetSource().
     327 + buf, err = message.GetSource()
     328 + if err != nil {
     329 + t.Fatal(err)
     330 + }
     331 + 
     332 + want = "source1"
     333 + got = buf.String()
     334 + if got != want {
     335 + t.Errorf("Got source %q, want %q", got, want)
     336 + }
     337 +}
     338 + 
     339 +// setup returns a client, router and server for API testing.
     340 +func setup() (c *client.Client, router *mux.Router, teardown func()) {
     341 + router = mux.NewRouter()
     342 + server := httptest.NewServer(router)
     343 + c, err := client.New(server.URL)
     344 + if err != nil {
     345 + panic(err)
     346 + }
     347 + return c, router, func() {
     348 + server.Close()
     349 + }
     350 +}
     351 + 
     352 +// jsonHandler returns the string in json when servicing a request.
     353 +type jsonHandler struct {
     354 + json string
     355 + called bool
     356 +}
     357 + 
     358 +func (j *jsonHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
     359 + j.called = true
     360 + w.Write([]byte(j.json))
     361 +}
     362 + 
  • ■ ■ ■ ■ ■ ■
    cmd/cspam/list.go
     1 +package main
     2 + 
     3 +import (
     4 + "context"
     5 + "flag"
     6 + "fmt"
     7 + 
     8 + "github.com/google/subcommands"
     9 + "crash.software/crash.fyi/cspam.go"
     10 +)
     11 + 
     12 +type listCmd struct {
     13 + mailbox string
     14 +}
     15 + 
     16 +func (*listCmd) Name() string {
     17 + return "list"
     18 +}
     19 + 
     20 +func (*listCmd) Synopsis() string {
     21 + return "list contents of mailbox"
     22 +}
     23 + 
     24 +func (*listCmd) Usage() string {
     25 + return `list <mailbox>:
     26 + list message IDs in mailbox
     27 +`
     28 +}
     29 + 
     30 +func (l *listCmd) SetFlags(f *flag.FlagSet) {
     31 +}
     32 + 
     33 +func (l *listCmd) Execute(
     34 + _ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
     35 + mailbox := f.Arg(0)
     36 + if mailbox == "" {
     37 + return usage("mailbox required")
     38 + }
     39 + // Setup rest client
     40 + c, err := client.New(baseURL())
     41 + if err != nil {
     42 + return fatal("Couldn't build client", err)
     43 + }
     44 + // Get list
     45 + headers, err := c.ListMailbox(mailbox)
     46 + if err != nil {
     47 + return fatal("REST call failed", err)
     48 + }
     49 + for _, h := range headers {
     50 + fmt.Println(h.ID)
     51 + }
     52 + 
     53 + return subcommands.ExitSuccess
     54 +}
     55 + 
  • ■ ■ ■ ■ ■ ■
    cmd/cspam/main.go
     1 +// Package main implements a command line client for crash.fyi REST API
     2 +package main
     3 + 
     4 +import (
     5 + "context"
     6 + "flag"
     7 + "fmt"
     8 + "os"
     9 + "regexp"
     10 + 
     11 + "github.com/google/subcommands"
     12 +)
     13 + 
     14 +var host = flag.String("host", "crash.fyi", "host/IP of crash.fyi server")
     15 +var port = flag.Uint("port", 443, "HTTPs port of crash.fyi server")
     16 + 
     17 +// Allow subcommands to accept regular expressions as flags
     18 +type regexFlag struct {
     19 + *regexp.Regexp
     20 +}
     21 + 
     22 +func (r *regexFlag) Defined() bool {
     23 + return r.Regexp != nil
     24 +}
     25 + 
     26 +func (r *regexFlag) Set(pattern string) error {
     27 + if pattern == "" {
     28 + r.Regexp = nil
     29 + return nil
     30 + }
     31 + re, err := regexp.Compile(pattern)
     32 + if err != nil {
     33 + return err
     34 + }
     35 + r.Regexp = re
     36 + return nil
     37 +}
     38 + 
     39 +func (r *regexFlag) String() string {
     40 + if r.Regexp == nil {
     41 + return ""
     42 + }
     43 + return r.Regexp.String()
     44 +}
     45 + 
     46 +// regexFlag must implement flag.Value
     47 +var _ flag.Value = &regexFlag{}
     48 + 
     49 +func main() {
     50 + // Important top-level flags
     51 + subcommands.ImportantFlag("host")
     52 + subcommands.ImportantFlag("port")
     53 + // Setup standard helpers
     54 + subcommands.Register(subcommands.HelpCommand(), "")
     55 + subcommands.Register(subcommands.FlagsCommand(), "")
     56 + subcommands.Register(subcommands.CommandsCommand(), "")
     57 + // Setup my commands
     58 + subcommands.Register(&listCmd{}, "")
     59 + subcommands.Register(&matchCmd{}, "")
     60 + subcommands.Register(&mboxCmd{}, "")
     61 + // Parse and execute
     62 + flag.Parse()
     63 + ctx := context.Background()
     64 + os.Exit(int(subcommands.Execute(ctx)))
     65 +}
     66 + 
     67 +func baseURL() string {
     68 + return fmt.Sprintf("http://%s:%v", *host, *port)
     69 +}
     70 + 
     71 +func fatal(msg string, err error) subcommands.ExitStatus {
     72 + fmt.Fprintf(os.Stderr, "%s: %v\n", msg, err)
     73 + return subcommands.ExitFailure
     74 +}
     75 + 
     76 +func usage(msg string) subcommands.ExitStatus {
     77 + fmt.Fprintln(os.Stderr, msg)
     78 + return subcommands.ExitUsageError
     79 +}
     80 + 
  • ■ ■ ■ ■ ■ ■
    cmd/cspam/match.go
     1 +package main
     2 + 
     3 +import (
     4 + "context"
     5 + "encoding/json"
     6 + "flag"
     7 + "fmt"
     8 + "net/mail"
     9 + "os"
     10 + "time"
     11 + 
     12 + "github.com/google/subcommands"
     13 + "crash.software/crash.fyi/cspam.go"
     14 +)
     15 + 
     16 +type matchCmd struct {
     17 + mailbox string
     18 + output string
     19 + outFunc func(headers []*client.MessageHeader) error
     20 + delete bool
     21 + // match criteria
     22 + from regexFlag
     23 + subject regexFlag
     24 + to regexFlag
     25 + maxAge time.Duration
     26 +}
     27 + 
     28 +func (*matchCmd) Name() string {
     29 + return "match"
     30 +}
     31 + 
     32 +func (*matchCmd) Synopsis() string {
     33 + return "output messages matching criteria"
     34 +}
     35 + 
     36 +func (*matchCmd) Usage() string {
     37 + return `match [flags] <mailbox>:
     38 + output messages matching all specified criteria
     39 + exit status will be 1 if no matches were found, otherwise 0
     40 +`
     41 +}
     42 + 
     43 +func (m *matchCmd) SetFlags(f *flag.FlagSet) {
     44 + f.StringVar(&m.output, "output", "id", "output format: id, json, or mbox")
     45 + f.BoolVar(&m.delete, "delete", false, "delete matched messages after output")
     46 + f.Var(&m.from, "from", "From header matching regexp (address, not name)")
     47 + f.Var(&m.subject, "subject", "Subject header matching regexp")
     48 + f.Var(&m.to, "to", "To header matching regexp (must match 1+ to address)")
     49 + f.DurationVar(
     50 + &m.maxAge, "maxage", 0,
     51 + "Matches must have been received in this time frame (ex: \"10s\", \"5m\")")
     52 +}
     53 + 
     54 +func (m *matchCmd) Execute(
     55 + _ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
     56 + mailbox := f.Arg(0)
     57 + if mailbox == "" {
     58 + return usage("mailbox required")
     59 + }
     60 + // Select output function
     61 + switch m.output {
     62 + case "id":
     63 + m.outFunc = outputID
     64 + case "json":
     65 + m.outFunc = outputJSON
     66 + case "mbox":
     67 + m.outFunc = outputMbox
     68 + default:
     69 + return usage("unknown output type: " + m.output)
     70 + }
     71 + // Setup REST client
     72 + c, err := client.New(baseURL())
     73 + if err != nil {
     74 + return fatal("Couldn't build client", err)
     75 + }
     76 + // Get list
     77 + headers, err := c.ListMailbox(mailbox)
     78 + if err != nil {
     79 + return fatal("List REST call failed", err)
     80 + }
     81 + // Find matches
     82 + matches := make([]*client.MessageHeader, 0, len(headers))
     83 + for _, h := range headers {
     84 + if m.match(h) {
     85 + matches = append(matches, h)
     86 + }
     87 + }
     88 + // Return error status if no matches
     89 + if len(matches) == 0 {
     90 + return subcommands.ExitFailure
     91 + }
     92 + // Output matches
     93 + err = m.outFunc(matches)
     94 + if err != nil {
     95 + return fatal("Error", err)
     96 + }
     97 + if m.delete {
     98 + // Delete matches
     99 + for _, h := range matches {
     100 + err = h.Delete()
     101 + if err != nil {
     102 + return fatal("Delete REST call failed", err)
     103 + }
     104 + }
     105 + }
     106 + return subcommands.ExitSuccess
     107 +}
     108 + 
     109 +// match returns true if header matches all defined criteria
     110 +func (m *matchCmd) match(header *client.MessageHeader) bool {
     111 + if m.maxAge > 0 {
     112 + if time.Since(header.Date) > m.maxAge {
     113 + return false
     114 + }
     115 + }
     116 + if m.subject.Defined() {
     117 + if !m.subject.MatchString(header.Subject) {
     118 + return false
     119 + }
     120 + }
     121 + if m.from.Defined() {
     122 + from := header.From
     123 + addr, err := mail.ParseAddress(from)
     124 + if err == nil {
     125 + // Parsed successfully
     126 + from = addr.Address
     127 + }
     128 + if !m.from.MatchString(from) {
     129 + return false
     130 + }
     131 + }
     132 + if m.to.Defined() {
     133 + match := false
     134 + for _, to := range header.To {
     135 + addr, err := mail.ParseAddress(to)
     136 + if err == nil {
     137 + // Parsed successfully
     138 + to = addr.Address
     139 + }
     140 + if m.to.MatchString(to) {
     141 + match = true
     142 + break
     143 + }
     144 + }
     145 + if !match {
     146 + return false
     147 + }
     148 + }
     149 + return true
     150 +}
     151 + 
     152 +func outputID(headers []*client.MessageHeader) error {
     153 + for _, h := range headers {
     154 + fmt.Println(h.ID)
     155 + }
     156 + return nil
     157 +}
     158 + 
     159 +func outputJSON(headers []*client.MessageHeader) error {
     160 + jsonEncoder := json.NewEncoder(os.Stdout)
     161 + jsonEncoder.SetEscapeHTML(false)
     162 + jsonEncoder.SetIndent("", " ")
     163 + return jsonEncoder.Encode(headers)
     164 +}
     165 + 
  • ■ ■ ■ ■ ■ ■
    cmd/cspam/mbox.go
     1 +package main
     2 + 
     3 +import (
     4 + "context"
     5 + "flag"
     6 + "fmt"
     7 + "os"
     8 + 
     9 + "github.com/google/subcommands"
     10 + "crash.software/crash.fyi/cspam.go"
     11 +)
     12 + 
     13 +type mboxCmd struct {
     14 + mailbox string
     15 + delete bool
     16 +}
     17 + 
     18 +func (*mboxCmd) Name() string {
     19 + return "mbox"
     20 +}
     21 + 
     22 +func (*mboxCmd) Synopsis() string {
     23 + return "output mailbox in mbox format"
     24 +}
     25 + 
     26 +func (*mboxCmd) Usage() string {
     27 + return `mbox [flags] <mailbox>:
     28 + output mailbox in mbox format
     29 +`
     30 +}
     31 + 
     32 +func (m *mboxCmd) SetFlags(f *flag.FlagSet) {
     33 + f.BoolVar(&m.delete, "delete", false, "delete messages after output")
     34 +}
     35 + 
     36 +func (m *mboxCmd) Execute(
     37 + _ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
     38 + mailbox := f.Arg(0)
     39 + if mailbox == "" {
     40 + return usage("mailbox required")
     41 + }
     42 + // Setup REST client
     43 + c, err := client.New(baseURL())
     44 + if err != nil {
     45 + return fatal("Couldn't build client", err)
     46 + }
     47 + // Get list
     48 + headers, err := c.ListMailbox(mailbox)
     49 + if err != nil {
     50 + return fatal("List REST call failed", err)
     51 + }
     52 + err = outputMbox(headers)
     53 + if err != nil {
     54 + return fatal("Error", err)
     55 + }
     56 + if m.delete {
     57 + // Delete matches
     58 + for _, h := range headers {
     59 + err = h.Delete()
     60 + if err != nil {
     61 + return fatal("Delete REST call failed", err)
     62 + }
     63 + }
     64 + }
     65 + return subcommands.ExitSuccess
     66 +}
     67 + 
     68 +// outputMbox renders messages in mbox format
     69 +// also used by match subcommand
     70 +func outputMbox(headers []*client.MessageHeader) error {
     71 + for _, h := range headers {
     72 + source, err := h.GetSource()
     73 + if err != nil {
     74 + return fmt.Errorf("Get source REST failed: %v", err)
     75 + }
     76 + fmt.Printf("From %s\n", h.From)
     77 + // TODO Escape "From " in message bodies with >
     78 + source.WriteTo(os.Stdout)
     79 + fmt.Println()
     80 + }
     81 + return nil
     82 +}
     83 + 
  • ■ ■ ■ ■ ■ ■
    example_test.go
     1 +package client_test
     2 + 
     3 +import (
     4 + "fmt"
     5 + "log"
     6 + "net/http"
     7 + "net/http/httptest"
     8 + 
     9 + "github.com/gorilla/mux"
     10 + "crash.software/crash.fyi/cspam.go"
     11 +)
     12 + 
     13 +// Example demonstrates basic usage for the crash.fyi REST client.
     14 +func Example() {
     15 + // Setup a fake crash.fyi server for this example.
     16 + baseURL, teardown := exampleSetup()
     17 + defer teardown()
     18 + 
     19 + // Begin by creating a new client using the base URL of crash.fyi server, i.e.
     20 + // `https://crash.fyi`.
     21 + restClient, err := client.New(baseURL)
     22 + if err != nil {
     23 + log.Fatal(err)
     24 + }
     25 + 
     26 + // Get a slice of message headers for the mailbox named `user1`.
     27 + headers, err := restClient.ListMailbox("user1")
     28 + if err != nil {
     29 + log.Fatal(err)
     30 + }
     31 + for _, header := range headers {
     32 + fmt.Printf("ID: %v, Subject: %v\n", header.ID, header.Subject)
     33 + }
     34 + 
     35 + // Get the content of the first message.
     36 + message, err := headers[0].GetMessage()
     37 + if err != nil {
     38 + log.Fatal(err)
     39 + }
     40 + fmt.Printf("\nFrom: %v\n", message.From)
     41 + fmt.Printf("Text body:\n%v", message.Body.Text)
     42 + 
     43 + // Delete the second message.
     44 + err = headers[1].Delete()
     45 + if err != nil {
     46 + log.Fatal(err)
     47 + }
     48 + 
     49 + // Output:
     50 + // ID: 20180107T224128-0000, Subject: First subject
     51 + // ID: 20180108T121212-0123, Subject: Second subject
     52 + //
     53 + // From: [email protected]
     54 + // Text body:
     55 + // This is the plain text body
     56 +}
     57 + 
     58 +// exampleSetup creates a fake crash.fyi server to power Example() below.
     59 +func exampleSetup() (baseURL string, teardown func()) {
     60 + router := mux.NewRouter()
     61 + server := httptest.NewServer(router)
     62 + 
     63 + // Handle ListMailbox request.
     64 + router.HandleFunc("/api/v1/mailbox/user1", func(w http.ResponseWriter, r *http.Request) {
     65 + w.Write([]byte(`[
     66 + {
     67 + "mailbox": "user1",
     68 + "id": "20180107T224128-0000",
     69 + "subject": "First subject"
     70 + },
     71 + {
     72 + "mailbox": "user1",
     73 + "id": "20180108T121212-0123",
     74 + "subject": "Second subject"
     75 + }
     76 + ]`))
     77 + })
     78 + 
     79 + // Handle GetMessage request.
     80 + router.HandleFunc("/api/v1/mailbox/user1/20180107T224128-0000",
     81 + func(w http.ResponseWriter, r *http.Request) {
     82 + w.Write([]byte(`{
     83 + "mailbox": "user1",
     84 + "id": "20180107T224128-0000",
     85 + "from": "[email protected]",
     86 + "subject": "First subject",
     87 + "body": {
     88 + "text": "This is the plain text body"
     89 + }
     90 + }`))
     91 + })
     92 + 
     93 + // Handle Delete request.
     94 + router.HandleFunc("/api/v1/mailbox/user1/20180108T121212-0123",
     95 + func(w http.ResponseWriter, r *http.Request) {
     96 + // Nop.
     97 + })
     98 + 
     99 + return server.URL, func() {
     100 + server.Close()
     101 + }
     102 +}
     103 + 
  • ■ ■ ■ ■ ■ ■
    model/apiv1_model.go
     1 +package model
     2 + 
     3 +import (
     4 + "time"
     5 +)
     6 + 
     7 +// JSONMessageHeaderV1 contains the basic header data for a message
     8 +type JSONMessageHeaderV1 struct {
     9 + Mailbox string `json:"mailbox"`
     10 + ID string `json:"id"`
     11 + From string `json:"from"`
     12 + To []string `json:"to"`
     13 + Subject string `json:"subject"`
     14 + Date time.Time `json:"date"`
     15 + PosixMillis int64 `json:"posix-millis"`
     16 + Size int64 `json:"size"`
     17 + Seen bool `json:"seen"`
     18 +}
     19 + 
     20 +// JSONMessageV1 contains the same data as the header plus a JSONMessageBody
     21 +type JSONMessageV1 struct {
     22 + Mailbox string `json:"mailbox"`
     23 + ID string `json:"id"`
     24 + From string `json:"from"`
     25 + To []string `json:"to"`
     26 + Subject string `json:"subject"`
     27 + Date time.Time `json:"date"`
     28 + PosixMillis int64 `json:"posix-millis"`
     29 + Size int64 `json:"size"`
     30 + Seen bool `json:"seen"`
     31 + Body *JSONMessageBodyV1 `json:"body"`
     32 + Header map[string][]string `json:"header"`
     33 + Attachments []*JSONMessageAttachmentV1 `json:"attachments"`
     34 +}
     35 + 
     36 +// JSONMessageAttachmentV1 contains information about a MIME attachment
     37 +type JSONMessageAttachmentV1 struct {
     38 + FileName string `json:"filename"`
     39 + ContentType string `json:"content-type"`
     40 + DownloadLink string `json:"download-link"`
     41 + ViewLink string `json:"view-link"`
     42 + MD5 string `json:"md5"`
     43 +}
     44 + 
     45 +// JSONMessageBodyV1 contains the Text and HTML versions of the message body
     46 +type JSONMessageBodyV1 struct {
     47 + Text string `json:"text"`
     48 + HTML string `json:"html"`
     49 +}
     50 + 
  • ■ ■ ■ ■ ■ ■
    rest.go
     1 +package client
     2 + 
     3 +import (
     4 + "bytes"
     5 + "encoding/json"
     6 + "fmt"
     7 + "io"
     8 + "net/http"
     9 + "net/url"
     10 +)
     11 + 
     12 +// httpClient allows http.Client to be mocked for tests
     13 +type httpClient interface {
     14 + Do(*http.Request) (*http.Response, error)
     15 +}
     16 + 
     17 +// Generic REST restClient
     18 +type restClient struct {
     19 + client httpClient
     20 + baseURL *url.URL
     21 +}
     22 + 
     23 +// do performs an HTTP request with this client and returns the response.
     24 +func (c *restClient) do(method, uri string, body []byte) (*http.Response, error) {
     25 + rel, err := url.Parse(uri)
     26 + if err != nil {
     27 + return nil, err
     28 + }
     29 + url := c.baseURL.ResolveReference(rel)
     30 + var r io.Reader
     31 + if body != nil {
     32 + r = bytes.NewReader(body)
     33 + }
     34 + req, err := http.NewRequest(method, url.String(), r)
     35 + if err != nil {
     36 + return nil, fmt.Errorf("%s for %q: %v", method, url, err)
     37 + }
     38 + return c.client.Do(req)
     39 +}
     40 + 
     41 +// doJSON performs an HTTP request with this client and marshalls the JSON response into v.
     42 +func (c *restClient) doJSON(method string, uri string, v interface{}) error {
     43 + resp, err := c.do(method, uri, nil)
     44 + if err != nil {
     45 + return err
     46 + }
     47 + 
     48 + defer func() {
     49 + _ = resp.Body.Close()
     50 + }()
     51 + if resp.StatusCode == http.StatusOK {
     52 + if v == nil {
     53 + return nil
     54 + }
     55 + // Decode response body
     56 + return json.NewDecoder(resp.Body).Decode(v)
     57 + }
     58 + 
     59 + return fmt.Errorf("%s for %q, unexpected %v: %s", method, uri, resp.StatusCode, resp.Status)
     60 +}
     61 + 
     62 +// doJSONBody performs an HTTP request with this client and marshalls the JSON response into v.
     63 +func (c *restClient) doJSONBody(method string, uri string, body []byte, v interface{}) error {
     64 + resp, err := c.do(method, uri, body)
     65 + if err != nil {
     66 + return err
     67 + }
     68 + 
     69 + defer func() {
     70 + _ = resp.Body.Close()
     71 + }()
     72 + if resp.StatusCode == http.StatusOK {
     73 + if v == nil {
     74 + return nil
     75 + }
     76 + // Decode response body
     77 + return json.NewDecoder(resp.Body).Decode(v)
     78 + }
     79 + 
     80 + return fmt.Errorf("%s for %q, unexpected %v: %s", method, uri, resp.StatusCode, resp.Status)
     81 +}
     82 + 
  • ■ ■ ■ ■ ■ ■
    rest_test.go
     1 +package client
     2 + 
     3 +import (
     4 + "bytes"
     5 + "io/ioutil"
     6 + "net/http"
     7 + "net/url"
     8 + "testing"
     9 +)
     10 + 
     11 +const baseURLStr = "http://test.local:8080"
     12 + 
     13 +var baseURL *url.URL
     14 + 
     15 +func init() {
     16 + var err error
     17 + baseURL, err = url.Parse(baseURLStr)
     18 + if err != nil {
     19 + panic(err)
     20 + }
     21 +}
     22 + 
     23 +type mockHTTPClient struct {
     24 + req *http.Request
     25 + statusCode int
     26 + body string
     27 +}
     28 + 
     29 +func (m *mockHTTPClient) Do(req *http.Request) (resp *http.Response, err error) {
     30 + m.req = req
     31 + if m.statusCode == 0 {
     32 + m.statusCode = 200
     33 + }
     34 + resp = &http.Response{
     35 + StatusCode: m.statusCode,
     36 + Body: ioutil.NopCloser(bytes.NewBufferString(m.body)),
     37 + }
     38 + return
     39 +}
     40 + 
     41 +func (m *mockHTTPClient) ReqBody() []byte {
     42 + r, err := m.req.GetBody()
     43 + if err != nil {
     44 + return nil
     45 + }
     46 + body, err := ioutil.ReadAll(r)
     47 + if err != nil {
     48 + return nil
     49 + }
     50 + _ = r.Close()
     51 + return body
     52 +}
     53 + 
     54 +func TestDo(t *testing.T) {
     55 + var want, got string
     56 + mth := &mockHTTPClient{}
     57 + c := &restClient{mth, baseURL}
     58 + body := []byte("Test body")
     59 + 
     60 + _, err := c.do("POST", "/dopost", body)
     61 + if err != nil {
     62 + t.Fatal(err)
     63 + }
     64 + 
     65 + want = "POST"
     66 + got = mth.req.Method
     67 + if got != want {
     68 + t.Errorf("req.Method == %q, want %q", got, want)
     69 + }
     70 + 
     71 + want = baseURLStr + "/dopost"
     72 + got = mth.req.URL.String()
     73 + if got != want {
     74 + t.Errorf("req.URL == %q, want %q", got, want)
     75 + }
     76 + 
     77 + b := mth.ReqBody()
     78 + if !bytes.Equal(b, body) {
     79 + t.Errorf("req.Body == %q, want %q", b, body)
     80 + }
     81 +}
     82 + 
     83 +func TestDoJSON(t *testing.T) {
     84 + var want, got string
     85 + 
     86 + mth := &mockHTTPClient{
     87 + body: `{"foo": "bar"}`,
     88 + }
     89 + c := &restClient{mth, baseURL}
     90 + 
     91 + var v map[string]interface{}
     92 + err := c.doJSON("GET", "/doget", &v)
     93 + if err != nil {
     94 + t.Fatal(err)
     95 + }
     96 + 
     97 + want = "GET"
     98 + got = mth.req.Method
     99 + if got != want {
     100 + t.Errorf("req.Method == %q, want %q", got, want)
     101 + }
     102 + 
     103 + want = baseURLStr + "/doget"
     104 + got = mth.req.URL.String()
     105 + if got != want {
     106 + t.Errorf("req.URL == %q, want %q", got, want)
     107 + }
     108 + 
     109 + want = "bar"
     110 + if val, ok := v["foo"]; ok {
     111 + got = val.(string)
     112 + if got != want {
     113 + t.Errorf("map[foo] == %q, want: %q", got, want)
     114 + }
     115 + } else {
     116 + t.Errorf("Map did not contain key foo, want: %q", want)
     117 + }
     118 +}
     119 + 
     120 +func TestDoJSONNilV(t *testing.T) {
     121 + var want, got string
     122 + 
     123 + mth := &mockHTTPClient{}
     124 + c := &restClient{mth, baseURL}
     125 + 
     126 + err := c.doJSON("GET", "/doget", nil)
     127 + if err != nil {
     128 + t.Fatal(err)
     129 + }
     130 + 
     131 + want = "GET"
     132 + got = mth.req.Method
     133 + if got != want {
     134 + t.Errorf("req.Method == %q, want %q", got, want)
     135 + }
     136 + 
     137 + want = baseURLStr + "/doget"
     138 + got = mth.req.URL.String()
     139 + if got != want {
     140 + t.Errorf("req.URL == %q, want %q", got, want)
     141 + }
     142 +}
     143 + 
Please wait...
Page is in error, reload to recover