Skip to content

Commit

Permalink
feat(mail): support the use of markdown in the email body
Browse files Browse the repository at this point in the history
  • Loading branch information
woowabrie authored Sep 13, 2023
1 parent 8c9638b commit eb83941
Show file tree
Hide file tree
Showing 9 changed files with 180 additions and 10 deletions.
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ dependencies {
implementation("com.amazonaws:aws-java-sdk-ses:1.11.880")
implementation("com.amazonaws:aws-java-sdk-sqs:1.12.+")
implementation("io.github.microutils:kotlin-logging-jvm:3.0.0")
implementation("org.jetbrains:markdown:0.5.0")
compileOnly("io.jsonwebtoken:jjwt-api:0.11.2")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.2")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.2")
Expand Down
23 changes: 14 additions & 9 deletions src/main/kotlin/apply/application/mail/MailService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import org.springframework.stereotype.Service
import org.springframework.transaction.event.TransactionalEventListener
import org.thymeleaf.context.Context
import org.thymeleaf.spring5.ISpringTemplateEngine
import support.markdownToEmbeddedHtml

private const val MAIL_SENDING_UNIT: Int = 50

Expand Down Expand Up @@ -89,15 +90,7 @@ class MailService(

@Async
fun sendMailsByBcc(request: MailData, files: Map<String, ByteArrayResource>) {
val context = Context().apply {
setVariables(
mapOf(
"content" to request.body,
"url" to applicationProperties.url
)
)
}
val body = templateEngine.process("mail/common", context)
val body = generateMailBody(request)
val recipients = request.recipients + mailProperties.username

// TODO: 성공과 실패를 분리하여 히스토리 관리
Expand All @@ -119,4 +112,16 @@ class MailService(
)
)
}

fun generateMailBody(mailData: MailData): String {
val context = Context().apply {
setVariables(
mapOf(
"content" to markdownToEmbeddedHtml(mailData.body),
"url" to applicationProperties.url
)
)
}
return templateEngine.process("mail/common", context)
}
}
26 changes: 26 additions & 0 deletions src/main/kotlin/support/Markdown.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package support

import org.intellij.markdown.IElementType
import org.intellij.markdown.ast.ASTNode
import org.intellij.markdown.flavours.MarkdownFlavourDescriptor
import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor
import org.intellij.markdown.html.HtmlGenerator
import org.intellij.markdown.parser.MarkdownParser

private val flavour: MarkdownFlavourDescriptor = CommonMarkFlavourDescriptor()
private val parser: MarkdownParser = MarkdownParser(flavour)

fun markdownToHtml(markdownText: String): String {
val parsedTree = parser.buildMarkdownTreeFromString(markdownText)
return generateHtml(markdownText, parsedTree)
}

// https://github.com/JetBrains/markdown/issues/72
fun markdownToEmbeddedHtml(markdownText: String): String {
val parsedTree = parser.parse(IElementType("ROOT"), markdownText)
return generateHtml(markdownText, parsedTree)
}

private fun generateHtml(markdownText: String, parsedTree: ASTNode): String {
return HtmlGenerator(markdownText, parsedTree, flavour).generateHtml()
}
1 change: 1 addition & 0 deletions src/main/resources/application-test.properties
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ spring.jpa.show-sql=true

spring.flyway.enabled=false

logging.level.apply.application.mail=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
logging.level.org.springframework.web.client.RestTemplate=DEBUG

Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/templates/mail/common.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
padding-right: 8px;
"
>
<span th:utext="${content}"></span>
[(${content})]
</td>
</tr>
</table>
Expand Down
23 changes: 23 additions & 0 deletions src/test/kotlin/apply/application/mail/FakeMailSender.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package apply.application.mail

import mu.KotlinLogging
import org.springframework.core.io.ByteArrayResource

private val logger = KotlinLogging.logger {}

class FakeMailSender : MailSender {
override fun send(toAddress: String, subject: String, body: String) {
logger.debug { "toAddress: $toAddress, subject: $subject" }
logger.debug { "body: $body" }
}

override fun sendBcc(
toAddresses: List<String>,
subject: String,
body: String,
attachments: Map<String, ByteArrayResource>
) {
logger.debug { "toAddresses: $toAddresses, subject: $subject" }
logger.debug { "body: $body" }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package apply.application.mail

import apply.config.TestMailConfiguration
import apply.createMailData
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.matchers.string.shouldContain
import org.springframework.context.annotation.Import
import support.test.IntegrationTest

@Import(TestMailConfiguration::class)
@IntegrationTest
class MailServiceIntegrationTest(
private val mailService: MailService
) : BehaviorSpec({
Given("마크다운으로 본문을 작성한 이메일이 있는 경우") {
val body = """
|안녕하세요. 우아한테크코스입니다.
|오늘은 미션 안내와 더불어 2주 차부터 시작되는 커뮤니티에 대한 가이드도 함께 공유드리니 **반드시 이메일 내용을 꼼꼼하게 정독**해 주세요.
""".trimMargin()
val mailData = createMailData(body = body)

When("이메일 본문을 생성하면") {
val actual = mailService.generateMailBody(mailData)

Then("본문이 HTML로 변환된 이메일이 생성된다") {
actual shouldContain "<title>email</title>"
actual shouldContain """
|<p>안녕하세요. 우아한테크코스입니다.<br />
|오늘은 미션 안내와 더불어 2주 차부터 시작되는 커뮤니티에 대한 가이드도 함께 공유드리니 <strong>반드시 이메일 내용을 꼼꼼하게 정독</strong>해 주세요.</p>
""".trimMargin()
}
}
}
})
16 changes: 16 additions & 0 deletions src/test/kotlin/apply/config/TestMailConfiguration.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package apply.config

import apply.application.mail.FakeMailSender
import apply.application.mail.MailSender
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Primary

@TestConfiguration
class TestMailConfiguration {
@Primary
@Bean
fun fakeMailSender(): MailSender {
return FakeMailSender()
}
}
64 changes: 64 additions & 0 deletions src/test/kotlin/support/MarkdownTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package support

import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldNotContain

class MarkdownTest : StringSpec({
"마크다운을 HTML로 변환한다" {
val markdownText = """
|# 마크다운
|
|## 마크다운(Markdown)이란?
|
|2004년에 존 그루버(John Gruber)와 애런 스워츠(Aaron Swartz)가 만든 마크업 언어의 하나로 읽기 쉽고 쓰기 쉬운 텍스트 포맷입니다.
|
|이 페이지의 왼쪽은 마크다운 편집기입니다. **자유롭게 연습해 보세요**. 여러분이 연습한 내용은 다른 사람에게 보이지 않고, 저장되지 않습니다.
|
|아래 링크를 클릭하여 해당 도움말(연습장)로 바로 이동할 수도 있습니다.
|
|* [문단](#paragraph)
|* [제목](#heading)
|* [인용](#blockquote)
|* [강조](#emphasis)
|* [취소선](#strike)
|* [목록](#list)
|* [할일 목록](#tasklist)
|* [링크](#link)
|* [표](#table)
|* [코드 포맷](#code)
|* [수식](#math)
|* [UML](#uml)
""".trimMargin()
val actual = markdownToHtml(markdownText)
actual shouldBe """
|<body>
|<h1>마크다운</h1>
|<h2>마크다운(Markdown)이란?</h2>
|<p>2004년에 존 그루버(John Gruber)와 애런 스워츠(Aaron Swartz)가 만든 마크업 언어의 하나로 읽기 쉽고 쓰기 쉬운 텍스트 포맷입니다.</p>
|<p>이 페이지의 왼쪽은 마크다운 편집기입니다. <strong>자유롭게 연습해 보세요</strong>. 여러분이 연습한 내용은 다른 사람에게 보이지 않고, 저장되지 않습니다.</p>
|<p>아래 링크를 클릭하여 해당 도움말(연습장)로 바로 이동할 수도 있습니다.</p>
|<ul>
|<li><a href="#paragraph">문단</a></li>
|<li><a href="#heading">제목</a></li>
|<li><a href="#blockquote">인용</a></li>
|<li><a href="#emphasis">강조</a></li>
|<li><a href="#strike">취소선</a></li>
|<li><a href="#list">목록</a></li>
|<li><a href="#tasklist">할일 목록</a></li>
|<li><a href="#link">링크</a></li>
|<li><a href="#table">표</a></li>
|<li><a href="#code">코드 포맷</a></li>
|<li><a href="#math">수식</a></li>
|<li><a href="#uml">UML</a></li>
|</ul>
|</body>
""".trimMargin().replace(System.lineSeparator(), "")
}

"마크다운을 <body> 태그를 제거한 HTML로 변환한다" {
val markdownText = "2004년에 존 그루버(John Gruber)와 애런 스워츠(Aaron Swartz)가 만든 마크업 언어의 하나로 읽기 쉽고 쓰기 쉬운 텍스트 포맷입니다."
val actual = markdownToEmbeddedHtml(markdownText)
actual shouldNotContain "<body>"
}
})

0 comments on commit eb83941

Please sign in to comment.