feat(backend): emails for notifications (#8435)

* email templates with pug for all possible notification emails

* more information in emails

* Individual email subjects to all notification emails

---------

Co-authored-by: Ulf Gebhardt <ulf.gebhardt@webcraft-media.de>
Co-authored-by: mahula <lenzmath@posteo.de>
This commit is contained in:
Moriz Wahl 2025-05-03 21:11:44 +02:00 committed by GitHub
parent 4f05b852af
commit e4ae0dfe50
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 4779 additions and 202 deletions

View File

@ -14,7 +14,7 @@ jobs:
run: | run: |
cp webapp/.env.template webapp/.env cp webapp/.env.template webapp/.env
cp frontend/.env.dist frontend/.env cp frontend/.env.dist frontend/.env
cp backend/.env.template backend/.env cp backend/.env.test_e2e backend/.env
- name: Build docker images - name: Build docker images
run: | run: |
@ -77,7 +77,7 @@ jobs:
docker load < /tmp/images/neo4j.tar docker load < /tmp/images/neo4j.tar
docker load < /tmp/images/backend.tar docker load < /tmp/images/backend.tar
docker load < /tmp/images/webapp.tar docker load < /tmp/images/webapp.tar
docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp neo4j backend --build docker compose -f docker-compose.yml -f docker-compose.test.yml up --build --detach --no-deps webapp neo4j backend mailserver
sleep 90s sleep 90s
- name: Full stack tests | run tests - name: Full stack tests | run tests

41
backend/.env.test_e2e Normal file
View File

@ -0,0 +1,41 @@
DEBUG=true
NEO4J_URI=bolt://localhost:7687
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=letmein
GRAPHQL_URI=http://localhost:4000
CLIENT_URI=http://localhost:3000
# E-Mail default settings
EMAIL_SUPPORT="devops@ocelot.social"
EMAIL_DEFAULT_SENDER="devops@ocelot.social"
SMTP_HOST=mailserver
SMTP_PORT=1025
SMTP_IGNORE_TLS=true
SMTP_MAX_CONNECTIONS=5
SMTP_MAX_MESSAGES=Infinity
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_SECURE="false" # true for 465, false for other ports
SMTP_DKIM_DOMAINNAME=
SMTP_DKIM_KEYSELECTOR=
SMTP_DKIM_PRIVATKEY=
JWT_SECRET="b/&&7b78BF&fv/Vd"
JWT_EXPIRES="2y"
MAPBOX_TOKEN="pk.eyJ1IjoiYnVzZmFrdG9yIiwiYSI6ImNraDNiM3JxcDBhaWQydG1uczhpZWtpOW4ifQ.7TNRTO-o9aK1Y6MyW_Nd4g"
PRIVATE_KEY_PASSPHRASE="a7dsf78sadg87ad87sfagsadg78"
SENTRY_DSN_BACKEND=
COMMIT=
PUBLIC_REGISTRATION=false
INVITE_REGISTRATION=true
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_ENDPOINT=
AWS_REGION=
AWS_BUCKET=
CATEGORIES_ACTIVE=false

View File

@ -36,6 +36,7 @@
"cheerio": "~1.0.0", "cheerio": "~1.0.0",
"cross-env": "~7.0.3", "cross-env": "~7.0.3",
"dotenv": "~16.5.0", "dotenv": "~16.5.0",
"email-templates": "^12.0.2",
"express": "^5.1.0", "express": "^5.1.0",
"graphql": "^14.6.0", "graphql": "^14.6.0",
"graphql-middleware": "~4.0.2", "graphql-middleware": "~4.0.2",
@ -77,6 +78,8 @@
"node-fetch": "^2.7.0", "node-fetch": "^2.7.0",
"nodemailer": "^6.10.1", "nodemailer": "^6.10.1",
"nodemailer-html-to-text": "^3.2.0", "nodemailer-html-to-text": "^3.2.0",
"preview-email": "^3.1.0",
"pug": "^3.0.3",
"request": "~2.88.2", "request": "~2.88.2",
"sanitize-html": "~2.16.0", "sanitize-html": "~2.16.0",
"slug": "~9.1.0", "slug": "~9.1.0",
@ -88,6 +91,7 @@
"devDependencies": { "devDependencies": {
"@eslint-community/eslint-plugin-eslint-comments": "^4.5.0", "@eslint-community/eslint-plugin-eslint-comments": "^4.5.0",
"@faker-js/faker": "9.7.0", "@faker-js/faker": "9.7.0",
"@types/email-templates": "^10.0.4",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/lodash": "^4.17.16", "@types/lodash": "^4.17.16",
"@types/node": "^22.15.3", "@types/node": "^22.15.3",
@ -119,7 +123,10 @@
}, },
"resolutions": { "resolutions": {
"**/**/fs-capacitor": "^6.2.0", "**/**/fs-capacitor": "^6.2.0",
"**/graphql-upload": "^11.0.0" "**/graphql-upload": "^11.0.0",
"**/strip-ansi": "6.0.1",
"**/string-width": "4.2.0",
"**/wrap-ansi": "7.0.0"
}, },
"engines": { "engines": {
"node": ">=20.12.1" "node": ">=20.12.1"

View File

@ -25,6 +25,7 @@ const environment = {
DISABLED_MIDDLEWARES: ['test', 'development'].includes(env.NODE_ENV as string) DISABLED_MIDDLEWARES: ['test', 'development'].includes(env.NODE_ENV as string)
? (env.DISABLED_MIDDLEWARES?.split(',') ?? []) ? (env.DISABLED_MIDDLEWARES?.split(',') ?? [])
: [], : [],
SEND_MAIL: env.NODE_ENV !== 'test',
} }
const required = { const required = {

View File

@ -26,6 +26,8 @@ if (CONFIG.PRODUCTION && !CONFIG.PRODUCTION_DB_CLEAN_ALLOW) {
throw new Error(`You cannot seed the database in a non-staging and real production environment!`) throw new Error(`You cannot seed the database in a non-staging and real production environment!`)
} }
CONFIG.SEND_MAIL = true
const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
;(async function () { ;(async function () {

View File

@ -0,0 +1,32 @@
import { Integer, Node } from 'neo4j-driver'
export interface UserDbProperties {
allowEmbedIframes: boolean
awaySince?: string
createdAt: string
deleted: boolean
disabled: boolean
emailNotificationsChatMessage?: boolean
emailNotificationsCommentOnObservedPost?: boolean
emailNotificationsFollowingUsers?: boolean
emailNotificationsGroupMemberJoined?: boolean
emailNotificationsGroupMemberLeft?: boolean
emailNotificationsGroupMemberRemoved?: boolean
emailNotificationsGroupMemberRoleChanged?: boolean
emailNotificationsMention?: boolean
emailNotificationsPostInGroup?: boolean
encryptedPassword: string
id: string
lastActiveAt?: string
lastOnlineStatus?: string
locale: string
name: string
role: string
showShoutsPublicly: boolean
slug: string
termsAndConditionsAgreedAt: string
termsAndConditionsAgreedVersion: string
updatedAt: string
}
export type User = Node<Integer, UserDbProperties>

View File

@ -0,0 +1,247 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`sendChatMessageMail English chat_message template 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"html": "<!DOCTYPE html>
<html lang="en">
<head>
<meta content="multipart/html; charset=UTF-8" http-equiv="content-type">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>.wf-force-outline-none[tabindex="-1"]:focus{outline:none;}</style>
<style>body{
display: block;
font-family: Lato, sans-serif;
font-size: 17px;
text-align: left;
text-align: -webkit-left;
justify-content: center;
padding: 15px;
margin: 0px;
}
h2 {
margin-top: 25px;
font-size: 25px;
font-weight: normal;
line-height: 22px;
color: #333333;
}
.container {
max-width: 680px;
margin: 0 auto;
display: block;
}
.head-logo {
width: 60%;
height: auto;
display: block;
margin-left: auto;
margin-right: auto;
}
a {
color: #17b53e;
}
a.button {
background: #17b53e;
font-family: Lato, sans-serif;
font-size: 16px;
line-height: 15px;
text-decoration: none;
text-align:center;
padding: 13px 17px;
color: #ffffff;
display: table;
margin-left: auto;
margin-right: auto;
border-radius: 4px;
}
.text-block {
margin-top: 20px;
color: #000000;
}
footer {
padding: 20px;
font-family: Lato, sans-serif;
font-size: 12px;
line-height: 15px;
text-align: center;
color: #888888;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="head"><img class="head-logo" alt="Welcome Image" loading="lazy" src="http://webapp:3000/img/custom/logo-squared.svg">
</div>
</header>
<h2>Hello chatReceiver,</h2>
<div class="wrapper">
<div class="content"></div>
<p>you have received a new chat message from <a class="user" href="http://webapp:3000/user/chatSender/chatsender">chatSender</a>.
</p><a class="button" href="http://webapp:3000/chat">Show Chat</a>
<div class="text-block">
<p>See you soon on <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
<p> The ocelot.social Team</p><br>
<p>PS: If you don't want to receive e-mails anymore, change your <a class="settings" href="http://webapp:3000/settings/notifications">notification settings</a>!</p>
</div>
</div>
<footer>
<div class="footer"></div><a href="https://ocelot.social">ocelot.social Community</a>
</footer>
</div>
</body>
</html>",
"subject": "ocelot.social Notification: New chat message",
"text": "HELLO CHATRECEIVER,
you have received a new chat message from chatSender
[http://webapp:3000/user/chatSender/chatsender].
Show Chat [http://webapp:3000/chat]
See you soon on ocelot.social [https://ocelot.social]!
The ocelot.social Team
PS: If you don't want to receive e-mails anymore, change your notification
settings [http://webapp:3000/settings/notifications]!
ocelot.social Community [https://ocelot.social]",
"to": "user@example.org",
}
`;
exports[`sendChatMessageMail German chat_message template 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"html": "<!DOCTYPE html>
<html lang="de">
<head>
<meta content="multipart/html; charset=UTF-8" http-equiv="content-type">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>.wf-force-outline-none[tabindex="-1"]:focus{outline:none;}</style>
<style>body{
display: block;
font-family: Lato, sans-serif;
font-size: 17px;
text-align: left;
text-align: -webkit-left;
justify-content: center;
padding: 15px;
margin: 0px;
}
h2 {
margin-top: 25px;
font-size: 25px;
font-weight: normal;
line-height: 22px;
color: #333333;
}
.container {
max-width: 680px;
margin: 0 auto;
display: block;
}
.head-logo {
width: 60%;
height: auto;
display: block;
margin-left: auto;
margin-right: auto;
}
a {
color: #17b53e;
}
a.button {
background: #17b53e;
font-family: Lato, sans-serif;
font-size: 16px;
line-height: 15px;
text-decoration: none;
text-align:center;
padding: 13px 17px;
color: #ffffff;
display: table;
margin-left: auto;
margin-right: auto;
border-radius: 4px;
}
.text-block {
margin-top: 20px;
color: #000000;
}
footer {
padding: 20px;
font-family: Lato, sans-serif;
font-size: 12px;
line-height: 15px;
text-align: center;
color: #888888;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="head"><img class="head-logo" alt="Welcome Image" loading="lazy" src="http://webapp:3000/img/custom/logo-squared.svg">
</div>
</header>
<h2>Hallo chatReceiver,</h2>
<div class="wrapper">
<div class="content"></div>
<p>du hast eine neue Chat-Nachricht von <a class="user" href="http://webapp:3000/user/chatSender/chatsender">chatSender</a> erhalten.
</p><a class="button" href="http://webapp:3000/chat">Chat anzeigen</a>
<div class="text-block">
<p>Bis bald bei <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
<p> Dein ocelot.social Team</p><br>
<p>PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine <a class="settings" href="http://webapp:3000/settings/notifications">Benachrichtigungseinstellungen</a>!</p>
</div>
</div>
<footer>
<div class="footer"></div><a href="https://ocelot.social">ocelot.social Community</a>
</footer>
</div>
</body>
</html>",
"subject": "ocelot.social Benachrichtigung: Neue Chat Nachricht",
"text": "HALLO CHATRECEIVER,
du hast eine neue Chat-Nachricht von chatSender
[http://webapp:3000/user/chatSender/chatsender] erhalten.
Chat anzeigen [http://webapp:3000/chat]
Bis bald bei ocelot.social [https://ocelot.social]!
Dein ocelot.social Team
PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine
Benachrichtigungseinstellungen [http://webapp:3000/settings/notifications]!
ocelot.social Community [https://ocelot.social]",
"to": "user@example.org",
}
`;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,39 @@
{
"notification": "Benachrichtigung",
"subjects": {
"changedGroupMemberRole": "Rolle in Gruppe geändert",
"chatMessage": "Neue Chat Nachricht",
"commentedOnPost": "Neuer Kommentar zu Beitrag",
"followedUserPosted": "Neuer Beitrag von gefolgtem Nutzer",
"mentionedInComment": "Erwähnung in Kommentar",
"mentionedInPost": "Erwähnung in Beitrag",
"removedUserFromGroup": "Aus Gruppe entfernt",
"postInGroup": "Neuer Beitrag in Gruppe",
"userJoinedGroup": "Nutzer tritt Gruppe bei",
"userLeftGroup": "Nutzer verlässt Gruppe"
},
"buttons": {
"viewChat": "Chat anzeigen",
"viewComment": "Kommentar ansehen",
"viewGroup": "Gruppe ansehen",
"viewPost": "Beitrag ansehen"
},
"general": {
"greeting": "Hallo",
"seeYou": "Bis bald bei ",
"yourTeam": " Dein {team} Team",
"settingsHint": "PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine ",
"settingsName": "Benachrichtigungseinstellungen"
},
"changedGroupMemberRole": "deine Rolle in der Gruppe „{groupName}“ wurde geändert. Klicke auf den Knopf, um diese Gruppe zu sehen:",
"chatMessageStart": "du hast eine neue Chat-Nachricht von ",
"chatMessageEnd": " erhalten.",
"commentedOnPost": " hat einen Beitrag den du beobachtest mit dem Titel „{postTitle}“ kommentiert. Klicke auf den Knopf, um diesen Kommentar zu sehen:",
"followedUserPosted": ", ein Nutzer dem du folgst, hat einen neuen Beitrag mit dem Titel „{postTitle}“ geschrieben. Klicke auf den Knopf, um diesen Beitrag zu sehen:",
"mentionedInComment": " hat dich in einem Kommentar zu dem Beitrag mit dem Titel „{postTitle}“ erwähnt. Klicke auf den Knopf, um den Kommentar zu sehen:",
"mentionedInPost": " hat Dich in einem Beitrag mit dem Titel „{postTitle}“ erwähnt. Klicke auf den Knopf, um den Beitrag zu sehen:",
"postInGroup": "jemand hat einen neuen Beitrag mit dem Titel „{postTitle}“ in einer deiner Gruppen geschrieben. Klicke auf den Knopf, um diesen Beitrag zu sehen:",
"removedUserFromGroup": "du wurdest aus der Gruppe „{groupName}“ entfernt.",
"userJoinedGroup": " ist der Gruppe „{groupName}“ beigetreten. Klicke auf den Knopf, um diese Gruppe zu sehen:",
"userLeftGroup": " hat die Gruppe „{groupName}“ verlassen. Klicke auf den Knopf, um diese Gruppe zu sehen:"
}

View File

@ -0,0 +1,39 @@
{
"notification": "Notification",
"subjects": {
"changedGroupMemberRole": "Role in group changed",
"chatMessage": "New chat message",
"commentedOnPost": "New comment on post",
"followedUserPosted": "New post by followd user",
"mentionedInComment": "Mentioned in comment",
"mentionedInPost": "Mentioned in post",
"removedUserFromGroup": "Removed from group",
"postInGroup": "New post in group",
"userJoinedGroup": "User joined group",
"userLeftGroup": "User left group"
},
"buttons": {
"viewChat": "Show Chat",
"viewComment": "View comment",
"viewGroup": "View group",
"viewPost": "View post"
},
"general": {
"greeting": "Hello",
"seeYou": "See you soon on ",
"yourTeam": " The {team} Team",
"settingsHint": "PS: If you don't want to receive e-mails anymore, change your ",
"settingsName": "notification settings"
},
"changedGroupMemberRole": "your role in the group “{groupName}” has been changed. Click on the button to view this group:",
"chatMessageStart": "you have received a new chat message from ",
"chatMessageEnd": ".",
"commentedOnPost": " commented on a post that you are observing with the title “{postTitle}”. Click on the button to view this comment:",
"followedUserPosted": ", a user you are following, wrote a new post with the title “{postTitle}”. Click on the button to view this post:",
"mentionedInComment": " mentioned you in a comment to the post with the title “{postTitle}”. Click on the button to view this comment:",
"mentionedInPost": " mentioned you in a post with the title “{postTitle}”. Click on the button to view this post:",
"removedUserFromGroup": "you have been removed from the group “{groupName}”.",
"postInGroup": "someone wrote a new post with the title “{postTitle}” in one of your groups. Click on the button to view this post:",
"userJoinedGroup": " joined the group “{groupName}”. Click on the button to view this group:",
"userLeftGroup": " left the group “{groupName}”. Click on the button to view this group:"
}

View File

@ -0,0 +1,87 @@
import { sendChatMessageMail } from './sendEmail'
const senderUser = {
allowEmbedIframes: false,
createdAt: '2025-04-30T00:16:49.610Z',
deleted: false,
disabled: false,
emailNotificationsChatMessage: true,
emailNotificationsCommentOnObservedPost: true,
emailNotificationsFollowingUsers: true,
emailNotificationsGroupMemberJoined: true,
emailNotificationsGroupMemberLeft: true,
emailNotificationsGroupMemberRemoved: true,
emailNotificationsGroupMemberRoleChanged: true,
emailNotificationsMention: true,
emailNotificationsPostInGroup: true,
encryptedPassword: '$2b$10$n.WujXapJrvn498lS97MD.gn8QwjWI9xlf8ckEYYtMTOPadMidcbG',
id: 'chatSender',
locale: 'en',
name: 'chatSender',
role: 'user',
showShoutsPublicly: false,
slug: 'chatsender',
termsAndConditionsAgreedAt: '2019-08-01T10:47:19.212Z',
termsAndConditionsAgreedVersion: '0.0.1',
updatedAt: '2025-04-30T00:16:49.610Z',
}
const recipientUser = {
allowEmbedIframes: false,
createdAt: '2025-04-30T00:16:49.716Z',
deleted: false,
disabled: false,
emailNotificationsChatMessage: true,
emailNotificationsCommentOnObservedPost: true,
emailNotificationsFollowingUsers: true,
emailNotificationsGroupMemberJoined: true,
emailNotificationsGroupMemberLeft: true,
emailNotificationsGroupMemberRemoved: true,
emailNotificationsGroupMemberRoleChanged: true,
emailNotificationsMention: true,
emailNotificationsPostInGroup: true,
encryptedPassword: '$2b$10$KOrCHvEB5CM7D.P3VcX2z.pSSBZKZhPqHW/QKym6V1S6fiG..xtBq',
id: 'chatReceiver',
locale: 'en',
name: 'chatReceiver',
role: 'user',
showShoutsPublicly: false,
slug: 'chatreceiver',
termsAndConditionsAgreedAt: '2019-08-01T10:47:19.212Z',
termsAndConditionsAgreedVersion: '0.0.1',
updatedAt: '2025-04-30T00:16:49.716Z',
}
describe('sendChatMessageMail', () => {
describe('English', () => {
beforeEach(() => {
recipientUser.locale = 'en'
})
it('chat_message template', async () => {
await expect(
sendChatMessageMail({
email: 'user@example.org',
senderUser,
recipientUser,
}),
).resolves.toMatchSnapshot()
})
})
describe('German', () => {
beforeEach(() => {
recipientUser.locale = 'de'
})
it('chat_message template', async () => {
await expect(
sendChatMessageMail({
email: 'user@example.org',
senderUser,
recipientUser,
}),
).resolves.toMatchSnapshot()
})
})
})

View File

@ -0,0 +1,204 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
import path from 'node:path'
import Email from 'email-templates'
import { createTransport } from 'nodemailer'
// import type Email as EmailType from '@types/email-templates'
import CONFIG from '@config/index'
import logosWebapp from '@config/logos'
import metadata from '@config/metadata'
import { UserDbProperties } from '@db/types/User'
const hasAuthData = CONFIG.SMTP_USERNAME && CONFIG.SMTP_PASSWORD
const hasDKIMData =
CONFIG.SMTP_DKIM_DOMAINNAME && CONFIG.SMTP_DKIM_KEYSELECTOR && CONFIG.SMTP_DKIM_PRIVATKEY
const welcomeImageUrl = new URL(logosWebapp.LOGO_WELCOME_PATH, CONFIG.CLIENT_URI)
const settingsUrl = new URL('/settings/notifications', CONFIG.CLIENT_URI)
const defaultParams = {
welcomeImageUrl,
APPLICATION_NAME: CONFIG.APPLICATION_NAME,
ORGANIZATION_NAME: metadata.ORGANIZATION_NAME,
ORGANIZATION_URL: CONFIG.ORGANIZATION_URL,
supportUrl: CONFIG.SUPPORT_URL,
settingsUrl,
}
export const transport = createTransport({
host: CONFIG.SMTP_HOST,
port: CONFIG.SMTP_PORT,
ignoreTLS: CONFIG.SMTP_IGNORE_TLS,
secure: CONFIG.SMTP_SECURE, // true for 465, false for other ports
pool: true,
maxConnections: CONFIG.SMTP_MAX_CONNECTIONS,
maxMessages: CONFIG.SMTP_MAX_MESSAGES,
auth: hasAuthData && {
user: CONFIG.SMTP_USERNAME,
pass: CONFIG.SMTP_PASSWORD,
},
dkim: hasDKIMData && {
domainName: CONFIG.SMTP_DKIM_DOMAINNAME,
keySelector: CONFIG.SMTP_DKIM_KEYSELECTOR,
privateKey: CONFIG.SMTP_DKIM_PRIVATKEY,
},
})
const email = new Email({
message: {
from: `${CONFIG.APPLICATION_NAME}`,
},
transport,
i18n: {
locales: ['en', 'de'],
defaultLocale: 'en',
retryInDefaultLocale: false,
directory: path.join(__dirname, 'locales'),
updateFiles: false,
objectNotation: true,
mustacheConfig: {
tags: ['{', '}'],
disable: false,
},
},
send: CONFIG.SEND_MAIL,
preview: false,
// This is very useful to see the emails sent by the unit tests
/*
preview: {
open: {
app: 'brave-browser',
},
},
*/
})
interface OriginalMessage {
to: string
from: string
attachments: string[]
subject: string
html: string
text: string
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const sendNotificationMail = async (notification: any): Promise<OriginalMessage> => {
const locale = notification?.to?.locale
const to = notification?.email
const name = notification?.to?.name
const template = notification?.reason
try {
const { originalMessage } = await email.send({
template: path.join(__dirname, 'templates', template),
message: {
to,
},
locals: {
...defaultParams,
locale,
name,
postTitle:
notification?.from?.__typename === 'Comment'
? notification?.from?.post?.title
: notification?.from?.title,
postUrl: new URL(
notification?.from?.__typename === 'Comment'
? `/post/${notification?.from?.post?.id}/${notification?.from?.post?.slug}`
: `/post/${notification?.from?.id}/${notification?.from?.slug}`,
CONFIG.CLIENT_URI,
),
postAuthorName:
notification?.from?.__typename === 'Comment'
? undefined
: notification?.from?.author?.name,
postAuthorUrl:
notification?.from?.__typename === 'Comment'
? undefined
: new URL(
`user/${notification?.from?.author?.id}/${notification?.from?.author?.slug}`,
CONFIG.CLIENT_URI,
),
commenterName:
notification?.from?.__typename === 'Comment'
? notification?.from?.author?.name
: undefined,
commenterUrl:
notification?.from?.__typename === 'Comment'
? new URL(
`/user/${notification?.from?.author?.id}/${notification?.from?.author?.slug}`,
CONFIG.CLIENT_URI,
)
: undefined,
commentUrl:
notification?.from?.__typename === 'Comment'
? new URL(
`/post/${notification?.from?.post?.id}/${notification?.from?.post?.slug}#commentId-${notification?.from?.id}`,
CONFIG.CLIENT_URI,
)
: undefined,
// chattingUser: 'SR-71',
// chatUrl: new URL('/chat', CONFIG.CLIENT_URI),
groupUrl:
notification?.from?.__typename === 'Group'
? new URL(
`/group/${notification?.from?.id}/${notification?.from?.slug}`,
CONFIG.CLIENT_URI,
)
: undefined,
groupName:
notification?.from?.__typename === 'Group' ? notification?.from?.name : undefined,
groupRelatedUserName:
notification?.from?.__typename === 'Group' ? notification?.relatedUser?.name : undefined,
groupRelatedUserUrl:
notification?.from?.__typename === 'Group'
? new URL(
`/user/${notification?.relatedUser?.id}/${notification?.relatedUser?.slug}`,
CONFIG.CLIENT_URI,
)
: undefined,
},
})
return originalMessage as OriginalMessage
} catch (error) {
throw new Error(error)
}
}
export interface ChatMessageEmailInput {
senderUser: UserDbProperties
recipientUser: UserDbProperties
email: string
}
export const sendChatMessageMail = async (
data: ChatMessageEmailInput,
): Promise<OriginalMessage> => {
const { senderUser, recipientUser } = data
const to = data.email
try {
const { originalMessage } = await email.send({
template: path.join(__dirname, 'templates', 'chat_message'),
message: {
to,
},
locals: {
...defaultParams,
locale: recipientUser.locale,
name: recipientUser.name,
chattingUser: senderUser.name,
chattingUserUrl: new URL(`/user/${senderUser.id}/${senderUser.slug}`, CONFIG.CLIENT_URI),
chatUrl: new URL('/chat', CONFIG.CLIENT_URI),
},
})
return originalMessage as OriginalMessage
} catch (error) {
throw new Error(error)
}
}

View File

@ -0,0 +1,475 @@
import { sendNotificationMail } from './sendEmail'
describe('sendNotificationMail', () => {
let locale = 'en'
describe('English', () => {
beforeEach(() => {
locale = 'en'
})
it('followed_user_posted template', async () => {
await expect(
sendNotificationMail({
reason: 'followed_user_posted',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
id: 'p1',
slug: 'new-post',
title: 'New Post',
author: {
id: 'u2',
name: 'Peter Lustig',
slug: 'peter-lustig',
},
},
}),
).resolves.toMatchSnapshot()
})
it('post_in_group template', async () => {
await expect(
sendNotificationMail({
reason: 'post_in_group',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
id: 'p1',
slug: 'new-post',
title: 'New Post',
author: {
id: 'u2',
name: 'Peter Lustig',
slug: 'peter-lustig',
},
},
}),
).resolves.toMatchSnapshot()
})
it('mentioned_in_post template', async () => {
await expect(
sendNotificationMail({
reason: 'mentioned_in_post',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
id: 'p1',
slug: 'new-post',
title: 'New Post',
author: {
id: 'u2',
name: 'Peter Lustig',
slug: 'peter-lustig',
},
},
}),
).resolves.toMatchSnapshot()
})
it('commented_on_post template', async () => {
await expect(
sendNotificationMail({
reason: 'commented_on_post',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
__typename: 'Comment',
id: 'c1',
slug: 'new-comment',
author: {
id: 'u2',
name: 'Peter Lustig',
slug: 'peter-lustig',
},
post: {
id: 'p1',
slug: 'new-post',
title: 'New Post',
},
},
}),
).resolves.toMatchSnapshot()
})
it('mentioned_in_comment template', async () => {
await expect(
sendNotificationMail({
reason: 'mentioned_in_comment',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
__typename: 'Comment',
id: 'c1',
slug: 'new-comment',
author: {
id: 'u2',
name: 'Peter Lustig',
slug: 'peter-lustig',
},
post: {
id: 'p1',
slug: 'new-post',
title: 'New Post',
},
},
}),
).resolves.toMatchSnapshot()
})
it('changed_group_member_role template', async () => {
await expect(
sendNotificationMail({
reason: 'changed_group_member_role',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
__typename: 'Group',
id: 'g1',
slug: 'the-group',
name: 'The Group',
},
}),
).resolves.toMatchSnapshot()
})
it('user_joined_group template', async () => {
await expect(
sendNotificationMail({
reason: 'user_joined_group',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
__typename: 'Group',
id: 'g1',
slug: 'the-group',
name: 'The Group',
},
relatedUser: {
id: 'u2',
name: 'Peter Lustig',
slug: 'peter-lustig',
},
}),
).resolves.toMatchSnapshot()
})
it('user_left_group template', async () => {
await expect(
sendNotificationMail({
reason: 'user_left_group',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
__typename: 'Group',
id: 'g1',
slug: 'the-group',
name: 'The Group',
},
relatedUser: {
id: 'u2',
name: 'Peter Lustig',
slug: 'peter-lustig',
},
}),
).resolves.toMatchSnapshot()
})
it('removed_user_from_group template', async () => {
await expect(
sendNotificationMail({
reason: 'removed_user_from_group',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
__typename: 'Group',
id: 'g1',
slug: 'the-group',
name: 'The Group',
},
}),
).resolves.toMatchSnapshot()
})
})
describe('German', () => {
beforeEach(() => {
locale = 'de'
})
it('followed_user_posted template', async () => {
await expect(
sendNotificationMail({
reason: 'followed_user_posted',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
id: 'p1',
slug: 'new-post',
title: 'New Post',
author: {
id: 'u2',
name: 'Peter Lustig',
slug: 'peter-lustig',
},
},
}),
).resolves.toMatchSnapshot()
})
it('post_in_group template', async () => {
await expect(
sendNotificationMail({
reason: 'post_in_group',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
id: 'p1',
slug: 'new-post',
title: 'New Post',
author: {
id: 'u2',
name: 'Peter Lustig',
slug: 'peter-lustig',
},
},
}),
).resolves.toMatchSnapshot()
})
it('mentioned_in_post template', async () => {
await expect(
sendNotificationMail({
reason: 'mentioned_in_post',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
id: 'p1',
slug: 'new-post',
title: 'New Post',
author: {
id: 'u2',
name: 'Peter Lustig',
slug: 'peter-lustig',
},
},
}),
).resolves.toMatchSnapshot()
})
it('commented_on_post template', async () => {
await expect(
sendNotificationMail({
reason: 'commented_on_post',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
__typename: 'Comment',
id: 'c1',
slug: 'new-comment',
author: {
id: 'u2',
name: 'Peter Lustig',
slug: 'peter-lustig',
},
post: {
id: 'p1',
slug: 'new-post',
title: 'New Post',
},
},
}),
).resolves.toMatchSnapshot()
})
it('mentioned_in_comment template', async () => {
await expect(
sendNotificationMail({
reason: 'mentioned_in_comment',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
__typename: 'Comment',
id: 'c1',
slug: 'new-comment',
author: {
id: 'u2',
name: 'Peter Lustig',
slug: 'peter-lustig',
},
post: {
id: 'p1',
slug: 'new-post',
title: 'New Post',
},
},
}),
).resolves.toMatchSnapshot()
})
it('changed_group_member_role template', async () => {
await expect(
sendNotificationMail({
reason: 'changed_group_member_role',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
__typename: 'Group',
id: 'g1',
slug: 'the-group',
name: 'The Group',
},
}),
).resolves.toMatchSnapshot()
})
it('user_joined_group template', async () => {
await expect(
sendNotificationMail({
reason: 'user_joined_group',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
__typename: 'Group',
id: 'g1',
slug: 'the-group',
name: 'The Group',
},
relatedUser: {
id: 'u2',
name: 'Peter Lustig',
slug: 'peter-lustig',
},
}),
).resolves.toMatchSnapshot()
})
it('user_left_group template', async () => {
await expect(
sendNotificationMail({
reason: 'user_left_group',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
__typename: 'Group',
id: 'g1',
slug: 'the-group',
name: 'The Group',
},
relatedUser: {
id: 'u2',
name: 'Peter Lustig',
slug: 'peter-lustig',
},
}),
).resolves.toMatchSnapshot()
})
it('removed_user_from_group template', async () => {
await expect(
sendNotificationMail({
reason: 'removed_user_from_group',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
__typename: 'Group',
id: 'g1',
slug: 'the-group',
name: 'The Group',
},
}),
).resolves.toMatchSnapshot()
})
})
})

View File

@ -0,0 +1,7 @@
extend ../layout.pug
block content
.content
- var groupUrl = groupUrl
p= t('changedGroupMemberRole', { groupName })
a.button(href=groupUrl)= t('buttons.viewGroup')

View File

@ -0,0 +1 @@
= `${APPLICATION_NAME} ${t('notification')}: ${t('subjects.changedGroupMemberRole')}`

View File

@ -0,0 +1,8 @@
extend ../layout.pug
block content
.content
p= t('chatMessageStart')
a.user(href=chattingUserUrl)= chattingUser
= t('chatMessageEnd')
a.button(href=chatUrl)= t('buttons.viewChat')

View File

@ -0,0 +1 @@
= `${APPLICATION_NAME} ${t('notification')}: ${t('subjects.chatMessage')}`

View File

@ -0,0 +1,8 @@
extend ../layout.pug
block content
.content
p
a.user(href=commenterUrl)= commenterName
= t('commentedOnPost', { postTitle})
a.button(href=commentUrl)= t('buttons.viewComment')

View File

@ -0,0 +1 @@
= `${APPLICATION_NAME} ${t('notification')}: ${t('subjects.commentedOnPost')}`

View File

@ -0,0 +1,8 @@
extend ../layout.pug
block content
.content
p
a.user(href=postAuthorUrl)= postAuthorName
= t('followedUserPosted', { postTitle })
a.button(href=postUrl)= t('buttons.viewPost')

