Skip to content

Commit

Permalink
feat(connector): added postmark connector
Browse files Browse the repository at this point in the history
  • Loading branch information
srs authored and wangsijie committed Jul 31, 2024
1 parent 3b4da16 commit e9581d8
Show file tree
Hide file tree
Showing 10 changed files with 868 additions and 27 deletions.
5 changes: 5 additions & 0 deletions .changeset/bright-carpets-relax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@logto/connector-postmark": major
---

add postmark connector
61 changes: 61 additions & 0 deletions packages/connectors/connector-postmark/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Postmark connector

Logto connector for Postmark email service.

## Get started

Postmark is a mail platform for transactional and marketing email. We can use its email sending function to send a _verification code_.

## Register Postmark account

Create a new account at [Postmark website](https://postmark.com/). You may skip this step if you've already got an account.

## Configure your connector

Fill out the `serverToken` field with the Server Token you find under settings for your
server in Postmark.

Fill out the `fromEmail` field with the senders' _From Address_.

In order to enable full user flows, templates with usageType `Register`, `SignIn`, `ForgotPassword` and `Generic` are required

Here is an example of Postmark connector template JSON.

```jsonc
[
{
"usageType": "Register",
"templateAlias": "logto-register"
},
{
"usageType": "SignIn",
"templateAlias": "logto-sign-in"
},
{
"usageType": "ForgotPassword",
"templateAlias": "logto-forgot-password"
},
{
"usageType": "Generic",
"templateAlias": "logto-generic"
},
]
```

## Test Postmark email connector

You can type in an email address and click on "Send" to see whether the settings can work before "Save and Done".

That's it. Don't forget to [Enable connector in sign-in experience](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-sms-or-email-passwordless-sign-in)

## Config types

| Name | Type |
|-------------|-------------------|
| serverToken | string |
| fromEmail | string |

| Template Properties | Type | Enum values |
|---------------------|-------------|------------------------------------------------------|
| usageType | enum string | 'Register' \| 'SignIn' \| 'ForgotPassword' \| 'Generic' |
| templateAlias | string | N/A |
17 changes: 17 additions & 0 deletions packages/connectors/connector-postmark/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
74 changes: 74 additions & 0 deletions packages/connectors/connector-postmark/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{
"name": "@logto/connector-postmark",
"version": "1.0.0",
"description": "Postmark connector implementation.",
"author": "Sten Sandvik <stenrs@gmail.com>",
"dependencies": {
"@logto/connector-kit": "workspace:^4.0.0",
"@silverhand/essentials": "^2.9.0",
"postmark": "^4.0.2",
"zod": "^3.22.4"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-typescript": "^11.1.6",
"@silverhand/eslint-config": "6.0.1",
"@silverhand/ts-config": "6.0.0",
"@types/node": "^20.11.20",
"@types/nodemailer": "^6.4.7",
"@types/supertest": "^6.0.2",
"@vitest/coverage-v8": "^1.4.0",
"eslint": "^8.56.0",
"lint-staged": "^15.0.2",
"nock": "^13.3.1",
"prettier": "^3.0.0",
"rollup": "^4.12.0",
"rollup-plugin-output-size": "^1.3.0",
"supertest": "^7.0.0",
"typescript": "^5.3.3",
"vitest": "^1.4.0"
},
"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"
}
}
57 changes: 57 additions & 0 deletions packages/connectors/connector-postmark/src/constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { ConnectorMetadata } from '@logto/connector-kit';
import { ConnectorConfigFormItemType } from '@logto/connector-kit';

