#include "email_sender_mailjet.h"
#include <chrono>
#include <cstddef>
#include <memory>
#include <span>
#include <string>
#include <utility>
#include <vector>
#include <nlohmann/json.hpp>
#include <mw/http_client.hpp>
#include <mw/utils.hpp>
namespace
{
constexpr std::string_view MAILJET_SEND_URL =
"https://api.mailjet.com/v3.1/send";
std::string htmlEscape(const std::string& input)
{
std::string result;
result.reserve(input.size());
for(char value : input)
{
switch(value)
{
case '&':
result += "&";
break;
case '<':
result += "<";
break;
case '>':
result += ">";
break;
case '"':
result += """;
break;
case '\'':
result += "'";
break;
default:
result.push_back(value);
}
}
return result;
}
std::string basicAuthorization(
const std::string& api_key, const std::string& secret_key)
{
const std::string credentials = api_key + ':' + secret_key;
std::vector<unsigned char> bytes(
credentials.begin(), credentials.end());
return "Basic " + mw::base64Encode(
std::span<unsigned char>(bytes), false, true);
}
} // namespace
MailjetEmailSender::MailjetEmailSender(
std::unique_ptr<mw::HTTPSessionInterface> session,
std::string from_address,
std::string from_name,
std::string api_key,
std::string secret_key)
: session_(std::move(session)),
from_address_(std::move(from_address)),
from_name_(std::move(from_name)),
api_key_(std::move(api_key)),
secret_key_(std::move(secret_key))
{}
mw::E<void> MailjetEmailSender::configure()
{
auto protocols = session_->allowedProtocols("https");
if(!protocols)
{
return std::unexpected(std::move(protocols.error()));
}
auto redirect_protocols = session_->allowedRedirectProtocols("https");
if(!redirect_protocols)
{
return std::unexpected(std::move(redirect_protocols.error()));
}
session_->followRedirects(false);
auto redirections = session_->maxRedirections(0);
if(!redirections)
{
return std::unexpected(std::move(redirections.error()));
}
auto maximum = session_->maxSize(64 * 1024);
if(!maximum)
{
return std::unexpected(std::move(maximum.error()));
}
auto connection_timeout = session_->connectionTimeout(
std::chrono::seconds(5));
if(!connection_timeout)
{
return std::unexpected(std::move(connection_timeout.error()));
}
return session_->transferTimeout(std::chrono::seconds(15));
}
mw::E<void> MailjetEmailSender::send(const AuthenticationEmail& email)
{
const std::string link = email.confirmation_url.str();
const std::string text =
"Use this link to sign in to Card Collection. The link expires in "
"ten minutes:\n\n" + link +
"\n\nIf you did not request this message, ignore it.";
const std::string html =
"<p>Use this link to sign in to Card Collection. The link expires "
"in ten minutes:</p><p><a href=\"" + htmlEscape(link) +
"\">Sign in</a></p><p>If you did not request this message, "
"ignore it.</p>";
nlohmann::json body = {
{"Messages", nlohmann::json::array({
{
{"From", {
{"Email", from_address_},
{"Name", from_name_},
}},
{"To", nlohmann::json::array({{
{"Email", email.recipient},
}})},
{"Subject", "Sign in to Card Collection"},
{"TextPart", text},
{"HTMLPart", html},
},
})},
};
mw::HTTPRequest request(MAILJET_SEND_URL);
request.setContentType("application/json");
request.setPayload(body.dump());
request.addHeader(
"Authorization", basicAuthorization(api_key_, secret_key_));
auto response = session_->post(request);
if(!response)
{
return std::unexpected(mw::runtimeError(
"Mailjet request failed"));
}
if((**response).status < 200 || (**response).status >= 300)
{
return std::unexpected(mw::runtimeError(
"Mailjet rejected the authentication email"));
}
try
{
const nlohmann::json parsed = nlohmann::json::parse(
(**response).payloadAsStr());
if(!parsed.contains("Messages") ||
!parsed["Messages"].is_array() ||
parsed["Messages"].size() != 1 ||
parsed["Messages"][0].value("Status", "") != "success")
{
return std::unexpected(mw::runtimeError(
"Mailjet returned an unsuccessful message status"));
}
}
catch(const nlohmann::json::exception&)
{
return std::unexpected(mw::runtimeError(
"Mailjet returned an invalid response"));
}
return {};
}
bool MailjetEmailSender::usesGlobalQuota() const
{
return true;
}