Create a Contact Form With Cloudflare Pages and Oracle Email

A step-by-step guide to building a secure contact form using Cloudflare Pages Functions and Oracle Email Delivery

In this tutorial, we’ll create a secure contact form using Cloudflare Pages with Pages Functions to handle form submissions and send emails through Oracle’s Email Delivery API.

Prerequisites

Before getting started, ensure you have:

  • A Cloudflare account with Pages enabled
  • An Oracle Cloud Infrastructure (OCI) account with Email Delivery service configured
  • OCI API key and configuration details ready:
    • Tenancy OCID
    • User OCID
    • API key fingerprint
    • Private key in PEM format
  • An approved sender email address in OCI Email Delivery

Project Structure

We’ll create two main files:

  • public/index.html - Contains the contact form
  • functions/api/contact.js - Handles form submission and email sending

1. Creating the Contact Form

First, let’s create the contact form in public/index.html. Replace <TURNSTILE_SITE_KEY> with your actual Cloudflare Turnstile site key:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf8" />
    <title>Contact Me</title>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <style>
        div {
            margin: 1em auto;
        }
    </style>
    <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
</head>
<body>
    <h1>Contact Me</h1>
    <form method="POST" action="/api/contact">
        <div>
            <label for="name">Name:</label>
            <input id="name" name="name" type="text" />
        </div>
        <div>
            <label for="email">Email:</label>
            <input id="email" name="email" type="email" />
        </div>
        <div>
            <label for="message">Message:</label>
            <textarea id="message" name="message"></textarea>
        </div>
        <div class="cf-turnstile" data-sitekey="<TURNSTILE_SITE_KEY>"></div>
        <button type="submit">Submit</button>
    </form>
</body>
</html>

2. Creating the API Handler

Now, let’s create functions/api/contact.js and add the following code sections:

Required Imports and Constants

Note
Remember to enable Node.js compatibility in Cloudflare before importing Node modules.
1
2
3
4
5
6
7
8
import crypto from "node:crypto";
import { Buffer } from "node:buffer";

// Constants
const TURNSTILE_VERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
const OCI_EMAIL_API_URL = "https://cell0.submit.email.ap-singapore-1.oci.oraclecloud.com/20220926/actions/submitEmail";
const PEM_HEADER = "-----BEGIN PRIVATE KEY-----";
const PEM_FOOTER = "-----END PRIVATE KEY-----";

POST Request Handler

The onRequestPost function exclusively handles POST requests and ignores other HTTP methods, since form submissions use POST requests. It implements proper error handling and response redirects to ensure a smooth user experience.

10
11
12
13
14
15
16
17
export async function onRequestPost(context) {
    try {
        return await handleRequest(context);
    } catch (e) {
        console.error("Error processing contact form:", e);
        return new Response("Error sending message", { status: 500 });
    }
}

Main Request Handler Function

The handleRequest function coordinates the entire contact form processing workflow. It extracts form data, validates the Turnstile token for spam protection, and forwards the message to Oracle Email Delivery.

19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
async function handleRequest({ request, env }) {
    const ip = request.headers.get("CF-Connecting-IP");
    const formData = await request.formData();
    
    const { name, email, message, token } = extractFormData(formData);
    
    const tokenValidated = await validateToken(ip, token, env);
    if (!tokenValidated) {
        return new Response("Token validation failed", { status: 403 });
    }

    const res = await forwardMessage(name, email, message, env);
    if (!res.messageId) {
        return new Response(JSON.stringify(res), { status: 403 });
    }

    return new Response(null, {
        status: 302,
        headers: { Location: "/thank-you" },
    });
}

Form Data Extraction

The extractFormData function safely extracts all required fields from the form submission. This includes the user’s name, email, message content, and the Turnstile response token used for spam verification.

41
42
43
44
45
46
47
    return {
        name: formData.get("name"),
        email: formData.get("email"),
        message: formData.get("message"),
        token: formData.get("cf-turnstile-response")
    };
}

Token Validation Function

The validateToken function validates the Turnstile widget response from the frontend to confirm that form submissions are from real users and blocks automated bots. It sends a verification request to Cloudflare’s Turnstile API with the user’s IP address, the token, and your secret key.

50
51
52
53
54
55
56
57
58
59
60
61
62
63
async function validateToken(ip, token, env) {
    const formData = new FormData();
    formData.append("secret", env.TURNSTILE_SECRET_KEY);
    formData.append("response", token);
    formData.append("remoteip", ip);

    const result = await fetch(TURNSTILE_VERIFY_URL, {
        method: "POST",
        body: formData,
    });

    const outcome = await result.json();
    return outcome.success;
}

Message Forwarding and Email Processing

The forwardMessage function orchestrates the email creation and delivery process through Oracle’s Email Delivery API. Since Oracle doesn’t provide a direct SDK for Cloudflare Pages Functions, we need to manually construct and sign API requests for authentication.

65
66
67
68
69
70
71
72
73
74
async function forwardMessage(name, email, message, env) {
    const date = new Date();
    const emailDate = formatDate(date);
    const { html, text } = generateEmailContent(name, email, message, emailDate);
    
    const ociConfig = extractOciConfig(env);
    const requestConfig = await buildOciRequest(html, text, email, env, ociConfig, date);
    
    return await sendEmailViaOci(requestConfig);
}

Date Formatting

The formatDate function formats the submission timestamp in a human-readable format with timezone information. This timestamp appears in the email notifications to show exactly when the form was submitted.

76
77
78
79
80
81
82
83
84
85
86
87
88
    return date.toLocaleString('en-GB', {
        timeZone: 'Asia/Brunei',
        timeZoneName: 'short',
        hour12: false,
        weekday: 'short',
        year: 'numeric',
        month: 'short',
        day: '2-digit',
        hour: '2-digit',
        minute: '2-digit',
        second: '2-digit',
    });
}

Email Content Generation

The generateEmailContent function creates both HTML and plain text versions of the email notification. The HTML version includes a nicely formatted table showing the form submission details, while the text version provides the same information in a simple format for email clients that don’t support HTML.

91
92
93
94
95
96
97
function generateEmailContent(name, email, message, emailDate) {
    const html = `<!DOCTYPE html><html lang="en-gb" dir="ltr"><head><meta charset="utf8"><title>Contact</title><meta name="viewport" content="width=device-width,initial-scale=1"></head><body><p>Someone just submitted your form on <a rel="noopener noreferrer" href="https://halimdaud.com/">https://halimdaud.com/</a>.</p><p>Here's what they had to say:</p><table style="font-family:'Trebuchet MS',Arial,Helvetica,sans-serif;border-collapse:collapse;width:100%"><tbody><tr><th style="border:1px solid #ddd;padding:12px 8px;text-align:left;background-color:#354d91;color:#fff">Name</th><th style="border:1px solid #ddd;padding:12px 8px;text-align:left;background-color:#354d91;color:#fff">Value</th></tr><tr><td style="border:1px solid #ddd;padding:8px"><strong>name</strong></td><td style="border:1px solid #ddd;padding:8px"><pre style="margin:0;white-space:pre-wrap">${name}</pre></td></tr><tr><td style="border:1px solid #ddd;padding:8px"><strong>email</strong></td><td style="border:1px solid #ddd;padding:8px"><pre style="margin:0;white-space:pre-wrap"><a href="mailto:${email}">${email}</a></pre></td></tr><tr><td style="border:1px solid #ddd;padding:8px"><strong>message</strong></td><td style="border:1px solid #ddd;padding:8px"><pre style="margin:0;white-space:pre-wrap">${message}</pre></td></tr></tbody></table><br><p style="text-align:center">Submitted at ${emailDate}</p><br></body></html>`;
    
    const text = `Someone just submitted your form on https://halimdaud.com/.\n\nHere's what they had to say:\n\nname: ${name}\nemail: ${email}\nmessage: ${message}\n\nSubmitted at ${emailDate}`;
    
    return { html, text };
}

OCI Configuration Extraction

The extractOciConfig function safely extracts Oracle Cloud Infrastructure configuration from environment variables. This includes your tenancy OCID, user OCID, API key fingerprint, and private key needed for API authentication.

 99
100
101
102
103
104
105
106
function extractOciConfig(env) {
    return {
        tenancy: env.OCI_TENANCY,
        user: env.OCI_USER,
        fingerprint: env.OCI_FINGERPRINT,
        privateKey: env.OCI_PRIVATE_KEY,
    };
}

Oracle Cloud Infrastructure Request Building

Oracle’s Email Delivery API requires properly signed requests for authentication. The following functions handle the complex process of building and signing HTTP requests according to OCI’s authentication requirements.

Main Request Builder

The buildOciRequest function coordinates the entire request building process. It constructs the URL, prepares the request body, builds headers, creates the signing string, generates the signature, and assembles the final authenticated request.

108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
async function buildOciRequest(html, text, email, env, ociConfig, date) {
    const { tenancy, user, fingerprint, privateKey } = ociConfig;
    const compartmentId = tenancy;
    const apiKeyId = `${tenancy}/${user}/${fingerprint}`;
    
    const reqUrl = new URL(OCI_EMAIL_API_URL);
    const reqHost = reqUrl.host;
    const reqPathname = reqUrl.pathname;
    const reqMethod = "POST";
    
    const reqBody = buildEmailRequestBody(html, text, email, env, compartmentId);
    const reqHeaders = buildRequestHeaders(reqBody, date);
    
    const signingString = buildSigningString(reqMethod, reqPathname, reqHost, reqHeaders, reqBody);
    const signature = await createSignature(privateKey, signingString);
    
    const authorizationHeader = buildAuthorizationHeader(apiKeyId, signature);
    reqHeaders.append("Authorization", authorizationHeader);
    
    return {
        url: reqUrl,
        method: reqMethod,
        body: reqBody,
        headers: reqHeaders
    };
}

Email Request Body Construction

The buildEmailRequestBody function constructs the JSON payload for Oracle’s Email Delivery API. It includes sender information, recipient details, email subject, both HTML and text content, and sets up the reply-to address to the form submitter’s email.

135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
function buildEmailRequestBody(html, text, email, env, compartmentId) {
    return JSON.stringify({
        sender: {
            senderAddress: {
                email: env.SENDER_EMAIL,
                name: env.SENDER_NAME
            },
            compartmentId: compartmentId
        },
        recipients: {
            to: [{
                email: env.RECEIVER_EMAIL
            }]
        },
        subject: "New submission from https://halimdaud.com/",
        bodyHtml: html,
        bodyText: text,
        replyTo: [{
            email: email
        }]
    });
}

Request Headers Preparation

The buildRequestHeaders function creates all the necessary HTTP headers for the OCI API request. This includes the date, content type, accept headers, and most importantly, the SHA-256 hash of the request body which is required for OCI’s signature verification.

158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
function buildRequestHeaders(reqBody, date) {
    const reqHeaders = new Headers();
    const reqDate = date.toUTCString();
    
    reqHeaders.append("date", reqDate);
    reqHeaders.append("content-type", "application/json");
    reqHeaders.append("accept", "application/json");
    
    const hash = crypto.createHash('sha256');
    hash.update(reqBody);
    const base64EncodedBodyHash = hash.digest("base64");
    reqHeaders.append("x-content-sha256", base64EncodedBodyHash);
    
    return reqHeaders;
}

Signing String Construction

The buildSigningString function creates the string that will be digitally signed for authentication. While the order of headers in the signing string doesn’t matter according to OCI documentation, you must specify the exact same order in the headers parameter of the Authorization header. This implementation uses a consistent order: request target, date, host, content SHA-256, content type, and content length.

174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
function buildSigningString(reqMethod, reqPathname, reqHost, reqHeaders, reqBody) {
    const requestTargetHeader = `(request-target): ${reqMethod.toLowerCase()} ${reqPathname}`;
    const dateHeader = `date: ${reqHeaders.get("date")}`;
    const hostHeader = `host: ${reqHost}`;
    const contentSha256Header = `x-content-sha256: ${reqHeaders.get("x-content-sha256")}`;
    const contentTypeHeader = "content-type: application/json";
    const contentLengthHeader = `content-length: ${reqBody.length}`;
    
    return [
        requestTargetHeader,
        dateHeader,
        hostHeader,
        contentSha256Header,
        contentTypeHeader,
        contentLengthHeader
    ].join("\n");
}

Digital Signature Creation

The createSignature function handles the cryptographic signing process. It imports your private key and uses it to sign the signing string, producing a digital signature that proves the request’s authenticity to Oracle’s servers.

192
193
194
195
async function createSignature(privateKey, signingString) {
    const signingKey = await importPrivateKey(privateKey);
    return await signMessage(signingKey, signingString);
}

Authorization Header Assembly

The buildAuthorizationHeader function constructs the final authorization header that contains all the signature information. This header tells Oracle’s API which key was used, what algorithm was used for signing, which headers were included, and provides the actual signature.

197
198
199
200
function buildAuthorizationHeader(apiKeyId, signature) {
    const headers = ["(request-target)", "date", "host", "x-content-sha256", "content-type", "content-length"].join(" ");
    return `Signature version="1",keyId="${apiKeyId}",algorithm="rsa-sha256",headers="${headers}",signature="${signature}"`;
}

Email Delivery via Oracle Cloud Infrastructure

API Request Execution

The sendEmailViaOci function executes the final API request to Oracle’s Email Delivery service. It handles both successful responses and error cases, providing appropriate logging for debugging purposes.

202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
async function sendEmailViaOci(requestConfig) {
    const { url, method, body, headers } = requestConfig;
    
    const request = new Request(url, {
        method,
        body,
        headers
    });

    const response = await fetch(request);
    
    let data;
    if (!response.ok) {
        data = await response.text();
        console.error("OCI Email API error:", data);
    } else {
        data = await response.json();
        console.log("Email sent successfully:", data);
    }

    return data;
}

Cryptographic Utility Functions

These essential functions handle the RSA cryptographic operations required for OCI API authentication.

Private Key Import

The importPrivateKey function converts a PEM-formatted private key string into a CryptoKey object that can be used for digital signing. It strips the PEM headers and footers, decodes the base64 content, and imports it using the Web Crypto API.

225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
async function importPrivateKey(pem) {
    // fetch the part of the PEM string between header and footer
    const pemContents = pem.substring(
        PEM_HEADER.length,
        pem.length - PEM_FOOTER.length - 1,
    );
    // convert from base64 to ArrayBuffer
    const binaryDer = Buffer.from(pemContents, "base64");

    const key = await crypto.subtle.importKey(
        "pkcs8",
        binaryDer,
        {
            name: "RSASSA-PKCS1-v1_5",
            hash: "SHA-256",
        },
        true,
        ["sign"],
    );

    return key;
}

Message Signing

The signMessage function performs the actual digital signing operation. It takes the imported private key and the signing string, then uses the RSASSA-PKCS1-v1_5 algorithm with SHA-256 hashing to create a digital signature that proves the request’s authenticity.

251
252
253
254
255
256
257
258
259
260
261
async function signMessage(signingKey, message) {
    const enc = new TextEncoder();
    const encoded = enc.encode(message);
    const signature = await crypto.subtle.sign(
        "RSASSA-PKCS1-v1_5",
        signingKey,
        encoded
    );

    return Buffer.from(signature).toString("base64");
}

Configuration

Set up the following environment variables in your Cloudflare Pages project:

  • TURNSTILE_SECRET_KEY - The Cloudflare Turnstile secret key
  • OCI_TENANCY - The Oracle Cloud Infrastructure tenancy OCID
  • OCI_USER - The OCI user OCID
  • OCI_FINGERPRINT - The API key fingerprint
  • OCI_PRIVATE_KEY - The private key in PEM format
  • SENDER_EMAIL - The approved sender email address from OCI Email Delivery
  • SENDER_NAME - The name to display as the sender
  • RECEIVER_EMAIL - The email address to receive the contact form submissions

Deployment

Deploy your project to Cloudflare Pages using their Git integration or direct upload. The Pages Functions will automatically handle the form submissions and email sending.

Your contact form is now ready to use! When users submit the form, it will validate the Turnstile token and send the message through Oracle’s Email Delivery service.

References

0%