Skip to content

Commit 4ac6a29

Browse files
Source code for the first release of the OutSsytems asset.
1 parent 677aff4 commit 4ac6a29

11 files changed

Lines changed: 383 additions & 0 deletions

.gitignore

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
## =========================================================================
2+
## .NET / Visual Studio
3+
## =========================================================================
4+
5+
# Build artifacts
6+
[Bb]in/
7+
[Oo]bj/
8+
out/
9+
artifacts/
10+
publish/
11+
12+
# User-specific files
13+
*.user
14+
*.userosscache
15+
*.sln.docstates
16+
*.suo
17+
18+
# Visual Studio cache & temporary files
19+
.vs/
20+
*.opendb
21+
*.dbmdl
22+
*.VC.db
23+
*.VC.opendb
24+
25+
# MSBuild / Roslyn logs
26+
*.log
27+
*.binlog
28+
*.msvcclog
29+
30+
## =========================================================================
31+
## NuGet
32+
## =========================================================================
33+
34+
# Restore artifacts
35+
project.lock.json
36+
project.assets.json
37+
*.nuget.props
38+
*.nuget.targets
39+
40+
# Package cache
41+
packages/
42+
43+
## =========================================================================
44+
## Release Assets (Zips & Packages)
45+
## =========================================================================
46+
47+
# Specific requested patterns
48+
YAEmailValidator_Asset.zip
49+
YAEmailValidator_v*.zip
50+
51+
# General safety for all compressed files
52+
*.zip
53+
*.tar.gz
54+
*.7z
55+
*.nupkg
56+
57+
## =========================================================================
58+
## IDE / Editor Specific
59+
## =========================================================================
60+
61+
# VS Code
62+
.vscode/
63+
!.vscode/settings.json
64+
!.vscode/tasks.json
65+
!.vscode/launch.json
66+
!.vscode/extensions.json
67+
*.code-workspace
68+
69+
# JetBrains Rider
70+
.idea/
71+
*.sln.iml
72+
73+
# Windows / macOS system files
74+
.DS_Store
75+
Thumbs.db
76+
77+
## =========================================================================
78+
## Environment & Testing
79+
## =========================================================================
80+
81+
# Secrets and Local Config
82+
.env
83+
*.dev.local
84+
appsettings.Development.json
85+
86+
# Test Results & Coverage
87+
TestResults/
88+
*.sequenced
89+
*.coverage
90+
*.opencoverxml
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
8+
<IsPackable>false</IsPackable>
9+
10+
<LangVersion>10</LangVersion>
11+
</PropertyGroup>
12+
13+
<ItemGroup>
14+
<PackageReference Include="EmailValidation" Version="1.3.0" />
15+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
16+
<PackageReference Include="NUnit" Version="4.4.0" />
17+
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" />
18+
<PackageReference Include="NUnit.Analyzers" Version="4.11.2">
19+
<PrivateAssets>all</PrivateAssets>
20+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
21+
</PackageReference>
22+
<PackageReference Include="coverlet.collector" Version="6.0.4">
23+
<PrivateAssets>all</PrivateAssets>
24+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
25+
</PackageReference>
26+
</ItemGroup>
27+
28+
</Project>
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
using NUnit.Framework;
2+
using EmailValidation;
3+
4+
namespace OutSystems.YAEmailValidalitor.Tests
5+
{
6+
[TestFixture]
7+
public class EmailValidatorTests
8+
{
9+
// Tests for valid email addresses
10+
[TestCase("test@example.com")]
11+
[TestCase("firstname.lastname@domain.com")]
12+
[TestCase("email@subdomain.domain.com")]
13+
[TestCase("1234567890@domain.com")]
14+
[TestCase("email@domain-one.com")]
15+
[TestCase("_______@domain.com")]
16+
[TestCase("email@domain.name")]
17+
[TestCase("email@domain.co.jp")]
18+
public void Validate_ValidEmails_ReturnsTrue(string email)
19+
{
20+
bool result = EmailValidator.Validate(email);
21+
Assert.That(result, Is.True, $"Expected '{email}' to be valid.");
22+
}
23+
24+
// Tests for invalid email addresses
25+
[TestCase("plainaddress")] // No @ or domain
26+
[TestCase("#@%^%#$@#$@#.com")] // Garbage characters
27+
[TestCase("@domain.com")] // Missing username
28+
[TestCase("Joe Smith <email@domain.com>")] // Contains display name
29+
[TestCase("email.domain.com")] // Missing @
30+
[TestCase("email@domain@domain.com")] // Two @ symbols
31+
[TestCase(".email@domain.com")] // Leading dot
32+
[TestCase("email.@domain.com")] // Trailing dot in username
33+
[TestCase("email..email@domain.com")] // Double dots
34+
[TestCase("email@domain..com")] // Double dots in domain
35+
public void Validate_InvalidEmails_ReturnsFalse(string email)
36+
{
37+
bool result = EmailValidator.Validate(email);
38+
Assert.That(result, Is.False, $"Expected '{email}' to be invalid.");
39+
}
40+
41+
// Tests for null or whitespace
42+
[TestCase("")]
43+
[TestCase(" ")]
44+
public void Validate_EmptyOrNull_ReturnsFalse(string email)
45+
{
46+
bool result = EmailValidator.Validate(email);
47+
Assert.That(result, Is.False);
48+
}
49+
50+
// To truly validate that your library is adhering to RFC 5321, you should use these specific test cases.
51+
// These are designed to catch "loose" validators that fail to enforce the technical limits of the SMTP protocol.
52+
53+
// INTERNATIONALIZATION (RFC 6531)
54+
[TestCase("tést@domain.com")]
55+
[TestCase("用户@例子.广告")]
56+
public void Validate_InternationalEmails(string email)
57+
{
58+
// Jeffrey's lib supports this if the overload is called correctly
59+
Assert.That(EmailValidator.Validate(email, allowInternational: true), Is.True);
60+
}
61+
62+
// 1. THE "DISPLAY NAME" TRAP
63+
// MailAddress passes this; EmailValidation correctly fails it.
64+
[TestCase("Jeffrey Stedfast <jestedfa@microsoft.com>")]
65+
[TestCase("jestedfa@microsoft.com (Jeffrey Stedfast)")]
66+
public void Validate_ShouldRejectDisplayNamesAndComments(string email)
67+
{
68+
Assert.That(EmailValidator.Validate(email), Is.False,
69+
"RFC 5321 Address literals should not include display names or comments.");
70+
}
71+
72+
// 2. LOCAL PART LENGTH (Exactly 64 chars is allowed, 65 is not)
73+
[Test]
74+
public void Validate_LocalPartBoundary()
75+
{
76+
string sixtyFourChars = new string('a', 64);
77+
string sixtyFiveChars = new string('a', 65);
78+
79+
Assert.That(EmailValidator.Validate($"{sixtyFourChars}@domain.com"), Is.True, "64 chars should pass.");
80+
Assert.That(EmailValidator.Validate($"{sixtyFiveChars}@domain.com"), Is.False, "65 chars must fail.");
81+
}
82+
83+
// 3. TOTAL LENGTH (Maximum 254 characters)
84+
[Test]
85+
public void Validate_TotalLengthBoundary()
86+
{// A valid domain must have labels no longer than 63 characters
87+
string label63 = new string('b', 63);
88+
string domain = $"{label63}.{label63}.{label63}.com"; // 63*3 + 3 dots + 3 'com' = 195 chars
89+
90+
// 254 - 1 (@) - 195 (domain) = 58
91+
string local = new string('a', 58);
92+
93+
string valid254 = $"{local}@{domain}";
94+
95+
Assert.Multiple(() =>
96+
{
97+
Assert.That(valid254.Length, Is.EqualTo(254), "Manual check that string is 254");
98+
Assert.That(EmailValidator.Validate(valid254), Is.True, "254 chars with valid labels should pass.");
99+
100+
string invalid255 = "a" + valid254;
101+
Assert.That(EmailValidator.Validate(invalid255), Is.False, "255 chars must fail.");
102+
});
103+
}
104+
105+
// 4. THE "DOUBLE DOT" AND PURE SYNTAX
106+
[TestCase("user..name@domain.com")] // Consecutive dots
107+
[TestCase(".user@domain.com")] // Leading dot
108+
[TestCase("user.@domain.com")] // Trailing dot in local part
109+
public void Validate_ShouldRejectInvalidDotPlacement(string email)
110+
{
111+
Assert.That(EmailValidator.Validate(email), Is.False);
112+
}
113+
}
114+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using OutSystems.ExternalLibraries.SDK;
2+
3+
namespace OutSystems.YAEmailValidator
4+
{
5+
/// <summary>
6+
/// Validates the specified email address (using RFC 5321) with optional flags for trimming and international/TLD support. Uses the lib EmailValidation 1.3.0 (https://github.com/jstedfast/EmailValidation).
7+
/// </summary>
8+
[OSInterface(
9+
Description = "Validates the specified email address (using RFC 5321) with optional flags for trimming and international/TLD support. Uses the lib EmailValidation 1.3.0 (https://github.com/jstedfast/EmailValidation).",
10+
IconResourceName = "OutSystems.YAEmailValidator.resources.YAEmailValidator_icon.png"
11+
)]
12+
public interface IYAEmailValidator
13+
{
14+
/// <summary>
15+
/// Validates the specified email address with optional flags for trimming and international/TLD support.
16+
/// </summary>
17+
/// <param name="emailToValidate">The email to validate.</param>
18+
/// <param name="allowLeadingTrailingWhitespace">
19+
/// When true, leading/trailing whitespace will be ignored (the email is trimmed before validation).
20+
/// When false, the method returns false if the provided email contains any leading or trailing whitespace.
21+
/// </param>
22+
/// <param name="allowInternational">If true, non-ASCII international addresses are allowed.</param>
23+
/// <param name="allowTopLevelDomains">If true, top-level domains are allowed (prevents local-only addresses like "user@localhost").
24+
/// True when the email is valid; otherwise false.</returns>
25+
[OSAction(
26+
Description = "Validates the specified email address (using the RFC 5321) with optional flags for trimming and international/TLD support.",
27+
IconResourceName = "OutSystems.YAEmailValidator.resources.YAEmailValidator_icon.png"
28+
)]
29+
void EmailValidate(
30+
[OSParameterAttribute(Description = "The email to validate.")]
31+
string emailToValidate,
32+
[OSParameterAttribute(Description = "When true, leading/trailing whitespace will be ignored (the email is trimmed before validation). When false, the method returns false if the provided email contains any leading or trailing whitespace.")]
33+
bool allowLeadingTrailingWhitespace,
34+
[OSParameterAttribute(Description = "If true, non-ASCII international addresses are allowed.")]
35+
bool allowInternational,
36+
[OSParameterAttribute(Description = "If true, top-level domains are allowed (prevents local-only addresses like 'user@localhost').Returns True when the email is valid; otherwise false.")]
37+
bool allowTopLevelDomains,
38+
[OSParameterAttribute(Description = "True, if email is valid according to RFC 5321. Otherwise, false.")]
39+
out bool isValidEmail
40+
);
41+
}
42+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<EmbeddedResource Include="resources\YAEmailValidator_icon.png">
10+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
11+
</EmbeddedResource>
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<PackageReference Include="EmailValidation" Version="1.3.0" />
16+
<PackageReference Include="OutSystems.ExternalLibraries.SDK" Version="1.5.0" />
17+
</ItemGroup>
18+
19+
</Project>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.2.32526.322
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OutSystems.YAEmailValidator", "OutSystems.YAEmailValidator.csproj", "{A372E341-8741-417D-81B2-79685AB7EE1A}"
7+
EndProject
8+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OutSystems.YAEmailValidator.UnitTests", "..\OutSystems.YAEmailValidator.UnitTests\OutSystems.YAEmailValidator.UnitTests.csproj", "{B521FEF6-A0C0-6B69-A11C-61753F9DF5DE}"
9+
EndProject
10+
Global
11+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
12+
Debug|Any CPU = Debug|Any CPU
13+
Release|Any CPU = Release|Any CPU
14+
EndGlobalSection
15+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
16+
{A372E341-8741-417D-81B2-79685AB7EE1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17+
{A372E341-8741-417D-81B2-79685AB7EE1A}.Debug|Any CPU.Build.0 = Debug|Any CPU
18+
{A372E341-8741-417D-81B2-79685AB7EE1A}.Release|Any CPU.ActiveCfg = Release|Any CPU
19+
{A372E341-8741-417D-81B2-79685AB7EE1A}.Release|Any CPU.Build.0 = Release|Any CPU
20+
{B521FEF6-A0C0-6B69-A11C-61753F9DF5DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21+
{B521FEF6-A0C0-6B69-A11C-61753F9DF5DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
22+
{B521FEF6-A0C0-6B69-A11C-61753F9DF5DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
23+
{B521FEF6-A0C0-6B69-A11C-61753F9DF5DE}.Release|Any CPU.Build.0 = Release|Any CPU
24+
EndGlobalSection
25+
GlobalSection(SolutionProperties) = preSolution
26+
HideSolutionNode = FALSE
27+
EndGlobalSection
28+
GlobalSection(ExtensibilityGlobals) = postSolution
29+
SolutionGuid = {8F9CB76A-F832-4708-8E4B-4AEE89D4D38C}
30+
EndGlobalSection
31+
EndGlobal

OutSystems.YAEmailValidator/README.html

Whitespace-only changes.

OutSystems.YAEmailValidator/README.md

Whitespace-only changes.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using EmailValidation;
2+
using System;
3+
4+
namespace OutSystems.YAEmailValidator
5+
{
6+
/// <summary>
7+
/// Provides email validation utilities that wrap the EmailValidation library.
8+
/// </summary>
9+
public class YAEmailValidator : IYAEmailValidator
10+
{
11+
/// <summary>
12+
/// Validates the specified email address with optional flags for trimming and international/TLD support.
13+
/// </summary>
14+
/// <param name="emailToValidate">The email to validate.</param>
15+
/// <param name="allowLeadingTrailingWhitespace_optional">
16+
/// When true, leading/trailing whitespace will be ignored (the email is trimmed before validation).
17+
/// When false, the method returns false if the provided email contains any leading or trailing whitespace.
18+
/// </param>
19+
/// <param name="allowInternational_optional">If true, non-ASCII international addresses are allowed.</param>
20+
/// <param name="allowTopLevelDomains_optional">If true, top-level domains are allowed (prevents local-only addresses like "user@localhost").</param>
21+
/// <returns>True when the email is valid; otherwise false.</returns>
22+
public void EmailValidate(
23+
string emailToValidate,
24+
bool allowLeadingTrailingWhitespace,
25+
bool allowInternational,
26+
bool allowTopLevelDomains,
27+
out bool isValidEmail)
28+
{
29+
if (emailToValidate is null) throw new ArgumentNullException(nameof(emailToValidate));
30+
31+
// If trimming is not allowed and the provided email contains leading/trailing whitespace,
32+
// treat it as invalid (return false).
33+
if (!allowLeadingTrailingWhitespace)
34+
{
35+
if (!string.Equals(emailToValidate, emailToValidate.Trim(), StringComparison.Ordinal))
36+
{
37+
isValidEmail = false;
38+
return;
39+
}
40+
41+
// No trimming allowed, validate the exact provided value.
42+
isValidEmail = EmailValidator.Validate(emailToValidate,
43+
allowInternational: allowInternational,
44+
allowTopLevelDomains: allowTopLevelDomains);
45+
return;
46+
}
47+
48+
// Trimming allowed: remove leading/trailing whitespace and validate the trimmed value.
49+
var email = emailToValidate.Trim();
50+
isValidEmail = EmailValidator.Validate(email,
51+
allowInternational: allowInternational,
52+
allowTopLevelDomains: allowTopLevelDomains);
53+
return;
54+
}
55+
}
56+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Set-ExecutionPolicy -Scope CurrentUser Unrestricted
2+
dotnet publish -c Release -r linux-x64 --self-contained false
3+
Compress-Archive -Path .\bin\Release\net8.0\linux-x64\publish\* -DestinationPath YAEmailValidator_Asset.zip

0 commit comments

Comments
 (0)