diff --git a/processor/emailer/emailer.go b/processor/emailer/emailer.go index 77e98c4..93e493c 100644 --- a/processor/emailer/emailer.go +++ b/processor/emailer/emailer.go @@ -23,6 +23,7 @@ import ( "os" "os/exec" "path/filepath" + "regexp" "strconv" "strings" "text/template" @@ -119,10 +120,11 @@ func (e *Emailer) loadTemplate() (*template.Template, error) { // Function map allows exporting functions to the template // funcMap := template.FuncMap{ - "env": env, - "quoteprintable": toQuotedPrintable, - "split": split, - "encodeHeader": encodeHeader, + "env": env, + "quoteprintable": toQuotedPrintable, + "split": split, + "encodeHeader": encodeHeader, + "makeListIdHeader": makeListIdHeader, } tmpl := template.Must(template.New("email.tmpl").Funcs(funcMap).Parse(string(content))) @@ -163,6 +165,28 @@ func encodeHeader(s string) string { return "=?utf-8?Q?" + se + "?=" } +// makeListIdHeader encodes email header entry to comply List-ID restriction of RFC 2919 +// according to DRUMS. +// +// The function has some special code to handle an URL. +func makeListIdHeader(s string) string { + // Strip scheme + sh := strings.TrimPrefix(s, "http://") + sh = strings.TrimPrefix(sh, "https://") + + // Only allow valid atext characters and dots; replace others with dots + re := regexp.MustCompile(`[^A-Za-z0-9!$%&'*+\-=^_` + "`" + `{|}~.]+`) + sh = re.ReplaceAllString(sh, ".") + + // Collapse multiple dots + sh = regexp.MustCompile(`\.+`).ReplaceAllString(sh, ".") + + // Clean url + sh = strings.Trim(sh, ".") + + return sh + ".localhost" +} + // Sendmail is a simple function that emails the given address. // // We send a MIME message with both a plain-text and a HTML-version of the diff --git a/processor/emailer/emailer_test.go b/processor/emailer/emailer_test.go new file mode 100644 index 0000000..4ea42a1 --- /dev/null +++ b/processor/emailer/emailer_test.go @@ -0,0 +1,53 @@ +package emailer + +import ( + "strings" + "testing" +) + +func TestMakeListIdHeader(t *testing.T) { + tests := []struct { + input string + expected string + }{ + { + input: "http://example.com/feed", + expected: "example.com.feed.localhost", + }, + { + input: "https://example.com/feed", + expected: "example.com.feed.localhost", + }, + { + input: "https://example.com/foo/bar?baz=qux", + expected: "example.com.foo.bar.baz=qux.localhost", + }, + { + input: "example.com/feed", + expected: "example.com.feed.localhost", + }, + { + input: "https://example.com/foo//bar///baz", + expected: "example.com.foo.bar.baz.localhost", + }, + { + input: "https://example.com/foo@bar#baz", + expected: "example.com.foo.bar.baz.localhost", + }, + } + + for _, tt := range tests { + got := makeListIdHeader(tt.input) + if got != tt.expected { + t.Errorf("makeListIdHeader(%q) = %q; want %q", tt.input, got, tt.expected) + } + // Check that every character in got is allowed + // from https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.4 + allowed := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#$%&'*+-/=?^_`{|}~." + for i, c := range got { + if !strings.ContainsRune(allowed, c) { + t.Errorf("makeListIdHeader(%q) produced invalid char %q at position %d", tt.input, c, i) + } + } + } +} diff --git a/template/template.txt b/template/template.txt index 193deb6..cb0b0f0 100644 --- a/template/template.txt +++ b/template/template.txt @@ -21,6 +21,7 @@ {{quoteprintable .Link}} -> Quote the specified field. {{encodeHeader .Subject}} -> Quote the specified field to be used in mail header. {{split "STRING:HERE" ":"}} -> Split a string into an array by deliminator + {{makeListIdHeader .Feed}} -> Generate a valid List-ID header from variable This comment will be stripped from the generated email. @@ -35,6 +36,7 @@ X-RSS-Feed: {{.Feed}} X-RSS-Tags: {{.Tag}} {{- end}} X-RSS-GUID: {{.RSSItem.GUID}} +List-ID: {{makeListIdHeader .Feed}} Content-Base: {{.Link}} Mime-Version: 1.0 diff --git a/template/template_test.go b/template/template_test.go index b469629..33216d0 100644 --- a/template/template_test.go +++ b/template/template_test.go @@ -8,7 +8,7 @@ func TestTemplate(t *testing.T) { // content and expected length content := EmailTemplate() - length := 2645 + length := 2766 if len(content) != length { t.Fatalf("unexpected template size %d != %d", length, len(content))