javaspring-bootjakarta-mailimap

Issue with Jakarta Mail - MessageCountAdapter doesn't seem to work


I'm encountering an issue while using Jakarta Mail to fetch emails and capture specific keywords. Here's the details:

The source code is as follows:

package org.example.receiver.kit.component;

import jakarta.mail.Folder;
import jakarta.mail.Message;
import jakarta.mail.MessagingException;
import jakarta.mail.Session;
import jakarta.mail.Store;
import jakarta.mail.event.MessageCountAdapter;
import jakarta.mail.event.MessageCountEvent;
import lombok.NonNull;
import org.eclipse.angus.mail.imap.IdleManager;
import org.springframework.stereotype.Component;

import java.io.Closeable;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
import java.util.logging.Logger;

@Component
public class MailReceiver implements Receiver {
    public static final Logger logger = Logger.getLogger(MailReceiver.class.getName());
    public static final String INBOX = "INBOX";

    public static class CustomMessageCountAdapter extends MessageCountAdapter {
       private final IdleManager idleManager;
       private final Folder inbox;
       private final Consumer<Message[]> messageConsumer;

       public CustomMessageCountAdapter(IdleManager idleManager, Folder inbox, Consumer<Message[]> messageConsumer) {
          this.idleManager = idleManager;
          this.inbox = inbox;
          this.messageConsumer = messageConsumer;
       }

       @Override
       public void messagesAdded(MessageCountEvent e) {
          logger.info("Receive" + e.getMessages().length + "mail(s)");
          messageConsumer.accept(e.getMessages());

          try {
             idleManager.watch(inbox);
          } catch (MessagingException ex) {
             throw new RuntimeException(ex);
          }
       }

       @Override
       public void messagesRemoved(MessageCountEvent e) {
          logger.info("Delete" + e.getMessages().length + "mail(s)");
          messageConsumer.accept(e.getMessages());

          try {
             idleManager.watch(inbox);
          } catch (MessagingException ex) {
             throw new RuntimeException(ex);
          }
       }
    }

    public record MailProcessCleaner(Store mailStore, Folder inbox, ExecutorService es) implements Closeable {
       public static final Logger logger = Logger.getLogger(MailReceiver.class.getName());
       @Override
          public void close() throws IOException {
             try {
                if (inbox.isOpen()) {
                   inbox.close();
                   logger.info("Mail server was closed");
                }
                if (mailStore.isConnected()) {
                   mailStore.close();
                   logger.info("Disconnection");
                }
                es.shutdown();
                logger.info("Close thread pool");
             } catch (MessagingException e) {
                throw new RuntimeException(e);
             }
          }
       }

    @Override
    public MailProcessCleaner receive(Consumer<Message[]> messageConsumer, @NonNull MailSessionProvider sessionProvider) throws MessagingException {
       Session session = sessionProvider.provide();
       ExecutorService es = Executors.newCachedThreadPool();
       try {
          IdleManager idleManager = new IdleManager(session, es);

          Store mailStore = session.getStore();
          mailStore.connect();

          Folder inbox = mailStore.getFolder(INBOX);
          inbox.open(Folder.READ_ONLY);

          inbox.addMessageCountListener(getMessageCountAdapter(idleManager, inbox, messageConsumer));

          idleManager.watch(inbox);

          // messageConsumer.accept(inbox.getMessages());

          return new MailProcessCleaner(mailStore, inbox, es);

       } catch (IOException e) {
          throw new RuntimeException(e);
       }
    }

    protected MessageCountAdapter getMessageCountAdapter(IdleManager idleManager, Folder inbox, Consumer<Message[]> messageConsumer) {
       return new CustomMessageCountAdapter(idleManager, inbox, messageConsumer);
    }

}

My Configuration:

package org.example.receiver.kit.component;

import jakarta.mail.Authenticator;
import jakarta.mail.PasswordAuthentication;
import jakarta.mail.Session;
import lombok.Getter;

import java.util.Properties;
import java.util.logging.Logger;

