diff --git a/packages/connector-smtp/README.md b/packages/connector-smtp/README.md index b7c40d82..bf25f9f5 100644 --- a/packages/connector-smtp/README.md +++ b/packages/connector-smtp/README.md @@ -51,8 +51,10 @@ By following the post, your connector JSON should be like this: { "host": "smtp.gmail.com", "port": 587, // your SMTP port - "username": "", - "password": "", + "auth": { + "user": "", + "pass": "", + }, "fromEmail": "", "templates": [ { @@ -79,8 +81,10 @@ After going through the guide, your connector JSON should look like this: { "host": "smtp.sendgrid.net", "port": 587, // your SMTP port - "username": "apiKey", - "password": "", + "auth": { + "user": "apiKey", + "pass": "", + }, "fromEmail": "", "templates": [ { @@ -109,8 +113,10 @@ After going through the guide, your connector JSON should look like this: { "host": "", "port": 1234, // your SMTP port - "username": "", - "password": "", + "auth": { + "user": "", + "pass": "", + }, "fromEmail": "", "templates": [ { @@ -141,8 +147,6 @@ That's it. Don't forget to [Enable connector in sign-in experience](https://docs |-----------|------------| | host | string | | port | string | -| username | string | -| password | string | | fromEmail | string | | templates | Template[] | @@ -153,12 +157,25 @@ That's it. Don't forget to [Enable connector in sign-in experience](https://docs | usageType | enum string | 'Register' \| 'SignIn' \| 'Test' | | contentType | enum string | 'text/plain' \| 'text/html' | +**Username and password Auth Options** + +| Name | Type | Enum values | +|------|------------------------|-------------| +| user | string | N/A | +| pass | string | N/A | +| type | enum string (OPTIONAL) | 'login' | + +You can also configure [OAuth2 Auth Options](https://nodemailer.com/smtp/oauth2/) and other advanced configurations. See [here](https://nodemailer.com/smtp/) for more details. + +We gave an example config with all configurable parameters in the text box to help you to set up own configuration. (You are responsible to the configuration, some values are for demonstration purpose and may not fit your use case.) + ## References - [Gmail - Send email from a printer, scanner, or app](https://support.google.com/a/answer/176600) - [SendGrid - Integrating with the SMTP API](https://docs.sendgrid.com/for-developers/sending-email/integrating-with-the-smtp-api) - [Aliyun Direct Mail - Send emails using SMTP](https://www.alibabacloud.com/help/en/directmail/latest/send-emails-using-smtp) - [Aliyun Direct Mail - SMTP Reference](https://www.alibabacloud.com/help/en/directmail/latest/smtp-reference) +- [Nodemailer - SMTP Transport](https://nodemailer.com/smtp/) # SMTP 连接器 @@ -189,8 +206,10 @@ SMTP 是一个所有邮件服务提供商通用的传输协议。 { "host": "smtp.gmail.com", "port": 587, // your SMTP port - "username": "", - "password": "", + "auth": { + "user": "", + "pass": "", + }, "fromEmail": "", "templates": [ { @@ -217,8 +236,10 @@ SMTP 是一个所有邮件服务提供商通用的传输协议。 { "host": "smtp.sendgrid.net", "port": 587, // your SMTP port - "username": "apiKey", - "password": "", + "auth": { + "user": "apiKey", + "pass": "", + }, "fromEmail": "", "templates": [ { @@ -247,8 +268,10 @@ SMTP 是一个所有邮件服务提供商通用的传输协议。 { "host": "", "port": 1234, // your SMTP port - "username": "", - "password": "", + "auth": { + "user": "", + "pass": "", + }, "fromEmail": "", "templates": [ { @@ -279,8 +302,6 @@ SMTP 是一个所有邮件服务提供商通用的传输协议。 |-----------|------------| | host | string | | port | string | -| username | string | -| password | string | | fromEmail | string | | templates | Template[] | @@ -291,9 +312,22 @@ SMTP 是一个所有邮件服务提供商通用的传输协议。 | usageType | enum string | 'Register' \| 'SignIn' \| 'Test' | | contentType | enum string | 'text/plain' \| 'text/html' | +**用户名密码的授权配置** + +| 模板属性 | 类型 | 枚举值 | +|----------|------------------------|---------| +| user | string | N/A | +| pass | string | N/A | +| type | enum string (OPTIONAL) | 'login' | + +你也可以使用 [OAuth2 授权配置](https://nodemailer.com/smtp/oauth2/) 和其他高级的 SMTP 配置。点按 [这里](https://nodemailer.com/smtp/) 了解更多. + +我们在文本输入框预填的配置样例里预填了所有可配置的参数,以便你可以用来参考并建立自己的配置。(你需要对自己的配置负责,样例配置中展示的一些值是为了示意,可能并不适用你的用户场景。) + ## 参考 - [Gmail - 从打印机、扫描仪或应用发送电子邮件](https://support.google.com/a/answer/176600?hl=zh-Hans) - [SendGrid - Integrating with the SMTP API](https://docs.sendgrid.com/for-developers/sending-email/integrating-with-the-smtp-api) - [阿里云邮件推送 - 使用 SMTP 发送邮件](https://www.alibabacloud.com/help/zh/directmail/latest/send-emails-using-smtp) - [阿里云邮件推送 - SMTP 参考](https://www.alibabacloud.com/help/zh/directmail/latest/smtp-reference) +- [Nodemailer - SMTP Transport](https://nodemailer.com/smtp/) diff --git a/packages/connector-smtp/docs/config-template.json b/packages/connector-smtp/docs/config-template.json index bd25830d..53f10ee0 100644 --- a/packages/connector-smtp/docs/config-template.json +++ b/packages/connector-smtp/docs/config-template.json @@ -1,8 +1,10 @@ { "host": "", "port": 80, - "password": "", - "username": "", + "auth": { + "pass": "", + "user": "" + }, "fromEmail": "", "templates": [ { @@ -29,5 +31,22 @@ "subject": "Logto Forgot Password with SMTP", "usageType": "ForgotPassword" } - ] + ], + "secure": true, + "tls": { + "rejectUnauthorized":false + }, + "servername": "", + "ignoreTLS": false, + "requireTLS": true, + "name": "", + "localAddress": "", + "connectionTimeout": 120000, + "greetingTimeout": 30000, + "socketTimeout": 600000, + "dnsTimeout": 30000, + "logger": true, + "debug": true, + "disableFileAccess": false, + "disableUrlAccess": false } diff --git a/packages/connector-smtp/src/index.test.ts b/packages/connector-smtp/src/index.test.ts index 17d7e27f..b4cba398 100644 --- a/packages/connector-smtp/src/index.test.ts +++ b/packages/connector-smtp/src/index.test.ts @@ -1,5 +1,16 @@ import createConnector from '.'; -import { mockedConfig } from './mock'; +import { + mockedConfig, + mockedOauth2AuthWithKey, + mockedOauth2AuthWithToken, + mockedTlsOptionsWithTls, + mockedTlsOptionsWithoutTls, + mockedConnectionOptionsValid, + mockedConnectionOptionsInvalid, + mockedDebuggingOptions, + mockedSecurityOptions, +} from './mock'; +import { smtpConfigGuard } from './types'; const getConfig = jest.fn().mockResolvedValue(mockedConfig); @@ -8,3 +19,58 @@ describe('SMTP connector', () => { await expect(createConnector({ getConfig })).resolves.not.toThrow(); }); }); + +describe('Test config guard', () => { + it('basic config', () => { + const result = smtpConfigGuard.safeParse(mockedConfig); + expect(result.success && result.data).toMatchObject(expect.objectContaining(mockedConfig)); + }); + + it('config with oauth2 auth (private key needed)', () => { + const testConfig = { ...mockedConfig, auth: mockedOauth2AuthWithKey }; + const result = smtpConfigGuard.safeParse(testConfig); + expect(result.success && result.data).toMatchObject(expect.objectContaining(testConfig)); + }); + + it('config with oauth2 auth (token needed)', () => { + const testConfig = { ...mockedConfig, auth: mockedOauth2AuthWithToken }; + const result = smtpConfigGuard.safeParse(testConfig); + expect(result.success && result.data).toMatchObject(expect.objectContaining(testConfig)); + }); + + it('config with tls options (with additional `tls` configuration)', () => { + const testConfig = { ...mockedConfig, ...mockedTlsOptionsWithTls }; + const result = smtpConfigGuard.safeParse(testConfig); + expect(result.success && result.data).toMatchObject( + expect.objectContaining(mockedTlsOptionsWithTls) + ); + }); + + it('config with tls options (without additional `tls` configuration)', () => { + const testConfig = { ...mockedConfig, ...mockedTlsOptionsWithoutTls }; + const result = smtpConfigGuard.safeParse(testConfig); + expect(result.success && result.data).toMatchObject(expect.objectContaining(mockedConfig)); + }); + + it('config with VALID connection options', () => { + const testConfig = { ...mockedConfig, ...mockedConnectionOptionsValid }; + const result = smtpConfigGuard.safeParse(testConfig); + expect(result.success && result.data).toMatchObject(expect.objectContaining(testConfig)); + }); + + it('config with INVALID connection options', () => { + const testConfig = { ...mockedConfig, ...mockedConnectionOptionsInvalid }; + const result = smtpConfigGuard.safeParse(testConfig); + expect(result.success && result.data).toMatchObject(expect.objectContaining(mockedConfig)); + }); + + it('config with debugging and security options', () => { + const testConfig = { + ...mockedConfig, + ...mockedDebuggingOptions, + ...mockedSecurityOptions, + }; + const result = smtpConfigGuard.safeParse(testConfig); + expect(result.success && result.data).toMatchObject(expect.objectContaining(testConfig)); + }); +}); diff --git a/packages/connector-smtp/src/index.ts b/packages/connector-smtp/src/index.ts index 8dacfbb1..2b2bcbfd 100644 --- a/packages/connector-smtp/src/index.ts +++ b/packages/connector-smtp/src/index.ts @@ -21,8 +21,7 @@ const sendMessage = const { to, type, payload } = data; const config = inputConfig ?? (await getConfig(defaultMetadata.id)); validateConfig(config, smtpConfigGuard); - const { host, port, username, password, fromEmail, replyTo, templates } = config; - const template = templates.find((template) => template.usageType === type); + const template = config.templates.find((template) => template.usageType === type); assert( template, @@ -32,19 +31,7 @@ const sendMessage = ) ); - const configOptions: SMTPTransport.Options = { - host, - port, - auth: { - user: username, - pass: password, - }, - // Set `secure` to be false and `requireTLS` to be true to make sure `nodemailer` calls STARTTLS, which is wildly adopted in email servers. - secure: false, - requireTLS: true, - // Enable `logger` to help debugging. - logger: true, - }; + const configOptions: SMTPTransport.Options = config; const transporter = nodemailer.createTransport(configOptions); @@ -57,8 +44,8 @@ const sendMessage = const mailOptions = { to, - from: fromEmail, - replyTo, + from: config.fromEmail, + replyTo: config.replyTo, subject: template.subject, ...contentsObject, }; diff --git a/packages/connector-smtp/src/mock.ts b/packages/connector-smtp/src/mock.ts index 84ab1a3c..7c602108 100644 --- a/packages/connector-smtp/src/mock.ts +++ b/packages/connector-smtp/src/mock.ts @@ -1,8 +1,7 @@ export const mockedConfig = { host: '', port: 80, - password: '', - username: '', + auth: { pass: '', user: '' }, fromEmail: '', templates: [ { @@ -13,3 +12,49 @@ export const mockedConfig = { }, ], }; + +export const mockedOauth2AuthWithToken = { + user: '', + type: 'oauth2', + clientId: '', + clientSecret: '', + accessToken: '', +}; + +export const mockedOauth2AuthWithKey = { + user: '', + serviceClient: '', + privateKey: '', +}; + +export const mockedTlsOptionsWithoutTls = { + servername: '', + ignoreTLS: false, + requireTLS: true, +}; + +export const mockedTlsOptionsWithTls = { + tls: { rejectUnauthorized: true }, + servername: '', + ignoreTLS: false, + requireTLS: true, +}; + +export const mockedConnectionOptionsValid = { + localAddress: '', + name: '', +}; + +export const mockedConnectionOptionsInvalid = { + name: '', +}; + +export const mockedDebuggingOptions = { + logger: true, + debug: false, +}; + +export const mockedSecurityOptions = { + disableFileAccess: true, + disableUrlAccess: false, +}; diff --git a/packages/connector-smtp/src/types.ts b/packages/connector-smtp/src/types.ts index dccff1b1..12808833 100644 --- a/packages/connector-smtp/src/types.ts +++ b/packages/connector-smtp/src/types.ts @@ -21,14 +21,104 @@ const templateGuard = z.object({ content: z.string(), // With variable {{code}}, support HTML }); -export const smtpConfigGuard = z.object({ - host: z.string(), - port: z.number(), - username: z.string(), - password: z.string(), - fromEmail: z.string().regex(emailRegEx), - replyTo: z.string().regex(emailRegEx).optional(), - templates: z.array(templateGuard), +/** + * Auth Options + * See https://nodemailer.com/smtp/#authentication and https://nodemailer.com/smtp/oauth2/. + */ +const loginAuthGuard = z.object({ + user: z.string(), + pass: z.string(), + type: z.enum(['login', 'Login', 'LOGIN']).optional(), +}); + +const oauth2AuthWithKeyGuard = z.object({ + type: z.enum(['oauth2', 'OAuth2', 'OAUTH2']).optional(), + user: z.string(), + privateKey: z.string().or(z.object({ key: z.string(), passphrase: z.string() })), + serviceClient: z.string(), +}); + +const oauth2AuthWithTokenGuard = z.object({ + type: z.enum(['oauth2', 'OAuth2', 'OAUTH2']).optional(), + user: z.string(), + clientId: z.string().optional(), + clientSecret: z.string().optional(), + refreshToken: z.string().optional(), + accessToken: z.string().optional(), + expires: z.number().optional(), // Optional Access Token expire time in ms. + accessUrl: z.string().optional(), +}); + +const authGuard = loginAuthGuard.or(oauth2AuthWithKeyGuard).or(oauth2AuthWithTokenGuard); + +/** + * TLS Options + */ +const tlsGuard = z.object({ + secure: z.boolean().default(false), + // See https://nodejs.org/api/tls.html#new-tlstlssocketsocket-options and https://nodemailer.com/smtp/#tls-options for more information. + tls: z.union([z.object({}).catchall(z.unknown()), z.object({})]), + servername: z.string().optional(), + ignoreTLS: z.boolean().optional(), + requireTLS: z.boolean().optional(), +}); + +/** + * Connection Options + * See https://nodemailer.com/smtp/#connection-options. + */ +const connectionGuard = z.object({ + name: z.string().optional(), + localAddress: z.string(), + connectionTimeout: z.number().default(2 * 60 * 1000), // In ms, default is 2 mins. + greetingTimeout: z.number().default(30 * 1000), // In ms, default is 30 seconds. + socketTimeout: z.number().default(10 * 60 * 1000), // In ms, default is 10 mins. + dnsTimeout: z.number().default(30 * 1000), // In ms, default is 30 seconds. +}); + +/** + * Debug Options + * See https://nodemailer.com/smtp/#debug-options. + */ +const debuggingGuard = z.object({ + logger: z.boolean().optional(), + debug: z.boolean().optional(), +}); + +/** + * Security Options + * See https://nodemailer.com/smtp/#security-options. + */ +const securityGuard = z.object({ + disableFileAccess: z.boolean().optional(), + disableUrlAccess: z.boolean().optional(), }); +export const smtpBaseConfigGuard = z + .object({ + host: z.string(), + port: z.number(), + auth: authGuard, + fromEmail: z.string().regex(emailRegEx), + replyTo: z.string().regex(emailRegEx).optional(), + templates: z.array(templateGuard), + }) + .merge(debuggingGuard) + .merge(securityGuard); + +export const smtpBaseConfigWithTlsGuard = smtpBaseConfigGuard.merge(tlsGuard); + +export const smtpBaseConfigWithConnectionGuard = smtpBaseConfigGuard.merge(connectionGuard); + +export const smtpBaseConfigWithTlsAndConnectionGuard = smtpBaseConfigGuard + .merge(tlsGuard) + .merge(connectionGuard); + +export const smtpConfigGuard = z.union([ + smtpBaseConfigWithTlsAndConnectionGuard, + smtpBaseConfigWithConnectionGuard, + smtpBaseConfigWithTlsGuard, + smtpBaseConfigGuard, +]); + export type SmtpConfig = z.infer;