…and add a “real”, TCP-based test server for testing.
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'
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
* 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();
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);
}
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;
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}.
*
}
@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));
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("<se@nd.er>"));
+ assertThat(smtpServer.rcptTo, contains("<re@cipie.nt>"));
+ }
+ }
+
+ private static class SmtpServer implements Consumer<Socket> {
+
+ public String mailFrom = null;
+ public List<String> 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);
+ }
+ }
+
+ }
+
}
--- /dev/null
+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<Socket>) {
+ 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<String>()
+ 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"))
+}