export const defaultMetadata: ConnectorMetadata = {
id: 'postmark-mail',
target: 'postmark-mail',
platform: null,
name: {
en: 'Postmark Mail',
},
logo: './logo.svg',
logoDark: null,
description: {
en: 'Postmark is a mail sending platform.',
},
readme: './README.md',
formItems: [
{
key: 'serverToken',
label: 'Server Token',
type: ConnectorConfigFormItemType.Text,
required: true,
placeholder: '<your-server-token>',
},
{
key: 'fromEmail',
label: 'From Email',
type: ConnectorConfigFormItemType.Text,
required: true,
placeholder: '<from_email_address@your.domain>',
},
{
key: 'templates',
label: 'Templates',
type: ConnectorConfigFormItemType.Json,
required: true,
defaultValue: [
{
usageType: 'SignIn',
templateAlias: 'logto-sign-in',
},
{
usageType: 'Register',
templateAlias: 'logto-register',
},
{
usageType: 'ForgotPassword',
templateAlias: 'logto-forgot-password',
},
{
usageType: 'Generic',
templateAlias: 'logto-generic',
},
],
},
],
};
42 changes: 42 additions & 0 deletions packages/connectors/connector-postmark/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { TemplateType } from '@logto/connector-kit';

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

const getConfig = vi.fn().mockResolvedValue(mockedConfig);
const sendEmailWithTemplate = vi.fn();
vi.mock('postmark', () => ({
ServerClient: vi.fn(() => ({
sendEmailWithTemplate,
})),
}));

const { default: createConnector } = await import('./index.js');

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

describe('sendMessage()', () => {
afterEach(() => {
vi.clearAllMocks();
});

it('should call sendEmailWithTemplate() with correct template and content', async () => {
const connector = await createConnector({ getConfig });
await connector.sendMessage({
to: 'to@email.com',
type: TemplateType.SignIn,
payload: { code: '1234' },
});
expect(sendEmailWithTemplate).toHaveBeenCalledWith(
expect.objectContaining({
From: mockedConfig.fromEmail,
TemplateAlias: 'logto-sign-in',
To: 'to@email.com',
TemplateModel: { code: '1234' },
})
);
});
});
});
65 changes: 65 additions & 0 deletions packages/connectors/connector-postmark/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { assert } from '@silverhand/essentials';

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

import { defaultMetadata } from './constant.js';
import { postmarkConfigGuard } 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, postmarkConfigGuard);

const { serverToken, fromEmail, templates } = config;
const template = templates.find((template) => template.usageType === type);

assert(
template,
new ConnectorError(
ConnectorErrorCodes.TemplateNotFound,
`Template not found for type: ${type}`
)
);

const client = new ServerClient(serverToken);

try {
await client.sendEmailWithTemplate({
From: fromEmail,
TemplateAlias: template.templateAlias,
To: to,
TemplateModel: payload,
});
} catch (error: unknown) {
throw new ConnectorError(
ConnectorErrorCodes.General,
error instanceof Error ? error.message : ''
);
}
};

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

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

export const mockedServerToken = 'serverToken';

export const mockedConfig: PostmarkConfig = {
serverToken: mockedServerToken,
fromEmail: 'noreply@logto.test.io',
templates: [
{
usageType: 'SignIn',
templateAlias: 'logto-sign-in',
},
{
usageType: 'Register',
templateAlias: 'logto-register',
},
{
usageType: 'ForgotPassword',
templateAlias: 'logto-forgot-password',
},
{
usageType: 'Generic',
templateAlias: 'logto-generic',
},
],
};
32 changes: 32 additions & 0 deletions packages/connectors/connector-postmark/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { z } from 'zod';

/**
* UsageType here is used to specify the use case of the template, can be either
* 'Register', 'SignIn', 'ForgotPassword', 'Generic'.
*/
const requiredTemplateUsageTypes = ['Register', 'SignIn', 'ForgotPassword', 'Generic'];

const templateGuard = z.object({
usageType: z.string(),
templateAlias: z.string(),
});

export const postmarkConfigGuard = z.object({
serverToken: z.string(),
fromEmail: z.string(),
templates: z.array(templateGuard).refine(
(templates) =>
requiredTemplateUsageTypes.every((requiredType) =>
templates.map((template) => template.usageType).includes(requiredType)
),
(templates) => ({
message: `Template with UsageType (${requiredTemplateUsageTypes
.filter(
(requiredType) => !templates.map((template) => template.usageType).includes(requiredType)
)
.join(', ')}) should be provided!`,
})
),
});

export type PostmarkConfig = z.infer<typeof postmarkConfigGuard>;
Loading

0 comments on commit e9581d8

Please sign in to comment.