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