Skip to content

WIP: Upgrade to CE3 #53

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
val catsVersion = "2.5.0"
val catsEffectVersion = "2.4.1"
val fs2Version = "2.5.4"
val catsEffectVersion = "3.0.1"
val fs2Version = "3.0.1"
val scodecBitsVersion = "1.1.25"
val scodecCoreVersion = "1.11.7"
val scodecStreamVersion = "2.0.1"
val scodecStreamVersion = "3.0.0-RC1"
val shapelessVersion = "2.3.3"
val specs2Version = "4.10.6"
val tikaVersion = "1.24"
Expand All @@ -16,8 +16,8 @@ lazy val root = (project in file("."))
.settings(
organization := "com.minosiants",
name := "pencil",
scalaVersion := "2.12.12",
crossScalaVersions := Seq("2.12.12", "2.13.4"),
scalaVersion := "2.12.13",
crossScalaVersions := Seq("2.12.13", "2.13.5"),
scalacOptions ++= Seq(
"-language:experimental.macros",
"-Yrangepos",
Expand All @@ -27,7 +27,7 @@ lazy val root = (project in file("."))
javacOptions ++= Seq("-source", "1.11", "-target", "1.8"),
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-core" % catsVersion,
"org.typelevel" %% "cats-effect" % catsEffectVersion,
"org.typelevel" %% "cats-effect-kernel" % catsEffectVersion,
"co.fs2" %% "fs2-core" % fs2Version,
"co.fs2" %% "fs2-io" % fs2Version,
"com.chuusai" %% "shapeless" % shapelessVersion,
Expand All @@ -36,16 +36,16 @@ lazy val root = (project in file("."))
"org.typelevel" %% "log4cats-core" % log4catsVersion,
"org.apache.tika" % "tika-core" % tikaVersion,
"org.scala-lang" % "scala-reflect" % scalaVersion.value,
"org.specs2" %% "specs2-core" % specs2Version % "test",
"org.specs2" %% "specs2-core" % specs2Version % Test,
"org.specs2" %% "specs2-scalacheck" % specs2Version % Test,
"org.scalacheck" %% "scalacheck" % scalacheckVersion % "test",
"com.codecommit" %% "cats-effect-testing-specs2" % catsEffectTestVersion % "test",
"org.scodec" %% "scodec-stream" % scodecStreamVersion % "test",
"ch.qos.logback" % "logback-classic" % logbackVersion % "test",
"org.typelevel" %% "log4cats-slf4j" % log4catsVersion % "test"
"org.scalacheck" %% "scalacheck" % scalacheckVersion %Test,
"com.codecommit" %% "cats-effect-testing-specs2" % catsEffectTestVersion % Test,
"org.scodec" %% "scodec-stream" % scodecStreamVersion % Test,
"ch.qos.logback" % "logback-classic" % logbackVersion % Test,
"org.typelevel" %% "log4cats-slf4j" % log4catsVersion % Test
),
addCompilerPlugin(
"org.typelevel" %% "kind-projector" % "0.11.1" cross CrossVersion.full
"org.typelevel" %% "kind-projector" % "0.11.3" cross CrossVersion.full
),
addCompilerPlugin(scalafixSemanticdb),
publishTo := sonatypePublishToBundle.value
Expand Down
66 changes: 26 additions & 40 deletions src/main/scala/com/minosiants/pencil/Client.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,14 @@

package com.minosiants.pencil

import java.net.InetSocketAddress
import java.time.Instant
import java.util.UUID

import cats.effect._
import com.minosiants.pencil.data.Email.{ MimeEmail, TextEmail }
import com.minosiants.pencil.data._
import cats.effect.Temporal
import com.comcast.ip4s._
import com.minosiants.pencil.protocol._
import fs2.io.tcp.{ Socket, SocketGroup }
import fs2.io.tls.TLSContext
import fs2.io.net.SocketGroup
import fs2.io.net.tls.{ TLSContext, TLSParameters }
import org.typelevel.log4cats.Logger

import scala.Function.const
Expand All @@ -36,47 +34,39 @@ import scala.concurrent.duration._
*
*/
trait Client[F[_]] {

/**
* Sends `email` to a smtp server
*
* @param email - email to be sent
* @param es - sender [[EmailSender]]
* @return - IO of [[Replies]] from smtp server
*/
def send(email: Email): F[Replies]

def send(email: data.Email): F[Replies]
}

object Client {
def apply[F[_]: Concurrent: ContextShift](
host: String = "localhost",
port: Int = 25,
credentials: Option[Credentials] = None,
readTimeout: FiniteDuration = 5.minutes,
writeTimeout: FiniteDuration = 5.minutes
def apply[F[_]](
host: Host = host"localhost",
port: Port = port"25",
credentials: Option[data.Credentials] = None,
readTimeout: FiniteDuration = 5.minutes,
writeTimeout: FiniteDuration = 5.minutes
)(
blocker: Blocker,
sg: SocketGroup,
tlsContext: TLSContext,
logger: Logger[F]
tlsContext: TLSContext[F],
tlsParameters: TLSParameters = TLSParameters.Default,
logger: Logger[F]
)(implicit
sg: SocketGroup[F],
ev: Temporal[F]
): Client[F] =
new Client[F] {
val socket: Resource[F, Socket[F]] =
sg.client[F](new InetSocketAddress(host, port))

def tlsSmtpSocket(s: Socket[F]): Resource[F, SmtpSocket[F]] =
tlsContext.client(s).map { cs =>
SmtpSocket.fromSocket(cs, logger, readTimeout, writeTimeout)
}

override def send(
email: Email
email: data.Email
): F[Replies] = {
val sockets = for {
s <- socket
tls <- tlsSmtpSocket(s)
} yield (s, tls)
rawSocket <- sg.client(SocketAddress(host, port))
tlsSocket <- tlsContext.client(rawSocket, tlsParameters, Some(msg => logger.debug(s"TLS Socket: ${msg}")))
} yield (rawSocket, SmtpSocket.fromSocket(tlsSocket, logger, readTimeout, writeTimeout))

sockets.use {
case (s, tls) =>
Expand All @@ -91,8 +81,7 @@ object Client {
Request(
email,
SmtpSocket.fromSocket(s, logger, readTimeout, writeTimeout),
blocker,
Host.local(),
data.Host.local(),
Instant.now(),
UUID.randomUUID().toString
)
Expand All @@ -113,17 +102,14 @@ object Client {
reply => reply.text.contains("AUTH") && reply.text.contains("LOGIN")
)

def sendEmailViaTls(
tls: SmtpSocket[F]
): Smtp[F, Replies] =
def sendEmailViaTls(tls: SmtpSocket[F]): Smtp[F, Replies] =
for {
_ <- Smtp.startTls[F]()
r <- Smtp.local { req: Request[F] =>
Request(
req.email,
tls,
req.blocker,
Host.local(),
data.Host.local(),
Instant.now(),
UUID.randomUUID().toString
)
Expand All @@ -136,7 +122,7 @@ object Client {

def sender: Smtp[F, Replies] = Smtp.ask[F].flatMap { r =>
r.email match {
case TextEmail(_, _, _, _, _, _) =>
case data.Email.TextEmail(_, _, _, _, _, _) =>
for {
_ <- Smtp.mail[F]()
_ <- Smtp.rcpt[F]()
Expand All @@ -147,7 +133,7 @@ object Client {
_ <- Smtp.quit[F]()
} yield r

case MimeEmail(_, _, _, _, _, _, _, _) =>
case data.Email.MimeEmail(_, _, _, _, _, _, _, _) =>
for {
_ <- Smtp.mail[F]()
_ <- Smtp.rcpt[F]()
Expand Down
4 changes: 1 addition & 3 deletions src/main/scala/com/minosiants/pencil/ContentTypeFinder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ import cats.effect.Sync
import org.apache.tika.Tika

object ContentTypeFinder {

lazy val tika = new Tika()
private lazy val tika = new Tika()

def findType[F[_]: Sync](is: InputStream): F[ContentType] =
Sync[F]
Expand All @@ -40,5 +39,4 @@ object ContentTypeFinder {
.handleErrorWith(
Error.tikaException[F, ContentType]("Unable to read input stream")
)

}
2 changes: 0 additions & 2 deletions src/main/scala/com/minosiants/pencil/Files.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import com.minosiants.pencil.data.Error
import Function._

object Files {

def inputStream[F[_]: Sync](file: Path): Resource[F, InputStream] = {
Resource
.make {
Expand Down Expand Up @@ -67,6 +66,5 @@ object Files {
.handleErrorWith { _ =>
Error.resourceNotFound[F, Path](file)
}

}
}
2 changes: 0 additions & 2 deletions src/main/scala/com/minosiants/pencil/Request.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@ package com.minosiants.pencil

import java.time.Instant

import cats.effect.Blocker
import com.minosiants.pencil.data.{ Email, Host }

final case class Request[F[_]](
email: Email,
socket: SmtpSocket[F],
blocker: Blocker,
host: Host,
timestamp: Instant,
uuid: () => String
Expand Down
13 changes: 6 additions & 7 deletions src/main/scala/com/minosiants/pencil/Smtp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ package com.minosiants.pencil
import java.time.format.DateTimeFormatter
import java.time.{ Instant, ZoneId, ZoneOffset }

import cats._
import cats.{Applicative, ApplicativeError, MonadError}
import cats.data.Kleisli
import cats.effect.{ ContextShift, Sync }
import cats.implicits._
import cats.effect.Sync
import cats.syntax.all._
import com.minosiants.pencil.data.Body.{ Ascii, Html, Utf8 }
import com.minosiants.pencil.data.Email._
import com.minosiants.pencil.data.{ Email, Mailbox, _ }
Expand All @@ -35,7 +35,6 @@ import com.minosiants.pencil.protocol._
import fs2.io.file.readAll
import fs2.{ Chunk, Stream }

import scala.Function._
object Smtp {
// Used for easier type inference
def apply[F[_]]: SmtpPartiallyApplied[F] =
Expand Down Expand Up @@ -115,7 +114,7 @@ object Smtp {
} yield res

def command[F[_]: MonadError[*[_], Throwable]](c: Command): Smtp[F, Replies] =
command1(const(c))
command1(_ => c)

def init[F[_]: MonadError[*[_], Throwable]](): Smtp[F, Replies] = read[F]

Expand Down Expand Up @@ -152,7 +151,7 @@ object Smtp {
def quit[F[_]: MonadError[*[_], Throwable]](): Smtp[F, Replies] =
command(Quit)

def text[F[_]](txt: String): Smtp[F, Unit] = write(const(Text(txt)))
def text[F[_]](txt: String): Smtp[F, Unit] = write(_ => Text(txt))

def startTls[F[_]: MonadError[*[_], Throwable]](): Smtp[F, Replies] =
command(StartTls)
Expand Down Expand Up @@ -366,7 +365,7 @@ object Smtp {
liftF(Error.smtpError[F, Unit]("not mime email"))
}

def attachments[F[_]: Sync: ContextShift: Applicative](): Smtp[F, Unit] = {
def attachments[F[_] : Applicative : Sync](): Smtp[F, Unit] = {
Smtp[F] { req =>
req.email match {
case TextEmail(_, _, _, _, _, _) =>
Expand Down
28 changes: 14 additions & 14 deletions src/main/scala/com/minosiants/pencil/SmtpSocket.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,26 @@

package com.minosiants.pencil

import cats._
import cats.effect.Sync
import cats.implicits._
import cats.MonadError
import cats.effect.Temporal
import cats.effect.syntax.all._
import cats.syntax.all._
import com.minosiants.pencil.data.Error
import com.minosiants.pencil.protocol.Command._
import com.minosiants.pencil.protocol._
import fs2.Chunk
import fs2.io.tcp.Socket
import fs2.io.net.Socket
import org.typelevel.log4cats.Logger
import scodec.bits.BitVector
import scodec.codecs._
import scodec.{ Attempt, DecodeResult }

import scala.concurrent.duration.FiniteDuration
import com.minosiants.pencil.data.Error

/**
* Wraps [[Socket[IO]]] with smtp specific protocol
*/
trait SmtpSocket[F[_]] {

/**
* Reads [[Replies]] from smtp server
*/
Expand All @@ -48,25 +48,25 @@ trait SmtpSocket[F[_]] {
}

object SmtpSocket {
def fromSocket[F[_]: Sync: MonadError[*[_], Throwable]](
s: Socket[F],
logger: Logger[F],
readTimeout: FiniteDuration,
writeTimeout: FiniteDuration
def fromSocket[F[_]: MonadError[*[_], Throwable] : Temporal](
s: Socket[F],
logger: Logger[F],
readTimeout: FiniteDuration,
writeTimeout: FiniteDuration
): SmtpSocket[F] = new SmtpSocket[F] {
def bytesToReply(bytes: Array[Byte]): F[Replies] =
Replies.codec.decode(BitVector(bytes)) match {
case Attempt.Successful(DecodeResult(value, _)) =>
logger.debug(s"Getting Replies: ${value.show}") *>
Applicative[F].pure(value)
value.pure[F]

case Attempt.Failure(cause) =>
logger.debug(s" Getting Error: ${cause.messageWithContext}") *>
Error.smtpError[F, Replies](cause.messageWithContext)
}

override def read(): F[Replies] =
s.read(8192, Some(readTimeout)).flatMap {
s.read(8192).timeout(readTimeout).flatMap {
case Some(chunk) => bytesToReply(chunk.toArray)
case None => Error.smtpError[F, Replies]("Nothing to read")
}
Expand All @@ -75,7 +75,7 @@ object SmtpSocket {
ascii.encode(command.show) match {
case Attempt.Successful(value) =>
logger.debug(s"Sending command: ${command.show}") *>
s.write(Chunk.array(value.toByteArray), Some(writeTimeout))
s.write(Chunk.array(value.toByteArray)).timeout(writeTimeout)

case Attempt.Failure(cause) =>
Error.smtpError[F, Unit](cause.messageWithContext)
Expand Down
2 changes: 0 additions & 2 deletions src/main/scala/com/minosiants/pencil/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import com.minosiants.pencil.syntax.LiteralsSyntax
import scodec.bits.{ BitVector, ByteVector }

package object pencil extends LiteralsSyntax {

type Smtp[F[_], A] = Kleisli[F, Request[F], A]

val CRLF: ByteVector = ByteVector("\r\n".getBytes)
Expand All @@ -33,5 +32,4 @@ package object pencil extends LiteralsSyntax {
def toBitVector: BitVector = BitVector(str.getBytes(StandardCharsets.UTF_8))
def toByteVector: ByteVector = toBitVector.bytes
}

}
2 changes: 1 addition & 1 deletion version.sbt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
version in ThisBuild := "0.6.7-SNAPSHOT"
ThisBuild / version := "0.6.7-SNAPSHOT"