Exim 4.95

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.
 
@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)
 
Last edited:
Thank you. I think I will use that separate warning line.
For the rest I rather comply to RFC and if something needs to be done in exim.conf that's something for DA staff or smtalk, I won't do that myself, but that's everybody's personal choice ofcourse.
 
Thank you. I think I will use that separate warning line.
For the rest I rather comply to RFC and if something needs to be done in exim.conf that's something for DA staff or smtalk, I won't do that myself, but that's everybody's personal choice ofcourse.
@Richard G @smtalk
It may be a good idea to add some include file that user can customize, e.g:
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.remote_smtp.conf.custom
.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.remote_smtp.conf.custom
.include_if_exists /etc/exim.dkim.conf

Then it would be possible to add a transport_filter line, or to modify the Exim 4.95 maximum line length option by everyone without modifying exim.conf

About RFC, if your mailserver is used as submission agent (465, 587 ports) then you can modify submission messages and add missing things (like existing rule in exim.conf which adds Message-ID if it is missing, Exim itself would do exactly the same thing if you add control=submission modifier in config).
It is 100% valid approach and it is necessary because mail clients are often buggy/broken.
In case of exceeding maximum line length, it is mainly Microsoft Outlook that causes this issue due to broken implementation of line folding).
Mails from relays and external mailservers are completely different matter and you shouldn't modify them, but submission? you can do much more including fixing defective messages.

e.g. Postfix does have line folding built-in, Exim unfortunately doesn't.
Code:
smtp_line_length_limit (default: 998)

    The maximal length of message header and body lines that Postfix will send via SMTP. This limit does not include the <CR><LF> at the end of each line. Longer lines are broken by inserting "<CR><LF><SPACE>", to minimize the damage to MIME formatted mail. Specify zero to disable this limit.

    The Postfix limit of 998 characters not including <CR><LF> is consistent with the SMTP limit of 1000 characters including <CR><LF>. The Postfix limit was 990 with Postfix 2.8 and earlier.
 
Last edited:
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.
Will this edit the body of the actual mail? I would not recommend that because it could break DKIM signing, which is pretty common nowdays. Unless the DKIM signing happens after the transport, I'm not sure about that.
 
@Richard G @smtalk
It may be a good idea to add some include file that user can customize, e.g:
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.remote_smtp.conf.custom
.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.remote_smtp.conf.custom
.include_if_exists /etc/exim.dkim.conf

Then it would be possible to add a transport_filter line, or to modify the Exim 4.95 maximum line length option by everyone without modifying exim.conf
[/CODE]

I hope your suggestion for the customisation gets picked up fast by DirectAdmin, I'm holding of the update until something conclusive happens.

I think anyone here would love to stick to the RFC but in out there that's not always possible. Certainly not when it suddenly gets enforced upon users were it all worked before (and not for just a day but for a long time).
 
Does this only affect the Microsoft Outlook (Outlook Express, Microsoft Mail ... no clue what Microsoft is calling their *default comes with Windows* email client these days)? i.e. Microsoft is not following RFC standards?

I've tried sending a message with really, really long lines with Thunderbird and it went through without issues. Looks like something (I assume Thunderbird) cut the lines before getting to 988 characters.

Microsoft likes to follow the "your rules don't apply to me" system - so that would be my thought.

Is the correct line of thinking here that Microsoft just isn't following rules that everyone else is following?
 
MS mostly is following RFC lines, however... as soon as they can walk on the edges of that, creating and dictating their own rules, they won't hesitate to use them.

Microsoft likes to follow the "your rules don't apply to me" system - so that would be my thought.
?? ? ? ? I love this one. (y)
 
I'm not much of a Windows fan boy. The last time I used anything Windows was back during Windows XP - not sure when exactly that was (mid, late 2000s?). So the point is, if I'm blasting Microsoft and that offends anyone, I'm sorry.

But from what I'm reading in this thread - although kind of reading between the lines - the ones that are experiencing problems with this are using some form of Microsoft branded email client, correct?

Perhaps the solution is to tell these people - "You're using a Microsoft email client, they don't adhere to standards, use something else or tell Microsoft to get with the program." I mean, if everyone else can follow the rules, why can't Microsoft?

If you're using some outdated email client, like Eudora - then it might be your own fault.

I'm just trying to determine if this is a problem with Microsoft branded email clients only. And if it is, why do we have to bend to their rules?
 
Does this only affect the Microsoft Outlook (Outlook Express, Microsoft Mail ... no clue what Microsoft is calling their *default comes with Windows* email client these days)? i.e. Microsoft is not following RFC standards?

