diff --git a/CHANGELOG.md b/CHANGELOG.md index 554bfd3..a3a1cfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,16 @@ Change Log All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). -[1.2.0-rc2] - 2017-12-15 ------------------------- +## [Unreleased] + +### Added +- Button to purge mailbox contents from the UI. +- Simple HTML/CSS sanitization; `Safe HTML` and `Plain Text` UI tabs. + +### Changed +- Reverse message display sort order in the UI; now newest first. + +## [1.2.0-rc2] - 2017-12-15 ### Added - `rest/client` types `MessageHeader` and `Message` with convenience methods; @@ -20,8 +28,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). types - Fixed panic when `monitor.history` set to 0 -[1.2.0-rc1] - 2017-01-29 ------------------------- +## [1.2.0-rc1] - 2017-01-29 ### Added - Storage of `To:` header in messages (likely breaks existing datastores) @@ -47,8 +54,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Allow increased local-part length of 128 chars for Mailgun - RedHat and Ubuntu now use systemd instead of legacy init systems -[1.1.0] - 2016-09-03 --------------------- +## [1.1.0] - 2016-09-03 ### Added - Homebrew inbucket.conf and formula (see README) @@ -56,8 +62,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - Log and continue when unable to delete oldest message during cap enforcement -[1.1.0-rc2] - 2016-03-06 ------------------------- +## [1.1.0-rc2] - 2016-03-06 ### Added - Message Cap to status page @@ -67,8 +72,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Shutdown hang in retention scanner - Display empty subject as `(No Subject)` -[1.1.0-rc1] - 2016-03-04 ------------------------- +## [1.1.0-rc1] - 2016-03-04 ### Added - Inbucket now builds with Go 1.5 or 1.6 @@ -82,8 +86,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - RESTful API moved to `/api/v1` base URI - More graceful shutdown on Ctrl-C or when errors encountered -[1.0] - 2014-04-14 ------------------- +## [1.0] - 2014-04-14 ### Added - Add new configuration option `mailbox.message.cap` to prevent individual @@ -100,8 +103,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). [1.0]: https://github.com/jhillyerd/inbucket/compare/1.0-rc1...1.0 -Release Checklist ------------------ +## Release Checklist 1. Create release branch: `git flow release start 1.x.0` 2. Update CHANGELOG.md: diff --git a/inbucket.go b/inbucket.go index 83c39f4..23c646a 100644 --- a/inbucket.go +++ b/inbucket.go @@ -86,7 +86,7 @@ func main() { } // Setup signal handler - sigChan := make(chan os.Signal) + sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGINT) // Initialize logging @@ -150,7 +150,7 @@ signalLoop: log.Infof("Received SIGTERM, shutting down") close(shutdownChan) } - case _ = <-shutdownChan: + case <-shutdownChan: rootCancel() break signalLoop } diff --git a/sanitize/css.go b/sanitize/css.go new file mode 100644 index 0000000..e37d04e --- /dev/null +++ b/sanitize/css.go @@ -0,0 +1,110 @@ +package sanitize + +import ( + "bytes" + "strings" + + "github.com/gorilla/css/scanner" +) + +// propertyRule may someday allow control of what values are valid for a particular property. +type propertyRule struct{} + +var allowedProperties = map[string]propertyRule{ + "align": {}, + "background-color": {}, + "border": {}, + "border-bottom": {}, + "border-left": {}, + "border-radius": {}, + "border-right": {}, + "border-top": {}, + "box-sizing": {}, + "clear": {}, + "color": {}, + "content": {}, + "display": {}, + "font-family": {}, + "font-size": {}, + "font-weight": {}, + "height": {}, + "line-height": {}, + "margin": {}, + "margin-bottom": {}, + "margin-left": {}, + "margin-right": {}, + "margin-top": {}, + "max-height": {}, + "max-width": {}, + "overflow": {}, + "padding": {}, + "padding-bottom": {}, + "padding-left": {}, + "padding-right": {}, + "padding-top": {}, + "table-layout": {}, + "text-align": {}, + "text-decoration": {}, + "text-shadow": {}, + "vertical-align": {}, + "width": {}, + "word-break": {}, +} + +// Handler Token, return next state. +type stateHandler func(b *bytes.Buffer, t *scanner.Token) stateHandler + +func sanitizeStyle(input string) string { + b := &bytes.Buffer{} + scan := scanner.New(input) + state := stateStart + for { + t := scan.Next() + if t.Type == scanner.TokenEOF { + return b.String() + } + if t.Type == scanner.TokenError { + return "" + } + state = state(b, t) + if state == nil { + return "" + } + } +} + +func stateStart(b *bytes.Buffer, t *scanner.Token) stateHandler { + switch t.Type { + case scanner.TokenIdent: + _, ok := allowedProperties[strings.ToLower(t.Value)] + if !ok { + return stateEat + } + b.WriteString(t.Value) + return stateValid + case scanner.TokenS: + return stateStart + } + // Unexpected type. + b.WriteString("/*" + t.Type.String() + "*/") + return stateEat +} + +func stateEat(b *bytes.Buffer, t *scanner.Token) stateHandler { + if t.Type == scanner.TokenChar && t.Value == ";" { + // Done eating. + return stateStart + } + // Throw away this token. + return stateEat +} + +func stateValid(b *bytes.Buffer, t *scanner.Token) stateHandler { + state := stateValid + if t.Type == scanner.TokenChar && t.Value == ";" { + // End of property. + state = stateStart + } + b.WriteString(t.Value) + return state +} diff --git a/sanitize/css_test.go b/sanitize/css_test.go new file mode 100644 index 0000000..dc72a82 --- /dev/null +++ b/sanitize/css_test.go @@ -0,0 +1,34 @@ +package sanitize + +import ( + "testing" +) + +func TestSanitizeStyle(t *testing.T) { + testCases := []struct { + input, want string + }{ + {"", ""}, + { + "color: red;", + "color: red;", + }, + { + "background-color: black; color: white", + "background-color: black;color: white", + }, + { + "background-color: black; invalid: true; color: white", + "background-color: black;color: white", + }, + } + for _, tc := range testCases { + t.Run(tc.input, func(t *testing.T) { + got := sanitizeStyle(tc.input) + if got != tc.want { + t.Errorf("got: %q, want: %q, input: %q", got, tc.want, tc.input) + } + }) + } + +} diff --git a/sanitize/html.go b/sanitize/html.go index d8e1c6e..475880f 100644 --- a/sanitize/html.go +++ b/sanitize/html.go @@ -1,9 +1,88 @@ package sanitize -import "github.com/microcosm-cc/bluemonday" +import ( + "bufio" + "bytes" + "io" + "regexp" + "strings" + + "github.com/microcosm-cc/bluemonday" + "golang.org/x/net/html" +) + +var ( + cssSafe = regexp.MustCompile(".*") + policy = bluemonday.UGCPolicy(). + AllowElements("center"). + AllowAttrs("style").Matching(cssSafe).Globally() +) func HTML(html string) (output string, err error) { - policy := bluemonday.UGCPolicy() - output = policy.Sanitize(html) + output, err = sanitizeStyleTags(html) + if err != nil { + return "", err + } + output = policy.Sanitize(output) return } + +func sanitizeStyleTags(input string) (string, error) { + r := strings.NewReader(input) + b := &bytes.Buffer{} + if err := styleTagFilter(b, r); err != nil { + return "", err + } + return b.String(), nil +} + +func styleTagFilter(w io.Writer, r io.Reader) error { + bw := bufio.NewWriter(w) + b := make([]byte, 256) + z := html.NewTokenizer(r) + for { + b = b[:0] + tt := z.Next() + switch tt { + case html.ErrorToken: + err := z.Err() + if err == io.EOF { + return bw.Flush() + } + return err + case html.StartTagToken, html.SelfClosingTagToken: + name, hasAttr := z.TagName() + if !hasAttr { + bw.Write(z.Raw()) + continue + } + b = append(b, '<') + b = append(b, name...) + for { + key, val, more := z.TagAttr() + strval := string(val) + style := false + if strings.ToLower(string(key)) == "style" { + style = true + strval = sanitizeStyle(strval) + } + if !style || strval != "" { + b = append(b, ' ') + b = append(b, key...) + b = append(b, '=', '"') + b = append(b, []byte(html.EscapeString(strval))...) + b = append(b, '"') + } + if !more { + break + } + } + if tt == html.SelfClosingTagToken { + b = append(b, '/') + } + bw.Write(append(b, '>')) + default: + bw.Write(z.Raw()) + } + } +} diff --git a/sanitize/html_test.go b/sanitize/html_test.go index fa72e0c..c6acf09 100644 --- a/sanitize/html_test.go +++ b/sanitize/html_test.go @@ -35,6 +35,7 @@ func TestHTMLSimpleFormatting(t *testing.T) { "emphasis", "strong", "
some text
`, + `some text
`, + }, + { + "invalid styles", + `
+
+ |
+ |||||||||
+
|
+ |||||||||
|
+
+
+
+
+
+
+
+
|
+ |||||||||
|
+
+
+
+
+
+
+
+
+
+
+
|
+ |||||||||
|
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
|
+ |||||||||
|
+
+
+
+
+
+
+
+
|
+ |||||||||
|
+
+
+
+
+
+
+
+
|
+
+ This is a test of HTML at the top level. +
diff --git a/swaks-tests/run-tests.sh b/swaks-tests/run-tests.sh index 0784a11..c9a93b8 100755 --- a/swaks-tests/run-tests.sh +++ b/swaks-tests/run-tests.sh @@ -53,5 +53,6 @@ swaks $* --data gmail.raw # Outlook test swaks $* --data outlook.raw -# Nonemime responsive HTML test +# Non-mime responsive HTML test swaks $* --data nonmime-html-responsive.raw +swaks $* --data nonmime-html-inlined.raw