Merge pull request #3364 from k9mail/header_parameter_encoding

MIME parameter encoding (RFC 2231)
This commit is contained in:
cketti 2018-05-01 15:49:23 +02:00 committed by GitHub
commit f3bac31b76
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 543 additions and 84 deletions

View file

@ -8,7 +8,7 @@ koinVersion=0.9.1
robolectricVersion=3.7.1
junitVersion=4.12
mockitoVersion=2.18.0
okioVersion=1.11.0
okioVersion=1.14.0
truthVersion=0.35
android.enableAapt2=false

View file

@ -11,6 +11,7 @@ if (rootProject.testCoverage) {
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:${kotlinVersion}"
implementation 'org.apache.james:apache-mime4j-core:0.8.1'
implementation 'org.apache.james:apache-mime4j-dom:0.8.1'
implementation "com.squareup.okio:okio:${okioVersion}"
@ -23,7 +24,6 @@ dependencies {
androidTestImplementation 'com.android.support.test:runner:0.4.1'
androidTestImplementation 'com.madgag.spongycastle:pg:1.51.0.0'
testImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:${kotlinVersion}"
testImplementation "org.robolectric:robolectric:${robolectricVersion}"
testImplementation "junit:junit:${junitVersion}"
testImplementation "com.google.truth:truth:${truthVersion}"

View file

@ -1,4 +1,5 @@
/*
* Copyright 2018 The K-9 Dog Walkers
* Copyright 2001-2004 The Apache Software Foundation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -14,43 +15,44 @@
* limitations under the License.
*/
package com.fsck.k9.mail.filter;
package com.fsck.k9.mail.filter
/**
* This code was copied from the Apache Commons project.
* The unnecessary parts have been left out.
*/
public class Hex {
/**
* Used building output as Hex
*/
private static final char[] DIGITS = {
'0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
};
object Hex {
private val LOWER_CASE = charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f')
private val UPPER_CASE = charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F')
/**
* Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order.
* The returned array will be double the length of the passed array, as it takes two characters to represent any
* given byte.
*
* @param data
* a byte[] to convert to Hex characters
* @param data a byte[] to convert to Hex characters
* @return A String containing lower-case hexadecimal characters
*/
public static String encodeHex(byte[] data) {
int l = data.length;
char[] out = new char[l << 1];
@JvmStatic
fun encodeHex(data: ByteArray): String {
val l = data.size
val out = CharArray(l shl 1)
// two characters form the hex value.
for (int i = 0, j = 0; i < l; i++) {
out[j++] = DIGITS[(0xF0 & data[i]) >>> 4 ];
out[j++] = DIGITS[ 0x0F & data[i] ];
var i = 0
var j = 0
while (i < l) {
out[j++] = LOWER_CASE[data[i].toInt() shr 4 and 0x0F]
out[j++] = LOWER_CASE[data[i].toInt() and 0x0F]
i++
}
return new String(out);
return String(out)
}
fun StringBuilder.appendHex(value: Byte, lowerCase: Boolean = true) {
val digits = if (lowerCase) LOWER_CASE else UPPER_CASE
append(digits[value.toInt() shr 4 and 0x0F])
append(digits[value.toInt() and 0x0F])
}
}

View file

@ -0,0 +1,115 @@
/*
* These functions are based on Okio's UTF-8 code.
*
* Copyright (C) 2018 The K-9 Dog Walkers
* Copyright (C) 2017 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.fsck.k9.mail.helper
/**
* Encodes this string using UTF-8.
*/
inline fun String.encodeUtf8(beginIndex: Int = 0, endIndex: Int = length, crossinline writeByte: (Byte) -> Unit) {
require(beginIndex >= 0) { "beginIndex < 0: $beginIndex" }
require(endIndex >= beginIndex) { "endIndex < beginIndex: $endIndex < $beginIndex" }
require(endIndex <= length) { "endIndex > length: $endIndex > $length" }
// Transcode a UTF-16 Java String to UTF-8 bytes.
var i = beginIndex
while (i < endIndex) {
val c = this[i].toInt()
if (c < 0x80) {
// Emit a 7-bit character with 1 byte.
writeByte(c.toByte()) // 0xxxxxxx
i++
} else if (c < 0x800) {
// Emit a 11-bit character with 2 bytes.
writeByte((c shr 6 or 0xc0).toByte()) // 110xxxxx
writeByte((c and 0x3f or 0x80).toByte()) // 10xxxxxx
i++
} else if (c < 0xd800 || c > 0xdfff) {
// Emit a 16-bit character with 3 bytes.
writeByte((c shr 12 or 0xe0).toByte()) // 1110xxxx
writeByte((c shr 6 and 0x3f or 0x80).toByte()) // 10xxxxxx
writeByte((c and 0x3f or 0x80).toByte()) // 10xxxxxx
i++
} else {
// c is a surrogate. Make sure it is a high surrogate and that its successor is a low surrogate.
// If not, the UTF-16 is invalid, in which case we emit a replacement character.
val low = if (i + 1 < endIndex) this[i + 1].toInt() else 0
if (c > 0xdbff || low < 0xdc00 || low > 0xdfff) {
writeByte('?'.toByte())
i++
continue
}
// UTF-16 high surrogate: 110110xxxxxxxxxx (10 bits)
// UTF-16 low surrogate: 110111yyyyyyyyyy (10 bits)
// Unicode code point: 00010000000000000000 + xxxxxxxxxxyyyyyyyyyy (21 bits)
val codePoint = 0x010000 + (c and 0xd800.inv() shl 10 or (low and 0xdc00.inv()))
// Emit a 21-bit character with 4 bytes.
writeByte((codePoint shr 18 or 0xf0).toByte()) // 11110xxx
writeByte((codePoint shr 12 and 0x3f or 0x80).toByte()) // 10xxxxxx
writeByte((codePoint shr 6 and 0x3f or 0x80).toByte()) // 10xxyyyy
writeByte((codePoint and 0x3f or 0x80).toByte()) // 10yyyyyy
i += 2
}
}
}
/**
* Returns the number of bytes used to encode `string` as UTF-8 when using [Int.encodeUtf8].
*/
fun Int.utf8Size(): Int {
return when {
this < 0x80 -> 1
this < 0x800 -> 2
this < 0xd800 -> 3
this < 0xe000 -> 1
this < 0x10000 -> 3
else -> 4
}
}
/**
* Encodes this code point using UTF-8.
*/
inline fun Int.encodeUtf8(crossinline writeByte: (Byte) -> Unit) {
val codePoint = this
if (codePoint < 0x80) {
// Emit a 7-bit character with 1 byte.
writeByte(codePoint.toByte()) // 0xxxxxxx
} else if (codePoint < 0x800) {
// Emit a 11-bit character with 2 bytes.
writeByte((codePoint shr 6 or 0xc0).toByte()) // 110xxxxx
writeByte((codePoint and 0x3f or 0x80).toByte()) // 10xxxxxx
} else if (codePoint < 0xd800 || codePoint in 0xe000..0x10000) {
// Emit a 16-bit character with 3 bytes.
writeByte((codePoint shr 12 or 0xe0).toByte()) // 1110xxxx
writeByte((codePoint shr 6 and 0x3f or 0x80).toByte()) // 10xxxxxx
writeByte((codePoint and 0x3f or 0x80).toByte()) // 10xxxxxx
} else if (codePoint in 0xd800..0xdfff) {
// codePoint is a surrogate. Emit a replacement character
writeByte('?'.toByte())
} else {
// Emit a 21-bit character with 4 bytes.
writeByte((codePoint shr 18 or 0xf0).toByte()) // 11110xxx
writeByte((codePoint shr 12 and 0x3f or 0x80).toByte()) // 10xxxxxx
writeByte((codePoint shr 6 and 0x3f or 0x80).toByte()) // 10xxyyyy
writeByte((codePoint and 0x3f or 0x80).toByte()) // 10yyyyyy
}
}

View file

@ -0,0 +1,20 @@
package com.fsck.k9.mail.internet
object Headers {
@JvmStatic
fun contentType(mimeType: String, name: String): String {
return MimeParameterEncoder.encode(mimeType, mapOf("name" to name))
}
@JvmStatic
@JvmOverloads
fun contentDisposition(disposition: String, fileName: String, size: Long? = null): String {
val parameters = if (size == null) {
mapOf("filename" to fileName)
} else {
mapOf("filename" to fileName, "size" to size.toString())
}
return MimeParameterEncoder.encode(disposition, parameters)
}
}

View file

@ -0,0 +1,225 @@
package com.fsck.k9.mail.internet
import com.fsck.k9.mail.filter.Hex.appendHex
import com.fsck.k9.mail.helper.encodeUtf8
import com.fsck.k9.mail.helper.utf8Size
/**
* Encode MIME parameter values as specified in RFC 2045 and RFC 2231.
*/
object MimeParameterEncoder {
// RFC 5322, section 2.1.1
private const val MAX_LINE_LENGTH = 78
// RFC 5234: CRLF = %d13.10
private const val CRLF = "\r\n"
// RFC 5234: HTAB = %x09
private const val HTAB = '\t'
// RFC 5234: SP = %x20
private const val SPACE = ' '
// RFC 5234: DQUOTE = %x22
private const val DQUOTE = '"'
// RFC 2045: tspecials := "(" / ")" / "<" / ">" / "@" / "," / ";" / ":" / "\" / <"> / "/" / "[" / "]" / "?" / "="
private val TSPECIALS = charArrayOf('(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '=')
private const val ENCODED_VALUE_PREFIX = "UTF-8''"
/**
* Create header field value with parameters encoded if necessary.
*/
@JvmStatic
fun encode(value: String, parameters: Map<String, String>): String {
return if (parameters.isEmpty()) {
value
} else {
buildString {
append(value)
encodeAndAppendParameters(parameters)
}
}
}
private fun StringBuilder.encodeAndAppendParameters(parameters: Map<String, String>) {
for ((name, value) in parameters) {
encodeAndAppendParameter(name, value)
}
}
private fun StringBuilder.encodeAndAppendParameter(name: String, value: String) {
val fixedCostLength = 1 /* folding space */ + name.length + 1 /* equals sign */ + 1 /* semicolon */
val unencodedValueFitsOnSingleLine = fixedCostLength + value.length <= MAX_LINE_LENGTH
val quotedValueMightFitOnSingleLine = fixedCostLength + value.length + 2 /* quotes */ <= MAX_LINE_LENGTH
if (unencodedValueFitsOnSingleLine && value.isToken()) {
appendParameter(name, value)
} else if (quotedValueMightFitOnSingleLine && value.isQuotable() &&
fixedCostLength + value.quotedLength() <= MAX_LINE_LENGTH) {
appendParameter(name, value.quoted())
} else {
rfc2231EncodeAndAppendParameter(name, value)
}
}
private fun StringBuilder.appendParameter(name: String, value: String) {
append(";$CRLF ")
append(name).append('=').append(value)
}
private fun StringBuilder.rfc2231EncodeAndAppendParameter(name: String, value: String) {
val encodedValueLength = 1 /* folding space */ + name.length + 1 /* asterisk */ + 1 /* equal sign */ +
ENCODED_VALUE_PREFIX.length + value.rfc2231EncodedLength() + 1 /* semicolon */
if (encodedValueLength <= MAX_LINE_LENGTH) {
appendRfc2231SingleLineParameter(name, value.rfc2231Encoded())
} else {
encodeAndAppendRfc2231MultiLineParameter(name, value)
}
}
private fun StringBuilder.appendRfc2231SingleLineParameter(name: String, encodedValue: String) {
append(";$CRLF ")
append(name)
append("*=$ENCODED_VALUE_PREFIX")
append(encodedValue)
}
private fun StringBuilder.encodeAndAppendRfc2231MultiLineParameter(name: String, value: String) {
var index = 0
var line = 0
var startOfLine = true
var remainingSpaceInLine = 0
val endIndex = value.length
while (index < endIndex) {
if (startOfLine) {
append(";$CRLF ")
val lineStartIndex = length - 1
append(name).append('*').append(line).append("*=")
if (line == 0) {
append(ENCODED_VALUE_PREFIX)
}
remainingSpaceInLine = MAX_LINE_LENGTH - (length - lineStartIndex) - 1 /* semicolon */
if (remainingSpaceInLine < 3) {
throw UnsupportedOperationException("Parameter name too long")
}
startOfLine = false
line++
}
val codePoint = value.codePointAt(index)
// Keep all characters encoding a single code point on the same line
val utf8Size = codePoint.utf8Size()
if (utf8Size == 1 && codePoint.toChar().isAttributeChar() && remainingSpaceInLine >= 1) {
append(codePoint.toChar())
index++
remainingSpaceInLine--
} else if (remainingSpaceInLine >= utf8Size * 3) {
codePoint.encodeUtf8 {
append('%')
appendHex(it, lowerCase = false)
remainingSpaceInLine -= 3
}
index += Character.charCount(codePoint)
} else {
startOfLine = true
}
}
}
private fun String.rfc2231Encoded() = buildString {
this@rfc2231Encoded.encodeUtf8 { byte ->
val c = byte.toChar()
if (c.isAttributeChar()) {
append(c)
} else {
append('%')
appendHex(byte, lowerCase = false)
}
}
}
private fun String.rfc2231EncodedLength(): Int {
var length = 0
encodeUtf8 { byte ->
length += if (byte.toChar().isAttributeChar()) 1 else 3
}
return length
}
private fun String.isToken() = when {
isEmpty() -> false
else -> all { it.isTokenChar() }
}
private fun String.isQuotable() = all { it.isQuotable() }
private fun String.quoted(): String {
// quoted-string = [CFWS] DQUOTE *([FWS] qcontent) [FWS] DQUOTE [CFWS]
// qcontent = qtext / quoted-pair
// quoted-pair = ("\" (VCHAR / WSP))
return buildString(capacity = length + 16) {
append(DQUOTE)
for (c in this@quoted) {
if (c.isQText() || c.isWsp()) {
append(c)
} else if (c.isVChar()) {
append('\\').append(c)
} else {
throw IllegalArgumentException("Unsupported character: $c")
}
}
append(DQUOTE)
}
}
private fun String.quotedLength(): Int {
var length = 2 /* start and end quote */
for (c in this) {
if (c.isQText() || c.isWsp()) {
length++
} else if (c.isVChar()) {
length += 2
} else {
throw IllegalArgumentException("Unsupported character: $c")
}
}
return length
}
private fun Char.isQuotable() = when {
isWsp() -> true
isVChar() -> true
else -> false
}
private fun Char.isTSpecial() = this in TSPECIALS
// RFC 2045: token := 1*<any (US-ASCII) CHAR except SPACE, CTLs, or tspecials>
// RFC 5234: CTL = %x00-1F / %x7F
private fun Char.isTokenChar() = isVChar() && !isTSpecial()
// RFC 5322: qtext = %d33 / %d35-91 / %d93-126 / obs-qtext
private fun Char.isQText() = when (toInt()) {
33 -> true
in 35..91 -> true
in 93..126 -> true
else -> false
}
// RFC 5234: VCHAR = %x21-7E
private fun Char.isVChar() = toInt() in 33..126
// RFC 5234: WSP = SP / HTAB
private fun Char.isWsp() = this == SPACE || this == HTAB
// RFC 2231: attribute-char := <any (US-ASCII) CHAR except SPACE, CTLs, "*", "'", "%", or tspecials>
private fun Char.isAttributeChar() = isVChar() && this != '*' && this != '\'' && this != '%' && !isTSpecial()
}

View file

@ -320,5 +320,3 @@ class MessageTest {
}
private fun Message.getFirstHeader(header: String): String = getHeader(header)[0]
private fun String.crlf() = replace("\n", "\r\n")

View file

@ -0,0 +1,3 @@
package com.fsck.k9.mail
fun String.crlf() = replace("\n", "\r\n")

View file

@ -0,0 +1,130 @@
package com.fsck.k9.mail.internet
import com.fsck.k9.mail.crlf
import com.google.common.truth.Truth.assertThat
import org.junit.Test
class MimeParameterEncoderTest {
@Test
fun valueWithoutParameters() {
val header = MimeParameterEncoder.encode("inline", emptyMap())
assertThat(header).isEqualTo("inline")
}
@Test
fun simpleParameterValue() {
val header = MimeParameterEncoder.encode("attachment", mapOf("filename" to "kitten.png"))
assertThat(header).isEqualTo("""
|attachment;
| filename=kitten.png
""".trimMargin().crlf())
}
@Test
fun backslashesInParameterValue() {
val header = MimeParameterEncoder.encode("attachment",
mapOf("filename" to "Important Document \\Confidential\\.pdf"))
assertThat(header).isEqualTo("""
|attachment;
| filename="Important Document \\Confidential\\.pdf"
""".trimMargin().crlf())
}
@Test
fun nonAsciiCharactersInParameterValue() {
val header = MimeParameterEncoder.encode("attachment", mapOf("filename" to "Übergrößenträger.dat"))
assertThat(header).isEqualTo("""
|attachment;
| filename*=UTF-8''%C3%9Cbergr%C3%B6%C3%9Fentr%C3%A4ger.dat
""".trimMargin().crlf())
}
@Test
fun longParameterValueWithAsciiOnlyCharacters() {
val header = MimeParameterEncoder.encode("attachment",
mapOf("filename" to "This file name is quite long and exceeds the recommended header line length " +
"of 78 characters.txt"))
// For now this is encoded like parameters that contain non-ASCII characters. However we could use
// continuations without character set encoding to make it look like this:
//
// attachment;
// filename*0="This file name is quite long and exceeds the recommended header";
// filename*1=" line length of 78 characters.txt"
assertThat(header).isEqualTo("""
|attachment;
| filename*0*=UTF-8''This%20file%20name%20is%20quite%20long%20and%20exceeds%20;
| filename*1*=the%20recommended%20header%20line%20length%20of%2078%20character;
| filename*2*=s.txt
""".trimMargin().crlf())
}
@Test
fun longParameterValueWithNonAsciiCharacters() {
val header = MimeParameterEncoder.encode("attachment",
mapOf("filename" to "üüüüüüüüüüüüüüüüüüüüüü.txt", "size" to "54321"))
assertThat(header).isEqualTo("""
|attachment;
| filename*0*=UTF-8''%C3%BC%C3%BC%C3%BC%C3%BC%C3%BC%C3%BC%C3%BC%C3%BC%C3%BC;
| filename*1*=%C3%BC%C3%BC%C3%BC%C3%BC%C3%BC%C3%BC%C3%BC%C3%BC%C3%BC%C3%BC;
| filename*2*=%C3%BC%C3%BC%C3%BC.txt;
| size=54321
""".trimMargin().crlf())
}
@Test
fun parameterValueWithControlCharacter() {
val header = MimeParameterEncoder.encode("value",
mapOf("something" to "foo\u0000bar"))
assertThat(header).isEqualTo("""
|value;
| something*=UTF-8''foo%00bar
""".trimMargin().crlf())
}
@Test
fun mixedParameterValues() {
val header = MimeParameterEncoder.encode("value", mapOf(
"token" to "foobar",
"quoted" to "something containing spaces",
"non-ascii" to "Grüße",
"long" to "one~two~three~four~five~six~seven~eight~nine~ten~eleven~twelve~thirteen~fourteen~fifteen"))
assertThat(header).isEqualTo("""
|value;
| token=foobar;
| quoted="something containing spaces";
| non-ascii*=UTF-8''Gr%C3%BC%C3%9Fe;
| long*0*=UTF-8''one~two~three~four~five~six~seven~eight~nine~ten~eleven~twelv;
| long*1*=e~thirteen~fourteen~fifteen
""".trimMargin().crlf())
}
@Test
fun nonAttributeCharactersInParameterValue() {
val header = MimeParameterEncoder.encode("value", mapOf(
"param1" to "*'%",
"param2" to "=*'%",
"param3" to "ü*'%"))
assertThat(header).isEqualTo("""
|value;
| param1=*'%;
| param2="=*'%";
| param3*=UTF-8''%C3%BC%2A%27%25
""".trimMargin().crlf())
}
@Test(expected = UnsupportedOperationException::class)
fun overlyLongParameterName_shouldThrow() {
MimeParameterEncoder.encode("attachment",
mapOf("parameter_name_that_exceeds_the_line_length_recommendation_almost_on_its_own" to "foobar"))
}
}

View file

@ -3,13 +3,14 @@ package com.fsck.k9.message;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import com.fsck.k9.mail.internet.Headers;
import timber.log.Timber;
import com.fsck.k9.Account.QuoteStyle;
@ -34,7 +35,6 @@ import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mail.internet.TextBody;
import com.fsck.k9.mailstore.TempFileBody;
import com.fsck.k9.message.quote.InsertableHtmlContent;
import org.apache.james.mime4j.codec.EncoderUtil;
import org.apache.james.mime4j.util.MimeUtil;
@ -233,51 +233,18 @@ public abstract class MessageBuilder {
}
}
/*
* Content-Type is defined in RFC 2045
*
* Example:
*
* Content-Type: text/plain; charset=us-ascii (Plain text)
*
* TODO: RFC 2231/2184 long parameter encoding
* Example:
*
* Content-Type: application/x-stuff
* title*1*=us-ascii'en'This%20is%20even%20more%20
* title*2*=%2A%2A%2Afun%2A%2A%2A%20
* title*3="isn't it!"
*/
private void addContentType(MimeBodyPart bp, String contentType, String name) throws MessagingException {
/*
* Correctly encode the filename here. Otherwise the whole
* header value (all parameters at once) will be encoded by
* MimeHeader.writeTo().
*/
bp.addHeader(MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\r\n name=\"%s\"",
contentType,
EncoderUtil.encodeIfNecessary(name,
EncoderUtil.Usage.WORD_ENTITY, 7)));
private void addContentType(MimeBodyPart bodyPart, String contentType, String name) throws MessagingException {
String value = Headers.contentType(contentType, name);
bodyPart.addHeader(MimeHeader.HEADER_CONTENT_TYPE, value);
if (!MimeUtil.isMessage(contentType)) {
bp.setEncoding(MimeUtility.getEncodingforType(contentType));
bodyPart.setEncoding(MimeUtility.getEncodingforType(contentType));
}
}
/*
* TODO: RFC 2231/2184 long parameter encoding
*
* From RFC 2183 (The Content-Disposition Header Field):
* "Parameter values longer than 78 characters, or which
* contain non-ASCII characters, MUST be encoded as specified
* in [RFC 2184]."
*
*/
private void addContentDisposition(MimeBodyPart bp, String name, Long size) {
bp.addHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, String.format(Locale.US,
"attachment;\r\n filename=\"%s\";\r\n size=%d",
EncoderUtil.encodeIfNecessary(name, EncoderUtil.Usage.WORD_ENTITY, 7),
size));
private void addContentDisposition(MimeBodyPart bodyPart, String fileName, Long size) {
String value = Headers.contentDisposition("attachment", fileName, size);
bodyPart.addHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, value);
}
/**

View file

@ -29,7 +29,6 @@ import com.fsck.k9.mail.internet.MimeMultipart;
import com.fsck.k9.message.MessageBuilder.Callback;
import com.fsck.k9.message.quote.InsertableHtmlContent;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.robolectric.Robolectric;
@ -99,24 +98,22 @@ public class MessageBuilderTest extends RobolectricTest {
"text =E2=98=AD\r\n" +
"--" + BOUNDARY_1 + "\r\n" +
"Content-Type: text/plain;\r\n" +
" name=\"attach.txt\"\r\n" +
" name=attach.txt\r\n" +
"Content-Transfer-Encoding: base64\r\n" +
"Content-Disposition: attachment;\r\n" +
" filename=\"attach.txt\";\r\n" +
" filename=attach.txt;\r\n" +
" size=23\r\n" +
"\r\n" +
"dGV4dCBkYXRhIGluIGF0dGFjaG1lbnQ=\r\n" +
"\r\n" +
"--" + BOUNDARY_1 + "--\r\n";
private static final String MESSAGE_CONTENT_WITH_LONG_CONTENT_TYPE =
private static final String MESSAGE_CONTENT_WITH_LONG_FILE_NAME =
"Content-Type: multipart/mixed; boundary=\"" + BOUNDARY_1 + "\"\r\n" +
"Content-Transfer-Encoding: 7bit\r\n" +
"\r\n" +
"--" + BOUNDARY_1 + "\r\n" +
"Content-Type: text/plain;\r\n" +
" title*1*=1234567891123456789212345678931234567894123456789\r\n" +
" title*2*=5123456789612345678971234567898123456789091234567890;\r\n" +
" charset=utf-8\r\n" +
"Content-Transfer-Encoding: quoted-printable\r\n" +
"\r\n" +
@ -124,10 +121,12 @@ public class MessageBuilderTest extends RobolectricTest {
"text =E2=98=AD\r\n" +
"--" + BOUNDARY_1 + "\r\n" +
"Content-Type: text/plain;\r\n" +
" name=\"attach.txt\"\r\n" +
" name*0*=UTF-8''~~~~~~~~~1~~~~~~~~~2~~~~~~~~~3~~~~~~~~~4~~~~~~~~~5~~~~~~~~~6~;\r\n" +
" name*1*=~~~~~~~~7.txt\r\n" +
"Content-Transfer-Encoding: base64\r\n" +
"Content-Disposition: attachment;\r\n" +
" filename=\"attach.txt\";\r\n" +
" filename*0*=UTF-8''~~~~~~~~~1~~~~~~~~~2~~~~~~~~~3~~~~~~~~~4~~~~~~~~~5~~~~~~~;\r\n" +
" filename*1*=~~6~~~~~~~~~7.txt;\r\n" +
" size=23\r\n" +
"\r\n" +
"dGV4dCBkYXRhIGluIGF0dGFjaG1lbnQ=\r\n" +
@ -148,10 +147,10 @@ public class MessageBuilderTest extends RobolectricTest {
"text =E2=98=AD\r\n" +
"--" + BOUNDARY_1 + "\r\n" +
"Content-Type: text/plain;\r\n" +
" name=\"=?UTF-8?B?44OG44K544OI5paH5pu4LnR4dA==?=\"\r\n" +
" name*=UTF-8''%E3%83%86%E3%82%B9%E3%83%88%E6%96%87%E6%9B%B8.txt\r\n" +
"Content-Transfer-Encoding: base64\r\n" +
"Content-Disposition: attachment;\r\n" +
" filename=\"=?UTF-8?B?44OG44K544OI5paH5pu4LnR4dA==?=\";\r\n" +
" filename*=UTF-8''%E3%83%86%E3%82%B9%E3%83%88%E6%96%87%E6%9B%B8.txt;\r\n" +
" size=23\r\n" +
"\r\n" +
"dGV4dCBkYXRhIGluIGF0dGFjaG1lbnQ=\r\n" +
@ -171,9 +170,9 @@ public class MessageBuilderTest extends RobolectricTest {
"text =E2=98=AD\r\n" +
"--" + BOUNDARY_1 + "\r\n" +
"Content-Type: message/rfc822;\r\n" +
" name=\"attach.txt\"\r\n" +
" name=attach.txt\r\n" +
"Content-Disposition: attachment;\r\n" +
" filename=\"attach.txt\";\r\n" +
" filename=attach.txt;\r\n" +
" size=23\r\n" +
"\r\n" +
"text data in attachment" +
@ -228,19 +227,19 @@ public class MessageBuilderTest extends RobolectricTest {
assertEquals(MESSAGE_HEADERS + MESSAGE_CONTENT_WITH_ATTACH, getMessageContents(message));
}
@Ignore("RFC2231/2184 not implemented") @Test
public void build_withAttachment_longContentType_shouldSucceed() throws Exception {
@Test
public void build_withAttachment_longFileName() throws Exception {
MessageBuilder messageBuilder = createSimpleMessageBuilder();
Attachment attachment = createAttachmentWithContent(
"text/plain;title=1234567891123456789212345678931234567894123456789" +
"5123456789612345678971234567898123456789091234567890",
"attach.txt", TEST_ATTACHMENT_TEXT);
"text/plain",
"~~~~~~~~~1~~~~~~~~~2~~~~~~~~~3~~~~~~~~~4~~~~~~~~~5~~~~~~~~~6~~~~~~~~~7.txt",
TEST_ATTACHMENT_TEXT);
messageBuilder.setAttachments(Collections.singletonList(attachment));
messageBuilder.buildAsync(callback);
MimeMessage message = getMessageFromCallback();
assertEquals(MESSAGE_HEADERS + MESSAGE_CONTENT_WITH_LONG_CONTENT_TYPE,
assertEquals(MESSAGE_HEADERS + MESSAGE_CONTENT_WITH_LONG_FILE_NAME,
getMessageContents(message));
}