Contact Form With reCAPTCHA for Shared Hosting


If you have a classic, static HTML site in a shared hosting service and you want to let your users reach you with a simple contact form, there is no need to install WordPress! Just upload the following Python CGI script protected by reCAPTCHA v2 into your public_html/ directory (or wherever you store your HTML files) as

#!/usr/bin/env python3
    Simple contact form with reCAPTCHA for shared hostings.
from email.message import EmailMessage
import json
import os
import urllib.parse
import urllib.request
import smtplib
import sys
# SMTP credentials.
SMTP_PASSWORD = "your-password-123"
# Where to send the messages.
# Your reCAPTCHA keys. Make sure to change these in production.
# Use the following keys in localhost.
# Change this as needed.
<form method="post">
<label for="name">Name</label>:<br /><input name="name" type="text" /><br />
<label for="email">Email</label>:<br /><input name="email" type="email" /><br />
<label for="message">Message</label>:<br /><textarea name="message"></textarea><br />
<div class="g-recaptcha" data-sitekey="{public_key}"></div><br />
<button type="submit">Submit</button>
def send_email(user_name, user_email, user_message):
    email = EmailMessage()
    email["From"] = SMTP_USER
    email["To"] = SMTP_RECIPIENT
    email["Subject"] = "Contact Form"
        f"Name: {user_name}\nEmail: {user_email}\n\n{user_message}"
    smtp = smtplib.SMTP_SSL(SMTP_SERVER, port=SMTP_PORT)
    # Or if TLS is used:
    # smtp = SMTP("", port=587)
    # smtp.starttls()
    # If you want to use other email providers, see
    smtp.login(SMTP_USER, SMTP_PASSWORD)
    smtp.sendmail(SMTP_USER, SMTP_RECIPIENT, email.as_string())
def body():
    if os.getenv("REQUEST_METHOD") == "GET":
        # Display the form.
        # Validate input and send email.
        form_input = dict(urllib.parse.parse_qsl(
        required_fields = ("name", "email", "message", "g-recaptcha-response")
        for field in required_fields:
            if field not in form_input:
                print("All fields are required.")
        # Verify reCAPTCHA.
        data = urllib.parse.urlencode({
            "secret": SECRET_KEY,
            "response": form_input["g-recaptcha-response"],
            "remote-ip": os.getenv("REMOTE_ADDR")
        req = urllib.request.Request(
        resp = urllib.request.urlopen(req)
        success = json.load(resp)["success"]
        if not success:
            print("Captcha failed. Try again.")
        print("Message sent! We'll be in touch soon!")
def main():
    # Headers.
    print("Content-Type: text/html")
    # This new line is required and part of the headers.
    # HTML code.
<!DOCTYPE html>
<html lang="en">
    <title>Contact Form</title>
    <meta http-equiv="content-type" content="text/html;charset=utf-8" />
    <script src=''></script>
    <h3>Contact Form</h3>\
if __name__ == "__main__":

Of course, you might want to throw some CSS into that form. Before uploading, make sure to change the SMTP_* constants and your reCAPTCHA's PUBLIC_KEY and SECRET_KEY (lines 15-26) accordingly.

Google has currently three supported versions for reCAPTCHA: v2, v3 and Enterprise. This form works with reCAPTCHA v2, for which you can generate a couple of keys from

You might want to test the script in localhost before uploading it to your production site. If so, use the following test server:

from http.server import CGIHTTPRequestHandler, HTTPServer

class Handler(CGIHTTPRequestHandler):
    cgi_directories = ["/"]

httpd = HTTPServer(("", 8000), Handler)

This will execute CGI scripts within the current working directory. Save the code as and run:


Then visit http://localhost:8000/ and try the form out. You still need to configure the SMTP_* constants when running in development, although you might keep reCAPTCHA's test keys.

After uploading to your shared hosting, if you get a 500 Internal Server Error, ensure that:

  1. The file is executable in the hosting machine. Using a FTP client, change permissions to 755 or run chmod 755 via SSH.

  2. Line endings are LF instead of CR (macOS) or CRLF (Windows) in (you can change these in your code editor), since your script probably runs on a Linux distribution.

If you still have problems, you might want to take a look at the errors.log file or run ./ within a SSH session to see what happens.