Skip to content

Commit acbd121

Browse files
committed
Generate the correct WITH keywords when James receives SMTPUTF8.
1 parent 2a613d3 commit acbd121

2 files changed

Lines changed: 157 additions & 4 deletions

File tree

protocols/smtp/src/main/java/org/apache/james/protocols/smtp/core/ReceivedHeaderGenerator.java

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ public class ReceivedHeaderGenerator {
4040
private static final String ESMTPSA = "ESMTPSA";
4141
private static final String ESMTP = "ESMTP";
4242
private static final String ESMTPS = "ESMTPS";
43+
private static final String UTF8SMTP = "UTF8SMTP";
44+
private static final String UTF8SMTPA = "UTF8SMTPA";
45+
private static final String UTF8SMTPS = "UTF8SMTPS";
46+
private static final String UTF8SMTPSA = "UTF8SMTPSA";
4347
private final ProtocolSession.AttachmentKey<Integer> mtPriority = ProtocolSession.AttachmentKey.of("MT-PRIORITY", Integer.class);
4448

4549
/**
@@ -48,24 +52,32 @@ public class ReceivedHeaderGenerator {
4852
protected String getServiceType(SMTPSession session, String heloMode) {
4953
// Check if EHLO was used
5054
if (EHLO.equals(heloMode)) {
55+
// See RFC 6531 §4.3:
56+
// The new keyword "UTF8SMTP" indicates the use of ESMTP when
57+
// the SMTPUTF8 extension is also used; the "A" / "S" / "SA"
58+
// suffixes have the same meaning as in the E* keywords.
59+
boolean smtpUtf8 = session.getAttachment(SMTPSession.SMTPUTF8_REQUESTED, ProtocolSession.State.Transaction)
60+
.orElse(Boolean.FALSE);
5161
// Not successful auth
5262
if (session.getUsername() == null) {
5363
if (session.isTLSStarted()) {
54-
return ESMTPS;
64+
return smtpUtf8 ? UTF8SMTPS : ESMTPS;
5565
} else {
56-
return ESMTP;
66+
return smtpUtf8 ? UTF8SMTP : ESMTP;
5767
}
5868
} else {
5969
// See RFC3848:
6070
// The new keyword "ESMTPA" indicates the use of ESMTP when the SMTP
6171
// AUTH [3] extension is also used and authentication is successfully achieved.
6272
if (session.isTLSStarted()) {
63-
return ESMTPSA;
73+
return smtpUtf8 ? UTF8SMTPSA : ESMTPSA;
6474
} else {
65-
return ESMTPA;
75+
return smtpUtf8 ? UTF8SMTPA : ESMTPA;
6676
}
6777
}
6878
} else {
79+
// HELO was used (not EHLO), so SMTPUTF8 cannot have been
80+
// negotiated — the extension requires EHLO. Plain SMTP only.
6981
return SMTP;
7082
}
7183
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/****************************************************************
2+
* Licensed to the Apache Software Foundation (ASF) under one *
3+
* or more contributor license agreements. See the NOTICE file *
4+
* distributed with this work for additional information *
5+
* regarding copyright ownership. The ASF licenses this file *
6+
* to you under the Apache License, Version 2.0 (the *
7+
* "License"); you may not use this file except in compliance *
8+
* with the License. You may obtain a copy of the License at *
9+
* *
10+
* http://www.apache.org/licenses/LICENSE-2.0 *
11+
* *
12+
* Unless required by applicable law or agreed to in writing, *
13+
* software distributed under the License is distributed on an *
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
15+
* KIND, either express or implied. See the License for the *
16+
* specific language governing permissions and limitations *
17+
* under the License. *
18+
****************************************************************/
19+
20+
package org.apache.james.protocols.smtp.core;
21+
22+
import static org.assertj.core.api.Assertions.assertThat;
23+
24+
import java.util.Optional;
25+
26+
import org.apache.james.core.Username;
27+
import org.apache.james.protocols.api.ProtocolSession.AttachmentKey;
28+
import org.apache.james.protocols.api.ProtocolSession.State;
29+
import org.apache.james.protocols.smtp.SMTPSession;
30+
import org.apache.james.protocols.smtp.utils.BaseFakeSMTPSession;
31+
import org.junit.jupiter.api.Test;
32+
33+
/**
34+
* Covers RFC 6531 §4.3 — the UTF8SMTP / UTF8SMTPA / UTF8SMTPS / UTF8SMTPSA
35+
* trace keywords used in the Received header when the transaction asserted
36+
* SMTPUTF8.
37+
*/
38+
class ReceivedHeaderGeneratorTest {
39+
40+
private static final ReceivedHeaderGenerator generator = new ReceivedHeaderGenerator();
41+
42+
private static String serviceTypeFor(String heloMode, boolean tls, boolean authenticated, boolean smtpUtf8) {
43+
SMTPSession session = new FakeSession(tls, authenticated, smtpUtf8);
44+
// getServiceType is protected; we exercise it through a thin
45+
// subclass that exposes it.
46+
return new ReceivedHeaderGenerator() {
47+
String invoke() {
48+
return getServiceType(session, heloMode);
49+
}
50+
}.invoke();
51+
}
52+
53+
// --- HELO (no extensions can have been negotiated) ---
54+
55+
@Test
56+
void heloShouldYieldSmtp() {
57+
assertThat(serviceTypeFor("HELO", false, false, false)).isEqualTo("SMTP");
58+
}
59+
60+
@Test
61+
void heloShouldYieldSmtpEvenWhenSmtpUtf8FlagIsSet() {
62+
// The flag should only ever be set after EHLO + an SMTPUTF8
63+
// parameter, but we double-check the HELO branch ignores it.
64+
assertThat(serviceTypeFor("HELO", false, false, true)).isEqualTo("SMTP");
65+
}
66+
67+
// --- EHLO without SMTPUTF8: existing RFC 3848 keywords ---
68+
69+
@Test
70+
void ehloShouldYieldEsmtp() {
71+
assertThat(serviceTypeFor("EHLO", false, false, false)).isEqualTo("ESMTP");
72+
}
73+
74+
@Test
75+
void ehloAuthenticatedShouldYieldEsmtpa() {
76+
assertThat(serviceTypeFor("EHLO", false, true, false)).isEqualTo("ESMTPA");
77+
}
78+
79+
@Test
80+
void ehloOverTlsShouldYieldEsmtps() {
81+
assertThat(serviceTypeFor("EHLO", true, false, false)).isEqualTo("ESMTPS");
82+
}
83+
84+
@Test
85+
void ehloOverTlsAuthenticatedShouldYieldEsmtpsa() {
86+
assertThat(serviceTypeFor("EHLO", true, true, false)).isEqualTo("ESMTPSA");
87+
}
88+
89+
// --- EHLO with SMTPUTF8: RFC 6531 §4.3 keywords ---
90+
91+
@Test
92+
void ehloWithSmtpUtf8ShouldYieldUtf8Smtp() {
93+
assertThat(serviceTypeFor("EHLO", false, false, true)).isEqualTo("UTF8SMTP");
94+
}
95+
96+
@Test
97+
void ehloAuthenticatedWithSmtpUtf8ShouldYieldUtf8Smtpa() {
98+
assertThat(serviceTypeFor("EHLO", false, true, true)).isEqualTo("UTF8SMTPA");
99+
}
100+
101+
@Test
102+
void ehloOverTlsWithSmtpUtf8ShouldYieldUtf8Smtps() {
103+
assertThat(serviceTypeFor("EHLO", true, false, true)).isEqualTo("UTF8SMTPS");
104+
}
105+
106+
@Test
107+
void ehloOverTlsAuthenticatedWithSmtpUtf8ShouldYieldUtf8Smtpsa() {
108+
assertThat(serviceTypeFor("EHLO", true, true, true)).isEqualTo("UTF8SMTPSA");
109+
}
110+
111+
private static class FakeSession extends BaseFakeSMTPSession {
112+
private final boolean tls;
113+
private final Username username;
114+
private final boolean smtpUtf8;
115+
116+
FakeSession(boolean tls, boolean authenticated, boolean smtpUtf8) {
117+
this.tls = tls;
118+
this.username = authenticated ? Username.of("alice@example.com") : null;
119+
this.smtpUtf8 = smtpUtf8;
120+
}
121+
122+
@Override
123+
public boolean isTLSStarted() {
124+
return tls;
125+
}
126+
127+
@Override
128+
public Username getUsername() {
129+
return username;
130+
}
131+
132+
@Override
133+
@SuppressWarnings("unchecked")
134+
public <T> Optional<T> getAttachment(AttachmentKey<T> key, State state) {
135+
if (key == SMTPSession.SMTPUTF8_REQUESTED) {
136+
return (Optional<T>) Optional.of(smtpUtf8);
137+
}
138+
return Optional.empty();
139+
}
140+
}
141+
}

0 commit comments

Comments
 (0)