Skip to content

Commit 23881a3

Browse files
committed
feat: Add a Message-ID generated header when sending a message.
1 parent e53e039 commit 23881a3

File tree

11 files changed

+322
-3
lines changed

11 files changed

+322
-3
lines changed

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ set(PROJECT_SOURCE_FILES ${SRC_PATH}/attachment.cpp
105105
${SRC_PATH}/htmlmessage.cpp
106106
${SRC_PATH}/message.cpp
107107
${SRC_PATH}/messageaddress.cpp
108+
${SRC_PATH}/messageidutils.cpp
108109
${SRC_PATH}/plaintextmessage.cpp
109110
${SRC_PATH}/smtpclientbase.cpp
110111
${SRC_PATH}/smtpclient.cpp
@@ -271,6 +272,7 @@ if (BUILD_TESTING)
271272
add_executable(${PROJECT_UNITTEST_NAME} ${INCLUDE_PATH}
272273
${TEST_SRC_PATH}/main.cpp
273274
${TEST_SRC_PATH}/messageaddress_unittest.cpp
275+
${TEST_SRC_PATH}/messageidutils_unittest.cpp
274276
${TEST_SRC_PATH}/message_unittest.cpp
275277
${TEST_SRC_PATH}/message_cpp_unittest.cpp
276278
${TEST_SRC_PATH}/attachment_unittest.cpp

src/cpp/messageaddress.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ std::string MessageAddress::getDisplayName() const {
2020
return jed_utils::MessageAddress::getDisplayName();
2121
}
2222

23+
std::string MessageAddress::getDomainName() const {
24+
return jed_utils::MessageAddress::getDomainName();
25+
}
26+
2327
jed_utils::MessageAddress MessageAddress::toStdMessageAddress() const {
2428
return jed_utils::MessageAddress(jed_utils::MessageAddress::getEmailAddress(),
2529
jed_utils::MessageAddress::getDisplayName());

src/cpp/messageaddress.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ class CPP_MESSAGEADDRESS_API MessageAddress : private jed_utils::MessageAddress
6464
/** Return the display name. */
6565
std::string getDisplayName() const;
6666

67+
/** Return the domain name. */
68+
std::string getDomainName() const;
69+
6770
jed_utils::MessageAddress toStdMessageAddress() const;
6871

6972
friend class Message;

src/messageaddress.cpp

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include "messageaddress.h"
22
#include <cstddef>
3+
#include <cstring>
34
#include <regex>
45
#include <stdexcept>
56
#include <string>
@@ -8,7 +9,7 @@
89
using namespace jed_utils;
910

1011
MessageAddress::MessageAddress(const char *pEmailAddress, const char *pDisplayName)
11-
: mEmailAddress(nullptr), mDisplayName(nullptr) {
12+
: mEmailAddress(nullptr), mDisplayName(nullptr), mDomainName(nullptr) {
1213
std::string email_address { pEmailAddress };
1314
// Check if the email address is not empty or white spaces
1415
if (email_address.length() == 0 || StringUtils::trim(email_address).empty()) {
@@ -28,30 +29,48 @@ MessageAddress::MessageAddress(const char *pEmailAddress, const char *pDisplayNa
2829
mDisplayName = new char[name_len+1];
2930
strncpy(mDisplayName, pDisplayName, name_len);
3031
mDisplayName[name_len] = '\0';
32+
33+
std::string emailAddress(mEmailAddress);
34+
auto atPos = emailAddress.find('@');
35+
if (atPos == std::string::npos || atPos == emailAddress.size() - 1) {
36+
mDomainName = new char[1];
37+
mDomainName[0] = '\0';
38+
} else {
39+
auto domainNamePart = emailAddress.substr(atPos + 1);
40+
mDomainName = new char[domainNamePart.length() +1];
41+
strncpy(mDomainName, domainNamePart.c_str(), domainNamePart.length());
42+
mDomainName[domainNamePart.length()] = '\0';
43+
}
3144
}
3245

3346
MessageAddress::~MessageAddress() {
3447
delete[] mEmailAddress;
3548
delete[] mDisplayName;
49+
delete[] mDomainName;
3650
}
3751

3852
// Copy constructor
3953
MessageAddress::MessageAddress(const MessageAddress& other)
4054
: mEmailAddress(new char[strlen(other.mEmailAddress) + 1]),
41-
mDisplayName(new char[strlen(other.mDisplayName) + 1]) {
55+
mDisplayName(new char[strlen(other.mDisplayName) + 1]),
56+
mDomainName(new char[strlen(other.mDomainName) + 1]) {
4257
size_t email_len = strlen(other.mEmailAddress);
4358
strncpy(mEmailAddress, other.mEmailAddress, email_len);
4459
mEmailAddress[email_len] = '\0';
4560
size_t name_len = strlen(other.mDisplayName);
4661
strncpy(mDisplayName, other.mDisplayName, name_len);
4762
mDisplayName[name_len] = '\0';
63+
size_t domain_len = strlen(other.mDomainName);
64+
strncpy(mDomainName, other.mDomainName, domain_len);
65+
mDomainName[domain_len] = '\0';
4866
}
4967

5068
// Assignment operator
5169
MessageAddress& MessageAddress::operator=(const MessageAddress& other) {
5270
if (this != &other) {
5371
delete[] mEmailAddress;
5472
delete[] mDisplayName;
73+
delete[] mDomainName;
5574
// mEmailAddress
5675
size_t email_len = strlen(other.mEmailAddress);
5776
mEmailAddress = new char[email_len + 1];
@@ -62,32 +81,42 @@ MessageAddress& MessageAddress::operator=(const MessageAddress& other) {
6281
mDisplayName = new char[name_len + 1];
6382
strncpy(mDisplayName, other.mDisplayName, name_len);
6483
mDisplayName[name_len] = '\0';
84+
// mDomainName
85+
size_t domain_len = strlen(other.mDomainName);
86+
mDomainName = new char[domain_len + 1];
87+
strncpy(mDomainName, other.mDomainName, domain_len);
88+
mDomainName[domain_len] = '\0';
6589
}
6690
return *this;
6791
}
6892

6993
// Move constructor
7094
MessageAddress::MessageAddress(MessageAddress&& other) noexcept
7195
: mEmailAddress(other.mEmailAddress),
72-
mDisplayName(other.mDisplayName) {
96+
mDisplayName(other.mDisplayName),
97+
mDomainName(other.mDomainName) {
7398
// Release the data pointer from the source object so that the destructor
7499
// does not free the memory multiple times.
75100
other.mEmailAddress = nullptr;
76101
other.mDisplayName = nullptr;
102+
other.mDomainName = nullptr;
77103
}
78104

79105
// Move assignement operator
80106
MessageAddress& MessageAddress::operator=(MessageAddress&& other) noexcept {
81107
if (this != &other) {
82108
delete[] mEmailAddress;
83109
delete[] mDisplayName;
110+
delete[] mDomainName;
84111
// Copy the data pointer and its length from the source object.
85112
mEmailAddress = other.mEmailAddress;
86113
mDisplayName = other.mDisplayName;
114+
mDomainName = other.mDomainName;
87115
// Release the data pointer from the source object so that
88116
// the destructor does not free the memory multiple times.
89117
other.mEmailAddress = nullptr;
90118
other.mDisplayName = nullptr;
119+
other.mDomainName = nullptr;
91120
}
92121
return *this;
93122
}
@@ -116,6 +145,10 @@ const char *MessageAddress::getDisplayName() const {
116145
return mDisplayName;
117146
}
118147

148+
const char *MessageAddress::getDomainName() const {
149+
return mDomainName;
150+
}
151+
119152
bool MessageAddress::isEmailAddressValid(const std::string &pEmailAddress) const {
120153
std::regex emailPattern("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*$");
121154
return regex_match(StringUtils::toLower(pEmailAddress), emailPattern);

src/messageaddress.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,15 @@ class MESSAGEADDRESS_API MessageAddress {
6060
/** Return the display name. */
6161
const char *getDisplayName() const;
6262

63+
/** Return the domain name. */
64+
const char *getDomainName() const;
65+
6366
friend class message;
6467

6568
private:
6669
char *mEmailAddress = nullptr;
6770
char *mDisplayName = nullptr;
71+
char *mDomainName = nullptr;
6872
bool isEmailAddressValid(const std::string &pEmailAddress) const;
6973
};
7074
} // namespace jed_utils

src/messageidutils.cpp

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#include <algorithm>
2+
#include <atomic>
3+
#include <chrono>
4+
#include <cstdint>
5+
#include <random>
6+
#include <stdexcept>
7+
#include <string>
8+
#include "messageidutils.h"
9+
10+
#if defined(_WIN32)
11+
#include <windows.h>
12+
#else
13+
#include <unistd.h>
14+
#include <sys/types.h>
15+
#endif
16+
17+
namespace jed_utils {
18+
19+
std::string MessageIDUtils::generate(const std::string& rhs,
20+
bool includeAngleBrackets,
21+
bool strictRhsCheck) {
22+
if (strictRhsCheck && !rhsLooksOk(rhs))
23+
throw std::invalid_argument("MessageIDUtils::generate: RHS contains invalid characters");
24+
25+
std::string lhs = generateLhs();
26+
std::string core = lhs + "@" + rhs;
27+
return includeAngleBrackets ? ("<" + core + ">") : core;
28+
}
29+
30+
std::string MessageIDUtils::generateLhs() {
31+
const auto micros = std::chrono::duration_cast<std::chrono::microseconds>(
32+
std::chrono::system_clock::now().time_since_epoch()).count();
33+
const uint32_t pid = getPid();
34+
const uint64_t ctr = counter().fetch_add(1, std::memory_order_relaxed);
35+
const std::string rnd = random128Base36();
36+
37+
std::string lhs;
38+
lhs.reserve(64);
39+
lhs.append(toBase36(static_cast<uint64_t>(micros)));
40+
lhs.push_back('.');
41+
lhs.append(toBase36(static_cast<uint64_t>(pid)));
42+
lhs.push_back('.');
43+
lhs.append(toBase36(ctr));
44+
lhs.push_back('.');
45+
lhs.append(rnd);
46+
return lhs;
47+
}
48+
49+
uint32_t MessageIDUtils::getPid() {
50+
#if defined(_WIN32)
51+
return static_cast<uint32_t>(GetCurrentProcessId());
52+
#else
53+
return static_cast<uint32_t>(getpid());
54+
#endif
55+
}
56+
57+
std::atomic<uint64_t>& MessageIDUtils::counter() {
58+
static std::atomic<uint64_t> c {
59+
static_cast<uint64_t>(std::random_device {}()) ^
60+
static_cast<uint64_t>(
61+
std::chrono::high_resolution_clock::now().time_since_epoch().count())
62+
};
63+
return c;
64+
}
65+
66+
std::string MessageIDUtils::toBase36(uint64_t v) {
67+
static const char* digits = "0123456789abcdefghijklmnopqrstuvwxyz";
68+
if (v == 0) return "0";
69+
std::string out;
70+
while (v) {
71+
out.push_back(digits[v % 36]);
72+
v /= 36;
73+
}
74+
std::reverse(out.begin(), out.end());
75+
return out;
76+
}
77+
78+
uint64_t MessageIDUtils::rand64() {
79+
static thread_local std::mt19937_64 rng(
80+
(static_cast<uint64_t>(std::random_device {}()) << 1) ^
81+
reinterpret_cast<uintptr_t>(&rng) ^
82+
static_cast<uint64_t>(
83+
std::chrono::high_resolution_clock::now().time_since_epoch().count()));
84+
return rng();
85+
}
86+
87+
std::string MessageIDUtils::random128Base36() {
88+
const uint64_t a = rand64();
89+
const uint64_t b = rand64();
90+
return toBase36(a) + toBase36(b);
91+
}
92+
93+
bool MessageIDUtils::rhsLooksOk(const std::string& rhs) {
94+
if (rhs.empty()) return false;
95+
return std::all_of(rhs.begin(), rhs.end(), [](unsigned char c) {
96+
return std::isalnum(c) || c == '.' || c == '-' || c == '[' || c == ']' || c == ':';
97+
});
98+
}
99+
100+
} // namespace jed_utils

src/messageidutils.h

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#ifndef MESSAGEIDUTILS_H
2+
#define MESSAGEIDUTILS_H
3+
4+
#include <atomic>
5+
#include <cstdint>
6+
#include <string>
7+
8+
namespace jed_utils {
9+
10+
class MessageIDUtils {
11+
public:
12+
// Generate a complete Message-ID string, e.g. "<abc.def.ghi.jkl@domain>"
13+
static std::string generate(const std::string& rhs,
14+
bool includeAngleBrackets = true,
15+
bool strictRhsCheck = false);
16+
// Generate only the LHS portion (dot-atom safe)
17+
static std::string generateLhs();
18+
19+
private:
20+
// --- platform-specific PID ---
21+
static uint32_t getPid();
22+
// --- atomic counter (process-wide) ---
23+
static std::atomic<uint64_t>& counter();
24+
// --- convert integer to base36 (a-z0-9) ---
25+
static std::string toBase36(uint64_t v);
26+
// --- thread-local RNG, seeded once per thread ---
27+
static uint64_t rand64();
28+
// --- 128-bit random number encoded in base36 ---
29+
static std::string random128Base36();
30+
// --- basic RHS validator (optional strict mode) ---
31+
static bool rhsLooksOk(const std::string& rhs);
32+
};
33+
34+
} // namespace jed_utils
35+
36+
#endif // MESSAGEIDUTILS_H
37+

src/smtpclientbase.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include "errorresolver.h"
2424
#include "message.h"
2525
#include "messageaddress.h"
26+
#include "messageidutils.h"
2627
#include "serverauthoptions.h"
2728
#include "serveroptionsanalyzer.h"
2829
#include "smtpclienterrors.h"
@@ -947,6 +948,14 @@ int SMTPClientBase::setMailHeaders(const Message &pMsg) {
947948
return header_date_ret_code;
948949
}
949950

951+
// Message-ID
952+
try {
953+
std::string msg_id = MessageIDUtils::generate(pMsg.getFrom().getDomainName());
954+
addMailHeader("Message-ID", msg_id.c_str(), CLIENT_SENDMAIL_HEADERMSGID_ERROR);
955+
} catch (const std::invalid_argument &err) {
956+
return CLIENT_SENDMAIL_HEADERMSGID_ERROR;
957+
}
958+
950959
// From
951960
std::stringstream from_header_ss;
952961
const MessageAddress &from_addr = pMsg.getFrom();

src/smtpclienterrors.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const int CLIENT_SENDMAIL_END_DATA_ERROR = -97;
2424
const int CLIENT_SENDMAIL_END_DATA_TIMEOUT = -98;
2525
const int CLIENT_SENDMAIL_QUIT_ERROR = -99;
2626
const int CLIENT_SENDMAIL_HEADERDATE_ERROR = -100;
27+
const int CLIENT_SENDMAIL_HEADERMSGID_ERROR = -101;
2728

2829
// SMTP standard error code
2930
const int SMTPSERVER_AUTHENTICATIONREQUIRED_ERROR = 530;

0 commit comments

Comments
 (0)