Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IMAP not Initializing In Session.getInstance(Properties) #29

Closed
PeteSL opened this issue Sep 28, 2022 · 25 comments
Closed

IMAP not Initializing In Session.getInstance(Properties) #29

PeteSL opened this issue Sep 28, 2022 · 25 comments

Comments

@PeteSL
Copy link

PeteSL commented Sep 28, 2022

I have an app that works with jakarta.mail 2.0.1 and before. I set the log.level to FINE so we could see everything generated at startup and this is it:

2022-09-28T21:07:08.242Z jakarta.mail.Session <init>
CONFIG: Jakarta Mail version ${mail.version}
2022-09-28T21:07:08.257Z jakarta.mail.Session loadAllResources
CONFIG: URL jar:file:///C:/Ham%20Apps/javAPRSSrvr4/lib/angus-mail-1.0.0.jar!/META-INF/javamail.providers
2022-09-28T21:07:08.257Z jakarta.mail.Session loadAllResources
CONFIG: successfully loaded resource: jar:file:///C:/Ham%20Apps/javAPRSSrvr4/lib/angus-mail-1.0.0.jar!/META-INF/javamail.providers
2022-09-28T21:07:08.257Z jakarta.mail.Session loadResource
CONFIG: successfully loaded resource: /META-INF/javamail.default.providers
2022-09-28T21:07:08.257Z jakarta.mail.Session loadProviders
CONFIG: Tables of loaded providers
2022-09-28T21:07:08.257Z jakarta.mail.Session loadProviders
CONFIG: Providers Listed By Class Name: {com.sun.mail.smtp.SMTPTransport=jakarta.mail.Provider[TRANSPORT,smtp,com.sun.mail.smtp.SMTPTransport,Oracle], com.sun.mail.imap.IMAPSSLStore=jakarta.mail.Provider[STORE,imaps,com.sun.mail.imap.IMAPSSLStore,Oracle], com.sun.mail.pop3.POP3Store=jakarta.mail.Provider[STORE,pop3,com.sun.mail.pop3.POP3Store,Oracle], com.sun.mail.smtp.SMTPSSLTransport=jakarta.mail.Provider[TRANSPORT,smtps,com.sun.mail.smtp.SMTPSSLTransport,Oracle], com.sun.mail.imap.IMAPStore=jakarta.mail.Provider[STORE,imap,com.sun.mail.imap.IMAPStore,Oracle], com.sun.mail.pop3.POP3SSLStore=jakarta.mail.Provider[STORE,pop3s,com.sun.mail.pop3.POP3SSLStore,Oracle]}
2022-09-28T21:07:08.257Z jakarta.mail.Session loadProviders
CONFIG: Providers Listed By Protocol: {imap=jakarta.mail.Provider[STORE,imap,com.sun.mail.imap.IMAPStore,Oracle], smtp=jakarta.mail.Provider[TRANSPORT,smtp,com.sun.mail.smtp.SMTPTransport,Oracle], pop3=jakarta.mail.Provider[STORE,pop3,com.sun.mail.pop3.POP3Store,Oracle], smtps=jakarta.mail.Provider[TRANSPORT,smtps,com.sun.mail.smtp.SMTPSSLTransport,Oracle], imaps=jakarta.mail.Provider[STORE,imaps,com.sun.mail.imap.IMAPSSLStore,Oracle], pop3s=jakarta.mail.Provider[STORE,pop3s,com.sun.mail.pop3.POP3SSLStore,Oracle]}
2022-09-28T21:07:08.257Z jakarta.mail.Session loadResource
CONFIG: successfully loaded resource: /META-INF/javamail.default.address.map
2022-09-28T21:07:08.257Z jakarta.mail.Session loadAllResources
CONFIG: URL jar:file:///C:/Ham%20Apps/javAPRSSrvr4/lib/angus-mail-1.0.0.jar!/META-INF/javamail.address.map
2022-09-28T21:07:08.257Z jakarta.mail.Session loadAllResources
CONFIG: successfully loaded resource: jar:file:///C:/Ham%20Apps/javAPRSSrvr4/lib/angus-mail-1.0.0.jar!/META-INF/javamail.address.map
2022-09-28T21:07:08.273Z jakarta.mail.Session <init>
CONFIG: Jakarta Mail version ${mail.version}
2022-09-28T21:07:08.273Z jakarta.mail.Session getProvider
FINE: getProvider() returning jakarta.mail.Provider[STORE,imap,com.sun.mail.imap.IMAPStore,Oracle]
2022-09-28T21:07:08.273Z jakarta.mail.Session loadAllResources
CONFIG: URL jar:file:///C:/Ham%20Apps/javAPRSSrvr4/lib/angus-mail-1.0.0.jar!/META-INF/javamail.providers
2022-09-28T21:07:08.273Z jakarta.mail.Session loadAllResources
CONFIG: successfully loaded resource: jar:file:///C:/Ham%20Apps/javAPRSSrvr4/lib/angus-mail-1.0.0.jar!/META-INF/javamail.providers
2022-09-28T21:07:08.282Z jakarta.mail.Session loadResource
CONFIG: successfully loaded resource: /META-INF/javamail.default.providers
2022-09-28T21:07:08.282Z jakarta.mail.Session loadProviders
CONFIG: Tables of loaded providers
2022-09-28T21:07:08.282Z jakarta.mail.Session loadProviders
CONFIG: Providers Listed By Class Name: {com.sun.mail.smtp.SMTPTransport=jakarta.mail.Provider[TRANSPORT,smtp,com.sun.mail.smtp.SMTPTransport,Oracle], com.sun.mail.imap.IMAPSSLStore=jakarta.mail.Provider[STORE,imaps,com.sun.mail.imap.IMAPSSLStore,Oracle], com.sun.mail.pop3.POP3Store=jakarta.mail.Provider[STORE,pop3,com.sun.mail.pop3.POP3Store,Oracle], com.sun.mail.smtp.SMTPSSLTransport=jakarta.mail.Provider[TRANSPORT,smtps,com.sun.mail.smtp.SMTPSSLTransport,Oracle], com.sun.mail.imap.IMAPStore=jakarta.mail.Provider[STORE,imap,com.sun.mail.imap.IMAPStore,Oracle], com.sun.mail.pop3.POP3SSLStore=jakarta.mail.Provider[STORE,pop3s,com.sun.mail.pop3.POP3SSLStore,Oracle]}
2022-09-28T21:07:08.282Z jakarta.mail.Session loadProviders
CONFIG: Providers Listed By Protocol: {imap=jakarta.mail.Provider[STORE,imap,com.sun.mail.imap.IMAPStore,Oracle], smtp=jakarta.mail.Provider[TRANSPORT,smtp,com.sun.mail.smtp.SMTPTransport,Oracle], pop3=jakarta.mail.Provider[STORE,pop3,com.sun.mail.pop3.POP3Store,Oracle], smtps=jakarta.mail.Provider[TRANSPORT,smtps,com.sun.mail.smtp.SMTPSSLTransport,Oracle], imaps=jakarta.mail.Provider[STORE,imaps,com.sun.mail.imap.IMAPSSLStore,Oracle], pop3s=jakarta.mail.Provider[STORE,pop3s,com.sun.mail.pop3.POP3SSLStore,Oracle]}
2022-09-28T21:07:08.282Z jakarta.mail.Session loadResource
CONFIG: successfully loaded resource: /META-INF/javamail.default.address.map
2022-09-28T21:07:08.282Z jakarta.mail.Session loadAllResources
CONFIG: URL jar:file:///C:/Ham%20Apps/javAPRSSrvr4/lib/angus-mail-1.0.0.jar!/META-INF/javamail.address.map
2022-09-28T21:07:08.282Z jakarta.mail.Session loadAllResources
CONFIG: successfully loaded resource: jar:file:///C:/Ham%20Apps/javAPRSSrvr4/lib/angus-mail-1.0.0.jar!/META-INF/javamail.address.map
2022-09-28T21:07:08.282Z com.sun.mail.imap.IMAPStore <init>
CONFIG: mail.imap.partialfetch: false
2022-09-28T21:07:08.282Z jakarta.mail.Session getProvider
FINE: getProvider() returning jakarta.mail.Provider[STORE,imap,com.sun.mail.imap.IMAPStore,Oracle]
2022-09-28T21:07:08.282Z com.sun.mail.imap.IMAPStore <init>
CONFIG: mail.imap.ignorebodystructuresize: true
2022-09-28T21:07:08.282Z com.sun.mail.imap.IMAPStore <init>
CONFIG: mail.imap.partialfetch: false
2022-09-28T21:07:08.289Z com.sun.mail.imap.IMAPStore <init>
CONFIG: mail.imap.statuscachetimeout: 1000
2022-09-28T21:07:08.289Z com.sun.mail.imap.IMAPStore <init>
CONFIG: mail.imap.ignorebodystructuresize: true
2022-09-28T21:07:08.289Z com.sun.mail.imap.IMAPStore <init>
CONFIG: mail.imap.appendbuffersize: -1
2022-09-28T21:07:08.289Z com.sun.mail.imap.IMAPStore <init>
CONFIG: mail.imap.statuscachetimeout: 1000
2022-09-28T21:07:08.290Z com.sun.mail.imap.IMAPStore <init>
CONFIG: mail.imap.minidletime: 10
2022-09-28T21:07:08.290Z com.sun.mail.imap.IMAPStore <init>
CONFIG: mail.imap.appendbuffersize: -1
2022-09-28T21:07:08.290Z com.sun.mail.imap.IMAPStore <init>
CONFIG: closeFoldersOnStoreFailure
2022-09-28T21:07:08.290Z com.sun.mail.imap.IMAPStore <init>
CONFIG: mail.imap.minidletime: 10
2022-09-28T21:07:08.290Z com.sun.mail.imap.IMAPStore <init>
CONFIG: closeFoldersOnStoreFailure
2022-09-28T21:07:08.388Z com.sun.mail.imap.IMAPStore cleanup
FINE: IMAPStore cleanup, not connected
2022-09-28T21:07:08.388Z com.sun.mail.imap.IMAPStore emptyConnectionPool
FINE: removed all authenticated connections from pool
2022-09-28T21:07:08.388Z com.sun.mail.imap.IMAPStore cleanup
FINE: IMAPStore cleanup, not connected
2022-09-28T21:07:08.388Z com.sun.mail.imap.IMAPStore emptyConnectionPool
FINE: removed all authenticated connections from pool