@Getter
public abstract class MailSessionProvider implements Provider {
    public static final Logger logger = Logger.getLogger(MailSessionProvider.class.getName());
    private final String username;
    private final String protocol;
    private final String host;
    private final Integer port;
    private final boolean sslEnabled;
    public static final String PASSWORD_ENV_KEY = "__MAIL_PASSWORD";

    protected Properties defaultProperties = new Properties();


    public MailSessionProvider(String username, String protocol, String host, Integer port, boolean sslEnabled) {
       this.username = username;
       this.protocol = protocol;
       this.host = host;
       this.port = port;
       this.sslEnabled = sslEnabled;
       createDefaultProperties();
    }


    private void createDefaultProperties() {
       // https://jakarta.ee/specifications/mail/2.1/jakarta-mail-spec-2.1#a823
       defaultProperties.setProperty("mail.store.protocol", this.protocol);
       defaultProperties.setProperty("mail.imap.host", this.host);
       defaultProperties.setProperty("mail.imap.port", String.valueOf(this.port));
       defaultProperties.setProperty("mail.imap.ssl.enable", String.valueOf(this.sslEnabled));
       defaultProperties.setProperty("mail.imap.starttls.enable", "false");
//     defaultProperties.setProperty("mail.imap.nio.enable", "true");
       defaultProperties.setProperty("mail.imap.usesocketchannels", "true");
    }

    @Override
    public Session provide() {
       Properties properties = getConnectionProperties();

       if (properties == null) {
          properties = defaultProperties;
       }

       return Session.getInstance(
             properties,
             new Authenticator() {
                @Override
                protected PasswordAuthentication getPasswordAuthentication() {
                   return new PasswordAuthentication(username, getPassword());
                }
             });
    }

    public final String getPassword() {
       return System.getenv(PASSWORD_ENV_KEY);
    }


    @Override
    public Properties getConnectionProperties() {
       return defaultProperties;
    }

    protected void setDefaultProperty(String key, String value) {
       defaultProperties.setProperty(key, value);
    }
}

Entry:

@Autowired
FeishuMailSessionProvider feishuMailSessionProvider;

@Autowired
FeishuMailReceiver feishuMailReceiver;
@Test
public void test() throws MessagingException {
    MailReceiver.MailProcessCleaner cleaner = feishuMailReceiver.receive((messages) -> {
       try {
          for (Message message : messages) {
             System.out.println("Got" + Arrays.toString(message.getFrom()));
             System.out.println(message.getSubject());
             System.out.println(message.getContent());

          }
       } catch (IOException | MessagingException e) {
          throw new RuntimeException(e);
       }
       System.out.println(messages.length);
    }, feishuMailSessionProvider);

    for(;;);
}

**Environment:**

Problem Description:

The program works well for a period after startup. When I send test emails to the registered email address, it successfully receives them and triggers notifications—I get the message "Receive 1 mail(s)".

However, there are two odd behaviors:

  1. It no longer sends notifications when I delete emails. While this doesn't affect my current functionality, I'm curious about the reason.

  2. After the program runs for about 30mins, the MessageCountAdapter seems to stop receiving new emails and doesn't invoke my overridden messagesAdded method. However, if I send the email within 1–5 minutes of the program starting, it works fine—running for 6 minutes, 10 minutes, or even longer (I haven’t tested the upper limit yet).

Could someone please explain this behavior?


Solution

  • The 30 minutes you see is very likely the autologout timer: «If a server has an inactivity autologout timer that applies to sessions after authentication, the duration of that timer MUST be at least 30 minutes.»

    The next sentence in the RFC hints to a solution: «The receipt of any command from the client during that interval resets the autologout timer.»

    Call ((IMAPFolder)inbox).forceCheckExpunged() every ten minutes or so. That'll send a NOOP command, which also has the side effect of telling the server that now's a good time to tell you about expunged (deleted) messages, and the side-side-side effect of telling any NAT middleboxes along the route that the connection is in use.