Files
o3de/Code/Tools/CrySCompileServer/CrySCompileServer/Core/Mailer.cpp
T
2021-03-08 14:30:57 -08:00

421 lines
12 KiB
C++

/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
// Original file Copyright Crytek GMBH or its affiliates, used under license.
#include "Mailer.h"
#include "WindowsAPIImplementation.h"
#include <AzCore/PlatformDef.h>
#include <assert.h>
#if defined(AZ_PLATFORM_MAC)
#include <netdb.h>
#include <unistd.h>
#elif defined(AZ_PLATFORM_WINDOWS)
#include <ws2tcpip.h>
#endif
#include <AzCore/base.h>
#include <AzCore/IO/SystemFile.h>
#pragma comment(lib,"ws2_32.lib")
namespace // helpers
{
static const char cb64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
void Base64EncodeBlock(const unsigned char* in, unsigned char* out)
{
out[0] = cb64[in[0] >> 2];
out[1] = cb64[((in[0] & 0x03) << 4) | ((in[1] & 0xf0) >> 4)];
out[2] = cb64[((in[1] & 0x0f) << 2) | ((in[2] & 0xc0) >> 6)];
out[3] = cb64[in[2] & 0x3f];
}
void Base64EncodeBlock(const unsigned char* in, unsigned char* out, int len)
{
out[0] = cb64[in[0] >> 2];
out[1] = cb64[((in[0] & 0x03) << 4) | ((in[1] & 0xf0) >> 4)];
out[2] = (unsigned char) (len > 1 ? cb64[((in[1] & 0x0f) << 2) | ((in[2] & 0xc0) >> 6)] : '=');
out[3] = (unsigned char) (len > 2 ? cb64[in[2] & 0x3f] : '=');
}
void Base64Encode(const unsigned char* pSrc, const size_t srcLen, unsigned char* pDst, [[maybe_unused]] const size_t dstLen)
{
assert(dstLen >= 4 * ((srcLen + 2) / 3));
size_t len = srcLen;
for (; len > 2; len -= 3, pSrc += 3, pDst += 4)
{
Base64EncodeBlock(pSrc, pDst);
}
if (len > 0)
{
unsigned char in[3];
in[0] = pSrc[0];
in[1] = len > 1 ? pSrc[1] : 0;
in[2] = 0;
Base64EncodeBlock(in, pDst, (int) len);
}
}
std::string Base64EncodeString(const std::string& in)
{
const size_t srcLen = in.size();
const size_t dstLen = 4 * ((srcLen + 2) / 3);
std::string out(dstLen, 0);
Base64Encode((const unsigned char*) in.c_str(), srcLen, (unsigned char*) out.c_str(), dstLen);
return out;
}
const char* ExtractFileName(const char* filepath)
{
for (const char* p = filepath + strlen(filepath) - 1; p >= filepath; --p)
{
if (*p == '\\' || *p == '/')
{
return p + 1;
}
}
return filepath;
}
}
AZStd::atomic_long CSMTPMailer::ms_OpenSockets = {0};
CSMTPMailer::CSMTPMailer(const tstr& username, const tstr& password, const tstr& server, int port)
: m_server(server)
, m_username(username)
, m_password(password)
, m_port(port)
, m_winSockAvail(false)
, m_response()
{
#if defined(AZ_PLATFORM_WINDOWS)
WSADATA wd;
m_winSockAvail = WSAStartup(MAKEWORD(1, 1), &wd) == 0;
if (!m_winSockAvail)
{
m_response += "Error: Unable to initialize WinSock 1.1\n";
}
#endif
}
CSMTPMailer::~CSMTPMailer()
{
#if defined(AZ_PLATFORM_WINDOWS)
if (m_winSockAvail)
{
WSACleanup();
}
#endif
}
void CSMTPMailer::ReceiveLine(SOCKET connection)
{
char buf[1025];
int ret = recv(connection, buf, sizeof(buf) - 1, 0);
if (ret == SOCKET_ERROR)
{
ret = azsnprintf(buf, sizeof(buf), "Error: WinSock error %d during recv()\n", WSAGetLastError());
if (ret == sizeof(buf) || ret < 0)
{
buf[sizeof(buf) - 1] = '\0';
}
}
else
{
buf[ret] = 0;
}
m_response += buf;
}
void CSMTPMailer::SendLine(SOCKET connection, const char* format, ...) const
{
char buf[2049];
va_list args;
va_start(args, format);
int len = azvsnprintf(buf, sizeof(buf), format, args);
if (len == sizeof(buf) || len < 0)
{
buf[sizeof(buf) - 1] = '\0';
len = sizeof(buf) - 1;
}
va_end(args);
send(connection, buf, len, 0);
}
void CSMTPMailer::SendRaw(SOCKET connection, const char* data, size_t dataLen) const
{
send(connection, data, (int) dataLen, 0);
}
void CSMTPMailer::SendFile(SOCKET connection, const tattachment& filepath, const char* boundary) const
{
AZ::IO::SystemFile inputFile;
const bool wasSuccessful = inputFile.Open(filepath.second.c_str(), AZ::IO::SystemFile::SF_OPEN_READ_ONLY);
if (wasSuccessful)
{
SendLine(connection, "--%s\r\n", boundary);
SendLine(connection, "Content-Type: application/octet-stream\r\n");
SendLine(connection, "Content-Transfer-Encoding: base64\r\n");
SendLine(connection, "Content-Disposition: attachment; filename=\"%s\"\r\n", filepath.first.c_str());
SendLine(connection, "\r\n");
AZ::IO::SystemFile::SizeType fileSize = inputFile.Length();
while (fileSize)
{
const int DEF_BLOCK_SIZE = 128; // 72
char in[3 * DEF_BLOCK_SIZE];
size_t blockSize = fileSize > sizeof(in) ? sizeof(in) : fileSize;
inputFile.Read(blockSize, in);
char out[4 * DEF_BLOCK_SIZE];
Base64Encode((unsigned char*) in, blockSize, (unsigned char*) out, sizeof(out));
SendRaw(connection, out, 4 * ((blockSize + 2) / 3));
SendLine(connection, "\r\n"); // seems to get sent faster if you split up the data lines
fileSize -= blockSize;
}
}
}
SOCKET CSMTPMailer::Open(const char* host, unsigned short port, sockaddr_in& serverAddress)
{
SOCKET connection = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (connection == INVALID_SOCKET)
{
m_response += "Error: Failed to create socket\n";
return 0;
}
struct addrinfo* addressInfo{};
char portBuffer[16];
struct addrinfo hints{};
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
azsnprintf(portBuffer, AZStd::size(portBuffer), "%hu", port);
int addrInfoReturnCode = getaddrinfo(host, portBuffer, &hints, &addressInfo);
if(addrInfoReturnCode != 0)
{
char buf[1025];
int ret = azsnprintf(buf, sizeof(buf), "Error: Host %s not found\n", host);
if (ret == sizeof(buf) || ret < 0)
{
buf[sizeof(buf) - 1] = '\0';
}
m_response += buf;
closesocket(connection);
return 0;
}
if (addressInfo)
{
++ms_OpenSockets;
serverAddress = *reinterpret_cast<sockaddr_in*>(addressInfo->ai_addr);
}
return connection;
}
void CSMTPMailer::AddReceivers(SOCKET connection, const tstrcol& receivers)
{
for (tstrcol::const_iterator it = receivers.begin(), itEnd = receivers.end(); it != itEnd; ++it)
{
if (!(*it).empty())
{
SendLine(connection, "rcpt to: %s\r\n", (*it).c_str());
ReceiveLine(connection);
}
}
}
void CSMTPMailer::AssignReceivers(SOCKET connection, const char* receiverTag, const tstrcol& receivers)
{
tstrcol::const_iterator it = receivers.begin();
tstrcol::const_iterator itEnd = receivers.end();
while (it != itEnd && (*it).empty())
{
++it;
}
if (it != itEnd)
{
tstr out(receiverTag);
out += *it;
++it;
for (; it != itEnd; ++it)
{
if (!(*it).empty())
{
out += "; ";
out += *it;
}
}
out += "\r\n";
SendLine(connection, out.c_str());
}
}
void CSMTPMailer::SendAttachments(SOCKET connection, const tattachlist& attachments, const char* boundary)
{
for (tattachlist::const_iterator it = attachments.begin(), itEnd = attachments.end(); it != itEnd; ++it)
{
if (!(*it).first.empty() && !(*it).second.empty())
{
SendFile(connection, *it, boundary);
}
}
}
bool CSMTPMailer::IsEmpty(const tstrcol& col) const
{
if (!col.empty())
{
for (tstrcol::const_iterator it = col.begin(), itEnd = col.end(); it != itEnd; ++it)
{
if (!(*it).empty())
{
return false;
}
}
}
return true;
}
bool CSMTPMailer::Send(const tstr& from, const tstrcol& to, const tstrcol& cc, const tstrcol& bcc, const tstr& subject, const tstr& body, const tattachlist& attachments)
{
if (!m_winSockAvail)
{
return false;
}
if (from.empty() || IsEmpty(to))
{
return false;
}
sockaddr_in serverAddress;
SOCKET connection = Open(m_server.c_str(), m_port, serverAddress); // SMTP telnet (usually port 25)
if (connection == INVALID_SOCKET)
{
return false;
}
if (connect(connection, (sockaddr*) &serverAddress, sizeof(serverAddress)) != SOCKET_ERROR)
{
ReceiveLine(connection);
SendLine(connection, "helo localhost\r\n");
ReceiveLine(connection);
if (!m_username.empty() && !m_password.empty())
{
SendLine(connection, "auth login\r\n"); // most servers should implement this (todo: otherwise fall back to PLAIN or CRAM-MD5 (requiring EHLO))
ReceiveLine(connection);
SendLine(connection, "%s\r\n", Base64EncodeString(m_username).c_str());
ReceiveLine(connection);
SendLine(connection, "%s\r\n", Base64EncodeString(m_password).c_str());
ReceiveLine(connection);
}
SendLine(connection, "mail from: %s\r\n", from.c_str());
ReceiveLine(connection);
AddReceivers(connection, to);
AddReceivers(connection, cc);
AddReceivers(connection, bcc);
SendLine(connection, "data\r\n");
ReceiveLine(connection);
SendLine(connection, "From: %s\r\n", from.c_str());
AssignReceivers(connection, "To: ", to);
AssignReceivers(connection, "Cc: ", cc);
AssignReceivers(connection, "Bcc: ", bcc);
SendLine(connection, "Subject: %s\r\n", subject.c_str());
static const char boundary[] = "------a95ed0b485e4a9b0fd4ff93f50ad06ca"; // beware, boundary should not clash with text content of message body!
SendLine(connection, "MIME-Version: 1.0\r\n");
SendLine(connection, "Content-Type: multipart/mixed; boundary=\"%s\"\r\n", boundary);
SendLine(connection, "\r\n");
SendLine(connection, "This is a multi-part message in MIME format.\r\n");
SendLine(connection, "--%s\r\n", boundary);
SendLine(connection, "Content-Type: text/plain; charset=iso-8859-1; format=flowed\r\n"); // the used charset should support the commonly used special characters of western languages
SendLine(connection, "Content-Transfer-Encoding: 7bit\r\n");
SendLine(connection, "\r\n");
SendRaw(connection, body.c_str(), body.size());
SendLine(connection, "\r\n");
SendAttachments(connection, attachments, boundary);
SendLine(connection, "--%s--\r\n", boundary);
SendLine(connection, "\r\n.\r\n");
ReceiveLine(connection);
SendLine(connection, "quit\r\n");
ReceiveLine(connection);
}
else
{
char buf[1025];
int ret = azsnprintf(buf, sizeof(buf), "Error: Failed to connect to %s:%d\n", m_server.c_str(), m_port);
if (ret == sizeof(buf) || ret < 0)
{
buf[sizeof(buf) - 1] = '\0';
}
m_response += buf;
return false;
}
closesocket(connection);
--ms_OpenSockets;
return true;
}
const char* CSMTPMailer::GetResponse() const
{
return m_response.c_str();
}