Explore Developer Center's New Chatbot! MongoDB AI Chatbot can be accessed at the top of your navigation to answer all your MongoDB questions.

MongoDB Developer
MongoDB
plus
Sign in to follow topics
MongoDB Developer Centerchevron-right
Developer Topicschevron-right
Productschevron-right
MongoDBchevron-right

Build a Newsletter Platform With Flask and MongoDB

Mercy Bassey11 min read • Published Jun 27, 2024 • Updated Sep 04, 2024
MongoDBPython
FULL APPLICATION
Facebook Icontwitter iconlinkedin icon
Rate this tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
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.

Prerequisites

To follow along in this article, you should meet the following requirements:
  • MongoDB installed on your machine — whether that’s Linux, Mac OS, or Windows
  • 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.

Setting up the environment

To begin, you must ensure MongoDB, Mongosh, and your RabbitMQ server are running. To achieve this, execute the following commands sequentially:
1sudo systemctl status mongod
2sudo systemctl status rabbitmq-server
3mongosh
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
11Jun 04 16:47:23 mercy systemd[1]: Started MongoDB Database Server.
12Jun 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
27Jun 04 16:47:19 mercy systemd[1]: Starting RabbitMQ Messaging Server...
28Jun 04 16:47:28 mercy systemd[1]: Started RabbitMQ Messaging Server.
29
30Current Mongosh Log ID: 665ff402cc42191d77a26a12
31Connecting to: mongodb://127.0.0.1:27017/?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+2.2.6
32Using MongoDB: 7.0.11
33Using Mongosh: 2.2.6
34
35For 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
44test>
Next, go ahead and create a working directory and virtual environment, and then activate the virtual environment using the following commands:
1mkdir newsletter
2cd newsletter
3code .
4python3 -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:
1pip 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.

Creating the template

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<!DOCTYPE html>
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<!DOCTYPE html>
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 ❤️ &copy; 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
3body {
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
18h1 {
19 color: white;
20}
21
22form {
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
32label {
33 display: block;
34 margin-bottom: 8px;
35 font-weight: bold;
36 color: white;
37}
38
39input[type="text"],
40input[type="email"],
41textarea {
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
50button {
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
61button:hover {
62 background: #DB6B18;
63}
64
65#response {
66 margin-top: 20px;
67 font-size: 16px;
68 color: #28a745;
69}
70
71footer {
72 text-align: center;
73 padding: 20px;
74 margin-top: 20px;
75 font-size: 16px;
76 color: #666;
77}

Creating the routes and tasks

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:
1import os
2
3class 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:
1from flask import render_template, request, abort, jsonify
2from app import app, db
3from tasks import send_emails
4
5@app.before_request
6def 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@app.route('/')
16def home():
17 return render_template('subscribe.html')
18
19@app.route('/admin')
20def admin():
21 return render_template('admin.html')
22
23@app.route('/subscribe', methods=['POST'])
24def 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">&#x2716;</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">&#x2714;</span> Subscribed successfully!
40 </div>
41 """, 200
42
43@app.route('/send-newsletters', methods=['POST'])
44def 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:
1from flask_mail import Message
2from app import app, mail, db, celery
3from datetime import datetime
4
5@celery.task(bind=True)
6def 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:
1from flask import Flask
2from flask_mail import Mail
3from pymongo import MongoClient
4from celery import Celery
5
6app = Flask(__name__)
7app.config.from_object('config.Config')
8
9mail = Mail(app)
10client = MongoClient(app.config['MONGO_URI'])
11db = client.get_database()
12
13celery = Celery(app.name, broker=app.config['CELERY_BROKER_URL'])
14celery.conf.update(app.config)
15
16from routes import *
17from tasks import *
18
19if __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.

Testing the application

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:
1flask --app app run
On another terminal window, execute the following command to start the celery worker:
1celery -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:
Viewing newsletter landing page
For administrative functions, visit the admin page at localhost:5000/admin. The page should look like this:
Viewing the newsletter admin page
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:
Adding a subscriber
The form will clear and a success message will be displayed.
To verify the subscription data, use the following MongoDB commands in your terminal:
1mongosh
2show dbs
3use newsletter
4show collections
You should see that a database named newsletter has been created along with a subscribers collection containing the subscription entries:
1Current Mongosh Log ID: 666151bb5bb9f923c2a26a12
2Connecting to: mongodb://127.0.0.1:27017/?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+2.2.6
3Using MongoDB: 7.0.11
4Using Mongosh: 2.2.6
5
6For 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
15test> show dbs
16admin 40.00 KiB
17config 60.00 KiB
18local 72.00 KiB
19newsletter 40.00 KiB
20test> use newsletter
21switched to db newsletter
22newsletter> show collections
23subscribers
24newsletter> 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]
35newsletter>
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:
1newsletter> show collections
2deliveries
3subscribers
4newsletter>
To review the contents of the deliveries collection and verify the details of the emails sent, use the following command in your MongoDB shell:
1db.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.
1newsletter> 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]
16newsletter>
This ensures you can track the success and content of each dispatch within your newsletter system.

Conclusion

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.
Start the Conversation

Facebook Icontwitter iconlinkedin icon
Rate this tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Related
Article

How Queryable Encryption Can Keep James Bond Safe


Apr 02, 2024 | 2 min read
Tutorial

Building with Patterns: The Bucket Pattern


May 16, 2022 | 3 min read
Quickstart

Working with Change Streams from Your Swift Application


Jan 25, 2023 | 4 min read
Quickstart

5 Different Ways to Deploy a Free Database with MongoDB Atlas


Feb 03, 2023 | 5 min read
Table of Contents