mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-16 05:47:09 +08:00
refactor(server): mail service (#10934)
This commit is contained in:
@@ -26,7 +26,6 @@ export * from './guard';
|
||||
export { CryptoHelper, URLHelper } from './helpers';
|
||||
export * from './job';
|
||||
export { AFFiNELogger } from './logger';
|
||||
export { MailService } from './mailer';
|
||||
export { CallMetric, metrics } from './metrics';
|
||||
export { Lock, Locker, Mutex, RequestMutex } from './mutex';
|
||||
export * from './nestjs';
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { JobModule, JobQueue, OnJob } from './queue';
|
||||
export { JOB_SIGNAL, JobModule, JobQueue, OnJob } from './queue';
|
||||
|
||||
@@ -93,10 +93,6 @@ test('should register job handler', async t => {
|
||||
|
||||
t.is(handler!.name, 'JobHandlers.handleJob');
|
||||
t.is(typeof handler!.fn, 'function');
|
||||
|
||||
const result = await handler!.fn({ name: 'test' });
|
||||
|
||||
t.is(result, 'test');
|
||||
});
|
||||
// #endregion
|
||||
|
||||
|
||||
@@ -63,3 +63,7 @@ If you want to introduce new job queue, please modify the Queue enum first in ${
|
||||
export function getJobHandlerMetadata(target: any): JobName[] {
|
||||
return sliceMetadata<JobName>(JOB_METADATA, target);
|
||||
}
|
||||
|
||||
export enum JOB_SIGNAL {
|
||||
RETRY = 'retry',
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { metrics, wrapCallMetric } from '../../metrics';
|
||||
import { QueueRedis } from '../../redis';
|
||||
import { Runtime } from '../../runtime';
|
||||
import { genRequestId } from '../../utils';
|
||||
import { namespace, Queue, QUEUES } from './def';
|
||||
import { JOB_SIGNAL, namespace, Queue, QUEUES } from './def';
|
||||
import { JobHandlerScanner } from './scanner';
|
||||
|
||||
@Injectable()
|
||||
@@ -69,8 +69,12 @@ export class JobExecutor
|
||||
try {
|
||||
this.logger.debug(`Job started: ${signature}`);
|
||||
const result = await handler.fn(payload);
|
||||
|
||||
if (result === JOB_SIGNAL.RETRY) {
|
||||
throw new Error(`Manually job retry`);
|
||||
}
|
||||
|
||||
this.logger.debug(`Job finished: ${signature}`);
|
||||
return result;
|
||||
} catch (e) {
|
||||
this.logger.error(`Job failed: ${signature}`, e);
|
||||
throw e;
|
||||
@@ -88,7 +92,7 @@ export class JobExecutor
|
||||
const activeJobs = metrics.queue.counter('active_jobs');
|
||||
activeJobs.add(1, { queue: ns });
|
||||
try {
|
||||
return await fn();
|
||||
await fn();
|
||||
} finally {
|
||||
activeJobs.add(-1, { queue: ns });
|
||||
}
|
||||
|
||||
@@ -42,4 +42,4 @@ export class JobModule {
|
||||
}
|
||||
|
||||
export { JobQueue };
|
||||
export { OnJob } from './def';
|
||||
export { JOB_SIGNAL, OnJob } from './def';
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
|
||||
import { ModuleScanner } from '../../nestjs';
|
||||
import { getJobHandlerMetadata } from './def';
|
||||
import { getJobHandlerMetadata, JOB_SIGNAL } from './def';
|
||||
|
||||
interface JobHandler {
|
||||
name: string;
|
||||
fn: (payload: any) => any;
|
||||
fn: (payload: any) => Promise<JOB_SIGNAL | undefined>;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import SMTPTransport from 'nodemailer/lib/smtp-transport';
|
||||
|
||||
import { defineStartupConfig, ModuleConfig } from '../config';
|
||||
|
||||
declare module '../config' {
|
||||
interface AppConfig {
|
||||
/**
|
||||
* Configurations for mail service used to post auth or bussiness mails.
|
||||
*
|
||||
* @see https://nodemailer.com/smtp/
|
||||
*/
|
||||
mailer: ModuleConfig<SMTPTransport.Options>;
|
||||
}
|
||||
}
|
||||
|
||||
defineStartupConfig('mailer', {});
|
||||
@@ -1,24 +0,0 @@
|
||||
import './config';
|
||||
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
|
||||
import { OptionalModule } from '../nestjs';
|
||||
import { MailService } from './mail.service';
|
||||
import { MAILER } from './mailer';
|
||||
|
||||
@Global()
|
||||
@OptionalModule({
|
||||
providers: [MAILER],
|
||||
exports: [MAILER],
|
||||
requires: ['mailer.host'],
|
||||
})
|
||||
class MailerModule {}
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [MailerModule],
|
||||
providers: [MailService],
|
||||
exports: [MailService],
|
||||
})
|
||||
export class MailModule {}
|
||||
export { MailService };
|
||||
@@ -1,193 +0,0 @@
|
||||
import { Inject, Injectable, Optional } from '@nestjs/common';
|
||||
import SMTPTransport from 'nodemailer/lib/smtp-transport';
|
||||
|
||||
import {
|
||||
EmailRenderer,
|
||||
renderChangeEmailMail,
|
||||
renderChangeEmailNotificationMail,
|
||||
renderChangePasswordMail,
|
||||
renderLinkInvitationApproveMail,
|
||||
renderLinkInvitationDeclineMail,
|
||||
renderLinkInvitationReviewRequestMail,
|
||||
renderMemberAcceptedMail,
|
||||
renderMemberInvitationMail,
|
||||
renderMemberLeaveMail,
|
||||
renderMemberRemovedMail,
|
||||
renderOwnershipReceivedMail,
|
||||
renderOwnershipTransferredMail,
|
||||
renderSetPasswordMail,
|
||||
renderSignInMail,
|
||||
renderSignUpMail,
|
||||
renderTeamBecomeAdminMail,
|
||||
renderTeamBecomeCollaboratorMail,
|
||||
renderTeamDeleteIn1MonthMail,
|
||||
renderTeamDeleteIn24HoursMail,
|
||||
renderTeamLicenseMail,
|
||||
renderTeamWorkspaceDeletedMail,
|
||||
renderTeamWorkspaceExpiredMail,
|
||||
renderTeamWorkspaceExpireSoonMail,
|
||||
renderTeamWorkspaceUpgradedMail,
|
||||
renderVerifyChangeEmailMail,
|
||||
renderVerifyEmailMail,
|
||||
} from '../../mails';
|
||||
import { WorkspaceProps } from '../../mails/components';
|
||||
import { Config } from '../config';
|
||||
import { MailerServiceIsNotConfigured } from '../error';
|
||||
import { metrics } from '../metrics';
|
||||
import type { MailerService, Options } from './mailer';
|
||||
import { MAILER_SERVICE } from './mailer';
|
||||
|
||||
type Props<T extends EmailRenderer<any>> =
|
||||
T extends EmailRenderer<infer P> ? P : never;
|
||||
type Sender<T extends EmailRenderer<any>> = (
|
||||
to: string,
|
||||
props: Props<T>
|
||||
) => Promise<SMTPTransport.SentMessageInfo>;
|
||||
|
||||
function make<T extends EmailRenderer<any>>(
|
||||
sender: {
|
||||
send: (options: Options) => Promise<SMTPTransport.SentMessageInfo>;
|
||||
},
|
||||
renderer: T,
|
||||
factory?: (props: Props<T>) => {
|
||||
props: Props<T>;
|
||||
options: Partial<Options>;
|
||||
}
|
||||
): Sender<T> {
|
||||
return async (to, props) => {
|
||||
const { props: overrideProps, options } = factory
|
||||
? factory(props)
|
||||
: { props, options: {} };
|
||||
|
||||
const { html, subject } = await renderer(overrideProps);
|
||||
return sender.send({
|
||||
to,
|
||||
subject,
|
||||
html,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class MailService {
|
||||
constructor(
|
||||
private readonly config: Config,
|
||||
@Optional() @Inject(MAILER_SERVICE) private readonly mailer?: MailerService
|
||||
) {}
|
||||
|
||||
readonly send = async (options: Options) => {
|
||||
if (!this.mailer) {
|
||||
throw new MailerServiceIsNotConfigured();
|
||||
}
|
||||
|
||||
metrics.mail.counter('total').add(1);
|
||||
try {
|
||||
const result = await this.mailer.sendMail({
|
||||
from: this.config.mailer?.from,
|
||||
...options,
|
||||
});
|
||||
|
||||
metrics.mail.counter('sent').add(1);
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
metrics.mail.counter('error').add(1);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
private make<T extends EmailRenderer<any>>(
|
||||
renderer: T,
|
||||
factory?: (props: Props<T>) => {
|
||||
props: Props<T>;
|
||||
options: Partial<Options>;
|
||||
}
|
||||
) {
|
||||
return make(this, renderer, factory);
|
||||
}
|
||||
|
||||
private readonly convertWorkspaceProps = <
|
||||
T extends { workspace: WorkspaceProps },
|
||||
>(
|
||||
props: T
|
||||
) => {
|
||||
return {
|
||||
props: {
|
||||
...props,
|
||||
workspace: {
|
||||
...props.workspace,
|
||||
avatar: 'cid:workspaceAvatar',
|
||||
},
|
||||
},
|
||||
options: {
|
||||
attachments: [
|
||||
{
|
||||
cid: 'workspaceAvatar',
|
||||
filename: 'workspaceAvatar',
|
||||
content: props.workspace.avatar,
|
||||
encoding: 'base64',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
private makeWorkspace<T extends EmailRenderer<any>>(renderer: T) {
|
||||
return this.make(renderer, this.convertWorkspaceProps);
|
||||
}
|
||||
|
||||
hasConfigured() {
|
||||
return !!this.mailer;
|
||||
}
|
||||
|
||||
// User mails
|
||||
sendSignUpMail = this.make(renderSignUpMail);
|
||||
sendSignInMail = this.make(renderSignInMail);
|
||||
sendChangePasswordMail = this.make(renderChangePasswordMail);
|
||||
sendSetPasswordMail = this.make(renderSetPasswordMail);
|
||||
sendChangeEmailMail = this.make(renderChangeEmailMail);
|
||||
sendVerifyChangeEmail = this.make(renderVerifyChangeEmailMail);
|
||||
sendVerifyEmail = this.make(renderVerifyEmailMail);
|
||||
sendNotificationChangeEmail = make(this, renderChangeEmailNotificationMail);
|
||||
|
||||
// =================== Workspace Mails ===================
|
||||
sendMemberInviteMail = this.makeWorkspace(renderMemberInvitationMail);
|
||||
sendMemberAcceptedEmail = this.makeWorkspace(renderMemberAcceptedMail);
|
||||
sendMemberLeaveEmail = this.makeWorkspace(renderMemberLeaveMail);
|
||||
sendLinkInvitationReviewRequestMail = this.makeWorkspace(
|
||||
renderLinkInvitationReviewRequestMail
|
||||
);
|
||||
sendLinkInvitationApproveMail = this.makeWorkspace(
|
||||
renderLinkInvitationApproveMail
|
||||
);
|
||||
sendLinkInvitationDeclineMail = this.makeWorkspace(
|
||||
renderLinkInvitationDeclineMail
|
||||
);
|
||||
sendMemberRemovedMail = this.makeWorkspace(renderMemberRemovedMail);
|
||||
sendOwnershipTransferredMail = this.makeWorkspace(
|
||||
renderOwnershipTransferredMail
|
||||
);
|
||||
sendOwnershipReceivedMail = this.makeWorkspace(renderOwnershipReceivedMail);
|
||||
|
||||
// =================== Team Workspace Mails ===================
|
||||
sendTeamWorkspaceUpgradedEmail = this.makeWorkspace(
|
||||
renderTeamWorkspaceUpgradedMail
|
||||
);
|
||||
sendTeamBecomeAdminMail = this.makeWorkspace(renderTeamBecomeAdminMail);
|
||||
sendTeamBecomeCollaboratorMail = this.makeWorkspace(
|
||||
renderTeamBecomeCollaboratorMail
|
||||
);
|
||||
sendTeamDeleteIn24HoursMail = this.makeWorkspace(
|
||||
renderTeamDeleteIn24HoursMail
|
||||
);
|
||||
sendTeamDeleteIn1MonthMail = this.makeWorkspace(renderTeamDeleteIn1MonthMail);
|
||||
sendTeamWorkspaceDeletedMail = this.makeWorkspace(
|
||||
renderTeamWorkspaceDeletedMail
|
||||
);
|
||||
sendTeamExpireSoonMail = this.makeWorkspace(
|
||||
renderTeamWorkspaceExpireSoonMail
|
||||
);
|
||||
sendTeamExpiredMail = this.makeWorkspace(renderTeamWorkspaceExpiredMail);
|
||||
sendTeamLicenseMail = this.make(renderTeamLicenseMail);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { FactoryProvider, Logger } from '@nestjs/common';
|
||||
import { createTransport, Transporter } from 'nodemailer';
|
||||
import SMTPTransport from 'nodemailer/lib/smtp-transport';
|
||||
|
||||
import { Config } from '../config';
|
||||
|
||||
export const MAILER_SERVICE = Symbol('MAILER_SERVICE');
|
||||
|
||||
export type MailerService = Transporter<SMTPTransport.SentMessageInfo>;
|
||||
export type Response = SMTPTransport.SentMessageInfo;
|
||||
export type Options = SMTPTransport.Options;
|
||||
|
||||
export const MAILER: FactoryProvider<
|
||||
Transporter<SMTPTransport.SentMessageInfo> | undefined
|
||||
> = {
|
||||
provide: MAILER_SERVICE,
|
||||
useFactory: (config: Config) => {
|
||||
if (config.mailer) {
|
||||
const logger = new Logger('Mailer');
|
||||
const auth = config.mailer.auth;
|
||||
if (auth && auth.user && !('pass' in auth)) {
|
||||
logger.warn(
|
||||
'Mailer service has not configured password, please make sure your mailer service allow empty password.'
|
||||
);
|
||||
}
|
||||
|
||||
return createTransport(config.mailer);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
inject: [Config],
|
||||
};
|
||||
Reference in New Issue
Block a user