Skip to content

Commit

Permalink
feat(mail): implement sending mail
Browse files Browse the repository at this point in the history
  • Loading branch information
MyaGya authored Jul 26, 2022
1 parent 2dacc15 commit 01a9fd8
Show file tree
Hide file tree
Showing 21 changed files with 377 additions and 90 deletions.
13 changes: 0 additions & 13 deletions src/main/kotlin/apply/application/MailHistoryService.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package apply.application

import apply.application.mail.MailData
import apply.domain.mail.MailHistory
import apply.domain.mail.MailHistoryRepository
import apply.domain.mail.getById
import org.springframework.stereotype.Service
Expand All @@ -12,18 +11,6 @@ import javax.transaction.Transactional
class MailHistoryService(
private val mailHistoryRepository: MailHistoryRepository
) {
fun save(request: MailData) {
mailHistoryRepository.save(
MailHistory(
request.subject,
request.body,
request.sender,
request.recipients,
request.sentTime
)
)
}

fun findAll(): List<MailData> {
return mailHistoryRepository.findAll().map { MailData(it) }
}
Expand Down
5 changes: 4 additions & 1 deletion src/main/kotlin/apply/application/mail/MailData.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package apply.application.mail

import apply.domain.mail.MailHistory
import org.springframework.core.io.ByteArrayResource
import java.time.LocalDateTime
import javax.validation.constraints.NotEmpty
import javax.validation.constraints.NotNull
Expand All @@ -24,6 +25,8 @@ data class MailData(
@field:NotNull
var sentTime: LocalDateTime = LocalDateTime.now(),

var attachments: Map<String, ByteArrayResource> = emptyMap(),

@field:NotNull
var id: Long = 0L
) {
Expand All @@ -33,6 +36,6 @@ data class MailData(
mailHistory.sender,
mailHistory.recipients,
mailHistory.sentTime,
mailHistory.id
id = mailHistory.id
)
}
4 changes: 4 additions & 0 deletions src/main/kotlin/apply/application/mail/MailSender.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package apply.application.mail

import org.springframework.core.io.ByteArrayResource

interface MailSender {
fun send(toAddress: String, subject: String, body: String)

fun sendBcc(toAddresses: List<String>, subject: String, body: String, attachments: Map<String, ByteArrayResource>)
}
43 changes: 42 additions & 1 deletion src/main/kotlin/apply/application/mail/MailService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,32 @@ package apply.application.mail

import apply.application.ApplicationProperties
import apply.domain.applicationform.ApplicationFormSubmittedEvent
import apply.domain.mail.MailHistory
import apply.domain.mail.MailHistoryRepository
import apply.domain.recruitment.RecruitmentRepository
import apply.domain.recruitment.getById
import apply.domain.user.PasswordResetEvent
import apply.domain.user.UserRepository
import apply.domain.user.getById
import org.springframework.boot.autoconfigure.mail.MailProperties
import org.springframework.core.io.ByteArrayResource
import org.springframework.scheduling.annotation.Async
import org.springframework.stereotype.Service
import org.springframework.transaction.event.TransactionalEventListener
import org.thymeleaf.context.Context
import org.thymeleaf.spring5.ISpringTemplateEngine

private const val MAIL_SENDING_UNIT: Int = 50

@Service
class MailService(
private val userRepository: UserRepository,
private val recruitmentRepository: RecruitmentRepository,
private val mailHistoryRepository: MailHistoryRepository,
private val applicationProperties: ApplicationProperties,
private val templateEngine: ISpringTemplateEngine,
private val mailSender: MailSender
private val mailSender: MailSender,
private val mailProperties: MailProperties
) {
@Async
@TransactionalEventListener
Expand Down Expand Up @@ -78,4 +86,37 @@ class MailService(
templateEngine.process("mail/email-authentication.html", context)
)
}

@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 recipients = request.recipients + mailProperties.username

// TODO: 성공과 실패를 분리하여 히스토리 관리
val succeeded = mutableListOf<String>()
val failed = mutableListOf<String>()
for (addresses in recipients.chunked(MAIL_SENDING_UNIT)) {
runCatching { mailSender.sendBcc(addresses, request.subject, body, files) }
.onSuccess { succeeded.addAll(addresses) }
.onFailure { failed.addAll(addresses) }
}

mailHistoryRepository.save(
MailHistory(
request.subject,
request.body,
request.sender,
request.recipients,
request.sentTime
)
)
}
}
115 changes: 114 additions & 1 deletion src/main/kotlin/apply/infra/mail/AwsMailSender.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,26 @@ import com.amazonaws.services.simpleemail.model.Body
import com.amazonaws.services.simpleemail.model.Content
import com.amazonaws.services.simpleemail.model.Destination
import com.amazonaws.services.simpleemail.model.Message
import com.amazonaws.services.simpleemail.model.RawMessage
import com.amazonaws.services.simpleemail.model.SendEmailRequest
import com.amazonaws.services.simpleemail.model.SendRawEmailRequest
import org.springframework.boot.autoconfigure.mail.MailProperties
import org.springframework.core.io.ByteArrayResource
import org.springframework.stereotype.Component
import support.infra.RateLimiter
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
import java.util.Properties
import javax.activation.DataHandler
import javax.activation.DataSource
import javax.activation.MimetypesFileTypeMap
import javax.mail.Message.RecipientType
import javax.mail.Session
import javax.mail.internet.InternetAddress
import javax.mail.internet.MimeBodyPart
import javax.mail.internet.MimeMessage
import javax.mail.internet.MimeMultipart
import javax.mail.util.ByteArrayDataSource

