I updated exim_line_folding.
1) Both compile and runtime maximum line length versions are combined into one, generic source code with boolean switch (Config::CompileTimeLength), if you set false, you can specify maximum line length at runtime by parameter.
2) Both runtime implementations - charconv and strtoll are also combined into the same source code, charconv is used if header is available, otherwise - strtoll is used.
3) Folding line sequence is now placed after whitespace (space, tab, etc.) position if this is possible to avoid breaking line in the middle of e.g. Message-ID.
4)
- Both the version with compile time and runtime line length can be used without parameters, in this case, the version with the runtime line length will use the default value (Config::MaxLineLength), exactly the same as compile-time version does.
Usage: /path/to/exim_line_folding
- For version with runtime line length, you can set the maximum line length as the first parameter, but this is optional:
Usage: /path/to/exim_line_folding 998
5) I put a toggle (Config::UseBuiltinIsSpace) to decide, if standard library std::isspace function or self-written equivalent routine is used to determine, if character is whitespace.
I don't know why, but the std::isspace slows down entire execution significantly, it's possible that the compiler doesn't inline std::isspace but instead compiles code into a function call.
6) Performance is nearly as good as it was before.
7) Replacing existing whitespace instead of inserting folding line sequence after whitespace in the future may be a good idea.
Compilation:
1) Save source code to e.g. exim_line_folding.cpp file
2) Change CompileTimeLength to false if you want to compile runtime line length version.
3) Run: g++ -std=c++20 -Wall -Wextra -Werror -pedantic -O3 -Wno-attributes -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)
4) Move exim_line_folding binary to destination folder you want, it may be /usr/bin, /usr/local/bin, /opt, etc.
5) Make sure, that binary has executable flag set (chmod).
Integration with Exim:
I rather comply to RFC's if this is an RFC thing. I've also updated to 4.95 a couple of weeks ago and so far 1 only had 1 line with those "too long" statements on 1 of 3 servers. It's better to teach customers than to violate RFC rules imho.
forum.directadmin.com
C++:
#include <iostream>
#include <optional>
#include <string>
#include <string_view>
#ifdef __has_include
#define has_include(x) __has_include(x)
#else
#define has_include(x) 0
#endif
#if has_include(<charconv>)
#include <charconv>
#else
#include <cstdlib>
#endif
using namespace std::string_view_literals;
struct Config
{
static inline constexpr auto CompileTimeLength = true;
static inline constexpr auto FoldLineSequence = "\n\t"sv;
static inline constexpr std::size_t MaxLineLength = 998;
static inline constexpr std::size_t LineBufferSize = 4096;
static inline constexpr auto UseBuiltinIsSpace = false;
static_assert(Config::MaxLineLength >= 2, "Config::MaxLineLength must be at least 2");
};
enum ReturnCode : int
{
Success = 0,
Error
};
#if has_include(<charconv>)
static std::optional<std::size_t> stringToSize(std::string_view str) noexcept
{
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;
}
#else
static std::optional<std::size_t> stringToSize(std::string_view str) noexcept
{
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;
}
#endif
static constexpr bool isCharWhitespace(char c) noexcept
{
if constexpr (Config::UseBuiltinIsSpace) {
return std::isspace(static_cast<unsigned char>(c));
} else {
switch (c) {
case ' ':
case '\t':
case '\r':
case '\f':
case '\v':
case '\n':
return true;
default:
return false;
}
}
}
static void foldLines(std::istream& inStream, std::ostream& outStream, std::size_t maxLineLength)
{
std::string line;
line.reserve(Config::LineBufferSize);
while (std::getline(inStream, line)) {
for (std::size_t prevPos = 0, curPos = maxLineLength; curPos < line.length(); ) {
if (curPos == line.length() - 1 && line[curPos] == '\r')
break;
const auto findInsertPosition = [curPos, prevPos](std::string_view str) {
if (isCharWhitespace(str[curPos]))
return curPos;
for (auto pos = curPos - 1; pos > prevPos; --pos) {
if (isCharWhitespace(str[pos]))
return pos + 1;
}
return curPos;
};
const auto insertPos = findInsertPosition(line);
line.insert(insertPos, Config::FoldLineSequence);
prevPos = insertPos + Config::FoldLineSequence.length();
curPos = prevPos + maxLineLength - 1;
}
outStream << line << '\n';
}
outStream << std::flush;
}
int main(int argc, char* argv[])
{
std::ios_base::sync_with_stdio(false);
std::cin.tie(nullptr);
if constexpr (Config::CompileTimeLength) {
if (argc != 1) [[unlikely]] {
std::cerr << "Usage: "sv << argv[0];
return ReturnCode::Error;
} else {
foldLines(std::cin, std::cout, Config::MaxLineLength);
}
} else {
const auto maxLineLength = [argc, argv]() -> std::optional<std::size_t> {
switch (argc) {
case 1: return Config::MaxLineLength;
case 2: return stringToSize(argv[1]);
default: [[unlikely]] {
std::cerr << "Usage: "sv << argv[0] << " [max_line_length]"sv << '\n';
std::exit(ReturnCode::Error);
}
}
}();
if (!maxLineLength) [[unlikely]] {
std::cerr << "Usage: "sv << argv[0] << " [max_line_length]"sv << '\n';
std::cerr << "max_line_length must be a positive number"sv << '\n';
return ReturnCode::Error;
} else if (*maxLineLength < 2) [[unlikely]] {
std::cerr << "Usage: "sv << argv[0] << " [max_line_length]"sv << '\n';
std::cerr << "max_line_length must be at least 2"sv << '\n';
return ReturnCode::Error;
} else {
foldLines(std::cin, std::cout, *maxLineLength);
}
}
return ReturnCode::Success;
}