I am using the patched jakarta.mail-api jar from #25 so we don't see those errors. It appears the Session has an issue setting up the connection (not SSL, just plain text as on LAN), The Session.getInstance() is by itself, does not throw an exception (I would see this with a stack trace) but it appears no other parts following it are executed including the SMTP transport setup? This is all that was in the log related to jakarta.mail which seems to tell me that the Session.getInstance() is never returning? The only change from 2.0.1 is the jakarta.mail jars that are being used. I do not use the IdleManager; the app calls idle(boolean) directly but that part is never shown in the log. JDK is 19.

@jbescos
Copy link
Member

jbescos commented Sep 30, 2022

Hi @PeteSL , I am not sure that I understand what is the issue you have.

My understanding is that you had one app using IMAP that was working with mail-api 2.0.1 and before, but it does not work with higher versions. From that log lines I don't see what could be the issue. Do you have more relevant logs?.

I have a demo that works with IMAP here and I didn't find any problem: https://github.com/jbescos/jakartaEE10MailDemo

@PeteSL
Copy link
Author

PeteSL commented Sep 30, 2022

The issue is with angus-mail apparently never getting past the initialization of the Session. I am attaching a zip with 3 log files. err2 log is the log file showing the entirety of the mail output using jakarta-mail 2.1.0/angus-mail 1.0.0. err1 and err0 (in that order) shows the entirety of the mail output using jakarta.mail 2.0.1. There are 2 independent mail clients in this app (maybe part of the issue?) and the basic mail properties are shown in the properties file. mail.debug=true was set for both mail clients and the modules were loaded via the module path.
err..log.0.zip
A closer look at the logs shows that it might be getting out of the Session creation as the output is very similar through the last IMAP message regarding setting closeFolderOnStoreFailure. So the question is why is 2.0.1 properly connecting to the server while 2.1.0/angus-mail is not even trying and going direct to cleanup?
Further note about app architecture: each mail client is exactly the same code, just different threads and mailboxes; Session establishment is on one thread while the session.connect() is done on a different thread. Just trying to be as clear as possible regarding what might be unique (or not).
The code in question first calls Session.getStore() and then Store.connect(). Because both of those would show an exception if it is not StoreClosedException and try again after 10 seconds, and we are not seeing a retry, it makes me think the IMAP cleanup is being called in the getStore() and getStore() is never returning?

@jbescos
Copy link
Member

jbescos commented Oct 10, 2022

@PeteSL are you able to provide the code of the reproducer (you don't need to write host, user and password)?. I will test it and debug it.

@PeteSL
Copy link
Author

PeteSL commented Oct 10, 2022

The code below is in the base class constructor (extraneous code removed, the execute method is on a ScheduledThreadPool Executor gotten from Executors).
sn = Session.getInstance(mailprops);
mailthreads.execute(store);
This is the run() method for the store object. It does not use polling as it configured to use the idle() loop.

    @Override
    public void run()
    {
        if (polled)
        {
            // Polled task
            super.run();
            return;
        }

        // Idle thread processing
        for (;;)
        {
            try (IMAPStore mailsrvr = (IMAPStore) parent.sn.getStore())
            {
                mailsrvr.connect(user, password);
                for (;;)
                    try (IMAPFolder inbox = (IMAPFolder) mailsrvr.getFolder("INBOX"))
                    {
                        inbox.open(IMAPFolder.READ_WRITE);
                        for (;;)
                        {
                            while (inbox.getMessageCount() > 0)
                                processInbox(inbox); // Do this before going to idle in case there is mail waiting
                            inbox.idle(true);
                        }
                    }
                    catch (FolderClosedException e)
                    {
                    }
            }
            catch (StoreClosedException e)
            {
            }
            catch (IOException | MessagingException e)
            {
                classlogger.log(Level.WARNING, "IMAP Error", e);
            }
            try
            {
                TimeUnit.SECONDS.sleep(30L);
            }
            catch (InterruptedException e)
            {
                break;
            }
        }
    }

From the debug log, it looks like it is the sn.getStore() method that is failing as it looks likes like the connect(name, pwd) method is never called? The IMAP properties are:
mail.from=email@address #mail.debug=true #smtp mail.transport.protocol=smtp mail.smtp.port=25 mail.smtp.host=mail.host #imap mail.store.protocol=imap mail.imap.host=mail.host mail.imap.user=imap.user mail.imap.partialfetch=false mail.imap.ignorebodystructuresize=true
The only difference between the 2 sessions is the user name (and password, of course) and from email address. Needless to say, there is more code but none that interferes with the sequence shown in the debug log.

@jbescos
Copy link
Member

jbescos commented Oct 10, 2022

I have tried your example and definitely I can connect to IMAP.

Try to add e.printStackTrace() in every catch, just in case there is one exception and it is silently failing.

Here there is one example sending one email and listing the emails with IMAP. Does it work to you after changing host, port, etc?.

import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Properties;

import jakarta.mail.BodyPart;
import jakarta.mail.Folder;
import jakarta.mail.Message;
import jakarta.mail.MessagingException;
import jakarta.mail.Multipart;
import jakarta.mail.Session;
import jakarta.mail.Store;
import jakarta.mail.Transport;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeMessage;
import jakarta.mail.internet.MimeMultipart;

public class Main {

    private static final Properties MAIL_PROPERTIES = new Properties();

    static {
        MAIL_PROPERTIES.setProperty("mail.smtp.host", "0.0.0.0");
        MAIL_PROPERTIES.setProperty("mail.smtp.port", "18000");
        MAIL_PROPERTIES.setProperty("mail.imap.host", "0.0.0.0");
        MAIL_PROPERTIES.setProperty("mail.imap.port", "18004");
        MAIL_PROPERTIES.setProperty("mail.imap.partialfetch", "false");
        MAIL_PROPERTIES.setProperty("mail.imap.ignorebodystructuresize", "true");
    }

    public static void main(String[] args)
            throws AddressException, MessagingException, IOException, InterruptedException {
        Session session = Session.getDefaultInstance(MAIL_PROPERTIES);
        sendEmail(session, "user01@james.local", "user02@james.local", "Test");
        readEmails(session, "user02@james.local", "imap");
    }

    private static void sendEmail(Session session, String from, String to, String text)
            throws AddressException, MessagingException, IOException {
        Message message = new MimeMessage(session);
        Multipart multipart = new MimeMultipart();
        message.setContent(multipart);
        message.setFrom(new InternetAddress(from));
        message.addRecipient(Message.RecipientType.TO, new InternetAddress(to));
        message.setSubject("Hello Demo!");
        message.setText(text);
        Transport.send(message);
    }

    private static void readEmails(Session session, String userMail, String protocol)
            throws MessagingException, IOException {
        try (Store store = session.getStore(protocol)) {
            store.connect(userMail, "1234");
            try (Folder folder = store.getFolder("INBOX")) {
                folder.open(Folder.READ_WRITE);
                System.out.println(protocol + " -> Message count: " + folder.getMessageCount());
                for (Message message : folder.getMessages()) {
                    System.out.println(print(message));
                }
            }
        }
    }

    private static String print(Message message) throws MessagingException, IOException {
        DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        StringBuilder builder = new StringBuilder();
        builder.append(format.format(message.getSentDate())).append("|").append("Subject:").append(message.getSubject())
                .append("|").append("From:").append(Arrays.asList(message.getFrom())).append("|").append("Recipients:")
                .append(Arrays.asList(message.getAllRecipients())).append("|").append("Content-Type:")
                .append(message.getContentType()).append("|").append("Content:");
        if (message.getContent() instanceof Multipart) {
            Multipart multipart = (Multipart) message.getContent();
            builder.append("[");
            for (int i = 0; i < multipart.getCount(); i++) {
                BodyPart bodyPart = multipart.getBodyPart(i);
                builder.append(bodyPart.getContentType()).append(" - ").append(bodyPart.getContent()).append(",");
            }
            builder.append("]");
        } else {
            builder.append(message.getContent());
        }
        return builder.toString();
    }
}

@PeteSL
Copy link
Author

PeteSL commented Oct 10, 2022

Difference is you are doing everything in a single main thread running a single session. My environment has my code being run in separate threads (both session creations are being done in the same thread but the IMAP run() method is in separate threads) and the failure appears to be before the connect() ever occurs according to the log (compare the log outputs in the log files I previously posted). If the exception is anything but StoreClosedException, we would see a stack trace with the exception that is occurring and a StoreClosedException would cause the getStore() method to be called again after 30 seconds which it is not. And since 2.0.1 and prior does not have any issues, I request you try to recreate as shown running 2 different sessions against 2 mailboxes in 2 separate threads. That is the only thing I can think of which would be causing the following apparently when either the getStore() is called or connect() is called:

2022-09-30T10:43:11.566Z  
STDOUT: DEBUG IMAP: IMAPStore cleanup, not connected
2022-09-30T10:43:11.566Z com.sun.mail.imap.IMAPStore cleanup
FINE: IMAPStore cleanup, not connected
2022-09-30T10:43:11.566Z com.sun.mail.imap.IMAPStore emptyConnectionPool
FINE: removed all authenticated connections from pool
2022-09-30T10:43:11.566Z  
STDOUT: DEBUG IMAP: IMAPStore cleanup, not connected
2022-09-30T10:43:11.566Z com.sun.mail.imap.IMAPStore cleanup
FINE: IMAPStore cleanup, not connected
2022-09-30T10:43:11.566Z com.sun.mail.imap.IMAPStore emptyConnectionPool
FINE: removed all authenticated connections from pool

Considering this is the same place with 2.0.1 is showing "STDOUT: DEBUG IMAP: trying to connect to host". Is 2.0.1 trying to connect as the end of init, getStore(), or connect()? I don't know. In any case, 2.1 (1.0.0) is not performing this connetion attempt but showing that the store is not connected and going straight to cleanup. The connect() method has never required a pre-existing connection in a connection pool as this would require the server and network to always be in place which is not a valid assumption. Even so, it appears the server is never being connected to, according to the debug logs, so this brings me back to the multi-session, multi-threaded environment I am working in which was not in your example.
You are also using getDefaultInstance() while I am using getInstance() in the session creation.

@jbescos
Copy link
Member

jbescos commented Oct 13, 2022

Hi @PeteSL

I have added one example where 2 threads are reading available emails with IMAP protocol in a while true. The thread hangs during one second having the connection open, to make sure the other thread is able to open another connection in parallel.

I didn't find the issue. Could you try it too?:

import java.io.IOException;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import jakarta.mail.Folder;
import jakarta.mail.MessagingException;
import jakarta.mail.Session;
import jakarta.mail.Store;
import jakarta.mail.internet.AddressException;

public class Main {

    private static final Properties MAIL_PROPERTIES = new Properties();

    static {
        MAIL_PROPERTIES.setProperty("mail.imap.host", "0.0.0.0");
        MAIL_PROPERTIES.setProperty("mail.imap.port", "18004");
        MAIL_PROPERTIES.setProperty("mail.imap.partialfetch", "false");
        MAIL_PROPERTIES.setProperty("mail.imap.ignorebodystructuresize", "true");
    }

    public static void main(String[] args)
            throws AddressException, MessagingException, IOException, InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        Session session = Session.getDefaultInstance(MAIL_PROPERTIES);
        executor.execute(() -> {
            while (true) {
                readEmails(session, "user02@james.local", "imap");
            }
        });
        executor.execute(() -> {
            while (true) {
                readEmails(session, "user02@james.local", "imap");
            }
        });
        executor.awaitTermination(1, TimeUnit.DAYS);
    }

    private static void readEmails(Session session, String userMail, String protocol) {
        try (Store store = session.getStore(protocol)) {
            store.connect(userMail, "1234");
            try (Folder folder = store.getFolder("INBOX")) {
                folder.open(Folder.READ_WRITE);
                Thread.sleep(1000L);
                System.out.println(Thread.currentThread().toString() + ": " + protocol + " -> Message count: " + folder.getMessageCount());
            }
        } catch (MessagingException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Output:


Thread[pool-1-thread-2,5,main]: imap -> Message count: 102
Thread[pool-1-thread-1,5,main]: imap -> Message count: 102
Thread[pool-1-thread-2,5,main]: imap -> Message count: 102
Thread[pool-1-thread-1,5,main]: imap -> Message count: 102
Thread[pool-1-thread-1,5,main]: imap -> Message count: 102
Thread[pool-1-thread-2,5,main]: imap -> Message count: 102
Thread[pool-1-thread-1,5,main]: imap -> Message count: 102
Thread[pool-1-thread-2,5,main]: imap -> Message count: 102
Thread[pool-1-thread-1,5,main]: imap -> Message count: 102
Thread[pool-1-thread-2,5,main]: imap -> Message count: 102
Thread[pool-1-thread-1,5,main]: imap -> Message count: 102
Thread[pool-1-thread-2,5,main]: imap -> Message count: 102
Thread[pool-1-thread-1,5,main]: imap -> Message count: 102
Thread[pool-1-thread-2,5,main]: imap -> Message count: 102

@PeteSL
Copy link
Author

PeteSL commented Oct 13, 2022

You are using a single session using getDefaultInstance() which is not usable in my situation where the 2 sessions must be independent. In my app, the 2 integral clients are completely independent in their stores (and transports) which is typical in any client where multiple mailboxes are being supported. For your tests, you must use multiple getInstance() methods in different threads which then will use independent stores in other threads. For instance (mail properties not shown):

public static final Executor exec = Executors.newCachedThreadPool();

static main() {
exec.execute(new establishSession(props1));
exec.execute(new establishSession(props2));
}

static class establishSession implements Runnable {
private final Properties props;
establishSession(Properties props) {
this.props = props;
}
public void run() {
exec.execute(new IMAPcheck(Session.getInstance(props)));
}
}

static class IMAPcheck implements Runnable {
private final Session sn;
IMAPcheck(Session sn) {
this.sn = sn;
}
public void run() {
    for (;;)
    {
        try (IMAPStore mailsrvr = (IMAPStore) sn.getStore())
        {
            mailsrvr.connect(user, password);
            for (;;)
                try (IMAPFolder inbox = (IMAPFolder) mailsrvr.getFolder("INBOX"))
                {
                    inbox.open(IMAPFolder.READ_WRITE);
                    for (;;)
                    {
                        while (inbox.getMessageCount() > 0)
                            System.out.println(inbox.getMessageCount());
// Purge mailbox
                        inbox.idle(true);
                    }
                }
                catch (FolderClosedException e)
                {
                }
        }
        catch (StoreClosedException e)
        {
               e.printStackTrace();
        }
        catch (IOException | MessagingException e)
        {
               e.printStackTrace();
        }
        try
        {
            TimeUnit.SECONDS.sleep(30L);
        }
        catch (InterruptedException e)
        {
               e.printStackTrace();
        }
    }
}
}

@jbescos
Copy link
Member

jbescos commented Oct 13, 2022

@PeteSL I have tried that too and it is working. In this case both sessions are connecting to the same host:port.

I cannot imagine why it is not working in your case. Each session instance is isolated, so having multiple threads should not make an issue.

This is the example with the modification:

public class Main {

    private static final Properties MAIL_PROPERTIES = new Properties();

    static {
        MAIL_PROPERTIES.setProperty("mail.imap.host", "0.0.0.0");
        MAIL_PROPERTIES.setProperty("mail.imap.port", "18004");
        MAIL_PROPERTIES.setProperty("mail.imap.partialfetch", "false");
        MAIL_PROPERTIES.setProperty("mail.imap.ignorebodystructuresize", "true");
    }

    public static void main(String[] args)
            throws AddressException, MessagingException, IOException, InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        executor.execute(() -> {
            Session session = Session.getInstance(MAIL_PROPERTIES);
            while (true) {
                readEmails(session, "user02@james.local", "imap");
            }
        });
        executor.execute(() -> {
            Session session = Session.getInstance(MAIL_PROPERTIES);
            while (true) {
                readEmails(session, "user02@james.local", "imap");
            }
        });
        executor.awaitTermination(1, TimeUnit.DAYS);
    }

    private static void readEmails(Session session, String userMail, String protocol) {
        try (Store store = session.getStore(protocol)) {
            store.connect(userMail, "1234");
            try (Folder folder = store.getFolder("INBOX")) {
                folder.open(Folder.READ_WRITE);
                Thread.sleep(1000L);
                System.out.println(Thread.currentThread().toString() + ": " + protocol + " -> Message count: " + folder.getMessageCount());
            }
        } catch (MessagingException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

@PeteSL
Copy link
Author

PeteSL commented Oct 13, 2022

The difference I see is your getStore is in the same thread as the Session.getInstance(). Try using an unbounded thread pool (or up your number of threads to 4) and have the store access in a different thread from the Session.getInstance(). Also, set up 2 mailboxes with 2 different from addresses (can be same server) to ensure there is no bleed over between sessions.

As I have said and the logs show, there is no difference in my code from 2.0.1 (and prior going back to 1.6.x) and using the 2.1.0 and Angus 1.0.0 packages/modules. but with 2.1.0/Angus 1.0.0 there is no attempt to connect to the server with either session and both sessions go directly to cleanup (either when connect() is called or getStore() is called, from what I can figure from the logs).

One last thing: in my code, the thread that creates the session is gone after the session is created. This is true in my example and in my code so if any ThreadLocal methods/variables are being used in the session instance, they will be gone by the time the actual connection code gets executed.

@PeteSL
Copy link
Author

PeteSL commented Oct 13, 2022

In my base code, the Session.getInstance() is in the class constructor. This means that it is possible the session instances are created on the same thread but the code that calls getStore() and connect() are on independent threads from executors. So your test could be

main(){
new establishSession(props1).run();
new establishSession(props2).run();
}

This would create the sessions on the same thread (which might disappear) similar to my production code but the store access will be on different threads. Again, be sure to use different mailboxes (same server ok) so no ThreadLocal interference.

@jbescos
Copy link
Member

jbescos commented Oct 13, 2022

Here is the new way. The session is created in a new thread and I added a new IMAP port to connect.

public class Main {

    public static void main(String[] args)
            throws AddressException, MessagingException, IOException, InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        executor.execute(() -> {
            try {
                Properties properties = new Properties();
                properties.setProperty("mail.imap.host", "0.0.0.0");
                properties.setProperty("mail.imap.port", "18004");
                properties.setProperty("mail.imap.partialfetch", "false");
                properties.setProperty("mail.imap.ignorebodystructuresize", "true");
                Session session = Executors.newSingleThreadExecutor().submit(() -> {
                        System.out.println(Thread.currentThread().toString() + ": Creating new session");
                        return Session.getInstance(properties);
                    }).get();
                while (true) {
                    readEmails(session, "user02@james.local", "imap");
                }
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        });
        executor.execute(() -> {
            try {
                Properties properties = new Properties();
                properties.setProperty("mail.imap.host", "0.0.0.0");
                properties.setProperty("mail.imap.port", "18014");
                properties.setProperty("mail.imap.partialfetch", "false");
                properties.setProperty("mail.imap.ignorebodystructuresize", "true");
                Session session = Executors.newSingleThreadExecutor().submit(() -> {
                        System.out.println(Thread.currentThread().toString() + ": Creating new session");
                        return Session.getInstance(properties);
                    }).get();
                while (true) {
                    readEmails(session, "user02@james.local", "imap");
                }
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        });
        executor.awaitTermination(1, TimeUnit.DAYS);
    }

    private static void readEmails(Session session, String userMail, String protocol) {
        try (Store store = session.getStore(protocol)) {
            store.connect(userMail, "1234");
            try (Folder folder = store.getFolder("INBOX")) {
                folder.open(Folder.READ_WRITE);
                Thread.sleep(1000L);
                System.out.println(Thread.currentThread().toString() + ": " + protocol + " -> Message count: " + folder.getMessageCount());
            }
        } catch (MessagingException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

This is the output:

Thread[pool-2-thread-1,5,main]: Creating new session
Thread[pool-3-thread-1,5,main]: Creating new session
Thread[pool-1-thread-2,5,main]: imap -> Message count: 0
Thread[pool-1-thread-1,5,main]: imap -> Message count: 102
Thread[pool-1-thread-2,5,main]: imap -> Message count: 0
Thread[pool-1-thread-1,5,main]: imap -> Message count: 102
Thread[pool-1-thread-2,5,main]: imap -> Message count: 0
Thread[pool-1-thread-1,5,main]: imap -> Message count: 102
Thread[pool-1-thread-2,5,main]: imap -> Message count: 0
Thread[pool-1-thread-1,5,main]: imap -> Message count: 102
Thread[pool-1-thread-2,5,main]: imap -> Message count: 0
Thread[pool-1-thread-1,5,main]: imap -> Message count: 102

@PeteSL
Copy link
Author

PeteSL commented Oct 13, 2022

Can you try the way I showed (main() creates the session on that thread but uses a cached thread pool for executing the store accesses)? Also, can you set up 2 different from addresses (mailboxes)? In my production environment, everything is the same between the 2 integral clients except the from address, user name, and password. Again, what we are trying to determine is what is causing Angus 1.0.0 go straight to cleanup saying no connections instead of even trying to connect. I am trying to provide as close an example to my production environment as I can considering threads, mailboxes, etc. I use no system properties that would directly affect jakarta.mail. I have provided the properties files which you are properly duplicating. And the run code that is actually accessing the store/mailbox has been shown above. I am asking where is the difference in the getStore()/connect() mechanism between 2.0.1 (and before) and 2.1.0/Angus 1.0.0 that would cause the connect sequence in earlier implementations but not now? Is there a difference in using SocketChannel instead of Socket? Is there a difference in defaults which needs to be addressed in the properties? I don't know but I am asking the question because, at this time, 2.1.0 is non-functional with my app yet all prior versions work on Java 8 through Java 19 (jakarta.mail with Java 11 on).

@jbescos
Copy link
Member

jbescos commented Oct 13, 2022

I tried it like this, but it also works:

    public static void main(String[] args)
            throws AddressException, MessagingException, IOException, InterruptedException, ExecutionException {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        ExecutorService singleThread = Executors.newSingleThreadExecutor();
        Properties properties1 = new Properties();
        properties1.setProperty("mail.imap.host", "0.0.0.0");
        properties1.setProperty("mail.imap.port", "18004");
        properties1.setProperty("mail.imap.partialfetch", "false");
        properties1.setProperty("mail.imap.ignorebodystructuresize", "true");
        Session session1 = singleThread.submit(() -> Session.getInstance(properties1)).get();
        executor.execute(() -> {
            while (true) {
                readEmails(session1, "user02@james.local", "imap");
            }
        });
        Properties properties2 = new Properties();
        properties2.setProperty("mail.imap.host", "0.0.0.0");
        properties2.setProperty("mail.imap.port", "18014");
        properties2.setProperty("mail.imap.partialfetch", "false");
        properties2.setProperty("mail.imap.ignorebodystructuresize", "true");
        Session session2 = singleThread.submit(() -> Session.getInstance(properties2)).get();
        executor.execute(() -> {
            while (true) {
                readEmails(session2, "user02@james.local", "imap");
            }
        });
        executor.awaitTermination(1, TimeUnit.DAYS);
    }

I have the IMAP server in a docker image. I created 2 containers of the same image. One is for port 18004 and the other for port 18014. These are 2 different addresses.

In mail 2.1.0, the only change was to make a split between API (jakarta.mail:jakarta.mail-api) and implementation (org.eclipse.angus:angus-mail). There are no changes related to sockets, improvements, etc.

Could you remove your dependency jakarta.mail and replace it by the next?:

		<dependency>
			<groupId>org.eclipse.angus</groupId>
			<artifactId>angus-mail</artifactId>
			<version>1.0.0</version>
		</dependency>

@PeteSL
Copy link
Author

PeteSL commented Oct 13, 2022

I am not using Maven. This is a production app and has no project settings. It is being run with everything on the module path, all modules on the module path are being added, and it is running as a modular Java application as it should be. There were other changes in the implementation (SSL issue showed a change from Socket to SocketChannel for SSL connections, for instance). You are still creating each session on different threads which is not what my code is doing. In my app and example, the sessions are being created on the same thread but the store accesses are on different threads. In your example, you are using the same user name/email from address for both stores. In my case, the user name/from address is different but the server is the same.

@jbescos
Copy link
Member

jbescos commented Oct 13, 2022

Can you print down the module path?, so I can see the mail, activation and angus dependencies.

What other changes related to SSL do you mean?.

The thread is created in a thread pool (ExecutorService singleThread = Executors.newSingleThreadExecutor();) that contains only one thread. So the same thread is creating 2 sessions.

I have changed the user and keep the same address, and it is working too. Here it is:


    public static void main(String[] args)
            throws AddressException, MessagingException, IOException, InterruptedException, ExecutionException {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        ExecutorService singleThread = Executors.newSingleThreadExecutor();
        Properties properties1 = new Properties();
        properties1.setProperty("mail.imap.host", "0.0.0.0");
        properties1.setProperty("mail.imap.port", "18004");
        properties1.setProperty("mail.imap.partialfetch", "false");
        properties1.setProperty("mail.imap.ignorebodystructuresize", "true");
        Session session1 = singleThread.submit(() -> Session.getInstance(properties1)).get();
        executor.execute(() -> {
            while (true) {
                readEmails(session1, "user02@james.local", "imap");
            }
        });
        Properties properties2 = new Properties();
        properties2.setProperty("mail.imap.host", "0.0.0.0");
        properties2.setProperty("mail.imap.port", "18004");
        properties2.setProperty("mail.imap.partialfetch", "false");
        properties2.setProperty("mail.imap.ignorebodystructuresize", "true");
        Session session2 = singleThread.submit(() -> Session.getInstance(properties2)).get();
        executor.execute(() -> {
            while (true) {
                readEmails(session2, "user01@james.local", "imap");
            }
        });
        executor.awaitTermination(1, TimeUnit.DAYS);
    }

@PeteSL
Copy link
Author

PeteSL commented Oct 13, 2022

As shown in my main() example, I do not use an executor to create the sessions but ok, I understand what you are saying about the single thread being reused although I believe the thread is reinitialized between uses by the single thread executor so there is no bleed over of ThreadLocal variables, etc.

The module path is application.jar;lib (running on Windows so semicolon) where lib contains all the rest of the application jars. The mail jars in the lib folder are jakarta.activation-api-2.1.0.jar, angus-activation-1.0.0.jar, jakarta.mail-api-2.1.0.jar, angus-mail-1.0.0.jar, dsn-1.0.0.jar and I have --add-modules ALL-MODULE-PATH. When running 2.0.1, activation remains the same but the angus mail jars are removed (dsn-1.0.0.jar, angus-mail-1.0.0.jar, and jakarta.mail-api-2.1.0.jar) and replaced with jakarta.mail-2.0.1.jar and dsn-2.0.1.jar.

The other thing I do is my properties has mail.store.protocol=imap That shouldn't make a difference but... If you turn on mail.debug=true, are you seeing the same sequences I provided in 2.0.1 or 2.1.0? I did not edit out anything in those sequences in the logs I provided (log 2 is 2.1.0, log 1 then log 0 is 2.0.1).

@jmehrens
Copy link
Contributor

jmehrens commented Oct 13, 2022

Is there a difference in using SocketChannel instead of Socket?

If you are using Folder.idle then you can't use SocketChannel

Since you are using ScheduledThreadPoolExecutor all runnables are wrapped in a FutureTask. This FutureTask can trap and hold java.lang.Error objects. These error tend to skip by catch blocks which is a problem if Future::get is not called to extract that error.

In your Runnable add:

catch(Error unexpected) { unexpected.printStackTrace(); }

This will help troubleshoot if there is a problem with say a LinkageError or ServiceConfigurationError

@PeteSL
Copy link
Author

PeteSL commented Oct 13, 2022

That is why I asked if there was a change in defaults because I have never used SocketChannel. I can add a catch (Exception ex) to the run task but that will have to wait until I am back in the office this evening.

@jmehrens
Copy link
Contributor

jmehrens commented Oct 13, 2022

Don't catch Exception but rather catch java.lang.Error and java.lang.RuntimeException

@PeteSL
Copy link
Author

PeteSL commented Oct 14, 2022

We now have the answer:

2022-10-14T01:40:51.053Z net.ae5pl.emailgate.SunImapTask run
SEVERE: Unexpected Error
java.lang.IllegalAccessError: class net.ae5pl.emailgate.SunImapTask (in module net.ae5pl.emailgate) cannot access class com.sun.mail.imap.IMAPStore (in module com.sun.mail) because module net.ae5pl.emailgate does not read module com.sun.mail
	at net.ae5pl.emailgate/net.ae5pl.emailgate.SunImapTask.run(SunImapTask.java:42)
	at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:577)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
	at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
	at java.base/java.lang.Thread.run(Thread.java:1589)

This error does not occur with 2.0.1 because there is no module com.sun.mail; these are all exported packages in the jakarta.mail module. This is where modules are going to get us caught. If I try to use the jakarta.mail jar that has angus bundled with the API, dsn won't work because dsn.jar requires module com.sun.mail. So we have a module issue where the implementation module, com.sun.mail, is overriding the API exposed definitions which would require a special module.class file just for 2.1.0 which would not be compatible with any prior versions.

Current workaround for any application which was working with 2.0.1 is to add the following to the command line:
--add-reads app.module.name=com.sun.mail
where app.module.name is the name of the module that does not have "requires com.sun.mail;" in module.java.
Module interdependencies are so easy to mess up; you can't have circular dependencies; java.mail has always exposed a lot of the com.sun.mail packages as part of the API; differentiating API declarations from implementation declarations is definitely going to be tricky.

@jbescos
Copy link
Member

jbescos commented Oct 14, 2022

Thank you all. I have created a new issue to keep it simple: #32

@PeteSL could you confirm that is the problem?

@PeteSL
Copy link
Author

PeteSL commented Oct 14, 2022

I have commented on #32 and also think that the release notes need to call out that a new module is being used for any references to com.sun.mail packages (like com.sun.mail.imap) and the --add-reads command line argument must be used with applications written for prior versions jakarta.mail, specifically for 2.0.

@PeteSL
Copy link
Author

PeteSL commented Nov 6, 2022

Using --add-reads for modules compiled pre-Angus mail works well. If you are compiling to Jakarta Mail 2.1.0/Angus Mail, use the following in your module-info.java to maintain compatibility with the bundled Jakarta Mail and with Jakarta Mail 2.0.1:

    requires jakarta.mail;
    requires static com.sun.mail; // To accommodate Jakarta EE 10
    requires com.sun.mail.dsn;

@jbescos
Copy link
Member

jbescos commented Nov 8, 2022

It was solved here: #32

We can close this.

@jbescos jbescos closed this as completed Nov 8, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants