diff --git a/smtpd/utils.go b/smtpd/utils.go index c06d11c..b4bd41f 100644 --- a/smtpd/utils.go +++ b/smtpd/utils.go @@ -123,9 +123,11 @@ LOOP: inCharQuote = false case '0' <= c && c <= '9': // Numbers are OK + buf.WriteByte(c) inCharQuote = false case bytes.IndexByte([]byte("!#$%&'*+-/=?^_`{|}~"), c) >= 0: // These specials can be used unquoted + buf.WriteByte(c) inCharQuote = false case c == '.': // A single period is OK @@ -133,10 +135,13 @@ LOOP: // Sequence of periods is not permitted return "", "", fmt.Errorf("Sequence of periods is not permitted") } + buf.WriteByte(c) + inCharQuote = false case c == '\\': inCharQuote = true case c == '"': if inCharQuote { + buf.WriteByte(c) inCharQuote = false } else if inStringQuote { inStringQuote = false @@ -149,6 +154,7 @@ LOOP: } case c == '@': if inCharQuote || inStringQuote { + buf.WriteByte(c) inCharQuote = false } else { // End of local-part @@ -165,6 +171,7 @@ LOOP: return "", "", fmt.Errorf("Characters outside of US-ASCII range not permitted") default: if inCharQuote || inStringQuote { + buf.WriteByte(c) inCharQuote = false } else { return "", "", fmt.Errorf("Character %q must be quoted", c) diff --git a/smtpd/utils_test.go b/smtpd/utils_test.go index 8812396..c38e776 100644 --- a/smtpd/utils_test.go +++ b/smtpd/utils_test.go @@ -69,6 +69,7 @@ func TestValidateLocal(t *testing.T) { {"user.", false, "Cannot end with a period"}, {"james@mail", false, "Unquoted @ not permitted"}, {"first last", false, "Unquoted space not permitted"}, + {"tricky\\. ", false, "Unquoted space not permitted"}, {"no,commas", false, "Unquoted comma not allowed"}, {"t[es]t", false, "Unquoted square brackets not allowed"}, {"james\\", false, "Cannot end with backslash quote"}, @@ -110,10 +111,26 @@ func TestValidateLocal(t *testing.T) { } func TestParseEmailAddress(t *testing.T) { + // Test some good email addresses var testTable = []struct { input, local, domain string }{ {"root@localhost", "root", "localhost"}, + {"FirstLast@domain.local", "FirstLast", "domain.local"}, + {"route66@prodigy.net", "route66", "prodigy.net"}, + {"lorbit!user@uucp", "lorbit!user", "uucp"}, + {"user+spam@gmail.com", "user+spam", "gmail.com"}, + {"first.last@domain.local", "first.last", "domain.local"}, + {"first\\ last@_key.domain.com", "first last", "_key.domain.com"}, + {"first\\\"last@a.b.c", "first\"last", "a.b.c"}, + {"user\\@internal@myhost.ca", "user@internal", "myhost.ca"}, + {"\"first last@evil\"@top-secret.gov", "first last@evil", "top-secret.gov"}, + {"\"line\nfeed\"@linenoise.co.uk", "line\nfeed", "linenoise.co.uk"}, + {"user+mailbox@host", "user+mailbox", "host"}, + {"customer/department=shipping@host", "customer/department=shipping", "host"}, + {"$A12345@host", "$A12345", "host"}, + {"!def!xyz%abc@host", "!def!xyz%abc", "host"}, + {"_somename@host", "_somename", "host"}, } for _, tt := range testTable { @@ -131,4 +148,27 @@ func TestParseEmailAddress(t *testing.T) { } } } + + // Check that validations fail correctly + var badTable = []struct { + input, msg string + }{ + {"", "Empty address not permitted"}, + {"user", "Missing domain part"}, + {"@host", "Missing local part"}, + {"user\\@host", "Missing domain part"}, + {"\"user@host\"", "Missing domain part"}, + {"\"user@host", "Unterminated quoted string"}, + {"first last@host", "Unquoted space"}, + {"user@bad!domain", "Invalid domain"}, + {".user@host", "Can't lead with a ."}, + {"user.@host", "Can't end local with a dot"}, + {"user@bad domain", "No spaces in domain permitted"}, + } + + for _, tt := range badTable { + if _, _, err := ParseEmailAddress(tt.input); err == nil { + t.Errorf("Did not get expected error when parsing %q: %s", tt.input, tt.msg) + } + } }