javax.mail:mail 1.4.7 is broken, and how to workaround

3 minute read

javax.mail and I

One of the current tasks on my agenda involves the modernization of a project that is currently built on Java 8. Given that this project is actively in use and under continuous development, a complete freeze of the codebase for a one-time migration isn’t a feasible option. The reason being, such an approach would necessitate extensive library upgrades and rewrites to the code using them. Instead, I’ve devised a more pragmatic plan:

  1. Start with building and running on Java 8.
  2. Modify code and imports to still build and run on Java 8, but also be able to build and run on Java 11.
  3. Switch the run and build environment to Java 11.
  4. Modify code and imports to build and run on both Java 11 and Java 17.
  5. Switch the run and build environment to Java 17.

I’ve decided to insert an intermediate step involving Java 11. This choice stems from the substantial gap between Java 8 and 17, making it unlikely for me to locate compatible versions of imports (e.g., Spring) that cater to both Java versions.

When attempting to build our code using Java 11, I promptly identified the absence of certain classes. These omissions were attributed to components of Java categorized as “Enterprise,” which were present in Java 8, deprecated in Java 9, and entirely eliminated in Java 11.

To ensure the availability of these classes, I made the following addition to our pom.xml:

<dependency>
    <groupId>javax.mail</groupId>
    <artifactId>mail</artifactId>
    <version>1.4.7</version>
</dependency>

Where version 1.4.7 represented the latest stable release mvnrepository) of that library. This adjustment allowed my code to compile successfully, as it encompassed all the classes and methods essential for its operation.

The runtime problem

The code compiled successfully; however, I encountered a peculiar error while testing our email sending functionality:

java.lang.NoSuchMethodError: com.sun.mail.util.LineOutputStream.<init>(Ljava/io/OutputStream;Z)V

I searched through our codebase and found no references to ‘LineOutputStream,’ so it doesn’t appear to be a misuse of any external library on my part. It was time to delve into the stack trace:

...
Caused by: java.lang.NoSuchMethodError: com.sun.mail.util.LineOutputStream.<init>(Ljava/io/OutputStream;Z)V
	at javax.mail.internet.MimeBodyPart.writeTo(MimeBodyPart.java:1631)
	at javax.mail.internet.MimeMessage.writeTo(MimeMessage.java:1889)
	at javax.mail.internet.MimeMessage.writeTo(MimeMessage.java:1863)
    at net.backslasher.EmailSender.send(EmailSender.java:69)
...

So somehow the library code is calling a nonexistent constructor. Weird! Digging through the decompiled code with IDEA, I found the following call:

// MimeBodyPart.class
static void writeTo(MimePart part, OutputStream os, String[] ignoreList) throws IOException, MessagingException {
    LineOutputStream los = null;
    if (os instanceof LineOutputStream) {
        los = (LineOutputStream)os;
    } else {
        los = new LineOutputStream(os, allowutf8);
    }
...

While the actual constructor looks like this:

// LineOutputStream.class
public LineOutputStream(OutputStream out) {
    super(out);
}

By reading the Java type signature list, we can see that this is what the exception is complaining about - it’s looking for a constructor with 2 arguments - OutputStream and a boolean (<init>(Ljava/io/OutputStream;Z)V), but finds no such thing.

I had some questions.

How did this happen?

From inspecting the code of javax.mail:mail:1.4.7 thoroughly, I can only conclude that the package itself has an internal contradiction. It provides a class with a specific constructor, but calls a different one. I was baffled as to how this library was able to be produced. This served as a reminder that it is possible to manually package Class files from different compilation runs in the same JAR, and their incompatibility only becomes apparent when attempting to use them together – which is precisely what I encountered.

How did I manage to compile?

Since I downloaded precompiled code (as in a collection of Class files and not java files), my compiler didn’t run on that code and missed the incompatibility. If we draw a parallel to C code compilation, we would typically have a final “linkage” step that scans all cross-file references and fails if any of them are incompatible. However, in this context, such a linkage step does not apply.

what should I do now?

One might say the “right” thing would be upgrading to the library that replaces javax.mail:mail (which is javax.mail:javax.mail-api, which was itself replaced by jakarta.mail:jakarta.mail-api), as it brings us the most up-to-date (and hopefully problem free) code. However, after 15 minutes of trying this I saw that there’s too much of our code we’d need to change as a resutlt. Instead, I went for the minimal effective change, and tried different version of java.mail:mail until I found one that didn’t have this internal contradiction - 1.4.4. I changed our pom.xml to look like:

<dependency>
    <groupId>javax.mail</groupId>
    <artifactId>mail</artifactId>
    <version>1.4.4</version>
</dependency>

And things went back to normal

Next steps

Once we stabilize on Java 11, I’m planning to replace this package with the “correct” one (jakarta.mail:jakarta.mail-api) Until we get there, this quick fix will do.

Tags:

Updated: