mirror of
https://github.com/Smaug123/dmarc-fsharp
synced 2025-10-05 15:38:42 +00:00
Initial commit
This commit is contained in:
18
.config/dotnet-tools.json
Normal file
18
.config/dotnet-tools.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"isRoot": true,
|
||||||
|
"tools": {
|
||||||
|
"fantomas": {
|
||||||
|
"version": "6.3.4",
|
||||||
|
"commands": [
|
||||||
|
"fantomas"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fsharp-analyzers": {
|
||||||
|
"version": "0.26.0",
|
||||||
|
"commands": [
|
||||||
|
"fsharp-analyzers"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
40
.editorconfig
Normal file
40
.editorconfig
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
root=true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset=utf-8
|
||||||
|
trim_trailing_whitespace=true
|
||||||
|
insert_final_newline=true
|
||||||
|
indent_style=space
|
||||||
|
indent_size=4
|
||||||
|
|
||||||
|
# ReSharper properties
|
||||||
|
resharper_xml_indent_size=2
|
||||||
|
resharper_xml_max_line_length=100
|
||||||
|
resharper_xml_tab_width=2
|
||||||
|
|
||||||
|
[*.{csproj,fsproj,sqlproj,targets,props,ts,tsx,css,json}]
|
||||||
|
indent_style=space
|
||||||
|
indent_size=2
|
||||||
|
|
||||||
|
[*.{fs,fsi}]
|
||||||
|
fsharp_bar_before_discriminated_union_declaration=true
|
||||||
|
fsharp_space_before_uppercase_invocation=true
|
||||||
|
fsharp_space_before_class_constructor=true
|
||||||
|
fsharp_space_before_member=true
|
||||||
|
fsharp_space_before_colon=true
|
||||||
|
fsharp_space_before_semicolon=true
|
||||||
|
fsharp_multiline_bracket_style=aligned
|
||||||
|
fsharp_newline_between_type_definition_and_members=true
|
||||||
|
fsharp_align_function_signature_to_indentation=true
|
||||||
|
fsharp_alternative_long_member_definitions=true
|
||||||
|
fsharp_multi_line_lambda_closing_newline=true
|
||||||
|
fsharp_experimental_keep_indent_in_branch=true
|
||||||
|
fsharp_max_value_binding_width=80
|
||||||
|
fsharp_max_record_width=0
|
||||||
|
max_line_length=120
|
||||||
|
end_of_line=lf
|
||||||
|
|
||||||
|
[*.{appxmanifest,build,dtd,nuspec,xaml,xamlx,xoml,xsd}]
|
||||||
|
indent_style=space
|
||||||
|
indent_size=2
|
||||||
|
tab_width=2
|
16
.github/dependabot.yml
vendored
Normal file
16
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# yaml-language-server: $schema=https://json.schemastore.org/dependabot-2.0.json
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
|
||||||
|
- package-ecosystem: "nuget"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
ignore:
|
||||||
|
# Target the lowest version of FSharp.Core, for max compat
|
||||||
|
- dependency-name: "FSharp.Core"
|
169
.github/workflows/dotnet.yaml
vendored
Normal file
169
.github/workflows/dotnet.yaml
vendored
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
# yaml-language-server: $schema=https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/github-workflow.json
|
||||||
|
name: .NET
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
DOTNET_NOLOGO: true
|
||||||
|
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||||
|
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
||||||
|
NUGET_XMLDOC_MODE: ''
|
||||||
|
DOTNET_MULTILEVEL_LOOKUP: 0
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
config:
|
||||||
|
- Release
|
||||||
|
- Debug
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # so that NerdBank.GitVersioning has access to history
|
||||||
|
- name: Install Nix
|
||||||
|
uses: cachix/install-nix-action@V27
|
||||||
|
with:
|
||||||
|
extra_nix_config: |
|
||||||
|
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Restore dependencies
|
||||||
|
run: nix develop --command dotnet restore
|
||||||
|
- name: Build
|
||||||
|
run: nix develop --command dotnet build --no-restore --configuration ${{matrix.config}}
|
||||||
|
- name: Test
|
||||||
|
run: nix develop --command dotnet test --no-build --verbosity normal --configuration ${{matrix.config}}
|
||||||
|
|
||||||
|
analyzers:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
security-events: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # so that NerdBank.GitVersioning has access to history
|
||||||
|
- name: Install Nix
|
||||||
|
uses: cachix/install-nix-action@V27
|
||||||
|
with:
|
||||||
|
extra_nix_config: |
|
||||||
|
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Prepare analyzers
|
||||||
|
run: nix develop --command dotnet restore analyzers/analyzers.fsproj
|
||||||
|
- name: Build project
|
||||||
|
run: nix develop --command dotnet build ./Dmarc/Dmarc.fsproj
|
||||||
|
- name: Run analyzers
|
||||||
|
run: nix run .#fsharp-analyzers -- --project ./Dmarc/Dmarc.fsproj --analyzers-path ./.analyzerpackages/g-research.fsharp.analyzers/*/ --verbosity detailed --report ./analysis.sarif --treat-as-error GRA-STRING-001 GRA-STRING-002 GRA-STRING-003 GRA-UNIONCASE-001 GRA-INTERPOLATED-001 GRA-TYPE-ANNOTATE-001 GRA-VIRTUALCALL-001 GRA-IMMUTABLECOLLECTIONEQUALITY-001 GRA-JSONOPTS-001 GRA-LOGARGFUNCFULLAPP-001 GRA-DISPBEFOREASYNC-001 --exclude-analyzers PartialAppAnalyzer
|
||||||
|
|
||||||
|
build-nix:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Install Nix
|
||||||
|
uses: cachix/install-nix-action@V27
|
||||||
|
with:
|
||||||
|
extra_nix_config: |
|
||||||
|
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Build
|
||||||
|
run: nix build
|
||||||
|
|
||||||
|
check-dotnet-format:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Install Nix
|
||||||
|
uses: cachix/install-nix-action@V27
|
||||||
|
with:
|
||||||
|
extra_nix_config: |
|
||||||
|
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Run Fantomas
|
||||||
|
run: nix run .#fantomas -- --check .
|
||||||
|
|
||||||
|
check-nix-format:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Install Nix
|
||||||
|
uses: cachix/install-nix-action@V27
|
||||||
|
with:
|
||||||
|
extra_nix_config: |
|
||||||
|
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Run Alejandra
|
||||||
|
run: nix develop --command alejandra --check .
|
||||||
|
|
||||||
|
linkcheck:
|
||||||
|
name: Check links
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@master
|
||||||
|
- name: Install Nix
|
||||||
|
uses: cachix/install-nix-action@V27
|
||||||
|
with:
|
||||||
|
extra_nix_config: |
|
||||||
|
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Run link checker
|
||||||
|
run: nix develop --command markdown-link-check README.md
|
||||||
|
|
||||||
|
flake-check:
|
||||||
|
name: Check flake
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@master
|
||||||
|
- name: Install Nix
|
||||||
|
uses: cachix/install-nix-action@V27
|
||||||
|
with:
|
||||||
|
extra_nix_config: |
|
||||||
|
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Flake check
|
||||||
|
run: nix flake check
|
||||||
|
|
||||||
|
nuget-pack:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # so that NerdBank.GitVersioning has access to history
|
||||||
|
- name: Install Nix
|
||||||
|
uses: cachix/install-nix-action@V27
|
||||||
|
with:
|
||||||
|
extra_nix_config: |
|
||||||
|
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Restore dependencies
|
||||||
|
run: nix develop --command dotnet restore
|
||||||
|
- name: Build
|
||||||
|
run: nix develop --command dotnet build --no-restore --configuration Release
|
||||||
|
- name: Pack
|
||||||
|
run: nix develop --command dotnet pack --configuration Release
|
||||||
|
- name: Upload NuGet artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: nuget-package-plugin
|
||||||
|
path: Dmarc/bin/Release/Dmarc.*.nupkg
|
||||||
|
|
||||||
|
expected-pack:
|
||||||
|
needs: [nuget-pack]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Download NuGet artifact (plugin)
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: nuget-package-plugin
|
||||||
|
path: packed-plugin
|
||||||
|
- name: Check NuGet contents
|
||||||
|
# Verify that there is exactly one nupkg in the artifact that would be NuGet published
|
||||||
|
run: if [[ $(find packed-plugin -maxdepth 1 -name 'Dmarc.*.nupkg' -printf c | wc -c) -ne "1" ]]; then exit 1; fi
|
||||||
|
|
||||||
|
all-required-checks-complete:
|
||||||
|
needs: [check-dotnet-format, check-nix-format, build, build-nix, linkcheck, flake-check, analyzers, nuget-pack, expected-pack]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: echo "All required checks complete."
|
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
/packages/
|
||||||
|
riderModule.iml
|
||||||
|
/_ReSharper.Caches/
|
||||||
|
.idea/
|
||||||
|
*.sln.DotSettings.user
|
||||||
|
.DS_Store
|
||||||
|
result
|
||||||
|
.analyzerpackages/
|
||||||
|
analysis.sarif
|
||||||
|
.direnv/
|
28
Directory.Build.props
Normal file
28
Directory.Build.props
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<DebugType Condition=" '$(DebugType)' == '' ">embedded</DebugType>
|
||||||
|
<Deterministic>true</Deterministic>
|
||||||
|
<NetCoreTargetingPackRoot>[UNDEFINED]</NetCoreTargetingPackRoot>
|
||||||
|
<DisableImplicitLibraryPacksFolder>true</DisableImplicitLibraryPacksFolder>
|
||||||
|
<DisableImplicitNuGetFallbackFolder>true</DisableImplicitNuGetFallbackFolder>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
<DebugType>embedded</DebugType>
|
||||||
|
<WarnOn>FS3388,FS3559</WarnOn>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Nerdbank.GitVersioning" Version="3.6.133" PrivateAssets="all"/>
|
||||||
|
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All"/>
|
||||||
|
<SourceLinkGitHubHost Include="github.com" ContentUrl="https://raw.githubusercontent.com"/>
|
||||||
|
</ItemGroup>
|
||||||
|
<!--
|
||||||
|
SourceLink doesn't support F# deterministic builds out of the box,
|
||||||
|
so tell SourceLink that our source root is going to be remapped.
|
||||||
|
-->
|
||||||
|
<Target Name="MapSourceRoot" BeforeTargets="_GenerateSourceLinkFile" Condition="'$(SourceRootMappedPathsFeatureSupported)' != 'true'">
|
||||||
|
<ItemGroup>
|
||||||
|
<SourceRoot Update="@(SourceRoot)">
|
||||||
|
<MappedPath>Z:\CheckoutRoot\WoofWare.Myriad\</MappedPath>
|
||||||
|
</SourceRoot>
|
||||||
|
</ItemGroup>
|
||||||
|
</Target>
|
||||||
|
</Project>
|
16
Dmarc.App/Dmarc.App.fsproj
Normal file
16
Dmarc.App/Dmarc.App.fsproj
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="Program.fs"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Dmarc\Dmarc.fsproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
20
Dmarc.App/Program.fs
Normal file
20
Dmarc.App/Program.fs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
namespace Dmarc.App
|
||||||
|
|
||||||
|
open System.IO
|
||||||
|
open System.Xml
|
||||||
|
open Dmarc
|
||||||
|
|
||||||
|
module Program =
|
||||||
|
[<EntryPoint>]
|
||||||
|
let main argv =
|
||||||
|
let file =
|
||||||
|
match argv with
|
||||||
|
| [| file |] -> file
|
||||||
|
| _ -> failwith "Call with exactly one arg, the XML file to parse"
|
||||||
|
|
||||||
|
use s = File.OpenRead file
|
||||||
|
let doc = XmlDocument ()
|
||||||
|
doc.Load s
|
||||||
|
|
||||||
|
let feedback = Feedback.ofXml doc.["feedback"]
|
||||||
|
0
|
27
Dmarc.Test/Dmarc.Test.fsproj
Normal file
27
Dmarc.Test/Dmarc.Test.fsproj
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="EmbeddedResource.fs" />
|
||||||
|
<Compile Include="TestParse.fs" />
|
||||||
|
<EmbeddedResource Include="example.xml" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="FsUnit" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0"/>
|
||||||
|
<PackageReference Include="NUnit" Version="4.1.0"/>
|
||||||
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Dmarc\Dmarc.fsproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
17
Dmarc.Test/EmbeddedResource.fs
Normal file
17
Dmarc.Test/EmbeddedResource.fs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
namespace Dmarc.Test
|
||||||
|
|
||||||
|
open System.IO
|
||||||
|
open System.Reflection
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module EmbeddedResource =
|
||||||
|
let read (fileName : string) : Stream =
|
||||||
|
let assy = Assembly.GetExecutingAssembly ()
|
||||||
|
let fileName = $"%s{assy.GetName().Name}.%s{fileName}"
|
||||||
|
let s = assy.GetManifestResourceStream fileName
|
||||||
|
|
||||||
|
if isNull s then
|
||||||
|
let names = assy.GetManifestResourceNames () |> String.concat "\n"
|
||||||
|
failwith $"Could not find resource %s{fileName}. Available:\n%s{names}"
|
||||||
|
|
||||||
|
s
|
287
Dmarc.Test/TestParse.fs
Normal file
287
Dmarc.Test/TestParse.fs
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
namespace Dmarc.Test
|
||||||
|
|
||||||
|
open System
|
||||||
|
open System.Net
|
||||||
|
open Dmarc
|
||||||
|
open NUnit.Framework
|
||||||
|
open System.Xml
|
||||||
|
open FsUnitTyped
|
||||||
|
|
||||||
|
[<TestFixture>]
|
||||||
|
module TestParse =
|
||||||
|
let expectedDateRange =
|
||||||
|
{
|
||||||
|
Begin = DateTimeOffset (2024, 05, 26, 00, 00, 00, TimeSpan.Zero)
|
||||||
|
End = DateTimeOffset (2024, 05, 26, 23, 59, 59, TimeSpan.Zero)
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Can parse DateRange`` () =
|
||||||
|
use example = EmbeddedResource.read "example.xml"
|
||||||
|
let doc = XmlDocument ()
|
||||||
|
doc.Load example
|
||||||
|
|
||||||
|
let dateRangeNode =
|
||||||
|
doc.["feedback"].FirstChild.ChildNodes
|
||||||
|
|> Seq.cast<XmlNode>
|
||||||
|
|> Seq.filter (fun i -> i.Name = "date_range")
|
||||||
|
|> Seq.exactlyOne
|
||||||
|
|
||||||
|
let actual =
|
||||||
|
if isNull dateRangeNode then
|
||||||
|
failwith "no version found"
|
||||||
|
else
|
||||||
|
DateRange.ofXml dateRangeNode
|
||||||
|
|
||||||
|
actual |> shouldEqual expectedDateRange
|
||||||
|
|
||||||
|
let expectedReportMetadata =
|
||||||
|
{
|
||||||
|
OrgName = Some "google.com"
|
||||||
|
Email = "noreply-dmarc-support@google.com"
|
||||||
|
ExtraContactInfo = Uri "https://support.google.com/a/answer/2466580"
|
||||||
|
ReportId = "12345678901234567890"
|
||||||
|
DateRange = expectedDateRange
|
||||||
|
Error = []
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Can parse ReportMetadata`` () =
|
||||||
|
use example = EmbeddedResource.read "example.xml"
|
||||||
|
let doc = XmlDocument ()
|
||||||
|
doc.Load example
|
||||||
|
|
||||||
|
let reportMetadataNode = doc.["feedback"].FirstChild
|
||||||
|
|
||||||
|
let actual =
|
||||||
|
if isNull reportMetadataNode then
|
||||||
|
failwith "no report metadata node found"
|
||||||
|
else
|
||||||
|
reportMetadataNode.Name |> shouldEqual "report_metadata"
|
||||||
|
ReportMetadata.ofXml reportMetadataNode
|
||||||
|
|
||||||
|
actual |> shouldEqual expectedReportMetadata
|
||||||
|
|
||||||
|
let expectedPolicyPublished =
|
||||||
|
{
|
||||||
|
Domain = "example.com"
|
||||||
|
DkimAlignment = Some Alignment.Relaxed
|
||||||
|
SpfAlignment = Some Alignment.Relaxed
|
||||||
|
Policy = Disposition.None
|
||||||
|
SubdomainPolicy = Disposition.None
|
||||||
|
Percentage = 100
|
||||||
|
FailureOptions = None
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Can parse PolicyPublished`` () =
|
||||||
|
use example = EmbeddedResource.read "example.xml"
|
||||||
|
let doc = XmlDocument ()
|
||||||
|
doc.Load example
|
||||||
|
|
||||||
|
let policyPublishedNode = doc.["feedback"].ChildNodes.[1]
|
||||||
|
|
||||||
|
let actual =
|
||||||
|
if isNull policyPublishedNode then
|
||||||
|
failwith "no policy published node found"
|
||||||
|
else
|
||||||
|
policyPublishedNode.Name |> shouldEqual "policy_published"
|
||||||
|
PolicyPublished.ofXml policyPublishedNode
|
||||||
|
|
||||||
|
actual |> shouldEqual expectedPolicyPublished
|
||||||
|
|
||||||
|
let expectedPolicyEvaluated : PolicyEvaluated =
|
||||||
|
{
|
||||||
|
Disposition = Disposition.None
|
||||||
|
Dkim = DmarcResult.Pass
|
||||||
|
Spf = DmarcResult.Pass
|
||||||
|
Reason = []
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Can parse PolicyEvaluated`` () =
|
||||||
|
use example = EmbeddedResource.read "example.xml"
|
||||||
|
let doc = XmlDocument ()
|
||||||
|
doc.Load example
|
||||||
|
|
||||||
|
let policyEvaluatedNode = doc.["feedback"].ChildNodes.[2].FirstChild.LastChild
|
||||||
|
|
||||||
|
let actual =
|
||||||
|
if isNull policyEvaluatedNode then
|
||||||
|
failwith "no policy evaluated node found"
|
||||||
|
else
|
||||||
|
policyEvaluatedNode.Name |> shouldEqual "policy_evaluated"
|
||||||
|
PolicyEvaluated.ofXml policyEvaluatedNode
|
||||||
|
|
||||||
|
actual |> shouldEqual expectedPolicyEvaluated
|
||||||
|
|
||||||
|
let expectedRow : Row =
|
||||||
|
{
|
||||||
|
SourceIp = IPAddress.Parse "192.168.0.1"
|
||||||
|
Count = 1
|
||||||
|
Policy = expectedPolicyEvaluated
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Can parse Row`` () =
|
||||||
|
use example = EmbeddedResource.read "example.xml"
|
||||||
|
let doc = XmlDocument ()
|
||||||
|
doc.Load example
|
||||||
|
|
||||||
|
let rowNode = doc.["feedback"].ChildNodes.[2].FirstChild
|
||||||
|
|
||||||
|
let actual =
|
||||||
|
if isNull rowNode then
|
||||||
|
failwith "no row node found"
|
||||||
|
else
|
||||||
|
rowNode.Name |> shouldEqual "row"
|
||||||
|
Row.ofXml rowNode
|
||||||
|
|
||||||
|
actual |> shouldEqual expectedRow
|
||||||
|
|
||||||
|
let expectedIdentifier =
|
||||||
|
{
|
||||||
|
EnvelopeTo = None
|
||||||
|
EnvelopeFrom = None
|
||||||
|
HeaderFrom = "example.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Can parse Identifiers`` () =
|
||||||
|
use example = EmbeddedResource.read "example.xml"
|
||||||
|
let doc = XmlDocument ()
|
||||||
|
doc.Load example
|
||||||
|
|
||||||
|
let idNode = doc.["feedback"].ChildNodes.[2].ChildNodes.[1]
|
||||||
|
|
||||||
|
let actual =
|
||||||
|
if isNull idNode then
|
||||||
|
failwith "no identifiers node found"
|
||||||
|
else
|
||||||
|
idNode.Name |> shouldEqual "identifiers"
|
||||||
|
Identifier.ofXml idNode
|
||||||
|
|
||||||
|
actual |> shouldEqual expectedIdentifier
|
||||||
|
|
||||||
|
let expectedDkim : DkimAuthResult =
|
||||||
|
{
|
||||||
|
Domain = "example.com"
|
||||||
|
Result = DkimResult.Pass
|
||||||
|
Selector = Some "mySelector"
|
||||||
|
HumanResult = None
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Can parse DKIM`` () =
|
||||||
|
use example = EmbeddedResource.read "example.xml"
|
||||||
|
let doc = XmlDocument ()
|
||||||
|
doc.Load example
|
||||||
|
|
||||||
|
let node = doc.["feedback"].ChildNodes.[2].LastChild.FirstChild
|
||||||
|
|
||||||
|
let actual =
|
||||||
|
if isNull node then
|
||||||
|
failwith "no dkim node found"
|
||||||
|
else
|
||||||
|
node.Name |> shouldEqual "dkim"
|
||||||
|
DkimAuthResult.ofXml node
|
||||||
|
|
||||||
|
actual |> shouldEqual expectedDkim
|
||||||
|
|
||||||
|
let expectedSpf : SpfAuthResult =
|
||||||
|
{
|
||||||
|
Domain = "example.com"
|
||||||
|
Scope = None
|
||||||
|
Result = SpfResult.Pass
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Can parse SPF`` () =
|
||||||
|
use example = EmbeddedResource.read "example.xml"
|
||||||
|
let doc = XmlDocument ()
|
||||||
|
doc.Load example
|
||||||
|
|
||||||
|
let node = doc.["feedback"].ChildNodes.[2].LastChild.LastChild
|
||||||
|
|
||||||
|
let actual =
|
||||||
|
if isNull node then
|
||||||
|
failwith "no spf node found"
|
||||||
|
else
|
||||||
|
node.Name |> shouldEqual "spf"
|
||||||
|
SpfAuthResult.ofXml node
|
||||||
|
|
||||||
|
actual |> shouldEqual expectedSpf
|
||||||
|
|
||||||
|
let expectedAuthResults =
|
||||||
|
{
|
||||||
|
Dkim = [ expectedDkim ]
|
||||||
|
SpfHead = expectedSpf
|
||||||
|
SpfTail = []
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Can parse auth results`` () =
|
||||||
|
use example = EmbeddedResource.read "example.xml"
|
||||||
|
let doc = XmlDocument ()
|
||||||
|
doc.Load example
|
||||||
|
|
||||||
|
let node = doc.["feedback"].LastChild.LastChild
|
||||||
|
|
||||||
|
let actual =
|
||||||
|
if isNull node then
|
||||||
|
failwith "no spf node found"
|
||||||
|
else
|
||||||
|
node.Name |> shouldEqual "auth_results"
|
||||||
|
AuthResult.ofXml node
|
||||||
|
|
||||||
|
actual |> shouldEqual expectedAuthResults
|
||||||
|
|
||||||
|
let expectedRecord : Record =
|
||||||
|
{
|
||||||
|
Row = expectedRow
|
||||||
|
Identifiers = expectedIdentifier
|
||||||
|
AuthResults = expectedAuthResults
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Can parse record`` () =
|
||||||
|
use example = EmbeddedResource.read "example.xml"
|
||||||
|
let doc = XmlDocument ()
|
||||||
|
doc.Load example
|
||||||
|
|
||||||
|
let node = doc.["feedback"].LastChild
|
||||||
|
|
||||||
|
let actual =
|
||||||
|
if isNull node then
|
||||||
|
failwith "no spf node found"
|
||||||
|
else
|
||||||
|
node.Name |> shouldEqual "record"
|
||||||
|
Record.ofXml node
|
||||||
|
|
||||||
|
actual |> shouldEqual expectedRecord
|
||||||
|
|
||||||
|
let expectedFeedback =
|
||||||
|
{
|
||||||
|
Version = None
|
||||||
|
ReportMetadata = expectedReportMetadata
|
||||||
|
PolicyPublished = expectedPolicyPublished
|
||||||
|
Records = [ expectedRecord ]
|
||||||
|
}
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Can parse feedback`` () =
|
||||||
|
use example = EmbeddedResource.read "example.xml"
|
||||||
|
let doc = XmlDocument ()
|
||||||
|
doc.Load example
|
||||||
|
|
||||||
|
let node = doc.["feedback"]
|
||||||
|
|
||||||
|
let actual =
|
||||||
|
if isNull node then
|
||||||
|
failwith "no feedback node found"
|
||||||
|
else
|
||||||
|
node.Name |> shouldEqual "feedback"
|
||||||
|
Feedback.ofXml node
|
||||||
|
|
||||||
|
actual |> shouldEqual expectedFeedback
|
47
Dmarc.Test/example.xml
Normal file
47
Dmarc.Test/example.xml
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<feedback>
|
||||||
|
<report_metadata>
|
||||||
|
<org_name>google.com</org_name>
|
||||||
|
<email>noreply-dmarc-support@google.com</email>
|
||||||
|
<extra_contact_info>https://support.google.com/a/answer/2466580</extra_contact_info>
|
||||||
|
<report_id>12345678901234567890</report_id>
|
||||||
|
<date_range>
|
||||||
|
<begin>1716681600</begin>
|
||||||
|
<end>1716767999</end>
|
||||||
|
</date_range>
|
||||||
|
</report_metadata>
|
||||||
|
<policy_published>
|
||||||
|
<domain>example.com</domain>
|
||||||
|
<adkim>r</adkim>
|
||||||
|
<aspf>r</aspf>
|
||||||
|
<p>none</p>
|
||||||
|
<sp>none</sp>
|
||||||
|
<pct>100</pct>
|
||||||
|
<np>none</np>
|
||||||
|
</policy_published>
|
||||||
|
<record>
|
||||||
|
<row>
|
||||||
|
<source_ip>192.168.0.1</source_ip>
|
||||||
|
<count>1</count>
|
||||||
|
<policy_evaluated>
|
||||||
|
<disposition>none</disposition>
|
||||||
|
<dkim>pass</dkim>
|
||||||
|
<spf>pass</spf>
|
||||||
|
</policy_evaluated>
|
||||||
|
</row>
|
||||||
|
<identifiers>
|
||||||
|
<header_from>example.com</header_from>
|
||||||
|
</identifiers>
|
||||||
|
<auth_results>
|
||||||
|
<dkim>
|
||||||
|
<domain>example.com</domain>
|
||||||
|
<result>pass</result>
|
||||||
|
<selector>mySelector</selector>
|
||||||
|
</dkim>
|
||||||
|
<spf>
|
||||||
|
<domain>example.com</domain>
|
||||||
|
<result>pass</result>
|
||||||
|
</spf>
|
||||||
|
</auth_results>
|
||||||
|
</record>
|
||||||
|
</feedback>
|
28
Dmarc.sln
Normal file
28
Dmarc.sln
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Dmarc", "Dmarc\Dmarc.fsproj", "{35773DF5-99B3-4624-A57D-A076E8E7DCC9}"
|
||||||
|
EndProject
|
||||||
|
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Dmarc.App", "Dmarc.App\Dmarc.App.fsproj", "{CDBC4619-B56F-4956-8BC5-1B64D5EC5662}"
|
||||||
|
EndProject
|
||||||
|
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Dmarc.Test", "Dmarc.Test\Dmarc.Test.fsproj", "{46C1EDBE-1E50-4946-BE8A-E5D2C6C45EE3}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{35773DF5-99B3-4624-A57D-A076E8E7DCC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{35773DF5-99B3-4624-A57D-A076E8E7DCC9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{35773DF5-99B3-4624-A57D-A076E8E7DCC9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{35773DF5-99B3-4624-A57D-A076E8E7DCC9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{CDBC4619-B56F-4956-8BC5-1B64D5EC5662}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{CDBC4619-B56F-4956-8BC5-1B64D5EC5662}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{CDBC4619-B56F-4956-8BC5-1B64D5EC5662}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{CDBC4619-B56F-4956-8BC5-1B64D5EC5662}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{46C1EDBE-1E50-4946-BE8A-E5D2C6C45EE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{46C1EDBE-1E50-4946-BE8A-E5D2C6C45EE3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{46C1EDBE-1E50-4946-BE8A-E5D2C6C45EE3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{46C1EDBE-1E50-4946-BE8A-E5D2C6C45EE3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
13
Dmarc/Dmarc.fsproj
Normal file
13
Dmarc/Dmarc.fsproj
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="XmlPatterns.fs" />
|
||||||
|
<Compile Include="Domain.fs" />
|
||||||
|
<PackageReference Update="FSharp.Core" Version="6.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
684
Dmarc/Domain.fs
Normal file
684
Dmarc/Domain.fs
Normal file
@@ -0,0 +1,684 @@
|
|||||||
|
namespace Dmarc
|
||||||
|
|
||||||
|
open System
|
||||||
|
open System.Net
|
||||||
|
open System.Xml
|
||||||
|
|
||||||
|
type DateRange =
|
||||||
|
{
|
||||||
|
Begin : DateTimeOffset
|
||||||
|
End : DateTimeOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
static member ofXml (node : XmlNode) : DateRange =
|
||||||
|
if node.ChildNodes.Count <> 2 then
|
||||||
|
failwith $"expected exactly two nodes in DateRange, got %i{node.ChildNodes.Count}: %s{node.InnerXml}"
|
||||||
|
|
||||||
|
let beginContents, endContents =
|
||||||
|
match node.FirstChild, node.LastChild with
|
||||||
|
| OneChildNode "begin" (NoChildrenNode (Int64 beginNode)),
|
||||||
|
OneChildNode "end" (NoChildrenNode (Int64 endNode))
|
||||||
|
| OneChildNode "end" (NoChildrenNode (Int64 endNode)),
|
||||||
|
OneChildNode "begin" (NoChildrenNode (Int64 beginNode)) -> beginNode, endNode
|
||||||
|
| c1, c2 -> failwith $"Expected a begin and an end node in DateRange, got %s{c1.Name} and %s{c2.Name}"
|
||||||
|
|
||||||
|
{
|
||||||
|
Begin = DateTimeOffset.FromUnixTimeSeconds beginContents
|
||||||
|
End = DateTimeOffset.FromUnixTimeSeconds endContents
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReportMetadata =
|
||||||
|
{
|
||||||
|
OrgName : string option
|
||||||
|
Email : string
|
||||||
|
ExtraContactInfo : Uri
|
||||||
|
ReportId : string
|
||||||
|
DateRange : DateRange
|
||||||
|
Error : string list
|
||||||
|
}
|
||||||
|
|
||||||
|
static member ofXml (node : XmlNode) : ReportMetadata =
|
||||||
|
if not node.HasChildNodes then
|
||||||
|
failwith "expected report_metadata node to have children, but it did not"
|
||||||
|
|
||||||
|
let mutable orgName = None
|
||||||
|
let mutable email = None
|
||||||
|
let mutable extraContactInfo = None
|
||||||
|
let mutable reportId = None
|
||||||
|
let mutable dateRange = None
|
||||||
|
let mutable errors = ResizeArray ()
|
||||||
|
|
||||||
|
for i in node.ChildNodes do
|
||||||
|
match i with
|
||||||
|
| OneChildNode "org_name" (NoChildrenNode v) ->
|
||||||
|
match orgName with
|
||||||
|
| None -> orgName <- Some v
|
||||||
|
| Some v2 -> failwith $"Duplicate values for org_name, %s{v2} and %s{v}"
|
||||||
|
| OneChildNode "email" (NoChildrenNode v) ->
|
||||||
|
match email with
|
||||||
|
| None -> email <- Some v
|
||||||
|
| Some v2 -> failwith $"Duplicate values for email, %s{v2} and %s{v}"
|
||||||
|
| OneChildNode "report_id" (NoChildrenNode v) ->
|
||||||
|
match reportId with
|
||||||
|
| None -> reportId <- Some v
|
||||||
|
| Some v2 -> failwith $"Duplicate values for reportId, %s{v2} and %s{v}"
|
||||||
|
| OneChildNode "extra_contact_info" (NoChildrenNode v) ->
|
||||||
|
match extraContactInfo with
|
||||||
|
| None -> extraContactInfo <- Some (Uri v)
|
||||||
|
| Some v2 -> failwith $"Duplicate values for extra_contact_info, %O{v2} and %s{v}"
|
||||||
|
| NodeWithChildren "date_range" ->
|
||||||
|
match dateRange with
|
||||||
|
| None -> dateRange <- Some (DateRange.ofXml i)
|
||||||
|
| Some v2 -> failwith $"Duplicate values for date_range, %O{v2} and %s{i.InnerText}"
|
||||||
|
| OneChildNode "error" (NoChildrenNode v) -> errors.Add v
|
||||||
|
| _ -> failwith $"Unrecognised node %s{i.Name}: %s{i.InnerText}"
|
||||||
|
|
||||||
|
let email =
|
||||||
|
email |> Option.defaultWith (fun () -> failwith "expected email, got none")
|
||||||
|
|
||||||
|
let reportId =
|
||||||
|
reportId
|
||||||
|
|> Option.defaultWith (fun () -> failwith "expected report_id, got none")
|
||||||
|
|
||||||
|
let extraContactInfo =
|
||||||
|
extraContactInfo
|
||||||
|
|> Option.defaultWith (fun () -> failwith "expected extra_contact_info, got none")
|
||||||
|
|
||||||
|
let dateRange =
|
||||||
|
dateRange
|
||||||
|
|> Option.defaultWith (fun () -> failwith "expected date_range, got none")
|
||||||
|
|
||||||
|
{
|
||||||
|
Error = errors |> Seq.toList
|
||||||
|
OrgName = orgName
|
||||||
|
Email = email
|
||||||
|
ExtraContactInfo = extraContactInfo
|
||||||
|
ReportId = reportId
|
||||||
|
DateRange = dateRange
|
||||||
|
}
|
||||||
|
|
||||||
|
type Alignment =
|
||||||
|
| Relaxed
|
||||||
|
| Strict
|
||||||
|
|
||||||
|
static member ofString (s : string) : Alignment =
|
||||||
|
match s with
|
||||||
|
| "r" -> Alignment.Relaxed
|
||||||
|
| "s" -> Alignment.Strict
|
||||||
|
| _ -> failwith $"Didn't recognise alignment %s{s}"
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
type Disposition =
|
||||||
|
| None
|
||||||
|
| Quarantine
|
||||||
|
| Reject
|
||||||
|
|
||||||
|
static member ofString (s : string) : Disposition =
|
||||||
|
match s with
|
||||||
|
| "none" -> Disposition.None
|
||||||
|
| "quarantine" -> Disposition.Quarantine
|
||||||
|
| "reject" -> Disposition.Reject
|
||||||
|
| _ -> failwith $"Didn't recognise disposition %s{s}"
|
||||||
|
|
||||||
|
type PolicyPublished =
|
||||||
|
{
|
||||||
|
Domain : string
|
||||||
|
DkimAlignment : Alignment option
|
||||||
|
SpfAlignment : Alignment option
|
||||||
|
Policy : Disposition
|
||||||
|
SubdomainPolicy : Disposition
|
||||||
|
Percentage : int
|
||||||
|
/// Mandated by RFC-7489 but absent from Google's response.
|
||||||
|
FailureOptions : string option
|
||||||
|
}
|
||||||
|
|
||||||
|
static member ofXml (node : XmlNode) : PolicyPublished =
|
||||||
|
if not node.HasChildNodes then
|
||||||
|
failwith "expected policy_published node to have children, but it did not"
|
||||||
|
|
||||||
|
let mutable domain = None
|
||||||
|
let mutable dkimAlignment = None
|
||||||
|
let mutable spfAlignment = None
|
||||||
|
let mutable policy = None
|
||||||
|
let mutable subdomainPolicy = None
|
||||||
|
let mutable percentage = None
|
||||||
|
let mutable failureOptions = None
|
||||||
|
|
||||||
|
for i in node.ChildNodes do
|
||||||
|
match i with
|
||||||
|
| OneChildNode "domain" (NoChildrenNode v) ->
|
||||||
|
match domain with
|
||||||
|
| None -> domain <- Some v
|
||||||
|
| Some v2 -> failwith $"domain appeared twice, values %s{v2} and %s{v}"
|
||||||
|
| OneChildNode "adkim" (NoChildrenNode v) ->
|
||||||
|
match dkimAlignment with
|
||||||
|
| None -> dkimAlignment <- Some (Alignment.ofString v)
|
||||||
|
| Some v2 -> failwith $"dkimAlignment appeared twice, values %O{v2} and %s{v}"
|
||||||
|
| OneChildNode "aspf" (NoChildrenNode v) ->
|
||||||
|
match spfAlignment with
|
||||||
|
| None -> spfAlignment <- Some (Alignment.ofString v)
|
||||||
|
| Some v2 -> failwith $"spfAlignment appeared twice, values %O{v2} and %s{v}"
|
||||||
|
| OneChildNode "p" (NoChildrenNode v) ->
|
||||||
|
match policy with
|
||||||
|
| None -> policy <- Some (Disposition.ofString v)
|
||||||
|
| Some v2 -> failwith $"policy appeared twice, values %O{v2} and %s{v}"
|
||||||
|
| OneChildNode "sp" (NoChildrenNode v) ->
|
||||||
|
match subdomainPolicy with
|
||||||
|
| None -> subdomainPolicy <- Some (Disposition.ofString v)
|
||||||
|
| Some v2 -> failwith $"subdomain policy appeared twice, values %O{v2} and %s{v}"
|
||||||
|
| OneChildNode "pct" (NoChildrenNode (Int v)) ->
|
||||||
|
match percentage with
|
||||||
|
| None -> percentage <- Some v
|
||||||
|
| Some v2 -> failwith $"percentage appeared twice, values %i{v2} and %i{v}"
|
||||||
|
| OneChildNode "fo" (NoChildrenNode v) ->
|
||||||
|
match failureOptions with
|
||||||
|
| None -> failureOptions <- Some v
|
||||||
|
| Some v2 -> failwith $"failure options appeared twice, values %s{v2} and %s{v}"
|
||||||
|
| OneChildNode "np" (NoChildrenNode _) ->
|
||||||
|
// RFC-7489 doesn't mention this but Google returns it
|
||||||
|
()
|
||||||
|
| _ -> failwith $"Unrecognised node: %s{i.Name}, %s{i.InnerText}"
|
||||||
|
|
||||||
|
let domain =
|
||||||
|
domain |> Option.defaultWith (fun () -> failwith "expected domain, got none")
|
||||||
|
|
||||||
|
let policy =
|
||||||
|
policy |> Option.defaultWith (fun () -> failwith "expected policy, got none")
|
||||||
|
|
||||||
|
let subdomainPolicy =
|
||||||
|
subdomainPolicy
|
||||||
|
|> Option.defaultWith (fun () -> failwith "expected subdomainPolicy, got none")
|
||||||
|
|
||||||
|
let percentage =
|
||||||
|
percentage
|
||||||
|
|> Option.defaultWith (fun () -> failwith "expected percentage, got none")
|
||||||
|
|
||||||
|
{
|
||||||
|
Domain = domain
|
||||||
|
DkimAlignment = dkimAlignment
|
||||||
|
SpfAlignment = spfAlignment
|
||||||
|
Policy = policy
|
||||||
|
SubdomainPolicy = subdomainPolicy
|
||||||
|
Percentage = percentage
|
||||||
|
FailureOptions = failureOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
type DmarcResult =
|
||||||
|
| Pass
|
||||||
|
| Fail
|
||||||
|
|
||||||
|
static member ofString (s : string) : DmarcResult =
|
||||||
|
match s with
|
||||||
|
| "pass" -> DmarcResult.Pass
|
||||||
|
| "fail" -> DmarcResult.Fail
|
||||||
|
| _ -> failwith $"Unrecognised DMARC result: %s{s}"
|
||||||
|
|
||||||
|
type PolicyOverride =
|
||||||
|
| Forwarded
|
||||||
|
| SampledOut
|
||||||
|
| TrustedForwarder
|
||||||
|
| MailingList
|
||||||
|
| LocalPolicy
|
||||||
|
| Other
|
||||||
|
|
||||||
|
static member ofString (s : string) : PolicyOverride =
|
||||||
|
match s with
|
||||||
|
| "forwarded" -> PolicyOverride.Forwarded
|
||||||
|
| "sampled_out" -> PolicyOverride.SampledOut
|
||||||
|
| "trusted_forwarder" -> PolicyOverride.TrustedForwarder
|
||||||
|
| "mailing_list" -> PolicyOverride.MailingList
|
||||||
|
| "local_policy" -> PolicyOverride.LocalPolicy
|
||||||
|
| "other" -> PolicyOverride.Other
|
||||||
|
| _ -> failwith $"unrecognised policy override: %s{s}"
|
||||||
|
|
||||||
|
type PolicyOverrideReason =
|
||||||
|
{
|
||||||
|
Type : PolicyOverride
|
||||||
|
Comment : string option
|
||||||
|
}
|
||||||
|
|
||||||
|
static member ofXml (node : XmlNode) : PolicyOverrideReason =
|
||||||
|
if not node.HasChildNodes then
|
||||||
|
failwith "expected policy override reason node to have children, but it did not"
|
||||||
|
|
||||||
|
let mutable ty = None
|
||||||
|
let mutable comment = None
|
||||||
|
|
||||||
|
for i in node.ChildNodes do
|
||||||
|
match i with
|
||||||
|
| OneChildNode "type" (NoChildrenNode v) ->
|
||||||
|
match ty with
|
||||||
|
| None -> ty <- Some (PolicyOverride.ofString v)
|
||||||
|
| Some v2 -> failwith $"type appeared twice, values %O{v2} and %s{v}"
|
||||||
|
| OneChildNode "comment" (NoChildrenNode v) ->
|
||||||
|
match comment with
|
||||||
|
| None -> comment <- Some v
|
||||||
|
| Some v2 -> failwith $"comment appeared twice, values %s{v2} and %s{v}"
|
||||||
|
| _ -> failwith $"unrecognised node: %s{i.Name}, %s{i.InnerText}"
|
||||||
|
|
||||||
|
let ty =
|
||||||
|
ty
|
||||||
|
|> Option.defaultWith (fun () -> failwith "expected policy override, got none")
|
||||||
|
|
||||||
|
{
|
||||||
|
Type = ty
|
||||||
|
Comment = comment
|
||||||
|
}
|
||||||
|
|
||||||
|
type PolicyEvaluated =
|
||||||
|
{
|
||||||
|
Disposition : Disposition
|
||||||
|
Dkim : DmarcResult
|
||||||
|
Spf : DmarcResult
|
||||||
|
Reason : PolicyOverrideReason list
|
||||||
|
}
|
||||||
|
|
||||||
|
static member ofXml (node : XmlNode) : PolicyEvaluated =
|
||||||
|
if not node.HasChildNodes then
|
||||||
|
failwith "expected policy evaluation node to have children, but it did not"
|
||||||
|
|
||||||
|
let mutable disp = None
|
||||||
|
let mutable dkim = None
|
||||||
|
let mutable spf = None
|
||||||
|
let reason = ResizeArray ()
|
||||||
|
|
||||||
|
for i in node.ChildNodes do
|
||||||
|
match i with
|
||||||
|
| OneChildNode "disposition" (NoChildrenNode v) ->
|
||||||
|
match disp with
|
||||||
|
| None -> disp <- Some (Disposition.ofString v)
|
||||||
|
| Some v2 -> failwith $"disposition appeared twice, values %O{v2} and %s{v}"
|
||||||
|
| OneChildNode "dkim" (NoChildrenNode v) ->
|
||||||
|
match dkim with
|
||||||
|
| None -> dkim <- Some (DmarcResult.ofString v)
|
||||||
|
| Some v2 -> failwith $"dkim appeared twice, values %O{v2} and %s{v}"
|
||||||
|
| OneChildNode "spf" (NoChildrenNode v) ->
|
||||||
|
match spf with
|
||||||
|
| None -> spf <- Some (DmarcResult.ofString v)
|
||||||
|
| Some v2 -> failwith $"spf appeared twice, values %O{v2} and %s{v}"
|
||||||
|
| OneChildNode "reason" v -> reason.Add (PolicyOverrideReason.ofXml v)
|
||||||
|
| _ -> failwith $"unrecognised node: %s{i.Name}, %s{i.InnerText}"
|
||||||
|
|
||||||
|
let spf = spf |> Option.defaultWith (fun () -> failwith "expected spf, got none")
|
||||||
|
let dkim = dkim |> Option.defaultWith (fun () -> failwith "expected dkim, got none")
|
||||||
|
|
||||||
|
let disp =
|
||||||
|
disp |> Option.defaultWith (fun () -> failwith "expected disposition, got none")
|
||||||
|
|
||||||
|
{
|
||||||
|
Disposition = disp
|
||||||
|
Dkim = dkim
|
||||||
|
Spf = spf
|
||||||
|
Reason = reason |> Seq.toList
|
||||||
|
}
|
||||||
|
|
||||||
|
type Row =
|
||||||
|
{
|
||||||
|
SourceIp : IPAddress
|
||||||
|
Count : int
|
||||||
|
Policy : PolicyEvaluated
|
||||||
|
}
|
||||||
|
|
||||||
|
static member ofXml (node : XmlNode) : Row =
|
||||||
|
if not node.HasChildNodes then
|
||||||
|
failwith "expected policy evaluation node to have children, but it did not"
|
||||||
|
|
||||||
|
let mutable source = None
|
||||||
|
let mutable count = None
|
||||||
|
let mutable policy = None
|
||||||
|
|
||||||
|
for i in node.ChildNodes do
|
||||||
|
match i with
|
||||||
|
| OneChildNode "source_ip" (NoChildrenNode v) ->
|
||||||
|
match source with
|
||||||
|
| None -> source <- Some (IPAddress.Parse v)
|
||||||
|
| Some v2 -> failwith $"source appeared twice, values %O{v2} and %s{v}"
|
||||||
|
| OneChildNode "count" (NoChildrenNode (Int v)) ->
|
||||||
|
match count with
|
||||||
|
| None -> count <- Some v
|
||||||
|
| Some v2 -> failwith $"count appeared twice, values %i{v2} and %i{v}"
|
||||||
|
| NodeWithChildren "policy_evaluated" ->
|
||||||
|
match policy with
|
||||||
|
| None -> policy <- Some (PolicyEvaluated.ofXml i)
|
||||||
|
| Some v2 -> failwith $"policy_evaluated appeared twice, values %O{v2} and %s{i.InnerText}"
|
||||||
|
| _ -> failwith $"unrecognised node: %s{i.Name}, %s{i.InnerText}"
|
||||||
|
|
||||||
|
let source =
|
||||||
|
source |> Option.defaultWith (fun () -> failwith "expected source, got none")
|
||||||
|
|
||||||
|
let count =
|
||||||
|
count |> Option.defaultWith (fun () -> failwith "expected count, got none")
|
||||||
|
|
||||||
|
let policy =
|
||||||
|
policy
|
||||||
|
|> Option.defaultWith (fun () -> failwith "expected policy_evaluated, got none")
|
||||||
|
|
||||||
|
{
|
||||||
|
SourceIp = source
|
||||||
|
Count = count
|
||||||
|
Policy = policy
|
||||||
|
}
|
||||||
|
|
||||||
|
type Identifier =
|
||||||
|
{
|
||||||
|
EnvelopeTo : string option
|
||||||
|
/// According to the RFC, this is required, but Google doesn't return it
|
||||||
|
EnvelopeFrom : string option
|
||||||
|
HeaderFrom : string
|
||||||
|
}
|
||||||
|
|
||||||
|
static member ofXml (node : XmlNode) : Identifier =
|
||||||
|
if not node.HasChildNodes then
|
||||||
|
failwith "expected identifiers node to have children, but it did not"
|
||||||
|
|
||||||
|
let mutable envelopeTo = None
|
||||||
|
let mutable envelopeFrom = None
|
||||||
|
let mutable headerFrom = None
|
||||||
|
|
||||||
|
for i in node.ChildNodes do
|
||||||
|
match i with
|
||||||
|
| OneChildNode "header_from" (NoChildrenNode v) ->
|
||||||
|
match headerFrom with
|
||||||
|
| None -> headerFrom <- Some v
|
||||||
|
| Some v2 -> failwith $"header_from appeared twice, values %O{v2} and %s{v}"
|
||||||
|
| OneChildNode "envelope_to" (NoChildrenNode v) ->
|
||||||
|
match envelopeTo with
|
||||||
|
| None -> envelopeTo <- Some v
|
||||||
|
| Some v2 -> failwith $"envelope_to appeared twice, values %s{v2} and %s{v}"
|
||||||
|
| OneChildNode "envelope_from" (NoChildrenNode v) ->
|
||||||
|
match envelopeFrom with
|
||||||
|
| None -> envelopeFrom <- Some v
|
||||||
|
| Some v2 -> failwith $"envelope_from appeared twice, values %O{v2} and %s{v}"
|
||||||
|
| _ -> failwith $"unrecognised node: %s{i.Name}, %s{i.InnerText}"
|
||||||
|
|
||||||
|
let headerFrom =
|
||||||
|
headerFrom
|
||||||
|
|> Option.defaultWith (fun () -> failwith "expected header_from, got none")
|
||||||
|
|
||||||
|
{
|
||||||
|
HeaderFrom = headerFrom
|
||||||
|
EnvelopeFrom = envelopeFrom
|
||||||
|
EnvelopeTo = envelopeTo
|
||||||
|
}
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
type DkimResult =
|
||||||
|
| None
|
||||||
|
| Pass
|
||||||
|
| Fail
|
||||||
|
| Policy
|
||||||
|
| Neutral
|
||||||
|
| TempError
|
||||||
|
| PermError
|
||||||
|
|
||||||
|
static member ofString (s : string) : DkimResult =
|
||||||
|
match s with
|
||||||
|
| "none" -> DkimResult.None
|
||||||
|
| "pass" -> DkimResult.Pass
|
||||||
|
| "fail" -> DkimResult.Fail
|
||||||
|
| "policy" -> DkimResult.Policy
|
||||||
|
| "neutral" -> DkimResult.Neutral
|
||||||
|
| "temperror" -> DkimResult.TempError
|
||||||
|
| "permerror" -> DkimResult.PermError
|
||||||
|
| _ -> failwith $"Unrecognised DKIM result: %s{s}"
|
||||||
|
|
||||||
|
type DkimAuthResult =
|
||||||
|
{
|
||||||
|
Domain : string
|
||||||
|
Selector : string option
|
||||||
|
Result : DkimResult
|
||||||
|
HumanResult : string option
|
||||||
|
}
|
||||||
|
|
||||||
|
static member ofXml (node : XmlNode) : DkimAuthResult =
|
||||||
|
if not node.HasChildNodes then
|
||||||
|
failwith "expected dkim auth result node to have children, but it did not"
|
||||||
|
|
||||||
|
let mutable domain = None
|
||||||
|
let mutable selector = None
|
||||||
|
let mutable result = None
|
||||||
|
let mutable humanResult = None
|
||||||
|
|
||||||
|
for i in node.ChildNodes do
|
||||||
|
match i with
|
||||||
|
| OneChildNode "domain" (NoChildrenNode v) ->
|
||||||
|
match domain with
|
||||||
|
| None -> domain <- Some v
|
||||||
|
| Some v2 -> failwith $"domain appeared twice, values %O{v2} and %s{v}"
|
||||||
|
| OneChildNode "selector" (NoChildrenNode v) ->
|
||||||
|
match selector with
|
||||||
|
| None -> selector <- Some v
|
||||||
|
| Some v2 -> failwith $"selctor appeared twice, values %s{v2} and %s{v}"
|
||||||
|
| OneChildNode "result" (NoChildrenNode v) ->
|
||||||
|
match result with
|
||||||
|
| None -> result <- Some (DkimResult.ofString v)
|
||||||
|
| Some v2 -> failwith $"result appeared twice, values %O{v2} and %s{v}"
|
||||||
|
| OneChildNode "human_result" (NoChildrenNode v) ->
|
||||||
|
match humanResult with
|
||||||
|
| None -> humanResult <- Some v
|
||||||
|
| Some v2 -> failwith $"human_result appeared twice, values %s{v2} and %s{v}"
|
||||||
|
| _ -> failwith $"unrecognised node: %s{i.Name}, %s{i.InnerText}"
|
||||||
|
|
||||||
|
let domain =
|
||||||
|
domain |> Option.defaultWith (fun () -> failwith "expected domain, got none")
|
||||||
|
|
||||||
|
let result =
|
||||||
|
result |> Option.defaultWith (fun () -> failwith "expected result, got none")
|
||||||
|
|
||||||
|
{
|
||||||
|
Domain = domain
|
||||||
|
Selector = selector
|
||||||
|
Result = result
|
||||||
|
HumanResult = humanResult
|
||||||
|
}
|
||||||
|
|
||||||
|
type SpfDomainScope =
|
||||||
|
| Helo
|
||||||
|
| Mfrom
|
||||||
|
|
||||||
|
static member ofString (s : string) : SpfDomainScope =
|
||||||
|
match s with
|
||||||
|
| "helo" -> SpfDomainScope.Helo
|
||||||
|
| "mfrom" -> SpfDomainScope.Mfrom
|
||||||
|
| _ -> failwith $"Unrecognised SPF domain scope: %s{s}"
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
type SpfResult =
|
||||||
|
| None
|
||||||
|
| Neutral
|
||||||
|
| Pass
|
||||||
|
| Fail
|
||||||
|
| SoftFail
|
||||||
|
| TempError
|
||||||
|
| PermError
|
||||||
|
|
||||||
|
static member ofString (s : string) : SpfResult =
|
||||||
|
match s with
|
||||||
|
| "none" -> SpfResult.None
|
||||||
|
| "neutral" -> SpfResult.Neutral
|
||||||
|
| "pass" -> SpfResult.Pass
|
||||||
|
| "fail" -> SpfResult.Fail
|
||||||
|
| "softfail" -> SpfResult.SoftFail
|
||||||
|
| "unknown"
|
||||||
|
| "temperror" -> SpfResult.TempError
|
||||||
|
| "error"
|
||||||
|
| "permerror" -> SpfResult.PermError
|
||||||
|
| _ -> failwith $"Unrecognised SPF result: %s{s}"
|
||||||
|
|
||||||
|
type SpfAuthResult =
|
||||||
|
{
|
||||||
|
Domain : string
|
||||||
|
/// Mandatory according to the RFC, but not supplied by Google
|
||||||
|
Scope : SpfDomainScope option
|
||||||
|
Result : SpfResult
|
||||||
|
}
|
||||||
|
|
||||||
|
static member ofXml (node : XmlNode) : SpfAuthResult =
|
||||||
|
if not node.HasChildNodes then
|
||||||
|
failwith "expected spf auth result to have children, but it did not"
|
||||||
|
|
||||||
|
let mutable domain = None
|
||||||
|
let mutable scope = None
|
||||||
|
let mutable result = None
|
||||||
|
|
||||||
|
for i in node.ChildNodes do
|
||||||
|
match i with
|
||||||
|
| OneChildNode "domain" (NoChildrenNode v) ->
|
||||||
|
match domain with
|
||||||
|
| None -> domain <- Some v
|
||||||
|
| Some v2 -> failwith $"domain appeared twice, values %s{v2} and %s{v}"
|
||||||
|
| OneChildNode "result" (NoChildrenNode v) ->
|
||||||
|
match result with
|
||||||
|
| None -> result <- Some (SpfResult.ofString v)
|
||||||
|
| Some v2 -> failwith $"result appeared twice, values %O{v2} and %s{v}"
|
||||||
|
| OneChildNode "scope" (NoChildrenNode v) ->
|
||||||
|
match scope with
|
||||||
|
| None -> scope <- Some (SpfDomainScope.ofString v)
|
||||||
|
| Some v2 -> failwith $"human_result appeared twice, values %O{v2} and %s{v}"
|
||||||
|
| _ -> failwith $"unrecognised node: %s{i.Name}, %s{i.InnerText}"
|
||||||
|
|
||||||
|
let domain =
|
||||||
|
domain |> Option.defaultWith (fun () -> failwith "expected domain, got none")
|
||||||
|
|
||||||
|
let result =
|
||||||
|
result |> Option.defaultWith (fun () -> failwith "expected result, got none")
|
||||||
|
|
||||||
|
{
|
||||||
|
Domain = domain
|
||||||
|
Scope = scope
|
||||||
|
Result = result
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthResult =
|
||||||
|
{
|
||||||
|
Dkim : DkimAuthResult list
|
||||||
|
SpfHead : SpfAuthResult
|
||||||
|
SpfTail : SpfAuthResult list
|
||||||
|
}
|
||||||
|
|
||||||
|
static member ofXml (node : XmlNode) : AuthResult =
|
||||||
|
if not node.HasChildNodes then
|
||||||
|
failwith "expected auth result to have children, but it did not"
|
||||||
|
|
||||||
|
let dkim = ResizeArray ()
|
||||||
|
let mutable spfHead = None
|
||||||
|
let spfTail = ResizeArray ()
|
||||||
|
|
||||||
|
for i in node.ChildNodes do
|
||||||
|
match i with
|
||||||
|
| NodeWithChildren "dkim" -> dkim.Add (DkimAuthResult.ofXml i)
|
||||||
|
| NodeWithChildren "spf" ->
|
||||||
|
let v = SpfAuthResult.ofXml i
|
||||||
|
|
||||||
|
match spfHead with
|
||||||
|
| None -> spfHead <- Some v
|
||||||
|
| Some _ -> spfTail.Add v
|
||||||
|
| _ -> failwith $"unrecognised node: %s{i.Name}, %s{i.InnerText}"
|
||||||
|
|
||||||
|
let spfHead =
|
||||||
|
spfHead
|
||||||
|
|> Option.defaultWith (fun () -> failwith "expected spf to have at least one element, got none")
|
||||||
|
|
||||||
|
{
|
||||||
|
Dkim = dkim |> Seq.toList
|
||||||
|
SpfHead = spfHead
|
||||||
|
SpfTail = spfTail |> Seq.toList
|
||||||
|
}
|
||||||
|
|
||||||
|
type Record =
|
||||||
|
{
|
||||||
|
Row : Row
|
||||||
|
Identifiers : Identifier
|
||||||
|
AuthResults : AuthResult
|
||||||
|
}
|
||||||
|
|
||||||
|
static member ofXml (node : XmlNode) : Record =
|
||||||
|
if not node.HasChildNodes then
|
||||||
|
failwith "expected record result to have children, but it did not"
|
||||||
|
|
||||||
|
let mutable row = None
|
||||||
|
let mutable identifiers = None
|
||||||
|
let mutable authResult = None
|
||||||
|
|
||||||
|
for i in node.ChildNodes do
|
||||||
|
match i with
|
||||||
|
| NodeWithChildren "auth_results" ->
|
||||||
|
match authResult with
|
||||||
|
| None -> authResult <- Some (AuthResult.ofXml i)
|
||||||
|
| Some v2 -> failwith $"auth_results appeared twice, values %O{v2} and %s{i.InnerText}"
|
||||||
|
| NodeWithChildren "row" ->
|
||||||
|
match row with
|
||||||
|
| None -> row <- Some (Row.ofXml i)
|
||||||
|
| Some v2 -> failwith $"row appeared twice, values %O{v2} and %s{i.InnerText}"
|
||||||
|
| NodeWithChildren "identifiers" ->
|
||||||
|
match identifiers with
|
||||||
|
| None -> identifiers <- Some (Identifier.ofXml i)
|
||||||
|
| Some v2 -> failwith $"identifiers appeared twice, values %O{v2} and %s{i.InnerText}"
|
||||||
|
| _ -> failwith $"unrecognised node: %s{i.Name}, %s{i.InnerText}"
|
||||||
|
|
||||||
|
let row = row |> Option.defaultWith (fun () -> failwith "expected row, got none")
|
||||||
|
|
||||||
|
let identifiers =
|
||||||
|
identifiers
|
||||||
|
|> Option.defaultWith (fun () -> failwith "expected identifiers, got none")
|
||||||
|
|
||||||
|
let authResult =
|
||||||
|
authResult
|
||||||
|
|> Option.defaultWith (fun () -> failwith "expected auth_results, got none")
|
||||||
|
|
||||||
|
{
|
||||||
|
Row = row
|
||||||
|
Identifiers = identifiers
|
||||||
|
AuthResults = authResult
|
||||||
|
}
|
||||||
|
|
||||||
|
type Feedback =
|
||||||
|
{
|
||||||
|
/// strictly speaking a decimal; also mandatory according to the RFC but not
|
||||||
|
/// supplied by Google
|
||||||
|
Version : string option
|
||||||
|
ReportMetadata : ReportMetadata
|
||||||
|
PolicyPublished : PolicyPublished
|
||||||
|
Records : Record list
|
||||||
|
}
|
||||||
|
|
||||||
|
static member ofXml (node : XmlNode) : Feedback =
|
||||||
|
if not node.HasChildNodes then
|
||||||
|
failwith "expected record result to have children, but it did not"
|
||||||
|
|
||||||
|
let mutable version = None
|
||||||
|
let mutable reportMetadata = None
|
||||||
|
let mutable policyPublished = None
|
||||||
|
let records = ResizeArray ()
|
||||||
|
|
||||||
|
for i in node.ChildNodes do
|
||||||
|
match i with
|
||||||
|
| NodeWithChildren "record" -> records.Add (Record.ofXml i)
|
||||||
|
| NodeWithChildren "policy_published" ->
|
||||||
|
match policyPublished with
|
||||||
|
| None -> policyPublished <- Some (PolicyPublished.ofXml i)
|
||||||
|
| Some v2 -> failwith $"policy_published appeared twice, values %O{v2} and %s{i.InnerText}"
|
||||||
|
| NodeWithChildren "report_metadata" ->
|
||||||
|
match reportMetadata with
|
||||||
|
| None -> reportMetadata <- Some (ReportMetadata.ofXml i)
|
||||||
|
| Some v2 -> failwith $"report_metadata appeared twice, values %O{v2} and %s{i.InnerText}"
|
||||||
|
| OneChildNode "version" (NoChildrenNode v) ->
|
||||||
|
match version with
|
||||||
|
| None -> version <- Some v
|
||||||
|
| Some v2 -> failwith $"version appeared twice, values %O{v2} and %s{v}"
|
||||||
|
| _ -> failwith $"unrecognised node: %s{i.Name}, %s{i.InnerText}"
|
||||||
|
|
||||||
|
let policyPublished =
|
||||||
|
policyPublished
|
||||||
|
|> Option.defaultWith (fun () -> failwith "expected policy_published, got none")
|
||||||
|
|
||||||
|
let reportMetadata =
|
||||||
|
reportMetadata
|
||||||
|
|> Option.defaultWith (fun () -> failwith "expected report_metadata, got none")
|
||||||
|
|
||||||
|
{
|
||||||
|
Records = records |> Seq.toList
|
||||||
|
PolicyPublished = policyPublished
|
||||||
|
ReportMetadata = reportMetadata
|
||||||
|
Version = version
|
||||||
|
}
|
34
Dmarc/XmlPatterns.fs
Normal file
34
Dmarc/XmlPatterns.fs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
namespace Dmarc
|
||||||
|
|
||||||
|
open System.Xml
|
||||||
|
|
||||||
|
[<AutoOpen>]
|
||||||
|
module internal XmlPatterns =
|
||||||
|
|
||||||
|
[<return : Struct>]
|
||||||
|
let (|NodeWithChildren|_|) (expectedName : string) (node : XmlNode) : unit voption =
|
||||||
|
if node.Name = expectedName && node.HasChildNodes then
|
||||||
|
ValueSome ()
|
||||||
|
else
|
||||||
|
ValueNone
|
||||||
|
|
||||||
|
let (|OneChildNode|_|) (expectedName : string) (node : XmlNode) : XmlNode option =
|
||||||
|
if node.Name = expectedName && node.HasChildNodes && node.ChildNodes.Count = 1 then
|
||||||
|
Some (node.FirstChild)
|
||||||
|
else
|
||||||
|
None
|
||||||
|
|
||||||
|
let (|NoChildrenNode|_|) (node : XmlNode) : string option =
|
||||||
|
if node.HasChildNodes then None else Some node.Value
|
||||||
|
|
||||||
|
[<return : Struct>]
|
||||||
|
let (|Int64|_|) (s : string) : int64 voption =
|
||||||
|
match System.Int64.TryParse s with
|
||||||
|
| false, _ -> ValueNone
|
||||||
|
| true, v -> ValueSome v
|
||||||
|
|
||||||
|
[<return : Struct>]
|
||||||
|
let (|Int|_|) (s : string) : int voption =
|
||||||
|
match System.Int32.TryParse s with
|
||||||
|
| false, _ -> ValueNone
|
||||||
|
| true, v -> ValueSome v
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Patrick Stevens
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# DMARC parser for F#
|
||||||
|
|
||||||
|
Based on the [RFC](https://datatracker.ietf.org/doc/html/rfc7489#appendix-C), with concessions to match what Google actually reported to me in its own reports.
|
16
analyzers/analyzers.fsproj
Normal file
16
analyzers/analyzers.fsproj
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<Project Sdk="Microsoft.Build.NoTargets/1.0.80"> <!-- This is not a project we want to build. -->
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsPublishable>false</IsPublishable>
|
||||||
|
<RestorePackagesPath>../.analyzerpackages/</RestorePackagesPath>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<DisableImplicitNuGetFallbackFolder>true</DisableImplicitNuGetFallbackFolder>
|
||||||
|
<AutomaticallyUseReferenceAssemblyPackages>false</AutomaticallyUseReferenceAssemblyPackages> <!-- We don't want to build this project, so we do not need the reference assemblies for the framework we chose.-->
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageDownload Include="G-Research.FSharp.Analyzers" Version="[0.10.0]" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1710146030,
|
||||||
|
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1717112898,
|
||||||
|
"narHash": "sha256-7R2ZvOnvd9h8fDd65p0JnB7wXfUvreox3xFdYWd1BnY=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "6132b0f6e344ce2fe34fc051b72fb46e34f668e0",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
67
flake.nix
Normal file
67
flake.nix
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
description = "DKIM/DMARC parsing";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = {
|
||||||
|
nixpkgs,
|
||||||
|
flake-utils,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
flake-utils.lib.eachDefaultSystem (system: let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
pname = "Dmarc-dotnet";
|
||||||
|
dotnet-sdk = pkgs.dotnet-sdk_8;
|
||||||
|
dotnet-runtime = pkgs.dotnetCorePackages.runtime_8_0;
|
||||||
|
version = "0.1";
|
||||||
|
dotnetTool = dllOverride: toolName: toolVersion: sha256:
|
||||||
|
pkgs.stdenvNoCC.mkDerivation rec {
|
||||||
|
name = toolName;
|
||||||
|
version = toolVersion;
|
||||||
|
nativeBuildInputs = [pkgs.makeWrapper];
|
||||||
|
src = pkgs.fetchNuGet {
|
||||||
|
pname = name;
|
||||||
|
version = version;
|
||||||
|
sha256 = sha256;
|
||||||
|
installPhase = ''mkdir -p $out/bin && cp -r tools/net6.0/any/* $out/bin'';
|
||||||
|
};
|
||||||
|
installPhase = let
|
||||||
|
dll =
|
||||||
|
if isNull dllOverride
|
||||||
|
then name
|
||||||
|
else dllOverride;
|
||||||
|
in ''
|
||||||
|
runHook preInstall
|
||||||
|
mkdir -p "$out/lib"
|
||||||
|
cp -r ./bin/* "$out/lib"
|
||||||
|
makeWrapper "${dotnet-runtime}/bin/dotnet" "$out/bin/${name}" --add-flags "$out/lib/${dll}.dll"
|
||||||
|
runHook postInstall
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
in {
|
||||||
|
packages = {
|
||||||
|
fantomas = dotnetTool null "fantomas" (builtins.fromJSON (builtins.readFile ./.config/dotnet-tools.json)).tools.fantomas.version (builtins.head (builtins.filter (elem: elem.pname == "fantomas") ((import ./nix/deps.nix) {fetchNuGet = x: x;}))).sha256;
|
||||||
|
fsharp-analyzers = dotnetTool "FSharp.Analyzers.Cli" "fsharp-analyzers" (builtins.fromJSON (builtins.readFile ./.config/dotnet-tools.json)).tools.fsharp-analyzers.version (builtins.head (builtins.filter (elem: elem.pname == "fsharp-analyzers") ((import ./nix/deps.nix) {fetchNuGet = x: x;}))).sha256;
|
||||||
|
default = pkgs.buildDotnetModule {
|
||||||
|
inherit pname version dotnet-sdk dotnet-runtime;
|
||||||
|
name = "Dmarc-dotnet";
|
||||||
|
src = ./.;
|
||||||
|
projectFile = "./Dmarc.App/Dmarc.App.fsproj";
|
||||||
|
testProjectFile = "./Dmarc.Test/Dmarc.Test.fsproj";
|
||||||
|
nugetDeps = ./nix/deps.nix; # `nix build .#default.passthru.fetch-deps && ./result` and put the result here
|
||||||
|
doCheck = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
devShell = pkgs.mkShell {
|
||||||
|
buildInputs = [dotnet-sdk];
|
||||||
|
packages = [
|
||||||
|
pkgs.alejandra
|
||||||
|
pkgs.nodePackages.markdown-link-check
|
||||||
|
pkgs.shellcheck
|
||||||
|
];
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
99
nix/deps.nix
Normal file
99
nix/deps.nix
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# This file was automatically generated by passthru.fetch-deps.
|
||||||
|
# Please dont edit it manually, your changes might get overwritten!
|
||||||
|
{fetchNuGet}: [
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "fantomas";
|
||||||
|
version = "6.3.4";
|
||||||
|
sha256 = "1bf57pzvl0i1bgic2vf08mqlzzbd5kys1ip9klrhm4f155ksm9fm";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "fsharp-analyzers";
|
||||||
|
version = "0.26.0";
|
||||||
|
sha256 = "0xgv5kvbwfdvcp6s8x7xagbbi4s3mqa4ixni6pazqvyflbgnah7b";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "FSharp.Core";
|
||||||
|
version = "6.0.0";
|
||||||
|
sha256 = "1hjhvr39c1vpgrdmf8xln5q86424fqkvy9nirkr29vl2461d2039";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "FSharp.Core";
|
||||||
|
version = "8.0.300";
|
||||||
|
sha256 = "158xxr9hnhz2ibyzzp2d249angvxfc58ifflm4g3hz8qx9zxaq04";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "FsUnit";
|
||||||
|
version = "6.0.0";
|
||||||
|
sha256 = "18q3p0z155znwj1l0qq3vq9nh9wl2i4mlfx4pmrnia4czr0xdkmb";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.Build.Tasks.Git";
|
||||||
|
version = "8.0.0";
|
||||||
|
sha256 = "0055f69q3hbagqp8gl3nk0vfn4qyqyxsxyy7pd0g7wm3z28byzmx";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.CodeCoverage";
|
||||||
|
version = "17.10.0";
|
||||||
|
sha256 = "0s0v7jmrq85n356xv7zixvwa4z94fszjcr5vll8x4im1a2lp00f9";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.NET.Test.Sdk";
|
||||||
|
version = "17.10.0";
|
||||||
|
sha256 = "13g8fwl09li8fc71nk13dgkb7gahd4qhamyg2xby7am63nlchhdf";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.NETCore.Platforms";
|
||||||
|
version = "1.1.0";
|
||||||
|
sha256 = "08vh1r12g6ykjygq5d3vq09zylgb84l63k49jc4v8faw9g93iqqm";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.SourceLink.Common";
|
||||||
|
version = "8.0.0";
|
||||||
|
sha256 = "0xrr8yd34ij7dqnyddkp2awfmf9qn3c89xmw2f3npaa4wnajmx81";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.SourceLink.GitHub";
|
||||||
|
version = "8.0.0";
|
||||||
|
sha256 = "1gdx7n45wwia3yvang3ls92sk3wrymqcx9p349j8wba2lyjf9m44";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.TestPlatform.ObjectModel";
|
||||||
|
version = "17.10.0";
|
||||||
|
sha256 = "07j69cw8r39533w4p39mnj00kahazz38760in3jfc45kmlcdb26x";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.TestPlatform.TestHost";
|
||||||
|
version = "17.10.0";
|
||||||
|
sha256 = "1bl471s7fx9jycr0cc8rylwf34mrvlg9qn1an6l86nisavfcyb7v";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Nerdbank.GitVersioning";
|
||||||
|
version = "3.6.133";
|
||||||
|
sha256 = "1cdw8krvsnx0n34f7fm5hiiy7bs6h3asvncqcikc0g46l50w2j80";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "NETStandard.Library";
|
||||||
|
version = "2.0.3";
|
||||||
|
sha256 = "1fn9fxppfcg4jgypp2pmrpr6awl3qz1xmnri0cygpkwvyx27df1y";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Newtonsoft.Json";
|
||||||
|
version = "13.0.1";
|
||||||
|
sha256 = "0fijg0w6iwap8gvzyjnndds0q4b8anwxxvik7y8vgq97dram4srb";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "NUnit";
|
||||||
|
version = "4.1.0";
|
||||||
|
sha256 = "0fj6xwgqaxq3mrai86bklclfmjkzf038mrslwfqf4ignaz9f7g5j";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "NUnit3TestAdapter";
|
||||||
|
version = "4.5.0";
|
||||||
|
sha256 = "1srx1629s0k1kmf02nmz251q07vj6pv58mdafcr5dr0bbn1fh78i";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "System.Reflection.Metadata";
|
||||||
|
version = "1.6.0";
|
||||||
|
sha256 = "1wdbavrrkajy7qbdblpbpbalbdl48q3h34cchz24gvdgyrlf15r4";
|
||||||
|
})
|
||||||
|
]
|
Reference in New Issue
Block a user