paint-brush
Avoid Email Spam By Building a Secure Form in Python by@tom2
327 reads
327 reads

Avoid Email Spam By Building a Secure Form in Python

by RutkatSeptember 4th, 2024
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

A valid email address is a gateway for establishing direct communication, generating leads, getting sales, private invites to online communities, etc. We don't have to rely on using 3rd party services such as Auth0, Facebook, or Google to have access to your app and services. We're going to use existing Python modules that simplify cleaning user input, generating a verification link, and communicating with the database.
featured image - Avoid Email Spam By Building a Secure Form in Python
Rutkat HackerNoon profile picture

A valid email address is a gateway for establishing direct communication, generating leads, getting sales, private invites to online communities, etc. Don’t take it for granted because social media is changing. Through evolutions in tech, email is still the tried and true way to connect. We’re going to keep things simple and not code from scratch because Python has existing modules to help you speed up coding.


I’ve had customers ask me to build email sign-up forms to promote their products, but none of those customers wanted to pay monthly fees for a 3rd party off-the-shelf service, so I saved them money by building customized contact forms that they can use forever. I can help you do the same whether it’s for your startup, client, marketing purposes, or best of all, to mitigate spam.


This is for anyone wanting to learn to code in Python and is especially useful for beginners who may not consider security features such as filtering user input, validating email addresses, and email double opt-ins. In this tutorial, we cover steps 1-3:


  1. Filtering user input for a valid email address
  2. Double opt-in sign-up
  3. Bot/spam prevention


We don’t have to rely on using 3rd party services such as Auth0, Facebook, or Google to have access to your app and services which can shut you down at any given time or share your data. Keep your app data yours!


Starting off, you should have some experience in Python because we're going to use the Flask framework with a MySQL database. This is going to be more fun (maybe) than using WordPress, the most popular CMS. You would have to pay for some WordPress plugin to have the same capability as a free Flask extension. I've built previously on Wordpress (PHP) and prefer Python Flask for web apps even though Wordpress is very capable of making web apps.


We're going to use existing Python modules that simplify cleaning user input, generating a verification link, and communicating with the database.


Each code snippet will be explained and will include some comments in the code. In case you haven't built user registration or know of the inner workings, I will describe the details for you, and then you can see the final code at the end (don't skip ahead).


Here is a summary of the features we will implement as stated in the first paragraph:


  1. A valid email address can be checked by parsing the input string from the user using a regular expression or a Flask extension. We won't allow random text or SQL injection type of hacks.


  2. The double opt-in method requires the recipient to give permission for you to email them by receiving a validation link to their inbox. This is mainly used to prevent someone else from using your email address. This also prevents test users who just sign up and abandon their accounts.


  3. Bot prevention can be done with a hidden field that is not shown to the user but is commonly auto-filled by bots crawling for vulnerable sign-up forms, but it is not as reliable as a "captcha" from a 3rd party service.


Let’s begin coding. Create a working directory:

mkdir signup
cd signup


Create your Python environment using python3 -m venv signup or conda create -n double-opt-contact python3. I prefer conda, and if you want to learn more, you can read my Python environments article.


Install the following dependencies:
pip flask flask-mail secure SQLAlchemy Flask-WTF Flask-SQLAlchemy mysql-connector-python bleach

Alternatively, you can have the same dependencies listed in arequirements.txt file and run pip install -r requirements.txt


Create app.py file with the following dependencies included:


from flask import Flask, render_template, request, url_for, redirect, flash
from flask_mail import Mail, Message
from datetime import datetime
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.sql import func
from itsdangerous import URLSafeTimedSerializer, SignatureExpired
import secrets
import bleach


Initialize the app object with the default template folder location:

app = Flask(__name__, template_folder='templates')


Enter your own server configuration data using these lines:

secret = secrets.token_urlsafe(32)
app.secret_key = secret
app.config['SECRET_KEY'] = secret # auto-generated secret key

# SQLAlchemy configurations
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+mysqlconnector://admin:user@localhost/tablename'

# Email configurations
app.config['MAIL_SERVER'] = 'smtp.example.com'
app.config['MAIL_PORT'] = 465 #check your port
app.config['MAIL_USERNAME'] = '[email protected]'
app.config['MAIL_PASSWORD'] = 'your_password'
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USE_SSL'] = False

db = SQLAlchemy(app)
mail = Mail(app)
sserialzer = URLSafeTimedSerializer(app.config['SECRET_KEY']) #set secret to the serliazer


Ultimately, you should have your config info in a .env file.


We will need a MySQL database to store users which can be created manually or by Python code. As part of the learning process, you can enter the following code using the command-line or using Python's with app.app_context() db_create_all() method.


The validated field is for a token string which allows the double opt-in technique.

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(120) NOT NULL UNIQUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    validated BOOLEAN DEFAULT FALSE
);


The next section uses SQLAlchemy's ORM structure to query the database for you. Take note thatthe class name should match your database table name, otherwise, you'll get an error. The db.model represents your table settings which include the column name, its type, length, key, and null value:


class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(120), unique=True, nullable=False)
    created_at = db.Column(db.DateTime, server_default=db.func.now())
    validated = db.Column(db.Boolean, default=False)


If you haven't manually created the MySQL database table already, you can do it with this Flask code directly after the class User code block:

# Create the database table
with app.app_context():
    db.create_all()


Now, we enter the back-end code which is 2 pages/routes (index, signup), the email message, and confirmation. The signup page includes theGET/POST methods that allow the form to be submitted. The bleach object is a Python extension that cleans the input from the user to ensure security and mitigate malicious scripts. Then the sserializer generates a one-time token to email the verification link.


@app.route('/')
def index():
    return '<h1>Index page</h1>'

@app.route('/signup', methods=['GET', 'POST'])
def signup():
    if request.method == 'POST':
        email = bleach.clean(request.form.get('email'))

        # Insert user into the database
        new_user = User(email=email)
        try:
            db.session.add(new_user)
            db.session.commit()
        except Exception as e:
            print(f"Error occurred saving to db: {e}")

        # Send confirmation email
        token = sserialzer.dumps(email, salt='email-confirm')
        msg = Message('Confirm your Email', sender='[email protected]', recipients=[email])
        link = url_for('confirm_email', token=token, _external=True)
        msg.body = f'Your link is {link}'
        try:
            mail.send(msg)
        except Exception as e:
            print(f"Error occurred sending message: {e}")
            flash("Error occurred sending message!")
            return render_template('signup.html')  
        flash('A confirmation email has been sent to your email address.', 'success')
        return redirect(url_for('index'))
    return render_template('signup.html')


Before adding the HTML sign-up form, let's complete the backend by adding the route for validating the double opt-in feature. This route uses the s variable we created earlier which generates the time-sensitive, secret token. See the docs for details.


The max-age is the seconds before the link expires, so in this case, the user has 20 minutes to confirm their email address.


@app.route('/confirm_email/<token>')
def confirm_email(token):
    try:
        email = sserialzer.loads(token, salt='email-confirm', max_age=1200)  # Token expires after 1 hour
    except SignatureExpired:
        return '<h1>The token is expired!</h1>'

    # Update field in database
    user = User.query.filter_by(email=email).first_or_404()
    user.validated = True
    db.session.commit()

    return '<h1>Email address confirmed!</h1>'


Now, for the ubiquitous main statement which tells Python to execute the script if the file is being executed directly (as opposed to an imported module):

if __name__ == '__main__':
    app.run()


Before we complete this back-end code, we still need the front-end HTML for the user input. We're going to do this with Flask's built-in Jinja template. Create a file namedtemplates/signup.html which should name-matches the route you created earlier in app.py. By default, Jinja uses the directory /templates for the html files. You can change this setting, but for this tutorial, we're going to use the /templatesdirectory of the app.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Email Sign Up</title>
</head>
<body>
    <h1>Sign Up</h1>
    <form action="{{ url_for('signup') }}" method="POST">
        <input type="email" name="email" placeholder="Enter your email" required>
        <input type="submit" value="Sign Up">
    </form>
    {% with messages = get_flashed_messages(with_categories=true) %}
      {% if messages %}
        <ul>
          {% for category, message in messages %}
            <li>{{ message }}</li>
          {% endfor %}
        </ul>
      {% endif %}
    {% endwith %}
</body>
</html>


Your code should be working from this point when you run the flask command with debugging enabled. This will allow you to see any errors in the command line as well as the browser window:


flask --app app.py --debug run 


Open your browser to the domain shown in the command-line (localhost) and the index page should render. Try to submit the form using a valid email address to receive the verification link. Once you get the link, it should look likehttp://localhost:5000/confirm_email/InRvbUByYXRldG91cmd1aWRlcy5jb20i.ZteEvQ.7o1_L0uM9Wl8uii7KhJdiWAH, you can follow it and get the email address validated using the validator route shown here:


@app.route('/confirm_email/<token>')
def confirm_email(token):
    try:
        email = sserializer.loads(token, salt='email-confirm', max_age=1200)  # Token expires after 1 hour
    except SignatureExpired:
        return '<h1>Oops, the token expired!</h1>'

    # Update field in database
    user = Users.query.filter_by(email=email).first_or_404()
    user.validated = True
    try:
        db.session.commit()
    except Exception as e:
        print(f"Error occurred saving to db: {e}")

    return '<h1>Email address confirmed!</h1>'


This route accepts the token string previously sent to you and checks it to see if it matches the corresponding database entry. If it does, it updates the validated field to True, and you can rest knowing your sign-up form wasn't abandoned.


This is an important step all successful businesses use in their registration systems and now you have it too. But wait, what if we get bot attacks submitting random email addresses without validating them? Then you will have a dirty database filled with useless entries. Let's prevent that!


To prevent bot attacks or at least mitigate the advanced ones, you can build your own time-consuming solution, including an IP limiter that requires an in-memory database such as Redis, or you can use a 3rd party service such as Google's captcha or hCaptcha.


In our tutorial, we will add hcaptcha's free plan. At the time of this writing, google's captcha isn't free and hcaptcha is. To have this functioning for your site, you need to register with them to get the API key from captcha.


We need new requirements so install them:
pip install flask-hcaptcha requests


Requests are needed to send the email address to hcaptcha for validation. Get the key, and integrate hcaptcha's javascript file with your HTML signup form. Add the file to the head of your HTML page and your site key to your form:


<head>
   ...
   <script src="https://hcaptcha.com/1/api.js" async defer></script>
</head>
<body>
   ...
   <form action="{{ url_for('signup') }}" method="POST">
       <input type="email" name="email" placeholder="Enter your email" required>
       <input type="submit" value="Sign Up">
       <div class="h-captcha" data-sitekey="b62gbcc-5cg2-41b2-cd5a-de95dd1eg61h" data-size="compact"></div>
   </form>


The site key in this code is an example; you will need your own from the free plan. This site key validates your form and inspects the site visitor with a comprehensive list of spam bots known by hcaptcha.


Next, modify your app.py file to include the hcaptcha's secret key (not the site key) in the app.config object, and post the response to hcaptcha prior to saving it to your own database.


app.config['HCAPTCHA_SECRET_KEY'] = 'your-secret-hcaptcha-key'
...
@app.route("/signup", methods=['GET', 'POST'])
def signup():
    if request.method == 'POST':
        email = bleach.clean(request.form.get('email'))
        hcaptcha_response = request.form.get('h-captcha-response')
        # Verify hCaptcha response
        payload = {
            'secret': app.config['HCAPTCHA_SECRET_KEY'],
            'response': hcaptcha_response
        }
        try:
            response = requests.post('https://hcaptcha.com/siteverify', data=payload, timeout=10)
            result = response.json()
        except requests.exceptions.RequestException as e:
            print(f"Request failed: {e}")

        if not result.get('success'):
            flash('CAPTCHA validation failed, please try again.', 'danger')
         ...
        # Insert user into the database
        new_user = Users(email=email)


Once this is done, you will have the hcaptcha icon showing in your sign-up form, and it should be enabled to prevent any spam. Now, you have a more robust form for your new app.


In case you encounter any errors or you have a typo in the code, you can check the completed code on my github.com


Comment if you want more.