【Scala】Scala ~ AWS SDK / SESサンプル ~

■ はじめに

https://dk521123.hatenablog.com/entry/2023/03/24/211033
https://dk521123.hatenablog.com/entry/2023/04/01/002005
https://dk521123.hatenablog.com/entry/2023/04/03/012600

の続き。

今回は、Scala で、AWS SDK for Java を使った
Amazon SES (Simple Email Service)によるEmail送信をする

目次

【1】公式ドキュメント
 1)サンプル集
【2】インストール
【3】ハマりポイント
 1)AWS JDK for Java (SES) が色々ある
 2)jakarta.mail の import が変わる
【4】サンプル

【1】公式ドキュメント

https://docs.aws.amazon.com/ja_jp/ses/latest/dg/example_sesv2_SendEmail_section.html
https://docs.aws.amazon.com/ja_jp/sdk-for-java/latest/developer-guide/java_ses_code_examples.html

1)サンプル集

https://github.com/awsdocs/aws-doc-sdk-examples/tree/main/javav2/example_code/ses/src/main/java/com/example
https://github.com/awsdocs/aws-doc-sdk-examples/tree/main/javav2/example_code/ses/src/main/java/com/example/sesv2

2)その他参考になりそうな資料

https://buildersbox.corp-sansan.com/entry/2020/05/18/110000

【2】インストール

libraryDependencies ++= Seq(
  "software.amazon.awssdk" % "sesv2" % "2.20.47",
  "com.sun.mail" % "jakarta.mail" % "2.0.1"
)

Maven

sesv2
https://mvnrepository.com/artifact/software.amazon.awssdk/sesv2
jakarta.mail
https://mvnrepository.com/artifact/com.sun.mail/jakarta.mail

【3】ハマりポイント

1)AWS JDK for Java (SES) が色々ある

* 1)AWS JDK for Java v1(SES) と v2 (SES/SESv2) がある
 => こんなん、わかるかい、、、

com.amazonaws / aws-java-sdk-ses
https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk-ses
software.amazon.awssdk / SES
https://mvnrepository.com/artifact/software.amazon.awssdk/ses
software.amazon.awssdk / SESv2
https://mvnrepository.com/artifact/software.amazon.awssdk/sesv2

2)jakarta.mail の import が変わる

* jakarta.mail (以前の javax.mail / com.sun.mail)の import が
 v1.6 -> v2.0で以下のように変わる
 => ネットの情報は、大抵、v1.6なので、ここでもハマる

https://stackoverflow.com/questions/68955884/update-jakarta-mail-1-6-5-to-2-0-1
v1.6

import javax.mail.internet.MimeMessage

v2.0

import jakarta.mail.internet.MimeMessage

【4】サンプル

import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider
import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider
import software.amazon.awssdk.core.SdkBytes
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.sesv2.model.RawMessage
import software.amazon.awssdk.services.sesv2.SesV2Client
import software.amazon.awssdk.services.sesv2.model.EmailContent
import software.amazon.awssdk.services.sesv2.model.SendEmailRequest
import software.amazon.awssdk.services.sesv2.model.SesV2Exception
import jakarta.activation.DataHandler
import jakarta.activation.DataSource
import jakarta.mail.Message
import jakarta.mail.Session
import jakarta.mail.internet.InternetAddress
import jakarta.mail.internet.MimeBodyPart
import jakarta.mail.internet.MimeMessage
import jakarta.mail.internet.MimeMultipart
import jakarta.mail.util.ByteArrayDataSource
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.nio.ByteBuffer
import java.nio.file.Files
import java.util.Properties
import java.net.URI

