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
, one
<':'>, field_value
, eol
> {};
125 struct raw_field
: seq
<field_name
, one
<':'>, 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 local_part
: sor
<dot_atom
, quoted_string
> {};
277 struct dtext
: ranges
<33, 90, 94, 126> {};
279 struct domain_literal
: seq
<opt
<CFWS
>,
281 star
<seq
<opt
<FWS
>, dtext
>>,
286 struct domain
: sor
<dot_atom
, domain_literal
> {};
288 // This addr_spec should be exactly the same as RFC5321 Mailbox, but it's not.
290 struct addr_spec
: seq
<local_part
, one
<'@'>, domain
> {};
292 struct addr_spec_only
: seq
<addr_spec
, eof
> {};
294 struct result
: sor
<TAO_PEGTL_ISTRING("Pass"),
295 TAO_PEGTL_ISTRING("Fail"),
296 TAO_PEGTL_ISTRING("SoftFail"),
297 TAO_PEGTL_ISTRING("Neutral"),
298 TAO_PEGTL_ISTRING("None"),
299 TAO_PEGTL_ISTRING("TempError"),
300 TAO_PEGTL_ISTRING("PermError")> {};
302 struct spf_key
: sor
<TAO_PEGTL_ISTRING("client-ip"),
303 TAO_PEGTL_ISTRING("envelope-from"),
304 TAO_PEGTL_ISTRING("helo"),
305 TAO_PEGTL_ISTRING("problem"),
306 TAO_PEGTL_ISTRING("receiver"),
307 TAO_PEGTL_ISTRING("identity"),
308 TAO_PEGTL_ISTRING("mechanism")> {};
310 // This value syntax (allowing addr_spec) is not in accordance with RFC
311 // 7208 (or 4408) but is what is effectivly used by libspf2 1.2.10 and
314 struct spf_value
: sor
<ip
, addr_spec
, dot_atom
, quoted_string
> {};
316 struct spf_kv_pair
: seq
<spf_key
, opt
<CFWS
>, one
<'='>, spf_value
> {};
318 struct spf_kv_list
: seq
<spf_kv_pair
,
319 star
<seq
<one
<';'>, opt
<CFWS
>, spf_kv_pair
>>,
322 struct spf_header
: seq
<opt
<CFWS
>,
324 opt
<seq
<FWS
, comment
>>,
325 opt
<seq
<FWS
, spf_kv_list
>>> {};
327 struct spf_header_only
: seq
<spf_header
, eof
> {};
329 //.............................................................................
331 struct display_name
: phrase
{};
333 struct angle_addr
: seq
<opt
<CFWS
>, one
<'<'>, addr_spec
, one
<'>'>, opt
<CFWS
>> {};
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
<one
<','>, opt
<sor
<mailbox
, CFWS
>>>
344 struct mailbox_list
: sor
<list
<mailbox
, one
<','>>,
348 // struct from : seq<TAO_PEGTL_ISTRING("From:"),
352 struct mailbox_list_only
: seq
<mailbox_list
, eof
> {};
354 //.............................................................................
356 // <https://www.rfc-editor.org/rfc/rfc2045.html>
358 // tspecials := "(" / ")" / "<" / ">" / "@" /
359 // "," / ";" / ":" / "\" / <">
360 // "/" / "[" / "]" / "?" / "="
362 // token := 1*<any (US-ASCII) CHAR except SPACE, CTLs,
377 struct tchar45
: ranges
< // NUL..' '
386 48, 57, // 0123456789
387 // 58, 64, // ;:<=>?@
390 94, 126 // ^_` a..z {|}~
394 struct token45
: plus
<tchar45
> {};
396 //.............................................................................
398 // <https://tools.ietf.org/html/rfc8601#section-2.2>
400 struct value
: sor
<token45
, quoted_string
> {};
402 struct authserv_id
: value
{};
404 struct authres_version
: seq
<plus
<DIGIT
>, opt
<CFWS
>> {};
406 struct no_result
: seq
<opt
<CFWS
>, one
<';'>, opt
<CFWS
>, TAO_PEGTL_ISTRING("none")> {};
408 struct let_dig
: sor
<ALPHA
, DIGIT
> {};
410 struct ldh_tail
: star
<sor
<seq
<plus
<one
<'-'>>, let_dig
>, let_dig
>> {};
412 struct ldh_str
: seq
<let_dig
, ldh_tail
> {};
414 struct keyword
: ldh_str
{};
416 struct method_version
: seq
<plus
<DIGIT
>, opt
<CFWS
>> {};
418 // method = Keyword [ [CFWS] "/" [CFWS] method-version ]
420 struct method
: seq
<keyword
, opt
<opt
<CFWS
>, one
<'/'>, opt
<CFWS
>, method_version
>> {};
422 // methodspec = [CFWS] method [CFWS] "=" [CFWS] result
423 // ; indicates which authentication method was evaluated
424 // ; and what its output was
426 struct methodspec
: seq
<opt
<CFWS
>, method
, opt
<CFWS
>, one
<'='>, opt
<CFWS
>, result
> {};
428 // reasonspec = "reason" [CFWS] "=" [CFWS] value
429 // ; a free-form comment on the reason the given result
432 struct reasonspec
: seq
<TAO_PEGTL_ISTRING("reason"), opt
<CFWS
>, one
<'='>, opt
<CFWS
>, value
> {};
434 // pvalue = [CFWS] ( value / [ [ local-part ] "@" ] domain-name )
437 struct pvalue
: seq
<opt
<CFWS
>, sor
<seq
<opt
<seq
<opt
<local_part
>, one
<'@'>>>, domain
>,
441 struct ptype
: keyword
{};
443 struct special_smtp_verb
: sor
<TAO_PEGTL_ISTRING("mailfrom"),
444 TAO_PEGTL_ISTRING("rcptto")> {};
446 struct property
: sor
<special_smtp_verb
, keyword
> {};
448 // propspec = ptype [CFWS] "." [CFWS] property [CFWS] "=" pvalue
449 // ; an indication of which properties of the message
450 // ; were evaluated by the authentication scheme being
451 // ; applied to yield the reported result
453 struct propspec
: seq
<ptype
, opt
<CFWS
>, one
<'.'>, opt
<CFWS
>, property
, opt
<CFWS
>, one
<'='>, pvalue
> {};
455 struct resinfo
: seq
<opt
<CFWS
>, one
<';'>, methodspec
, opt
<seq
<CFWS
, reasonspec
>>,
456 opt
<seq
<CFWS
, plus
<propspec
>>>
459 struct ar_results
: sor
<no_result
, plus
<resinfo
>> {};
461 struct authres_payload
: seq
<opt
<CFWS
>, authserv_id
,
462 opt
<seq
<CFWS
, authres_version
>>,
466 struct authres_header_field
: seq
<TAO_PEGTL_ISTRING("Authentication-Results:"),
469 struct authres_header_field_only
: seq
<authres_header_field
, eof
> {};
471 //.............................................................................
475 template <typename Rule
>
476 struct ar_action
: nothing
<Rule
> {
480 struct ar_action
<ar_results
> {
481 template <typename Input
>
483 apply(Input
const& in
, std::string
& authservid
, std::string
& ar_results
)
485 ar_results
= in
.string();
490 struct ar_action
<authserv_id
> {
491 template <typename Input
>
493 apply(Input
const& in
, std::string
& authservid
, std::string
& ar_results
)
495 authservid
= in
.string();
499 //.............................................................................
501 template <typename Rule
>
502 struct msg_action
: nothing
<Rule
> {
506 struct msg_action
<field_name
> {
507 template <typename Input
>
508 static void apply(Input
const& in
, ::message::parsed
& msg
)
510 msg
.field_name
= make_view(in
);
515 struct msg_action
<field_value
> {
516 template <typename Input
>
517 static void apply(Input
const& in
, ::message::parsed
& msg
)
519 msg
.field_value
= make_view(in
);
524 struct msg_action
<field
> {
525 template <typename Input
>
526 static void apply(Input
const& in
, ::message::parsed
& msg
)
528 msg
.headers
.emplace_back(
529 ::message::header(msg
.field_name
, msg
.field_value
));
534 struct msg_action
<raw_field
> {
535 template <typename Input
>
536 static void apply(Input
const& in
, ::message::parsed
& msg
)
538 msg
.headers
.emplace_back(
539 ::message::header(msg
.field_name
, msg
.field_value
));
544 struct msg_action
<body
> {
545 template <typename Input
>
546 static void apply(Input
const& in
, ::message::parsed
& msg
)
548 msg
.body
= make_view(in
);
552 //.............................................................................
554 struct received_spf_parsed
{
555 bool parse(std::string_view input
);
557 std::string_view whole_thing
;
559 std::string_view result
;
560 std::string_view comment
;
562 std::string_view key
;
563 std::string_view value
;
565 std::vector
<std::pair
<std::string_view
, std::string_view
>> kv_list
;
566 std::map
<std::string_view
, std::string_view
, ci_less
> kv_map
;
568 std::string
as_string() const { return fmt::format("{}", whole_thing
); }
571 template <typename Rule
>
572 struct spf_action
: nothing
<Rule
> {
576 struct spf_action
<result
> {
577 template <typename Input
>
578 static void apply(const Input
& in
, received_spf_parsed
& spf
)
580 spf
.result
= make_view(in
);
585 struct spf_action
<comment
> {
586 template <typename Input
>
587 static void apply(const Input
& in
, received_spf_parsed
& spf
)
589 spf
.comment
= make_view(in
);
594 struct spf_action
<spf_key
> {
595 template <typename Input
>
596 static void apply(const Input
& in
, received_spf_parsed
& spf
)
598 spf
.key
= make_view(in
);
603 struct spf_action
<spf_value
> {
604 template <typename Input
>
605 static void apply(const Input
& in
, received_spf_parsed
& spf
)
607 // RFC5322 syntax is full of optional WS, so we trim
608 spf
.value
= trim(make_view(in
));
613 struct spf_action
<spf_kv_pair
> {
614 template <typename Input
>
615 static void apply(const Input
& in
, received_spf_parsed
& spf
)
617 spf
.kv_list
.emplace_back(spf
.key
, spf
.value
);
618 spf
.key
= spf
.value
= "";
623 struct spf_action
<spf_kv_list
> {
624 static void apply0(received_spf_parsed
& spf
)
626 for (auto const& kvp
: spf
.kv_list
) {
627 if (spf
.kv_map
.contains(kvp
.first
)) {
628 LOG(WARNING
) << "dup key: " << kvp
.first
<< "=" << kvp
.second
;
629 LOG(WARNING
) << " and: " << kvp
.first
<< "="
630 << spf
.kv_map
[kvp
.first
];
632 spf
.kv_map
[kvp
.first
] = kvp
.second
;
637 bool received_spf_parsed::parse(std::string_view input
)
640 auto in
{memory_input
<>(input
.data(), input
.size(), "spf_header")};
641 return tao::pegtl::parse
<spf_header_only
, spf_action
>(in
, *this);
644 //.............................................................................
646 // Parse a grammar and extract each addr_spec
648 template <typename Rule
>
649 struct mailbox_list_action
: nothing
<Rule
> {
653 struct mailbox_list_action
<display_name
> {
654 template <typename Input
>
655 static void apply(Input
const& in
,
656 ::message::mailbox_name_addr_list
& from_parsed
)
658 from_parsed
.name
= in
.string();
663 struct mailbox_list_action
<addr_spec
> {
664 template <typename Input
>
665 static void apply(Input
const& in
,
666 ::message::mailbox_name_addr_list
& from_parsed
)
668 from_parsed
.name_addr_list
.push_back({from_parsed
.name
, in
.string()});
669 from_parsed
.name
.clear();
673 } // namespace RFC5322
675 // Map SPF result string to DMARC policy code.
677 static int result_to_pol(std::string_view result
)
680 if (iequal(result
, Pass
)) return DMARC_POLICY_SPF_OUTCOME_PASS
;
681 if (iequal(result
, Fail
)) return DMARC_POLICY_SPF_OUTCOME_FAIL
;
682 if (iequal(result
, SoftFail
)) return DMARC_POLICY_SPF_OUTCOME_TMPFAIL
;
683 if (iequal(result
, Neutral
)) return DMARC_POLICY_SPF_OUTCOME_NONE
;
684 if (iequal(result
, None
)) return DMARC_POLICY_SPF_OUTCOME_NONE
;
685 if (iequal(result
, TempError
)) return DMARC_POLICY_SPF_OUTCOME_NONE
;
686 if (iequal(result
, PermError
)) return DMARC_POLICY_SPF_OUTCOME_NONE
;
687 LOG(WARNING
) << "unknown SPF result: \"" << result
<< "\"";
688 return DMARC_POLICY_SPF_OUTCOME_NONE
;
692 static bool is_postmaster(std::string_view from
)
694 return from
== "<>" || istarts_with(from
, "<Postmaster@");
697 static bool sender_comment(std::string_view comment
, std::string_view sender
)
699 auto const prefix
= fmt::format("({}:", sender
);
700 return istarts_with(comment
, prefix
);
703 static void spf_result_to_dmarc(OpenDMARC::policy
& dmp
,
704 RFC5322::received_spf_parsed
& spf
)
706 LOG(INFO
) << "spf_result_to_dmarc";
708 if (spf
.kv_map
.contains(problem
)) {
709 LOG(WARNING
) << "SPF problem: " << spf
.kv_map
[problem
];
712 auto const spf_pol
= result_to_pol(spf
.result
);
714 if (spf_pol
== DMARC_POLICY_SPF_OUTCOME_NONE
) {
715 LOG(WARNING
) << "Ignoring for DMARC purposes: " << spf
.as_string();
723 if (spf
.kv_map
.contains(identity
)) {
724 if (iequal(spf
.kv_map
[identity
], mailfrom
)) {
725 if (spf
.kv_map
.contains(envelope_from
)) {
726 if (Mailbox::validate(spf
.kv_map
[envelope_from
])) {
727 Mailbox
mbx(spf
.kv_map
[envelope_from
]);
728 spf_dom
= mbx
.domain().ascii();
729 spf_origin
= DMARC_POLICY_SPF_ORIGIN_MAILFROM
;
731 auto const human_result
=
732 fmt::format("{}, explicit origin mail from, mailbox {}",
733 spf
.result
, mbx
.as_string());
734 LOG(INFO
) << "SPF result " << human_result
;
735 dmp
.store_spf(spf_dom
.c_str(), spf_pol
, spf_origin
,
736 human_result
.c_str());
740 LOG(WARNING
) << "invalid mailbox in envelope-from: "
741 << spf
.kv_map
[envelope_from
];
746 << "identity checked was mail from, but no envelope_from key";
749 else if (iequal(spf
.kv_map
[identity
], helo
)) {
750 if (spf
.kv_map
.contains(helo
)) {
751 if (Domain::validate(spf
.kv_map
[helo
])) {
752 Domain
dom(spf
.kv_map
[helo
]);
753 spf_dom
= dom
.ascii();
754 spf_origin
= DMARC_POLICY_SPF_ORIGIN_HELO
;
756 auto const human_result
= fmt::format(
757 "{}, explicit origin hello, domain {}", spf
.result
, dom
.ascii());
758 LOG(INFO
) << "SPF result " << human_result
;
759 dmp
.store_spf(spf_dom
.c_str(), spf_pol
, spf_origin
,
760 human_result
.c_str());
764 LOG(WARNING
) << "invalid domain in helo: " << spf
.kv_map
[helo
];
768 LOG(WARNING
) << "identity checked was helo, but no helo key";
772 LOG(WARNING
) << "unknown identity " << spf
.kv_map
[identity
];
776 LOG(INFO
) << "no explicit tag for which identity was checked";
779 if (spf
.kv_map
.contains(envelope_from
)) {
780 auto const efrom
= spf
.kv_map
[envelope_from
];
782 if (is_postmaster(efrom
)) {
783 if (spf
.kv_map
.contains(helo
)) {
784 if (Domain::validate(spf
.kv_map
[helo
])) {
785 Domain
dom(spf
.kv_map
[helo
]);
786 spf_dom
= dom
.ascii();
787 spf_origin
= DMARC_POLICY_SPF_ORIGIN_HELO
;
789 auto const human_result
= fmt::format(
790 "{}, RFC5321.MailFrom is <>, implicit origin hello, domain {}",
791 spf
.result
, dom
.ascii());
792 LOG(INFO
) << "SPF result " << human_result
;
793 dmp
.store_spf(spf_dom
.c_str(), spf_pol
, spf_origin
,
794 human_result
.c_str());
798 LOG(WARNING
) << "RFC5321.MailFrom is postmaster or <> but helo is "
804 LOG(WARNING
) << "envelope-from is <> but no helo key";
807 else if (Mailbox::validate(efrom
)) {
810 spf_dom
= mbx
.domain().ascii();
811 spf_origin
= DMARC_POLICY_SPF_ORIGIN_MAILFROM
;
813 auto const human_result
= fmt::format(
814 "{}, implicit RFC5321.MailFrom <{}>", spf
.result
, mbx
.as_string());
815 LOG(INFO
) << "SPF result " << human_result
;
816 dmp
.store_spf(spf_dom
.c_str(), spf_pol
, spf_origin
, human_result
.c_str());
820 LOG(WARNING
) << "envelope-from invalid mailbox: " << efrom
;
823 else if (spf
.kv_map
.contains(helo
)) {
824 if (Domain::validate(spf
.kv_map
[helo
])) {
825 Domain
dom(spf
.kv_map
[helo
]);
826 spf_dom
= dom
.ascii();
827 spf_origin
= DMARC_POLICY_SPF_ORIGIN_HELO
;
829 auto const human_result
=
830 fmt::format("{}, hello domain {}", spf
.result
, dom
.ascii());
831 LOG(INFO
) << "SPF result " << human_result
;
832 dmp
.store_spf(spf_dom
.c_str(), spf_pol
, spf_origin
, human_result
.c_str());
836 LOG(WARNING
) << "helo is invalid domain:" << spf
.kv_map
[helo
];
841 << "no explicit \"identity\" key, and no envelope-from or helo key";
847 bool authentication_results_parse(std::string_view input
,
848 std::string
& authservid
,
849 std::string
& ar_results
)
851 auto in
{memory_input
<>(input
.data(), input
.size(),
852 "authentication_results_header")};
853 return tao::pegtl::parse
<RFC5322::authres_header_field_only
,
854 RFC5322::ar_action
>(in
, authservid
, ar_results
);
857 bool authentication(message::parsed
& msg
,
859 char const* selector
,
862 LOG(INFO
) << "add_authentication_results";
863 CHECK(!msg
.headers
.empty());
865 // Remove any redundant Authentication-Results headers
867 std::remove_if(msg
.headers
.begin(), msg
.headers
.end(),
868 [sender
](auto const& hdr
) {
869 if (hdr
== Authentication_Results
) {
870 std::string authservid
;
871 std::string ar_results
;
872 if (message::authentication_results_parse(
873 hdr
.as_view(), authservid
, ar_results
)) {
874 return Domain::match(authservid
, sender
);
876 LOG(WARNING
) << "failed to parse " << hdr
.as_string();
882 // Run our message through OpenDKIM verify
884 OpenDKIM::verify dkv
;
885 for (auto const& header
: msg
.headers
) {
886 auto const hv
= header
.as_view();
891 // LOG(INFO) << "body «" << msg.body << "»";
896 OpenDMARC::policy dmp
;
898 // Build up Authentication-Results header
899 fmt::memory_buffer bfr
;
901 std::unordered_set
<Domain
> validated_doms
;
904 for (auto hdr
: msg
.headers
) {
905 if (hdr
== Received_SPF
) {
906 RFC5322::received_spf_parsed spf_parsed
;
907 if (!spf_parsed
.parse(hdr
.value
)) {
908 LOG(WARNING
) << "failed to parse SPF record: " << hdr
.value
;
912 LOG(INFO
) << "SPF record parsed";
913 if (!sender_comment(spf_parsed
.comment
, sender
)) {
914 LOG(INFO
) << "comment == \"" << spf_parsed
.comment
<< "\" not by "
919 if (!Mailbox::validate(spf_parsed
.kv_map
[envelope_from
])) {
920 LOG(WARNING
) << "invalid mailbox: " << spf_parsed
.kv_map
[envelope_from
];
924 if (!Domain::validate(spf_parsed
.kv_map
[helo
])) {
925 LOG(WARNING
) << "invalid helo domain: " << spf_parsed
.kv_map
[helo
];
929 Mailbox
env_from(spf_parsed
.kv_map
[envelope_from
]);
930 Domain
helo_dom(spf_parsed
.kv_map
[helo
]);
932 if (iequal(env_from
.local_part(), "Postmaster") &&
933 env_from
.domain() == helo_dom
) {
934 if (validated_doms
.count(helo_dom
) == 0) {
935 fmt::format_to(std::back_inserter(bfr
), ";\r\n\tspf={}", spf_parsed
.result
);
936 fmt::format_to(std::back_inserter(bfr
), " {}", spf_parsed
.comment
);
937 fmt::format_to(std::back_inserter(bfr
), " smtp.helo={}", helo_dom
.ascii());
938 validated_doms
.emplace(helo_dom
);
940 if (spf_parsed
.kv_map
.contains(client_ip
)) {
941 std::string ip
= make_string(spf_parsed
.kv_map
[client_ip
]);
942 dmp
.connect(ip
.c_str());
944 spf_result_to_dmarc(dmp
, spf_parsed
);
948 if (validated_doms
.count(env_from
.domain()) == 0) {
949 fmt::format_to(std::back_inserter(bfr
), ";\r\n\tspf={}", spf_parsed
.result
);
950 fmt::format_to(std::back_inserter(bfr
), " {}", spf_parsed
.comment
);
951 fmt::format_to(std::back_inserter(bfr
), " smtp.mailfrom={}",
952 env_from
.as_string(Mailbox::domain_encoding::ascii
));
953 validated_doms
.emplace(env_from
.domain());
955 if (spf_parsed
.kv_map
.contains(client_ip
)) {
956 std::string ip
= make_string(spf_parsed
.kv_map
[client_ip
]);
957 dmp
.connect(ip
.c_str());
959 spf_result_to_dmarc(dmp
, spf_parsed
);
965 LOG(INFO
) << "fetching From: header";
966 // Should be only one From:
967 if (auto hdr
= std::find(begin(msg
.headers
), end(msg
.headers
), From
);
968 hdr
!= end(msg
.headers
)) {
969 auto const from_str
= make_string(hdr
->value
);
971 memory_input
<> from_in(from_str
, "from");
972 if (!parse
<RFC5322::mailbox_list_only
, RFC5322::mailbox_list_action
>(
973 from_in
, msg
.from_parsed
)) {
974 LOG(WARNING
) << "failed to parse From:" << from_str
;
977 for (auto hdr_next
= std::next(hdr
); hdr_next
!= end(msg
.headers
);
978 hdr_next
= std::next(hdr_next
)) {
979 if (*hdr_next
== From
) {
980 LOG(WARNING
) << "additional RFC5322.From header found: "
981 << hdr_next
->as_string();
986 if (msg
.from_parsed
.name_addr_list
.empty()) {
987 LOG(WARNING
) << "No address in RFC5322.From header";
992 <https://tools.ietf.org/html/rfc7489#section-6.6>
993 6.6.1. Extract Author Domain
995 The case of a syntactically valid multi-valued RFC5322.From field
996 presents a particular challenge. The process in this case is to
997 apply the DMARC check using each of those domains found in the
998 RFC5322.From field as the Author Domain and apply the most strict
999 policy selected among the checks that fail.
1004 if (msg
.from_parsed
.name_addr_list
.size() > 1) {
1005 LOG(WARNING
) << "More than one address in RFC5322.From header";
1008 auto from_addr
= msg
.from_parsed
.name_addr_list
[0].addr
;
1010 boost::trim(from_addr
);
1012 if (!Mailbox::validate(from_addr
)) {
1013 LOG(WARNING
) << "Mailbox syntax valid for RFC-5322, not for RFC-5321: \""
1014 << from_addr
<< "\"";
1015 // Maybe we can pick out a valid domain?
1019 Mailbox
from_mbx(from_addr
);
1020 msg
.dmarc_from
= from_mbx
.as_string(Mailbox::domain_encoding::ascii
);
1021 msg
.dmarc_from_domain
= from_mbx
.domain().ascii();
1023 LOG(INFO
) << "dmarc_from_domain == " << msg
.dmarc_from_domain
;
1024 dmp
.store_from_domain(msg
.dmarc_from_domain
.c_str());
1026 // Check each DKIM sig, inform DMARC processor, put in AR
1028 dkv
.foreach_sig([&dmp
, &bfr
](char const* domain
, bool passed
,
1029 char const* identity
, char const* sel
,
1031 int const result
= passed
? DMARC_POLICY_DKIM_OUTCOME_PASS
1032 : DMARC_POLICY_DKIM_OUTCOME_FAIL
;
1033 auto const human_result
= (passed
? "pass" : "fail");
1035 LOG(INFO
) << "DKIM check for " << domain
<< " " << human_result
;
1037 dmp
.store_dkim(domain
, sel
, result
, human_result
);
1039 auto bs
= std::string_view(b
, strlen(b
)).substr(0, 8);
1041 fmt::format_to(std::back_inserter(bfr
), ";\r\n\tdkim={}", human_result
);
1042 fmt::format_to(std::back_inserter(bfr
), " header.i={}", identity
);
1043 fmt::format_to(std::back_inserter(bfr
), " header.s={}", sel
);
1044 fmt::format_to(std::back_inserter(bfr
), " header.b=\"{}\"", bs
);
1047 // Set DMARC status in AR
1049 auto const dmarc_passed
= dmp
.query_dmarc(msg
.dmarc_from_domain
.c_str());
1051 auto const dmarc_result
= (dmarc_passed
? "pass" : "fail");
1052 LOG(INFO
) << "DMARC " << dmarc_result
;
1054 fmt::format_to(std::back_inserter(bfr
), ";\r\n\tdmarc={} header.from={}", dmarc_result
,
1055 msg
.dmarc_from_domain
);
1059 OpenARC::verify arv
;
1060 for (auto const& header
: msg
.headers
) {
1061 arv
.header(header
.as_view());
1067 LOG(INFO
) << "ARC status == " << arv
.chain_status_str();
1068 LOG(INFO
) << "ARC custody == " << arv
.chain_custody_str();
1070 auto const arc_status
= arv
.chain_status_str();
1072 fmt::format_to(std::back_inserter(bfr
), ";\r\n\tarc={}", arc_status
);
1074 // New AR header on the top
1076 auto const ar_results
= [&bfr
]() {
1077 // Ug, OpenARC adds an extra one, arc.c:3213
1078 auto s
= fmt::to_string(bfr
);
1079 if (s
.length() && s
[0] == ';')
1085 fmt::format("{}: {};{}", Authentication_Results
, sender
, ar_results
);
1087 LOG(INFO
) << "new AR header «" << esc(msg
.ar_str
, esc_line_option::multi
)
1090 CHECK(msg
.parse_hdr(msg
.ar_str
));
1092 // Run our message through ARC::sign
1096 if (iequal(arc_status
, "none")) {
1099 else if (iequal(arc_status
, "fail")) {
1102 else if (iequal(arc_status
, "pass")) {
1109 for (auto const& header
: msg
.headers
) {
1110 ars
.header(header
.as_view());
1116 boost::iostreams::mapped_file_source priv
;
1117 priv
.open(key_file
);
1119 if (ars
.seal(sender
, selector
, sender
, priv
.data(), priv
.size(),
1120 ar_results
.c_str())) {
1121 msg
.arc_hdrs
= ars
.whole_seal();
1122 for (auto const& hdr
: msg
.arc_hdrs
) {
1123 CHECK(msg
.parse_hdr(hdr
));
1127 LOG(INFO
) << "failed to generate seal";
1130 OpenARC::verify arv2
;
1131 for (auto const& header
: msg
.headers
) {
1132 arv2
.header(header
.as_view());
1135 arv2
.body(msg
.body
);
1138 LOG(INFO
) << "check ARC status == " << arv2
.chain_status_str();
1139 LOG(INFO
) << "check ARC custody == " << arv2
.chain_custody_str();
1141 return dmarc_passed
;
1144 void print_spf_envelope_froms(char const* file
, message::parsed
& msg
)
1146 CHECK(!msg
.headers
.empty());
1147 for (auto const& hdr
: msg
.headers
) {
1148 if (hdr
== Received_SPF
) {
1149 RFC5322::received_spf_parsed spf_parsed
;
1150 if (spf_parsed
.parse(hdr
.value
)) {
1151 std::cout
<< spf_parsed
.kv_map
[envelope_from
] << '\n';
1155 LOG(WARNING
) << "failed to parse " << file
<< ":\n" << hdr
.as_string();
1161 void remove_delivery_headers(message::parsed
& msg
)
1163 // Remove headers that are added by the "delivery agent"
1164 // aka (Session::added_headers_)
1166 std::remove(msg
.headers
.begin(), msg
.headers
.end(), Return_Path
),
1169 // just in case, but right now this header should not exist.
1171 std::remove(msg
.headers
.begin(), msg
.headers
.end(), Delivered_To
),
1175 void dkim_check(message::parsed
& msg
, char const* domain
)
1177 LOG(INFO
) << "dkim";
1179 CHECK(!msg
.body
.empty());
1181 OpenDKIM::verify dkv
;
1183 // Run our message through OpenDKIM verify
1185 for (auto const& header
: msg
.headers
) {
1186 auto const hv
= header
.as_view();
1193 // Check each DKIM sig, inform DMARC processor, put in AR
1195 dkv
.foreach_sig([](char const* domain
, bool passed
, char const* identity
,
1196 char const* sel
, char const* b
) {
1197 auto const human_result
= (passed
? "pass" : "fail");
1199 auto bs
= std::string_view(b
, strlen(b
)).substr(0, 8);
1201 LOG(INFO
) << "DKIM check bfor " << domain
<< " " << human_result
;
1202 LOG(INFO
) << " header.i=" << identity
;
1203 LOG(INFO
) << " header.s=" << sel
;
1204 LOG(INFO
) << " header.b=\"" << bs
<< "\"";
1208 //.............................................................................
1210 bool parsed::parse(std::string_view input
)
1212 auto in
{memory_input
<>(input
.data(), input
.size(), "message")};
1213 return tao::pegtl::parse
<RFC5322::message
, RFC5322::msg_action
>(in
, *this);
1216 bool parsed::parse_hdr(std::string_view input
)
1218 auto in
{memory_input
<>(input
.data(), input
.size(), "message")};
1219 if (tao::pegtl::parse
<RFC5322::raw_field
, RFC5322::msg_action
>(in
, *this)) {
1220 std::rotate(headers
.rbegin(), headers
.rbegin() + 1, headers
.rend());
1226 std::string
parsed::as_string() const
1228 fmt::memory_buffer bfr
;
1230 for (auto const& h
: headers
)
1231 fmt::format_to(std::back_inserter(bfr
), "{}\r\n", h
.as_string());
1234 fmt::format_to(std::back_inserter(bfr
), "\r\n{}", body
);
1236 return fmt::to_string(bfr
);
1239 bool parsed::write(std::ostream
& os
) const
1241 for (auto const& h
: headers
)
1242 os
<< h
.as_string() << "\r\n";
1245 os
<< "\r\n" << body
;
1250 std::string
header::as_string() const
1252 return fmt::format("{}:{}", name
, value
);
1255 std::string_view
parsed::get_header(std::string_view name
) const
1257 if (auto hdr
= std::find(begin(headers
), end(headers
), name
);
1258 hdr
!= end(headers
)) {
1259 return trim(hdr
->value
);
1264 void dkim_sign(message::parsed
& msg
,
1266 char const* selector
,
1269 CHECK(msg
.sig_str
.empty());
1271 boost::iostreams::mapped_file_source priv
;
1272 priv
.open(key_file
);
1274 auto const key_str
= std::string(priv
.data(), priv
.size());
1276 // Run our message through DKIM::sign
1277 OpenDKIM::sign
dks(key_str
.c_str(), // textual data
1278 selector
, sender
, OpenDKIM::sign::body_type::text
);
1279 for (auto const& header
: msg
.headers
) {
1280 dks
.header(header
.as_view());
1286 auto const sig
= dks
.getsighdr();
1288 msg
.sig_str
= fmt::format("DKIM-Signature: {}", sig
);
1289 CHECK(msg
.parse_hdr(msg
.sig_str
));
1292 void rewrite_from_to(message::parsed
& msg
,
1293 std::string mail_from
,
1294 std::string reply_to
,
1296 char const* selector
,
1299 LOG(INFO
) << "rewrite_from_to";
1301 remove_delivery_headers(msg
);
1303 if (!mail_from
.empty()) {
1304 msg
.headers
.erase(std::remove(msg
.headers
.begin(), msg
.headers
.end(), From
),
1307 msg
.from_str
= mail_from
;
1308 CHECK(msg
.parse_hdr(msg
.from_str
));
1311 if (!reply_to
.empty()) {
1313 std::remove(msg
.headers
.begin(), msg
.headers
.end(), Reply_To
),
1316 msg
.reply_to_str
= reply_to
;
1317 CHECK(msg
.parse_hdr(msg
.reply_to_str
));
1320 // modify plain text body
1323 if (iequal(msg.get_header(MIME_Version), "1.0") &&
1324 istarts_with(msg.get_header(Content_Type), "text/plain;")) {
1325 LOG(INFO) << "Adding footer to message body.";
1326 msg.body_str = msg.body;
1327 msg.body_str.append("\r\n\r\n\t-- Added Footer --\r\n");
1328 msg.body = msg.body_str;
1331 LOG(INFO) << "Not adding footer to message body.";
1332 LOG(INFO) << "MIME-Version == " << msg.get_header(MIME_Version);
1333 LOG(INFO) << "Content-Type == " << msg.get_header(Content_Type);
1335 // LOG(INFO) << "body == " << msg.body;
1338 dkim_sign(msg
, sender
, selector
, key_file
);
1341 } // namespace message