Skip to content

Commit

Permalink
feat(核心模块): 新增mailer模块 并建立注册和找回密码邮件验证服务
Browse files Browse the repository at this point in the history
  • Loading branch information
jiayisheji committed Dec 21, 2018
1 parent 6a22fae commit 8cce6fd
Show file tree
Hide file tree
Showing 11 changed files with 304 additions and 0 deletions.
18 changes: 18 additions & 0 deletions src/core/core.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,24 @@ import { MailerModule, SMTPTransportOptions } from './mailer';
}),
inject: [ConfigService],
}),
MailerModule.forRootAsync<SMTPTransportOptions>({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => {
const mailer = configService.getKeys(['MAIL_HOST', 'MAIL_PORT', 'MAIL_USER', 'MAIL_PASS']);
return {
host: mailer.MAIL_HOST, // 邮箱smtp地址
port: mailer.MAIL_PORT * 1, // 端口号
secure: true,
secureConnection: true,
auth: {
user: mailer.MAIL_USER, // 邮箱账号
pass: mailer.MAIL_PASS, // 授权码
},
ignoreTLS: true,
};
},
inject: [ConfigService],
}),
],
})
export class CoreModule {
Expand Down
4 changes: 4 additions & 0 deletions src/core/mailer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './mailer.module';
export * from './mailer.decorators';
export * from './mailer-options.interface';
export * from './mailer.service';
44 changes: 44 additions & 0 deletions src/core/mailer/mailer-options.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { ModuleMetadata } from '@nestjs/common/interfaces';
import { Type } from '@nestjs/common';
export interface MailerModuleOptions{
transport: 'SMTPTransport' | 'SMTPPool' | 'SendmailTransport' | 'StreamTransport' | 'JSONTransport' | 'SESTransport' | 'Transport';
[key: string]: any;
}

import * as JSONTransport from 'nodemailer/lib/json-transport';
import * as SendmailTransport from 'nodemailer/lib/sendmail-transport';
import * as SESTransport from 'nodemailer/lib/ses-transport';
import * as SMTPPool from 'nodemailer/lib/smtp-pool';
import * as SMTPTransport from 'nodemailer/lib/smtp-transport';
import * as StreamTransport from 'nodemailer/lib/stream-transport';

export { TransportOptions } from 'nodemailer';
export interface SendmailTransportOptions extends SendmailTransport.Options {}
export interface JSONTransportOptions extends JSONTransport.Options {}
export interface SESTransportOptions extends SESTransport.Options {}
export interface SMTPTransportOptions extends SMTPTransport.Options {}
export interface SMTPPoolOptions extends SMTPPool.Options {}
export interface StreamTransportOptions extends StreamTransport.Options {}

export interface MailerOptionsFactory<T> {
createMailerOptions(): Promise<T> | T;
}

export interface MailerModuleAsyncOptions<T> extends Pick<ModuleMetadata, 'imports'> {
/**
* 模块的名称
*/
name?: string;
/**
* 应该用于提供MailerOptions的类
*/
useClass?: Type<T>;
/**
* 工厂应该用来提供MailerOptions
*/
useFactory?: (...args: any[]) => Promise<T> | T;
/**
* 应该注入的提供者
*/
inject?: any[];
}
2 changes: 2 additions & 0 deletions src/core/mailer/mailer.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const MAILER_MODULE_OPTIONS = 'MAILER_MODULE_OPTIONS';
export const MAILER_TOKEN = 'MAILER_TOKEN';
5 changes: 5 additions & 0 deletions src/core/mailer/mailer.decorators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Inject } from '@nestjs/common';

import { MAILER_TOKEN } from './mailer.constants';

export const InjectMailer = () => Inject(MAILER_TOKEN);
82 changes: 82 additions & 0 deletions src/core/mailer/mailer.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { DynamicModule, Module, Provider, Global } from '@nestjs/common';
import { MailerModuleAsyncOptions, MailerOptionsFactory } from './mailer-options.interface';
import { MailerService } from './mailer.service';
import { MAILER_MODULE_OPTIONS } from './mailer.constants';
import { createMailerClient } from './mailer.provider';

@Global()
@Module({})
export class MailerModule {
/**
* 同步引导邮箱模块
* @param options 邮箱模块的选项
*/
static forRoot<T>(options: T): DynamicModule {
return {
module: MailerModule,
providers: [
{ provide: MAILER_MODULE_OPTIONS, useValue: options },
createMailerClient<T>(),
MailerService,
],
exports: [MailerService],
};
}

/**
* 异步引导邮箱模块
* @param options 邮箱模块的选项
*/
static forRootAsync<T>(options: MailerModuleAsyncOptions<T>): DynamicModule {
return {
module: MailerModule,
imports: options.imports || [],
providers: [
...this.createAsyncProviders(options),
createMailerClient<T>(),
MailerService,
],
exports: [MailerService],
};
}

/**
* 根据给定的模块选项返回异步提供程序
* @param options 邮箱模块的选项
*/
private static createAsyncProviders<T>(
options: MailerModuleAsyncOptions<T>,
): Provider[] {
if (options.useFactory) {
return [this.createAsyncOptionsProvider<T>(options)];
}
return [
this.createAsyncOptionsProvider(options),
{
provide: options.useClass,
useClass: options.useClass,
},
];
}

/**
* 根据给定的模块选项返回异步邮箱选项提供程序
* @param options 邮箱模块的选项
*/
private static createAsyncOptionsProvider<T>(
options: MailerModuleAsyncOptions<T>,
): Provider {
if (options.useFactory) {
return {
provide: MAILER_MODULE_OPTIONS,
useFactory: options.useFactory,
inject: options.inject || [],
};
}
return {
provide: MAILER_MODULE_OPTIONS,
useFactory: async (optionsFactory: MailerOptionsFactory<T>) => await optionsFactory.createMailerOptions(),
inject: [options.useClass],
};
}
}
10 changes: 10 additions & 0 deletions src/core/mailer/mailer.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { MAILER_MODULE_OPTIONS, MAILER_TOKEN } from './mailer.constants';
import { createTransport } from 'nodemailer';

