Chapter 19 Quiz: Email Automation and Notifications

Instructions: Answer all questions. The answer key with explanations is at the bottom. Do not look at the answers until you have attempted every question.


Part A — Multiple Choice (1 point each)

Question 1

Which Python standard library module is responsible for connecting to an SMTP server and sending a message?

a. email b. smtplib c. mime d. requests


Question 2

What does MIMEMultipart("alternative") signal to an email client?

a. The message has multiple attachments b. The message was sent to multiple recipients c. The message parts are alternative representations of the same content (e.g., HTML and plain text) d. The message uses an alternative encoding (not base64)


Question 3

You want to load credentials from a .env file at the start of your script. Which call is correct?

a. import dotenv; dotenv.read() b. from dotenv import load_dotenv; load_dotenv() c. os.environ.load(".env") d. with open(".env") as f: os.environ.update(f.read())


Question 4

What is an App Password (in the context of Gmail or Outlook)?

a. A password you create for each application you install from the store b. A secondary, limited-scope password for third-party apps that does not expose your main account password c. A two-factor authentication backup code d. The password your company IT department gives you for email access


Question 5

You are sending an email to Sandra (To) and Marcus (CC), with a blind copy to the CFO. Which statement is correct?

a. Put all three addresses in the msg["To"] header b. Put Sandra in msg["To"], Marcus in msg["Cc"], and pass all three to to_addrs in sendmail() — do not add a BCC header c. Put all three in msg["Bcc"] to hide all recipients from each other d. BCC is not supported in Python's smtplib


Question 6

Which port is typically used for SMTP with SSL (SMTP_SSL in Python)?

a. 25 b. 80 c. 587 d. 465


Question 7

What is the purpose of the plain text part in an HTML email?

a. It is required by law for commercial emails b. It serves as a fallback for email clients that cannot render HTML and helps deliverability c. It is encrypted while the HTML part is not d. It is used by email clients to index the message for search


Question 8

In Python's smtplib, what is the advantage of using with smtplib.SMTP_SSL(...) as server: instead of manually calling server.quit()?

a. The with statement makes the connection faster b. The with statement ensures the connection is closed even if an exception occurs c. The with statement automatically handles authentication d. There is no practical difference between the two approaches


Question 9

You call os.environ.get("EMAIL_PASSWORD") and the variable is not set. What happens?

a. Python raises a KeyError b. Python raises an EnvironmentError c. Python returns None d. Python prompts you to enter the value interactively


Question 10

When attaching a file to an email using MIMEBase, what does encoders.encode_base64(part) do?

a. Encrypts the attachment with AES-256 b. Converts the binary file data to a text-safe base64 encoding that can be transmitted through email servers c. Compresses the attachment to reduce file size d. Validates that the file is not corrupted


Part B — True or False (1 point each)

Question 11

Storing your email password as a string literal inside a Python script is acceptable if you never share the file with anyone.

Question 12

load_dotenv() from the python-dotenv package will override environment variables that are already set in the system environment.

Question 13

A Slack Incoming Webhook URL should be stored in a .env file just like email credentials, because it provides write access to your Slack workspace.

Question 14

smtplib.SMTPAuthenticationError is the exception raised when you provide a wrong password.

Question 15

Jinja2 templates and Python f-strings accomplish the same goal but Jinja2 is preferred for complex, multi-section HTML emails because it supports template inheritance, loops, and conditionals in a file-based template.


Part C — Short Answer (2–3 points each)

Question 16

Explain the difference between smtplib.SMTP_SSL and using smtplib.SMTP followed by .starttls(). When would you use each?


Question 17

A coworker says: "I put our email password in the Python script and then committed the file to GitHub. Is that a problem?" Write a response explaining the risks and what they should do immediately.


Question 18

Write the Python code to construct a .env file path and load it using python-dotenv. Then write the code to safely retrieve a required variable (EMAIL_SENDER) and raise a helpful error message if it is missing.


Question 19

Describe the purpose of the Reply-To email header. Give a business scenario where it is useful.


Question 20

You have a Slack Incoming Webhook URL and want to send a simple alert message. Write the Python code using the requests library to send the message "ALERT: Revenue is below threshold" to the webhook. Include error checking.


Answer Key

Part A — Multiple Choice

Q1: b — smtplib The smtplib module handles the SMTP protocol connection and message transmission. The email module handles message construction. These are complementary: email builds the message, smtplib sends it.

Q2: c — Alternative representations of the same content MIMEMultipart("alternative") tells email clients that its child parts are different versions of the same content. The client should render the richest version it supports (typically HTML). If it cannot render HTML, it falls back to the plain text part.

Q3: b — from dotenv import load_dotenv; load_dotenv() This is the standard usage of python-dotenv. The load_dotenv() call reads the .env file (by default from the current directory) and sets each key-value pair as an environment variable in the current process.

Q4: b — A secondary, limited-scope password for third-party apps App Passwords are separate credentials generated specifically for applications that cannot handle OAuth flows. They can be revoked independently without changing your main password. They are required for Gmail and Outlook when 2FA is enabled.

Q5: b — Pass all three to to_addrs but do not add a BCC header BCC recipients receive the email (so they must be in to_addrs) but their addresses must not appear in any message header (so no msg["Bcc"] header). Using send_message() instead of sendmail() derives recipients from headers, which would omit BCC entirely — so BCC requires using sendmail() with an explicit address list.

Q6: d — 465 Port 465 is used for SMTP over SSL (the connection is encrypted from the start). Port 587 is used for SMTP with STARTTLS (starts unencrypted, then upgrades). Port 25 is traditional server-to-server SMTP (not used for client submission). Port 80 is HTTP.

