If you did not request this, you can ignore this email.
-email-subject-forgotPassword = Password Reset Request
-
-email-notification-template-ban =
+email-subject-accountNotificationBan = Your account has been banned
+email-template-accountNotificationBan =
{ $customMessage }
If you think this has been done in error, please contact our community team
at { $organizationContactEmail }.
-email-subject-ban = Your account has been banned
-
-email-notification-template-passwordChange =
+email-subject-accountNotificationPasswordChange = Your password has been changed
+email-template-accountNotificationPasswordChange =
Hello { $username },
The password on your account has been changed.
If you did not request this change,
please contact our community team at { $organizationContactEmail }.
-email-subject-passwordChange = Your password has been changed
-
-email-subject-updateUsername = Your username has been changed
-
-email-notification-template-updateUsername =
+email-subject-accountNotificationUpdateUsername = Your username has been changed
+email-template-accountNotificationUpdateUsername =
Hello { $username },
Thank you for updating your { $organizationName } commenter account information. The changes you made are effective immediately.
If you did not make this change please reach out to our community team at { $organizationContactEmail }.
-email-notification-template-suspend =
+email-subject-accountNotificationSuspend = Your account has been suspended
+email-template-accountNotificationSuspend =
{ $customMessage }
If you think this has been done in error, please contact our community team
at { $organizationContactEmail }.
-email-subject-suspend = Your account has been suspended
-
-email-notification-template-confirmEmail =
+email-subject-accountNotificationConfirmEmail = Confirm Email
+email-template-accountNotificationConfirmEmail =
Hello { $username },
To confirm your email address for use with your commenting account at { $organizationName },
please follow this link: Click here to confirm your email
If you did not recently create a commenting account with
{ $organizationName }, you can safely ignore this email.
-email-subject-confirmEmail = Confirm Email
-
-email-subject-invite = Coral Team invite
-
-email-notification-template-invite =
+email-subject-accountNotificationInvite = Coral Team invite
+email-template-accountNotificationInvite =
You have been invited to join the { $organizationName } team on Coral. Finish
setting up your account here.
-email-subject-downloadComments = Your comments are ready for download
-email-notification-template-downloadComments =
+email-subject-accountNotificationDownloadComments = Your comments are ready for download
+email-template-accountNotificationDownloadComments =
Your comments from { $organizationName } as of { $date } are now available for download.
Download my comment archive
-email-subject-deleteRequestConfirmation =
+email-subject-accountNotificationDeleteRequestConfirmation =
Your commenter account is scheduled to be deleted
-email-notification-template-deleteRequestConfirmation =
+email-template-accountNotificationDeleteRequestConfirmation =
A request to delete your commenter account was received.
Your account is scheduled for deletion on { $requestDate }.
After that time all of your comments will be removed from the site,
@@ -69,15 +64,15 @@ email-notification-template-deleteRequestConfirmation =
If you change your mind you can sign into your account and cancel the
request before your scheduled account deletion time.
-email-subject-deleteRequestCancel =
+email-subject-accountNotificationDeleteRequestCancel =
Your account deletion request has been cancelled
-email-notification-template-deleteRequestCancel =
+email-template-accountNotificationDeleteRequestCancel =
You have cancelled your account deletion request for { $organizationName }.
Your account is now reactivated.
-email-subject-deleteRequestCompleted =
+email-subject-accountNotificationDeleteRequestCompleted =
Your account has been deleted
-email-notification-template-deleteRequestCompleted =
+email-template-accountNotificationDeleteRequestCompleted =
Your commenter account for { $organizationName } is now deleted. We're sorry to
see you go!
If you'd like to re-join the discussion in the future, you can sign up for
@@ -86,3 +81,25 @@ email-notification-template-deleteRequestCompleted =
the commenting experience better, please email us at
{ $organizationContactEmail }.
+# Notification
+
+email-footer-notification =
+ Sent by { $organizationName } - Unsubscribe from these notifications
+
+## On Reply
+
+email-subject-notificationOnReply = Someone has replied to your comment on { $organizationName }
+email-template-notificationOnReply =
+ { $organizationName } - { $storyTitle }
+ { $authorUsername } has replied to your comment: View comment
+
+## On Staff Reply
+
+email-subject-notificationOnStaffReply = Someone at { $organizationName } has replied to your comment
+email-template-notificationOnStaffReply =
+ { $organizationName } - { $storyTitle }
+ { $authorUsername } works for { $organizationName } and has replied to your comment: View comment
+
+# Notification Digest
+
+email-subject-notificationDigest = Your latest comment activity at { $organizationName }
diff --git a/src/core/server/locales/pt-BR/email.ftl b/src/core/server/locales/pt-BR/email.ftl
index 08adad66b..bc4677c8d 100644
--- a/src/core/server/locales/pt-BR/email.ftl
+++ b/src/core/server/locales/pt-BR/email.ftl
@@ -1,47 +1,45 @@
-email-notification-footer =
+# Account Notifications
+
+email-footer-accountNotification =
Enviado por { $organizationName }
-email-notification-template-forgotPassword =
+email-subject-accountNotificationForgotPassword = Pedido de Redefinição de Senha
+email-template-accountNotificationForgotPassword =
Olá { $username },
Se você não solicitou isso, ignore este e-mail.
-email-subject-forgotPassword = Pedido de Redefinição de Senha
-
-email-notification-template-ban =
+email-subject-accountNotificationBan = Sua conta foi banida
+email-template-accountNotificationBan =
Olá { $username },
Se você não solicitou essa alteração,
entre em contato com nossa equipe da comunidade em { $organizationContactEmail }.
-email-subject-passwordChange = Sua senha foi alterada
-
-email-notification-template-suspend =
+email-subject-accountNotificationSuspend = A sua conta foi suspensa
+email-template-accountNotificationSuspend =
Olá { $username },
Em concordância com as diretrizes da comunidade { $organizationName }, sua
  conta foi temporariamente suspensa. Durante a suspensão, você será
-  incapaz de comentar, interagir ou se envolver com outros comentários. Por favor, junte-se a
+  incapaz de comentar, interagir ou se envolver com outros comentários. Por favor, junte-se a
nós em { $until }.
Se você acha que isso foi feito por engano, entre em contato com nossa equipe em
{ $organizationContactEmail }.
-email-subject-suspend = A sua conta foi suspensa
-
-email-notification-template-confirmEmail =
+email-subject-accountNotificationConfirmEmail = Confirmar e-mail
+email-template-accountNotificationConfirmEmail =
Olá { $username },
Para confirmar seu endereço de e-mail para usar sua conta nos comentários em { $organizationName }
Clique aqui
Se você não criou recentemente uma conta de comentários em
{ $organizationName }, você pode ignorar este email.
-email-subject-confirmEmail = Confirmar e-mail
diff --git a/src/core/server/models/story/helpers.ts b/src/core/server/models/story/helpers.ts
new file mode 100644
index 000000000..cab52a844
--- /dev/null
+++ b/src/core/server/models/story/helpers.ts
@@ -0,0 +1,17 @@
+import { URL } from "url";
+
+import { parseQuery, stringifyQuery } from "coral-common/utils";
+
+/**
+ * getURLWithCommentID returns the url with the comment id.
+ *
+ * @param storyURL url of the story
+ * @param commentID id of the comment
+ */
+export function getURLWithCommentID(storyURL: string, commentID?: string) {
+ const url = new URL(storyURL);
+ const query = parseQuery(url.search);
+ url.search = stringifyQuery({ ...query, commentID });
+
+ return url.toString();
+}
diff --git a/src/core/server/models/story/index.ts b/src/core/server/models/story/index.ts
index 436344759..02165e088 100644
--- a/src/core/server/models/story/index.ts
+++ b/src/core/server/models/story/index.ts
@@ -26,8 +26,8 @@ import {
StoryCommentCounts,
} from "./counts";
-// Export everything under counts.
export * from "./counts";
+export * from "./helpers";
const collection = createCollection("stories");
diff --git a/src/core/server/models/user/helpers.ts b/src/core/server/models/user/helpers.ts
index b4fec4940..cec072dae 100644
--- a/src/core/server/models/user/helpers.ts
+++ b/src/core/server/models/user/helpers.ts
@@ -14,7 +14,7 @@ export function roleIsStaff(role: GQLUSER_ROLE) {
return false;
}
-export function userIsStaff(user: Pick) {
+export function hasStaffRole(user: Pick) {
return roleIsStaff(user.role);
}
diff --git a/src/core/server/models/user/user.ts b/src/core/server/models/user/user.ts
index 014172373..bf47c4acb 100644
--- a/src/core/server/models/user/user.ts
+++ b/src/core/server/models/user/user.ts
@@ -19,10 +19,12 @@ import {
} from "coral-server/errors";
import {
GQLBanStatus,
+ GQLDIGEST_FREQUENCY,
GQLSuspensionStatus,
GQLTimeRange,
GQLUSER_ROLE,
GQLUsernameStatus,
+ GQLUserNotificationSettings,
} from "coral-server/graph/tenant/schema/__generated__/types";
import logger from "coral-server/logger";
import {
@@ -35,6 +37,7 @@ import {
resolveConnection,
} from "coral-server/models/helpers";
import { TenantResource } from "coral-server/models/tenant";
+import { DigestibleTemplate } from "coral-server/queue/tasks/mailer/templates";
import { getLocalProfile, hasLocalProfile } from "./helpers";
@@ -268,6 +271,24 @@ export interface IgnoredUser {
createdAt: Date;
}
+/**
+ * Digest is the actual digest entry that is created every time a digest is
+ * queued for aUser.
+ */
+export interface Digest {
+ /**
+ * template is a given digestable template that was generated during the
+ * notification processing phase. This contains the context typically provided
+ * to the individual notification emails that are sent.
+ */
+ template: DigestibleTemplate;
+
+ /**
+ * createdAt is the date that the digest entry was created at.
+ */
+ createdAt: Date;
+}
+
/**
* User is someone that leaves Comments, and logs in.
*/
@@ -324,6 +345,17 @@ export interface User extends TenantResource {
*/
role: GQLUSER_ROLE;
+ /**
+ * notifications stores the notification settings for the given User.
+ */
+ notifications: GQLUserNotificationSettings;
+
+ /**
+ * digests stores all the notification digests on the User that are scheduled
+ * to be sent out based on the User's notification preferences.
+ */
+ digests: Digest[];
+
/**
* status stores the user status information regarding moderation state.
*/
@@ -442,6 +474,8 @@ export type InsertUserInput = Omit<
| "tokens"
| "status"
| "ignoredUsers"
+ | "notifications"
+ | "digests"
| "emailVerificationID"
| "createdAt"
> &
@@ -466,6 +500,14 @@ export async function insertUser(
suspension: { history: [] },
ban: { active: false, history: [] },
},
+ notifications: {
+ onReply: false,
+ onFeatured: false,
+ onModeration: false,
+ onStaffReplies: false,
+ digestFrequency: GQLDIGEST_FREQUENCY.NONE,
+ },
+ digests: [],
createdAt: now,
};
@@ -1975,3 +2017,122 @@ export async function setUserLastDownloadedAt(
return result.value;
}
+
+export type NotificationSettingsInput = Partial;
+
+export async function updateUserNotificationSettings(
+ mongo: Db,
+ tenantID: string,
+ id: string,
+ settings: NotificationSettingsInput
+) {
+ const result = await collection(mongo).findOneAndUpdate(
+ {
+ id,
+ tenantID,
+ },
+ {
+ $set: dotize({
+ notifications: settings,
+ }),
+ },
+ {
+ // False to return the updated document instead of the original
+ // document.
+ returnOriginal: false,
+ }
+ );
+ if (!result.value) {
+ // Get the user so we can figure out why the update operation failed.
+ const user = await retrieveUser(mongo, tenantID, id);
+ if (!user) {
+ throw new UserNotFoundError(id);
+ }
+
+ throw new Error("an unexpected error occurred");
+ }
+
+ return result.value;
+}
+
+/**
+ * insertUserNotificationDigests will push the notification contexts onto the
+ * User so that notifications can now be queued.
+ *
+ * @param mongo the database to put the notification digests into
+ * @param tenantID the ID of the Tenant that this User exists on
+ * @param id the ID of the User to insert the digests onto
+ * @param templates the templates that represent the digests to be inserted
+ * @param now the current time
+ */
+export async function insertUserNotificationDigests(
+ mongo: Db,
+ tenantID: string,
+ id: string,
+ templates: DigestibleTemplate[],
+ now: Date
+) {
+ // Form the templates into digests to be sent.
+ const digests: Digest[] = templates.map(template => ({
+ template,
+ createdAt: now,
+ }));
+
+ const result = await collection(mongo).findOneAndUpdate(
+ {
+ id,
+ tenantID,
+ },
+ {
+ $push: {
+ digests: { $each: digests },
+ },
+ },
+ {
+ // False to return the updated document instead of the original
+ // document.
+ returnOriginal: false,
+ }
+ );
+ if (!result.value) {
+ // Get the user so we can figure out why the update operation failed.
+ const user = await retrieveUser(mongo, tenantID, id);
+ if (!user) {
+ throw new UserNotFoundError(id);
+ }
+
+ throw new Error("an unexpected error occurred");
+ }
+
+ return result.value;
+}
+
+/**
+ * pullUserNotificationDigests will pull notification digests for a given User
+ * so it can be added to the mailer queue.
+ *
+ * @param mongo the database to pull digests from
+ * @param tenantID the tenant ID to pull digests for
+ * @param frequency the frequency that we're scanning for to limit the digest
+ * operation
+ */
+export async function pullUserNotificationDigests(
+ mongo: Db,
+ tenantID: string,
+ frequency: GQLDIGEST_FREQUENCY
+) {
+ const result = await collection(mongo).findOneAndUpdate(
+ {
+ tenantID,
+ "notifications.digestFrequency": frequency,
+ digests: { $ne: [] },
+ },
+ { $set: { digests: [] } },
+ {
+ // True to return the original document instead of the updated document.
+ returnOriginal: true,
+ }
+ );
+
+ return result.value || null;
+}
diff --git a/src/core/server/queue/index.ts b/src/core/server/queue/index.ts
index 50ce7725a..3561ad5e1 100644
--- a/src/core/server/queue/index.ts
+++ b/src/core/server/queue/index.ts
@@ -2,15 +2,15 @@ import Queue from "bull";
import { Db } from "mongodb";
import { Config } from "coral-server/config";
-import { createMailerTask, MailerQueue } from "coral-server/queue/tasks/mailer";
-import {
- createScraperTask,
- ScraperQueue,
-} from "coral-server/queue/tasks/scraper";
import { I18n } from "coral-server/services/i18n";
+import { JWTSigningConfig } from "coral-server/services/jwt";
import { createRedisClient } from "coral-server/services/redis";
import TenantCache from "coral-server/services/tenant/cache";
+import { createMailerTask, MailerQueue } from "./tasks/mailer";
+import { createNotifierTask, NotifierQueue } from "./tasks/notifier";
+import { createScraperTask, ScraperQueue } from "./tasks/scraper";
+
const createQueueOptions = async (
config: Config
): Promise => {
@@ -46,11 +46,13 @@ export interface QueueOptions {
config: Config;
tenantCache: TenantCache;
i18n: I18n;
+ signingConfig: JWTSigningConfig;
}
export interface TaskQueue {
mailer: MailerQueue;
scraper: ScraperQueue;
+ notifier: NotifierQueue;
}
export async function createQueue(options: QueueOptions): Promise {
@@ -61,10 +63,15 @@ export async function createQueue(options: QueueOptions): Promise {
// Attach process functions to the various tasks in the queue.
const mailer = createMailerTask(queueOptions, options);
const scraper = createScraperTask(queueOptions, options);
+ const notifier = createNotifierTask(queueOptions, {
+ mailerQueue: mailer,
+ ...options,
+ });
// Return the tasks + client.
return {
mailer,
scraper,
+ notifier,
};
}
diff --git a/src/core/server/queue/tasks/Task.ts b/src/core/server/queue/tasks/Task.ts
index bfd77fdfb..df86ec389 100644
--- a/src/core/server/queue/tasks/Task.ts
+++ b/src/core/server/queue/tasks/Task.ts
@@ -46,13 +46,20 @@ export default class Task {
"processing job from queue"
);
- // Send the job off to the job processor to be handled.
- const promise: U = await this.options.jobProcessor(job);
- logger.trace(
- { jobID: job.id, jobName: this.options.jobName },
- "processing completed"
- );
- return promise;
+ try {
+ // Send the job off to the job processor to be handled.
+ const promise: U = await this.options.jobProcessor(job);
+ logger.trace(
+ { jobID: job.id, jobName: this.options.jobName },
+ "processing completed"
+ );
+
+ return promise;
+ } catch (err) {
+ logger.error({ err }, "failed to process job from queue");
+
+ throw err;
+ }
});
logger.trace(
diff --git a/src/core/server/queue/tasks/mailer/README.md b/src/core/server/queue/tasks/mailer/README.md
new file mode 100644
index 000000000..dae8800e2
--- /dev/null
+++ b/src/core/server/queue/tasks/mailer/README.md
@@ -0,0 +1,37 @@
+# mailer
+
+The mailer is responsible for rendering and translating all email messages sent
+by Coral.
+
+## Adding a new email
+
+The first step to defining your new email is to add a new email template to
+`src/core/server/queue/tasks/mailer/templates/index.ts`. There you can see how
+other templates define their requirements, and how you ensure that you get the
+required context.
+
+### Templates
+
+Depending on the type of email you're adding, you're likely adding a:
+
+- _Account Notification_ - an email sent as a result of an account action by the
+ system or an administrator.
+- _Notification_ - an email sent to a user based on a notification preference
+ they've enabled.
+
+There are folders under `templates` for each of these. Use any of the templates
+in those folders as an example to craft your own template.
+
+### Translations
+
+Once you've added a template, you need to add a translation into the `src/core/server/locales/${locale}/email.ftl`
+file. There are two translation lines you need to add to support a new email:
+
+- _Subject_ - the subject of the email to be sent. Keys for these translations
+ follow the form `email-subject-${camelCase(templateName)}`.
+ - For example, for the template name `account-notification/confirm-email`
+ the subject translation key would become `email-subject-accountNotificationConfirmEmail`.
+- _Template_ - the translated body of the email to be sent. Keys for these
+ translations follow the form: `email-template-${camelCase(templateName)}`.
+ - For example, for the template name `account-notification/confirm-email`
+ the subject translation key would become `email-template-accountNotificationConfirmEmail`.
diff --git a/src/core/server/queue/tasks/mailer/assets/main.css b/src/core/server/queue/tasks/mailer/assets/main.css
index 39f2ddddb..ae87dfe19 100644
--- a/src/core/server/queue/tasks/mailer/assets/main.css
+++ b/src/core/server/queue/tasks/mailer/assets/main.css
@@ -12,3 +12,7 @@ table {
padding: 10px;
background-color: whitesmoke;
}
+
+.footer {
+ text-align: center;
+}
diff --git a/src/core/server/queue/tasks/mailer/content.ts b/src/core/server/queue/tasks/mailer/content.ts
index b3a31991a..6e672725f 100644
--- a/src/core/server/queue/tasks/mailer/content.ts
+++ b/src/core/server/queue/tasks/mailer/content.ts
@@ -7,7 +7,7 @@ import TenantCache from "coral-server/services/tenant/cache";
import { TenantCacheAdapter } from "coral-server/services/tenant/cache/adapter";
import { Tenant } from "coral-server/models/tenant";
-import { Template } from "./templates";
+import { EmailTemplate } from "./templates";
// templateDirectory is the directory containing the email templates.
const templateDirectory = path.join(__dirname, "templates");
@@ -23,11 +23,11 @@ export interface MailerContentOptions {
* @param env the nunjucks rendering environment
* @param template the template to render
*/
-function render(env: Environment, { name, context }: Template) {
+function render(env: Environment, { name, context }: EmailTemplate) {
return new Promise((resolve, reject) =>
env.render(
name + ".html",
- { context, name: camelCase(name) },
+ { context, name: camelCase(name), baseName: path.basename(name) },
(err, html) => {
if (err) {
return reject(err);
@@ -90,7 +90,7 @@ export default class MailerContent {
*/
public async generateHTML(
tenant: Tenant,
- template: Template
+ template: EmailTemplate
): Promise {
// Get the environment to render with.
const env = this.getEnvironment(tenant);
diff --git a/src/core/server/queue/tasks/mailer/index.ts b/src/core/server/queue/tasks/mailer/index.ts
index 8a545e7c3..8a91ffdf6 100644
--- a/src/core/server/queue/tasks/mailer/index.ts
+++ b/src/core/server/queue/tasks/mailer/index.ts
@@ -12,13 +12,13 @@ import {
MailerData,
MailProcessorOptions,
} from "./processor";
-import { Template } from "./templates";
+import { EmailTemplate } from "./templates";
export interface MailerInput {
message: {
to: string;
};
- template: Template;
+ template: EmailTemplate;
tenantID: string;
}
diff --git a/src/core/server/queue/tasks/mailer/processor.ts b/src/core/server/queue/tasks/mailer/processor.ts
index dd7ddfea2..f423cf137 100644
--- a/src/core/server/queue/tasks/mailer/processor.ts
+++ b/src/core/server/queue/tasks/mailer/processor.ts
@@ -1,4 +1,5 @@
import { Job } from "bull";
+import createDOMPurify from "dompurify";
import { DOMLocalization } from "fluent-dom/compat";
import { FluentBundle } from "fluent/compat";
import { minify } from "html-minifier";
@@ -16,6 +17,7 @@ import { LanguageCode } from "coral-common/helpers/i18n/locales";
import { Config } from "coral-server/config";
import { InternalError } from "coral-server/errors";
import logger from "coral-server/logger";
+import { Tenant } from "coral-server/models/tenant";
import { I18n, translate } from "coral-server/services/i18n";
import TenantCache from "coral-server/services/tenant/cache";
import { TenantCacheAdapter } from "coral-server/services/tenant/cache/adapter";
@@ -98,6 +100,7 @@ function createMessageTranslator(i18n: I18n) {
* @param data data used to send the message
*/
return async (
+ tenant: Tenant,
templateName: string,
locale: LanguageCode,
fromAddress: string,
@@ -121,22 +124,35 @@ function createMessageTranslator(i18n: I18n) {
// Translate the bundle.
await loc.translateFragment(dom.window.document);
- // TODO: (wyattjoh) strip the i18n attributes from the source.
-
// Grab the rendered HTML from the dom, and juice them.
if (!dom.window.document.documentElement) {
throw new Error("dom did not have a document element");
}
- const translatedHTML = dom.window.document.documentElement.outerHTML;
+
+ // Configure the purification.
+ const purify = createDOMPurify(dom.window);
+
+ // Strip the l10n attributes from the email HTML.
+ purify.sanitize(dom.window.document.documentElement, {
+ ALLOW_DATA_ATTR: false,
+ WHOLE_DOCUMENT: true,
+ SANITIZE_DOM: false,
+ RETURN_DOM: false,
+ ADD_TAGS: ["link"],
+ FORBID_TAGS: [],
+ FORBID_ATTR: [],
+ IN_PLACE: true,
+ });
// Juice the HTML to inline resources.
- const html = await juiceHTML(translatedHTML);
+ const html = await juiceHTML(dom.serialize());
// Get the translated subject.
const subject = translate(
bundle,
templateName,
- `email-subject-${camelCase(templateName)}`
+ `email-subject-${camelCase(templateName)}`,
+ { organizationName: tenant.organization.name }
);
// Generate the text content of the message from the HTML.
@@ -231,6 +247,7 @@ export const createJobProcessor = (options: MailProcessorOptions) => {
let message: Message;
try {
message = await translateMessage(
+ tenant,
data.templateName,
tenant.locale,
fromAddress,
diff --git a/src/core/server/queue/tasks/mailer/templates/account-notification/ban.html b/src/core/server/queue/tasks/mailer/templates/account-notification/ban.html
new file mode 100644
index 000000000..5819cae3b
--- /dev/null
+++ b/src/core/server/queue/tasks/mailer/templates/account-notification/ban.html
@@ -0,0 +1,11 @@
+{% extends "layouts/account-notification.html" %}
+
+{% block content %}
+
+ Hello {{ context.username }},
+ Someone with access to your account has violated our community guidelines.
+ As a result, your account has been banned. You will no longer be able to
+ comment, react or report comments. If you think this has been done in error,
+ please contact our community team at {{ context.organizationContactEmail }}.
+
+ To confirm your email address for use with your commenting account at {{ context.organizationName }},
+ please follow this link: Click here to confirm your email
+ If you did not recently create a commenting account with
+ {{ context.organizationName }}, you can safely ignore this email.
+
+{% endblock %}
diff --git a/src/core/server/queue/tasks/mailer/templates/delete-request-cancel.html b/src/core/server/queue/tasks/mailer/templates/account-notification/delete-request-cancel.html
similarity index 75%
rename from src/core/server/queue/tasks/mailer/templates/delete-request-cancel.html
rename to src/core/server/queue/tasks/mailer/templates/account-notification/delete-request-cancel.html
index 5b41a7744..99b4f900d 100644
--- a/src/core/server/queue/tasks/mailer/templates/delete-request-cancel.html
+++ b/src/core/server/queue/tasks/mailer/templates/account-notification/delete-request-cancel.html
@@ -1,4 +1,4 @@
-{% extends "layouts/user-notification.html" %}
+{% extends "layouts/account-notification.html" %}
{% block content %}
You have cancelled your account deletion request for {{ context.organizationName }}. Your account is now reactivated.
diff --git a/src/core/server/queue/tasks/mailer/templates/delete-request-completed.html b/src/core/server/queue/tasks/mailer/templates/account-notification/delete-request-completed.html
similarity index 88%
rename from src/core/server/queue/tasks/mailer/templates/delete-request-completed.html
rename to src/core/server/queue/tasks/mailer/templates/account-notification/delete-request-completed.html
index a2a6ca104..443bfe3ce 100644
--- a/src/core/server/queue/tasks/mailer/templates/delete-request-completed.html
+++ b/src/core/server/queue/tasks/mailer/templates/account-notification/delete-request-completed.html
@@ -1,4 +1,4 @@
-{% extends "layouts/user-notification.html" %}
+{% extends "layouts/account-notification.html" %}
{% block content %}
Your commenter account for {{ context.organizationName }} is now deleted. We're sorry to see you go!
diff --git a/src/core/server/queue/tasks/mailer/templates/delete-request-confirmation.html b/src/core/server/queue/tasks/mailer/templates/account-notification/delete-request-confirmation.html
similarity index 90%
rename from src/core/server/queue/tasks/mailer/templates/delete-request-confirmation.html
rename to src/core/server/queue/tasks/mailer/templates/account-notification/delete-request-confirmation.html
index 34c97589d..ea86a3fce 100644
--- a/src/core/server/queue/tasks/mailer/templates/delete-request-confirmation.html
+++ b/src/core/server/queue/tasks/mailer/templates/account-notification/delete-request-confirmation.html
@@ -1,4 +1,4 @@
-{% extends "layouts/user-notification.html" %}
+{% extends "layouts/account-notification.html" %}
{% block content %}
A request to delete your commenter account was received. Your account is scheduled for deletion on {{ context.requestDate }}.
diff --git a/src/core/server/queue/tasks/mailer/templates/account-notification/download-comments.html b/src/core/server/queue/tasks/mailer/templates/account-notification/download-comments.html
new file mode 100644
index 000000000..d9ffb56d3
--- /dev/null
+++ b/src/core/server/queue/tasks/mailer/templates/account-notification/download-comments.html
@@ -0,0 +1,9 @@
+{% extends "layouts/account-notification.html" %}
+
+{% block content %}
+
+ Your comments from {{ context.organizationName }} as of {{ context.date }} are
+ now available for download.
+
+ In accordance with {{ context.organizationName }}'s community guidelines, your
+ account has been temporarily suspended. During the suspension, you will be
+ unable to comment, flag or engage with fellow commenters. Please rejoin the
+ conversation {{ context.until }}.
+
+ Thank you for updating your {{ context.organizationName }} commenter account
+ information. The changes you made are effective immediately. If you did not make
+ this change please reach out to {{ context.organizationContactEmail }}.
+
- Someone with access to your account has violated our community guidelines.
- As a result, your account has been banned. You will no longer be able to
- comment, react or report comments. If you think this has been done in error,
- please contact our community team at {{ context.organizationContactEmail }}.
-{% endblock %}
diff --git a/src/core/server/queue/tasks/mailer/templates/confirm-email.html b/src/core/server/queue/tasks/mailer/templates/confirm-email.html
deleted file mode 100644
index 973156938..000000000
--- a/src/core/server/queue/tasks/mailer/templates/confirm-email.html
+++ /dev/null
@@ -1,9 +0,0 @@
-{% extends "layouts/user-notification.html" %}
-
-{% block content %}
- Hello {{ context.username }},
- To confirm your email address for use with your commenting account at {{ context.organizationName }},
- please follow this link: Click here to confirm your email
- If you did not recently create a commenting account with
- {{ context.organizationName }}, you can safely ignore this email.
-{% endblock %}
diff --git a/src/core/server/queue/tasks/mailer/templates/download-comments.html b/src/core/server/queue/tasks/mailer/templates/download-comments.html
deleted file mode 100644
index 6ee13b1be..000000000
--- a/src/core/server/queue/tasks/mailer/templates/download-comments.html
+++ /dev/null
@@ -1,7 +0,0 @@
-{% extends "layouts/user-notification.html" %}
-
-{% block content %}
- Your comments from {{ context.organizationName }} as of {{ context.date }} are
- now available for download.