Skip to content

Commit 3a26580

Browse files
authored
Merge pull request #57 from t-miyake/develop
メールヘッダの検証(なりすまし判定)の改善や確認画面の挙動改善
2 parents 366e153 + 56450c2 commit 3a26580

31 files changed

+1763
-570
lines changed
+170-9
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,115 @@
1-
using System.Collections.Generic;
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
24
using System.Text.RegularExpressions;
35

46
namespace OutlookOkan.Handlers
57
{
8+
/// <summary>
9+
/// メールヘッダの解析を行う
10+
/// </summary>
611
internal static class MailHeaderHandler
712
{
13+
/// <summary>
14+
/// メールヘッダを解析し、SPF、DKIM、DMARCなどの検証結果を返す
15+
/// </summary>
16+
/// <param name="emailHeader">メールヘッダ</param>
17+
/// <returns>解析結果</returns>
818
internal static Dictionary<string, string> ValidateEmailHeader(string emailHeader)
919
{
1020
var results = new Dictionary<string, string>
1121
{
22+
["From Domain"] = "NONE",
23+
["ReturnPath Domain"] = "NONE",
1224
["SPF"] = "NONE",
1325
["SPF IP"] = "NONE",
26+
["SPF Alignment"] = "NONE",
1427
["DKIM"] = "NONE",
1528
["DKIM Domain"] = "NONE",
16-
["DMARC"] = "NONE"
29+
["DKIM Alignment"] = "NONE",
30+
["DMARC"] = "NONE",
31+
["Internal"] = "FALSE"
1732
};
1833

19-
// SPF Validation
34+
if (string.IsNullOrEmpty(emailHeader))
35+
{
36+
return null;
37+
}
38+
39+
if (IsInternalMail(emailHeader))
40+
{
41+
results["Internal"] = "TRUE";
42+
}
43+
44+
var fromDomain = string.Empty;
45+
var fromRegex = new Regex(@"^From:\s*.*(?:\r?\n\s+.*)*", RegexOptions.IgnoreCase | RegexOptions.Multiline);
46+
var fromMatch = fromRegex.Match(emailHeader);
47+
if (fromMatch.Success)
48+
{
49+
var fromHeader = fromMatch.Value;
50+
var domainRegex = new Regex(@"<.*?@(?<domain>[^\s>]+)>", RegexOptions.IgnoreCase);
51+
var domainMatch = domainRegex.Match(fromHeader);
52+
53+
if (!domainMatch.Success)
54+
{
55+
var alternativeDomainRegex = new Regex(@"[^<\s]+@(?<domain>[^\s>]+)", RegexOptions.IgnoreCase);
56+
domainMatch = alternativeDomainRegex.Match(fromHeader);
57+
}
58+
59+
fromDomain = domainMatch.Success ? domainMatch.Groups["domain"].Value : string.Empty;
60+
results["From Domain"] = fromDomain;
61+
}
62+
63+
// SPF検証
2064
var spfRegex = new Regex(@"Received-SPF:\s*(?<result>pass|fail|softfail|neutral|temperror|permerror|none).*\b(does\s+not\s+)?designate[s]?\s+(?<ip>[^ ]+)\s+as", RegexOptions.IgnoreCase | RegexOptions.Singleline);
2165
var spfMatch = spfRegex.Match(emailHeader);
2266
if (spfMatch.Success)
2367
{
2468
results["SPF"] = spfMatch.Groups["result"].Value.ToUpper();
2569
results["SPF IP"] = spfMatch.Groups["ip"].Value;
2670
}
27-
else
71+
72+
// SPFアライメント検証
73+
var returnPathRegex = new Regex(@"Return-Path:\s*.*@(?<domain>[^\s>]+)");
74+
var returnPathMatch = returnPathRegex.Match(emailHeader);
75+
if (returnPathMatch.Success && fromDomain != string.Empty)
2876
{
29-
results["SPF"] = "NULL";
77+
var returnPathDomain = returnPathMatch.Groups["domain"].Value;
78+
results["ReturnPath Domain"] = returnPathDomain;
79+
results["SPF Alignment"] = returnPathDomain.Equals(fromDomain, StringComparison.OrdinalIgnoreCase) || returnPathDomain.ToLower().Contains(fromDomain.ToLower()) || fromDomain.ToLower().Contains(returnPathDomain.ToLower()) ? "PASS" : "FAIL";
3080
}
3181

32-
// DKIM Validation
33-
var dkimRegex = new Regex(@"Authentication-Results:.*?dkim=(?<result>pass|policy|fail|softfail|hardfail|neutral|temperror|permerror|none).*?header.(d|i)=(?<domain>[^(;| )]+)", RegexOptions.IgnoreCase | RegexOptions.Singleline);
82+
// DKIM検証
83+
var dkimRegex = new Regex(@"Authentication-Results:.*?dkim=(?<result>pass|policy|fail|softfail|hardfail|neutral|temperror|permerror|none).*?header.d=(?<domain>[^(;| )]+)", RegexOptions.IgnoreCase | RegexOptions.Singleline);
3484
var dkimMatch = dkimRegex.Match(emailHeader);
3585
if (dkimMatch.Success)
3686
{
3787
results["DKIM"] = dkimMatch.Groups["result"].Value.ToUpper();
38-
results["DKIM Domain"] = dkimMatch.Groups["domain"].Value;
3988
}
4089

41-
// DMARC Validation
90+
// DKIMアライメント検証
91+
var dkimSignatureRegex = new Regex(@"DKIM-Signature:.*?d=(?<domain>[^(;| )]+)", RegexOptions.IgnoreCase | RegexOptions.Singleline);
92+
var dkimMatches = dkimSignatureRegex.Matches(emailHeader);
93+
var dkimAlignmentPass = false;
94+
var dkimDomains = new List<string>();
95+
96+
foreach (Match match in dkimMatches)
97+
{
98+
var dkimDomain = match.Groups["domain"].Value;
99+
if (string.IsNullOrEmpty(dkimDomain)) continue;
100+
101+
dkimDomains.Add(dkimDomain);
102+
if (dkimDomain.Equals(fromDomain, StringComparison.OrdinalIgnoreCase) ||
103+
dkimDomain.ToLower().Contains(fromDomain.ToLower()) ||
104+
fromDomain.ToLower().Contains(dkimDomain.ToLower()))
105+
{
106+
dkimAlignmentPass = true;
107+
}
108+
}
109+
results["DKIM Domain"] = string.Join(", ", dkimDomains);
110+
results["DKIM Alignment"] = dkimAlignmentPass ? "PASS" : "FAIL";
111+
112+
// DMARC検証
42113
var dmarcRegex = new Regex(@"Authentication-Results:.*?dmarc=(?<result>pass|bestguesspass|softfail|fail|none)", RegexOptions.IgnoreCase | RegexOptions.Singleline);
43114
var dmarcMatch = dmarcRegex.Match(emailHeader);
44115
if (dmarcMatch.Success)
@@ -48,5 +119,95 @@ internal static Dictionary<string, string> ValidateEmailHeader(string emailHeade
48119

49120
return results;
50121
}
122+
123+
/// <summary>
124+
/// DMARCの検証結果を独自判定する
125+
/// </summary>
126+
/// <param name="spfResult"></param>
127+
/// <param name="spfAlignmentResult"></param>
128+
/// <param name="dkimResult"></param>
129+
/// <param name="dkimAlignmentResult"></param>
130+
/// <returns>DMARCの検証結果</returns>
131+
public static string DetermineDmarcResult(string spfResult, string spfAlignmentResult, string dkimResult, string dkimAlignmentResult)
132+
{
133+
if (string.IsNullOrEmpty(spfResult) || string.IsNullOrEmpty(spfAlignmentResult) || string.IsNullOrEmpty(dkimResult) || string.IsNullOrEmpty(dkimAlignmentResult))
134+
{
135+
return "FAIL";
136+
}
137+
138+
// NONEはFAILとして扱う
139+
spfResult = spfResult.ToUpper() == "NONE" ? "FAIL" : spfResult.ToUpper();
140+
spfAlignmentResult = spfAlignmentResult.ToUpper() == "NONE" ? "FAIL" : spfAlignmentResult.ToUpper();
141+
dkimResult = dkimResult.ToUpper() == "NONE" ? "FAIL" : dkimResult.ToUpper();
142+
dkimAlignmentResult = dkimAlignmentResult.ToUpper() == "NONE" ? "FAIL" : dkimAlignmentResult.ToUpper();
143+
144+
var key = $"{spfResult}_{spfAlignmentResult}_{dkimResult}_{dkimAlignmentResult}";
145+
146+
//SPF認証_SPFアライメント_DKIM認証_DKIMアライメント
147+
var dmarcResults = new Dictionary<string, string>
148+
{
149+
{ "PASS_PASS_PASS_PASS", "PASS" }, // 両方の認証とアライメントが成功
150+
{ "PASS_PASS_PASS_FAIL", "PASS" }, // SPFの認証とアライメントが成功、DKIMの認証が成功
151+
{ "PASS_PASS_FAIL_PASS", "PASS" }, // SPFの認証とアライメントが成功、DKIMのアライメントが成功
152+
{ "PASS_PASS_FAIL_FAIL", "PASS" }, // SPFの認証とアライメントが成功
153+
{ "PASS_FAIL_PASS_PASS", "PASS" }, // SPFの認証が成功、DKIMの認証とアライメントが成功
154+
{ "FAIL_PASS_PASS_PASS", "PASS" }, // SPFのアライメントが成功、DKIMの認証とアライメントが成功
155+
{ "FAIL_FAIL_PASS_PASS", "PASS" }, // DKIMの認証とアライメントが成功
156+
157+
{ "PASS_FAIL_PASS_FAIL", "FAIL" }, // SPFの認証が成功、DKIMの認証が成功
158+
{ "PASS_FAIL_FAIL_PASS", "FAIL" }, // SPFの認証が成功、DKIMのアライメントが成功
159+
{ "PASS_FAIL_FAIL_FAIL", "FAIL" }, // SPFの認証が成功
160+
{ "FAIL_PASS_PASS_FAIL", "FAIL" }, // SPFのアライメントが成功、DKIMの認証が成功
161+
{ "FAIL_PASS_FAIL_PASS", "FAIL" }, // SPFのアライメントが成功、DKIMのアライメントが成功
162+
{ "FAIL_PASS_FAIL_FAIL", "FAIL" }, // SPFのアライメントが成功
163+
{ "FAIL_FAIL_PASS_FAIL", "FAIL" }, // DKIMの認証が成功
164+
{ "FAIL_FAIL_FAIL_PASS", "FAIL" }, // DKIMのアライメントが成功
165+
{ "FAIL_FAIL_FAIL_FAIL", "FAIL" } // すべて失敗
166+
};
167+
return dmarcResults.TryGetValue(key, out var result) ? result : "FAIL";
168+
}
169+
170+
/// <summary>
171+
/// 内部メールか否かの判定
172+
/// </summary>
173+
/// <param name="emailHeader">メールヘッダ</param>
174+
/// <returns>判定結果</returns>
175+
internal static bool IsInternalMail(string emailHeader)
176+
{
177+
// Receivedヘッダをすべて取得
178+
var receivedRegex = new Regex(@"^Received:.*", RegexOptions.Multiline);
179+
var matches = receivedRegex.Matches(emailHeader);
180+
181+
var receivedHeaders = (from Match match in matches select match.Value).ToList();
182+
183+
// 受信ヘッダの数が多い場合は外部メールと判定
184+
if (receivedHeaders.Count > 3)
185+
{
186+
return false;
187+
}
188+
189+
// 受信ヘッダが複数ある場合、連続したドメイン名が一致するかどうかを確認
190+
var domainRegex = new Regex(@"from\s([^\s]+)", RegexOptions.IgnoreCase);
191+
string previousDomain = null;
192+
193+
foreach (var currentDomain in from header in receivedHeaders select domainRegex.Match(header) into domainMatch where domainMatch.Success select ExtractMainDomain(domainMatch.Groups[1].Value))
194+
{
195+
if (previousDomain != null && previousDomain.Equals(currentDomain, StringComparison.OrdinalIgnoreCase))
196+
{
197+
return true;
198+
}
199+
previousDomain = currentDomain;
200+
}
201+
202+
return false;
203+
}
204+
205+
private static string ExtractMainDomain(string domain)
206+
{
207+
var parts = domain.Split('.');
208+
var length = parts.Length;
209+
210+
return length > 2 ? string.Join(".", parts.Skip(length - 3)) : domain;
211+
}
51212
}
52213
}

OutlookOkan/OutlookOkan.csproj

+12-7
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
<PublishUrl>publish\</PublishUrl>
3636
<InstallUrl />
3737
<TargetCulture>en</TargetCulture>
38-
<ApplicationVersion>2.8.1.00</ApplicationVersion>
38+
<ApplicationVersion>2.8.2.00</ApplicationVersion>
3939
<AutoIncrementApplicationRevision>false</AutoIncrementApplicationRevision>
4040
<UpdateEnabled>false</UpdateEnabled>
4141
<UpdateInterval>0</UpdateInterval>
@@ -128,12 +128,18 @@
128128
This section specifies references for the project.
129129
-->
130130
<ItemGroup>
131-
<Reference Include="CsvHelper, Version=30.0.0.0, Culture=neutral, PublicKeyToken=8c4959082be5c823, processorArchitecture=MSIL">
132-
<HintPath>..\packages\CsvHelper.30.0.1\lib\net45\CsvHelper.dll</HintPath>
131+
<Reference Include="CsvHelper, Version=32.0.0.0, Culture=neutral, PublicKeyToken=8c4959082be5c823, processorArchitecture=MSIL">
132+
<HintPath>..\packages\CsvHelper.32.0.3\lib\net462\CsvHelper.dll</HintPath>
133133
</Reference>
134134
<Reference Include="ICSharpCode.SharpZipLib, Version=1.4.2.13, Culture=neutral, PublicKeyToken=1b03e6acf1164f73, processorArchitecture=MSIL">
135135
<HintPath>..\packages\SharpZipLib.1.4.2\lib\netstandard2.0\ICSharpCode.SharpZipLib.dll</HintPath>
136136
</Reference>
137+
<Reference Include="Microsoft.Bcl.AsyncInterfaces, Version=7.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
138+
<HintPath>..\packages\Microsoft.Bcl.AsyncInterfaces.7.0.0\lib\net462\Microsoft.Bcl.AsyncInterfaces.dll</HintPath>
139+
</Reference>
140+
<Reference Include="Microsoft.Bcl.HashCode, Version=1.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
141+
<HintPath>..\packages\Microsoft.Bcl.HashCode.1.1.1\lib\net461\Microsoft.Bcl.HashCode.dll</HintPath>
142+
</Reference>
137143
<Reference Include="Microsoft.Office.Interop.Excel, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c, processorArchitecture=MSIL">
138144
<EmbedInteropTypes>True</EmbedInteropTypes>
139145
</Reference>
@@ -240,6 +246,7 @@
240246
</Compile>
241247
<Compile Include="Ribbon.cs" />
242248
<Compile Include="Services\ResourceService.cs" />
249+
<Compile Include="Properties\Settings.cs" />
243250
<Compile Include="Types\AlertAddress.cs" />
244251
<Compile Include="Types\AlertKeywordAndMessage.cs" />
245252
<Compile Include="Types\AlertKeywordAndMessageForSubject.cs" />
@@ -251,6 +258,7 @@
251258
<Compile Include="Types\AutoCcBccAttachedFile.cs" />
252259
<Compile Include="Types\AutoCcBccKeyword.cs" />
253260
<Compile Include="Types\AutoCcBccRecipient.cs" />
261+
<Compile Include="Types\AutoDeleteRecipient.cs" />
254262
<Compile Include="Types\CcOrBcc.cs" />
255263
<Compile Include="Types\CheckList.cs" />
256264
<Compile Include="Types\DeferredDeliveryMinutes.cs" />
@@ -304,7 +312,6 @@
304312
<SubType>Designer</SubType>
305313
</EmbeddedResource>
306314
<None Include="app.config" />
307-
<None Include="CodeSigningCertificate_NoranekoInc.pfx" />
308315
<None Include="packages.config" />
309316
<None Include="Properties\Settings.settings">
310317
<Generator>SettingsSingleFileGenerator</Generator>
@@ -313,6 +320,7 @@
313320
<Compile Include="Properties\Settings.Designer.cs">
314321
<AutoGen>True</AutoGen>
315322
<DependentUpon>Settings.settings</DependentUpon>
323+
<DesignTimeSharedInput>True</DesignTimeSharedInput>
316324
</Compile>
317325
<Compile Include="ThisAddIn.cs">
318326
<SubType>Code</SubType>
@@ -369,9 +377,6 @@
369377
<ManifestKeyFile>
370378
</ManifestKeyFile>
371379
</PropertyGroup>
372-
<PropertyGroup>
373-
<ManifestCertificateThumbprint>2D87A382896E579AFE953BA4E742DFDA1BCB31E5</ManifestCertificateThumbprint>
374-
</PropertyGroup>
375380
<PropertyGroup>
376381
<SignAssembly>false</SignAssembly>
377382
</PropertyGroup>

OutlookOkan/Properties/AssemblyInfo.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
[assembly: AssemblyConfiguration("")]
1111
[assembly: AssemblyCompany("Noraneko Inc.")]
1212
[assembly: AssemblyProduct("OutlookOkan")]
13-
[assembly: AssemblyCopyright("Copyright © Noraneko Inc. 2023")]
13+
[assembly: AssemblyCopyright("Copyright © Noraneko Inc. 2024")]
1414
[assembly: AssemblyTrademark("")]
1515
[assembly: AssemblyCulture("")]
1616

@@ -32,7 +32,7 @@
3232
// You can specify all the values or you can default the Build and Revision Numbers
3333
// by using the '*' as shown below:
3434
// [assembly: AssemblyVersion("1.0.*")]
35-
[assembly: AssemblyVersion("2.8.1.00")]
36-
[assembly: AssemblyFileVersion("2.8.1.00")]
35+
[assembly: AssemblyVersion("2.8.2.00")]
36+
[assembly: AssemblyFileVersion("2.8.2.00")]
3737
[assembly: NeutralResourcesLanguage("en-US")]
3838

0 commit comments

Comments
 (0)