├── .gitignore ├── LICENSE ├── README.md ├── build.sbt ├── conf └── application.conf ├── core └── src │ ├── main │ └── scala │ │ └── net │ │ └── kaliber │ │ └── mailer │ │ ├── Email.scala │ │ ├── Mailer.scala │ │ ├── exceptions.scala │ │ └── package.scala │ └── test │ └── scala │ ├── net │ └── kaliber │ │ └── mailer │ │ ├── AsyncMailerTests.scala │ │ ├── EmailTests.scala │ │ ├── MailerTests.scala │ │ └── SessionTests.scala │ └── testUtils │ ├── FullEmail.scala │ ├── FullMessage.scala │ ├── MailboxUtilities.scala │ └── TestSettings.scala ├── play └── src │ ├── main │ └── scala │ │ └── net │ │ └── kaliber │ │ └── play │ │ └── mailer │ │ └── package.scala │ └── test │ ├── resources │ └── logback-test.xml │ └── scala │ └── net │ └── kaliber │ └── play │ └── mailer │ └── MailerTests.scala ├── project ├── bintray.sbt ├── build.properties ├── pgp.sbt └── release.sbt └── version.sbt /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | project/project 3 | project/target 4 | conf/test.conf 5 | target 6 | tmp 7 | .history 8 | dist/.project 9 | /.classpath 10 | /.cache 11 | /.cache-main 12 | /.cache-tests 13 | /.settings 14 | /.project 15 | test/conf 16 | .target 17 | .directory 18 | 19 | .idea/ 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2012 Rhinofly 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | *This repository is no longer maintained* 3 | 4 | Just create a fork, if you want I can list it here. 5 | 6 | ------------- 7 | 8 | Scala mailer module with support for Play 2.5.x 9 | ===================================================== 10 | 11 | Scala wrapper around java mail which allows you to send emails. The default configuration options exposed in Configuration work using Amazon SES SMTP. 12 | 13 | Installation 14 | ------------ 15 | You can add the scala-mailer to SBT, by adding the following to your build.sbt file 16 | 17 | ``` scala 18 | libraryDependencies += "net.kaliber" %% "play-mailer" % "6.0.0" 19 | ``` 20 | 21 | Usage 22 | ----- 23 | 24 | ### Creating an email 25 | 26 | ``` scala 27 | import net.kaliber.mailer._ 28 | 29 | Email( 30 | subject = "Test mail", 31 | from = EmailAddress("Erik Westra sender", "ewestra@rhinofly.nl"), 32 | text = "text", 33 | htmlText = "htmlText", 34 | replyTo = None, 35 | recipients = List(Recipient( 36 | RecipientType.TO, EmailAddress("Erik Westra recipient", "ewestra@rhinofly.nl"))), 37 | attachments = Seq.empty) 38 | 39 | // a more convenient way to create an email 40 | val email = Email( 41 | subject = "Test mail", 42 | from = EmailAddress("Erik Westra sender", "ewestra@rhinofly.nl"), 43 | text = "text", 44 | htmlText = "htmlText") 45 | .to("Erik Westra TO", "ewestra+to@rhinofly.nl") 46 | .cc("Erik Westra CC", "ewestra+cc@rhinofly.nl") 47 | .bcc("Erik Westra BCC", "ewestra+bcc@rhinofly.nl") 48 | .replyTo("Erik Westra REPLY_TO", "ewestra+replyTo@rhinofly.nl") 49 | .withAttachments( 50 | Attachment("attachment1", Array[Byte](0, 1), "application/octet-stream"), 51 | Attachment("attachment2", Array[Byte](0, 1), "application/octet-stream", Disposition.Inline)) 52 | ``` 53 | 54 | ### Sending an email asynchronously 55 | 56 | ``` scala 57 | import net.kaliber.mailer._ 58 | 59 | val mailerSettings = MailerSettings( 60 | protocol = Some("smtps"), 61 | host = "email-smtp.us-east-1.amazonaws.com", 62 | port = "465", 63 | failTo = "failto+customer@company.org", 64 | auth = Some(true), 65 | username = Some("Smtp username as generated by Amazon"), 66 | password = Some("Smtp password") 67 | 68 | val result = new Mailer(Session.fromSetting(mailerSettings)).sendEmail(email) 69 | 70 | result 71 | .map { unit => 72 | // mail sent successfully 73 | } 74 | .recover { 75 | case SendEmailException(email, cause) => 76 | // problem sending email 77 | case SendEmailTransportCloseException(result, cause) => 78 | // problem closing connection 79 | } 80 | 81 | ``` 82 | 83 | ### Sending multiple emails asynchronously 84 | 85 | ``` scala 86 | import net.kaliber.mailer._ 87 | 88 | val mailerSettings = MailerSettings( 89 | protocol = Some("smtps"), 90 | host = "email-smtp.us-east-1.amazonaws.com", 91 | port = "465", 92 | failTo = "failto+customer@company.org", 93 | auth = Some(true), 94 | username = Some("Smtp username as generated by Amazon"), 95 | password = Some("Smtp password") 96 | 97 | val result = new Mailer(Session.fromSetting(mailerSettings)).sendEmails(email1, email2) 98 | 99 | result 100 | .map { results => 101 | results.foreach { 102 | case Success(_) => 103 | //mail sent successfully 104 | case Failure(SendEmailException(email, cause)) => 105 | //failed to send email, cause provides more information 106 | } 107 | } 108 | .recover { 109 | case SendEmailException(email, cause) => 110 | // problem sending email 111 | case SendEmailTransportCloseException(result, cause) => 112 | // problem closing connection 113 | } 114 | ``` 115 | 116 | Play Configuration 117 | ------------- 118 | 119 | `application.conf` should contain the following information: 120 | 121 | ``` scala 122 | mail.failTo="failto+customer@company.org" 123 | 124 | mail.host=email-smtp.us-east-1.amazonaws.com 125 | mail.port=465 126 | 127 | #only required if mail.auth=true (the default) 128 | mail.username="Smtp username as generated by Amazon" 129 | mail.password="Smtp password" 130 | ``` 131 | 132 | `application.conf` can additionally contain the following information: 133 | ``` scala 134 | #default is smtps 135 | mail.transport.protocol=smtp 136 | 137 | #default is true 138 | mail.auth=false 139 | ``` 140 | 141 | Usage with Play configuration 142 | ------------- 143 | 144 | ### Sending an email asynchronously 145 | 146 | ``` scala 147 | import net.kaliber.mailer._ 148 | import net.kaliber.play.mailer.Session 149 | 150 | val result = new Mailer(Session.fromConfiguration(configuration)).sendEmail(email) 151 | 152 | result 153 | .map { unit => 154 | // mail sent successfully 155 | } 156 | .recover { 157 | case SendEmailException(email, cause) => 158 | // problem sending email 159 | case SendEmailTransportCloseException(result, cause) => 160 | // problem closing connection 161 | } 162 | ``` 163 | 164 | ### Sending multiple emails asynchronously 165 | ``` scala 166 | import net.kaliber.mailer._ 167 | import net.kaliber.play.mailer.Session 168 | 169 | val results = new Mailer(Session.fromConfiguration(configuration)).sendEmails(Seq(email1, email2)) 170 | 171 | results 172 | .map { results => 173 | results.foreach { 174 | case Success(_) => 175 | //mail sent successfully 176 | case Failure(SendEmailException(email, cause)) => 177 | //failed to send email, cause provides more information 178 | } 179 | } 180 | .recover { 181 | case SendEmailException(email, cause) => 182 | // problem sending email 183 | case SendEmailTransportCloseException(result, cause) => 184 | // problem closing connection 185 | } 186 | ``` 187 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | 2 | resolvers ++= Seq( 3 | "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/", 4 | "scalaz-bintray" at "https://dl.bintray.com/scalaz/releases" /* specs2-core depends on scalaz-stream */ 5 | ) 6 | 7 | lazy val commonSettings = Seq( 8 | organization := "net.kaliber", 9 | scalaVersion := "2.11.8" 10 | ) 11 | 12 | val doNotPublishSettings = Seq( 13 | Keys.`package` := file(""), 14 | packageBin in Global := file(""), 15 | packagedArtifacts := Map(), 16 | publishArtifact := false, 17 | publish := {}, 18 | bintrayRelease := (), 19 | bintrayReleaseOnPublish := false, 20 | bintrayUnpublish := () 21 | ) 22 | 23 | lazy val core = (project in file("core")) 24 | .settings( 25 | name := "scala-mailer-core" 26 | ) 27 | .settings(commonSettings: _*) 28 | .settings( 29 | libraryDependencies ++= Seq( 30 | "com.typesafe" % "config" % "1.3.1" % "provided", 31 | "javax.mail" % "mail" % "1.4.7" 32 | ) 33 | ) 34 | .settings(bintraySettings: _*) 35 | .settings(testSettings: _*) 36 | 37 | lazy val play = (project in file("play")) 38 | .settings( 39 | name := "scala-mailer-play" 40 | ) 41 | .settings(commonSettings: _*) 42 | .settings(bintraySettings: _*) 43 | .settings(testSettings: _*) 44 | .settings(playSettings: _*) 45 | .dependsOn(core % "compile->compile;test->test") 46 | 47 | lazy val root = (project in file(".")) 48 | .settings( 49 | name := "scala-mailer", 50 | publishArtifact := false 51 | ) 52 | .settings(doNotPublishSettings: _*) 53 | .aggregate(core, play) 54 | 55 | 56 | lazy val playSettings = { 57 | val playVersion = "2.5.11" 58 | 59 | libraryDependencies ++= Seq( 60 | "com.typesafe.play" %% "play" % playVersion % "provided", 61 | "com.typesafe.play" %% "play-test" % playVersion % "test", 62 | "com.typesafe.play" %% "play-specs2" % playVersion % "test" 63 | ) 64 | } 65 | 66 | lazy val testSettings = Seq( 67 | libraryDependencies ++= Seq( 68 | "org.specs2" %% "specs2-core" % "3.6.2" % "test", 69 | "org.jvnet.mock-javamail" % "mock-javamail" % "1.9" % "test" 70 | ) 71 | ) 72 | 73 | lazy val bintraySettings = Seq( 74 | licenses += ("MIT", url("http://opensource.org/licenses/MIT")), 75 | homepage := Some(url("https://github.com/kaliber-scala/scala-mailer")), 76 | bintrayOrganization := Some("kaliber-scala"), 77 | bintrayReleaseOnPublish := false, 78 | publishMavenStyle := true, 79 | 80 | pomExtra := ( 81 | 82 | scm:git@github.com:kaliber-scala/scala-mailer.git 83 | scm:git@github.com:kaliber-scala/scala-mailer.git 84 | https://github.com/kaliber-scala/scala-mailer 85 | 86 | 87 | 88 | Kaliber 89 | Kaliber Interactive 90 | https://kaliber.net/ 91 | 92 | 93 | ) 94 | ) 95 | 96 | // Release 97 | import ReleaseTransformations._ 98 | releasePublishArtifactsAction := PgpKeys.publishSigned.value 99 | releaseProcess := Seq[ReleaseStep]( 100 | checkSnapshotDependencies, 101 | inquireVersions, 102 | runClean, 103 | runTest, 104 | setReleaseVersion, 105 | commitReleaseVersion, 106 | tagRelease, 107 | publishArtifacts, 108 | releaseStepTask(bintrayRelease in core), 109 | releaseStepTask(bintrayRelease in play), 110 | setNextVersion, 111 | commitNextVersion, 112 | pushChanges 113 | ) 114 | 115 | fork in Test := true -------------------------------------------------------------------------------- /conf/application.conf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliber-scala/scala-mailer/030558897c03e43e73a7e037b49ee8e1fb48fef0/conf/application.conf -------------------------------------------------------------------------------- /core/src/main/scala/net/kaliber/mailer/Email.scala: -------------------------------------------------------------------------------- 1 | package net.kaliber.mailer 2 | 3 | import java.util.Date 4 | 5 | import javax.activation.DataHandler 6 | import javax.mail.Message 7 | import javax.mail.Part 8 | import javax.mail.internet.InternetAddress 9 | import javax.mail.internet.MimeBodyPart 10 | import javax.mail.internet.MimeMessage 11 | import javax.mail.internet.MimeMultipart 12 | 13 | case class Email(subject: String, from: EmailAddress, text: String, htmlText: Option[String] = None, replyTo: Option[EmailAddress] = None, recipients: Seq[Recipient] = Seq.empty, attachments: Seq[Attachment] = Seq.empty) { 14 | 15 | def withHtmlText(htmlText: String) = 16 | copy(htmlText = Some(htmlText)) 17 | 18 | def to(name: String, address: String) = 19 | copy(recipients = recipients :+ 20 | Recipient(RecipientType.TO, EmailAddress(name, address))) 21 | 22 | def cc(name: String, address: String) = 23 | copy(recipients = recipients :+ 24 | Recipient(RecipientType.CC, EmailAddress(name, address))) 25 | 26 | def bcc(name: String, address: String) = 27 | copy(recipients = recipients :+ 28 | Recipient(RecipientType.BCC, EmailAddress(name, address))) 29 | 30 | def replyTo(name: String, address: String) = 31 | copy(replyTo = Some(EmailAddress(name, address))) 32 | 33 | def withAttachments(attachments: Attachment*) = 34 | copy(attachments = this.attachments ++ attachments) 35 | 36 | def createFor(session: Session): Message = { 37 | 38 | val (root, related, alternative) = messageStructure 39 | 40 | val message = createMimeMessage(session, root) 41 | addRecipients(message) 42 | addTextPart(alternative) 43 | htmlText.foreach(addHtmlPart(alternative)) 44 | addAttachments(root, related) 45 | 46 | message.saveChanges() 47 | 48 | message 49 | } 50 | 51 | private type Root = MimeMultipart 52 | private type Related = MimeMultipart 53 | private type Alternative = MimeMultipart 54 | 55 | private def messageStructure: (Root, Related, Alternative) = { 56 | val root = new MimeMultipart("mixed") 57 | val relatedPart = new MimeBodyPart 58 | val related = new MimeMultipart("related") 59 | 60 | root addBodyPart relatedPart 61 | relatedPart setContent related 62 | 63 | val alternativePart = new MimeBodyPart 64 | val alternative = new MimeMultipart("alternative") 65 | 66 | related addBodyPart alternativePart 67 | alternativePart setContent alternative 68 | 69 | (root, related, alternative) 70 | } 71 | 72 | private def createMimeMessage(session: Session, root: Email.this.Root): javax.mail.internet.MimeMessage = { 73 | 74 | val message = new MimeMessage(session) 75 | message setSubject (subject, "UTF-8") 76 | message setFrom from 77 | replyTo foreach (replyTo => message setReplyTo Array(replyTo)) 78 | message setContent root 79 | message setSentDate new Date 80 | message 81 | } 82 | 83 | private def addRecipients(message: javax.mail.internet.MimeMessage): Unit = 84 | recipients foreach { 85 | case Recipient(tpe, emailAddress) => 86 | message addRecipient (tpe, emailAddress) 87 | } 88 | 89 | private def addTextPart(alternative: Alternative): Unit = { 90 | val messagePart = new MimeBodyPart 91 | messagePart setText (text, "UTF-8") 92 | alternative addBodyPart messagePart 93 | } 94 | 95 | private def addHtmlPart(alternative: Alternative)(htmlText: String): Unit = { 96 | val messagePartHtml = new MimeBodyPart 97 | messagePartHtml setContent (htmlText, "text/html; charset=UTF-8") 98 | alternative addBodyPart messagePartHtml 99 | } 100 | 101 | private def addAttachments(root: Root, related: Related): Unit = 102 | attachments foreach { 103 | case a @ Attachment(_, _, Disposition.Inline) => 104 | related addBodyPart a 105 | case a @ Attachment(_, _, Disposition.Attachment) => 106 | root addBodyPart a 107 | } 108 | 109 | import scala.language.implicitConversions 110 | 111 | private implicit def emailAddressToInternetAddress(emailAddress: EmailAddress): InternetAddress = 112 | new InternetAddress(emailAddress.address, emailAddress.name) 113 | 114 | private implicit def attachmentToMimeBodyPart(attachment: Attachment): MimeBodyPart = { 115 | val Attachment(name, datasource, disposition) = attachment 116 | 117 | val datasourceName = datasource.getName 118 | 119 | val attachmentPart = new MimeBodyPart 120 | attachmentPart.setDataHandler(new DataHandler(datasource)) 121 | attachmentPart.setFileName(name) 122 | attachmentPart.setHeader("Content-Type", datasource.getContentType + "; filename=" + datasourceName + "; name=" + datasourceName) 123 | attachmentPart.setContentID("<" + datasourceName + ">") 124 | attachmentPart.setDisposition(disposition.value + "; size=0") 125 | 126 | attachmentPart 127 | } 128 | } 129 | 130 | object Email { 131 | def apply(subject: String, from: EmailAddress, text: String, htmlText: String): Email = 132 | new Email(subject, from, text, Some(htmlText)) 133 | 134 | def apply(subject: String, from: EmailAddress, text: String, htmlText: String, replyTo: Option[EmailAddress]): Email = 135 | new Email(subject, from, text, Some(htmlText), replyTo) 136 | 137 | def apply(subject: String, from: EmailAddress, text: String, htmlText: String, replyTo: Option[EmailAddress], recipients: Seq[Recipient]): Email = 138 | new Email(subject, from, text, Some(htmlText), replyTo, recipients) 139 | 140 | def apply(subject: String, from: EmailAddress, text: String, htmlText: String, replyTo: Option[EmailAddress], recipients: Seq[Recipient], attachments: Seq[Attachment]): Email = 141 | new Email(subject, from, text, Some(htmlText), replyTo, recipients, attachments) 142 | } 143 | 144 | case class EmailAddress(name: String, address: String) 145 | case class Recipient(tpe: RecipientType, emailAddress: EmailAddress) 146 | 147 | abstract sealed class Disposition(val value: String) 148 | object Disposition { 149 | case object Inline extends Disposition(Part.INLINE) 150 | case object Attachment extends Disposition(Part.ATTACHMENT) 151 | } 152 | 153 | case class Attachment(name: String, datasource: DataSource, disposition: Disposition) { 154 | def inline = copy(disposition = Disposition.Inline) 155 | } 156 | 157 | case class ByteArrayDataSource(data: Array[Byte], mimeType: String) 158 | extends javax.mail.util.ByteArrayDataSource(data, mimeType) 159 | 160 | object Attachment extends ((String, DataSource, Disposition) => Attachment) { 161 | 162 | def apply(name: String, data: Array[Byte], mimeType: String): Attachment = 163 | apply(name, data, mimeType, Disposition.Attachment) 164 | 165 | def apply(name: String, data: Array[Byte], mimeType: String, disposition: Disposition): Attachment = { 166 | require(mimeType matches ".+/.+", "Invalid MIME type, should contain a /") 167 | 168 | val dataSource = ByteArrayDataSource(data, mimeType) 169 | dataSource setName name 170 | apply(name, dataSource, disposition) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /core/src/main/scala/net/kaliber/mailer/Mailer.scala: -------------------------------------------------------------------------------- 1 | package net.kaliber.mailer 2 | 3 | import javax.mail.Transport 4 | import scala.concurrent.ExecutionContext 5 | import scala.concurrent.Future 6 | import scala.util.Failure 7 | import scala.util.Success 8 | import scala.util.Try 9 | 10 | class Mailer(session: Session) { 11 | 12 | private lazy val mailer = new Mailer.Synchronous(session) 13 | 14 | def sendEmail(email: Email)(implicit ec: ExecutionContext): Future[Unit] = 15 | Future(mailer sendEmail email).flatMap { 16 | case Failure(t) => Future.failed(t) 17 | case Success(u) => Future.successful(u) 18 | } 19 | 20 | def sendEmails(emails: Seq[Email])(implicit ec: ExecutionContext): Future[Seq[Try[Unit]]] = 21 | Future(mailer sendEmails emails).flatMap { 22 | case Failure(t) => Future.failed(t) 23 | case Success(u) => Future.successful(u) 24 | } 25 | } 26 | 27 | object Mailer { 28 | 29 | /** 30 | * The Java mailing utilities are synchronous. In order to keep the code readable 31 | * the synchronous version is a separate class. 32 | */ 33 | private class Synchronous(session: Session) { 34 | 35 | def sendEmail(email: Email): Try[Unit] = 36 | tryWithTransport { implicit transport => 37 | send(email) 38 | } 39 | .flatten 40 | .recoverWith { 41 | case TransportCloseException(result, cause) => 42 | Failure(SendEmailTransportCloseException(result.asInstanceOf[Option[Try[Unit]]], cause)) 43 | 44 | case cause: SendEmailException => 45 | Failure(cause) 46 | 47 | case cause => 48 | Failure(SendEmailException(email, cause)) 49 | } 50 | 51 | def sendEmails(emails: Seq[Email]): Try[Seq[Try[Unit]]] = 52 | tryWithTransport { implicit transport => 53 | emails.map(send) 54 | } 55 | .recoverWith { 56 | case TransportCloseException(results, cause) => 57 | Failure(SendEmailsTransportCloseException(results.asInstanceOf[Option[Seq[Try[Unit]]]], cause)) 58 | 59 | case cause => 60 | Failure(SendEmailsException(emails, cause)) 61 | } 62 | 63 | private def tryWithTransport[T](code: Transport => T): Try[T] = 64 | for { 65 | transport <- Try(session.getTransport) 66 | _ = transport.connect() 67 | // save the try instead of extracting the value to make sure we can close the transport 68 | possibleResult = Try(code(transport)) 69 | _ <- Try(transport.close()) recoverWith createTransportCloseException(possibleResult) 70 | result <- possibleResult 71 | } yield result 72 | 73 | private def createTransportCloseException[T](result: Try[T]): PartialFunction[Throwable, Try[TransportCloseException[T]]] = { 74 | case t: Throwable => Failure(TransportCloseException(result.toOption, t)) 75 | } 76 | 77 | private def send(email: Email)(implicit transport: Transport): Try[Unit] = 78 | Try { 79 | val message = email createFor session 80 | transport.sendMessage(message, message.getAllRecipients) 81 | }.recoverWith { 82 | case cause => Failure(SendEmailException(email, cause)) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /core/src/main/scala/net/kaliber/mailer/exceptions.scala: -------------------------------------------------------------------------------- 1 | package net.kaliber.mailer 2 | 3 | import scala.util.Try 4 | 5 | case class SendEmailException(email: Email, cause: Throwable) extends RuntimeException(cause) 6 | case class SendEmailsException(email: Seq[Email], cause: Throwable) extends RuntimeException(cause) 7 | case class TransportCloseException[T](result:Option[T], cause: Throwable) extends RuntimeException(cause) 8 | case class SendEmailTransportCloseException(result: Option[Try[Unit]], cause: Throwable) extends RuntimeException(cause) 9 | case class SendEmailsTransportCloseException(results: Option[Seq[Try[Unit]]], cause: Throwable) extends RuntimeException(cause) 10 | -------------------------------------------------------------------------------- /core/src/main/scala/net/kaliber/mailer/package.scala: -------------------------------------------------------------------------------- 1 | package net.kaliber 2 | 3 | import java.util.Properties 4 | import javax.mail.{Authenticator, PasswordAuthentication} 5 | 6 | import com.typesafe.config.Config 7 | 8 | package object mailer { 9 | 10 | type DataSource = javax.activation.DataSource 11 | 12 | type RecipientType = javax.mail.Message.RecipientType 13 | object RecipientType { 14 | val TO: RecipientType = javax.mail.Message.RecipientType.TO 15 | val BCC: RecipientType = javax.mail.Message.RecipientType.BCC 16 | val CC: RecipientType = javax.mail.Message.RecipientType.CC 17 | } 18 | 19 | type Session = javax.mail.Session 20 | 21 | case class MailerSettings( 22 | protocol: Option[String], 23 | host: String, 24 | port: String, 25 | failTo: String, 26 | auth: Option[Boolean], 27 | username: Option[String], 28 | password: Option[String] 29 | ) 30 | 31 | object Session { 32 | 33 | def mailerSettings(config: Config): MailerSettings = { 34 | def error(key: String) = throw new RuntimeException(s"Could not find $key in settings") 35 | 36 | def getStringOption(key: String): Option[String] = 37 | if(config.hasPath(key)) Some(config getString key) else None 38 | 39 | def getString(key: String, default: Option[String] = None): String = 40 | getStringOption(key) orElse default getOrElse error(key) 41 | 42 | def getStringWithFallback(key: String, fallback: String): String = getString(key, Some(fallback)) 43 | 44 | MailerSettings( 45 | Some(getStringWithFallback("mail.transport.protocol", fallback = "smtps")), 46 | getString("mail.host"), 47 | getString("mail.port"), 48 | getString("mail.failTo"), 49 | Some(getStringWithFallback("mail.auth", fallback = "true").toBoolean), 50 | getStringOption("mail.username"), 51 | getStringOption("mail.password") 52 | ) 53 | } 54 | 55 | def fromConfig(config: Config): Session = 56 | fromSetting(mailerSettings(config)) 57 | 58 | def fromSetting(setting: MailerSettings): Session = { 59 | val protocol = setting.protocol.getOrElse("smtps") 60 | val auth = setting.auth.getOrElse(true) 61 | 62 | val properties = new Properties() 63 | properties.put(s"mail.transport.protocol", protocol) 64 | properties.put(s"mail.$protocol.quitwait", "false") 65 | properties.put(s"mail.$protocol.host", setting.host) 66 | properties.put(s"mail.$protocol.port", setting.port) 67 | properties.put(s"mail.$protocol.from", setting.failTo) 68 | properties.put(s"mail.$protocol.auth", auth.toString) 69 | 70 | val authenticator = 71 | if (auth) { 72 | 73 | val username = setting.username.getOrElse(throw new RuntimeException("username is expected in te MailerSettings")) 74 | val password = setting.password.getOrElse(throw new RuntimeException("password is expected in te MailerSettings")) 75 | 76 | new Authenticator { 77 | override def getPasswordAuthentication = new PasswordAuthentication(username, password) 78 | } 79 | } else null 80 | 81 | javax.mail.Session.getInstance(properties, authenticator) 82 | } 83 | } 84 | } 85 | 86 | -------------------------------------------------------------------------------- /core/src/test/scala/net/kaliber/mailer/AsyncMailerTests.scala: -------------------------------------------------------------------------------- 1 | package net.kaliber.mailer 2 | 3 | import java.util.Properties 4 | import javax.mail.NoSuchProviderException 5 | 6 | import org.specs2.mutable.Specification 7 | import testUtils.{FullEmail, MailboxUtilities, TestApplication, TestSettings} 8 | 9 | import scala.concurrent.ExecutionContext.Implicits.global 10 | import scala.concurrent.{Await, Future} 11 | import scala.concurrent.duration.Duration 12 | import scala.util.{Failure, Success} 13 | 14 | object AsyncMailerTests extends Specification with TestApplication 15 | with FullEmail with MailboxUtilities { 16 | 17 | "AsyncMailer" should { 18 | "have a method sendMail that" >> { 19 | 20 | "correctly converts a failure to a failed future" in { 21 | 22 | val session = javax.mail.Session.getInstance(new Properties()) 23 | val mailer = new Mailer(session) 24 | 25 | val result = await(mailer.sendEmail(simpleEmail)) 26 | result.value must beLike { 27 | case Some(Failure(SendEmailException(email, t))) => 28 | email === simpleEmail 29 | t must beAnInstanceOf[NoSuchProviderException] 30 | } 31 | } 32 | 33 | "correctly converts a success to a succeeded future" in { 34 | 35 | val result = await(new Mailer(Session.fromSetting(TestSettings.mailerSettings)).sendEmail(simpleEmail)) 36 | result.value === Some(Success(())) 37 | } 38 | } 39 | 40 | "have a method sendMails that" >> { 41 | "correctly converts a failure to a failed future" in { 42 | 43 | val session = javax.mail.Session.getInstance(new Properties()) 44 | val mailer = new Mailer(session) 45 | 46 | val result = await(mailer.sendEmails(simpleEmails)) 47 | result.value must beLike { 48 | case Some(Failure(SendEmailsException(emails, t))) => 49 | emails === simpleEmails 50 | t must beAnInstanceOf[NoSuchProviderException] 51 | } 52 | } 53 | 54 | "correctly converts a success to a succeeded future" in { 55 | 56 | val result = await(new Mailer(Session.fromSetting(TestSettings.mailerSettings)).sendEmails(simpleEmails)) 57 | result.value === Some(Success(Seq(Success(()), Success(())))) 58 | } 59 | } 60 | } 61 | 62 | def await[T](awaitable: Future[T]) = 63 | Await.ready(awaitable, Duration.Inf) 64 | 65 | } -------------------------------------------------------------------------------- /core/src/test/scala/net/kaliber/mailer/EmailTests.scala: -------------------------------------------------------------------------------- 1 | package net.kaliber.mailer 2 | 3 | import org.specs2.mutable.Specification 4 | import testUtils.{FullEmail, FullMessageTest, TestApplication, TestSettings} 5 | 6 | object EmailTests extends Specification with TestApplication 7 | with FullEmail with FullMessageTest { 8 | 9 | "Attachment" should { 10 | 11 | val name = "testName" 12 | val mimeType = "test/mimeType" 13 | val byteArray = Array[Byte](1, 2) 14 | 15 | "have an alternative apply method for Array[Byte]" in { 16 | 17 | val attachment = Attachment(name, byteArray, mimeType) 18 | 19 | val inputStream = attachment.datasource.getInputStream 20 | val bytes = getByteArray(inputStream) 21 | 22 | attachment.name === (name) 23 | attachment.disposition === Disposition.Attachment 24 | attachment.datasource.getContentType === mimeType 25 | bytes === byteArray 26 | } 27 | 28 | "have an alternative apply method for Array[Byte] and custom disposition" in { 29 | 30 | val attachment = Attachment(name, byteArray, mimeType, Disposition.Inline) 31 | attachment.disposition === Disposition.Inline 32 | } 33 | 34 | "should throw an error if the mimetype does not contain a /" in { 35 | Attachment("", Array.empty[Byte], "mime") must throwAn[IllegalArgumentException] 36 | Attachment("", Array.empty[Byte], "/mime") must throwAn[IllegalArgumentException] 37 | Attachment("", Array.empty[Byte], "mime/") must throwAn[IllegalArgumentException] 38 | } 39 | 40 | "should have a convenience method for inline attachments" in { 41 | Attachment(name, byteArray, mimeType).inline === Attachment(name, byteArray, mimeType, Disposition.Inline) 42 | } 43 | } 44 | 45 | "Email" should { 46 | 47 | "have utility methods to easily create an htmlEmail" in { 48 | import fullEmailProperties._ 49 | val email = 50 | Email(subject, EmailAddress(fromName, fromAddress), textContent) 51 | .withHtmlText(htmlTextContent) 52 | .to(toName, toAddress) 53 | .cc(ccName, ccAddress) 54 | .bcc(bccName, bccAddress) 55 | .replyTo(replyToName, replyToAddress) 56 | .withAttachments( 57 | Attachment(attachmentName, attachmentData, attachmentMimeType), 58 | Attachment( 59 | inlineAttachmentName, 60 | inlineAttachmentData, 61 | inlineAttachmentMimeType).inline) 62 | 63 | email === fullEmail 64 | } 65 | 66 | "have utility methods to easily create an textEmail" in { 67 | import fullEmailProperties._ 68 | 69 | val email = 70 | Email(subject, EmailAddress(fromName, fromAddress), textContent, None) 71 | .to(toName, toAddress) 72 | .cc(ccName, ccAddress) 73 | .bcc(bccName, bccAddress) 74 | .replyTo(replyToName, replyToAddress) 75 | .withAttachments( 76 | Attachment(attachmentName, attachmentData, attachmentMimeType), 77 | Attachment( 78 | inlineAttachmentName, 79 | inlineAttachmentData, 80 | inlineAttachmentMimeType).inline) 81 | 82 | email === textEmail 83 | } 84 | 85 | "create a javax.mail.Message with the correct parts" in { 86 | val session = Session.fromSetting(TestSettings.mailerSettings) 87 | 88 | val fullMessage = fullEmail createFor session 89 | fullMessageTest(fullMessage) 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /core/src/test/scala/net/kaliber/mailer/MailerTests.scala: -------------------------------------------------------------------------------- 1 | package net.kaliber.mailer 2 | 3 | import java.util.Properties 4 | 5 | import scala.util.Failure 6 | import scala.util.Success 7 | import org.specs2.mutable.Specification 8 | import javax.mail.Address 9 | import javax.mail.Message 10 | import javax.mail.MessagingException 11 | import javax.mail.NoSuchProviderException 12 | import javax.mail.Provider 13 | import javax.mail.Provider.Type 14 | import javax.mail.Transport 15 | import javax.mail.URLName 16 | 17 | import scala.concurrent.Await 18 | import scala.concurrent.Future 19 | import scala.concurrent.duration._ 20 | import scala.concurrent.ExecutionContext.Implicits.global 21 | import scala.util.Try 22 | import testUtils._ 23 | 24 | object MailerTests extends Specification with TestApplication 25 | with FullEmail with FullMessageTest with MailboxUtilities { 26 | sequential 27 | 28 | "Mailer" should { 29 | 30 | import fullEmailProperties._ 31 | 32 | val providerFailure = "fails correctly if no provider can be found" 33 | val connectionFailure = "fails correctly if the connection fails" 34 | val messageFailure = "fails correctly if sending the message fails" 35 | val closeFailure = "fails correctly if closing the transport fails" 36 | def simulatedErrorMessage(name: String, address: String) = s"Simulated error sending message to $name <$address>" 37 | 38 | "have a method sendEmail that" >> { 39 | 40 | providerFailure in { 41 | 42 | val session = javax.mail.Session.getInstance(new Properties()) 43 | val result = sendMail(session) 44 | 45 | result must beLike { 46 | case Failure(SendEmailException(email, t)) => 47 | email === simpleEmail 48 | t must beAnInstanceOf[NoSuchProviderException] 49 | } 50 | } 51 | 52 | connectionFailure in { 53 | 54 | val result = sendMail(session(classOf[FaultyConnectionTransport])) 55 | result must beLike { 56 | case Failure(SendEmailException(email, t)) => 57 | email === simpleEmail 58 | messagingExceptionWithMessage(t, "connectionFailed") 59 | } 60 | 61 | } 62 | 63 | messageFailure in { 64 | 65 | val result = sendMail(session(classOf[FaultyMessageTransport])) 66 | result must beLike { 67 | case Failure(SendEmailException(email, t)) => 68 | email === simpleEmail 69 | messagingExceptionWithMessage(t, "sendMessageFailed") 70 | } 71 | } 72 | 73 | closeFailure in { 74 | 75 | val result = sendMail(session(classOf[FaultyCloseTransport])) 76 | result must beLike { 77 | case Failure(SendEmailTransportCloseException(result, t)) => 78 | result === Some(Success(())) 79 | messagingExceptionWithMessage(t, "closeFailed") 80 | } 81 | } 82 | 83 | "fails if the mailbox responds in failure" in { 84 | 85 | withFaultyMailbox { mailbox => 86 | 87 | val result = sendMail() 88 | 89 | result must beLike { 90 | case Failure(SendEmailException(email, t)) => 91 | email === simpleEmail 92 | messagingExceptionWithMessage(t, simulatedErrorMessage(toName, toAddress)) 93 | } 94 | } 95 | } 96 | 97 | "correctly sends a full email" in { 98 | 99 | withDefaultMailbox { mailbox => 100 | 101 | val result = sendMail(email = fullEmail) 102 | 103 | result === Success(()) 104 | mailbox.size === 1 105 | fullMessageTest(mailbox.get(0)) 106 | } 107 | } 108 | } 109 | 110 | "have a method sendEmails that" >> { 111 | 112 | providerFailure in { 113 | 114 | val session = javax.mail.Session.getInstance(new Properties()) 115 | val result = sendMails(session) 116 | 117 | result must beLike { 118 | case Failure(SendEmailsException(emails, t)) => 119 | emails === simpleEmails 120 | t must beAnInstanceOf[NoSuchProviderException] 121 | } 122 | } 123 | 124 | connectionFailure in { 125 | 126 | val result = sendMails(session(classOf[FaultyConnectionTransport])) 127 | result must beLike { 128 | case Failure(SendEmailsException(emails, t)) => 129 | emails === simpleEmails 130 | messagingExceptionWithMessage(t, "connectionFailed") 131 | } 132 | 133 | } 134 | 135 | messageFailure in { 136 | 137 | val result = sendMails(session(classOf[FaultyMessageTransport])) 138 | result must beLike { 139 | case Success(Seq( 140 | Failure(SendEmailException(email1, t1)), 141 | Failure(SendEmailException(email2, t2)))) => 142 | email1 === simpleEmail 143 | email2 === simpleFailEmail 144 | messagingExceptionWithMessage(t1, "sendMessageFailed") 145 | messagingExceptionWithMessage(t2, "sendMessageFailed") 146 | } 147 | } 148 | 149 | closeFailure in { 150 | 151 | val result = sendMails(session(classOf[FaultyCloseTransport])) 152 | result must beLike { 153 | case Failure(SendEmailsTransportCloseException(results, t)) => 154 | results === Some(Seq(Success(()), Success(()))) 155 | messagingExceptionWithMessage(t, "closeFailed") 156 | } 157 | } 158 | 159 | "partially fails if the mailbox responds in failure for one of the emails" in { 160 | 161 | withMailboxes(toAddress, failAddress) { mailboxes => 162 | 163 | mailboxes.last.setError(true) 164 | 165 | val result = sendMails() 166 | 167 | result must beLike { 168 | case Success(Seq(Success(_), Failure(SendEmailException(email, t)))) => 169 | email === simpleFailEmail 170 | messagingExceptionWithMessage(t, simulatedErrorMessage(failName, failAddress)) 171 | } 172 | 173 | mailboxes.head.size === 1 174 | } 175 | } 176 | 177 | "correctly sends 2 emails" in { 178 | 179 | withDefaultMailbox { mailbox => 180 | 181 | val result = sendMails(emails = Seq(simpleEmail, simpleEmail)) 182 | 183 | result === Success(Seq(Success(()), Success(()))) 184 | mailbox.size === 2 185 | } 186 | } 187 | } 188 | } 189 | 190 | lazy val defaultSession = Session.fromSetting(TestSettings.mailerSettings) 191 | 192 | def sendMail(session: Session = defaultSession, email: Email = simpleEmail) = { 193 | val mailer = new Mailer(session) 194 | futureToTry(mailer.sendEmail(email)) 195 | } 196 | 197 | def sendMails(session: Session = defaultSession, emails: Seq[Email] = simpleEmails) = { 198 | val mailer = new Mailer(session) 199 | futureToTry(mailer.sendEmails(emails)) 200 | } 201 | 202 | def futureToTry[T](future: Future[T]): Try[T] = 203 | Await.ready(future, 1.second).value.get 204 | 205 | def messagingExceptionWithMessage(t: Throwable, message: String) = { 206 | t must beAnInstanceOf[MessagingException] 207 | t.getMessage === message 208 | } 209 | 210 | def session[T](tpe: Class[T]) = { 211 | val provider = 212 | new Provider(Type.TRANSPORT, "testProtocol", tpe.getName, "Test", "0.1-SNAPSHOT") 213 | 214 | val properties = new Properties() 215 | properties.put("mail.transport.protocol", "testProtocol") 216 | 217 | val session = javax.mail.Session.getInstance(properties) 218 | session.addProvider(provider) 219 | session 220 | } 221 | 222 | class FaultyConnectionTransport(session: Session, urlname: URLName) extends Transport(session, urlname) { 223 | 224 | def sendMessage(msg: Message, addresses: Array[Address]) = 225 | ??? 226 | 227 | override protected def protocolConnect(host: String, port: Int, user: String, password: String): Boolean = 228 | throw new MessagingException("connectionFailed") 229 | } 230 | 231 | class FaultyMessageTransport(session: Session, urlname: URLName) extends Transport(session, urlname) { 232 | 233 | def sendMessage(msg: Message, addresses: Array[Address]) = 234 | throw new MessagingException("sendMessageFailed") 235 | 236 | override protected def protocolConnect(host: String, port: Int, user: String, password: String): Boolean = 237 | true 238 | } 239 | 240 | class FaultyCloseTransport(session: Session, urlname: URLName) extends Transport(session, urlname) { 241 | 242 | def sendMessage(msg: Message, addresses: Array[Address]) = 243 | {} 244 | 245 | override protected def protocolConnect(host: String, port: Int, user: String, password: String): Boolean = 246 | true 247 | 248 | override def close() = 249 | throw new MessagingException("closeFailed") 250 | } 251 | } -------------------------------------------------------------------------------- /core/src/test/scala/net/kaliber/mailer/SessionTests.scala: -------------------------------------------------------------------------------- 1 | package net.kaliber.mailer 2 | 3 | import org.specs2.mutable.Specification 4 | import testUtils.TestSettings.{mailerSettings, _} 5 | 6 | object SessionTests extends Specification { 7 | 8 | "Session" should { 9 | 10 | "have have the correct default protocol" in { 11 | val settings = MailerSettings(None, "", "", "", Some(false), None, None) 12 | 13 | val session = Session.fromSetting(settings) 14 | 15 | val properties = session.getProperties 16 | 17 | def p = properties.getProperty(_: String) 18 | 19 | p("mail.transport.protocol") === "smtps" 20 | } 21 | 22 | "have have the correct default value for auth" in { 23 | val settings = MailerSettings(Some("protocol"), "", "", "", None, Some("for"), Some("bar")) 24 | 25 | val session = Session.fromSetting(settings) 26 | 27 | val properties = session.getProperties 28 | 29 | def p = properties.getProperty(_: String) 30 | 31 | p("mail.protocol.auth") === "true" 32 | } 33 | 34 | "extract the correct information from the mailerSettings" in { 35 | val expectedMailerSettings = mailerSettingsWithAuth.copy(protocol = Some("protocol")) 36 | 37 | val session = Session.fromSetting(expectedMailerSettings) 38 | 39 | val properties = session.getProperties 40 | 41 | def p = properties.getProperty(_: String) 42 | 43 | p("mail.transport.protocol") === expectedMailerSettings.protocol.get 44 | p("mail.protocol.quitwait") === "false" 45 | p("mail.protocol.host") === expectedMailerSettings.host 46 | p("mail.protocol.port") === expectedMailerSettings.port 47 | p("mail.protocol.from") === expectedMailerSettings.failTo 48 | p("mail.protocol.auth") === expectedMailerSettings.auth.get.toString 49 | 50 | val authentication = session.requestPasswordAuthentication(null, 0, null, null, null) 51 | authentication.getUserName === expectedMailerSettings.username.get 52 | authentication.getPassword === expectedMailerSettings.password.get 53 | } 54 | 55 | "allow for a configuration that disables authentication" in { 56 | val session = Session.fromSetting(mailerSettings.copy(protocol = Some("protocol"))) 57 | 58 | val properties = session.getProperties 59 | 60 | def p = properties.getProperty(_: String) 61 | 62 | p("mail.protocol.auth") === "false" 63 | 64 | val authentication = session.requestPasswordAuthentication(null, 0, null, null, null) 65 | authentication === null 66 | } 67 | 68 | } 69 | } 70 | 71 | -------------------------------------------------------------------------------- /core/src/test/scala/testUtils/FullEmail.scala: -------------------------------------------------------------------------------- 1 | package testUtils 2 | 3 | import net.kaliber.mailer.Disposition 4 | import net.kaliber.mailer.Email 5 | import net.kaliber.mailer.EmailAddress 6 | import net.kaliber.mailer.Recipient 7 | import net.kaliber.mailer.RecipientType 8 | import net.kaliber.mailer.Attachment 9 | 10 | trait FullEmail { 11 | 12 | object fullEmailProperties { 13 | val subject = "subject" 14 | val fromName = "from" 15 | val fromAddress = "from@domain" 16 | val replyToName = "replyTo" 17 | val replyToAddress = "replyTo@domain" 18 | val toName = "to" 19 | val toAddress = "to@domain" 20 | val ccName = "cc" 21 | val ccAddress = "cc@domain" 22 | val bccName = "bcc" 23 | val bccAddress = "bcc@domain" 24 | val textContent = "text" 25 | val htmlTextContent = "htmlText" 26 | val attachmentName = "attachment" 27 | val attachmentData = Array[Byte](1, 2) 28 | val attachmentMimeType = "attachment/type" 29 | val inlineAttachmentName = "inline" 30 | val inlineAttachmentData = Array[Byte](3, 4) 31 | val inlineAttachmentMimeType = "inline/type" 32 | } 33 | 34 | val fullEmail = { 35 | import fullEmailProperties._ 36 | 37 | Email(subject, 38 | EmailAddress(fromName, fromAddress), 39 | textContent, 40 | htmlTextContent, 41 | replyTo = Some(EmailAddress(replyToName, replyToAddress)), 42 | recipients = Seq( 43 | Recipient( 44 | tpe = RecipientType.TO, 45 | emailAddress = EmailAddress(toName, toAddress)), 46 | Recipient( 47 | tpe = RecipientType.CC, 48 | emailAddress = EmailAddress(ccName, ccAddress)), 49 | Recipient( 50 | tpe = RecipientType.BCC, 51 | emailAddress = EmailAddress(bccName, bccAddress))), 52 | attachments = Seq( 53 | Attachment(attachmentName, attachmentData, attachmentMimeType), 54 | Attachment( 55 | inlineAttachmentName, inlineAttachmentData, inlineAttachmentMimeType, 56 | Disposition.Inline))) 57 | } 58 | 59 | val textEmail = { 60 | import fullEmailProperties._ 61 | 62 | Email(subject, 63 | EmailAddress(fromName, fromAddress), 64 | textContent, 65 | None, 66 | replyTo = Some(EmailAddress(replyToName, replyToAddress)), 67 | recipients = Seq( 68 | Recipient( 69 | tpe = RecipientType.TO, 70 | emailAddress = EmailAddress(toName, toAddress)), 71 | Recipient( 72 | tpe = RecipientType.CC, 73 | emailAddress = EmailAddress(ccName, ccAddress)), 74 | Recipient( 75 | tpe = RecipientType.BCC, 76 | emailAddress = EmailAddress(bccName, bccAddress))), 77 | attachments = Seq( 78 | Attachment(attachmentName, attachmentData, attachmentMimeType), 79 | Attachment( 80 | inlineAttachmentName, inlineAttachmentData, inlineAttachmentMimeType, 81 | Disposition.Inline))) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /core/src/test/scala/testUtils/FullMessage.scala: -------------------------------------------------------------------------------- 1 | package testUtils 2 | 3 | import java.io.InputStream 4 | import javax.mail.Message 5 | import javax.mail.internet.MimeBodyPart 6 | import javax.mail.internet.MimeMultipart 7 | import javax.mail.Address 8 | import javax.mail.Message.RecipientType 9 | import org.specs2.mutable.Specification 10 | import scala.Array.canBuildFrom 11 | 12 | trait FullMessageTest { self: Specification with FullEmail => 13 | 14 | def fullMessageTest(message: Message) = { 15 | 16 | /* 17 | Message 18 | | 19 | |_ MimeMultipart - multipart/mixed 20 | | 21 | | 22 | |_ MimeBodyPart 23 | | | 24 | | |_ MimeMultipart - multipart/related 25 | | | 26 | | |_ MimeBodyPart 27 | | | | 28 | | | |_ MimeMultipart - multipart/alternative 29 | | | | 30 | | | |_ MimeBodyPart - text/plain 31 | | | |_ MimeBodyPart - text/html 32 | | | 33 | | |_ MimeBodyPart - inline/type 34 | | 35 | |_ MimeBodyPart - attachment/type 36 | */ 37 | 38 | import fullEmailProperties._ 39 | 40 | message must beAnInstanceOf[javax.mail.Message] 41 | message.getSubject === subject 42 | recipients(message.getFrom) === Array(s"$fromName <$fromAddress>") 43 | recipients(message.getReplyTo) === Array(s"$replyToName <$replyToAddress>") 44 | recipients(message, RecipientType.TO) === Array(s"$toName <$toAddress>") 45 | recipients(message, RecipientType.CC) === Array(s"$ccName <$ccAddress>") 46 | recipients(message, RecipientType.BCC) === Array(s"$bccName <$bccAddress>") 47 | 48 | message.getContent must beAnInstanceOf[MimeMultipart] 49 | val root = message.getContent.asInstanceOf[MimeMultipart] 50 | root.getContentType must startWith("multipart/mixed;") 51 | root.getCount === 2 52 | 53 | root.getBodyPart(0) must beAnInstanceOf[MimeBodyPart] 54 | val relatedPart = root.getBodyPart(0).asInstanceOf[MimeBodyPart] 55 | 56 | relatedPart.getContent must beAnInstanceOf[MimeMultipart] 57 | val related = relatedPart.getContent.asInstanceOf[MimeMultipart] 58 | related.getContentType must startWith("multipart/related;") 59 | related.getCount === 2 60 | 61 | related.getBodyPart(0) must beAnInstanceOf[MimeBodyPart] 62 | val alternativePart = related.getBodyPart(0).asInstanceOf[MimeBodyPart] 63 | 64 | alternativePart.getContent must beAnInstanceOf[MimeMultipart] 65 | val alternative = alternativePart.getContent.asInstanceOf[MimeMultipart] 66 | alternative.getContentType must startWith("multipart/alternative;") 67 | alternative.getCount === 2 68 | 69 | alternative.getBodyPart(0) must beAnInstanceOf[MimeBodyPart] 70 | val textPart = alternative.getBodyPart(0).asInstanceOf[MimeBodyPart] 71 | textPart.getContentType === "text/plain; charset=UTF-8" 72 | val textContent = textPart.getContent 73 | textContent must beAnInstanceOf[String] 74 | textContent === textContent 75 | 76 | alternative.getBodyPart(1) must beAnInstanceOf[MimeBodyPart] 77 | val htmlPart = alternative.getBodyPart(1).asInstanceOf[MimeBodyPart] 78 | htmlPart.getContentType === "text/html; charset=UTF-8" 79 | val htmlContent = htmlPart.getContent 80 | htmlContent must beAnInstanceOf[String] 81 | htmlContent === htmlTextContent 82 | 83 | related.getBodyPart(1) must beAnInstanceOf[MimeBodyPart] 84 | val inlineAttachment = related.getBodyPart(1).asInstanceOf[MimeBodyPart] 85 | inlineAttachment.getContentID === s"<$inlineAttachmentName>" 86 | inlineAttachment.getContentType === s"$inlineAttachmentMimeType; filename=$inlineAttachmentName; name=$inlineAttachmentName" 87 | inlineAttachment.getDisposition must startWith("inline") 88 | val inlineAttachmentBytes = getByteArray(inlineAttachment.getDataHandler.getInputStream) 89 | inlineAttachmentBytes === inlineAttachmentData 90 | 91 | root.getBodyPart(1) must beAnInstanceOf[MimeBodyPart] 92 | val attachment = root.getBodyPart(1).asInstanceOf[MimeBodyPart] 93 | attachment.getContentID === s"<$attachmentName>" 94 | attachment.getContentType === s"$attachmentMimeType; filename=$attachmentName; name=$attachmentName" 95 | attachment.getDisposition must startWith("attachment") 96 | val attachmentBytes = getByteArray(attachment.getDataHandler.getInputStream) 97 | attachmentBytes === attachmentData 98 | 99 | } 100 | 101 | def getByteArray(inputStream: InputStream) = 102 | Stream 103 | .continually(inputStream.read) 104 | .takeWhile(_ != -1) 105 | .map(_.toByte) 106 | .toArray 107 | 108 | def recipients(r: Array[Address]): Array[String] = 109 | r.map(_.toString) 110 | 111 | def recipients(message: Message, tpe: RecipientType): Array[String] = 112 | recipients(message.getRecipients(tpe)) 113 | 114 | } 115 | -------------------------------------------------------------------------------- /core/src/test/scala/testUtils/MailboxUtilities.scala: -------------------------------------------------------------------------------- 1 | package testUtils 2 | 3 | import org.jvnet.mock_javamail.Mailbox 4 | 5 | import net.kaliber.mailer.Email 6 | import net.kaliber.mailer.EmailAddress 7 | 8 | trait MailboxUtilities { self: FullEmail => 9 | 10 | val simpleEmail = { 11 | import fullEmailProperties._ 12 | 13 | Email( 14 | subject, 15 | EmailAddress(fromName, fromAddress), 16 | textContent) 17 | .withHtmlText(htmlTextContent) 18 | .to(toName, toAddress) 19 | } 20 | 21 | val failName = "fail" 22 | val failAddress = "fail@domain" 23 | 24 | val simpleFailEmail = 25 | simpleEmail 26 | .copy(recipients = Seq.empty) 27 | .to(failName, failAddress) 28 | 29 | val simpleEmails = 30 | Seq(simpleEmail, simpleFailEmail) 31 | 32 | def withMailboxes[T](addresses: String*)(code: Seq[Mailbox] => T):T = { 33 | val mailboxes = addresses.map(Mailbox.get) 34 | val result = code(mailboxes) 35 | mailboxes.foreach(_.setError(false)) 36 | mailboxes.foreach(_.clear()) 37 | result 38 | } 39 | 40 | def withDefaultMailbox[T](code: Mailbox => T):T = 41 | withMailboxes(fullEmailProperties.toAddress) { mailboxes => 42 | code(mailboxes.head) 43 | } 44 | 45 | def withFaultyMailbox[T](code: Mailbox => T):T = 46 | withDefaultMailbox { mailbox => 47 | mailbox.setError(true) 48 | code(mailbox) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /core/src/test/scala/testUtils/TestSettings.scala: -------------------------------------------------------------------------------- 1 | package testUtils 2 | 3 | import net.kaliber.mailer.{MailerSettings, Session} 4 | import org.jvnet.mock_javamail.Mailbox 5 | import org.specs2.mutable.After 6 | 7 | object TestSettings { 8 | 9 | lazy val mailerSettings = MailerSettings( 10 | Some("smtp"), 11 | "localhost", 12 | "10000", 13 | "toto@localhost", 14 | Some(false), 15 | None, 16 | None 17 | ) 18 | 19 | lazy val defaultSession = Session.fromSetting(TestSettings.mailerSettings) 20 | 21 | lazy val mailerSettingsWithAuth = MailerSettings( 22 | Some("smtp"), 23 | "localhost", 24 | "10000", 25 | "toto@localhost", 26 | Some(true), 27 | Some("foo"), 28 | Some("bar") 29 | ) 30 | } 31 | 32 | trait TestApplication extends After { 33 | def after = { 34 | Mailbox.clearAll() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /play/src/main/scala/net/kaliber/play/mailer/package.scala: -------------------------------------------------------------------------------- 1 | package net.kaliber.play 2 | 3 | import net.kaliber.mailer.Session 4 | import play.api.{Application, Configuration} 5 | 6 | package object mailer { 7 | 8 | object Session { 9 | def fromApplication(implicit app: Application): Session = fromConfiguration(app.configuration) 10 | 11 | def fromConfiguration(configuration: Configuration): Session = 12 | net.kaliber.mailer.Session.fromConfig(configuration.underlying) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /play/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ${application.home:-.}/logs/application.log 7 | 8 | %date [%level] from %logger in %thread - %message%n%xException 9 | 10 | 11 | 12 | 13 | 14 | logger{15} - %message%n%xException{10} 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /play/src/test/scala/net/kaliber/play/mailer/MailerTests.scala: -------------------------------------------------------------------------------- 1 | package net.kaliber.play.mailer 2 | 3 | import net.kaliber.mailer.{Mailer, MailerSettings} 4 | import org.specs2.mutable.Specification 5 | import play.api.Configuration 6 | import play.api.test.{FakeApplication, WithApplication} 7 | import testUtils.{FullEmail, MailboxUtilities} 8 | 9 | import scala.concurrent.duration.Duration 10 | import scala.concurrent.{Await, Future} 11 | import scala.util.Success 12 | 13 | object MailerTests extends Specification with TestApplication with FullEmail with MailboxUtilities { 14 | 15 | implicit def ec = play.api.libs.concurrent.Execution.Implicits.defaultContext 16 | 17 | lazy val configuration = Configuration from configurationValues 18 | 19 | "AsyncMailer" should { 20 | 21 | "have a settingFromConfiguration that" >> { 22 | "correctly converts play Configuration to MailerSettings" in { 23 | val convertedMailerSettings = net.kaliber.mailer.Session.mailerSettings(configuration.underlying) 24 | 25 | val expectedMailerSettings = MailerSettings( 26 | Some("smtp"), 27 | "localhost", 28 | "10000", 29 | "toto@localhost", 30 | Some(true), 31 | Some("foo"), 32 | Some("bar") 33 | ) 34 | 35 | expectedMailerSettings === convertedMailerSettings 36 | } 37 | } 38 | 39 | "have a fromApplication that" >> { 40 | "return a Session that can be used with the Mailer" in new TestApp { 41 | 42 | val result = await(new Mailer(Session.fromApplication).sendEmails(simpleEmails)) 43 | result.value === Some(Success(Seq(Success(()), Success(())))) 44 | } 45 | } 46 | 47 | "have a fromConfiguration that" >> { 48 | "return a Session that can be used with the Mailer" in new TestApp { 49 | 50 | val result = await(new Mailer(Session.fromConfiguration(configuration)).sendEmails(simpleEmails)) 51 | result.value === Some(Success(Seq(Success(()), Success(())))) 52 | } 53 | } 54 | } 55 | 56 | def await[T](awaitable: Future[T]) = 57 | Await.ready(awaitable, Duration.Inf) 58 | } 59 | 60 | trait TestApplication extends testUtils.TestApplication { 61 | 62 | lazy val configurationValues = Map( 63 | "mail.transport.protocol" -> "smtp", 64 | "mail.host" -> "localhost", 65 | "mail.port" -> "10000", 66 | "mail.failTo" -> "toto@localhost", 67 | "mail.username" -> "foo", 68 | "mail.password" -> "bar") 69 | 70 | private def f = FakeApplication(additionalConfiguration = configurationValues) 71 | 72 | abstract class TestApp extends WithApplication(f) 73 | } 74 | -------------------------------------------------------------------------------- /project/bintray.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("me.lessis" % "bintray-sbt" % "0.3.0") 2 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.12 2 | -------------------------------------------------------------------------------- /project/pgp.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0") -------------------------------------------------------------------------------- /project/release.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.3") 2 | -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | version in ThisBuild := "6.0.2-SNAPSHOT" 2 | --------------------------------------------------------------------------------