Work in progress ...

This commit is contained in:
Konrad Neitzel
2025-11-30 18:58:05 +01:00
parent 9bbddaaae9
commit 5503dd192e
64 changed files with 4855 additions and 4252 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
@@ -7,6 +7,7 @@ import java.util.Objects;
* This class provides methods to set or get the account details and overrides methods for string
* representation, equality checks, and hashing.
*/
@SuppressWarnings("unused")
public class ImapAccount {
/**
@@ -15,15 +16,6 @@ public class ImapAccount {
* of the mail server that handles IMAP communication.
*/
protected String server;
/**
* Retrieves the server address associated with this IMAP*/
public String getServer() { return server; }
/**
* Sets the server address for the IMAP account.
*
* @param server the server address to be configured for the IMAP account
*/
public void setServer(String server) { this.server = server; }
/**
* Represents the username or identifier used to authenticate the IMAP account.
@@ -31,66 +23,94 @@ public class ImapAccount {
* to the IMAP server.
*/
protected String user;
/**
* Retrieves the username associated with this IMAP account.
*
* @return the username of the IMAP account as a String.
*/
public String getUser() { return user; }
/**
* Sets the username associated with the IMAP account.
*
* @param user The username to be set for this IMAP account.
*/
public void setUser(String user) { this.user = user; }
/**
* The password associated with the IMAP account.
* This is used to authenticate the user when connecting to the IMAP server.
*/
protected String password;
/**
* Retrieves the password associated with this IMAP account.
*
* @return the password of the IMAP account.
*/
public String getPassword() { return password; }
/**
* Sets the password for the IMAP account.
*
* @param password The password to be set for the account.
*/
public void setPassword(String password) { this.password = password; }
/**
* Specifies the protocol used for the IMAP account, such as "IMAP" or "IMAPS".
* This field determines the communication protocol for interacting with the server.
*/
protected String protocol;
/**
* Default constructor only
*/
public ImapAccount() {
// default constructor only
}
/**
* Retrieves the server address associated with this IMAP
*/
public String getServer() {
return server;
}
/**
* Sets the server address for the IMAP account.
*
* @param server the server address to be configured for the IMAP account
*/
public void setServer(String server) {
this.server = server;
}
/**
* Retrieves the username associated with this IMAP account.
*
* @return the username of the IMAP account as a String.
*/
public String getUser() {
return user;
}
/**
* Sets the username associated with the IMAP account.
*
* @param user The username to be set for this IMAP account.
*/
public void setUser(String user) {
this.user = user;
}
/**
* Retrieves the password associated with this IMAP account.
*
* @return the password of the IMAP account.
*/
public String getPassword() {
return password;
}
/**
* Sets the password for the IMAP account.
*
* @param password The password to be set for the account.
*/
public void setPassword(String password) {
this.password = password;
}
/**
* Retrieves the protocol used by this IMAP account.
*
* @return the protocol as a String, which defines the communication method for the IMAP account.
*/
public String getProtocol() { return protocol; }
/**
* Sets the protocol used by the IMAP account.
* @param protocol the protocol to be used, e.g., "IMAP" or "IMAPS".
*/
public void setProtocol(String protocol) { this.protocol = protocol; }
public String getProtocol() {
return protocol;
}
/**
* Provides a string representation of the ImapAccount object including its server, user,
* a masked password, and protocol information. The password is represented as '########' if it is not empty.
* Sets the protocol used by the IMAP account.
*
* @return A formatted string that describes the ImapAccount object with its server, user, masked password, and protocol.
* @param protocol the protocol to be used, e.g., "IMAP" or "IMAPS".
*/
@Override public String toString() {
return String.format("ImapAccount(%s, %s, %s, %s",
server,
user,
password.length()==0 ? "''" : "########" ,
protocol);
public void setProtocol(String protocol) {
this.protocol = protocol;
}
/**
@@ -100,7 +120,8 @@ public class ImapAccount {
*
* @return An integer hash code representing this ImapAccount instance.
*/
@Override public int hashCode() {
@Override
public int hashCode() {
return Objects.hash(server, user, password, protocol);
}
@@ -111,7 +132,8 @@ public class ImapAccount {
* @param obj the object to compare with this instance
* @return true if the specified object is equal to this ImapAccount instance; false otherwise
*/
@Override public boolean equals(Object obj) {
@Override
public boolean equals(Object obj) {
// Check type of argument, includes check of null.
if (!(obj instanceof ImapAccount)) return false;
@@ -120,9 +142,24 @@ public class ImapAccount {
// Check the comparison of all field
ImapAccount other = (ImapAccount) obj;
return (server == other.server || (server != null && server.equals(other.server))) &&
(user == other.user || (user != null && user.equals(other.user))) &&
(password == other.password || (password != null && password.equals(other.password))) &&
(protocol == other.protocol || (protocol != null && protocol.equals(other.protocol)));
return Objects.equals(server, other.server) &&
Objects.equals(user, other.user) &&
Objects.equals(password, other.password) &&
Objects.equals(protocol, other.protocol);
}
/**
* Provides a string representation of the ImapAccount object including its server, user,
* a masked password, and protocol information. The password is represented as '########' if it is not empty.
*
* @return A formatted string that describes the ImapAccount object with its server, user, masked password, and protocol.
*/
@Override
public String toString() {
return String.format("ImapAccount(%s, %s, %s, %s",
server,
user,
(password == null || password.isEmpty()) ? "''" : "########",
protocol);
}
}
@@ -2,8 +2,18 @@ package de.neitzel.net.imap;
import lombok.extern.slf4j.Slf4j;
import javax.mail.*;
import java.util.*;
import javax.mail.Flags;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.NoSuchProviderException;
import javax.mail.Session;
import javax.mail.Store;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
* The ImapAccountMonitor class is responsible for monitoring an IMAP email account
@@ -44,13 +54,20 @@ public class ImapAccountMonitor {
*/
protected Map<String, Folder> folders = new HashMap<String, Folder>();
/**
* A collection of listeners that respond to IMAP-related events.
* Each listener in this collection can perform specific actions when triggered
* by events such as receiving new email notifications.
*/
private Collection<ImapListener> imapListeners = new ArrayList<ImapListener>();
/**
* Monitors an IMAP account by establishing a connection to the specified mail server
* using the provided account credentials and protocol.
*
* @param account the IMAP account containing details such as server, user, password, and protocol
* @throws NoSuchProviderException if the specified mail store protocol is not available
* @throws MessagingException if the connection to the mail server fails
* @throws MessagingException if the connection to the mail server fails
*/
public ImapAccountMonitor(ImapAccount account) throws NoSuchProviderException, MessagingException {
log.trace("Constructor ({})", account.toString());
@@ -77,19 +94,19 @@ public class ImapAccountMonitor {
* Closes the resources associated with the current instance, including
* IMAP folders and the store. The method ensures that all folders are
* closed properly, followed by the closure of the store.
*
* <p>
* Any exceptions encountered during the closure of folders or the store
* are caught and logged without interrupting the overall closing process.
* This is to ensure that all resources are attempted to be closed even if
* an error occurs with one of them.
*
* <p>
* The method logs the start and end of the closing process for traceability.
*/
public void close() {
log.trace("close() called.");
// Close the folders
for (Folder folder: folders.values()) {
for (Folder folder : folders.values()) {
try {
folder.close(false);
} catch (MessagingException ex) {
@@ -152,28 +169,28 @@ public class ImapAccountMonitor {
* Checks all monitored folders for new email messages, processes unseen and undeleted messages,
* and raises a NewMailEvent for each new email discovered. The method ensures processed messages
* are marked as seen.
*
* <p>
* The method iterates through all folders currently being monitored, fetching their messages and
* evaluating the state of each. If a message is neither marked as seen nor deleted, it is marked
* as seen and a NewMailEvent is raised for it.
*
* <p>
* Proper error handling is implemented to log any MessagingException that occurs while processing
* the messages of a folder without interrupting the processing of other folders.
*
* <p>
* Note: This method relies on external logging (via `log`) and appropriate event handling
* via `raiseNewEmailEvent`. The `folders` collection holds the monitored folders required.
*
* <p>
* The folder processing stops only in case of irrecoverable exceptions outside this method's scope.
*/
public void check() {
log.trace("check() called.");
// Loop through all folders that are monitored
for (Folder folder: folders.values()) {
for (Folder folder : folders.values()) {
try {
// Loop through all messages.
Message[] messages = folder.getMessages();
for (Message message: messages) {
for (Message message : messages) {
// Check if message wasn't seen and wasn't deleted
if (!message.isSet(Flags.Flag.SEEN) && !message.isSet(Flags.Flag.DELETED)) {
// Mark message as seen.
@@ -193,11 +210,35 @@ public class ImapAccountMonitor {
}
/**
* A collection of listeners that respond to IMAP-related events.
* Each listener in this collection can perform specific actions when triggered
* by events such as receiving new email notifications.
* Raises a new email event and notifies all registered listeners.
* The method loops through the listeners and invokes their {@code NewEmailReceived}
* method with a newly created {@link NewEmailEvent}.
* If a listener marks the event as handled, the remaining listeners will
* not be notified.
*
* @param message The email message that triggered the event. This message
* is included in the {@link NewEmailEvent} passed to the listeners.
*/
private Collection<ImapListener> imapListeners = new ArrayList<ImapListener>();
protected void raiseNewEmailEvent(Message message) {
log.trace("raiseNewEmailEvent() called!");
// Create the event.
NewEmailEvent event = new NewEmailEvent(this, message);
// Call all listeners.
for (ImapListener listener : imapListeners) {
log.trace("Calling listener in {} ....", listener.getClass().getName());
listener.newEmailReceived(event);
// Check if the event was handled so no further ImaListeners will be called.
if (event.isHandled()) {
log.trace("raiseNewEmailEvent(): Breaking out of listener loop because event was handled.");
break;
}
}
log.trace("raiseNewEmailEvent() call ended!");
}
/**
* Adds an IMAP listener to receive notifications about IMAP-related events such as new email arrivals.
@@ -222,35 +263,4 @@ public class ImapAccountMonitor {
imapListeners.remove(listener);
}
}
/**
* Raises a new email event and notifies all registered listeners.
* The method loops through the listeners and invokes their {@code NewEmailReceived}
* method with a newly created {@link NewEmailEvent}.
* If a listener marks the event as handled, the remaining listeners will
* not be notified.
*
* @param message The email message that triggered the event. This message
* is included in the {@link NewEmailEvent} passed to the listeners.
*/
protected void raiseNewEmailEvent(Message message) {
log.trace("raiseNewEmailEvent() called!");
// Create the event.
NewEmailEvent event = new NewEmailEvent(this, message);
// Call all listeners.
for (ImapListener listener: imapListeners) {
log.trace("Calling listener in {} ....", listener.getClass().getName());
listener.newEmailReceived(event);
// Check if the event was handled so no further ImaListeners will be called.
if (event.isHandled()) {
log.trace("raiseNewEmailEvent(): Breaking out of listener loop because event was handled.");
break;
}
}
log.trace("raiseNewEmailEvent() call ended!");
}
}
@@ -37,8 +37,8 @@ public class NewEmailEvent extends EventObject {
* This event encapsulates information about a newly received email,
* including its associated message and the source of the event.
*
* @param source the object on which the event initially occurred; typically represents
* the source of the email event, such as an email client or server.
* @param source the object on which the event initially occurred; typically represents
* the source of the email event, such as an email client or server.
* @param message the Message object representing the email associated with this event.
*/
public NewEmailEvent(Object source, Message message) {
@@ -8,15 +8,49 @@ import javax.mail.internet.MimeMultipart;
import java.io.IOException;
/**
* Utility functions to use with javax.mail.Message instances.
* Utility methods for extracting textual content from {@link javax.mail.Message} instances.
*
* <p>This class provides helpers to obtain a best-effort plain-text representation of an email's
* content. It supports the common MIME types:
* <ul>
* <li>{@code text/plain} — returned as-is</li>
* <li>{@code text/html} — the HTML body is returned as a string (not converted to plain text)</li>
* <li>{@code multipart/*} — parts are inspected recursively; for {@code multipart/alternative}
* the last (most faithful) alternative is preferred; for other multipart types the textual
* content of all parts is concatenated in order.</li>
* </ul>
*
* <p>All methods return an empty string when no textual content can be found. The methods may throw
* {@link IOException} or {@link MessagingException} when the underlying message parts cannot be read.
*/
@SuppressWarnings("unused")
public class MessageUtils {
/**
* Gets the text of the email message.
* @param message Message to get the text from.
* @return Text of the email message.
* @throws IOException
* @throws MessagingException
* Private constructor for utility class.
* Prevents instantiation of this utility class.
*/
private MessageUtils() {
// Utility class - private constructor
}
/**
* Extracts a best-effort textual representation from the provided {@link Message}.
*
* <p>Behavior:
* <ul>
* <li>If the message mime-type is {@code text/plain} the plain text content is returned.</li>
* <li>If the message mime-type is {@code text/html} the HTML content is returned as a string.
* (This method does not perform HTML-to-plain-text conversion.)</li>
* <li>If the message mime-type is {@code multipart/*} the method inspects all parts recursively.
* For {@code multipart/alternative} the most faithful alternative (the last part) is chosen.
* For other multipart types the textual contents of all parts are concatenated in order.</li>
* </ul>
*
* @param message the email message from which to extract text; must not be {@code null}
* @return a <strong>non-null</strong> string containing the extracted text (possibly empty)
* @throws IOException if an I/O error occurs while reading message content
* @throws MessagingException if the message content cannot be processed due to MIME/format errors
*/
public static String getTextFromMessage(Message message) throws IOException, MessagingException {
String result = "";
@@ -30,11 +64,23 @@ public class MessageUtils {
}
/**
* Gets the text of a MimeMultipart.
* @param mimeMultipart MimeMultipart to get the text from.
* @return Text of the MimeMultipart instance.
* @throws IOException
* @throws MessagingException
* Extracts the textual content from a {@link MimeMultipart} instance.
*
* <p>Algorithm:
* <ul>
* <li>If the multipart has zero parts a {@link MessagingException} is thrown.</li>
* <li>If the multipart's content type matches {@code multipart/alternative} the last part
* (the most faithful alternative) is returned.</li>
* <li>Otherwise the method concatenates the result of {@link #getTextFromBodyPart(BodyPart)}
* for each part in document order and returns the combined text.</li>
* </ul>
*
* <p>The method handles nested multiparts by recursive calls.
*
* @param mimeMultipart the multipart object to inspect
* @return a non-null string containing the concatenated textual content of the multipart
* @throws IOException if an I/O error occurs while reading parts
* @throws MessagingException if the multipart is malformed or cannot be processed
*/
private static String getTextFromMimeMultipart(
MimeMultipart mimeMultipart) throws IOException, MessagingException {
@@ -47,20 +93,29 @@ public class MessageUtils {
// alternatives appear in an order of increasing
// faithfulness to the original content. Customize as req'd.
return getTextFromBodyPart(mimeMultipart.getBodyPart(count - 1));
String result = "";
StringBuilder result = new StringBuilder();
for (int i = 0; i < count; i++) {
BodyPart bodyPart = mimeMultipart.getBodyPart(i);
result += getTextFromBodyPart(bodyPart);
result.append(getTextFromBodyPart(bodyPart));
}
return result;
return result.toString();
}
/**
* Gets the text of a BodyPart
* @param bodyPart BodyPart to get text from.
* @return Text of body part or empty string if not a known type.
* @throws IOException
* @throws MessagingException
* Extracts textual content from a single {@link BodyPart}.
*
* <p>Behavior:
* <ul>
* <li>{@code text/plain}: returns the plain text content.</li>
* <li>{@code text/html}: returns the HTML content as a string (no conversion performed).</li>
* <li>If the body part itself contains a {@link MimeMultipart} this method recurses into it.</li>
* <li>For unknown or non-textual content types an empty string is returned.</li>
* </ul>
*
* @param bodyPart the body part to extract text from
* @return the textual content of the body part, or an empty string if none is available
* @throws IOException if an I/O error occurs while reading the body part content
* @throws MessagingException if the body part cannot be processed
*/
private static String getTextFromBodyPart(
BodyPart bodyPart) throws IOException, MessagingException {
@@ -73,8 +128,8 @@ public class MessageUtils {
//String html = (String) bodyPart.getContent();
// Not parsing the html right now!
// result = org.jsoup.Jsoup.parse(html).text();
} else if (bodyPart.getContent() instanceof MimeMultipart){
result = getTextFromMimeMultipart((MimeMultipart)bodyPart.getContent());
} else if (bodyPart.getContent() instanceof MimeMultipart) {
result = getTextFromMimeMultipart((MimeMultipart) bodyPart.getContent());
}
return result;
}