Q7: b — Fallback for HTML-incapable clients and deliverability Plain text fallback ensures the message is readable on any email client, including command-line tools and older corporate mail systems. It also improves spam filter scores — HTML-only emails with no text alternative are a common spam signal.

Q8: b — Ensures the connection is closed even if an exception occurs Context managers implement __enter__ and __exit__ methods. When used with with, __exit__ is called even if an exception is raised within the block, ensuring proper cleanup (connection closure) in all cases.

Q9: c — Returns None os.environ.get("KEY") returns None if the key is not set. os.environ["KEY"] would raise a KeyError. This distinction matters: use .get() when a variable is optional, and dict-style access when it is required.

Q10: b — Converts binary data to base64 text encoding Email was historically designed for ASCII text. Binary files (images, PDFs, Excel files) must be converted to a text-safe format before transmission. Base64 encoding converts arbitrary binary data to a set of 64 ASCII characters, increasing size by about 33% but guaranteeing safe transmission through any mail server.

Part B — True or False

Q11: FALSE Even if you never intentionally share the file, hardcoded passwords create serious risks. The file can end up in version control history (even if deleted later), backup systems, clipboard history, log files, or editor crash files. The risk is not just sharing — it is any uncontrolled exposure path. Always use environment variables.

Q12: FALSE By default, load_dotenv() does NOT override existing environment variables. If EMAIL_SENDER is already set in the system environment, load_dotenv() leaves it alone. This is the correct behavior — it means the same code works in development (reads from .env) and production (reads from real environment variables set by the deployment system).

Q13: TRUE A Slack webhook URL grants anyone who has it the ability to post messages to your Slack workspace. It should be treated as a credential and stored in .env, never hardcoded or committed to version control.

Q14: TRUE smtplib.SMTPAuthenticationError is raised when the server rejects the username/password combination. This is the most common error when setting up email automation for the first time. When you see it, check your App Password and ensure your email address is correct.

Q15: TRUE Both tools produce string output from templates. But Jinja2 offers template inheritance (DRY base templates), proper looping ({% for %}), conditionals ({% if %}), filters, and macros — all in .html files that can be edited by someone who knows HTML but not Python. For anything more than a few paragraphs, Jinja2 is substantially better.

Part C — Short Answer

Q16: SMTP_SSL vs. STARTTLS SMTP_SSL opens the connection with encryption active from the first byte. SMTP + starttls() opens an unencrypted connection and then negotiates an upgrade to TLS. The end result is the same — encrypted transmission — but the mechanism differs. Use SMTP_SSL on port 465 (Gmail, common configuration). Use SMTP + starttls() on port 587 (Microsoft/Outlook, many corporate mail servers). Check your email provider's documentation if unsure.

Q17: The GitHub exposure problem Yes, it is a serious problem. Even if the repository is private, the password is now in git history. Anyone who ever clones the repository, anyone who gains access to the repository in the future, and anyone who can access GitHub's servers can see it. Additionally, GitHub automatically scans repositories for credential patterns and may alert you — but the damage may already be done.

Immediate steps: (1) Revoke the exposed password immediately — change your email password or, preferably, revoke the App Password and generate a new one. (2) Remove the password from the code and move it to a .env file. (3) Add .env to .gitignore. (4) Consider using git filter-branch or BFG Repo Cleaner to remove the credential from git history, though note that anyone who already cloned it still has the old history. (5) If the repository is public, assume the credential is compromised.

Q18: Loading a required .env variable

from pathlib import Path
from dotenv import load_dotenv
import os

# Load .env from the same directory as this script
env_path = Path(__file__).parent / ".env"
load_dotenv(dotenv_path=env_path)

# Retrieve required variable with a helpful error message
email_sender = os.environ.get("EMAIL_SENDER")
if not email_sender:
    raise EnvironmentError(
        "EMAIL_SENDER is not set. Create a .env file in the project "
        "directory with: EMAIL_SENDER=your-email@example.com\n"
        "See .env.example for all required variables."
    )

Q19: The Reply-To header Reply-To specifies the address where replies should go, independent of the From address. This is useful when sending from a no-reply or automated mailbox but wanting human replies to reach a real person.

Business scenario: Acme Corp sends automated reports from reports@acmecorp.com (a mailbox nobody monitors). By setting Reply-To: priya@acmecorp.com, any recipient who clicks Reply reaches Priya directly rather than sending a message into the void. The sending address stays clean and automated; replies still reach a human.

Q20: Sending to Slack webhook

import requests
import json
import os
from dotenv import load_dotenv

load_dotenv()

def send_slack_message(text: str) -> bool:
    webhook_url = os.environ.get("SLACK_WEBHOOK_URL")
    if not webhook_url:
        print("SLACK_WEBHOOK_URL not configured.")
        return False

    payload = {"text": text}
    try:
        response = requests.post(
            webhook_url,
            data=json.dumps(payload),
            headers={"Content-Type": "application/json"},
            timeout=10,
        )
        if response.status_code == 200 and response.text == "ok":
            print("Message sent to Slack.")
            return True
        else:
            print(f"Slack error: {response.status_code} — {response.text}")
            return False
    except requests.RequestException as e:
        print(f"Could not reach Slack: {e}")
        return False

send_slack_message("ALERT: Revenue is below threshold")

Key points: check for the webhook URL before attempting to send, handle network errors with try/except, verify the response (Slack returns the string "ok" on success), and use a timeout to avoid hanging indefinitely on network issues.


Scoring Guide

Score Level
45–50 Excellent — you are ready for production email automation
38–44 Proficient — review the areas where you lost points
28–37 Developing — re-read the chapter sections you found difficult
Below 28 Needs review — work through the chapter examples again before the exercises