Build a Newsletter Platform With Flask and MongoDB
Mercy Bassey11 min read • Published Jun 27, 2024 • Updated Sep 04, 2024
FULL APPLICATION
Rate this tutorial
A newsletter is a powerful tool for maintaining regular communication with your audience. It connects you with your subscribers, allowing you to share updates, news, and other valuable content directly to their inboxes. In this article, you’ll learn how to build a dynamic and interactive newsletter platform where subscribers can easily subscribe to your newsletter, and you, as the admin, can efficiently manage and send batch emails to all subscribed users. By the end of this guide, you will have a fully functional newsletter platform that leverages JavaScript for interactivity, Flask on the server side, and MongoDB for efficient data storage.
To follow along in this article, you should meet the following requirements:
- Mongosh for interacting with your MongoDB instance — view the instructions for installation
- RabbitMQ installed on your machine for message brokering — view the installation instructions for Debian and Ubuntu, then Windows and MacOS using Homebrew
- Gmail as the SMTP server for sending emails — in this case, generate an app password to securely authenticate your application without using your Gmail account's primary password
The commands used in this tutorial are demonstrated on a Linux machine with an Ubuntu 22.04LTS distro. The code used in this tutorial can be found in the GitHub repository.
To begin, you must ensure MongoDB, Mongosh, and your RabbitMQ server are running. To achieve this, execute the following commands sequentially:
1 sudo systemctl status mongod 2 sudo systemctl status rabbitmq-server 3 mongosh
If you have the following output, you are set:
1 ● mongod.service - MongoDB Database Server 2 Loaded: loaded (/lib/systemd/system/mongod.service; enabled; vendor preset: enabled) 3 Active: active (running) since Tue 2024-06-04 16:47:23 WAT; 13h ago 4 Docs: https://docs.mongodb.org/manual 5 Main PID: 1305 (mongod) 6 Memory: 259.0M 7 CPU: 2min 8.508s 8 CGroup: /system.slice/mongod.service 9 └─1305 /usr/bin/mongod --config /etc/mongod.conf 10 11 Jun 04 16:47:23 mercy systemd[1]: Started MongoDB Database Server. 12 Jun 04 16:47:24 mercy mongod[1305]: {"t":{"$date":"2024-06-04T15:47:24.620Z"},"s":"I", "c":"CONTROL", "id":7484500, "ctx":"main","msg":"Environment variabl> 13 14 ● rabbitmq-server.service - RabbitMQ Messaging Server 15 Loaded: loaded (/lib/systemd/system/rabbitmq-server.service; enabled; vendor preset: enabled) 16 Active: active (running) since Tue 2024-06-04 16:47:28 WAT; 13h ago 17 Main PID: 781 (beam.smp) 18 Tasks: 27 (limit: 9003) 19 Memory: 125.1M 20 CPU: 1min 55.438s 21 CGroup: /system.slice/rabbitmq-server.service 22 ├─ 781 /usr/lib/erlang/erts-12.2.1/bin/beam.smp -W w -MBas ageffcbf -MHas ageffcbf -MBlmbcs 512 -MHlmbcs 512 -MMmcs 30 -P 1048576 -t 5000000 -st> 23 ├─ 866 erl_child_setup 65536 24 ├─1204 inet_gethost 4 25 └─1205 inet_gethost 4 26 27 Jun 04 16:47:19 mercy systemd[1]: Starting RabbitMQ Messaging Server... 28 Jun 04 16:47:28 mercy systemd[1]: Started RabbitMQ Messaging Server. 29 30 Current Mongosh Log ID: 665ff402cc42191d77a26a12 31 Connecting to: mongodb://127.0.0.1:27017/?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+2.2.6 32 Using MongoDB: 7.0.11 33 Using Mongosh: 2.2.6 34 35 For mongosh info see: https://docs.mongodb.com/mongodb-shell/ 36 37 ------ 38 The server generated these startup warnings when booting 39 2024-06-04T16:47:24.825+01:00: Using the XFS filesystem is strongly recommended with the WiredTiger storage engine. See http://dochub.mongodb.org/core/prodnotes-filesystem 40 2024-06-04T16:47:26.247+01:00: Access control is not enabled for the database. Read and write access to data and configuration is unrestricted 41 2024-06-04T16:47:26.247+01:00: vm.max_map_count is too low 42 ------ 43 44 test>
Next, go ahead and create a working directory and virtual environment, and then activate the virtual environment using the following commands:
1 mkdir newsletter 2 cd newsletter 3 code . 4 python3 -m venv .venv 5 . .venv/bin/activate # For Mac/Linux OS 6 .venv\Scripts\activate # For Windows OS
With the virtual environment activated, you’re now ready to install the libraries needed to develop your newsletter platform. To proceed, you’ll need to install the following libraries:
- Flask for handling the web server and routing
- Flask Mail for sending emails from your application
- PyMongo for interfacing with MongoDB, which you can use to manage subscriber data and persistent storage of newsletter content
- Celery for managing asynchronous tasks, such as sending batch emails
Using the following command, you will have them installed in your virtual environment:
1 pip install Flask Flask-Mail pymongo celery
Finally, in your working directory, create two directories called
static
and templates
. These will serve as the locations for static files like CSS and HTML templates, essential for rendering your web application's front-end part. After that, create the following files:app.py
: This will serve as the main entry point for your Flask application, where you initialize your app and tie together the other components.config.py
: This file will contain all configuration settings for your application, such as MongoDB connection details, mail server configuration, and Celery broker connection alongside any other environment-specific variables.routes.py
: Here, you will define the routes (URLs) that your application will respond to. It will include functions to handle requests and responses, connecting URLs to Python functions.tasks.py
: This file will be used for defining background tasks that can be processed asynchronously — which is, in this case, to send emails.
With your environment set up, the next thing to do is develop the look of your newsletter platform. In the templates directory, create two HTML files —
admin.html
and subscrbe.html
. In admin.html
, add the following HTML markup:1 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>Admin - Send Newsletter</title> 7 <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}"> 8 </head> 9 <body> 10 <h1>Send Newsletter</h1> 11 <form id="admin-form"> 12 <label for="title">Title:</label> 13 <input type="text" id="title" name="title" required> 14 <br> 15 <label for="body">Body:</label> 16 <textarea id="body" name="body" required></textarea> 17 <br> 18 <button type="submit">Send</button> 19 </form> 20 <div id="response"></div> 21 </body> 22 </html>
And then add the following code in the
subscribe.html
file:1 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>Subscribe to Newsletter</title> 7 <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}"> 8 </head> 9 <body> 10 <h1>Subscribe to our Newsletter</h1> 11 <form id="subscribe-form"> 12 <label for="firstname">First Name:</label> 13 <input type="text" id="firstname" name="firstname" required> 14 <br> 15 <label for="lastname">Last Name:</label> 16 <input type="text" id="lastname" name="lastname" required> 17 <br> 18 <label for="email">Email:</label> 19 <input type="email" id="email" name="email" required> 20 <br> 21 <button type="submit">Subscribe</button> 22 </form> 23 <div id="response"></div> 24 25 <footer> 26 Made with ❤️ © 2024 Mercy 27 </footer> 28 </body> 29 </html>
In the
admin.html
and subscribe.html
templates, add the following scripts respectively in the body tag:1 <script> 2 document.getElementById('admin-form').addEventListener('submit', function(event) { 3 event.preventDefault(); 4 var formData = new FormData(event.target); 5 fetch('/send-newsletters', { 6 method: 'POST', 7 body: formData 8 }) 9 .then(response => response.json()) 10 .then(() => { 11 document.getElementById('response').innerText = 'Emails are being sent!'; 12 setTimeout(() => { 13 document.getElementById('response').innerText = ''; 14 }, 3000); 15 document.getElementById('admin-form').reset(); 16 }) 17 .catch(error => { 18 document.getElementById('response').innerText = 'Error sending emails.'; 19 setTimeout(() => { 20 document.getElementById('response').innerText = ''; 21 }, 3000); 22 console.error('Error:', error); 23 }); 24 }); 25 </script>
1 <script> 2 document.getElementById('subscribe-form').addEventListener('submit', function(event) { 3 event.preventDefault(); 4 var formData = new FormData(event.target); 5 fetch('/subscribe', { 6 method: 'POST', 7 body: formData 8 }).then(response => { 9 if (!response.ok) { 10 throw response; 11 } 12 return response.text(); 13 }).then(data => { 14 document.getElementById('response').innerHTML = data; 15 document.getElementById('subscribe-form').reset(); 16 setTimeout(() => { 17 document.getElementById('response').innerHTML = ''; 18 }, 3000); 19 }).catch(error => { 20 error.text().then(errorMessage => { 21 document.getElementById('response').innerHTML = errorMessage; 22 setTimeout(() => { 23 document.getElementById('response').innerHTML = ''; 24 }, 3000); 25 }); 26 }); 27 }); 28 </script>
For the
admin.html
template, the script block ensures that the form submission for sending newsletters is handled asynchronously. When the admin form is submitted, the JavaScript intercepts this event to prevent a page reload. It then sends the form data to the server using the Fetch API. Upon success, it displays a message that emails are being sent, and this message clears after a few seconds. In case of an error during the form submission, it captures that error and displays an appropriate message to the admin.For the
subscribe.html
template, the script block ensures that the subscription process is similarly handled asynchronously. When a user submits their subscription details, the script prevents the default form submission to the server and instead sends the data using Fetch. The server's response is then displayed directly in the HTML. If the response indicates success, a confirmation message is shown, and if there is an error (such as the server responding that the email already exists in the database), the error message is displayed.Finally, create a
styles.css
file in your static folder and add the following styles:1 @import url('https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap'); 2 3 body { 4 font-family: "Nunito", sans-serif; 5 font-optical-sizing: auto; 6 font-weight: 300; 7 font-style: normal; 8 margin: 0; 9 padding: 0; 10 display: flex; 11 flex-direction: column; 12 align-items: center; 13 justify-content: center; 14 height: 100vh; 15 background-color: #040100; 16 } 17 18 h1 { 19 color: white; 20 } 21 22 form { 23 background: #DB4918; 24 padding: 30px 40px; 25 border-radius: 8px; 26 box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); 27 width: 100%; 28 max-width: 400px; 29 margin: 20px 0; 30 } 31 32 label { 33 display: block; 34 margin-bottom: 8px; 35 font-weight: bold; 36 color: white; 37 } 38 39 input[type="text"], 40 input[type="email"], 41 textarea { 42 width: 100%; 43 padding: 10px; 44 margin-bottom: 10px; 45 border: 1px solid #ccc; 46 border-radius: 4px; 47 font-size: 16px; 48 } 49 50 button { 51 background: #DB8218; 52 color: white; 53 padding: 10px 20px; 54 border: none; 55 border-radius: 4px; 56 cursor: pointer; 57 font-size: 16px; 58 font-family: "Nunito", sans-serif; 59 } 60 61 button:hover { 62 background: #DB6B18; 63 } 64 65 #response { 66 margin-top: 20px; 67 font-size: 16px; 68 color: #28a745; 69 } 70 71 footer { 72 text-align: center; 73 padding: 20px; 74 margin-top: 20px; 75 font-size: 16px; 76 color: #666; 77 }
With the template set up, you are now ready to define the specific routes and background tasks that will drive the functionality of your newsletter platform. This involves mapping URLs to Python functions in your Flask application, which will handle form submissions, user interactions, and any other required processes. Additionally, you'll configure Celery tasks to handle asynchronous operations such as sending emails in batches.
To begin, add the following in your
config.py
:1 import os 2 3 class Config: 4 CELERY_BROKER_URL = 'amqp://guest:guest@localhost//' 5 RESULT_BACKEND = 'mongodb://localhost:27017/celery_results' 6 MAIL_SERVER = 'smtp.gmail.com' 7 MAIL_PORT = 587 8 MAIL_USE_TLS = True 9 MAIL_USERNAME = '<username>' # Your email address without the '@gmail.com' 10 MAIL_PASSWORD = '<app password>' 11 MAIL_DEFAULT_SENDER = '<email address>' 12 ALLOWED_IPS = ['127.0.0.1'] 13 MONGO_URI = 'mongodb://localhost:27017/newsletter'
Here you are setting up the configuration for your Flask application. This configuration includes the settings for Celery to connect to its message broker (RabbitMQ in this case, indicated by the AMQP URL) and MongoDB for the results back end. You're also configuring Flask-Mail with Gmail as the SMTP server to handle outgoing emails, which requires the mail server details, port, and secure connection preferences, along with your Gmail credentials. The
MAIL_DEFAULT_SENDER
is used as the default sender email address for your outgoing emails. Additionally, ALLOWED_IPS
is specified for an extra layer of security, limiting access to certain functionalities based on the requester's IP address. Lastly, the MONGO_URI
sets the connection string to your MongoDB database, where subscriber information and other data relevant to your newsletter system will be stored.Next, in your
routes.py
file, add the following code snippets:1 from flask import render_template, request, abort, jsonify 2 from app import app, db 3 from tasks import send_emails 4 5 6 def limit_remote_addr(): 7 if 'X-Forwarded-For' in request.headers: 8 remote_addr = request.headers['X-Forwarded-For'].split(',')[0] 9 else: 10 remote_addr = request.remote_addr 11 12 if request.endpoint == 'admin' and remote_addr not in app.config['ALLOWED_IPS']: 13 abort(403) 14 15 16 def home(): 17 return render_template('subscribe.html') 18 19 20 def admin(): 21 return render_template('admin.html') 22 23 24 def subscribe(): 25 first_name = request.form['firstname'] 26 last_name = request.form['lastname'] 27 email = request.form['email'] 28 29 if db.users.find_one({'email': email}): 30 return """ 31 <div class="response error"> 32 <span class="icon">✖</span> This email is already subscribed! 33 </div> 34 """, 409 35 36 db.users.insert_one({'firstname': first_name, 'lastname': last_name, 'email': email, 'subscribed': True}) 37 return """ 38 <div class="response success"> 39 <span class="icon">✔</span> Subscribed successfully! 40 </div> 41 """, 200 42 43 44 def send_newsletters(): 45 title = request.form['title'] 46 body = request.form['body'] 47 subscribers = list(db.users.find({'subscribed': True})) 48 49 for subscriber in subscribers: 50 subscriber['_id'] = str(subscriber['_id']) 51 52 send_emails.apply_async(args=[subscribers, title, body]) 53 return jsonify({'message': 'Emails are being sent!'}), 202
The code snippet above establishes the essential routes and functionalities for your newsletter platform. The
limit_remote_addr
function restricts access to the admin interface based on IP address to enhance security. The /subscribe
endpoint handles user subscriptions by checking for duplicate entries before saving new subscriber data to MongoDB. The /send-newsletters
route triggers asynchronous email dispatch to subscribers using Celery, allowing for non-blocking operations and immediate feedback to the user that emails are being sent.In your
tasks.py
, add the following code snippets:1 from flask_mail import Message 2 from app import app, mail, db, celery 3 from datetime import datetime 4 5 6 def send_emails(self, subscribers, title, body): 7 with app.app_context(): 8 for subscriber in subscribers: 9 try: 10 print(f"Sending email to {subscriber['email']}") 11 msg = Message(title, recipients=[subscriber['email']]) 12 msg.body = body 13 mail.send(msg) 14 db.deliveries.insert_one({ 15 'email': subscriber['email'], 16 'title': title, 17 'body': body, 18 'delivered_at': datetime.utcnow() 19 }) 20 print("Email sent") 21 22 except Exception as e: 23 print(f"Failed to send email to {subscriber['email']}: {str(e)}") 24 25 return {'result': 'All emails sent'}
This will set up a Celery task that handles the sending of emails asynchronously. When the
send_emails
task is called, it iterates through each subscriber, composing and sending an email using Flask-Mail. For each successful email sent, the details are logged into the MongoDB deliveries
collection with the recipient's email, email title, body, and the timestamp of when it was sent. If an error occurs during the email-sending process, it is caught and logged, which ensures that Celery handles failures and maintains a record of all email transmission attempts.Finally, in your
app.py
, add the following code snippets:1 from flask import Flask 2 from flask_mail import Mail 3 from pymongo import MongoClient 4 from celery import Celery 5 6 app = Flask(__name__) 7 app.config.from_object('config.Config') 8 9 mail = Mail(app) 10 client = MongoClient(app.config['MONGO_URI']) 11 db = client.get_database() 12 13 celery = Celery(app.name, broker=app.config['CELERY_BROKER_URL']) 14 celery.conf.update(app.config) 15 16 from routes import * 17 from tasks import * 18 19 if __name__ == '__main__': 20 app.run(debug=True)
This will initialize your Flask application using configurations loaded from your configuration file. It sets up Flask-Mail for email functionality, connects to MongoDB using PyMongo, and configures Celery with the specified broker from the configuration file. Then, the routes and tasks are imported to integrate endpoint definitions and background task functions into the app. Additionally, it runs the application in debug mode to allow for real-time debugging and development, making it easier to track down issues as they arise.
Everything is all set up for your newsletter platform. Now, it's time to see how it works. To start your Flask application, navigate to your project directory in the terminal and execute the following command:
1 flask --app app run
On another terminal window, execute the following command to start the celery worker:
1 celery -A app.celery worker --loglevel=info
To access your newsletter platform, navigate to localhost:5000 on your web browser. You will see the initial landing page as depicted below:
For administrative functions, visit the admin page at localhost:5000/admin. The page should look like this:
To test your application, subscribe via the homepage. You can add as many subscribers as you wish. Upon successful subscription, you should see a confirmation similar to this:
The form will clear and a success message will be displayed.
To verify the subscription data, use the following MongoDB commands in your terminal:
1 mongosh 2 show dbs 3 use newsletter 4 show collections
You should see that a database named newsletter has been created along with a subscribers collection containing the subscription entries:
1 Current Mongosh Log ID: 666151bb5bb9f923c2a26a12 2 Connecting to: mongodb://127.0.0.1:27017/?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+2.2.6 3 Using MongoDB: 7.0.11 4 Using Mongosh: 2.2.6 5 6 For mongosh info see: https://docs.mongodb.com/mongodb-shell/ 7 8 ------ 9 The server generated these startup warnings when booting 10 2024-06-06T06:44:55.413+01:00: Using the XFS filesystem is strongly recommended with the WiredTiger storage engine. See http://dochub.mongodb.org/core/prodnotes-filesystem 11 2024-06-06T06:44:56.369+01:00: Access control is not enabled for the database. Read and write access to data and configuration is unrestricted 12 2024-06-06T06:44:56.369+01:00: vm.max_map_count is too low 13 ------ 14 15 test> show dbs 16 admin 40.00 KiB 17 config 60.00 KiB 18 local 72.00 KiB 19 newsletter 40.00 KiB 20 test> use newsletter 21 switched to db newsletter 22 newsletter> show collections 23 subscribers 24 newsletter> db.subscribers.find().pretty() 25 [ 26 { 27 _id: ObjectId('66614cf25c0255f4a797fe97'), 28 firstname: 'Mercy', 29 lastname: 'Udoh', 30 email: 'mercybassey683@gmail.com', 31 subscribed: true 32 } 33 ... 34 ] 35 newsletter>
Next, to send batch emails, go to your admin page at localhost:5000/admin and dispatch a newsletter. Your Celery backend should log the following upon successful email dispatch:
1 [2024-06-06 13:34:37,304: WARNING/ForkPoolWorker-4] Email sent 2 [2024-06-06 13:34:37,305: INFO/ForkPoolWorker-4] Task tasks.send_emails[b119bb9e-b2ef-4c85-b048-ca96e0e60ae1] succeeded in 17.155154566993588s: {'result': 'All emails sent'}
Confirm the creation of the
deliveries
collection alongside subscribers
in your MongoDB database:1 newsletter> show collections 2 deliveries 3 subscribers 4 newsletter>
To review the contents of the
deliveries
collection and verify the details of the emails sent, use the following command in your MongoDB shell:1 db.deliveries.find().pretty()
This command will display the records in the deliveries collection in a formatted manner, allowing you to easily review each entry. You should see data similar to the following output, which includes details such as email address, title of the newsletter, body content, and the timestamp of when each email was sent.
1 newsletter> db.deliveries.find().pretty() 2 [ 3 { 4 _id: ObjectId('6661acdd5d0b93accd9fba1e'), 5 email: 'mercybassey683@gmail.com', 6 title: 'Green Thumbs Monthly: June Garden Tips & Upcoming Events', 7 body: 'Dear Green Thumbs Members,\r\n' + 8 '\r\n' + 9 "Welcome to your June edition of the Green Thumbs Monthly! As the days grow longer and the soil warms, it’s the perfect time to dive deep into the joys of summer gardening. Here's what's inside this issue:\r\n" + 10 '\r\n' + 11 '1. Member Spotlight:\r\n' + 12 'This month, we’re featuring Jane Doe, who has transformed her backyard into a vibrant oasis of native plants and vegetables. ', 13 delivered_at: ISODate('2024-06-06T12:34:37.237Z') 14 } 15 ] 16 newsletter>
This ensures you can track the success and content of each dispatch within your newsletter system.
And there you have it! You've created a powerful and interactive newsletter platform. This project not only allowed you to deepen your understanding of Python, JavaScript, Flask, and MongoDB but also gave you a glimpse of how these technologies intertwine to create a functional web application.
As you admire your creation, remember that this is just the beginning. You can continue to enhance this platform with more features like automated responses, analytics integration to track subscriber engagement, and customizable email templates. The beauty of creating is that it's never truly finished. There's always something more to add, to improve, and to innovate. So, keep exploring, keep learning, and most importantly, keep creating.
Want to continue the conversation? If you have questions or want to connect with other folks building cool things with MongoDB, head to our Developer Community next.
Top Comments in Forums
There are no comments on this article yet.