1
- using System . Collections . Generic ;
1
+ using System ;
2
+ using System . Collections . Generic ;
3
+ using System . Linq ;
2
4
using System . Text . RegularExpressions ;
3
5
4
6
namespace OutlookOkan . Handlers
5
7
{
8
+ /// <summary>
9
+ /// メールヘッダの解析を行う
10
+ /// </summary>
6
11
internal static class MailHeaderHandler
7
12
{
13
+ /// <summary>
14
+ /// メールヘッダを解析し、SPF、DKIM、DMARCなどの検証結果を返す
15
+ /// </summary>
16
+ /// <param name="emailHeader">メールヘッダ</param>
17
+ /// <returns>解析結果</returns>
8
18
internal static Dictionary < string , string > ValidateEmailHeader ( string emailHeader )
9
19
{
10
20
var results = new Dictionary < string , string >
11
21
{
22
+ [ "From Domain" ] = "NONE" ,
23
+ [ "ReturnPath Domain" ] = "NONE" ,
12
24
[ "SPF" ] = "NONE" ,
13
25
[ "SPF IP" ] = "NONE" ,
26
+ [ "SPF Alignment" ] = "NONE" ,
14
27
[ "DKIM" ] = "NONE" ,
15
28
[ "DKIM Domain" ] = "NONE" ,
16
- [ "DMARC" ] = "NONE"
29
+ [ "DKIM Alignment" ] = "NONE" ,
30
+ [ "DMARC" ] = "NONE" ,
31
+ [ "Internal" ] = "FALSE"
17
32
} ;
18
33
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検証
20
64
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 ) ;
21
65
var spfMatch = spfRegex . Match ( emailHeader ) ;
22
66
if ( spfMatch . Success )
23
67
{
24
68
results [ "SPF" ] = spfMatch . Groups [ "result" ] . Value . ToUpper ( ) ;
25
69
results [ "SPF IP" ] = spfMatch . Groups [ "ip" ] . Value ;
26
70
}
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 )
28
76
{
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" ;
30
80
}
31
81
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 ) ;
34
84
var dkimMatch = dkimRegex . Match ( emailHeader ) ;
35
85
if ( dkimMatch . Success )
36
86
{
37
87
results [ "DKIM" ] = dkimMatch . Groups [ "result" ] . Value . ToUpper ( ) ;
38
- results [ "DKIM Domain" ] = dkimMatch . Groups [ "domain" ] . Value ;
39
88
}
40
89
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検証
42
113
var dmarcRegex = new Regex ( @"Authentication-Results:.*?dmarc=(?<result>pass|bestguesspass|softfail|fail|none)" , RegexOptions . IgnoreCase | RegexOptions . Singleline ) ;
43
114
var dmarcMatch = dmarcRegex . Match ( emailHeader ) ;
44
115
if ( dmarcMatch . Success )
@@ -48,5 +119,95 @@ internal static Dictionary<string, string> ValidateEmailHeader(string emailHeade
48
119
49
120
return results ;
50
121
}
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
+ }
51
212
}
52
213
}
0 commit comments