Skip to content

Commit 32f11f4

Browse files
committed
WIP implements sendmail
1 parent 533a028 commit 32f11f4

File tree

10 files changed

+275
-80
lines changed

10 files changed

+275
-80
lines changed

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
"mailparser": "^3.6.5",
5757
"nest-access-control": "^3.0.0",
5858
"nest-commander": "^3.11.1",
59-
"nodemailer": "^6.9.4",
59+
"nodemailer": "^6.9.6",
6060
"passport": "^0.6.0",
6161
"passport-jwt": "^4.0.1",
6262
"radash": "^11.0.0",
@@ -77,9 +77,9 @@
7777
"@types/jest": "^29.5.3",
7878
"@types/lru-cache": "^7.10.10",
7979
"@types/mailparser": "^3.4.0",
80-
"@types/multer": "^1.4.7",
80+
"@types/multer": "^1.4.8",
8181
"@types/node": "^18.0.0",
82-
"@types/nodemailer": "^6.4.9",
82+
"@types/nodemailer": "^6.4.11",
8383
"@types/passport-jwt": "^3.0.9",
8484
"@typescript-eslint/eslint-plugin": "^6.4.0",
8585
"@typescript-eslint/parser": "^6.4.0",
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { ApiProperty } from '@nestjs/swagger'
2+
import { IsDateString, IsEmail, IsEnum, IsObject, IsOptional, IsString } from 'class-validator'
3+
import { TextEncoding } from 'nodemailer/lib/mailer'
4+
5+
export class AccountSubmitDto {
6+
@IsEmail({}, { each: true })
7+
@ApiProperty()
8+
public to: string[]
9+
10+
@IsEmail({}, { each: true })
11+
@IsOptional()
12+
@ApiProperty()
13+
public cc?: string[]
14+
15+
@IsEmail({}, { each: true })
16+
@IsOptional()
17+
@ApiProperty()
18+
public bcc?: string[]
19+
20+
@IsEmail({}, { each: true })
21+
@IsOptional()
22+
@ApiProperty()
23+
public replyTo?: string[]
24+
25+
@IsEmail()
26+
@IsOptional()
27+
@ApiProperty()
28+
public inReplyTo?: string
29+
30+
@IsString()
31+
@IsOptional()
32+
@ApiProperty()
33+
public subject?: string
34+
35+
@IsString()
36+
@IsOptional()
37+
@ApiProperty()
38+
public text?: string
39+
40+
@IsString()
41+
@IsOptional()
42+
@ApiProperty()
43+
public html?: string
44+
45+
@IsString()
46+
@IsOptional()
47+
@ApiProperty()
48+
public raw?: string
49+
50+
@IsEnum({ quotedPrintable: 'quoted-printable', base64: 'base64' })
51+
@IsOptional()
52+
@ApiProperty({ enum: ['quoted-printable', 'base64'] })
53+
public textEncoding?: TextEncoding
54+
55+
@IsDateString()
56+
@IsOptional()
57+
@ApiProperty()
58+
public date?: string
59+
60+
@IsString({ each: true })
61+
@IsOptional()
62+
@ApiProperty()
63+
public references?: string[]
64+
65+
@IsString()
66+
@IsOptional()
67+
@ApiProperty()
68+
public encoding?: string
69+
70+
@IsObject()
71+
@IsOptional()
72+
@ApiProperty()
73+
public headers?: {
74+
[name: string]: any
75+
}
76+
77+
@IsString()
78+
@IsOptional()
79+
@ApiProperty()
80+
public context?: {
81+
[name: string]: any
82+
}
83+
84+
@IsString()
85+
@IsOptional()
86+
@ApiProperty()
87+
public template?: string
88+
}

src/accounts/accounts.controller.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
1-
import { Body, Controller, Delete, Get, HttpStatus, Logger, Param, Patch, Post, Res, Sse } from '@nestjs/common'
1+
import {
2+
Body,
3+
Controller,
4+
Delete,
5+
Get,
6+
HttpStatus,
7+
Logger,
8+
Param, ParseFilePipe,
9+
Patch,
10+
Post,
11+
Res,
12+
Sse, UploadedFiles,
13+
UseInterceptors,
14+
} from '@nestjs/common'
215
import { ModuleRef } from '@nestjs/core'
316
import { Response } from 'express'
417
import { AbstractController } from '~/_common/abstracts/abstract.controller'
@@ -15,6 +28,8 @@ import { ApiReadResponseDecorator } from '~/_common/decorators/api-read-response
1528
import { ApiUpdateDecorator } from '~/_common/decorators/api-update.decorator'
1629
import { ApiDeletedResponseDecorator } from '~/_common/decorators/api-deleted-response.decorator'
1730
import { ApiTags } from '@nestjs/swagger'
31+
import { FileInterceptor } from '@nestjs/platform-express'
32+
import { AccountSubmitDto } from '~/accounts/_dto/account-submit.dto'
1833