case class SesUtils(region: Region = Region.US_WEST_1, isDev: Boolean = false) {
  val DEFAULT_CHAR_CODE = "UTF-8"
  val sesClient = createClient(region, isDev)

  private def createClient(
    region: Region = Region.US_WEST_1,
    isDev: Boolean = false
  ): SesV2Client = {
    if (isDev) {
      val accessKey = "dummy"
      val secretAccessKey = "dummy"
      val endpoint = "http://localhost:4566"
      val credentials = AwsBasicCredentials.create(accessKey, secretAccessKey)
      SesV2Client.builder()
        .region(region)
        .credentialsProvider(StaticCredentialsProvider.create(credentials))
        .endpointOverride(new URI(endpoint))
        .build()
    } else {
      SesV2Client.builder()
        .region(region)
        .credentialsProvider(ProfileCredentialsProvider.create())
        .build()
    }
  }

  def sendEmail(
    mailFrom: String,
    mailAddressList: Seq[String],
    mailTitle: String,
    mailBody: String,
    mailFileLocation: Option[String] = None
  ): Unit = {
    val mimeMessage = generateMimeMessage()

    setMailFrom(mimeMessage, mailFrom)
    setMailTo(mimeMessage, mailAddressList)
    setMailTitle(mimeMessage, mailTitle)
    val mimeMultipartForMixed = setMailBody(mimeMessage, mailBody)
    if (mailFileLocation.isDefined) {
      setMailAttachment(mimeMessage, mailFileLocation.get, mimeMultipartForMixed)
    }
    val request = createRequest(mimeMessage)

    sesClient.sendEmail(request)
  }

  def generateMimeMessage(): MimeMessage = {
    val session = Session.getDefaultInstance(new Properties())
    new MimeMessage(session)
  }

  def setMailFrom(mimeMessage: MimeMessage, mailFrom: String): Unit = {
    mimeMessage.setFrom(new InternetAddress(mailFrom))
  }

  def setMailTo(
    mimeMessage: MimeMessage,
    mailAddressList: Seq[String],
    mailRecipientType: Message.RecipientType = Message.RecipientType.TO
  ): Unit = {
    mailAddressList.foreach(mailAddress =>
      mimeMessage.addRecipient(
        mailRecipientType,
        new InternetAddress(mailAddress)
      )
    )
  }

  def setMailTitle(mimeMessage: MimeMessage, mailTitle: String): Unit = {
    mimeMessage.setSubject(mailTitle, DEFAULT_CHAR_CODE)
  }
  def setMailBody(mimeMessage: MimeMessage, mailBody: String): MimeMultipart = {
    // Define the HTML part.
    val mimeBodyPartForHtml = new MimeBodyPart();
    mimeBodyPartForHtml.setContent(
      mailBody, s"text/html; charset=${DEFAULT_CHAR_CODE}")

    // Create a multipart/alternative child container.
    val mimeMultipartForBody = new MimeMultipart("alternative")
    // Add the HTML parts to the child container.
    mimeMultipartForBody.addBodyPart(mimeBodyPartForHtml);

    // Create a wrapper for the HTML and text parts.
    val mimeBodyPart = new MimeBodyPart()
    // Add the child container to the wrapper object.
    mimeBodyPart.setContent(mimeMultipartForBody)

    // Create a multipart/mixed parent container.
    val mimeMultipartForMixed = new MimeMultipart("mixed")
    // Add the parent container to the message.
    mimeMessage.setContent(mimeMultipartForMixed)
    // Add the multipart/alternative part to the message.
    mimeMultipartForMixed.addBodyPart(mimeBodyPart)

    mimeMultipartForMixed
  }

  def setMailAttachment(
    mimeMessage: MimeMessage,
    mailFileLocation: String,
    mimeMultipartForMixed: MimeMultipart,
    mailAttachmentFileName: String = "test.zip",
    mailMimeType: String = "application/zip"
  ): Unit = {
    val attachmentFile = new java.io.File(mailFileLocation);
    val fileContentByArrayByte = Files.readAllBytes(attachmentFile.toPath());
    // https://spring.pleiades.io/specifications/platform/8/apidocs/javax/mail/util/bytearraydatasource
    val dataSourceForAttachment = new ByteArrayDataSource(
      fileContentByArrayByte,
      mailMimeType
    )

    // Define the attachment.
    val mimeBodyPartForAttachment = new MimeBodyPart()
    mimeBodyPartForAttachment.setDataHandler(
      new DataHandler(dataSourceForAttachment)
    )
    mimeBodyPartForAttachment.setFileName(mailAttachmentFileName)
    // Add the attachment to the message.
    mimeMultipartForMixed.addBodyPart(mimeBodyPartForAttachment)
  }

  def createRequest(mimeMessage: MimeMessage): SendEmailRequest = {
    val outputStream = new ByteArrayOutputStream();
    mimeMessage.writeTo(outputStream);

    val outputStreamBuffer = ByteBuffer.wrap(outputStream.toByteArray())
    val outputStreamBufferAsByteArray = new Array[Byte](outputStreamBuffer.remaining)
    outputStreamBuffer.get(outputStreamBufferAsByteArray)
    val messageData = SdkBytes.fromByteArray(outputStreamBufferAsByteArray)
    val rawMessage = RawMessage.builder()
      .data(messageData)
      .build()
    val emailContent = EmailContent.builder()
      .raw(rawMessage)
      .build()
    SendEmailRequest.builder()
      .content(emailContent)
      .build()
  }
}

補足:デバッグ

* Localstack で試そうと思ったが、以下のエラー、、、
~~~~~
[error] (Compile / run) software.amazon.awssdk.services.sesv2.model.SesV2Exception: 
API action 'SendEmail' for service 'sesv2' not yet implemented or pro feature
 - check https://docs.localstack.cloud/user-guide/aws/feature
-coverage for further information (Service: SesV2, Status Code: 501, Request ID: xxxx)
~~~~~

https://docs.localstack.cloud/user-guide/aws/feature

Simple Storage Service (SES) v2 (Pro)
 ... 有料版でしか対応していないっぽい、、、

参考文献

https://qiita.com/an_riri88/items/525ea900e5474eeef295
https://qiita.com/bicep_sasa/items/618c8ec728677c625afa

関連記事

Scala ~ 環境構築編 ~
https://dk521123.hatenablog.com/entry/2023/03/10/193805
Scala ~ 入門編 ~
https://dk521123.hatenablog.com/entry/2023/03/12/184331
ScalaJSON
https://dk521123.hatenablog.com/entry/2023/04/04/000733
ScalaAWS SDK
https://dk521123.hatenablog.com/entry/2023/03/24/211033
ScalaAWS SDK / S3サンプル ~
https://dk521123.hatenablog.com/entry/2023/04/01/002005
ScalaAWS SDK / Secrets Managerサンプル ~
https://dk521123.hatenablog.com/entry/2023/04/03/012600
Amazon SES ~ 入門編 ~
https://dk521123.hatenablog.com/entry/2017/04/28/234103
JavaでEmail ~ JavaMail / Text ~
https://dk521123.hatenablog.com/entry/2016/07/16/222422
JavaでEmail ~ JavaMail / 添付ファイル ~
https://dk521123.hatenablog.com/entry/2016/07/17/023459
JavaでEmail ~ SMTP認証 ~
https://dk521123.hatenablog.com/entry/2016/11/07/215251
JavaでEmail ~ SMTP認証 / DIGEST-MD5
https://dk521123.hatenablog.com/entry/2016/12/07/222229
JavaでEmail ~ JavaMail / TLS
https://dk521123.hatenablog.com/entry/2017/05/03/163219
JavaでEmail ~ JavaMail / Return-Path・Errors-To ~
https://dk521123.hatenablog.com/entry/2017/05/07/000344
Docker compose ~ LocalStack/Glue4.0 ~
https://dk521123.hatenablog.com/entry/2023/03/25/021432