├── .envrc ├── project ├── build.properties ├── plugins.sbt └── ScalacOptions.scala ├── notes ├── about.markdown ├── 0.1.1.markdown ├── 0.1.3.markdown ├── 0.1.4.markdown ├── 0.1.2.markdown └── 0.1.0.markdown ├── courier └── src │ ├── main │ ├── scala-2.11 │ │ └── compat.scala │ ├── scala-2.12 │ │ └── compat.scala │ ├── scala-3 │ │ └── compat.scala │ ├── scala-2.13 │ │ └── compat.scala │ └── scala │ │ ├── package.scala │ │ ├── defaults.scala │ │ ├── signer.scala │ │ ├── envelope.scala │ │ ├── mailer.scala │ │ ├── content.scala │ │ └── sessions.scala │ └── test │ └── scala │ └── mailspec.scala ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── integration-tests └── src │ └── test │ └── scala │ └── mailspec.scala ├── LICENSE └── README.md /.envrc: -------------------------------------------------------------------------------- 1 | dotenv 2 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.5.8 2 | -------------------------------------------------------------------------------- /notes/about.markdown: -------------------------------------------------------------------------------- 1 | [courier](https://github.com/softprops/courier#readme) delivers electronic mail in scala 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.1.2") 2 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.3") 3 | addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.4") -------------------------------------------------------------------------------- /courier/src/main/scala-2.11/compat.scala: -------------------------------------------------------------------------------- 1 | package courier 2 | 3 | object Compat { 4 | def asJava[T](set: Set[T]): java.util.Set[T] = { 5 | import collection.JavaConverters._ 6 | set.asJava 7 | } 8 | } -------------------------------------------------------------------------------- /courier/src/main/scala-2.12/compat.scala: -------------------------------------------------------------------------------- 1 | package courier 2 | 3 | object Compat { 4 | def asJava[T](set: Set[T]): java.util.Set[T] = { 5 | import collection.JavaConverters._ 6 | set.asJava 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /courier/src/main/scala-3/compat.scala: -------------------------------------------------------------------------------- 1 | package courier 2 | 3 | object Compat { 4 | def asJava[T](set: Set[T]): java.util.Set[T] = { 5 | import scala.jdk.CollectionConverters._ 6 | set.asJava 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /courier/src/main/scala-2.13/compat.scala: -------------------------------------------------------------------------------- 1 | package courier 2 | 3 | object Compat { 4 | def asJava[T](set: Set[T]): java.util.Set[T] = { 5 | import scala.jdk.CollectionConverters._ 6 | set.asJava 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /notes/0.1.1.markdown: -------------------------------------------------------------------------------- 1 | ## fixes 2 | 3 | * Actually set the `cc` and `bcc` of messages ( cause everyone's gotta know ) 4 | * Not setting a default envelope body was a poor choice. A default empty body is now provided with all envelopes. 5 | -------------------------------------------------------------------------------- /notes/0.1.3.markdown: -------------------------------------------------------------------------------- 1 | # fixes 2 | 3 | * [#8](https://github.com/softprops/courier/pull/8) SMTP headers were not being send [mariusmuja][https://github.com/mariusmuja] 4 | 5 | # enhancements 6 | 7 | * [#7](https://github.com/softprops/courier/pull/7) added documentation on testing [mcamou](https://github.com/mcamou) 8 | 9 | # misc 10 | 11 | * dropped scala 2.9.3 support, added scala 2.11 support 12 | -------------------------------------------------------------------------------- /courier/src/main/scala/package.scala: -------------------------------------------------------------------------------- 1 | import javax.mail.internet.InternetAddress 2 | 3 | /** An agreeable email interface for scala. */ 4 | package object courier { 5 | 6 | implicit class addr(name: String){ 7 | def `@`(domain: String): InternetAddress = new InternetAddress(s"$name@$domain") 8 | def at = `@` _ 9 | /** In case whole string is email address already */ 10 | def addr = new InternetAddress(name) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /courier/src/main/scala/defaults.scala: -------------------------------------------------------------------------------- 1 | package courier 2 | 3 | import javax.mail.{Session => MailSession} 4 | import java.util.Properties 5 | import javax.mail.internet.MimeMessage 6 | import scala.concurrent.ExecutionContext 7 | 8 | object Defaults { 9 | val session = MailSession.getDefaultInstance(new Properties()) 10 | 11 | val mimeMessageFactory: MailSession => MimeMessage = new MimeMessage(_) 12 | 13 | implicit val executionContext: ExecutionContext = ExecutionContext.Implicits.global 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Scala CI 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Set up JDK 11 20 | uses: actions/setup-java@v3 21 | with: 22 | java-version: '11' 23 | distribution: 'temurin' 24 | cache: 'sbt' 25 | - name: Run tests 26 | run: sbt ci 27 | -------------------------------------------------------------------------------- /notes/0.1.4.markdown: -------------------------------------------------------------------------------- 1 | # fixes 2 | 3 | * [#17](https://github.com/softprops/courier/pull/17) The reply-to address was not being sent - [laurencer](https://github.com/laurencer) 4 | * [#13](https://github.com/softprops/courier/pull/13) Allow to set charset for subject - [tzeman77](https://github.com/tzeman77) 5 | 6 | # enhancements 7 | * [#19](https://github.com/softprops/courier/pull/19) Add Scala 2.12 support - [rleibman](https://github.com/rleibman) 8 | 9 | # misc 10 | * [#18](https://github.com/softprops/courier/pull/18) Use implicit class [ChristopherDavenport](https://github.com/ChristopherDavenport) 11 | -------------------------------------------------------------------------------- /notes/0.1.2.markdown: -------------------------------------------------------------------------------- 1 | ## additions 2 | 3 | * Includes a patch from [@rowlandwatkins](https://github.com/rowlandwatkins) for adding an interface for attachments as bytes. 4 | 5 | Below is an example 6 | 7 | mailer(Envelope.from("you" `@` "work.com") 8 | .to("boss" `@` "work.com") 9 | .subject("tps report") 10 | .content(Multipart() 11 | .attachBytes("ASCII TPS Template".getBytes(), "ascii-tps-template.txt", "text/plain") 12 | .html("

IT'S IMPORTANT

"))) 13 | .onSuccess { 14 | case _ => println("delivered report") 15 | } 16 | 17 | Published for scala 2.9.3 _and_ 2.10 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *# 2 | *.iml 3 | *.ipr 4 | *.iws 5 | *.pyc 6 | *.tm.epoch 7 | *.vim 8 | */project/boot 9 | */project/build/target 10 | */project/project.target.config-classes 11 | *-shim.sbt 12 | *~ 13 | .#* 14 | .*.swp 15 | .DS_Store 16 | .cache 17 | .cache 18 | .classpath 19 | .codefellow 20 | .ensime* 21 | .eprj 22 | .history 23 | .idea 24 | .manager 25 | .multi-jvm 26 | .project 27 | .scala_dependencies 28 | .scalastyle 29 | .settings 30 | .tags 31 | .tags_sorted_by_file 32 | .target 33 | .worksheet 34 | Makefile 35 | TAGS 36 | lib_managed 37 | logs 38 | project/boot/* 39 | project/plugins/project 40 | src_managed 41 | target 42 | tm*.lck 43 | tm*.log 44 | tm.out 45 | worker*.log 46 | /bin 47 | .creds.sh 48 | project/.gnupg/ 49 | .bloop/ 50 | .metals/ 51 | .bsp/ 52 | .env 53 | -------------------------------------------------------------------------------- /notes/0.1.0.markdown: -------------------------------------------------------------------------------- 1 | ## initial release 2 | 3 | Courier is meant to simplify the ease in which you can use to tools built into the jdk to send electronic mail in scala using [Futures](http://www.scala-lang.org/api/current/index.html#scala.concurrent.Future). 4 | 5 | To send a message to your mom 6 | 7 | import courier._, Defaults._ 8 | val mailer = Mailer("smtp.gmail.com", 587) 9 | .auth(true) 10 | .as("you@gmail.com", "p@$$w3rd") 11 | .startTtls(true)() 12 | 13 | mailer(Envelope.from("you" `@` "gmail.com") 14 | .to("mom" `@` "gmail.com") 15 | .subject("miss you") 16 | .content(Text("hi mom"))).onSuccess { 17 | case _ => println("message delivered") 18 | } 19 | 20 | For more information see the project's [readme](https://github.com/softprops/courier#readme) 21 | -------------------------------------------------------------------------------- /integration-tests/src/test/scala/mailspec.scala: -------------------------------------------------------------------------------- 1 | package courier 2 | 3 | import scala.concurrent.Await 4 | import scala.concurrent.ExecutionContext.Implicits.global 5 | import scala.concurrent.duration._ 6 | 7 | 8 | class MailSpec extends munit.FunSuite { 9 | // create a gmail app password https://myaccount.google.com/apppasswords 10 | test("the mailer should send an email") { 11 | assume(sys.env("CI").isEmpty(), "This test is meant to be ran locally.") 12 | val email = sys.env("IT_EMAIL") 13 | val password = sys.env("IT_PASSWORD") 14 | val mailer = Mailer("smtp.gmail.com", 587) 15 | .auth(true) 16 | .as(email, password) 17 | .startTls(true)() 18 | val mId = Await.result(mailer(Envelope.from(email.addr) 19 | .to(email.addr) 20 | .subject("miss you") 21 | .content(Text("hi mom"))), 10.seconds) 22 | assert(mId.nonEmpty, "Message-ID should be set by the Transport.send call") 23 | } 24 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Doug Tangren 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /courier/src/test/scala/mailspec.scala: -------------------------------------------------------------------------------- 1 | package courier 2 | 3 | import java.util.Properties 4 | 5 | import javax.mail.Provider 6 | import org.jvnet.mock_javamail.{Mailbox, MockTransport} 7 | 8 | import scala.concurrent.Await 9 | import scala.concurrent.ExecutionContext.Implicits.global 10 | import scala.concurrent.duration._ 11 | 12 | class MockedSMTPProvider extends Provider(Provider.Type.TRANSPORT, "mocked", classOf[MockTransport].getName, "Mock", null) 13 | 14 | class MailSpec extends munit.FunSuite { 15 | private val mockedSession = javax.mail.Session.getDefaultInstance(new Properties() {{ 16 | put("mail.transport.protocol.rfc822", "mocked") 17 | }}) 18 | mockedSession.setProvider(new MockedSMTPProvider) 19 | 20 | 21 | test("the mailer should send an email") { 22 | val mailer = Mailer(mockedSession) 23 | val future = mailer(Envelope.from("someone@example.com".addr) 24 | .to("mom@gmail.com".addr) 25 | .cc("dad@gmail.com".addr) 26 | .subject("miss you") 27 | .content(Text("hi mom"))) 28 | 29 | Await.ready(future, 5.seconds) 30 | val momsInbox = Mailbox.get("mom@gmail.com") 31 | assertEquals(momsInbox.size, 1) 32 | val momsMsg = momsInbox.get(0) 33 | assertEquals(momsMsg.getContent, "hi mom") 34 | assertEquals(momsMsg.getSubject, "miss you") 35 | } 36 | } -------------------------------------------------------------------------------- /courier/src/main/scala/signer.scala: -------------------------------------------------------------------------------- 1 | package courier 2 | 3 | import java.security.PrivateKey 4 | import java.security.cert.X509Certificate 5 | 6 | import javax.mail.internet.{MimeBodyPart, MimeMultipart} 7 | import org.bouncycastle.cert.jcajce.JcaCertStore 8 | import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoGeneratorBuilder 9 | import org.bouncycastle.mail.smime.SMIMESignedGenerator 10 | 11 | case class Signer(signingKey: PrivateKey, signingCert: X509Certificate, certStore: Set[X509Certificate]) { 12 | private val gen: SMIMESignedGenerator = new SMIMESignedGenerator() 13 | private val certs = new JcaCertStore(Compat.asJava(certStore + signingCert)) 14 | gen.addSignerInfoGenerator(new JcaSimpleSignerInfoGeneratorBuilder().build("SHA256withRSA", signingKey, signingCert)) 15 | gen.addCertificates(certs) 16 | 17 | def sign(content: Content): MimeMultipart = { 18 | val preppedContent = getContentToSign(content) 19 | gen.generate(preppedContent) 20 | } 21 | 22 | private def getContentToSign(c: Content): MimeBodyPart = { 23 | val bp = new MimeBodyPart() 24 | c match { 25 | case mp: Multipart => bp.setContent(mp.parts) 26 | case Text(txt, charset) => bp.setContent(txt, s"text/plain; charset=$charset") 27 | case _: Signed => throw new UnsupportedOperationException("Nested signed entities not supported") 28 | } 29 | bp 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /courier/src/main/scala/envelope.scala: -------------------------------------------------------------------------------- 1 | package courier 2 | 3 | import java.nio.charset.Charset 4 | import javax.mail.internet.InternetAddress 5 | 6 | object Envelope { 7 | def from(addr: InternetAddress) = 8 | Envelope(addr) 9 | } 10 | 11 | case class Envelope( 12 | from: InternetAddress, 13 | _subject: Option[(String, Option[Charset])] = None, 14 | _to: Seq[InternetAddress] = Seq.empty[InternetAddress], 15 | _cc: Seq[InternetAddress] = Seq.empty[InternetAddress], 16 | _bcc: Seq[InternetAddress] = Seq.empty[InternetAddress], 17 | _replyTo: Option[InternetAddress] = None, 18 | _replyToAll: Option[Boolean] = None, 19 | _headers: Seq[(String, String)] = Seq.empty[(String, String)], 20 | _content: Content = Text("")) { 21 | 22 | def subject(s: String) = copy(_subject = Some((s, None))) 23 | def subject(s: String, ch: Charset) = copy(_subject = Some((s, Some(ch)))) 24 | def to(addrs: InternetAddress*) = copy(_to = _to ++ addrs) 25 | def cc(addrs: InternetAddress*) = copy(_cc = _cc ++ addrs) 26 | def bcc(addrs: InternetAddress*) = copy(_bcc = _bcc ++ addrs) 27 | def replyTo(addr: InternetAddress) = copy(_replyTo = Some(addr)) 28 | def replyAll = copy(_replyToAll = Some(true)) 29 | def headers(hdrs: (String, String)*) = copy(_headers = _headers ++ hdrs) 30 | def content(c: Content) = copy(_content = c) 31 | 32 | def contents = _content 33 | def subject = _subject 34 | @deprecated("use `to` instead", "0.1.1") 35 | def recipients = _to 36 | def to = _to 37 | def cc = _cc 38 | def bcc = _bcc 39 | def replyTo = _replyTo 40 | def headers = _headers 41 | } 42 | -------------------------------------------------------------------------------- /courier/src/main/scala/mailer.scala: -------------------------------------------------------------------------------- 1 | package courier 2 | 3 | import javax.mail.internet.MimeMessage 4 | import javax.mail.{Message, Transport, Session => MailSession} 5 | 6 | import scala.concurrent.{ExecutionContext, Future} 7 | 8 | object Mailer { 9 | def apply(host: String, port: Int): Session.Builder = 10 | Mailer().session.host(host).port(port) 11 | } 12 | 13 | case class Mailer(_session: MailSession = Defaults.session, 14 | signer: Option[Signer] = None, 15 | mimeMessageFactory: MailSession => MimeMessage = Defaults.mimeMessageFactory) { 16 | def session = Session.Builder(this) 17 | 18 | def apply(e: Envelope)(implicit ec: ExecutionContext): Future[Option[String]] = { 19 | val msg = mimeMessageFactory(_session) 20 | 21 | e.subject.foreach { 22 | case (subject, Some(charset)) => msg.setSubject(subject, charset.name()) 23 | case (subject, None) => msg.setSubject(subject) 24 | } 25 | msg.setFrom(e.from) 26 | e.to.foreach(msg.addRecipient(Message.RecipientType.TO, _)) 27 | e.cc.foreach(msg.addRecipient(Message.RecipientType.CC, _)) 28 | e.bcc.foreach(msg.addRecipient(Message.RecipientType.BCC, _)) 29 | e.replyTo.foreach(a => msg.setReplyTo(Array(a))) 30 | e.headers.foreach(h => msg.addHeader(h._1, h._2)) 31 | e.contents match { 32 | case Text(txt, charset) => msg.setText(txt, charset.displayName) 33 | case mp: Multipart => msg.setContent(mp.parts) 34 | case Signed(body) => 35 | if (signer.isDefined) msg.setContent(signer.get.sign(body)) 36 | else throw new IllegalArgumentException("No signer defined, cannot sign!") 37 | } 38 | 39 | Future { 40 | // sending the email sets the message id 41 | val _ = Transport.send(msg) 42 | Option(msg.getMessageID()) 43 | } 44 | } 45 | } 46 | 47 | 48 | -------------------------------------------------------------------------------- /courier/src/main/scala/content.scala: -------------------------------------------------------------------------------- 1 | package courier 2 | 3 | import java.io.File 4 | import java.nio.charset.Charset 5 | 6 | import javax.activation.{DataHandler, FileDataSource} 7 | import javax.mail.internet.{MimeBodyPart, MimeMultipart} 8 | import javax.mail.util.ByteArrayDataSource 9 | 10 | sealed trait Content 11 | 12 | case class Text(body: String, charset: Charset = Charset.defaultCharset) extends Content 13 | 14 | case class Signed(body: Content) extends Content 15 | 16 | case class Multipart(_parts: Seq[MimeBodyPart] = Seq.empty[MimeBodyPart], subtype: String = "mixed") extends Content { 17 | def add(part: MimeBodyPart): Multipart = 18 | this.copy(_parts = _parts :+ part) 19 | 20 | def add( 21 | bytes: Array[Byte], 22 | mimetype: String, 23 | name: Option[String] = None, 24 | disposition: Option[String] = None, 25 | description: Option[String] = None): Multipart = 26 | add(new MimeBodyPart { 27 | setContent(bytes, mimetype) 28 | disposition.foreach(setDisposition) 29 | description.foreach(setDescription) 30 | name.foreach(setFileName) 31 | }) 32 | 33 | def text(str: String, charset: Charset = Charset.defaultCharset) = 34 | add(new MimeBodyPart { 35 | setText(str, charset.displayName()) 36 | }) 37 | 38 | def html(str: String, charset: Charset = Charset.defaultCharset) = 39 | add(new MimeBodyPart { 40 | setContent(str, s"text/html; charset=${charset.displayName()}") 41 | }) 42 | 43 | def attach(file: File, name: Option[String] = None) = 44 | add(new MimeBodyPart { 45 | setDataHandler(new DataHandler(new FileDataSource(file))) 46 | setFileName(name.getOrElse(file.getName)) 47 | }) 48 | 49 | def attachBytes(bytes: Array[Byte], name: String, mimeType: String) = 50 | add(new MimeBodyPart { 51 | setDataHandler(new DataHandler(new ByteArrayDataSource(bytes, mimeType))) 52 | setFileName(name) 53 | }) 54 | 55 | def parts = 56 | new MimeMultipart(subtype) { 57 | _parts.foreach(addBodyPart(_)) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /courier/src/main/scala/sessions.scala: -------------------------------------------------------------------------------- 1 | package courier 2 | 3 | import javax.mail.{ PasswordAuthentication, Session => MailSession } 4 | import java.util.Properties 5 | 6 | object Session { 7 | case class Builder( 8 | mailer: Mailer, 9 | _auth: Option[Boolean] = None, 10 | _startTls: Option[Boolean] = None, 11 | _ssl: Option[Boolean] = None, 12 | _trustAll: Option[Boolean] = None, 13 | _socketFactory: Option[String] = None, 14 | _host: Option[String] = None, 15 | _port: Option[Int] = None, 16 | _debug: Option[Boolean] = None, 17 | _creds: Option[(String, String)] = None, 18 | _signer: Option[Signer] = None) { 19 | def auth(a: Boolean) = copy(_auth= Some(a)) 20 | def startTls(s: Boolean) = copy(_startTls = Some(s)) 21 | def ssl(l: Boolean) = copy(_ssl = Some(l)) 22 | def trustAll(t: Boolean) = copy(_trustAll = Some(t)) 23 | def host(h: String) = copy(_host = Some(h)) 24 | def port(p: Int) = copy(_port = Some(p)) 25 | def debug(d: Boolean) = copy(_debug = Some(d)) 26 | def as(user: String, pass: String) = 27 | copy(_creds = Some((user, pass))) 28 | def socketFactory(cls: String) = copy(_socketFactory = Some(cls)) 29 | def sslSocketFactory = socketFactory("javax.net.ssl.SSLSocketFactory") 30 | def withSigner(s: Signer): Builder = copy(_signer=Some(s)) 31 | def apply() = 32 | mailer.copy(_session = MailSession.getInstance( 33 | new Properties(System.getProperties) { 34 | _debug.map(d => put("mail.smtp.debug", d.toString)) 35 | _auth.map(a => put("mail.smtp.auth", a.toString)) 36 | // enable ESMTP 37 | _startTls.map(s => put("mail.smtp.starttls.enable", s.toString)) 38 | _ssl.map(l => put("mail.smtp.ssl.enable", l.toString)) 39 | _socketFactory.map(put("mail.smtp.socketFactory.class", _)) 40 | _host.map(put("mail.smtp.host", _)) 41 | _port.map(p => put("mail.smtp.port", p.toString)) 42 | _trustAll.collect { 43 | case true => put("mail.smtp.ssl.trust", "*") 44 | } 45 | }, _creds.map { 46 | case (user, pass) => 47 | new javax.mail.Authenticator { 48 | protected override def getPasswordAuthentication() = 49 | new PasswordAuthentication(user, pass) 50 | } 51 | } 52 | .orNull), 53 | signer=_signer) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /project/ScalacOptions.scala: -------------------------------------------------------------------------------- 1 | // https://tpolecat.github.io/2017/04/25/scalac-flags.html 2 | // Thanks Rob 3 | object ScalacOptions { 4 | val All = Seq( 5 | "-deprecation", // Emit warning and location for usages of deprecated APIs. 6 | "-encoding", "utf-8", // Specify character encoding used by source files. 7 | "-explaintypes", // Explain type errors in more detail. 8 | "-feature", // Emit warning and location for usages of features that should be imported explicitly. 9 | "-language:existentials", // Existential types (besides wildcard types) can be written and inferred 10 | "-language:experimental.macros", // Allow macro definition (besides implementation and application) 11 | "-language:higherKinds", // Allow higher-kinded types 12 | "-language:implicitConversions", // Allow definition of implicit functions called views 13 | "-unchecked", // Enable additional warnings where generated code depends on assumptions. 14 | "-Xcheckinit", // Wrap field accessors to throw an exception on uninitialized access. 15 | "-Xfatal-warnings", // Fail the compilation if there are any warnings. 16 | "-Xlint:adapted-args", // Warn if an argument list is modified to match the receiver. 17 | "-Xlint:constant", // Evaluation of a constant arithmetic expression results in an error. 18 | "-Xlint:delayedinit-select", // Selecting member of DelayedInit. 19 | "-Xlint:doc-detached", // A Scaladoc comment appears to be detached from its element. 20 | "-Xlint:inaccessible", // Warn about inaccessible types in method signatures. 21 | "-Xlint:infer-any", // Warn when a type argument is inferred to be `Any`. 22 | "-Xlint:missing-interpolator", // A string literal appears to be missing an interpolator id. 23 | "-Xlint:nullary-unit", // Warn when nullary methods return Unit. 24 | "-Xlint:option-implicit", // Option.apply used implicit view. 25 | "-Xlint:package-object-classes", // Class or object defined in package object. 26 | "-Xlint:poly-implicit-overload", // Parameterized overloaded implicit methods are not visible as view bounds. 27 | "-Xlint:private-shadow", // A private field (or class parameter) shadows a superclass field. 28 | "-Xlint:stars-align", // Pattern sequence wildcard must align with sequence component. 29 | "-Xlint:type-parameter-shadow", // A local type parameter shadows a type already in scope. 30 | "-Ywarn-dead-code", // Warn when dead code is identified. 31 | "-Ywarn-extra-implicit", // Warn when more than one implicit parameter section is defined. 32 | "-Ywarn-numeric-widen", // Warn when numerics are widened. 33 | "-Ywarn-unused:implicits", // Warn if an implicit parameter is unused. 34 | "-Ywarn-unused:imports", // Warn if an import selector is not referenced. 35 | "-Ywarn-unused:locals", // Warn if a local definition is unused. 36 | "-Ywarn-unused:params", // Warn if a value parameter is unused. 37 | "-Ywarn-unused:patvars", // Warn if a variable bound in a pattern is unused. 38 | "-Ywarn-unused:privates", // Warn if a private member is unused. 39 | "-Ywarn-value-discard" // Warn when non-Unit expression results are unused. 40 | ) 41 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # courier 2 | 3 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.daddykotex/courier_2.13/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.daddykotex/courier_2.13) 4 | 5 | deliver electronic mail with scala from the [future](http://www.scala-lang.org/api/current/index.html#scala.concurrent.Future) 6 | 7 | ![courier](http://upload.wikimedia.org/wikipedia/commons/thumb/a/a0/Courrier.jpg/337px-Courrier.jpg) 8 | 9 | ## install 10 | 11 | Via the copy and paste method 12 | 13 | ```scala 14 | libraryDependencies += "com.github.daddykotex" %% "courier" % "4.0.0-RC1" 15 | ``` 16 | 17 | 3.2.0+ supports scala 2.11 to 3.1 18 | 19 | Note: Scala3 (or Dotty) is supported. 20 | 21 | - `3.0.0-RC1` for dotty: `0.27.0-RC1` 22 | - `3.0.0-M1` for dotty: `3.0.0-M1` 23 | - `3.0.0-M2` for dotty: `3.0.0-M2` 24 | - `3.0.1` scala 3: `3.0.1` 25 | - `3.1.9` scala 3: `3.1.0` 26 | 27 | ## usage 28 | 29 | deliver electronic mail via gmail 30 | 31 | ```scala 32 | import courier._, Defaults._ 33 | import scala.util._ 34 | val mailer = Mailer("smtp.gmail.com", 587) 35 | .auth(true) 36 | .as("you@gmail.com", "p@$$w3rd") 37 | .startTls(true)() 38 | mailer(Envelope.from("you" `@` "gmail.com") 39 | .to("mom" `@` "gmail.com") 40 | .cc("dad" `@` "gmail.com") 41 | .subject("miss you") 42 | .content(Text("hi mom"))).onComplete { 43 | case Success(_) => println("message delivered") 44 | case Failure(_) => println("delivery failed") 45 | } 46 | 47 | mailer(Envelope.from("you" `@` "work.com") 48 | .to("boss" `@` "work.com") 49 | .subject("tps report") 50 | .content(Multipart() 51 | .attach(new java.io.File("tps.xls")) 52 | .html("

IT'S IMPORTANT

"))) 53 | .onComplete { 54 | case Success(_) => println("delivered report") 55 | case Failure(_) => println("delivery failed") 56 | } 57 | ``` 58 | 59 | If using SSL/TLS instead of STARTTLS, substitute `.startTls(true)` with `.ssl(true)` when setting up the `Mailer`. 60 | 61 | ### S/MIME 62 | 63 | Courier supports sending S/MIME signed email through its optional dependencies on the Bouncycastle cryptography libraries. It does not yet support sending encrypted email. 64 | 65 | Make sure the Mailer is instantiated with a signer, and then wrap your message in a Signed() object. 66 | 67 | ```scala 68 | import courier._ 69 | import java.security.cert.X509Certificate 70 | import java.security.PrivateKey 71 | 72 | val certificate: X509Certificate = ??? 73 | val privateKey: PrivateKey = ??? 74 | val trustChain: Set[X509Certificate] = ??? 75 | 76 | val signer = Signer(privateKey, certificate, trustChain) 77 | val mailer = Mailer("smtp.gmail.com", 587) 78 | .auth(true) 79 | .as("you@gmail.com", "p@$$w3rd") 80 | .withSigner(signer) 81 | .startTtls(true)() 82 | val envelope = Envelope 83 | .from("mr_pink" `@` "gmail.com") 84 | .to("mr_white" `@` "gmail.com") 85 | .subject("the jewelry store") 86 | .content(Signed(Text("For all I know, you're the rat."))) 87 | 88 | mailer(envelope) 89 | ``` 90 | 91 | ## testing 92 | 93 | Since courier is based on JavaMail, you can use [Mock JavaMail](https://java.net/projects/mock-javamail) to execute your tests. Simply add the following to your `build.sbt`: 94 | 95 | ```scala 96 | libraryDependencies += "org.jvnet.mock-javamail" % "mock-javamail" % "1.9" % "test" 97 | ``` 98 | 99 | With this library, you should, given a little bit of boilerplate, be able to set a test against a mocked Mailbox. This repo contains [an example](src/test/scala/mailspec.scala). 100 | 101 | ## changelog 102 | 103 | **4.0.0-RC1** 104 | 105 | - includes a breaking change in the Mailer#apply function. It now returns a `Future[String]` as opposed to a `Future[Unit]`. 106 | 107 | (C) Doug Tangren (softprops) and others, 2013-2018 108 | --------------------------------------------------------------------------------