View File

@ -0,0 +1 @@
= `${APPLICATION_NAME} ${t('notification')}: ${t('subjects.followedUserPosted')}`

View File

@ -0,0 +1,5 @@
footer
.footer
- var organizationUrl = ORGANIZATION_URL
- var organizationName = ORGANIZATION_NAME
a(href=organizationUrl)= organizationName

View File

@ -0,0 +1,14 @@
//- This sets the greeting at the end of every e-mail
.text-block
- var organizationUrl = ORGANIZATION_URL
- var team = APPLICATION_NAME
- var settingsUrl = settingsUrl
p= t('general.seeYou')
a.organization(href=organizationUrl)= team
| !
p= t('general.yourTeam', { team })
br
p= t('general.settingsHint')
a.settings(href=settingsUrl)= t('general.settingsName')
| !

View File

@ -0,0 +1,9 @@
header
.head
- var img = welcomeImageUrl
img.head-logo(
alt="Welcome Image"
loading="lazy"
src=img
)

View File

@ -0,0 +1 @@
h2= `${t('general.greeting')} ${name},`

View File

@ -0,0 +1,65 @@
body{
display: block;
font-family: Lato, sans-serif;
font-size: 17px;
text-align: left;
text-align: -webkit-left;
justify-content: center;
padding: 15px;
margin: 0px;
}
h2 {
margin-top: 25px;
font-size: 25px;
font-weight: normal;
line-height: 22px;
color: #333333;
}
.container {
max-width: 680px;
margin: 0 auto;
display: block;
}
.head-logo {
width: 60%;
height: auto;
display: block;
margin-left: auto;
margin-right: auto;
}
a {
color: #17b53e;
}
a.button {
background: #17b53e;
font-family: Lato, sans-serif;
font-size: 16px;
line-height: 15px;
text-decoration: none;
text-align:center;
padding: 13px 17px;
color: #ffffff;
display: table;
margin-left: auto;
margin-right: auto;
border-radius: 4px;
}
.text-block {
margin-top: 20px;
color: #000000;
}
footer {
padding: 20px;
font-family: Lato, sans-serif;
font-size: 12px;
line-height: 15px;
text-align: center;
color: #888888;
}

View File

@ -0,0 +1,26 @@
doctype html
html(lang=locale)
head
meta(
content="multipart/html; charset=UTF-8"
http-equiv="content-type"
)
meta(
name="viewport"
content="width=device-width, initial-scale=1"
)
style.
.wf-force-outline-none[tabindex="-1"]:focus{outline:none;}
style
include includes/webflow.css
body
div.container
include includes/header.pug
include includes/salutation.pug
.wrapper
block content
include includes/greeting.pug
include includes/footer.pug

View File

@ -0,0 +1,8 @@
extend ../layout.pug
block content
.content
p
a.user(href=commenterUrl)= commenterName
= t('mentionedInComment', { postTitle})
a.button(href=commentUrl)= t('buttons.viewComment')

View File

@ -0,0 +1 @@
= `${APPLICATION_NAME} ${t('notification')}: ${t('subjects.mentionedInComment')}`

View File

@ -0,0 +1,8 @@
extend ../layout.pug
block content
.content
p
a.user(href=postAuthorUrl)= postAuthorName
= t('mentionedInPost', { postTitle })
a.button(href=postUrl)= t('buttons.viewPost')

View File

@ -0,0 +1 @@
= `${APPLICATION_NAME} ${t('notification')}: ${t('subjects.mentionedInPost')}`

View File

@ -0,0 +1,7 @@
extend ../layout.pug
block content
.content
- var postUrl = postUrl
p= t('postInGroup', { postTitle})
a.button(href=postUrl)= t('buttons.viewPost')

View File

@ -0,0 +1 @@
= `${APPLICATION_NAME} ${t('notification')}: ${t('subjects.postInGroup')}`

View File

@ -0,0 +1,5 @@
extend ../layout.pug
block content
.content
p= t('removedUserFromGroup', { groupName })

View File

@ -0,0 +1 @@
= `${APPLICATION_NAME} ${t('notification')}: ${t('subjects.removedUserFromGroup')}`

View File

@ -0,0 +1,8 @@
extend ../layout.pug
block content
.content
p
a.user(href=groupRelatedUserUrl)= groupRelatedUserName
= t('userJoinedGroup', { groupName })
a.button(href=groupUrl)= t('buttons.viewGroup')

View File

@ -0,0 +1 @@
= `${APPLICATION_NAME} ${t('notification')}: ${t('subjects.userJoinedGroup')}`

View File

@ -0,0 +1,8 @@
extend ../layout.pug
block content
.content
p
a.user(href=groupRelatedUserUrl)= groupRelatedUserName
= t('userLeftGroup', { groupName })
a.button(href=groupUrl)= t('buttons.viewGroup')

View File

@ -0,0 +1 @@
= `${APPLICATION_NAME} ${t('notification')}: ${t('subjects.userLeftGroup')}`

View File

