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

We will create a contact form using Cloudflare Pages with Pages Functions to handle form submissions and send emails through Oracle’s Email Delivery API.

Prerequisites

  • A Cloudflare account with Pages enabled
  • Oracle Cloud Infrastructure (OCI) account with Email Delivery service configured
  • OCI API key and config details ready:
    • Tenancy OCID
    • User OCID
    • API key fingerprint
    • Private key in PEM format
  • 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, create the contact form in public/index.html by replacing <TURNSTILE_SITE_KEY> with the 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

Create functions/api/contact.js and add the following code sections:

Required Imports

Note
Enable Node.js compatibility in Cloudflare before importing Node modules.
1
2
import crypto from "node:crypto";
import { Buffer } from "node:buffer";

Post Request Handler

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

 4
 5
 6
 7
 8
 9
10
11
export async function onRequestPost(context) {
    try {
        return await handleRequest(context);
    } catch (e) {
        console.error(e);
        return new Response("Error sending message", { status: 500 });
    }
}

Main Request Handler Function

The handleRequest function extracts form data, validates the Turnstile token, and forwards the message to Oracle Email Delivery.

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
async function handleRequest({ request, env }) {
    const ip = request.headers.get("CF-Connecting-IP");
    const formData = await request.formData();
    const name = formData.get("name");
    const email = formData.get("email");
    const message = formData.get("message");
    const token = formData.get("cf-turnstile-response");

    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" },
    });
}

Token Validation Function

The validateToken function validates the checks performed by the Turnstile widget in the frontend to confirm whether form submissions are from real people and block unwanted bots.

37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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 url = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
    const result = await fetch(url, {
        body: formData,
        method: "POST",
    });

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

Message Forwarding Function

The forwardMessage function handles email creation and sending through Oracle’s Email Delivery API. While Oracle provides an SDK for email sending, Cloudflare Pages Functions currently doesn’t support it. Therefore, we must manually sign our API requests for authentication.

First, set up the email template and date formatting:

53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
async function forwardMessage(name, email, message, env) {
    const date = new Date();
    const emailDate = 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',
    });
    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}`;
    
    // ... (rest of the function implementation)

Next, set up the environment variables:

68
69
70
71
72
73
74
75
76
    // ... (previous code for email template and date formatting)

    const tenancy = env.OCI_TENANCY;
    const user = env.OCI_USER;
    const fingerprint = env.OCI_FINGERPRINT;
    const privateKey = env.OCI_PRIVATE_KEY;
    const compartmentId = tenancy;

    // ... (rest of the function implementation)

Then, prepare the headers and body for signing:

 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
    // ... (previous code for environment variables)
    
    const apiKeyId = `${tenancy}/${user}/${fingerprint}`;
    const region = "ap-singapore-1"
    const reqUrl = new URL(`https://cell0.submit.email.${region}.oci.oraclecloud.com/20220926/actions/submitEmail`);
    const reqHost = reqUrl.host;
    const reqPathname = reqUrl.pathname;
    const reqHeaders = new Headers();
    const reqMethod = "POST";
    
    const requestTargetHeader = `(request-target): ${reqMethod.toLowerCase()} ${reqPathname}`;
    const reqDate = date.toUTCString();
    reqHeaders.append("date", reqDate);
    const dateHeader = `date: ${reqDate}`;
    const hostHeader = `host: ${reqHost}`;
    const reqBody = 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
            }
        ]
    });
    const hash = crypto.createHash('sha256');
    hash.update(reqBody);
    const base64EncodedBodyHash = hash.digest("base64");
    reqHeaders.append("x-content-sha256", base64EncodedBodyHash);
    const contentSha256Header = `x-content-sha256: ${base64EncodedBodyHash}`;
    const contentLengthHeader = `content-length: ${reqBody.length}`;
    reqHeaders.append("content-type", "application/json");
    reqHeaders.append("accept", "application/json");
    const contentTypeHeader = "content-type: application/json";
    
    // ... (rest of the function implementation)

Next, combine the headers to be signed:

121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
    // ... (previous code for headers and body preparation)
    
    const signingStringArray = [requestTargetHeader, dateHeader, hostHeader, contentSha256Header, contentTypeHeader, contentLengthHeader];
    const headersToSign = ["(request-target)", "date", "host", "x-content-sha256", "content-type", "content-length"];
    
    const headers = headersToSign.join(" ");
    const signingString = signingStringArray.join("\n");

    const signingKey = await importPrivateKey(privateKey);
    const base64EncodedSignature = await signMessage(signingKey, signingString);

    const authorizationHeader = `Signature version="1",keyId="${apiKeyId}",algorithm="rsa-sha256",headers="${headers}",signature="${base64EncodedSignature}"`;
    reqHeaders.append("Authorization", authorizationHeader);
    
    // ... (rest of the function implementation)

Finally, send the POST request to Oracle’s HTTPS API:

133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
    // ... (previous code for combining headers)

    const newRequest = new Request(reqUrl, {
        method: reqMethod,
        body: reqBody,
        headers: reqHeaders
    });

    let data;
    const response = await fetch(newRequest);
    if (!response.ok) {
        data = await response.text();
    } else {
        data = await response.json();
    }

    return data;
}

Utility Functions

These helper functions handle RSA key operations. The importPrivateKey function imports RSA private keys in PEM format, while signMessage signs API requests for authentication.

152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
async function importPrivateKey(pem) {
    const pemHeader = "-----BEGIN PRIVATE KEY-----";
    const pemFooter = "-----END PRIVATE KEY-----";
    const pemContents = pem.substring(
        pemHeader.length,
        pem.length - pemFooter.length - 1,
    );
    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;
}

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 the 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 project to Cloudflare Pages using their Git integration or direct upload. The Pages Functions will automatically handle the form submissions and email sending.

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%