wip(backend): Added HTML email
This commit is contained in:
parent
d63b7e5397
commit
3fc08d8bdb
17 changed files with 2665 additions and 442 deletions
|
@ -16,6 +16,7 @@
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"express": "4.18.2",
|
"express": "4.18.2",
|
||||||
|
"mustache": "^4.2.0",
|
||||||
"passport-http": "^0.3.0",
|
"passport-http": "^0.3.0",
|
||||||
"pino": "^8.7.0"
|
"pino": "^8.7.0"
|
||||||
},
|
},
|
||||||
|
|
|
@ -24,6 +24,8 @@ model Confirmation {
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
data String
|
data String
|
||||||
type String
|
type String
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
userId Int
|
||||||
}
|
}
|
||||||
|
|
||||||
model Subscriber {
|
model Subscriber {
|
||||||
|
@ -49,6 +51,7 @@ model User {
|
||||||
password String
|
password String
|
||||||
patron Int @default(0)
|
patron Int @default(0)
|
||||||
apikeys Apikey[]
|
apikeys Apikey[]
|
||||||
|
confirmations Confirmation[]
|
||||||
people Person[]
|
people Person[]
|
||||||
patterns Pattern[]
|
patterns Pattern[]
|
||||||
role String @default("user")
|
role String @default("user")
|
||||||
|
|
Binary file not shown.
|
@ -32,7 +32,7 @@ ApikeyController.prototype.whoami = async (req, res, tools) => {
|
||||||
// Load user making the call
|
// Load user making the call
|
||||||
await User.loadAuthenticatedUser(req.user)
|
await User.loadAuthenticatedUser(req.user)
|
||||||
|
|
||||||
const key = User.user.apikeys.filter((key) => key.id === req.user.id)
|
const key = User.authenticatedUser.apikeys.filter((key) => key.id === req.user.id)
|
||||||
|
|
||||||
if (key.length === 1)
|
if (key.length === 1)
|
||||||
Apikey.setResponse(200, 'success', {
|
Apikey.setResponse(200, 'success', {
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { hash, hashPassword, randomString, verifyPassword } from '../utils/crypt
|
||||||
import { clean, asJson } from '../utils/index.mjs'
|
import { clean, asJson } from '../utils/index.mjs'
|
||||||
import { getUserAvatar } from '../utils/sanity.mjs'
|
import { getUserAvatar } from '../utils/sanity.mjs'
|
||||||
import { log } from '../utils/log.mjs'
|
import { log } from '../utils/log.mjs'
|
||||||
import { emailTemplate } from '../utils/email.mjs'
|
|
||||||
import set from 'lodash.set'
|
import set from 'lodash.set'
|
||||||
import { UserModel } from '../models/user.mjs'
|
import { UserModel } from '../models/user.mjs'
|
||||||
|
|
||||||
|
@ -68,7 +67,7 @@ export function UserController() {}
|
||||||
*/
|
*/
|
||||||
UserController.prototype.signup = async (req, res, tools) => {
|
UserController.prototype.signup = async (req, res, tools) => {
|
||||||
const User = new UserModel(tools)
|
const User = new UserModel(tools)
|
||||||
await User.create(req.body)
|
await User.create(req)
|
||||||
|
|
||||||
return User.sendResponse(res)
|
return User.sendResponse(res)
|
||||||
}
|
}
|
||||||
|
@ -80,77 +79,10 @@ UserController.prototype.signup = async (req, res, tools) => {
|
||||||
* See: https://freesewing.dev/reference/backend/api
|
* See: https://freesewing.dev/reference/backend/api
|
||||||
*/
|
*/
|
||||||
UserController.prototype.confirm = async (req, res, tools) => {
|
UserController.prototype.confirm = async (req, res, tools) => {
|
||||||
if (!req.params.id) return res.status(404).send({ error: 'missingConfirmationId', result })
|
const User = new UserModel(tools)
|
||||||
if (Object.keys(req.body) < 1) return res.status(400).json({ error: 'postBodyMissing', result })
|
await User.confirm(req)
|
||||||
if (!req.body.consent || req.body.consent < 1)
|
|
||||||
return res.status(400).send({ error: 'consentRequired', result })
|
|
||||||
|
|
||||||
// Destructure what we need from tools
|
return User.sendResponse(res)
|
||||||
const { prisma, config, decrypt } = tools
|
|
||||||
|
|
||||||
// Retrieve confirmation record
|
|
||||||
let confirmation
|
|
||||||
try {
|
|
||||||
confirmation = await prisma.confirmation.findUnique({
|
|
||||||
where: {
|
|
||||||
id: req.params.id,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
log.warn(err, `Could not lookup confirmation id ${req.params.id}`)
|
|
||||||
return res.status(404).send({ error: 'failedToRetrieveConfirmationId', result })
|
|
||||||
}
|
|
||||||
if (!confirmation) {
|
|
||||||
log.warn(err, `Could not find confirmation id ${req.params.id}`)
|
|
||||||
return res.status(404).send({ error: 'failedToFindConfirmationId', result })
|
|
||||||
}
|
|
||||||
if (confirmation.type !== 'signup') {
|
|
||||||
log.warn(err, `Confirmation mismatch; ${req.params.id} is not a signup id`)
|
|
||||||
return res.status(404).send({ error: 'confirmationIdTypeMismatch', result })
|
|
||||||
}
|
|
||||||
const data = decrypt(confirmation.data)
|
|
||||||
|
|
||||||
// Retrieve user account
|
|
||||||
let account
|
|
||||||
try {
|
|
||||||
account = await prisma.user.findUnique({
|
|
||||||
where: {
|
|
||||||
id: data.id,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
log.warn(err, `Could not lookup user id ${data.id} from confirmation data`)
|
|
||||||
return res.status(404).send({ error: 'failedToRetrieveUserIdFromConfirmationData', result })
|
|
||||||
}
|
|
||||||
if (!account) {
|
|
||||||
log.warn(err, `Could not find user id ${data.id} from confirmation data`)
|
|
||||||
return res.status(404).send({ error: 'failedToLoadUserFromConfirmationData', result })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update user consent and status
|
|
||||||
let updateUser
|
|
||||||
try {
|
|
||||||
updateUser = await prisma.user.update({
|
|
||||||
where: {
|
|
||||||
id: account.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
status: 1,
|
|
||||||
consent: req.body.consent,
|
|
||||||
lastLogin: new Date(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
log.warn(err, `Could not update user id ${data.id} after confirmation`)
|
|
||||||
return res.status(404).send({ error: 'failedToUpdateUserAfterConfirmation', result })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Account is now active, let's return a passwordless login
|
|
||||||
return res.status(200).send({
|
|
||||||
result: 'success',
|
|
||||||
token: getToken(account, config),
|
|
||||||
account: asAccount({ ...account, status: 1, consent: req.body.consent }, decrypt),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -8,8 +8,13 @@ export function ConfirmationModel(tools) {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
ConfirmationModel.prototype.load = async function (where) {
|
ConfirmationModel.prototype.read = async function (where) {
|
||||||
this.record = await this.prisma.confirmation.findUnique({ where })
|
this.record = await this.prisma.confirmation.findUnique({
|
||||||
|
where,
|
||||||
|
include: {
|
||||||
|
user: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
return this.setExists()
|
return this.setExists()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
|
import jwt from 'jsonwebtoken'
|
||||||
import { log } from '../utils/log.mjs'
|
import { log } from '../utils/log.mjs'
|
||||||
import { hash, hashPassword, randomString, verifyPassword } from '../utils/crypto.mjs'
|
import { hash, hashPassword, randomString, verifyPassword } from '../utils/crypto.mjs'
|
||||||
import { clean, asJson } from '../utils/index.mjs'
|
import { clean, asJson, i18nUrl } from '../utils/index.mjs'
|
||||||
import { ConfirmationModel } from './confirmation.mjs'
|
import { ConfirmationModel } from './confirmation.mjs'
|
||||||
import { emailTemplate } from '../utils/email.mjs'
|
|
||||||
|
|
||||||
export function UserModel(tools) {
|
export function UserModel(tools) {
|
||||||
this.config = tools.config
|
this.config = tools.config
|
||||||
|
@ -20,9 +20,10 @@ export function UserModel(tools) {
|
||||||
*
|
*
|
||||||
* Stores result in this.record
|
* Stores result in this.record
|
||||||
*/
|
*/
|
||||||
UserModel.prototype.load = async function (where) {
|
UserModel.prototype.read = async function (where) {
|
||||||
this.record = await this.prisma.user.findUnique({ where })
|
this.record = await this.prisma.user.findUnique({ where })
|
||||||
if (this.record?.email) this.email = this.decrypt(this.record.email)
|
if (this.record?.email) this.email = this.decrypt(this.record.email)
|
||||||
|
if (this.record?.initial) this.initial = this.decrypt(this.record.initial)
|
||||||
|
|
||||||
return this.setExists()
|
return this.setExists()
|
||||||
}
|
}
|
||||||
|
@ -60,14 +61,14 @@ UserModel.prototype.setExists = function () {
|
||||||
/*
|
/*
|
||||||
* Creates a user+confirmation and sends out signup email
|
* Creates a user+confirmation and sends out signup email
|
||||||
*/
|
*/
|
||||||
UserModel.prototype.create = async function (body) {
|
UserModel.prototype.create = async function ({ body }) {
|
||||||
if (Object.keys(body) < 1) return this.setResponse(400, 'postBodyMissing')
|
if (Object.keys(body) < 1) return this.setResponse(400, 'postBodyMissing')
|
||||||
if (!body.email) return this.setResponse(400, 'emailMissing')
|
if (!body.email) return this.setResponse(400, 'emailMissing')
|
||||||
if (!body.password) return this.setResponse(400, 'passwordMissing')
|
if (!body.password) return this.setResponse(400, 'passwordMissing')
|
||||||
if (!body.language) return this.setResponse(400, 'languageMissing')
|
if (!body.language) return this.setResponse(400, 'languageMissing')
|
||||||
|
|
||||||
const ehash = hash(clean(body.email))
|
const ehash = hash(clean(body.email))
|
||||||
await this.load({ ehash })
|
await this.read({ ehash })
|
||||||
if (this.exists) return this.setResponse(400, 'emailExists')
|
if (this.exists) return this.setResponse(400, 'emailExists')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -100,9 +101,8 @@ UserModel.prototype.create = async function (body) {
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.warn(err, 'Could not update username after user creation')
|
log.warn(err, 'Could not update username after user creation')
|
||||||
return this.setResponse(500, 'error', 'usernameUpdateAfterUserCreationFailed')
|
return this.setResponse(500, 'usernameUpdateAfterUserCreationFailed')
|
||||||
}
|
}
|
||||||
log.info({ user: this.record.id }, 'Account created')
|
|
||||||
|
|
||||||
// Create confirmation
|
// Create confirmation
|
||||||
this.confirmation = await this.Confirmation.create({
|
this.confirmation = await this.Confirmation.create({
|
||||||
|
@ -113,10 +113,20 @@ UserModel.prototype.create = async function (body) {
|
||||||
id: this.record.id,
|
id: this.record.id,
|
||||||
ehash: ehash,
|
ehash: ehash,
|
||||||
}),
|
}),
|
||||||
|
userId: this.record.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Send signup email
|
// Send signup email
|
||||||
//await this.sendSignupEmail()
|
await this.mailer.send({
|
||||||
|
template: 'signup',
|
||||||
|
language: this.language,
|
||||||
|
to: 'joost@decock.org', // this.email,
|
||||||
|
replacements: {
|
||||||
|
actionUrl: i18nUrl(this.language, `/confirm/signup/${this.Confirmation.record.id}`),
|
||||||
|
whyUrl: i18nUrl(this.language, `/docs/faq/email/why-signup`),
|
||||||
|
supportUrl: i18nUrl(this.language, `/patrons/join`),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
return body.unittest && this.email.split('@').pop() === this.config.tests.domain
|
return body.unittest && this.email.split('@').pop() === this.config.tests.domain
|
||||||
? this.setResponse(201, false, { email: this.email, confirmation: this.confirmation.record.id })
|
? this.setResponse(201, false, { email: this.email, confirmation: this.confirmation.record.id })
|
||||||
|
@ -124,21 +134,52 @@ UserModel.prototype.create = async function (body) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Sends out signup email
|
* Confirms a user account
|
||||||
* FIXME: Move to utils
|
|
||||||
*/
|
*/
|
||||||
UserModel.prototype.sendSignupEmail = async function () {
|
UserModel.prototype.confirm = async function ({ body, params }) {
|
||||||
try {
|
if (!params.id) return this.setReponse(404, 'missingConfirmationId')
|
||||||
this.confirmationSent = await this.mailer.send(
|
if (Object.keys(body) < 1) return this.setResponse(400, 'postBodyMissing')
|
||||||
this.email,
|
if (!body.consent || typeof body.consent !== 'number' || body.consent < 1)
|
||||||
...emailTemplate.signup(this.email, this.language, this.confirmation)
|
return this.setResponse(400, 'consentRequired')
|
||||||
)
|
|
||||||
} catch (err) {
|
// Retrieve confirmation record
|
||||||
log.warn(err, 'Unable to send signup email')
|
await this.Confirmation.read({ id: params.id })
|
||||||
return this.setResponse(500, 'error', 'unableToSendSignupEmail')
|
|
||||||
|
if (!this.Confirmation.exists) {
|
||||||
|
log.warn(err, `Could not find confirmation id ${params.id}`)
|
||||||
|
return this.setResponse(404, 'failedToFindConfirmationId')
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.setResponse(200)
|
if (this.Confirmation.record.type !== 'signup') {
|
||||||
|
log.warn(err, `Confirmation mismatch; ${params.id} is not a signup id`)
|
||||||
|
return this.setResponse(404, 'confirmationIdTypeMismatch')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.error) return this
|
||||||
|
const data = await this.decrypt(this.Confirmation.record.data)
|
||||||
|
if (data.ehash !== this.Confirmation.record.user.ehash)
|
||||||
|
return this.setResponse(404, 'confirmationEhashMismatch')
|
||||||
|
if (data.id !== this.Confirmation.record.user.id)
|
||||||
|
return this.setResponse(404, 'confirmationUserIdMismatch')
|
||||||
|
|
||||||
|
// Load user
|
||||||
|
await this.read({ id: this.Confirmation.record.user.id })
|
||||||
|
if (this.error) return this
|
||||||
|
|
||||||
|
// Update user status, consent, and last login
|
||||||
|
await this.update({
|
||||||
|
//data: this.encrypt({...this.decrypt(this.record.data), status: 1}),
|
||||||
|
consent: body.consent,
|
||||||
|
lastLogin: new Date(),
|
||||||
|
})
|
||||||
|
if (this.error) return this
|
||||||
|
|
||||||
|
// Account is now active, let's return a passwordless login
|
||||||
|
return this.setResponse(200, false, {
|
||||||
|
result: 'success',
|
||||||
|
token: this.getToken(),
|
||||||
|
account: this.asAccount(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -153,12 +194,52 @@ UserModel.prototype.update = async function (data) {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.warn(err, 'Could not update user record')
|
log.warn(err, 'Could not update user record')
|
||||||
process.exit()
|
process.exit()
|
||||||
return this.setResponse(500, 'error', 'updateUserFailed')
|
return this.setResponse(500, 'updateUserFailed')
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.setResponse(200)
|
return this.setResponse(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Returns account data
|
||||||
|
*/
|
||||||
|
UserModel.prototype.asAccount = function () {
|
||||||
|
return {
|
||||||
|
id: this.record.id,
|
||||||
|
consent: this.record.consent,
|
||||||
|
createdAt: this.record.createdAt,
|
||||||
|
data: this.record.data,
|
||||||
|
email: this.email,
|
||||||
|
initial: this.initial,
|
||||||
|
lastLogin: this.record.lastLogin,
|
||||||
|
newsletter: this.record.newsletter,
|
||||||
|
patron: this.record.patron,
|
||||||
|
role: this.record.role,
|
||||||
|
status: this.record.status,
|
||||||
|
updatedAt: this.record.updatedAt,
|
||||||
|
username: this.record.username,
|
||||||
|
lusername: this.record.lusername,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Returns a JSON Web Token (jwt)
|
||||||
|
*/
|
||||||
|
UserModel.prototype.getToken = function () {
|
||||||
|
return jwt.sign(
|
||||||
|
{
|
||||||
|
_id: this.record.id,
|
||||||
|
username: this.record.username,
|
||||||
|
role: this.record.role,
|
||||||
|
status: this.record.status,
|
||||||
|
aud: this.config.jwt.audience,
|
||||||
|
iss: this.config.jwt.issuer,
|
||||||
|
},
|
||||||
|
this.config.jwt.secretOrKey,
|
||||||
|
{ expiresIn: this.config.jwt.expiresIn }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Helper method to set the response code, result, and body
|
* Helper method to set the response code, result, and body
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,8 +1,3 @@
|
||||||
/*
|
|
||||||
* buttonRow uses the following replacements:
|
|
||||||
* - actionUrl
|
|
||||||
* - button
|
|
||||||
*/
|
|
||||||
export const buttonRow = {
|
export const buttonRow = {
|
||||||
html: `
|
html: `
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -16,25 +11,17 @@ export const buttonRow = {
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>`,
|
</tr>`,
|
||||||
text: `{{ actionUrl }}`,
|
text: `{{{ actionUrl }}}`,
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* closingRow uses the following replacements:
|
|
||||||
* - closing
|
|
||||||
* - greeting
|
|
||||||
* - ps-pre-link
|
|
||||||
* - ps-link
|
|
||||||
* - ps-post-link
|
|
||||||
*/
|
|
||||||
export const closingRow = {
|
export const closingRow = {
|
||||||
html: `
|
html: `
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left" class="sm-p-15px" style="padding-top: 30px">
|
<td align="left" class="sm-p-15px" style="padding-top: 30px">
|
||||||
<p style="margin: 0; font-size: 16px; line-height: 25px; color: #262626">
|
<p style="margin: 0; font-size: 16px; line-height: 25px; color: #262626">
|
||||||
{{ closing }}.
|
{{ closing }}
|
||||||
<br><br>
|
<br><br>
|
||||||
{{ greeting }},
|
{{ greeting }}
|
||||||
<br>
|
<br>
|
||||||
joost
|
joost
|
||||||
<br><br>
|
<br><br>
|
||||||
|
@ -42,7 +29,7 @@ export const closingRow = {
|
||||||
PS: {{ ps-pre-link}}
|
PS: {{ ps-pre-link}}
|
||||||
<a href="{{ supportUrl }}" target="_blank" style="text-decoration: none; color: #262626">
|
<a href="{{ supportUrl }}" target="_blank" style="text-decoration: none; color: #262626">
|
||||||
<b>{{ ps-link}}</b>
|
<b>{{ ps-link}}</b>
|
||||||
</a> {{ ps-post-link }}.
|
</a> {{ ps-post-link }}
|
||||||
</small>
|
</small>
|
||||||
</p>
|
</p>
|
||||||
</td>
|
</td>
|
||||||
|
@ -53,14 +40,9 @@ export const closingRow = {
|
||||||
{{ greeting }}
|
{{ greeting }}
|
||||||
joost
|
joost
|
||||||
|
|
||||||
PS: {{ text-ps }} : {{ text-ps-link }}`,
|
PS: {{ text-ps }} : {{{ text-ps-link }}}`,
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* headingRow uses the following replacements:
|
|
||||||
* - actionUrl
|
|
||||||
* - heading
|
|
||||||
*/
|
|
||||||
export const headingRow = {
|
export const headingRow = {
|
||||||
html: `
|
html: `
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -77,11 +59,6 @@ export const headingRow = {
|
||||||
`,
|
`,
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* lead1Row uses the following replacements:
|
|
||||||
* - actionUrl
|
|
||||||
* - lead
|
|
||||||
*/
|
|
||||||
export const lead1Row = {
|
export const lead1Row = {
|
||||||
html: `
|
html: `
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -94,20 +71,10 @@ export const lead1Row = {
|
||||||
</td>
|
</td>
|
||||||
</tr>`,
|
</tr>`,
|
||||||
text: `{{ textLead }}
|
text: `{{ textLead }}
|
||||||
{{ actionUrl }}
|
{{{ actionUrl }}}
|
||||||
`,
|
`,
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Helper methods to wrap the body with all it takes
|
|
||||||
* Uses the following replacements:
|
|
||||||
* - title
|
|
||||||
* - intro
|
|
||||||
* - body
|
|
||||||
* - urlWebsite
|
|
||||||
* - urlWhy
|
|
||||||
* - whyDidIGetThis
|
|
||||||
*/
|
|
||||||
export const wrap = {
|
export const wrap = {
|
||||||
html: (body) => `<!DOCTYPE html>
|
html: (body) => `<!DOCTYPE html>
|
||||||
<html lang="en" xmlns:v="urn:schemas-microsoft-com:vml">
|
<html lang="en" xmlns:v="urn:schemas-microsoft-com:vml">
|
||||||
|
@ -260,10 +227,33 @@ Plantin en Moretuslei 69
|
||||||
Antwerp
|
Antwerp
|
||||||
Belgium
|
Belgium
|
||||||
|
|
||||||
{{ website }} : {{ urlWebsite }}
|
{{ website }} : {{{ urlWebsite }}}
|
||||||
Github : https://github.com/fresewing/freesewing
|
Github : https://github.com/freesewing/freesewing
|
||||||
Discord : https://discord.freesewing.org/
|
Discord : https://discord.freesewing.org/
|
||||||
Twitter : https://twitter.com/freesewing_org
|
Twitter : https://twitter.com/freesewing_org
|
||||||
{{ whyDidIGetThis }} : {{ urlWhy }}
|
{{ whyDidIGetThis }} : {{{ whyUrl }}}
|
||||||
`,
|
`,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const translations = {
|
||||||
|
en: {
|
||||||
|
whyDidIGetThis: 'Why did I get this email?',
|
||||||
|
website: 'freesewing.org',
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
whyDidIGetThis: 'Why did I get this?', // FIXME: Provide German translation
|
||||||
|
website: 'freesewing.org/de',
|
||||||
|
},
|
||||||
|
es: {
|
||||||
|
whyDidIGetThis: 'Why did I get this?', // FIXME: Provide Spanish translation
|
||||||
|
website: 'freesewing.org/es',
|
||||||
|
},
|
||||||
|
fr: {
|
||||||
|
whyDidIGetThis: 'Why did I get this?', // FIXME: Provide French translation
|
||||||
|
website: 'freesewing.org/fr',
|
||||||
|
},
|
||||||
|
nl: {
|
||||||
|
whyDidIGetThis: 'Waarom kreeg ik deze email?',
|
||||||
|
website: 'freesewing.org/nl',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
@ -1,17 +1,6 @@
|
||||||
import { buttonRow, closingRow, headingRow, lead1Row, wrap } from './blocks.mjs'
|
import { buttonRow, closingRow, headingRow, lead1Row, wrap } from './blocks.mjs'
|
||||||
|
import { translations as sharedTranslations } from './blocks.mjs'
|
||||||
|
|
||||||
/*
|
|
||||||
* Used the following replacements:
|
|
||||||
* - actionUrl
|
|
||||||
* - heading
|
|
||||||
* - lead
|
|
||||||
* - button
|
|
||||||
* - closing
|
|
||||||
* - greeting
|
|
||||||
* - ps-pre-link
|
|
||||||
* - ps-link
|
|
||||||
* - ps-post-link
|
|
||||||
*/
|
|
||||||
export const emailChange = {
|
export const emailChange = {
|
||||||
html: wrap.html(`
|
html: wrap.html(`
|
||||||
${headingRow.html}
|
${headingRow.html}
|
||||||
|
@ -21,3 +10,64 @@ export const emailChange = {
|
||||||
`),
|
`),
|
||||||
text: wrap.text(`${headingRow.text}${lead1Row.text}${buttonRow.text}${closingRow.text}`),
|
text: wrap.text(`${headingRow.text}${lead1Row.text}${buttonRow.text}${closingRow.text}`),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const translations = {
|
||||||
|
en: {
|
||||||
|
heading: 'Does this new E-mail address work?',
|
||||||
|
lead: 'To confirm your new E-mail address, click the big black rectangle below:',
|
||||||
|
button: 'Confirm E-mail change',
|
||||||
|
closing: "That's all it takes.",
|
||||||
|
greeting: 'love',
|
||||||
|
'ps-pre-link': 'FreeSewing is free (duh), but please',
|
||||||
|
'ps-link': 'become a patron',
|
||||||
|
'ps-post-link': 'if you cxan afford it.',
|
||||||
|
...sharedTranslations.en,
|
||||||
|
},
|
||||||
|
// FIXME: Translate German
|
||||||
|
de: {
|
||||||
|
heading: 'Does this new E-mail address work?',
|
||||||
|
lead: 'To confirm your new E-mail address, click the big black rectangle below:',
|
||||||
|
button: 'Confirm E-mail change',
|
||||||
|
closing: "That's all it takes.",
|
||||||
|
greeting: 'love',
|
||||||
|
'ps-pre-link': 'FreeSewing is free (duh), but please',
|
||||||
|
'ps-link': 'become a patron',
|
||||||
|
'ps-post-link': 'if you cxan afford it.',
|
||||||
|
...sharedTranslations.de,
|
||||||
|
},
|
||||||
|
// FIXME: Translate Spanish
|
||||||
|
es: {
|
||||||
|
heading: 'Does this new E-mail address work?',
|
||||||
|
lead: 'To confirm your new E-mail address, click the big black rectangle below:',
|
||||||
|
button: 'Confirm E-mail change',
|
||||||
|
closing: "That's all it takes.",
|
||||||
|
greeting: 'love',
|
||||||
|
'ps-pre-link': 'FreeSewing is free (duh), but please',
|
||||||
|
'ps-link': 'become a patron',
|
||||||
|
'ps-post-link': 'if you cxan afford it.',
|
||||||
|
...sharedTranslations.es,
|
||||||
|
},
|
||||||
|
// FIXME: Translate French
|
||||||
|
fr: {
|
||||||
|
heading: 'Does this new E-mail address work?',
|
||||||
|
lead: 'To confirm your new E-mail address, click the big black rectangle below:',
|
||||||
|
button: 'Confirm E-mail change',
|
||||||
|
closing: "That's all it takes.",
|
||||||
|
greeting: 'love',
|
||||||
|
'ps-pre-link': 'FreeSewing is free (duh), but please',
|
||||||
|
'ps-link': 'become a patron',
|
||||||
|
'ps-post-link': 'if you cxan afford it.',
|
||||||
|
...sharedTranslations.fr,
|
||||||
|
},
|
||||||
|
nl: {
|
||||||
|
heading: 'Werkt dit E-mail adres?',
|
||||||
|
lead: 'Om je E-mail wijziging te bevestigen moet je op de grote zwarte rechthoek hieronder te klikken:',
|
||||||
|
button: 'Bevestig je E-mail wijziging',
|
||||||
|
closing: 'Dat is al wat je moet doen.',
|
||||||
|
greeting: 'liefs',
|
||||||
|
'ps-pre-link': 'FreeSewing is gratis (echt), maar gelieve',
|
||||||
|
'ps-link': 'ons werk te ondersteunen',
|
||||||
|
'ps-post-link': 'als het even kan.',
|
||||||
|
...sharedTranslations.nl,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { headingRow, wrap } from './blocks.mjs'
|
import { headingRow, wrap } from './blocks.mjs'
|
||||||
|
import { translations as sharedTranslations } from './blocks.mjs'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Used the following replacements:
|
* Used the following replacements:
|
||||||
|
@ -36,3 +37,5 @@ joost
|
||||||
|
|
||||||
PS: {{ps}}`),
|
PS: {{ps}}`),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const translations = {}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { emailChange } from './emailchange.mjs'
|
import { emailChange, translations as emailChangeTranslations } from './emailchange.mjs'
|
||||||
import { goodbye } from './goodbye.mjs'
|
import { goodbye, translations as goodbyeTranslations } from './goodbye.mjs'
|
||||||
import { loginLink } from './loginlink.mjs'
|
import { loginLink, translations as loginLinkTranslations } from './loginlink.mjs'
|
||||||
import { newsletterSub } from './newslettersub.mjs'
|
import { newsletterSub, translations as newsletterSubTranslations } from './newslettersub.mjs'
|
||||||
import { passwordReset } from './passwordreset.mjs'
|
import { passwordReset, translations as passwordResetTranslations } from './passwordreset.mjs'
|
||||||
import { signup } from './signup.mjs'
|
import { signup, translations as signupTranslations } from './signup.mjs'
|
||||||
|
|
||||||
export const templates = {
|
export const templates = {
|
||||||
emailChange,
|
emailChange,
|
||||||
|
@ -13,3 +13,16 @@ export const templates = {
|
||||||
passwordReset,
|
passwordReset,
|
||||||
signup,
|
signup,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This is not part of our i18n package for... reasons
|
||||||
|
* It's not an accident, let's put it that way.
|
||||||
|
*/
|
||||||
|
export const translations = {
|
||||||
|
emailChange: emailChangeTranslations,
|
||||||
|
goodbye: goodbyeTranslations,
|
||||||
|
loginLink: loginLinkTranslations,
|
||||||
|
newsletterSub: newsletterSubTranslations,
|
||||||
|
passwordReset: passwordResetTranslations,
|
||||||
|
signup: signupTranslations,
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { buttonRow, closingRow, headingRow, lead1Row, wrap } from './blocks.mjs'
|
import { buttonRow, closingRow, headingRow, lead1Row, wrap } from './blocks.mjs'
|
||||||
|
import { translations as sharedTranslations } from './blocks.mjs'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Used the following replacements:
|
* Used the following replacements:
|
||||||
|
@ -37,3 +38,5 @@ ${buttonRow.text}
|
||||||
${closingRow.text}
|
${closingRow.text}
|
||||||
`),
|
`),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const translations = {}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { buttonRow, closingRow, headingRow, lead1Row, wrap } from './blocks.mjs'
|
import { buttonRow, closingRow, headingRow, lead1Row, wrap } from './blocks.mjs'
|
||||||
|
import { translations as sharedTranslations } from './blocks.mjs'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Used the following replacements:
|
* Used the following replacements:
|
||||||
|
@ -26,3 +27,5 @@ ${buttonRow.text}
|
||||||
${closingRow.text}
|
${closingRow.text}
|
||||||
`),
|
`),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const translations = {}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { buttonRow, closingRow, headingRow, lead1Row, wrap } from './blocks.mjs'
|
import { buttonRow, closingRow, headingRow, lead1Row, wrap } from './blocks.mjs'
|
||||||
|
import { translations as sharedTranslations } from './blocks.mjs'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Used the following replacements:
|
* Used the following replacements:
|
||||||
|
@ -39,3 +40,5 @@ ${buttonRow.text}
|
||||||
${closingRow.text}
|
${closingRow.text}
|
||||||
`),
|
`),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const translations = {}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { buttonRow, closingRow, headingRow, lead1Row, wrap } from './blocks.mjs'
|
import { buttonRow, closingRow, headingRow, lead1Row, wrap } from './blocks.mjs'
|
||||||
|
import { translations as sharedTranslations } from './blocks.mjs'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Used the following replacements:
|
* Used the following replacements:
|
||||||
|
@ -26,3 +27,69 @@ export const signup = {
|
||||||
${closingRow.text}
|
${closingRow.text}
|
||||||
`),
|
`),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const translations = {
|
||||||
|
en: {
|
||||||
|
subject: '[FreeSewing] Confirm your E-mail address to activate your account',
|
||||||
|
heading: 'Welcome to FreeSewing',
|
||||||
|
lead: 'To activate your account, click the big black rectangle below:',
|
||||||
|
button: 'Activate account',
|
||||||
|
closing: "That's all for now.",
|
||||||
|
greeting: 'love',
|
||||||
|
'ps-pre-link': 'FreeSewing is free (duh), but please',
|
||||||
|
'ps-link': 'become a patron',
|
||||||
|
'ps-post-link': 'if you cxan afford it.',
|
||||||
|
...sharedTranslations.en,
|
||||||
|
},
|
||||||
|
// FIXME: Translate German
|
||||||
|
de: {
|
||||||
|
subject: '[FreeSewing] Confirm your E-mail address to activate your account',
|
||||||
|
heading: 'Welcome to FreeSewing',
|
||||||
|
lead: 'To activate your account, click the big black rectangle below:',
|
||||||
|
button: 'Activate account',
|
||||||
|
closing: "That's all for now.",
|
||||||
|
greeting: 'love',
|
||||||
|
'ps-pre-link': 'FreeSewing is free (duh), but please',
|
||||||
|
'ps-link': 'become a patron',
|
||||||
|
'ps-post-link': 'if you cxan afford it.',
|
||||||
|
...sharedTranslations.de,
|
||||||
|
},
|
||||||
|
// FIXME: Translate Spanish
|
||||||
|
es: {
|
||||||
|
subject: '[FreeSewing] Confirm your E-mail address to activate your account',
|
||||||
|
heading: 'Welcome to FreeSewing',
|
||||||
|
lead: 'To activate your account, click the big black rectangle below:',
|
||||||
|
button: 'Activate account',
|
||||||
|
closing: "That's all for now.",
|
||||||
|
greeting: 'love',
|
||||||
|
'ps-pre-link': 'FreeSewing is free (duh), but please',
|
||||||
|
'ps-link': 'become a patron',
|
||||||
|
'ps-post-link': 'if you cxan afford it.',
|
||||||
|
...sharedTranslations.es,
|
||||||
|
},
|
||||||
|
// FIXME: Translate French
|
||||||
|
fr: {
|
||||||
|
subject: '[FreeSewing] Confirm your E-mail address to activate your account',
|
||||||
|
heading: 'Welcome to FreeSewing',
|
||||||
|
lead: 'To activate your account, click the big black rectangle below:',
|
||||||
|
button: 'Activate account',
|
||||||
|
closing: "That's all for now.",
|
||||||
|
greeting: 'love',
|
||||||
|
'ps-pre-link': 'FreeSewing is free (duh), but please',
|
||||||
|
'ps-link': 'become a patron',
|
||||||
|
'ps-post-link': 'if you can afford it.',
|
||||||
|
...sharedTranslations.fr,
|
||||||
|
},
|
||||||
|
nl: {
|
||||||
|
subject: '[FreeSewing] Bevestig je E-mail adres om je account te activeren',
|
||||||
|
heading: 'Welkom bij FreeSewing',
|
||||||
|
lead: 'Om je account te activeren moet je op de grote zwarte rechthoek hieronder te klikken:',
|
||||||
|
button: 'Account activeren',
|
||||||
|
closing: 'Daarmee is dat ook weer geregeld.',
|
||||||
|
greeting: 'liefs',
|
||||||
|
'ps-pre-link': 'FreeSewing is gratis (echt), maar gelieve',
|
||||||
|
'ps-link': 'ons werk te ondersteunen',
|
||||||
|
'ps-post-link': 'als het even kan.',
|
||||||
|
...sharedTranslations.nl,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
@ -1,188 +1,6 @@
|
||||||
import axios from 'axios'
|
import { templates, translations } from '../templates/email/index.mjs'
|
||||||
import { templates } from '../templates/email/index.mjs'
|
|
||||||
// FIXME: Update this after we re-structure the i18n package
|
|
||||||
import en from '../../../../packages/i18n/dist/en/email.mjs'
|
|
||||||
import nl from '../../../../packages/i18n/dist/en/email.mjs'
|
|
||||||
import fr from '../../../../packages/i18n/dist/en/email.mjs'
|
|
||||||
import es from '../../../../packages/i18n/dist/en/email.mjs'
|
|
||||||
import de from '../../../../packages/i18n/dist/en/email.mjs'
|
|
||||||
import { i18nUrl } from './index.mjs'
|
|
||||||
import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2'
|
import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2'
|
||||||
|
import mustache from 'mustache'
|
||||||
const i18n = { en, nl, fr, es, de }
|
|
||||||
|
|
||||||
export const emailTemplate = {
|
|
||||||
signup: (to, language, uuid) => [
|
|
||||||
i18n[language].signupTitle,
|
|
||||||
templates.signup(i18n[language], to, i18nUrl(language, `/confirm/signup/${uuid}`)),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
emailTemplate.emailchange = (newAddress, currentAddress, language, id) => {
|
|
||||||
let html = loadTemplate('emailchange', 'html', language)
|
|
||||||
let text = loadTemplate('emailchange', 'text', language)
|
|
||||||
let from = [
|
|
||||||
'__emailchangeActionLink__',
|
|
||||||
'__emailchangeActionText__',
|
|
||||||
'__emailchangeTitle__',
|
|
||||||
'__emailchangeCopy1__',
|
|
||||||
'__headerOpeningLine__',
|
|
||||||
'__hiddenIntro__',
|
|
||||||
'__footerWhy__',
|
|
||||||
'__questionsJustReply__',
|
|
||||||
'__signature__',
|
|
||||||
]
|
|
||||||
let to = [
|
|
||||||
createUrl(language, `/confirm/email/${id}`),
|
|
||||||
i18n[language]['email.emailchangeActionText'],
|
|
||||||
i18n[language]['email.emailchangeTitle'],
|
|
||||||
i18n[language]['email.emailchangeCopy1'],
|
|
||||||
i18n[language]['email.emailchangeHeaderOpeningLine'],
|
|
||||||
i18n[language]['email.emailchangeHiddenIntro'],
|
|
||||||
i18n[language]['email.emailchangeWhy'],
|
|
||||||
i18n[language]['email.questionsJustReply'],
|
|
||||||
i18n[language]['email.signature'],
|
|
||||||
]
|
|
||||||
html = replace(html, from, to)
|
|
||||||
text = replace(text, from, to)
|
|
||||||
|
|
||||||
let options = {
|
|
||||||
from: `"${i18n[language]['email.joostFromFreesewing']}" <info@freesewing.org>`,
|
|
||||||
to: newAddress,
|
|
||||||
cc: currentAddress,
|
|
||||||
subject: i18n[language]['email.emailchangeSubject'],
|
|
||||||
headers: {
|
|
||||||
'X-Freesewing-Confirmation-ID': '' + id,
|
|
||||||
},
|
|
||||||
text,
|
|
||||||
html,
|
|
||||||
}
|
|
||||||
deliver(options, (error, info) => {
|
|
||||||
if (error) return console.log(error)
|
|
||||||
console.log('Message sent', info)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
emailTemplate.passwordreset = (recipient, language, id) => {
|
|
||||||
let html = loadTemplate('passwordreset', 'html', language)
|
|
||||||
let text = loadTemplate('passwordreset', 'text', language)
|
|
||||||
let from = [
|
|
||||||
'__passwordresetActionLink__',
|
|
||||||
'__headerOpeningLine__',
|
|
||||||
'__hiddenIntro__',
|
|
||||||
'__footerWhy__',
|
|
||||||
]
|
|
||||||
let to = [
|
|
||||||
createUrl(language, `/confirm/reset/${id}`),
|
|
||||||
i18n[language]['email.passwordresetHeaderOpeningLine'],
|
|
||||||
i18n[language]['email.passwordresetHiddenIntro'],
|
|
||||||
i18n[language]['email.passwordresetWhy'],
|
|
||||||
]
|
|
||||||
html = replace(html, from, to)
|
|
||||||
text = replace(text, from, to)
|
|
||||||
|
|
||||||
let options = {
|
|
||||||
from: `"${i18n[language]['email.joostFromFreesewing']}" <info@freesewing.org>`,
|
|
||||||
to: recipient,
|
|
||||||
subject: i18n[language]['email.passwordresetSubject'],
|
|
||||||
headers: {
|
|
||||||
'X-Freesewing-Confirmation-ID': '' + id,
|
|
||||||
},
|
|
||||||
text,
|
|
||||||
html,
|
|
||||||
}
|
|
||||||
deliver(options, (error, info) => {
|
|
||||||
if (error) return console.log(error)
|
|
||||||
console.log('Message sent', info)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
emailTemplate.goodbye = async (recipient, language) => {
|
|
||||||
let html = loadTemplate('goodbye', 'html', language)
|
|
||||||
let text = loadTemplate('goodbye', 'text', language)
|
|
||||||
let from = ['__headerOpeningLine__', '__hiddenIntro__', '__footerWhy__']
|
|
||||||
let to = [
|
|
||||||
i18n[language]['email.goodbyeHeaderOpeningLine'],
|
|
||||||
i18n[language]['email.goodbyeHiddenIntro'],
|
|
||||||
i18n[language]['email.goodbyeWhy'],
|
|
||||||
]
|
|
||||||
html = replace(html, from, to)
|
|
||||||
text = replace(text, from, to)
|
|
||||||
|
|
||||||
let options = {
|
|
||||||
from: `"${i18n[language]['email.joostFromFreesewing']}" <info@freesewing.org>`,
|
|
||||||
to: recipient,
|
|
||||||
subject: i18n[language]['email.goodbyeSubject'],
|
|
||||||
text,
|
|
||||||
html,
|
|
||||||
}
|
|
||||||
deliver(options, (error, info) => {
|
|
||||||
if (error) return console.log(error)
|
|
||||||
console.log('Message sent', info)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
emailTemplate.subscribe = async (recipient, token) => {
|
|
||||||
let html = loadTemplate('newsletterSubscribe', 'html', 'en')
|
|
||||||
let text = loadTemplate('newsletterSubscribe', 'text', 'en')
|
|
||||||
let from = [
|
|
||||||
'__hiddenIntro__',
|
|
||||||
'__headerOpeningLine__',
|
|
||||||
'__newsletterConfirmationLink__',
|
|
||||||
'__footerWhy__',
|
|
||||||
]
|
|
||||||
let to = [
|
|
||||||
'Confirm your subscription to the FreeSewing newsletter',
|
|
||||||
'Please confirm it was you who requested this',
|
|
||||||
`https://backend.freesewing.org/newsletter/confirm/${token}`,
|
|
||||||
`You received this email because somebody tried to subscribe ${recipient} to the FreeSewing newsletter`,
|
|
||||||
]
|
|
||||||
html = replace(html, from, to)
|
|
||||||
text = replace(text, from, to)
|
|
||||||
|
|
||||||
let options = {
|
|
||||||
from: `"FreeSewing" <newsletter@freesewing.org>`,
|
|
||||||
to: recipient,
|
|
||||||
subject: 'Confirm your subscription to the FreeSewing newsletter',
|
|
||||||
text,
|
|
||||||
html,
|
|
||||||
}
|
|
||||||
deliver(options, (error, info) => {
|
|
||||||
if (error) return console.log(error)
|
|
||||||
console.log('Message sent', info)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
emailTemplate.newsletterWelcome = async (recipient, ehash) => {
|
|
||||||
let html = loadTemplate('newsletterWelcome', 'html', 'en')
|
|
||||||
let text = loadTemplate('newsletterWelcome', 'text', 'en')
|
|
||||||
let from = [
|
|
||||||
'__hiddenIntro__',
|
|
||||||
'__headerOpeningLine__',
|
|
||||||
'__newsletterUnsubscribeLink__',
|
|
||||||
'__footerWhy__',
|
|
||||||
]
|
|
||||||
let to = [
|
|
||||||
'No action required; This is just an FYI',
|
|
||||||
"You're in. Now what?",
|
|
||||||
`https://backend.freesewing.org/newsletter/unsubscribe/${ehash}`,
|
|
||||||
`You received this email because you subscribed to the FreeSewing newsletter`,
|
|
||||||
]
|
|
||||||
html = replace(html, from, to)
|
|
||||||
text = replace(text, from, to)
|
|
||||||
|
|
||||||
let options = {
|
|
||||||
from: `"FreeSewing" <newsletter@freesewing.org>`,
|
|
||||||
to: recipient,
|
|
||||||
subject: 'Welcome to the FreeSewing newsletter',
|
|
||||||
text,
|
|
||||||
html,
|
|
||||||
}
|
|
||||||
deliver(options, (error, info) => {
|
|
||||||
if (error) return console.log(error)
|
|
||||||
console.log('Message sent', info)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Exporting this closure that makes sure we have access to the
|
* Exporting this closure that makes sure we have access to the
|
||||||
|
@ -190,7 +8,7 @@ emailTemplate.newsletterWelcome = async (recipient, ehash) => {
|
||||||
*/
|
*/
|
||||||
export const mailer = (config) => ({
|
export const mailer = (config) => ({
|
||||||
email: {
|
email: {
|
||||||
send: (...params) => sendEmailViaAwsSes(config, ...params),
|
send: (params) => sendEmailViaAwsSes(config, params),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -200,19 +18,37 @@ export const mailer = (config) => ({
|
||||||
* If you want to use another way to send email, change the mailer
|
* If you want to use another way to send email, change the mailer
|
||||||
* assignment above to point to another method to deliver email
|
* assignment above to point to another method to deliver email
|
||||||
*/
|
*/
|
||||||
async function sendEmailViaAwsSes(config, to, subject, text) {
|
async function sendEmailViaAwsSes(config, { template, to, language = 'en', replacements = {} }) {
|
||||||
|
// Make sure we have what it takes
|
||||||
|
if (!template || !to || typeof templates[template] === 'undefined') return false
|
||||||
|
|
||||||
|
// Load template
|
||||||
|
const { html, text } = templates[template]
|
||||||
|
const replace = {
|
||||||
|
...translations[template][language],
|
||||||
|
...replacements,
|
||||||
|
}
|
||||||
|
|
||||||
// IMHO the AWS apis are a complete clusterfuck
|
// IMHO the AWS apis are a complete clusterfuck
|
||||||
// can't even use them without their garbage SDK
|
|
||||||
const Charset = 'utf-8'
|
|
||||||
const client = new SESv2Client({ region: config.aws.ses.region })
|
const client = new SESv2Client({ region: config.aws.ses.region })
|
||||||
const command = new SendEmailCommand({
|
const command = new SendEmailCommand({
|
||||||
ConfigurationSetName: 'backend',
|
ConfigurationSetName: 'backend',
|
||||||
Content: {
|
Content: {
|
||||||
Simple: {
|
Simple: {
|
||||||
Body: {
|
Body: {
|
||||||
Text: { Charset, Data: text },
|
Text: {
|
||||||
|
Charset: 'utf-8',
|
||||||
|
Data: mustache.render(text, replace),
|
||||||
|
},
|
||||||
|
Html: {
|
||||||
|
Charset: 'utf-8',
|
||||||
|
Data: mustache.render(html, replace),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Subject: {
|
||||||
|
Charset: 'utf-8',
|
||||||
|
Data: replace.subject,
|
||||||
},
|
},
|
||||||
Subject: { Charset, Data: subject },
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Destination: {
|
Destination: {
|
||||||
|
@ -224,7 +60,13 @@ async function sendEmailViaAwsSes(config, to, subject, text) {
|
||||||
FromEmailAddress: config.aws.ses.from,
|
FromEmailAddress: config.aws.ses.from,
|
||||||
ReplyToAddresses: config.aws.ses.replyTo || [],
|
ReplyToAddresses: config.aws.ses.replyTo || [],
|
||||||
})
|
})
|
||||||
const result = await client.send(command)
|
let result
|
||||||
|
try {
|
||||||
|
result = await client.send(command)
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return result['$metadata']?.httpStatusCode === 200
|
return result['$metadata']?.httpStatusCode === 200
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue