don't insist on CDB files existing
[ghsmtp.git] / Reply.cpp
blob1c38852877e073cde42b3202933541867b1fe840
1 #include "Reply.hpp"
3 #include "Hash.hpp"
4 #include "Mailbox.hpp"
5 #include "iequal.hpp"
6 #include "is_ascii.hpp"
8 #include <algorithm>
9 #include <cctype>
10 #include <iterator>
11 #include <string>
13 #include <cppcodec/base32_crockford.hpp>
15 #include <arpa/inet.h>
16 #include <time.h>
18 #include <glog/logging.h>
20 #include <fmt/format.h>
21 #include <fmt/ostream.h>
23 using std::begin;
24 using std::end;
26 constexpr int hash_bytes_reply = 6;
28 constexpr std::string_view REP_PREFIX = "rep=";
30 constexpr char sep_char = '='; // must match above *_PREFIX values
32 std::string to_lower(std::string data)
34 std::transform(data.begin(), data.end(), data.begin(),
35 [](unsigned char c) { return std::tolower(c); });
36 return data;
39 static std::string hash_rep(Reply::from_to const& rep, std::string_view secret)
41 Hash h;
42 h.update(secret);
43 h.update(to_lower(rep.mail_from));
44 h.update(to_lower(rep.rcpt_to_local_part));
45 return h.final().substr(0, hash_bytes_reply);
48 std::string enc_reply_blob(Reply::from_to const& rep, std::string_view secret)
50 auto const hash = hash_rep(rep, secret);
52 auto const pkt = fmt::format("{}{}{}{}{}", // clang-format off
53 hash, '\0',
54 rep.rcpt_to_local_part, '\0',
55 rep.mail_from); // clang-format on
57 return fmt::format("{}{}", REP_PREFIX,
58 cppcodec::base32_crockford::encode(pkt));
61 std::string Reply::enc_reply(Reply::from_to const& rep, std::string_view secret)
63 auto const result = Mailbox::parse(rep.mail_from);
64 if (!result) {
65 throw std::invalid_argument("invalid mailbox syntax in enc_reply");
68 // If it's "local part"@example.com or local-part@[127.0.0.1] we
69 // must fall back to the blob style.
70 if (result->local_type == Mailbox::local_types::quoted_string ||
71 result->domain_type == Mailbox::domain_types::address_literal) {
72 return enc_reply_blob(rep, secret);
75 // If rcpt_to_local_part contain a '=' fall back.
76 if (rep.rcpt_to_local_part.find(sep_char) != std::string_view::npos) {
77 return enc_reply_blob(rep, secret);
80 auto const mail_from = Mailbox(rep.mail_from);
82 auto const hash_enc = hash_rep(rep, secret);
84 return fmt::format("{}{}{}{}{}{}{}{}", // clang-format off
85 REP_PREFIX, // includes sep_char
86 hash_enc, sep_char,
87 rep.rcpt_to_local_part, sep_char,
88 mail_from.local_part(), sep_char,
89 mail_from.domain().utf8());
90 // clang-format on
93 auto split(std::string const& str, const char delim)
95 std::vector<std::string> out;
97 size_t start;
98 size_t end = 0;
99 while ((start = str.find_first_not_of(delim, end)) != std::string::npos) {
100 end = str.find(delim, start);
101 out.push_back(str.substr(start, end - start));
104 return out;
107 static std::optional<Reply::from_to> dec_reply_blob(std::string_view addr,
108 std::string_view secret)
110 auto const pktv = cppcodec::base32_crockford::decode(addr);
111 auto const pkt =
112 std::string(reinterpret_cast<char const*>(pktv.data()), pktv.size());
114 auto const parts = split(pkt, '\0');
116 auto const hash = parts[0];
118 Reply::from_to rep;
119 rep.rcpt_to_local_part = parts[1];
120 rep.mail_from = parts[2];
122 auto const hash_computed = hash_rep(rep, secret);
124 if (!iequal(hash_computed, hash)) {
125 LOG(WARNING) << "hash check failed";
126 return {};
129 return rep;
132 static bool is_pure_base32(std::string_view s)
134 auto constexpr alpha =
135 std::string_view(cppcodec::detail::base32_crockford_alphabet,
136 sizeof(cppcodec::detail::base32_crockford_alphabet));
137 // If we can't find anything not in the base32 alphabet, it's pure
138 return s.find_first_not_of(alpha) == std::string_view::npos;
141 std::optional<Reply::from_to> Reply::dec_reply(std::string_view addr,
142 std::string_view secret)
144 if (!istarts_with(addr, REP_PREFIX)) {
145 LOG(WARNING) << addr << " not a valid reply address";
146 return {};
148 addr.remove_prefix(REP_PREFIX.length());
150 if (is_pure_base32(addr)) {
151 // if everything after REP= is base32 we have a blob
152 return dec_reply_blob(addr, secret);
155 // REP= has been removed, addr is now:
156 // {hash}={rcpt_to_local_part}={mail_from.local}={mail_from.domain}
157 // ^1st ^2nd ^last
158 // and mail_from.local can contain '=' chars
160 auto const first_sep = addr.find_first_of(sep_char);
161 auto const last_sep = addr.find_last_of(sep_char);
162 auto const second_sep = addr.find_first_of(sep_char, first_sep + 1);
164 if (first_sep == last_sep || second_sep == last_sep) {
165 LOG(WARNING) << "unrecognized reply format " << addr;
166 return {};
169 auto const rcpt_to_pos = first_sep + 1;
170 auto const mf_loc_pos = second_sep + 1;
171 auto const mf_dom_pos = last_sep + 1;
173 auto const rcpt_to_len = second_sep - rcpt_to_pos;
174 auto const mf_loc_len = last_sep - mf_loc_pos;
176 auto const reply_hash = addr.substr(0, first_sep);
177 auto const rcpt_to_loc = addr.substr(rcpt_to_pos, rcpt_to_len);
178 auto const mail_from_loc = addr.substr(mf_loc_pos, mf_loc_len);
179 auto const mail_from_dom = addr.substr(mf_dom_pos, std::string_view::npos);
181 Reply::from_to rep;
182 rep.rcpt_to_local_part = rcpt_to_loc;
183 rep.mail_from = fmt::format("{}@{}", mail_from_loc, mail_from_dom);
185 auto const hash_enc = hash_rep(rep, secret);
187 if (!iequal(reply_hash, hash_enc)) {
188 LOG(WARNING) << "hash mismatch in reply " << addr;
189 LOG(WARNING) << " reply_hash == " << reply_hash;
190 LOG(WARNING) << " hash_enc == " << hash_enc;
191 LOG(WARNING) << " rcpt_to_loc == " << rcpt_to_loc;
192 LOG(WARNING) << "mail_from_loc == " << mail_from_loc;
193 LOG(WARNING) << "mail_from_dom == " << mail_from_dom;
194 return {};
197 return rep;