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