1934
@ApiTags('accounts')
2035
@Controller('accounts')
@@ -96,6 +111,27 @@ export class AccountsController extends AbstractController {
96111
})
97112
}
98113

114+
@Post(':account([\\w-.]+)/submit')
115+
@UseInterceptors(FileInterceptor('file'))
116+
@UseRoles({
117+
resource: ScopesEnum.Accounts,
118+
action: ActionEnum.Create,
119+
})
120+
public async submit(
121+
@Res() res: Response,
122+
@Param('account') id: string,
123+
@Body() body: AccountSubmitDto,
124+
@UploadedFiles(
125+
new ParseFilePipe({ fileIsRequired: false }),
126+
) files?: Array<Express.Multer.File>,
127+
): Promise<Response> {
128+
const data = await this.service.submit(id, body, files)
129+
return res.status(HttpStatus.OK).json({
130+
statusCode: HttpStatus.OK,
131+
data,
132+
})
133+
}
134+
99135
@Sse('changes')
100136
public async sse(@Res() res: Response): Promise<Observable<MessageEvent>> {
101137
let subscriber: FSWatcher

src/accounts/accounts.service.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { AbstractService } from '~/_common/abstracts/abstract.service'
66
import { InjectImapflow } from '~/imapflow/imapflow.decorators'
77
import { AccountsFileV1, AccountsMetadataV1, readAccountsFile, writeAccountsFile } from './accounts.setup'
88
import { PartialType } from '@nestjs/swagger'
9+
import { MailerService } from '@nestjs-modules/mailer'
10+
import { AccountSubmitDto } from '~/accounts/_dto/account-submit.dto'
911

1012
class InternalAccountMetadataV1 extends PartialType(AccountsMetadataV1) {
1113
}
@@ -18,6 +20,7 @@ export class AccountsService extends AbstractService {
1820
public constructor(
1921
protected readonly moduleRef: ModuleRef,
2022
@InjectImapflow() protected imapflow: Map<string, ImapFlow>,
23+
private readonly mailerService: MailerService,
2124
) {
2225
super({ moduleRef })
2326
// noinspection JSUnusedGlobalSymbols
@@ -73,4 +76,16 @@ export class AccountsService extends AbstractService {
7376
await writeAccountsFile(accounts, this.cache)
7477
return account
7578
}
79+
80+
public async submit(id: string, body: AccountSubmitDto, files?: Express.Multer.File[]) {
81+
const accounts = await readAccountsFile(this.cache)
82+
const account = accounts.accounts.find((a) => a.id === id)
83+
if (!account) throw new NotFoundException(`Account not found: ${id}`)
84+
return this.mailerService.sendMail({
85+
...body,
86+
attachments: files,
87+
from: account.smtp.from || account.smtp.auth.user,
88+
transporterName: id,
89+
})
90+
}
7691
}

src/accounts/accounts.setup.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,16 @@ export class AccountsMetadataImapV1 implements ImapFlowOptions {
9898
public maxIdleTime: number = 60_000
9999
}
100100

101+
export class AccountsMetadataSmtpAuthV1 {
102+
@IsString()
103+
@ApiProperty()
104+
public user: string
105+
106+
@IsString()
107+
@ApiProperty()
108+
public pass: string
109+
}
110+
101111
export class AccountsMetadataSmtpV1 {
102112
@IsString()
103113
@ApiProperty()
@@ -110,18 +120,26 @@ export class AccountsMetadataSmtpV1 {
110120
@ApiProperty()
111121
public port?: number = 25
112122

123+
@IsString()
124+
@IsOptional()
125+
@ApiProperty()
126+
public from?: string
127+
113128
@IsBoolean()
114129
@IsOptional()
115130
@ApiProperty()
116-
public tls: boolean = false
131+
public ignoreTLS: boolean = false
117132

118-
@IsString()
133+
@IsBoolean()
134+
@IsOptional()
119135
@ApiProperty()
120-
public username: string
136+
public secure: boolean = true
121137

122-
@IsString()
138+
@ValidateNested()
139+
@IsOptional()
140+
@Type(() => AccountsMetadataSmtpAuthV1)
123141
@ApiProperty()
124-
public password: string
142+
public auth?: AccountsMetadataSmtpAuthV1
125143

126144
// noinspection JSUnusedGlobalSymbols
127145
public toJSON() {
@@ -228,7 +246,7 @@ export class AccountsMetadataV1 {
228246
public webhooks?: AccountsMetadataWebhooksV1[]
229247
}
230248

231-
export default async function setupAccounts(): Promise<any[]> {
249+
export default async function setupAccounts(): Promise<AccountsMetadataV1[]> {
232250
try {
233251
if (existsSync(ACCOUNTS_FILE_PATH)) {
234252
Logger.verbose('Account file found, validating...', 'setupAccounts')
@@ -240,7 +258,7 @@ export default async function setupAccounts(): Promise<any[]> {
240258
}
241259
}
242260

243-
export async function validateAccounts(): Promise<any[]> {
261+
export async function validateAccounts(): Promise<AccountsMetadataV1[]> {
244262
const data = readFileSync(ACCOUNTS_FILE_PATH, 'utf8')
245263
const yml = parse(data)
246264
const schema = plainToInstance(AccountsFileV1, yml)

src/app.module.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ import { AccountsMetadataV1 } from './accounts/accounts.setup'
1313
import { APP_GUARD, APP_PIPE } from '@nestjs/core'
1414
import { CronModule } from '~/accounts/cron/cron.module'
1515
import { AuthGuard } from '~/_common/guards/auth.guard'
16-
import { AccessControlModule, RolesBuilder } from "nest-access-control";
17-
import { AclsService } from "~/acls/acls.service";
18-
import { AclsModule } from "~/acls/acls.module";
19-
import { AclGuard } from "~/_common/guards/acl.guard";
16+
import { AccessControlModule, RolesBuilder } from 'nest-access-control'
17+
import { AclsService } from '~/acls/acls.service'
18+
import { AclsModule } from '~/acls/acls.module'
19+
import { AclGuard } from '~/_common/guards/acl.guard'
2020
import { DtoValidationPipe } from '~/_common/pipes/dto-validation.pipe'
2121
import { ScheduleModule } from '@nestjs/schedule'
22+
import { MailerModule, MailerOptions } from '@nestjs-modules/mailer'
23+
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter'
2224

2325
@Module({
2426
imports: [
@@ -43,13 +45,18 @@ import { ScheduleModule } from '@nestjs/schedule'
4345
config: config.get<AccountsMetadataV1[]>('mailer.accounts'),
4446
}),
4547
}),
48+
MailerModule.forRootAsync({
49+
imports: [ConfigModule],
50+
inject: [ConfigService],
51+
useFactory: async (config: ConfigService) => ({
52+
...config.get<MailerOptions>('mailer.options'),
53+
}),
54+
}),
4655
AccessControlModule.forRootAsync({
4756
imports: [AclsModule],
4857
inject: [AclsService],
4958
useFactory: async (aclService: AclsService) => {
50-
return new RolesBuilder(
51-
await aclService.getGrantsObject(),
52-
)
59+
return new RolesBuilder(await aclService.getGrantsObject())
5360
},
5461
}),
5562
ScheduleModule.forRoot(),

src/config.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { BinaryLike, CipherCCMTypes, CipherGCMTypes, CipherKey, createHash } fro
77
import { SwaggerCustomOptions } from '@nestjs/swagger'
88
import setupAccounts, { AccountsMetadataV1 } from './accounts/accounts.setup'
99
import setupTokens, { TokensMetadataV1 } from "~/tokens/tokens.setup";
10+
import { MailerOptions } from '@nestjs-modules/mailer'
11+
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter'
1012

1113
export interface ConfigInstance {
1214
application: NestApplicationContextOptions
@@ -16,6 +18,7 @@ export interface ConfigInstance {
1618
}
1719
mailer: {
1820
accounts: AccountsMetadataV1[]
21+
options?: MailerOptions
1922
}
2023
crypt: {
2124
algorithm: string | CipherCCMTypes | CipherGCMTypes
@@ -60,6 +63,24 @@ export default async (): Promise<ConfigInstance> => {
6063
},
6164
mailer: {
6265
accounts: mailerAccounts,
66+
options: {
67+
transports: mailerAccounts.reduce((acc, account) => {
68+
if (account.smtp) {
69+
acc[account.id] = account.smtp
70+
}
71+
return acc
72+
}, {}),
73+
defaults: {
74+
from: '"nest-modules" <modules@nestjs.com>',
75+
},
76+
template: {
77+
dir: __dirname + '/../../templates',
78+
adapter: new HandlebarsAdapter(),
79+
options: {
80+
strict: true,
81+
},
82+
},
83+
},
6384
},
6485
jwt: {
6586
options: {

src/main.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// noinspection JSUnresolvedReference
22

3-
import { BadRequestException, HttpStatus, INestApplication, Logger, ValidationError, ValidationPipe } from '@nestjs/common'
3+
import { INestApplication, Logger } from '@nestjs/common'
44
import { NestFactory } from '@nestjs/core'
55
import { NestExpressApplication } from '@nestjs/platform-express'
66
import { Response } from 'express'
@@ -9,6 +9,7 @@ import { join } from 'path'
99
import configInstance from './config'
1010
import { AppModule } from './app.module'
1111
import passport from 'passport'
12+
import 'multer'
1213

1314
declare const module: any
1415
;(async (): Promise<void> => {

templates/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*
2+
!.gitignore

0 commit comments

Comments
 (0)