From d6ba885c562d545f4d5cc54b55f71fde0f96945f Mon Sep 17 00:00:00 2001 From: =?utf8?q?David=20=E2=80=98Bombe=E2=80=99=20Roden?= Date: Sun, 7 Dec 2025 18:34:48 +0100 Subject: [PATCH] =?utf8?q?=E2=AC=86=EF=B8=8F=20Update=20javax.mail=20to=20?= =?utf8?q?Eclipse=20Angus?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit …and add a “real”, TCP-based test server for testing. --- build.gradle | 2 +- .../rhynodge/actions/EmailAction.java | 39 ++++++--- .../rhynodge/actions/EmailActionTest.java | 92 +++++++++++++++++++--- .../net/pterodactylus/util/test/TcpServer.kt | 89 +++++++++++++++++++++ 4 files changed, 197 insertions(+), 25 deletions(-) create mode 100644 src/test/kotlin/net/pterodactylus/util/test/TcpServer.kt diff --git a/build.gradle b/build.gradle index 6720059..2d24ea2 100644 --- a/build.gradle +++ b/build.gradle @@ -48,7 +48,7 @@ dependencies { implementation group: "org.apache.logging.log4j", name: "log4j-core", version: "2.25.2" implementation group: 'org.apache.httpcomponents.client5', name: 'httpclient5', version: '5.5.1' implementation group: "org.jsoup", name: "jsoup", version: "1.16.1" - implementation group: "javax.mail", name: "mail", version: "1.4.6-rc1" + implementation group: 'org.eclipse.angus', name: 'angus-mail', version: '2.0.5' implementation group: "com.fasterxml.jackson.core", name: "jackson-databind", version: "2.16.1" implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.16.1' implementation group: 'com.fasterxml.jackson.module', name: 'jackson-module-kotlin', version: '2.16.1' diff --git a/src/main/java/net/pterodactylus/rhynodge/actions/EmailAction.java b/src/main/java/net/pterodactylus/rhynodge/actions/EmailAction.java index 945b909..3960018 100644 --- a/src/main/java/net/pterodactylus/rhynodge/actions/EmailAction.java +++ b/src/main/java/net/pterodactylus/rhynodge/actions/EmailAction.java @@ -17,29 +17,28 @@ package net.pterodactylus.rhynodge.actions; +import static jakarta.mail.Session.getInstance; import static java.lang.System.getProperties; -import static javax.mail.Session.getInstance; +import jakarta.mail.Message.RecipientType; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.Transport; +import jakarta.mail.URLName; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeBodyPart; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.internet.MimeMultipart; import java.util.Arrays; import java.util.Properties; -import javax.mail.Message.RecipientType; -import javax.mail.MessagingException; -import javax.mail.Session; -import javax.mail.Transport; -import javax.mail.URLName; -import javax.mail.internet.InternetAddress; -import javax.mail.internet.MimeBodyPart; -import javax.mail.internet.MimeMessage; -import javax.mail.internet.MimeMultipart; - import net.pterodactylus.rhynodge.Action; import net.pterodactylus.rhynodge.output.Output; import com.google.common.annotations.VisibleForTesting; -import com.sun.mail.smtp.SMTPTransport; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.eclipse.angus.mail.smtp.SMTPTransport; /** * {@link Action} implementation that sends an email containing the triggering @@ -71,6 +70,20 @@ public class EmailAction implements Action { * The email address of the recipient */ public EmailAction(String hostname, String sender, String recipient) { + this(hostname, -1, sender, recipient); + } + + /** + * Creates a new email action. + * + * @param hostname + * The hostname of the SMTP server + * @param sender + * The email address of the sender + * @param recipient + * The email address of the recipient + */ + public EmailAction(String hostname, int port, String sender, String recipient) { this.sender = sender; this.recipient = recipient; Properties properties = getProperties(); @@ -78,7 +91,7 @@ public class EmailAction implements Action { properties.put("mail.smtp.host", hostname); session = getInstance(properties); logger.debug("Created session: " + session); - transport = new SMTPTransport(session, new URLName("smtp", hostname, 25, null, "", "")); + transport = new SMTPTransport(session, new URLName("smtp", hostname, port, null, "", "")); logger.debug("Created transport: " + transport); } diff --git a/src/test/java/net/pterodactylus/rhynodge/actions/EmailActionTest.java b/src/test/java/net/pterodactylus/rhynodge/actions/EmailActionTest.java index 97e504d..f2d3085 100644 --- a/src/test/java/net/pterodactylus/rhynodge/actions/EmailActionTest.java +++ b/src/test/java/net/pterodactylus/rhynodge/actions/EmailActionTest.java @@ -1,5 +1,28 @@ package net.pterodactylus.rhynodge.actions; +import jakarta.mail.Address; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.Transport; +import jakarta.mail.internet.InternetAddress; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.Socket; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import net.pterodactylus.rhynodge.output.DefaultOutput; +import net.pterodactylus.rhynodge.output.Output; +import net.pterodactylus.util.test.DisableLog4jLogging; +import net.pterodactylus.util.test.TcpServer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItemInArray; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doThrow; @@ -8,17 +31,6 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.hamcrest.MockitoHamcrest.argThat; -import javax.mail.Address; -import javax.mail.Message; -import javax.mail.MessagingException; -import javax.mail.Transport; -import javax.mail.internet.InternetAddress; - -import net.pterodactylus.rhynodge.output.DefaultOutput; -import net.pterodactylus.rhynodge.output.Output; -import net.pterodactylus.util.test.DisableLog4jLogging; -import org.junit.jupiter.api.Test; - /** * Unit test for {@link EmailAction}. * @@ -39,6 +51,11 @@ public class EmailActionTest { } @Test + public void canCreateActionWithConstructorWithPortNumber() { + new EmailAction("hostname", 1234, "sender", "recipient"); + } + + @Test public void emailIsGeneratedCorrectly() throws MessagingException { emailAction.execute(output); verify(transport).sendMessage(any(Message.class), any(Address[].class)); @@ -58,4 +75,57 @@ public class EmailActionTest { verify(transport, times(2)).sendMessage(any(Message.class), any(Address[].class)); } + @Test + @Timeout(5) + public void emailActionTransmitsSenderAndReceiverCorrectly() throws Exception { + try (TcpServer tcpServer = new TcpServer()) { + EmailAction emailAction2 = new EmailAction("localhost", tcpServer.getPort(), "se@nd.er", "re@cipie.nt"); + SmtpServer smtpServer = new SmtpServer(); + tcpServer.connect(smtpServer); + emailAction2.execute(output); + assertThat(smtpServer.mailFrom, equalTo("")); + assertThat(smtpServer.rcptTo, contains("")); + } + } + + private static class SmtpServer implements Consumer { + + public String mailFrom = null; + public List rcptTo = new ArrayList<>(); + + public void accept(Socket socket) { + try { + var reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); + var writer = new OutputStreamWriter(socket.getOutputStream()); + String line; + writer.write("220 smtp.test/FakeTcpServer\r\n"); + writer.flush(); + boolean inData = false; + while ((line = reader.readLine()) != null) { + if (line.toLowerCase().startsWith("mail from:")) { + mailFrom = line.substring(10); + writer.write("250 ok\r\n"); + } else if (line.toLowerCase().startsWith("rcpt to:")) { + rcptTo.add(line.substring(8)); + writer.write("250 ok\r\n"); + } else if (line.toLowerCase().startsWith("data")) { + writer.write("354 go on\r\n"); + inData = true; + } else if (line.toLowerCase().startsWith("quit")) { + writer.write("221 bye\r\n"); + } else if (inData && (line.equals("."))) { + inData = false; + writer.write("250 ok\r\n"); + } else if (!inData) { + writer.write("250 ok\r\n"); + } + writer.flush(); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + } + } diff --git a/src/test/kotlin/net/pterodactylus/util/test/TcpServer.kt b/src/test/kotlin/net/pterodactylus/util/test/TcpServer.kt new file mode 100644 index 0000000..42b8ed2 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/util/test/TcpServer.kt @@ -0,0 +1,89 @@ +package net.pterodactylus.util.test + +import java.io.Closeable +import java.io.IOException +import java.net.ServerSocket +import java.net.Socket +import java.net.URL +import java.util.function.Consumer +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo + +/** + * A TCP server implementation that listens on a random port (which is + * published via [port] and lets a client handle each connection. + * + * ## Usage Example + * + * This will implement a tiny HTTP server which will capture all request + * lines until an empty line was sent, after which it sends back status code 200. + * + * @sample example + */ +class TcpServer : Closeable { + + /** + * The port number the TCP server has been bound to. + */ + val port by lazy { serverSocket.localPort } + + /** + * Starts a thread that will handle each connection using the given + * handler. + * + * @param [handler] A connection handler for a client connection + */ + fun connect(handler: Consumer) { + connect(handler::accept) + } + + /** + * Starts a thread that will handle each connection using the given + * handler. + * + * @param [handler] A connection handler for a client connection + */ + fun connect(handler: (clientSocket: Socket) -> Unit) { + Thread { + while (!serverSocket.isClosed) { + try { + serverSocket.accept().let { socket -> + Thread { + socket.use(handler) + }.start(); + } + } catch (e: IOException) { + throw RuntimeException(e) + } + } + }.apply { isDaemon = true }.start() + } + + override fun close() { + serverSocket.close() + } + + private val serverSocket = ServerSocket(0) + +} + +@Suppress("unused") +private fun example() { + val receivedLines = mutableListOf() + TcpServer().connect { socket -> + val bufferedReader = socket.inputStream.bufferedReader() + while (true) { + val line = bufferedReader.readLine()?.let { if (it == "") null else it } ?: break + receivedLines.add(line) + } + socket.outputStream.writer().let { writer -> + writer.appendLine("HTTP/1.1 200 OK\r") + .appendLine("Content-Length: 0\r") + .appendLine("\r") + writer.flush() + } + } + URL("http://localhost:${TcpServer().port}/test") + .openStream().readAllBytes() + assertThat(receivedLines.first(), equalTo("GET /test HTTP/1.1")) +} -- 2.7.4