Home > Article > Backend Development > Build an anti-spam, opt-in Email registration with Python
We all do and if you're a beginner you need to take the following email sign-up features into consideration.
A solid email sign-up system is essential for web apps, newsletters, freebie downloads, invites to private groups, and lead generation. Let's not rely on using 3rd party services such as Auth0, Facebook, or Google to have access to your app. Keep your app data yours!
For you to start, 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 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 build previously built on both and prefer Python Flask for web apps even though Wordpress is very capable of making web apps.
Each code snippet will be explained and include some comments in the code. In case you haven't build user-registration or know of the inner workings, I will describe the details for you. Here is a summary of the features we will implement as stated in the first paragraph:
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 nor SQL injection type of hacks.
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.
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.
Let's code it out!
Create a working directory:
mkdir signup cd signup
Create your Python environment using python3 -m venv signup or conda create -n signup python3. I prefer conda.
Create MySQL table to store your users. The validated field is for the double opt-in:
CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, email VARCHAR(120) NOT NULL UNIQUE, password VARCHAR(120) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, validated BOOLEAN DEFAULT FALSE );
Install dependencies:
pip flask flask-mail secure SQLAlchemy Flask-WTF Flask-SQLAlchemy mysql-connector-python
Alternatively, you can have the same listed in a requirements.txt file and run pip install -r requirements.txt
Create app.py file with the following dependencies:
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 from werkzeug.security import generate_password_hash, check_password_hash import secrets
Enter your own server configuration data using these lines:
# Flask configurations secret = secrets.token_urlsafe(32) app.secret_key = secret app.config['SECRET_KEY'] = secret # auto-generated secret key # SQLAlchemy configurations SQLALCHEMY_DATABASE_URI = 'mysql+mysqlconnector://admin:user@localhost/tablename' # Email configurations app.config['MAIL_SERVER'] = 'smtp.example.com' app.config['MAIL_PORT'] = 587 app.config['MAIL_USERNAME'] = 'your_email@example.com' app.config['MAIL_PASSWORD'] = 'your_password' app.config['MAIL_USE_TLS'] = True app.config['MAIL_USE_SSL'] = False db = SQLAlchemy(app) mail = Mail(app) s = URLSafeTimedSerializer(app.config['SECRET_KEY']) #set secret to the serliazer
Ultimately, you should have your config info in a .env file.
The next section uses SQLAlchemy's ORM structure to query the database for you. Take note that the 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, it's 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) password = db.Column(db.String(120), 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()
For brevity of this tutorial, we're skipping the index page or what you would want to call your app homepage and just show the sign-up page using Python's decorator function for the page route:
@app.route('/') def index(): return '<h1>Homepage</h1>' @app.route('/signup', methods=['GET', 'POST']) def signup(): if request.method == 'POST': # Hidden field validation to prevent bot submission hidden_field = request.form.get('hidden_field') if hidden_field: return redirect(url_for('index')) # Bot detected, ignore submission email = request.form['email'] password = request.form['password'] hashed_password = generate_password_hash(password, method='sha256') # Insert user into the database new_user = User(email=email, password=hashed_password) db.session.add(new_user) db.session.commit() # Send confirmation email token = s.dumps(email, salt='email-confirm') msg = Message('Confirm your Email', sender='your_email@example.com', recipients=[email]) link = url_for('confirm_email', token=token, _external=True) msg.body = f'Your link is {link}' mail.send(msg) 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 = s.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(debug=True)
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 named templates/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 /templates directory 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="password" name="password" placeholder="Enter your password" required> <input type="hidden" name="bot_check"> <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
The above is the detailed content of Build an anti-spam, opt-in Email registration with Python. For more information, please follow other related articles on the PHP Chinese website!