export const createMailerClient = <T>() => ({
provide: MAILER_TOKEN,
useFactory: (options: T) => {
return createTransport(options);
},
inject: [MAILER_MODULE_OPTIONS],
});
59 changes: 59 additions & 0 deletions src/core/mailer/mailer.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Inject, Injectable, Logger } from '@nestjs/common';
import { MAILER_TOKEN } from './mailer.constants';
import * as Mail from 'nodemailer/lib/mailer';
import { Options as MailMessageOptions } from 'nodemailer/lib/mailer';

import { from, Observable } from 'rxjs';
import { tap, retryWhen, scan, delay } from 'rxjs/operators';

const logger = new Logger('MailerModule');

@Injectable()
export class MailerService {
constructor(
@Inject(MAILER_TOKEN) private readonly mailer: Mail,
) { }
// 注册插件
use(name: string, pluginFunc: (...args) => any): ThisType<MailerService> {
this.mailer.use(name, pluginFunc);
return this;
}

// 设置配置
set(key: string, handler: (...args) => any): ThisType<MailerService> {
this.mailer.set(key, handler);
return this;
}

// 发送邮件配置
async send(mailMessage: MailMessageOptions): Promise<any> {
return await from(this.mailer.sendMail(mailMessage))
.pipe(handleRetry(), tap(() => {
logger.log('send mail success');
this.mailer.close();
}))
.toPromise();
}
}

export function handleRetry(
retryAttempts = 5,
retryDelay = 3000,
): <T>(source: Observable<T>) => Observable<T> {
return <T>(source: Observable<T>) => source.pipe(
retryWhen(e =>
e.pipe(
scan((errorCount, error) => {
logger.error(
`Unable to connect to the database. Retrying (${errorCount + 1})...`);
if (errorCount + 1 >= retryAttempts) {
logger.error('send mail finally error', JSON.stringify(error));
throw error;
}
return errorCount + 1;
}, 0),
delay(retryDelay),
),
),
);
}
2 changes: 2 additions & 0 deletions src/shared/services/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './services.module';
export * from './mail.services';
69 changes: 69 additions & 0 deletions src/shared/services/mail.services.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Injectable } from '@nestjs/common';
import { InjectMailer, MailerService } from 'core/mailer';
import { ConfigService, EnvConfig } from 'config';
import { InjectConfig } from 'config/config.decorators';

@Injectable()
export class MailService {
private readonly from: string;
private readonly name: string;
private readonly host: string;
constructor(
@InjectMailer() private readonly mailer: MailerService,
@InjectConfig() private readonly config: ConfigService<EnvConfig>,
) {
this.name = this.config.get('name');
this.host = `${this.config.get('HOST')}:${this.config.get('PORT')}`;
this.from = `${this.name} <${this.config.get('MAIL_USER')}>`;
}

/**
* 激活邮件
* @param to 激活人邮箱
* @param token token
* @param username 名字
*/
sendActiveMail(to: string, token: string, username: string){
const name = this.name;
const subject = `${name}社区帐号激活`;
const html = `<p>您好:${username}</p>
<p>我们收到您在${name}社区的注册信息,请点击下面的链接来激活帐户:</p>
<a href="${this.host}/active_account?key=${token}&name=${username}">激活链接</a>
<p>若您没有在${name}社区填写过注册信息,说明有人滥用了您的电子邮箱,请删除此邮件,我们对给您造成的打扰感到抱歉。</p>
<p>${name}社区 谨上。</p>`;
this.mailer.send({
from: this.from,
to,
subject,
html,
});
}

/**
* 重置密码邮件
* @param to 重置人邮箱
* @param token token
* @param username 名字
*/
sendResetPassMail(to: string, token: string, username: string) {
const name = this.name;
const subject = `${name}社区密码重置`;
const html = `<p>您好:${username}</p>
<p>我们收到您在${name}社区重置密码的请求,请在24小时内单击下面的链接来重置密码:</p>
<a href="${this.host}/reset_pass?key=${token}&name=${username}">重置密码链接</a>
<p>若您没有在${name}社区填写过注册信息,说明有人滥用了您的电子邮箱,请删除此邮件,我们对给您造成的打扰感到抱歉。</p>
<p>${name}社区 谨上。</p>`;
this.mailer.send({
from: this.from,
to,
subject,
html,
});
this.mailer.send({
from: this.from,
to,
subject,
html,
});
}
}
9 changes: 9 additions & 0 deletions src/shared/services/services.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { MailService } from './mail.services';

@Module({
imports: [],
providers: [MailService],
exports: [MailService],
})
export class ServicesModule { }

0 comments on commit 8cce6fd

Please sign in to comment.