Skip to content

Commit

Permalink
Add display name to mailbox headers
Browse files Browse the repository at this point in the history
  • Loading branch information
minosiants committed Jun 20, 2023
1 parent ac4f157 commit 9a31b25
Show file tree
Hide file tree
Showing 29 changed files with 114 additions and 56 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ libraryDependencies += "com.minosiants" %% "pencil" % "1.2.0"

```scala
val email = Email.text(
from"user1@mydomain.tld",
from"user name <user1@mydomain.tld>",
to"user1@example.com",
subject"first email",
Body.Ascii("hello")
Expand Down
6 changes: 5 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ ThisBuild / scalafixScalaBinaryVersion := (ThisBuild / scalaBinaryVersion).value
ThisBuild / semanticdbEnabled := true
ThisBuild / semanticdbVersion := scalafixSemanticdb.revision


lazy val root = (project in file("."))
.settings(
organization := "com.minosiants",
Expand All @@ -25,7 +26,10 @@ lazy val root = (project in file("."))
scalacOptions ++= Seq(
"-language:experimental.macros",
"-new-syntax",
"-indent"
"-indent",
"-source:future",
"-deprecation",
"-feature"
),
javacOptions ++= Seq("-source", "1.17", "-target", "1.17"),
libraryDependencies ++= Seq(
Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/pencil/Client.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import com.comcast.ip4s.*
import fs2.io.net.tls.TLSContext
import fs2.io.net.{Network, Socket}
import org.typelevel.log4cats.Logger
import pencil.{Host => PHost}
import pencil.Host as PHost

import java.time.Instant
import java.util.UUID
Expand Down
4 changes: 2 additions & 2 deletions src/main/scala/pencil/ContentTypeFinder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import java.io.InputStream
import cats.implicits.*
import cats.effect.Sync
import org.apache.tika.Tika
import protocol._
import data._
import protocol.*
import data.*
import java.lang

object ContentTypeFinder:
Expand Down
8 changes: 4 additions & 4 deletions src/main/scala/pencil/Files.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@

package pencil

import cats.implicits._
import cats.implicits.*
import java.io.InputStream
import java.nio.file.{Path, Paths, Files => JFiles}
import java.nio.file.{Path, Paths, Files as JFiles}

import cats.MonadError
import cats.MonadThrow
import cats.effect.{Resource, Sync}
import Function._
import data._
import Function.*
import data.*
object Files:
def inputStream[F[_]: Sync](file: Path): Resource[F, InputStream] =
Resource
Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/pencil/data/Attachment.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ package pencil
package data

import java.nio.file.Path
import cats.implicits._
import cats.implicits.*
import cats.effect.Sync

object AttachmentType:
Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/pencil/data/Cc.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ package pencil
package data

import cats.Show
import cats.syntax.show._
import cats.syntax.show.*
import cats.data.NonEmptyList
import cats.kernel.Semigroup
object CcType:
Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/pencil/data/Email.scala
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ final case class Email(

/** Combine `to` values.
*/
def addTo(to: To): Email = addTo(to.toList: _*)
def addTo(to: To): Email = addTo(to.toList*)

/** Combine `to` values.
*/
Expand Down
9 changes: 6 additions & 3 deletions src/main/scala/pencil/data/Mailbox.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@
* limitations under the License.
*/

package pencil.data
package pencil
package data

import cats.Show
import scodec.Codec

final case class Mailbox(localPart: String, domain: String) extends Product with Serializable:
final case class Mailbox(localPart: String, domain: String, name: Option[Name] = None)
extends Product
with Serializable:

def address: String = s"$localPart@$domain"

Expand All @@ -31,6 +34,6 @@ object Mailbox:
fromString(mailbox).fold(throw _, identity)

given Show[Mailbox] =
Show.show[Mailbox](mb => s"<${mb.address}>")
Show.show[Mailbox](mb => s"${mb.name.getOrElse("")} <${mb.address}>")

given Codec[Mailbox] = MailboxCodec()
2 changes: 1 addition & 1 deletion src/main/scala/pencil/data/MailboxCodec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ final case class MailboxCodec() extends Codec[Mailbox]:
val from = bytes.indexOfSlice(`<`)
val to = bytes.indexOfSlice(`>`)
if from < 0 || to < 0 then Attempt.failure(Err("email does not included into '<' '>'"))
else Attempt.successful(bytes.slice(from + `<`.size, to).bits)
else Attempt.successful(bits)

}

Expand Down
37 changes: 30 additions & 7 deletions src/main/scala/pencil/data/MailboxParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
* limitations under the License.
*/

package pencil.data
package pencil
package data

import scala.util.matching.Regex
import cats.syntax.either.*
Expand Down Expand Up @@ -76,22 +77,44 @@ object MailboxParser {
else Right(box)

private def split(box: String): Either[Error, (LocalPart, Domain)] =
box.split("@").toList match {
box.split("@").toList match
case Nil => Left(Error.InvalidMailBox(s" '$box' does not have '@'"))
case _ :: Nil => Left(Error.InvalidMailBox(s" '$box' does not have '@'"))
case lp :: _ if lp.isBlank =>
Left(Error.InvalidMailBox(s" '$box' does not have local part"))
case _ :: d if d.mkString.isBlank =>
Left(Error.InvalidMailBox(s" '$box' does not have domain"))
case lp :: d => Right((lp, d.mkString))
}

val mailboxPattern: Regex = """(.*)<(.*)>""".r
def extractName(mailbox: String): (Option[Name], String) =
def toName(str: String): Option[Name] =
val inValid = str.isBlank || str.foldLeft(false) { case (acc, r) =>
acc || special.contains(r)
}
Option.when(!inValid)(Name(str.trim))

mailboxPattern
.findFirstMatchIn(mailbox)
.map { v =>
(toName(v.group(1)), v.group(2))
}
.getOrElse((None, mailbox))

def parse(mailbox: String): Either[Error, Mailbox] =
for
_ <- verifyLength(mailbox)
parts <- split(mailbox)
lp <- verifyLocalPart(parts._1)
dom <- verifyDomain(parts._2)
yield Mailbox(lp, dom)
(name, box) = extractName(mailbox)
(l, d) <- split(box)
lp <- verifyLocalPart(l)
dom <- verifyDomain(d)
yield Mailbox(lp, dom, name)

}

object A {
def main(args: Array[String]): Unit = {
val r = MailboxParser.extractName("hello<may@d.com>")
println(r)
}
}
10 changes: 10 additions & 0 deletions src/main/scala/pencil/data/Name.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package pencil
package data

object NameType {
opaque type Name = String

object Name:
def apply(name: String): Name = name

}
2 changes: 1 addition & 1 deletion src/main/scala/pencil/data/To.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package pencil.data
import cats.Show
import cats.data.NonEmptyList
import cats.kernel.Semigroup
import cats.syntax.show._
import cats.syntax.show.*
object ToType:

opaque type To = NonEmptyList[Mailbox]
Expand Down
2 changes: 2 additions & 0 deletions src/main/scala/pencil/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,5 @@ type Username = pencil.data.UsernameType.Username
val Username = pencil.data.UsernameType.Username
type Password = pencil.data.PasswordType.Password
val Password = pencil.data.PasswordType.Password
type Name = pencil.data.NameType.Name
val Name = pencil.data.NameType.Name
4 changes: 2 additions & 2 deletions src/main/scala/pencil/protocol/Command.scala
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ object Command:
val endEmail: String = s"$end.$end"
given Show[Command] = Show.show {
case Ehlo(domain) => s"EHLO $domain$end"
case Mail(Mailbox(localPart, domain)) =>
case Mail(Mailbox(localPart, domain, name)) =>
s"MAIL FROM: <$localPart@$domain>$end"
case Rcpt(Mailbox(localPart, domain)) =>
case Rcpt(Mailbox(localPart, domain, name)) =>
s"RCPT TO: <$localPart@$domain>$end"
case Data => s"DATA$end"
case Rset => s"RSET$end"
Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/pencil/protocol/CommandCodec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ final case class CommandCodec() extends Codec[Command] {
DecodeResult(Ehlo(domain), BitVector.empty)
}
case "MAIL" =>
summon[Codec[Mailbox]].decode(rest).map { case DecodeResult(email, _) =>
summon[Codec[Mailbox]].decode(rest.drop(6*8)).map { case DecodeResult(email, _) =>
DecodeResult(Mail(email), BitVector.empty)
}
case "RCPT" =>
Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/pencil/protocol/Header.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
package pencil.protocol

import cats.Show
import cats.syntax.show._
import cats.syntax.show.*

enum Header:
case `MIME-Version`(value: String = "1.0")
Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/pencil/syntax/LiteralsSyntax.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

package pencil
package syntax
import data._
import data.*
import java.nio.file.Paths
import org.typelevel.literally.Literally

Expand Down
2 changes: 1 addition & 1 deletion src/test/resources/logback.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<logger name="pencil" level="DEBUG" >
<appender-ref ref="STDOUT" />
</logger>
<root level="debug">
<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>
1 change: 1 addition & 0 deletions src/test/scala/pencil/MailServerContainer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ object MailServerContainer:
override def start(): Unit =
container.start()
container.waitingFor(Wait.forListeningPort())
println(s"http port:$httpPort")

override def stop(): Unit = container.stop()

Expand Down
4 changes: 4 additions & 0 deletions src/test/scala/pencil/SendEmailSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import io.circe.generic.auto.*
import org.http4s.EntityDecoder
import pencil.data.{Email, Mailbox}
import org.http4s.circe.*
import org.specs2.execute.Pending
import pencil.protocol.Replies
class SendEmailSpec extends MailServerSpec {
sequential
Expand All @@ -35,13 +36,16 @@ class SendEmailSpec extends MailServerSpec {
}
.unsafeRunSync()
println(message)
message.Bcc.map(_.Address) ==== email.bcc.toList.flatMap(_.toList.map(_.address))
message.Cc.map(_.Address) ==== email.cc.toList.flatMap(_.toList.map(_.address))
message.To.map(_.Address) ==== email.to.toList.map(_.address)
message.From.Address ==== email.from.address
Name(message.From.Name) ==== email.from.mailbox.name.get
message.Subject ==== email.subject.get.asString
message.Text ==== email.body.get.value
}
Pending("this is integration test")
}
def sendEmail(email: Email): IO[Replies] = for
Expand Down
4 changes: 2 additions & 2 deletions src/test/scala/pencil/SmtpBaseSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import scodec.{Codec, DecodeResult}
import java.time.{Clock, Instant, ZoneId, ZoneOffset}
import java.util.UUID
import scala.concurrent.duration.*
trait SmtpBaseSpec extends SpecificationLike with LiteralsSyntax {
trait SmtpBaseSpec extends SpecificationLike with LiteralsSyntax:

val logger = Slf4jLogger.getLogger[IO]
val timestamp = Instant.now()
Expand Down Expand Up @@ -85,4 +85,4 @@ trait SmtpBaseSpec extends SpecificationLike with LiteralsSyntax {
)
}.attempt.unsafeRunSync()

}

4 changes: 2 additions & 2 deletions src/test/scala/pencil/SmtpSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class SmtpSpec extends SmtpBaseSpec {
val email = SmtpSpec.mime
val result = testCommand(Smtp.rcpt(), email, codecs.ascii)
val rcpts = email.recipients
.map(box => s"RCPT TO: ${box.show}${Command.end}")
.map(box => s"RCPT TO: <${box.address}>${Command.end}")
.toList

result.map(_._1) must beRight(
Expand All @@ -46,7 +46,7 @@ class SmtpSpec extends SmtpBaseSpec {
val from = SmtpSpec.mime.from.mailbox
result.map(_._1) must beRight(DataSamples.`250 OK`)
result.map(_._2) must beRight(
beEqualTo(List(s"MAIL FROM: ${from.show}${Command.end}"))
beEqualTo(List(s"MAIL FROM: <${from.address}>${Command.end}"))
)
}

Expand Down
6 changes: 2 additions & 4 deletions src/test/scala/pencil/SmtpSpec2.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package pencil

/*
import cats.effect.IO
import pencil.syntax.*
import pencil.protocol.Code
Expand Down Expand Up @@ -30,10 +29,9 @@ class SmtpSpec2 extends MailServerSpec {

object SmtpSpec2 extends LiteralsSyntax:
given mimeEmail: Email = Email.mime(
from"user1@mydomain.tld",
to"pencil@mail.pencil.com",
from"kaspar minosyants<user1@mydomain.tld>",
to"pencil <pencil@mail.pencil.com>",
subject"привет",
Body.Utf8("hi there")
// List(attachment"files/jpeg-sample.jpg")
)
*/
17 changes: 14 additions & 3 deletions src/test/scala/pencil/data/EmailGens.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ trait EmailGens {

val localPartGen: Gen[String] = Gen
.nonEmptyListOf(localPartCharGen)
.map(_.take(200).mkString)
.map(_.take(180).mkString)
.retryUntil(v => !v.contains(".."))

val domainGen: Gen[String] = for {
Expand All @@ -30,10 +30,21 @@ trait EmailGens {
domain = (ch :: d) :+ ch
} yield domain.mkString

val mailboxGen: Gen[Mailbox] = for {
val nameGen: Gen[Option[String]] = Gen.option(
Gen
.nonEmptyListOf(localPartCharGen)
.map(_.take(20).mkString)
)

val mailboxGen: Gen[Mailbox] = for
lp <- localPartGen
domain <- domainGen
} yield Mailbox.unsafeFromString(s"$lp@$domain")
name <- nameGen
email = s"$lp@$domain"
mb = name match
case Some(name) if name.trim.nonEmpty => s"$name<$email>"
case _ => email
yield Mailbox.unsafeFromString(mb)

given Arbitrary[From] = Arbitrary(mailboxGen.map(From(_)))
given Arbitrary[To] = Arbitrary(Gen.nonEmptyListOf(mailboxGen).map(To(_*)))
Expand Down
Loading

0 comments on commit 9a31b25

Please sign in to comment.