3 // RFC5321.HELO/.EHLO domain
4 // RFC5321.MailFrom mailbox
5 // RFC5322.From mailbox-list
9 // MAIL FROM:<reverse-path>
10 // RCPT TO:<forward-path>
12 #include "message.hpp"
14 #include "Mailbox.hpp"
15 #include "OpenARC.hpp"
16 #include "OpenDKIM.hpp"
17 #include "OpenDMARC.hpp"
21 #include "imemstream.hpp"
25 #include <unordered_set>
27 #include <fmt/format.h>
28 #include <fmt/ostream.h>
30 #include <boost/algorithm/string.hpp>
31 #include <boost/iostreams/device/mapped_file.hpp>
33 #include <tao/pegtl.hpp>
34 #include <tao/pegtl/contrib/abnf.hpp>
40 auto constexpr Pass
= "Pass";
41 auto constexpr Fail
= "Fail";
42 auto constexpr SoftFail
= "SoftFail";
43 auto constexpr Neutral
= "Neutral";
44 auto constexpr None
= "None";
45 auto constexpr TempError
= "TempError";
46 auto constexpr PermError
= "PermError";
49 auto constexpr client_ip
= "client-ip";
50 auto constexpr envelope_from
= "envelope-from";
51 auto constexpr problem
= "problem";
52 auto constexpr receiver
= "receiver";
53 auto constexpr identity
= "identity";
54 auto constexpr mechanism
= "mechanism";
55 // auto constexpr helo = "helo"; // both key and value
58 auto constexpr helo
= "helo";
59 auto constexpr mailfrom
= "mailfrom";
61 using namespace tao::pegtl
;
62 using namespace tao::pegtl::abnf
;
64 using namespace std::string_literals
;
66 static std::string
make_string(std::string_view v
)
68 return std::string(v
.begin(),
69 static_cast<size_t>(std::distance(v
.begin(), v
.end())));
72 static std::string_view
trim(std::string_view v
)
74 auto constexpr WS
= " \t";
75 v
.remove_prefix(std::min(v
.find_first_not_of(WS
), v
.size()));
76 v
.remove_suffix(std::min(v
.size() - v
.find_last_not_of(WS
) - 1, v
.size()));
80 template <typename Input
>
81 static std::string_view
make_view(Input
const& in
)
83 return std::string_view(in
.begin(), std::distance(in
.begin(), in
.end()));
89 using colon
= one
<':'>;
95 //.............................................................................
97 struct ftext
: ranges
<33, 57, 59, 126> {};
99 struct field_name
: plus
<ftext
> {};
101 struct FWS
: seq
<opt
<seq
<star
<WSP
>, eol
>>, plus
<WSP
>> {};
103 // *([FWS] VCHAR) *WSP
104 struct field_value
: seq
<star
<seq
<opt
<FWS
>, VUCHAR
>>, star
<WSP
>> {};
106 struct field
: seq
<field_name
, colon
, field_value
, eol
> {};
108 struct raw_field
: seq
<field_name
, colon
, field_value
, eof
> {};
110 struct fields
: star
<field
> {};
112 struct body
: until
<eof
> {};
114 struct message
: seq
<fields
, opt
<seq
<eol
, body
>>, eof
> {};
116 //.............................................................................
118 // <https://tools.ietf.org/html/rfc2047>
120 // especials = "(" / ")" / "<" / ">" / "@" / "," / ";" / ":" / "
121 // <"> / "/" / "[" / "]" / "?" / "." / "="
123 // token = 1*<Any CHAR except SPACE, CTLs, and especials>
125 struct tchar47
: ranges
< // NUL..' '
134 48, 57, // 0123456789
135 // 58, 64, // ;:<=>?@
140 94, 126 // ^_` a..z {|}~
144 struct token47
: plus
<tchar47
> {};
146 struct charset
: token47
{};
147 struct encoding
: token47
{};
149 // encoded-text = 1*<Any printable ASCII character other than "?"
152 struct echar
: ranges
< // NUL..' '
155 64, 126 // @A..Z[\]^_` a..z {|}~
159 struct encoded_text
: plus
<echar
> {};
161 // encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
163 // leading opt<FWS> is not in RFC 2047
165 struct encoded_word_book
: seq
<string
<'=', '?'>,
166 charset
, string
<'?'>,
167 encoding
, string
<'?'>,
172 struct encoded_word
: seq
<opt
<FWS
>, encoded_word_book
> {};
174 //.............................................................................
176 // Comments are recursive, hence the forward declaration:
179 struct quoted_pair
: seq
<one
<'\\'>, sor
<VUCHAR
, WSP
>> {};
181 // ctext is ASCII not '(' or ')' or '\\'
182 struct ctext
: sor
<ranges
<33, 39, 42, 91, 93, 126>, UTF8_non_ascii
> {};
184 struct ccontent
: sor
<ctext
, quoted_pair
, comment
, encoded_word
> {};
186 // from <https://tools.ietf.org/html/rfc2047>
187 // comment = "(" *(ctext / quoted-pair / comment / encoded-word) ")"
189 struct comment
: seq
<one
<'('>,
190 star
<seq
<opt
<FWS
>, ccontent
>>,
195 struct CFWS
: sor
<seq
<plus
<seq
<opt
<FWS
>, comment
>, opt
<FWS
>>>,
198 struct qtext
: sor
<one
<33>, ranges
<35, 91, 93, 126>, UTF8_non_ascii
> {};
200 struct qcontent
: sor
<qtext
, quoted_pair
> {};
202 // Corrected in RFC-5322, errata ID: 3135 <https://www.rfc-editor.org/errata/eid3135>
203 struct quoted_string
: seq
<opt
<CFWS
>,
205 sor
<seq
<star
<seq
<opt
<FWS
>, qcontent
>>, opt
<FWS
>>, FWS
>,
210 struct atext
: sor
<ALPHA
, DIGIT
,
223 struct atom
: seq
<opt
<CFWS
>, plus
<atext
>, opt
<CFWS
>> {};
225 struct dot_atom_text
: list
<plus
<atext
>, dot
> {};
227 struct dot_atom
: seq
<opt
<CFWS
>, dot_atom_text
, opt
<CFWS
>> {};
229 struct word
: sor
<atom
, quoted_string
> {};
231 struct phrase
: plus
<sor
<encoded_word
, word
>> {};
233 struct dec_octet
: sor
<seq
<string
<'2','5'>, range
<'0','5'>>,
234 seq
<one
<'2'>, range
<'0','4'>, DIGIT
>,
235 seq
<one
<'1'>, rep
<2, DIGIT
>>,
236 seq
<range
<'1', '9'>, DIGIT
>,
239 struct ipv4_address
: seq
<dec_octet
, dot
, dec_octet
, dot
, dec_octet
, dot
, dec_octet
> {};
241 struct h16
: rep_min_max
<1, 4, HEXDIG
> {};
243 struct ls32
: sor
<seq
<h16
, colon
, h16
>, ipv4_address
> {};
245 struct dcolon
: two
<':'> {};
247 struct ipv6_address
: sor
<seq
< rep
<6, h16
, colon
>, ls32
>,
248 seq
< dcolon
, rep
<5, h16
, colon
>, ls32
>,
249 seq
<opt
<h16
>, dcolon
, rep
<4, h16
, colon
>, ls32
>,
250 seq
<opt
<h16
, opt
< colon
, h16
>>, dcolon
, rep
<3, h16
, colon
>, ls32
>,
251 seq
<opt
<h16
, rep_opt
<2, colon
, h16
>>, dcolon
, rep
<2, h16
, colon
>, ls32
>,
252 seq
<opt
<h16
, rep_opt
<3, colon
, h16
>>, dcolon
, h16
, colon
, ls32
>,
253 seq
<opt
<h16
, rep_opt
<4, colon
, h16
>>, dcolon
, ls32
>,
254 seq
<opt
<h16
, rep_opt
<5, colon
, h16
>>, dcolon
, h16
>,
255 seq
<opt
<h16
, rep_opt
<6, colon
, h16
>>, dcolon
>> {};
257 struct ip
: sor
<ipv4_address
, ipv6_address
> {};
259 struct obs_local_part
: seq
<word
, star
<seq
<dot
, word
>>> {};
261 struct local_part
: sor
<quoted_string
, dot_atom
> {};
263 struct dtext
: ranges
<33, 90, 94, 126> {};
265 struct domain_literal
: seq
<opt
<CFWS
>,
267 star
<seq
<opt
<FWS
>, dtext
>>,
272 struct domain
: sor
<dot_atom
, domain_literal
> {};
274 struct obs_domain
: sor
<list
<atom
, dot
>, domain_literal
> {};
276 // This addr_spec should be exactly the same as RFC5321 Mailbox, but it's not.
278 struct new_addr_spec
: seq
<local_part
, one
<'@'>, domain
> {};
280 struct obs_addr_spec
: seq
<obs_local_part
, one
<'@'>, obs_domain
> {};
282 struct addr_spec
: sor
<obs_addr_spec
, new_addr_spec
> {};
284 struct result
: sor
<TAO_PEGTL_ISTRING("Pass"),
285 TAO_PEGTL_ISTRING("Fail"),
286 TAO_PEGTL_ISTRING("SoftFail"),
287 TAO_PEGTL_ISTRING("Neutral"),
288 TAO_PEGTL_ISTRING("None"),
289 TAO_PEGTL_ISTRING("TempError"),
290 TAO_PEGTL_ISTRING("PermError")> {};
292 struct spf_key
: sor
<TAO_PEGTL_ISTRING("client-ip"),
293 TAO_PEGTL_ISTRING("envelope-from"),
294 TAO_PEGTL_ISTRING("helo"),
295 TAO_PEGTL_ISTRING("problem"),
296 TAO_PEGTL_ISTRING("receiver"),
297 TAO_PEGTL_ISTRING("identity"),
298 TAO_PEGTL_ISTRING("mechanism")> {};
300 // This value syntax (allowing addr_spec) is not in accordance with RFC
301 // 7208 (or 4408) but is what is effectivly used by libspf2 1.2.10 and
304 struct spf_value
: sor
<ip
, addr_spec
, dot_atom
, quoted_string
> {};
306 struct spf_kv_pair
: seq
<spf_key
, opt
<CFWS
>, one
<'='>, spf_value
> {};
308 struct spf_kv_list
: seq
<spf_kv_pair
,
309 star
<seq
<one
<';'>, opt
<CFWS
>, spf_kv_pair
>>,
312 struct spf_header
: seq
<opt
<CFWS
>,
314 opt
<seq
<FWS
, comment
>>,
315 opt
<seq
<FWS
, spf_kv_list
>>> {};
317 struct spf_header_only
: seq
<spf_header
, eof
> {};
319 //.............................................................................
321 struct obs_domain_list
: seq
<
322 star
<sor
<CFWS
, one
<','>>>, one
<'@'>, domain
,
323 star
<seq
<one
<','>, opt
<CFWS
>, opt
<seq
<one
<'@'>, domain
>>>>
326 struct obs_route
: seq
<obs_domain_list
, colon
> {};
328 struct obs_angle_addr
: seq
<opt
<CFWS
>, one
<'<'>, obs_route
, addr_spec
, one
<'>'>, opt
<CFWS
>> {};
330 struct angle_addr
: sor
<seq
<opt
<CFWS
>, one
<'<'>, addr_spec
, one
<'>'>, opt
<CFWS
>>,
334 struct display_name
: phrase
{};
336 struct name_addr
: seq
<opt
<display_name
>, angle_addr
> {};
338 struct mailbox
: sor
<name_addr
, addr_spec
> {};
340 struct obs_mbox_list
: seq
<star
<seq
<opt
<CFWS
>, one
<','>>>,
342 star
<seq
<one
<','>, opt
<sor
<mailbox
, CFWS
>>>>
345 struct mailbox_list
: sor
<list
<mailbox
, one
<','>>,
348 struct from
: seq
<TAO_PEGTL_ISTRING("From"), opt
<CFWS
>, colon
,
351 struct mailbox_list_only
: seq
<mailbox_list
, eof
> {};
353 //.............................................................................
355 // <https://www.rfc-editor.org/rfc/rfc2045.html>
357 // tspecials := "(" / ")" / "<" / ">" / "@" /
358 // "," / ";" / ":" / "\" / <">
359 // "/" / "[" / "]" / "?" / "="
361 // token := 1*<any (US-ASCII) CHAR except SPACE, CTLs,
376 struct tchar45
: ranges
< // NUL..' '
385 48, 57, // 0123456789
386 // 58, 64, // ;:<=>?@
389 94, 126 // ^_` a..z {|}~
393 struct token45
: plus
<tchar45
> {};
395 //.............................................................................
397 // <https://tools.ietf.org/html/rfc8601#section-2.2>
399 struct value
: sor
<token45
, quoted_string
> {};
401 struct authserv_id
: value
{};
403 struct authres_version
: seq
<plus
<DIGIT
>, opt
<CFWS
>> {};
405 struct no_result
: seq
<opt
<CFWS
>, one
<';'>, opt
<CFWS
>, TAO_PEGTL_ISTRING("none")> {};
407 struct let_dig
: sor
<ALPHA
, DIGIT
> {};
409 struct ldh_tail
: star
<sor
<seq
<plus
<one
<'-'>>, let_dig
>, let_dig
>> {};
411 struct ldh_str
: seq
<let_dig
, ldh_tail
> {};
413 struct keyword
: ldh_str
{};
415 struct method_version
: seq
<plus
<DIGIT
>, opt
<CFWS
>> {};
417 // method = Keyword [ [CFWS] "/" [CFWS] method-version ]
419 struct method
: seq
<keyword
, opt
<opt
<CFWS
>, one
<'/'>, opt
<CFWS
>, method_version
>> {};
421 // methodspec = [CFWS] method [CFWS] "=" [CFWS] result
422 // ; indicates which authentication method was evaluated
423 // ; and what its output was
425 struct methodspec
: seq
<opt
<CFWS
>, method
, opt
<CFWS
>, one
<'='>, opt
<CFWS
>, result
> {};
427 // reasonspec = "reason" [CFWS] "=" [CFWS] value
428 // ; a free-form comment on the reason the given result
431 struct reasonspec
: seq
<TAO_PEGTL_ISTRING("reason"), opt
<CFWS
>, one
<'='>, opt
<CFWS
>, value
> {};
433 // pvalue = [CFWS] ( value / [ [ local-part ] "@" ] domain-name )
436 struct pvalue
: seq
<opt
<CFWS
>, sor
<seq
<opt
<seq
<opt
<local_part
>, one
<'@'>>>, domain
>,
440 struct ptype
: keyword
{};
442 struct special_smtp_verb
: sor
<TAO_PEGTL_ISTRING("mailfrom"),
443 TAO_PEGTL_ISTRING("rcptto")> {};
445 struct property
: sor
<special_smtp_verb
, keyword
> {};
447 // propspec = ptype [CFWS] "." [CFWS] property [CFWS] "=" pvalue
448 // ; an indication of which properties of the message
449 // ; were evaluated by the authentication scheme being
450 // ; applied to yield the reported result
452 struct propspec
: seq
<ptype
, opt
<CFWS
>, dot
, opt
<CFWS
>, property
, opt
<CFWS
>, one
<'='>, pvalue
> {};
454 struct resinfo
: seq
<opt
<CFWS
>, one
<';'>, methodspec
, opt
<seq
<CFWS
, reasonspec
>>,
455 opt
<seq
<CFWS
, plus
<propspec
>>>
458 struct ar_results
: sor
<no_result
, plus
<resinfo
>> {};
460 struct authres_payload
: seq
<opt
<CFWS
>, authserv_id
,
461 opt
<seq
<CFWS
, authres_version
>>,
465 struct authres_header_field
: seq
<TAO_PEGTL_ISTRING("Authentication-Results"), opt
<CFWS
>, colon
,
468 struct authres_header_field_only
: seq
<authres_header_field
, eof
> {};
470 //.............................................................................
474 template <typename Rule
>
475 struct ar_action
: nothing
<Rule
> {
479 struct ar_action
<ar_results
> {
480 template <typename Input
>
482 apply(Input
const& in
, std::string
& authservid
, std::string
& ar_results
)
484 ar_results
= in
.string();
489 struct ar_action
<authserv_id
> {
490 template <typename Input
>
492 apply(Input
const& in
, std::string
& authservid
, std::string
& ar_results
)
494 authservid
= in
.string();
498 //.............................................................................
500 template <typename Rule
>
501 struct msg_action
: nothing
<Rule
> {
505 struct msg_action
<field_name
> {
506 template <typename Input
>
507 static void apply(Input
const& in
, ::message::parsed
& msg
)
509 msg
.field_name
= make_view(in
);
514 struct msg_action
<field_value
> {
515 template <typename Input
>
516 static void apply(Input
const& in
, ::message::parsed
& msg
)
518 msg
.field_value
= make_view(in
);
523 struct msg_action
<field
> {
524 template <typename Input
>
525 static void apply(Input
const& in
, ::message::parsed
& msg
)
527 msg
.headers
.emplace_back(
528 ::message::header(msg
.field_name
, msg
.field_value
));
533 struct msg_action
<raw_field
> {
534 template <typename Input
>
535 static void apply(Input
const& in
, ::message::parsed
& msg
)
537 msg
.headers
.emplace_back(
538 ::message::header(msg
.field_name
, msg
.field_value
));
543 struct msg_action
<body
> {
544 template <typename Input
>
545 static void apply(Input
const& in
, ::message::parsed
& msg
)
547 msg
.body
= make_view(in
);
551 //.............................................................................
553 struct received_spf_parsed
{
554 bool parse(std::string_view input
);
556 std::string_view whole_thing
;
558 std::string_view result
;
559 std::string_view comment
;
561 std::string_view key
;
562 std::string_view value
;
564 std::vector
<std::pair
<std::string_view
, std::string_view
>> kv_list
;
565 std::map
<std::string_view
, std::string_view
, ci_less
> kv_map
;
567 std::string
as_string() const { return fmt::format("{}", whole_thing
); }
570 template <typename Rule
>
571 struct spf_action
: nothing
<Rule
> {
575 struct spf_action
<result
> {
576 template <typename Input
>
577 static void apply(const Input
& in
, received_spf_parsed
& spf
)
579 spf
.result
= make_view(in
);
584 struct spf_action
<comment
> {
585 template <typename Input
>
586 static void apply(const Input
& in
, received_spf_parsed
& spf
)
588 spf
.comment
= make_view(in
);
593 struct spf_action
<spf_key
> {
594 template <typename Input
>
595 static void apply(const Input
& in
, received_spf_parsed
& spf
)
597 spf
.key
= make_view(in
);
602 struct spf_action
<spf_value
> {
603 template <typename Input
>
604 static void apply(const Input
& in
, received_spf_parsed
& spf
)
606 // RFC5322 syntax is full of optional WS, so we trim
607 spf
.value
= trim(make_view(in
));
612 struct spf_action
<spf_kv_pair
> {
613 template <typename Input
>
614 static void apply(const Input
& in
, received_spf_parsed
& spf
)
616 spf
.kv_list
.emplace_back(spf
.key
, spf
.value
);
617 spf
.key
= spf
.value
= "";
622 struct spf_action
<spf_kv_list
> {
623 static void apply0(received_spf_parsed
& spf
)
625 for (auto const& kvp
: spf
.kv_list
) {
626 if (spf
.kv_map
.contains(kvp
.first
)) {
627 LOG(WARNING
) << "dup key: " << kvp
.first
<< "=" << kvp
.second
;
628 LOG(WARNING
) << " and: " << kvp
.first
<< "="
629 << spf
.kv_map
[kvp
.first
];
631 spf
.kv_map
[kvp
.first
] = kvp
.second
;
636 bool received_spf_parsed::parse(std::string_view input
)
639 auto in
{memory_input
<>(input
.data(), input
.size(), "spf_header")};
640 return tao::pegtl::parse
<spf_header_only
, spf_action
>(in
, *this);
643 //.............................................................................
645 template <typename Rule
>
646 struct mailbox_list_action
: nothing
<Rule
> {};
649 struct mailbox_list_action
<local_part
> {
650 template <typename Input
>
651 static void apply(Input
const& in
,
652 ::message::mailbox_name_addr_list
& from_parsed
)
654 LOG(INFO
) << "local_part: " << in
.string();
659 struct mailbox_list_action
<domain
> {
660 template <typename Input
>
661 static void apply(Input
const& in
,
662 ::message::mailbox_name_addr_list
& from_parsed
)
664 LOG(INFO
) << "domain: " << in
.string();
669 struct mailbox_list_action
<obs_local_part
> {
670 template <typename Input
>
671 static void apply(Input
const& in
,
672 ::message::mailbox_name_addr_list
& from_parsed
)
674 LOG(INFO
) << "obs_local_part: " << in
.string();
679 struct mailbox_list_action
<obs_domain
> {
680 template <typename Input
>
681 static void apply(Input
const& in
,
682 ::message::mailbox_name_addr_list
& from_parsed
)
684 LOG(INFO
) << "obs_domain: " << in
.string();
689 struct mailbox_list_action
<display_name
> {
690 template <typename Input
>
691 static void apply(Input
const& in
,
692 ::message::mailbox_name_addr_list
& from_parsed
)
694 from_parsed
.maybe_name
= in
.string();
699 struct mailbox_list_action
<angle_addr
> {
700 template <typename Input
>
701 static void apply(Input
const& in
,
702 ::message::mailbox_name_addr_list
& from_parsed
)
704 std::swap(from_parsed
.name
, from_parsed
.maybe_name
);
709 struct mailbox_list_action
<addr_spec
> {
710 template <typename Input
>
711 static void apply(Input
const& in
,
712 ::message::mailbox_name_addr_list
& from_parsed
)
714 from_parsed
.name_addr_list
.push_back({from_parsed
.name
, in
.string()});
715 from_parsed
.name
.clear();
716 from_parsed
.maybe_name
.clear();
720 } // namespace RFC5322
722 // Map SPF result string to DMARC policy code.
724 static int result_to_pol(std::string_view result
)
727 if (iequal(result
, Pass
)) return DMARC_POLICY_SPF_OUTCOME_PASS
;
728 if (iequal(result
, Fail
)) return DMARC_POLICY_SPF_OUTCOME_FAIL
;
729 if (iequal(result
, SoftFail
)) return DMARC_POLICY_SPF_OUTCOME_TMPFAIL
;
730 if (iequal(result
, Neutral
)) return DMARC_POLICY_SPF_OUTCOME_NONE
;
731 if (iequal(result
, None
)) return DMARC_POLICY_SPF_OUTCOME_NONE
;
732 if (iequal(result
, TempError
)) return DMARC_POLICY_SPF_OUTCOME_NONE
;
733 if (iequal(result
, PermError
)) return DMARC_POLICY_SPF_OUTCOME_NONE
;
734 LOG(WARNING
) << "unknown SPF result: \"" << result
<< "\"";
735 return DMARC_POLICY_SPF_OUTCOME_NONE
;
739 static bool is_postmaster(std::string_view from
)
741 return from
== "<>" || iequal(from
, "<Postmaster>") ||
742 istarts_with(from
, "<Postmaster@");
745 static bool sender_comment(std::string_view comment
, std::string_view sender
)
747 auto const prefix
= fmt::format("({}:", sender
);
748 return istarts_with(comment
, prefix
);
751 static void spf_result_to_dmarc(OpenDMARC::policy
& dmp
,
752 RFC5322::received_spf_parsed
& spf
)
754 LOG(INFO
) << "spf_result_to_dmarc";
756 if (spf
.kv_map
.contains(problem
)) {
757 LOG(WARNING
) << "SPF problem: " << spf
.kv_map
[problem
];
760 auto const spf_pol
= result_to_pol(spf
.result
);
762 if (spf_pol
== DMARC_POLICY_SPF_OUTCOME_NONE
) {
763 LOG(WARNING
) << "Ignoring for DMARC purposes: " << spf
.as_string();
771 if (spf
.kv_map
.contains(identity
)) {
772 if (iequal(spf
.kv_map
[identity
], mailfrom
)) {
773 if (spf
.kv_map
.contains(envelope_from
)) {
774 if (Mailbox::validate(spf
.kv_map
[envelope_from
])) {
775 Mailbox
mbx(spf
.kv_map
[envelope_from
]);
776 spf_dom
= mbx
.domain().ascii();
777 spf_origin
= DMARC_POLICY_SPF_ORIGIN_MAILFROM
;
779 auto const human_result
=
780 fmt::format("{}, explicit origin mail from, mailbox {}",
781 spf
.result
, mbx
.as_string());
782 LOG(INFO
) << "SPF result " << human_result
;
783 dmp
.store_spf(spf_dom
.c_str(), spf_pol
, spf_origin
,
784 human_result
.c_str());
788 LOG(WARNING
) << "invalid mailbox in envelope-from: "
789 << spf
.kv_map
[envelope_from
];
794 << "identity checked was mail from, but no envelope_from key";
797 else if (iequal(spf
.kv_map
[identity
], helo
)) {
798 if (spf
.kv_map
.contains(helo
)) {
799 if (Domain::validate(spf
.kv_map
[helo
])) {
800 Domain
dom(spf
.kv_map
[helo
]);
801 spf_dom
= dom
.ascii();
802 spf_origin
= DMARC_POLICY_SPF_ORIGIN_HELO
;
804 auto const human_result
= fmt::format(
805 "{}, explicit origin hello, domain {}", spf
.result
, dom
.ascii());
806 LOG(INFO
) << "SPF result " << human_result
;
807 dmp
.store_spf(spf_dom
.c_str(), spf_pol
, spf_origin
,
808 human_result
.c_str());
812 LOG(WARNING
) << "invalid domain in helo: " << spf
.kv_map
[helo
];
816 LOG(WARNING
) << "identity checked was helo, but no helo key";
820 LOG(WARNING
) << "unknown identity " << spf
.kv_map
[identity
];
824 LOG(INFO
) << "no explicit tag for which identity was checked";
827 if (spf
.kv_map
.contains(envelope_from
)) {
828 auto const efrom
= spf
.kv_map
[envelope_from
];
830 if (is_postmaster(efrom
)) {
831 if (spf
.kv_map
.contains(helo
)) {
832 if (Domain::validate(spf
.kv_map
[helo
])) {
833 Domain
dom(spf
.kv_map
[helo
]);
834 spf_dom
= dom
.ascii();
835 spf_origin
= DMARC_POLICY_SPF_ORIGIN_HELO
;
837 auto const human_result
= fmt::format(
838 "{}, RFC5321.MailFrom is <>, implicit origin hello, domain {}",
839 spf
.result
, dom
.ascii());
840 LOG(INFO
) << "SPF result " << human_result
;
841 dmp
.store_spf(spf_dom
.c_str(), spf_pol
, spf_origin
,
842 human_result
.c_str());
846 LOG(WARNING
) << "RFC5321.MailFrom is postmaster or <> but helo is "
852 LOG(WARNING
) << "envelope-from is <> but no helo key";
855 else if (Mailbox::validate(efrom
)) {
858 spf_dom
= mbx
.domain().ascii();
859 spf_origin
= DMARC_POLICY_SPF_ORIGIN_MAILFROM
;
861 auto const human_result
= fmt::format(
862 "{}, implicit RFC5321.MailFrom <{}>", spf
.result
, mbx
.as_string());
863 LOG(INFO
) << "SPF result " << human_result
;
864 dmp
.store_spf(spf_dom
.c_str(), spf_pol
, spf_origin
, human_result
.c_str());
868 LOG(WARNING
) << "envelope-from invalid mailbox: " << efrom
;
871 else if (spf
.kv_map
.contains(helo
)) {
872 if (Domain::validate(spf
.kv_map
[helo
])) {
873 Domain
dom(spf
.kv_map
[helo
]);
874 spf_dom
= dom
.ascii();
875 spf_origin
= DMARC_POLICY_SPF_ORIGIN_HELO
;
877 auto const human_result
=
878 fmt::format("{}, hello domain {}", spf
.result
, dom
.ascii());
879 LOG(INFO
) << "SPF result " << human_result
;
880 dmp
.store_spf(spf_dom
.c_str(), spf_pol
, spf_origin
, human_result
.c_str());
884 LOG(WARNING
) << "helo is invalid domain:" << spf
.kv_map
[helo
];
889 << "no explicit \"identity\" key, and no envelope-from or helo key";
895 bool mailbox_list_parse(std::string_view input
,
896 mailbox_name_addr_list
& name_addr_list
)
898 name_addr_list
= mailbox_name_addr_list
{};
899 auto in
{memory_input
<>(input
.data(), input
.size(), "mailbox_list_only")};
900 return tao::pegtl::parse
<RFC5322::mailbox_list_only
,
901 RFC5322::mailbox_list_action
>(in
, name_addr_list
);
904 bool authentication_results_parse(std::string_view input
,
905 std::string
& authservid
,
906 std::string
& ar_results
)
908 auto in
{memory_input
<>(input
.data(), input
.size(),
909 "authentication_results_header")};
910 return tao::pegtl::parse
<RFC5322::authres_header_field_only
,
911 RFC5322::ar_action
>(in
, authservid
, ar_results
);
914 bool authentication(message::parsed
& msg
,
916 char const* selector
,
919 LOG(INFO
) << "add_authentication_results";
920 CHECK(!msg
.headers
.empty());
922 // Remove any redundant Authentication-Results headers
924 std::remove_if(msg
.headers
.begin(), msg
.headers
.end(),
925 [sender
](auto const& hdr
) {
926 if (hdr
== Authentication_Results
) {
927 std::string authservid
;
928 std::string ar_results
;
929 if (message::authentication_results_parse(
930 hdr
.as_view(), authservid
, ar_results
)) {
931 return Domain::match(authservid
, sender
);
933 LOG(WARNING
) << "failed to parse " << hdr
.as_string();
939 // Run our message through OpenDKIM verify
941 OpenDKIM::verify dkv
;
942 for (auto const& header
: msg
.headers
) {
943 auto const hv
= header
.as_view();
948 // LOG(INFO) << "body «" << msg.body << "»";
953 OpenDMARC::policy dmp
;
955 // Build up Authentication-Results header
956 fmt::memory_buffer bfr
;
958 std::unordered_set
<Domain
> validated_doms
;
961 for (auto hdr
: msg
.headers
) {
962 if (hdr
== Received_SPF
) {
963 RFC5322::received_spf_parsed spf_parsed
;
964 if (!spf_parsed
.parse(hdr
.value
)) {
965 LOG(WARNING
) << "failed to parse SPF record: " << hdr
.value
;
969 LOG(INFO
) << "SPF record parsed";
970 if (!sender_comment(spf_parsed
.comment
, sender
)) {
971 LOG(INFO
) << "comment == \"" << spf_parsed
.comment
<< "\" not by "
976 if (!Mailbox::validate(spf_parsed
.kv_map
[envelope_from
])) {
977 LOG(WARNING
) << "invalid mailbox: " << spf_parsed
.kv_map
[envelope_from
];
981 if (!Domain::validate(spf_parsed
.kv_map
[helo
])) {
982 LOG(WARNING
) << "invalid helo domain: " << spf_parsed
.kv_map
[helo
];
986 Mailbox
env_from(spf_parsed
.kv_map
[envelope_from
]);
987 Domain
helo_dom(spf_parsed
.kv_map
[helo
]);
989 if (iequal(env_from
.local_part(), "Postmaster") &&
990 env_from
.domain() == helo_dom
) {
991 if (validated_doms
.count(helo_dom
) == 0) {
992 fmt::format_to(std::back_inserter(bfr
), ";\r\n\tspf={}",
994 fmt::format_to(std::back_inserter(bfr
), " {}", spf_parsed
.comment
);
995 fmt::format_to(std::back_inserter(bfr
), " smtp.helo={}",
997 validated_doms
.emplace(helo_dom
);
999 if (spf_parsed
.kv_map
.contains(client_ip
)) {
1000 std::string ip
= make_string(spf_parsed
.kv_map
[client_ip
]);
1001 dmp
.connect(ip
.c_str());
1003 spf_result_to_dmarc(dmp
, spf_parsed
);
1007 if (validated_doms
.count(env_from
.domain()) == 0) {
1008 fmt::format_to(std::back_inserter(bfr
), ";\r\n\tspf={}",
1010 fmt::format_to(std::back_inserter(bfr
), " {}", spf_parsed
.comment
);
1011 fmt::format_to(std::back_inserter(bfr
), " smtp.mailfrom={}",
1012 env_from
.as_string(Mailbox::domain_encoding::ascii
));
1013 validated_doms
.emplace(env_from
.domain());
1015 if (spf_parsed
.kv_map
.contains(client_ip
)) {
1016 std::string ip
= make_string(spf_parsed
.kv_map
[client_ip
]);
1017 dmp
.connect(ip
.c_str());
1019 spf_result_to_dmarc(dmp
, spf_parsed
);
1025 LOG(INFO
) << "fetching From: header";
1026 // Should be only one From:
1027 if (auto hdr
= std::find(begin(msg
.headers
), end(msg
.headers
), From
);
1028 hdr
!= end(msg
.headers
)) {
1029 auto const from_str
= make_string(hdr
->value
);
1031 if (!mailbox_list_parse(from_str
, msg
.from_parsed
)) {
1032 LOG(WARNING
) << "failed to parse «From:" << from_str
<< "»";
1035 for (auto hdr_next
= std::next(hdr
); hdr_next
!= end(msg
.headers
);
1036 hdr_next
= std::next(hdr_next
)) {
1037 if (*hdr_next
== From
) {
1038 LOG(WARNING
) << "additional RFC5322.From header «"
1039 << hdr_next
->as_string() << "»";
1044 if (msg
.from_parsed
.name_addr_list
.empty()) {
1045 LOG(WARNING
) << "No address in RFC5322.From header";
1050 <https://tools.ietf.org/html/rfc7489#section-6.6>
1051 6.6.1. Extract Author Domain
1053 The case of a syntactically valid multi-valued RFC5322.From field
1054 presents a particular challenge. The process in this case is to
1055 apply the DMARC check using each of those domains found in the
1056 RFC5322.From field as the Author Domain and apply the most strict
1057 policy selected among the checks that fail.
1062 if (msg
.from_parsed
.name_addr_list
.size() > 1) {
1063 LOG(WARNING
) << "More than one address in RFC5322.From header";
1066 auto from_addr
= msg
.from_parsed
.name_addr_list
[0].addr
;
1068 boost::trim(from_addr
);
1070 if (!Mailbox::validate(from_addr
)) {
1071 LOG(WARNING
) << "Mailbox syntax valid for RFC-5322, not for RFC-5321: \""
1072 << from_addr
<< "\"";
1073 // Maybe we can pick out a valid domain?
1077 Mailbox
from_mbx(from_addr
);
1078 msg
.dmarc_from
= from_mbx
.as_string(Mailbox::domain_encoding::ascii
);
1079 msg
.dmarc_from_domain
= from_mbx
.domain().ascii();
1081 LOG(INFO
) << "dmarc_from_domain == " << msg
.dmarc_from_domain
;
1082 dmp
.store_from_domain(msg
.dmarc_from_domain
.c_str());
1084 // Check each DKIM sig, inform DMARC processor, put in AR
1086 dkv
.foreach_sig([&dmp
, &bfr
](char const* domain
, bool passed
,
1087 char const* identity
, char const* sel
,
1089 int const result
= passed
? DMARC_POLICY_DKIM_OUTCOME_PASS
1090 : DMARC_POLICY_DKIM_OUTCOME_FAIL
;
1091 auto const human_result
= (passed
? "pass" : "fail");
1093 LOG(INFO
) << "DKIM check for " << domain
<< " " << human_result
;
1095 dmp
.store_dkim(domain
, sel
, result
, human_result
);
1097 auto bs
= std::string_view(b
, strlen(b
)).substr(0, 8);
1099 fmt::format_to(std::back_inserter(bfr
), ";\r\n\tdkim={}", human_result
);
1100 fmt::format_to(std::back_inserter(bfr
), " header.i={}", identity
);
1101 fmt::format_to(std::back_inserter(bfr
), " header.s={}", sel
);
1102 fmt::format_to(std::back_inserter(bfr
), " header.b=\"{}\"", bs
);
1105 // Set DMARC status in AR
1107 auto const dmarc_passed
= dmp
.query_dmarc(msg
.dmarc_from_domain
.c_str());
1109 auto const dmarc_result
= (dmarc_passed
? "pass" : "fail");
1110 LOG(INFO
) << "DMARC " << dmarc_result
;
1112 fmt::format_to(std::back_inserter(bfr
), ";\r\n\tdmarc={} header.from={}",
1113 dmarc_result
, msg
.dmarc_from_domain
);
1117 OpenARC::verify arv
;
1118 for (auto const& header
: msg
.headers
) {
1119 arv
.header(header
.as_view());
1125 LOG(INFO
) << "ARC status == " << arv
.chain_status_str();
1126 LOG(INFO
) << "ARC custody == " << arv
.chain_custody_str();
1128 auto const arc_status
= arv
.chain_status_str();
1130 fmt::format_to(std::back_inserter(bfr
), ";\r\n\tarc={}", arc_status
);
1132 // New AR header on the top
1134 auto const ar_results
= [&bfr
]() {
1135 // Ug, OpenARC adds an extra one, arc.c:3213
1136 auto s
= fmt::to_string(bfr
);
1137 if (s
.length() && s
[0] == ';')
1143 fmt::format("{}: {};{}", Authentication_Results
, sender
, ar_results
);
1145 LOG(INFO
) << "new AR header «" << esc(msg
.ar_str
, esc_line_option::multi
)
1148 CHECK(msg
.parse_hdr(msg
.ar_str
));
1150 // Run our message through ARC::sign
1154 if (iequal(arc_status
, "none")) {
1157 else if (iequal(arc_status
, "fail")) {
1160 else if (iequal(arc_status
, "pass")) {
1167 for (auto const& header
: msg
.headers
) {
1168 ars
.header(header
.as_view());
1174 boost::iostreams::mapped_file_source priv
;
1175 priv
.open(key_file
);
1177 if (ars
.seal(sender
, selector
, sender
, priv
.data(), priv
.size(),
1178 ar_results
.c_str())) {
1179 msg
.arc_hdrs
= ars
.whole_seal();
1180 for (auto const& hdr
: msg
.arc_hdrs
) {
1181 CHECK(msg
.parse_hdr(hdr
));
1185 LOG(INFO
) << "failed to generate seal";
1188 OpenARC::verify arv2
;
1189 for (auto const& header
: msg
.headers
) {
1190 arv2
.header(header
.as_view());
1193 arv2
.body(msg
.body
);
1196 LOG(INFO
) << "check ARC status == " << arv2
.chain_status_str();
1197 LOG(INFO
) << "check ARC custody == " << arv2
.chain_custody_str();
1199 return dmarc_passed
;
1202 void print_spf_envelope_froms(char const* file
, message::parsed
& msg
)
1204 CHECK(!msg
.headers
.empty());
1205 for (auto const& hdr
: msg
.headers
) {
1206 if (hdr
== Received_SPF
) {
1207 RFC5322::received_spf_parsed spf_parsed
;
1208 if (spf_parsed
.parse(hdr
.value
)) {
1209 std::cout
<< spf_parsed
.kv_map
[envelope_from
] << '\n';
1213 LOG(WARNING
) << "failed to parse " << file
<< ":\n" << hdr
.as_string();
1219 void remove_delivery_headers(message::parsed
& msg
)
1221 // Remove headers that are added by the "delivery agent"
1222 // aka (Session::added_headers_)
1224 std::remove(msg
.headers
.begin(), msg
.headers
.end(), Return_Path
),
1227 // just in case, but right now this header should not exist.
1229 std::remove(msg
.headers
.begin(), msg
.headers
.end(), Delivered_To
),
1233 void dkim_check(message::parsed
& msg
, char const* domain
)
1235 LOG(INFO
) << "dkim";
1237 CHECK(!msg
.body
.empty());
1239 OpenDKIM::verify dkv
;
1241 // Run our message through OpenDKIM verify
1243 for (auto const& header
: msg
.headers
) {
1244 auto const hv
= header
.as_view();
1251 // Check each DKIM sig, inform DMARC processor, put in AR
1253 dkv
.foreach_sig([](char const* domain
, bool passed
, char const* identity
,
1254 char const* sel
, char const* b
) {
1255 auto const human_result
= (passed
? "pass" : "fail");
1257 auto bs
= std::string_view(b
, strlen(b
)).substr(0, 8);
1259 LOG(INFO
) << "DKIM check bfor " << domain
<< " " << human_result
;
1260 LOG(INFO
) << " header.i=" << identity
;
1261 LOG(INFO
) << " header.s=" << sel
;
1262 LOG(INFO
) << " header.b=\"" << bs
<< "\"";
1266 //.............................................................................
1268 bool parsed::parse(std::string_view input
)
1270 auto in
{memory_input
<>(input
.data(), input
.size(), "message")};
1271 return tao::pegtl::parse
<RFC5322::message
, RFC5322::msg_action
>(in
, *this);
1274 bool parsed::parse_hdr(std::string_view input
)
1276 auto in
{memory_input
<>(input
.data(), input
.size(), "message")};
1277 if (tao::pegtl::parse
<RFC5322::raw_field
, RFC5322::msg_action
>(in
, *this)) {
1278 std::rotate(headers
.rbegin(), headers
.rbegin() + 1, headers
.rend());
1284 std::string
parsed::as_string() const
1286 fmt::memory_buffer bfr
;
1288 for (auto const& h
: headers
)
1289 fmt::format_to(std::back_inserter(bfr
), "{}\r\n", h
.as_string());
1292 fmt::format_to(std::back_inserter(bfr
), "\r\n{}", body
);
1294 return fmt::to_string(bfr
);
1297 bool parsed::write(std::ostream
& os
) const
1299 for (auto const& h
: headers
)
1300 os
<< h
.as_string() << "\r\n";
1303 os
<< "\r\n" << body
;
1308 std::string
header::as_string() const
1310 return fmt::format("{}:{}", name
, value
);
1313 std::string_view
parsed::get_header(std::string_view name
) const
1315 if (auto hdr
= std::find(begin(headers
), end(headers
), name
);
1316 hdr
!= end(headers
)) {
1317 return trim(hdr
->value
);
1322 void dkim_sign(message::parsed
& msg
,
1324 char const* selector
,
1327 CHECK(msg
.sig_str
.empty());
1329 boost::iostreams::mapped_file_source priv
;
1330 priv
.open(key_file
);
1332 auto const key_str
= std::string(priv
.data(), priv
.size());
1334 // Run our message through DKIM::sign
1335 OpenDKIM::sign
dks(key_str
.c_str(), // textual data
1336 selector
, sender
, OpenDKIM::sign::body_type::text
);
1337 for (auto const& header
: msg
.headers
) {
1338 dks
.header(header
.as_view());
1344 auto const sig
= dks
.getsighdr();
1346 msg
.sig_str
= fmt::format("DKIM-Signature: {}", sig
);
1347 CHECK(msg
.parse_hdr(msg
.sig_str
));
1350 void rewrite_from_to(message::parsed
& msg
,
1351 std::string mail_from
,
1352 std::string reply_to
,
1354 char const* selector
,
1357 LOG(INFO
) << "rewrite_from_to";
1359 remove_delivery_headers(msg
);
1361 if (!mail_from
.empty()) {
1362 msg
.headers
.erase(std::remove(msg
.headers
.begin(), msg
.headers
.end(), From
),
1365 msg
.from_str
= mail_from
;
1366 CHECK(msg
.parse_hdr(msg
.from_str
));
1369 if (!reply_to
.empty()) {
1371 std::remove(msg
.headers
.begin(), msg
.headers
.end(), Reply_To
),
1374 msg
.reply_to_str
= reply_to
;
1375 CHECK(msg
.parse_hdr(msg
.reply_to_str
));
1378 // modify plain text body
1381 if (iequal(msg.get_header(MIME_Version), "1.0") &&
1382 istarts_with(msg.get_header(Content_Type), "text/plain;")) {
1383 LOG(INFO) << "Adding footer to message body.";
1384 msg.body_str = msg.body;
1385 msg.body_str.append("\r\n\r\n\t-- Added Footer --\r\n");
1386 msg.body = msg.body_str;
1389 LOG(INFO) << "Not adding footer to message body.";
1390 LOG(INFO) << "MIME-Version == " << msg.get_header(MIME_Version);
1391 LOG(INFO) << "Content-Type == " << msg.get_header(Content_Type);
1393 // LOG(INFO) << "body == " << msg.body;
1396 dkim_sign(msg
, sender
, selector
, key_file
);
1399 } // namespace message