Compare commits

...

2 Commits

Author SHA1 Message Date
Tom Moor 4e19e7131d feedback 2024-10-27 15:10:05 -05:00
Tom Moor 60236da65b Make from address for authentication-related emails unguessable 2024-10-27 09:00:12 -05:00
19 changed files with 127 additions and 35 deletions
+3 -16
View File
@@ -1,5 +1,4 @@
import addressparser from "addressparser";
import invariant from "invariant";
import { EmailAddress } from "addressparser";
import nodemailer, { Transporter } from "nodemailer";
import SMTPTransport from "nodemailer/lib/smtp-transport";
import Oy from "oy-vey";
@@ -12,7 +11,7 @@ const useTestEmailService = env.isDevelopment && !env.SMTP_USERNAME;
type SendMailOptions = {
to: string;
fromName?: string;
from?: EmailAddress | string;
replyTo?: string;
messageId?: string;
references?: string[];
@@ -143,20 +142,8 @@ export class Mailer {
try {
Logger.info("email", `Sending email "${data.subject}" to ${data.to}`);
invariant(
env.SMTP_FROM_EMAIL,
"SMTP_FROM_EMAIL is required to send emails"
);
const from = addressparser(env.SMTP_FROM_EMAIL)[0];
const info = await transporter.sendMail({
from: data.fromName
? {
name: data.fromName,
address: from.address,
}
: env.SMTP_FROM_EMAIL,
from: data.from ?? env.SMTP_FROM_EMAIL,
replyTo: data.replyTo ?? env.SMTP_REPLY_EMAIL ?? env.SMTP_FROM_EMAIL,
to: data.to,
messageId: data.messageId,
+38 -1
View File
@@ -1,6 +1,10 @@
import addressparser from "addressparser";
import Bull from "bull";
import invariant from "invariant";
import randomstring from "randomstring";
import * as React from "react";
import mailer from "@server/emails/mailer";
import env from "@server/env";
import Logger from "@server/logging/Logger";
import Metrics from "@server/logging/Metrics";
import Notification from "@server/models/Notification";
@@ -9,6 +13,13 @@ import { TaskPriority } from "@server/queues/tasks/BaseTask";
import { NotificationMetadata } from "@server/types";
import { getEmailMessageId } from "@server/utils/emails";
export enum EmailMessageCategory {
Authentication = "authentication",
Invitation = "invitation",
Notification = "notification",
Marketing = "marketing",
}
export interface EmailProps {
to: string | null;
}
@@ -20,6 +31,9 @@ export default abstract class BaseEmail<
private props: T;
private metadata?: NotificationMetadata;
/** The message category for the email. */
protected abstract get category(): EmailMessageCategory;
/**
* Schedule this email type to be sent asyncronously by a worker.
*
@@ -113,7 +127,7 @@ export default abstract class BaseEmail<
try {
await mailer.sendMail({
to: this.props.to,
fromName: this.fromName?.(data),
from: this.from(data),
subject: this.subject(data),
messageId,
references,
@@ -148,6 +162,29 @@ export default abstract class BaseEmail<
}
}
private from(props: S & T) {
invariant(
env.SMTP_FROM_EMAIL,
"SMTP_FROM_EMAIL is required to send emails"
);
const parsedFrom = addressparser(env.SMTP_FROM_EMAIL)[0];
const name = this.fromName?.(props);
if (this.category === EmailMessageCategory.Authentication) {
const domain = parsedFrom.address.split("@")[1];
return {
name: name ?? parsedFrom.name,
address: `noreply-${randomstring.generate(24)}@${domain}`,
};
}
return {
name: name ?? parsedFrom.name,
address: parsedFrom.address,
};
}
private pixel(notification: Notification) {
return <img src={notification.pixelUrl} width="1" height="1" />;
}
@@ -2,7 +2,7 @@ import * as React from "react";
import { NotificationEventType } from "@shared/types";
import { Collection } from "@server/models";
import NotificationSettingsHelper from "@server/models/helpers/NotificationSettingsHelper";
import BaseEmail, { EmailProps } from "./BaseEmail";
import BaseEmail, { EmailMessageCategory, EmailProps } from "./BaseEmail";
import Body from "./components/Body";
import Button from "./components/Button";
import EmailTemplate from "./components/EmailLayout";
@@ -32,6 +32,10 @@ export default class CollectionCreatedEmail extends BaseEmail<
InputProps,
BeforeSend
> {
protected get category() {
return EmailMessageCategory.Notification;
}
protected async beforeSend(props: InputProps) {
const collection = await Collection.scope("withUser").findByPk(
props.collectionId
@@ -1,7 +1,7 @@
import * as React from "react";
import { CollectionPermission } from "@shared/types";
import { Collection, UserMembership } from "@server/models";
import BaseEmail, { EmailProps } from "./BaseEmail";
import BaseEmail, { EmailMessageCategory, EmailProps } from "./BaseEmail";
import Body from "./components/Body";
import Button from "./components/Button";
import EmailTemplate from "./components/EmailLayout";
@@ -29,6 +29,10 @@ export default class CollectionSharedEmail extends BaseEmail<
InputProps,
BeforeSend
> {
protected get category() {
return EmailMessageCategory.Notification;
}
protected async beforeSend({ userId, collectionId }: InputProps) {
const collection = await Collection.findByPk(collectionId);
if (!collection) {
@@ -6,7 +6,7 @@ import HTMLHelper from "@server/models/helpers/HTMLHelper";
import NotificationSettingsHelper from "@server/models/helpers/NotificationSettingsHelper";
import { ProsemirrorHelper } from "@server/models/helpers/ProsemirrorHelper";
import { TextHelper } from "@server/models/helpers/TextHelper";
import BaseEmail, { EmailProps } from "./BaseEmail";
import BaseEmail, { EmailMessageCategory, EmailProps } from "./BaseEmail";
import Body from "./components/Body";
import Button from "./components/Button";
import Diff from "./components/Diff";
@@ -43,6 +43,10 @@ export default class CommentCreatedEmail extends BaseEmail<
InputProps,
BeforeSend
> {
protected get category() {
return EmailMessageCategory.Notification;
}
protected async beforeSend(props: InputProps) {
const { documentId, commentId } = props;
const document = await Document.unscoped().findByPk(documentId);
@@ -6,7 +6,7 @@ import HTMLHelper from "@server/models/helpers/HTMLHelper";
import NotificationSettingsHelper from "@server/models/helpers/NotificationSettingsHelper";
import { ProsemirrorHelper } from "@server/models/helpers/ProsemirrorHelper";
import { TextHelper } from "@server/models/helpers/TextHelper";
import BaseEmail, { EmailProps } from "./BaseEmail";
import BaseEmail, { EmailMessageCategory, EmailProps } from "./BaseEmail";
import Body from "./components/Body";
import Button from "./components/Button";
import Diff from "./components/Diff";
@@ -41,6 +41,10 @@ export default class CommentMentionedEmail extends BaseEmail<
InputProps,
BeforeSend
> {
protected get category() {
return EmailMessageCategory.Notification;
}
protected async beforeSend(props: InputProps) {
const { documentId, commentId } = props;
const document = await Document.unscoped().findByPk(documentId);
@@ -1,6 +1,6 @@
import * as React from "react";
import env from "@server/env";
import BaseEmail, { EmailProps } from "./BaseEmail";
import BaseEmail, { EmailMessageCategory, EmailProps } from "./BaseEmail";
import Body from "./components/Body";
import CopyableCode from "./components/CopyableCode";
import EmailTemplate from "./components/EmailLayout";
@@ -17,6 +17,10 @@ type Props = EmailProps & {
* Email sent to a user when they request to delete their workspace.
*/
export default class ConfirmTeamDeleteEmail extends BaseEmail<Props> {
protected get category() {
return EmailMessageCategory.Notification;
}
protected subject() {
return `Your workspace deletion request`;
}
@@ -1,6 +1,6 @@
import * as React from "react";
import env from "@server/env";
import BaseEmail, { EmailProps } from "./BaseEmail";
import BaseEmail, { EmailMessageCategory, EmailProps } from "./BaseEmail";
import Body from "./components/Body";
import CopyableCode from "./components/CopyableCode";
import EmailTemplate from "./components/EmailLayout";
@@ -17,6 +17,10 @@ type Props = EmailProps & {
* Email sent to a user when they request to delete their account.
*/
export default class ConfirmUserDeleteEmail extends BaseEmail<Props> {
protected get category() {
return EmailMessageCategory.Notification;
}
protected subject() {
return `Your account deletion request`;
}
@@ -6,7 +6,7 @@ import { DocumentHelper } from "@server/models/helpers/DocumentHelper";
import HTMLHelper from "@server/models/helpers/HTMLHelper";
import { ProsemirrorHelper } from "@server/models/helpers/ProsemirrorHelper";
import { TextHelper } from "@server/models/helpers/TextHelper";
import BaseEmail, { EmailProps } from "./BaseEmail";
import BaseEmail, { EmailMessageCategory, EmailProps } from "./BaseEmail";
import Body from "./components/Body";
import Button from "./components/Button";
import Diff from "./components/Diff";
@@ -37,6 +37,10 @@ export default class DocumentMentionedEmail extends BaseEmail<
InputProps,
BeforeSend
> {
protected get category() {
return EmailMessageCategory.Notification;
}
protected async beforeSend({ documentId, revisionId, userId }: InputProps) {
const document = await Document.unscoped().findByPk(documentId);
if (!document) {
@@ -6,7 +6,7 @@ import { DocumentHelper } from "@server/models/helpers/DocumentHelper";
import HTMLHelper from "@server/models/helpers/HTMLHelper";
import NotificationSettingsHelper from "@server/models/helpers/NotificationSettingsHelper";
import SubscriptionHelper from "@server/models/helpers/SubscriptionHelper";
import BaseEmail, { EmailProps } from "./BaseEmail";
import BaseEmail, { EmailMessageCategory, EmailProps } from "./BaseEmail";
import Body from "./components/Body";
import Button from "./components/Button";
import Diff from "./components/Diff";
@@ -44,6 +44,10 @@ export default class DocumentPublishedOrUpdatedEmail extends BaseEmail<
InputProps,
BeforeSend
> {
protected get category() {
return EmailMessageCategory.Notification;
}
protected async beforeSend(props: InputProps) {
const { documentId, revisionId } = props;
const document = await Document.unscoped().findByPk(documentId, {
@@ -1,7 +1,7 @@
import * as React from "react";
import { DocumentPermission } from "@shared/types";
import { Document, UserMembership } from "@server/models";
import BaseEmail, { EmailProps } from "./BaseEmail";
import BaseEmail, { EmailMessageCategory, EmailProps } from "./BaseEmail";
import Body from "./components/Body";
import Button from "./components/Button";
import EmailTemplate from "./components/EmailLayout";
@@ -29,6 +29,10 @@ export default class DocumentSharedEmail extends BaseEmail<
InputProps,
BeforeSend
> {
protected get category() {
return EmailMessageCategory.Notification;
}
protected async beforeSend({ documentId, userId }: InputProps) {
const document = await Document.unscoped().findByPk(documentId);
if (!document) {
@@ -1,7 +1,7 @@
import * as React from "react";
import { NotificationEventType } from "@shared/types";
import NotificationSettingsHelper from "@server/models/helpers/NotificationSettingsHelper";
import BaseEmail, { EmailProps } from "./BaseEmail";
import BaseEmail, { EmailMessageCategory, EmailProps } from "./BaseEmail";
import Body from "./components/Body";
import Button from "./components/Button";
import EmailTemplate from "./components/EmailLayout";
@@ -29,6 +29,10 @@ export default class ExportFailureEmail extends BaseEmail<
InputProps,
BeforeSend
> {
protected get category() {
return EmailMessageCategory.Notification;
}
protected async beforeSend(props: InputProps) {
return {
unsubscribeUrl: this.unsubscribeUrl(props),
@@ -2,7 +2,7 @@ import * as React from "react";
import { NotificationEventType } from "@shared/types";
import env from "@server/env";
import NotificationSettingsHelper from "@server/models/helpers/NotificationSettingsHelper";
import BaseEmail, { EmailProps } from "./BaseEmail";
import BaseEmail, { EmailMessageCategory, EmailProps } from "./BaseEmail";
import Body from "./components/Body";
import Button from "./components/Button";
import EmailTemplate from "./components/EmailLayout";
@@ -32,6 +32,10 @@ export default class ExportSuccessEmail extends BaseEmail<
InputProps,
BeforeSend
> {
protected get category() {
return EmailMessageCategory.Notification;
}
protected async beforeSend(props: InputProps) {
return {
unsubscribeUrl: this.unsubscribeUrl(props),
@@ -2,7 +2,7 @@ import * as React from "react";
import { NotificationEventType } from "@shared/types";
import env from "@server/env";
import NotificationSettingsHelper from "@server/models/helpers/NotificationSettingsHelper";
import BaseEmail, { EmailProps } from "./BaseEmail";
import BaseEmail, { EmailMessageCategory, EmailProps } from "./BaseEmail";
import Body from "./components/Body";
import Button from "./components/Button";
import EmailTemplate from "./components/EmailLayout";
@@ -30,6 +30,10 @@ export default class InviteAcceptedEmail extends BaseEmail<
InputProps,
BeforeSend
> {
protected get category() {
return EmailMessageCategory.Notification;
}
protected async beforeSend(props: InputProps) {
return {
unsubscribeUrl: this.unsubscribeUrl(props),
+5 -1
View File
@@ -1,6 +1,6 @@
import * as React from "react";
import env from "@server/env";
import BaseEmail, { EmailProps } from "./BaseEmail";
import BaseEmail, { EmailMessageCategory, EmailProps } from "./BaseEmail";
import Body from "./components/Body";
import Button from "./components/Button";
import EmailTemplate from "./components/EmailLayout";
@@ -21,6 +21,10 @@ type Props = EmailProps & {
* Email sent to an external user when an admin sends them an invite.
*/
export default class InviteEmail extends BaseEmail<Props, Record<string, any>> {
protected get category() {
return EmailMessageCategory.Invitation;
}
protected subject({ actorName, teamName }: Props) {
return `${actorName} invited you to join ${teamName}s workspace`;
}
@@ -1,6 +1,6 @@
import * as React from "react";
import env from "@server/env";
import BaseEmail, { EmailProps } from "./BaseEmail";
import BaseEmail, { EmailMessageCategory, EmailProps } from "./BaseEmail";
import Body from "./components/Body";
import Button from "./components/Button";
import EmailTemplate from "./components/EmailLayout";
@@ -22,6 +22,10 @@ type Props = EmailProps & {
* haven't signed in after a few days.
*/
export default class InviteReminderEmail extends BaseEmail<Props> {
protected get category() {
return EmailMessageCategory.Invitation;
}
protected subject({ actorName, teamName }: Props) {
return `Reminder: ${actorName} invited you to join ${teamName}s workspace`;
}
+5 -1
View File
@@ -2,7 +2,7 @@ import * as React from "react";
import { Client } from "@shared/types";
import env from "@server/env";
import logger from "@server/logging/Logger";
import BaseEmail, { EmailProps } from "./BaseEmail";
import BaseEmail, { EmailMessageCategory, EmailProps } from "./BaseEmail";
import Body from "./components/Body";
import Button from "./components/Button";
import EmailTemplate from "./components/EmailLayout";
@@ -21,6 +21,10 @@ type Props = EmailProps & {
* Email sent to a user when they request a magic sign-in link.
*/
export default class SigninEmail extends BaseEmail<Props, Record<string, any>> {
protected get category() {
return EmailMessageCategory.Authentication;
}
protected subject() {
return "Magic signin link";
}
@@ -1,5 +1,5 @@
import * as React from "react";
import BaseEmail, { EmailProps } from "./BaseEmail";
import BaseEmail, { EmailMessageCategory, EmailProps } from "./BaseEmail";
import Body from "./components/Body";
import Button from "./components/Button";
import EmailTemplate from "./components/EmailLayout";
@@ -18,6 +18,10 @@ type Props = EmailProps & {
* due to repeated failure.
*/
export default class WebhookDisabledEmail extends BaseEmail<Props> {
protected get category() {
return EmailMessageCategory.Notification;
}
protected subject() {
return `Warning: Webhook disabled`;
}
@@ -28,7 +32,7 @@ export default class WebhookDisabledEmail extends BaseEmail<Props> {
protected renderAsText({ webhookName, teamUrl }: Props): string {
return `
Your webhook (${webhookName}) has been automatically disabled as the last 25
Your webhook (${webhookName}) has been automatically disabled as the last 25
delivery attempts have failed. You can re-enable by editing the webhook.
Webhook settings: ${teamUrl}/settings/webhooks
+5 -1
View File
@@ -1,7 +1,7 @@
import * as React from "react";
import { UserRole } from "@shared/types";
import env from "@server/env";
import BaseEmail, { EmailProps } from "./BaseEmail";
import BaseEmail, { EmailMessageCategory, EmailProps } from "./BaseEmail";
import Body from "./components/Body";
import Button from "./components/Button";
import EmailTemplate from "./components/EmailLayout";
@@ -22,6 +22,10 @@ type BeforeSend = Record<string, never>;
* in for the first time from an invite.
*/
export default class WelcomeEmail extends BaseEmail<Props, BeforeSend> {
protected get category() {
return EmailMessageCategory.Notification;
}
protected subject() {
return `Welcome to ${env.APP_NAME}`;
}