Table of Contents
- ★ Prerequisites — Google Cloud OAuth & Gmail API setup
- Problem explanation — what is spam detection?
- Importing libraries — your toolbox
- Dataset exploration — understanding the data
- Data analysis & cleaning — preparing text for ML
- Feature engineering — turning words into numbers (CountVectorizer)
- Training & evaluating the KNN model
- Groq / Llama 3 — AI classifier with zero training
- ★ Connect Gmail — scan your real inbox
- ★ Live email spam tester — try it right here
Before running any code, you must set up access to the Gmail API through Google Cloud Console. This is a one-time browser process — no Python coding required. Once complete, your Python script can securely read emails from your inbox to classify them.
Why is this necessary? Google does not allow any app to access Gmail by default. You must register your app, declare what data it needs (read-only inbox access), and obtain permission. This process is called OAuth 2.0 — think of it as getting a visitor badge before entering a secure building. Without it, the Gmail code in Section 8 will fail with a 403 access_denied error.
Part A — Create a Google Cloud project & enable Gmail API
A Google Cloud project is a container for all your API settings and credentials. Every app that uses Google APIs requires one.
- 1Go to console.cloud.google.com and sign in with your Google account.
- 2Click the project dropdown at the top → click New Project. Name it something like
spam-detector→ click Create. - 3Ensure your new project is selected in the dropdown, then search for "Gmail API" in the top search bar.
- 4Click the Gmail API result → click Enable. Wait for the confirmation screen before continuing.
Part B — Configure the OAuth consent screen
The consent screen is the permission pop-up users see when authorising your app. Even if you are the only user, Google requires this to be configured before it will issue credentials.
- 1In the left sidebar go to APIs & Services → OAuth consent screen.
- 2Select External as the user type → click Create. (External + Test Users is the right choice for personal and student projects.)
- 3Fill in App name (e.g.
Spam Detector), User support email, and Developer contact email. Click Save and Continue. - 4On the Scopes page, click Add or Remove Scopes. Search for
gmail.readonlyand tickhttps://www.googleapis.com/auth/gmail.readonly. Click Update → Save and Continue. (Read-only scope means your app can read emails only — it cannot send, delete, or modify anything.) - 5Click Save and Continue on the Test Users page — you will add yourself in Part C.
Part C — Add yourself as a Test User
While your app is in Testing mode (the default for new projects), only explicitly approved accounts can complete the sign-in flow. You must add your own Gmail address here — otherwise the OAuth step in Section 8 will be blocked with a 403 error.
- 1From the left sidebar go to APIs & Services → OAuth consent screen.
- 2Scroll to the Test users section → click + Add Users.
- 3Enter your full Gmail address → click Add → click Save.
- 4Confirm your address appears in the Test users list. You can add up to 100 test users — useful for group projects.
403: access_denied error students encounter in Section 8. Always complete Part C before running any Python code.
Part D — Create OAuth 2.0 credentials & download credentials.json
Now we create the credential file that Python will use to identify your app when making Gmail API requests. Think of it as a digital ID card for your application.
- 1In the left sidebar go to APIs & Services → Credentials.
- 2Click + Create Credentials → choose OAuth client ID.
- 3Set Application type to Desktop app. Give it a name (e.g.
Colab Client) → click Create. (Choosing "Web Application" will cause a redirect_uri_mismatch error.) - 4In the popup that appears, click Download JSON.
- 5Rename the downloaded file to exactly
credentials.json. Keep this file secure — treat it like a password. Never share it or commit it to GitHub.
credentials.json saved on your computer. Proceed to Section 1 to start coding.
Spam emails are unsolicited messages sent in bulk — advertisements, scams, phishing attempts, and malware delivery. They clog inboxes, waste time, and cause real harm when users click malicious links or open dangerous attachments. The scale is enormous: roughly 45% of all email sent globally is spam.
Traditional anti-spam methods relied on blocklists of known bad senders and hardcoded keyword filters. The problem? Spammers constantly rotate domains and rephrase messages to evade those rules. Machine learning changes this: instead of memorising bad patterns, we teach a model to recognise the statistical fingerprints of spam — word distributions, email lengths, language style — and apply those learned patterns to emails it has never seen before.
What this workbook builds — end to end
By the end of this workbook, you will have:
- Loaded and explored a real email dataset with 5,171 labelled messages
- Cleaned and preprocessed raw email text for machine learning
- Trained a K-Nearest Neighbours (KNN) classifier on word frequency features
- Used Groq / Llama 3 (a free AI API) to classify emails with zero training data
- Connected the classifier to your real Gmail inbox to run a live spam scan
- Exported the flagged spam results to a CSV for review
Two approaches — and why both matter
A K-Nearest Neighbours classifier trained on thousands of labelled emails. It learns which word patterns appear in spam vs ham and votes on new emails by similarity. ~83% accuracy.
A large language model (LLM) that classifies emails using deep language understanding. Zero training required — the model already understands language and uses a single prompt to decide spam vs ham.
We connect to your real Gmail inbox via the Gmail API (OAuth 2.0). Your 10 most recent emails are fetched and classified automatically — real results, not sample data.
Binary classification — the core concept
A spam detector is a binary classifier. It reads an email and outputs one of two labels:
- Spam (label = 1) — unwanted, unsolicited email that should be filtered out
- Ham (label = 0) — legitimate email that should reach the inbox. "Ham" is the traditional NLP term for non-spam — it has no other special meaning.
❶ A KNN model trained on thousands of labelled email examples — ~83% accuracy.
❷ Groq / Llama 3 — a free LLM that classifies with a zero-shot prompt and no training step at all.
The core idea — pseudocode first
Before diving into real code, here is the conceptual logic at the heart of any spam classifier:
# Simplified logic — the real classifier learns this automatically from # thousands of examples rather than relying on hardcoded rules like these if message_contains_spam_keywords(message): # e.g. "free", "winner", "click here" classify_as_spam() else: classify_as_ham() # The ML model learns which words, patterns and lengths appear in spam # vs ham emails — then applies those patterns to new, unseen messages. # It does NOT need explicit rules — it discovers them from the data.
The first cell of any Python project imports the libraries (pre-built packages) you will use. Think of this as laying out your tools before starting a job. In Google Colab, all of these come pre-installed — no extra setup needed.
Run this cell every time you open the notebook. Python imports are session-based — if Colab restarts due to inactivity or a runtime error, all imports are erased and must be re-run from the top.
What each library does in this project
| Library | Purpose in this workbook |
|---|---|
| numpy | Efficient numerical arrays. Used behind the scenes by scikit-learn for matrix operations during model training. |
| pandas | Loads the CSV dataset into a DataFrame (think: Excel in Python). Used to filter, group, and display email data throughout. |
| seaborn | Builds statistical charts. We use it to visualise the spam vs ham class distribution and email length boxplots. |
| matplotlib | The base plotting library that seaborn builds on. We call plt.show() to render charts in the notebook output cell. |
Additional libraries (sklearn, groq, google-auth) are imported in the sections where they are first needed, keeping each section self-contained and easier to debug.
# ─── Core data science libraries ────────────────────────────────────────── import numpy as np # numerical arrays and math operations import pandas as pd # DataFrame (table) operations import seaborn as sns # statistical visualisations (bar charts, boxplots) import matplotlib.pyplot as plt # display charts in notebook output print('✓ Libraries imported successfully')
ModuleNotFoundError, run !pip install seaborn in a new cell above. The ! prefix tells Colab to run a shell command rather than Python code. This is rarely needed since most packages are pre-installed on Colab's default runtime.
Before training any model, a data scientist always starts with Exploratory Data Analysis (EDA) — getting to know the dataset's structure and quirks before touching the model. Skipping EDA leads to wasted training time and confusing results. Think of it as reading the manual before operating new equipment.
Getting the dataset
We use the spam_ham_dataset.csv from Kaggle — a collection of 5,171 real emails labelled as spam or ham. Download it from: kaggle.com → venky73/spam-mails-dataset
Once downloaded, upload it to your Colab session via the Files panel (folder icon in the left sidebar → Upload) or by running from google.colab import files; files.upload() in a cell.
Understanding the dataset columns
| Column | Type | What it contains | Role in the model |
|---|---|---|---|
| Unnamed: 0 | int | Row index — a sequential number assigned when the CSV was saved | Not used — ignored during training |
| label | str | "spam" or "ham" — the human-readable class name | Used for display and analysis only |
| text | str | Full email body text, exactly as received | Main input feature — what the model reads |
| label_num | int | 0 = ham, 1 = spam — the numeric version of the label | Target variable y — what the model predicts |
text column — raw strings of words. ML models cannot read text directly. Section 5 explains how we convert it into the numbers a model can process.
# Load the dataset into a pandas DataFrame data = pd.read_csv('spam_ham_dataset.csv') # ── Quick overview ───────────────────────────────────────────────────────── print(f'Dataset shape: {data.shape}') # (rows, columns) — expect ~(5172, 4) print(f'\nColumn names: {data.columns.tolist()}') print(f'\nData types:\n{data.dtypes}') print(f'\nClass distribution:\n{data["label"].value_counts()}') # Preview the first 5 rows to confirm the data loaded correctly data.head()
# Print one raw email from each class to build intuition before preprocessing print("=== Example HAM (legitimate) email (first 300 chars) ===") print(data[data['label'] == 'ham']['text'].iloc[0][:300]) print("\n=== Example SPAM email (first 300 chars) ===") print(data[data['label'] == 'spam']['text'].iloc[0][:300])
Raw email text is messy — it contains punctuation, mixed capitalisation, and common "filler" words like "the" and "is" that carry no spam signal. Machine learning works best on clean, consistent input. This section walks through four steps: class balance check, word frequency analysis, email length analysis, and text cleaning.
Step 1 — Visualise the class balance
Before any cleaning, we check how many emails are spam vs ham. This matters because an imbalanced dataset can mislead a model. If 90% of emails are ham and 10% are spam, a model that always guesses "ham" scores 90% accuracy while being completely useless at detecting spam. Understanding the balance helps us interpret results honestly.
# ── Class distribution bar chart ───────────────────────────────────────── class_counts = data['label'].value_counts() plt.figure(figsize=(8, 5)) sns.barplot(x=class_counts.index, y=class_counts.values, palette=['#00e676', '#ff4d6d']) plt.title('Class Distribution: Ham vs Spam', fontweight='bold') plt.ylabel('Number of Emails') plt.xlabel('Class Label') plt.show() print(f'Ham (legitimate): {class_counts["ham"]} emails ({class_counts["ham"]/len(data)*100:.1f}%)') print(f'Spam: {class_counts["spam"]} emails ({class_counts["spam"]/len(data)*100:.1f}%)')
Step 2 — Word frequency analysis
Before cleaning, we inspect which words appear most often in spam vs ham emails. This builds intuition about which words are strong spam signals. Words like "free", "winner", "prize", and "click" are far more common in spam. This is exactly the kind of pattern the KNN classifier will learn to detect. We write a reusable helper function so we can run the same analysis before and after cleaning to compare.
from collections import Counter def get_most_common_words(label, num_words, column_name, data): """ Returns the N most common words in all emails with a given label. Parameters: label : 'spam' or 'ham' num_words : how many top words to return column_name : which column to read text from ('text' or 'cleaned_txt') data : the DataFrame """ # Join all emails of this class into one long string, split into words text = " ".join(msg for msg in data[data['label'] == label][column_name]) words = text.lower().split() return Counter(words).most_common(num_words) # Raw text (before cleaning) — includes stopwords and punctuation ham_words = get_most_common_words("ham", 20, "text", data) spam_words = get_most_common_words("spam", 20, "text", data) print("Top 20 words in HAM emails (before cleaning):") print(ham_words) print("\nTop 20 words in SPAM emails (before cleaning):") print(spam_words)
Step 3 — Email length analysis
Email length is a surprisingly useful signal. Spam emails tend to be either very short (one-liners promoting a product) or very long (walls of text hiding a malicious link). A boxplot of character length by class shows whether length alone carries predictive value and helps us understand the range of data our model will work with.
# Create a new column with the character length of each email data['text_length'] = data['text'].apply(len) # Summary statistics: mean, median, min, max per class print('Email length statistics by class:') print(data.groupby('label')['text_length'].describe()) # Boxplot: box = middle 50% of data, centre line = median, dots = outliers plt.figure(figsize=(10, 5)) sns.boxplot(x='label', y='text_length', data=data, palette={'ham': '#00e676', 'spam': '#ff4d6d'}) plt.title('Email Character Length by Class', fontweight='bold') plt.ylabel('Character Count') plt.xlabel('Label') plt.ylim(0, 5000) # cap at 5000 to stop extreme outliers dominating the chart plt.show()
Step 4 — Text cleaning: lowercasing, removing punctuation & stopwords
This is the most important preprocessing step. We transform raw email text into clean text in three stages. Each stage reduces noise and helps the model focus on what matters:
data['cleaned.txt'] (with a dot). Pandas interprets the dot as an attribute accessor, causing a KeyError. The correct column name throughout this workbook is data['cleaned_txt'] with an underscore. Check your code uses the underscore version consistently.
import string from sklearn.feature_extraction.text import ENGLISH_STOP_WORDS def preprocess_text(text): """ Clean a single email string: 1. Lowercase all characters → "FREE" becomes "free" 2. Remove punctuation → "Click!" becomes "Click" 3. Remove English stopwords → "the", "is", "a" etc. stripped out Returns a cleaned string ready for CountVectorizer. """ # Step 1: lowercase — unifies different capitalisations of the same word text = text.lower() # Step 2: remove all punctuation using Python's built-in string.punctuation text = text.translate(str.maketrans("", "", string.punctuation)) # Step 3: split into words, filter out stopwords, rejoin into a clean string words = text.split() words = [w for w in words if w not in ENGLISH_STOP_WORDS] return ' '.join(words) # Apply cleaning to every email — use 'cleaned_txt' (underscore, not dot) data['cleaned_txt'] = data['text'].apply(preprocess_text) print('✓ Text cleaning applied to all emails') print('\nOriginal email (first 200 chars):') print(data['text'].iloc[0][:200]) print('\nCleaned version:') print(data['cleaned_txt'].iloc[0][:200]) data.head()
# Compare top words AFTER cleaning — stopwords are now gone # Distinctive spam words should now be clearly visible in the spam list ham_words_clean = get_most_common_words("ham", 20, "cleaned_txt", data) spam_words_clean = get_most_common_words("spam", 20, "cleaned_txt", data) print("Top 20 words in cleaned HAM emails:") print(ham_words_clean) print("\nTop 20 words in cleaned SPAM emails:") print(spam_words_clean)
Machine learning models work exclusively with numbers — they cannot read or understand words directly. We need to convert each email's cleaned text into a numerical representation that captures which words appear and how often. This process is called feature engineering.
The Bag of Words (BoW) model
We use the Bag of Words approach. The process works in three stages:
Why "Bag"? — and its limitation
It's called a "bag" because the model throws all words into a bag and counts them — it ignores word order and sentence structure. "The cat sat on the mat" and "The mat sat on the cat" produce identical feature vectors. This is a known limitation, but for spam detection it works well because spam keywords ("free", "click", "winner") matter more than their sentence position.
Sparse matrices — saving memory efficiently
The resulting matrix is sparse — mostly zeros, because any single email only uses a tiny fraction of all ~50,000 possible words. A dense matrix storing every zero would be enormous. scikit-learn stores only the non-zero values using a compressed format, which is far more efficient. You will see scipy.sparse matrix printed when you check the type.
from sklearn.feature_extraction.text import CountVectorizer # ── Initialise CountVectorizer ───────────────────────────────────────────── # fit_transform() does two things in one step: # .fit() — scans all emails and builds the vocabulary # .transform() — converts each email into its word-count vector vectorizer = CountVectorizer() # X = feature matrix (inputs to the model) # y = target vector (labels: 0 = ham, 1 = spam) X = vectorizer.fit_transform(data['cleaned_txt']) y = data['label_num'] print(f'Feature matrix shape: {X.shape}') # Expected output: (5172, ~50179) — 5172 emails, ~50179 unique vocabulary words print(f'Matrix type: {type(X)}') # Shows 'scipy.sparse matrix' — only non-zero values are stored in memory print(f'Vocabulary size: {len(vectorizer.vocabulary_)}') print(f'\nTarget class distribution:\n{y.value_counts()}')
fit_transform() only once on the training data to build the vocabulary. When classifying new emails (like your real Gmail inbox), we use only .transform() — never fit_transform(). Calling fit_transform() again would rebuild the vocabulary from new data, changing the feature dimensions and breaking the trained model completely.
Now we train our first classifier. We use K-Nearest Neighbours (KNN) — a simple but intuitive algorithm that classifies new emails by comparing them to labelled examples it has already memorised.
How KNN works — step by step
k=5, the model identifies the 5 most similar emails in the training set, measured by Euclidean distance across 50,000 word dimensions.Why an 80/20 train-test split?
We hold back 20% of the data for testing — the model never sees these emails during training. Imagine studying for an exam using the exact questions that will appear: you'd do perfectly without having actually learned anything. Holding back a test set gives an honest measure of how well the model generalises to new, unseen emails — which is what matters in a real inbox.
from sklearn.model_selection import train_test_split from sklearn.neighbors import KNeighborsClassifier from sklearn.metrics import accuracy_score, classification_report # ── 80/20 train-test split ──────────────────────────────────────────────── # random_state=42 makes the split reproducible — every student gets # the same train/test division so results can be directly compared. X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) print(f'Training set: {X_train.shape[0]} emails ({X_train.shape[0]/len(y)*100:.0f}% of data)') print(f'Test set: {X_test.shape[0]} emails ({X_test.shape[0]/len(y)*100:.0f}% of data)') # ── Initialise and train KNN ────────────────────────────────────────────── # n_neighbors=5: find 5 most similar training emails, then majority-vote their labels knn = KNeighborsClassifier(n_neighbors=5) print('\n⏳ Training KNN (may take 30–60 seconds on Colab)...') knn.fit(X_train, y_train) print('✓ Training complete') # ── Generate predictions on the held-out test set ───────────────────────── y_pred = knn.predict(X_test) # ── Overall accuracy ────────────────────────────────────────────────────── knn_accuracy = accuracy_score(y_test, y_pred) print(f'\n✅ KNN Accuracy: {knn_accuracy:.4f} ({knn_accuracy*100:.2f}%)') print('\n📊 Full Classification Report:') print(classification_report(y_test, y_pred, target_names=['ham', 'spam']))
Reading the classification report — four metrics explained
Accuracy alone is not enough to evaluate a spam filter. The report gives four numbers per class. Understanding each one is essential for any security application:
| Metric | What it measures | Why it matters for spam |
|---|---|---|
| Precision | Of all emails flagged as spam, how many actually were spam? (True Spam / All Flagged) | Low precision = legitimate emails end up in the spam folder — can cause missed important messages |
| Recall | Of all actual spam emails, how many were caught? (True Spam / All Real Spam) | Low recall = spam slips through to the inbox — annoying and potentially dangerous |
| F1-Score | Harmonic mean of precision and recall — a balanced single score | Useful when both error types matter and you want one number to optimise |
| Support | Number of actual emails of this class in the test set | Tells you if the results are based on enough examples to be statistically trustworthy |
The KNN model required training on thousands of labelled emails to learn spam patterns. A Large Language Model (LLM) takes a completely different approach — it already understands language deeply from pre-training on vast text corpora, and can classify emails using just a clear natural-language instruction (a "prompt"). No labelled dataset, no training step, no CountVectorizer.
We use Groq — a free API that provides access to Meta's open-source Llama 3 model. Groq's specialised hardware makes inference extremely fast, and the free tier is generous enough for student projects.
How zero-shot prompt-based classification works
0 or 1. We set max_tokens=5 and temperature=0 — no randomness means consistent, deterministic classifications.0 (ham) if the model returns anything unexpected. Error handling prevents one bad API response from crashing the entire classification loop.Getting your free Groq API key
- Go to console.groq.com
- Sign up with your Google account — no credit card required
- Click API Keys → Create API Key → copy the key
- In Colab, click the 🔑 Secrets icon in the left sidebar → add a new secret named
GROQ_API_KEYand paste your key as the value
userdata.get('KEY_NAME'). The key is never visible in your notebook output or version history.
# Install the Groq Python client (run once per Colab session)
!pip install -q groq
from groq import Groq from google.colab import userdata import time # Load the API key from Colab Secrets — never paste keys directly into code client = Groq(api_key=userdata.get('GROQ_API_KEY')) print('✅ Groq client ready — connected to Llama 3')
def classify_with_groq(email_text): """ Classify a single email as spam (1) or ham (0) using Llama 3 via Groq. Sends the first 500 characters. temperature=0 = deterministic output. Returns: int — 0 (ham) or 1 (spam) """ prompt = f"""You are an email spam classifier. Classify the following email as either spam or ham (not spam). Reply with ONLY the number 1 if it is spam, or 0 if it is ham. Do not explain your reasoning. Output ONLY 0 or 1. Email: {email_text[:500]} """ try: response = client.chat.completions.create( model="llama-3.3-70b-versatile", messages=[{"role": "user", "content": prompt}], max_tokens=5, # we only need 1 character (0 or 1) temperature=0 # no randomness — consistent output ) result = response.choices[0].message.content.strip() return int(result) if result in ['0', '1'] else 0 except Exception as e: print(f' ⏳ Rate limit hit — waiting 10s... ({e})') time.sleep(10) return classify_with_groq(email_text) # recursive retry print('✅ classify_with_groq() defined and ready')
from sklearn.metrics import accuracy_score, classification_report sample_size = 10 test_sample = data.sample(n=sample_size, random_state=42) groq_preds = [] print(f'Classifying {sample_size} emails with Groq (Llama 3)...\n') for i, email in enumerate(test_sample['text']): pred = classify_with_groq(email) groq_preds.append(pred) print(f' Email {i+1:>2}/{sample_size} → {"SPAM" if pred==1 else "HAM"}') time.sleep(1) # pause between calls to respect rate limits groq_accuracy = accuracy_score(test_sample['label_num'], groq_preds) print(f'\n✅ Groq/Llama 3 accuracy on {sample_size} samples: {groq_accuracy:.4f}') print('\n📊 Classification Report:') print(classification_report( test_sample['label_num'], groq_preds, target_names=['ham', 'spam'] ))
Interpreting the results — what the numbers tell us
Below is a typical result. Your exact numbers may vary since the sample is only 10 emails. The key insight is the trade-off between precision and recall when an LLM is over-cautious:
| Class | Metric | Typical value | What this means in plain English |
|---|---|---|---|
| HAM | Precision | 1.00 | Every email the model called HAM was genuinely legitimate — zero false alarms on the ham side |
| HAM | Recall | 0.78 | It found 7 of 9 real HAM emails — 2 legitimate emails were misclassified as spam (over-cautious) |
| HAM | F1-Score | 0.88 | Strong overall HAM performance despite the over-caution |
| SPAM | Precision | 0.33 | Only 1 of 3 emails it labelled SPAM was actual spam — 2 HAM emails were falsely flagged |
| SPAM | Recall | 1.00 | It caught the 1 real SPAM email — it never missed actual spam in this sample |
| SPAM | F1-Score | 0.50 | Weak SPAM performance on this small sample due to false positives |
This is where the project becomes real. Instead of classifying sample emails from a dataset, we connect to your actual Gmail inbox using the Gmail API and run a live spam scan. You completed the Google Cloud setup in Section 0 — this section runs the Python code that uses those credentials.
credentials.json — do that first. The code below will fail without it.
How the OAuth 2.0 flow works in code
OAuth 2.0 is the security protocol that lets your Python code access Gmail without needing your password directly. Here is the four-step flow:
credentials.json to Colab. This file is your app's identity document — it tells Google which app is requesting permission.token.json, and builds a Gmail service object. On future runs, if token.json is still valid, steps 1–3 are skipped automatically.# Install Google API client libraries (pre-installed on Colab, but just in case)
!pip install -q google-auth google-auth-oauthlib google-api-python-client
import os from google.colab import files from google.auth.transport.requests import Request from google.oauth2.credentials import Credentials from google_auth_oauthlib.flow import Flow from googleapiclient.discovery import build print('📁 A file picker will appear — select your credentials.json file') uploaded = files.upload() # Read-only scope: can read emails only — cannot send, delete, or modify SCOPES = ['https://www.googleapis.com/auth/gmail.readonly'] CREDENTIALS_PATH = 'credentials.json' TOKEN_PATH = 'token.json' # Try to load a saved token from a previous run creds = None if os.path.exists(TOKEN_PATH): creds = Credentials.from_authorized_user_file(TOKEN_PATH, SCOPES) # Run the OAuth flow if no valid token exists if not creds or not creds.valid: if creds and creds.expired and creds.refresh_token: creds.refresh(Request()) # silently refresh expired token else: flow = Flow.from_client_secrets_file( CREDENTIALS_PATH, scopes=SCOPES, redirect_uri='urn:ietf:wg:oauth:2.0:oob' # out-of-band flow for Colab ) auth_url, _ = flow.authorization_url(prompt='consent') print('='*60) print('👉 STEP 1: Open this URL in a new browser tab:') print('='*60) print(auth_url) print('='*60) print('👉 STEP 2: Sign in with your Gmail and click Allow') print('👉 STEP 3: Copy the authorisation code shown by Google') print('='*60) code = input('📋 STEP 4: Paste the code here and press Enter: ') flow.fetch_token(code=code) creds = flow.credentials with open(TOKEN_PATH, 'w') as token: token.write(creds.to_json()) print('✅ Token saved to token.json') gmail_service = build('gmail', 'v1', credentials=creds) print('✅ Gmail connected successfully!')
✓ Gmail connected successfully!
Email fetching functions
Gmail's API returns emails as nested JSON with base64-encoded bodies. Two helper functions handle this complexity. get_email_body() recursively navigates Gmail's nested payload structure to extract plain text. fetch_inbox_emails() calls the API and returns a clean list of email dictionaries. You do not need to understand the Gmail API internals to use these.
import base64 def get_email_body(payload): """ Recursively extract plain text body from a Gmail message payload. Gmail stores bodies in a nested 'parts' structure. Some emails have a single body; others have multiple parts (plain text + HTML). We extract plain text and ignore HTML parts. """ if 'parts' in payload: for part in payload['parts']: result = get_email_body(part) # recursive call for nested parts if result: return result if payload.get('mimeType') == 'text/plain': data = payload.get('body', {}).get('data', '') if data: # Gmail encodes body bytes as URL-safe base64 — decode to readable text return base64.urlsafe_b64decode(data).decode('utf-8', errors='ignore') return '' def fetch_inbox_emails(service, max_results=10): """Fetch N most recent emails. Returns list of {id, subject, sender, body}.""" results = service.users().messages().list( userId='me', labelIds=['INBOX'], maxResults=max_results ).execute() messages = results.get('messages', []) emails = [] for msg in messages: full_msg = service.users().messages().get( userId='me', id=msg['id'], format='full' ).execute() headers = full_msg['payload']['headers'] subject = next((h['value'] for h in headers if h['name']=='Subject'), 'No Subject') sender = next((h['value'] for h in headers if h['name']=='From'), 'Unknown') body = get_email_body(full_msg['payload']) emails.append({'id':msg['id'], 'subject':subject, 'sender':sender, 'body':body}) print(f'✅ Fetched {len(emails)} emails from inbox') return emails inbox_emails = fetch_inbox_emails(gmail_service, max_results=10)
import time print('🔍 Running spam detection on your inbox...\n') print(f"{'#':<4} {'VERDICT':<10} {'SENDER':<35} {'SUBJECT'}") print('─' * 95) results = [] for i, email in enumerate(inbox_emails, 1): # Subject + body combined — subject often has the most direct spam signals text_to_classify = email['subject'] + ' ' + email['body'] pred = classify_with_groq(text_to_classify) verdict = '🚨 SPAM' if pred == 1 else '✅ HAM' subject_short = email['subject'][:40] + ('...' if len(email['subject']) > 40 else '') sender_short = email['sender'][:33] results.append({'subject':email['subject'],'sender':email['sender'], 'verdict':'spam' if pred==1 else 'ham','pred':pred}) print(f"{i:<4} {verdict:<10} {sender_short:<35} {subject_short}") time.sleep(2) # 2-second pause — respects Groq free-tier rate limits spam_count = sum(1 for r in results if r['pred'] == 1) ham_count = len(results) - spam_count print(f"\n{'='*60}") print(f"📊 Summary: {ham_count} HAM | {spam_count} SPAM | {len(results)} total scanned") print(f"{'='*60}")
import pandas as pd results_df = pd.DataFrame(results) print(results_df[['verdict', 'sender', 'subject']].to_string(index=False)) # Save flagged spam to CSV — download from Colab Files panel (left sidebar) spam_df = results_df[results_df['verdict'] == 'spam'] spam_df.to_csv('flagged_spam.csv', index=False) print(f'\n✅ {len(spam_df)} spam email(s) saved to flagged_spam.csv') results_df.to_csv('all_scan_results.csv', index=False) print(f'✅ All {len(results_df)} results saved to all_scan_results.csv') print('\nDownload both files from the Files panel in the left sidebar.')
Troubleshooting common Gmail API errors
| Error message | Cause | Fix |
|---|---|---|
403: access_denied | Your Gmail address is not on the Test Users list | Go to Section 0 Part C and add your email as a test user |
redirect_uri_mismatch | Wrong application type selected for credentials | Credentials → edit OAuth client → change type to Desktop app |
| Access blocked — app not verified | App is in Testing mode (expected, normal) | Click Advanced → Go to [app] (unsafe). Normal for student test apps. |
| Empty body on some emails | Email is HTML-only with no plain text part | Classifier will use the subject line only. Slightly lower accuracy on these. |
| 429 Rate limit from Groq | API calls made too rapidly | Increase the time.sleep() value in Cell 18 to 3 or 5 seconds |
This interactive panel lets you test spam detection heuristics instantly — no Python, no Colab, no API key needed. Enter a subject line and email body below and click Analyse. The browser scores the email against patterns derived from the same signals the KNN model learns during training.
What the analyser checks
The scorer looks for a combination of signals that are statistically more common in spam than in legitimate email:
- Spam keyword density — words and phrases like "free", "winner", "urgent", "click here", "act now", "limited time", "guaranteed"
- Excessive capitalisation — ALL CAPS words signal urgency and shouting common in spam
- Exclamation mark count — multiple exclamation marks are a classic spam indicator
- Suspicious link patterns — raw IP addresses used as domains, or hyphens in domain names
- Short high-pressure text — very short emails that contain multiple spam keywords
- Price/percentage references — multiple monetary references common in promotional spam
Enter a subject and body from any email you want to test. Click Analyse to see a spam probability score and a signal breakdown.
Going further — project extension ideas
- Improve KNN accuracy: Try
TfidfVectorizerinstead ofCountVectorizer— it downweights common words and emphasises rare, informative ones, often giving a 5–10% accuracy boost. - Try Naive Bayes: Replace KNN with
MultinomialNB— the classic spam algorithm that typically achieves 95%+ accuracy on this dataset and trains in milliseconds. - Compare models side by side: Run KNN, Naive Bayes, and Groq/Llama 3 on the same test emails. Which misses spam? Which produces false alarms? Build a comparison table.
- Auto-label in Gmail: Extend the code to use Gmail's
labelsendpoint to automatically add a "Suspected Spam" label to flagged emails in your inbox. - Scan more emails: Increase
max_resultsinfetch_inbox_emails()and add longer sleep times to process your full inbox history. - Build a Streamlit app: Wrap the classifier in a
streamlitweb interface so anyone can paste an email and get an instant verdict — no Colab needed.