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
<range
<'0', '1'>, rep
<2, DIGIT
>>,
236 rep_min_max
<1, 2, DIGIT
>> {};
238 struct ipv4_address
: seq
<dec_octet
, dot
, dec_octet
, dot
, dec_octet
, dot
, dec_octet
> {};
240 struct h16
: rep_min_max
<1, 4, HEXDIG
> {};
242 struct ls32
: sor
<seq
<h16
, colon
, h16
>, ipv4_address
> {};
244 struct dcolon
: two
<':'> {};
246 struct ipv6_address
: sor
<seq
< rep
<6, h16
, colon
>, ls32
>,
247 seq
< dcolon
, rep
<5, h16
, colon
>, ls32
>,
248 seq
<opt
<h16
>, dcolon
, rep
<4, h16
, colon
>, ls32
>,
249 seq
<opt
<h16
, opt
< colon
, h16
>>, dcolon
, rep
<3, h16
, colon
>, ls32
>,
250 seq
<opt
<h16
, rep_opt
<2, colon
, h16
>>, dcolon
, rep
<2, h16
, colon
>, ls32
>,
251 seq
<opt
<h16
, rep_opt
<3, colon
, h16
>>, dcolon
, h16
, colon
, ls32
>,
252 seq
<opt
<h16
, rep_opt
<4, colon
, h16
>>, dcolon
, ls32
>,
253 seq
<opt
<h16
, rep_opt
<5, colon
, h16
>>, dcolon
, h16
>,
254 seq
<opt
<h16
, rep_opt
<6, colon
, h16
>>, dcolon
>> {};
256 struct ip
: sor
<ipv4_address
, ipv6_address
> {};
258 struct obs_local_part
: seq
<word
, star
<seq
<dot
, word
>>> {};
260 struct local_part
: sor
<quoted_string
, dot_atom
> {};
262 struct dtext
: ranges
<33, 90, 94, 126> {};
264 struct domain_literal
: seq
<opt
<CFWS
>,
266 star
<seq
<opt
<FWS
>, dtext
>>,
271 struct domain
: sor
<dot_atom
, domain_literal
> {};
273 struct obs_domain
: sor
<list
<atom
, dot
>, domain_literal
> {};
275 // This addr_spec should be exactly the same as RFC5321 Mailbox, but it's not.
277 struct new_addr_spec
: seq
<local_part
, one
<'@'>, domain
> {};
279 struct obs_addr_spec
: seq
<obs_local_part
, one
<'@'>, obs_domain
> {};
281 struct addr_spec
: sor
<obs_addr_spec
, new_addr_spec
> {};
283 struct result
: sor
<TAO_PEGTL_ISTRING("Pass"),
284 TAO_PEGTL_ISTRING("Fail"),
285 TAO_PEGTL_ISTRING("SoftFail"),
286 TAO_PEGTL_ISTRING("Neutral"),
287 TAO_PEGTL_ISTRING("None"),
288 TAO_PEGTL_ISTRING("TempError"),
289 TAO_PEGTL_ISTRING("PermError")> {};
291 struct spf_key
: sor
<TAO_PEGTL_ISTRING("client-ip"),
292 TAO_PEGTL_ISTRING("envelope-from"),
293 TAO_PEGTL_ISTRING("helo"),
294 TAO_PEGTL_ISTRING("problem"),
295 TAO_PEGTL_ISTRING("receiver"),
296 TAO_PEGTL_ISTRING("identity"),
297 TAO_PEGTL_ISTRING("mechanism")> {};
299 // This value syntax (allowing addr_spec) is not in accordance with RFC
300 // 7208 (or 4408) but is what is effectivly used by libspf2 1.2.10 and
303 struct spf_value
: sor
<ip
, addr_spec
, dot_atom
, quoted_string
> {};
305 struct spf_kv_pair
: seq
<spf_key
, opt
<CFWS
>, one
<'='>, spf_value
> {};
307 struct spf_kv_list
: seq
<spf_kv_pair
,
308 star
<seq
<one
<';'>, opt
<CFWS
>, spf_kv_pair
>>,
311 struct spf_header
: seq
<opt
<CFWS
>,
313 opt
<seq
<FWS
, comment
>>,
314 opt
<seq
<FWS
, spf_kv_list
>>> {};
316 struct spf_header_only
: seq
<spf_header
, eof
> {};
318 //.............................................................................
320 struct obs_domain_list
: seq
<
321 star
<sor
<CFWS
, one
<','>>>, one
<'@'>, domain
,
322 star
<seq
<one
<','>, opt
<CFWS
>, opt
<seq
<one
<'@'>, domain
>>>>
325 struct obs_route
: seq
<obs_domain_list
, colon
> {};
327 struct obs_angle_addr
: seq
<opt
<CFWS
>, one
<'<'>, obs_route
, addr_spec
, one
<'>'>, opt
<CFWS
>> {};
329 struct angle_addr
: sor
<seq
<opt
<CFWS
>, one
<'<'>, addr_spec
, one
<'>'>, opt
<CFWS
>>,
333 struct display_name
: phrase
{};
335 struct name_addr
: seq
<opt
<display_name
>, angle_addr
> {};
337 struct mailbox
: sor
<name_addr
, addr_spec
> {};
339 struct obs_mbox_list
: seq
<star
<seq
<opt
<CFWS
>, one
<','>>>,
341 star
<seq
<one
<','>, opt
<sor
<mailbox
, CFWS
>>>>
344 struct mailbox_list
: sor
<list
<mailbox
, one
<','>>,
347 struct from
: seq
<TAO_PEGTL_ISTRING("From"), opt
<CFWS
>, colon
,
350 struct mailbox_list_only
: seq
<mailbox_list
, eof
> {};
352 //.............................................................................
354 // <https://www.rfc-editor.org/rfc/rfc2045.html>
356 // tspecials := "(" / ")" / "<" / ">" / "@" /
357 // "," / ";" / ":" / "\" / <">
358 // "/" / "[" / "]" / "?" / "="
360 // token := 1*<any (US-ASCII) CHAR except SPACE, CTLs,
375 struct tchar45
: ranges
< // NUL..' '
384 48, 57, // 0123456789
385 // 58, 64, // ;:<=>?@
388 94, 126 // ^_` a..z {|}~
392 struct token45
: plus
<tchar45
> {};
394 //.............................................................................
396 // <https://tools.ietf.org/html/rfc8601#section-2.2>
398 struct value
: sor
<token45
, quoted_string
> {};
400 struct authserv_id
: value
{};
402 struct authres_version
: seq
<plus
<DIGIT
>, opt
<CFWS
>> {};
404 struct no_result
: seq
<opt
<CFWS
>, one
<';'>, opt
<CFWS
>, TAO_PEGTL_ISTRING("none")> {};
406 struct let_dig
: sor
<ALPHA
, DIGIT
> {};
408 struct ldh_tail
: star
<sor
<seq
<plus
<one
<'-'>>, let_dig
>, let_dig
>> {};
410 struct ldh_str
: seq
<let_dig
, ldh_tail
> {};
412 struct keyword
: ldh_str
{};
414 struct method_version
: seq
<plus
<DIGIT
>, opt
<CFWS
>> {};
416 // method = Keyword [ [CFWS] "/" [CFWS] method-version ]
418 struct method
: seq
<keyword
, opt
<opt
<CFWS
>, one
<'/'>, opt
<CFWS
>, method_version
>> {};
420 // methodspec = [CFWS] method [CFWS] "=" [CFWS] result
421 // ; indicates which authentication method was evaluated
422 // ; and what its output was
424 struct methodspec
: seq
<opt
<CFWS
>, method
, opt
<CFWS
>, one
<'='>, opt
<CFWS
>, result
> {};
426 // reasonspec = "reason" [CFWS] "=" [CFWS] value
427 // ; a free-form comment on the reason the given result
430 struct reasonspec
: seq
<TAO_PEGTL_ISTRING("reason"), opt
<CFWS
>, one
<'='>, opt
<CFWS
>, value
> {};
432 // pvalue = [CFWS] ( value / [ [ local-part ] "@" ] domain-name )
435 struct pvalue
: seq
<opt
<CFWS
>, sor
<seq
<opt
<seq
<opt
<local_part
>, one
<'@'>>>, domain
>,
439 struct ptype
: keyword
{};
441 struct special_smtp_verb
: sor
<TAO_PEGTL_ISTRING("mailfrom"),
442 TAO_PEGTL_ISTRING("rcptto")> {};
444 struct property
: sor
<special_smtp_verb
, keyword
> {};
446 // propspec = ptype [CFWS] "." [CFWS] property [CFWS] "=" pvalue
447 // ; an indication of which properties of the message
448 // ; were evaluated by the authentication scheme being
449 // ; applied to yield the reported result
451 struct propspec
: seq
<ptype
, opt
<CFWS
>, dot
, opt
<CFWS
>, property
, opt
<CFWS
>, one
<'='>, pvalue
> {};
453 struct resinfo
: seq
<opt
<CFWS
>, one
<';'>, methodspec
, opt
<seq
<CFWS
, reasonspec
>>,
454 opt
<seq
<CFWS
, plus
<propspec
>>>
457 struct ar_results
: sor
<no_result
, plus
<resinfo
>> {};
459 struct authres_payload
: seq
<opt
<CFWS
>, authserv_id
,
460 opt
<seq
<CFWS
, authres_version
>>,
464 struct authres_header_field
: seq
<TAO_PEGTL_ISTRING("Authentication-Results"), opt
<CFWS
>, colon
,
467 struct authres_header_field_only
: seq
<authres_header_field
, eof
> {};
469 //.............................................................................
473 template <typename Rule
>
474 struct ar_action
: nothing
<Rule
> {
478 struct ar_action
<ar_results
> {
479 template <typename Input
>
481 apply(Input
const& in
, std::string
& authservid
, std::string
& ar_results
)
483 ar_results
= in
.string();
488 struct ar_action
<authserv_id
> {
489 template <typename Input
>
491 apply(Input
const& in
, std::string
& authservid
, std::string
& ar_results
)
493 authservid
= in
.string();
497 //.............................................................................
499 template <typename Rule
>
500 struct msg_action
: nothing
<Rule
> {
504 struct msg_action
<field_name
> {
505 template <typename Input
>
506 static void apply(Input
const& in
, ::message::parsed
& msg
)
508 msg
.field_name
= make_view(in
);
513 struct msg_action
<field_value
> {
514 template <typename Input
>
515 static void apply(Input
const& in
, ::message::parsed
& msg
)
517 msg
.field_value
= make_view(in
);
522 struct msg_action
<field
> {
523 template <typename Input
>
524 static void apply(Input
const& in
, ::message::parsed
& msg
)
526 msg
.headers
.emplace_back(
527 ::message::header(msg
.field_name
, msg
.field_value
));
532 struct msg_action
<raw_field
> {
533 template <typename Input
>
534 static void apply(Input
const& in
, ::message::parsed
& msg
)
536 msg
.headers
.emplace_back(
537 ::message::header(msg
.field_name
, msg
.field_value
));
542 struct msg_action
<body
> {
543 template <typename Input
>
544 static void apply(Input
const& in
, ::message::parsed
& msg
)
546 msg
.body
= make_view(in
);
550 //.............................................................................
552 struct received_spf_parsed
{
553 bool parse(std::string_view input
);
555 std::string_view whole_thing
;
557 std::string_view result
;
558 std::string_view comment
;
560 std::string_view key
;
561 std::string_view value
;
563 std::vector
<std::pair
<std::string_view
, std::string_view
>> kv_list
;
564 std::map
<std::string_view
, std::string_view
, ci_less
> kv_map
;
566 std::string
as_string() const { return fmt::format("{}", whole_thing
); }
569 template <typename Rule
>
570 struct spf_action
: nothing
<Rule
> {
574 struct spf_action
<result
> {
575 template <typename Input
>
576 static void apply(const Input
& in
, received_spf_parsed
& spf
)
578 spf
.result
= make_view(in
);
583 struct spf_action
<comment
> {
584 template <typename Input
>
585 static void apply(const Input
& in
, received_spf_parsed
& spf
)
587 spf
.comment
= make_view(in
);
592 struct spf_action
<spf_key
> {
593 template <typename Input
>
594 static void apply(const Input
& in
, received_spf_parsed
& spf
)
596 spf
.key
= make_view(in
);
601 struct spf_action
<spf_value
> {
602 template <typename Input
>
603 static void apply(const Input
& in
, received_spf_parsed
& spf
)
605 // RFC5322 syntax is full of optional WS, so we trim
606 spf
.value
= trim(make_view(in
));
611 struct spf_action
<spf_kv_pair
> {
612 template <typename Input
>
613 static void apply(const Input
& in
, received_spf_parsed
& spf
)
615 spf
.kv_list
.emplace_back(spf
.key
, spf
.value
);
616 spf
.key
= spf
.value
= "";
621 struct spf_action
<spf_kv_list
> {
622 static void apply0(received_spf_parsed
& spf
)
624 for (auto const& kvp
: spf
.kv_list
) {
625 if (spf
.kv_map
.contains(kvp
.first
)) {
626 LOG(WARNING
) << "dup key: " << kvp
.first
<< "=" << kvp
.second
;
627 LOG(WARNING
) << " and: " << kvp
.first
<< "="
628 << spf
.kv_map
[kvp
.first
];
630 spf
.kv_map
[kvp
.first
] = kvp
.second
;
635 bool received_spf_parsed::parse(std::string_view input
)
638 auto in
{memory_input
<>(input
.data(), input
.size(), "spf_header")};
639 return tao::pegtl::parse
<spf_header_only
, spf_action
>(in
, *this);
642 //.............................................................................
644 template <typename Rule
>
645 struct mailbox_list_action
: nothing
<Rule
> {};
648 struct mailbox_list_action
<local_part
> {
649 template <typename Input
>
650 static void apply(Input
const& in
,
651 ::message::mailbox_name_addr_list
& from_parsed
)
653 LOG(INFO
) << "local_part: " << in
.string();
658 struct mailbox_list_action
<domain
> {
659 template <typename Input
>
660 static void apply(Input
const& in
,
661 ::message::mailbox_name_addr_list
& from_parsed
)
663 LOG(INFO
) << "domain: " << in
.string();
668 struct mailbox_list_action
<obs_local_part
> {
669 template <typename Input
>
670 static void apply(Input
const& in
,
671 ::message::mailbox_name_addr_list
& from_parsed
)
673 LOG(INFO
) << "obs_local_part: " << in
.string();
678 struct mailbox_list_action
<obs_domain
> {
679 template <typename Input
>
680 static void apply(Input
const& in
,
681 ::message::mailbox_name_addr_list
& from_parsed
)
683 LOG(INFO
) << "obs_domain: " << in
.string();
688 struct mailbox_list_action
<display_name
> {
689 template <typename Input
>
690 static void apply(Input
const& in
,
691 ::message::mailbox_name_addr_list
& from_parsed
)
693 from_parsed
.maybe_name
= in
.string();
698 struct mailbox_list_action
<angle_addr
> {
699 template <typename Input
>
700 static void apply(Input
const& in
,
701 ::message::mailbox_name_addr_list
& from_parsed
)
703 std::swap(from_parsed
.name
, from_parsed
.maybe_name
);
708 struct mailbox_list_action
<addr_spec
> {
709 template <typename Input
>
710 static void apply(Input
const& in
,
711 ::message::mailbox_name_addr_list
& from_parsed
)
713 from_parsed
.name_addr_list
.push_back({from_parsed
.name
, in
.string()});
714 from_parsed
.name
.clear();
715 from_parsed
.maybe_name
.clear();
719 } // namespace RFC5322
721 // Map SPF result string to DMARC policy code.
723 static int result_to_pol(std::string_view result
)
726 if (iequal(result
, Pass
)) return DMARC_POLICY_SPF_OUTCOME_PASS
;
727 if (iequal(result
, Fail
)) return DMARC_POLICY_SPF_OUTCOME_FAIL
;
728 if (iequal(result
, SoftFail
)) return DMARC_POLICY_SPF_OUTCOME_TMPFAIL
;
729 if (iequal(result
, Neutral
)) return DMARC_POLICY_SPF_OUTCOME_NONE
;
730 if (iequal(result
, None
)) return DMARC_POLICY_SPF_OUTCOME_NONE
;
731 if (iequal(result
, TempError
)) return DMARC_POLICY_SPF_OUTCOME_NONE
;
732 if (iequal(result
, PermError
)) return DMARC_POLICY_SPF_OUTCOME_NONE
;
733 LOG(WARNING
) << "unknown SPF result: \"" << result
<< "\"";
734 return DMARC_POLICY_SPF_OUTCOME_NONE
;
738 static bool is_postmaster(std::string_view from
)
740 return from
== "<>" || iequal(from
, "<Postmaster>") ||
741 istarts_with(from
, "<Postmaster@");
744 static bool sender_comment(std::string_view comment
, std::string_view sender
)
746 auto const prefix
= fmt::format("({}:", sender
);
747 return istarts_with(comment
, prefix
);
750 static void spf_result_to_dmarc(OpenDMARC::policy
& dmp
,
751 RFC5322::received_spf_parsed
& spf
)
753 LOG(INFO
) << "spf_result_to_dmarc";
755 if (spf
.kv_map
.contains(problem
)) {
756 LOG(WARNING
) << "SPF problem: " << spf
.kv_map
[problem
];
759 auto const spf_pol
= result_to_pol(spf
.result
);
761 if (spf_pol
== DMARC_POLICY_SPF_OUTCOME_NONE
) {
762 LOG(WARNING
) << "Ignoring for DMARC purposes: " << spf
.as_string();
770 if (spf
.kv_map
.contains(identity
)) {
771 if (iequal(spf
.kv_map
[identity
], mailfrom
)) {
772 if (spf
.kv_map
.contains(envelope_from
)) {
773 if (Mailbox::validate(spf
.kv_map
[envelope_from
])) {
774 Mailbox
mbx(spf
.kv_map
[envelope_from
]);
775 spf_dom
= mbx
.domain().ascii();
776 spf_origin
= DMARC_POLICY_SPF_ORIGIN_MAILFROM
;
778 auto const human_result
=
779 fmt::format("{}, explicit origin mail from, mailbox {}",
780 spf
.result
, mbx
.as_string());
781 LOG(INFO
) << "SPF result " << human_result
;
782 dmp
.store_spf(spf_dom
.c_str(), spf_pol
, spf_origin
,
783 human_result
.c_str());
787 LOG(WARNING
) << "invalid mailbox in envelope-from: "
788 << spf
.kv_map
[envelope_from
];
793 << "identity checked was mail from, but no envelope_from key";
796 else if (iequal(spf
.kv_map
[identity
], helo
)) {
797 if (spf
.kv_map
.contains(helo
)) {
798 if (Domain::validate(spf
.kv_map
[helo
])) {
799 Domain
dom(spf
.kv_map
[helo
]);
800 spf_dom
= dom
.ascii();
801 spf_origin
= DMARC_POLICY_SPF_ORIGIN_HELO
;
803 auto const human_result
= fmt::format(
804 "{}, explicit origin hello, domain {}", spf
.result
, dom
.ascii());
805 LOG(INFO
) << "SPF result " << human_result
;
806 dmp
.store_spf(spf_dom
.c_str(), spf_pol
, spf_origin
,
807 human_result
.c_str());
811 LOG(WARNING
) << "invalid domain in helo: " << spf
.kv_map
[helo
];
815 LOG(WARNING
) << "identity checked was helo, but no helo key";
819 LOG(WARNING
) << "unknown identity " << spf
.kv_map
[identity
];
823 LOG(INFO
) << "no explicit tag for which identity was checked";
826 if (spf
.kv_map
.contains(envelope_from
)) {
827 auto const efrom
= spf
.kv_map
[envelope_from
];
829 if (is_postmaster(efrom
)) {
830 if (spf
.kv_map
.contains(helo
)) {
831 if (Domain::validate(spf
.kv_map
[helo
])) {
832 Domain
dom(spf
.kv_map
[helo
]);
833 spf_dom
= dom
.ascii();
834 spf_origin
= DMARC_POLICY_SPF_ORIGIN_HELO
;
836 auto const human_result
= fmt::format(
837 "{}, RFC5321.MailFrom is <>, implicit origin hello, domain {}",
838 spf
.result
, dom
.ascii());
839 LOG(INFO
) << "SPF result " << human_result
;
840 dmp
.store_spf(spf_dom
.c_str(), spf_pol
, spf_origin
,
841 human_result
.c_str());
845 LOG(WARNING
) << "RFC5321.MailFrom is postmaster or <> but helo is "
851 LOG(WARNING
) << "envelope-from is <> but no helo key";
854 else if (Mailbox::validate(efrom
)) {
857 spf_dom
= mbx
.domain().ascii();
858 spf_origin
= DMARC_POLICY_SPF_ORIGIN_MAILFROM
;
860 auto const human_result
= fmt::format(
861 "{}, implicit RFC5321.MailFrom <{}>", spf
.result
, mbx
.as_string());
862 LOG(INFO
) << "SPF result " << human_result
;
863 dmp
.store_spf(spf_dom
.c_str(), spf_pol
, spf_origin
, human_result
.c_str());
867 LOG(WARNING
) << "envelope-from invalid mailbox: " << efrom
;
870 else if (spf
.kv_map
.contains(helo
)) {
871 if (Domain::validate(spf
.kv_map
[helo
])) {
872 Domain
dom(spf
.kv_map
[helo
]);
873 spf_dom
= dom
.ascii();
874 spf_origin
= DMARC_POLICY_SPF_ORIGIN_HELO
;
876 auto const human_result
=
877 fmt::format("{}, hello domain {}", spf
.result
, dom
.ascii());
878 LOG(INFO
) << "SPF result " << human_result
;
879 dmp
.store_spf(spf_dom
.c_str(), spf_pol
, spf_origin
, human_result
.c_str());
883 LOG(WARNING
) << "helo is invalid domain:" << spf
.kv_map
[helo
];
888 << "no explicit \"identity\" key, and no envelope-from or helo key";
894 bool mailbox_list_parse(std::string_view input
,
895 mailbox_name_addr_list
& name_addr_list
)
897 name_addr_list
= mailbox_name_addr_list
{};
898 auto in
{memory_input
<>(input
.data(), input
.size(), "mailbox_list_only")};
899 return tao::pegtl::parse
<RFC5322::mailbox_list_only
,
900 RFC5322::mailbox_list_action
>(in
, name_addr_list
);
903 bool authentication_results_parse(std::string_view input
,
904 std::string
& authservid
,
905 std::string
& ar_results
)
907 auto in
{memory_input
<>(input
.data(), input
.size(),
908 "authentication_results_header")};
909 return tao::pegtl::parse
<RFC5322::authres_header_field_only
,
910 RFC5322::ar_action
>(in
, authservid
, ar_results
);
913 bool authentication(message::parsed
& msg
,
915 char const* selector
,
918 LOG(INFO
) << "add_authentication_results";
919 CHECK(!msg
.headers
.empty());
921 // Remove any redundant Authentication-Results headers
923 std::remove_if(msg
.headers
.begin(), msg
.headers
.end(),
924 [sender
](auto const& hdr
) {
925 if (hdr
== Authentication_Results
) {
926 std::string authservid
;
927 std::string ar_results
;
928 if (message::authentication_results_parse(
929 hdr
.as_view(), authservid
, ar_results
)) {
930 return Domain::match(authservid
, sender
);
932 LOG(WARNING
) << "failed to parse " << hdr
.as_string();
938 // Run our message through OpenDKIM verify
940 OpenDKIM::verify dkv
;
941 for (auto const& header
: msg
.headers
) {
942 auto const hv
= header
.as_view();
947 // LOG(INFO) << "body «" << msg.body << "»";
952 OpenDMARC::policy dmp
;
954 // Build up Authentication-Results header
955 fmt::memory_buffer bfr
;
957 std::unordered_set
<Domain
> validated_doms
;
960 for (auto hdr
: msg
.headers
) {
961 if (hdr
== Received_SPF
) {
962 RFC5322::received_spf_parsed spf_parsed
;
963 if (!spf_parsed
.parse(hdr
.value
)) {
964 LOG(WARNING
) << "failed to parse SPF record: " << hdr
.value
;
968 LOG(INFO
) << "SPF record parsed";
969 if (!sender_comment(spf_parsed
.comment
, sender
)) {
970 LOG(INFO
) << "comment == \"" << spf_parsed
.comment
<< "\" not by "
975 if (!Mailbox::validate(spf_parsed
.kv_map
[envelope_from
])) {
976 LOG(WARNING
) << "invalid mailbox: " << spf_parsed
.kv_map
[envelope_from
];
980 if (!Domain::validate(spf_parsed
.kv_map
[helo
])) {
981 LOG(WARNING
) << "invalid helo domain: " << spf_parsed
.kv_map
[helo
];
985 Mailbox
env_from(spf_parsed
.kv_map
[envelope_from
]);
986 Domain
helo_dom(spf_parsed
.kv_map
[helo
]);
988 if (iequal(env_from
.local_part(), "Postmaster") &&
989 env_from
.domain() == helo_dom
) {
990 if (validated_doms
.count(helo_dom
) == 0) {
991 fmt::format_to(std::back_inserter(bfr
), ";\r\n\tspf={}",
993 fmt::format_to(std::back_inserter(bfr
), " {}", spf_parsed
.comment
);
994 fmt::format_to(std::back_inserter(bfr
), " smtp.helo={}",
996 validated_doms
.emplace(helo_dom
);
998 if (spf_parsed
.kv_map
.contains(client_ip
)) {
999 std::string ip
= make_string(spf_parsed
.kv_map
[client_ip
]);
1000 dmp
.connect(ip
.c_str());
1002 spf_result_to_dmarc(dmp
, spf_parsed
);
1006 if (validated_doms
.count(env_from
.domain()) == 0) {
1007 fmt::format_to(std::back_inserter(bfr
), ";\r\n\tspf={}",
1009 fmt::format_to(std::back_inserter(bfr
), " {}", spf_parsed
.comment
);
1010 fmt::format_to(std::back_inserter(bfr
), " smtp.mailfrom={}",
1011 env_from
.as_string(Mailbox::domain_encoding::ascii
));
1012 validated_doms
.emplace(env_from
.domain());
1014 if (spf_parsed
.kv_map
.contains(client_ip
)) {
1015 std::string ip
= make_string(spf_parsed
.kv_map
[client_ip
]);
1016 dmp
.connect(ip
.c_str());
1018 spf_result_to_dmarc(dmp
, spf_parsed
);
1024 LOG(INFO
) << "fetching From: header";
1025 // Should be only one From:
1026 if (auto hdr
= std::find(begin(msg
.headers
), end(msg
.headers
), From
);
1027 hdr
!= end(msg
.headers
)) {
1028 auto const from_str
= make_string(hdr
->value
);
1030 if (!mailbox_list_parse(from_str
, msg
.from_parsed
)) {
1031 LOG(WARNING
) << "failed to parse «From:" << from_str
<< "»";
1034 for (auto hdr_next
= std::next(hdr
); hdr_next
!= end(msg
.headers
);
1035 hdr_next
= std::next(hdr_next
)) {
1036 if (*hdr_next
== From
) {
1037 LOG(WARNING
) << "additional RFC5322.From header «"
1038 << hdr_next
->as_string() << "»";
1043 if (msg
.from_parsed
.name_addr_list
.empty()) {
1044 LOG(WARNING
) << "No address in RFC5322.From header";
1049 <https://tools.ietf.org/html/rfc7489#section-6.6>
1050 6.6.1. Extract Author Domain
1052 The case of a syntactically valid multi-valued RFC5322.From field
1053 presents a particular challenge. The process in this case is to
1054 apply the DMARC check using each of those domains found in the
1055 RFC5322.From field as the Author Domain and apply the most strict
1056 policy selected among the checks that fail.
1061 if (msg
.from_parsed
.name_addr_list
.size() > 1) {
1062 LOG(WARNING
) << "More than one address in RFC5322.From header";
1065 auto from_addr
= msg
.from_parsed
.name_addr_list
[0].addr
;
1067 boost::trim(from_addr
);
1069 if (!Mailbox::validate(from_addr
)) {
1070 LOG(WARNING
) << "Mailbox syntax valid for RFC-5322, not for RFC-5321: \""
1071 << from_addr
<< "\"";
1072 // Maybe we can pick out a valid domain?
1076 Mailbox
from_mbx(from_addr
);
1077 msg
.dmarc_from
= from_mbx
.as_string(Mailbox::domain_encoding::ascii
);
1078 msg
.dmarc_from_domain
= from_mbx
.domain().ascii();
1080 LOG(INFO
) << "dmarc_from_domain == " << msg
.dmarc_from_domain
;
1081 dmp
.store_from_domain(msg
.dmarc_from_domain
.c_str());
1083 // Check each DKIM sig, inform DMARC processor, put in AR
1085 dkv
.foreach_sig([&dmp
, &bfr
](char const* domain
, bool passed
,
1086 char const* identity
, char const* sel
,
1088 int const result
= passed
? DMARC_POLICY_DKIM_OUTCOME_PASS
1089 : DMARC_POLICY_DKIM_OUTCOME_FAIL
;
1090 auto const human_result
= (passed
? "pass" : "fail");
1092 LOG(INFO
) << "DKIM check for " << domain
<< " " << human_result
;
1094 dmp
.store_dkim(domain
, sel
, result
, human_result
);
1096 auto bs
= std::string_view(b
, strlen(b
)).substr(0, 8);
1098 fmt::format_to(std::back_inserter(bfr
), ";\r\n\tdkim={}", human_result
);
1099 fmt::format_to(std::back_inserter(bfr
), " header.i={}", identity
);
1100 fmt::format_to(std::back_inserter(bfr
), " header.s={}", sel
);
1101 fmt::format_to(std::back_inserter(bfr
), " header.b=\"{}\"", bs
);
1104 // Set DMARC status in AR
1106 auto const dmarc_passed
= dmp
.query_dmarc(msg
.dmarc_from_domain
.c_str());
1108 auto const dmarc_result
= (dmarc_passed
? "pass" : "fail");
1109 LOG(INFO
) << "DMARC " << dmarc_result
;
1111 fmt::format_to(std::back_inserter(bfr
), ";\r\n\tdmarc={} header.from={}",
1112 dmarc_result
, msg
.dmarc_from_domain
);
1116 OpenARC::verify arv
;
1117 for (auto const& header
: msg
.headers
) {
1118 arv
.header(header
.as_view());
1124 LOG(INFO
) << "ARC status == " << arv
.chain_status_str();
1125 LOG(INFO
) << "ARC custody == " << arv
.chain_custody_str();
1127 auto const arc_status
= arv
.chain_status_str();
1129 fmt::format_to(std::back_inserter(bfr
), ";\r\n\tarc={}", arc_status
);
1131 // New AR header on the top
1133 auto const ar_results
= [&bfr
]() {
1134 // Ug, OpenARC adds an extra one, arc.c:3213
1135 auto s
= fmt::to_string(bfr
);
1136 if (s
.length() && s
[0] == ';')
1142 fmt::format("{}: {};{}", Authentication_Results
, sender
, ar_results
);
1144 LOG(INFO
) << "new AR header «" << esc(msg
.ar_str
, esc_line_option::multi
)
1147 CHECK(msg
.parse_hdr(msg
.ar_str
));
1149 // Run our message through ARC::sign
1153 if (iequal(arc_status
, "none")) {
1156 else if (iequal(arc_status
, "fail")) {
1159 else if (iequal(arc_status
, "pass")) {
1166 for (auto const& header
: msg
.headers
) {
1167 ars
.header(header
.as_view());
1173 boost::iostreams::mapped_file_source priv
;
1174 priv
.open(key_file
);
1176 if (ars
.seal(sender
, selector
, sender
, priv
.data(), priv
.size(),
1177 ar_results
.c_str())) {
1178 msg
.arc_hdrs
= ars
.whole_seal();
1179 for (auto const& hdr
: msg
.arc_hdrs
) {
1180 CHECK(msg
.parse_hdr(hdr
));
1184 LOG(INFO
) << "failed to generate seal";
1187 OpenARC::verify arv2
;
1188 for (auto const& header
: msg
.headers
) {
1189 arv2
.header(header
.as_view());
1192 arv2
.body(msg
.body
);
1195 LOG(INFO
) << "check ARC status == " << arv2
.chain_status_str();
1196 LOG(INFO
) << "check ARC custody == " << arv2
.chain_custody_str();
1198 return dmarc_passed
;
1201 void print_spf_envelope_froms(char const* file
, message::parsed
& msg
)
1203 CHECK(!msg
.headers
.empty());
1204 for (auto const& hdr
: msg
.headers
) {
1205 if (hdr
== Received_SPF
) {
1206 RFC5322::received_spf_parsed spf_parsed
;
1207 if (spf_parsed
.parse(hdr
.value
)) {
1208 std::cout
<< spf_parsed
.kv_map
[envelope_from
] << '\n';
1212 LOG(WARNING
) << "failed to parse " << file
<< ":\n" << hdr
.as_string();
1218 void remove_delivery_headers(message::parsed
& msg
)
1220 // Remove headers that are added by the "delivery agent"
1221 // aka (Session::added_headers_)
1223 std::remove(msg
.headers
.begin(), msg
.headers
.end(), Return_Path
),
1226 // just in case, but right now this header should not exist.
1228 std::remove(msg
.headers
.begin(), msg
.headers
.end(), Delivered_To
),
1232 void dkim_check(message::parsed
& msg
, char const* domain
)
1234 LOG(INFO
) << "dkim";
1236 CHECK(!msg
.body
.empty());
1238 OpenDKIM::verify dkv
;
1240 // Run our message through OpenDKIM verify
1242 for (auto const& header
: msg
.headers
) {
1243 auto const hv
= header
.as_view();
1250 // Check each DKIM sig, inform DMARC processor, put in AR
1252 dkv
.foreach_sig([](char const* domain
, bool passed
, char const* identity
,
1253 char const* sel
, char const* b
) {
1254 auto const human_result
= (passed
? "pass" : "fail");
1256 auto bs
= std::string_view(b
, strlen(b
)).substr(0, 8);
1258 LOG(INFO
) << "DKIM check bfor " << domain
<< " " << human_result
;
1259 LOG(INFO
) << " header.i=" << identity
;
1260 LOG(INFO
) << " header.s=" << sel
;
1261 LOG(INFO
) << " header.b=\"" << bs
<< "\"";
1265 //.............................................................................
1267 bool parsed::parse(std::string_view input
)
1269 auto in
{memory_input
<>(input
.data(), input
.size(), "message")};
1270 return tao::pegtl::parse
<RFC5322::message
, RFC5322::msg_action
>(in
, *this);
1273 bool parsed::parse_hdr(std::string_view input
)
1275 auto in
{memory_input
<>(input
.data(), input
.size(), "message")};
1276 if (tao::pegtl::parse
<RFC5322::raw_field
, RFC5322::msg_action
>(in
, *this)) {
1277 std::rotate(headers
.rbegin(), headers
.rbegin() + 1, headers
.rend());
1283 std::string
parsed::as_string() const
1285 fmt::memory_buffer bfr
;
1287 for (auto const& h
: headers
)
1288 fmt::format_to(std::back_inserter(bfr
), "{}\r\n", h
.as_string());
1291 fmt::format_to(std::back_inserter(bfr
), "\r\n{}", body
);
1293 return fmt::to_string(bfr
);
1296 bool parsed::write(std::ostream
& os
) const
1298 for (auto const& h
: headers
)
1299 os
<< h
.as_string() << "\r\n";
1302 os
<< "\r\n" << body
;
1307 std::string
header::as_string() const
1309 return fmt::format("{}:{}", name
, value
);
1312 std::string_view
parsed::get_header(std::string_view name
) const
1314 if (auto hdr
= std::find(begin(headers
), end(headers
), name
);
1315 hdr
!= end(headers
)) {
1316 return trim(hdr
->value
);
1321 void dkim_sign(message::parsed
& msg
,
1323 char const* selector
,
1326 CHECK(msg
.sig_str
.empty());
1328 boost::iostreams::mapped_file_source priv
;
1329 priv
.open(key_file
);
1331 auto const key_str
= std::string(priv
.data(), priv
.size());
1333 // Run our message through DKIM::sign
1334 OpenDKIM::sign
dks(key_str
.c_str(), // textual data
1335 selector
, sender
, OpenDKIM::sign::body_type::text
);
1336 for (auto const& header
: msg
.headers
) {
1337 dks
.header(header
.as_view());
1343 auto const sig
= dks
.getsighdr();
1345 msg
.sig_str
= fmt::format("DKIM-Signature: {}", sig
);
1346 CHECK(msg
.parse_hdr(msg
.sig_str
));
1349 void rewrite_from_to(message::parsed
& msg
,
1350 std::string mail_from
,
1351 std::string reply_to
,
1353 char const* selector
,
1356 LOG(INFO
) << "rewrite_from_to";
1358 remove_delivery_headers(msg
);
1360 if (!mail_from
.empty()) {
1361 msg
.headers
.erase(std::remove(msg
.headers
.begin(), msg
.headers
.end(), From
),
1364 msg
.from_str
= mail_from
;
1365 CHECK(msg
.parse_hdr(msg
.from_str
));
1368 if (!reply_to
.empty()) {
1370 std::remove(msg
.headers
.begin(), msg
.headers
.end(), Reply_To
),
1373 msg
.reply_to_str
= reply_to
;
1374 CHECK(msg
.parse_hdr(msg
.reply_to_str
));
1377 // modify plain text body
1380 if (iequal(msg.get_header(MIME_Version), "1.0") &&
1381 istarts_with(msg.get_header(Content_Type), "text/plain;")) {
1382 LOG(INFO) << "Adding footer to message body.";
1383 msg.body_str = msg.body;
1384 msg.body_str.append("\r\n\r\n\t-- Added Footer --\r\n");
1385 msg.body = msg.body_str;
1388 LOG(INFO) << "Not adding footer to message body.";
1389 LOG(INFO) << "MIME-Version == " << msg.get_header(MIME_Version);
1390 LOG(INFO) << "Content-Type == " << msg.get_header(Content_Type);
1392 // LOG(INFO) << "body == " << msg.body;
1395 dkim_sign(msg
, sender
, selector
, key_file
);
1398 } // namespace message