From ba943cb682c1efde252fa68ac7c871f5fa78b445 Mon Sep 17 00:00:00 2001 From: James Hillyerd Date: Sun, 3 Nov 2013 10:25:34 -0800 Subject: [PATCH] Initial version of ValidateLocalPart Does not support any quoting yet, not RFC compliant --- smtpd/utils.go | 38 ++++++++++++++++++++++++++++++++++++++ smtpd/utils_test.go | 35 ++++++++++++++++++++++++++++++++--- 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/smtpd/utils.go b/smtpd/utils.go index 0e4c2e3..133c1bb 100644 --- a/smtpd/utils.go +++ b/smtpd/utils.go @@ -1,6 +1,7 @@ package smtpd import ( + "bytes" "container/list" "crypto/sha1" "fmt" @@ -89,3 +90,40 @@ func ValidateDomainPart(domain string) bool { return true } + +// ValidateLocalPart returns true if the string complies with RFC3696 recommendations +func ValidateLocalPart(local string) bool { + length := len(local) + if 1 > length || length > 64 { + // Invalid length + return false + } + if local[length-1] == '.' { + // Cannot end with a period + return false + } + + prev := byte('.') + for i := 0; i < length; i++ { + c := local[i] + switch { + case ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'): + // Letters are OK + case '0' <= c && c <= '9': + // Numbers are OK + case bytes.IndexByte([]byte("!#$%&'*+-/=?^_`{|}~"), c) >= 0: + // These specials can be used unquoted + case c == '.': + // A single period is OK + if prev == '.' { + // Sequence of periods is not permitted + return false + } + default: + return false + } + prev = c + } + + return true +} diff --git a/smtpd/utils_test.go b/smtpd/utils_test.go index cc30d8f..47940e8 100644 --- a/smtpd/utils_test.go +++ b/smtpd/utils_test.go @@ -17,9 +17,6 @@ func TestHashMailboxName(t *testing.T) { } func TestValidateDomain(t *testing.T) { - assert.True(t, ValidateDomainPart("jhillyerd.github.com"), - "Simple domain failed") - assert.False(t, ValidateDomainPart(""), "Empty domain is not valid") assert.False(t, ValidateDomainPart(strings.Repeat("a", 256)), "Max domain length is 255") assert.False(t, ValidateDomainPart(strings.Repeat("a", 64)+".com"), @@ -32,6 +29,7 @@ func TestValidateDomain(t *testing.T) { expect bool msg string }{ + {"", false, "Empty domain is not valid"}, {"hostname", true, "Just a hostname is valid"}, {"github.com", true, "Two labels should be just fine"}, {"my-domain.com", true, "Hyphen is allowed mid-label"}, @@ -52,3 +50,34 @@ func TestValidateDomain(t *testing.T) { } } } + +func TestValidateLocal(t *testing.T) { + var testTable = []struct { + input string + expect bool + msg string + }{ + {"", false, "Empty local is not valid"}, + {"a", true, "Single letter should be fine"}, + {strings.Repeat("a", 65), false, "Only valid up to 64 characters"}, + {"FirstLast", true, "Mixed case permitted"}, + {"user123", true, "Numbers permitted"}, + {"a!#$%&'*+-/=?^_`{|}~", true, "Any of !#$%&'*+-/=?^_`{|}~ are permitted"}, + {"james@mail", false, "Unquoted @ not permitted"}, + {"first.last", true, "Embedded period is permitted"}, + {"first..last", false, "Sequence of periods is not allowed"}, + {".user", false, "Cannot lead with a period"}, + {"user.", false, "Cannot end with a period"}, + {"user+mailbox", true, "RFC3696 test case should be valid"}, + {"customer/department=shipping", true, "RFC3696 test case should be valid"}, + {"$A12345", true, "RFC3696 test case should be valid"}, + {"!def!xyz%abc", true, "RFC3696 test case should be valid"}, + {"_somename", true, "RFC3696 test case should be valid"}, + } + + for _, tt := range testTable { + if ValidateLocalPart(tt.input) != tt.expect { + t.Errorf("Expected %v for %q: %s", tt.expect, tt.input, tt.msg) + } + } +}