Skip to content
This repository has been archived by the owner on Apr 3, 2023. It is now read-only.

feat(tencent-mail): add tencent mail connector #47

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions packages/connector-tencent-email/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Tencent push mail service connector

The official Logto connector for Tencent push email service.

腾讯云邮件推送服务 Logto 官方连接器 [中文文档](#腾讯云邮件连接器)

**Table of contents**

- [Tencent push mail service connector](#tencent-push-mail-service-connector)
- [腾讯云邮件推送服务连接器](#腾讯云邮件推送服务连接器)
- [在腾讯云邮件推送服务控制台中配置邮件服务](#在腾讯云邮件推送服务控制台中配置邮件服务)
- [创建腾讯云账号](#创建腾讯云账号)
- [配置腾讯云邮件推送服务](#配置腾讯云邮件推送服务)
- [编写连接器的 JSON 配置](#编写连接器的-json-配置)
- [测试腾讯云邮件连接器](#测试腾讯云邮件连接器)
- [配置类型](#配置类型)
- [参考](#参考)

# 腾讯云邮件推送服务连接器

腾讯云是亚洲地区一个重要的云服务厂商,提供了包括邮件推送服务在内的诸多云服务。

本连接器是 Logto 官方提供的腾讯云邮件连接器,帮助终端用户通过邮件验证码进行登录注册。

## 在腾讯云邮件推送服务控制台中配置邮件服务

> 💡 **Tip**
>
> 你可以跳过已经完成的部分。

### 创建腾讯云账号

前往 [腾讯云](https://cloud.tencent.com/) 并完成账号注册并进行实名认证。

### 配置腾讯云邮件推送服务

1. 使用刚刚注册的账号登录并前往 [邮件推送服务控制台](https://console.cloud.tencent.com/ses)。按照官方「快速入门」的步骤,逐步完成配置。
2. 配置发信域名:腾讯云邮件推送支持所有行业标准的身份验证机制,包括域名密钥识别邮件 (DKIM)、发件人策略框架 (SPF)、基于域的邮件身份验证、报告和一致性 (DMARC)、邮件交换记录(MX record)。[官方文档](https://cloud.tencent.com/document/product/1288/60652)
- 首先需要另行购买域名,如:example.com
- 在发信域名配置页中,点击「新建」,输入你的发信域名,点击「提交」。
- 之后返回至发信域名页面,在列表中选择你刚刚添加的域名,点击「验证」按钮。
- 此时在弹出的发信域名配置弹窗中,按照文档提示在域名托管商的 DNS 设置中,添加如下解析记录(以 cloudflare 为例):
<img width="1082" alt="image" src="https://user-images.githubusercontent.com/12833674/210088125-1c2c36e9-7f0b-4573-83ca-fb28cb52c356.png">

- DNS 设置完成后,点击「提交验证」按钮,如上述 DNS 设置成功则会看到「验证通过」状态。
3. 配置发信地址:在发信地址配置页中,点击「新建」按钮,选择你在上一步绑定的域名,并指定发信地址的前缀,如:`[email protected]`。
4. 创建发信模板:在发信模板配置页中,点击「新建」,输入模板名称,选择模板类型,并在邮件正文中使用 `{{变量名}}`作为模板的可替换内容。模板提交后需要通过审核之后才可使用。注:通常我们需要创建多个邮件模板,以满足我们在注册、登录、忘记密码等多个场合的不同需求。
5. 发送测试邮件:点击官方「快速入门」的最后一步,来到 [邮件发送](https://console.cloud.tencent.com/ses/send) 页面。选择发信模板,输入主题和收件人等信息,点击「发送」按钮。如能成功收到测试邮件,那么恭喜,你已成功完成腾讯云邮件的设置。

### 编写连接器的 JSON 配置

1. 前往腾讯云 [API 密钥管理](https://console.cloud.tencent.com/cam/capi) 页面,如尚未生成过密钥,可以点击「新建密钥」按钮,完成身份认证之后系统会自动生成一对 `SecretId` 和 `SecretKey`,请注意妥善保管密钥避免泄漏而造成不必要的损失。
2. 在 Logto Admin Console 的连接器页面,点击邮件连接器右侧的「配置」按钮,再在弹出的菜单中选择「腾讯云邮件服务」,点击「下一步」,进入配置页面。
3. 在右侧的编辑器中,将配置模版中的字段值替换为真实字段。以下为最小可用的模板示例:

```json
{
"region": "ap-hongkong",
"accessKeyId": "<your-secret-id>",
"accessKeySecret": "<your-secret-key>",
"fromAddress": "<replace-with-your-email-sender-address>",
"templates": [
{
"params": [
{
"name": "code",
"value": "code"
}
],
"subject": "Sign-in with Logto",
"usageType": "SignIn",
"templateId": "<replace-with-your-template-id>"
},
{
"params": [
{
"name": "code",
"value": "code"
}
],
"subject": "Sign-up with Logto",
"usageType": "Register",
"templateId": "<replace-with-your-template-id>"
},
{
"params": [
{
"name": "code",
"value": "code"
}
],
"subject": "Reset your password in Logto",
"usageType": "ForgotPassword",
"templateId": "<replace-with-your-template-id>"
},
{
"params": [
{
"name": "code",
"value": "code"
}
],
"subject": "Continue with Logto",
"usageType": "Continue",
"templateId": "<replace-with-your-template-id>"
},
{
"params": [
{
"name": "code",
"value": "code"
}
],
"subject": "Test with Logto",
"usageType": "Test",
"templateId": "<replace-with-your-template-id>"
}
]
}
```

### 测试腾讯云邮件连接器

你可以在「保存并完成」之前输入一个邮件地址并点按「发送」来测试配置是否可以正常工作。

大功告成!快去「登录体验」页面 [启用邮件验证码登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-passcode-sign-in/#%E5%9C%A8%E7%99%BB%E5%BD%95%E4%BD%93%E9%AA%8C%E4%B8%AD%E5%90%AF%E7%94%A8%E8%BF%9E%E6%8E%A5%E5%99%A8)
吧。

### 配置类型

| 名称 | 类型 | 必填 |
|-----------------|------------|-----------|
| accessKeyId | string | Yes |
| accessKeySecret | string | Yes |
| region | string | Yes |
| fromAddress | string | Yes |
| fromName | string | No |
| replyAddress | string | No |
| templates | Template[] | Yes |

| 模板属性 | 类型 | 枚举值 | 必填 |
|--------------|-------------|--------------|----------|
| templateId | string | N/A | Yes |
| usageType | enum string | 'Register' \ 'SignIn' \ 'ForgotPassword' \ 'Continue' \ 'Test' | Yes |
| subject | string | N/A | Yes |
| params | object | N/A | Yes |

## 参考

- [腾讯云文档中心 邮件推送](https://cloud.tencent.com/document/product/1288)
72 changes: 72 additions & 0 deletions packages/connector-tencent-email/docs/config-template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{
"accessKeyId": "<access-key-id>",
"accessKeySecret": "<access-key-secret>",
"region": "<ap-hongkong|ap-singapore>",
"fromAddress": "<from-address>",
"fromName": "<from-alias>",
"replyAddress": "<reply-address>",
StringKe marked this conversation as resolved.
Show resolved Hide resolved
"templates": [
{
"fromAddress": "<from-address>",
"fromName": "<from-alias>",
"replyAddress": "<reply-address>",
"usageType": "SignIn",
"subject": "<subject>",
"templateId": "<templateId>",
"params": [
{
"name": "<tencent-cloud-template-param-name>",
"value": "code or toAddress or fromAddress or fromName or replayAddress or subject"
},
{
"name": "<tencent-cloud-template-param-name>",
"value": "code or toAddress or fromAddress or fromName or replayAddress or subject"
}
]
},
{
"usageType": "Register",
"templateId": "<templateId>",
"subject": "<subject>",
"params": [
{
"name": "code",
"value": "code"
}
]
},
{
"usageType": "ForgotPassword",
"templateId": "<templateId>",
"subject": "<subject>",
"params": [
{
"name": "code",
"value": "code"
}
]
},
{
"usageType": "Continue",
"templateId": "<templateId>",
"subject": "<subject>",
"params": [
{
"name": "code",
"value": "code"
}
]
},
{
"usageType": "Test",
"templateId": "<templateId>",
"subject": "<subject>",
"params": [
{
"name": "code",
"value": "code"
}
]
}
]
}
12 changes: 12 additions & 0 deletions packages/connector-tencent-email/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions packages/connector-tencent-email/package.extend.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "https://json.schemastore.org/package",
"name": "@logto/connector-tencent-email",
"version": "1.0.0-beta.15",
"description": "Tencent email connector implementation.",
"author": "StringKe"
}
21 changes: 21 additions & 0 deletions packages/connector-tencent-email/src/constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { ConnectorMetadata } from '@logto/connector-kit';

export const endpoint = 'ses.tencentcloudapi.com';

export const defaultMetadata: ConnectorMetadata = {
id: 'tencent-email-service',
target: 'tencent-mail',
platform: null,
name: {
en: 'Tencent Mail Service',
'zh-CN': '腾讯云邮件服务',
},
logo: './logo.svg',
logoDark: null,
description: {
en: 'Tencent',
'zh-CN': 'Tencent',
},
readme: './README.md',
configTemplate: './docs/config-template.json',
};
123 changes: 123 additions & 0 deletions packages/connector-tencent-email/src/http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import type { BinaryToTextEncoding } from 'crypto';
import crypto from 'crypto';

import got from 'got';

import type { TencentErrorResponse, TencentSuccessResponse } from '@/types';
import { tencentErrorResponse } from '@/types';

import { endpoint } from './constant';

function sha256Hmac(message: string, secret: string): string;
function sha256Hmac(message: string, secret: string, encoding: BinaryToTextEncoding): Buffer;

function sha256Hmac(message: string, secret: string, encoding?: BinaryToTextEncoding) {
const hmac = crypto.createHmac('sha256', secret);

return encoding ? hmac.update(message).digest(encoding) : hmac.update(message).digest();
}

function getHash(message: string, encoding: BinaryToTextEncoding = 'hex') {
const hash = crypto.createHash('sha256');

return hash.update(message).digest(encoding);
}

function getDate(timestamp: number) {
const date = new Date(timestamp * 1000);
const year = date.getUTCFullYear();
const month = (date.getUTCMonth() + 1).toString().padStart(2, '0');
const day = date.getUTCDate().toString().padStart(2, '0');

return `${year}-${month}-${day}`;
}

export function isErrorResponse(response: unknown): response is TencentErrorResponse {
const result = tencentErrorResponse.safeParse(response);

return result.success;
}

export function request(
parameters: {
fromEmailAddress: string;
replyAddress: string;
destination: string;
template: {
templateId: string;
templateData: string;
};
subject: string;
},
config: {
secretId: string;
secretKey: string;
region: string;
}
) {
const { secretId, secretKey, region } = config;
const timestamp = Math.floor(Date.now() / 1000);
const date = getDate(timestamp);
const service = 'ses';

const firstPayload = {
FromEmailAddress: parameters.fromEmailAddress,
ReplyToAddresses: parameters.replyAddress,
Destination: [parameters.destination],
Template: {
TemplateID: Number(parameters.template.templateId),
TemplateData: parameters.template.templateData,
},
Subject: parameters.subject,
};

const payload = JSON.stringify(firstPayload);

const hashedRequestPayload = getHash(payload);
const signedHeaders = 'content-type;host';
const httpRequestMethod = 'POST';
const canonicalUri = '/';
const canonicalQueryString = '';
const canonicalHeaders = `content-type:application/json; charset=utf-8\nhost:${endpoint}\n`;

const canonicalRequest = [
httpRequestMethod,
canonicalUri,
canonicalQueryString,
canonicalHeaders,
signedHeaders,
hashedRequestPayload,
].join('\n');

const algorithm = 'TC3-HMAC-SHA256';
const hashedCanonicalRequest = getHash(canonicalRequest);
const credentialScope = `${date}/${service}/tc3_request`;
const stringToSign = [algorithm, timestamp, credentialScope, hashedCanonicalRequest].join('\n');

const secretDate = sha256Hmac(date, `TC3${secretKey}`);
const secretService = sha256Hmac(service, secretDate);
const secretSigning = sha256Hmac('tc3_request', secretService);
const signature = sha256Hmac(stringToSign, secretSigning, 'hex').toString();
const credential = `${secretId}/${credentialScope}`;

const authorization = [
algorithm,
`Credential=${credential},`,
`SignedHeaders=${signedHeaders},`,
`Signature=${signature}`,
].join(' ');
charIeszhao marked this conversation as resolved.
Show resolved Hide resolved

return got.post<TencentErrorResponse | TencentSuccessResponse>(`https://${endpoint}`, {
headers: {
Authorization: authorization,
'Content-Type': 'application/json; charset=utf-8',
Host: endpoint,
'X-TC-Action': 'SendEmail',
'X-TC-Timestamp': String(timestamp),
'X-TC-Version': '2020-10-02',
'X-TC-Region': region,
},
body: payload,
responseType: 'json',
});
}
Loading