From 6ce78387ec3418f470ddd7cdf8508f733d27d2cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=A6Ltorio?= Date: Mon, 26 Aug 2024 21:26:07 +0200 Subject: [PATCH] chore: Add Cloudflare workers types for web-smtp-relay-client --- web-smtp-relay-client/README.md | 63 +++++++++++++ web-smtp-relay-client/package-lock.json | 12 ++- web-smtp-relay-client/package.json | 41 +++++---- web-smtp-relay-client/src/client.ts | 46 ++++++++++ web-smtp-relay-client/src/index.ts | 36 +++++++- web-smtp-relay-client/src/server.ts | 93 ++++++++++++++++++++ web-smtp-relay-client/src/test_cloudflare.ts | 25 ++++++ web-smtp-relay-client/tsconfig.json | 4 + 8 files changed, 298 insertions(+), 22 deletions(-) create mode 100644 web-smtp-relay-client/src/client.ts create mode 100644 web-smtp-relay-client/src/server.ts create mode 100644 web-smtp-relay-client/src/test_cloudflare.ts diff --git a/web-smtp-relay-client/README.md b/web-smtp-relay-client/README.md index c577f7e..8967a6f 100644 --- a/web-smtp-relay-client/README.md +++ b/web-smtp-relay-client/README.md @@ -34,6 +34,69 @@ client.sendEmail(message) .catch((error) => console.error('Error sending email:', error)); ``` +## Cloudflare Pages client and serverless functions + +### Client-side (Single Page Application) + +```typescript +import { sendEmailWithCaptcha, EmailMessage } from '@sctg/web-smtp-relay-client'; + +const message: EmailMessage = { + subject: 'Test Subject', + body: 'This is a test email', + destinations: ['recipient@example.com'] +}; + +const captchaToken = 'hCaptcha-token-from-client'; + +sendEmailWithCaptcha(message, captchaToken) + .then((success) => { + if (success) { + console.log('Email sent successfully'); + } else { + console.error('Failed to send email'); + } + }) + .catch((error) => console.error('Error:', error)); +``` + +### Server-side (Cloudflare Pages Serverless Function) + +```typescript +import { cfSendEmailWithCaptcha, EmailMessage, WebSMTPRelayConfig } from '.'; +import { PagesFunction, Response, EventContext } from '@cloudflare/workers-types'; +interface Env { + HCAPTCHA_SECRET: string; + WEB_SMTP_RELAY_SCHEME: string; + WEB_SMTP_RELAY_HOST: string; + WEB_SMTP_RELAY_PORT: number; + WEB_SMTP_RELAY_USERNAME: string; + WEB_SMTP_RELAY_PASSWORD: string; +} +export const onRequestPost: PagesFunction = async (context) => { + const { message, captcha } = await context.request.json() as { message: EmailMessage, captcha: string }; + const config: WebSMTPRelayConfig = { + scheme: 'https', + host: 'relay.example.com', + port: 443, + username: 'admin', + password: 'admin123' + }; + const result = await cfSendEmailWithCaptcha(message, captcha, context.env.HCAPTCHA_SECRET, config); + + return new Response(result, { + headers: { 'Content-Type': 'application/json' }, + }); +} +``` + +Make sure to set the following environment variables for the server-side function: + +- `HCAPTCHA_SECRET`: Your hCaptcha secret key +- `WEB_SMTP_RELAY_HOST`: The URL of your web-smtp-relay service +- `WEB_SMTP_RELAY_USERNAME`: Username for web-smtp-relay authentication +- `WEB_SMTP_RELAY_PASSWORD`: Password for web-smtp-relay authentication + ## License This project is licensed under the GNU Affero General Public License v3.0 (AGPLv3). diff --git a/web-smtp-relay-client/package-lock.json b/web-smtp-relay-client/package-lock.json index ebdbabe..2b2938c 100644 --- a/web-smtp-relay-client/package-lock.json +++ b/web-smtp-relay-client/package-lock.json @@ -1,16 +1,17 @@ { "name": "@sctg/web-smtp-relay-client", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@sctg/web-smtp-relay-client", - "version": "1.0.0", + "version": "1.1.0", "license": "AGPL-3.0", "devDependencies": { "@babel/parser": "^7.25.4", "@babel/types": "^7.25.4", + "@cloudflare/workers-types": "^4.20240821.1", "@types/node": "^20", "typescript": "^5.5.4" } @@ -66,6 +67,13 @@ "node": ">=6.9.0" } }, + "node_modules/@cloudflare/workers-types": { + "version": "4.20240821.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20240821.1.tgz", + "integrity": "sha512-icAkbnAqgVl6ef9lgLTom8na+kj2RBw2ViPAQ586hbdj0xZcnrjK7P46Eu08OU9D/lNDgN2sKU/sxhe2iK/gIg==", + "dev": true, + "license": "MIT OR Apache-2.0" + }, "node_modules/@types/node": { "version": "20.16.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.1.tgz", diff --git a/web-smtp-relay-client/package.json b/web-smtp-relay-client/package.json index 840b950..fcb019a 100644 --- a/web-smtp-relay-client/package.json +++ b/web-smtp-relay-client/package.json @@ -1,39 +1,44 @@ { "name": "@sctg/web-smtp-relay-client", - "version": "1.0.1", + "version": "1.1.0", "description": "A simple client for the web-smtp-relay server", "main": "dist/index.js", "types": "dist/index.d.ts", "repository": { - "type": "git", - "url": "git+https://github.com/sctg-development/web-smtp-relay.git" - }, + "type": "git", + "url": "git+https://github.com/sctg-development/web-smtp-relay.git" + }, "private": false, "licenses": [ - { - "type": "AGPL-3.0", - "url": "https://www.gnu.org/licenses/agpl-3.0.html" - } + { + "type": "AGPL-3.0", + "url": "https://www.gnu.org/licenses/agpl-3.0.html" + } ], "publishConfig": { - "access": "public" + "access": "public" }, "tag": "latest", "scripts": { - "build": "tsc", + "build": "tsc", "test": "echo \"Error: no test specified\" && exit 0", - "prepublishOnly": "npm run build" + "prepublishOnly": "npm run build" }, - "keywords": ["smtp", "relay", "client"], + "keywords": [ + "smtp", + "relay", + "client" + ], "author": "Ronan LE MEILLAT", "license": "AGPL-3.0", "devDependencies": { - "typescript": "^5.5.4", - "@types/node": "^20", - "@babel/parser": "^7.25.4", - "@babel/types":"^7.25.4" + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "@cloudflare/workers-types": "^4.20240821.1", + "@types/node": "^20", + "typescript": "^5.5.4" }, "files": [ - "dist/**/*" + "dist/**/*" ] - } \ No newline at end of file +} diff --git a/web-smtp-relay-client/src/client.ts b/web-smtp-relay-client/src/client.ts new file mode 100644 index 0000000..6e149c5 --- /dev/null +++ b/web-smtp-relay-client/src/client.ts @@ -0,0 +1,46 @@ +/** + * Copyright (C) 2024 Ronan LE MEILLAT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { EmailMessage } from "."; + +/** + * Sends an email to the Cloudflare Page serverless function with a captcha. + * + * @param message - The email message to send. + * @param captcha - The captcha string. + * @returns A promise that resolves to a boolean indicating whether the email was sent successfully. + */ +export async function sendEmailWithCaptcha(message: EmailMessage, captcha: string): Promise { + try { + const response = await fetch('/api/send-email', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ message, captcha }), + }); + + if (!response.ok) { + throw new Error('Failed to send email'); + } + + const result = await response.json(); + return result.error === null; + } catch (error) { + console.error('Error sending email:', error); + return false; + } +} \ No newline at end of file diff --git a/web-smtp-relay-client/src/index.ts b/web-smtp-relay-client/src/index.ts index 55c6605..1b0b7c6 100644 --- a/web-smtp-relay-client/src/index.ts +++ b/web-smtp-relay-client/src/index.ts @@ -1,14 +1,43 @@ +/** + * Copyright (C) 2024 Ronan LE MEILLAT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * Represents the configuration for a Web SMTP Relay. + */ export interface WebSMTPRelayConfig { - scheme: string; + /** the url scheme */ + scheme: "http" | "https"; + /** the host of the relay */ host: string; + /** the port of the relay */ port: number; + /** the username for the relay */ username: string; + /** the password for the relay */ password: string; } +/** Represents an email message */ export interface EmailMessage { + /** Subject of the message */ subject: string; + /** Body of the message */ body: string; + /** Destination(s) (array of string) of the message */ destinations: string[]; } @@ -36,4 +65,7 @@ export class WebSMTPRelayClient { throw new Error(`HTTP error! status: ${response.status}`); } } -} \ No newline at end of file +} +export {sendEmailWithCaptcha} from './client.js'; +export {cfSendEmailWithCaptcha} from './server'; +export type {HCaptchaVerifyResponse} from './server'; \ No newline at end of file diff --git a/web-smtp-relay-client/src/server.ts b/web-smtp-relay-client/src/server.ts new file mode 100644 index 0000000..b0c1ea8 --- /dev/null +++ b/web-smtp-relay-client/src/server.ts @@ -0,0 +1,93 @@ +/** + * Copyright (C) 2024 Ronan LE MEILLAT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { EmailMessage, WebSMTPRelayConfig } from '.'; + +const HCAPTCHA_VERIFY_URL = 'https://api.hcaptcha.com/siteverify'; +type HCaptchaVerifyError = string | string[] + +export type HCaptchaVerifyResponse = { + /** Is the passcode valid, and does it meet security criteria you specified, e.g. sitekey? */ + success: boolean + /** Timestamp of the challenge (ISO format yyyy-MM-dd'T'HH:mm:ssZZ) */ + challenge_ts: string + /** The hostname of the site where the challenge was solved */ + hostname: string + /** Optional: whether the response will be credited */ + credit?: boolean + /** Optional: any error codes */ + 'error-codes'?: HCaptchaVerifyError + /** ENTERPRISE feature: a score denoting malicious activity */ + score?: number + /** ENTERPRISE feature: reason(s) for score */ + score_reason?: string[] +} + +/** + * Sends an email with captcha verification using the web-smtp-relay client. + * this function is intended to be used in a Cloudflare Pages serverless function. + * the web-smtp-relay client is a simple client wrote in Go for sending emails through a web SMTP relay. + * see https://github.com/sctg-development/web-smtp-relay + * + * @param message - The email message to send. + * @param captcha - The captcha response string. + * @param config - The configuration for the web-smtp-relay client. + * @param hCaptchaSecret - The secret key for hCaptcha verification. + * @returns A promise that resolves to a string indicating the result of the email sending operation. + */ +export async function cfSendEmailWithCaptcha(message: EmailMessage, captcha: string, hCaptchaSecret:string, config:WebSMTPRelayConfig): Promise { + try { + // Verify hCaptcha + const hCaptchaResponse = await fetch(HCAPTCHA_VERIFY_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: `response=${captcha}&secret=${hCaptchaSecret}`, + }); + + const hCaptchaResult = await hCaptchaResponse.json() as HCaptchaVerifyResponse; + + if (!hCaptchaResult.success) { + return JSON.stringify({ error: 'Invalid captcha' }); + } + + // Send email via web-smtp-relay + const relayHost = `${config.scheme}://${config.host}:${config.port}`; + const relayUsername = config.username; + const relayPassword = config.password; + + const auth = Buffer.from(`${relayUsername}:${relayPassword}`).toString('base64'); + + const relayResponse = await fetch(`${relayHost}/send`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Basic ${auth}`, + }, + body: JSON.stringify(message), + }); + + if (!relayResponse.ok) { + throw new Error('Failed to send email through relay'); + } + + return JSON.stringify({ error: null }); + } catch (error) { + console.error('Error in sendEmailWithCaptcha:', error); + return JSON.stringify({ error: 'Failed to send email' }); + } +} \ No newline at end of file diff --git a/web-smtp-relay-client/src/test_cloudflare.ts b/web-smtp-relay-client/src/test_cloudflare.ts new file mode 100644 index 0000000..aeac481 --- /dev/null +++ b/web-smtp-relay-client/src/test_cloudflare.ts @@ -0,0 +1,25 @@ +import { cfSendEmailWithCaptcha, EmailMessage, WebSMTPRelayConfig } from '.'; +import { PagesFunction, Response, EventContext } from '@cloudflare/workers-types'; +interface Env { + HCAPTCHA_SECRET: string; + WEB_SMTP_RELAY_SCHEME: string; + WEB_SMTP_RELAY_HOST: string; + WEB_SMTP_RELAY_PORT: number; + WEB_SMTP_RELAY_USERNAME: string; + WEB_SMTP_RELAY_PASSWORD: string; +} +export const onRequestPost: PagesFunction = async (context) => { + const { message, captcha } = await context.request.json() as { message: EmailMessage, captcha: string }; + const config: WebSMTPRelayConfig = { + scheme: 'https', + host: 'relay.example.com', + port: 443, + username: 'admin', + password: 'admin123' + }; + const result = await cfSendEmailWithCaptcha(message, captcha, context.env.HCAPTCHA_SECRET, config); + + return new Response(result, { + headers: { 'Content-Type': 'application/json' }, + }); +} \ No newline at end of file diff --git a/web-smtp-relay-client/tsconfig.json b/web-smtp-relay-client/tsconfig.json index 559ce38..6e0fc1b 100644 --- a/web-smtp-relay-client/tsconfig.json +++ b/web-smtp-relay-client/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "forceConsistentCasingInFileNames": true, "target": "esnext", "module": "NodeNext", "moduleResolution": "NodeNext", @@ -18,5 +19,8 @@ "exclude": [ "node_modules", "**/__tests__/*" + ], + "types": [ + "@cloudflare/workers-types" ] } \ No newline at end of file