-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 5eef0ba
Showing
21 changed files
with
621 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
# This file was automatically generated by sbt-blowout and should not be edited manually. | ||
# Instead, run blowoutGenerate after making the desired changes to your build definition. | ||
name: CI | ||
'on': | ||
pull_request: | ||
branches: | ||
- main | ||
jobs: | ||
lint: | ||
name: Fatal warnings and code formatting | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Checkout | ||
uses: actions/checkout@v4 | ||
- name: Setup Java JDK | ||
uses: actions/setup-java@v4 | ||
with: | ||
cache: sbt | ||
distribution: temurin | ||
java-version: '21' | ||
- name: Workflows | ||
run: sbt -Dmode=ci blowoutCheck | ||
- name: Code formatting | ||
run: sbt -Dmode=ci scalafmtCheckAll | ||
- name: Fatal warnings | ||
run: sbt -Dmode=ci compile | ||
test: | ||
name: Unit tests | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Checkout | ||
uses: actions/checkout@v4 | ||
- name: Setup Java JDK | ||
uses: actions/setup-java@v4 | ||
with: | ||
cache: sbt | ||
distribution: temurin | ||
java-version: '21' | ||
- run: sbt test |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
# This file was automatically generated by sbt-blowout and should not be edited manually. | ||
# Instead, run blowoutGenerate after making the desired changes to your build definition. | ||
name: CI & CD | ||
'on': | ||
push: | ||
branches: | ||
- main | ||
tags: | ||
- '*.*.*' | ||
jobs: | ||
lint: | ||
name: Fatal warnings and code formatting | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Checkout | ||
uses: actions/checkout@v4 | ||
- name: Setup Java JDK | ||
uses: actions/setup-java@v4 | ||
with: | ||
cache: sbt | ||
distribution: temurin | ||
java-version: '21' | ||
- name: Workflows | ||
run: sbt -Dmode=ci blowoutCheck | ||
- name: Code formatting | ||
run: sbt -Dmode=ci scalafmtCheckAll | ||
- name: Fatal warnings | ||
run: sbt -Dmode=ci compile | ||
test: | ||
name: Unit tests | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Checkout | ||
uses: actions/checkout@v4 | ||
- name: Setup Java JDK | ||
uses: actions/setup-java@v4 | ||
with: | ||
cache: sbt | ||
distribution: temurin | ||
java-version: '21' | ||
- run: sbt test | ||
deploy: | ||
name: Deploy | ||
runs-on: ubuntu-latest | ||
needs: | ||
- lint | ||
- test | ||
steps: | ||
- name: Checkout | ||
uses: actions/checkout@v4 | ||
- name: Setup Java JDK | ||
uses: actions/setup-java@v4 | ||
with: | ||
cache: sbt | ||
distribution: temurin | ||
java-version: '21' | ||
- name: Release | ||
run: sbt -Dmode=release ci-release | ||
env: | ||
PGP_PASSPHRASE: ${{secrets.PGP_PASSPHRASE}} | ||
PGP_SECRET: ${{secrets.PGP_SECRET}} | ||
SONATYPE_PASSWORD: ${{secrets.SONATYPE_PASSWORD}} | ||
SONATYPE_USERNAME: ${{secrets.SONATYPE_USERNAME}} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
.scalafmt.conf | ||
.bsp/ | ||
target/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# Changelog |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2024 Niklas Klein | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
# Backmail | ||
|
||
> Email templating utilities | ||
## Motivation | ||
|
||
Creating beautiful transactional HTML emails is usually a horrible experience. So we tend to resort to the HTML templating feature of our transactional email providers. But this comes with a price: we are getting vendor-locked, argument substituion is wonky and i18n becomes an even more difficult challenge than it already is. This library provides the fundamental building blocks to manage HTML emails in the backend. | ||
|
||
## Installation | ||
|
||
```sbt | ||
libraryDependencies ++= | ||
"io.taig" %% "backmail-core" % "x.y.z" :: | ||
"io.taig" %% "backmail-circe" % "x.y.z" :: | ||
Nil | ||
``` | ||
|
||
## Usage | ||
|
||
This library provides data structures to describe emails. The provided printers can then be used to turn the email descriptions into plaintext emails, HTML emails or debug messages that hide secret values. | ||
|
||
### Define an email | ||
|
||
```scala | ||
import io.taig.backmail.dsl.* | ||
|
||
val myEmail = email(title = "Title")( | ||
headline(text("Title")), | ||
block(text(plain("Plaintext"), plain(" "), secret("Secret"))), | ||
block(paragraph = false)(text("Lorem ipusm dolar sit amet."), linebreak, text("Lorem ipusm dolar sit amet.")), | ||
space, | ||
button(href = attr(plain("?token="), secret("foobar")))(text("Confirm email")), | ||
space, | ||
block(paragraph = false)(text("Lorem ipusm dolar sit amet.")) | ||
) | ||
``` | ||
|
||
### Print as plaintext | ||
|
||
An HTML email should always have a plaintext fallback. This printer is intended to provide this email variant. | ||
|
||
```scala | ||
import io.taig.backmail.* | ||
PlaintextPrinter.print(myEmail) | ||
|
||
val res0: String = Title | ||
|
||
Plaintext Secret | ||
|
||
Lorem ipusm dolar sit amet. | ||
Lorem ipusm dolar sit amet. | ||
|
||
Confirm email: ?token=foobar | ||
|
||
Lorem ipusm dolar sit amet. | ||
``` | ||
|
||
### Print as debug message | ||
|
||
Print the email as a plaintext message with hidden secret values so it is safe to log. | ||
|
||
```scala | ||
import io.taig.backmail.* | ||
DebugPrinter.print(myEmail) | ||
|
||
val res1: String = Title | ||
|
||
Plaintext ****** | ||
|
||
Lorem ipusm dolar sit amet. | ||
Lorem ipusm dolar sit amet. | ||
|
||
Confirm email: ?token=****** | ||
|
||
Lorem ipusm dolar sit amet. | ||
``` | ||
|
||
### Print as HTML | ||
|
||
Now this is where things get interesting: | ||
|
||
1. Start by picking an HTML email templating generator of your choice (e.g. [Maizzle](https://maizzle.com) or [Bootstrap Email](https://bootstrapemail.com)) | ||
2. Create a template that includes a headline, a button and paragraphs of text | ||
3. Use the generated HTML to implement your own `HtmlPrinter` | ||
|
||
## Disclaimer | ||
|
||
This library was created to meet my personal needs and may lack features that are important to you. Contributions welcome (-: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import sbtcrossproject.CrossProject | ||
val Version = new { | ||
val Circe = "0.14.6" | ||
val MUnit = "1.0.0-M11" | ||
val Scala = "3.3.3" | ||
} | ||
|
||
def module(identifier: Option[String]): CrossProject = { | ||
CrossProject(identifier.getOrElse("root"), file(identifier.fold(".")("modules/" + _)))(JVMPlatform, JSPlatform) | ||
.crossType(CrossType.Pure) | ||
.withoutSuffixFor(JVMPlatform) | ||
.build() | ||
.settings( | ||
Compile / scalacOptions ++= "-source:future" :: "-rewrite" :: "-new-syntax" :: "-Wunused:all" :: Nil, | ||
name := "backmail" + identifier.fold("")("-" + _) | ||
) | ||
} | ||
|
||
inThisBuild( | ||
Def.settings( | ||
developers := List(Developer("taig", "Niklas Klein", "mail@taig.io", url("https://taig.io/"))), | ||
dynverVTagPrefix := false, | ||
homepage := Some(url("https://github.com/taig/backmail/")), | ||
licenses := List("MIT" -> url("https://raw.githubusercontent.com/taig/backmail/main/LICENSE")), | ||
organization := "io.taig", | ||
organizationHomepage := Some(url("https://taig.io/")), | ||
scalaVersion := Version.Scala, | ||
versionScheme := Some("early-semver") | ||
) | ||
) | ||
|
||
lazy val root = module(identifier = None) | ||
.enablePlugins(BlowoutYamlPlugin) | ||
.settings(noPublishSettings) | ||
.settings( | ||
blowoutGenerators ++= { | ||
val workflows = file(".github") / "workflows" | ||
BlowoutYamlGenerator.lzy(workflows / "main.yml", GitHubActionsGenerator.main) :: | ||
BlowoutYamlGenerator.lzy(workflows / "branches.yml", GitHubActionsGenerator.branches) :: | ||
Nil | ||
} | ||
) | ||
.aggregate(core, circe) | ||
|
||
lazy val core = module(identifier = Some("core")) | ||
.settings( | ||
libraryDependencies ++= | ||
"org.scalameta" %%% "munit" % Version.MUnit % "test" :: | ||
Nil | ||
) | ||
|
||
lazy val circe = module(identifier = Some("circe")) | ||
.settings( | ||
libraryDependencies ++= | ||
"io.circe" %%% "circe-core" % Version.Circe :: | ||
Nil | ||
) | ||
.dependsOn(core) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
package io.taig.backmail | ||
|
||
import io.circe.Encoder | ||
import io.circe.JsonObject | ||
import io.circe.syntax.* | ||
import io.circe.Decoder | ||
import io.circe.DecodingFailure | ||
|
||
object circe: | ||
private object Key: | ||
val Block: String = "block" | ||
val Body: String = "body" | ||
val Button: String = "button" | ||
val Children: String = "children" | ||
val Headline: String = "headline" | ||
val Href: String = "href" | ||
val Linebreak: String = "linebreak" | ||
val Paragraph: String = "paragraph" | ||
val Plain: String = "plain" | ||
val Preheader: String = "preheader" | ||
val Secret: String = "secret" | ||
val Space: String = "space" | ||
val Text: String = "text" | ||
val Title: String = "title" | ||
val Type: String = "type" | ||
val Value: String = "value" | ||
|
||
given Decoder[Value] = cursor => | ||
cursor | ||
.get[String](Key.Type) | ||
.flatMap: | ||
case Key.Plain => cursor.get[String](Key.Value).map(Value.Plain.apply) | ||
case Key.Secret => cursor.get[String](Key.Value).map(Value.Secret.apply) | ||
case tpe => Left(DecodingFailure(s"Unknown: '$tpe'", cursor.downField(Key.Type).history)) | ||
|
||
given Decoder[Attribute] = Decoder[List[Value]].map(Attribute.apply) | ||
|
||
given Encoder[Attribute] = _.toList.asJson | ||
|
||
given Encoder.AsObject[Value] = | ||
case Value.Plain(value) => JsonObject(Key.Type := Key.Plain, Key.Value := value) | ||
case Value.Secret(value) => JsonObject(Key.Type := Key.Secret, Key.Value := value) | ||
|
||
given Decoder[Template] = cursor => | ||
cursor | ||
.get[String](Key.Type) | ||
.flatMap: | ||
case Key.Block => | ||
for | ||
children <- cursor.get[List[Template]](Key.Children) | ||
paragraph <- cursor.get[Boolean](Key.Paragraph) | ||
yield Template.Block(children, paragraph) | ||
case Key.Button => | ||
for | ||
children <- cursor.get[List[Template]](Key.Children) | ||
href <- cursor.get[Attribute](Key.Href) | ||
yield Template.Button(children, href) | ||
case Key.Headline => cursor.get[List[Template]](Key.Children).map(Template.Headline.apply) | ||
case Key.Linebreak => Right(Template.Linebreak) | ||
case Key.Space => Right(Template.Space) | ||
case Key.Text => cursor.get[List[Value]](Key.Children).map(Template.Text.apply) | ||
case tpe => Left(DecodingFailure(s"Unknown: '$tpe'", cursor.downField(Key.Type).history)) | ||
|
||
given Encoder.AsObject[Template] = | ||
case Template.Block(children, paragraph) => | ||
JsonObject( | ||
Key.Type := Key.Block, | ||
Key.Value := JsonObject(Key.Children := children.asJson, Key.Paragraph := paragraph) | ||
) | ||
case Template.Button(children, href) => | ||
JsonObject( | ||
Key.Type := Key.Button, | ||
Key.Value := JsonObject(Key.Children := children.asJson, Key.Href := href.asJson) | ||
) | ||
case Template.Headline(children) => | ||
JsonObject(Key.Type := Key.Headline, Key.Value := JsonObject(Key.Children := children)) | ||
case Template.Linebreak => JsonObject(Key.Type := Key.Linebreak) | ||
case Template.Space => JsonObject(Key.Type := Key.Space) | ||
case Template.Text(children) => JsonObject(Key.Type := Key.Text, Key.Value := JsonObject(Key.Children := children)) | ||
|
||
given Decoder[Email] = cursor => | ||
for | ||
title <- cursor.get[String](Key.Title) | ||
preheader <- cursor.get[Option[String]](Key.Preheader) | ||
body <- cursor.get[List[Template]](Key.Body) | ||
yield Email(title, preheader, body) | ||
|
||
given Encoder.AsObject[Email] = email => | ||
JsonObject(Key.Title := email.title, Key.Preheader := email.preheader, Key.Body := email.body) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package io.taig.backmail | ||
|
||
opaque type Attribute = List[Value] | ||
|
||
object Attribute: | ||
extension (self: Attribute) def toList: List[Value] = self | ||
def apply(values: List[Value]): Attribute = values |
20 changes: 20 additions & 0 deletions
20
modules/core/src/main/scala/io/taig/backmail/DebugPrinter.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package io.taig.backmail | ||
|
||
object DebugPrinter extends Printer: | ||
override def print(email: Email): String = email.body.map(print).mkString | ||
|
||
def print(template: Template): String = template match | ||
case Template.Block(children, paragrpah) => | ||
children.map(print).mkString + (if paragrpah then "\n\n" else "") | ||
case Template.Button(children, href) => | ||
val target = href.toList.map(print).mkString | ||
val label = children.map(print).mkString | ||
s"$label: $target" | ||
case Template.Headline(children) => s"${children.map(print).mkString}\n\n" | ||
case Template.Linebreak => "\n" | ||
case Template.Space => "\n\n" | ||
case Template.Text(children) => children.map(print).mkString | ||
|
||
def print(value: Value): String = value match | ||
case Value.Plain(value) => value | ||
case Value.Secret(value) => "*" * value.length |
Oops, something went wrong.