Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(connector): postmark email connector #5886

Merged
merged 1 commit into from
Jul 31, 2024
Merged
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
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
Loading