Skip to content

Regression in 2.9.0: non-ASCII (8bit) text bodies emit bare LF instead of CRLF (to_crlf removed from Body#initialize) #1662

Open

Description

@akichim21

Summary

Since v2.9.0, Mail::Body#encoded returns bare LF (\n) line endings for non-ASCII text parts (those that fall back to 8bit/binary transfer
encoding), instead of the RFC-compliant CRLF (\r\n) produced by v2.8.1 and earlier.

Pure-ASCII bodies are unaffected (they use 7bit, and SevenBit.encode still calls to_crlf). The regression only manifests for messages whose body
contains non-ASCII characters (e.g. UTF-8 Japanese, accented Latin, etc.), which is exactly the common case for internationalized ActionMailer mail.

Nothing downstream restores CRLF either — Message#encoded, Mail::SMTPEnvelope, and the SMTP delivery method all pass the body through unchanged — so
this affects actually delivered mail, not just in-process inspection.

Environment

  • mail: 2.9.0 (also present on master)
  • Ruby: 3.4.7

Reproduction

require 'mail'

# Non-ASCII (UTF-8) body — typical internationalized ActionMailer content
m1 = Mail.new do
  to 'a@example.com'; from 'b@example.com'; subject 'x'
  text_part { body "Café\nSecond line\n" }   # contains non-ASCII
end
puts m1.text_part.body.encoded.inspect
#  2.9.0 => "Café\nSecond line\n"        <-- bare LF
#  2.8.1 => "Café\r\nSecond line\r\n"    <-- CRLF (expected)

# Pure ASCII body — unaffected
m2 = Mail.new do
  to 'a@example.com'; from 'b@example.com'; subject 'x'
  text_part { body "Hello\nSecond line\n" }
end
puts m2.text_part.body.encoded.inspect
#  2.9.0 => "Hello\r\nSecond line\r\n"   (CRLF, OK)

Direct Body check:

Mail::Body.new("line1\nline2\n").raw_source
#  2.9.0 => "line1\nline2\n"
#  2.8.1 => "line1\r\nline2\r\n"

Expected vs. Actual

  • Expected: Body#encoded (and therefore the on-the-wire message) uses CRLF line endings, as in 2.8.1, per RFC 5322 §2.3.
  • Actual: Non-ASCII text parts retain whatever line endings the caller passed in (typically bare LF from templates), and no layer normalizes them before
    SMTP delivery.

Root cause

Mail::Body#initialize no longer normalizes the raw source to CRLF.

In 2.8.1 (lib/mail/body.rb):

@raw_source = ::Mail::Utilities.to_crlf(string.join(''))
# and
@raw_source = ::Mail::Utilities.to_crlf(string.to_s)

In 2.9.0 / master (lib/mail/body.rb, initialize):

@raw_source = string.join('')
# and
@raw_source = string.to_s

Body#encoded is unchanged between the two versions. For non-ASCII content the selected transfer encoding is 8bit → EightBit → Binary → Identity,
whose decode/encode are no-ops, so the (now un-normalized) raw_source is emitted verbatim. For ASCII content SevenBit.encode calls
Mail::Utilities.binary_unsafe_to_crlf, which is why ASCII bodies still come out as CRLF and the regression is easy to miss.

This change does not appear in the 2.9.0 CHANGELOG. It seems to be a side effect of the line-ending/charset cleanup work in this release line (related area:
PR #1113, issues #1174, #1190).

Impact

  • Internationalized text/plain (and text/html) parts are transmitted with bare LF, which is non-compliant with RFC 5322 §2.3 / RFC 5321. Lenient MTAs
    tolerate it; strict ones may reject or mishandle it (e.g. DKIM canonicalization, length checks).
  • Silent behavioral change for any app that upgrades 2.8.x → 2.9.0 and sends non-ASCII mail.

Question / suggested direction

Was removing CRLF normalization from Body#initialize intentional, with the expectation that the delivery method normalizes line endings? If so,
Mail::SMTPEnvelope / the bundled SMTP delivery method don't currently do that. If not, restoring CRLF normalization for 8bit/binary parts at encode
time (mirroring SevenBit.encode) would fix the regression without reintroducing the attachment-corruption problem PR #1113 addressed for binary
attachments.

Happy to provide a failing spec / PR if a preferred approach is indicated.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions