Temp mail pentest
Creates and manages temporary email addresses via the Guerrilla Mail API for pentesting and bugbounty workflows. Use when you need to register on a site requiring email confirmation, receive password reset links, test for data leakage in outgoing emails, validate that an email-sending endpoint actually triggers, or test rate limits/spam handling by receiving multiple emails. Saves inbox state to a per-target JSON file for evidence and session reuse. Supports uv runtime.From its SKILL.md
npx -y skills add prof-ramos/skills --skill temp-mail-pentestAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its file declares
Copied from the file, not written here
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
17.5 KB, ~4.3k tokens by cl100k_base, as published. Nobody here has run it
Temp Mail Pentest
Create disposable inboxes on demand. Use the Guerrilla Mail API to receive real emails during active pentesting — registration confirmations, password resets, data leakage verification, and rate-limit testing. Each target gets its own JSON state file so evidence stays organized per engagement.
Reference implementation: This skill ships with a full installable CLI tool alongside this file (
tempmail/). Run it directly withuv run -p <skill-dir>/tempmail tempmail init --target example.comor install globally withuv tool install <skill-dir>/tempmail. The templates below teach an agent how to implement the same logic from scratch when more flexibility is needed.
When to Use
This skill activates when the user asks to:
- Register on a target site that requires email confirmation
- Receive and extract a password reset link or token
- Test whether sensitive data leaks in outgoing transactional emails
- Validate that an email-sending endpoint (contact form, invite, notification) actually fires
- Test rate limits or spam handling by receiving multiple emails to the same inbox
Do not use this skill for general email automation, email marketing, non-security email verification, or any task unrelated to authorized security testing.
API Overview
Base URL: http://api.guerrillamail.com/ajax.php
All requests require these parameters:
| Param | Value |
|---|---|
f | Function name (see API Reference) |
ip | Placeholder: "0.0.0.0" |
agent | Placeholder: "Mozilla_pentest_agent" |
Session: The server sets a PHPSESSID cookie on first call. Capture it from the Set-Cookie response header and send it back as Cookie: PHPSESSID=<sid> on every subsequent call. The SID can change at any response — always check the response headers for a new one.
Note: The API uses plain HTTP. Some pentest environments may block it. If the API is unreachable, inform the user and suggest alternatives (e.g., a different temp-mail provider).
JSON State File
Every target gets its own state file in the current working directory:
inbox-<normalized_target>.json
The target name is derived from the engagement — prompt the user for a label or derive it from the target domain (e.g., inbox-target.com.json).
Schema:
{
"session": {
"email": "[email protected]",
"sid": "phpsessid_value",
"created_at": 1717200000
},
"inbox": [
{
"id": 425,
"from": "[email protected]",
"subject": "Confirm your registration",
"excerpt": "Click the link below to activate your account...",
"ts": 1717200100,
"read": false,
"body": "<html>...</html>"
}
]
}
- If the file exists and the session is still valid (< 18 min inactivity, email not expired), reuse it.
- Append new emails as they arrive. Do not overwrite existing messages.
- Mark
read: trueafter the agent fetches and processes the email body. - Save after every meaningful state change (new email fetched, session created).
Session Management
Use this template to initialize or resume a session:
import requests
import json
import time
import os
import re
API_BASE = "http://api.guerrillamail.com/ajax.php"
def get_or_create_session(state_file: str) -> dict:
"""Load existing state or create a fresh session."""
if os.path.exists(state_file):
with open(state_file) as f:
state = json.load(f)
# Session may have expired — attempt email check to validate
return state
params = {"f": "get_email_address", "ip": "0.0.0.0", "agent": "Mozilla_pentest_agent"}
resp = requests.get(API_BASE, params=params)
sid = extract_phpsessid(resp)
data = resp.json()
state = {
"session": {
"email": data["email_addr"],
"sid": sid,
"created_at": data["email_timestamp"]
},
"inbox": []
}
save_state(state_file, state)
print(f"[+] Session created: {data['email_addr']} (SID: {sid[:8]}...)")
return state
def extract_phpsessid(resp: requests.Response) -> str:
"""Extract PHPSESSID from Set-Cookie header."""
if "Set-Cookie" not in resp.headers:
return ""
match = re.search(r"PHPSESSID=([a-zA-Z0-9]+);", resp.headers["Set-Cookie"])
return match.group(1) if match else ""
def save_state(path: str, state: dict):
with open(path, "w") as f:
json.dump(state, f, indent=2)
Instructions to the agent:
- Always capture
PHPSESSIDfrom theSet-Cookieheader on every response — it can change at any time - The
Set-Cookieheader is in the HTTP response headers, not the JSON body - Pass the SID via the
Cookieheader on every request:cookies = {"PHPSESSID": state["session"]["sid"]} requests.get(API_BASE, params=params, cookies=cookies) - If the SID changes, update
state["session"]["sid"]immediately and save - If a request returns a 200 but the response is empty or unexpected, the session likely expired — create a new one and inform the user
Polling Loop
Use this template to wait for a specific email. Set timeout based on the use case (default: 60s).
def poll_for_email(
state: dict,
state_file: str,
timeout: int = 60,
match_from: str = "",
match_subject: str = "",
match_body: str = "",
interval: int = 5
) -> dict | None:
"""Poll check_email until a matching email arrives or timeout."""
import urllib.parse
params = {
"f": "check_email",
"ip": "0.0.0.0",
"agent": "Mozilla_pentest_agent",
"seq": "0"
}
cookies = {"PHPSESSID": state["session"]["sid"]}
deadline = time.time() + timeout
while time.time() < deadline:
resp = requests.get(API_BASE, params=params, cookies=cookies)
# Update SID if server changed it
new_sid = extract_phpsessid(resp)
if new_sid and new_sid != state["session"]["sid"]:
state["session"]["sid"] = new_sid
cookies["PHPSESSID"] = new_sid
data = resp.json()
remaining = int(deadline - time.time())
print(f" [~] {len(data.get('list', []))} emails, {remaining}s remaining")
for mail in data.get("list", []):
if mail["mail_id"] in [m["id"] for m in state["inbox"]]:
continue # already processed
if match_from and match_from.lower() not in mail.get("mail_from", "").lower():
continue
if match_subject and match_subject.lower() not in mail.get("mail_subject", "").lower():
continue
# Matched — fetch full body
mail["read"] = False
state["inbox"].append({
"id": mail["mail_id"],
"from": mail["mail_from"],
"subject": mail["mail_subject"],
"excerpt": mail["mail_excerpt"],
"ts": mail["mail_timestamp"],
"read": False,
"body": None # fetched on demand
})
save_state(state_file, state)
return mail
time.sleep(interval)
print(" [!] Timeout reached — email did not arrive")
return None
Instructions to the agent:
- Start polling after the action that should trigger the email (e.g., after submitting a registration form)
- Log each poll attempt so the user sees progress
- Use
match_from(sender domain),match_subject(keyword), ormatch_body(keyword in excerpt) — at least one filter - If the target sends multiple emails (e.g., welcome + confirmation), poll in a loop calling
fetch_emailfor each - The
mail_excerptfield is HTML-entity-escaped — usehtml.unescape()if needed
Fetching Email Body
Once a matching email is found in the inbox list, fetch its full body:
def fetch_email_body(state: dict, mail_id: int) -> str | None:
"""Fetch the full HTML body of an email by ID."""
params = {
"f": "fetch_email",
"ip": "0.0.0.0",
"agent": "Mozilla_pentest_agent",
"email_id": mail_id
}
cookies = {"PHPSESSID": state["session"]["sid"]}
resp = requests.get(API_BASE, params=params, cookies=cookies)
data = resp.json()
# The body is in data["mail_body"] or similar — inspect the response
if isinstance(data, list):
data = data[0]
body = data.get("mail_body", data.get("body", ""))
return body
Instructions to the agent:
- Always fetch the body after matching an email in the poll loop
- Save the body to the inbox entry and mark
read: true - The GM API returns the email body in
mail_bodyfield of the fetch response - The body is already HTML-filtered by Guerrilla Mail (scripts, iframes, applets removed)
Displaying Images (res.php replacement)
Guerrilla Mail replaces image sources with links to their proxy:
/res.php?r=1&n=img&q=<original_url_encoded>
To view real images, replace these references:
import urllib.parse
import html
def resolve_gm_images(html_body: str) -> str:
"""Replace Guerrilla Mail image proxies with original URLs."""
# Match within double quotes
html_body = re.sub(
r'"/res\.php\?r=1&n=[a-z]+&q=([^"^&]+)"',
lambda m: '"' + urllib.parse.unquote(m.group(1)) + '"',
html_body
)
# Match within HTML-encoded quotes
html_body = re.sub(
r'"/res\.php\?r=1&n=[a-z]+&q=([^"]+)"',
lambda m: '"' + urllib.parse.unquote(m.group(1)) + '"',
html_body
)
return html_body
Only apply this replacement when the user asks to see images. By default, save the body with proxy URLs intact.
Use Cases
Registration Confirmation
Scenario: User wants to register on a target site and confirm the account via email.
Workflow:
def workflow_registration(target_url: str, state_file: str):
# 1. Create session
state = get_or_create_session(state_file)
print(f"[+] Using email: {state['session']['email']}")
print(f"[+] Register at: {target_url}")
print("[!] PAUSE — complete the registration form manually, then press Enter")
input()
# 2. Poll for confirmation email
mail = poll_for_email(
state, state_file,
match_subject="confirm",
match_from=extract_domain(target_url),
timeout=60
)
if not mail:
print("[-] Confirmation email not received")
return
# 3. Fetch body and extract link
body = fetch_email_body(state, mail["mail_id"])
confirm_link = extract_link_from_body(body)
if confirm_link:
print(f"[+] Confirmation link: {confirm_link}")
else:
print("[!] No link found in email body — check manually")
def extract_domain(url: str) -> str:
from urllib.parse import urlparse
return urlparse(url).netloc
def extract_link_from_body(html_body: str) -> str | None:
"""Find first https?:// link in HTML body."""
match = re.search(r'https?://[^\s"\'<>]+', html_body)
return match.group(0) if match else None
Password Reset
Scenario: User wants to trigger and capture a password reset link.
Workflow:
Same base as registration, but:
- Match subject on
"reset"or"password" - Extract the reset token/link from the body
- Optionally replace the reset link domain to test SSRF or host-header injection if applicable
def workflow_password_reset(target_url: str, state_file: str):
state = get_or_create_session(state_file)
print(f"[+] Using email: {state['session']['email']}")
print(f"[+] Trigger password reset at: {target_url}")
print("[!] PAUSE — submit the password reset form, then press Enter")
input()
mail = poll_for_email(
state, state_file,
match_subject="reset",
timeout=60
)
if not mail:
print("[-] Reset email not received")
return
body = fetch_email_body(state, mail["mail_id"])
reset_link = extract_link_from_body(body)
if reset_link:
print(f"[+] Reset link: {reset_link}")
return body # return for further analysis
Data Leakage Detection
Scenario: User wants to test what sensitive data the target leaks in transactional emails.
Workflow:
- Create session and register for an action that triggers an email
- Fetch the full email body
- Scan for sensitive patterns:
LEAK_PATTERNS = {
"email": r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}",
"phone": r"\+?[\d\s\-\(\)]{7,20}",
"cpf": r"\d{3}\.?\d{3}\.?\d{3}-?\d{2}",
"credit_card": r"\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}",
"ip_internal": r"(10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})",
"api_key": r"(?i)(api[_-]?key|apikey|secret|token)[:=]\s*['\"]?[a-zA-Z0-9_\-]{16,}"
}
def scan_for_leaks(body: str) -> dict:
findings = {}
for name, pattern in LEAK_PATTERNS.items():
matches = re.findall(pattern, body)
if matches:
findings[name] = list(set(matches))
return findings
Document any findings in the inbox state file notes and report to the user.
Endpoint Validation
Scenario: User wants to confirm that an email-sending endpoint actually fires (e.g., contact form, invite system, notification mechanism).
Workflow:
def workflow_validate_endpoint(target_url: str, state_file: str):
state = get_or_create_session(state_file)
print(f"[+] Target email: {state['session']['email']}")
print(f"[+] Submit at: {target_url}")
print("[!] PAUSE — trigger the endpoint (submit the form), then press Enter")
input()
# Check for any incoming email (no specific match filter)
mail = poll_for_email(state, state_file, timeout=90)
if mail:
print(f"[+] Email received from: {mail['mail_from']}")
print(f"[+] Subject: {mail['mail_subject']}")
return True
else:
print("[-] No email received — endpoint may not be firing")
return False
Rate Limit / Spam Testing
Scenario: User wants to test how many emails the target sends before rate-limiting or to collect a sample.
Workflow:
def workflow_rate_limit_test(target_url: str, state_file: str, max_emails: int = 20):
state = get_or_create_session(state_file)
print(f"[+] Using email: {state['session']['email']}")
print(f"[+] Target: {target_url}")
print(f"[+] Collecting up to {max_emails} emails")
print("[!] PAUSE — trigger the email-generating actions, then press Enter")
input()
collected = 0
deadline = time.time() + 180 # 3 min max for batch
while collected < max_emails and time.time() < deadline:
mail = poll_for_email(state, state_file, timeout=30, interval=5)
if not mail:
break
body = fetch_email_body(state, mail["mail_id"])
collected += 1
print(f" [{collected}/{max_emails}] {mail['mail_from']} - {mail['mail_subject']}")
print(f"\n[+] Collected {collected} emails total")
print(f"[+] State saved in: {state_file}")
API Reference Summary
| Function | Params | Returns | Notes |
|---|---|---|---|
get_email_address | lang (optional) | email_addr, email_timestamp | Initializes session. Returns existing session if SID is valid. |
set_email_user | email_user, lang | Same as get_email_address | Sets a custom email username. Requires valid SID. |
check_email | seq | list, count, email, ts | Returns up to 20 newest messages. |
get_email_list | offset, seq | Same as check_email | Paginated email list. offset=0 → first 20. |
fetch_email | email_id | Full email with body | Only emails owned by current session. |
forget_me | email_addr | true | Forgets current address, keeps session alive. |
del_email | email_ids[] | Array of deleted IDs | Delete one or more emails. |
extend | (none) | expired, email_timestamp, affected | Extends address lifetime by 1h (max 2h). |
Troubleshooting
Problem: requests not installed
Solution: uv pip install requests or uv add requests
Problem: API returns empty or unexpected response Causes:
- Session expired (SID invalid after ~18 min inactivity) → create new session
- IP blocked (rate limiting) → wait 30s and retry
- HTTP (not HTTPS) blocked by environment → inform user, suggest alternative
Problem: Set-Cookie header not found
Cause: Python's requests by default does not expose Set-Cookie. Use resp.headers.get("Set-Cookie", "") or access resp.cookies directly:
sid = resp.cookies.get("PHPSESSID", "")
Problem: PHPSESSID changed mid-session Fix: Always extract SID from the last response before each request. Update state immediately.
Problem: Email never arrives (poll timeout) Check:
- Is the user's trigger action correct? (form submitted, email sent?)
- Is the match filter too narrow? Try matching on fewer fields
- Is the target actually sending emails? Sometimes they queue or delay
What ships with it: 10 files
28.2 KB alongside SKILL.md, 8 of them executable
tempmail/
- client.pyruns5.7 KB
- cli.pyruns13.8 KB
- __init__.pyruns408 B
- __main__.pyruns85 B
- models.pyruns1.7 KB
- poller.pyruns2.9 KB
- storage.pyruns1.8 KB
- _version.pyruns22 B
- pyproject.toml531 B
- README.md1.2 KB