@ -16,9 +16,9 @@ import createServer, { getContext } from '@src/server'
CONFIG.CATEGORIES_ACTIVE = false CONFIG.CATEGORIES_ACTIVE = false
const sendMailMock: (notification) => void = jest.fn() const sendNotificationMailMock: (notification) => void = jest.fn()
jest.mock('@middleware/helpers/email/sendMail', () => ({ jest.mock('@src/emails/sendEmail', () => ({
sendMail: (notification) => sendMailMock(notification), sendNotificationMail: (notification) => sendNotificationMailMock(notification),
})) }))
let query, mutate, authenticatedUser, emaillessMember let query, mutate, authenticatedUser, emaillessMember
@ -208,7 +208,13 @@ describe('emails sent for notifications', () => {
}) })
it('sends only one email', () => { it('sends only one email', () => {
expect(sendMailMock).toHaveBeenCalledTimes(1) expect(sendNotificationMailMock).toHaveBeenCalledTimes(1)
expect(sendNotificationMailMock).toHaveBeenCalledWith(
expect.objectContaining({
reason: 'mentioned_in_post',
email: 'group.member@example.org',
}),
)
}) })
it('sends 3 notifications', async () => { it('sends 3 notifications', async () => {
@ -280,7 +286,13 @@ describe('emails sent for notifications', () => {
}) })
it('sends only one email', () => { it('sends only one email', () => {
expect(sendMailMock).toHaveBeenCalledTimes(1) expect(sendNotificationMailMock).toHaveBeenCalledTimes(1)
expect(sendNotificationMailMock).toHaveBeenCalledWith(
expect.objectContaining({
reason: 'followed_user_posted',
email: 'group.member@example.org',
}),
)
}) })
it('sends 3 notifications', async () => { it('sends 3 notifications', async () => {
@ -353,7 +365,13 @@ describe('emails sent for notifications', () => {
}) })
it('sends only one email', () => { it('sends only one email', () => {
expect(sendMailMock).toHaveBeenCalledTimes(1) expect(sendNotificationMailMock).toHaveBeenCalledTimes(1)
expect(sendNotificationMailMock).toHaveBeenCalledWith(
expect.objectContaining({
reason: 'post_in_group',
email: 'group.member@example.org',
}),
)
}) })
it('sends 3 notifications', async () => { it('sends 3 notifications', async () => {
@ -427,7 +445,7 @@ describe('emails sent for notifications', () => {
}) })
it('sends NO email', () => { it('sends NO email', () => {
expect(sendMailMock).not.toHaveBeenCalled() expect(sendNotificationMailMock).not.toHaveBeenCalled()
}) })
it('sends 3 notifications', async () => { it('sends 3 notifications', async () => {
@ -521,7 +539,13 @@ describe('emails sent for notifications', () => {
}) })
it('sends only one email', () => { it('sends only one email', () => {
expect(sendMailMock).toHaveBeenCalledTimes(1) expect(sendNotificationMailMock).toHaveBeenCalledTimes(1)
expect(sendNotificationMailMock).toHaveBeenCalledWith(
expect.objectContaining({
reason: 'mentioned_in_comment',
email: 'group.member@example.org',
}),
)
}) })
it('sends 2 notifications', async () => { it('sends 2 notifications', async () => {
@ -603,7 +627,13 @@ describe('emails sent for notifications', () => {
}) })
it('sends only one email', () => { it('sends only one email', () => {
expect(sendMailMock).toHaveBeenCalledTimes(1) expect(sendNotificationMailMock).toHaveBeenCalledTimes(1)
expect(sendNotificationMailMock).toHaveBeenCalledWith(
expect.objectContaining({
reason: 'mentioned_in_comment',
email: 'group.member@example.org',
}),
)
}) })
it('sends 2 notifications', async () => { it('sends 2 notifications', async () => {
@ -686,7 +716,7 @@ describe('emails sent for notifications', () => {
}) })
it('sends NO email', () => { it('sends NO email', () => {
expect(sendMailMock).not.toHaveBeenCalled() expect(sendNotificationMailMock).not.toHaveBeenCalled()
}) })
it('sends 2 notifications', async () => { it('sends 2 notifications', async () => {

View File

@ -14,9 +14,9 @@ import createServer, { getContext } from '@src/server'
CONFIG.CATEGORIES_ACTIVE = false CONFIG.CATEGORIES_ACTIVE = false
const sendMailMock: (notification) => void = jest.fn() const sendNotificationMailMock: (notification) => void = jest.fn()
jest.mock('@middleware/helpers/email/sendMail', () => ({ jest.mock('@src/emails/sendEmail', () => ({
sendMail: (notification) => sendMailMock(notification), sendNotificationMail: (notification) => sendNotificationMailMock(notification),
})) }))
let query, mutate, authenticatedUser let query, mutate, authenticatedUser
@ -268,17 +268,17 @@ describe('following users notifications', () => {
}) })
it('sends only two emails, as second follower has emails disabled and email-less follower has no email', () => { it('sends only two emails, as second follower has emails disabled and email-less follower has no email', () => {
expect(sendMailMock).toHaveBeenCalledTimes(2) expect(sendNotificationMailMock).toHaveBeenCalledTimes(2)
expect(sendMailMock).toHaveBeenCalledWith( expect(sendNotificationMailMock).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
html: expect.stringContaining('Hello First Follower'), email: 'first-follower@example.org',
to: 'first-follower@example.org', reason: 'followed_user_posted',
}), }),
) )
expect(sendMailMock).toHaveBeenCalledWith( expect(sendNotificationMailMock).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
html: expect.stringContaining('Hello Third Follower'), email: 'third-follower@example.org',
to: 'third-follower@example.org', reason: 'followed_user_posted',
}), }),
) )
}) })

View File

@ -17,9 +17,9 @@ import createServer, { getContext } from '@src/server'
CONFIG.CATEGORIES_ACTIVE = false CONFIG.CATEGORIES_ACTIVE = false
const sendMailMock: (notification) => void = jest.fn() const sendNotificationMailMock: (notification) => void = jest.fn()
jest.mock('@middleware/helpers/email/sendMail', () => ({ jest.mock('@src/emails/sendEmail', () => ({
sendMail: (notification) => sendMailMock(notification), sendNotificationMail: (notification) => sendNotificationMailMock(notification),
})) }))
let query, mutate, authenticatedUser let query, mutate, authenticatedUser
@ -394,7 +394,25 @@ describe('mentions in groups', () => {
}) })
it('sends only 3 emails, one for each user with an email', () => { it('sends only 3 emails, one for each user with an email', () => {
expect(sendMailMock).toHaveBeenCalledTimes(3) expect(sendNotificationMailMock).toHaveBeenCalledTimes(3)
expect(sendNotificationMailMock).toHaveBeenCalledWith(
expect.objectContaining({
email: 'group.member@example.org',
reason: 'mentioned_in_post',
}),
)
expect(sendNotificationMailMock).toHaveBeenCalledWith(
expect.objectContaining({
email: 'no.member@example.org',
reason: 'mentioned_in_post',
}),
)
expect(sendNotificationMailMock).toHaveBeenCalledWith(
expect.objectContaining({
email: 'pending.member@example.org',
reason: 'mentioned_in_post',
}),
)
}) })
}) })
@ -490,7 +508,13 @@ describe('mentions in groups', () => {
}) })
it('sends only 1 email', () => { it('sends only 1 email', () => {
expect(sendMailMock).toHaveBeenCalledTimes(1) expect(sendNotificationMailMock).toHaveBeenCalledTimes(1)
expect(sendNotificationMailMock).toHaveBeenCalledWith(
expect.objectContaining({
email: 'group.member@example.org',
reason: 'mentioned_in_post',
}),
)
}) })
}) })
@ -586,7 +610,13 @@ describe('mentions in groups', () => {
}) })
it('sends only 1 email', () => { it('sends only 1 email', () => {
expect(sendMailMock).toHaveBeenCalledTimes(1) expect(sendNotificationMailMock).toHaveBeenCalledTimes(1)
expect(sendNotificationMailMock).toHaveBeenCalledWith(
expect.objectContaining({
email: 'group.member@example.org',
reason: 'mentioned_in_post',
}),
)
}) })
}) })
@ -670,7 +700,19 @@ describe('mentions in groups', () => {
}) })
it('sends 2 emails', () => { it('sends 2 emails', () => {
expect(sendMailMock).toHaveBeenCalledTimes(3) expect(sendNotificationMailMock).toHaveBeenCalledTimes(3)
expect(sendNotificationMailMock).toHaveBeenCalledWith(
expect.objectContaining({
email: 'group.member@example.org',
reason: 'mentioned_in_comment',
}),
)
expect(sendNotificationMailMock).toHaveBeenCalledWith(
expect.objectContaining({
email: 'no.member@example.org',
reason: 'mentioned_in_comment',
}),
)
}) })
}) })
@ -761,7 +803,13 @@ describe('mentions in groups', () => {
}) })
it('sends 1 email', () => { it('sends 1 email', () => {
expect(sendMailMock).toHaveBeenCalledTimes(1) expect(sendNotificationMailMock).toHaveBeenCalledTimes(1)
expect(sendNotificationMailMock).toHaveBeenCalledWith(
expect.objectContaining({
email: 'group.member@example.org',
reason: 'mentioned_in_comment',
}),
)
}) })
}) })
@ -852,7 +900,13 @@ describe('mentions in groups', () => {
}) })
it('sends 1 email', () => { it('sends 1 email', () => {
expect(sendMailMock).toHaveBeenCalledTimes(1) expect(sendNotificationMailMock).toHaveBeenCalledTimes(1)
expect(sendNotificationMailMock).toHaveBeenCalledWith(
expect.objectContaining({
email: 'group.member@example.org',
reason: 'mentioned_in_comment',
}),
)
}) })
}) })
}) })

View File

@ -14,9 +14,9 @@ import createServer, { getContext } from '@src/server'
CONFIG.CATEGORIES_ACTIVE = false CONFIG.CATEGORIES_ACTIVE = false
const sendMailMock: (notification) => void = jest.fn() const sendNotificationMailMock: (notification) => void = jest.fn()
jest.mock('@middleware/helpers/email/sendMail', () => ({ jest.mock('@src/emails/sendEmail', () => ({
sendMail: (notification) => sendMailMock(notification), sendNotificationMail: (notification) => sendNotificationMailMock(notification),
})) }))
let query, mutate, authenticatedUser let query, mutate, authenticatedUser
@ -213,10 +213,11 @@ describe('notifications for users that observe a post', () => {
}) })
it('sends one email', () => { it('sends one email', () => {
expect(sendMailMock).toHaveBeenCalledTimes(1) expect(sendNotificationMailMock).toHaveBeenCalledTimes(1)
expect(sendMailMock).toHaveBeenCalledWith( expect(sendNotificationMailMock).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
to: 'post-author@example.org', email: 'post-author@example.org',
reason: 'commented_on_post',
}), }),
) )
}) })
@ -303,15 +304,17 @@ describe('notifications for users that observe a post', () => {
}) })
it('sends two emails', () => { it('sends two emails', () => {
expect(sendMailMock).toHaveBeenCalledTimes(2) expect(sendNotificationMailMock).toHaveBeenCalledTimes(2)
expect(sendMailMock).toHaveBeenCalledWith( expect(sendNotificationMailMock).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
to: 'post-author@example.org', email: 'post-author@example.org',
reason: 'commented_on_post',
}), }),
) )
expect(sendMailMock).toHaveBeenCalledWith( expect(sendNotificationMailMock).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
to: 'first-commenter@example.org', email: 'first-commenter@example.org',
reason: 'commented_on_post',
}), }),
) )
}) })
@ -417,10 +420,11 @@ describe('notifications for users that observe a post', () => {
}) })
it('sends one email', () => { it('sends one email', () => {
expect(sendMailMock).toHaveBeenCalledTimes(1) expect(sendNotificationMailMock).toHaveBeenCalledTimes(1)
expect(sendMailMock).toHaveBeenCalledWith( expect(sendNotificationMailMock).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
to: 'second-commenter@example.org', email: 'second-commenter@example.org',
reason: 'commented_on_post',
}), }),
) )
}) })

