mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-27 07:55:08 +01:00
Merge pull request #388 from wneessen/bug/filename-sanitization
Improve filename sanitization in MIME headers
This commit is contained in:
commit
acf3c58220
2 changed files with 123 additions and 4 deletions
38
msgwriter.go
38
msgwriter.go
|
@ -273,7 +273,7 @@ func (mw *msgWriter) addFiles(files []*File, isAttachment bool) {
|
||||||
mimeType = string(file.ContentType)
|
mimeType = string(file.ContentType)
|
||||||
}
|
}
|
||||||
file.setHeader(HeaderContentType, fmt.Sprintf(`%s; name="%s"`, mimeType,
|
file.setHeader(HeaderContentType, fmt.Sprintf(`%s; name="%s"`, mimeType,
|
||||||
mw.encoder.Encode(mw.charset.String(), file.Name)))
|
mw.encoder.Encode(mw.charset.String(), sanitizeFilename(file.Name))))
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := file.getHeader(HeaderContentTransferEnc); !ok {
|
if _, ok := file.getHeader(HeaderContentTransferEnc); !ok {
|
||||||
|
@ -285,7 +285,7 @@ func (mw *msgWriter) addFiles(files []*File, isAttachment bool) {
|
||||||
|
|
||||||
if file.Desc != "" {
|
if file.Desc != "" {
|
||||||
if _, ok := file.getHeader(HeaderContentDescription); !ok {
|
if _, ok := file.getHeader(HeaderContentDescription); !ok {
|
||||||
file.setHeader(HeaderContentDescription, file.Desc)
|
file.setHeader(HeaderContentDescription, mw.encoder.Encode(mw.charset.String(), file.Desc))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -295,12 +295,12 @@ func (mw *msgWriter) addFiles(files []*File, isAttachment bool) {
|
||||||
disposition = "attachment"
|
disposition = "attachment"
|
||||||
}
|
}
|
||||||
file.setHeader(HeaderContentDisposition, fmt.Sprintf(`%s; filename="%s"`,
|
file.setHeader(HeaderContentDisposition, fmt.Sprintf(`%s; filename="%s"`,
|
||||||
disposition, mw.encoder.Encode(mw.charset.String(), file.Name)))
|
disposition, mw.encoder.Encode(mw.charset.String(), sanitizeFilename(file.Name))))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isAttachment {
|
if !isAttachment {
|
||||||
if _, ok := file.getHeader(HeaderContentID); !ok {
|
if _, ok := file.getHeader(HeaderContentID); !ok {
|
||||||
file.setHeader(HeaderContentID, fmt.Sprintf("<%s>", file.Name))
|
file.setHeader(HeaderContentID, fmt.Sprintf("<%s>", sanitizeFilename(file.Name)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if mw.depth == 0 {
|
if mw.depth == 0 {
|
||||||
|
@ -498,3 +498,33 @@ func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encodin
|
||||||
mw.bytesWritten += n
|
mw.bytesWritten += n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sanitizeFilename sanitizes a given filename string by replacing specific unwanted characters with
|
||||||
|
// an underscore ('_').
|
||||||
|
//
|
||||||
|
// This method replaces any control character and any special character that is problematic for
|
||||||
|
// MIME headers and file systems with an underscore ('_') character.
|
||||||
|
//
|
||||||
|
// The following characters are replaced
|
||||||
|
// - Any control character (US-ASCII < 32)
|
||||||
|
// - ", /, :, <, >, ?, \, |, [DEL]
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - input: A string of a filename that is supposed to be sanitized
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A string representing the sanitized version of the filename
|
||||||
|
func sanitizeFilename(input string) string {
|
||||||
|
var sanitized strings.Builder
|
||||||
|
for i := 0; i < len(input); i++ {
|
||||||
|
// We do not allow control characters in file names.
|
||||||
|
if input[i] < 32 || input[i] == 34 || input[i] == 47 || input[i] == 58 ||
|
||||||
|
input[i] == 60 || input[i] == 62 || input[i] == 63 || input[i] == 92 ||
|
||||||
|
input[i] == 124 || input[i] == 127 {
|
||||||
|
sanitized.WriteRune('_')
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sanitized.WriteByte(input[i])
|
||||||
|
}
|
||||||
|
return sanitized.String()
|
||||||
|
}
|
||||||
|
|
|
@ -304,6 +304,65 @@ func TestMsgWriter_addFiles(t *testing.T) {
|
||||||
charset: CharsetUTF8,
|
charset: CharsetUTF8,
|
||||||
encoder: getEncoder(EncodingQP),
|
encoder: getEncoder(EncodingQP),
|
||||||
}
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
filename string
|
||||||
|
expect string
|
||||||
|
}{
|
||||||
|
{"normal US-ASCII filename", "test.txt", "test.txt"},
|
||||||
|
{"normal US-ASCII filename with space", "test file.txt", "test file.txt"},
|
||||||
|
{"filename with new lines", "test\r\n.txt", "test__.txt"},
|
||||||
|
{"filename with disallowed character:\x22", "test\x22.txt", "test_.txt"},
|
||||||
|
{"filename with disallowed character:\x2f", "test\x2f.txt", "test_.txt"},
|
||||||
|
{"filename with disallowed character:\x3a", "test\x3a.txt", "test_.txt"},
|
||||||
|
{"filename with disallowed character:\x3c", "test\x3c.txt", "test_.txt"},
|
||||||
|
{"filename with disallowed character:\x3e", "test\x3e.txt", "test_.txt"},
|
||||||
|
{"filename with disallowed character:\x3f", "test\x3f.txt", "test_.txt"},
|
||||||
|
{"filename with disallowed character:\x5c", "test\x5c.txt", "test_.txt"},
|
||||||
|
{"filename with disallowed character:\x7c", "test\x7c.txt", "test_.txt"},
|
||||||
|
{"filename with disallowed character:\x7f", "test\x7f.txt", "test_.txt"},
|
||||||
|
{
|
||||||
|
"japanese characters filename", "添付ファイル.txt",
|
||||||
|
"=?UTF-8?q?=E6=B7=BB=E4=BB=98=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB.txt?=",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"simplified chinese characters filename", "测试附件文件.txt",
|
||||||
|
"=?UTF-8?q?=E6=B5=8B=E8=AF=95=E9=99=84=E4=BB=B6=E6=96=87=E4=BB=B6.txt?=",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cyrillic characters filename", "Тестовый прикрепленный файл.txt",
|
||||||
|
"=?UTF-8?q?=D0=A2=D0=B5=D1=81=D1=82=D0=BE=D0=B2=D1=8B=D0=B9_=D0=BF=D1=80?= " +
|
||||||
|
"=?UTF-8?q?=D0=B8=D0=BA=D1=80=D0=B5=D0=BF=D0=BB=D0=B5=D0=BD=D0=BD=D1=8B?= " +
|
||||||
|
"=?UTF-8?q?=D0=B9_=D1=84=D0=B0=D0=B9=D0=BB.txt?=",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run("addFile with filename sanitization: "+tt.name, func(t *testing.T) {
|
||||||
|
buffer := bytes.NewBuffer(nil)
|
||||||
|
msgwriter.writer = buffer
|
||||||
|
message := testMessage(t)
|
||||||
|
message.AttachFile("testdata/attachment.txt", WithFileName(tt.filename))
|
||||||
|
msgwriter.writeMsg(message)
|
||||||
|
if msgwriter.err != nil {
|
||||||
|
t.Errorf("msgWriter failed to write: %s", msgwriter.err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ctExpect string
|
||||||
|
cdExpect := fmt.Sprintf(`Content-Disposition: attachment; filename="%s"`, tt.expect)
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "freebsd":
|
||||||
|
ctExpect = fmt.Sprintf(`Content-Type: application/octet-stream; name="%s"`, tt.expect)
|
||||||
|
default:
|
||||||
|
ctExpect = fmt.Sprintf(`Content-Type: text/plain; charset=utf-8; name="%s"`, tt.expect)
|
||||||
|
}
|
||||||
|
if !strings.Contains(buffer.String(), ctExpect) {
|
||||||
|
t.Errorf("expected content-type: %q, got: %q", ctExpect, buffer.String())
|
||||||
|
}
|
||||||
|
if !strings.Contains(buffer.String(), cdExpect) {
|
||||||
|
t.Errorf("expected content-disposition: %q, got: %q", cdExpect, buffer.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
t.Run("message with a single file attached", func(t *testing.T) {
|
t.Run("message with a single file attached", func(t *testing.T) {
|
||||||
buffer := bytes.NewBuffer(nil)
|
buffer := bytes.NewBuffer(nil)
|
||||||
msgwriter.writer = buffer
|
msgwriter.writer = buffer
|
||||||
|
@ -676,3 +735,33 @@ func TestMsgWriter_writeBody(t *testing.T) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMsgWriter_sanitizeFilename(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
given string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"test.txt", "test.txt"},
|
||||||
|
{"test file.txt", "test file.txt"},
|
||||||
|
{"test\\ file.txt", "test_ file.txt"},
|
||||||
|
{`"test" file.txt`, "_test_ file.txt"},
|
||||||
|
{`test file .txt`, "test_file_.txt"},
|
||||||
|
{"test\r\nfile.txt", "test__file.txt"},
|
||||||
|
{"test\x22file.txt", "test_file.txt"},
|
||||||
|
{"test\x2ffile.txt", "test_file.txt"},
|
||||||
|
{"test\x3afile.txt", "test_file.txt"},
|
||||||
|
{"test\x3cfile.txt", "test_file.txt"},
|
||||||
|
{"test\x3efile.txt", "test_file.txt"},
|
||||||
|
{"test\x3ffile.txt", "test_file.txt"},
|
||||||
|
{"test\x5cfile.txt", "test_file.txt"},
|
||||||
|
{"test\x7cfile.txt", "test_file.txt"},
|
||||||
|
{"test\x7ffile.txt", "test_file.txt"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.given+"=>"+tt.want, func(t *testing.T) {
|
||||||
|
if got := sanitizeFilename(tt.given); got != tt.want {
|
||||||
|
t.Errorf("sanitizeFilename failed, expected: %q, got: %q", tt.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue