Mail extension: cfmail fails with "jakarta.mail.Provider: com.sun.mail.imap.IMAPProvider not a subtype" (duplicate jakarta.mail on classpath), survives restart and extension upgrade

Production mail had been working for days on Mail Extension 1.1.0.7. On 2026-09-14 at about 09:17, with no deploy, no restart and no admin change, every cfmail call started failing with:

java.util.ServiceConfigurationError: jakarta.mail.Provider: com.sun.mail.imap.IMAPProvider not a subtype

It affected every cfmail call, from request threads and from cfthread alike.

STACK TRACE (top)
at java.base/java.util.ServiceLoader.fail(Unknown Source)
at java.base/java.util.ServiceLoader$LazyClassPathLookupIterator.hasNextService(Unknown Source)
at java.base/java.util.ServiceLoader$LazyClassPathLookupIterator.hasNext(Unknown Source)
at java.base/java.util.ServiceLoader$2.hasNext(Unknown Source)
at java.base/java.util.ServiceLoader$3.hasNext(Unknown Source)
at jakarta.mail.Session.loadProviders(Session.java:964)
at jakarta.mail.Session.(Session.java:254)
at jakarta.mail.Session.getInstance(Session.java:281)
at org.lucee.extension.mail.smtp.SMTPConnectionPool.createSession(SMTPConnectionPool.java:148)
at org.lucee.extension.mail.smtp.SMTPConnectionPool$SessionAndTransport.(SMTPConnectionPool.java:175)
at org.lucee.extension.mail.smtp.SMTPConnectionPool.getSessionAndTransport(SMTPConnectionPool.java:57)
at org.lucee.extension.mail.smtp.SMTPClient.createMimeMessage(SMTPClient.java:492)
at org.lucee.extension.mail.smtp.SMTPClient._send(SMTPClient.java:909)
at org.lucee.extension.mail.smtp.SMTPClient.send(SMTPClient.java:798)
at org.lucee.extension.mail.tag.Mail.doEndTag(Mail.java:651)

WHAT WE FOUND
Three copies of the jakarta.mail classes are visible to the server:

  1. lucee-server/mvn/com/sun/mail/jakarta.mail/2.0.2/jakarta.mail-2.0.2.jar
    (direct dependency of mail-1.1.0.7 / 1.1.0.8-RC)
  2. lucee-server/mvn/com/sun/mail/jakarta.mail/2.0.1/jakarta.mail-2.0.1.jar
    (pulled in transitively by org.apache.commons:commons-email2-jakarta:2.0.0-M1)
  3. lucee-server/context/lib/jakarta.jakartaee-api-10.0.0.jar
    (contains the jakarta.mail and jakarta.activation API packages)

The Session.java line numbers in the trace (964, 254, 281) match the com.sun jakarta.mail 2.0.x jars, not the EE 10 API jar. From a request thread:

  • createObject(“java”, “jakarta.mail.Session”) and “jakarta.mail.Provider” resolve to context/lib/jakarta.jakartaee-api-10.0.0.jar (PhysicalClassLoader)
  • the thread context class loader resolves com.sun.mail.imap.IMAPProvider to mvn/…/jakarta.mail-2.0.2.jar (a different PhysicalClassLoader)

So Session and Provider come from one class loader, and the IMAP provider found by ServiceLoader comes from another. Which copy wins seems to depend on load order. A development server with the same files, same versions and same extension works fine.

WHAT WE TRIED

  • Service restart: still failing
  • Moving mvn/…/jakarta.mail/2.0.1 out: Lucee downloaded it again at startup
  • Upgrading Mail Extension to 1.1.0.8-RC plus restart: still failing
  • Replacing jakarta.mail-2.0.1.jar with an empty (manifest-only) jar, keeping the .pom so it is not downloaded again, plus restart: cfmail worked
  • About 6 minutes later Lucee logged a spontaneous config reload (“Init factory” / “Loaded config” in application.log, no admin action). Within minutes cfmail failed again with the same error, with the empty 2.0.1 jar still in place.

WORKAROUND

  • Code: we added an HTTP API fallback for our most important sends.
  • Planned: remove the jakarta/mail and jakarta/activation packages from context/lib/jakarta.jakartaee-api-10.0.0.jar, so only the Mail extension’s own jakarta.mail 2.0.2 is visible.

SUGGESTED FIX

  • The Mail extension should not resolve two jakarta.mail versions: exclude com.sun.mail:jakarta.mail from commons-email2-jakarta, or align it on one version.
  • The extension’s mail classes (Session, Provider and the providers found through ServiceLoader) should load from the extension’s own class loader, so other copies on the server (such as a Jakarta EE API jar in context/lib) cannot mix in.
  • The error should not appear suddenly after days of working, or after a config reload.

Follow-up (2026-09-15):

We removed every other copy of the jakarta.mail classes and it still fails:

  • jakarta.mail-2.0.1.jar in lucee-server/mvn replaced with an empty jar (manifest only)
  • jakarta/mail and jakarta/activation packages removed from context/lib/jakarta.jakartaee-api-10.0.0.jar
  • felix-cache cleared, full service restart

After the restart, every cfmail still fails with:
java.util.ServiceConfigurationError: jakarta.mail.Provider: com.sun.mail.imap.IMAPProvider not a subtype

Class lookups from a CFML request after the restart:

  • createObject(“java”, “jakarta.mail.Session”) and “jakarta.mail.Provider”: not found (so no other copy is visible any more)
  • thread context loader (EnvClassLoader) loadClass(“com.sun.mail.imap.IMAPProvider”):
    file:/C:/lucee/tomcat/lucee-server/mvn/com/sun/mail/jakarta.mail/2.0.2/jakarta.mail-2.0.2.jar
    loader = lucee.commons.lang.PhysicalClassLoader@1835dd38
  • getResources(“META-INF/services/jakarta.mail.Provider”) through the thread context loader: none

jakarta.mail-2.0.2.jar is now the only jar on the server that contains jakarta.mail.Provider, and the error still happens. So it looks like the same jar is loaded by two different class loaders: the Mail extension’s Session/Provider come from one PhysicalClassLoader, and ServiceLoader resolves IMAPProvider through the thread context class loader (EnvClassLoader), which returns the class from another PhysicalClassLoader instance of the same jar.

The same install worked for days before this. It fails consistently now, across restarts. A possible fix: set the thread context class loader to the extension’s own class loader around Session.getInstance in SMTPConnectionPool.createSession.

ROOT CAUSE FOUND IN OUR ENVIRONMENT — leaving this open, questions at the end

Short version: cfmail is working again here, after a server configuration change and
undoing one of my own workarounds. I am not marking this resolved, because nothing in
the Mail extension changed: the same error will come back in any setup where the mail
classes end up in two class loaders, and I would like to know whether 1.1.0.8 addresses
that. Questions at the bottom.

Environment: Lucee 7.1.0.204 / Java 21.0.9 / Tomcat 11.0.15 / Windows Server, IIS in
front via mod_cfml (BonCode connector), Mail extension 1.1.0.6.

The error, on every send:
jakarta.mail.Provider: com.sun.mail.imap.IMAPProvider not a subtype
(ServiceConfigurationError, from jakarta.mail.Session.loadProviders via
SMTPConnectionPool.createSession)

CAUSE 1 — a second Tomcat Host context loaded the mail stack twice

mod_cfml creates a Tomcat Host on first request for any IIS site that has no matching
binding. On this server that was IIS’s “Default Web Site”, which pointed at the same
docBase as the default ROOT, so Tomcat deployed conf\Catalina\w3svc1\ROOT.xml and the
JVM ended up with a second Lucee web context over the same files.

The timing was exact and reproducible three times in one day: after every service
restart cfmail worked, and the first send AFTER the w3svc1 deploy failed.

07:14 restart → 07:16:50 “Deploying deployment descriptor conf\Catalina\w3svc1\ROOT.xml” → next send fails
13:03 restart → 13:04 cfmail OK → 13:10:50 w3svc1 deploy → 13:25 fails

Fix: the host disabled IIS’s Default Web Site, deleted C:\lucee\tomcat\conf\Catalina\w3svc1
and restarted; the folder is not recreated. (The alternative they offered was a rewrite
rule setting the X-Webserver-Context server variable to “localhost” in that site’s
web.config, so mod_cfml reuses the existing host instead of creating one.)

If you run IIS + mod_cfml, it is worth checking conf\Catalina for w3svc* folders you
did not create.

CAUSE 2 — a stub jar we had put in place ourselves

On 14 September, as a workaround, I had replaced
mvn\com\sun\mail\jakarta.mail\2.0.1\jakarta.mail-2.0.1.jar with a 260-byte manifest-only
jar of the same name (moving the folder aside does not work — Lucee re-downloads it).
That bought about six minutes before the next context deploy, and then it became a
problem in its own right: with that file empty, jakarta.mail.Provider could only be
found in context\lib\jakarta.jakartaee-api-10.0.0.jar while com.sun.mail.imap.IMAPProvider
came from elsewhere, which is the very mismatch the error reports.

Restoring the real 689,294-byte jar (copied from a working install) plus cause 1 above
is what produced a clean start. Lesson for anyone else: do not leave a hollowed-out jar
in the mvn repository.

AN OBSERVATION I CANNOT EXPLAIN, and would like to

Mail extension 1.1.0.7 was installed on 11 September. Mail broke on 14 September, at the
first restart after that. I downgraded to 1.1.0.6 in the Admin and restarted; the Admin
showed 1.1.0.6.

Yet on 17 September, while the Admin still showed 1.1.0.6, Windows refused to delete
C:\lucee\tomcat\lucee-server\mvn\org\lucee\mail\1.1.0.7\mail-1.1.0.7.jar with “the file
is open in Lucee … Apache Tomcat”. So the running JVM had the 1.1.0.7 jar open while
the Admin reported 1.1.0.6.

I stopped the service, moved the 1.1.0.7 and 1.1.0.8-RC folders out, and started. cfmail
worked. Within the same minute, Lucee had re-created the 1.1.0.7 folder and downloaded
the jar again — and mail has kept working since.

So the newer jar being present on disk is clearly not fatal by itself (a working install
elsewhere has all three versions). But two things seem worth a look:

  • an Admin downgrade leaves the superseded library in place and the running engine
    can still hold it open, which makes it very hard to tell which version is actually
    in use;
  • a downgraded extension still causes the newer library to be re-downloaded at startup.

If that is expected behaviour, it would help to say so in the Admin, because right now a
downgrade looks complete and is impossible to verify without a file listing.

WHAT THE CLASS LOADERS SHOWED

A diagnostic page (createObject + thread context loader, plus a scan of the mail jars on
disk) made the difference visible.

Broken:
jakarta.mail.Provider not found via the thread loader; only in
context\lib\jakarta.jakartaee-api-10.0.0.jar (PhysicalClassLoader)
com.sun.mail.imap.IMAPProvider via thread loader: mvn.…\jakarta.mail-2.0.2.jar (PhysicalClassLoader)
via createObject: bundles\org.lucee.commons-email-all-1.6.0.jar [22]

Working (after both fixes):
com.sun.mail.imap.IMAPProvider from bundles\org.lucee.commons-email-all-1.6.0.jar [22]
through BOTH the thread loader and createObject

So the subclass was coming from a bundle while its superclass came from a plain jar
through a different loader. Whatever the mail code does to look up providers, it should
resolve them through the extension bundle’s own loader rather than the thread context
loader, which is not the same in a Lucee context.

THINGS THAT DID NOT HELP, in case they save someone else time

  • Service restarts on their own (mail worked only until the next w3svc1 deploy)
  • Moving mvn\com\sun\mail\jakarta.mail\2.0.1\ out of the way (Lucee re-downloads it at startup)
  • Replacing that 2.0.1 jar with a 260-byte manifest-only stub (worked for 6 minutes, then
    the next context deploy; and it later became a problem of its own — restore the real jar)
  • Upgrading to Mail extension 1.1.0.8-RC
  • Stripping the jakarta.mail and jakarta.activation packages out of
    context\lib\jakarta.jakartaee-api-10.0.0.jar
  • Clearing felix-cache

What kept the business running meanwhile: every cfmail in the application was wrapped so a
failure falls back to sending through the Postmark HTTP API. Three days of ticket
confirmations went out that way while this was being chased. Worth having regardless of
this bug.

QUESTIONS — why I am leaving this open

  1. Does the mail code look providers up through the thread context loader? In a Lucee
    context that is not the same loader the extension’s own classes came from, which is
    exactly what the class locations above show. Does 1.1.0.8 change this?

  2. Is a Lucee web context per Tomcat host expected to load its own copy of the
    extension’s libraries? If so, any server with two hosts pointing at the same files
    hits this, and it is not obvious from the outside. A warning at startup when the mail
    classes are visible through more than one loader would have saved me three days.

  3. Is it intended that a downgraded extension re-downloads the newer library into
    lucee-server\mvn at startup, and that the Admin can show 1.1.0.6 while the JVM holds
    the 1.1.0.7 jar open?

Happy to run any diagnostic you want on this server — it is a live ticketing system, so
I would rather test a fix than discover the next edge case during an on-sale.

UPDATE: cfmail still fails, and I think I can now say exactly when it breaks

Short version: the Mail extension works until its classes are loaded by more than one
Lucee web context in the same JVM. On a multi-site server that is unavoidable, so on
our production box cfmail works for a few minutes after a restart and then fails on
every send that creates a new mail session. Detail and evidence below, and three
questions at the end.

Environment: Lucee 7.1.0.204 / Java 21.0.9 / Tomcat 11.0.15 / Windows Server, IIS in
front via mod_cfml (BonCode), Mail extension 1.1.0.6. Roughly six domains, each its own
Tomcat Host, all serving the same application directory.

The error, on every send that builds a new session:
jakarta.mail.Provider: com.sun.mail.imap.IMAPProvider not a subtype
(ServiceConfigurationError from jakarta.mail.Session.loadProviders,
via SMTPConnectionPool.createSession)

WHAT THE CLASS LOADERS SHOW

I built a small diagnostic page that reports where each class resolves from, through the
thread context loader and through createObject, plus every mail jar on disk.

Minutes after a restart (mail working):
com.sun.mail.imap.IMAPProvider thread loader: bundles\org.lucee.commons-email-all-1.6.0.jar [22]
com.sun.mail.imap.IMAPProvider createObject: bundles\org.lucee.commons-email-all-1.6.0.jar [22]

Three hours later (mail failing):
com.sun.mail.imap.IMAPProvider thread loader: mvn\com\sun\mail\jakarta.mail\2.0.2\jakarta.mail-2.0.2.jar
(lucee.commons.lang.PhysicalClassLoader)
com.sun.mail.imap.IMAPProvider createObject: bundles\org.lucee.commons-email-all-1.6.0.jar [22]

jakarta.mail.Provider thread loader: not found in the core, the loader or any extension bundle
jakarta.mail.Provider createObject: context\lib\jakarta.jakartaee-api-10.0.0.jar

So the subclass arrives from a bundle while its superclass arrives from a plain jar
through a different loader, and the JDK rejects the pair. Nothing on disk changed
between those two readings.

WHY I THINK IT IS THE NUMBER OF WEB CONTEXTS

  • A single-context install with the SAME jars sends mail fine indefinitely (our dev box:
    same Lucee build, same extension, same mvn contents, one context).
  • Production has one Tomcat Host per domain, so several Lucee web contexts in one JVM.
  • Earlier in this saga, mod_cfml was also auto-creating an extra host context
    (conf\Catalina\w3svc1) for an IIS site with no binding. Three times in one day, in the
    logs: restart → cfmail OK → “Deploying deployment descriptor conf\Catalina\w3svc1
    ROOT.xml” → the next send fails. Our host disabled that site and removed the context,
    which removed one context but not the legitimate per-domain ones.
  • After that cleanup: restart at 08:54, diagnostic at 08:54 shows both lookups on the
    bundle and a test send succeeds. By 09:xx real sends are failing again, and by 12:06
    the diagnostic shows the split above.

One detail that cost me time, in case it helps somebody else read their own logs: a
plain cfmail with no server attribute kept succeeding from the diagnostic page while
every application send failed. The application sends specify each site’s own SMTP
server and credentials, which forces a NEW session; the default-server session was
already cached. A passing test send proves very little here.

THINGS THAT DID NOT HELP

  • Restarts on their own (mail works only until the next context initialises)
  • Moving mvn\com\sun\mail\jakarta.mail\2.0.1\ aside (Lucee re-downloads it at startup)
  • Replacing that 2.0.1 jar with a 260-byte manifest-only stub. It worked for about six
    minutes and then became a second, self-inflicted cause of the same error - with that
    file empty, jakarta.mail.Provider can only come from the EE API jar. Restore the real
    jar if you tried this.
  • Upgrading to Mail extension 1.1.0.8-RC
  • Stripping the jakarta.mail and jakarta.activation packages out of
    context\lib\jakarta.jakartaee-api-10.0.0.jar
  • Clearing felix-cache

One more oddity: after downgrading the extension from 1.1.0.7 to 1.1.0.6 in the Admin,
Windows refused to delete mvn\org\lucee\mail\1.1.0.7\mail-1.1.0.7.jar - “the file is
open in Lucee” - while the Admin reported 1.1.0.6. I stopped the service, moved the
1.1.0.7 and 1.1.0.8-RC folders out, and started; within the same minute Lucee had
re-downloaded 1.1.0.7.

QUESTIONS

  1. Is the provider lookup expected to go through the thread context loader? In a Lucee
    context that is not the loader the extension’s own classes came from, which is what
    the two readings above show.

  2. Is each Lucee web context expected to load its own copy of the extension’s libraries?
    If so, every multi-site Lucee server will hit this as soon as a second context starts

    • and the only warning is a ServiceConfigurationError on the next send.
  3. Is it intended that a downgraded extension re-downloads the newer library at startup,
    and that the Admin can report 1.1.0.6 while the JVM holds the 1.1.0.7 jar open?

Happy to run any diagnostic you want on this box, or to try a patched extension. It is a
live ticketing system, so I would much rather test a fix than meet the next edge case
during an on-sale.

In the meantime we wrapped every cfmail in the application so a failure falls back to
sending through the Postmark HTTP API. It carried 220+ emails this morning alone -
ticket confirmations, receipts and sales notices - while cfmail was failing on every
one of them. Worth doing regardless of how this ends.

@Vahe thanks for all the digging and the detailed write-ups.

The “IMAPProvider not a subtype” error is tracked as LDEV-6485. The Mail extension now creates the mail session and transport with its own classloader. The fix is in Lucee 7.1.1.15-RC with Mail extension 1.1.0.10-RC: Lucee 7.1.1.15-RC available for testing

Your questions about several web contexts in one JVM and the duplicate jakarta.mail jars are with the team. If you can test the RC on a box with more than one Tomcat Host, it would really help to know whether cfmail keeps working there once all contexts have loaded.