public class EmailAddressAttribute : DataTypeAttribute
{
private const string DefaultErrorMessage = "The {0} field is not a valid e-mail address.";
+ const string AtomCharacters = "!#$%&'*+-/=?^_`{|}~";
+
+ static bool IsLetterOrDigit (char c)
+ {
+ return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9');
+ }
+
+ static bool IsAtom (char c)
+ {
+ return IsLetterOrDigit (c) || AtomCharacters.IndexOf (c) != -1;
+ }
+
+ static bool IsDomain (char c)
+ {
+ return IsLetterOrDigit (c) || c == '-';
+ }
+
+ static bool SkipAtom (string text, ref int index)
+ {
+ int startIndex = index;
+
+ while (index < text.Length && IsAtom (text[index]))
+ index++;
+
+ return index > startIndex;
+ }
+
+ static bool SkipSubDomain (string text, ref int index)
+ {
+ if (!IsDomain (text[index]) || text[index] == '-')
+ return false;
+
+ index++;
+
+ while (index < text.Length && IsDomain (text[index]))
+ index++;
+
+ return true;
+ }
+
+ static bool SkipDomain (string text, ref int index)
+ {
+ if (!SkipSubDomain (text, ref index))
+ return false;
+
+ while (index < text.Length && text[index] == '.') {
+ index++;
+
+ if (index == text.Length)
+ return false;
+
+ if (!SkipSubDomain (text, ref index))
+ return false;
+ }
+
+ return true;
+ }
+
+ static bool SkipQuoted (string text, ref int index)
+ {
+ bool escaped = false;
+
+ // skip over leading '"'
+ index++;
+
+ while (index < text.Length) {
+ if (text[index] == (byte) '\\') {
+ escaped = !escaped;
+ } else if (!escaped) {
+ if (text[index] == (byte) '"')
+ break;
+ } else {
+ escaped = false;
+ }
+
+ index++;
+ }
+
+ if (index >= text.Length || text[index] != (byte) '"')
+ return false;
+
+ index++;
+
+ return true;
+ }
+
+ static bool SkipWord (string text, ref int index)
+ {
+ if (text[index] == (byte) '"')
+ return SkipQuoted (text, ref index);
+
+ return SkipAtom (text, ref index);
+ }
+
+ static bool SkipIPv4Literal (string text, ref int index)
+ {
+ int groups = 0;
+
+ while (index < text.Length && groups < 4) {
+ int startIndex = index;
+ int value = 0;
+
+ while (index < text.Length && text[index] >= '0' && text[index] <= '9') {
+ value = (value * 10) + (text[index] - '0');
+ index++;
+ }
+
+ if (index == startIndex || index - startIndex > 3 || value > 255)
+ return false;
+
+ groups++;
+
+ if (groups < 4 && index < text.Length && text[index] == '.')
+ index++;
+ }
+
+ return groups == 4;
+ }
+
+ static bool IsHexDigit (char c)
+ {
+ return (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f') || (c >= '0' && c <= '9');
+ }
+
+ // This needs to handle the following forms:
+ //
+ // IPv6-addr = IPv6-full / IPv6-comp / IPv6v4-full / IPv6v4-comp
+ // IPv6-hex = 1*4HEXDIG
+ // IPv6-full = IPv6-hex 7(":" IPv6-hex)
+ // IPv6-comp = [IPv6-hex *5(":" IPv6-hex)] "::" [IPv6-hex *5(":" IPv6-hex)]
+ // ; The "::" represents at least 2 16-bit groups of zeros
+ // ; No more than 6 groups in addition to the "::" may be
+ // ; present
+ // IPv6v4-full = IPv6-hex 5(":" IPv6-hex) ":" IPv4-address-literal
+ // IPv6v4-comp = [IPv6-hex *3(":" IPv6-hex)] "::"
+ // [IPv6-hex *3(":" IPv6-hex) ":"] IPv4-address-literal
+ // ; The "::" represents at least 2 16-bit groups of zeros
+ // ; No more than 4 groups in addition to the "::" and
+ // ; IPv4-address-literal may be present
+ static bool SkipIPv6Literal (string text, ref int index)
+ {
+ bool compact = false;
+ int colons = 0;
+
+ while (index < text.Length) {
+ int startIndex = index;
+
+ while (index < text.Length && IsHexDigit (text[index]))
+ index++;
+
+ if (index >= text.Length)
+ break;
+
+ if (index > startIndex && colons > 2 && text[index] == '.') {
+ // IPv6v4
+ index = startIndex;
+
+ if (!SkipIPv4Literal (text, ref index))
+ return false;
+
+ break;
+ }
+
+ int count = index - startIndex;
+ if (count > 4)
+ return false;
+
+ if (text[index] != ':')
+ break;
+
+ startIndex = index;
+ while (index < text.Length && text[index] == ':')
+ index++;
+
+ count = index - startIndex;
+ if (count > 2)
+ return false;
+
+ if (count == 2) {
+ if (compact)
+ return false;
+
+ compact = true;
+ colons += 2;
+ } else {
+ colons++;
+ }
+ }
+
+ if (colons < 2)
+ return false;
+
+ if (compact)
+ return colons < 6;
+
+ return colons < 7;
+ }
+
+ static bool Validate (string email)
+ {
+ int index = 0;
+
+ if (email.Length == 0)
+ return false;
+
+ if (!SkipWord (email, ref index) || index >= email.Length)
+ return false;
+
+ while (index < email.Length && email[index] == '.') {
+ index++;
+
+ if (!SkipWord (email, ref index) || index >= email.Length)
+ return false;
+ }
+
+ if (index + 1 >= email.Length || email[index++] != '@')
+ return false;
+
+ if (email[index] != '[') {
+ // domain
+ if (!SkipDomain (email, ref index))
+ return false;
+
+ return index == email.Length;
+ }
+
+ // address literal
+ index++;
+
+ // we need at least 8 more characters
+ if (index + 8 >= email.Length)
+ return false;
+
+ var ipv6 = email.Substring (index, 5);
+ if (ipv6.ToLowerInvariant () == "ipv6:") {
+ index += "IPv6:".Length;
+ if (!SkipIPv6Literal (email, ref index))
+ return false;
+ } else {
+ if (!SkipIPv4Literal (email, ref index))
+ return false;
+ }
+
+ if (index >= email.Length || email[index++] != ']')
+ return false;
+
+ return index == email.Length;
+ }
public EmailAddressAttribute ()
: base(DataType.EmailAddress)
if (value == null)
return true;
- if (value is string)
- return true;
+ string email = value as string;
+ if (email == null)
+ return false;
- return false;
+ return Validate (email);
}
}
}
[TestFixture]
public class EmailAddressAttributeTest
{
+ static readonly object[] ValidAddresses = new object[] {
+ null,
+ "\"Abc\\@def\"@example.com",
+ "\"Fred Bloggs\"@example.com",
+ "\"Joe\\\\Blow\"@example.com",
+ "\"Abc@def\"@example.com",
+ "customer/department=shipping@example.com",
+ "$A12345@example.com",
+ "!def!xyz%abc@example.com",
+ "_somename@example.com",
+ "valid.ipv4.addr@[123.1.72.10]",
+ "valid.ipv6.addr@[IPv6:0::1]",
+ "valid.ipv6.addr@[IPv6:2607:f0d0:1002:51::4]",
+ "valid.ipv6.addr@[IPv6:fe80::230:48ff:fe33:bc33]",
+ "valid.ipv6v4.addr@[IPv6:aaaa:aaaa:aaaa:aaaa:aaaa:aaaa:127.0.0.1]",
+ };
+
+ static readonly object[] InvalidAddresses = new object[] {
+ "",
+ 123,
+ DateTime.Now,
+ "invalid",
+ "invalid@",
+ "invalid @",
+ "invalid@[555.666.777.888]",
+ "invalid@[IPv6:123456]",
+ "invalid@[127.0.0.1.]",
+ "invalid@[127.0.0.1].",
+ "invalid@[127.0.0.1]x",
+ };
+
[Test]
public void IsValid ()
{
var sla = new EmailAddressAttribute ();
- Assert.IsTrue (sla.IsValid (null), "#A1-1");
-#if false
- Assert.IsFalse (sla.IsValid (String.Empty), "#A1-2");
- Assert.IsFalse (sla.IsValid ("string"), "#A1-3");
-#endif
- Assert.IsTrue (sla.IsValid ("addr@mail.com"), "#A1-4");
- Assert.IsTrue (sla.IsValid ("addr@sub.mail.com"), "#A1-5");
- Assert.IsFalse (sla.IsValid (123), "#A1-6");
- Assert.IsFalse (sla.IsValid (DateTime.Now), "#A1-7");
+ for (int i = 0; i < ValidAddresses.Length; i++)
+ Assert.IsTrue (sla.IsValid (ValidAddresses[i]), "#A1-{0}", i);
+
+ for (int i = 0; i < InvalidAddresses.Length; i++)
+ Assert.IsFalse (sla.IsValid (InvalidAddresses[i]), "#B1-{0}", i);
}
}
#endif