I've tried sending a message with really, really long lines with Thunderbird and it went through without issues. Looks like something (I assume Thunderbird) cut the lines before getting to 988 characters.

Microsoft likes to follow the "your rules don't apply to me" system - so that would be my thought.

Is the correct line of thinking here that Microsoft just isn't following rules that everyone else is following?

Mine case from October 5th, with message has lines too long for transport, was for system mails generated from OpenCart 3 but the client developer manage to change something
 
And if it is, why do we have to bend to their rules?
Because they are very big (probably the biggest), they obey most rules and they have an enormous lot of customers, either on Office Outlook versions, Office 365 or Hotmail c.s. so they are probably the biggest out there.
So that's why they can tell you what you have to do to be able to reach them, and not the other way around. As partly this is a good thing because this forces party's which are a bit too customer friendly to get their systems up to speed and obey rules, there is also that dynamic filter they use (or rather their customers) which can put systems on grey or banlists and then it can be hard to get off these lists.
Example: our server got on a greylist (so all mail gets into spamboxes which nowadays are cleared every 10 days with MS) because a forum send a user a birtday message. On another server because they got a pm notification which they enabled themselves. The users just reported them as spam. So since there is no check if it is indeed spam, that dynamic filter can be a real nusance and cause trouble in cases it shouldn't.

Is this issue with the too long stuff MS only? Sorry, don't know.
 
Last edited:
In my case, I had my client try to send the same email again using webmail - it went through without issue. Best guess is the problem lies with their older version of Outlook 2013. Need to do further testing, but I haven't had any other clients complain yet. If it is just an older version of Outlook causing the problem, that's an easy conversation with the client.
 
Does this only affect the Microsoft Outlook (Outlook Express, Microsoft Mail ... no clue what Microsoft is calling their *default comes with Windows* email client these days)? i.e. Microsoft is not following RFC standards?
No, I also noticed some LFD warning emails having this trouble. So, not only Microsoft Outlook.
 
No, I also noticed some LFD warning emails having this trouble. So, not only Microsoft Outlook.
Well, I was really referring more to end-user email clients. But the concern here is also valid.

Someone also mentioned OpenCart.

I suppose anyone that's sending out mail from scripts really needs to add a line check to insure that the lines aren't longer than 998 characters. I doubt many are doing so (I have many custom written scripts, and I don't have any such line check written in any of my scripts).

I would suspect that Chirpy over at ConfigServers would quickly update CSF/LFD to resolve this if he knows about it. A new version of csf was released yesterday although the blog post doesn't seem to indicate that this was fixed.

I would suspect any reputable and developer maintained script or application would work to resolve this if they know this is an issue.

My main concern would be end-user email clients. Those might be harder to get fixes rolled out for. And specifically to Microsoft branded email clients - I don't think they will ever fix them. They're too big to fail, right?
 
This wasn't an issue in 4.94.2, because messages were not validated for maximum line length specified in RFC standard in that version

Thanks! After upgrading Exim to 4.95, we had a customer using Outlook who got the dreaded "message has lines too long for transport" when trying to submit e-mail messages. Cranking up the allowed line length as in your suggestion seems to have worked perfectly. Deploying the new exim.conf to all my servers now, fingers crossed ?
 
Will this edit the body of the actual mail? I would not recommend that because it could break DKIM signing, which is pretty common nowdays. Unless the DKIM signing happens after the transport, I'm not sure about that.
I wasn't 100% percent sure, so I tested it and DKIM signing happens after transport_filter execution, so there is no hash mismatch.
 
Doing some early checks on this - it looks like Gmail is also not adhering to this standard. That is they are not cutting lines automatically and sending emails with lines longer than 998 characters.

So this would tend to lend credence to circumventing this through Exim configuration.

I'm generally a big fan of following the RFC rules. Because if you don't follow rules, you end up with multiple sets of standards to accomplish the same thing and who's rules are right?

I still think the correct answer here is for sending clients (end-user email clients, web scripts/applications, webmail services) to cut lines that exceed 998 characters as per the RFC standard.

But, it would appear that one of two things has happened:

1) The memo about RFC 5322 and max line length wasn't received by many email sending client developers.

or

2) RFC 5322 needs to be rewritten to include a longer max line length or abolished altogether.

I can't really fault Exim, because these are the rules according to RFC 5322. RFC 5322 was published in 2008 (?) perhaps it's due for an update?

It would appear that many of these email sending clients are either unaware of RFC 5322 or believe that it is no longer necessary. If it's the former, then they need to be made aware of this and the industry has done a poor job of making this known. If it's the latter (and is true), then RFC 5322 needs to be updated.
 
Back
Top