mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
feat: Notification Email Templates (#13940)
This commit is contained in:
parent
4bf1b84cc2
commit
292182fa7f
32 changed files with 1136 additions and 105 deletions
|
|
@ -146,6 +146,13 @@ export interface SystemConfig {
|
|||
};
|
||||
};
|
||||
};
|
||||
templates: {
|
||||
email: {
|
||||
welcomeTemplate: string;
|
||||
albumInviteTemplate: string;
|
||||
albumUpdateTemplate: string;
|
||||
};
|
||||
};
|
||||
server: {
|
||||
externalDomain: string;
|
||||
loginPageMessage: string;
|
||||
|
|
@ -313,6 +320,13 @@ export const defaults = Object.freeze<SystemConfig>({
|
|||
},
|
||||
},
|
||||
},
|
||||
templates: {
|
||||
email: {
|
||||
welcomeTemplate: '',
|
||||
albumInviteTemplate: '',
|
||||
albumUpdateTemplate: '',
|
||||
},
|
||||
},
|
||||
user: {
|
||||
deleteDelay: 7,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
|
||||
import { Body, Controller, HttpCode, HttpStatus, Param, Post } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { TestEmailResponseDto } from 'src/dtos/notification.dto';
|
||||
import { TemplateDto, TemplateResponseDto, TestEmailResponseDto } from 'src/dtos/notification.dto';
|
||||
import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
|
||||
import { EmailTemplate } from 'src/interfaces/notification.interface';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { NotificationService } from 'src/services/notification.service';
|
||||
|
||||
|
|
@ -17,4 +18,15 @@ export class NotificationController {
|
|||
sendTestEmail(@Auth() auth: AuthDto, @Body() dto: SystemConfigSmtpDto): Promise<TestEmailResponseDto> {
|
||||
return this.service.sendTestEmail(auth.user.id, dto);
|
||||
}
|
||||
|
||||
@Post('templates/:name')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated({ admin: true })
|
||||
getNotificationTemplate(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param('name') name: EmailTemplate,
|
||||
@Body() dto: TemplateDto,
|
||||
): Promise<TemplateResponseDto> {
|
||||
return this.service.getTemplate(name, dto.template);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,13 @@
|
|||
import { IsString } from 'class-validator';
|
||||
|
||||
export class TestEmailResponseDto {
|
||||
messageId!: string;
|
||||
}
|
||||
export class TemplateResponseDto {
|
||||
name!: string;
|
||||
html!: string;
|
||||
}
|
||||
export class TemplateDto {
|
||||
@IsString()
|
||||
template!: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -465,6 +465,24 @@ class SystemConfigNotificationsDto {
|
|||
smtp!: SystemConfigSmtpDto;
|
||||
}
|
||||
|
||||
class SystemConfigTemplateEmailsDto {
|
||||
@IsString()
|
||||
albumInviteTemplate!: string;
|
||||
|
||||
@IsString()
|
||||
welcomeTemplate!: string;
|
||||
|
||||
@IsString()
|
||||
albumUpdateTemplate!: string;
|
||||
}
|
||||
|
||||
class SystemConfigTemplatesDto {
|
||||
@Type(() => SystemConfigTemplateEmailsDto)
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
email!: SystemConfigTemplateEmailsDto;
|
||||
}
|
||||
|
||||
class SystemConfigStorageTemplateDto {
|
||||
@ValidateBoolean()
|
||||
enabled!: boolean;
|
||||
|
|
@ -636,6 +654,11 @@ export class SystemConfigDto implements SystemConfig {
|
|||
@IsObject()
|
||||
notifications!: SystemConfigNotificationsDto;
|
||||
|
||||
@Type(() => SystemConfigTemplatesDto)
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
templates!: SystemConfigTemplatesDto;
|
||||
|
||||
@Type(() => SystemConfigServerDto)
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import * as React from 'react';
|
|||
import { ImmichButton } from 'src/emails/components/button.component';
|
||||
import ImmichLayout from 'src/emails/components/immich.layout';
|
||||
import { AlbumInviteEmailProps } from 'src/interfaces/notification.interface';
|
||||
import { replaceTemplateTags } from 'src/utils/replace-template-tags';
|
||||
|
||||
export const AlbumInviteEmail = ({
|
||||
baseUrl,
|
||||
|
|
@ -11,39 +12,64 @@ export const AlbumInviteEmail = ({
|
|||
senderName,
|
||||
albumId,
|
||||
cid,
|
||||
}: AlbumInviteEmailProps) => (
|
||||
<ImmichLayout preview="You have been added to a shared album.">
|
||||
<Text className="m-0">
|
||||
Hey <strong>{recipientName}</strong>!
|
||||
</Text>
|
||||
customTemplate,
|
||||
}: AlbumInviteEmailProps) => {
|
||||
const variables = {
|
||||
albumName,
|
||||
recipientName,
|
||||
senderName,
|
||||
albumId,
|
||||
baseUrl,
|
||||
};
|
||||
|
||||
<Text>
|
||||
{senderName} has added you to the album <strong>{albumName}</strong>.
|
||||
</Text>
|
||||
const emailContent = customTemplate ? (
|
||||
replaceTemplateTags(customTemplate, variables)
|
||||
) : (
|
||||
<>
|
||||
<Text className="m-0">
|
||||
Hey <strong>{recipientName}</strong>!
|
||||
</Text>
|
||||
|
||||
{cid && (
|
||||
<Section className="flex justify-center my-0">
|
||||
<Img
|
||||
className="max-w-[300px] w-full rounded-lg"
|
||||
src={`cid:${cid}`}
|
||||
style={{
|
||||
boxShadow: 'rgba(50, 50, 93, 0.25) 0px 13px 27px -5px, rgba(0, 0, 0, 0.3) 0px 8px 16px -8px',
|
||||
}}
|
||||
/>
|
||||
<Text>
|
||||
{senderName} has added you to the album <strong>{albumName}</strong>.
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<ImmichLayout preview={customTemplate ? emailContent.toString() : 'You have been added to a shared album.'}>
|
||||
{customTemplate && (
|
||||
<Text className="m-0">
|
||||
<div dangerouslySetInnerHTML={{ __html: emailContent }}></div>
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{!customTemplate && emailContent}
|
||||
|
||||
{cid && (
|
||||
<Section className="flex justify-center my-0">
|
||||
<Img
|
||||
className="max-w-[300px] w-full rounded-lg"
|
||||
src={`cid:${cid}`}
|
||||
style={{
|
||||
boxShadow: 'rgba(50, 50, 93, 0.25) 0px 13px 27px -5px, rgba(0, 0, 0, 0.3) 0px 8px 16px -8px',
|
||||
}}
|
||||
/>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
<Section className="flex justify-center my-6">
|
||||
<ImmichButton href={`${baseUrl}/albums/${albumId}`}>View Album</ImmichButton>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
<Section className="flex justify-center my-6">
|
||||
<ImmichButton href={`${baseUrl}/albums/${albumId}`}>View Album</ImmichButton>
|
||||
</Section>
|
||||
|
||||
<Text className="text-xs">
|
||||
If you cannot click the button use the link below to view the album.
|
||||
<br />
|
||||
<Link href={`${baseUrl}/albums/${albumId}`}>{`${baseUrl}/albums/${albumId}`}</Link>
|
||||
</Text>
|
||||
</ImmichLayout>
|
||||
);
|
||||
<Text className="text-xs">
|
||||
If you cannot click the button use the link below to view the album.
|
||||
<br />
|
||||
<Link href={`${baseUrl}/albums/${albumId}`}>{`${baseUrl}/albums/${albumId}`}</Link>
|
||||
</Text>
|
||||
</ImmichLayout>
|
||||
);
|
||||
};
|
||||
|
||||
AlbumInviteEmail.PreviewProps = {
|
||||
baseUrl: 'https://demo.immich.app',
|
||||
|
|
|
|||
|
|
@ -3,47 +3,80 @@ import * as React from 'react';
|
|||
import { ImmichButton } from 'src/emails/components/button.component';
|
||||
import ImmichLayout from 'src/emails/components/immich.layout';
|
||||
import { AlbumUpdateEmailProps } from 'src/interfaces/notification.interface';
|
||||
import { replaceTemplateTags } from 'src/utils/replace-template-tags';
|
||||
|
||||
export const AlbumUpdateEmail = ({ baseUrl, albumName, recipientName, albumId, cid }: AlbumUpdateEmailProps) => (
|
||||
<ImmichLayout preview="New media has been added to a shared album.">
|
||||
<Text className="m-0">
|
||||
Hey <strong>{recipientName}</strong>!
|
||||
</Text>
|
||||
export const AlbumUpdateEmail = ({
|
||||
baseUrl,
|
||||
albumName,
|
||||
recipientName,
|
||||
albumId,
|
||||
cid,
|
||||
customTemplate,
|
||||
}: AlbumUpdateEmailProps) => {
|
||||
const usableTemplateVariables = {
|
||||
albumName,
|
||||
recipientName,
|
||||
albumId,
|
||||
baseUrl,
|
||||
};
|
||||
|
||||
<Text>
|
||||
New media has been added to <strong>{albumName}</strong>,
|
||||
<br /> check it out!
|
||||
</Text>
|
||||
const emailContent = customTemplate ? (
|
||||
replaceTemplateTags(customTemplate, usableTemplateVariables)
|
||||
) : (
|
||||
<>
|
||||
<Text className="m-0">
|
||||
Hey <strong>{recipientName}</strong>!
|
||||
</Text>
|
||||
|
||||
{cid && (
|
||||
<Section className="flex justify-center my-0">
|
||||
<Img
|
||||
className="max-w-[300px] w-full rounded-lg"
|
||||
src={`cid:${cid}`}
|
||||
style={{
|
||||
boxShadow: 'rgba(50, 50, 93, 0.25) 0px 13px 27px -5px, rgba(0, 0, 0, 0.3) 0px 8px 16px -8px',
|
||||
}}
|
||||
/>
|
||||
<Text>
|
||||
New media has been added to <strong>{albumName}</strong>,
|
||||
<br /> check it out!
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<ImmichLayout preview={customTemplate ? emailContent.toString() : 'New media has been added to a shared album.'}>
|
||||
{customTemplate && (
|
||||
<Text className="m-0">
|
||||
<div dangerouslySetInnerHTML={{ __html: emailContent }}></div>
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{!customTemplate && emailContent}
|
||||
|
||||
{cid && (
|
||||
<Section className="flex justify-center my-0">
|
||||
<Img
|
||||
className="max-w-[300px] w-full rounded-lg"
|
||||
src={`cid:${cid}`}
|
||||
style={{
|
||||
boxShadow: 'rgba(50, 50, 93, 0.25) 0px 13px 27px -5px, rgba(0, 0, 0, 0.3) 0px 8px 16px -8px',
|
||||
}}
|
||||
/>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
<Section className="flex justify-center my-6">
|
||||
<ImmichButton href={`${baseUrl}/albums/${albumId}`}>View Album</ImmichButton>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
<Section className="flex justify-center my-6">
|
||||
<ImmichButton href={`${baseUrl}/albums/${albumId}`}>View Album</ImmichButton>
|
||||
</Section>
|
||||
|
||||
<Text className="text-xs">
|
||||
If you cannot click the button use the link below to view the album.
|
||||
<br />
|
||||
<Link href={`${baseUrl}/albums/${albumId}`}>{`${baseUrl}/albums/${albumId}`}</Link>
|
||||
</Text>
|
||||
</ImmichLayout>
|
||||
);
|
||||
<Text className="text-xs">
|
||||
If you cannot click the button use the link below to view the album.
|
||||
<br />
|
||||
<Link href={`${baseUrl}/albums/${albumId}`}>{`${baseUrl}/albums/${albumId}`}</Link>
|
||||
</Text>
|
||||
</ImmichLayout>
|
||||
);
|
||||
};
|
||||
|
||||
AlbumUpdateEmail.PreviewProps = {
|
||||
baseUrl: 'https://demo.immich.app',
|
||||
albumName: 'Trip to Europe',
|
||||
albumId: 'b63f6dae-e1c9-401b-9a85-9dbbf5612539',
|
||||
recipientName: 'Alan Turing',
|
||||
cid: '',
|
||||
customTemplate: '',
|
||||
} as AlbumUpdateEmailProps;
|
||||
|
||||
export default AlbumUpdateEmail;
|
||||
|
|
|
|||
|
|
@ -3,36 +3,62 @@ import * as React from 'react';
|
|||
import { ImmichButton } from 'src/emails/components/button.component';
|
||||
import ImmichLayout from 'src/emails/components/immich.layout';
|
||||
import { WelcomeEmailProps } from 'src/interfaces/notification.interface';
|
||||
import { replaceTemplateTags } from 'src/utils/replace-template-tags';
|
||||
|
||||
export const WelcomeEmail = ({ baseUrl, displayName, username, password }: WelcomeEmailProps) => (
|
||||
<ImmichLayout preview="You have been invited to a new Immich instance.">
|
||||
<Text className="m-0">
|
||||
Hey <strong>{displayName}</strong>!
|
||||
</Text>
|
||||
export const WelcomeEmail = ({ baseUrl, displayName, username, password, customTemplate }: WelcomeEmailProps) => {
|
||||
const usableTemplateVariables = {
|
||||
displayName,
|
||||
username,
|
||||
password,
|
||||
baseUrl,
|
||||
};
|
||||
|
||||
<Text>A new account has been created for you.</Text>
|
||||
const emailContent = customTemplate ? (
|
||||
replaceTemplateTags(customTemplate, usableTemplateVariables)
|
||||
) : (
|
||||
<>
|
||||
<Text className="m-0">
|
||||
Hey <strong>{displayName}</strong>!
|
||||
</Text>
|
||||
|
||||
<Text>
|
||||
<strong>Username</strong>: {username}
|
||||
{password && (
|
||||
<>
|
||||
<br />
|
||||
<strong>Password</strong>: {password}
|
||||
</>
|
||||
<Text>A new account has been created for you.</Text>
|
||||
|
||||
<Text>
|
||||
<strong>Username</strong>: {username}
|
||||
{password && (
|
||||
<>
|
||||
<br />
|
||||
<strong>Password</strong>: {password}
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<ImmichLayout
|
||||
preview={customTemplate ? emailContent.toString() : 'You have been invited to a new Immich instance.'}
|
||||
>
|
||||
{customTemplate && (
|
||||
<Text className="m-0">
|
||||
<div dangerouslySetInnerHTML={{ __html: emailContent }}></div>
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
|
||||
<Section className="flex justify-center my-6">
|
||||
<ImmichButton href={`${baseUrl}/auth/login`}>Login</ImmichButton>
|
||||
</Section>
|
||||
{!customTemplate && emailContent}
|
||||
|
||||
<Text className="text-xs">
|
||||
If you cannot click the button use the link below to proceed with first login.
|
||||
<br />
|
||||
<Link href={baseUrl}>{baseUrl}</Link>
|
||||
</Text>
|
||||
</ImmichLayout>
|
||||
);
|
||||
<Section className="flex justify-center my-6">
|
||||
<ImmichButton href={`${baseUrl}/auth/login`}>Login</ImmichButton>
|
||||
</Section>
|
||||
|
||||
<Text className="text-xs">
|
||||
If you cannot click the button use the link below to proceed with first login.
|
||||
<br />
|
||||
<Link href={baseUrl}>{baseUrl}</Link>
|
||||
</Text>
|
||||
</ImmichLayout>
|
||||
);
|
||||
};
|
||||
|
||||
WelcomeEmail.PreviewProps = {
|
||||
baseUrl: 'https://demo.immich.app/auth/login',
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ export enum EmailTemplate {
|
|||
|
||||
interface BaseEmailProps {
|
||||
baseUrl: string;
|
||||
customTemplate?: string;
|
||||
}
|
||||
|
||||
export interface TestEmailProps extends BaseEmailProps {
|
||||
|
|
@ -70,18 +71,22 @@ export type EmailRenderRequest =
|
|||
| {
|
||||
template: EmailTemplate.TEST_EMAIL;
|
||||
data: TestEmailProps;
|
||||
customTemplate: string;
|
||||
}
|
||||
| {
|
||||
template: EmailTemplate.WELCOME;
|
||||
data: WelcomeEmailProps;
|
||||
customTemplate: string;
|
||||
}
|
||||
| {
|
||||
template: EmailTemplate.ALBUM_INVITE;
|
||||
data: AlbumInviteEmailProps;
|
||||
customTemplate: string;
|
||||
}
|
||||
| {
|
||||
template: EmailTemplate.ALBUM_UPDATE;
|
||||
data: AlbumUpdateEmailProps;
|
||||
customTemplate: string;
|
||||
};
|
||||
|
||||
export type SendEmailResponse = {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ describe(NotificationRepository.name, () => {
|
|||
const request: EmailRenderRequest = {
|
||||
template: EmailTemplate.TEST_EMAIL,
|
||||
data: { displayName: 'Alen Turing', baseUrl: 'http://localhost' },
|
||||
customTemplate: '',
|
||||
};
|
||||
|
||||
const result = await sut.renderEmail(request);
|
||||
|
|
@ -33,6 +34,7 @@ describe(NotificationRepository.name, () => {
|
|||
const request: EmailRenderRequest = {
|
||||
template: EmailTemplate.WELCOME,
|
||||
data: { displayName: 'Alen Turing', username: 'turing', baseUrl: 'http://localhost' },
|
||||
customTemplate: '',
|
||||
};
|
||||
|
||||
const result = await sut.renderEmail(request);
|
||||
|
|
@ -51,6 +53,7 @@ describe(NotificationRepository.name, () => {
|
|||
recipientName: 'Jane',
|
||||
baseUrl: 'http://localhost',
|
||||
},
|
||||
customTemplate: '',
|
||||
};
|
||||
|
||||
const result = await sut.renderEmail(request);
|
||||
|
|
@ -63,6 +66,7 @@ describe(NotificationRepository.name, () => {
|
|||
const request: EmailRenderRequest = {
|
||||
template: EmailTemplate.ALBUM_UPDATE,
|
||||
data: { albumName: 'Holiday', albumId: '123', recipientName: 'Jane', baseUrl: 'http://localhost' },
|
||||
customTemplate: '',
|
||||
};
|
||||
|
||||
const result = await sut.renderEmail(request);
|
||||
|
|
|
|||
|
|
@ -55,22 +55,22 @@ export class NotificationRepository implements INotificationRepository {
|
|||
}
|
||||
}
|
||||
|
||||
private render({ template, data }: EmailRenderRequest): React.FunctionComponentElement<any> {
|
||||
private render({ template, data, customTemplate }: EmailRenderRequest): React.FunctionComponentElement<any> {
|
||||
switch (template) {
|
||||
case EmailTemplate.TEST_EMAIL: {
|
||||
return React.createElement(TestEmail, data);
|
||||
return React.createElement(TestEmail, { ...data, customTemplate });
|
||||
}
|
||||
|
||||
case EmailTemplate.WELCOME: {
|
||||
return React.createElement(WelcomeEmail, data);
|
||||
return React.createElement(WelcomeEmail, { ...data, customTemplate });
|
||||
}
|
||||
|
||||
case EmailTemplate.ALBUM_INVITE: {
|
||||
return React.createElement(AlbumInviteEmail, data);
|
||||
return React.createElement(AlbumInviteEmail, { ...data, customTemplate });
|
||||
}
|
||||
|
||||
case EmailTemplate.ALBUM_UPDATE: {
|
||||
return React.createElement(AlbumUpdateEmail, data);
|
||||
return React.createElement(AlbumUpdateEmail, { ...data, customTemplate });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ export class NotificationService extends BaseService {
|
|||
setTimeout(() => this.eventRepository.clientSend('on_session_delete', sessionId, sessionId), 500);
|
||||
}
|
||||
|
||||
async sendTestEmail(id: string, dto: SystemConfigSmtpDto) {
|
||||
async sendTestEmail(id: string, dto: SystemConfigSmtpDto, tempTemplate?: string) {
|
||||
const user = await this.userRepository.get(id, { withDeleted: false });
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
|
|
@ -160,8 +160,8 @@ export class NotificationService extends BaseService {
|
|||
baseUrl: getExternalDomain(server, port),
|
||||
displayName: user.name,
|
||||
},
|
||||
customTemplate: tempTemplate!,
|
||||
});
|
||||
|
||||
const { messageId } = await this.notificationRepository.sendEmail({
|
||||
to: user.email,
|
||||
subject: 'Test email from Immich',
|
||||
|
|
@ -175,6 +175,69 @@ export class NotificationService extends BaseService {
|
|||
return { messageId };
|
||||
}
|
||||
|
||||
async getTemplate(name: EmailTemplate, customTemplate: string) {
|
||||
const { server, templates } = await this.getConfig({ withCache: false });
|
||||
const { port } = this.configRepository.getEnv();
|
||||
|
||||
let templateResponse = '';
|
||||
|
||||
switch (name) {
|
||||
case EmailTemplate.WELCOME: {
|
||||
const { html: _welcomeHtml } = await this.notificationRepository.renderEmail({
|
||||
template: EmailTemplate.WELCOME,
|
||||
data: {
|
||||
baseUrl: getExternalDomain(server, port),
|
||||
displayName: 'John Doe',
|
||||
username: 'john@doe.com',
|
||||
password: 'thisIsAPassword123',
|
||||
},
|
||||
customTemplate: customTemplate || templates.email.welcomeTemplate,
|
||||
});
|
||||
|
||||
templateResponse = _welcomeHtml;
|
||||
break;
|
||||
}
|
||||
case EmailTemplate.ALBUM_UPDATE: {
|
||||
const { html: _updateAlbumHtml } = await this.notificationRepository.renderEmail({
|
||||
template: EmailTemplate.ALBUM_UPDATE,
|
||||
data: {
|
||||
baseUrl: getExternalDomain(server, port),
|
||||
albumId: '1',
|
||||
albumName: 'Favorite Photos',
|
||||
recipientName: 'Jane Doe',
|
||||
cid: undefined,
|
||||
},
|
||||
customTemplate: customTemplate || templates.email.albumInviteTemplate,
|
||||
});
|
||||
templateResponse = _updateAlbumHtml;
|
||||
break;
|
||||
}
|
||||
|
||||
case EmailTemplate.ALBUM_INVITE: {
|
||||
const { html } = await this.notificationRepository.renderEmail({
|
||||
template: EmailTemplate.ALBUM_INVITE,
|
||||
data: {
|
||||
baseUrl: getExternalDomain(server, port),
|
||||
albumId: '1',
|
||||
albumName: "John Doe's Favorites",
|
||||
senderName: 'John Doe',
|
||||
recipientName: 'Jane Doe',
|
||||
cid: undefined,
|
||||
},
|
||||
customTemplate: customTemplate || templates.email.albumInviteTemplate,
|
||||
});
|
||||
templateResponse = html;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
templateResponse = '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { name, html: templateResponse };
|
||||
}
|
||||
|
||||
@OnJob({ name: JobName.NOTIFY_SIGNUP, queue: QueueName.NOTIFICATION })
|
||||
async handleUserSignup({ id, tempPassword }: JobOf<JobName.NOTIFY_SIGNUP>) {
|
||||
const user = await this.userRepository.get(id, { withDeleted: false });
|
||||
|
|
@ -182,7 +245,7 @@ export class NotificationService extends BaseService {
|
|||
return JobStatus.SKIPPED;
|
||||
}
|
||||
|
||||
const { server } = await this.getConfig({ withCache: true });
|
||||
const { server, templates } = await this.getConfig({ withCache: true });
|
||||
const { port } = this.configRepository.getEnv();
|
||||
const { html, text } = await this.notificationRepository.renderEmail({
|
||||
template: EmailTemplate.WELCOME,
|
||||
|
|
@ -192,6 +255,7 @@ export class NotificationService extends BaseService {
|
|||
username: user.email,
|
||||
password: tempPassword,
|
||||
},
|
||||
customTemplate: templates.email.welcomeTemplate,
|
||||
});
|
||||
|
||||
await this.jobRepository.queue({
|
||||
|
|
@ -227,7 +291,7 @@ export class NotificationService extends BaseService {
|
|||
|
||||
const attachment = await this.getAlbumThumbnailAttachment(album);
|
||||
|
||||
const { server } = await this.getConfig({ withCache: false });
|
||||
const { server, templates } = await this.getConfig({ withCache: false });
|
||||
const { port } = this.configRepository.getEnv();
|
||||
const { html, text } = await this.notificationRepository.renderEmail({
|
||||
template: EmailTemplate.ALBUM_INVITE,
|
||||
|
|
@ -239,6 +303,7 @@ export class NotificationService extends BaseService {
|
|||
recipientName: recipient.name,
|
||||
cid: attachment ? attachment.cid : undefined,
|
||||
},
|
||||
customTemplate: templates.email.albumInviteTemplate,
|
||||
});
|
||||
|
||||
await this.jobRepository.queue({
|
||||
|
|
@ -273,7 +338,7 @@ export class NotificationService extends BaseService {
|
|||
);
|
||||
const attachment = await this.getAlbumThumbnailAttachment(album);
|
||||
|
||||
const { server } = await this.getConfig({ withCache: false });
|
||||
const { server, templates } = await this.getConfig({ withCache: false });
|
||||
const { port } = this.configRepository.getEnv();
|
||||
|
||||
for (const recipient of recipients) {
|
||||
|
|
@ -297,6 +362,7 @@ export class NotificationService extends BaseService {
|
|||
recipientName: recipient.name,
|
||||
cid: attachment ? attachment.cid : undefined,
|
||||
},
|
||||
customTemplate: templates.email.albumUpdateTemplate,
|
||||
});
|
||||
|
||||
await this.jobRepository.queue({
|
||||
|
|
|
|||
|
|
@ -190,6 +190,13 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
|||
},
|
||||
},
|
||||
},
|
||||
templates: {
|
||||
email: {
|
||||
albumInviteTemplate: '',
|
||||
welcomeTemplate: '',
|
||||
albumUpdateTemplate: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe(SystemConfigService.name, () => {
|
||||
|
|
|
|||
5
server/src/utils/replace-template-tags.ts
Normal file
5
server/src/utils/replace-template-tags.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export const replaceTemplateTags = (template: string, variables: Record<string, string | undefined>) => {
|
||||
return template.replaceAll(/{(.*?)}/g, (_, key) => {
|
||||
return variables[key] || `{${key}}`;
|
||||
});
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue