├── .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 |
--------------------------------------------------------------------------------