diff --git a/lib/src/codecs/quoted_printable_mail_codec.dart b/lib/src/codecs/quoted_printable_mail_codec.dart index d3e8c6e7..27d83629 100644 --- a/lib/src/codecs/quoted_printable_mail_codec.dart +++ b/lib/src/codecs/quoted_printable_mail_codec.dart @@ -197,6 +197,14 @@ class QuotedPrintableMailCodec extends MailCodec { for (var i = 0; i < cleaned.length; i++) { final char = cleaned[i]; if (char == '=') { + // A '=' within two characters of the end has no complete hex escape + // following it. RFC 2045 §6.7 permits consumers to preserve such + // malformed input verbatim; the alternative here is a RangeError + // from substring on the two lines below. + if (i + 3 > cleaned.length) { + buffer.write(cleaned.substring(i)); + break; + } final hexText = cleaned.substring(i + 1, i + 3); var charCode = int.tryParse(hexText, radix: 16); if (charCode == null) { @@ -208,6 +216,11 @@ class QuotedPrintableMailCodec extends MailCodec { } else { final charCodes = [charCode]; while (cleaned.length > (i + 4) && cleaned[i + 3] == '=') { + // The while check only proves cleaned[i+3] exists; the substring + // below still reads cleaned[i+4..i+5], so stop if the trailing + // '=' has fewer than two hex chars after it and let the outer + // loop pick it up via the truncated-tail branch above. + if (i + 6 > cleaned.length) break; i += 3; final hexText = cleaned.substring(i + 1, i + 3); charCode = int.parse(hexText, radix: 16); diff --git a/test/codecs/quoted_printable_mail_codec_test.dart b/test/codecs/quoted_printable_mail_codec_test.dart index bac1c6fd..d8da4b92 100644 --- a/test/codecs/quoted_printable_mail_codec_test.dart +++ b/test/codecs/quoted_printable_mail_codec_test.dart @@ -74,6 +74,37 @@ void main() { 'jeden Tag ändern können', ); }); + + group('malformed truncated =XY escapes are preserved verbatim', () { + // Regression: a '=' within two characters of the end used to throw + // RangeError from substring(i+1, i+3). Per RFC 2045 §6.7 consumers may + // preserve the malformed tail; matches the existing behaviour for + // non-hex '=XY' where the raw text is written to the buffer. + test('trailing bare "="', () { + expect( + MailCodec.quotedPrintable.decodeText('hello=', convert.utf8), + 'hello=', + ); + }); + test('trailing "=A"', () { + expect( + MailCodec.quotedPrintable.decodeText('hello=A', convert.utf8), + 'hello=A', + ); + }); + test('lone "=A" at end of a short input', () { + expect( + MailCodec.quotedPrintable.decodeText('h=A', convert.utf8), + 'h=A', + ); + }); + test('valid multi-byte escape followed by truncated "=A"', () { + expect( + MailCodec.quotedPrintable.decodeText('hi=C3=B6=A', convert.utf8), + 'hiö=A', + ); + }); + }); }); group('Quoted Printable encoding', () {