(geronimo-mail) branch main updated: GERONIMO-6906 - Implement the Jakarta Mail 2.1 API additions: StreamProvider SPI, Session.getStreamProvider(), MimeUtility.getBytes, volatile Service.url (fixes all 18 signature test errors)
[email protected] Sat, 18 Jul 2026 19:20:37 +0000
| Newsgroups | gmane.comp.java.geronimo.cvs |
|---|---|
| Message-ID | <178440243775.3021926.17656210147498266698@gitbox3-he-fi.apache.org> |
This is an automated email from the ASF dual-hosted git repository.
rzo1 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/geronimo-mail.git
The following commit(s) were added to refs/heads/main by this push:
new 5407069 GERONIMO-6906 - Implement the Jakarta Mail 2.1 API additions: StreamProvider SPI, Session.getStreamProvider(), MimeUtility.getBytes, volatile Service.url (fixes all 18 signature test errors)
5407069 is described below
commit 540706986bccc8481a94eab0a079e5a84fee472a
Author: Richard Zowalla <[email protected]>
AuthorDate: Sat Jul 18 21:19:38 2026 +0200
GERONIMO-6906 - Implement the Jakarta Mail 2.1 API additions: StreamProvider SPI, Session.getStreamProvider(), MimeUtility.getBytes, volatile Service.url (fixes all 18 signature test errors)
---
.../src/main/java/jakarta/mail/BodyPart.java | 10 +
.../src/main/java/jakarta/mail/Multipart.java | 11 +
.../src/main/java/jakarta/mail/Service.java | 2 +-
.../src/main/java/jakarta/mail/Session.java | 16 ++
.../java/jakarta/mail/internet/MimeUtility.java | 48 +++++
.../main/java/jakarta/mail/util/FactoryFinder.java | 129 ++++++++++++
.../java/jakarta/mail/util/LineInputStream.java | 44 ++++
.../java/jakarta/mail/util/LineOutputStream.java | 57 ++++++
.../java/jakarta/mail/util/StreamProvider.java | 226 +++++++++++++++++++++
.../geronimo/mail/util/MailLineInputStream.java | 103 ++++++++++
.../geronimo/mail/util/MailLineOutputStream.java | 81 ++++++++
.../geronimo/mail/util/MailStreamProvider.java | 117 +++++++++++
.../apache/geronimo/mail/util/QDecoderStream.java | 79 +++++++
.../apache/geronimo/mail/util/QEncoderStream.java | 89 ++++++++
.../services/jakarta.mail.util.StreamProvider | 1 +
geronimo-mail_2.1_tck/src/tck/geronimo.jtx | 6 -
16 files changed, 1012 insertions(+), 7 deletions(-)
diff --git a/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/BodyPart.java b/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/BodyPart.java
index 956496e..5920427 100644
--- a/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/BodyPart.java
+++ b/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/BodyPart.java
@@ -19,11 +19,21 @@
package jakarta.mail;
+import jakarta.mail.util.StreamProvider;
+
/**
* @version $Rev$ $Date$
*/
public abstract class BodyPart implements Part {
+ /**
+ * The stream provider this part uses for its encoder, decoder and
+ * line-oriented streams, resolved once at construction time.
+ *
+ * @since JavaMail 2.1
+ */
+ protected final StreamProvider streamProvider = StreamProvider.provider();
+
protected Multipart parent;
public Multipart getParent() {
diff --git a/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/Multipart.java b/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/Multipart.java
index 5407b38..7835030 100644
--- a/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/Multipart.java
+++ b/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/Multipart.java
@@ -23,12 +23,23 @@ import java.io.IOException;
import java.io.OutputStream;
import java.util.Vector;
+import jakarta.mail.util.StreamProvider;
+
/**
* A container for multiple {@link BodyPart BodyParts}.
*
* @version $Rev$ $Date$
*/
public abstract class Multipart {
+
+ /**
+ * The stream provider this part uses for its encoder, decoder and
+ * line-oriented streams, resolved once at construction time.
+ *
+ * @since JavaMail 2.1
+ */
+ protected final StreamProvider streamProvider = StreamProvider.provider();
+
/**
* Vector of sub-parts.
*/
diff --git a/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/Service.java b/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/Service.java
index 0070c95..d617c18 100644
--- a/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/Service.java
+++ b/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/Service.java
@@ -40,7 +40,7 @@ public abstract class Service implements AutoCloseable {
/**
* The URLName of this service
*/
- protected URLName url;
+ protected volatile URLName url;
/**
* Debug flag for this service, set from the Session's debug flag.
*/
diff --git a/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/Session.java b/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/Session.java
index 1fcb1b6..6f1ed8c 100644
--- a/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/Session.java
+++ b/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/Session.java
@@ -40,6 +40,8 @@ import java.util.Properties;
import java.util.StringTokenizer;
import java.util.WeakHashMap;
+import jakarta.mail.util.StreamProvider;
+
import org.apache.geronimo.mail.MailProviderRegistry;
import org.apache.geronimo.osgi.locator.ProviderLocator;
@@ -65,6 +67,7 @@ public final class Session {
private final Properties properties;
private final Authenticator authenticator;
+ private final StreamProvider streamProvider;
private boolean debug;
private PrintStream debugOut = System.out;
@@ -76,9 +79,22 @@ public final class Session {
private Session(final Properties properties, final Authenticator authenticator) {
this.properties = properties;
this.authenticator = authenticator;
+ this.streamProvider = StreamProvider.provider();
debug = Boolean.valueOf(properties.getProperty("mail.debug")).booleanValue();
}
+ /**
+ * Returns the stream provider this session resolved at creation time,
+ * giving access to the encoder, decoder and line-oriented streams of
+ * the underlying implementation.
+ *
+ * @return the session's stream provider
+ * @since JavaMail 2.1
+ */
+ public StreamProvider getStreamProvider() {
+ return streamProvider;
+ }
+
/**
* Create a new session initialized with the supplied properties which uses the supplied authenticator.
* Clients should ensure the properties listed in Appendix A of the JavaMail specification are
diff --git a/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/internet/MimeUtility.java b/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/internet/MimeUtility.java
index 5c0311d..49231fe 100644
--- a/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/internet/MimeUtility.java
+++ b/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/internet/MimeUtility.java
@@ -20,6 +20,7 @@
package jakarta.mail.internet;
import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -1329,6 +1330,53 @@ public class MimeUtility {
static final boolean nonascii (int a){
return a >= 0177 || (a < 040 && a != '\r' && a != '\n' && a != '\t');
}
+
+ /**
+ * Convert a string to a byte array by taking the low-order 8 bits of
+ * each character. The string is expected to contain only US-ASCII
+ * characters.
+ *
+ * @param s the string to convert
+ * @return the byte representation of the string
+ * @since JavaMail 2.1
+ */
+ public static byte[] getBytes(final String s) {
+ final char[] chars = s.toCharArray();
+ final int size = chars.length;
+ final byte[] bytes = new byte[size];
+
+ for (int i = 0; i < size; i++) {
+ bytes[i] = (byte) chars[i];
+ }
+ return bytes;
+ }
+
+ /**
+ * Read all of the data from the given InputStream into a byte array.
+ *
+ * @param is the InputStream to read
+ * @return a byte array containing the fully read data
+ * @exception IOException for errors reading the stream
+ * @since JavaMail 2.1
+ */
+ public static byte[] getBytes(final InputStream is) throws IOException {
+ int len;
+ int size = 1024;
+ byte[] buf;
+ if (is instanceof ByteArrayInputStream) {
+ size = is.available();
+ buf = new byte[size];
+ len = is.read(buf, 0, size);
+ } else {
+ final ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ buf = new byte[size];
+ while ((len = is.read(buf, 0, size)) != -1) {
+ bos.write(buf, 0, len);
+ }
+ buf = bos.toByteArray();
+ }
+ return buf;
+ }
}
diff --git a/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/util/FactoryFinder.java b/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/util/FactoryFinder.java
new file mode 100644
index 0000000..af55a6f
--- /dev/null
+++ b/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/util/FactoryFinder.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 jakarta.mail.util;
+
+import java.util.Iterator;
+import java.util.ServiceLoader;
+
+/**
+ * Simple service lookup helper for locating a {@link StreamProvider}
+ * implementation. The lookup order is:
+ * <ol>
+ * <li>a system property naming the implementation class (the property
+ * name is the factory interface name),</li>
+ * <li>the standard {@link ServiceLoader} mechanism, tried with the
+ * thread context class loader, the class loader of the factory
+ * interface and the system class loader,</li>
+ * <li>the built-in default implementation shipped with this bundle.</li>
+ * </ol>
+ */
+class FactoryFinder {
+
+ private static final String DEFAULT_PROVIDER = "org.apache.geronimo.mail.util.MailStreamProvider";
+
+ private FactoryFinder() {
+ }
+
+ /**
+ * Finds an implementation of the given factory type.
+ *
+ * @param factoryClass factory abstract class or interface to be found
+ * @return an instance of the factory implementation
+ * @throws IllegalStateException if no implementation can be located or instantiated
+ */
+ static <T> T find(final Class<T> factoryClass) {
+ // a system property naming the implementation class always wins.
+ final String className = System.getProperty(factoryClass.getName());
+ if (className != null) {
+ final T result = newInstance(className, factoryClass);
+ if (result != null) {
+ return result;
+ }
+ }
+
+ // regular ServiceLoader lookup, using the different candidate class loaders.
+ final ClassLoader[] loaders = new ClassLoader[] {
+ Thread.currentThread().getContextClassLoader(),
+ factoryClass.getClassLoader(),
+ ClassLoader.getSystemClassLoader()
+ };
+
+ for (final ClassLoader loader : loaders) {
+ if (loader == null) {
+ continue;
+ }
+ final T result = fromServiceLoader(factoryClass, loader);
+ if (result != null) {
+ return result;
+ }
+ }
+
+ // fall back to the default implementation included in this bundle. This keeps
+ // the API functional in environments where the ServiceLoader mechanism does not
+ // work (e.g. some OSGi containers).
+ final T result = newInstance(DEFAULT_PROVIDER, factoryClass);
+ if (result != null) {
+ return result;
+ }
+
+ throw new IllegalStateException("No provider of " + factoryClass.getName() + " was found");
+ }
+
+ private static <T> T newInstance(final String className, final Class<T> factoryClass) {
+ try {
+ ClassLoader loader = Thread.currentThread().getContextClassLoader();
+ if (loader == null) {
+ loader = factoryClass.getClassLoader();
+ }
+ if (loader == null) {
+ loader = ClassLoader.getSystemClassLoader();
+ }
+ Class<?> clazz;
+ try {
+ clazz = Class.forName(className, false, loader);
+ } catch (final ClassNotFoundException e) {
+ // retry with the loader of the factory class itself (e.g. the TCCL
+ // cannot see the implementation classes).
+ clazz = Class.forName(className, false, factoryClass.getClassLoader());
+ }
+ return clazz.asSubclass(factoryClass).getConstructor().newInstance();
+ } catch (final ClassCastException wrongLoader) {
+ return null;
+ } catch (final ReflectiveOperationException e) {
+ throw new IllegalStateException("Cannot instantiate " + className, e);
+ }
+ }
+
+ private static <T> T fromServiceLoader(final Class<T> factoryClass, final ClassLoader loader) {
+ try {
+ final ServiceLoader<T> serviceLoader = ServiceLoader.load(factoryClass, loader);
+ final Iterator<T> iterator = serviceLoader.iterator();
+ if (iterator.hasNext()) {
+ return factoryClass.cast(iterator.next());
+ }
+ return null;
+ } catch (final ClassCastException wrongLoader) {
+ return null;
+ } catch (final Throwable t) {
+ // e.g. a ServiceConfigurationError; ignore and try the next mechanism.
+ return null;
+ }
+ }
+}
diff --git a/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/util/LineInputStream.java b/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/util/LineInputStream.java
new file mode 100644
index 0000000..9e7955a
--- /dev/null
+++ b/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/util/LineInputStream.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 jakarta.mail.util;
+
+import java.io.IOException;
+
+/**
+ * A reader that splits an underlying byte stream into text lines, in the
+ * way mail protocols and RFC 822 header blocks require. Depending on how
+ * the implementation was configured, line content is interpreted either
+ * as US-ASCII or as UTF-8.
+ *
+ * @since JavaMail 2.1
+ */
+public interface LineInputStream {
+
+ /**
+ * Returns the next line from the underlying stream, or null once the
+ * stream is exhausted. Any of CR, LF or CR LF ends a line (the
+ * malformed CR CR LF sequence produced by some agents is tolerated as
+ * well), and the terminator itself is never part of the result.
+ *
+ * @return the next line, without its terminator, or null at end of data
+ * @throws IOException if reading the underlying stream fails
+ */
+ String readLine() throws IOException;
+}
diff --git a/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/util/LineOutputStream.java b/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/util/LineOutputStream.java
new file mode 100644
index 0000000..f5de63f
--- /dev/null
+++ b/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/util/LineOutputStream.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 jakarta.mail.util;
+
+import java.io.IOException;
+
+/**
+ * A writer that turns strings into CRLF-terminated byte sequences on an
+ * underlying stream, as needed when generating RFC 822 header blocks and
+ * similar protocol data. Unless the implementation was configured to
+ * permit UTF-8, the strings are expected to be pure US-ASCII.
+ *
+ * @since JavaMail 2.1
+ */
+public interface LineOutputStream {
+
+ /**
+ * Emits the given string followed by a CRLF terminator.
+ *
+ * @param s the line content to emit
+ * @throws IOException if writing to the underlying stream fails
+ */
+ void writeln(String s) throws IOException;
+
+ /**
+ * Emits a bare CRLF terminator, producing an empty line.
+ *
+ * @throws IOException if writing to the underlying stream fails
+ */
+ void writeln() throws IOException;
+
+ /**
+ * Copies the given bytes to the underlying stream unchanged, without
+ * appending a terminator.
+ *
+ * @param content the bytes to copy
+ * @throws IOException if writing to the underlying stream fails
+ */
+ void write(byte[] content) throws IOException;
+}
diff --git a/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/util/StreamProvider.java b/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/util/StreamProvider.java
new file mode 100644
index 0000000..8a1664e
--- /dev/null
+++ b/geronimo-mail_2.1_spec/src/main/java/jakarta/mail/util/StreamProvider.java
@@ -0,0 +1,226 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 jakarta.mail.util;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.ServiceLoader;
+
+/**
+ * Factory for the transfer-encoding and line-oriented streams that a
+ * Jakarta Mail implementation needs. Implementations of this interface
+ * are discovered through the service-loader mechanism; application code
+ * usually obtains an instance from {@link jakarta.mail.Session#getStreamProvider}
+ * rather than performing the lookup itself.
+ *
+ * @since JavaMail 2.1
+ */
+public interface StreamProvider {
+
+ /**
+ * The content-transfer-encoding names understood by the Mail API,
+ * each paired with its wire label.
+ *
+ * @since JavaMail 2.1
+ */
+ enum EncoderTypes {
+
+ BASE_64("base64"),
+ B_ENCODER("b"),
+ Q_ENCODER("q"),
+ BINARY_ENCODER("binary"),
+ BIT7_ENCODER("7bit"),
+ BIT8_ENCODER("8bit"),
+ QUOTED_PRINTABLE_ENCODER("quoted-printable"),
+ UU_ENCODER("uuencode"),
+ X_UU_ENCODER("x-uuencode"),
+ X_UUE("x-uue");
+
+ private final String encoder;
+
+ EncoderTypes(final String encoder) {
+ this.encoder = encoder;
+ }
+
+ public String getEncoder() {
+ return encoder;
+ }
+ }
+
+ /**
+ * Wraps a stream carrying base64 data so that reads deliver the
+ * decoded bytes.
+ *
+ * @param in the stream carrying base64 data
+ * @return a decoding stream
+ */
+ InputStream inputBase64(InputStream in);
+
+ /**
+ * Wraps a stream so that bytes written to it are emitted in
+ * base64 form.
+ *
+ * @param out the target stream for the encoded data
+ * @return an encoding stream
+ */
+ OutputStream outputBase64(OutputStream out);
+
+ /**
+ * Returns a pass-through reader for the identity encodings
+ * ("binary", "7bit" and "8bit"), which need no transformation.
+ *
+ * @param in the stream to read from
+ * @return a stream delivering the data unchanged
+ */
+ InputStream inputBinary(InputStream in);
+
+ /**
+ * Returns a pass-through writer for the identity encodings
+ * ("binary", "7bit" and "8bit"), which need no transformation.
+ *
+ * @param out the stream to write to
+ * @return a stream passing the data through unchanged
+ */
+ OutputStream outputBinary(OutputStream out);
+
+ /**
+ * Wraps a stream so that written bytes are emitted using the
+ * RFC 2047 "B" encoding (base64 for encoded words).
+ *
+ * @param out the target stream for the encoded data
+ * @return an encoding stream
+ */
+ OutputStream outputB(OutputStream out);
+
+ /**
+ * Wraps a stream carrying RFC 2047 "Q"-encoded data so that reads
+ * deliver the decoded bytes.
+ *
+ * @param in the stream carrying the encoded data
+ * @return a decoding stream
+ */
+ InputStream inputQ(InputStream in);
+
+ /**
+ * Wraps a stream so that written bytes are emitted using the
+ * RFC 2047 "Q" encoding.
+ *
+ * @param out the target stream for the encoded data
+ * @param encodingWord whether the data forms a word in a phrase,
+ * which tightens the set of characters that may
+ * appear unencoded
+ * @return an encoding stream
+ */
+ OutputStream outputQ(OutputStream out, boolean encodingWord);
+
+ /**
+ * Returns a reader that splits the underlying stream into lines,
+ * as needed for mail protocol and header parsing.
+ *
+ * @param in the stream to read from
+ * @param allowutf8 whether line content may be interpreted as UTF-8
+ * instead of pure US-ASCII
+ * @return the line-oriented reader
+ */
+ LineInputStream inputLineStream(InputStream in, boolean allowutf8);
+
+ /**
+ * Returns a writer that emits strings as CRLF-terminated lines,
+ * as needed for generating mail protocol data and headers.
+ *
+ * @param out the stream to write to
+ * @param allowutf8 whether line content may be written as UTF-8
+ * instead of pure US-ASCII
+ * @return the line-oriented writer
+ */
+ LineOutputStream outputLineStream(OutputStream out, boolean allowutf8);
+
+ /**
+ * Wraps a stream carrying quoted-printable data so that reads
+ * deliver the decoded bytes.
+ *
+ * @param in the stream carrying the encoded data
+ * @return a decoding stream
+ */
+ InputStream inputQP(InputStream in);
+
+ /**
+ * Wraps a stream so that written bytes are emitted in
+ * quoted-printable form.
+ *
+ * @param out the target stream for the encoded data
+ * @return an encoding stream
+ */
+ OutputStream outputQP(OutputStream out);
+
+ /**
+ * Returns a stream over the given byte array whose underlying data
+ * may be shared by several concurrent readers.
+ *
+ * @param buff the bytes to read
+ * @return a stream over the shared data
+ */
+ InputStream inputSharedByteArray(byte[] buff);
+
+ /**
+ * Wraps a stream carrying uuencoded data (any of the "uuencode",
+ * "x-uuencode" or "x-uue" labels) so that reads deliver the decoded
+ * bytes.
+ *
+ * @param in the stream carrying the encoded data
+ * @return a decoding stream
+ */
+ InputStream inputUU(InputStream in);
+
+ /**
+ * Wraps a stream so that written bytes are emitted in uuencoded
+ * form (the "uuencode", "x-uuencode" and "x-uue" labels).
+ *
+ * @param out the target stream for the encoded data
+ * @param filename an optional name to record in the encoded
+ * preamble, may be null
+ * @return an encoding stream
+ */
+ OutputStream outputUU(OutputStream out, String filename);
+
+ /**
+ * Locates and returns a {@link StreamProvider} implementation. The
+ * lookup honors a system property naming the implementation class,
+ * then the {@link ServiceLoader} mechanism, and finally falls back
+ * to the provider bundled with this specification jar. Callers are
+ * encouraged to reuse the returned instance instead of repeating
+ * the lookup.
+ *
+ * @return the stream provider implementation
+ */
+ static StreamProvider provider() {
+ if (System.getSecurityManager() != null) {
+ return AccessController.doPrivileged(new PrivilegedAction<StreamProvider>() {
+ public StreamProvider run() {
+ return FactoryFinder.find(StreamProvider.class);
+ }
+ });
+ } else {
+ return FactoryFinder.find(StreamProvider.class);
+ }
+ }
+}
diff --git a/geronimo-mail_2.1_spec/src/main/java/org/apache/geronimo/mail/util/MailLineInputStream.java b/geronimo-mail_2.1_spec/src/main/java/org/apache/geronimo/mail/util/MailLineInputStream.java
new file mode 100644
index 0000000..e0a7df6
--- /dev/null
+++ b/geronimo-mail_2.1_spec/src/main/java/org/apache/geronimo/mail/util/MailLineInputStream.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.geronimo.mail.util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PushbackInputStream;
+import java.nio.charset.StandardCharsets;
+
+import jakarta.mail.util.LineInputStream;
+
+/**
+ * Default implementation of {@link jakarta.mail.util.LineInputStream} that
+ * reads lines terminated by CR, NL, CR-NL (or the common error case CR-CR-NL)
+ * from an input stream. The terminator is not part of the returned string.
+ */
+public class MailLineInputStream extends FilterInputStream implements LineInputStream {
+
+ // do we decode the raw bytes as UTF-8 (true) or as an 8-bit charset (false)?
+ private final boolean allowutf8;
+
+ public MailLineInputStream(final InputStream in) {
+ this(in, false);
+ }
+
+ public MailLineInputStream(final InputStream in, final boolean allowutf8) {
+ // we need pushback capability to handle a lone CR line terminator followed
+ // by real data.
+ super(in instanceof PushbackInputStream ? in : new PushbackInputStream(in, 2));
+ this.allowutf8 = allowutf8;
+ }
+
+ /**
+ * Read a line terminated by a CR, NL or CR-NL sequence (a common error
+ * case, CR-CR-NL, also terminates the line). The line terminator is not
+ * returned as part of the string.
+ *
+ * @return the next line from the stream, or null if no data is available.
+ * @exception IOException for input errors.
+ */
+ @Override
+ public String readLine() throws IOException {
+ final ByteArrayOutputStream lineBuffer = new ByteArrayOutputStream(128);
+ final PushbackInputStream in = (PushbackInputStream) this.in;
+
+ int ch = in.read();
+ // no data at all available? this is the end-of-data marker.
+ if (ch == -1) {
+ return null;
+ }
+
+ while (ch != -1) {
+ // a new-line character is always an unconditional terminator.
+ if (ch == '\n') {
+ break;
+ }
+ if (ch == '\r') {
+ // check what follows the CR. A NL is consumed as part of the
+ // terminator, anything else is pushed back.
+ int next = in.read();
+ boolean twoCRs = false;
+ if (next == '\r') {
+ // the common error case of a CR-CR-NL sequence.
+ twoCRs = true;
+ next = in.read();
+ }
+ if (next != '\n') {
+ if (next != -1) {
+ in.unread(next);
+ }
+ if (twoCRs) {
+ in.unread('\r');
+ }
+ }
+ break;
+ }
+ lineBuffer.write(ch);
+ ch = in.read();
+ }
+
+ final byte[] bytes = lineBuffer.toByteArray();
+ return new String(bytes, 0, bytes.length, allowutf8 ? StandardCharsets.UTF_8 : StandardCharsets.ISO_8859_1);
+ }
+}
diff --git a/geronimo-mail_2.1_spec/src/main/java/org/apache/geronimo/mail/util/MailLineOutputStream.java b/geronimo-mail_2.1_spec/src/main/java/org/apache/geronimo/mail/util/MailLineOutputStream.java
new file mode 100644
index 0000000..178c520
--- /dev/null
+++ b/geronimo-mail_2.1_spec/src/main/java/org/apache/geronimo/mail/util/MailLineOutputStream.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.geronimo.mail.util;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+
+import jakarta.mail.util.LineOutputStream;
+
+/**
+ * Default implementation of {@link jakarta.mail.util.LineOutputStream} that
+ * writes out strings as a sequence of bytes terminated by a CRLF sequence.
+ */
+public class MailLineOutputStream extends FilterOutputStream implements LineOutputStream {
+
+ private static final byte[] CRLF = { (byte) '\r', (byte) '\n' };
+
+ // do we encode strings as UTF-8 (true) or by truncating each char to 8 bits (false)?
+ private final boolean allowutf8;
+
+ public MailLineOutputStream(final OutputStream out) {
+ this(out, false);
+ }
+
+ public MailLineOutputStream(final OutputStream out, final boolean allowutf8) {
+ super(out);
+ this.allowutf8 = allowutf8;
+ }
+
+ @Override
+ public void writeln(final String s) throws IOException {
+ write(getBytes(s));
+ writeln();
+ }
+
+ @Override
+ public void writeln() throws IOException {
+ out.write(CRLF);
+ }
+
+ @Override
+ public void write(final byte[] content) throws IOException {
+ out.write(content);
+ }
+
+ private byte[] getBytes(final String s) {
+ if (s == null || s.length() == 0) {
+ return new byte[0];
+ }
+ if (allowutf8) {
+ return s.getBytes(StandardCharsets.UTF_8);
+ }
+ // the string is expected to contain only US-ASCII characters, so just
+ // take the low-order 8 bits of each character.
+ final char[] chars = s.toCharArray();
+ final byte[] bytes = new byte[chars.length];
+ for (int i = 0; i < chars.length; i++) {
+ bytes[i] = (byte) chars[i];
+ }
+ return bytes;
+ }
+}
diff --git a/geronimo-mail_2.1_spec/src/main/java/org/apache/geronimo/mail/util/MailStreamProvider.java b/geronimo-mail_2.1_spec/src/main/java/org/apache/geronimo/mail/util/MailStreamProvider.java
new file mode 100644
index 0000000..1ecf834
--- /dev/null
+++ b/geronimo-mail_2.1_spec/src/main/java/org/apache/geronimo/mail/util/MailStreamProvider.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.geronimo.mail.util;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import jakarta.mail.util.LineInputStream;
+import jakarta.mail.util.LineOutputStream;
+import jakarta.mail.util.SharedByteArrayInputStream;
+import jakarta.mail.util.StreamProvider;
+
+/**
+ * Default {@link StreamProvider} implementation backed by the encoder and
+ * decoder streams shipped with the Geronimo Jakarta Mail spec bundle.
+ */
+public class MailStreamProvider implements StreamProvider {
+
+ /**
+ * Public no-argument constructor as required by the ServiceLoader mechanism.
+ */
+ public MailStreamProvider() {
+ }
+
+ @Override
+ public InputStream inputBase64(final InputStream in) {
+ return new Base64DecoderStream(in);
+ }
+
+ @Override
+ public OutputStream outputBase64(final OutputStream out) {
+ return new Base64EncoderStream(out);
+ }
+
+ @Override
+ public InputStream inputBinary(final InputStream in) {
+ // 'binary', '7bit' and '8bit' are pass-through encodings.
+ return in;
+ }
+
+ @Override
+ public OutputStream outputBinary(final OutputStream out) {
+ // 'binary', '7bit' and '8bit' are pass-through encodings.
+ return out;
+ }
+
+ @Override
+ public OutputStream outputB(final OutputStream out) {
+ // the "B" encoding is base64 without any line breaks inserted.
+ return new Base64EncoderStream(out, Integer.MAX_VALUE);
+ }
+
+ @Override
+ public InputStream inputQ(final InputStream in) {
+ return new QDecoderStream(in);
+ }
+
+ @Override
+ public OutputStream outputQ(final OutputStream out, final boolean encodingWord) {
+ return new QEncoderStream(out, encodingWord);
+ }
+
+ @Override
+ public LineInputStream inputLineStream(final InputStream in, final boolean allowutf8) {
+ return new MailLineInputStream(in, allowutf8);
+ }
+
+ @Override
+ public LineOutputStream outputLineStream(final OutputStream out, final boolean allowutf8) {
+ return new MailLineOutputStream(out, allowutf8);
+ }
+
+ @Override
+ public InputStream inputQP(final InputStream in) {
+ return new QuotedPrintableDecoderStream(in);
+ }
+
+ @Override
+ public OutputStream outputQP(final OutputStream out) {
+ return new QuotedPrintableEncoderStream(out);
+ }
+
+ @Override
+ public InputStream inputSharedByteArray(final byte[] buff) {
+ return new SharedByteArrayInputStream(buff);
+ }
+
+ @Override
+ public InputStream inputUU(final InputStream in) {
+ return new UUDecoderStream(in);
+ }
+
+ @Override
+ public OutputStream outputUU(final OutputStream out, final String filename) {
+ if (filename == null) {
+ return new UUEncoderStream(out);
+ }
+ return new UUEncoderStream(out, filename);
+ }
+}
diff --git a/geronimo-mail_2.1_spec/src/main/java/org/apache/geronimo/mail/util/QDecoderStream.java b/geronimo-mail_2.1_spec/src/main/java/org/apache/geronimo/mail/util/QDecoderStream.java
new file mode 100644
index 0000000..d000277
--- /dev/null
+++ b/geronimo-mail_2.1_spec/src/main/java/org/apache/geronimo/mail/util/QDecoderStream.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.geronimo.mail.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * An implementation of a FilterInputStream that decodes the stream data
+ * using the RFC 2047 "Q" encoding. This is the quoted-printable encoding
+ * with the addition that an underscore character decodes to a space.
+ */
+public class QDecoderStream extends QuotedPrintableDecoderStream {
+
+ /**
+ * Stream constructor.
+ *
+ * @param in The InputStream this stream is filtering.
+ */
+ public QDecoderStream(final InputStream in) {
+ super(in);
+ }
+
+ /**
+ * Read a single byte from the stream, translating the "Q" encoding
+ * underscore convention into a space character.
+ *
+ * @return The next decoded byte of the stream. Returns -1 for an EOF condition.
+ * @exception IOException
+ */
+ @Override
+ public int read() throws IOException {
+ final int ch = super.read();
+ // in the "Q" encoding scheme, an un-encoded underscore represents a space.
+ if (ch == '_') {
+ return ' ';
+ }
+ return ch;
+ }
+
+ /**
+ * Read a buffer of data from the input stream.
+ *
+ * @param buffer The target byte array the data is placed into.
+ * @param offset The starting offset for the read data.
+ * @param length How much data is requested.
+ *
+ * @return The number of bytes of data read.
+ * @exception IOException
+ */
+ @Override
+ public int read(final byte[] buffer, final int offset, final int length) throws IOException {
+ for (int i = 0; i < length; i++) {
+ final int ch = read();
+ if (ch == -1) {
+ return i == 0 ? -1 : i;
+ }
+ buffer[offset + i] = (byte) ch;
+ }
+ return length;
+ }
+}
diff --git a/geronimo-mail_2.1_spec/src/main/java/org/apache/geronimo/mail/util/QEncoderStream.java b/geronimo-mail_2.1_spec/src/main/java/org/apache/geronimo/mail/util/QEncoderStream.java
new file mode 100644
index 0000000..a5133f9
--- /dev/null
+++ b/geronimo-mail_2.1_spec/src/main/java/org/apache/geronimo/mail/util/QEncoderStream.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.geronimo.mail.util;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * An implementation of a FilterOutputStream that encodes the stream data
+ * using the RFC 2047 "Q" encoding. This is a variation of the
+ * quoted-printable encoding used for encoded words: a space is encoded
+ * as an underscore and, depending on whether a word within a phrase or
+ * unstructured text is encoded, a different set of characters needs to
+ * be written encoded.
+ */
+public class QEncoderStream extends FilterOutputStream {
+
+ // characters that must be encoded, in addition to the non-printable ones,
+ // when encoding a word within a phrase (RFC 2047, section 5 (3)).
+ private static final String WORD_SPECIALS = "=_?\"#$%&'(),.:;<>@[\\]^`{|}~";
+ // characters that must be encoded, in addition to the non-printable ones,
+ // when encoding unstructured text.
+ private static final String TEXT_SPECIALS = "=_?";
+
+ private static final byte[] HEX_CHARS = {
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
+ };
+
+ // the specials set active for this stream.
+ private final String specials;
+
+ /**
+ * Create a "Q" encoder stream that wraps the specified stream.
+ *
+ * @param out The wrapped output stream.
+ * @param encodingWord true if we are Q-encoding a word within a phrase.
+ */
+ public QEncoderStream(final OutputStream out, final boolean encodingWord) {
+ super(out);
+ this.specials = encodingWord ? WORD_SPECIALS : TEXT_SPECIALS;
+ }
+
+ @Override
+ public void write(final int ch) throws IOException {
+ final int c = ch & 0xff;
+ if (c == ' ') {
+ // spaces are encoded as underscores.
+ out.write('_');
+ } else if (c < 040 || c >= 0177 || specials.indexOf(c) >= 0) {
+ // non-printable characters and the specials get written encoded.
+ out.write('=');
+ out.write(HEX_CHARS[c >> 4]);
+ out.write(HEX_CHARS[c & 0x0f]);
+ } else {
+ // printable ASCII characters just get written unchanged.
+ out.write(c);
+ }
+ }
+
+ @Override
+ public void write(final byte[] data) throws IOException {
+ write(data, 0, data.length);
+ }
+
+ @Override
+ public void write(final byte[] data, final int offset, final int length) throws IOException {
+ for (int i = 0; i < length; i++) {
+ write(data[offset + i]);
+ }
+ }
+}
diff --git a/geronimo-mail_2.1_spec/src/main/resources/META-INF/services/jakarta.mail.util.StreamProvider b/geronimo-mail_2.1_spec/src/main/resources/META-INF/services/jakarta.mail.util.StreamProvider
new file mode 100644
index 0000000..7745598
--- /dev/null
+++ b/geronimo-mail_2.1_spec/src/main/resources/META-INF/services/jakarta.mail.util.StreamProvider
@@ -0,0 +1 @@
+org.apache.geronimo.mail.util.MailStreamProvider
diff --git a/geronimo-mail_2.1_tck/src/tck/geronimo.jtx b/geronimo-mail_2.1_tck/src/tck/geronimo.jtx
index 1788637..1e1495e 100644
--- a/geronimo-mail_2.1_tck/src/tck/geronimo.jtx
+++ b/geronimo-mail_2.1_tck/src/tck/geronimo.jtx
@@ -22,12 +22,6 @@
# Baseline recorded 2026-07-18 against Jakarta Mail TCK 2.1.1 / James 3.9.0.
#
-# API signature verification: needs a dedicated sigtest setup (signature
-# records, sigtest.jar invocation) and is out of scope for the functional
-# gate; currently reports 18 signature mismatches in the spec classes that
-# should be reviewed separately.
-SignatureTest.html
-
# Remaining baseline failures (284 passed / 31 failed after the
# IMAPFolder.renameTo fix removed a ~90-test cascade). Distinct defects: