@Richard G @Erulezz @smtalk @cDGo @jamgames2 @kristian @chronic @tomputer @ArashiInteractive
There is another way, in my opinion the best way to deal with it - folding too long lines
It is possible to use transport_filter option and pass message through some program that implements needed functionality.
I made a naive implementation in C++.
I guess there are some corner-cases that I missed, but it looks like everything is working fine.
It folds too long lines by adding \n[whitespace character] every 'MaxLineLength' characters, in this case \n\t, \r\n\t is not necessary, because Exim has builtin \n -> \r\n conversion that takes place after executing transport_filter.
https://www.exim.org/exim-html-current/doc/html/spec_html/ch-generic_options_for_transports.html
The lines of the message that are written to the transport filter are terminated by newline (“\n”). The message is passed to the filter before any SMTP-specific processing, such as turning “\n” into “\r\n” and escaping lines beginning with a dot, and also before any processing implied by the settings of check_string and escape_string in the appendfile or pipe transports.
Despite using iostream, program performance is nice.
I tested several implementations, reading char by char by cin.get(), constant-size buffered read by rdbuf, copying line content into secondary buffer and add needed characters, instead of modyfing existing std::string buffer, however, performance wasn't better and the code readability was much worse.
Performance (including piping file into stdin and redirect stdout to another file):
real 0m0,04
user 0m0,03
sys 0m0,016s
for 16MB message, Xeon E-2274G, with max length set at 25 characters to perform some kind of stress test.
I made two versions:
1) Max line length hardcoded in code, better performance, less work, because value is constant expression.
C++:
#include <iostream>
#include <string>
#include <string_view>
using namespace std::string_view_literals;
struct Constants
{
static inline constexpr std::size_t BufferSize = 4096;
static inline constexpr std::size_t MaxLineLength = 998;
static_assert(Constants::MaxLineLength >= 2, "Max length must be at least 2");
};
static void foldLines(std::istream& inStream, std::ostream& outStream)
{
std::string line;
line.reserve(Constants::BufferSize);
constexpr auto foldLineSequence = "\n\t"sv;
while (std::getline(inStream, line)) {
for (auto pos = Constants::MaxLineLength; pos < line.length(); pos += Constants::MaxLineLength + foldLineSequence.length() - 1) {
if (pos != line.length() - 1 || line[pos] != '\r')
line.insert(pos, foldLineSequence);
}
outStream << line << '\n';
}
outStream << std::flush;
}
int main(int argc, char* argv[])
{
if (argc != 1) {
std::cerr << "Usage: "sv << argv[0];
return 1;
}
std::ios_base::sync_with_stdio(false);
std::cin.tie(nullptr);
foldLines(std::cin, std::cout);
return 0;
}
2) Maximum length value is taken from the argument at run-time.
More flexible, because maximum line length can be changed without recompilation.
C++:
#include <charconv>
#include <iostream>
#include <optional>
#include <string>
#include <string_view>
using namespace std::string_view_literals;
struct Constants
{
static inline constexpr std::size_t BufferSize = 4096;
};
static std::optional<std::size_t> stringToSize(std::string_view str)
{
std::size_t value{};
const auto result = std::from_chars(str.data(), str.data() + str.size(), value);
if (result.ec == std::errc{})
return value;
return std::nullopt;
}
static void foldLines(std::istream& inStream, std::ostream& outStream, std::size_t maxLineLength)
{
std::string line;
line.reserve(Constants::BufferSize);
constexpr auto foldLineSequence = "\n\t"sv;
while (std::getline(inStream, line)) {
for (auto pos = maxLineLength; pos < line.length(); pos += maxLineLength + foldLineSequence.length() - 1) {
if (pos != line.length() - 1 || line[pos] != '\r')
line.insert(pos, foldLineSequence);
}
outStream << line << '\n';
}
outStream << std::flush;
}
int main(int argc, char* argv[])
{
if (argc != 2) {
std::cerr << "Usage: "sv << argv[0] << " max_line_length"sv << '\n';
return 1;
}
const auto maxLineLength = stringToSize(argv[1]);
if (!maxLineLength) {
std::cerr << "Usage: "sv << argv[0] << " max_line_length"sv << '\n';
std::cerr << "max_line_length must be a positive number"sv << '\n';
return 1;
} else if (*maxLineLength < 2) {
std::cerr << "Usage: "sv << argv[0] << " max_line_length"sv << '\n';
std::cerr << "max_line_length must be at least 2"sv << '\n';
return 1;
}
std::ios_base::sync_with_stdio(false);
std::cin.tie(nullptr);
foldLines(std::cin, std::cout, *maxLineLength);
return 0;
}
Variation of the v2 for those whose compiler does not support charconv (std::from_chars / std::to_chars)
C++:
#include <iostream>
#include <optional>
#include <string>
#include <string_view>
#include <cstdlib>
using namespace std::string_view_literals;
struct Constants
{
static inline constexpr std::size_t BufferSize = 4096;
};
static std::optional<std::size_t> stringToSize(std::string_view str)
{
char* end;
const auto value = std::strtoll(str.data(), &end, 10);
if (str.data() != end && value >= 0)
return static_cast<std::size_t>(value);
return std::nullopt;
}
static void foldLines(std::istream& inStream, std::ostream& outStream, std::size_t maxLineLength)
{
std::string line;
line.reserve(Constants::BufferSize);
constexpr auto foldLineSequence = "\n\t"sv;
while (std::getline(inStream, line)) {
for (auto pos = maxLineLength; pos < line.length(); pos += maxLineLength + foldLineSequence.length() - 1) {
if (pos != line.length() - 1 || line[pos] != '\r')
line.insert(pos, foldLineSequence);
}
outStream << line << '\n';
}
outStream << std::flush;
}
int main(int argc, char* argv[])
{
if (argc != 2) {
std::cerr << "Usage: "sv << argv[0] << " max_line_length"sv << '\n';
return 1;
}
const auto maxLineLength = stringToSize(argv[1]);
if (!maxLineLength) {
std::cerr << "Usage: "sv << argv[0] << " max_line_length"sv << '\n';
std::cerr << "max_line_length must be a positive number"sv << '\n';
return 1;
} else if (*maxLineLength < 2) {
std::cerr << "Usage: "sv << argv[0] << " max_line_length"sv << '\n';
std::cerr << "max_line_length must be at least 2"sv << '\n';
return 1;
}
std::ios_base::sync_with_stdio(false);
std::cin.tie(nullptr);
foldLines(std::cin, std::cout, *maxLineLength);
return 0;
}
Compilation:
1) Save chosen source code to e.g. exim_line_folding.cpp file
2) Run: g++ -std=c++20 -Wall -Wextra -Werror -pedantic -O3 -o exim_line_folding exim_line_folding.cpp
(if you can't compile with c++20, the code should compile with -std=c++17 as well)
3) Move exim_line_folding binary to destination folder you want, it may be /usr/bin, /usr/local/bin, /opt, whatever.
4) Make sure, that binary has executable flag set (chmod).
Integration with Exim:
- Edit /etc/exim.conf:
Code:
#COMMENT#61:
remote_smtp:
driver = smtp
headers_add = "${if def:authenticated_id{X-Authenticated-Id: ${authenticated_id}}}"
interface = <; ${if exists{/etc/virtual/domainips}{${lookup{$sender_address_domain}lsearch*{/etc/virtual/domainips}}}}
helo_data = ${if exists{/etc/virtual/helo_data}{${lookup{$sending_ip_address}iplsearch{/etc/virtual/helo_data}{$value}{$primary_hostname}}}{$primary_hostname}}
hosts_try_chunking =
hosts_try_fastopen =
.include_if_exists /etc/exim.dkim.conf
remote_smtp_forward_transport:
driver = smtp
headers_add = "${if def:authenticated_id{X-Authenticated-Id: ${authenticated_id}}}"
interface = <; ${if exists{/etc/virtual/domainips}{${lookup{$original_domain}lsearch*{/etc/virtual/domainips}}}}
helo_data = ${if exists{/etc/virtual/helo_data}{${lookup{$sending_ip_address}iplsearch{/etc/virtual/helo_data}{$value}{$primary_hostname}}}{$primary_hostname}}
hosts_try_chunking =
hosts_try_fastopen =
.include_if_exists /etc/exim.dkim.conf
to:
Code:
#COMMENT#61:
remote_smtp:
driver = smtp
headers_add = "${if def:authenticated_id{X-Authenticated-Id: ${authenticated_id}}}"
interface = <; ${if exists{/etc/virtual/domainips}{${lookup{$sender_address_domain}lsearch*{/etc/virtual/domainips}}}}
helo_data = ${if exists{/etc/virtual/helo_data}{${lookup{$sending_ip_address}iplsearch{/etc/virtual/helo_data}{$value}{$primary_hostname}}}{$primary_hostname}}
hosts_try_chunking =
hosts_try_fastopen =
transport_filter = '${if >{$max_received_linelength}{998}{/path/to/exim_line_folding}{}}'
.include_if_exists /etc/exim.dkim.conf
remote_smtp_forward_transport:
driver = smtp
headers_add = "${if def:authenticated_id{X-Authenticated-Id: ${authenticated_id}}}"
interface = <; ${if exists{/etc/virtual/domainips}{${lookup{$original_domain}lsearch*{/etc/virtual/domainips}}}}
helo_data = ${if exists{/etc/virtual/helo_data}{${lookup{$sending_ip_address}iplsearch{/etc/virtual/helo_data}{$value}{$primary_hostname}}}{$primary_hostname}}
hosts_try_chunking =
hosts_try_fastopen =
transport_filter = '${if >{$max_received_linelength}{998}{/path/to/exim_line_folding}{}}'
.include_if_exists /etc/exim.dkim.conf
- Replace /path/to/exim_line_folding with the path where the compiled program is located.
- If you choose v2 then transport_filter option looks like this, 998 is the maximum line length, if you want to change it, just adjust this value.
Code:
transport_filter = '${if >{$max_received_linelength}{998}{/path/to/exim_line_folding 998}{}}'
Edit:
Something weird happened during copy/paste and there wasn't #include <iostream> in v1 + I tweaked code a little bit.
PS: If you want to add some information in Exim log if someone send message with too long line, just add something like this in /etc/exim.acl_check_message.pre.conf
Code:
# Message line length exceeds maximum RFC 5322 value
warn
set acl_m_linelength_limit = 998
condition = ${if >{$max_received_linelength}{$acl_m_linelength_limit}}
logwrite = Warning: Message from <${sender_address}> to <${recipients}> exceeds RFC 5322 maximum line length ($max_received_linelength > $acl_m_linelength_limit)