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
<':'>;
93 struct UTF8_tail
: range
<'\x80', '\xBF'> {};
95 struct UTF8_1
: range
<0x00, 0x7F> {};
97 struct UTF8_2
: seq
<range
<'\xC2', '\xDF'>, UTF8_tail
> {};
99 struct UTF8_3
: sor
<seq
<one
<'\xE0'>, range
<'\xA0', '\xBF'>, UTF8_tail
>,
100 seq
<range
<'\xE1', '\xEC'>, rep
<2, UTF8_tail
>>,
101 seq
<one
<'\xED'>, range
<'\x80', '\x9F'>, UTF8_tail
>,
102 seq
<range
<'\xEE', '\xEF'>, rep
<2, UTF8_tail
>>> {};
104 struct UTF8_4
: sor
<seq
<one
<'\xF0'>, range
<'\x90', '\xBF'>, rep
<2, UTF8_tail
>>,
105 seq
<range
<'\xF1', '\xF3'>, rep
<3, UTF8_tail
>>,
106 seq
<one
<'\xF4'>, range
<'\x80', '\x8F'>, rep
<2, UTF8_tail
>>> {};
108 struct UTF8_non_ascii
: sor
<UTF8_2
, UTF8_3
, UTF8_4
> {};
110 struct VUCHAR
: sor
<VCHAR
, UTF8_non_ascii
> {};
112 //.............................................................................
114 struct ftext
: ranges
<33, 57, 59, 126> {};
116 struct field_name
: plus
<ftext
> {};
118 struct FWS
: seq
<opt
<seq
<star
<WSP
>, eol
>>, plus
<WSP
>> {};
120 // *([FWS] VCHAR) *WSP
121 struct field_value
: seq
<star
<seq
<opt
<FWS
>, VUCHAR
>>, star
<WSP
>> {};
123 struct field
: seq
<field_name
, colon
, field_value
, eol
> {};
125 struct raw_field
: seq
<field_name
, colon
, field_value
, eof
> {};
127 struct fields
: star
<field
> {};
129 struct body
: until
<eof
> {};
131 struct message
: seq
<fields
, opt
<seq
<eol
, body
>>, eof
> {};
133 //.............................................................................
135 // <https://tools.ietf.org/html/rfc2047>
137 // especials = "(" / ")" / "<" / ">" / "@" / "," / ";" / ":" / "
138 // <"> / "/" / "[" / "]" / "?" / "." / "="
140 // token = 1*<Any CHAR except SPACE, CTLs, and especials>
142 struct tchar47
: ranges
< // NUL..' '
151 48, 57, // 0123456789
152 // 58, 64, // ;:<=>?@
157 94, 126 // ^_` a..z {|}~
161 struct token47
: plus
<tchar47
> {};
163 struct charset
: token47
{};
164 struct encoding
: token47
{};
166 // encoded-text = 1*<Any printable ASCII character other than "?"
169 struct echar
: ranges
< // NUL..' '
172 64, 126 // @A..Z[\]^_` a..z {|}~
176 struct encoded_text
: plus
<echar
> {};
178 // encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
180 // leading opt<FWS> is not in RFC 2047
182 struct encoded_word_book
: seq
<string
<'=', '?'>,
183 charset
, string
<'?'>,
184 encoding
, string
<'?'>,
189 struct encoded_word
: seq
<opt
<FWS
>, encoded_word_book
> {};
191 //.............................................................................
193 // Comments are recursive, hence the forward declaration:
196 struct quoted_pair
: seq
<one
<'\\'>, sor
<VUCHAR
, WSP
>> {};
198 // ctext is ASCII not '(' or ')' or '\\'
199 struct ctext
: sor
<ranges
<33, 39, 42, 91, 93, 126>, UTF8_non_ascii
> {};
201 struct ccontent
: sor
<ctext
, quoted_pair
, comment
, encoded_word
> {};
203 // from <https://tools.ietf.org/html/rfc2047>
204 // comment = "(" *(ctext / quoted-pair / comment / encoded-word) ")"
206 struct comment
: seq
<one
<'('>,
207 star
<seq
<opt
<FWS
>, ccontent
>>,
212 struct CFWS
: sor
<seq
<plus
<seq
<opt
<FWS
>, comment
>, opt
<FWS
>>>,
215 struct qtext
: sor
<one
<33>, ranges
<35, 91, 93, 126>, UTF8_non_ascii
> {};
217 struct qcontent
: sor
<qtext
, quoted_pair
> {};
219 // Corrected in RFC-5322, errata ID: 3135 <https://www.rfc-editor.org/errata/eid3135>
220 struct quoted_string
: seq
<opt
<CFWS
>,
222 sor
<seq
<star
<seq
<opt
<FWS
>, qcontent
>>, opt
<FWS
>>, FWS
>,
227 struct atext
: sor
<ALPHA
, DIGIT
,
240 struct atom
: seq
<opt
<CFWS
>, plus
<atext
>, opt
<CFWS
>> {};
242 struct dot_atom_text
: list
<plus
<atext
>, dot
> {};
244 struct dot_atom
: seq
<opt
<CFWS
>, dot_atom_text
, opt
<CFWS
>> {};
246 struct word
: sor
<atom
, quoted_string
> {};
248 struct phrase
: plus
<sor
<encoded_word
, word
>> {};
250 struct dec_octet
: sor
<seq
<string
<'2','5'>, range
<'0','5'>>,
251 seq
<one
<'2'>, range
<'0','4'>, DIGIT
>,
252 seq
<range
<'0', '1'>, rep
<2, DIGIT
>>,
253 rep_min_max
<1, 2, DIGIT
>> {};
255 struct ipv4_address
: seq
<dec_octet
, dot
, dec_octet
, dot
, dec_octet
, dot
, dec_octet
> {};
257 struct h16
: rep_min_max
<1, 4, HEXDIG
> {};
259 struct ls32
: sor
<seq
<h16
, colon
, h16
>, ipv4_address
> {};
261 struct dcolon
: two
<':'> {};
263 struct ipv6_address
: sor
<seq
< rep
<6, h16
, colon
>, ls32
>,
264 seq
< dcolon
, rep
<5, h16
, colon
>, ls32
>,
265 seq
<opt
<h16
>, dcolon
, rep
<4, h16
, colon
>, ls32
>,
266 seq
<opt
<h16
, opt
< colon
, h16
>>, dcolon
, rep
<3, h16
, colon
>, ls32
>,
267 seq
<opt
<h16
, rep_opt
<2, colon
, h16
>>, dcolon
, rep
<2, h16
, colon
>, ls32
>,
268 seq
<opt
<h16
, rep_opt
<3, colon
, h16
>>, dcolon
, h16
, colon
, ls32
>,
269 seq
<opt
<h16
, rep_opt
<4, colon
, h16
>>, dcolon
, ls32
>,
270 seq
<opt
<h16
, rep_opt
<5, colon
, h16
>>, dcolon
, h16
>,
271 seq
<opt
<h16
, rep_opt
<6, colon
, h16
>>, dcolon
>> {};
273 struct ip
: sor
<ipv4_address
, ipv6_address
> {};
275 struct obs_local_part
: seq
<word
, star
<seq
<dot
, word
>>> {};
277 struct local_part
: sor
<quoted_string
, dot_atom
> {};
279 struct dtext
: ranges
<33, 90, 94, 126> {};
281 struct domain_literal
: seq
<opt
<CFWS
>,
283 star
<seq
<opt
<FWS
>, dtext
>>,
288 struct domain
: sor
<dot_atom
, domain_literal
> {};
290 struct obs_domain
: sor
<list
<atom
, dot
>, domain_literal
> {};
292 // This addr_spec should be exactly the same as RFC5321 Mailbox, but it's not.
294 struct new_addr_spec
: seq
<local_part
, one
<'@'>, domain
> {};
296 struct obs_addr_spec
: seq
<obs_local_part
, one
<'@'>, obs_domain
> {};
298 struct addr_spec
: sor
<obs_addr_spec
, new_addr_spec
> {};
300 struct result
: sor
<TAO_PEGTL_ISTRING("Pass"),
301 TAO_PEGTL_ISTRING("Fail"),
302 TAO_PEGTL_ISTRING("SoftFail"),
303 TAO_PEGTL_ISTRING("Neutral"),
304 TAO_PEGTL_ISTRING("None"),
305 TAO_PEGTL_ISTRING("TempError"),
306 TAO_PEGTL_ISTRING("PermError")> {};
308 struct spf_key
: sor
<TAO_PEGTL_ISTRING("client-ip"),
309 TAO_PEGTL_ISTRING("envelope-from"),
310 TAO_PEGTL_ISTRING("helo"),
311 TAO_PEGTL_ISTRING("problem"),
312 TAO_PEGTL_ISTRING("receiver"),
313 TAO_PEGTL_ISTRING("identity"),
314 TAO_PEGTL_ISTRING("mechanism")> {};
316 // This value syntax (allowing addr_spec) is not in accordance with RFC
317 // 7208 (or 4408) but is what is effectivly used by libspf2 1.2.10 and
320 struct spf_value
: sor
<ip
, addr_spec
, dot_atom
, quoted_string
> {};
322 struct spf_kv_pair
: seq
<spf_key
, opt
<CFWS
>, one
<'='>, spf_value
> {};
324 struct spf_kv_list
: seq
<spf_kv_pair
,
325 star
<seq
<one
<';'>, opt
<CFWS
>, spf_kv_pair
>>,
328 struct spf_header
: seq
<opt
<CFWS
>,
330 opt
<seq
<FWS
, comment
>>,
331 opt
<seq
<FWS
, spf_kv_list
>>> {};
333 struct spf_header_only
: seq
<spf_header
, eof
> {};
335 //.............................................................................
337 struct obs_domain_list
: seq
<
338 star
<sor
<CFWS
, one
<','>>>, one
<'@'>, domain
,
339 star
<seq
<one
<','>, opt
<CFWS
>, opt
<seq
<one
<'@'>, domain
>>>>
342 struct obs_route
: seq
<obs_domain_list
, colon
> {};
344 struct obs_angle_addr
: seq
<opt
<CFWS
>, one
<'<'>, obs_route
, addr_spec
, one
<'>'>, opt
<CFWS
>> {};
346 struct angle_addr
: sor
<seq
<opt
<CFWS
>, one
<'<'>, addr_spec
, one
<'>'>, opt
<CFWS
>>,
350 struct display_name
: phrase
{};
352 struct name_addr
: seq
<opt
<display_name
>, angle_addr
> {};
354 struct mailbox
: sor
<name_addr
, addr_spec
> {};
356 struct obs_mbox_list
: seq
<star
<seq
<opt
<CFWS
>, one
<','>>>,
358 star
<seq
<one
<','>, opt
<sor
<mailbox
, CFWS
>>>>
361 struct mailbox_list
: sor
<list
<mailbox
, one
<','>>,
364 struct from
: seq
<TAO_PEGTL_ISTRING("From"), opt
<CFWS
>, colon
,
367 struct mailbox_list_only
: seq
<mailbox_list
, eof
> {};
369 //.............................................................................
371 // <https://www.rfc-editor.org/rfc/rfc2045.html>
373 // tspecials := "(" / ")" / "<" / ">" / "@" /
374 // "," / ";" / ":" / "\" / <">
375 // "/" / "[" / "]" / "?" / "="
377 // token := 1*<any (US-ASCII) CHAR except SPACE, CTLs,
392 struct tchar45
: ranges
< // NUL..' '
401 48, 57, // 0123456789
402 // 58, 64, // ;:<=>?@
405 94, 126 // ^_` a..z {|}~
409 struct token45
: plus
<tchar45
> {};
411 //.............................................................................
413 // <https://tools.ietf.org/html/rfc8601#section-2.2>
415 struct value
: sor
<token45
, quoted_string
> {};
417 struct authserv_id
: value
{};
419 struct authres_version
: seq
<plus
<DIGIT
>, opt
<CFWS
>> {};
421 struct no_result
: seq
<opt
<CFWS
>, one
<';'>, opt
<CFWS
>, TAO_PEGTL_ISTRING("none")> {};
423 struct let_dig
: sor
<ALPHA
, DIGIT
> {};
425 struct ldh_tail
: star
<sor
<seq
<plus
<one
<'-'>>, let_dig
>, let_dig
>> {};
427 struct ldh_str
: seq
<let_dig
, ldh_tail
> {};
429 struct keyword
: ldh_str
{};
431 struct method_version
: seq
<plus
<DIGIT
>, opt
<CFWS
>> {};
433 // method = Keyword [ [CFWS] "/" [CFWS] method-version ]
435 struct method
: seq
<keyword
, opt
<opt
<CFWS
>, one
<'/'>, opt
<CFWS
>, method_version
>> {};
437 // methodspec = [CFWS] method [CFWS] "=" [CFWS] result
438 // ; indicates which authentication method was evaluated
439 // ; and what its output was
441 struct methodspec
: seq
<opt
<CFWS
>, method
, opt
<CFWS
>, one
<'='>, opt
<CFWS
>, result
> {};
443 // reasonspec = "reason" [CFWS] "=" [CFWS] value
444 // ; a free-form comment on the reason the given result
447 struct reasonspec
: seq
<TAO_PEGTL_ISTRING("reason"), opt
<CFWS
>, one
<'='>, opt
<CFWS
>, value
> {};
449 // pvalue = [CFWS] ( value / [ [ local-part ] "@" ] domain-name )
452 struct pvalue
: seq
<opt
<CFWS
>, sor
<seq
<opt
<seq
<opt
<local_part
>, one
<'@'>>>, domain
>,
456 struct ptype
: keyword
{};
458 struct special_smtp_verb
: sor
<TAO_PEGTL_ISTRING("mailfrom"),
459 TAO_PEGTL_ISTRING("rcptto")> {};
461 struct property
: sor
<special_smtp_verb
, keyword
> {};
463 // propspec = ptype [CFWS] "." [CFWS] property [CFWS] "=" pvalue
464 // ; an indication of which properties of the message
465 // ; were evaluated by the authentication scheme being
466 // ; applied to yield the reported result
468 struct propspec
: seq
<ptype
, opt
<CFWS
>, dot
, opt
<CFWS
>, property
, opt
<CFWS
>, one
<'='>, pvalue
> {};
470 struct resinfo
: seq
<opt
<CFWS
>, one
<';'>, methodspec
, opt
<seq
<CFWS
, reasonspec
>>,
471 opt
<seq
<CFWS
, plus
<propspec
>>>
474 struct ar_results
: sor
<no_result
, plus
<resinfo
>> {};
476 struct authres_payload
: seq
<opt
<CFWS
>, authserv_id
,
477 opt
<seq
<CFWS
, authres_version
>>,
481 struct authres_header_field
: seq
<TAO_PEGTL_ISTRING("Authentication-Results"), opt
<CFWS
>, colon
,
484 struct authres_header_field_only
: seq
<authres_header_field
, eof
> {};
486 //.............................................................................
490 template <typename Rule
>
491 struct ar_action
: nothing
<Rule
> {
495 struct ar_action
<ar_results
> {
496 template <typename Input
>
498 apply(Input
const& in
, std::string
& authservid
, std::string
& ar_results
)
500 ar_results
= in
.string();
505 struct ar_action
<authserv_id
> {
506 template <typename Input
>
508 apply(Input
const& in
, std::string
& authservid
, std::string
& ar_results
)
510 authservid
= in
.string();
514 //.............................................................................
516 template <typename Rule
>
517 struct msg_action
: nothing
<Rule
> {
521 struct msg_action
<field_name
> {
522 template <typename Input
>
523 static void apply(Input
const& in
, ::message::parsed
& msg
)
525 msg
.field_name
= make_view(in
);
530 struct msg_action
<field_value
> {
531 template <typename Input
>
532 static void apply(Input
const& in
, ::message::parsed
& msg
)
534 msg
.field_value
= make_view(in
);
539 struct msg_action
<field
> {
540 template <typename Input
>
541 static void apply(Input
const& in
, ::message::parsed
& msg
)
543 msg
.headers
.emplace_back(
544 ::message::header(msg
.field_name
, msg
.field_value
));
549 struct msg_action
<raw_field
> {
550 template <typename Input
>
551 static void apply(Input
const& in
, ::message::parsed
& msg
)
553 msg
.headers
.emplace_back(
554 ::message::header(msg
.field_name
, msg
.field_value
));
559 struct msg_action
<body
> {
560 template <typename Input
>
561 static void apply(Input
const& in
, ::message::parsed
& msg
)
563 msg
.body
= make_view(in
);
567 //.............................................................................
569 struct received_spf_parsed
{
570 bool parse(std::string_view input
);
572 std::string_view whole_thing
;
574 std::string_view result
;
575 std::string_view comment
;
577 std::string_view key
;
578 std::string_view value
;
580 std::vector
<std::pair
<std::string_view
, std::string_view
>> kv_list
;
581 std::map
<std::string_view
, std::string_view
, ci_less
> kv_map
;
583 std::string
as_string() const { return fmt::format("{}", whole_thing
); }
586 template <typename Rule
>
587 struct spf_action
: nothing
<Rule
> {
591 struct spf_action
<result
> {
592 template <typename Input
>
593 static void apply(const Input
& in
, received_spf_parsed
& spf
)
595 spf
.result
= make_view(in
);
600 struct spf_action
<comment
> {
601 template <typename Input
>
602 static void apply(const Input
& in
, received_spf_parsed
& spf
)
604 spf
.comment
= make_view(in
);
609 struct spf_action
<spf_key
> {
610 template <typename Input
>
611 static void apply(const Input
& in
, received_spf_parsed
& spf
)
613 spf
.key
= make_view(in
);
618 struct spf_action
<spf_value
> {
619 template <typename Input
>
620 static void apply(const Input
& in
, received_spf_parsed
& spf
)
622 // RFC5322 syntax is full of optional WS, so we trim
623 spf
.value
= trim(make_view(in
));
628 struct spf_action
<spf_kv_pair
> {
629 template <typename Input
>
630 static void apply(const Input
& in
, received_spf_parsed
& spf
)
632 spf
.kv_list
.emplace_back(spf
.key
, spf
.value
);
633 spf
.key
= spf
.value
= "";
638 struct spf_action
<spf_kv_list
> {
639 static void apply0(received_spf_parsed
& spf
)
641 for (auto const& kvp
: spf
.kv_list
) {
642 if (spf
.kv_map
.contains(kvp
.first
)) {
643 LOG(WARNING
) << "dup key: " << kvp
.first
<< "=" << kvp
.second
;
644 LOG(WARNING
) << " and: " << kvp
.first
<< "="
645 << spf
.kv_map
[kvp
.first
];
647 spf
.kv_map
[kvp
.first
] = kvp
.second
;
652 bool received_spf_parsed::parse(std::string_view input
)
655 auto in
{memory_input
<>(input
.data(), input
.size(), "spf_header")};
656 return tao::pegtl::parse
<spf_header_only
, spf_action
>(in
, *this);
659 //.............................................................................
661 template <typename Rule
>
662 struct mailbox_list_action
: nothing
<Rule
> {};
665 struct mailbox_list_action
<local_part
> {
666 template <typename Input
>
667 static void apply(Input
const& in
,
668 ::message::mailbox_name_addr_list
& from_parsed
)
670 LOG(INFO
) << "local_part: " << in
.string();
675 struct mailbox_list_action
<domain
> {
676 template <typename Input
>
677 static void apply(Input
const& in
,
678 ::message::mailbox_name_addr_list
& from_parsed
)
680 LOG(INFO
) << "domain: " << in
.string();
685 struct mailbox_list_action
<obs_local_part
> {
686 template <typename Input
>
687 static void apply(Input
const& in
,
688 ::message::mailbox_name_addr_list
& from_parsed
)
690 LOG(INFO
) << "obs_local_part: " << in
.string();
695 struct mailbox_list_action
<obs_domain
> {
696 template <typename Input
>
697 static void apply(Input
const& in
,
698 ::message::mailbox_name_addr_list
& from_parsed
)
700 LOG(INFO
) << "obs_domain: " << in
.string();
705 struct mailbox_list_action
<display_name
> {
706 template <typename Input
>
707 static void apply(Input
const& in
,
708 ::message::mailbox_name_addr_list
& from_parsed
)
710 from_parsed
.maybe_name
= in
.string();
715 struct mailbox_list_action
<angle_addr
> {
716 template <typename Input
>
717 static void apply(Input
const& in
,
718 ::message::mailbox_name_addr_list
& from_parsed
)
720 std::swap(from_parsed
.name
, from_parsed
.maybe_name
);
725 struct mailbox_list_action
<addr_spec
> {
726 template <typename Input
>
727 static void apply(Input
const& in
,
728 ::message::mailbox_name_addr_list
& from_parsed
)
730 from_parsed
.name_addr_list
.push_back({from_parsed
.name
, in
.string()});
731 from_parsed
.name
.clear();
732 from_parsed
.maybe_name
.clear();
736 } // namespace RFC5322
738 // Map SPF result string to DMARC policy code.
740 static int result_to_pol(std::string_view result
)
743 if (iequal(result
, Pass
)) return DMARC_POLICY_SPF_OUTCOME_PASS
;
744 if (iequal(result
, Fail
)) return DMARC_POLICY_SPF_OUTCOME_FAIL
;
745 if (iequal(result
, SoftFail
)) return DMARC_POLICY_SPF_OUTCOME_TMPFAIL
;
746 if (iequal(result
, Neutral
)) return DMARC_POLICY_SPF_OUTCOME_NONE
;
747 if (iequal(result
, None
)) return DMARC_POLICY_SPF_OUTCOME_NONE
;
748 if (iequal(result
, TempError
)) return DMARC_POLICY_SPF_OUTCOME_NONE
;
749 if (iequal(result
, PermError
)) return DMARC_POLICY_SPF_OUTCOME_NONE
;
750 LOG(WARNING
) << "unknown SPF result: \"" << result
<< "\"";
751 return DMARC_POLICY_SPF_OUTCOME_NONE
;
755 static bool is_postmaster(std::string_view from
)
757 return from
== "<>" || iequal(from
, "<Postmaster>") ||
758 istarts_with(from
, "<Postmaster@");
761 static bool sender_comment(std::string_view comment
, std::string_view sender
)
763 auto const prefix
= fmt::format("({}:", sender
);
764 return istarts_with(comment
, prefix
);
767 static void spf_result_to_dmarc(OpenDMARC::policy
& dmp
,
768 RFC5322::received_spf_parsed
& spf
)
770 LOG(INFO
) << "spf_result_to_dmarc";
772 if (spf
.kv_map
.contains(problem
)) {
773 LOG(WARNING
) << "SPF problem: " << spf
.kv_map
[problem
];
776 auto const spf_pol
= result_to_pol(spf
.result
);
778 if (spf_pol
== DMARC_POLICY_SPF_OUTCOME_NONE
) {
779 LOG(WARNING
) << "Ignoring for DMARC purposes: " << spf
.as_string();
787 if (spf
.kv_map
.contains(identity
)) {
788 if (iequal(spf
.kv_map
[identity
], mailfrom
)) {
789 if (spf
.kv_map
.contains(envelope_from
)) {
790 if (Mailbox::validate(spf
.kv_map
[envelope_from
])) {
791 Mailbox
mbx(spf
.kv_map
[envelope_from
]);
792 spf_dom
= mbx
.domain().ascii();
793 spf_origin
= DMARC_POLICY_SPF_ORIGIN_MAILFROM
;
795 auto const human_result
=
796 fmt::format("{}, explicit origin mail from, mailbox {}",
797 spf
.result
, mbx
.as_string());
798 LOG(INFO
) << "SPF result " << human_result
;
799 dmp
.store_spf(spf_dom
.c_str(), spf_pol
, spf_origin
,
800 human_result
.c_str());
804 LOG(WARNING
) << "invalid mailbox in envelope-from: "
805 << spf
.kv_map
[envelope_from
];
810 << "identity checked was mail from, but no envelope_from key";
813 else if (iequal(spf
.kv_map
[identity
], helo
)) {
814 if (spf
.kv_map
.contains(helo
)) {
815 if (Domain::validate(spf
.kv_map
[helo
])) {
816 Domain
dom(spf
.kv_map
[helo
]);
817 spf_dom
= dom
.ascii();
818 spf_origin
= DMARC_POLICY_SPF_ORIGIN_HELO
;
820 auto const human_result
= fmt::format(
821 "{}, explicit origin hello, domain {}", spf
.result
, dom
.ascii());
822 LOG(INFO
) << "SPF result " << human_result
;
823 dmp
.store_spf(spf_dom
.c_str(), spf_pol
, spf_origin
,
824 human_result
.c_str());
828 LOG(WARNING
) << "invalid domain in helo: " << spf
.kv_map
[helo
];
832 LOG(WARNING
) << "identity checked was helo, but no helo key";
836 LOG(WARNING
) << "unknown identity " << spf
.kv_map
[identity
];
840 LOG(INFO
) << "no explicit tag for which identity was checked";
843 if (spf
.kv_map
.contains(envelope_from
)) {
844 auto const efrom
= spf
.kv_map
[envelope_from
];
846 if (is_postmaster(efrom
)) {
847 if (spf
.kv_map
.contains(helo
)) {
848 if (Domain::validate(spf
.kv_map
[helo
])) {
849 Domain
dom(spf
.kv_map
[helo
]);
850 spf_dom
= dom
.ascii();
851 spf_origin
= DMARC_POLICY_SPF_ORIGIN_HELO
;
853 auto const human_result
= fmt::format(
854 "{}, RFC5321.MailFrom is <>, implicit origin hello, domain {}",
855 spf
.result
, dom
.ascii());
856 LOG(INFO
) << "SPF result " << human_result
;
857 dmp
.store_spf(spf_dom
.c_str(), spf_pol
, spf_origin
,
858 human_result
.c_str());
862 LOG(WARNING
) << "RFC5321.MailFrom is postmaster or <> but helo is "
868 LOG(WARNING
) << "envelope-from is <> but no helo key";
871 else if (Mailbox::validate(efrom
)) {
874 spf_dom
= mbx
.domain().ascii();
875 spf_origin
= DMARC_POLICY_SPF_ORIGIN_MAILFROM
;
877 auto const human_result
= fmt::format(
878 "{}, implicit RFC5321.MailFrom <{}>", spf
.result
, mbx
.as_string());
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
) << "envelope-from invalid mailbox: " << efrom
;
887 else if (spf
.kv_map
.contains(helo
)) {
888 if (Domain::validate(spf
.kv_map
[helo
])) {
889 Domain
dom(spf
.kv_map
[helo
]);
890 spf_dom
= dom
.ascii();
891 spf_origin
= DMARC_POLICY_SPF_ORIGIN_HELO
;
893 auto const human_result
=
894 fmt::format("{}, hello domain {}", spf
.result
, dom
.ascii());
895 LOG(INFO
) << "SPF result " << human_result
;
896 dmp
.store_spf(spf_dom
.c_str(), spf_pol
, spf_origin
, human_result
.c_str());
900 LOG(WARNING
) << "helo is invalid domain:" << spf
.kv_map
[helo
];
905 << "no explicit \"identity\" key, and no envelope-from or helo key";
911 bool mailbox_list_parse(std::string_view input
,
912 mailbox_name_addr_list
& name_addr_list
)
914 name_addr_list
= mailbox_name_addr_list
{};
915 auto in
{memory_input
<>(input
.data(), input
.size(), "mailbox_list_only")};
916 return tao::pegtl::parse
<RFC5322::mailbox_list_only
,
917 RFC5322::mailbox_list_action
>(in
, name_addr_list
);
920 bool authentication_results_parse(std::string_view input
,
921 std::string
& authservid
,
922 std::string
& ar_results
)
924 auto in
{memory_input
<>(input
.data(), input
.size(),
925 "authentication_results_header")};
926 return tao::pegtl::parse
<RFC5322::authres_header_field_only
,
927 RFC5322::ar_action
>(in
, authservid
, ar_results
);
930 bool authentication(message::parsed
& msg
,
932 char const* selector
,
935 LOG(INFO
) << "add_authentication_results";
936 CHECK(!msg
.headers
.empty());
938 // Remove any redundant Authentication-Results headers
940 std::remove_if(msg
.headers
.begin(), msg
.headers
.end(),
941 [sender
](auto const& hdr
) {
942 if (hdr
== Authentication_Results
) {
943 std::string authservid
;
944 std::string ar_results
;
945 if (message::authentication_results_parse(
946 hdr
.as_view(), authservid
, ar_results
)) {
947 return Domain::match(authservid
, sender
);
949 LOG(WARNING
) << "failed to parse " << hdr
.as_string();
955 // Run our message through OpenDKIM verify
957 OpenDKIM::verify dkv
;
958 for (auto const& header
: msg
.headers
) {
959 auto const hv
= header
.as_view();
964 // LOG(INFO) << "body «" << msg.body << "»";
969 OpenDMARC::policy dmp
;
971 // Build up Authentication-Results header
972 fmt::memory_buffer bfr
;
974 std::unordered_set
<Domain
> validated_doms
;
977 for (auto hdr
: msg
.headers
) {
978 if (hdr
== Received_SPF
) {
979 RFC5322::received_spf_parsed spf_parsed
;
980 if (!spf_parsed
.parse(hdr
.value
)) {
981 LOG(WARNING
) << "failed to parse SPF record: " << hdr
.value
;
985 LOG(INFO
) << "SPF record parsed";
986 if (!sender_comment(spf_parsed
.comment
, sender
)) {
987 LOG(INFO
) << "comment == \"" << spf_parsed
.comment
<< "\" not by "
992 if (!Mailbox::validate(spf_parsed
.kv_map
[envelope_from
])) {
993 LOG(WARNING
) << "invalid mailbox: " << spf_parsed
.kv_map
[envelope_from
];
997 if (!Domain::validate(spf_parsed
.kv_map
[helo
])) {
998 LOG(WARNING
) << "invalid helo domain: " << spf_parsed
.kv_map
[helo
];
1002 Mailbox
env_from(spf_parsed
.kv_map
[envelope_from
]);
1003 Domain
helo_dom(spf_parsed
.kv_map
[helo
]);
1005 if (iequal(env_from
.local_part(), "Postmaster") &&
1006 env_from
.domain() == helo_dom
) {
1007 if (validated_doms
.count(helo_dom
) == 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.helo={}",
1013 validated_doms
.emplace(helo_dom
);
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
);
1023 if (validated_doms
.count(env_from
.domain()) == 0) {
1024 fmt::format_to(std::back_inserter(bfr
), ";\r\n\tspf={}",
1026 fmt::format_to(std::back_inserter(bfr
), " {}", spf_parsed
.comment
);
1027 fmt::format_to(std::back_inserter(bfr
), " smtp.mailfrom={}",
1028 env_from
.as_string(Mailbox::domain_encoding::ascii
));
1029 validated_doms
.emplace(env_from
.domain());
1031 if (spf_parsed
.kv_map
.contains(client_ip
)) {
1032 std::string ip
= make_string(spf_parsed
.kv_map
[client_ip
]);
1033 dmp
.connect(ip
.c_str());
1035 spf_result_to_dmarc(dmp
, spf_parsed
);
1041 LOG(INFO
) << "fetching From: header";
1042 // Should be only one From:
1043 if (auto hdr
= std::find(begin(msg
.headers
), end(msg
.headers
), From
);
1044 hdr
!= end(msg
.headers
)) {
1045 auto const from_str
= make_string(hdr
->value
);
1047 if (!mailbox_list_parse(from_str
, msg
.from_parsed
)) {
1048 LOG(WARNING
) << "failed to parse «From:" << from_str
<< "»";
1051 for (auto hdr_next
= std::next(hdr
); hdr_next
!= end(msg
.headers
);
1052 hdr_next
= std::next(hdr_next
)) {
1053 if (*hdr_next
== From
) {
1054 LOG(WARNING
) << "additional RFC5322.From header «"
1055 << hdr_next
->as_string() << "»";
1060 if (msg
.from_parsed
.name_addr_list
.empty()) {
1061 LOG(WARNING
) << "No address in RFC5322.From header";
1066 <https://tools.ietf.org/html/rfc7489#section-6.6>
1067 6.6.1. Extract Author Domain
1069 The case of a syntactically valid multi-valued RFC5322.From field
1070 presents a particular challenge. The process in this case is to
1071 apply the DMARC check using each of those domains found in the
1072 RFC5322.From field as the Author Domain and apply the most strict
1073 policy selected among the checks that fail.
1078 if (msg
.from_parsed
.name_addr_list
.size() > 1) {
1079 LOG(WARNING
) << "More than one address in RFC5322.From header";
1082 auto from_addr
= msg
.from_parsed
.name_addr_list
[0].addr
;
1084 boost::trim(from_addr
);
1086 if (!Mailbox::validate(from_addr
)) {
1087 LOG(WARNING
) << "Mailbox syntax valid for RFC-5322, not for RFC-5321: \""
1088 << from_addr
<< "\"";
1089 // Maybe we can pick out a valid domain?
1093 Mailbox
from_mbx(from_addr
);
1094 msg
.dmarc_from
= from_mbx
.as_string(Mailbox::domain_encoding::ascii
);
1095 msg
.dmarc_from_domain
= from_mbx
.domain().ascii();
1097 LOG(INFO
) << "dmarc_from_domain == " << msg
.dmarc_from_domain
;
1098 dmp
.store_from_domain(msg
.dmarc_from_domain
.c_str());
1100 // Check each DKIM sig, inform DMARC processor, put in AR
1102 dkv
.foreach_sig([&dmp
, &bfr
](char const* domain
, bool passed
,
1103 char const* identity
, char const* sel
,
1105 int const result
= passed
? DMARC_POLICY_DKIM_OUTCOME_PASS
1106 : DMARC_POLICY_DKIM_OUTCOME_FAIL
;
1107 auto const human_result
= (passed
? "pass" : "fail");
1109 LOG(INFO
) << "DKIM check for " << domain
<< " " << human_result
;
1111 dmp
.store_dkim(domain
, sel
, result
, human_result
);
1113 auto bs
= std::string_view(b
, strlen(b
)).substr(0, 8);
1115 fmt::format_to(std::back_inserter(bfr
), ";\r\n\tdkim={}", human_result
);
1116 fmt::format_to(std::back_inserter(bfr
), " header.i={}", identity
);
1117 fmt::format_to(std::back_inserter(bfr
), " header.s={}", sel
);
1118 fmt::format_to(std::back_inserter(bfr
), " header.b=\"{}\"", bs
);
1121 // Set DMARC status in AR
1123 auto const dmarc_passed
= dmp
.query_dmarc(msg
.dmarc_from_domain
.c_str());
1125 auto const dmarc_result
= (dmarc_passed
? "pass" : "fail");
1126 LOG(INFO
) << "DMARC " << dmarc_result
;
1128 fmt::format_to(std::back_inserter(bfr
), ";\r\n\tdmarc={} header.from={}",
1129 dmarc_result
, msg
.dmarc_from_domain
);
1133 OpenARC::verify arv
;
1134 for (auto const& header
: msg
.headers
) {
1135 arv
.header(header
.as_view());
1141 LOG(INFO
) << "ARC status == " << arv
.chain_status_str();
1142 LOG(INFO
) << "ARC custody == " << arv
.chain_custody_str();
1144 auto const arc_status
= arv
.chain_status_str();
1146 fmt::format_to(std::back_inserter(bfr
), ";\r\n\tarc={}", arc_status
);
1148 // New AR header on the top
1150 auto const ar_results
= [&bfr
]() {
1151 // Ug, OpenARC adds an extra one, arc.c:3213
1152 auto s
= fmt::to_string(bfr
);
1153 if (s
.length() && s
[0] == ';')
1159 fmt::format("{}: {};{}", Authentication_Results
, sender
, ar_results
);
1161 LOG(INFO
) << "new AR header «" << esc(msg
.ar_str
, esc_line_option::multi
)
1164 CHECK(msg
.parse_hdr(msg
.ar_str
));
1166 // Run our message through ARC::sign
1170 if (iequal(arc_status
, "none")) {
1173 else if (iequal(arc_status
, "fail")) {
1176 else if (iequal(arc_status
, "pass")) {
1183 for (auto const& header
: msg
.headers
) {
1184 ars
.header(header
.as_view());
1190 boost::iostreams::mapped_file_source priv
;
1191 priv
.open(key_file
);
1193 if (ars
.seal(sender
, selector
, sender
, priv
.data(), priv
.size(),
1194 ar_results
.c_str())) {
1195 msg
.arc_hdrs
= ars
.whole_seal();
1196 for (auto const& hdr
: msg
.arc_hdrs
) {
1197 CHECK(msg
.parse_hdr(hdr
));
1201 LOG(INFO
) << "failed to generate seal";
1204 OpenARC::verify arv2
;
1205 for (auto const& header
: msg
.headers
) {
1206 arv2
.header(header
.as_view());
1209 arv2
.body(msg
.body
);
1212 LOG(INFO
) << "check ARC status == " << arv2
.chain_status_str();
1213 LOG(INFO
) << "check ARC custody == " << arv2
.chain_custody_str();
1215 return dmarc_passed
;
1218 void print_spf_envelope_froms(char const* file
, message::parsed
& msg
)
1220 CHECK(!msg
.headers
.empty());
1221 for (auto const& hdr
: msg
.headers
) {
1222 if (hdr
== Received_SPF
) {
1223 RFC5322::received_spf_parsed spf_parsed
;
1224 if (spf_parsed
.parse(hdr
.value
)) {
1225 std::cout
<< spf_parsed
.kv_map
[envelope_from
] << '\n';
1229 LOG(WARNING
) << "failed to parse " << file
<< ":\n" << hdr
.as_string();
1235 void remove_delivery_headers(message::parsed
& msg
)
1237 // Remove headers that are added by the "delivery agent"
1238 // aka (Session::added_headers_)
1240 std::remove(msg
.headers
.begin(), msg
.headers
.end(), Return_Path
),
1243 // just in case, but right now this header should not exist.
1245 std::remove(msg
.headers
.begin(), msg
.headers
.end(), Delivered_To
),
1249 void dkim_check(message::parsed
& msg
, char const* domain
)
1251 LOG(INFO
) << "dkim";
1253 CHECK(!msg
.body
.empty());
1255 OpenDKIM::verify dkv
;
1257 // Run our message through OpenDKIM verify
1259 for (auto const& header
: msg
.headers
) {
1260 auto const hv
= header
.as_view();
1267 // Check each DKIM sig, inform DMARC processor, put in AR
1269 dkv
.foreach_sig([](char const* domain
, bool passed
, char const* identity
,
1270 char const* sel
, char const* b
) {
1271 auto const human_result
= (passed
? "pass" : "fail");
1273 auto bs
= std::string_view(b
, strlen(b
)).substr(0, 8);
1275 LOG(INFO
) << "DKIM check bfor " << domain
<< " " << human_result
;
1276 LOG(INFO
) << " header.i=" << identity
;
1277 LOG(INFO
) << " header.s=" << sel
;
1278 LOG(INFO
) << " header.b=\"" << bs
<< "\"";
1282 //.............................................................................
1284 bool parsed::parse(std::string_view input
)
1286 auto in
{memory_input
<>(input
.data(), input
.size(), "message")};
1287 return tao::pegtl::parse
<RFC5322::message
, RFC5322::msg_action
>(in
, *this);
1290 bool parsed::parse_hdr(std::string_view input
)
1292 auto in
{memory_input
<>(input
.data(), input
.size(), "message")};
1293 if (tao::pegtl::parse
<RFC5322::raw_field
, RFC5322::msg_action
>(in
, *this)) {
1294 std::rotate(headers
.rbegin(), headers
.rbegin() + 1, headers
.rend());
1300 std::string
parsed::as_string() const
1302 fmt::memory_buffer bfr
;
1304 for (auto const& h
: headers
)
1305 fmt::format_to(std::back_inserter(bfr
), "{}\r\n", h
.as_string());
1308 fmt::format_to(std::back_inserter(bfr
), "\r\n{}", body
);
1310 return fmt::to_string(bfr
);
1313 bool parsed::write(std::ostream
& os
) const
1315 for (auto const& h
: headers
)
1316 os
<< h
.as_string() << "\r\n";
1319 os
<< "\r\n" << body
;
1324 std::string
header::as_string() const
1326 return fmt::format("{}:{}", name
, value
);
1329 std::string_view
parsed::get_header(std::string_view name
) const
1331 if (auto hdr
= std::find(begin(headers
), end(headers
), name
);
1332 hdr
!= end(headers
)) {
1333 return trim(hdr
->value
);
1338 void dkim_sign(message::parsed
& msg
,
1340 char const* selector
,
1343 CHECK(msg
.sig_str
.empty());
1345 boost::iostreams::mapped_file_source priv
;
1346 priv
.open(key_file
);
1348 auto const key_str
= std::string(priv
.data(), priv
.size());
1350 // Run our message through DKIM::sign
1351 OpenDKIM::sign
dks(key_str
.c_str(), // textual data
1352 selector
, sender
, OpenDKIM::sign::body_type::text
);
1353 for (auto const& header
: msg
.headers
) {
1354 dks
.header(header
.as_view());
1360 auto const sig
= dks
.getsighdr();
1362 msg
.sig_str
= fmt::format("DKIM-Signature: {}", sig
);
1363 CHECK(msg
.parse_hdr(msg
.sig_str
));
1366 void rewrite_from_to(message::parsed
& msg
,
1367 std::string mail_from
,
1368 std::string reply_to
,
1370 char const* selector
,
1373 LOG(INFO
) << "rewrite_from_to";
1375 remove_delivery_headers(msg
);
1377 if (!mail_from
.empty()) {
1378 msg
.headers
.erase(std::remove(msg
.headers
.begin(), msg
.headers
.end(), From
),
1381 msg
.from_str
= mail_from
;
1382 CHECK(msg
.parse_hdr(msg
.from_str
));
1385 if (!reply_to
.empty()) {
1387 std::remove(msg
.headers
.begin(), msg
.headers
.end(), Reply_To
),
1390 msg
.reply_to_str
= reply_to
;
1391 CHECK(msg
.parse_hdr(msg
.reply_to_str
));
1394 // modify plain text body
1397 if (iequal(msg.get_header(MIME_Version), "1.0") &&
1398 istarts_with(msg.get_header(Content_Type), "text/plain;")) {
1399 LOG(INFO) << "Adding footer to message body.";
1400 msg.body_str = msg.body;
1401 msg.body_str.append("\r\n\r\n\t-- Added Footer --\r\n");
1402 msg.body = msg.body_str;
1405 LOG(INFO) << "Not adding footer to message body.";
1406 LOG(INFO) << "MIME-Version == " << msg.get_header(MIME_Version);
1407 LOG(INFO) << "Content-Type == " << msg.get_header(Content_Type);
1409 // LOG(INFO) << "body == " << msg.body;
1412 dkim_sign(msg
, sender
, selector
, key_file
);
1415 } // namespace message