(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: