# 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.

- Skill: `prof-ramos/temp-mail-pentest` (Agent Skill, multi-file: 11 files)
- Install (CLI): `npx skillmds@latest add prof-ramos/temp-mail-pentest`
- Raw SKILL.md: https://api.skillmd.com/api/skills/prof-ramos/temp-mail-pentest/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- License: MIT
- Author: prof-ramos (https://skillmd.com/u/prof-ramos)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/prof-ramos/temp-mail-pentest

---


# 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 with `uv run -p <skill-dir>/tempmail tempmail init --target example.com` or install globally with `uv 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:**

```json
{
  "session": {
    "email": "abc123@guerrillamailblock.com",
    "sid": "phpsessid_value",
    "created_at": 1717200000
  },
  "inbox": [
    {
      "id": 425,
      "from": "noreply@target.com",
      "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: true` after 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:

```python
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 `PHPSESSID` from the `Set-Cookie` header on **every** response — it can change at any time
- The `Set-Cookie` header is in the **HTTP response headers**, not the JSON body
- Pass the SID via the `Cookie` header on every request:
  ```python
  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).

```python
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), or `match_body` (keyword in excerpt) — at least one filter
- If the target sends multiple emails (e.g., welcome + confirmation), poll in a loop calling `fetch_email` for each
- The `mail_excerpt` field is HTML-entity-escaped — use `html.unescape()` if needed

## Fetching Email Body

Once a matching email is found in the inbox list, fetch its full body:

```python
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_body` field 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:

```python
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&amp;n=[a-z]+&amp;q=([^"^&]+)"',
        lambda m: '"' + urllib.parse.unquote(m.group(1)) + '"',
        html_body
    )
    # Match within HTML-encoded quotes
    html_body = re.sub(
        r'&quot;/res\.php\?r=1&amp;n=[a-z]+&amp;q=([^"]+)&quot;',
        lambda m: '&quot;' + urllib.parse.unquote(m.group(1)) + '&quot;',
        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:**

```python
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

```python
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:**

1. Create session and register for an action that triggers an email
2. Fetch the full email body
3. Scan for sensitive patterns:

```python
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:**

```python
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:**

```python
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:
```python
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