View File

@ -12,9 +12,9 @@ import createServer, { getContext } from '@src/server'
CONFIG.CATEGORIES_ACTIVE = false CONFIG.CATEGORIES_ACTIVE = false
const sendMailMock: (notification) => void = jest.fn() const sendNotificationMailMock: (notification) => void = jest.fn()
jest.mock('@middleware/helpers/email/sendMail', () => ({ jest.mock('@src/emails/sendEmail', () => ({
sendMail: (notification) => sendMailMock(notification), sendNotificationMail: (notification) => sendNotificationMailMock(notification),
})) }))
let isUserOnlineMock = jest.fn().mockReturnValue(false) let isUserOnlineMock = jest.fn().mockReturnValue(false)
@ -109,7 +109,7 @@ describe('online status and sending emails', () => {
}) })
it('sends NO email to the other user', () => { it('sends NO email to the other user', () => {
expect(sendMailMock).not.toBeCalled() expect(sendNotificationMailMock).not.toBeCalled()
}) })
}) })
}) })
@ -135,7 +135,7 @@ describe('online status and sending emails', () => {
}) })
it('sends email to the other user', () => { it('sends email to the other user', () => {
expect(sendMailMock).toBeCalledTimes(1) expect(sendNotificationMailMock).toBeCalledTimes(1)
}) })
}) })
}) })

View File

@ -17,9 +17,9 @@ import createServer, { getContext } from '@src/server'
CONFIG.CATEGORIES_ACTIVE = false CONFIG.CATEGORIES_ACTIVE = false
const sendMailMock: (notification) => void = jest.fn() const sendNotificationMailMock: (notification) => void = jest.fn()
jest.mock('@middleware/helpers/email/sendMail', () => ({ jest.mock('@src/emails/sendEmail', () => ({
sendMail: (notification) => sendMailMock(notification), sendNotificationMail: (notification) => sendNotificationMailMock(notification),
})) }))
let query, mutate, authenticatedUser let query, mutate, authenticatedUser
@ -137,7 +137,7 @@ describe('notify group members of new posts in group', () => {
slug: 'group-member', slug: 'group-member',
}, },
{ {
email: 'test2@example.org', email: 'group.member@example.org',
password: '1234', password: '1234',
}, },
) )
@ -295,7 +295,13 @@ describe('notify group members of new posts in group', () => {
}) })
it('sends one email', () => { it('sends one email', () => {
expect(sendMailMock).toHaveBeenCalledTimes(1) expect(sendNotificationMailMock).toHaveBeenCalledTimes(1)
expect(sendNotificationMailMock).toHaveBeenCalledWith(
expect.objectContaining({
reason: 'post_in_group',
email: 'group.member@example.org',
}),
)
}) })
describe('group member mutes group', () => { describe('group member mutes group', () => {
@ -337,7 +343,7 @@ describe('notify group members of new posts in group', () => {
}) })
it('sends NO email', () => { it('sends NO email', () => {
expect(sendMailMock).not.toHaveBeenCalled() expect(sendNotificationMailMock).not.toHaveBeenCalled()
}) })
describe('group member unmutes group again but disables email', () => { describe('group member unmutes group again but disables email', () => {
@ -392,7 +398,7 @@ describe('notify group members of new posts in group', () => {
}) })
it('sends NO email', () => { it('sends NO email', () => {
expect(sendMailMock).not.toHaveBeenCalled() expect(sendNotificationMailMock).not.toHaveBeenCalled()
}) })
}) })
}) })
@ -433,7 +439,7 @@ describe('notify group members of new posts in group', () => {
}) })
it('sends NO email', () => { it('sends NO email', () => {
expect(sendMailMock).not.toHaveBeenCalled() expect(sendNotificationMailMock).not.toHaveBeenCalled()
}) })
}) })
@ -473,7 +479,7 @@ describe('notify group members of new posts in group', () => {
}) })
it('sends NO email', () => { it('sends NO email', () => {
expect(sendMailMock).not.toHaveBeenCalled() expect(sendNotificationMailMock).not.toHaveBeenCalled()
}) })
}) })
}) })

View File

