Commit
+3393 -640 +/-29 browse
1 | diff --git a/Cargo.toml b/Cargo.toml |
2 | index 984215d..2ae36a2 100644 |
3 | --- a/Cargo.toml |
4 | +++ b/Cargo.toml |
5 | @@ -17,6 +17,10 @@ lru-cache = "0.1.2" |
6 | parking_lot = "0.12.0" |
7 | ahash = "0.8.0" |
8 | quick-xml = "0.26.0" |
9 | + serde = { version = "1.0", features = ["derive"] } |
10 | + zip = "0.6.3" |
11 | + flate2 = "1.0.25" |
12 | |
13 | [dev-dependencies] |
14 | tokio = { version = "1.16", features = ["net", "io-util", "time", "rt-multi-thread", "macros"] } |
15 | + serde_json = "1.0" |
16 | diff --git a/resources/dmarc-agg-report/001.xml b/resources/dmarc-agg-report/001.xml |
17 | deleted file mode 100644 |
18 | index dad8479..0000000 |
19 | --- a/resources/dmarc-agg-report/001.xml |
20 | +++ /dev/null |
21 | @@ -1,49 +0,0 @@ |
22 | - <?xml version="1.0" encoding="UTF-8" ?> |
23 | - <feedback> |
24 | - <report_metadata> |
25 | - <version>2.0</version> |
26 | - <org_name>Sample Reporter</org_name> |
27 | - <email>report_sender@example-reporter.com</email> |
28 | - <extra_contact_info>...</extra_contact_info> |
29 | - <report_id>3v98abbp8ya9n3va8yr8oa3ya</report_id> |
30 | - <date_range> |
31 | - <begin>161212415</begin> |
32 | - <end>161221511</end> |
33 | - </date_range> |
34 | - </report_metadata> |
35 | - <policy_published> |
36 | - <domain>example.com</domain> |
37 | - <p>quarantine</p> |
38 | - <sp>none</sp> |
39 | - <testing>n</testing> |
40 | - </policy_published> |
41 | - <record> |
42 | - <row> |
43 | - <source_ip>192.168.4.4</source_ip> |
44 | - <count>123</count> |
45 | - <policy_evaluated> |
46 | - <disposition>quarantine</disposition> |
47 | - <dkim>pass</dkim> |
48 | - <spf>fail</spf> |
49 | - </policy_evaluated> |
50 | - </row> |
51 | - <identifiers> |
52 | - <header_from>example.com</header_from> |
53 | - </identifiers> |
54 | - <auth_results> |
55 | - <dkim> |
56 | - <domain>example.com</domain> |
57 | - <result>pass</result> |
58 | - <selector>abc123</selector> |
59 | - </dkim> |
60 | - <spf> |
61 | - <domain>example.com</domain> |
62 | - <result>fail</result> |
63 | - </spf> |
64 | - </auth_results> |
65 | - <extensions> |
66 | - </extensions> |
67 | - </record> |
68 | - <extensions> |
69 | - </extensions> |
70 | - </feedback> |
71 | diff --git a/resources/dmarc-feedback/001.json b/resources/dmarc-feedback/001.json |
72 | new file mode 100644 |
73 | index 0000000..ac141e4 |
74 | --- /dev/null |
75 | +++ b/resources/dmarc-feedback/001.json |
76 | @@ -0,0 +1,77 @@ |
77 | + { |
78 | + "version": 2.0, |
79 | + "report_metadata": { |
80 | + "org_name": "Sample Reporter", |
81 | + "email": "report_sender@example-reporter.com", |
82 | + "extra_contact_info": "...", |
83 | + "report_id": "3v98abbp8ya9n3va8yr8oa3ya", |
84 | + "date_range": { |
85 | + "begin": 161212415, |
86 | + "end": 161221511 |
87 | + }, |
88 | + "error": [] |
89 | + }, |
90 | + "policy_published": { |
91 | + "domain": "example.com", |
92 | + "version_published": null, |
93 | + "adkim": "Unspecified", |
94 | + "aspf": "Unspecified", |
95 | + "p": "Quarantine", |
96 | + "sp": "None", |
97 | + "testing": false, |
98 | + "fo": null |
99 | + }, |
100 | + "record": [ |
101 | + { |
102 | + "row": { |
103 | + "source_ip": "192.168.4.4", |
104 | + "count": 123, |
105 | + "policy_evaluated": { |
106 | + "disposition": "Quarantine", |
107 | + "dkim": "Pass", |
108 | + "spf": "Fail", |
109 | + "reason": [] |
110 | + } |
111 | + }, |
112 | + "identifiers": { |
113 | + "envelope_to": null, |
114 | + "envelope_from": "", |
115 | + "header_from": "example.com" |
116 | + }, |
117 | + "auth_results": { |
118 | + "dkim": [ |
119 | + { |
120 | + "domain": "example.com", |
121 | + "selector": "abc123", |
122 | + "result": "Pass", |
123 | + "human_result": null |
124 | + } |
125 | + ], |
126 | + "spf": [ |
127 | + { |
128 | + "domain": "example.com", |
129 | + "scope": "Unspecified", |
130 | + "result": "Fail", |
131 | + "human_result": null |
132 | + } |
133 | + ] |
134 | + }, |
135 | + "extensions": [ |
136 | + { |
137 | + "name": "extensionName", |
138 | + "definition": "https://path/to/spec" |
139 | + }, |
140 | + { |
141 | + "name": "extensionName2", |
142 | + "definition": "https://path/to/spec2" |
143 | + } |
144 | + ] |
145 | + } |
146 | + ], |
147 | + "extensions": [ |
148 | + { |
149 | + "name": "otherExtension", |
150 | + "definition": "https://path/to/spec" |
151 | + } |
152 | + ] |
153 | + } |
154 | \ No newline at end of file |
155 | diff --git a/resources/dmarc-feedback/001.xml b/resources/dmarc-feedback/001.xml |
156 | new file mode 100644 |
157 | index 0000000..9cce0f6 |
158 | --- /dev/null |
159 | +++ b/resources/dmarc-feedback/001.xml |
160 | @@ -0,0 +1,60 @@ |
161 | + <?xml version="1.0" encoding="UTF-8" ?> |
162 | + <feedback> |
163 | + <version>2.0</version> |
164 | + <report_metadata> |
165 | + <org_name>Sample Reporter</org_name> |
166 | + <email>report_sender@example-reporter.com</email> |
167 | + <extra_contact_info>...</extra_contact_info> |
168 | + <report_id>3v98abbp8ya9n3va8yr8oa3ya</report_id> |
169 | + <date_range> |
170 | + <begin>161212415</begin> |
171 | + <end>161221511</end> |
172 | + <unknown-tag1><unknown-tag2></unknown-tag2></unknown-tag1> |
173 | + </date_range> |
174 | + </report_metadata> |
175 | + <policy_published> |
176 | + <domain>example.com</domain> |
177 | + <p>quarantine</p> |
178 | + <sp>none</sp> |
179 | + <testing>n</testing> |
180 | + <unknown-tag1><unknown-tag2></unknown-tag2></unknown-tag1> |
181 | + </policy_published> |
182 | + <record> |
183 | + <row> |
184 | + <source_ip>192.168.4.4</source_ip> |
185 | + <count>123</count> |
186 | + <policy_evaluated> |
187 | + <disposition>quarantine</disposition> |
188 | + <dkim>pass</dkim> |
189 | + <spf>fail</spf> |
190 | + </policy_evaluated> |
191 | + </row> |
192 | + <identifiers> |
193 | + <header_from>example.com</header_from> |
194 | + </identifiers> |
195 | + <auth_results> |
196 | + <dkim> |
197 | + <domain>example.com</domain> |
198 | + <result>pass</result> |
199 | + <selector>abc123</selector> |
200 | + </dkim> |
201 | + <spf> |
202 | + <domain>example.com</domain> |
203 | + <result>fail</result> |
204 | + </spf> |
205 | + </auth_results> |
206 | + <extensions> |
207 | + <extension name="extensionName" definition="https://path/to/spec"> |
208 | + <data>...</data> |
209 | + </extension> |
210 | + <extension name="extensionName2" definition="https://path/to/spec2"> |
211 | + <data>...</data> |
212 | + </extension> |
213 | + </extensions> |
214 | + </record> |
215 | + <extensions> |
216 | + <extension name="otherExtension" definition="https://path/to/spec"> |
217 | + <data>...</data> |
218 | + </extension> |
219 | + </extensions> |
220 | + </feedback> |
221 | diff --git a/resources/dmarc-feedback/002.json b/resources/dmarc-feedback/002.json |
222 | new file mode 100644 |
223 | index 0000000..7a36f04 |
224 | --- /dev/null |
225 | +++ b/resources/dmarc-feedback/002.json |
226 | @@ -0,0 +1,63 @@ |
227 | + { |
228 | + "version": 0.0, |
229 | + "report_metadata": { |
230 | + "org_name": "google.com", |
231 | + "email": "noreply-dmarc-support@google.com", |
232 | + "extra_contact_info": "https://support.google.com/a/answer/2466580", |
233 | + "report_id": "2122885654478337555", |
234 | + "date_range": { |
235 | + "begin": 1661558400, |
236 | + "end": 1661644799 |
237 | + }, |
238 | + "error": [] |
239 | + }, |
240 | + "policy_published": { |
241 | + "domain": "example.org", |
242 | + "version_published": null, |
243 | + "adkim": "Relaxed", |
244 | + "aspf": "Relaxed", |
245 | + "p": "None", |
246 | + "sp": "None", |
247 | + "testing": false, |
248 | + "fo": null |
249 | + }, |
250 | + "record": [ |
251 | + { |
252 | + "row": { |
253 | + "source_ip": "209.85.220.41", |
254 | + "count": 2, |
255 | + "policy_evaluated": { |
256 | + "disposition": "None", |
257 | + "dkim": "Fail", |
258 | + "spf": "Pass", |
259 | + "reason": [] |
260 | + } |
261 | + }, |
262 | + "identifiers": { |
263 | + "envelope_to": null, |
264 | + "envelope_from": "", |
265 | + "header_from": "example.org" |
266 | + }, |
267 | + "auth_results": { |
268 | + "dkim": [ |
269 | + { |
270 | + "domain": "example-org.20210112.gappssmtp.com", |
271 | + "selector": "20210112", |
272 | + "result": "Pass", |
273 | + "human_result": null |
274 | + } |
275 | + ], |
276 | + "spf": [ |
277 | + { |
278 | + "domain": "example.org", |
279 | + "scope": "Unspecified", |
280 | + "result": "Pass", |
281 | + "human_result": null |
282 | + } |
283 | + ] |
284 | + }, |
285 | + "extensions": [] |
286 | + } |
287 | + ], |
288 | + "extensions": [] |
289 | + } |
290 | \ No newline at end of file |
291 | diff --git a/resources/dmarc-feedback/002.xml b/resources/dmarc-feedback/002.xml |
292 | new file mode 100644 |
293 | index 0000000..638e46c |
294 | --- /dev/null |
295 | +++ b/resources/dmarc-feedback/002.xml |
296 | @@ -0,0 +1,46 @@ |
297 | + <?xml version="1.0" encoding="UTF-8" ?> |
298 | + <feedback> |
299 | + <report_metadata> |
300 | + <org_name>google.com</org_name> |
301 | + <email>noreply-dmarc-support@google.com</email> |
302 | + <extra_contact_info>https://support.google.com/a/answer/2466580</extra_contact_info> |
303 | + <report_id>2122885654478337555</report_id> |
304 | + <date_range> |
305 | + <begin>1661558400</begin> |
306 | + <end>1661644799</end> |
307 | + </date_range> |
308 | + </report_metadata> |
309 | + <policy_published> |
310 | + <domain>example.org</domain> |
311 | + <adkim>r</adkim> |
312 | + <aspf>r</aspf> |
313 | + <p>none</p> |
314 | + <sp>none</sp> |
315 | + <pct>100</pct> |
316 | + </policy_published> |
317 | + <record> |
318 | + <row> |
319 | + <source_ip>209.85.220.41</source_ip> |
320 | + <count>2</count> |
321 | + <policy_evaluated> |
322 | + <disposition>none</disposition> |
323 | + <dkim>fail</dkim> |
324 | + <spf>pass</spf> |
325 | + </policy_evaluated> |
326 | + </row> |
327 | + <identifiers> |
328 | + <header_from>example.org</header_from> |
329 | + </identifiers> |
330 | + <auth_results> |
331 | + <dkim> |
332 | + <domain>example-org.20210112.gappssmtp.com</domain> |
333 | + <result>pass</result> |
334 | + <selector>20210112</selector> |
335 | + </dkim> |
336 | + <spf> |
337 | + <domain>example.org</domain> |
338 | + <result>pass</result> |
339 | + </spf> |
340 | + </auth_results> |
341 | + </record> |
342 | + </feedback> |
343 | \ No newline at end of file |
344 | diff --git a/resources/dmarc-feedback/003.json b/resources/dmarc-feedback/003.json |
345 | new file mode 100644 |
346 | index 0000000..066e91e |
347 | --- /dev/null |
348 | +++ b/resources/dmarc-feedback/003.json |
349 | @@ -0,0 +1,192 @@ |
350 | + { |
351 | + "version": 1.0, |
352 | + "report_metadata": { |
353 | + "org_name": "Fastmail Pty Ltd", |
354 | + "email": "reports@fastmaildmarc.com", |
355 | + "extra_contact_info": "https://fastmail.com/", |
356 | + "report_id": "758848224", |
357 | + "date_range": { |
358 | + "begin": 1667347200, |
359 | + "end": 1667433599 |
360 | + }, |
361 | + "error": [] |
362 | + }, |
363 | + "policy_published": { |
364 | + "domain": "stalw.art", |
365 | + "version_published": null, |
366 | + "adkim": "Unspecified", |
367 | + "aspf": "Unspecified", |
368 | + "p": "None", |
369 | + "sp": "None", |
370 | + "testing": false, |
371 | + "fo": "0" |
372 | + }, |
373 | + "record": [ |
374 | + { |
375 | + "row": { |
376 | + "source_ip": "64.147.108.117", |
377 | + "count": 3, |
378 | + "policy_evaluated": { |
379 | + "disposition": "None", |
380 | + "dkim": "Fail", |
381 | + "spf": "Fail", |
382 | + "reason": [ |
383 | + { |
384 | + "type_": "TrustedForwarder", |
385 | + "comment": "Policy ignored due to local white list" |
386 | + } |
387 | + ] |
388 | + } |
389 | + }, |
390 | + "identifiers": { |
391 | + "envelope_to": null, |
392 | + "envelope_from": "jmap.bounce.topicbox.com", |
393 | + "header_from": "stalw.art" |
394 | + }, |
395 | + "auth_results": { |
396 | + "dkim": [ |
397 | + { |
398 | + "domain": "jmap.topicbox.com", |
399 | + "selector": "dkim-1", |
400 | + "result": "Pass", |
401 | + "human_result": "pass" |
402 | + } |
403 | + ], |
404 | + "spf": [ |
405 | + { |
406 | + "domain": "jmap.bounce.topicbox.com", |
407 | + "scope": "MailFrom", |
408 | + "result": "Pass", |
409 | + "human_result": null |
410 | + } |
411 | + ] |
412 | + }, |
413 | + "extensions": [] |
414 | + }, |
415 | + { |
416 | + "row": { |
417 | + "source_ip": "173.228.157.66", |
418 | + "count": 4, |
419 | + "policy_evaluated": { |
420 | + "disposition": "None", |
421 | + "dkim": "Fail", |
422 | + "spf": "Fail", |
423 | + "reason": [ |
424 | + { |
425 | + "type_": "TrustedForwarder", |
426 | + "comment": "Policy ignored due to local white list" |
427 | + } |
428 | + ] |
429 | + } |
430 | + }, |
431 | + "identifiers": { |
432 | + "envelope_to": null, |
433 | + "envelope_from": "jmap.bounce.topicbox.com", |
434 | + "header_from": "stalw.art" |
435 | + }, |
436 | + "auth_results": { |
437 | + "dkim": [ |
438 | + { |
439 | + "domain": "jmap.topicbox.com", |
440 | + "selector": "dkim-1", |
441 | + "result": "Pass", |
442 | + "human_result": "pass" |
443 | + } |
444 | + ], |
445 | + "spf": [ |
446 | + { |
447 | + "domain": "jmap.bounce.topicbox.com", |
448 | + "scope": "MailFrom", |
449 | + "result": "Pass", |
450 | + "human_result": null |
451 | + } |
452 | + ] |
453 | + }, |
454 | + "extensions": [] |
455 | + }, |
456 | + { |
457 | + "row": { |
458 | + "source_ip": "64.147.108.173", |
459 | + "count": 1, |
460 | + "policy_evaluated": { |
461 | + "disposition": "None", |
462 | + "dkim": "Fail", |
463 | + "spf": "Fail", |
464 | + "reason": [ |
465 | + { |
466 | + "type_": "TrustedForwarder", |
467 | + "comment": "Policy ignored due to local white list" |
468 | + } |
469 | + ] |
470 | + } |
471 | + }, |
472 | + "identifiers": { |
473 | + "envelope_to": null, |
474 | + "envelope_from": "jmap.bounce.topicbox.com", |
475 | + "header_from": "stalw.art" |
476 | + }, |
477 | + "auth_results": { |
478 | + "dkim": [ |
479 | + { |
480 | + "domain": "jmap.topicbox.com", |
481 | + "selector": "dkim-1", |
482 | + "result": "Pass", |
483 | + "human_result": "pass" |
484 | + } |
485 | + ], |
486 | + "spf": [ |
487 | + { |
488 | + "domain": "jmap.bounce.topicbox.com", |
489 | + "scope": "MailFrom", |
490 | + "result": "Pass", |
491 | + "human_result": null |
492 | + } |
493 | + ] |
494 | + }, |
495 | + "extensions": [] |
496 | + }, |
497 | + { |
498 | + "row": { |
499 | + "source_ip": "54.240.8.13", |
500 | + "count": 1, |
501 | + "policy_evaluated": { |
502 | + "disposition": "None", |
503 | + "dkim": "Pass", |
504 | + "spf": "Fail", |
505 | + "reason": [] |
506 | + } |
507 | + }, |
508 | + "identifiers": { |
509 | + "envelope_to": null, |
510 | + "envelope_from": "amazonses.com", |
511 | + "header_from": "stalw.art" |
512 | + }, |
513 | + "auth_results": { |
514 | + "dkim": [ |
515 | + { |
516 | + "domain": "stalw.art", |
517 | + "selector": "kvoujh5guu2jpweurhpt2ioscpkpukc3", |
518 | + "result": "Pass", |
519 | + "human_result": "pass" |
520 | + }, |
521 | + { |
522 | + "domain": "amazonses.com", |
523 | + "selector": "6gbrjpgwjskckoa6a5zn6fwqkn67xbtw", |
524 | + "result": "Pass", |
525 | + "human_result": "pass" |
526 | + } |
527 | + ], |
528 | + "spf": [ |
529 | + { |
530 | + "domain": "amazonses.com", |
531 | + "scope": "MailFrom", |
532 | + "result": "Pass", |
533 | + "human_result": null |
534 | + } |
535 | + ] |
536 | + }, |
537 | + "extensions": [] |
538 | + } |
539 | + ], |
540 | + "extensions": [] |
541 | + } |
542 | \ No newline at end of file |
543 | diff --git a/resources/dmarc-feedback/003.xml b/resources/dmarc-feedback/003.xml |
544 | new file mode 100644 |
545 | index 0000000..6889e89 |
546 | --- /dev/null |
547 | +++ b/resources/dmarc-feedback/003.xml |
548 | @@ -0,0 +1,151 @@ |
549 | + <?xml version="1.0"?> |
550 | + <feedback> |
551 | + <version>1.0</version> |
552 | + <report_metadata> |
553 | + <org_name>Fastmail Pty Ltd</org_name> |
554 | + <email>reports@fastmaildmarc.com</email> |
555 | + <extra_contact_info>https://fastmail.com/</extra_contact_info> |
556 | + <report_id>758848224</report_id> |
557 | + <date_range> |
558 | + <begin>1667347200</begin> |
559 | + <end>1667433599</end> |
560 | + </date_range> |
561 | + </report_metadata> |
562 | + <policy_published> |
563 | + <domain>stalw.art</domain> |
564 | + <p>none</p> |
565 | + <sp>none</sp> |
566 | + <pct>100</pct> |
567 | + <fo>0</fo> |
568 | + </policy_published> |
569 | + <record> |
570 | + <row> |
571 | + <source_ip>64.147.108.117</source_ip> |
572 | + <count>3</count> |
573 | + <policy_evaluated> |
574 | + <disposition>none</disposition> |
575 | + <dkim>fail</dkim> |
576 | + <spf>fail</spf> |
577 | + <reason> |
578 | + <type>trusted_forwarder</type> |
579 | + <comment>Policy ignored due to local white list</comment> |
580 | + </reason> |
581 | + </policy_evaluated> |
582 | + </row> |
583 | + <identifiers> |
584 | + <envelope_from>jmap.bounce.topicbox.com</envelope_from> |
585 | + <header_from>stalw.art</header_from> |
586 | + </identifiers> |
587 | + <auth_results> |
588 | + <dkim> |
589 | + <domain>jmap.topicbox.com</domain> |
590 | + <selector>dkim-1</selector> |
591 | + <result>pass</result> |
592 | + <human_result>pass</human_result> |
593 | + </dkim> |
594 | + <spf> |
595 | + <domain>jmap.bounce.topicbox.com</domain> |
596 | + <scope>mfrom</scope> |
597 | + <result>pass</result> |
598 | + </spf> |
599 | + </auth_results> |
600 | + </record> |
601 | + <record> |
602 | + <row> |
603 | + <source_ip>173.228.157.66</source_ip> |
604 | + <count>4</count> |
605 | + <policy_evaluated> |
606 | + <disposition>none</disposition> |
607 | + <dkim>fail</dkim> |
608 | + <spf>fail</spf> |
609 | + <reason> |
610 | + <type>trusted_forwarder</type> |
611 | + <comment>Policy ignored due to local white list</comment> |
612 | + </reason> |
613 | + </policy_evaluated> |
614 | + </row> |
615 | + <identifiers> |
616 | + <envelope_from>jmap.bounce.topicbox.com</envelope_from> |
617 | + <header_from>stalw.art</header_from> |
618 | + </identifiers> |
619 | + <auth_results> |
620 | + <dkim> |
621 | + <domain>jmap.topicbox.com</domain> |
622 | + <selector>dkim-1</selector> |
623 | + <result>pass</result> |
624 | + <human_result>pass</human_result> |
625 | + </dkim> |
626 | + <spf> |
627 | + <domain>jmap.bounce.topicbox.com</domain> |
628 | + <scope>mfrom</scope> |
629 | + <result>pass</result> |
630 | + </spf> |
631 | + </auth_results> |
632 | + </record> |
633 | + <record> |
634 | + <row> |
635 | + <source_ip>64.147.108.173</source_ip> |
636 | + <count>1</count> |
637 | + <policy_evaluated> |
638 | + <disposition>none</disposition> |
639 | + <dkim>fail</dkim> |
640 | + <spf>fail</spf> |
641 | + <reason> |
642 | + <type>trusted_forwarder</type> |
643 | + <comment>Policy ignored due to local white list</comment> |
644 | + </reason> |
645 | + </policy_evaluated> |
646 | + </row> |
647 | + <identifiers> |
648 | + <envelope_from>jmap.bounce.topicbox.com</envelope_from> |
649 | + <header_from>stalw.art</header_from> |
650 | + </identifiers> |
651 | + <auth_results> |
652 | + <dkim> |
653 | + <domain>jmap.topicbox.com</domain> |
654 | + <selector>dkim-1</selector> |
655 | + <result>pass</result> |
656 | + <human_result>pass</human_result> |
657 | + </dkim> |
658 | + <spf> |
659 | + <domain>jmap.bounce.topicbox.com</domain> |
660 | + <scope>mfrom</scope> |
661 | + <result>pass</result> |
662 | + </spf> |
663 | + </auth_results> |
664 | + </record> |
665 | + <record> |
666 | + <row> |
667 | + <source_ip>54.240.8.13</source_ip> |
668 | + <count>1</count> |
669 | + <policy_evaluated> |
670 | + <disposition>none</disposition> |
671 | + <dkim>pass</dkim> |
672 | + <spf>fail</spf> |
673 | + </policy_evaluated> |
674 | + </row> |
675 | + <identifiers> |
676 | + <envelope_from>amazonses.com</envelope_from> |
677 | + <header_from>stalw.art</header_from> |
678 | + </identifiers> |
679 | + <auth_results> |
680 | + <dkim> |
681 | + <domain>stalw.art</domain> |
682 | + <selector>kvoujh5guu2jpweurhpt2ioscpkpukc3</selector> |
683 | + <result>pass</result> |
684 | + <human_result>pass</human_result> |
685 | + </dkim> |
686 | + <dkim> |
687 | + <domain>amazonses.com</domain> |
688 | + <selector>6gbrjpgwjskckoa6a5zn6fwqkn67xbtw</selector> |
689 | + <result>pass</result> |
690 | + <human_result>pass</human_result> |
691 | + </dkim> |
692 | + <spf> |
693 | + <domain>amazonses.com</domain> |
694 | + <scope>mfrom</scope> |
695 | + <result>pass</result> |
696 | + </spf> |
697 | + </auth_results> |
698 | + </record> |
699 | + </feedback> |
700 | diff --git a/resources/dmarc-feedback/100.eml b/resources/dmarc-feedback/100.eml |
701 | new file mode 100644 |
702 | index 0000000..9a2ed1d |
703 | --- /dev/null |
704 | +++ b/resources/dmarc-feedback/100.eml |
705 | @@ -0,0 +1,66 @@ |
706 | + Received: from mail.stalw.art ([mail.stalw.art]) |
707 | + by 127.0.0.1 (Stalwart JMAP) with LMTP; |
708 | + Mon, 28 Nov 2022 10:51:56 +0000 |
709 | + Received: from mail-qv1-xf4a.google.com (mail-qv1-xf4a.google.com [IPv6:2607:f8b0:4864:20::f4a]) |
710 | + (using TLSv1.3 with cipher TLS_AES_128_GCM_SHA256 (128/128 bits) |
711 | + key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) |
712 | + (No client certificate requested) |
713 | + by mail.stalw.art (Postfix) with ESMTPS id 1145E7CC0B |
714 | + for <domains@stalw.art>; Mon, 28 Nov 2022 10:51:53 +0000 (UTC) |
715 | + Received: by mail-qv1-xf4a.google.com with SMTP id 71-20020a0c804d000000b004b2fb260447so12985969qva.10 |
716 | + for <domains@stalw.art>; Mon, 28 Nov 2022 02:51:52 -0800 (PST) |
717 | + DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; |
718 | + d=google.com; s=20210112; |
719 | + h=content-transfer-encoding:content-disposition:to:from:subject |
720 | + :message-id:date:mime-version:from:to:cc:subject:date:message-id |
721 | + :reply-to; |
722 | + bh=sMF/38UFRhmUYFRJST4vLBu/U1BXgsdCUE02HF8nXx8=; |
723 | + b=I7WONP7tMsULp4eKjJeeKtM+nDYqMSIYMxqNHqCP1bTsnUiW2xM278I2+F8EjtFNYf |
724 | + XOgusNn8kqbSnA4w1+q4G87zTF4K3tGnxNpuUMQ7GzcofBKtr7VPv9XFqvTPJ+N8YSwe |
725 | + 926ec7xi71BpSHAgqp5Wqocj8ruIVjcCZ37hYrG0C4s+FVBtbaU3EeyPpkESaaY2vE5y |
726 | + Qa2KsrMsyJXlbyW/sFJ7AGDDuXwyGkTa+btP/xIiQM2HlBKy7vNOFZKkxInOuQsXJgZy |
727 | + 3H7ivlpD3hMrszwU77o5jBArVwN0RIkUSosAPQf+pzgvRlkseRlDrmzKQutvYWIaTP3/ |
728 | + FHPA== |
729 | + X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; |
730 | + d=1e100.net; s=20210112; |
731 | + h=content-transfer-encoding:content-disposition:to:from:subject |
732 | + :message-id:date:mime-version:x-gm-message-state:from:to:cc:subject |
733 | + :date:message-id:reply-to; |
734 | + bh=sMF/38UFRhmUYFRJST4vLBu/U1BXgsdCUE02HF8nXx8=; |
735 | + b=D3oClvT5AKcTpEjjffHQqPPQ9j5mmtExiviSq7iBYkoq+322LtR2hGqGxtvlAwRDsQ |
736 | + VIfuKVExygw3c9bckjzKtJYX128HGK35gHnmsrzqvCC93JlRaC/55kcM9Bhks0xJnl7i |
737 | + yNFHPZ0DY/jasdUdQ1QqnI+8qiPy+/12JvD+/TGlaDuS+RWYFU4/ky46S3vMXwXmRt6D |
738 | + IGggXoW7snSaM4s88DzMUl0U7DH823UPQrUxnA5Oxscwn9M1ENJUWD/3EJo5ZEUMw0ll |
739 | + Y8AlyhjWFgVqs1Y4V/LVWeXdF10fpm78+jm8QyIZYZJjh4I33AekdsWVM71ZNNYGL0+8 |
740 | + +GDg== |
741 | + X-Gm-Message-State: ANoB5pn0zRZSWXdFXd9G0tawbSeUYuhxToVkIYoLf8OJzoBLcIc0wKcB |
742 | + AfU9Coz5vAuiM1mASWJhbg== |
743 | + X-Google-Smtp-Source: AA0mqf6WWsnqMD4cHE40jB89/zblmT7yNKeHKlsvvCmlYANKmpKLTQTaCm5qCA0mVmxR/PTQogsNndWH/qe0ug== |
744 | + MIME-Version: 1.0 |
745 | + X-Received: by 2002:ac8:5182:0:b0:39c:cb6a:300b with SMTP id |
746 | + c2-20020ac85182000000b0039ccb6a300bmr48409299qtn.181.1669632711968; Mon, 28 |
747 | + Nov 2022 02:51:51 -0800 (PST) |
748 | + Date: Sun, 27 Nov 2022 15:59:59 -0800 |
749 | + Message-ID: <5264580628977113351@google.com> |
750 | + Subject: Report domain: stalw.art Submitter: google.com Report-ID: 5264580628977113351 |
751 | + From: noreply-dmarc-support@google.com |
752 | + To: domains@stalw.art |
753 | + Content-Type: application/zip; |
754 | + name="google.com!stalw.art!1669507200!1669593599.zip" |
755 | + Content-Disposition: attachment; |
756 | + filename="google.com!stalw.art!1669507200!1669593599.zip" |
757 | + Content-Transfer-Encoding: base64 |
758 | + |
759 | + UEsDBAoAAAAIAHFUfFWAeOSU8QEAAKkEAAAuAAAAZ29vZ2xlLmNvbSFzdGFsdy5hcnQhMTY2OTUw |
760 | + NzIwMCExNjY5NTkzNTk5LnhtbKVUwZKjIBC9z1ekck9Qk5hoMcye9gt2zxbB1lBBoACTmb9fHNCw |
761 | + ma257El83f2632sUv70PYnUDY7mSr+t8m61XIJlquexf179//dyc1qs38oI7gPZM2ZW8rFbYgFbG |
762 | + NQM42lJHJ8yjyvSNpAOQXqlewJapAaMFDDkwUC6IVJ5BfGzagRq2saOe6H6kZSEv1rw7QxumpKPM |
763 | + NVx2ilyc07ZGKJZuH6WIIirtHQwq9mV5OGWe62t9II4yeEsORbn3uWVxqo7HPN/tDjlGj3BI91Kh |
764 | + MVT2UYyHztBzSfKyrA7Zsch8s4DMcZBtiFa7Q1X5UeRMhv5mW7qlnmKtBGcfjR7PgtsLLIMo744k |
765 | + 1lFx31LjPFlAQpi2Vz4Qg1E4RNDq7hObngHSfg8SMNLx3c6AnRHNHMknVdPhc8p/TeR9ZMrMwxl1 |
766 | + X+RbNRoGDdekoFle77uqZlme1+f9jtW1t/iRMJcwNUrfFKNwmOHYF25UjN64dg5MbnCrleXOX+A4 |
767 | + f4okeZMZmlrrExZfovAuBhZzEq1PPf2mZoWYtyAd77j/fJayC9AWTNMZNaQbSuHI86Ua09FdGgN2 |
768 | + FO5B+DTs98uP93piiJLiS6IWBDCnDLmB4FdujaayKLz2GV8MSDvjxJr/niIx2t/IJ9FTcrhPGD3+ |
769 | + On8AUEsBAgoACgAAAAgAcVR8VYB45JTxAQAAqQQAAC4AAAAAAAAAAAAAAAAAAAAAAGdvb2dsZS5j |
770 | + b20hc3RhbHcuYXJ0ITE2Njk1MDcyMDAhMTY2OTU5MzU5OS54bWxQSwUGAAAAAAEAAQBcAAAAPQIA |
771 | + AAAA |
772 | \ No newline at end of file |
773 | diff --git a/resources/dmarc-feedback/100.json b/resources/dmarc-feedback/100.json |
774 | new file mode 100644 |
775 | index 0000000..36b48c7 |
776 | --- /dev/null |
777 | +++ b/resources/dmarc-feedback/100.json |
778 | @@ -0,0 +1,63 @@ |
779 | + { |
780 | + "version": 0.0, |
781 | + "report_metadata": { |
782 | + "org_name": "google.com", |
783 | + "email": "noreply-dmarc-support@google.com", |
784 | + "extra_contact_info": "https://support.google.com/a/answer/2466580", |
785 | + "report_id": "5264580628977113351", |
786 | + "date_range": { |
787 | + "begin": 1669507200, |
788 | + "end": 1669593599 |
789 | + }, |
790 | + "error": [] |
791 | + }, |
792 | + "policy_published": { |
793 | + "domain": "stalw.art", |
794 | + "version_published": null, |
795 | + "adkim": "Relaxed", |
796 | + "aspf": "Relaxed", |
797 | + "p": "None", |
798 | + "sp": "None", |
799 | + "testing": false, |
800 | + "fo": null |
801 | + }, |
802 | + "record": [ |
803 | + { |
804 | + "row": { |
805 | + "source_ip": "2a01:4f9:c011:b43c::1", |
806 | + "count": 1, |
807 | + "policy_evaluated": { |
808 | + "disposition": "None", |
809 | + "dkim": "Pass", |
810 | + "spf": "Pass", |
811 | + "reason": [] |
812 | + } |
813 | + }, |
814 | + "identifiers": { |
815 | + "envelope_to": null, |
816 | + "envelope_from": "", |
817 | + "header_from": "stalw.art" |
818 | + }, |
819 | + "auth_results": { |
820 | + "dkim": [ |
821 | + { |
822 | + "domain": "stalw.art", |
823 | + "selector": "velikisrpan22", |
824 | + "result": "Pass", |
825 | + "human_result": null |
826 | + } |
827 | + ], |
828 | + "spf": [ |
829 | + { |
830 | + "domain": "stalw.art", |
831 | + "scope": "Unspecified", |
832 | + "result": "Pass", |
833 | + "human_result": null |
834 | + } |
835 | + ] |
836 | + }, |
837 | + "extensions": [] |
838 | + } |
839 | + ], |
840 | + "extensions": [] |
841 | + } |
842 | \ No newline at end of file |
843 | diff --git a/resources/dmarc-feedback/101.eml b/resources/dmarc-feedback/101.eml |
844 | new file mode 100644 |
845 | index 0000000..3a66174 |
846 | --- /dev/null |
847 | +++ b/resources/dmarc-feedback/101.eml |
848 | @@ -0,0 +1,68 @@ |
849 | + Received: from mail.stalw.art ([mail.stalw.art]) |
850 | + by 127.0.0.1 (Stalwart JMAP) with LMTP; |
851 | + Thu, 10 Nov 2022 03:27:19 +0000 |
852 | + Received: from mx0.backschues.net (lnxs001.backschues.net [85.183.142.13]) |
853 | + (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) |
854 | + key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) |
855 | + (No client certificate requested) |
856 | + by mail.stalw.art (Postfix) with ESMTPS id 6DD117CC0B |
857 | + for <domains@stalw.art>; Thu, 10 Nov 2022 03:27:16 +0000 (UTC) |
858 | + Received: from mx0.backschues.net (localhost [127.0.0.1]) |
859 | + by mx0.backschues.net with SMTP id 4N76hg4lNgz9ryP |
860 | + for <domains@stalw.art>; Thu, 10 Nov 2022 04:27:15 +0100 (CET) |
861 | + DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=backschues.net; |
862 | + s=mail-2014-01; t=1668050835; |
863 | + h=from:from:reply-to:subject:subject:date:date:message-id:message-id: |
864 | + to:to:cc:mime-version:mime-version:content-type:content-type; |
865 | + bh=LTj1tdFz9JQFL/mVJASN0b9hGcolcCtY5v0bhnChJYY=; |
866 | + b=AtRegYc51PTYqDOy/6fB4xETTWAbVc2ivf8AfF4ygu3+6+oqBPyloTuOnEt7xYmjLFnll/ |
867 | + SMZFFpRETsMlkiVg/1O0VpPRpIpiTbh4dwtUrRyo1Uw/cDJv5auz4rBMxcRNnDKypHwUKs |
868 | + BUahHWsVKH/TL5SzV79kqyjlYAs1HdJvS+wRINYBaptkeT6UeHGZakL21NnQUdOGt0fj4y |
869 | + eJvWVtCYHZ5DUJ8K8h2W1NlTAWP8nTBoQVVDQrI5Zi1AEvnUWw+H7E8d/q2cF756/IBYso |
870 | + rT56D3PYo2iuSt3aIBth1wL7/GJwc6N4JHcNpJ9XPV6xQbt+lm2b3+W59osL0Q== |
871 | + DKIM-Signature: v=1; a=ed25519-sha256; c=relaxed/relaxed; d=backschues.net; |
872 | + s=ed25519-mail-2018-10; t=1668050835; |
873 | + h=from:from:reply-to:subject:subject:date:date:message-id:message-id: |
874 | + to:to:cc:mime-version:mime-version:content-type:content-type; |
875 | + bh=LTj1tdFz9JQFL/mVJASN0b9hGcolcCtY5v0bhnChJYY=; |
876 | + b=y7d79OWWCrDX40k91FoBdGnUcrjN7xvWYyqskPfQmMoaSqFNSlTHH8gMXC/vXwiYIP3Oxp |
877 | + d/hVvEuIIQBlwMDQ== |
878 | + From: "DMARC Aggregate Report" <noreply-dmarc-support@backschues.net> |
879 | + To: domains@stalw.art |
880 | + Subject: Report Domain: stalw.art |
881 | + Submitter: backschues.net |
882 | + Report-ID: stalw.art.1667948400.1668034800 |
883 | + Date: Thu, 10 Nov 2022 03:27:02 GMT |
884 | + MIME-Version: 1.0 |
885 | + Message-ID: <afe541eab3ec091f@backschues.net> |
886 | + Content-Type: multipart/mixed; |
887 | + boundary="----=_NextPart_84e1fdd0-b285-4922-9fc7-88b070204303" |
888 | + |
889 | + This is a multipart message in MIME format. |
890 | + |
891 | + ------=_NextPart_84e1fdd0-b285-4922-9fc7-88b070204303 |
892 | + Content-Type: text/plain; charset="us-ascii" |
893 | + Content-Transfer-Encoding: 7bit |
894 | + |
895 | + This is an aggregate report from backschues.net. |
896 | + |
897 | + Report domain: stalw.art |
898 | + Submitter: backschues.net |
899 | + Report ID: stalw.art.1667948400.1668034800 |
900 | + |
901 | + ------=_NextPart_84e1fdd0-b285-4922-9fc7-88b070204303 |
902 | + Content-Type: application/gzip |
903 | + Content-Transfer-Encoding: base64 |
904 | + Content-Disposition: attachment; |
905 | + filename="backschues.net!stalw.art!1667948400!1668034800.xml.gz" |
906 | + |
907 | + H4sIAAAAAAAAA5VUsXLbMAzd/RU6D9ksSo6b2heG6dKOndJZR5OQzYtEsiSVNH9fUqQoqXWHT |
908 | + gIfgAfgASf8/KvvijcwVij5tK3LaluAZIoLeXna/nj5tjtui2eywS0AP1P2SjZFgQ1oZVzTg6 |
909 | + OcOhowjypzaSTtgdz9HJR7DNGWXQew5fevLxhld4yGnoqOSOW5uo8d76lhOzvoQPxlkSrBYRR |
910 | + jY16qLTixjnbvJTWurB8ePp8Ox0NVBfNY3R+OVYXRHBpTfa/QGCovqQcPneEiJJnzMYrI5AfJ |
911 | + yZIyvCMZWrPlaktRsFadYB+NHs6dsFfIjSg/kJwH8GQRiW7KX0VPDEbRSKDV7YiFb4S0l08CR |
912 | + jq97QTYCdHMkTr0HYyxy7878ooyZXhcrHqfuNRgGDRCk09Vud/fl/X+VNangyfPnhjJ1CB9FY |
913 | + yikQrHMvBGu8HrxLOgXFitrHD+3FKzSyRHhblbv3TvzhKME7YJnlVAt2r5dcRRsOAgnWiFP/G |
914 | + UcAXKwTStUf1yBUt4ZPgjE9PBXRsDdujcRLVqLu1QgGtLf+zrpY6XG1KJptaGaxkf0y0Fnpts |
915 | + /7iRmS7KcYNukxX7zwbjWtaMicaf30qEEBaPB6P8h/gNNHLX4VQEAAA= |
916 | + ------=_NextPart_84e1fdd0-b285-4922-9fc7-88b070204303-- |
917 | diff --git a/resources/dmarc-feedback/101.json b/resources/dmarc-feedback/101.json |
918 | new file mode 100644 |
919 | index 0000000..53d7e63 |
920 | --- /dev/null |
921 | +++ b/resources/dmarc-feedback/101.json |
922 | @@ -0,0 +1,69 @@ |
923 | + { |
924 | + "version": 0.0, |
925 | + "report_metadata": { |
926 | + "org_name": "\"backschues.NET", |
927 | + "email": "noreply-dmarc-support@backschues.net", |
928 | + "extra_contact_info": null, |
929 | + "report_id": "stalw.art.1667948400.1668034800", |
930 | + "date_range": { |
931 | + "begin": 1667948400, |
932 | + "end": 1668034800 |
933 | + }, |
934 | + "error": [] |
935 | + }, |
936 | + "policy_published": { |
937 | + "domain": "stalw.art", |
938 | + "version_published": null, |
939 | + "adkim": "Relaxed", |
940 | + "aspf": "Relaxed", |
941 | + "p": "None", |
942 | + "sp": "None", |
943 | + "testing": false, |
944 | + "fo": null |
945 | + }, |
946 | + "record": [ |
947 | + { |
948 | + "row": { |
949 | + "source_ip": "50.223.129.194", |
950 | + "count": 1, |
951 | + "policy_evaluated": { |
952 | + "disposition": "None", |
953 | + "dkim": "Fail", |
954 | + "spf": "Fail", |
955 | + "reason": [] |
956 | + } |
957 | + }, |
958 | + "identifiers": { |
959 | + "envelope_to": null, |
960 | + "envelope_from": "", |
961 | + "header_from": "stalw.art" |
962 | + }, |
963 | + "auth_results": { |
964 | + "dkim": [ |
965 | + { |
966 | + "domain": "ietf.org", |
967 | + "selector": "", |
968 | + "result": "Pass", |
969 | + "human_result": null |
970 | + }, |
971 | + { |
972 | + "domain": "stalw.art", |
973 | + "selector": "", |
974 | + "result": "Fail", |
975 | + "human_result": null |
976 | + } |
977 | + ], |
978 | + "spf": [ |
979 | + { |
980 | + "domain": "ietf.org", |
981 | + "scope": "Unspecified", |
982 | + "result": "None", |
983 | + "human_result": null |
984 | + } |
985 | + ] |
986 | + }, |
987 | + "extensions": [] |
988 | + } |
989 | + ], |
990 | + "extensions": [] |
991 | + } |
992 | \ No newline at end of file |
993 | diff --git a/resources/dmarc-feedback/102.eml b/resources/dmarc-feedback/102.eml |
994 | new file mode 100644 |
995 | index 0000000..7acd93e |
996 | --- /dev/null |
997 | +++ b/resources/dmarc-feedback/102.eml |
998 | @@ -0,0 +1,52 @@ |
999 | + Received: from mail.stalw.art ([mail.stalw.art]) |
1000 | + by 127.0.0.1 (Stalwart JMAP) with LMTP; |
1001 | + Tue, 08 Nov 2022 23:26:41 +0000 |
1002 | + Received: from relay7.m.smailru.net (relay7.m.smailru.net [94.100.178.51]) |
1003 | + (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) |
1004 | + (No client certificate requested) |
1005 | + by mail.stalw.art (Postfix) with ESMTPS id DD4337CC09 |
1006 | + for <domains@stalw.art>; Tue, 8 Nov 2022 23:26:38 +0000 (UTC) |
1007 | + DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=corp.mail.ru; s=mail4; |
1008 | + h=Date:Message-ID:To:From:Subject:MIME-Version:Content-Type:From:Subject:Content-Type:Content-Transfer-Encoding:To:Cc; bh=fooa0+RBCZvyV2mP8Nx/UsLQ5RhazFg+SPGNtxZrCX0=; |
1009 | + t=1667950001;x=1668040001; |
1010 | + b=J9aMEkY9eVdOxjkNxaPFJ2Yk+/NCux9uOZl3iJXI0hEFaeYj9g7l+WtmXczk+YvgH3yhVhtvONUEYFsValRWWCAfmePm429N3mSuclVktk7t6RPJ4O5EcMjwrD9882vmX1xpI7ecPOzd5AD67HPt5SIA1RIa5injaOI5CWUXBBa5c0zDfmciyANAiDw0gm1axEMK4AUc61txPsX7H1qRq/FxGNITnnpYdqkkT2lR8sTl5HPwTjEsw4sYGKr5SiMpROhhbLZTM8RpojkP73bmw3UBZ9FI8iKApJUFB8i9tu0hjzHkev4uoDXgOXFYs/RAI1JkCWEp2Rjb3LpTSHT6cA==; |
1011 | + Received: from [10.161.4.115] (port=60844 helo=60) |
1012 | + by relay7.m.smailru.net with esmtp (envelope-from <dmarc_support@corp.mail.ru>) |
1013 | + id 1osXzK-0007VC-BD |
1014 | + for domains@stalw.art; Wed, 09 Nov 2022 02:26:38 +0300 |
1015 | + Content-Type: multipart/mixed; boundary="===============5640625649776607409==" |
1016 | + MIME-Version: 1.0 |
1017 | + Subject: Report Domain: stalw.art; Submitter: Mail.Ru; |
1018 | + Report-ID: 28551467700969547611667865600 |
1019 | + From: dmarc_support@corp.mail.ru |
1020 | + To: domains@stalw.art |
1021 | + Message-ID: <dmarc-1667949998@corp.mail.ru> |
1022 | + Date: Wed, 09 Nov 2022 02:26:38 +0300 |
1023 | + Auto-Submitted: auto-generated |
1024 | + Authentication-Results: relay7.m.smailru.net; auth=pass smtp.auth=dmarc_support@corp.mail.ru smtp.mailfrom=dmarc_support@corp.mail.ru; iprev=pass policy.iprev=10.161.4.115 |
1025 | + |
1026 | + --===============5640625649776607409== |
1027 | + MIME-Version: 1.0 |
1028 | + Content-Type: text/plain; charset="utf-8" |
1029 | + Content-Transfer-Encoding: base64 |
1030 | + |
1031 | + VGhpcyBpcyBhbiBhZ2dyZWdhdGUgcmVwb3J0IGZyb20gTWFpbC5SdS4= |
1032 | + |
1033 | + --===============5640625649776607409== |
1034 | + Content-Type: application/gzip |
1035 | + MIME-Version: 1.0 |
1036 | + Content-Transfer-Encoding: base64 |
1037 | + Content-Disposition: attachment; |
1038 | + filename="mail.ru!stalw.art!1667865600!1667952000.xml.gz" |
1039 | + |
1040 | + H4sICK7lamMC/21haWwucnUhc3RhbHcuYXJ0ITE2Njc4NjU2MDAhMTY2Nzk1MjAwMC54bWwAdVNB |
1041 | + cqMwELzvK3LzKQhYg01qouwHctkPULIYjMogqSThJL/fEQSC18kFzbRmWt0jAS/vQ/9wReeV0c+7 |
1042 | + LEl3D6ilaZQ+P+/G0D4edy/8F7SIzUnICweH1rhQDxhEI4LgYNy51mJA/ipUn/wdga0I4EAYbwbh |
1043 | + ZO1HGzv/SONsEvHEUe1cAfgenKil0UHIUCvdGt6FYJ8Y67Bfy1lcHyNCjfcdizbV8PxYFNm+PBzS |
1044 | + tCqrYn8os6wsD8eyKNMU2FchkAmsndBnknvCs9J8WzgjgLqZ4KrI0wjHHNi2ld3NxZpeyY/ajqde |
1045 | + +Q7jUYb0a+6D6N8S4QIxzAiI5qIG7oDNAQhv2ymNK1iujUZgloNfYgrAysCzKCcG9L070CENO67m |
1046 | + jVrN6CTWyvIiTfL8d5LlVZJVe+Jad0CaURMpsDlYTOBV9CO5jSaUt8arQO/lU8oWgUl/S9dE+GQl |
1047 | + OpjzyQu7Z2STPNWgDqpV9BQ5dCgadHXrzLAd1xYGdtMhxtDVDv3YB/+pYpm3wtAm9Ca/xu2xRxmM |
1048 | + m7bI7JrDzMCt8D5e6ZQsTm5Iv7nEleWKvboo76zQef4N+zyO/9in6fysWBqLfIjOiXBKftA6T/l2 |
1049 | + HGx5CGz9j/8BQWPZIPkDAAA= |
1050 | + --===============5640625649776607409==-- |
1051 | \ No newline at end of file |
1052 | diff --git a/resources/dmarc-feedback/102.json b/resources/dmarc-feedback/102.json |
1053 | new file mode 100644 |
1054 | index 0000000..3c604e0 |
1055 | --- /dev/null |
1056 | +++ b/resources/dmarc-feedback/102.json |
1057 | @@ -0,0 +1,69 @@ |
1058 | + { |
1059 | + "version": 0.0, |
1060 | + "report_metadata": { |
1061 | + "org_name": "Mail.Ru", |
1062 | + "email": "dmarc_support@corp.mail.ru", |
1063 | + "extra_contact_info": "http://help.mail.ru/mail-help", |
1064 | + "report_id": "28551467700969547611667865600", |
1065 | + "date_range": { |
1066 | + "begin": 1667865600, |
1067 | + "end": 1667952000 |
1068 | + }, |
1069 | + "error": [] |
1070 | + }, |
1071 | + "policy_published": { |
1072 | + "domain": "stalw.art", |
1073 | + "version_published": null, |
1074 | + "adkim": "Relaxed", |
1075 | + "aspf": "Relaxed", |
1076 | + "p": "None", |
1077 | + "sp": "None", |
1078 | + "testing": false, |
1079 | + "fo": null |
1080 | + }, |
1081 | + "record": [ |
1082 | + { |
1083 | + "row": { |
1084 | + "source_ip": "50.223.129.194", |
1085 | + "count": 1, |
1086 | + "policy_evaluated": { |
1087 | + "disposition": "None", |
1088 | + "dkim": "Fail", |
1089 | + "spf": "Fail", |
1090 | + "reason": [] |
1091 | + } |
1092 | + }, |
1093 | + "identifiers": { |
1094 | + "envelope_to": null, |
1095 | + "envelope_from": "", |
1096 | + "header_from": "stalw.art" |
1097 | + }, |
1098 | + "auth_results": { |
1099 | + "dkim": [ |
1100 | + { |
1101 | + "domain": "ietf.org", |
1102 | + "selector": "ietf1", |
1103 | + "result": "Pass", |
1104 | + "human_result": null |
1105 | + }, |
1106 | + { |
1107 | + "domain": "stalw.art", |
1108 | + "selector": "velikisrpan22", |
1109 | + "result": "Fail", |
1110 | + "human_result": null |
1111 | + } |
1112 | + ], |
1113 | + "spf": [ |
1114 | + { |
1115 | + "domain": "ietf.org", |
1116 | + "scope": "MailFrom", |
1117 | + "result": "Pass", |
1118 | + "human_result": null |
1119 | + } |
1120 | + ] |
1121 | + }, |
1122 | + "extensions": [] |
1123 | + } |
1124 | + ], |
1125 | + "extensions": [] |
1126 | + } |
1127 | \ No newline at end of file |
1128 | diff --git a/resources/dmarc-feedback/103.eml b/resources/dmarc-feedback/103.eml |
1129 | new file mode 100644 |
1130 | index 0000000..2fe512f |
1131 | --- /dev/null |
1132 | +++ b/resources/dmarc-feedback/103.eml |
1133 | @@ -0,0 +1,126 @@ |
1134 | + Received: from mail.stalw.art ([mail.stalw.art]) by 127.0.0.1 (Stalwart JMAP) with LMTP; Tue, 25 Oct 2022 04:08:22 +0000 |
1135 | + Received: from NAM12-MW2-obe.outbound.protection.outlook.com (mail-mw2nam12on2073.outbound.protection.outlook.com [40.107.244.73]) |
1136 | + (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) |
1137 | + (No client certificate requested) |
1138 | + by mail.stalw.art (Postfix) with ESMTPS id A24107CC0A |
1139 | + for <domains@stalw.art>; Tue, 25 Oct 2022 04:08:22 +0000 (UTC) |
1140 | + ARC-Seal: i=1; a=rsa-sha256; s=arcselector9901; d=microsoft.com; cv=none; |
1141 | + b=SvolQ1oIEgdfCI6dbwmJ1jS0ovWmprW6kT3q9NgrbX+CMhIsdrqyS3Q1sO16KT2wCQAyNofiEZ5tKY0e1PzzMqeR29jUWvEye9T43fCfUeLFx9b45YrfkGYwqLeDIq0Ywl+ggVmsm7X83XqI6+9EC6qMukCb0cbLazu3rW/Rbyc6d5+fq6QTFZovATGRvHz71H9t7e//hYI23XjU5Q3Enw0Qq3xPSyusWDi3t7CfGXn9i2120XlNLnPxef5PCmwy4E+OTJ5qC5WtMthOskKKuFvx8onOYmc/JjJ3VrtZwALx9C+ulzix5US6H7pFvZ2jtDbMnW4U7ir/hp5xn5adFw== |
1142 | + ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=microsoft.com; |
1143 | + s=arcselector9901; |
1144 | + h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-AntiSpam-MessageData-ChunkCount:X-MS-Exchange-AntiSpam-MessageData-0:X-MS-Exchange-AntiSpam-MessageData-1; |
1145 | + bh=65M39Uvc5w8zbmK6TxEdoblSMXlIyqTXJHNalJ80wk4=; |
1146 | + b=PN7QPeXJLr6tmH2CxydbDQjHqBtFKNN9HjGimeUHaIeSr82WHf4R295QbVX7gxw6sFE7Z9lZMTrMSqbRVI7rhbx+SEkxCfAothf9207FDX6t37Zt0wd/5EwR6dzfbcNJBL+U0/iG4J03L5b1geWY+e68mHKYH4/ybGcr+SBKuv/LgfZNtOfbQ3ioiKvFcpSDqd/qGUs4U9l2tVlXgbcKkct04sCuPciqgLEuIGirPLLbDUaBRJc51ZZB6CeporySRdHp6uFXyy3VBvvLVuwDNnnPrW4BUL05AuutzK7rc8ZQEpWf7r0gUEg2ArSrvs6Znnfe97oRa01L2SeFwuZsMA== |
1147 | + ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=none; dmarc=none |
1148 | + action=none header.from=microsoft.com; dkim=none (message not signed); |
1149 | + arc=none |
1150 | + DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; |
1151 | + d=notification.microsoft.com; s=selector1; |
1152 | + h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck; |
1153 | + bh=65M39Uvc5w8zbmK6TxEdoblSMXlIyqTXJHNalJ80wk4=; |
1154 | + b=NjqsA7D6sEq1WgCZ/E1f5/B+XUXe5F4uv6CF2KQYVuyRnxItdox09LCWqZQ+fNQ6BbJ4Ne05Cb1BPbPP9yvb8Y6B1s2QvuxkUb69UFbAhoFgsRT6A4K76ykKQQyiPoYpxlO6FEyy+gel4y7c9XRLiWW6OxMIBcjBGB5ziP7mGFaJx4qXJ2mROfO7uZfrCu5pzOimkjPw6extWv4i0Kl3XKvBtXZnsr9eoC10mJvEAp7E2cpnaZnP46RQc9cmXzlmvhKPvCQCUWipJN9f1BTTvFjJ9ff6ehmN9RSzCckj3SZGw9XAnd0WYqh4evt6Y1RxQ4iQDSaZHNRpyMOtmkWc/w== |
1155 | + Authentication-Results: dkim=none (message not signed) |
1156 | + header.d=none;dmarc=none action=none header.from=microsoft.com; |
1157 | + Received: from BN9PR03CA0046.namprd03.prod.outlook.com (2603:10b6:408:fb::21) |
1158 | + by SJ0PR18MB3916.namprd18.prod.outlook.com (2603:10b6:a03:2c9::21) with |
1159 | + Microsoft SMTP Server (version=TLS1_2, |
1160 | + cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.5746.21; Tue, 25 Oct |
1161 | + 2022 04:08:19 +0000 |
1162 | + Received: from BN7NAM10FT048.eop-nam10.prod.protection.outlook.com |
1163 | + (2603:10b6:408:fb:cafe::d7) by BN9PR03CA0046.outlook.office365.com |
1164 | + (2603:10b6:408:fb::21) with Microsoft SMTP Server (version=TLS1_2, |
1165 | + cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.5746.27 via Frontend |
1166 | + Transport; Tue, 25 Oct 2022 04:08:19 +0000 |
1167 | + Received: from nam10.map.protection.outlook.com (2a01:111:f400:fe53::30) by |
1168 | + BN7NAM10FT048.mail.protection.outlook.com (2a01:111:e400:7e8f::199) with |
1169 | + Microsoft SMTP Server (version=TLS1_2, |
1170 | + cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.5746.16 via Frontend |
1171 | + Transport; Tue, 25 Oct 2022 04:08:19 +0000 |
1172 | + Message-ID: <725cbfbe133940149987cfc528387235@microsoft.com> |
1173 | + X-Sender: <dmarcreport@microsoft.com> XATTRDIRECT=Originating XATTRORGID=xorgid:96f9e21d-a1c4-44a3-99e4-37191ac61848 |
1174 | + MIME-Version: 1.0 |
1175 | + From: "DMARC Aggregate Report" <dmarcreport@microsoft.com> |
1176 | + To: <domains@stalw.art> |
1177 | + Subject: =?utf-8?B?UmVwb3J0IERvbWFpbjogc3RhbHcuYXJ0IFN1Ym1pdHRlcjogcHJvdGVjdGlvbi5vdXRsb29rLmNvbSBSZXBvcnQtSUQ6IDcyNWNiZmJlMTMzOTQwMTQ5OTg3Y2ZjNTI4Mzg3MjM1?= |
1178 | + Content-Type: multipart/mixed; |
1179 | + boundary="_mpm_a4bcd9a515b44b9d8eceb05d7333675fpiotk5m200exchangecorpm_" |
1180 | + Date: Tue, 25 Oct 2022 04:08:19 +0000 |
1181 | + X-EOPAttributedMessage: 0 |
1182 | + X-MS-PublicTrafficType: Email |
1183 | + X-MS-TrafficTypeDiagnostic: BN7NAM10FT048:EE_|SJ0PR18MB3916:EE_ |
1184 | + X-MS-Office365-Filtering-Correlation-Id: 7f843e40-ccc6-4c17-1ce7-08dab63e8cd1 |
1185 | + X-MS-Exchange-SenderADCheck: 2 |
1186 | + X-MS-Exchange-AntiSpam-Relay: 0 |
1187 | + X-Microsoft-Antispam: BCL:0; |
1188 | + X-Microsoft-Antispam-Message-Info: |
1189 | + PSiI3L4DKj/cyRBl8/bmbyQMrr1DvsYEB1+aTn/3Y39oHnyJ5HXcxu6jNUl32WcPW6Gfqmhc6P1RFE5L/9ev0cWnqh4GgIs2qmHicLexPmMjP8viPdjb1N7TSSOv1hhXMT+gVLx889X5sltd4qpfIAWhoxNonjQpVIgt4VOVnbCWTu1hyOjOVplq0rKqIF04BQGHZnBRfkcD1No+mZrvx8RLWIwInU3fpPeGz77Wn3TIvHtzypR/d22WpZ8eHk3aIxxdjwp5WLg4unpiJaieyQN7BRhD/v6b3pLFVJP8Ii2+FGjTsKASczEL4dHnIoIrHYE0wwaFFPcSNzovLhzYguDV42EGS8Fm7soiew4ch+hICM0LPNTGTZIDe7wm2eSwhN2tkJK4QCfh1DON39jXninVr88ZlzMcDXnXpgvWHHiur8az7Gvs9zHH/1tFMsPVSh7BS+8fHEcBYpdtihrP22GcjbOd98IiTAs/dVzSy0TUg6WEgJO6oUklGjqVbi99CrNZI1BtLP4vH4aSlz9JYg4et6SxiJlKyoSzqUr2NN9/pyFdQ//5d/EEjKJz8CAcQmCjjPEObGFttT3maY2+zsa2THodZgpfMyDbA3WUKxE= |
1190 | + X-Forefront-Antispam-Report: |
1191 | + CIP:255.255.255.255;CTRY:;LANG:en;SCL:1;SRV:;IPV:NLI;SFV:NSPM;H:nam10.map.protection.outlook.com;PTR:;CAT:NONE;SFS:(13230022)(396003)(39860400002)(346002)(34036004)(366004)(376002)(136003)(47540400005)(451199015)(2616005)(52230400001)(121820200001)(83380400001)(166002)(86362001)(41300700001)(2906002)(4001150100001)(8936002)(316002)(5660300002)(235185007)(41320700001)(508600001)(6486002)(6512007)(6506007)(24736004)(108616005)(68406010)(85236043)(8676002)(10290500003)(6916009)(36736006)(36756003)(66899015);DIR:OUT;SFP:1101; |
1192 | + X-OriginatorOrg: dmarcrep.onmicrosoft.com |
1193 | + X-MS-Exchange-CrossTenant-OriginalArrivalTime: 25 Oct 2022 04:08:19.1682 |
1194 | + (UTC) |
1195 | + X-MS-Exchange-CrossTenant-Network-Message-Id: 7f843e40-ccc6-4c17-1ce7-08dab63e8cd1 |
1196 | + X-MS-Exchange-CrossTenant-AuthSource: BN7NAM10FT048.eop-nam10.prod.protection.outlook.com |
1197 | + X-MS-Exchange-CrossTenant-AuthAs: Internal |
1198 | + X-MS-Exchange-CrossTenant-Id: 96f9e21d-a1c4-44a3-99e4-37191ac61848 |
1199 | + X-MS-Exchange-CrossTenant-FromEntityHeader: Internet |
1200 | + X-MS-Exchange-Transport-CrossTenantHeadersStamped: SJ0PR18MB3916 |
1201 | + |
1202 | + This is a multi-part message in MIME format. |
1203 | + |
1204 | + --_mpm_a4bcd9a515b44b9d8eceb05d7333675fpiotk5m200exchangecorpm_ |
1205 | + Content-Type: multipart/related; |
1206 | + boundary="_rv_a4bcd9a515b44b9d8eceb05d7333675fpiotk5m200exchangecorpm_" |
1207 | + |
1208 | + --_rv_a4bcd9a515b44b9d8eceb05d7333675fpiotk5m200exchangecorpm_ |
1209 | + Content-Type: multipart/alternative; |
1210 | + boundary="_av_a4bcd9a515b44b9d8eceb05d7333675fpiotk5m200exchangecorpm_" |
1211 | + |
1212 | + --_av_a4bcd9a515b44b9d8eceb05d7333675fpiotk5m200exchangecorpm_ |
1213 | + |
1214 | + |
1215 | + --_av_a4bcd9a515b44b9d8eceb05d7333675fpiotk5m200exchangecorpm_ |
1216 | + Content-Type: text/html; charset=us-ascii |
1217 | + Content-Transfer-Encoding: base64 |
1218 | + |
1219 | + PGRpdiBzdHlsZSA9ImZvbnQtZmFtaWx5OlNlZ29lIFVJOyBmb250LXNpemU6MTRweDsiPlRoaXMgaX |
1220 | + MgYSBETUFSQyBhZ2dyZWdhdGUgcmVwb3J0IGZyb20gTWljcm9zb2Z0IENvcnBvcmF0aW9uLiBGb3Ig |
1221 | + RW1haWxzIHJlY2VpdmVkIGJldHdlZW4gMjAyMi0xMC0yMyAwMDowMDowMCBVVEMgdG8gMjAyMi0xMC |
1222 | + 0yNCAwMDowMDowMCBVVEMuPC8gZGl2PjxiciAvPjxiciAvPllvdSdyZSByZWNlaXZpbmcgdGhpcyBl |
1223 | + bWFpbCBiZWNhdXNlIHlvdSBoYXZlIGluY2x1ZGVkIHlvdXIgZW1haWwgYWRkcmVzcyBpbiB0aGUgJ3 |
1224 | + J1YScgdGFnIG9mIHlvdXIgRE1BUkMgcmVjb3JkIGluIEROUyBmb3Igc3RhbHcuYXJ0LiBQbGVhc2Ug |
1225 | + cmVtb3ZlIHlvdXIgZW1haWwgYWRkcmVzcyBmcm9tIHRoZSAncnVhJyB0YWcgaWYgeW91IGRvbid0IH |
1226 | + dhbnQgdG8gcmVjZWl2ZSB0aGlzIGVtYWlsLjxiciAvPjxiciAvPjxkaXYgc3R5bGUgPSJmb250LWZh |
1227 | + bWlseTpTZWdvZSBVSTsgZm9udC1zaXplOjEycHg7IGNvbG9yOiM2NjY2NjY7Ij5QbGVhc2UgZG8gbm |
1228 | + 90IHJlc3BvbmQgdG8gdGhpcyBlLW1haWwuIFRoaXMgbWFpbGJveCBpcyBub3QgbW9uaXRvcmVkIGFu |
1229 | + ZCB5b3Ugd2lsbCBub3QgcmVjZWl2ZSBhIHJlc3BvbnNlLiBGb3IgYW55IGZlZWRiYWNrL3N1Z2dlc3 |
1230 | + Rpb25zLCBraW5kbHkgbWFpbCB0byBkbWFyY3JlcG9ydGZlZWRiYWNrQG1pY3Jvc29mdC5jb20uPGJy |
1231 | + IC8+PGJyIC8+TWljcm9zb2Z0IHJlc3BlY3RzIHlvdXIgcHJpdmFjeS4gUmV2aWV3IG91ciBPbmxpbm |
1232 | + UgU2VydmljZXMgPGEgaHJlZiA9Imh0dHBzOi8vcHJpdmFjeS5taWNyb3NvZnQuY29tL2VuLXVzL3By |
1233 | + aXZhY3lzdGF0ZW1lbnQiPlByaXZhY3kgU3RhdGVtZW50PC9hPi48YnIgLz5PbmUgTWljcm9zb2Z0IF |
1234 | + dheSwgUmVkbW9uZCwgV0EsIFVTQSA5ODA1Mi48LyBkaXYgPg== |
1235 | + |
1236 | + --_av_a4bcd9a515b44b9d8eceb05d7333675fpiotk5m200exchangecorpm_-- |
1237 | + |
1238 | + --_rv_a4bcd9a515b44b9d8eceb05d7333675fpiotk5m200exchangecorpm_-- |
1239 | + |
1240 | + --_mpm_a4bcd9a515b44b9d8eceb05d7333675fpiotk5m200exchangecorpm_ |
1241 | + Content-Type: application/gzip |
1242 | + Content-Transfer-Encoding: base64 |
1243 | + Content-ID: <3ff45643-7977-4f3c-a97d-14b9e7faa5e7> |
1244 | + Content-Description: protection.outlook.com!stalw.art!1666483200!1666569600.xml.gz |
1245 | + Content-Disposition: attachment; filename="protection.outlook.com!stalw.art!1666483200!1666569600.xml.gz"; |
1246 | + |
1247 | + H4sIAAAAAAAEAM1VzY7bIBi8V+o7RLnXxHZ+VyzbB2jVQy+9WQTjBMUGBDjZvn0/G4JJsu3usZcE5h |
1248 | + vzDcNg45fXrp2dubFCyed5ni3mL+TzJ9xwXu8pO82gLO3Tq62f50fn9BNCl8slu5SZMgdULBY5+vX9 |
1249 | + 20925B2dR7J4n/xFSOuoZHwO7WYzHCQQUIDRdTJWDNfKuKrjjtbU0REEGJasJO04+dG7VqlTxlSHUU |
1250 | + QDCzqJltQdNcyv87UTzCirGucf8ITADq1ETTbFiu2bPc/Lcrdc5MvdbrthDVsV23K7KcoVRhM3PAzi |
1251 | + eGWoPFybA7bnBwF7Wq/Xy20JBmDkkUjgsh7Lq/VuPZSHeVgP3S0YW944gbVqBftd6X7fCnvkkxwFO5 |
1252 | + METG4vGTUO1vNIqNP6JDpiMPKDK2p1M4LDf8A0kUpyjPQVsFfERkgzR/JhA8MgYI0iAMCvV/+mULCc |
1253 | + KRNFG3WZvLGqN4xXQpPVIiuKMsuLXZbvltA3ViKZqV6CBIz8IOKhKz/Ttgc/61gZLBJWKyvcEDW/oR |
1254 | + RJiYNDDQQFGJNZwYsmVCbHkt3e94VDjFvEoubSiUZA2tNEnHmrNK+cIiqNdlp4ZDdGdURw1wx3LSGP |
1255 | + eKQfOa258WCSjBS+6nwUh2nvjpXhtm9dIvjekRCzSctN7rxpvOXMKTOS4MziPOH4PkRTa4fkj5PJ3p |
1256 | + um/7GEv12/Ww1wVuIkrNFUFsU/tfiovaMlTeIH3WCQFdINAYD24+TDPiRvCvSQkIEfLji8CsJHhfwB |
1257 | + wJC79XYGAAA= |
1258 | + |
1259 | + --_mpm_a4bcd9a515b44b9d8eceb05d7333675fpiotk5m200exchangecorpm_-- |
1260 | diff --git a/resources/dmarc-feedback/103.json b/resources/dmarc-feedback/103.json |
1261 | new file mode 100644 |
1262 | index 0000000..79d03a1 |
1263 | --- /dev/null |
1264 | +++ b/resources/dmarc-feedback/103.json |
1265 | @@ -0,0 +1,75 @@ |
1266 | + { |
1267 | + "version": 1.0, |
1268 | + "report_metadata": { |
1269 | + "org_name": "Outlook.com", |
1270 | + "email": "dmarcreport@microsoft.com", |
1271 | + "extra_contact_info": null, |
1272 | + "report_id": "725cbfbe133940149987cfc528387235", |
1273 | + "date_range": { |
1274 | + "begin": 1666483200, |
1275 | + "end": 1666569600 |
1276 | + }, |
1277 | + "error": [] |
1278 | + }, |
1279 | + "policy_published": { |
1280 | + "domain": "stalw.art", |
1281 | + "version_published": null, |
1282 | + "adkim": "Relaxed", |
1283 | + "aspf": "Relaxed", |
1284 | + "p": "None", |
1285 | + "sp": "None", |
1286 | + "testing": false, |
1287 | + "fo": "0" |
1288 | + }, |
1289 | + "record": [ |
1290 | + { |
1291 | + "row": { |
1292 | + "source_ip": "50.223.129.194", |
1293 | + "count": 1, |
1294 | + "policy_evaluated": { |
1295 | + "disposition": "None", |
1296 | + "dkim": "Fail", |
1297 | + "spf": "Fail", |
1298 | + "reason": [] |
1299 | + } |
1300 | + }, |
1301 | + "identifiers": { |
1302 | + "envelope_to": "outlook.com", |
1303 | + "envelope_from": "ietf.org", |
1304 | + "header_from": "stalw.art" |
1305 | + }, |
1306 | + "auth_results": { |
1307 | + "dkim": [ |
1308 | + { |
1309 | + "domain": "ietf.org", |
1310 | + "selector": "ietf1", |
1311 | + "result": "Pass", |
1312 | + "human_result": null |
1313 | + }, |
1314 | + { |
1315 | + "domain": "ietf.org", |
1316 | + "selector": "ietf1", |
1317 | + "result": "Pass", |
1318 | + "human_result": null |
1319 | + }, |
1320 | + { |
1321 | + "domain": "stalw.art", |
1322 | + "selector": "velikisrpan22", |
1323 | + "result": "Fail", |
1324 | + "human_result": null |
1325 | + } |
1326 | + ], |
1327 | + "spf": [ |
1328 | + { |
1329 | + "domain": "ietf.org", |
1330 | + "scope": "MailFrom", |
1331 | + "result": "Pass", |
1332 | + "human_result": null |
1333 | + } |
1334 | + ] |
1335 | + }, |
1336 | + "extensions": [] |
1337 | + } |
1338 | + ], |
1339 | + "extensions": [] |
1340 | + } |
1341 | \ No newline at end of file |
1342 | diff --git a/resources/dmarc-feedback/104.eml b/resources/dmarc-feedback/104.eml |
1343 | new file mode 100644 |
1344 | index 0000000..3463d8b |
1345 | --- /dev/null |
1346 | +++ b/resources/dmarc-feedback/104.eml |
1347 | @@ -0,0 +1,54 @@ |
1348 | + Received: from mail.stalw.art ([mail.stalw.art]) by 127.0.0.1 (Stalwart JMAP) with LMTP; Tue, 20 Sep 2022 10:28:19 +0000 |
1349 | + Received: from a14-92.smtp-out.amazonses.com (a14-92.smtp-out.amazonses.com [54.240.14.92]) |
1350 | + (using TLSv1.2 with cipher ECDHE-RSA-AES128-SHA256 (128/128 bits)) |
1351 | + (No client certificate requested) |
1352 | + by mail.stalw.art (Postfix) with ESMTPS id 1337D7E19D |
1353 | + for <domains@stalw.art>; Tue, 20 Sep 2022 10:28:18 +0000 (UTC) |
1354 | + DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/simple; |
1355 | + s=a66wkfbz3zwxdt2n5p6d7lj2ja7sdwuc; d=amazonses.com; t=1663669697; |
1356 | + h=From:To:Message-ID:Subject:MIME-Version:Content-Type:Date; |
1357 | + bh=h9v7dueDYfUxVokuKSLTqLuOwisdgdDRQ6TLwJOzXes=; |
1358 | + b=dHR5EJhoY9s8g2/Y4K4rHdz44k67r7fyC4wr2AWZmemrVBoxYHJPwa295S2VJQtY |
1359 | + kxTxppN2GEcNxhUMw8TXBrRwNKdoOLU38ZtrAN1a4hWVxmlwky1dtjXETQ/qJ257Nzg |
1360 | + bsXkAo4S1RABFmkQQJ0zSPZGkMW+lpZTBCDzlOHU= |
1361 | + DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/simple; |
1362 | + s=6gbrjpgwjskckoa6a5zn6fwqkn67xbtw; d=amazonses.com; t=1663669697; |
1363 | + h=From:To:Message-ID:Subject:MIME-Version:Content-Type:Date:Feedback-ID; |
1364 | + bh=h9v7dueDYfUxVokuKSLTqLuOwisdgdDRQ6TLwJOzXes=; |
1365 | + b=UDIvc6rvbihyGbzGRsmSSSzVNFgpfb3V3j0UivcNjlX2y63vjLinol463Z/+3Xh3 |
1366 | + BmxAOiLHF/DbVnqqNg5ygdxsa7MBHXEJ5we3W8vQr37xNk5DqhV7HPBSFttWP5sy0dg |
1367 | + rdjyfMIjqJ1J/2+aM4opFA/6EWif7TGmjo7N1KKM= |
1368 | + From: postmaster@amazonses.com |
1369 | + To: domains@stalw.art |
1370 | + Message-ID: <010001835a70fc8d-a3d7eff5-7adb-41cc-87bd-a646d9776a69-000000@email.amazonses.com> |
1371 | + Subject: Dmarc Aggregate Report Domain: {stalw.art} Submitter: {Amazon SES} |
1372 | + Date: {2022-09-19} Report-ID: {6b06c366-0631-4ca0-8337-f5aecf137918} |
1373 | + MIME-Version: 1.0 |
1374 | + Content-Type: multipart/mixed; |
1375 | + boundary="----=_Part_42492_694130218.1663669697673" |
1376 | + Date: Tue, 20 Sep 2022 10:28:17 +0000 |
1377 | + Feedback-ID: 1.us-east-1.CTa/CO4t1eWkL0VlHBu5/eINCZhxZraAIsQC/FZHIgk=:AmazonSES |
1378 | + X-SES-Outgoing: 2022.09.20-54.240.14.92 |
1379 | + |
1380 | + ------=_Part_42492_694130218.1663669697673 |
1381 | + Content-Type: text/plain; charset=us-ascii |
1382 | + Content-Transfer-Encoding: 7bit |
1383 | + |
1384 | + This MIME email was sent through Amazon SES. |
1385 | + ------=_Part_42492_694130218.1663669697673 |
1386 | + Content-Type: application/octet-stream; |
1387 | + name=amazonses.com!stalw.art!1663545600!1663632000.xml.gz |
1388 | + Content-Transfer-Encoding: base64 |
1389 | + Content-Disposition: attachment; |
1390 | + filename=amazonses.com!stalw.art!1663545600!1663632000.xml.gz |
1391 | + |
1392 | + H4sIAAAAAAAAAG1TwXLbIBA9O1/RyV1CWLHszlDSHHJMe8itFw1GK5uJBAwgp+3XlwXJVjK9SOzb |
1393 | + 1b59vBV7/D0OXy7gvDL62z0tq/tHfsd6gO4o5Bu/27A5yauSMrIEEXdgjQvtCEF0IogIbZhxp1aL |
1394 | + EfjTy9Ovnz+K1+dXRq4gVsAo1MCt8WEUPoD7Lkbx12gPvpRmZCTnsXLurzreHKtG1k1TVE1Niwcp |
1395 | + quJQ1/ui3wmQPa33X+mBkVs9fh1HgtYJfUq0G3aEk9KcNk29e9g1VcVIRlISdJdSTb2tMIUxNiEf |
1396 | + ulwpVpKZNYOSf1o7HQflzzCTm6hCcx/E8F4KF2KjjGBSdG9q5I6RfEiQt31C8I2A5dpoYMSmyC+h |
1397 | + z7GVgVOcEw8I9IbHKD5xyP9MFO9SGpdnc+Y9i/ZmchJaZfm22pd0T0t6OJRb7HtLpUppJh0ZGcmH |
1398 | + hM0scBHDFC8p9UblykdvVcAdyTOvkbkGZffR5picbyCJ7GdwvoSblA8k0YWsgKkOdFC9iiu52HiB |
1399 | + wVhoe2dGnher1AP6uU6k2jOIDlwGVj6t4UT2iYSJKZxbB34awsy6jHu1fUV8sz0tNH7FrfAeVykF |
1400 | + WediO/nUHcuycdHd5Zf8BxMenbqzAwAA |
1401 | + ------=_Part_42492_694130218.1663669697673-- |
1402 | diff --git a/resources/dmarc-feedback/104.json b/resources/dmarc-feedback/104.json |
1403 | new file mode 100644 |
1404 | index 0000000..602ec66 |
1405 | --- /dev/null |
1406 | +++ b/resources/dmarc-feedback/104.json |
1407 | @@ -0,0 +1,56 @@ |
1408 | + { |
1409 | + "version": 0.1, |
1410 | + "report_metadata": { |
1411 | + "org_name": "AMAZON-SES", |
1412 | + "email": "postmaster@amazonses.com", |
1413 | + "extra_contact_info": null, |
1414 | + "report_id": "6b06c366-0631-4ca0-8337-f5aecf137918", |
1415 | + "date_range": { |
1416 | + "begin": 1663545600, |
1417 | + "end": 1663632000 |
1418 | + }, |
1419 | + "error": [] |
1420 | + }, |
1421 | + "policy_published": { |
1422 | + "domain": "stalw.art", |
1423 | + "version_published": null, |
1424 | + "adkim": "Relaxed", |
1425 | + "aspf": "Relaxed", |
1426 | + "p": "None", |
1427 | + "sp": "None", |
1428 | + "testing": false, |
1429 | + "fo": "0" |
1430 | + }, |
1431 | + "record": [ |
1432 | + { |
1433 | + "row": { |
1434 | + "source_ip": "207.171.188.200", |
1435 | + "count": 1, |
1436 | + "policy_evaluated": { |
1437 | + "disposition": "None", |
1438 | + "dkim": "Fail", |
1439 | + "spf": "Fail", |
1440 | + "reason": [] |
1441 | + } |
1442 | + }, |
1443 | + "identifiers": { |
1444 | + "envelope_to": null, |
1445 | + "envelope_from": "amazon.nl", |
1446 | + "header_from": "stalw.art" |
1447 | + }, |
1448 | + "auth_results": { |
1449 | + "dkim": [], |
1450 | + "spf": [ |
1451 | + { |
1452 | + "domain": "amazon.nl", |
1453 | + "scope": "Unspecified", |
1454 | + "result": "Pass", |
1455 | + "human_result": null |
1456 | + } |
1457 | + ] |
1458 | + }, |
1459 | + "extensions": [] |
1460 | + } |
1461 | + ], |
1462 | + "extensions": [] |
1463 | + } |
1464 | \ No newline at end of file |
1465 | diff --git a/src/common/auth_results.rs b/src/common/auth_results.rs |
1466 | index 0b4fbcf..ca7575e 100644 |
1467 | --- a/src/common/auth_results.rs |
1468 | +++ b/src/common/auth_results.rs |
1469 | @@ -88,21 +88,14 @@ impl<'x> AuthenticationResults<'x> { |
1470 | |
1471 | pub fn with_dmarc_result(mut self, dmarc: &DMARCOutput) -> Self { |
1472 | self.auth_results.push_str(";\r\n\tdmarc="); |
1473 | - match &dmarc.result { |
1474 | - DMARCResult::Pass => self.auth_results.push_str("pass"), |
1475 | - DMARCResult::Fail(err) => { |
1476 | - self.auth_results.push_str("fail"); |
1477 | - err.as_auth_result(&mut self.auth_results); |
1478 | - } |
1479 | - DMARCResult::PermError(err) => { |
1480 | - self.auth_results.push_str("permerror"); |
1481 | - err.as_auth_result(&mut self.auth_results); |
1482 | - } |
1483 | - DMARCResult::TempError(err) => { |
1484 | - self.auth_results.push_str("temperror"); |
1485 | - err.as_auth_result(&mut self.auth_results); |
1486 | - } |
1487 | - DMARCResult::None => self.auth_results.push_str("none"), |
1488 | + if dmarc.spf_result == DMARCResult::Pass || dmarc.dkim_result == DMARCResult::Pass { |
1489 | + DMARCResult::Pass.as_auth_result(&mut self.auth_results); |
1490 | + } else if dmarc.spf_result != DMARCResult::None { |
1491 | + dmarc.spf_result.as_auth_result(&mut self.auth_results); |
1492 | + } else if dmarc.dkim_result != DMARCResult::None { |
1493 | + dmarc.dkim_result.as_auth_result(&mut self.auth_results); |
1494 | + } else { |
1495 | + DMARCResult::None.as_auth_result(&mut self.auth_results); |
1496 | } |
1497 | write!( |
1498 | self.auth_results, |
1499 | @@ -211,6 +204,27 @@ pub trait AsAuthResult { |
1500 | fn as_auth_result(&self, header: &mut String); |
1501 | } |
1502 | |
1503 | + impl AsAuthResult for DMARCResult { |
1504 | + fn as_auth_result(&self, header: &mut String) { |
1505 | + match &self { |
1506 | + DMARCResult::Pass => header.push_str("pass"), |
1507 | + DMARCResult::Fail(err) => { |
1508 | + header.push_str("fail"); |
1509 | + err.as_auth_result(header); |
1510 | + } |
1511 | + DMARCResult::PermError(err) => { |
1512 | + header.push_str("permerror"); |
1513 | + err.as_auth_result(header); |
1514 | + } |
1515 | + DMARCResult::TempError(err) => { |
1516 | + header.push_str("temperror"); |
1517 | + err.as_auth_result(header); |
1518 | + } |
1519 | + DMARCResult::None => header.push_str("none"), |
1520 | + } |
1521 | + } |
1522 | + } |
1523 | + |
1524 | impl AsAuthResult for DKIMResult { |
1525 | fn as_auth_result(&self, header: &mut String) { |
1526 | match &self { |
1527 | @@ -407,6 +421,7 @@ mod test { |
1528 | auth_results = auth_results.with_spf_result( |
1529 | &SPFOutput { |
1530 | result, |
1531 | + domain: "".to_string(), |
1532 | report: None, |
1533 | explanation: None, |
1534 | }, |
1535 | @@ -417,6 +432,7 @@ mod test { |
1536 | let received_spf = ReceivedSPF::new( |
1537 | &SPFOutput { |
1538 | result, |
1539 | + domain: "".to_string(), |
1540 | report: None, |
1541 | explanation: None, |
1542 | }, |
1543 | @@ -436,7 +452,8 @@ mod test { |
1544 | ( |
1545 | "dmarc=pass header.from=example.org policy.dmarc=none", |
1546 | DMARCOutput { |
1547 | - result: DMARCResult::Pass, |
1548 | + spf_result: DMARCResult::Pass, |
1549 | + dkim_result: DMARCResult::None, |
1550 | domain: "example.org".to_string(), |
1551 | policy: Policy::None, |
1552 | record: None, |
1553 | @@ -445,7 +462,8 @@ mod test { |
1554 | ( |
1555 | "dmarc=fail (dmarc not aligned) header.from=example.com policy.dmarc=quarantine", |
1556 | DMARCOutput { |
1557 | - result: DMARCResult::Fail(Error::DMARCNotAligned), |
1558 | + dkim_result: DMARCResult::Fail(Error::DMARCNotAligned), |
1559 | + spf_result: DMARCResult::None, |
1560 | domain: "example.com".to_string(), |
1561 | policy: Policy::Quarantine, |
1562 | record: None, |
1563 | diff --git a/src/dmarc/mod.rs b/src/dmarc/mod.rs |
1564 | index 7e275d3..5723446 100644 |
1565 | --- a/src/dmarc/mod.rs |
1566 | +++ b/src/dmarc/mod.rs |
1567 | @@ -7,20 +7,20 @@ pub mod verify; |
1568 | |
1569 | #[derive(Debug, Clone, PartialEq, Eq)] |
1570 | pub struct DMARC { |
1571 | - v: Version, |
1572 | - adkim: Alignment, |
1573 | - aspf: Alignment, |
1574 | - fo: Report, |
1575 | - np: Policy, |
1576 | - p: Policy, |
1577 | - psd: Psd, |
1578 | - pct: u8, |
1579 | - rf: u8, |
1580 | - ri: u32, |
1581 | - rua: Vec<URI>, |
1582 | - ruf: Vec<URI>, |
1583 | - sp: Policy, |
1584 | - t: bool, |
1585 | + pub(crate) v: Version, |
1586 | + pub(crate) adkim: Alignment, |
1587 | + pub(crate) aspf: Alignment, |
1588 | + pub(crate) fo: Report, |
1589 | + pub(crate) np: Policy, |
1590 | + pub(crate) p: Policy, |
1591 | + pub(crate) psd: Psd, |
1592 | + pub(crate) pct: u8, |
1593 | + pub(crate) rf: u8, |
1594 | + pub(crate) ri: u32, |
1595 | + pub(crate) rua: Vec<URI>, |
1596 | + pub(crate) ruf: Vec<URI>, |
1597 | + pub(crate) sp: Policy, |
1598 | + pub(crate) t: bool, |
1599 | } |
1600 | |
1601 | #[derive(Debug, Clone, PartialEq, Eq)] |
1602 | @@ -95,10 +95,11 @@ impl From<Error> for DMARCResult { |
1603 | impl Default for DMARCOutput { |
1604 | fn default() -> Self { |
1605 | Self { |
1606 | - result: DMARCResult::None, |
1607 | domain: String::new(), |
1608 | policy: Policy::None, |
1609 | record: None, |
1610 | + spf_result: DMARCResult::None, |
1611 | + dkim_result: DMARCResult::None, |
1612 | } |
1613 | } |
1614 | } |
1615 | @@ -109,13 +110,13 @@ impl DMARCOutput { |
1616 | self |
1617 | } |
1618 | |
1619 | - pub(crate) fn with_result(mut self, result: DMARCResult) -> Self { |
1620 | - self.result = result; |
1621 | + pub(crate) fn with_spf_result(mut self, result: DMARCResult) -> Self { |
1622 | + self.spf_result = result; |
1623 | self |
1624 | } |
1625 | |
1626 | - pub(crate) fn with_policy(mut self, policy: Policy) -> Self { |
1627 | - self.policy = policy; |
1628 | + pub(crate) fn with_dkim_result(mut self, result: DMARCResult) -> Self { |
1629 | + self.dkim_result = result; |
1630 | self |
1631 | } |
1632 | |
1633 | @@ -127,14 +128,10 @@ impl DMARCOutput { |
1634 | |
1635 | impl Display for Policy { |
1636 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
1637 | - write!( |
1638 | - f, |
1639 | - "{}", |
1640 | - match self { |
1641 | - Policy::Quarantine => "quarantine", |
1642 | - Policy::Reject => "reject", |
1643 | - Policy::None | Policy::Unspecified => "none", |
1644 | - } |
1645 | - ) |
1646 | + f.write_str(match self { |
1647 | + Policy::Quarantine => "quarantine", |
1648 | + Policy::Reject => "reject", |
1649 | + Policy::None | Policy::Unspecified => "none", |
1650 | + }) |
1651 | } |
1652 | } |
1653 | diff --git a/src/dmarc/verify.rs b/src/dmarc/verify.rs |
1654 | index 46a81e2..50d5ccf 100644 |
1655 | --- a/src/dmarc/verify.rs |
1656 | +++ b/src/dmarc/verify.rs |
1657 | @@ -29,10 +29,8 @@ impl Resolver { |
1658 | } |
1659 | } |
1660 | |
1661 | - if from_domain.is_empty() |
1662 | - || (spf_output.result != SPFResult::Pass |
1663 | - && !dkim_output.iter().any(|o| o.result == DKIMResult::Pass)) |
1664 | - { |
1665 | + let has_dkim_pass = dkim_output.iter().any(|o| o.result == DKIMResult::Pass); |
1666 | + if from_domain.is_empty() || (spf_output.result != SPFResult::Pass && !has_dkim_pass) { |
1667 | // No domain found or no mechanism passed, skip DMARC. |
1668 | return DMARCOutput::default().with_domain(from_domain); |
1669 | } |
1670 | @@ -42,55 +40,60 @@ impl Resolver { |
1671 | Ok(Some(dmarc)) => dmarc, |
1672 | Ok(None) => return DMARCOutput::default().with_domain(from_domain), |
1673 | Err(err) => { |
1674 | + let err = DMARCResult::from(err); |
1675 | return DMARCOutput::default() |
1676 | .with_domain(from_domain) |
1677 | - .with_result(err.into()); |
1678 | + .with_dkim_result(err.clone()) |
1679 | + .with_spf_result(err); |
1680 | } |
1681 | }; |
1682 | |
1683 | - let output = DMARCOutput { |
1684 | - result: DMARCResult::None, |
1685 | + let mut output = DMARCOutput { |
1686 | + spf_result: DMARCResult::None, |
1687 | + dkim_result: DMARCResult::None, |
1688 | domain: from_domain.to_string(), |
1689 | policy: dmarc.p, |
1690 | record: None, |
1691 | }; |
1692 | |
1693 | - // Check SPF and DKIM strict alignment |
1694 | - if (spf_output.result == SPFResult::Pass && mail_from_domain == from_domain) |
1695 | - || (dkim_output.iter().any(|o| { |
1696 | + // Check SPF alignment |
1697 | + let from_subdomain = format!(".{}", from_domain); |
1698 | + if spf_output.result == SPFResult::Pass { |
1699 | + output.spf_result = if mail_from_domain == from_domain { |
1700 | + DMARCResult::Pass |
1701 | + } else if dmarc.aspf == Alignment::Relaxed |
1702 | + && mail_from_domain.ends_with(&from_subdomain) |
1703 | + || from_domain.ends_with(&format!(".{}", mail_from_domain)) |
1704 | + { |
1705 | + output.policy = dmarc.sp; |
1706 | + DMARCResult::Pass |
1707 | + } else { |
1708 | + DMARCResult::Fail(Error::DMARCNotAligned) |
1709 | + }; |
1710 | + } |
1711 | + |
1712 | + // Check DKIM alignment |
1713 | + if has_dkim_pass { |
1714 | + output.dkim_result = if dkim_output.iter().any(|o| { |
1715 | o.result == DKIMResult::Pass && o.signature.as_ref().unwrap().d.eq(from_domain) |
1716 | - })) |
1717 | - { |
1718 | - output.with_record(dmarc).with_result(DMARCResult::Pass) |
1719 | - } else if dmarc.adkim == Alignment::Strict && dmarc.aspf == Alignment::Strict { |
1720 | - output |
1721 | - .with_record(dmarc) |
1722 | - .with_result(DMARCResult::Fail(Error::DMARCNotAligned)) |
1723 | - } else { |
1724 | - // Check SPF relaxed alignment |
1725 | - let from_subdomain = format!(".{}", from_domain); |
1726 | - if (spf_output.result == SPFResult::Pass |
1727 | - && dmarc.aspf == Alignment::Relaxed |
1728 | - && (mail_from_domain.ends_with(&from_subdomain) |
1729 | - || from_domain.ends_with(&format!(".{}", mail_from_domain)))) |
1730 | - || (dmarc.adkim == Alignment::Relaxed |
1731 | - && dkim_output.iter().any(|o| { |
1732 | - o.result == DKIMResult::Pass |
1733 | - && (o.signature.as_ref().unwrap().d.ends_with(&from_subdomain) |
1734 | - || from_domain |
1735 | - .ends_with(&format!(".{}", o.signature.as_ref().unwrap().d))) |
1736 | - })) |
1737 | + }) { |
1738 | + DMARCResult::Pass |
1739 | + } else if dmarc.adkim == Alignment::Relaxed |
1740 | + && dkim_output.iter().any(|o| { |
1741 | + o.result == DKIMResult::Pass |
1742 | + && (o.signature.as_ref().unwrap().d.ends_with(&from_subdomain) |
1743 | + || from_domain |
1744 | + .ends_with(&format!(".{}", o.signature.as_ref().unwrap().d))) |
1745 | + }) |
1746 | { |
1747 | - output |
1748 | - .with_policy(dmarc.sp) |
1749 | - .with_record(dmarc) |
1750 | - .with_result(DMARCResult::Pass) |
1751 | + output.policy = dmarc.sp; |
1752 | + DMARCResult::Pass |
1753 | } else { |
1754 | - output |
1755 | - .with_record(dmarc) |
1756 | - .with_result(DMARCResult::Fail(Error::DMARCNotAligned)) |
1757 | - } |
1758 | + DMARCResult::Fail(Error::DMARCNotAligned) |
1759 | + }; |
1760 | } |
1761 | + |
1762 | + output.with_record(dmarc) |
1763 | } |
1764 | |
1765 | async fn dmarc_tree_walk(&self, domain: &str) -> crate::Result<Option<Arc<DMARC>>> { |
1766 | diff --git a/src/lib.rs b/src/lib.rs |
1767 | index e92eec7..f6bb344 100644 |
1768 | --- a/src/lib.rs |
1769 | +++ b/src/lib.rs |
1770 | @@ -137,13 +137,15 @@ pub enum SPFResult { |
1771 | #[derive(Debug, PartialEq, Eq, Clone)] |
1772 | pub struct SPFOutput { |
1773 | result: SPFResult, |
1774 | + domain: String, |
1775 | report: Option<String>, |
1776 | explanation: Option<String>, |
1777 | } |
1778 | |
1779 | #[derive(Debug, PartialEq, Eq, Clone)] |
1780 | pub struct DMARCOutput { |
1781 | - result: DMARCResult, |
1782 | + spf_result: DMARCResult, |
1783 | + dkim_result: DMARCResult, |
1784 | domain: String, |
1785 | policy: dmarc::Policy, |
1786 | record: Option<Arc<DMARC>>, |
1787 | diff --git a/src/report/agg_parse.rs b/src/report/agg_parse.rs |
1788 | deleted file mode 100644 |
1789 | index 144cd69..0000000 |
1790 | --- a/src/report/agg_parse.rs |
1791 | +++ /dev/null |
1792 | @@ -1,452 +0,0 @@ |
1793 | - use std::io::BufRead; |
1794 | - use std::str::FromStr; |
1795 | - |
1796 | - use quick_xml::events::{BytesStart, Event}; |
1797 | - use quick_xml::reader::Reader; |
1798 | - |
1799 | - use super::{ |
1800 | - Alignment, DateRange, Disposition, Extension, Feedback, PolicyPublished, Record, ReportMetadata, |
1801 | - }; |
1802 | - |
1803 | - impl Feedback { |
1804 | - pub fn parse(report: &[u8]) -> Result<Self, String> { |
1805 | - let mut version = 0; |
1806 | - let mut report_metadata = None; |
1807 | - let mut policy_published = None; |
1808 | - let mut record = Vec::new(); |
1809 | - let mut extensions = Vec::new(); |
1810 | - |
1811 | - let mut reader = Reader::from_reader(report); |
1812 | - reader.trim_text(true); |
1813 | - |
1814 | - let mut buf = Vec::with_capacity(128); |
1815 | - let mut found_feedback = false; |
1816 | - |
1817 | - while let Some(tag) = reader.next_tag(&mut buf)? { |
1818 | - match tag.name().as_ref() { |
1819 | - b"feedback" if !found_feedback => { |
1820 | - found_feedback = true; |
1821 | - } |
1822 | - b"version" if found_feedback => { |
1823 | - version = reader.next_value(&mut buf)?.unwrap_or(0); |
1824 | - } |
1825 | - b"report_metadata" if found_feedback => { |
1826 | - report_metadata = ReportMetadata::parse(&mut reader, &mut buf)?.into(); |
1827 | - } |
1828 | - b"policy_published" if found_feedback => { |
1829 | - policy_published = PolicyPublished::parse(&mut reader, &mut buf)?.into(); |
1830 | - } |
1831 | - b"record" if found_feedback => { |
1832 | - record.push(Record::parse(&mut reader, &mut buf)?); |
1833 | - } |
1834 | - b"extensions" if found_feedback => { |
1835 | - if let Some(extension) = Extension::parse(&mut reader, &mut buf)? { |
1836 | - extensions.push(extension); |
1837 | - } |
1838 | - } |
1839 | - b"" => {} |
1840 | - other if !found_feedback => { |
1841 | - return Err(format!( |
1842 | - "Unexpected tag {} at position {}.", |
1843 | - String::from_utf8_lossy(other), |
1844 | - reader.buffer_position() |
1845 | - )); |
1846 | - } |
1847 | - _ => (), |
1848 | - } |
1849 | - } |
1850 | - |
1851 | - Ok(Feedback { |
1852 | - version, |
1853 | - report_metadata: report_metadata.ok_or("Missing feedback/report_metadata tag.")?, |
1854 | - policy_published: policy_published.ok_or("Missing feedback/policy_published tag.")?, |
1855 | - record, |
1856 | - extensions, |
1857 | - }) |
1858 | - } |
1859 | - } |
1860 | - |
1861 | - impl ReportMetadata { |
1862 | - pub(crate) fn parse<R: BufRead>( |
1863 | - reader: &mut Reader<R>, |
1864 | - buf: &mut Vec<u8>, |
1865 | - ) -> Result<Self, String> { |
1866 | - let mut org_name = String::new(); |
1867 | - let mut email = String::new(); |
1868 | - let mut extra_contact_info = None; |
1869 | - let mut report_id = String::new(); |
1870 | - let mut date_range = None; |
1871 | - let mut error = Vec::new(); |
1872 | - |
1873 | - while let Some(tag) = reader.next_tag(buf)? { |
1874 | - match tag.name().as_ref() { |
1875 | - b"org_name" => { |
1876 | - org_name = reader.next_value::<String>(buf)?.unwrap_or_default(); |
1877 | - } |
1878 | - b"email" => { |
1879 | - email = reader.next_value::<String>(buf)?.unwrap_or_default(); |
1880 | - } |
1881 | - b"extra_contact_info" => { |
1882 | - extra_contact_info = reader.next_value::<String>(buf)?; |
1883 | - } |
1884 | - b"report_id" => { |
1885 | - report_id = reader.next_value::<String>(buf)?.unwrap_or_default(); |
1886 | - } |
1887 | - b"date_range" => { |
1888 | - date_range = DateRange::parse(reader, buf)?.into(); |
1889 | - } |
1890 | - b"error" => { |
1891 | - if let Some(err) = reader.next_value::<String>(buf)? { |
1892 | - error.push(err); |
1893 | - } |
1894 | - } |
1895 | - b"" => (), |
1896 | - _ => { |
1897 | - reader.skip_tag(buf)?; |
1898 | - } |
1899 | - } |
1900 | - } |
1901 | - |
1902 | - Ok(ReportMetadata { |
1903 | - org_name, |
1904 | - email, |
1905 | - extra_contact_info, |
1906 | - report_id, |
1907 | - date_range: date_range.unwrap_or_default(), |
1908 | - error, |
1909 | - }) |
1910 | - } |
1911 | - } |
1912 | - |
1913 | - impl DateRange { |
1914 | - pub(crate) fn parse<R: BufRead>( |
1915 | - reader: &mut Reader<R>, |
1916 | - buf: &mut Vec<u8>, |
1917 | - ) -> Result<Self, String> { |
1918 | - let mut begin = 0; |
1919 | - let mut end = 0; |
1920 | - |
1921 | - while let Some(tag) = reader.next_tag(buf)? { |
1922 | - match tag.name().as_ref() { |
1923 | - b"begin" => { |
1924 | - begin = reader.next_value(buf)?.unwrap_or_default(); |
1925 | - } |
1926 | - b"end" => { |
1927 | - end = reader.next_value(buf)?.unwrap_or_default(); |
1928 | - } |
1929 | - b"" => (), |
1930 | - _ => { |
1931 | - reader.skip_tag(buf)?; |
1932 | - } |
1933 | - } |
1934 | - } |
1935 | - |
1936 | - Ok(DateRange { begin, end }) |
1937 | - } |
1938 | - } |
1939 | - |
1940 | - impl PolicyPublished { |
1941 | - pub(crate) fn parse<R: BufRead>( |
1942 | - reader: &mut Reader<R>, |
1943 | - buf: &mut Vec<u8>, |
1944 | - ) -> Result<Self, String> { |
1945 | - let mut domain = String::new(); |
1946 | - let mut version_published = None; |
1947 | - let mut adkim = Alignment::Unspecified; |
1948 | - let mut aspf = Alignment::Unspecified; |
1949 | - let mut p = Disposition::Unspecified; |
1950 | - let mut sp = Disposition::Unspecified; |
1951 | - let mut testing = false; |
1952 | - let mut fo = None; |
1953 | - |
1954 | - while let Some(tag) = reader.next_tag(buf)? { |
1955 | - match tag.name().as_ref() { |
1956 | - b"domain" => { |
1957 | - domain = reader.next_value::<String>(buf)?.unwrap_or_default(); |
1958 | - } |
1959 | - b"version_published" => { |
1960 | - version_published = reader.next_value(buf)?; |
1961 | - } |
1962 | - b"adkim" => { |
1963 | - adkim = reader.next_value(buf)?.unwrap_or(Alignment::Unspecified); |
1964 | - } |
1965 | - b"aspf" => { |
1966 | - aspf = reader.next_value(buf)?.unwrap_or(Alignment::Unspecified); |
1967 | - } |
1968 | - b"p" => { |
1969 | - p = reader.next_value(buf)?.unwrap_or(Disposition::Unspecified); |
1970 | - } |
1971 | - b"sp" => { |
1972 | - sp = reader.next_value(buf)?.unwrap_or(Disposition::Unspecified); |
1973 | - } |
1974 | - b"testing" => { |
1975 | - testing = reader |
1976 | - .next_value::<String>(buf)? |
1977 | - .map_or(false, |s| s.eq_ignore_ascii_case("y")); |
1978 | - } |
1979 | - b"fo" => { |
1980 | - fo = reader.next_value::<String>(buf)?; |
1981 | - } |
1982 | - b"" => (), |
1983 | - _ => { |
1984 | - reader.skip_tag(buf)?; |
1985 | - } |
1986 | - } |
1987 | - } |
1988 | - |
1989 | - Ok(PolicyPublished { |
1990 | - domain, |
1991 | - version_published, |
1992 | - adkim, |
1993 | - aspf, |
1994 | - p, |
1995 | - sp, |
1996 | - testing, |
1997 | - fo, |
1998 | - }) |
1999 | - } |
2000 | - } |
2001 | - |
2002 | - impl Extension { |
2003 | - pub(crate) fn parse<R: BufRead>( |
2004 | - reader: &mut Reader<R>, |
2005 | - buf: &mut Vec<u8>, |
2006 | - ) -> Result<Option<Self>, String> { |
2007 | - let mut name = String::new(); |
2008 | - let mut definition = String::new(); |
2009 | - let mut extension = None; |
2010 | - |
2011 | - while let Some(tag) = reader.next_tag(buf)? { |
2012 | - match tag.name().as_ref() { |
2013 | - b"extension" => { |
2014 | - if let Ok(Some(attr)) = tag.try_get_attribute("name") { |
2015 | - if let Ok(attr) = attr.unescape_value() { |
2016 | - name = attr.to_string(); |
2017 | - } |
2018 | - } |
2019 | - if let Ok(Some(attr)) = tag.try_get_attribute("definition") { |
2020 | - if let Ok(attr) = attr.unescape_value() { |
2021 | - definition = attr.to_string(); |
2022 | - } |
2023 | - } |
2024 | - |
2025 | - extension = reader.next_value(buf)?; |
2026 | - } |
2027 | - b"" => (), |
2028 | - _ => { |
2029 | - reader.skip_tag(buf)?; |
2030 | - } |
2031 | - } |
2032 | - } |
2033 | - |
2034 | - Ok(if !name.is_empty() { |
2035 | - Some(Extension { |
2036 | - extension, |
2037 | - name, |
2038 | - definition, |
2039 | - }) |
2040 | - } else { |
2041 | - None |
2042 | - }) |
2043 | - } |
2044 | - } |
2045 | - |
2046 | - impl Record { |
2047 | - pub(crate) fn parse<R: BufRead>( |
2048 | - reader: &mut Reader<R>, |
2049 | - buf: &mut Vec<u8>, |
2050 | - ) -> Result<Self, String> { |
2051 | - let mut begin = 0; |
2052 | - let mut end = 0; |
2053 | - |
2054 | - while let Some(tag) = reader.next_tag(buf)? { |
2055 | - match tag.name().as_ref() { |
2056 | - b"begin" => { |
2057 | - begin = reader.next_value(buf)?.unwrap_or_default(); |
2058 | - } |
2059 | - b"end" => { |
2060 | - end = reader.next_value(buf)?.unwrap_or_default(); |
2061 | - } |
2062 | - b"" => (), |
2063 | - _ => { |
2064 | - reader.skip_tag(buf)?; |
2065 | - } |
2066 | - } |
2067 | - } |
2068 | - |
2069 | - Ok(Record { |
2070 | - row: todo!(), |
2071 | - identifiers: todo!(), |
2072 | - auth_results: todo!(), |
2073 | - }) |
2074 | - } |
2075 | - } |
2076 | - |
2077 | - impl FromStr for Disposition { |
2078 | - type Err = (); |
2079 | - |
2080 | - fn from_str(s: &str) -> Result<Self, Self::Err> { |
2081 | - Ok(match s.as_bytes() { |
2082 | - b"none" => Disposition::None, |
2083 | - b"quarantine" => Disposition::Quarantine, |
2084 | - b"reject" => Disposition::Reject, |
2085 | - _ => Disposition::Unspecified, |
2086 | - }) |
2087 | - } |
2088 | - } |
2089 | - |
2090 | - impl FromStr for Alignment { |
2091 | - type Err = (); |
2092 | - |
2093 | - fn from_str(s: &str) -> Result<Self, Self::Err> { |
2094 | - Ok(match s.as_bytes().first() { |
2095 | - Some(b'r') => Alignment::Relaxed, |
2096 | - Some(b's') => Alignment::Simple, |
2097 | - _ => Alignment::Unspecified, |
2098 | - }) |
2099 | - } |
2100 | - } |
2101 | - |
2102 | - trait ReaderHelper { |
2103 | - fn next_tag<'x>(&mut self, buf: &'x mut Vec<u8>) -> Result<Option<BytesStart<'x>>, String>; |
2104 | - fn next_value<T: FromStr>(&mut self, buf: &mut Vec<u8>) -> Result<Option<T>, String>; |
2105 | - fn skip_tag(&mut self, buf: &mut Vec<u8>) -> Result<(), String>; |
2106 | - } |
2107 | - |
2108 | - impl<R: BufRead> ReaderHelper for Reader<R> { |
2109 | - fn next_tag<'x>(&mut self, buf: &'x mut Vec<u8>) -> Result<Option<BytesStart<'x>>, String> { |
2110 | - match self.read_event_into(buf) { |
2111 | - Ok(Event::Start(e)) => Ok(Some(e)), |
2112 | - Ok(Event::End(_)) | Ok(Event::Eof) => Ok(None), |
2113 | - Err(e) => Err(format!( |
2114 | - "Error at position {}: {:?}", |
2115 | - self.buffer_position(), |
2116 | - e |
2117 | - )), |
2118 | - _ => Ok(Some(BytesStart::new(""))), |
2119 | - } |
2120 | - } |
2121 | - |
2122 | - fn next_value<T: FromStr>(&mut self, buf: &mut Vec<u8>) -> Result<Option<T>, String> { |
2123 | - let mut value = None; |
2124 | - loop { |
2125 | - match self.read_event_into(buf) { |
2126 | - Ok(Event::Text(e)) => { |
2127 | - let value_ = e.unescape().map_err(|err| { |
2128 | - format!( |
2129 | - "Failed to unescape value at {}: {}", |
2130 | - self.buffer_position(), |
2131 | - err |
2132 | - ) |
2133 | - })?; |
2134 | - |
2135 | - value = T::from_str(value_.as_ref()) |
2136 | - .map_err(|_| { |
2137 | - format!( |
2138 | - "Failed to parse value {:?} at {}.", |
2139 | - value_, |
2140 | - self.buffer_position(), |
2141 | - ) |
2142 | - })? |
2143 | - .into(); |
2144 | - } |
2145 | - Ok(Event::End(_)) => { |
2146 | - break; |
2147 | - } |
2148 | - Ok(Event::Start(e)) => { |
2149 | - return Err(format!( |
2150 | - "Expected value, found unexpected tag {} at position {}.", |
2151 | - String::from_utf8_lossy(e.name().as_ref()), |
2152 | - self.buffer_position() |
2153 | - )); |
2154 | - } |
2155 | - Ok(Event::Eof) => { |
2156 | - return Err(format!( |
2157 | - "Expected value, found unexpected EOF at position {}.", |
2158 | - self.buffer_position() |
2159 | - )) |
2160 | - } |
2161 | - _ => (), |
2162 | - } |
2163 | - } |
2164 | - |
2165 | - Ok(value) |
2166 | - } |
2167 | - |
2168 | - fn skip_tag(&mut self, buf: &mut Vec<u8>) -> Result<(), String> { |
2169 | - let mut tag_count = 0; |
2170 | - loop { |
2171 | - match self.read_event_into(buf) { |
2172 | - Ok(Event::End(_)) => { |
2173 | - if tag_count == 0 { |
2174 | - break; |
2175 | - } else { |
2176 | - tag_count -= 1; |
2177 | - } |
2178 | - } |
2179 | - Ok(Event::Start(_)) => { |
2180 | - tag_count += 1; |
2181 | - } |
2182 | - Ok(Event::Eof) => { |
2183 | - return Err(format!( |
2184 | - "Expected value, found unexpected EOF at position {}.", |
2185 | - self.buffer_position() |
2186 | - )) |
2187 | - } |
2188 | - _ => (), |
2189 | - } |
2190 | - } |
2191 | - Ok(()) |
2192 | - } |
2193 | - } |
2194 | - |
2195 | - #[cfg(test)] |
2196 | - mod test { |
2197 | - use std::{fs, path::PathBuf}; |
2198 | - |
2199 | - #[test] |
2200 | - fn dmarc_aggregate_report_parse() { |
2201 | - use quick_xml::events::Event; |
2202 | - use quick_xml::reader::Reader; |
2203 | - |
2204 | - let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); |
2205 | - test_dir.push("resources"); |
2206 | - test_dir.push("dmarc-agg-report"); |
2207 | - |
2208 | - for file_name in fs::read_dir(&test_dir).unwrap() { |
2209 | - let file_name = file_name.unwrap().path(); |
2210 | - println!("file {}", file_name.to_str().unwrap()); |
2211 | - let mut reader = Reader::from_file(file_name).unwrap(); |
2212 | - reader.trim_text(true); |
2213 | - |
2214 | - let mut count = 0; |
2215 | - let mut txt = Vec::new(); |
2216 | - let mut buf = Vec::new(); |
2217 | - |
2218 | - // The `Reader` does not implement `Iterator` because it outputs borrowed data (`Cow`s) |
2219 | - loop { |
2220 | - // NOTE: this is the generic case when we don't know about the input BufRead. |
2221 | - // when the input is a &str or a &[u8], we don't actually need to use another |
2222 | - // buffer, we could directly call `reader.read_event()` |
2223 | - match reader.read_event_into(&mut buf) { |
2224 | - Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e), |
2225 | - // exits the loop when reaching end of file |
2226 | - Ok(Event::Eof) => break, |
2227 | - |
2228 | - Ok(Event::Start(e)) => { |
2229 | - println!("start: {}", std::str::from_utf8(e.name().as_ref()).unwrap()) |
2230 | - } |
2231 | - Ok(Event::End(e)) => { |
2232 | - println!("end: {}", std::str::from_utf8(e.name().as_ref()).unwrap()) |
2233 | - } |
2234 | - Ok(Event::Text(e)) => txt.push(e.unescape().unwrap().into_owned()), |
2235 | - |
2236 | - // There are several other `Event`s we do not consider here |
2237 | - _ => (), |
2238 | - } |
2239 | - // if we don't keep a borrow elsewhere, we can clear the buffer to keep memory usage low |
2240 | - buf.clear(); |
2241 | - } |
2242 | - } |
2243 | - } |
2244 | - } |
2245 | diff --git a/src/report/feedback/generate.rs b/src/report/feedback/generate.rs |
2246 | new file mode 100644 |
2247 | index 0000000..8381b74 |
2248 | --- /dev/null |
2249 | +++ b/src/report/feedback/generate.rs |
2250 | @@ -0,0 +1,515 @@ |
2251 | + use flate2::{write::GzEncoder, Compression}; |
2252 | + use mail_builder::{headers::HeaderType, MessageBuilder}; |
2253 | + |
2254 | + use crate::report::{ |
2255 | + ActionDisposition, Alignment, AuthResult, DKIMAuthResult, DKIMResult, DMARCResult, DateRange, |
2256 | + Disposition, Feedback, Identifier, PolicyEvaluated, PolicyOverride, PolicyOverrideReason, |
2257 | + PolicyPublished, Record, ReportMetadata, Row, SPFAuthResult, SPFDomainScope, SPFResult, |
2258 | + }; |
2259 | + |
2260 | + use std::{ |
2261 | + borrow::Cow, |
2262 | + fmt::{Display, Formatter, Write}, |
2263 | + }; |
2264 | + |
2265 | + impl Feedback { |
2266 | + pub fn write_rfc5322<'x>( |
2267 | + &self, |
2268 | + receiver_domain: &'x str, |
2269 | + submitter: &'x str, |
2270 | + from: &'x str, |
2271 | + to: &'x str, |
2272 | + writer: impl std::io::Write, |
2273 | + ) -> std::io::Result<()> { |
2274 | + // Compress XML report |
2275 | + let xml = self.as_xnl(); |
2276 | + let mut e = GzEncoder::new(Vec::with_capacity(xml.len()), Compression::default()); |
2277 | + std::io::Write::write_all(&mut e, xml.as_bytes())?; |
2278 | + let compressed_bytes = e.finish()?; |
2279 | + |
2280 | + MessageBuilder::new() |
2281 | + .header("From", HeaderType::Text(from.into())) |
2282 | + .header("To", HeaderType::Text(to.into())) |
2283 | + .header("Auto-Submitted", HeaderType::Text("auto-generated".into())) |
2284 | + .subject(format!( |
2285 | + "Report Domain: {} Submitter: {} Report-ID: <{}>", |
2286 | + self.domain(), |
2287 | + submitter, |
2288 | + self.report_id() |
2289 | + )) |
2290 | + .text_body(format!( |
2291 | + concat!( |
2292 | + "DMARC aggregate report from {}\r\n\r\n", |
2293 | + "Report Domain: {}\r\n", |
2294 | + "Submitter: {}\r\n", |
2295 | + "Report-ID: {}\r\n", |
2296 | + ), |
2297 | + receiver_domain, |
2298 | + self.domain(), |
2299 | + submitter, |
2300 | + self.report_id() |
2301 | + )) |
2302 | + .binary_attachment( |
2303 | + "application/gzip", |
2304 | + format!( |
2305 | + "{}!{}!{}!{}.xml.gz", |
2306 | + receiver_domain, |
2307 | + self.domain(), |
2308 | + self.date_range_begin(), |
2309 | + self.date_range_end() |
2310 | + ), |
2311 | + compressed_bytes, |
2312 | + ) |
2313 | + .write_to(writer) |
2314 | + } |
2315 | + |
2316 | + pub fn as_rfc5322<'x>( |
2317 | + &self, |
2318 | + receiver_domain: &'x str, |
2319 | + submitter: &'x str, |
2320 | + from: &'x str, |
2321 | + to: &'x str, |
2322 | + ) -> std::io::Result<String> { |
2323 | + let mut buf = Vec::new(); |
2324 | + self.write_rfc5322(receiver_domain, submitter, from, to, &mut buf)?; |
2325 | + String::from_utf8(buf).map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err)) |
2326 | + } |
2327 | + |
2328 | + pub fn as_xnl(&self) -> String { |
2329 | + let mut xml = String::with_capacity(128); |
2330 | + writeln!(&mut xml, "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>").ok(); |
2331 | + writeln!(&mut xml, "<feedback>").ok(); |
2332 | + if self.version != 0.0 { |
2333 | + writeln!(&mut xml, "\t<version>{}</version>", self.version).ok(); |
2334 | + } |
2335 | + self.report_metadata.as_xml(&mut xml); |
2336 | + self.policy_published.as_xml(&mut xml); |
2337 | + for record in &self.record { |
2338 | + record.as_xml(&mut xml); |
2339 | + } |
2340 | + writeln!(&mut xml, "</feedback>").ok(); |
2341 | + xml |
2342 | + } |
2343 | + } |
2344 | + |
2345 | + impl ReportMetadata { |
2346 | + pub(crate) fn as_xml(&self, xml: &mut String) { |
2347 | + writeln!(xml, "\t<report_metadata>").ok(); |
2348 | + writeln!( |
2349 | + xml, |
2350 | + "\t\t<org_name>{}</org_name>", |
2351 | + escape_xml(&self.org_name) |
2352 | + ) |
2353 | + .ok(); |
2354 | + writeln!(xml, "\t\t<email>{}</email>", escape_xml(&self.email)).ok(); |
2355 | + if let Some(eci) = &self.extra_contact_info { |
2356 | + writeln!( |
2357 | + xml, |
2358 | + "\t\t<extra_contact_info>{}</extra_contact_info>", |
2359 | + escape_xml(eci) |
2360 | + ) |
2361 | + .ok(); |
2362 | + } |
2363 | + writeln!( |
2364 | + xml, |
2365 | + "\t\t<report_id>{}</report_id>", |
2366 | + escape_xml(&self.report_id) |
2367 | + ) |
2368 | + .ok(); |
2369 | + self.date_range.as_xml(xml); |
2370 | + for error in &self.error { |
2371 | + writeln!(xml, "\t\t<error>{}</error>", escape_xml(error)).ok(); |
2372 | + } |
2373 | + writeln!(xml, "\t</report_metadata>").ok(); |
2374 | + } |
2375 | + } |
2376 | + |
2377 | + impl PolicyPublished { |
2378 | + pub(crate) fn as_xml(&self, xml: &mut String) { |
2379 | + writeln!(xml, "\t<policy_published>").ok(); |
2380 | + writeln!(xml, "\t\t<domain>{}</domain>", escape_xml(&self.domain)).ok(); |
2381 | + if let Some(vp) = &self.version_published { |
2382 | + writeln!(xml, "\t\t<version_published>{}</version_published>", vp).ok(); |
2383 | + } |
2384 | + writeln!(xml, "\t\t<adkim>{}</adkim>", &self.adkim).ok(); |
2385 | + writeln!(xml, "\t\t<aspf>{}</aspf>", &self.aspf).ok(); |
2386 | + writeln!(xml, "\t\t<p>{}</p>", &self.p).ok(); |
2387 | + writeln!(xml, "\t\t<sp>{}</sp>", &self.sp).ok(); |
2388 | + if self.testing { |
2389 | + writeln!(xml, "\t\t<testing>y</testing>").ok(); |
2390 | + } |
2391 | + if let Some(fo) = &self.fo { |
2392 | + writeln!(xml, "\t\t<fo>{}</fo>", escape_xml(fo)).ok(); |
2393 | + } |
2394 | + writeln!(xml, "\t</policy_published>").ok(); |
2395 | + } |
2396 | + } |
2397 | + |
2398 | + impl DateRange { |
2399 | + pub(crate) fn as_xml(&self, xml: &mut String) { |
2400 | + writeln!(xml, "\t\t<date_range>").ok(); |
2401 | + writeln!(xml, "\t\t\t<begin>{}</begin>", self.begin).ok(); |
2402 | + writeln!(xml, "\t\t\t<end>{}</end>", self.end).ok(); |
2403 | + writeln!(xml, "\t\t</date_range>").ok(); |
2404 | + } |
2405 | + } |
2406 | + |
2407 | + impl Record { |
2408 | + pub(crate) fn as_xml(&self, xml: &mut String) { |
2409 | + writeln!(xml, "\t<record>").ok(); |
2410 | + self.row.as_xml(xml); |
2411 | + self.identifiers.as_xml(xml); |
2412 | + self.auth_results.as_xml(xml); |
2413 | + writeln!(xml, "\t</record>").ok(); |
2414 | + } |
2415 | + } |
2416 | + |
2417 | + impl Row { |
2418 | + pub(crate) fn as_xml(&self, xml: &mut String) { |
2419 | + writeln!(xml, "\t\t<row>").ok(); |
2420 | + writeln!(xml, "\t\t\t<source_ip>{}</source_ip>", self.source_ip).ok(); |
2421 | + writeln!(xml, "\t\t\t<count>{}</count>", self.count).ok(); |
2422 | + self.policy_evaluated.as_xml(xml); |
2423 | + writeln!(xml, "\t\t</row>").ok(); |
2424 | + } |
2425 | + } |
2426 | + |
2427 | + impl PolicyEvaluated { |
2428 | + pub(crate) fn as_xml(&self, xml: &mut String) { |
2429 | + writeln!(xml, "\t\t\t<policy_evaluated>").ok(); |
2430 | + writeln!( |
2431 | + xml, |
2432 | + "\t\t\t\t<disposition>{}</disposition>", |
2433 | + self.disposition |
2434 | + ) |
2435 | + .ok(); |
2436 | + writeln!(xml, "\t\t\t\t<dkim>{}</dkim>", self.dkim).ok(); |
2437 | + writeln!(xml, "\t\t\t\t<spf>{}</spf>", self.spf).ok(); |
2438 | + for reason in &self.reason { |
2439 | + reason.as_xml(xml); |
2440 | + } |
2441 | + writeln!(xml, "\t\t\t</policy_evaluated>").ok(); |
2442 | + } |
2443 | + } |
2444 | + |
2445 | + impl PolicyOverrideReason { |
2446 | + pub(crate) fn as_xml(&self, xml: &mut String) { |
2447 | + writeln!(xml, "\t\t\t\t<reason>").ok(); |
2448 | + writeln!(xml, "\t\t\t\t\t<type>{}</type>", self.type_).ok(); |
2449 | + if let Some(comment) = &self.comment { |
2450 | + writeln!(xml, "\t\t\t\t\t<comment>{}</comment>", escape_xml(comment)).ok(); |
2451 | + } |
2452 | + writeln!(xml, "\t\t\t\t</reason>").ok(); |
2453 | + } |
2454 | + } |
2455 | + |
2456 | + impl Identifier { |
2457 | + pub(crate) fn as_xml(&self, xml: &mut String) { |
2458 | + writeln!(xml, "\t\t<identifiers>").ok(); |
2459 | + if let Some(envelope_to) = &self.envelope_to { |
2460 | + writeln!( |
2461 | + xml, |
2462 | + "\t\t\t<envelope_to>{}</envelope_to>", |
2463 | + escape_xml(envelope_to) |
2464 | + ) |
2465 | + .ok(); |
2466 | + } |
2467 | + writeln!( |
2468 | + xml, |
2469 | + "\t\t\t<envelope_from>{}</envelope_from>", |
2470 | + escape_xml(&self.envelope_from) |
2471 | + ) |
2472 | + .ok(); |
2473 | + writeln!( |
2474 | + xml, |
2475 | + "\t\t\t<header_from>{}</header_from>", |
2476 | + escape_xml(&self.header_from) |
2477 | + ) |
2478 | + .ok(); |
2479 | + writeln!(xml, "\t\t</identifiers>").ok(); |
2480 | + } |
2481 | + } |
2482 | + |
2483 | + impl AuthResult { |
2484 | + pub(crate) fn as_xml(&self, xml: &mut String) { |
2485 | + writeln!(xml, "\t\t<auth_results>").ok(); |
2486 | + for dkim in &self.dkim { |
2487 | + dkim.as_xml(xml); |
2488 | + } |
2489 | + for spf in &self.spf { |
2490 | + spf.as_xml(xml); |
2491 | + } |
2492 | + writeln!(xml, "\t\t</auth_results>").ok(); |
2493 | + } |
2494 | + } |
2495 | + |
2496 | + impl DKIMAuthResult { |
2497 | + pub(crate) fn as_xml(&self, xml: &mut String) { |
2498 | + writeln!(xml, "\t\t\t<dkim>").ok(); |
2499 | + writeln!(xml, "\t\t\t\t<domain>{}</domain>", escape_xml(&self.domain)).ok(); |
2500 | + writeln!( |
2501 | + xml, |
2502 | + "\t\t\t\t<selector>{}</selector>", |
2503 | + escape_xml(&self.selector) |
2504 | + ) |
2505 | + .ok(); |
2506 | + writeln!(xml, "\t\t\t\t<result>{}</result>", self.result).ok(); |
2507 | + if let Some(result) = &self.human_result { |
2508 | + writeln!( |
2509 | + xml, |
2510 | + "\t\t\t\t<human_result>{}</human_result>", |
2511 | + escape_xml(result) |
2512 | + ) |
2513 | + .ok(); |
2514 | + } |
2515 | + writeln!(xml, "\t\t\t</dkim>").ok(); |
2516 | + } |
2517 | + } |
2518 | + |
2519 | + impl SPFAuthResult { |
2520 | + pub(crate) fn as_xml(&self, xml: &mut String) { |
2521 | + writeln!(xml, "\t\t\t<spf>").ok(); |
2522 | + writeln!(xml, "\t\t\t\t<domain>{}</domain>", escape_xml(&self.domain)).ok(); |
2523 | + writeln!(xml, "\t\t\t\t<scope>{}</scope>", self.scope).ok(); |
2524 | + writeln!(xml, "\t\t\t\t<result>{}</result>", self.result).ok(); |
2525 | + if let Some(result) = &self.human_result { |
2526 | + writeln!( |
2527 | + xml, |
2528 | + "\t\t\t\t<human_result>{}</human_result>", |
2529 | + escape_xml(result) |
2530 | + ) |
2531 | + .ok(); |
2532 | + } |
2533 | + writeln!(xml, "\t\t\t</spf>").ok(); |
2534 | + } |
2535 | + } |
2536 | + |
2537 | + impl Display for Alignment { |
2538 | + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { |
2539 | + f.write_str(match self { |
2540 | + Alignment::Strict => "s", |
2541 | + _ => "r", |
2542 | + }) |
2543 | + } |
2544 | + } |
2545 | + |
2546 | + impl Display for Disposition { |
2547 | + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { |
2548 | + f.write_str(match self { |
2549 | + Disposition::None | Disposition::Unspecified => "none", |
2550 | + Disposition::Quarantine => "quarantine", |
2551 | + Disposition::Reject => "reject", |
2552 | + }) |
2553 | + } |
2554 | + } |
2555 | + |
2556 | + impl Display for ActionDisposition { |
2557 | + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { |
2558 | + f.write_str(match self { |
2559 | + ActionDisposition::None | ActionDisposition::Unspecified => "none", |
2560 | + ActionDisposition::Pass => "pass", |
2561 | + ActionDisposition::Quarantine => "quarantine", |
2562 | + ActionDisposition::Reject => "reject", |
2563 | + }) |
2564 | + } |
2565 | + } |
2566 | + |
2567 | + impl Display for DMARCResult { |
2568 | + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { |
2569 | + f.write_str(match self { |
2570 | + DMARCResult::Pass => "pass", |
2571 | + DMARCResult::Fail => "fail", |
2572 | + DMARCResult::Unspecified => "", |
2573 | + }) |
2574 | + } |
2575 | + } |
2576 | + |
2577 | + impl Display for PolicyOverride { |
2578 | + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { |
2579 | + f.write_str(match self { |
2580 | + PolicyOverride::Forwarded => "forwarded", |
2581 | + PolicyOverride::SampledOut => "sampled_out", |
2582 | + PolicyOverride::TrustedForwarder => "trusted_forwarder", |
2583 | + PolicyOverride::MailingList => "mailing_list", |
2584 | + PolicyOverride::LocalPolicy => "local_policy", |
2585 | + PolicyOverride::Other => "other", |
2586 | + }) |
2587 | + } |
2588 | + } |
2589 | + |
2590 | + impl Display for DKIMResult { |
2591 | + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { |
2592 | + f.write_str(match self { |
2593 | + DKIMResult::None => "none", |
2594 | + DKIMResult::Pass => "pass", |
2595 | + DKIMResult::Fail => "fail", |
2596 | + DKIMResult::Policy => "policy", |
2597 | + DKIMResult::Neutral => "neutral", |
2598 | + DKIMResult::TempError => "temperror", |
2599 | + DKIMResult::PermError => "permerror", |
2600 | + }) |
2601 | + } |
2602 | + } |
2603 | + |
2604 | + impl Display for SPFDomainScope { |
2605 | + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { |
2606 | + f.write_str(match self { |
2607 | + SPFDomainScope::Helo => "helo", |
2608 | + SPFDomainScope::MailFrom | SPFDomainScope::Unspecified => "mfrom", |
2609 | + }) |
2610 | + } |
2611 | + } |
2612 | + |
2613 | + impl Display for SPFResult { |
2614 | + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { |
2615 | + f.write_str(match self { |
2616 | + SPFResult::None => "none", |
2617 | + SPFResult::Neutral => "neutral", |
2618 | + SPFResult::Pass => "pass", |
2619 | + SPFResult::Fail => "fail", |
2620 | + SPFResult::SoftFail => "softfail", |
2621 | + SPFResult::TempError => "temperror", |
2622 | + SPFResult::PermError => "permerror", |
2623 | + }) |
2624 | + } |
2625 | + } |
2626 | + |
2627 | + fn escape_xml(text: &str) -> Cow<'_, str> { |
2628 | + for ch in text.as_bytes() { |
2629 | + if [b'"', b'\'', b'<', b'>', b'&'].contains(ch) { |
2630 | + let mut escaped = String::with_capacity(text.len()); |
2631 | + for ch in text.chars() { |
2632 | + match ch { |
2633 | + '"' => { |
2634 | + escaped.push_str("""); |
2635 | + } |
2636 | + '\'' => { |
2637 | + escaped.push_str("'"); |
2638 | + } |
2639 | + '<' => { |
2640 | + escaped.push_str("<"); |
2641 | + } |
2642 | + '>' => { |
2643 | + escaped.push_str(">"); |
2644 | + } |
2645 | + '&' => { |
2646 | + escaped.push_str("&"); |
2647 | + } |
2648 | + _ => { |
2649 | + escaped.push(ch); |
2650 | + } |
2651 | + } |
2652 | + } |
2653 | + |
2654 | + return escaped.into(); |
2655 | + } |
2656 | + } |
2657 | + text.into() |
2658 | + } |
2659 | + |
2660 | + #[cfg(test)] |
2661 | + mod test { |
2662 | + use crate::report::{ |
2663 | + ActionDisposition, Alignment, DKIMAuthResult, DKIMResult, DMARCResult, Disposition, |
2664 | + Feedback, PolicyOverride, PolicyOverrideReason, Record, SPFAuthResult, SPFDomainScope, |
2665 | + SPFResult, |
2666 | + }; |
2667 | + |
2668 | + #[test] |
2669 | + fn dmarc_aggregate_report_generate() { |
2670 | + let feedback = Feedback::new() |
2671 | + .with_version(2.0) |
2672 | + .with_org_name("Initech Industries Incorporated") |
2673 | + .with_email("dmarc@initech.net") |
2674 | + .with_extra_contact_info("XMPP:dmarc@initech.net") |
2675 | + .with_report_id("abc-123") |
2676 | + .with_date_range_begin(12345) |
2677 | + .with_date_range_end(12346) |
2678 | + .with_error("Did not include TPS report cover.") |
2679 | + .with_domain("example.org") |
2680 | + .with_version_published(1.0) |
2681 | + .with_adkim(Alignment::Relaxed) |
2682 | + .with_aspf(Alignment::Strict) |
2683 | + .with_p(Disposition::Quarantine) |
2684 | + .with_sp(Disposition::Reject) |
2685 | + .with_testing(true) |
2686 | + .with_record( |
2687 | + Record::new() |
2688 | + .with_source_ip("192.168.1.2".parse().unwrap()) |
2689 | + .with_count(3) |
2690 | + .with_action_disposition(ActionDisposition::Pass) |
2691 | + .with_dmarc_dkim_result(DMARCResult::Pass) |
2692 | + .with_dmarc_spf_result(DMARCResult::Fail) |
2693 | + .with_policy_override_reason( |
2694 | + PolicyOverrideReason::new(PolicyOverride::Forwarded) |
2695 | + .with_comment("it was forwarded"), |
2696 | + ) |
2697 | + .with_policy_override_reason( |
2698 | + PolicyOverrideReason::new(PolicyOverride::MailingList) |
2699 | + .with_comment("sent from mailing list"), |
2700 | + ) |
2701 | + .with_envelope_from("hello@example.org") |
2702 | + .with_envelope_to("other@example.org") |
2703 | + .with_header_from("bye@example.org") |
2704 | + .with_dkim_auth_result( |
2705 | + DKIMAuthResult::new() |
2706 | + .with_domain("test.org") |
2707 | + .with_selector("my-selector") |
2708 | + .with_result(DKIMResult::PermError) |
2709 | + .with_human_result("failed to parse record"), |
2710 | + ) |
2711 | + .with_spf_auth_result( |
2712 | + SPFAuthResult::new() |
2713 | + .with_domain("test.org") |
2714 | + .with_scope(SPFDomainScope::Helo) |
2715 | + .with_result(SPFResult::SoftFail) |
2716 | + .with_human_result("dns timed out"), |
2717 | + ), |
2718 | + ) |
2719 | + .with_record( |
2720 | + Record::new() |
2721 | + .with_source_ip("a:b:c::e:f".parse().unwrap()) |
2722 | + .with_count(99) |
2723 | + .with_action_disposition(ActionDisposition::Reject) |
2724 | + .with_dmarc_dkim_result(DMARCResult::Fail) |
2725 | + .with_dmarc_spf_result(DMARCResult::Pass) |
2726 | + .with_policy_override_reason( |
2727 | + PolicyOverrideReason::new(PolicyOverride::LocalPolicy) |
2728 | + .with_comment("on the white list"), |
2729 | + ) |
2730 | + .with_policy_override_reason( |
2731 | + PolicyOverrideReason::new(PolicyOverride::SampledOut) |
2732 | + .with_comment("it was sampled out"), |
2733 | + ) |
2734 | + .with_envelope_from("hello2example.org") |
2735 | + .with_envelope_to("other2@example.org") |
2736 | + .with_header_from("bye2@example.org") |
2737 | + .with_dkim_auth_result( |
2738 | + DKIMAuthResult::new() |
2739 | + .with_domain("test2.org") |
2740 | + .with_selector("my-other-selector") |
2741 | + .with_result(DKIMResult::Neutral) |
2742 | + .with_human_result("something went wrong"), |
2743 | + ) |
2744 | + .with_spf_auth_result( |
2745 | + SPFAuthResult::new() |
2746 | + .with_domain("test.org") |
2747 | + .with_scope(SPFDomainScope::MailFrom) |
2748 | + .with_result(SPFResult::None) |
2749 | + .with_human_result("no policy found"), |
2750 | + ), |
2751 | + ); |
2752 | + |
2753 | + let message = feedback |
2754 | + .as_rfc5322( |
2755 | + "initech.net", |
2756 | + "Initech Industries", |
2757 | + "noreply-dmarc@initech.net", |
2758 | + "dmarc-reports@example.org", |
2759 | + ) |
2760 | + .unwrap(); |
2761 | + let parsed_feedback = Feedback::parse_rfc5322(message.as_bytes()).unwrap(); |
2762 | + |
2763 | + assert_eq!(feedback, parsed_feedback); |
2764 | + } |
2765 | + } |
2766 | diff --git a/src/report/feedback/mod.rs b/src/report/feedback/mod.rs |
2767 | new file mode 100644 |
2768 | index 0000000..6caf68a |
2769 | --- /dev/null |
2770 | +++ b/src/report/feedback/mod.rs |
2771 | @@ -0,0 +1,507 @@ |
2772 | + pub mod generate; |
2773 | + pub mod parse; |
2774 | + |
2775 | + use std::fmt::Write; |
2776 | + use std::net::IpAddr; |
2777 | + |
2778 | + use crate::{ |
2779 | + dmarc::DMARC, |
2780 | + report::{ |
2781 | + ActionDisposition, Alignment, DKIMAuthResult, DKIMResult, DMARCResult, Disposition, |
2782 | + Feedback, PolicyOverride, PolicyOverrideReason, Record, SPFAuthResult, SPFDomainScope, |
2783 | + SPFResult, |
2784 | + }, |
2785 | + ARCOutput, DKIMOutput, DMARCOutput, SPFOutput, |
2786 | + }; |
2787 | + |
2788 | + impl Feedback { |
2789 | + pub fn new() -> Self { |
2790 | + Self::default() |
2791 | + } |
2792 | + |
2793 | + pub fn version(&self) -> f32 { |
2794 | + self.version |
2795 | + } |
2796 | + |
2797 | + pub fn with_version(mut self, version: f32) -> Self { |
2798 | + self.version = version; |
2799 | + self |
2800 | + } |
2801 | + |
2802 | + pub fn org_name(&self) -> &str { |
2803 | + &self.report_metadata.org_name |
2804 | + } |
2805 | + |
2806 | + pub fn with_org_name(mut self, org_name: impl Into<String>) -> Self { |
2807 | + self.report_metadata.org_name = org_name.into(); |
2808 | + self |
2809 | + } |
2810 | + |
2811 | + pub fn email(&self) -> &str { |
2812 | + &self.report_metadata.email |
2813 | + } |
2814 | + |
2815 | + pub fn with_email(mut self, email: impl Into<String>) -> Self { |
2816 | + self.report_metadata.email = email.into(); |
2817 | + self |
2818 | + } |
2819 | + |
2820 | + pub fn extra_contact_info(&self) -> Option<&str> { |
2821 | + self.report_metadata.extra_contact_info.as_deref() |
2822 | + } |
2823 | + |
2824 | + pub fn with_extra_contact_info(mut self, extra_contact_info: impl Into<String>) -> Self { |
2825 | + self.report_metadata.extra_contact_info = Some(extra_contact_info.into()); |
2826 | + self |
2827 | + } |
2828 | + |
2829 | + pub fn report_id(&self) -> &str { |
2830 | + &self.report_metadata.report_id |
2831 | + } |
2832 | + |
2833 | + pub fn with_report_id(mut self, report_id: impl Into<String>) -> Self { |
2834 | + self.report_metadata.report_id = report_id.into(); |
2835 | + self |
2836 | + } |
2837 | + |
2838 | + pub fn date_range_begin(&self) -> u64 { |
2839 | + self.report_metadata.date_range.begin |
2840 | + } |
2841 | + |
2842 | + pub fn with_date_range_begin(mut self, date_range_begin: u64) -> Self { |
2843 | + self.report_metadata.date_range.begin = date_range_begin; |
2844 | + self |
2845 | + } |
2846 | + |
2847 | + pub fn date_range_end(&self) -> u64 { |
2848 | + self.report_metadata.date_range.end |
2849 | + } |
2850 | + |
2851 | + pub fn with_date_range_end(mut self, date_range_end: u64) -> Self { |
2852 | + self.report_metadata.date_range.end = date_range_end; |
2853 | + self |
2854 | + } |
2855 | + |
2856 | + pub fn error(&self) -> &[String] { |
2857 | + &self.report_metadata.error |
2858 | + } |
2859 | + |
2860 | + pub fn with_error(mut self, error: impl Into<String>) -> Self { |
2861 | + self.report_metadata.error.push(error.into()); |
2862 | + self |
2863 | + } |
2864 | + |
2865 | + pub fn domain(&self) -> &str { |
2866 | + &self.policy_published.domain |
2867 | + } |
2868 | + |
2869 | + pub fn with_domain(mut self, domain: impl Into<String>) -> Self { |
2870 | + self.policy_published.domain = domain.into(); |
2871 | + self |
2872 | + } |
2873 | + |
2874 | + pub fn fo(&self) -> Option<&str> { |
2875 | + self.policy_published.fo.as_deref() |
2876 | + } |
2877 | + |
2878 | + pub fn with_fo(mut self, fo: impl Into<String>) -> Self { |
2879 | + self.policy_published.fo = Some(fo.into()); |
2880 | + self |
2881 | + } |
2882 | + |
2883 | + pub fn version_published(&self) -> Option<f32> { |
2884 | + self.policy_published.version_published |
2885 | + } |
2886 | + |
2887 | + pub fn with_version_published(mut self, version_published: f32) -> Self { |
2888 | + self.policy_published.version_published = Some(version_published); |
2889 | + self |
2890 | + } |
2891 | + |
2892 | + pub fn adkim(&self) -> Alignment { |
2893 | + self.policy_published.adkim |
2894 | + } |
2895 | + |
2896 | + pub fn with_adkim(mut self, adkim: Alignment) -> Self { |
2897 | + self.policy_published.adkim = adkim; |
2898 | + self |
2899 | + } |
2900 | + |
2901 | + pub fn aspf(&self) -> Alignment { |
2902 | + self.policy_published.aspf |
2903 | + } |
2904 | + |
2905 | + pub fn with_aspf(mut self, aspf: Alignment) -> Self { |
2906 | + self.policy_published.aspf = aspf; |
2907 | + self |
2908 | + } |
2909 | + |
2910 | + pub fn p(&self) -> Disposition { |
2911 | + self.policy_published.p |
2912 | + } |
2913 | + |
2914 | + pub fn with_p(mut self, p: Disposition) -> Self { |
2915 | + self.policy_published.p = p; |
2916 | + self |
2917 | + } |
2918 | + |
2919 | + pub fn sp(&self) -> Disposition { |
2920 | + self.policy_published.sp |
2921 | + } |
2922 | + |
2923 | + pub fn with_sp(mut self, sp: Disposition) -> Self { |
2924 | + self.policy_published.sp = sp; |
2925 | + self |
2926 | + } |
2927 | + |
2928 | + pub fn testing(&self) -> bool { |
2929 | + self.policy_published.testing |
2930 | + } |
2931 | + |
2932 | + pub fn with_testing(mut self, testing: bool) -> Self { |
2933 | + self.policy_published.testing = testing; |
2934 | + self |
2935 | + } |
2936 | + |
2937 | + pub fn records(&self) -> &[Record] { |
2938 | + &self.record |
2939 | + } |
2940 | + |
2941 | + pub fn with_record(mut self, record: Record) -> Self { |
2942 | + self.record.push(record); |
2943 | + self |
2944 | + } |
2945 | + |
2946 | + pub fn with_policy_published(mut self, dmarc: &DMARC) -> Self { |
2947 | + self.policy_published.adkim = (&dmarc.adkim).into(); |
2948 | + self.policy_published.aspf = (&dmarc.aspf).into(); |
2949 | + self.policy_published.p = (&dmarc.p).into(); |
2950 | + self.policy_published.sp = (&dmarc.sp).into(); |
2951 | + self.policy_published.testing = dmarc.t; |
2952 | + self.policy_published.fo = match &dmarc.fo { |
2953 | + crate::dmarc::Report::All => "0", |
2954 | + crate::dmarc::Report::Any => "1", |
2955 | + crate::dmarc::Report::Dkim => "d", |
2956 | + crate::dmarc::Report::Spf => "s", |
2957 | + crate::dmarc::Report::DkimSpf => "d:s", |
2958 | + } |
2959 | + .to_string() |
2960 | + .into(); |
2961 | + self |
2962 | + } |
2963 | + } |
2964 | + |
2965 | + impl Record { |
2966 | + pub fn new() -> Self { |
2967 | + Record::default() |
2968 | + } |
2969 | + |
2970 | + pub fn with_dkim_output(mut self, dkim_output: &[DKIMOutput]) { |
2971 | + for dkim in dkim_output { |
2972 | + if let Some(signature) = &dkim.signature { |
2973 | + let (result, human_result) = match &dkim.result { |
2974 | + crate::DKIMResult::Pass => (DKIMResult::Pass, None), |
2975 | + crate::DKIMResult::Neutral(err) => { |
2976 | + (DKIMResult::Neutral, err.to_string().into()) |
2977 | + } |
2978 | + crate::DKIMResult::Fail(err) => (DKIMResult::Fail, err.to_string().into()), |
2979 | + crate::DKIMResult::PermError(err) => { |
2980 | + (DKIMResult::PermError, err.to_string().into()) |
2981 | + } |
2982 | + crate::DKIMResult::TempError(err) => { |
2983 | + (DKIMResult::TempError, err.to_string().into()) |
2984 | + } |
2985 | + crate::DKIMResult::None => (DKIMResult::None, None), |
2986 | + }; |
2987 | + |
2988 | + self.auth_results.dkim.push(DKIMAuthResult { |
2989 | + domain: signature.d.to_string(), |
2990 | + selector: signature.s.to_string(), |
2991 | + result, |
2992 | + human_result, |
2993 | + }); |
2994 | + } |
2995 | + } |
2996 | + } |
2997 | + |
2998 | + pub fn with_spf_output(mut self, spf_output: &SPFOutput, scope: SPFDomainScope) { |
2999 | + self.auth_results.spf.push(SPFAuthResult { |
3000 | + domain: spf_output.domain.to_string(), |
3001 | + scope, |
3002 | + result: match spf_output.result { |
3003 | + crate::SPFResult::Pass => SPFResult::Pass, |
3004 | + crate::SPFResult::Fail => SPFResult::Fail, |
3005 | + crate::SPFResult::SoftFail => SPFResult::SoftFail, |
3006 | + crate::SPFResult::Neutral => SPFResult::Neutral, |
3007 | + crate::SPFResult::TempError => SPFResult::TempError, |
3008 | + crate::SPFResult::PermError => SPFResult::PermError, |
3009 | + crate::SPFResult::None => SPFResult::None, |
3010 | + }, |
3011 | + human_result: None, |
3012 | + }); |
3013 | + } |
3014 | + |
3015 | + pub fn with_dmarc_output(mut self, dmarc_output: &DMARCOutput) { |
3016 | + self.row.policy_evaluated.disposition = match dmarc_output.policy { |
3017 | + crate::dmarc::Policy::None => ActionDisposition::None, |
3018 | + crate::dmarc::Policy::Quarantine => ActionDisposition::Quarantine, |
3019 | + crate::dmarc::Policy::Reject => ActionDisposition::Reject, |
3020 | + crate::dmarc::Policy::Unspecified => ActionDisposition::None, |
3021 | + }; |
3022 | + self.row.policy_evaluated.dkim = (&dmarc_output.dkim_result).into(); |
3023 | + self.row.policy_evaluated.spf = (&dmarc_output.spf_result).into(); |
3024 | + } |
3025 | + |
3026 | + pub fn with_arc_output(mut self, arc_output: &ARCOutput) { |
3027 | + if arc_output.result == crate::DKIMResult::Pass { |
3028 | + let mut comment = "arc=pass".to_string(); |
3029 | + for set in arc_output.set.iter().rev() { |
3030 | + let seal = &set.seal.header; |
3031 | + write!( |
3032 | + &mut comment, |
3033 | + " as[{}].d={} as[{}].s={}", |
3034 | + seal.i, seal.d, seal.i, seal.s |
3035 | + ) |
3036 | + .ok(); |
3037 | + } |
3038 | + self.row |
3039 | + .policy_evaluated |
3040 | + .reason |
3041 | + .push(PolicyOverrideReason::new(PolicyOverride::LocalPolicy).with_comment(comment)); |
3042 | + } |
3043 | + } |
3044 | + |
3045 | + pub fn source_ip(&self) -> IpAddr { |
3046 | + self.row.source_ip |
3047 | + } |
3048 | + |
3049 | + pub fn with_source_ip(mut self, source_ip: IpAddr) -> Self { |
3050 | + self.row.source_ip = source_ip; |
3051 | + self |
3052 | + } |
3053 | + |
3054 | + pub fn count(&self) -> u32 { |
3055 | + self.row.count |
3056 | + } |
3057 | + |
3058 | + pub fn with_count(mut self, count: u32) -> Self { |
3059 | + self.row.count = count; |
3060 | + self |
3061 | + } |
3062 | + |
3063 | + pub fn action_disposition(&self) -> ActionDisposition { |
3064 | + self.row.policy_evaluated.disposition |
3065 | + } |
3066 | + |
3067 | + pub fn with_action_disposition(mut self, disposition: ActionDisposition) -> Self { |
3068 | + self.row.policy_evaluated.disposition = disposition; |
3069 | + self |
3070 | + } |
3071 | + |
3072 | + pub fn dmarc_dkim_result(&self) -> DMARCResult { |
3073 | + self.row.policy_evaluated.dkim |
3074 | + } |
3075 | + |
3076 | + pub fn with_dmarc_dkim_result(mut self, dkim: DMARCResult) -> Self { |
3077 | + self.row.policy_evaluated.dkim = dkim; |
3078 | + self |
3079 | + } |
3080 | + |
3081 | + pub fn dmarc_spf_result(&self) -> DMARCResult { |
3082 | + self.row.policy_evaluated.spf |
3083 | + } |
3084 | + |
3085 | + pub fn with_dmarc_spf_result(mut self, spf: DMARCResult) -> Self { |
3086 | + self.row.policy_evaluated.spf = spf; |
3087 | + self |
3088 | + } |
3089 | + |
3090 | + pub fn policy_override_reason(&self) -> &[PolicyOverrideReason] { |
3091 | + &self.row.policy_evaluated.reason |
3092 | + } |
3093 | + |
3094 | + pub fn with_policy_override_reason(mut self, reason: PolicyOverrideReason) -> Self { |
3095 | + self.row.policy_evaluated.reason.push(reason); |
3096 | + self |
3097 | + } |
3098 | + |
3099 | + pub fn envelope_from(&self) -> &str { |
3100 | + &self.identifiers.envelope_from |
3101 | + } |
3102 | + |
3103 | + pub fn with_envelope_from(mut self, envelope_from: impl Into<String>) -> Self { |
3104 | + self.identifiers.envelope_from = envelope_from.into(); |
3105 | + self |
3106 | + } |
3107 | + |
3108 | + pub fn header_from(&self) -> &str { |
3109 | + &self.identifiers.header_from |
3110 | + } |
3111 | + |
3112 | + pub fn with_header_from(mut self, header_from: impl Into<String>) -> Self { |
3113 | + self.identifiers.header_from = header_from.into(); |
3114 | + self |
3115 | + } |
3116 | + |
3117 | + pub fn envelope_to(&self) -> Option<&str> { |
3118 | + self.identifiers.envelope_to.as_deref() |
3119 | + } |
3120 | + |
3121 | + pub fn with_envelope_to(mut self, envelope_to: impl Into<String>) -> Self { |
3122 | + self.identifiers.envelope_to = Some(envelope_to.into()); |
3123 | + self |
3124 | + } |
3125 | + |
3126 | + pub fn dkim_auth_result(&self) -> &[DKIMAuthResult] { |
3127 | + &self.auth_results.dkim |
3128 | + } |
3129 | + |
3130 | + pub fn with_dkim_auth_result(mut self, auth_result: DKIMAuthResult) -> Self { |
3131 | + self.auth_results.dkim.push(auth_result); |
3132 | + self |
3133 | + } |
3134 | + |
3135 | + pub fn spf_auth_result(&self) -> &[SPFAuthResult] { |
3136 | + &self.auth_results.spf |
3137 | + } |
3138 | + |
3139 | + pub fn with_spf_auth_result(mut self, auth_result: SPFAuthResult) -> Self { |
3140 | + self.auth_results.spf.push(auth_result); |
3141 | + self |
3142 | + } |
3143 | + } |
3144 | + |
3145 | + impl DKIMAuthResult { |
3146 | + pub fn new() -> Self { |
3147 | + DKIMAuthResult::default() |
3148 | + } |
3149 | + |
3150 | + pub fn domain(&self) -> &str { |
3151 | + &self.domain |
3152 | + } |
3153 | + |
3154 | + pub fn with_domain(mut self, domain: impl Into<String>) -> Self { |
3155 | + self.domain = domain.into(); |
3156 | + self |
3157 | + } |
3158 | + |
3159 | + pub fn selector(&self) -> &str { |
3160 | + &self.selector |
3161 | + } |
3162 | + |
3163 | + pub fn with_selector(mut self, selector: impl Into<String>) -> Self { |
3164 | + self.selector = selector.into(); |
3165 | + self |
3166 | + } |
3167 | + |
3168 | + pub fn result(&self) -> DKIMResult { |
3169 | + self.result |
3170 | + } |
3171 | + |
3172 | + pub fn with_result(mut self, result: DKIMResult) -> Self { |
3173 | + self.result = result; |
3174 | + self |
3175 | + } |
3176 | + |
3177 | + pub fn human_result(&self) -> Option<&str> { |
3178 | + self.human_result.as_deref() |
3179 | + } |
3180 | + |
3181 | + pub fn with_human_result(mut self, human_result: impl Into<String>) -> Self { |
3182 | + self.human_result = Some(human_result.into()); |
3183 | + self |
3184 | + } |
3185 | + } |
3186 | + |
3187 | + impl SPFAuthResult { |
3188 | + pub fn new() -> Self { |
3189 | + SPFAuthResult::default() |
3190 | + } |
3191 | + |
3192 | + pub fn domain(&self) -> &str { |
3193 | + &self.domain |
3194 | + } |
3195 | + |
3196 | + pub fn with_domain(mut self, domain: impl Into<String>) -> Self { |
3197 | + self.domain = domain.into(); |
3198 | + self |
3199 | + } |
3200 | + |
3201 | + pub fn scope(&self) -> SPFDomainScope { |
3202 | + self.scope |
3203 | + } |
3204 | + |
3205 | + pub fn with_scope(mut self, scope: SPFDomainScope) -> Self { |
3206 | + self.scope = scope; |
3207 | + self |
3208 | + } |
3209 | + |
3210 | + pub fn result(&self) -> SPFResult { |
3211 | + self.result |
3212 | + } |
3213 | + |
3214 | + pub fn with_result(mut self, result: SPFResult) -> Self { |
3215 | + self.result = result; |
3216 | + self |
3217 | + } |
3218 | + |
3219 | + pub fn human_result(&self) -> Option<&str> { |
3220 | + self.human_result.as_deref() |
3221 | + } |
3222 | + |
3223 | + pub fn with_human_result(mut self, human_result: impl Into<String>) -> Self { |
3224 | + self.human_result = Some(human_result.into()); |
3225 | + self |
3226 | + } |
3227 | + } |
3228 | + |
3229 | + impl PolicyOverrideReason { |
3230 | + pub fn new(type_: PolicyOverride) -> Self { |
3231 | + PolicyOverrideReason { |
3232 | + type_, |
3233 | + comment: None, |
3234 | + } |
3235 | + } |
3236 | + |
3237 | + pub fn with_comment(mut self, comment: impl Into<String>) -> Self { |
3238 | + self.comment = Some(comment.into()); |
3239 | + self |
3240 | + } |
3241 | + |
3242 | + pub fn comment(&self) -> Option<&str> { |
3243 | + self.comment.as_deref() |
3244 | + } |
3245 | + |
3246 | + pub fn policy_override(&self) -> PolicyOverride { |
3247 | + self.type_ |
3248 | + } |
3249 | + } |
3250 | + |
3251 | + impl From<&crate::DMARCResult> for DMARCResult { |
3252 | + fn from(result: &crate::DMARCResult) -> Self { |
3253 | + match result { |
3254 | + crate::DMARCResult::Pass => DMARCResult::Pass, |
3255 | + _ => DMARCResult::Fail, |
3256 | + } |
3257 | + } |
3258 | + } |
3259 | + |
3260 | + impl From<&crate::dmarc::Alignment> for Alignment { |
3261 | + fn from(aligment: &crate::dmarc::Alignment) -> Self { |
3262 | + match aligment { |
3263 | + crate::dmarc::Alignment::Relaxed => Alignment::Relaxed, |
3264 | + crate::dmarc::Alignment::Strict => Alignment::Strict, |
3265 | + } |
3266 | + } |
3267 | + } |
3268 | + |
3269 | + impl From<&crate::dmarc::Policy> for Disposition { |
3270 | + fn from(policy: &crate::dmarc::Policy) -> Self { |
3271 | + match policy { |
3272 | + crate::dmarc::Policy::None => Disposition::None, |
3273 | + crate::dmarc::Policy::Quarantine => Disposition::Quarantine, |
3274 | + crate::dmarc::Policy::Reject => Disposition::Reject, |
3275 | + crate::dmarc::Policy::Unspecified => Disposition::None, |
3276 | + } |
3277 | + } |
3278 | + } |
3279 | diff --git a/src/report/feedback/parse.rs b/src/report/feedback/parse.rs |
3280 | new file mode 100644 |
3281 | index 0000000..5252fdf |
3282 | --- /dev/null |
3283 | +++ b/src/report/feedback/parse.rs |
3284 | @@ -0,0 +1,821 @@ |
3285 | + use std::io::{BufRead, Cursor, Read}; |
3286 | + use std::str::FromStr; |
3287 | + |
3288 | + use flate2::read::GzDecoder; |
3289 | + use mail_parser::{Message, MimeHeaders, PartType}; |
3290 | + use quick_xml::events::{BytesStart, Event}; |
3291 | + use quick_xml::reader::Reader; |
3292 | + |
3293 | + use crate::report::{ |
3294 | + ActionDisposition, Alignment, AuthResult, DKIMAuthResult, DKIMResult, DMARCResult, DateRange, |
3295 | + Disposition, Error, Extension, Feedback, Identifier, PolicyEvaluated, PolicyOverride, |
3296 | + PolicyOverrideReason, PolicyPublished, Record, ReportMetadata, Row, SPFAuthResult, |
3297 | + SPFDomainScope, SPFResult, |
3298 | + }; |
3299 | + |
3300 | + impl Feedback { |
3301 | + pub fn parse_rfc5322(report: &[u8]) -> Result<Self, Error> { |
3302 | + let message = Message::parse(report).ok_or(Error::MailParseError)?; |
3303 | + let mut error = Error::NoReportsFound; |
3304 | + |
3305 | + for part in &message.parts { |
3306 | + match &part.body { |
3307 | + PartType::Text(report) |
3308 | + if part |
3309 | + .content_type() |
3310 | + .and_then(|ct| ct.subtype()) |
3311 | + .map_or(false, |t| t.eq_ignore_ascii_case("xml")) |
3312 | + || part |
3313 | + .attachment_name() |
3314 | + .and_then(|n| n.rsplit_once('.')) |
3315 | + .map_or(false, |(_, e)| e.eq_ignore_ascii_case("xml")) => |
3316 | + { |
3317 | + match Feedback::parse_xml(report.as_bytes()) { |
3318 | + Ok(feedback) => return Ok(feedback), |
3319 | + Err(err) => { |
3320 | + error = err.into(); |
3321 | + } |
3322 | + } |
3323 | + } |
3324 | + PartType::Binary(report) | PartType::InlineBinary(report) => { |
3325 | + enum ReportType { |
3326 | + Xml, |
3327 | + Gzip, |
3328 | + Zip, |
3329 | + } |
3330 | + |
3331 | + let (_, ext) = part |
3332 | + .attachment_name() |
3333 | + .unwrap_or("file.none") |
3334 | + .rsplit_once('.') |
3335 | + .unwrap_or(("file", "none")); |
3336 | + let subtype = part |
3337 | + .content_type() |
3338 | + .and_then(|ct| ct.subtype()) |
3339 | + .unwrap_or("none"); |
3340 | + let rt = if subtype.eq_ignore_ascii_case("gzip") { |
3341 | + ReportType::Gzip |
3342 | + } else if subtype.eq_ignore_ascii_case("zip") { |
3343 | + ReportType::Zip |
3344 | + } else if subtype.eq_ignore_ascii_case("xml") { |
3345 | + ReportType::Xml |
3346 | + } else if ext.eq_ignore_ascii_case("gz") { |
3347 | + ReportType::Gzip |
3348 | + } else if ext.eq_ignore_ascii_case("zip") { |
3349 | + ReportType::Zip |
3350 | + } else if ext.eq_ignore_ascii_case("xml") { |
3351 | + ReportType::Xml |
3352 | + } else { |
3353 | + continue; |
3354 | + }; |
3355 | + |
3356 | + match rt { |
3357 | + ReportType::Gzip => { |
3358 | + let mut file = GzDecoder::new(report.as_ref()); |
3359 | + let mut buf = Vec::new(); |
3360 | + file.read_to_end(&mut buf) |
3361 | + .map_err(|err| Error::UncompressError(err.to_string()))?; |
3362 | + |
3363 | + match Feedback::parse_xml(&buf) { |
3364 | + Ok(feedback) => return Ok(feedback), |
3365 | + Err(err) => { |
3366 | + error = err.into(); |
3367 | + } |
3368 | + } |
3369 | + } |
3370 | + ReportType::Zip => { |
3371 | + let mut archive = zip::ZipArchive::new(Cursor::new(report.as_ref())) |
3372 | + .map_err(|err| Error::UncompressError(err.to_string()))?; |
3373 | + for i in 0..archive.len() { |
3374 | + match archive.by_index(i) { |
3375 | + Ok(mut file) => { |
3376 | + let mut buf = |
3377 | + Vec::with_capacity(file.compressed_size() as usize); |
3378 | + file.read_to_end(&mut buf).map_err(|err| { |
3379 | + Error::UncompressError(err.to_string()) |
3380 | + })?; |
3381 | + match Feedback::parse_xml(&buf) { |
3382 | + Ok(feedback) => return Ok(feedback), |
3383 | + Err(err) => { |
3384 | + error = err.into(); |
3385 | + } |
3386 | + } |
3387 | + } |
3388 | + Err(err) => { |
3389 | + error = Error::UncompressError(err.to_string()); |
3390 | + } |
3391 | + } |
3392 | + } |
3393 | + } |
3394 | + ReportType::Xml => match Feedback::parse_xml(report) { |
3395 | + Ok(feedback) => return Ok(feedback), |
3396 | + Err(err) => { |
3397 | + error = err.into(); |
3398 | + } |
3399 | + }, |
3400 | + } |
3401 | + } |
3402 | + _ => (), |
3403 | + } |
3404 | + } |
3405 | + |
3406 | + Err(error) |
3407 | + } |
3408 | + |
3409 | + pub fn parse_xml(report: &[u8]) -> Result<Self, String> { |
3410 | + let mut version: f32 = 0.0; |
3411 | + let mut report_metadata = None; |
3412 | + let mut policy_published = None; |
3413 | + let mut record = Vec::new(); |
3414 | + let mut extensions = Vec::new(); |
3415 | + |
3416 | + let mut reader = Reader::from_reader(report); |
3417 | + reader.trim_text(true); |
3418 | + |
3419 | + let mut buf = Vec::with_capacity(128); |
3420 | + let mut found_feedback = false; |
3421 | + |
3422 | + while let Some(tag) = reader.next_tag(&mut buf)? { |
3423 | + match tag.name().as_ref() { |
3424 | + b"feedback" if !found_feedback => { |
3425 | + found_feedback = true; |
3426 | + } |
3427 | + b"version" if found_feedback => { |
3428 | + version = reader.next_value(&mut buf)?.unwrap_or(0.0); |
3429 | + } |
3430 | + b"report_metadata" if found_feedback => { |
3431 | + report_metadata = ReportMetadata::parse(&mut reader, &mut buf)?.into(); |
3432 | + } |
3433 | + b"policy_published" if found_feedback => { |
3434 | + policy_published = PolicyPublished::parse(&mut reader, &mut buf)?.into(); |
3435 | + } |
3436 | + b"record" if found_feedback => { |
3437 | + record.push(Record::parse(&mut reader, &mut buf)?); |
3438 | + } |
3439 | + b"extensions" if found_feedback => { |
3440 | + Extension::parse(&mut reader, &mut buf, &mut extensions)?; |
3441 | + } |
3442 | + b"" => {} |
3443 | + other if !found_feedback => { |
3444 | + return Err(format!( |
3445 | + "Unexpected tag {} at position {}.", |
3446 | + String::from_utf8_lossy(other), |
3447 | + reader.buffer_position() |
3448 | + )); |
3449 | + } |
3450 | + _ => (), |
3451 | + } |
3452 | + } |
3453 | + |
3454 | + Ok(Feedback { |
3455 | + version, |
3456 | + report_metadata: report_metadata.ok_or("Missing feedback/report_metadata tag.")?, |
3457 | + policy_published: policy_published.ok_or("Missing feedback/policy_published tag.")?, |
3458 | + record, |
3459 | + extensions, |
3460 | + }) |
3461 | + } |
3462 | + } |
3463 | + |
3464 | + impl ReportMetadata { |
3465 | + pub(crate) fn parse<R: BufRead>( |
3466 | + reader: &mut Reader<R>, |
3467 | + buf: &mut Vec<u8>, |
3468 | + ) -> Result<Self, String> { |
3469 | + let mut rm = ReportMetadata::default(); |
3470 | + |
3471 | + while let Some(tag) = reader.next_tag(buf)? { |
3472 | + match tag.name().as_ref() { |
3473 | + b"org_name" => { |
3474 | + rm.org_name = reader.next_value::<String>(buf)?.unwrap_or_default(); |
3475 | + } |
3476 | + b"email" => { |
3477 | + rm.email = reader.next_value::<String>(buf)?.unwrap_or_default(); |
3478 | + } |
3479 | + b"extra_contact_info" => { |
3480 | + rm.extra_contact_info = reader.next_value::<String>(buf)?; |
3481 | + } |
3482 | + b"report_id" => { |
3483 | + rm.report_id = reader.next_value::<String>(buf)?.unwrap_or_default(); |
3484 | + } |
3485 | + b"date_range" => { |
3486 | + rm.date_range = DateRange::parse(reader, buf)?; |
3487 | + } |
3488 | + b"error" => { |
3489 | + if let Some(err) = reader.next_value::<String>(buf)? { |
3490 | + rm.error.push(err); |
3491 | + } |
3492 | + } |
3493 | + b"" => (), |
3494 | + _ => { |
3495 | + reader.skip_tag(buf)?; |
3496 | + } |
3497 | + } |
3498 | + } |
3499 | + |
3500 | + Ok(rm) |
3501 | + } |
3502 | + } |
3503 | + |
3504 | + impl DateRange { |
3505 | + pub(crate) fn parse<R: BufRead>( |
3506 | + reader: &mut Reader<R>, |
3507 | + buf: &mut Vec<u8>, |
3508 | + ) -> Result<Self, String> { |
3509 | + let mut dr = DateRange::default(); |
3510 | + |
3511 | + while let Some(tag) = reader.next_tag(buf)? { |
3512 | + match tag.name().as_ref() { |
3513 | + b"begin" => { |
3514 | + dr.begin = reader.next_value(buf)?.unwrap_or_default(); |
3515 | + } |
3516 | + b"end" => { |
3517 | + dr.end = reader.next_value(buf)?.unwrap_or_default(); |
3518 | + } |
3519 | + b"" => (), |
3520 | + _ => { |
3521 | + reader.skip_tag(buf)?; |
3522 | + } |
3523 | + } |
3524 | + } |
3525 | + |
3526 | + Ok(dr) |
3527 | + } |
3528 | + } |
3529 | + |
3530 | + impl PolicyPublished { |
3531 | + pub(crate) fn parse<R: BufRead>( |
3532 | + reader: &mut Reader<R>, |
3533 | + buf: &mut Vec<u8>, |
3534 | + ) -> Result<Self, String> { |
3535 | + let mut p = PolicyPublished::default(); |
3536 | + |
3537 | + while let Some(tag) = reader.next_tag(buf)? { |
3538 | + match tag.name().as_ref() { |
3539 | + b"domain" => { |
3540 | + p.domain = reader.next_value::<String>(buf)?.unwrap_or_default(); |
3541 | + } |
3542 | + b"version_published" => { |
3543 | + p.version_published = reader.next_value(buf)?; |
3544 | + } |
3545 | + b"adkim" => { |
3546 | + p.adkim = reader.next_value(buf)?.unwrap_or_default(); |
3547 | + } |
3548 | + b"aspf" => { |
3549 | + p.aspf = reader.next_value(buf)?.unwrap_or_default(); |
3550 | + } |
3551 | + b"p" => { |
3552 | + p.p = reader.next_value(buf)?.unwrap_or_default(); |
3553 | + } |
3554 | + b"sp" => { |
3555 | + p.sp = reader.next_value(buf)?.unwrap_or_default(); |
3556 | + } |
3557 | + b"testing" => { |
3558 | + p.testing = reader |
3559 | + .next_value::<String>(buf)? |
3560 | + .map_or(false, |s| s.eq_ignore_ascii_case("y")); |
3561 | + } |
3562 | + b"fo" => { |
3563 | + p.fo = reader.next_value::<String>(buf)?; |
3564 | + } |
3565 | + b"" => (), |
3566 | + _ => { |
3567 | + reader.skip_tag(buf)?; |
3568 | + } |
3569 | + } |
3570 | + } |
3571 | + |
3572 | + Ok(p) |
3573 | + } |
3574 | + } |
3575 | + |
3576 | + impl Extension { |
3577 | + pub(crate) fn parse<R: BufRead>( |
3578 | + reader: &mut Reader<R>, |
3579 | + buf: &mut Vec<u8>, |
3580 | + extensions: &mut Vec<Extension>, |
3581 | + ) -> Result<(), String> { |
3582 | + while let Some(tag) = reader.next_tag(buf)? { |
3583 | + match tag.name().as_ref() { |
3584 | + b"extension" => { |
3585 | + let mut e = Extension::default(); |
3586 | + if let Ok(Some(attr)) = tag.try_get_attribute("name") { |
3587 | + if let Ok(attr) = attr.unescape_value() { |
3588 | + e.name = attr.to_string(); |
3589 | + } |
3590 | + } |
3591 | + if let Ok(Some(attr)) = tag.try_get_attribute("definition") { |
3592 | + if let Ok(attr) = attr.unescape_value() { |
3593 | + e.definition = attr.to_string(); |
3594 | + } |
3595 | + } |
3596 | + extensions.push(e); |
3597 | + reader.skip_tag(buf)?; |
3598 | + } |
3599 | + b"" => (), |
3600 | + _ => { |
3601 | + reader.skip_tag(buf)?; |
3602 | + } |
3603 | + } |
3604 | + } |
3605 | + |
3606 | + Ok(()) |
3607 | + } |
3608 | + } |
3609 | + |
3610 | + impl Record { |
3611 | + pub(crate) fn parse<R: BufRead>( |
3612 | + reader: &mut Reader<R>, |
3613 | + buf: &mut Vec<u8>, |
3614 | + ) -> Result<Self, String> { |
3615 | + let mut r = Record::default(); |
3616 | + |
3617 | + while let Some(tag) = reader.next_tag(buf)? { |
3618 | + match tag.name().as_ref() { |
3619 | + b"row" => { |
3620 | + r.row = Row::parse(reader, buf)?; |
3621 | + } |
3622 | + b"identifiers" => { |
3623 | + r.identifiers = Identifier::parse(reader, buf)?; |
3624 | + } |
3625 | + b"auth_results" => { |
3626 | + r.auth_results = AuthResult::parse(reader, buf)?; |
3627 | + } |
3628 | + b"extensions" => { |
3629 | + Extension::parse(reader, buf, &mut r.extensions)?; |
3630 | + } |
3631 | + b"" => (), |
3632 | + _ => { |
3633 | + reader.skip_tag(buf)?; |
3634 | + } |
3635 | + } |
3636 | + } |
3637 | + |
3638 | + Ok(r) |
3639 | + } |
3640 | + } |
3641 | + |
3642 | + impl Row { |
3643 | + pub(crate) fn parse<R: BufRead>( |
3644 | + reader: &mut Reader<R>, |
3645 | + buf: &mut Vec<u8>, |
3646 | + ) -> Result<Self, String> { |
3647 | + let mut r = Row::default(); |
3648 | + |
3649 | + while let Some(tag) = reader.next_tag(buf)? { |
3650 | + match tag.name().as_ref() { |
3651 | + b"source_ip" => { |
3652 | + if let Some(ip) = reader.next_value(buf)? { |
3653 | + r.source_ip = ip; |
3654 | + } |
3655 | + } |
3656 | + b"count" => { |
3657 | + r.count = reader.next_value(buf)?.unwrap_or_default(); |
3658 | + } |
3659 | + b"policy_evaluated" => { |
3660 | + r.policy_evaluated = PolicyEvaluated::parse(reader, buf)?; |
3661 | + } |
3662 | + b"" => (), |
3663 | + _ => { |
3664 | + reader.skip_tag(buf)?; |
3665 | + } |
3666 | + } |
3667 | + } |
3668 | + |
3669 | + Ok(r) |
3670 | + } |
3671 | + } |
3672 | + |
3673 | + impl PolicyEvaluated { |
3674 | + pub(crate) fn parse<R: BufRead>( |
3675 | + reader: &mut Reader<R>, |
3676 | + buf: &mut Vec<u8>, |
3677 | + ) -> Result<Self, String> { |
3678 | + let mut pe = PolicyEvaluated::default(); |
3679 | + |
3680 | + while let Some(tag) = reader.next_tag(buf)? { |
3681 | + match tag.name().as_ref() { |
3682 | + b"disposition" => { |
3683 | + pe.disposition = reader.next_value(buf)?.unwrap_or_default(); |
3684 | + } |
3685 | + b"dkim" => { |
3686 | + pe.dkim = reader.next_value(buf)?.unwrap_or_default(); |
3687 | + } |
3688 | + b"spf" => { |
3689 | + pe.spf = reader.next_value(buf)?.unwrap_or_default(); |
3690 | + } |
3691 | + b"reason" => { |
3692 | + pe.reason.push(PolicyOverrideReason::parse(reader, buf)?); |
3693 | + } |
3694 | + b"" => (), |
3695 | + _ => { |
3696 | + reader.skip_tag(buf)?; |
3697 | + } |
3698 | + } |
3699 | + } |
3700 | + |
3701 | + Ok(pe) |
3702 | + } |
3703 | + } |
3704 | + |
3705 | + impl PolicyOverrideReason { |
3706 | + pub(crate) fn parse<R: BufRead>( |
3707 | + reader: &mut Reader<R>, |
3708 | + buf: &mut Vec<u8>, |
3709 | + ) -> Result<Self, String> { |
3710 | + let mut por = PolicyOverrideReason::default(); |
3711 | + |
3712 | + while let Some(tag) = reader.next_tag(buf)? { |
3713 | + match tag.name().as_ref() { |
3714 | + b"type" => { |
3715 | + por.type_ = reader.next_value(buf)?.unwrap_or_default(); |
3716 | + } |
3717 | + b"comment" => { |
3718 | + por.comment = reader.next_value(buf)?; |
3719 | + } |
3720 | + b"" => (), |
3721 | + _ => { |
3722 | + reader.skip_tag(buf)?; |
3723 | + } |
3724 | + } |
3725 | + } |
3726 | + |
3727 | + Ok(por) |
3728 | + } |
3729 | + } |
3730 | + |
3731 | + impl Identifier { |
3732 | + pub(crate) fn parse<R: BufRead>( |
3733 | + reader: &mut Reader<R>, |
3734 | + buf: &mut Vec<u8>, |
3735 | + ) -> Result<Self, String> { |
3736 | + let mut i = Identifier::default(); |
3737 | + |
3738 | + while let Some(tag) = reader.next_tag(buf)? { |
3739 | + match tag.name().as_ref() { |
3740 | + b"envelope_to" => { |
3741 | + i.envelope_to = reader.next_value(buf)?; |
3742 | + } |
3743 | + b"envelope_from" => { |
3744 | + i.envelope_from = reader.next_value(buf)?.unwrap_or_default(); |
3745 | + } |
3746 | + b"header_from" => { |
3747 | + i.header_from = reader.next_value(buf)?.unwrap_or_default(); |
3748 | + } |
3749 | + b"" => (), |
3750 | + _ => { |
3751 | + reader.skip_tag(buf)?; |
3752 | + } |
3753 | + } |
3754 | + } |
3755 | + |
3756 | + Ok(i) |
3757 | + } |
3758 | + } |
3759 | + |
3760 | + impl AuthResult { |
3761 | + pub(crate) fn parse<R: BufRead>( |
3762 | + reader: &mut Reader<R>, |
3763 | + buf: &mut Vec<u8>, |
3764 | + ) -> Result<Self, String> { |
3765 | + let mut ar = AuthResult::default(); |
3766 | + |
3767 | + while let Some(tag) = reader.next_tag(buf)? { |
3768 | + match tag.name().as_ref() { |
3769 | + b"dkim" => { |
3770 | + ar.dkim.push(DKIMAuthResult::parse(reader, buf)?); |
3771 | + } |
3772 | + b"spf" => { |
3773 | + ar.spf.push(SPFAuthResult::parse(reader, buf)?); |
3774 | + } |
3775 | + b"" => (), |
3776 | + _ => { |
3777 | + reader.skip_tag(buf)?; |
3778 | + } |
3779 | + } |
3780 | + } |
3781 | + |
3782 | + Ok(ar) |
3783 | + } |
3784 | + } |
3785 | + |
3786 | + impl DKIMAuthResult { |
3787 | + pub(crate) fn parse<R: BufRead>( |
3788 | + reader: &mut Reader<R>, |
3789 | + buf: &mut Vec<u8>, |
3790 | + ) -> Result<Self, String> { |
3791 | + let mut dar = DKIMAuthResult::default(); |
3792 | + |
3793 | + while let Some(tag) = reader.next_tag(buf)? { |
3794 | + match tag.name().as_ref() { |
3795 | + b"domain" => { |
3796 | + dar.domain = reader.next_value(buf)?.unwrap_or_default(); |
3797 | + } |
3798 | + b"selector" => { |
3799 | + dar.selector = reader.next_value(buf)?.unwrap_or_default(); |
3800 | + } |
3801 | + b"result" => { |
3802 | + dar.result = reader.next_value(buf)?.unwrap_or_default(); |
3803 | + } |
3804 | + b"human_result" => { |
3805 | + dar.human_result = reader.next_value(buf)?; |
3806 | + } |
3807 | + b"" => (), |
3808 | + _ => { |
3809 | + reader.skip_tag(buf)?; |
3810 | + } |
3811 | + } |
3812 | + } |
3813 | + |
3814 | + Ok(dar) |
3815 | + } |
3816 | + } |
3817 | + |
3818 | + impl SPFAuthResult { |
3819 | + pub(crate) fn parse<R: BufRead>( |
3820 | + reader: &mut Reader<R>, |
3821 | + buf: &mut Vec<u8>, |
3822 | + ) -> Result<Self, String> { |
3823 | + let mut sar = SPFAuthResult::default(); |
3824 | + |
3825 | + while let Some(tag) = reader.next_tag(buf)? { |
3826 | + match tag.name().as_ref() { |
3827 | + b"domain" => { |
3828 | + sar.domain = reader.next_value(buf)?.unwrap_or_default(); |
3829 | + } |
3830 | + b"scope" => { |
3831 | + sar.scope = reader.next_value(buf)?.unwrap_or_default(); |
3832 | + } |
3833 | + b"result" => { |
3834 | + sar.result = reader.next_value(buf)?.unwrap_or_default(); |
3835 | + } |
3836 | + b"human_result" => { |
3837 | + sar.human_result = reader.next_value(buf)?; |
3838 | + } |
3839 | + b"" => (), |
3840 | + _ => { |
3841 | + reader.skip_tag(buf)?; |
3842 | + } |
3843 | + } |
3844 | + } |
3845 | + |
3846 | + Ok(sar) |
3847 | + } |
3848 | + } |
3849 | + |
3850 | + impl FromStr for PolicyOverride { |
3851 | + type Err = (); |
3852 | + |
3853 | + fn from_str(s: &str) -> Result<Self, Self::Err> { |
3854 | + Ok(match s.as_bytes() { |
3855 | + b"forwarded" => PolicyOverride::Forwarded, |
3856 | + b"sampled_out" => PolicyOverride::SampledOut, |
3857 | + b"trusted_forwarder" => PolicyOverride::TrustedForwarder, |
3858 | + b"mailing_list" => PolicyOverride::MailingList, |
3859 | + b"local_policy" => PolicyOverride::LocalPolicy, |
3860 | + b"other" => PolicyOverride::Other, |
3861 | + _ => PolicyOverride::Other, |
3862 | + }) |
3863 | + } |
3864 | + } |
3865 | + |
3866 | + impl FromStr for DMARCResult { |
3867 | + type Err = (); |
3868 | + |
3869 | + fn from_str(s: &str) -> Result<Self, Self::Err> { |
3870 | + Ok(match s.as_bytes() { |
3871 | + b"pass" => DMARCResult::Pass, |
3872 | + b"fail" => DMARCResult::Fail, |
3873 | + _ => DMARCResult::Unspecified, |
3874 | + }) |
3875 | + } |
3876 | + } |
3877 | + |
3878 | + impl FromStr for DKIMResult { |
3879 | + type Err = (); |
3880 | + |
3881 | + fn from_str(s: &str) -> Result<Self, Self::Err> { |
3882 | + Ok(match s.as_bytes() { |
3883 | + b"none" => DKIMResult::None, |
3884 | + b"pass" => DKIMResult::Pass, |
3885 | + b"fail" => DKIMResult::Fail, |
3886 | + b"policy" => DKIMResult::Policy, |
3887 | + b"neutral" => DKIMResult::Neutral, |
3888 | + b"temperror" => DKIMResult::TempError, |
3889 | + b"permerror" => DKIMResult::PermError, |
3890 | + _ => DKIMResult::None, |
3891 | + }) |
3892 | + } |
3893 | + } |
3894 | + |
3895 | + impl FromStr for SPFResult { |
3896 | + type Err = (); |
3897 | + |
3898 | + fn from_str(s: &str) -> Result<Self, Self::Err> { |
3899 | + Ok(match s.as_bytes() { |
3900 | + b"none" => SPFResult::None, |
3901 | + b"pass" => SPFResult::Pass, |
3902 | + b"fail" => SPFResult::Fail, |
3903 | + b"softfail" => SPFResult::SoftFail, |
3904 | + b"neutral" => SPFResult::Neutral, |
3905 | + b"temperror" => SPFResult::TempError, |
3906 | + b"permerror" => SPFResult::PermError, |
3907 | + _ => SPFResult::None, |
3908 | + }) |
3909 | + } |
3910 | + } |
3911 | + |
3912 | + impl FromStr for SPFDomainScope { |
3913 | + type Err = (); |
3914 | + |
3915 | + fn from_str(s: &str) -> Result<Self, Self::Err> { |
3916 | + Ok(match s.as_bytes() { |
3917 | + b"helo" => SPFDomainScope::Helo, |
3918 | + b"mfrom" => SPFDomainScope::MailFrom, |
3919 | + _ => SPFDomainScope::Unspecified, |
3920 | + }) |
3921 | + } |
3922 | + } |
3923 | + |
3924 | + impl FromStr for ActionDisposition { |
3925 | + type Err = (); |
3926 | + |
3927 | + fn from_str(s: &str) -> Result<Self, Self::Err> { |
3928 | + Ok(match s.as_bytes() { |
3929 | + b"none" => ActionDisposition::None, |
3930 | + b"pass" => ActionDisposition::Pass, |
3931 | + b"quarantine" => ActionDisposition::Quarantine, |
3932 | + b"reject" => ActionDisposition::Reject, |
3933 | + _ => ActionDisposition::Unspecified, |
3934 | + }) |
3935 | + } |
3936 | + } |
3937 | + |
3938 | + impl FromStr for Disposition { |
3939 | + type Err = (); |
3940 | + |
3941 | + fn from_str(s: &str) -> Result<Self, Self::Err> { |
3942 | + Ok(match s.as_bytes() { |
3943 | + b"none" => Disposition::None, |
3944 | + b"quarantine" => Disposition::Quarantine, |
3945 | + b"reject" => Disposition::Reject, |
3946 | + _ => Disposition::Unspecified, |
3947 | + }) |
3948 | + } |
3949 | + } |
3950 | + |
3951 | + impl FromStr for Alignment { |
3952 | + type Err = (); |
3953 | + |
3954 | + fn from_str(s: &str) -> Result<Self, Self::Err> { |
3955 | + Ok(match s.as_bytes().first() { |
3956 | + Some(b'r') => Alignment::Relaxed, |
3957 | + Some(b's') => Alignment::Strict, |
3958 | + _ => Alignment::Unspecified, |
3959 | + }) |
3960 | + } |
3961 | + } |
3962 | + |
3963 | + trait ReaderHelper { |
3964 | + fn next_tag<'x>(&mut self, buf: &'x mut Vec<u8>) -> Result<Option<BytesStart<'x>>, String>; |
3965 | + fn next_value<T: FromStr>(&mut self, buf: &mut Vec<u8>) -> Result<Option<T>, String>; |
3966 | + fn skip_tag(&mut self, buf: &mut Vec<u8>) -> Result<(), String>; |
3967 | + } |
3968 | + |
3969 | + impl<R: BufRead> ReaderHelper for Reader<R> { |
3970 | + fn next_tag<'x>(&mut self, buf: &'x mut Vec<u8>) -> Result<Option<BytesStart<'x>>, String> { |
3971 | + match self.read_event_into(buf) { |
3972 | + Ok(Event::Start(e)) => Ok(Some(e)), |
3973 | + Ok(Event::End(_)) | Ok(Event::Eof) => Ok(None), |
3974 | + Err(e) => Err(format!( |
3975 | + "Error at position {}: {:?}", |
3976 | + self.buffer_position(), |
3977 | + e |
3978 | + )), |
3979 | + _ => Ok(Some(BytesStart::new(""))), |
3980 | + } |
3981 | + } |
3982 | + |
3983 | + fn next_value<T: FromStr>(&mut self, buf: &mut Vec<u8>) -> Result<Option<T>, String> { |
3984 | + let mut value = None; |
3985 | + loop { |
3986 | + match self.read_event_into(buf) { |
3987 | + Ok(Event::Text(e)) => { |
3988 | + value = e.unescape().ok().and_then(|v| T::from_str(v.as_ref()).ok()); |
3989 | + } |
3990 | + Ok(Event::End(_)) => { |
3991 | + break; |
3992 | + } |
3993 | + Ok(Event::Start(e)) => { |
3994 | + return Err(format!( |
3995 | + "Expected value, found unexpected tag {} at position {}.", |
3996 | + String::from_utf8_lossy(e.name().as_ref()), |
3997 | + self.buffer_position() |
3998 | + )); |
3999 | + } |
4000 | + Ok(Event::Eof) => { |
4001 | + return Err(format!( |
4002 | + "Expected value, found unexpected EOF at position {}.", |
4003 | + self.buffer_position() |
4004 | + )) |
4005 | + } |
4006 | + _ => (), |
4007 | + } |
4008 | + } |
4009 | + |
4010 | + Ok(value) |
4011 | + } |
4012 | + |
4013 | + fn skip_tag(&mut self, buf: &mut Vec<u8>) -> Result<(), String> { |
4014 | + let mut tag_count = 0; |
4015 | + loop { |
4016 | + match self.read_event_into(buf) { |
4017 | + Ok(Event::End(_)) => { |
4018 | + if tag_count == 0 { |
4019 | + break; |
4020 | + } else { |
4021 | + tag_count -= 1; |
4022 | + } |
4023 | + } |
4024 | + Ok(Event::Start(_)) => { |
4025 | + tag_count += 1; |
4026 | + } |
4027 | + Ok(Event::Eof) => { |
4028 | + return Err(format!( |
4029 | + "Expected value, found unexpected EOF at position {}.", |
4030 | + self.buffer_position() |
4031 | + )) |
4032 | + } |
4033 | + _ => (), |
4034 | + } |
4035 | + } |
4036 | + Ok(()) |
4037 | + } |
4038 | + } |
4039 | + |
4040 | + #[cfg(test)] |
4041 | + mod test { |
4042 | + use std::{fs, path::PathBuf}; |
4043 | + |
4044 | + use crate::report::Feedback; |
4045 | + |
4046 | + #[test] |
4047 | + fn dmarc_aggregate_report_parse() { |
4048 | + let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); |
4049 | + test_dir.push("resources"); |
4050 | + test_dir.push("dmarc-feedback"); |
4051 | + |
4052 | + for file_name in fs::read_dir(&test_dir).unwrap() { |
4053 | + let mut file_name = file_name.unwrap().path(); |
4054 | + if !file_name.extension().unwrap().to_str().unwrap().eq("xml") { |
4055 | + continue; |
4056 | + } |
4057 | + println!("Parsing DMARC feedback {}", file_name.to_str().unwrap()); |
4058 | + |
4059 | + let feedback = Feedback::parse_xml(&fs::read(&file_name).unwrap()).unwrap(); |
4060 | + |
4061 | + file_name.set_extension("json"); |
4062 | + |
4063 | + let expected_feedback = |
4064 | + serde_json::from_slice::<Feedback>(&fs::read(&file_name).unwrap()).unwrap(); |
4065 | + |
4066 | + assert_eq!(expected_feedback, feedback); |
4067 | + |
4068 | + /*fs::write( |
4069 | + &file_name, |
4070 | + serde_json::to_string_pretty(&feedback).unwrap().as_bytes(), |
4071 | + ) |
4072 | + .unwrap();*/ |
4073 | + } |
4074 | + } |
4075 | + |
4076 | + #[test] |
4077 | + fn dmarc_aggregate_report_eml_parse() { |
4078 | + let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); |
4079 | + test_dir.push("resources"); |
4080 | + test_dir.push("dmarc-feedback"); |
4081 | + |
4082 | + for file_name in fs::read_dir(&test_dir).unwrap() { |
4083 | + let mut file_name = file_name.unwrap().path(); |
4084 | + if !file_name.extension().unwrap().to_str().unwrap().eq("eml") { |
4085 | + continue; |
4086 | + } |
4087 | + println!("Parsing DMARC feedback {}", file_name.to_str().unwrap()); |
4088 | + |
4089 | + let feedback = Feedback::parse_rfc5322(&fs::read(&file_name).unwrap()).unwrap(); |
4090 | + |
4091 | + file_name.set_extension("json"); |
4092 | + |
4093 | + let expected_feedback = |
4094 | + serde_json::from_slice::<Feedback>(&fs::read(&file_name).unwrap()).unwrap(); |
4095 | + |
4096 | + assert_eq!(expected_feedback, feedback); |
4097 | + |
4098 | + /*fs::write( |
4099 | + &file_name, |
4100 | + serde_json::to_string_pretty(&feedback).unwrap().as_bytes(), |
4101 | + ) |
4102 | + .unwrap();*/ |
4103 | + } |
4104 | + } |
4105 | + } |
4106 | diff --git a/src/report/mod.rs b/src/report/mod.rs |
4107 | index 75054cb..ffad6d4 100644 |
4108 | --- a/src/report/mod.rs |
4109 | +++ b/src/report/mod.rs |
4110 | @@ -1,13 +1,16 @@ |
4111 | - mod agg_parse; |
4112 | + mod feedback; |
4113 | |
4114 | - use std::net::IpAddr; |
4115 | + use std::net::{IpAddr, Ipv4Addr}; |
4116 | |
4117 | - #[derive(Default)] |
4118 | + use serde::{Deserialize, Serialize}; |
4119 | + |
4120 | + #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] |
4121 | pub struct DateRange { |
4122 | - begin: u32, |
4123 | - end: u32, |
4124 | + begin: u64, |
4125 | + end: u64, |
4126 | } |
4127 | |
4128 | + #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] |
4129 | pub struct ReportMetadata { |
4130 | org_name: String, |
4131 | email: String, |
4132 | @@ -17,12 +20,14 @@ pub struct ReportMetadata { |
4133 | error: Vec<String>, |
4134 | } |
4135 | |
4136 | + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] |
4137 | pub enum Alignment { |
4138 | Relaxed, |
4139 | - Simple, |
4140 | + Strict, |
4141 | Unspecified, |
4142 | } |
4143 | |
4144 | + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] |
4145 | pub enum Disposition { |
4146 | None, |
4147 | Quarantine, |
4148 | @@ -30,16 +35,19 @@ pub enum Disposition { |
4149 | Unspecified, |
4150 | } |
4151 | |
4152 | + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] |
4153 | pub enum ActionDisposition { |
4154 | None, |
4155 | Pass, |
4156 | Quarantine, |
4157 | Reject, |
4158 | + Unspecified, |
4159 | } |
4160 | |
4161 | + #[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] |
4162 | pub struct PolicyPublished { |
4163 | domain: String, |
4164 | - version_published: Option<u32>, |
4165 | + version_published: Option<f32>, |
4166 | adkim: Alignment, |
4167 | aspf: Alignment, |
4168 | p: Disposition, |
4169 | @@ -48,11 +56,16 @@ pub struct PolicyPublished { |
4170 | fo: Option<String>, |
4171 | } |
4172 | |
4173 | + impl Eq for PolicyPublished {} |
4174 | + |
4175 | + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] |
4176 | pub enum DMARCResult { |
4177 | Pass, |
4178 | Fail, |
4179 | + Unspecified, |
4180 | } |
4181 | |
4182 | + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] |
4183 | pub enum PolicyOverride { |
4184 | Forwarded, |
4185 | SampledOut, |
4186 | @@ -62,11 +75,13 @@ pub enum PolicyOverride { |
4187 | Other, |
4188 | } |
4189 | |
4190 | + #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] |
4191 | pub struct PolicyOverrideReason { |
4192 | type_: PolicyOverride, |
4193 | comment: Option<String>, |
4194 | } |
4195 | |
4196 | + #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] |
4197 | pub struct PolicyEvaluated { |
4198 | disposition: ActionDisposition, |
4199 | dkim: DMARCResult, |
4200 | @@ -74,25 +89,27 @@ pub struct PolicyEvaluated { |
4201 | reason: Vec<PolicyOverrideReason>, |
4202 | } |
4203 | |
4204 | + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] |
4205 | pub struct Row { |
4206 | source_ip: IpAddr, |
4207 | count: u32, |
4208 | policy_evaluated: PolicyEvaluated, |
4209 | - extensions: Vec<Extension>, |
4210 | } |
4211 | |
4212 | + #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] |
4213 | pub struct Extension { |
4214 | - extension: Option<String>, |
4215 | name: String, |
4216 | definition: String, |
4217 | } |
4218 | |
4219 | + #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] |
4220 | pub struct Identifier { |
4221 | envelope_to: Option<String>, |
4222 | envelope_from: String, |
4223 | header_from: String, |
4224 | } |
4225 | |
4226 | + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] |
4227 | pub enum DKIMResult { |
4228 | None, |
4229 | Pass, |
4230 | @@ -103,6 +120,7 @@ pub enum DKIMResult { |
4231 | PermError, |
4232 | } |
4233 | |
4234 | + #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] |
4235 | pub struct DKIMAuthResult { |
4236 | domain: String, |
4237 | selector: String, |
4238 | @@ -110,12 +128,14 @@ pub struct DKIMAuthResult { |
4239 | human_result: Option<String>, |
4240 | } |
4241 | |
4242 | + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] |
4243 | pub enum SPFDomainScope { |
4244 | Helo, |
4245 | MailFrom, |
4246 | - Undefined, |
4247 | + Unspecified, |
4248 | } |
4249 | |
4250 | + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] |
4251 | pub enum SPFResult { |
4252 | None, |
4253 | Neutral, |
4254 | @@ -126,6 +146,7 @@ pub enum SPFResult { |
4255 | PermError, |
4256 | } |
4257 | |
4258 | + #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] |
4259 | pub struct SPFAuthResult { |
4260 | domain: String, |
4261 | scope: SPFDomainScope, |
4262 | @@ -133,21 +154,99 @@ pub struct SPFAuthResult { |
4263 | human_result: Option<String>, |
4264 | } |
4265 | |
4266 | + #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] |
4267 | pub struct AuthResult { |
4268 | dkim: Vec<DKIMAuthResult>, |
4269 | spf: Vec<SPFAuthResult>, |
4270 | } |
4271 | |
4272 | + #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] |
4273 | pub struct Record { |
4274 | row: Row, |
4275 | identifiers: Identifier, |
4276 | auth_results: AuthResult, |
4277 | + extensions: Vec<Extension>, |
4278 | } |
4279 | |
4280 | + #[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] |
4281 | pub struct Feedback { |
4282 | - version: u32, |
4283 | + version: f32, |
4284 | report_metadata: ReportMetadata, |
4285 | policy_published: PolicyPublished, |
4286 | record: Vec<Record>, |
4287 | extensions: Vec<Extension>, |
4288 | } |
4289 | + |
4290 | + impl Eq for Feedback {} |
4291 | + |
4292 | + impl Default for Row { |
4293 | + fn default() -> Self { |
4294 | + Self { |
4295 | + source_ip: IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), |
4296 | + count: 0, |
4297 | + policy_evaluated: PolicyEvaluated::default(), |
4298 | + } |
4299 | + } |
4300 | + } |
4301 | + |
4302 | + impl Default for Alignment { |
4303 | + fn default() -> Self { |
4304 | + Alignment::Unspecified |
4305 | + } |
4306 | + } |
4307 | + |
4308 | + impl Default for Disposition { |
4309 | + fn default() -> Self { |
4310 | + Disposition::Unspecified |
4311 | + } |
4312 | + } |
4313 | + |
4314 | + impl Default for ActionDisposition { |
4315 | + fn default() -> Self { |
4316 | + ActionDisposition::None |
4317 | + } |
4318 | + } |
4319 | + |
4320 | + impl Default for DMARCResult { |
4321 | + fn default() -> Self { |
4322 | + DMARCResult::Unspecified |
4323 | + } |
4324 | + } |
4325 | + |
4326 | + impl Default for PolicyOverride { |
4327 | + fn default() -> Self { |
4328 | + PolicyOverride::Other |
4329 | + } |
4330 | + } |
4331 | + |
4332 | + impl Default for DKIMResult { |
4333 | + fn default() -> Self { |
4334 | + DKIMResult::None |
4335 | + } |
4336 | + } |
4337 | + |
4338 | + impl Default for SPFResult { |
4339 | + fn default() -> Self { |
4340 | + SPFResult::None |
4341 | + } |
4342 | + } |
4343 | + |
4344 | + impl Default for SPFDomainScope { |
4345 | + fn default() -> Self { |
4346 | + SPFDomainScope::Unspecified |
4347 | + } |
4348 | + } |
4349 | + |
4350 | + #[derive(Debug, Clone, PartialEq, Eq)] |
4351 | + pub enum Error { |
4352 | + MailParseError, |
4353 | + ReportParseError(String), |
4354 | + UncompressError(String), |
4355 | + NoReportsFound, |
4356 | + } |
4357 | + |
4358 | + impl From<String> for Error { |
4359 | + fn from(err: String) -> Self { |
4360 | + Error::ReportParseError(err) |
4361 | + } |
4362 | + } |
4363 | diff --git a/src/spf/mod.rs b/src/spf/mod.rs |
4364 | index ce613d2..e3fc194 100644 |
4365 | --- a/src/spf/mod.rs |
4366 | +++ b/src/spf/mod.rs |
4367 | @@ -193,11 +193,12 @@ impl TryFrom<String> for SPFResult { |
4368 | } |
4369 | |
4370 | impl SPFOutput { |
4371 | - pub(crate) fn new() -> Self { |
4372 | + pub(crate) fn new(domain: String) -> Self { |
4373 | SPFOutput { |
4374 | result: SPFResult::None, |
4375 | report: None, |
4376 | explanation: None, |
4377 | + domain, |
4378 | } |
4379 | } |
4380 | |
4381 | @@ -206,7 +207,7 @@ impl SPFOutput { |
4382 | self |
4383 | } |
4384 | |
4385 | - pub(crate) fn with_report(mut self, spf: &SPF, domain: &str) -> Self { |
4386 | + pub(crate) fn with_report(mut self, spf: &SPF) -> Self { |
4387 | match &spf.ra { |
4388 | Some(ra) if is_within_pct(spf.rp) => { |
4389 | if match self.result { |
4390 | @@ -218,7 +219,7 @@ impl SPFOutput { |
4391 | } |
4392 | SPFResult::Pass => false, |
4393 | } { |
4394 | - self.report = format!("{}@{}", String::from_utf8_lossy(ra), domain).into(); |
4395 | + self.report = format!("{}@{}", String::from_utf8_lossy(ra), self.domain).into(); |
4396 | } |
4397 | } |
4398 | _ => (), |
4399 | diff --git a/src/spf/verify.rs b/src/spf/verify.rs |
4400 | index 08f0c09..c0f43eb 100644 |
4401 | --- a/src/spf/verify.rs |
4402 | +++ b/src/spf/verify.rs |
4403 | @@ -18,7 +18,7 @@ impl Resolver { |
4404 | ) |
4405 | .await |
4406 | } else { |
4407 | - SPFOutput::new().with_result(SPFResult::None) |
4408 | + SPFOutput::new(helo_domain.to_string()).with_result(SPFResult::None) |
4409 | } |
4410 | } |
4411 | |
4412 | @@ -58,10 +58,10 @@ impl Resolver { |
4413 | helo_domain: &str, |
4414 | sender: &str, |
4415 | ) -> SPFOutput { |
4416 | + let output = SPFOutput::new(domain.to_string()); |
4417 | if domain.is_empty() || domain.len() > 63 || !domain.has_labels() { |
4418 | - return SPFOutput::new().with_result(SPFResult::None); |
4419 | + return output.with_result(SPFResult::None); |
4420 | } |
4421 | - let base_domain = domain; |
4422 | let mut vars = Variables::new(); |
4423 | let mut has_p_var = false; |
4424 | vars.set_ip(&ip); |
4425 | @@ -77,7 +77,7 @@ impl Resolver { |
4426 | let mut lookup_limit = LookupLimit::new(); |
4427 | let mut spf_record = match self.txt_lookup::<SPF>(domain).await { |
4428 | Ok(spf_record) => spf_record, |
4429 | - Err(err) => return SPFOutput::new().with_result(err.into()), |
4430 | + Err(err) => return output.with_result(err.into()), |
4431 | }; |
4432 | |
4433 | let mut domain = domain.to_string(); |
4434 | @@ -90,9 +90,9 @@ impl Resolver { |
4435 | while let Some((pos, directive)) = directives.next() { |
4436 | if !has_p_var && directive.mechanism.needs_ptr() { |
4437 | if !lookup_limit.can_lookup() { |
4438 | - return SPFOutput::new() |
4439 | + return output |
4440 | .with_result(SPFResult::PermError) |
4441 | - .with_report(&spf_record, base_domain); |
4442 | + .with_report(&spf_record); |
4443 | } |
4444 | if let Some(ptr) = self |
4445 | .ptr_lookup(ip) |
4446 | @@ -115,9 +115,9 @@ impl Resolver { |
4447 | ip6_mask, |
4448 | } => { |
4449 | if !lookup_limit.can_lookup() { |
4450 | - return SPFOutput::new() |
4451 | + return output |
4452 | .with_result(SPFResult::PermError) |
4453 | - .with_report(&spf_record, base_domain); |
4454 | + .with_report(&spf_record); |
4455 | } |
4456 | match self |
4457 | .ip_matches( |
4458 | @@ -131,9 +131,9 @@ impl Resolver { |
4459 | Ok(true) => true, |
4460 | Ok(false) | Err(Error::DNSRecordNotFound(_)) => false, |
4461 | Err(_) => { |
4462 | - return SPFOutput::new() |
4463 | + return output |
4464 | .with_result(SPFResult::TempError) |
4465 | - .with_report(&spf_record, base_domain); |
4466 | + .with_report(&spf_record); |
4467 | } |
4468 | } |
4469 | } |
4470 | @@ -143,9 +143,9 @@ impl Resolver { |
4471 | ip6_mask, |
4472 | } => { |
4473 | if !lookup_limit.can_lookup() { |
4474 | - return SPFOutput::new() |
4475 | + return output |
4476 | .with_result(SPFResult::PermError) |
4477 | - .with_report(&spf_record, base_domain); |
4478 | + .with_report(&spf_record); |
4479 | } |
4480 | |
4481 | let mut matches = false; |
4482 | @@ -156,9 +156,9 @@ impl Resolver { |
4483 | Ok(records) => { |
4484 | for record in records.iter() { |
4485 | if !lookup_limit.can_lookup() { |
4486 | - return SPFOutput::new() |
4487 | + return output |
4488 | .with_result(SPFResult::PermError) |
4489 | - .with_report(&spf_record, base_domain); |
4490 | + .with_report(&spf_record); |
4491 | } |
4492 | |
4493 | match self |
4494 | @@ -171,27 +171,27 @@ impl Resolver { |
4495 | } |
4496 | Ok(false) | Err(Error::DNSRecordNotFound(_)) => (), |
4497 | Err(_) => { |
4498 | - return SPFOutput::new() |
4499 | + return output |
4500 | .with_result(SPFResult::TempError) |
4501 | - .with_report(&spf_record, base_domain); |
4502 | + .with_report(&spf_record); |
4503 | } |
4504 | } |
4505 | } |
4506 | } |
4507 | Err(Error::DNSRecordNotFound(_)) => (), |
4508 | Err(_) => { |
4509 | - return SPFOutput::new() |
4510 | + return output |
4511 | .with_result(SPFResult::TempError) |
4512 | - .with_report(&spf_record, base_domain); |
4513 | + .with_report(&spf_record); |
4514 | } |
4515 | } |
4516 | matches |
4517 | } |
4518 | Mechanism::Include { macro_string } => { |
4519 | if !lookup_limit.can_lookup() { |
4520 | - return SPFOutput::new() |
4521 | + return output |
4522 | .with_result(SPFResult::PermError) |
4523 | - .with_report(&spf_record, base_domain); |
4524 | + .with_report(&spf_record); |
4525 | } |
4526 | |
4527 | let target_name = macro_string.eval(&vars, &domain, true); |
4528 | @@ -213,22 +213,22 @@ impl Resolver { |
4529 | | Error::InvalidRecordType |
4530 | | Error::ParseError, |
4531 | ) => { |
4532 | - return SPFOutput::new() |
4533 | + return output |
4534 | .with_result(SPFResult::PermError) |
4535 | - .with_report(&spf_record, base_domain) |
4536 | + .with_report(&spf_record) |
4537 | } |
4538 | Err(_) => { |
4539 | - return SPFOutput::new() |
4540 | + return output |
4541 | .with_result(SPFResult::TempError) |
4542 | - .with_report(&spf_record, base_domain) |
4543 | + .with_report(&spf_record) |
4544 | } |
4545 | } |
4546 | } |
4547 | Mechanism::Ptr { macro_string } => { |
4548 | if !lookup_limit.can_lookup() { |
4549 | - return SPFOutput::new() |
4550 | + return output |
4551 | .with_result(SPFResult::PermError) |
4552 | - .with_report(&spf_record, base_domain); |
4553 | + .with_report(&spf_record); |
4554 | } |
4555 | |
4556 | let target_addr = macro_string.eval(&vars, &domain, true).to_lowercase(); |
4557 | @@ -254,9 +254,9 @@ impl Resolver { |
4558 | } |
4559 | Mechanism::Exists { macro_string } => { |
4560 | if !lookup_limit.can_lookup() { |
4561 | - return SPFOutput::new() |
4562 | + return output |
4563 | .with_result(SPFResult::PermError) |
4564 | - .with_report(&spf_record, base_domain); |
4565 | + .with_report(&spf_record); |
4566 | } |
4567 | |
4568 | if let Ok(result) = self |
4569 | @@ -265,9 +265,9 @@ impl Resolver { |
4570 | { |
4571 | result |
4572 | } else { |
4573 | - return SPFOutput::new() |
4574 | + return output |
4575 | .with_result(SPFResult::TempError) |
4576 | - .with_report(&spf_record, base_domain); |
4577 | + .with_report(&spf_record); |
4578 | } |
4579 | } |
4580 | }; |
4581 | @@ -295,9 +295,9 @@ impl Resolver { |
4582 | // Follow redirect |
4583 | if let (Some(macro_string), None) = (&spf_record.redirect, &result) { |
4584 | if !lookup_limit.can_lookup() { |
4585 | - return SPFOutput::new() |
4586 | + return output |
4587 | .with_result(SPFResult::PermError) |
4588 | - .with_report(&spf_record, base_domain); |
4589 | + .with_report(&spf_record); |
4590 | } |
4591 | |
4592 | let target_name = macro_string.eval(&vars, &domain, true); |
4593 | @@ -315,14 +315,14 @@ impl Resolver { |
4594 | | Error::InvalidRecordType |
4595 | | Error::ParseError, |
4596 | ) => { |
4597 | - return SPFOutput::new() |
4598 | + return output |
4599 | .with_result(SPFResult::PermError) |
4600 | - .with_report(&spf_record, base_domain) |
4601 | + .with_report(&spf_record) |
4602 | } |
4603 | Err(_) => { |
4604 | - return SPFOutput::new() |
4605 | + return output |
4606 | .with_result(SPFResult::TempError) |
4607 | - .with_report(&spf_record, base_domain) |
4608 | + .with_report(&spf_record) |
4609 | } |
4610 | } |
4611 | } |
4612 | @@ -337,16 +337,16 @@ impl Resolver { |
4613 | .txt_lookup::<Macro>(macro_string.eval(&vars, &domain, true).to_string()) |
4614 | .await |
4615 | { |
4616 | - return SPFOutput::new() |
4617 | + return output |
4618 | .with_result(SPFResult::Fail) |
4619 | .with_explanation(macro_string.eval(&vars, &domain, false).to_string()) |
4620 | - .with_report(&spf_record, base_domain); |
4621 | + .with_report(&spf_record); |
4622 | } |
4623 | } |
4624 | |
4625 | - SPFOutput::new() |
4626 | + output |
4627 | .with_result(result.unwrap_or(SPFResult::Neutral)) |
4628 | - .with_report(&spf_record, base_domain) |
4629 | + .with_report(&spf_record) |
4630 | } |
4631 | |
4632 | async fn ip_matches( |