From 65206f82311fe5d673c56591d4e07b2c0bd5eb3f Mon Sep 17 00:00:00 2001 From: Anton Keks Date: Tue, 7 Jan 2025 20:30:08 +0200 Subject: [PATCH] make it possible to redefine from address in an implementation of EmailContent --- smtp/src/EmailContent.kt | 2 ++ smtp/src/EmailService.kt | 32 ++++++++++++++++--------------- smtp/test/RealEmailServiceTest.kt | 13 ++++++------- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/smtp/src/EmailContent.kt b/smtp/src/EmailContent.kt index bbc48d8..a5028b3 100644 --- a/smtp/src/EmailContent.kt +++ b/smtp/src/EmailContent.kt @@ -6,11 +6,13 @@ import klite.i18n.Lang.translate import org.intellij.lang.annotations.Language import java.net.URI import java.util.* +import javax.mail.internet.InternetAddress open class EmailContent(val lang: String, val labelKey: String, val substitutions: Map = emptyMap(), val actionUrl: URI? = null) { open val subject get() = translate(lang, "emails.$labelKey.subject", substitutions) open val body get() = translate(lang, "emails.$labelKey.body", substitutions) open val actionLabel get() = translate(lang, "emails.$labelKey.action", substitutions) + open val from: InternetAddress? get() = null override fun equals(other: Any?) = other is EmailContent && lang == other.lang && labelKey == other.labelKey && substitutions == other.substitutions && actionUrl == other.actionUrl override fun hashCode() = Objects.hash(lang, labelKey, substitutions, actionUrl) diff --git a/smtp/src/EmailService.kt b/smtp/src/EmailService.kt index 8c02b66..4ca0f48 100644 --- a/smtp/src/EmailService.kt +++ b/smtp/src/EmailService.kt @@ -1,30 +1,34 @@ package klite.smtp import klite.* +import klite.i18n.Lang +import klite.i18n.Lang.translate import java.util.* import javax.activation.DataHandler import javax.mail.Authenticator -import javax.mail.Message.RecipientType.* +import javax.mail.Message.RecipientType.CC +import javax.mail.Message.RecipientType.TO import javax.mail.Part.ATTACHMENT import javax.mail.PasswordAuthentication import javax.mail.Session import javax.mail.Transport import javax.mail.internet.* import javax.mail.util.ByteArrayDataSource -import kotlin.text.Charsets.UTF_8 interface EmailService { - fun send(to: Email, subject: String, body: String, bodyMimeType: String = MimeTypes.text, attachments: Map = emptyMap(), cc: List = emptyList()) + val defaultFrom: InternetAddress get() = InternetAddress(Config["MAIL_FROM"], Config.optional("MAIL_FROM_NAME", translate(Lang.available.first(), "title"))) + + fun send(to: Email, subject: String, body: String, bodyMimeType: String = MimeTypes.text, attachments: Map = emptyMap(), cc: List = emptyList(), from: InternetAddress = defaultFrom) fun send(to: Email, content: EmailContent, attachments: Map = emptyMap(), cc: List = emptyList()) = - send(to, content.subject, content.fullHtml(), MimeTypes.html, attachments, cc) + send(to, content.subject, content.fullHtml(), MimeTypes.html, attachments, cc, content.from ?: defaultFrom) } -class FakeEmailService: EmailService { +open class FakeEmailService: EmailService { private val log = logger() lateinit var lastSentEmail: String - override fun send(to: Email, subject: String, body: String, bodyMimeType: String, attachments: Map, cc: List) { + override fun send(to: Email, subject: String, body: String, bodyMimeType: String, attachments: Map, cc: List, from: InternetAddress) { lastSentEmail = """ Email to $to, CC: $cc Subject: $subject @@ -35,8 +39,7 @@ class FakeEmailService: EmailService { } } -class RealEmailService( - internal val mailFrom: InternetAddress = InternetAddress(Config["MAIL_FROM"], Config.optional("MAIL_FROM_NAME")), +open class RealEmailService( smtpUser: String? = Config.optional("SMTP_USER"), smtpPort: String? = Config.optional("SMTP_PORT", "25"), props: Properties = Properties().apply { @@ -51,8 +54,8 @@ class RealEmailService( }, private val session: Session = Session.getInstance(props, authenticator.takeIf { smtpUser != null }) ): EmailService { - override fun send(to: Email, subject: String, body: String, bodyMimeType: String, attachments: Map, cc: List) { - send(to, subject) { + override fun send(to: Email, subject: String, body: String, bodyMimeType: String, attachments: Map, cc: List, from: InternetAddress) { + send(to, subject, from) { cc.forEach { setRecipient(CC, InternetAddress(it.value)) } if (attachments.isEmpty()) setBody(body, bodyMimeType) @@ -71,13 +74,12 @@ class RealEmailService( } } - private fun MimePart.setBody(body: String, bodyMimeType: String) = setContent(body, "$bodyMimeType; charset=UTF-8") + private fun MimePart.setBody(body: String, bodyMimeType: String) = setContent(body, MimeTypes.withCharset(bodyMimeType)) - private fun send(to: Email, subject: String, block: MimeMessage.() -> Unit) = MimeMessage(session).apply { - setFrom(mailFrom) - setRecipient(BCC, mailFrom) + protected fun send(to: Email, subject: String, from: InternetAddress, block: MimeMessage.() -> Unit) = MimeMessage(session).apply { + setFrom(from) setRecipient(TO, InternetAddress(to.value)) - setSubject(subject, UTF_8.name()) + setSubject(subject, MimeTypes.textCharset.name()) block() Transport.send(this) } diff --git a/smtp/test/RealEmailServiceTest.kt b/smtp/test/RealEmailServiceTest.kt index 903a9ab..4fdcbb0 100644 --- a/smtp/test/RealEmailServiceTest.kt +++ b/smtp/test/RealEmailServiceTest.kt @@ -8,9 +8,9 @@ import io.mockk.mockk import io.mockk.slot import io.mockk.verify import klite.Config +import klite.MimeTypes import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test -import javax.mail.Message.RecipientType.BCC import javax.mail.Session import javax.mail.internet.InternetAddress import javax.mail.internet.MimeMessage @@ -25,19 +25,18 @@ class RealEmailServiceTest { service.send(email, subject = "Subject", body = "Body") val message = slot() val toAddress = InternetAddress(email.value) - verify { session.getTransport(toAddress).sendMessage(capture(message), arrayOf(toAddress, service.mailFrom)) } - expect(message.captured.from.toList()).toContainExactly(service.mailFrom) - expect(message.captured.getRecipients(BCC).toList()).toContainExactly(service.mailFrom) + verify { session.getTransport(toAddress).sendMessage(capture(message), arrayOf(toAddress)) } + expect(message.captured.from.toList()).toContainExactly(service.defaultFrom) expect(message.captured.subject).toEqual("Subject") expect(message.captured.contentType).toEqual("text/plain; charset=UTF-8") expect(message.captured.content).toEqual("Body") } @Test fun `send html`() = runTest { - service.send(email, subject = "Subject", body = "", bodyMimeType = "text/html") + service.send(email, subject = "Subject", body = "", bodyMimeType = MimeTypes.html) val message = slot() val toAddress = InternetAddress(email.value) - verify { session.getTransport(toAddress).sendMessage(capture(message), arrayOf(toAddress, service.mailFrom)) } + verify { session.getTransport(toAddress).sendMessage(capture(message), arrayOf(toAddress)) } expect(message.captured.contentType).toEqual("text/html; charset=UTF-8") expect(message.captured.content).toEqual("") } @@ -46,7 +45,7 @@ class RealEmailServiceTest { service.send(email, subject = "Subject", body = "Body", attachments = mapOf("hello.pdf" to ByteArray(0))) val message = slot() val toAddress = InternetAddress(email.value) - verify { session.getTransport(toAddress).sendMessage(capture(message), arrayOf(toAddress, service.mailFrom)) } + verify { session.getTransport(toAddress).sendMessage(capture(message), arrayOf(toAddress)) } expect(message.captured.from.size).toEqual(1) expect(message.captured.subject).toEqual("Subject") expect(message.captured.getHeader("Content-Type")[0]).toStartWith("multipart/mixed")