Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Scala でクロスプラットフォームな SMTP クライアントを書く

110416
March 14, 2025
47

Scala でクロスプラットフォームな SMTP クライアントを書く

このスライドは Typelevel Stack を使ったクロスプラットフォームな SMTP クライアント実装について紹介する。
Typelevel Stack はプラットフォームの差分をうまく吸収してくれるので、開発者はプラットフォームの都合をほとんど考えずにクロスプラットフォームで動作するコードを書ける。

110416

March 14, 2025
Tweet

Transcript

  1. SMTP の実装 (TLSを使う) SMTP クライアントは次のような手順でサーバーに接続してメールを送信しま す。 1. サーバーとソケット通信を開始 2. HELO

    コマンドでサーバー側がサポートする機能を確認 3. STARTTLS コマンドを送信し upgrade 4. HELO コマンドでサーバー側がサポートする機能を確認 5. [optional] AUTH LOGIN コマンドで認証 6. SMTP の仕様に従ってメールをシリアライズして送信 7. QUIT コマンドで接続を切る
  2. NOTE SMTP は 7 bits のプロトコルなので、7 bits で表現できない特殊文字、日本語やバイナリファ イルなどのデータのシリアライゼーションには工夫が必要です。7 bits

    の制約を回避するには base64 でデータをエンコードする方法があります。 今回は時間の都合上この説明は省きます
  3. Typelevel Stack でクロスプラットフォームなアプリケーションを書く 非同期ランタイム cats-effect と関数型ストリーミングライブラリ fs2(https://github.com/typelevel/fs2) を使うと簡単にクロスプラットフォーム対応した Scala コードを書けます。

    また、バイナリコーデックのscodec や scodec-bits もクロスプラットフォームに対応してい るのでバイナリ形式でデータをやり取りするプロトコルを簡単に実装できます。
  4. SMTPConnection.scala object SMTPConnection: def apply[F[_]](...): SMTPConnection[F] = ??? trait SMTPConnection[F[_]]:

    def connect: Resource[F, Mailer[F]] Mailer.scala trait Mailer[F[_]]: def send(message: Message): F[Unit]
  5. 実装例 上のインターフェースの実装例です。 mkTransport はメールを受け付ける準備ができたソ ケットを返します。 object SMTPConnection extends TLSParametersPlatform {

    def apply[F[_]: Network: Concurrent]( smtpServer: Host, credential: Option[Credential] = None, port: Port = port"587" ): SMTPConnection[F] = new SMTPConnection[F] { def connect: Resource[F, Mailer[F]] = mkTransport(smtpServer, credential, port).flatMap(socket => // ...
  6. mkTransport fs2.io.net.Network インスタンスを使ってソケット通信をはじめます。 ネットワーク通 信のように I/O を伴う処理はプラットフォーム依存になりがちですが、fs2 は JVM・JS・ Native

    の違いをうまく吸収してくれます。 SMTPSocket は Socket 型を扱いやすくするヘルパーです。 for { socket <- Network[F] .client(SocketAddress(smtpServer, port)) smtpSocket = SMTPSocket.from(socket) // ...
  7. SMTPSocket fs2.io.net.Socket には Byte や fs2.Chunk[Byte] が書き込まれます。そのままでは 扱いにくいのでより型安全なヘルパーとして SMTPSocket を定義しています。

    trait SMTPSocket[F[_]] { def check: F[Unit] def helo(domain: Host): F[Unit] def startTLS: F[Unit] def authn(credential: Credential.Password): F[Unit] // ...
  8. mkTransport まずTLS で SMTPでメールを送信するために、サーバーに接続し TLS へ upgrade するように 要求します。 //

    ... _ <- ( // サーバーが SMTP をサポートしている場合は // グリーティング(code 220) を返します。 smtpSocket.check // HELO コマンドをサーバーに送ります *> smtpSocket.helo(smtpServer) // サーバーにTLS 接続を要求します *> smtpSocket.startTLS ).toResource // ...
  9. mkTransport TLS に upgrade します。 TLS には暗号処理が必要なのでプラットフォームごとに実装が変わりますが、ここでも fs2 がよしなに抽象化してくれます。 //

    upgrade to TLS socket <- for { tlsCtx <- Network[F].tlsContext.system.toResource tlsSocket <- tlsCtx .clientBuilder(socket) .withParameters(startTLSParameters(smtpServer)) .build .map(SMTPSocket.from[F])
  10. mkTransport TLS 前後でサーバーがサポートする機能が変わることがあるので再度 HELO コマンドを送り ます。また、必要に応じて認証をします。 // greet again _

    <- tlsSocket.helo(smtpServer).toResource } yield tlsSocket _ <- (credential match { case None => F.unit case Some(credential: Credential.Password) => socket.authn(credential) case Some(credential: Credential.OAuth2Token) => socket.xoauth2(credential) }).toResource } yield socket
  11. 呼び出し側から send が並列に呼び出された際にソケットに送るデータが混線しないよう Mutex でガードします。 メールを送り終えたら QUIT コマンドで接続を切ります。 mkTransport(smtpServer, credential,

    port).flatMap(socket => Resource.make( for { mu <- Mutex[F] } yield new Mailer[F] { def send(message: Message): F[Unit] = mu.lock.surround { for { _ <- socket.mail(message.from.email) _ <- message.to.map(_.email).traverse(socket.rcpt) _ <- socket.data _ <- socket.send(message) } yield () } } )(_ => socket.quit.as(()))
  12. Key Takeaways Typelevel Stack(e.g. cats-effect, fs2, scodec) を使うとクロスプラットフォームなコー ドを(ほとんど)意識しないで書ける F[_]

    で 副作用を分離して書くとシグニチャに副作用が明示的に現れるので嬉しい Pure Scala でクロスプラットフォームなエコシステムはまだまだ発展途上 コントリビューションチャンス