not used
[ghsmtp.git] / message.cpp
blob37a4f26304c4e0d6b358e6f9ce8c68bb629cf1f6
1 // What you get where:
3 // RFC5321.HELO/.EHLO domain
4 // RFC5321.MailFrom mailbox
5 // RFC5322.From mailbox-list
7 // Reply-To:
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"
18 #include "esc.hpp"
19 #include "fs.hpp"
20 #include "iequal.hpp"
21 #include "imemstream.hpp"
23 #include <cstring>
24 #include <map>
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>
36 using std::begin;
37 using std::end;
39 // SPF Results
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";
48 // SPF keys
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
57 // SPF identities
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()));
77 return v;
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()));
86 namespace RFC5322 {
88 using dot = one<'.'>;
89 using colon = one<':'>;
91 // clang-format off
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..' '
143 33, 33, // !
144 // 34, 34, // "
145 35, 39, // #$%&'
146 // 40, 41, // ()
147 42, 43, // *+
148 // 44, 44, // ,
149 45, 45, // -
150 // 46, 47, // ./
151 48, 57, // 0123456789
152 // 58, 64, // ;:<=>?@
153 65, 90, // A..Z
154 // 91, 91, // [
155 92, 92, // '\\'
156 // 93, 93, // ]
157 94, 126 // ^_` a..z {|}~
158 // 127,127 // DEL
159 > {};
161 struct token47 : plus<tchar47> {};
163 struct charset : token47 {};
164 struct encoding : token47 {};
166 // encoded-text = 1*<Any printable ASCII character other than "?"
167 // or SPACE>
169 struct echar : ranges< // NUL..' '
170 33, 62, // !..>
171 // 63, 63, // ?
172 64, 126 // @A..Z[\]^_` a..z {|}~
173 // 127,127 // DEL
174 > {};
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<'?'>,
185 encoded_text,
186 string<'=', '?'>
187 > {};
189 struct encoded_word : seq<opt<FWS>, encoded_word_book> {};
191 //.............................................................................
193 // Comments are recursive, hence the forward declaration:
194 struct comment;
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>>,
208 opt<FWS>,
209 one<')'>
210 > {};
212 struct CFWS : sor<seq<plus<seq<opt<FWS>, comment>, opt<FWS>>>,
213 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>,
221 DQUOTE,
222 sor<seq<star<seq<opt<FWS>, qcontent>>, opt<FWS>>, FWS>,
223 DQUOTE,
224 opt<CFWS>
225 > {};
227 struct atext : sor<ALPHA, DIGIT,
228 one<'!', '#',
229 '$', '%',
230 '&', '\'',
231 '*', '+',
232 '-', '/',
233 '=', '?',
234 '^', '_',
235 '`', '{',
236 '|', '}',
237 '~'>,
238 UTF8_non_ascii> {};
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>,
282 one<'['>,
283 star<seq<opt<FWS>, dtext>>,
284 opt<FWS>,
285 one<']'>,
286 opt<CFWS>> {};
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
318 // before.
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>>,
326 opt<one<';'>>> {};
328 struct spf_header : seq<opt<CFWS>,
329 result,
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>>>>
340 > {};
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>>,
347 obs_angle_addr
348 > {};
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<','>>>,
357 mailbox,
358 star<seq<one<','>, opt<sor<mailbox, CFWS>>>>
359 > {};
361 struct mailbox_list : sor<list<mailbox, one<','>>,
362 obs_mbox_list> {};
364 struct from : seq<TAO_PEGTL_ISTRING("From"), opt<CFWS>, colon,
365 mailbox_list> {};
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,
378 // or tspecials>
380 // CTL 0..31 127
381 // SPACE 32
383 // tspecials
384 // 34 "
385 // 40..41 ()
386 // 44 ,
387 // 47 /
388 // 58..64 ;:<=>?@
389 // 91..93 [\]
390 // 127 DEL
392 struct tchar45 : ranges< // NUL..' '
393 33, 33, // !
394 // 34, 34, // "
395 35, 39, // #$%&'
396 // 40, 41, // ()
397 42, 43, // *+
398 // 44, 44, // ,
399 45, 46, // -.
400 // 47, 47, // /
401 48, 57, // 0123456789
402 // 58, 64, // ;:<=>?@
403 65, 90, // A..Z
404 // 91, 93, // [\]
405 94, 126 // ^_` a..z {|}~
406 // 127,127 // DEL
407 > {};
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
445 // ; was returned
447 struct reasonspec : seq<TAO_PEGTL_ISTRING("reason"), opt<CFWS>, one<'='>, opt<CFWS>, value> {};
449 // pvalue = [CFWS] ( value / [ [ local-part ] "@" ] domain-name )
450 // [CFWS]
452 struct pvalue : seq<opt<CFWS>, sor<seq<opt<seq<opt<local_part>, one<'@'>>>, domain>,
453 value>,
454 opt<CFWS>> {};
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>>>
472 > {};
474 struct ar_results : sor<no_result, plus<resinfo>> {};
476 struct authres_payload : seq<opt<CFWS>, authserv_id,
477 opt<seq<CFWS, authres_version>>,
478 ar_results,
479 opt<CFWS>> {};
481 struct authres_header_field: seq<TAO_PEGTL_ISTRING("Authentication-Results"), opt<CFWS>, colon,
482 authres_payload> {};
484 struct authres_header_field_only: seq<authres_header_field, eof> {};
486 //.............................................................................
488 // clang-format on
490 template <typename Rule>
491 struct ar_action : nothing<Rule> {
494 template <>
495 struct ar_action<ar_results> {
496 template <typename Input>
497 static void
498 apply(Input const& in, std::string& authservid, std::string& ar_results)
500 ar_results = in.string();
504 template <>
505 struct ar_action<authserv_id> {
506 template <typename Input>
507 static void
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> {
520 template <>
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);
529 template <>
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);
538 template <>
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));
548 template <>
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));
558 template <>
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> {
590 template <>
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);
599 template <>
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);
608 template <>
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);
617 template <>
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));
627 template <>
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 = "";
637 template <>
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)
654 whole_thing = 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> {};
664 template <>
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();
674 template <>
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();
684 template <>
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();
694 template <>
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();
704 template <>
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();
714 template <>
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);
724 template <>
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)
742 // clang-format off
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;
752 // clang-format on
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();
780 return;
783 std::string spf_dom;
785 int spf_origin;
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());
801 return;
803 else {
804 LOG(WARNING) << "invalid mailbox in envelope-from: "
805 << spf.kv_map[envelope_from];
808 else {
809 LOG(WARNING)
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());
825 return;
827 else {
828 LOG(WARNING) << "invalid domain in helo: " << spf.kv_map[helo];
831 else {
832 LOG(WARNING) << "identity checked was helo, but no helo key";
835 else {
836 LOG(WARNING) << "unknown identity " << spf.kv_map[identity];
839 else {
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());
859 return;
861 else {
862 LOG(WARNING) << "RFC5321.MailFrom is postmaster or <> but helo is "
863 "invalid domain:"
864 << spf.kv_map[helo];
867 else {
868 LOG(WARNING) << "envelope-from is <> but no helo key";
871 else if (Mailbox::validate(efrom)) {
872 // We're good to go
873 Mailbox mbx(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());
881 return;
883 else {
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());
897 return;
899 else {
900 LOG(WARNING) << "helo is invalid domain:" << spf.kv_map[helo];
903 else {
904 LOG(WARNING)
905 << "no explicit \"identity\" key, and no envelope-from or helo key";
909 namespace message {
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,
931 char const* sender,
932 char const* selector,
933 fs::path key_file)
935 LOG(INFO) << "add_authentication_results";
936 CHECK(!msg.headers.empty());
938 // Remove any redundant Authentication-Results headers
939 msg.headers.erase(
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();
951 return false;
953 msg.headers.end());
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();
960 dkv.header(hv);
962 dkv.eoh();
964 // LOG(INFO) << "body «" << msg.body << "»";
965 dkv.body(msg.body);
967 dkv.eom();
969 OpenDMARC::policy dmp;
971 // Build up Authentication-Results header
972 fmt::memory_buffer bfr;
974 std::unordered_set<Domain> validated_doms;
976 // Grab SPF records
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;
982 continue;
985 LOG(INFO) << "SPF record parsed";
986 if (!sender_comment(spf_parsed.comment, sender)) {
987 LOG(INFO) << "comment == \"" << spf_parsed.comment << "\" not by "
988 << sender;
989 continue;
992 if (!Mailbox::validate(spf_parsed.kv_map[envelope_from])) {
993 LOG(WARNING) << "invalid mailbox: " << spf_parsed.kv_map[envelope_from];
994 continue;
997 if (!Domain::validate(spf_parsed.kv_map[helo])) {
998 LOG(WARNING) << "invalid helo domain: " << spf_parsed.kv_map[helo];
999 continue;
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={}",
1009 spf_parsed.result);
1010 fmt::format_to(std::back_inserter(bfr), " {}", spf_parsed.comment);
1011 fmt::format_to(std::back_inserter(bfr), " smtp.helo={}",
1012 helo_dom.ascii());
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);
1022 else {
1023 if (validated_doms.count(env_from.domain()) == 0) {
1024 fmt::format_to(std::back_inserter(bfr), ";\r\n\tspf={}",
1025 spf_parsed.result);
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";
1062 return false;
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.
1077 // FIXME
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?
1090 return false;
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,
1104 char const* b) {
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);
1131 // ARC
1133 OpenARC::verify arv;
1134 for (auto const& header : msg.headers) {
1135 arv.header(header.as_view());
1137 arv.eoh();
1138 arv.body(msg.body);
1139 arv.eom();
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] == ';')
1154 s.erase(0, 1);
1155 return s;
1156 }();
1158 msg.ar_str =
1159 fmt::format("{}: {};{}", Authentication_Results, sender, ar_results);
1161 LOG(INFO) << "new AR header «" << esc(msg.ar_str, esc_line_option::multi)
1162 << "»";
1164 CHECK(msg.parse_hdr(msg.ar_str));
1166 // Run our message through ARC::sign
1168 OpenARC::sign ars;
1170 if (iequal(arc_status, "none")) {
1171 ars.set_cv_none();
1173 else if (iequal(arc_status, "fail")) {
1174 ars.set_cv_fail();
1176 else if (iequal(arc_status, "pass")) {
1177 ars.set_cv_pass();
1179 else {
1180 ars.set_cv_unkn();
1183 for (auto const& header : msg.headers) {
1184 ars.header(header.as_view());
1186 ars.eoh();
1187 ars.body(msg.body);
1188 ars.eom();
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));
1200 else {
1201 LOG(INFO) << "failed to generate seal";
1204 OpenARC::verify arv2;
1205 for (auto const& header : msg.headers) {
1206 arv2.header(header.as_view());
1208 arv2.eoh();
1209 arv2.body(msg.body);
1210 arv2.eom();
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';
1226 break;
1228 else {
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_)
1239 msg.headers.erase(
1240 std::remove(msg.headers.begin(), msg.headers.end(), Return_Path),
1241 msg.headers.end());
1243 // just in case, but right now this header should not exist.
1244 msg.headers.erase(
1245 std::remove(msg.headers.begin(), msg.headers.end(), Delivered_To),
1246 msg.headers.end());
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();
1261 dkv.header(hv);
1263 dkv.eoh();
1264 dkv.body(msg.body);
1265 dkv.eom();
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());
1295 return true;
1297 return false;
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());
1307 if (!body.empty())
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";
1318 if (!body.empty())
1319 os << "\r\n" << body;
1321 return true;
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);
1335 return "";
1338 void dkim_sign(message::parsed& msg,
1339 char const* sender,
1340 char const* selector,
1341 fs::path key_file)
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());
1356 dks.eoh();
1357 dks.body(msg.body);
1358 dks.eom();
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,
1369 char const* sender,
1370 char const* selector,
1371 fs::path key_file)
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),
1379 msg.headers.end());
1381 msg.from_str = mail_from;
1382 CHECK(msg.parse_hdr(msg.from_str));
1385 if (!reply_to.empty()) {
1386 msg.headers.erase(
1387 std::remove(msg.headers.begin(), msg.headers.end(), Reply_To),
1388 msg.headers.end());
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;
1404 else {
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