Enforce email templating (#3355)
* Add react-email * WIP * Fix import error * Rename services * Update logging * Update email template * Update email template * Add Base Email template * Move to proper place * Remove test files * Update logo * Add email theme * Revert "Remove test files" This reverts commit fe062dd05166b95125cf99f2165cc20efb6c275a. * Add email theme 2 * Revert "Revert "Remove test files"" This reverts commit 6c6471273ad765788f2eaf5a5614209edfb965ce. * Revert "Revert "Revert "Remove test files""" This reverts commit f851333c24e9cfe3f425c9cbbd1e079efce5c3dd. * Revert "Revert "Revert "Revert "Remove test files"""" This reverts commit 7838e19e88e269026e24803f26cd52b467b4ef36. * Fix theme
This commit is contained in:
48
packages/twenty-server/src/emails/common-style.ts
Normal file
48
packages/twenty-server/src/emails/common-style.ts
Normal file
@ -0,0 +1,48 @@
|
||||
const grayScale = {
|
||||
gray100: '#000000',
|
||||
gray90: '#141414',
|
||||
gray85: '#171717',
|
||||
gray80: '#1b1b1b',
|
||||
gray75: '#1d1d1d',
|
||||
gray70: '#222222',
|
||||
gray65: '#292929',
|
||||
gray60: '#333333',
|
||||
gray55: '#4c4c4c',
|
||||
gray50: '#666666',
|
||||
gray45: '#818181',
|
||||
gray40: '#999999',
|
||||
gray35: '#b3b3b3',
|
||||
gray30: '#cccccc',
|
||||
gray25: '#d6d6d6',
|
||||
gray20: '#ebebeb',
|
||||
gray15: '#f1f1f1',
|
||||
gray10: '#fcfcfc',
|
||||
gray0: '#ffffff',
|
||||
};
|
||||
|
||||
export const emailTheme = {
|
||||
font: {
|
||||
colors: {
|
||||
highlighted: grayScale.gray60,
|
||||
primary: grayScale.gray50,
|
||||
inverted: grayScale.gray0,
|
||||
},
|
||||
weight: {
|
||||
regular: 400,
|
||||
bold: 600,
|
||||
},
|
||||
size: {
|
||||
md: '13px',
|
||||
lg: '16px',
|
||||
},
|
||||
},
|
||||
background: {
|
||||
colors: { highlight: grayScale.gray15 },
|
||||
radialGradient: `radial-gradient(50% 62.62% at 50% 0%, #505050 0%, ${grayScale.gray60} 100%)`,
|
||||
radialGradientHover: `radial-gradient(76.32% 95.59% at 50% 0%, #505050 0%, ${grayScale.gray60} 100%)`,
|
||||
transparent: {
|
||||
medium: 'rgba(0, 0, 0, 0.08)',
|
||||
light: 'rgba(0, 0, 0, 0.04)',
|
||||
},
|
||||
},
|
||||
};
|
||||
17
packages/twenty-server/src/emails/components/BaseEmail.tsx
Normal file
17
packages/twenty-server/src/emails/components/BaseEmail.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import * as React from 'react';
|
||||
import { Container, Html } from '@react-email/components';
|
||||
|
||||
import { BaseHead } from 'src/emails/components/BaseHead';
|
||||
import { Logo } from 'src/emails/components/Logo';
|
||||
|
||||
export const BaseEmail = ({ children }) => {
|
||||
return (
|
||||
<Html lang="en">
|
||||
<BaseHead />
|
||||
<Container width={290}>
|
||||
<Logo />
|
||||
{children}
|
||||
</Container>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
22
packages/twenty-server/src/emails/components/BaseHead.tsx
Normal file
22
packages/twenty-server/src/emails/components/BaseHead.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { Font, Head } from '@react-email/components';
|
||||
import * as React from 'react';
|
||||
|
||||
import { emailTheme } from 'src/emails/common-style';
|
||||
|
||||
export const BaseHead = () => {
|
||||
return (
|
||||
<Head>
|
||||
<title>Twenty email</title>
|
||||
<Font
|
||||
fontFamily="Inter"
|
||||
fallbackFontFamily="sans-serif"
|
||||
webFont={{
|
||||
url: 'https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap',
|
||||
format: 'woff2',
|
||||
}}
|
||||
fontStyle="normal"
|
||||
fontWeight={emailTheme.font.weight.regular}
|
||||
/>
|
||||
</Head>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,23 @@
|
||||
import { Button } from '@react-email/button';
|
||||
import * as React from 'react';
|
||||
|
||||
import { emailTheme } from 'src/emails/common-style';
|
||||
const callToActionStyle = {
|
||||
display: 'flex',
|
||||
padding: '8px 32px',
|
||||
borderRadius: '8px',
|
||||
border: `1px solid ${emailTheme.background.transparent.light}`,
|
||||
background: emailTheme.background.radialGradient,
|
||||
boxShadow: `0px 2px 4px 0px ${emailTheme.background.transparent.light}, 0px 0px 4px 0px ${emailTheme.background.transparent.medium}`,
|
||||
color: emailTheme.font.colors.inverted,
|
||||
fontSize: emailTheme.font.size.md,
|
||||
fontWeight: emailTheme.font.weight.bold,
|
||||
};
|
||||
|
||||
export const CallToAction = ({ value, href }) => {
|
||||
return (
|
||||
<Button href={href} style={callToActionStyle}>
|
||||
{value}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,30 @@
|
||||
import * as React from 'react';
|
||||
import { Row } from '@react-email/row';
|
||||
import { Text } from '@react-email/text';
|
||||
import { Column } from '@react-email/components';
|
||||
|
||||
import { emailTheme } from 'src/emails/common-style';
|
||||
|
||||
const rowStyle = {
|
||||
display: 'flex',
|
||||
};
|
||||
|
||||
const highlightedStyle = {
|
||||
borderRadius: '4px',
|
||||
background: emailTheme.background.colors.highlight,
|
||||
padding: '4px 8px',
|
||||
margin: 0,
|
||||
fontSize: emailTheme.font.size.lg,
|
||||
fontWeight: emailTheme.font.weight.bold,
|
||||
color: emailTheme.font.colors.highlighted,
|
||||
};
|
||||
|
||||
export const HighlightedText = ({ value }) => {
|
||||
return (
|
||||
<Row style={rowStyle}>
|
||||
<Column>
|
||||
<Text style={highlightedStyle}>{value}</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
17
packages/twenty-server/src/emails/components/Logo.tsx
Normal file
17
packages/twenty-server/src/emails/components/Logo.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { Img } from '@react-email/components';
|
||||
|
||||
const logoStyle = {
|
||||
marginBottom: '40px',
|
||||
};
|
||||
|
||||
export const Logo = () => {
|
||||
return (
|
||||
<Img
|
||||
src="https://app.twenty.com/icons/windows11/Square150x150Logo.scale-100.png"
|
||||
alt="Twenty logo"
|
||||
width="40"
|
||||
height="40"
|
||||
style={logoStyle}
|
||||
/>
|
||||
);
|
||||
};
|
||||
14
packages/twenty-server/src/emails/components/MainText.tsx
Normal file
14
packages/twenty-server/src/emails/components/MainText.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { Text } from '@react-email/text';
|
||||
import * as React from 'react';
|
||||
|
||||
import { emailTheme } from 'src/emails/common-style';
|
||||
|
||||
const mainTextStyle = {
|
||||
fontSize: emailTheme.font.size.md,
|
||||
fontWeight: emailTheme.font.weight.regular,
|
||||
color: emailTheme.font.colors.primary,
|
||||
};
|
||||
|
||||
export const MainText = ({ children }) => {
|
||||
return <Text style={mainTextStyle}>{children}</Text>;
|
||||
};
|
||||
6
packages/twenty-server/src/emails/components/Title.tsx
Normal file
6
packages/twenty-server/src/emails/components/Title.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import { Heading } from '@react-email/components';
|
||||
import * as React from 'react';
|
||||
|
||||
export const Title = ({ value }) => {
|
||||
return <Heading as="h1">{value}</Heading>;
|
||||
};
|
||||
@ -0,0 +1,18 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { SendMailOptions } from 'nodemailer';
|
||||
|
||||
import { MessageQueueJob } from 'src/integrations/message-queue/interfaces/message-queue-job.interface';
|
||||
|
||||
import { EmailSenderService } from 'src/integrations/email/email-sender.service';
|
||||
|
||||
@Injectable()
|
||||
export class EmailSenderJob implements MessageQueueJob<SendMailOptions> {
|
||||
constructor(private readonly emailSenderService: EmailSenderService) {}
|
||||
|
||||
async handle(data: SendMailOptions): Promise<void> {
|
||||
process.stdout.write(`Sending email to ${data.to} ...`);
|
||||
await this.emailSenderService.send(data);
|
||||
console.log(' done!');
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
|
||||
import { SendMailOptions } from 'nodemailer';
|
||||
|
||||
import { EmailDriver } from 'src/integrations/email/drivers/interfaces/email-driver.interface';
|
||||
|
||||
import { EMAIL_DRIVER } from 'src/integrations/email/email.constants';
|
||||
|
||||
@Injectable()
|
||||
export class EmailSenderService implements EmailDriver {
|
||||
constructor(@Inject(EMAIL_DRIVER) private driver: EmailDriver) {}
|
||||
|
||||
async send(sendMailOptions: SendMailOptions): Promise<void> {
|
||||
await this.driver.send(sendMailOptions);
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,7 @@ import { EMAIL_DRIVER } from 'src/integrations/email/email.constants';
|
||||
import { LoggerDriver } from 'src/integrations/email/drivers/logger.driver';
|
||||
import { SmtpDriver } from 'src/integrations/email/drivers/smtp.driver';
|
||||
import { EmailService } from 'src/integrations/email/email.service';
|
||||
import { EmailSenderService } from 'src/integrations/email/email-sender.service';
|
||||
|
||||
@Global()
|
||||
export class EmailModule {
|
||||
@ -22,8 +23,8 @@ export class EmailModule {
|
||||
|
||||
return {
|
||||
module: EmailModule,
|
||||
providers: [EmailService, provider],
|
||||
exports: [EmailService],
|
||||
providers: [EmailSenderService, EmailService, provider],
|
||||
exports: [EmailSenderService, EmailService],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,15 +2,22 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
|
||||
import { SendMailOptions } from 'nodemailer';
|
||||
|
||||
import { EmailDriver } from 'src/integrations/email/drivers/interfaces/email-driver.interface';
|
||||
|
||||
import { EMAIL_DRIVER } from 'src/integrations/email/email.constants';
|
||||
import { MessageQueue } from 'src/integrations/message-queue/message-queue.constants';
|
||||
import { MessageQueueService } from 'src/integrations/message-queue/services/message-queue.service';
|
||||
import { EmailSenderJob } from 'src/integrations/email/email-sender.job';
|
||||
|
||||
@Injectable()
|
||||
export class EmailService implements EmailDriver {
|
||||
constructor(@Inject(EMAIL_DRIVER) private driver: EmailDriver) {}
|
||||
export class EmailService {
|
||||
constructor(
|
||||
@Inject(MessageQueue.emailQueue)
|
||||
private readonly messageQueueService: MessageQueueService,
|
||||
) {}
|
||||
|
||||
async send(sendMailOptions: SendMailOptions): Promise<void> {
|
||||
await this.driver.send(sendMailOptions);
|
||||
await this.messageQueueService.add<SendMailOptions>(
|
||||
EmailSenderJob.name,
|
||||
sendMailOptions,
|
||||
{ retryLimit: 3 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,8 +6,8 @@ import { exceptionHandlerModuleFactory } from 'src/integrations/exception-handle
|
||||
import { fileStorageModuleFactory } from 'src/integrations/file-storage/file-storage.module-factory';
|
||||
import { loggerModuleFactory } from 'src/integrations/logger/logger.module-factory';
|
||||
import { messageQueueModuleFactory } from 'src/integrations/message-queue/message-queue.module-factory';
|
||||
import { emailModuleFactory } from 'src/integrations/email/email.module-factory';
|
||||
import { EmailModule } from 'src/integrations/email/email.module';
|
||||
import { emailModuleFactory } from 'src/integrations/email/email.module-factory';
|
||||
|
||||
import { EnvironmentModule } from './environment/environment.module';
|
||||
import { EnvironmentService } from './environment/environment.service';
|
||||
|
||||
@ -10,6 +10,7 @@ import { ObjectMetadataModule } from 'src/metadata/object-metadata/object-metada
|
||||
import { DataSourceModule } from 'src/metadata/data-source/data-source.module';
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { FetchWorkspaceMessagesModule } from 'src/workspace/messaging/services/fetch-workspace-messages.module';
|
||||
import { EmailSenderJob } from 'src/integrations/email/email-sender.job';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -33,6 +34,10 @@ import { FetchWorkspaceMessagesModule } from 'src/workspace/messaging/services/f
|
||||
provide: CallWebhookJob.name,
|
||||
useClass: CallWebhookJob,
|
||||
},
|
||||
{
|
||||
provide: EmailSenderJob.name,
|
||||
useClass: EmailSenderJob,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class JobsModule {
|
||||
|
||||
@ -5,4 +5,5 @@ export enum MessageQueue {
|
||||
messagingQueue = 'messaging-queue',
|
||||
webhookQueue = 'webhook-queue',
|
||||
cronQueue = 'cron-queue',
|
||||
emailQueue = 'email-queue',
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import { loggerModuleFactory } from 'src/integrations/logger/logger.module-facto
|
||||
import { JobsModule } from 'src/integrations/message-queue/jobs.module';
|
||||
import { MessageQueueModule } from 'src/integrations/message-queue/message-queue.module';
|
||||
import { messageQueueModuleFactory } from 'src/integrations/message-queue/message-queue.module-factory';
|
||||
import { IntegrationsModule } from 'src/integrations/integrations.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -20,6 +21,7 @@ import { messageQueueModuleFactory } from 'src/integrations/message-queue/messag
|
||||
inject: [EnvironmentService],
|
||||
}),
|
||||
JobsModule,
|
||||
IntegrationsModule,
|
||||
],
|
||||
})
|
||||
export class QueueWorkerModule {}
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
"forceConsistentCasingInFileNames": false,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
"resolveJsonModule": true,
|
||||
"types": ["jest", "node"]
|
||||
"types": ["jest", "node"],
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user