Skip to content

Commit

Permalink
feat(connector): add http email connector (#6592)
Browse files Browse the repository at this point in the history
  • Loading branch information
wangsijie authored Sep 23, 2024
1 parent 21a5936 commit 9adbb31
Show file tree
Hide file tree
Showing 10 changed files with 332 additions and 15 deletions.
3 changes: 3 additions & 0 deletions packages/connectors/connector-http-email/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# HTTP email connector

TBD
13 changes: 13 additions & 0 deletions packages/connectors/connector-http-email/logo-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions packages/connectors/connector-http-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.
68 changes: 68 additions & 0 deletions packages/connectors/connector-http-email/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{
"name": "@logto/connector-http-email",
"version": "0.1.0",
"description": "Email connector to send email via HTTP call.",
"author": "Silverhand Inc. <contact@silverhand.io>",
"dependencies": {
"@logto/connector-kit": "workspace:^4.0.0",
"@silverhand/essentials": "^2.9.1",
"got": "^14.0.0",
"zod": "^3.23.8"
},
"main": "./lib/index.js",
"module": "./lib/index.js",
"exports": "./lib/index.js",
"license": "MPL-2.0",
"type": "module",
"files": [
"lib",
"docs",
"logo.svg",
"logo-dark.svg"
],
"scripts": {
"precommit": "lint-staged",
"check": "tsc --noEmit",
"build": "tsup",
"dev": "tsup --watch",
"lint": "eslint --ext .ts src",
"lint:report": "pnpm lint --format json --output-file report.json",
"test": "vitest src",
"test:ci": "pnpm run test --silent --coverage",
"prepublishOnly": "pnpm build"
},
"engines": {
"node": "^20.9.0"
},
"eslintConfig": {
"extends": "@silverhand",
"settings": {
"import/core-modules": [
"@silverhand/essentials",
"got",
"nock",
"snakecase-keys",
"zod"
]
}
},
"prettier": "@silverhand/eslint-config/.prettierrc",
"publishConfig": {
"access": "public"
},
"devDependencies": {
"@silverhand/eslint-config": "6.0.1",
"@silverhand/ts-config": "6.0.0",
"@types/node": "^20.11.20",
"@types/supertest": "^6.0.2",
"@vitest/coverage-v8": "^2.0.0",
"eslint": "^8.56.0",
"lint-staged": "^15.0.2",
"nock": "^13.3.1",
"prettier": "^3.0.0",
"supertest": "^7.0.0",
"tsup": "^8.1.0",
"typescript": "^5.5.3",
"vitest": "^2.0.0"
}
}
33 changes: 33 additions & 0 deletions packages/connectors/connector-http-email/src/constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { ConnectorMetadata } from '@logto/connector-kit';
import { ConnectorConfigFormItemType } from '@logto/connector-kit';

export const defaultMetadata: ConnectorMetadata = {
id: 'http-email-service',
target: 'http-mail',
platform: null,
name: {
en: 'HTTP Email',
},
logo: './logo.svg',
logoDark: null,
description: {
en: 'Send email via HTTP call.',
},
readme: './README.md',
formItems: [
{
key: 'endpoint',
label: 'Endpoint',
type: ConnectorConfigFormItemType.Text,
required: true,
placeholder: '<https://example.com/your-http-endpoint>',
},
{
key: 'authorization',
label: 'Authorization Header',
type: ConnectorConfigFormItemType.Text,
required: false,
placeholder: '<Bearer your-token>',
},
],
};
47 changes: 47 additions & 0 deletions packages/connectors/connector-http-email/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import nock from 'nock';

import { TemplateType } from '@logto/connector-kit';

import createConnector from './index.js';
import { mockedConfig } from './mock.js';

const getConfig = vi.fn().mockResolvedValue(mockedConfig);

describe('HTTP email connector', () => {
afterEach(() => {
nock.cleanAll();
});

it('should init without throwing errors', async () => {
await expect(createConnector({ getConfig })).resolves.not.toThrow();
});

it('should call endpoint with correct parameters', async () => {
const url = new URL(mockedConfig.endpoint);
const mockPost = nock(url.origin)
.post(url.pathname, (body) => {
expect(body).toMatchObject({
to: 'foo@logto.io',
type: TemplateType.SignIn,
payload: {
code: '123456',
},
});
return true;
})
.reply(200, {
message: 'Email sent successfully',
});

const connector = await createConnector({ getConfig });
await connector.sendMessage({
to: 'foo@logto.io',
type: TemplateType.SignIn,
payload: {
code: '123456',
},
});

expect(mockPost.isDone()).toBe(true);
});
});
70 changes: 70 additions & 0 deletions packages/connectors/connector-http-email/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { assert } from '@silverhand/essentials';
import { got, HTTPError } from 'got';

import type {
GetConnectorConfig,
SendMessageFunction,
CreateConnector,
EmailConnector,
} from '@logto/connector-kit';
import {
ConnectorError,
ConnectorErrorCodes,
validateConfig,
ConnectorType,
} from '@logto/connector-kit';

import { defaultMetadata } from './constant.js';
import { httpMailConfigGuard } from './types.js';

const sendMessage =
(getConfig: GetConnectorConfig): SendMessageFunction =>
async (data, inputConfig) => {
const { to, type, payload } = data;
const config = inputConfig ?? (await getConfig(defaultMetadata.id));
validateConfig(config, httpMailConfigGuard);
const { endpoint, authorization } = config;

try {
return await got.post(endpoint, {
headers: {
Authorization: authorization,
'Content-Type': 'application/json',
},
json: {
to,
type,
payload,
},
});
} catch (error: unknown) {
if (error instanceof HTTPError) {
const {
response: { body: rawBody },
} = error;

assert(
typeof rawBody === 'string',
new ConnectorError(
ConnectorErrorCodes.InvalidResponse,
`Invalid response raw body type: ${typeof rawBody}`
)
);

throw new ConnectorError(ConnectorErrorCodes.General, rawBody);
}

throw new ConnectorError(ConnectorErrorCodes.General, error);
}
};

const createHttpMailConnector: CreateConnector<EmailConnector> = async ({ getConfig }) => {
return {
metadata: defaultMetadata,
type: ConnectorType.Email,
configGuard: httpMailConfigGuard,
sendMessage: sendMessage(getConfig),
};
};

export default createHttpMailConnector;
6 changes: 6 additions & 0 deletions packages/connectors/connector-http-email/src/mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { HttpMailConfig } from './types.js';

export const mockedConfig: HttpMailConfig = {
endpoint: 'https://example.com/your-http-endpoint',
authorization: 'SampleToken',
};
8 changes: 8 additions & 0 deletions packages/connectors/connector-http-email/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { z } from 'zod';

export const httpMailConfigGuard = z.object({
endpoint: z.string(),
authorization: z.string().optional(),
});

export type HttpMailConfig = z.infer<typeof httpMailConfigGuard>;
Loading

0 comments on commit 9adbb31

Please sign in to comment.