Skip to content

Build shared WC24 mail foundation - #7

Merged
ColinGamez merged 1 commit into
mainfrom
agent/shared-wc24-mail-foundation
Jul 24, 2026
Merged

Build shared WC24 mail foundation#7
ColinGamez merged 1 commit into
mainfrom
agent/shared-wc24-mail-foundation

Conversation

@ColinGamez

@ColinGamez ColinGamez commented Jul 24, 2026

Copy link
Copy Markdown
Owner

What changed

  • add a channel-independent filesystem-backed WC24 account and mail store
  • implement account.cgi, challenge/HMAC check.cgi, multipart send.cgi,
    multipart receive.cgi, and delete.cgi
  • accept the Wii's SMTP-envelope message format and preserve delivered MIME
  • add dry-run-first mail-config provisioning with checksum repair and backups
  • add mail-serve and expanded account auditing commands
  • add an isolated two-account protocol verifier covering the complete CGI flow

Why

Wii Mail is shared infrastructure used by the Message Board, friend
registration, and channel features such as TV no Tomo stamp sharing. It should
not be embedded in any one channel adapter.

Validation

  • isolated account → check → send → receive → delete flow passes
  • Dolphin-compatible SHA-1 challenge HMAC independently recomputed
  • real configured Dolphin account returns cd=100 with a valid HMAC
  • configured mail flag is 33 characters
  • nwc24msg.cfg checksum remains valid after provisioning
  • all five original WC24 mail URLs redirect to the shared local service
  • Python compilation and diff checks pass

Known emulator boundary

Dolphin 2606 currently skips incoming WC24 download entries with an empty
destination filename. Server-side receive/delete support is complete, but
moving downloaded MIME into wc24recv.mbx requires a Dolphin KD implementation
patch; no channel WAD patch is needed.

Summary by Sourcery

Introduce a shared filesystem-backed WC24 mail service and Dolphin mail provisioning tooling, decoupling Wii Mail from individual channel adapters.

New Features:

  • Add a MailStore abstraction and HTTP CGI mail server implementing account registration, challenge/HMAC checks, message sending, receiving, and deletion.
  • Add CLI commands to run the shared mail service and to provision Dolphin WC24 mail URLs and credentials into nwc24msg.cfg.
  • Add a protocol verifier tool that exercises the full account/check/send/receive/delete flow against the mail service.

Enhancements:

  • Extend WC24 config handling to manage mail-related fields, URLs, and credentials with backup-aware, checksum-preserving updates.
  • Enhance account auditing output to report mail credential presence and configured Wii Mail URLs.

Documentation:

  • Update README to describe the shared mail service, new CLI usage, provisioning behavior, and current Dolphin receive limitations.

Tests:

  • Add an isolated end-to-end mail service verification script that validates HMAC behavior, MIME preservation, mailbox flag advancement, receive multipart responses, and delete semantics.

@gitguardian

gitguardian Bot commented Jul 24, 2026

Copy link
Copy Markdown

️✅ There are no secrets present in this pull request anymore.

If these secrets were true positive and are still valid, we highly recommend you to revoke them.
While these secrets were previously flagged, we no longer have a reference to the
specific commits where they were detected. Once a secret has been leaked into a git
repository, you should consider it compromised, even if it was deleted immediately.
Find here more information about risks.


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@sourcery-ai

sourcery-ai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Reviewer's Guide

Introduce a shared filesystem-backed WC24 mail service plus Dolphin provisioning, wiring it into the CLI and WC24 config handling, and add a protocol verifier to exercise the complete CGI flow and HMAC behavior.

Sequence diagram for mail-config provisioning flow

sequenceDiagram
    actor User
    participant Jwc24CLI as jwc24_main
    participant WC24Config as wc24_config
    participant Store as MailStore
    participant FS as filesystem

    User->>Jwc24CLI: invoke mail-config
    Jwc24CLI->>WC24Config: read(args.config)
    Jwc24CLI->>WC24Config: summarize(config)
    Jwc24CLI->>Store: MailStore(args.data_dir)
    Jwc24CLI->>Store: register(state.nwc24_id)
    Store->>FS: _read_accounts / _write_accounts
    Store-->>Jwc24CLI: MailAccount(password, mlchkid)
    Jwc24CLI->>WC24Config: configure_mail(args.config, args.base_url, account.password, account.mlchkid, args.apply)
    WC24Config->>FS: read / backup / write_bytes
    WC24Config-->>Jwc24CLI: (before_urls, after_urls, backup)
    Jwc24CLI-->>User: print URL changes and apply/dry-run status
Loading

File-Level Changes

Change Details Files
Extend WC24 configuration handling to support mail-related fields and safe provisioning of mail URLs and credentials.
  • Define offsets and helpers for reading/writing C-strings within nwc24msg.cfg
  • Expose mail_urls and mail_credentials_present inspectors used by the CLI audit path
  • Implement configure_mail to derive the Wii Mail address, apply password and mlchkid, populate five WC24 mail CGI URLs, recompute checksum, and write timestamped backups on apply
jwc24/wc24_config.py
Add CLI surface for running the shared mail service and provisioning Dolphin WC24 mail settings, and enhance account auditing to report mail state.
  • Register mail-serve and mail-config subcommands with host/port/data-dir and config/base-url parameters
  • Implement mail-serve dispatch to the HTTP mail server and mail-config orchestration that creates a MailStore account then calls configure_mail, printing URL changes and dry-run vs apply status
  • Extend account command to show mail credential presence and configured mail URLs using the new wc24_config helpers
jwc24/__main__.py
Implement a shared filesystem-backed WC24 mail store with account management, authentication, HMAC check responses, SMTP-envelope parsing, message queuing, and mailbox lifecycle operations.
  • Define MailAccount dataclass and MailStore class that manages accounts.json and per-recipient message directories under a data root
  • Implement register, by_check_id, authenticate, and check_response using the public MAIL_CHECK_KEY to match Dolphin’s SHA-1 HMAC contract and mail_flag semantics
  • Parse SMTP RCPT TO and DATA to extract recipients and message payload, compute a SHA-256-based filename, enforce size limits, and advance per-recipient mail_flag on new deliveries
  • Provide pending, claim, and delete_claimed mailbox operations with basic size limiting and normalization of line endings for outgoing MIME
jwc24/mail.py
Expose an HTTP CGI-compatible WC24 mail server implementing account, check, send, receive, delete, and a health endpoint on top of MailStore.
  • Build a ThreadingHTTPServer with a BaseHTTPRequestHandler that routes POSTs to account.cgi, check.cgi, send.cgi, receive.cgi, and delete.cgi plus GET /healthz
  • Parse x-www-form-urlencoded and multipart/form-data bodies, including a custom mlid/passwd auth field format, and translate errors into WC24 status codes
  • Implement receive.cgi to return a multipart/mixed response carrying a text status part and one part per queued MIME message with size accounting and headers mirroring WC24 expectations
  • Provide serve_mail helper to run the server, log requests, and handle shutdown on KeyboardInterrupt
jwc24/mail_server.py
Add an isolated verifier tool that spins up the mail server and confirms end-to-end CGI and HMAC behavior with two accounts.
  • Create a temporary MailStore and HTTP server instance bound to localhost and construct WC24 CGI URLs for testing
  • Exercise account.cgi, check.cgi with a known challenge, and recompute expected HMAC using MAIL_CHECK_KEY and NO_MAIL_FLAG to ensure contract correctness
  • Send a multipart/form-data SMTP-envelope message from sender to recipient, then assert mailbox contents and mail_flag advancement
  • Test receive.cgi returns the MIME payload and delete.cgi clears the inbox, printing a concise success summary and exiting with appropriate status
tools/verify_mail_service.py
Update documentation to describe the shared mail service, CLI usage, and current emulator limitation around received mail.
  • Document the shared Wii Mail service capabilities and its role as channel-independent infrastructure in the main README
  • Add example invocations for mail-serve and mail-config, noting dry-run behavior, backup semantics, and where credentials/queued messages are stored
  • Clarify that server-side receive/delete is implemented but Dolphin currently skips downloads with empty destination filenames, requiring a KD patch rather than WAD modification
README.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@ColinGamez
ColinGamez force-pushed the agent/shared-wc24-mail-foundation branch from 6e20a13 to db04b6b Compare July 24, 2026 03:25
@ColinGamez
ColinGamez force-pushed the agent/shared-wc24-mail-foundation branch from db04b6b to 09d9931 Compare July 24, 2026 03:26
@ColinGamez
ColinGamez marked this pull request as ready for review July 24, 2026 03:27
@ColinGamez
ColinGamez merged commit 78f5889 into main Jul 24, 2026
3 checks passed
@ColinGamez
ColinGamez deleted the agent/shared-wc24-mail-foundation branch July 24, 2026 03:27

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="jwc24/__main__.py" line_range="77" />
<code_context>
+            )
+            for old, new in zip(before, after):
+                print(f"{old} -> {new}")
+            print(f"applied; backup: {backup}" if backup else "dry run only; pass --apply")
+            return 0
         if args.command == "account":
</code_context>
<issue_to_address>
**suggestion (bug_risk):** The message conflates a no-op `--apply` run with a dry run, which can be misleading.

In `mail-config`, `configure_mail` returns `backup=None` both when `--apply` is omitted and when `--apply` is used but no changes are needed (`data == original`). Since the message is based solely on `backup is None`, users who did pass `--apply` but had no changes see “dry run only; pass --apply,” which is incorrect. It would be clearer to branch on `apply` and whether changes were made, e.g. distinct messages for: `apply=False`, `apply=True && data==original`, and `apply=True && data!=original`.

Suggested implementation:

```python
            account = MailStore(args.data_dir).register(f"{state.nwc24_id:016d}")
            before, after, backup = wc24_config.configure_mail(
                args.config,
                args.base_url,
                account.password,
                account.mlchkid,
                args.apply,
            )
            for old, new in zip(before, after):
                print(f"{old} -> {new}")

            # Distinguish between dry-run, no-op apply, and applied changes
            changes_made = list(before) != list(after)

            if not args.apply:
                if changes_made:
                    print("dry run only; pass --apply to apply these changes")
                else:
                    print("dry run; no changes needed")
            elif not changes_made:
                print("no changes needed; configuration already up to date")
            else:
                print(f"applied; backup: {backup}")
            return 0
        if args.command == "account":

```

```python
            if args.apply and not args.bootstrap_local:
                raise ValueError("--apply requires --bootstrap-local")
                print(f"checksum: {after.calculated_checksum:08x} (valid={after.checksum_valid})")
                # Reflect whether this was an apply or a dry-run, and if any changes were needed
                if args.apply:
                    print(f"applied; backup: {backup}" if backup else "applied; no changes needed")
                else:
                    print("dry run; no changes needed")
            else:
                config = wc24_config.read(args.config)
                state = wc24_config.summarize(config)
                print(f"WiiConnect24 ID: {state.nwc24_id}")
                print(f"ID generation: {state.id_generation}")

```

- In the second block, I don’t see `before`/`after` being set in the snippet; if this branch also has access to them, you may want to mirror the `changes_made = list(before) != list(after)` logic from the first block for consistency and to avoid inferring “no changes” solely from `backup is None`.
- If `configure_mail` (or the code in the `account` branch) can ever produce a non-`None` `backup` for a dry run, you should adjust the second branch’s messages to account for that (e.g., by computing `changes_made` there as well).
</issue_to_address>

### Comment 2
<location path="jwc24/mail_server.py" line_range="148-156" />
<code_context>
+                    self.end_headers()
+                    self.wfile.write(response)
+                    return
+                if path.endswith("/delete.cgi"):
+                    form = parse_qs(body.decode("ascii"), keep_blank_values=True)
+                    account = store.authenticate(
+                        normalize_wii_id(form["mlid"][0]), form["passwd"][0]
+                    )
+                    if account is None:
+                        self._reply(cgi_response(cd=250))
+                        return
+                    deleted = store.delete_claimed(account)
+                    self._reply(cgi_response(cd=100, deletenum=deleted))
+                    return
</code_context>
<issue_to_address>
**issue (bug_risk):** `delete.cgi` ignores the `delnum` parameter and always deletes all claimed messages.

`/delete.cgi` parses `delnum` but then calls `store.delete_claimed(account)` without using it, so all `.sent` messages are deleted regardless of the requested count. This matches the current test setup (single message) but conflicts with the semantics of `delnum` and could surprise callers expecting partial deletion. Either remove `delnum` from the request/response if it’s not needed, or implement deletion of up to `delnum` messages to align with the protocol.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread jwc24/__main__.py
)
for old, new in zip(before, after):
print(f"{old} -> {new}")
print(f"applied; backup: {backup}" if backup else "dry run only; pass --apply")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): The message conflates a no-op --apply run with a dry run, which can be misleading.

In mail-config, configure_mail returns backup=None both when --apply is omitted and when --apply is used but no changes are needed (data == original). Since the message is based solely on backup is None, users who did pass --apply but had no changes see “dry run only; pass --apply,” which is incorrect. It would be clearer to branch on apply and whether changes were made, e.g. distinct messages for: apply=False, apply=True && data==original, and apply=True && data!=original.

Suggested implementation:

            account = MailStore(args.data_dir).register(f"{state.nwc24_id:016d}")
            before, after, backup = wc24_config.configure_mail(
                args.config,
                args.base_url,
                account.password,
                account.mlchkid,
                args.apply,
            )
            for old, new in zip(before, after):
                print(f"{old} -> {new}")

            # Distinguish between dry-run, no-op apply, and applied changes
            changes_made = list(before) != list(after)

            if not args.apply:
                if changes_made:
                    print("dry run only; pass --apply to apply these changes")
                else:
                    print("dry run; no changes needed")
            elif not changes_made:
                print("no changes needed; configuration already up to date")
            else:
                print(f"applied; backup: {backup}")
            return 0
        if args.command == "account":
            if args.apply and not args.bootstrap_local:
                raise ValueError("--apply requires --bootstrap-local")
                print(f"checksum: {after.calculated_checksum:08x} (valid={after.checksum_valid})")
                # Reflect whether this was an apply or a dry-run, and if any changes were needed
                if args.apply:
                    print(f"applied; backup: {backup}" if backup else "applied; no changes needed")
                else:
                    print("dry run; no changes needed")
            else:
                config = wc24_config.read(args.config)
                state = wc24_config.summarize(config)
                print(f"WiiConnect24 ID: {state.nwc24_id}")
                print(f"ID generation: {state.id_generation}")
  • In the second block, I don’t see before/after being set in the snippet; if this branch also has access to them, you may want to mirror the changes_made = list(before) != list(after) logic from the first block for consistency and to avoid inferring “no changes” solely from backup is None.
  • If configure_mail (or the code in the account branch) can ever produce a non-None backup for a dry run, you should adjust the second branch’s messages to account for that (e.g., by computing changes_made there as well).

Comment thread jwc24/mail_server.py
Comment on lines +148 to +156
if path.endswith("/delete.cgi"):
form = parse_qs(body.decode("ascii"), keep_blank_values=True)
account = store.authenticate(
normalize_wii_id(form["mlid"][0]), form["passwd"][0]
)
if account is None:
self._reply(cgi_response(cd=250))
return
deleted = store.delete_claimed(account)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): delete.cgi ignores the delnum parameter and always deletes all claimed messages.

/delete.cgi parses delnum but then calls store.delete_claimed(account) without using it, so all .sent messages are deleted regardless of the requested count. This matches the current test setup (single message) but conflicts with the semantics of delnum and could surprise callers expecting partial deletion. Either remove delnum from the request/response if it’s not needed, or implement deletion of up to delnum messages to align with the protocol.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 09d9931186

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread jwc24/mail_server.py
Comment on lines +67 to +70
form = parse_qs(body.decode("ascii"), keep_blank_values=True)
account = store.register(normalize_wii_id(form["mlid"][0]))
self._reply(
cgi_response(cd=100, passwd=account.password, mlchkid=account.mlchkid)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Refuse to disclose credentials for existing accounts

When mail-serve is bound to a non-loopback host, anyone who knows a Wii address can submit its caller-controlled mlid to account.cgi; register() returns the existing record, and this response exposes its password and check ID without validating hdid or any other device credential. Because the Wii address is necessarily shared with correspondents, this lets a remote caller authenticate as the victim to send, receive, or delete mail; existing registrations need authentication or must not return stored credentials.

Useful? React with 👍 / 👎.

Comment thread jwc24/mail.py
Comment on lines +202 to +208
for path in self.pending(account.wii_id)[:limit]:
payload = path.read_bytes().replace(b"\n", b"\r\n").replace(b"\r\r\n", b"\r\n")
if claimed and used + len(payload) > max_size:
break
if len(payload) > max_size:
continue
path.replace(path.with_suffix(".sent"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep claimed mail available until deletion is acknowledged

If the connection closes while receive.cgi is writing its multipart response, this rename has already removed the message from the *.eml set used by both pending() and later claim() calls. Since the client never gets a chance to acknowledge the download with delete.cgi, subsequent checks report no mail and retries cannot retrieve the stranded *.sent message, causing permanent delivery loss; claims must remain retryable or be rolled back when the response is not completed.

Useful? React with 👍 / 👎.

Comment thread jwc24/mail.py
Comment on lines +182 to +188
digest = hashlib.sha256(message_payload).hexdigest()
for recipient in recipients:
inbox = self.messages / recipient
inbox.mkdir(parents=True, exist_ok=True)
destination = inbox / f"{digest}.eml"
if not destination.exists():
destination.write_bytes(message_payload)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve repeated deliveries with unique queue entries

When the same serialized MIME payload is sent to a recipient more than once before the first copy is consumed, both deliveries produce the same digest-derived path, so the second is silently skipped while send.cgi still reports cd=100. Identical messages are valid—especially for automated/template mail—and a queue should preserve each accepted delivery rather than deduplicating it by content hash; use a unique per-delivery filename.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant