Message won't get delivered if data is changed using msg:set_data()

To implement link tracking, I’m sending the message data to an API that replaces the links URLs in the message HTML body with tracking URLs, and then use msg:set_data() to update the message data. Without calling msg:set_data() the message gets delivered without any problems, but as soon as msg:set_data() is called, I get

failed to send message to srv3-s2->(alt1|alt2|alt3|alt4)?.gmail-smtp-in.l.google.com@smtp_client Some(ResolvedAddress { name: "gmail-smtp-in.l.google.com.", addr: 142.251.2.26 }): Timed Out waiting 60s for response to Some(DataDot)

from gmail and outlook (although I don’t get this error when I send the same email with msg:set_data to my own mail servers running postfix).

In my examples. the tracking API changes html body from:

<html><head></head><body>This is the HTML message body <b>in bold!</b>. <a href="https://taskulu.com">Taskulu</a><br/>Sent by <a data-ah-no-track="" href="https://ahasend.com">AhaSend</a>

to

<html><head></head><body>This is the HTML message body <b>in bold!</b>. <a href="https://t.kumo.taskulu.com/msg/08bfebca2ae211efbdb5960002cafe7c?lu=aHR0cHM6Ly90YXNrdWx1LmNvbQ%3D%3D&amp;s=WTwVJZnPG0vkI4MyhMeeM5pZxBFWjwCwds79xJjmbL8%3D">Taskulu</a><br/>Sent by <a data-ah-no-track="" href="https://ahasend.com">AhaSend</a>

(eml files are attached: msg-original.eml is written to a file in init.lua before calling msg:set_data(), and msg-tracked.eml after calling msg:set_data())

The thing is that when I disable tracking and send the html body with the tracking URL myself (which is to say when msg:set_data() is not called), gmail and outlook accept and deliver the message.

Hey there @original-baboon, thanks for posting. Please read the “Troubleshooting” and “How to Ask for Help” buttons below. If you would like a 1:1 support session from the KumoMTA team, details are at the “Book a Support Session” button below.

This is the part that does the tracking in init.lua in kumo.on('smtp_server_message_received'):

  local account_approved = aha.cached_account_is_verified(tenant)
  if account_approved then
    local track_clicks = aha.cached_account_tracks_clicks(tenant)
    local track_clicks_header = msg:get_first_named_header_value("ahasend-track-clicks")
    if track_clicks_header ~= nil then
      track_clicks_header = string.lower(track_clicks_header)
    end
    if (track_clicks and track_clicks_header ~= "false") or track_clicks_header == "true" then
      local f1 = io.open('/tmp/msg-original.eml', 'w')
      f1:write(msg:get_data())
      f1:close()
      local response = aha.track_links(msg)
      if response:status_is_success() then
        local txt = response:text()
        local f2 = io.open('/tmp/msg-tracked.eml', 'w')
        f2:write(txt)
        f2:close()
        msg:set_data(txt)
      end
    end
  end

With this code, the message is accepted by kumo, and because the ahasend-track-clicks header is set, it will call the tracking API and replace the message data, and then the request to gmail will time out:

use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;

require 'vendor/autoload.php';

$mail = new PHPMailer(true);

try {
    $mail->SMTPDebug  = SMTP::DEBUG_SERVER;
    $mail->Host       = 'send.ahasend.com';
    $mail->SMTPAuth   = true;
    $mail->Username   = 'MY_USERNAME';
    $mail->Password   = 'MY_PASSWORD';
    $mail->Port       = 587;
    $mail->isSMTP();

    $mail->setFrom('info@kumo.taskulu.com');
    $mail->addAddress('hf.farhad@gmail.com', 'Farhad Hedayatifard');
    $mail->addCustomHeader("ahasend-track-clicks", "true");
    
    //Content
    $mail->isHTML(true);
    $mail->Subject = 'Test Email';
    $mail->Body    = '<html><head></head><body>This is the HTML message body <b>in bold!</b>. <a href="https://taskulu.com">Taskulu</a><br/>Sent by <a data-ah-no-track="" href="https://ahasend.com">AhaSend</a>';
    
    $mail->AltBody = "This is the body in plain text for non-HTML mail clients. https://taskulu.com\nSent by https://ahasend.com";

    $mail->send();
    echo 'Message has been sent';
} catch (Exception $e) {
    echo "Message could not be sent. Mailer Error: {$mail->ErrorInfo}";
}

eml files logged by kumo
msg-tracked.eml (1.43 KB)
msg-original.eml (1.2 KB)

But then when I change the code and use the html body from msg-tracked.eml and send it without setting the ahasend-track-clicks header, it will deliver the message without any issues:

use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;

require 'vendor/autoload.php';

$mail = new PHPMailer(true);

try {
    $mail->SMTPDebug  = SMTP::DEBUG_SERVER;
    $mail->Host       = 'send.ahasend.com';
    $mail->SMTPAuth   = true;
    $mail->Username   = 'MY_USERNAME';
    $mail->Password   = 'MY_PASSWORD';
    $mail->Port       = 587;
    $mail->isSMTP();

    $mail->setFrom('info@kumo.taskulu.com');
    $mail->addAddress('hf.farhad@gmail.com', 'Farhad Hedayatifard');
    
    $mail->isHTML(true);
    $mail->Subject = 'Test Email';
    $mail->Body    = "<html><head></head><body>This is the HTML message body <b>in bold!</b>. <a href=\"https://t.kumo.taskulu.com/msg/117098372a9b11ef913e960002cafe7c?lu=aHR0cHM6Ly90YXNrdWx1LmNvbQ%3D%3D&amp;s=WTwVJZnPG0vkI4MyhMeeM5pZxBFWjwCwds79xJjmbL8%3D\">Taskulu</a><br/>Sent by <a data-ah-no-track=\"\" href=\"https://ahasend.com\">AhaSend</a>\n\n</body></html>";    
    $mail->AltBody = "This is the body in plain text for non-HTML mail clients. https://taskulu.com\nSent by https://ahasend.com";

    $mail->send();
    echo 'Message has been sent';
} catch (Exception $e) {
    echo "Message could not be sent. Mailer Error: {$mail->ErrorInfo}";
}

I might be wrong, but I have a feeling that this might be due to the message data being malformed in some way where postfix accepts the message, but gmail and outlook don’t, but I can’t figure what / why…

I think this is a CRLF issue. When I vim msg-tracked.eml I see that most lines have a ^M marker, but not the line with your tracking.

What I would expect to see is no ^M on any line, and that :set ff will report dos

You can also inspect with tools like od -c msg-tracked.eml

SMTP requires that every line use CRLF

Thanks @free-spirited-yorksh, that was it, fixed the line endings and it’s getting delivered now!