Skip to content

Commit

Permalink
chore: Add Cloudflare workers types for web-smtp-relay-client
Browse files Browse the repository at this point in the history
  • Loading branch information
aeltorio committed Aug 26, 2024
1 parent afefc5a commit 6ce7838
Show file tree
Hide file tree
Showing 8 changed files with 298 additions and 22 deletions.
63 changes: 63 additions & 0 deletions web-smtp-relay-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: ['[email protected]']
};

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<Env> = 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).
12 changes: 10 additions & 2 deletions web-smtp-relay-client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 23 additions & 18 deletions web-smtp-relay-client/package.json
Original file line number Diff line number Diff line change
@@ -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/**/*"
]
}
}
46 changes: 46 additions & 0 deletions web-smtp-relay-client/src/client.ts
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<boolean> {
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;
}
}
36 changes: 34 additions & 2 deletions web-smtp-relay-client/src/index.ts
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/

/**
* 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[];
}

Expand Down Expand Up @@ -36,4 +65,7 @@ export class WebSMTPRelayClient {
throw new Error(`HTTP error! status: ${response.status}`);
}
}
}
}
export {sendEmailWithCaptcha} from './client.js';
export {cfSendEmailWithCaptcha} from './server';
export type {HCaptchaVerifyResponse} from './server';
93 changes: 93 additions & 0 deletions web-smtp-relay-client/src/server.ts
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<string> {
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' });
}
}
25 changes: 25 additions & 0 deletions web-smtp-relay-client/src/test_cloudflare.ts
Original file line number Diff line number Diff line change
@@ -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<Env> = 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' },
});
}
Loading

0 comments on commit 6ce7838

Please sign in to comment.