Skip to content

fix: assemble RFC 2231 multi-segment filename params with non-UTF-8 charsets - #401

Open
troclaux wants to merge 1 commit into
jhillyerd:mainfrom
troclaux:fix/issue-109
Open

fix: assemble RFC 2231 multi-segment filename params with non-UTF-8 charsets#401
troclaux wants to merge 1 commit into
jhillyerd:mainfrom
troclaux:fix/issue-109

Conversation

@troclaux

@troclaux troclaux commented Jun 7, 2026

Copy link
Copy Markdown

Summary

Go's mime.ParseMediaType only supports UTF-8 and US-ASCII in RFC 2231 charset-encoded continuation parameters. When a Content-Disposition header uses a non-ASCII charset (e.g. EUC-KR) split across multiple filename*N*= segments, stdlib discards all but the last successfully decoded segment, producing a truncated filename like p.zip instead of the full Korean filename.

Changes

  • Add assembleRFC2231Params in mediatype/mediatype.go to detect RFC 2231 continuation parameters (name*N*= / name*N=), sort them by segment number, concatenate the percent-decoded bytes, and convert from the declared charset to UTF-8 using enmime's existing coding.ConvertToUTF8String.
  • The assembled value is re-encoded as a single-segment name*=utf-8''... parameter before being passed to mime.ParseMediaType, which stdlib handles correctly.
  • Add unit tests in mediatype/mediatype_test.go for both EUC-KR and US-ASCII multi-segment cases.
  • Add an integration test in part_test.go and test data testdata/parts/long-filename-euckr.raw to verify the FileName field is set correctly end-to-end.

Testing

  • All existing tests pass (go test ./...)
  • New tests cover the EUC-KR multi-segment case (개.txt from segments filename*0*=euc-kr''%B0%B3 + filename*1*=%2E%74%78%74)
  • New test covers US-ASCII multi-segment continuation

Fixes #109

@coveralls

Copy link
Copy Markdown

Coverage Status

coverage: 86.41% (+0.01%) from 86.4% — troclaux:fix/issue-109 into jhillyerd:main

…harsets

Go's mime.ParseMediaType only supports UTF-8 and US-ASCII in RFC 2231
charset-encoded continuation parameters. When a Content-Disposition header
uses a non-ASCII charset (e.g. EUC-KR) split across multiple filename*N*=
segments, stdlib discards all but the last successfully decoded segment,
producing a truncated filename.

Add assembleRFC2231Params in the mediatype package to detect continuation
parameters (name*N*= / name*N=), sort by segment number, concatenate the
percent-decoded bytes, convert from the declared charset to UTF-8 using
enmime's existing coding package, and replace the N-segment params with a
single RFC 2231 UTF-8 encoded parameter before passing to mime.ParseMediaType.

Fixes jhillyerd#109
Comment thread mediatype/mediatype.go
// order, applies charset conversion, and returns a new string where the continuation parameters
// are replaced by a single decoded parameter. This is needed because Go's standard
// mime.ParseMediaType only supports UTF-8 and US-ASCII in RFC 2231 charset-encoded parameters.
func assembleRFC2231Params(s string) string {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s param name isn't very descriptive, maybe use ctype to align with our other funcs

Comment thread mediatype/mediatype.go

// ParseWithOptions parses media-type with additional options controlling the parsing behavior.
// rfc2231SegmentRe matches RFC 2231 continuation parameter names such as "filename*0*" or "filename*1".
var rfc2231SegmentRe = regexp.MustCompile(`(?i)^([a-zA-Z0-9!#$&\-^_.+]+)\*(\d+)(\*?)$`)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifying (?i) and both a-z & A-Z is redundant

mtype: "application/pdf",
params: map[string]string{"name": "key=value"},
},
{

@jhillyerd jhillyerd Jun 13, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the code coverage report isn't loading, but these tests look like they just cover the happy path. We should make sure we are triggering some of the failure/continue paths and confirming reasonable output. Doesn't need to be in this particular test func.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves enmime’s parsing of RFC 2231 continuation parameters (e.g. filename*0*= / filename*1*=) so multi-segment filenames using non-UTF-8 charsets (like EUC-KR) are correctly reassembled and converted to UTF-8 before delegating to Go’s mime.ParseMediaType.

Changes:

  • Added assembleRFC2231Params to detect/sort/concatenate RFC 2231 continuation segments, charset-convert the byte stream, and re-encode as a single name*=utf-8''... param prior to mime.ParseMediaType.
  • Added unit tests to cover EUC-KR and US-ASCII multi-segment filename continuations at the mediatype.Parse level.
  • Added an end-to-end regression test + raw fixture to ensure (*enmime.Part).FileName is set correctly from a real multipart message.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
mediatype/mediatype.go Adds RFC 2231 continuation assembly + charset conversion shim before calling stdlib parsing.
mediatype/mediatype_test.go Adds unit coverage for multi-segment RFC 2231 filename* handling (EUC-KR and ASCII).
part_test.go Adds integration regression test verifying Part.FileName is populated correctly from continuation segments.
testdata/parts/long-filename-euckr.raw Adds fixture email reproducing the segmented EUC-KR filename case from #109.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread mediatype/mediatype.go
Comment on lines +117 to +136
for i, seg := range segs {
val := seg.value
if i == 0 {
// Segment 0 may carry a charset''value prefix per RFC 2231.
if before, after, found := strings.Cut(val, "''"); found {
charset = before
val = after
}
}
if seg.encoded {
decoded, err := url.PathUnescape(val)
if err != nil {
failed = true
break
}
rawBytes = append(rawBytes, decoded...)
} else {
rawBytes = append(rawBytes, val...)
}
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how we should handle non-contiguous segments, enmime tries to be lenient nowdays... but only respecting charset on seg#0 may be a good idea?

Comment thread mediatype/mediatype.go
Comment on lines +185 to +186
// rfc2231PercentEncode percent-encodes a UTF-8 string for use in an RFC 2231 parameter value.
// Only unreserved token characters (alphanumeric and !#$&-^_.+~) are left unencoded.
@jhillyerd

Copy link
Copy Markdown
Owner

Thanks for sending this. Sorry for the Copilot review, but I don't spend much time reading Go code nowdays, need all the help I can get. :)

I think you may also want to run go fix, the enmime codebase has been "fixed" recently so we are using some of the newer range syntaxes etc.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

It does not correctly extract long filename.

4 participants