@Component
class AwsMailSender(
Expand All @@ -32,7 +49,10 @@ class AwsMailSender(
.withRegion(Regions.AP_NORTHEAST_2)
.build()

private val rateLimiter = RateLimiter(14)

override fun send(toAddress: String, subject: String, body: String) {
rateLimiter.acquire()
val request = SendEmailRequest()
.withSource(mailProperties.username)
.withDestination(Destination().withToAddresses(toAddress))
Expand All @@ -44,7 +64,100 @@ class AwsMailSender(
client.sendEmail(request)
}

override fun sendBcc(
toAddresses: List<String>,
subject: String,
body: String,
attachments: Map<String, ByteArrayResource>
) {
rateLimiter.acquire()
val multipartMimeMessage = MultipartMimeMessage(
subject = subject,
userName = mailProperties.username,
recipient = toAddresses,
body = body,
files = attachments
)
val rawEmailRequest = multipartMimeMessage.getRawEmailRequest()
// TODO: MessageRejectedException, AmazonSimpleEmailServiceException 처리
client.sendRawEmail(rawEmailRequest)
}

private fun createContent(data: String): Content {
return Content(data).withCharset("UTF-8")
return Content(data).withCharset(Charsets.UTF_8.name())
}
}

private class MultipartMimeMessage(
val message: MimeMessage = MimeMessage(Session.getDefaultInstance(Properties()))
) {
constructor(
subject: String,
userName: String,
recipient: List<String>,
body: String,
files: Map<String, ByteArrayResource>
) : this() {
val mimeMultipart = MimeMultipart("mixed")
setSubject(subject)
setFrom(userName)
setRecipient(recipient)
addBody(body, mimeMultipart)
addAttachment(files, mimeMultipart)
}

fun setSubject(subject: String) {
message.setSubject(subject, Charsets.UTF_8.name())
}

fun setFrom(userName: String) {
message.setFrom(InternetAddress(userName))
}

fun setRecipient(recipient: List<String>) {
message.setRecipients(
RecipientType.BCC,
recipient.map { InternetAddress(it) }.toTypedArray()
)
}

fun addBody(body: String, mimeMixedPart: MimeMultipart) {
val messageBody = MimeMultipart("alternative")
val wrap = MimeBodyPart()
val htmlPart = MimeBodyPart()
htmlPart.setContent(body, "text/html; charset=UTF-8")
messageBody.addBodyPart(htmlPart)
wrap.setContent(messageBody)
message.setContent(mimeMixedPart)
mimeMixedPart.addBodyPart(wrap)
}

fun addAttachment(files: Map<String, ByteArrayResource>, mimeMixedPart: MimeMultipart) {
for ((fileName, fileData) in files) {
val bodyPart = MimeBodyPart()
val fds: DataSource = ByteArrayDataSource(
fileData.byteArray,
findMimeContentTypeByFileName(fileName)
)
bodyPart.dataHandler = DataHandler(fds)
bodyPart.fileName = fileName
mimeMixedPart.addBodyPart(bodyPart)
}
}

fun findMimeContentTypeByFileName(fileName: String): String {
return MimetypesFileTypeMap().getContentType(fileName) ?: throw IllegalArgumentException("잘못된 확장자입니다.")
}

fun getRawEmailRequest(): SendRawEmailRequest {
val rawMessage = createRawMessage(message)
return SendRawEmailRequest(rawMessage)
}

fun createRawMessage(message: MimeMessage): RawMessage {
ByteArrayOutputStream().use { outputStream ->
message.writeTo(outputStream)
return RawMessage(ByteBuffer.wrap(outputStream.toByteArray()))
}
}
}
20 changes: 20 additions & 0 deletions src/main/kotlin/apply/infra/mail/SimpleMailSender.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package apply.infra.mail

import apply.application.mail.MailSender
import org.springframework.boot.autoconfigure.mail.MailProperties
import org.springframework.core.io.ByteArrayResource
import org.springframework.mail.javamail.JavaMailSender
import org.springframework.mail.javamail.MimeMessageHelper

Expand All @@ -19,4 +20,23 @@ class SimpleMailSender(
}
mailSender.send(message)
}

override fun sendBcc(
toAddresses: List<String>,
subject: String,
body: String,
attachments: Map<String, ByteArrayResource>
) {
val message = mailSender.createMimeMessage()
val mimeMessageHelper = MimeMessageHelper(message, true).apply {
setFrom(mailProperties.username)
setBcc(toAddresses.toTypedArray())
setSubject(subject)
setText(body, true)
}
attachments.forEach { (fileName, data) ->
mimeMessageHelper.addAttachment(fileName, data)
}
mailSender.send(message)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ class EvaluationForm() : BindingIdentityFormLayout<EvaluationData>(EvaluationDat
result?.beforeEvaluation = beforeEvaluation.value
}
return result?.apply {
recruitment
evaluationItems = items
}
}
Expand Down
32 changes: 21 additions & 11 deletions src/main/kotlin/apply/ui/admin/mail/MailForm.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import com.vaadin.flow.component.upload.Upload
import com.vaadin.flow.component.upload.receivers.MultiFileMemoryBuffer
import com.vaadin.flow.data.renderer.ComponentRenderer
import com.vaadin.flow.data.renderer.Renderer
import elemental.json.JsonObject
import org.springframework.boot.autoconfigure.mail.MailProperties
import org.springframework.core.io.ByteArrayResource
import support.views.BindingFormLayout
import support.views.NO_NAME
import support.views.addSortableColumn
Expand All @@ -34,20 +36,22 @@ class MailForm(
private val mailProperties: MailProperties
) : BindingFormLayout<MailData>(MailData::class) {
private val subject: TextField = TextField("제목").apply { setWidthFull() }
private val sender: TextField = createSender()
private val body: TextArea = createBody()
private val mailTargets: MutableSet<MailTargetResponse> = mutableSetOf()
private val uploadFile: MutableMap<String, ByteArrayResource> = mutableMapOf()
private val mailTargetsGrid: Grid<MailTargetResponse> = createMailTargetsGrid(mailTargets)
private val recipientFilter: Component = createRecipientFilter()
private val fileUpload: Component = createFileUpload()
private val fileUpload: Upload = createFileUpload()

init {
add(subject, createSender(), recipientFilter, mailTargetsGrid, body, fileUpload)
add(subject, sender, recipientFilter, mailTargetsGrid, body, fileUpload)
setResponsiveSteps(ResponsiveStep("0", 1))
drawRequired()
refreshGridFooter()
}

private fun createSender(): Component {
private fun createSender(): TextField {
return TextField("보낸사람").apply {
value = mailProperties.username
isReadOnly = true
Expand Down Expand Up @@ -103,15 +107,17 @@ class MailForm(
}

private fun createFileUpload(): Upload {
return createUpload("파일첨부", MultiFileMemoryBuffer()) {
// TODO: 추후 업로드 된 파일을 메일로 첨부하는 로직이 추가되어야 함
// (uploadFiles 같은 필드를 두고 mail을 보내는 기능에 포함시키면 될 것 같음)
// it.files.forEach { fileName ->
// val fileData = it.getFileData(fileName)
// val inputStream = it.getInputStream(fileName)
// val readBytes = inputStream.readBytes()
// }
val upload = createUpload("파일첨부", MultiFileMemoryBuffer()) {
it.files.forEach { fileName ->
val byteArray = it.getInputStream(fileName).readBytes()
uploadFile[fileName] = ByteArrayResource(byteArray)
}
}
upload.element.addEventListener("file-remove") { event ->
val eventData: JsonObject = event.eventData
uploadFile.remove(eventData.getString("event.detail.file.name"))
}.addEventData("event.detail.file.name")
return upload
}

private fun createRemoveButton(): Renderer<MailTargetResponse> {
Expand All @@ -123,8 +129,12 @@ class MailForm(
}

override fun bindOrNull(): MailData? {
if (mailTargets.isEmpty()) {
return null
}
return bindDefaultOrNull()?.apply {
recipients = mailTargets.map { it.email }.toList()
attachments = uploadFile
}
}

Expand Down
Loading

0 comments on commit 01a9fd8

Please sign in to comment.