Merge pull request #5511 from k9mail/fix_identity_header

Fold identity header value so we're not generating invalid messages
This commit is contained in:
cketti 2021-08-04 15:36:00 +02:00 committed by GitHub
commit 97891d15e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 130 additions and 7 deletions

View file

@ -3,16 +3,20 @@ package com.fsck.k9.message;
import android.net.Uri;
import android.net.Uri.Builder;
import timber.log.Timber;
import com.fsck.k9.Account.QuoteStyle;
import com.fsck.k9.Identity;
import com.fsck.k9.K9;
import com.fsck.k9.controller.MessageReference;
import com.fsck.k9.mail.internet.TextBody;
import com.fsck.k9.message.quote.InsertableHtmlContent;
import timber.log.Timber;
public class IdentityHeaderBuilder {
private static final int MAX_LINE_LENGTH = 72;
private static final int FIRST_LINE_EXTRA_LENGTH = K9.IDENTITY_HEADER.length() + 2;
private InsertableHtmlContent quotedHtmlContent;
private QuoteStyle quoteStyle;
private SimpleMessageFormat messageFormat;
@ -95,9 +99,33 @@ public class IdentityHeaderBuilder {
appendValue(IdentityField.QUOTED_TEXT_MODE, quotedTextMode);
String k9identity = IdentityField.IDENTITY_VERSION_1 + uri.build().getEncodedQuery();
String headerValue = foldHeaderValue(k9identity);
Timber.d("Generated identity: %s", k9identity);
return k9identity;
Timber.d("Generated identity: %s", headerValue);
return headerValue;
}
private String foldHeaderValue(String input) {
int inputLength = input.length();
int endOfFirstLine = MAX_LINE_LENGTH - FIRST_LINE_EXTRA_LENGTH;
if (inputLength <= endOfFirstLine) {
return input;
}
int extraLines = (inputLength - endOfFirstLine - 1) / (MAX_LINE_LENGTH - 1) + 1;
int builderSize = inputLength + extraLines * 3 /* CR LF SPACE */;
StringBuilder headerValue = new StringBuilder(builderSize);
headerValue.append(input, 0, endOfFirstLine);
int start = endOfFirstLine;
while (start < inputLength) {
headerValue.append("\r\n ");
int end = start + Math.min(MAX_LINE_LENGTH - 1, inputLength - start);
headerValue.append(input, start, end);
start = end;
}
return headerValue.toString();
}
private void appendValue(IdentityField field, int value) {

View file

@ -29,10 +29,12 @@ public class IdentityHeaderParser {
return identity;
}
String encodedString = unfoldHeaderValue(identityString);
// Check to see if this is a "next gen" identity.
if (identityString.charAt(0) == IdentityField.IDENTITY_VERSION_1.charAt(0) && identityString.length() > 2) {
if (encodedString.charAt(0) == IdentityField.IDENTITY_VERSION_1.charAt(0) && encodedString.length() > 2) {
Uri.Builder builder = new Uri.Builder();
builder.encodedQuery(identityString.substring(1)); // Need to cut off the ! at the beginning.
builder.encodedQuery(encodedString.substring(1)); // Need to cut off the ! at the beginning.
Uri uri = builder.build();
for (IdentityField key : IdentityField.values()) {
String value = uri.getQueryParameter(key.value());
@ -56,9 +58,9 @@ public class IdentityHeaderParser {
} else {
// Legacy identity
Timber.d("Got a saved legacy identity: %s", identityString);
Timber.d("Got a saved legacy identity: %s", encodedString);
StringTokenizer tokenizer = new StringTokenizer(identityString, ":", false);
StringTokenizer tokenizer = new StringTokenizer(encodedString, ":", false);
// First item is the body length. We use this to separate the composed reply from the quoted text.
if (tokenizer.hasMoreTokens()) {
@ -85,4 +87,8 @@ public class IdentityHeaderParser {
return identity;
}
private static String unfoldHeaderValue(String identityString) {
return identityString.replaceAll("\r?\n ", "");
}
}

View file

@ -0,0 +1,56 @@
package com.fsck.k9.message
import com.fsck.k9.Account.QuoteStyle
import com.fsck.k9.Identity
import com.fsck.k9.RobolectricTest
import com.fsck.k9.mail.internet.MimeHeaderChecker
import com.fsck.k9.mail.internet.TextBody
import com.google.common.truth.Truth.assertThat
import org.junit.Test
private const val IDENTITY_HEADER = "X-K9mail-Identity"
class IdentityHeaderBuilderTest : RobolectricTest() {
@Test
fun `valid unstructured header field value`() {
val signature = "a".repeat(1000)
val identityHeader = IdentityHeaderBuilder()
.setCursorPosition(0)
.setIdentity(createIdentity(signatureUse = true))
.setIdentityChanged(false)
.setMessageFormat(SimpleMessageFormat.TEXT)
.setMessageReference(null)
.setQuotedHtmlContent(null)
.setQuoteStyle(QuoteStyle.PREFIX)
.setQuoteTextMode(QuotedTextMode.NONE)
.setSignature(signature)
.setSignatureChanged(true)
.setBody(TextBody("irrelevant"))
.setBodyPlain(null)
.build()
assertThat(identityHeader.length).isGreaterThan(1000)
assertIsValidHeader(identityHeader)
}
private fun assertIsValidHeader(identityHeader: String) {
try {
MimeHeaderChecker.checkHeader(IDENTITY_HEADER, identityHeader)
} catch (e: Exception) {
println("$IDENTITY_HEADER: $identityHeader")
throw e
}
}
private fun createIdentity(
description: String? = null,
name: String? = null,
email: String? = null,
signature: String? = null,
signatureUse: Boolean = false,
replyTo: String? = null
): Identity {
return Identity(description, name, email, signature, signatureUse, replyTo)
}
}

View file

@ -0,0 +1,33 @@
package com.fsck.k9.message
import com.fsck.k9.RobolectricTest
import com.fsck.k9.helper.toCrLf
import com.google.common.truth.Truth.assertThat
import org.junit.Test
class IdentityHeaderParserTest : RobolectricTest() {
@Test
fun `folded header value`() {
val input = """
|!l=10&o=0&qs=PREFIX&f=TEXT&s=aaaaaaaaaaaaaaaaaaaaaaaa
| aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
| aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
| aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
| aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
| aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
| aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
| aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
| aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
| aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
| aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
| aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
| aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
| aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
| aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&p=0&q=NONE
""".trimMargin().toCrLf()
val result = IdentityHeaderParser.parse(input)
assertThat(result).containsEntry(IdentityField.SIGNATURE, "a".repeat(1000))
}
}