Skip to content
Open
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
27 changes: 27 additions & 0 deletions .github/workflows/cloudflare-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Deploy

on:
push:
branches:
- main

jobs:
deploy:
runs-on: ubuntu-latest
name: Deploy
steps:
- uses: actions/checkout@v3
- name: Deploy
id: deploy
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
env:
TOKEN: ${{ secrets.WORKERS_API_TOKEN }}
DKIM_DOMAIN: ${{ secrets.DKIM_DOMAIN }}
DKIM_SELECTOR: ${{ secrets.DKIM_SELECTOR }}
DKIM_PRIVATE_KEY: ${{ secrets.DKIM_PRIVATE_KEY }}
- name: print deployment-url
env:
DEPLOYMENT_URL: ${{ steps.deploy.outputs.deployment-url }}
run: echo $DEPLOYMENT_URL
48 changes: 48 additions & 0 deletions dns-setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/bin/bash

echo "Please enter the domain name:"
read DOMAIN

echo "Please enter your worker subdomain:"
echo "(only the subdomain, for example admin.workers.dev than admin)"
read WORKER_SUBDOMAIN

echo "Please enter the DKIM selector:"
read DKIM_SELECTOR

echo
echo "Generate RSA 2048 private key"
PRIVATE_KEY=$(openssl genrsa 2048)

echo "Generate public key in base64 format"
PUBLIC_KEY_BASE64=$(openssl rsa -pubout -outform der <<< "$PRIVATE_KEY" | openssl base64 -A)

echo "Convert private key to base64"
DKIM_PRIVATE_KEY=$(openssl rsa -outform der <<< "$PRIVATE_KEY"| openssl base64 -A)

echo "Generate DKIM public key"
DKIM_PUBLIC_KEY=$(echo -n "v=DKIM1;p=" && echo $PUBLIC_KEY_BASE64)

echo "Getting SPF record for $DOMAIN"
CURRENT_SPF_RECORD=$(dig +short $DOMAIN TXT | grep spf1)
if [ -z "$CURRENT_SPF_RECORD" ]
then
SPF_RECORD="@ IN TXT v=spf1 a mx include:relay.mailchannels.net ~all"
else
echo "Current SPF record found: $CURRENT_SPF_RECORD"
SPF_RECORD="@ IN TXT $(echo $CURRENT_SPF_RECORD | sed 's/\(.*\)\(-all\|~all\|+all\|?all\)/\1 include:relay.mailchannels.net \2/')"
fi
echo
echo

echo "Add the following TXT record to your DNS zone:"
echo $SPF_RECORD
echo "_mailchannels IN TXT v=mc1 cfid=$WORKER_SUBDOMAIN.workers.dev cfid=$DOMAIN"
echo "$DKIM_SELECTOR._domainkey.$DOMAIN IN TXT $DKIM_PUBLIC_KEY"
echo
echo
echo "Add the following environment variables to your worker enviroment variables (wrangler secret put <KEY> and than VALUE):"
echo "TOKEN=$(openssl rand -base64 36)"
echo "DKIM_PRIVATE_KEY=$DKIM_PRIVATE_KEY"
echo "DKIM_SELECTOR=$DKIM_SELECTOR"
echo "DKIM_DOMAIN=$DOMAIN"
16 changes: 16 additions & 0 deletions package-lock.json

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

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20230419.0",
"@types/node": "^20.12.7",
"itty-router": "^4.0.9",
"prettier": "^2.8.8",
"typescript": "^5.0.4",
"wrangler": "^3.0.0",
"prettier": "^2.8.8"
"wrangler": "^3.0.0"
}
}
32 changes: 26 additions & 6 deletions src/controllers/email.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { IContact, IEmail } from '../schema/email';

type IMCPersonalization = { to: IMCContact[] };
type IMCPersonalization = {
to: IMCContact[];
dkim_domain?: string;
dkim_selector?: string;
dkim_private_key?: string;
};
type IMCContact = { email: string; name: string | undefined };
type IMCContent = { type: string; value: string };

Expand All @@ -18,10 +23,11 @@ class Email {
/**
*
* @param email
* @param env
*/
static async send(email: IEmail) {
static async send(email: IEmail, env: Env) {
// convert email to IMCEmail (MailChannels Email)
const mcEmail: IMCEmail = Email.convertEmail(email);
const mcEmail: IMCEmail = Email.convertEmail(email, env);

// send email through MailChannels
const resp = await fetch(
Expand All @@ -36,21 +42,35 @@ class Email {

// check if email was sent successfully
if (resp.status > 299 || resp.status < 200) {
throw new Error(`Error sending email: ${resp.status} ${resp.statusText}`);
throw new Error(`Error sending email: ${resp.status} ${resp.statusText} ${await resp.text()}`);
}
console.log(`Email sent: ${resp.status} ${resp.statusText} ${await resp.text()}`)
}

/**
* Converts an IEmail to an IMCEmail
* @param email
* @param env
* @protected
*/
protected static convertEmail(email: IEmail): IMCEmail {
protected static convertEmail(email: IEmail, env: Env): IMCEmail {
const personalizations: IMCPersonalization[] = [];

// Convert 'to' field
const toContacts: IMCContact[] = Email.convertContacts(email.to);
personalizations.push({ to: toContacts });
const personalization: IMCPersonalization = { to: toContacts };

if (env.DKIM_DOMAIN && env.DKIM_SELECTOR && env.DKIM_PRIVATE_KEY) {
personalization.dkim_domain = env.DKIM_DOMAIN;
personalization.dkim_selector = env.DKIM_SELECTOR;
personalization.dkim_private_key = env.DKIM_PRIVATE_KEY;
console.log(`DKIM signing enabled`)
} else {
console.log(`DKIM signing disabled, missing environment variables`)
}

personalizations.push(personalization);


let replyTo: IMCContact | undefined = undefined;
let bccContacts: IMCContact[] | undefined = undefined;
Expand Down
12 changes: 6 additions & 6 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IRequest, Router } from 'itty-router';
import { Router } from 'itty-router';
import Email from './controllers/email';
import AuthMiddleware from './middlewares/auth';
import EmailSchemaMiddleware, { EmailRequest } from './middlewares/email';
Expand All @@ -7,14 +7,14 @@ import { IEmail } from './schema/email';
const router = Router();

// POST /api/email
router.post<EmailRequest>('/api/email', AuthMiddleware, EmailSchemaMiddleware, async (request) => {
router.post<EmailRequest>('/api/email', AuthMiddleware, EmailSchemaMiddleware, async (request, env) => {
const email = request.email as IEmail;

try {
await Email.send(email);
await Email.send(email, env);
} catch (e) {
console.error(`Error sending email: ${e}`);
return new Response('Internal Server Error', { status: 500 });
return new Response(`Internal Server Error | ${e}`, { status: 500 });
}

return new Response('OK', { status: 200 });
Expand All @@ -23,5 +23,5 @@ router.post<EmailRequest>('/api/email', AuthMiddleware, EmailSchemaMiddleware, a
router.all('*', (request) => new Response('Not Found', { status: 404 }));

export default {
fetch: router.handle,
fetch: (request: Request, env: Env) => router.handle(request, env),
};
20 changes: 19 additions & 1 deletion src/middlewares/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,31 @@ const AuthMiddleware = (request: Request, env: Env) => {
const token = request.headers.get('Authorization');

// Strict check for token existence

if (!env.TOKEN || env.TOKEN.length === 0) {
return new Response('You must set the TOKEN environment variable.', {
status: 401,
});
}

if (token !== env.TOKEN) {
// Possible password length leak (Timing attack)

if (env.TOKEN.length !== token?.length) {
return new Response('Unauthorized', { status: 401 });
}

const encoder = new TextEncoder();

const tokenEnc = encoder.encode(token)
const envTokenEnc = encoder.encode(env.TOKEN)

if (tokenEnc.length !== envTokenEnc.length) {
return new Response('Unauthorized', { status: 401 });
}

let isValidToken = crypto.subtle.timingSafeEqual(tokenEnc, envTokenEnc);

if (!isValidToken) {
return new Response('Unauthorized', { status: 401 });
}
};
Expand Down
3 changes: 3 additions & 0 deletions worker-configuration.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
interface Env {
TOKEN: string;
DKIM_DOMAIN: string;
DKIM_SELECTOR: string;
DKIM_PRIVATE_KEY: string;
}
1 change: 0 additions & 1 deletion wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,3 @@ main = "src/main.ts"
compatibility_date = "2023-05-18"

[env.production]