@ -20,16 +20,11 @@ import { leaveGroupMutation } from '@graphql/queries/leaveGroupMutation'
import { removeUserFromGroupMutation } from '@graphql/queries/removeUserFromGroupMutation' import { removeUserFromGroupMutation } from '@graphql/queries/removeUserFromGroupMutation'
import createServer, { getContext } from '@src/server' import createServer, { getContext } from '@src/server'
const sendMailMock: (notification) => void = jest.fn() const sendChatMessageMailMock: (notification) => void = jest.fn()
jest.mock('@middleware/helpers/email/sendMail', () => ({ const sendNotificationMailMock: (notification) => void = jest.fn()
sendMail: (notification) => sendMailMock(notification), jest.mock('@src/emails/sendEmail', () => ({
})) sendChatMessageMail: (notification) => sendChatMessageMailMock(notification),
sendNotificationMail: (notification) => sendNotificationMailMock(notification),
const chatMessageTemplateMock = jest.fn()
const notificationTemplateMock = jest.fn()
jest.mock('../helpers/email/templateBuilder', () => ({
chatMessageTemplate: () => chatMessageTemplateMock(),
notificationTemplate: () => notificationTemplateMock(),
})) }))
let isUserOnlineMock = jest.fn() let isUserOnlineMock = jest.fn()
@ -240,8 +235,13 @@ describe('notifications', () => {
) )
// Mail // Mail
expect(sendMailMock).toHaveBeenCalledTimes(1) expect(sendNotificationMailMock).toHaveBeenCalledTimes(1)
expect(notificationTemplateMock).toHaveBeenCalledTimes(1) expect(sendNotificationMailMock).toHaveBeenCalledWith(
expect.objectContaining({
reason: 'commented_on_post',
email: 'test@example.org',
}),
)
}) })
describe('if I have disabled `emailNotificationsCommentOnObservedPost`', () => { describe('if I have disabled `emailNotificationsCommentOnObservedPost`', () => {
@ -276,8 +276,7 @@ describe('notifications', () => {
) )
// No Mail // No Mail
expect(sendMailMock).not.toHaveBeenCalled() expect(sendNotificationMailMock).not.toHaveBeenCalled()
expect(notificationTemplateMock).not.toHaveBeenCalled()
}) })
}) })
@ -398,8 +397,13 @@ describe('notifications', () => {
}) })
// Mail // Mail
expect(sendMailMock).toHaveBeenCalledTimes(1) expect(sendNotificationMailMock).toHaveBeenCalledTimes(1)
expect(notificationTemplateMock).toHaveBeenCalledTimes(1) expect(sendNotificationMailMock).toHaveBeenCalledWith(
expect.objectContaining({
reason: 'mentioned_in_post',
email: 'test@example.org',
}),
)
}) })
describe('if I have disabled `emailNotificationsMention`', () => { describe('if I have disabled `emailNotificationsMention`', () => {
@ -434,8 +438,7 @@ describe('notifications', () => {
}) })
// Mail // Mail
expect(sendMailMock).not.toHaveBeenCalled() expect(sendNotificationMailMock).not.toHaveBeenCalled()
expect(notificationTemplateMock).not.toHaveBeenCalled()
}) })
}) })
@ -941,8 +944,7 @@ describe('notifications', () => {
userId: 'chatReceiver', userId: 'chatReceiver',
}) })
expect(sendMailMock).not.toHaveBeenCalled() expect(sendChatMessageMailMock).not.toHaveBeenCalled()
expect(chatMessageTemplateMock).not.toHaveBeenCalled()
}) })
}) })
@ -977,8 +979,20 @@ describe('notifications', () => {
userId: 'chatReceiver', userId: 'chatReceiver',
}) })
expect(sendMailMock).toHaveBeenCalledTimes(1) expect(sendChatMessageMailMock).toHaveBeenCalledTimes(1)
expect(chatMessageTemplateMock).toHaveBeenCalledTimes(1) expect(sendChatMessageMailMock).toHaveBeenCalledWith({
email: 'user@example.org',
senderUser: expect.objectContaining({
name: 'chatSender',
slug: 'chatsender',
id: 'chatSender',
}),
recipientUser: expect.objectContaining({
name: 'chatReceiver',
slug: 'chatreceiver',
id: 'chatReceiver',
}),
})
}) })
}) })
@ -998,8 +1012,7 @@ describe('notifications', () => {
expect(pubsubSpy).not.toHaveBeenCalled() expect(pubsubSpy).not.toHaveBeenCalled()
expect(pubsubSpy).not.toHaveBeenCalled() expect(pubsubSpy).not.toHaveBeenCalled()
expect(sendMailMock).not.toHaveBeenCalled() expect(sendChatMessageMailMock).not.toHaveBeenCalled()
expect(chatMessageTemplateMock).not.toHaveBeenCalled()
}) })
}) })
@ -1019,8 +1032,7 @@ describe('notifications', () => {
expect(pubsubSpy).not.toHaveBeenCalled() expect(pubsubSpy).not.toHaveBeenCalled()
expect(pubsubSpy).not.toHaveBeenCalled() expect(pubsubSpy).not.toHaveBeenCalled()
expect(sendMailMock).not.toHaveBeenCalled() expect(sendChatMessageMailMock).not.toHaveBeenCalled()
expect(chatMessageTemplateMock).not.toHaveBeenCalled()
}) })
}) })
@ -1056,8 +1068,7 @@ describe('notifications', () => {
userId: 'chatReceiver', userId: 'chatReceiver',
}) })
expect(sendMailMock).not.toHaveBeenCalled() expect(sendChatMessageMailMock).not.toHaveBeenCalled()
expect(chatMessageTemplateMock).not.toHaveBeenCalled()
}) })
}) })
}) })
@ -1137,8 +1148,13 @@ describe('notifications', () => {
}) })
// Mail // Mail
expect(sendMailMock).toHaveBeenCalledTimes(1) expect(sendNotificationMailMock).toHaveBeenCalledTimes(1)
expect(notificationTemplateMock).toHaveBeenCalledTimes(1) expect(sendNotificationMailMock).toHaveBeenCalledWith(
expect.objectContaining({
reason: 'user_joined_group',
email: 'owner@example.org',
}),
)
}) })
describe('if the group owner has disabled `emailNotificationsGroupMemberJoined`', () => { describe('if the group owner has disabled `emailNotificationsGroupMemberJoined`', () => {
@ -1170,8 +1186,7 @@ describe('notifications', () => {
}) })
// Mail // Mail
expect(sendMailMock).not.toHaveBeenCalled() expect(sendNotificationMailMock).not.toHaveBeenCalled()
expect(notificationTemplateMock).not.toHaveBeenCalled()
}) })
}) })
}) })
@ -1240,8 +1255,19 @@ describe('notifications', () => {
}) })
// Mail // Mail
expect(sendMailMock).toHaveBeenCalledTimes(2) expect(sendNotificationMailMock).toHaveBeenCalledTimes(2)
expect(notificationTemplateMock).toHaveBeenCalledTimes(2) expect(sendNotificationMailMock).toHaveBeenCalledWith(
expect.objectContaining({
reason: 'user_joined_group',
email: 'owner@example.org',
}),
)
expect(sendNotificationMailMock).toHaveBeenCalledWith(
expect.objectContaining({
reason: 'user_left_group',
email: 'owner@example.org',
}),
)
}) })
describe('if the group owner has disabled `emailNotificationsGroupMemberLeft`', () => { describe('if the group owner has disabled `emailNotificationsGroupMemberLeft`', () => {
@ -1285,8 +1311,7 @@ describe('notifications', () => {
}) })
// Mail // Mail
expect(sendMailMock).toHaveBeenCalledTimes(1) expect(sendNotificationMailMock).toHaveBeenCalledTimes(1)
expect(notificationTemplateMock).toHaveBeenCalledTimes(1)
}) })
}) })
}) })
@ -1345,8 +1370,13 @@ describe('notifications', () => {
}) })
// Mail // Mail
expect(sendMailMock).toHaveBeenCalledTimes(1) expect(sendNotificationMailMock).toHaveBeenCalledTimes(1)
expect(notificationTemplateMock).toHaveBeenCalledTimes(1) expect(sendNotificationMailMock).toHaveBeenCalledWith(
expect.objectContaining({
reason: 'changed_group_member_role',
email: 'test@example.org',
}),
)
}) })
describe('if the group member has disabled `emailNotificationsGroupMemberRoleChanged`', () => { describe('if the group member has disabled `emailNotificationsGroupMemberRoleChanged`', () => {
@ -1378,8 +1408,7 @@ describe('notifications', () => {
}) })
// Mail // Mail
expect(sendMailMock).not.toHaveBeenCalled() expect(sendNotificationMailMock).not.toHaveBeenCalled()
expect(notificationTemplateMock).not.toHaveBeenCalled()
}) })
}) })
}) })
@ -1437,8 +1466,13 @@ describe('notifications', () => {
}) })
// Mail // Mail
expect(sendMailMock).toHaveBeenCalledTimes(1) expect(sendNotificationMailMock).toHaveBeenCalledTimes(1)
expect(notificationTemplateMock).toHaveBeenCalledTimes(1) expect(sendNotificationMailMock).toHaveBeenCalledWith(
expect.objectContaining({
reason: 'removed_user_from_group',
email: 'test@example.org',
}),
)
}) })
describe('if the previous group member has disabled `emailNotificationsGroupMemberRemoved`', () => { describe('if the previous group member has disabled `emailNotificationsGroupMemberRemoved`', () => {
@ -1470,8 +1504,7 @@ describe('notifications', () => {
}) })
// Mail // Mail
expect(sendMailMock).not.toHaveBeenCalled() expect(sendNotificationMailMock).not.toHaveBeenCalled()
expect(notificationTemplateMock).not.toHaveBeenCalled()
}) })
}) })
}) })

View File

@ -10,13 +10,9 @@ import {
CHAT_MESSAGE_ADDED, CHAT_MESSAGE_ADDED,
} from '@constants/subscriptions' } from '@constants/subscriptions'
import { getUnreadRoomsCount } from '@graphql/resolvers/rooms' import { getUnreadRoomsCount } from '@graphql/resolvers/rooms'
import { sendMail } from '@middleware/helpers/email/sendMail'
import {
chatMessageTemplate,
notificationTemplate,
} from '@middleware/helpers/email/templateBuilder'
import { isUserOnline } from '@middleware/helpers/isUserOnline' import { isUserOnline } from '@middleware/helpers/isUserOnline'
import { validateNotifyUsers } from '@middleware/validation/validationMiddleware' import { validateNotifyUsers } from '@middleware/validation/validationMiddleware'
import { sendNotificationMail, sendChatMessageMail } from '@src/emails/sendEmail'
import extractMentionedUsers from './mentions/extractMentionedUsers' import extractMentionedUsers from './mentions/extractMentionedUsers'
@ -35,12 +31,7 @@ const publishNotifications = async (
!isUserOnline(notificationAdded.to) && !isUserOnline(notificationAdded.to) &&
!emailsSent.includes(notificationAdded.email) !emailsSent.includes(notificationAdded.email)
) { ) {
sendMail( void sendNotificationMail(notificationAdded)
notificationTemplate({
email: notificationAdded.email,
variables: { notification: notificationAdded },
}),
)
emailsSent.push(notificationAdded.email) emailsSent.push(notificationAdded.email)
} }
}) })
@ -496,7 +487,7 @@ const handleCreateMessage = async (resolve, root, args, context, resolveInfo) =>
// Send EMail if we found a user(not blocked) and he is not considered online // Send EMail if we found a user(not blocked) and he is not considered online
if (recipientUser.emailNotificationsChatMessage !== false && !isUserOnline(recipientUser)) { if (recipientUser.emailNotificationsChatMessage !== false && !isUserOnline(recipientUser)) {
void sendMail(chatMessageTemplate({ email, variables: { senderUser, recipientUser } })) void sendChatMessageMail({ email, senderUser, recipientUser })
} }
} }

File diff suppressed because it is too large Load Diff