# Echomail (FidoNet-style networks)

Echomail = old-school message networks shared across BBSes. ANetBBS's
echomail subsystem has grown into several cooperating pieces: networks
and areas, netmail (private point-to-point mail), the AreaFix
subscription robot, TIC file-echo distribution, a QWK offline reader,
and — on the one install designated as the network hub — a separate
Hub Management admin panel. This doc covers all of them.

## Two transports

- **BinkP** — the FidoNet TCP/IP standard. ANetBBS speaks BinkP
  natively (poll out, accept inbound).
- **QWK** — packet-based, popular for Dove-Net. ANetBBS can
  download / upload QWK packets over HTTP/FTP.

### Known BinkP limitations (deliberate scope, not bugs)

- **FREQ (file request, `M_GET`) is not implemented.** ANetBBS never
  sends one, and doesn't answer one from a peer either. File
  distribution here is TIC-push only (see the TIC section below) —
  legacy FREQ is rarely used over BinkP today and isn't planned.
- **Outbound bundles are always sent uncompressed.** Inbound zipped
  arcmail bundles ARE accepted and unpacked fine (a peer compressing
  their side works), but ANetBBS itself never produces a compressed
  `.pkt` bundle on send — spec-legal (compression is optional), just
  an asymmetry worth knowing about if you're comparing outbound
  bandwidth against a peer that does compress.

### Outbound spool directory — for external programs (v1.0.34+)

Everything above is DB-queue-driven: outbound netmail/echomail lives
as `EchomailMessage`/`NetmailMessage` rows, outbound file distribution
as `HatchQueue` rows (see [doc 7 — File Areas](07-file-areas.md)).
Neither gives an *external* program — a door that writes its own real
FTS-0001 `.pkt` netmail packets straight to disk, the way a
traditional FTN mailer's flat-file outbound spool works — anywhere to
hand ANetBBS a file to transmit.

`resolve_outbound_dir()` in `anetbbs/echomail/binkp.py` fixes that:
any file dropped in a peer's spool directory is sent to that peer
exactly as found (no packet-building, no manifest) on the next BinkP
session, in either direction — us dialing out (`poller.py`) or a peer
dialing in to us (`binkp_server.py`). Successfully-sent files move
into a `sent/` subfolder rather than being deleted.

The spool is **per peer**, not one shared directory the way inbound
is — a loose file has no address of its own to route by, and more
than one network/node can be configured at once:

```
<DATA_DIR>/binkp/outbound/<sanitized peer FTN address>/
```

Override the root with the `BINKP_OUTBOUND_DIR` env var (mirrors
`BINKP_INBOUND_DIR`). You don't need to compute the sanitized path by
hand — the resolved directory for each configured peer is shown
directly on **Admin → Echomail Networks** (an "Outbound Spool" column)
and on each BinkP node's own detail page under **Hub Management**.

This is what ANetCHESS's own `-ibbs out` (or any similar external
netmail-writing program) should be pointed at as its per-node
outbound directory.

## ANotherNetwork — bundled by default

Every fresh ANetBBS install seeds a real, working echomail/QWK network
called **ANotherNetwork** (hub `bbs.a-net.fyi`, Zone 1200) — the same
way Synchronet installs ship pre-configured for Dove-Net. This isn't a
placeholder; it's the project's own live hub, wired up out of the box
so a new sysop has somewhere to actually go once they're ready to join
a network, instead of starting from a totally blank echomail config.

Seeded automatically (in `_create_default_data()` in
`anetbbs/web_app.py`):

- **Two network rows**, both `is_active=False` until the sysop fills
  in a node number/password and flips them on:
  - `ANotherNetwork` — BinkP transport, host `bbs.a-net.fyi:24554`,
    hub address `1200:1/1`, `ftn_domain='anet'`.
  - `ANotherNetwork (QWK)` — QWK/FTP transport, same host, hub ID
    `ANET`.
- **26 message echo areas**, tag prefix `ANN.`, shared between both
  network rows (same tag exists under both the BinkP and QWK entries,
  since it's one hub distributing to both transports). All start
  `is_active=True` / `is_subscribed=False` — visible as "available"
  once the sysop activates a network, but nothing polls until
  subscribed. Areas span 8 categories:
  - **General** (5): `ANN.GENERAL`, `ANN.INTRO`, `ANN.HUMOR`,
    `ANN.DEBATE`, `ANN.FEEDBACK`
  - **Technology** (4): `ANN.TECH`, `ANN.LINUX`, `ANN.SECURITY`,
    `ANN.NET`
  - **BBS Scene** (5): `ANN.BBS`, `ANN.BBSDEV`, `ANN.ANETBBS`,
    `ANN.DOORS`, `ANN.ANSIART`
  - **Retro** (3): `ANN.RETRO`, `ANN.GAMES`, `ANN.MUSIC`
  - **Hobby** (4): `ANN.MOVIES`, `ANN.BOOKS`, `ANN.FOOD`, `ANN.SPORTS`
  - **Trading** (1): `ANN.ADS`
  - **Data** (1): `ANN.DATA`
  - **SysOp** (2, sysop-only): `ANN.SYSOP`, `ANN.SYSOP.HELP`
  - **Test** (1): `ANN.TEST`
- **9 file echo areas**, tag prefix `ANN.FILES.`, attached only to the
  BinkP network row (TIC is BinkP-native, so unlike message areas
  these aren't duplicated across the QWK entry — `FileArea.tag` also
  has a DB-level unique constraint, the TIC processor's lookup key):
  `ANN.FILES.NODELIST` (Weekly Nodelists — flagged
  `is_nodelist_source=True`, see Hub Management below),
  `ANN.FILES.INFOPACK` (Infopacks), `ANN.FILES.BBSSOFT` (BBS Software),
  `ANN.FILES.DOORS` (Door Games & Utilities), `ANN.FILES.EBOOKS`,
  `ANN.FILES.LINUX`, `ANN.FILES.RETRO`, `ANN.FILES.ANSIART`, and
  `ANN.FILES.TEST`. All start inactive-subscription same as the
  message areas.

To actually join: apply for a node number at `bbs.a-net.fyi`, then
fill in your assigned FTN address / packet ID and password on the
seeded network row(s) at **Admin → Echomail Networks**, subscribe to
whichever areas you want, and activate the network.

## Setting up a network

**Admin → Echomail Networks** (`/admin/echomail/`):

1. **Add Network** — pick BinkP or QWK, give it a name, set your FTN
   address (e.g. `1:142/0`), hub/uplink address, and password. BinkP
   networks also take a CRAM-MD5 toggle (recommended — Synchronet
   requires it), an optional TLS toggle, and an optional **default
   netmail recipient** (a local username that catches netmail
   addressed to a name the BBS doesn't recognize).
2. **Add Areas** to the network — each `EchoArea` has a tag like
   `FIDO.GENERAL`, a name, optional moderator address.
3. **Subscribe** to areas you want to read.

### Domain suffix override (`ftn_domain`)

Per FSP-1028, a qualified FTN address (`addr@domain`) needs a domain
suffix of 8 characters or fewer. If you leave this blank, the poller
derives one automatically from the network's display **name**
(truncated/lowercased) — which produces an awkward result for a long
name. `"ANotherNetwork"` truncates to `"anothern"`, for example (this
is exactly why the bundled ANotherNetwork network row ships with
`ftn_domain='anet'` set explicitly). Set the **"Domain suffix
override"** field on the network's edit page
(`templates/echomail/admin/network_form.html`) to override this with
something shorter and cleaner while keeping the full display name
intact everywhere else. Blank/`NULL` falls back to the old
name-derived behavior, so existing networks aren't affected until you
choose to set it.

## Real-name posting policy

Some FTN networks/areas have a real-world policy requiring the
poster's actual name rather than a handle/alias (e.g. network
coordination areas). ANetBBS supports this per-area (for echomail) and
per-network (for netmail, which has no area concept of its own):

- **User side** — Profile has a **Real Name** field (optional unless
  an area/network you post in requires it) and a **"Post
  echomail/netmail as"** preference: handle (default) or real name.
  The preference only controls the *default* — an area/network that
  requires a real name always uses it regardless of this setting.
- **Admin side** — the area edit form has a **"Require Real Name"**
  checkbox; the network edit form has a matching **"Require real name
  for netmail"** checkbox. When set, a user with no real name in their
  Profile is hard-blocked from posting there, with a message pointing
  them to Profile — never silently allowed to post under a handle
  anyway.

Enforced identically across every local compose surface — web
echomail compose, web QWK-style netmail compose, web netmail.py's true
FTN netmail compose, web telegram.py, the terminal compose flow (both
`_compose_echomail` and the reply-from-read path), and the PETSCII
compose flow — via a single shared `resolve_post_name()` helper in
`anetbbs/features/access_control.py` rather than one-off logic per
surface.

## `[ANSI]` subject auto-tag

A message whose body contains real ANSI escape sequences (not just
CP437 box-drawing characters alone, which show up in ordinary quoted
replies/taglines and don't warrant a warning) automatically gets an
`[ANSI] ` prefix added to its subject, so a reader on a non-ANSI-capable
client — or just skimming a message list — knows to expect colored/
box-drawing content before opening it. Idempotent (never double-tags an
already-tagged subject). Applied via a shared `maybe_tag_ansi_subject()`
helper in `anetbbs/models.py`, wired into both the 3 inbound import
paths (BinkP poll, BinkP inbound session, QWK REP upload) and every
local compose surface — so it fires the same way whether the ANSI
content came from a peer or was typed locally.

## Deleting a message

Admins get a "Delete" button on the message-read view
(`POST /echomail/<area_id>/<message_id>/delete`) to permanently remove a
single echomail message — e.g. one that was composed/received in error
and shouldn't have gone out. Admin-only: echomail is shared FTN network
content, not a personal post, so (unlike board-post deletion) authorship
alone doesn't grant delete rights. Removes the local row and cleans up
every table that references it (the outbound `BinkPHoldQueue`, per-user
`EchomailReadStatus`, and `QWKNodeLastSent`'s delivery pointer) — but can
only stop copies still pending delivery; it cannot recall a copy already
sent to a peer.

## AreaFix — subscription requests from peers

AreaFix is the standard FTN robot that lets a downstream peer manage
their own area subscriptions by sending a netmail instead of the hub
sysop doing it by hand. It lives in `anetbbs/echomail/areafix.py`.

A peer addresses a netmail to **`AreaFix`** (also recognized:
`Area Fix`, `AreaMgr`) at your hub address, with the **AreaFix
password in the Subject line** (per FTS-0024 — NOT in the body) and
one command per line in the body:

```
+FIDO.GENERAL       subscribe to an area
-FIDO.GENERAL       unsubscribe from an area
+ALL / -ALL         subscribe / unsubscribe to everything available
%LIST (or %QUERY)   reply with current subscriptions
%HELP               reply with a help/command summary
%RESCAN [AREA.TAG]  hub-only: re-queue every existing message in AREA.TAG
                    (or every subscribed area if no tag given) for this
                    node's hold queue -- a real backlog-catchup request,
                    not a subscription change
%COMPRESS GZIP      accepted, no-op (bundles are always sent uncompressed)
%PASSWORD newpass   hub-only: change your own AreaFix/BinkP password --
                    only reachable already authenticated with the OLD
                    password, so this can't bootstrap a password on a
                    node that doesn't have one set yet
```

The bot replies with a netmail confirming what changed. If the
requester's FTN address matches a `BinkPNode` this BBS hosts as a hub
peer (see Hub Management below), the change is scoped to that peer's
own `EchoAreaNode` subscription rows instead of the global
`EchoArea.is_subscribed` flag — so on a hub install, each downstream
node keeps an independent subscription list. `%RESCAN`/`%PASSWORD` are
hub-side-only (no-op on an upstream leaf request — there's no per-us
hold queue for a network we poll rather than host, and no separate
password to rotate from a leaf's perspective).

Hub-side subscriptions never include `is_sysop_only` areas (e.g.
InterBBS Wall/Last-Callers-sync/casino-score-sync machine-to-machine
channels) — those can't be reached via `+TAG`/`+ALL` from any
downstream node, same content gate enforced everywhere else.

**Configuring the password**: each network has a `binkp_password`
(the BinkP session secret) and an optional separate
**`areafix_password`** field — many real-world hubs use a different
password for AreaFix requests than for the session handshake itself.
Leave it blank to fall back to the network's `binkp_password`. Set
both on the network's edit page.

The sysop side of outbound AreaFix requests (subscribe/unsubscribe
buttons on **Admin → Echomail Networks → Manage Areas**) queues the
same kind of netmail automatically — see `send_areafix_request()` in
`areafix.py`. There's also a free-form box on each network's admin
page (**custom AreaFix command**, `POST /admin/echomail/networks/
<id>/custom_areafix`) to send any raw command line straight to the
uplink — `+TAG`, `%RESCAN <tag>`, `%COMPRESS GZIP`, anything the
robot accepts above — with an optional `robot` form field
(`robot=FileFix`) to target the FileFix robot instead. There are also
two dedicated buttons that trigger the same re-toss without needing the
raw command form: a per-area **Rescan** button on **Manage Areas**
(`POST /admin/echomail/areas/<id>/rescan`) and a per-network
**Rescan All** button (`POST /admin/echomail/networks/<id>/rescan_all`).
All AreaFix (and FileFix) traffic is logged at
**Admin → Echomail Networks → AreaFix Log** (`/admin/echomail/areafix_log`),
with a Bot column and an AreaFix/FileFix filter.

## Other admin utilities

A few working routes worth knowing about that don't have their own
nav entry:

- **FTN AKAs** (`/admin/echomail/akas`) — self-service alternate-
  address management for the logged-in admin's own account, directly
  feeding the `UserAka` matching netmail routing already relies on.
- **Test connection** (`POST /admin/echomail/networks/<id>/test`) —
  live diagnostic button: BinkP does a real TCP connect, QWK does a
  real FTP login + `LIST`, no mail is sent either way.
- **Bad Areas** (`/admin/echomail/bad_areas`) — review queue for echo
  tags arriving from peers that don't match any known/subscribed
  area; promote to a real subscription or dismiss.
- **Unclaimed Netmail** (`/admin/echomail/unclaimed_netmail`) —
  companion to Bad Areas for netmail instead of echomail: inbound
  netmail whose To: name/address never resolved to a local user
  (`NetmailMessage.to_user_id IS NULL`) is otherwise invisible in
  every inbox view (all of them filter by the logged-in user's own
  identity) — this page lists it and lets an admin manually assign it
  to the right account, firing the same notification the automatic
  resolver would have.
- **Bulk import areas** (`POST /admin/echomail/areas/bulk_import`) —
  paste or upload a Fidonet-style AREAS.BBS/backbone file to
  bulk-create areas instead of adding them one at a time.
- **QWK quick-add** (`POST /admin/echomail/networks/<id>/qwk_quick_add`)
  — bulk-add QWK areas from pasted `<conf_num> <name>` lines.

## Netmail — private FTN mail

Netmail is FTN's point-to-point private mail, as distinct from
**echomail** (public, broadcast to every peer subscribed to an area)
and from local PMs (BBS-internal only, never leaves this system).
Model: `NetmailMessage` in `anetbbs/models.py`; user-facing blueprint:
`anetbbs/web/netmail.py`.

Each message has a single `from_address`/`to_address` FTN pair (not an
area), FTN attribute flags (`is_private`, `is_crash`, `is_hold`, etc.),
and the standard kludge lines (`MSGID`/`REPLY`/`INTL`/`FMPT`/`TOPT`/
`CHRS`/...) preserved verbatim as a JSON array so they round-trip
correctly if the message needs forwarding.

Routes (all under `/netmail/`): inbox, sent items, read, compose, and
reply. A user's inbox matches netmail addressed to any of their
`UserAka` FTN addresses, or to their username directly for first-time
arrivals not yet linked to an account. AreaFix replies and hub-related
netmail (see above) flow through this same table.

Note: netmail currently only has a **web** UI — there's no terminal
menu action for it yet (unlike echomail, which has both `echo`/
`echo_post` terminal actions and a web view).

## Per-board ↔ per-area

Boards and echomail areas are intentionally separate concepts:

- **Boards** — local discussion, threaded, with reactions.
- **Echomail areas** — networked message bases, FidoNet-style with
  origin/path lines, propagated to peers.

You can post to either independently.

## Per-user QWK offline reader

Anyone can grab their unread echomail as a `.QWK` packet, read it
offline in MultiMail / BlueWave / OLX, then upload a `.REP` packet
with replies.

URL: `/qwk/`. Download builds a fresh packet on demand. Upload
extracts MESSAGES.DAT and posts each reply as outbound echomail in
the matching area.

## Polling

The `anetbbs-web` process spins up a background poller that:

1. Reads `EchomailNetwork` rows.
2. For BinkP: polls each uplink at interval, exchanges packets.
3. For QWK: fetches new `.QWK` URLs on schedule.

Poll status/history is visible at **Admin → Echomail Networks →
Poll Logs** (`/admin/echomail/logs`) — filterable by network, showing
start/completion time, sent/received counts, and any error.

### Self-referential poll skip (informational, not an error)

If a network's poller looks like it's configured to dial the BBS's own
install instead of a genuine remote peer — for BinkP, `our_address`
equals `hub_address`; for QWK, `qwk_host` equals this BBS's own public
host — the poller detects this and safely skips the dial-out instead of
failing. This is expected and harmless on the hub's own install: the
seeded "point at the hub" network row needs to exist and be configured
exactly that way for *other* sysops to reach the hub, it's just never
meaningful for the hub to dial itself. Areas stay visible either way —
this only stops the pointless connection attempt, nothing else.

As of v1.0b2.41, the check happens first in `_do_poll()`
(`anetbbs/echomail/poller.py`), before any `EchomailPollLog` row gets
created — a self-referential network produces **no poll log entry at
all**, just a single `logger.debug(...)` line (invisible at the
default INFO log level) for anyone specifically chasing this down.
Earlier versions logged the skip as a `status='skipped'` poll-log row
every time the loop re-checked the network (once a minute), which
flooded the Poll Logs page with dozens of identical entries within
about 20 minutes of uptime and drowned out real poll activity. See
`_self_referential_reason()` in `anetbbs/echomail/poller.py`.

### Don't poll too aggressively

`poll_interval_minutes` defaults to 60 and is hard-floored at 5 (set
below that and it's silently clamped back up) — but the floor existing
doesn't mean polling every 5 minutes is a good idea for a real remote
uplink. Some hubs rate-limit or temporarily block a peer that connects
too frequently, which then makes the *original* problem (a genuinely
failed poll) harder to recover from, not easier. A standard interval
(30–60 minutes) is normal for most FTN/QWK links; only go tighter if
the hub's own docs say it's fine. Separately: a poll that fails no
longer retries on the very next scheduler tick (fixed in v1.0b2.114) —
it correctly waits out the configured interval before trying again,
same as a successful poll would.

## Hub Management — running ANetBBS as a network hub

If this install is the designated hub for a network (`.env`:
`REGISTRY_MODE_ENABLED=true`), a separate admin panel becomes
available at **Admin → Echomail Networks → Hub Management**
(`/admin/echomail/hub/`, `anetbbs/web/hub_admin.py`). On any install
where `REGISTRY_MODE_ENABLED` isn't set, every route in this blueprint
404s — the button on the Echomail dashboard is always visible, but
clicking it does nothing on a non-hub install. This is deliberate:
earlier, every install exposed this same UI, so a sysop applying for a
node number from their own terminal would have the request land in
their own local queue instead of the real hub's.

This panel is the **hub operator's** admin surface, separate from the
regular per-network echomail admin every install has:

- **BinkP node management** (`/admin/echomail/hub/binkp/`) — add,
  edit, and delete downstream `BinkPNode` peers (FTN address, session
  password, sysop/system/location metadata used for nodelist
  generation).
- **QWK node management** (`/admin/echomail/hub/qwk/`) — add, edit,
  and delete downstream `QWKNode` peers (packet ID, download
  password). Each node's detail page
  (`/admin/echomail/hub/qwk/<node_id>`, `qwk_node_detail()` in
  `hub_admin.py`) manages which echo areas that node actually
  receives — subscription is per-node, not global, tracked via
  `QWKNodeLastSent` rows (each carries the `conf_number` used in the
  QWK packet format). Areas can be subscribed/unsubscribed one at a
  time, or in bulk with the **"Subscribe to All"** button
  (`POST /admin/echomail/hub/qwk/<node_id>/subscribe-all`,
  `qwk_subscribe_all()`). This isn't a single click — the sysop first
  checks one or more networks from a checkbox list; submitting with
  none checked flashes an error and subscribes nothing. Requiring an
  explicit pick avoids sweeping in every QWK network on the install at
  once, which is what this used to do — a real problem for a sysop
  running more than one QWK network who wanted to add just one node's
  home network. The area query is further scoped two ways: to active
  areas on QWK-transport networks only (never BinkP-only networks),
  and, when the node belongs to a hub identity
  (`node.hub_identity_id`), to networks under that same hub identity —
  a node has no business receiving areas from a different hub
  identity's network. Networks with no hub identity set (a
  pre-migration edge case) are treated as identity-agnostic and always
  included.
- **Hold queue** (`/admin/echomail/hub/holdqueue`) — outbound BinkP
  items queued per node, filterable by status (pending/sent/failed).
- **QWK node-request approval queue**
  (`/admin/echomail/hub/qwk/requests`) — sysops can apply for a QWK
  node number from their own BBS's terminal; those applications land
  here for the hub operator to approve or deny. QWK-terminal-wizard-
  specific — see the **Public join form** section below for the
  newer, broader review queue that covers both transports.
- **Generation & Distribution** panel, four tabs:
  - **Nodelist** — shows the weekly nodelist-generation schedule (a
    `ScheduledEvent` seeded automatically when `REGISTRY_MODE_ENABLED`
    is on) plus last-run status/output, and a **Generate Now** button
    that runs the same handler on demand. Generated nodelists publish
    straight into the `ANN.FILES.NODELIST` file area so peers pick
    them up like any other file-echo item.
  - **QWK Packets** — lists registered QWK nodes with a **Preview**
    button per node. Preview builds and downloads a real packet on
    demand for testing, but deliberately does **not** call
    `mark_qwk_sent()`, so it never consumes the node's actual unsent
    message queue — the node's real next download is unaffected.
  - **TIC / File Distribution** — a status dashboard (pending/failed
    counts) for the outbound TIC hatch queue — see [doc 7 — File
    areas](07-file-areas.md#hatched-files-tic) for how files get
    queued in the first place; no manual step needed, files uploaded
    to any `ANN.FILES.*`-style network-attached area queue themselves
    automatically. Links to two dedicated log pages for drilling into
    the raw queue rows: **TIC Out Log** (`/admin/hatch-log` — every
    outbound hatch item, pending/sent/failed, which peer, retry count,
    last error, filterable by status) and **TIC In Log**
    (`/admin/tic-log` — inbound TIC manifests received).
  - **Join Form** — enable/configure the public application page
    described below.

### Public join form (v1.0b2.43)

A public web page at `/join/` (`anetbbs/web/network_join.py`, blueprint
`network_join_bp`) lets anyone — no login required — apply to join this
hub's echomail network, instead of the sysop having to walk them
through it by email or a terminal wizard. It's gated by two independent
checks: `REGISTRY_MODE_ENABLED` (same as the rest of Hub Management)
**and** the sysop explicitly enabling it on the Join Form tab; if
either is off, `/join/` 404s just like the rest of this blueprint.

**Applicant flow**: read the rules text and (optionally) download the
full infopack zip, check a box confirming the rules were read (the
form won't submit without it), then fill in an application — name,
location, BBS name, BBS software, OS, telnet address, website URL,
email are always asked for, followed by an optional BinkP section (FTN
address + crash-or-hold) and an optional QWK section (packet ID).
Leave either transport section blank if it doesn't apply, but at least
one must be filled in. There are no password fields anywhere on the
form — real session/download credentials are always hub-generated at
approval time, never applicant-supplied, the same security rule the
QWK node-request wizard already follows. The endpoint is fully
unauthenticated, so it's rate-limited per IP: a 30-second floor between
submissions and a 10/hour cap.

**Setting it up**, on the **Join Form** tab:

1. Enable the checkbox, optionally set a **Network Name** (shown on
   the public page) and **Intro Text** (a blurb shown above the
   rules).
2. Upload a single infopack zip — a real-world bundle of whatever the
   sysop wants applicants to see: rules/info text, a readme, a node
   list, ANSI art, machine-readable area lists, etc.
3. The system automatically picks the **largest `.txt` member** in the
   zip as "the rules text" and displays it inline on the public page
   (not just offered as a download). If the auto-pick guesses wrong, a
   dropdown lists every other `.txt` member found in the zip so the
   sysop can manually re-pick one, without re-uploading.
4. The whole zip is also offered as a public download,
   `GET /join/infopack.zip`, no login required — same pattern as the
   existing public file-download route.

**Review queue**: applications land on a new **Join Requests** page
(`/admin/echomail/hub/join/requests`) — separate from, and in addition
to, the QWK-only node-request queue above; that older queue is
specific to the terminal AreaFix/QWK wizard, this one is for the
public web form and covers both transports. Every admin gets an
in-app notification when a new application arrives (see
[doc 2 — Sysop daily ops](02-sysop-daily-ops.md) for the notification
system itself).

**Approving a request** creates a `BinkPNode` and/or `QWKNode` — zero,
one, or both, driven entirely by which section(s) the applicant filled
in, not by an admin choice. Each new node gets its own independently
hub-generated random password. If either transport's address/packet ID
collides with an existing node, the whole approval is rejected — the
BBS won't partially create one side and silently drop the other — so
the sysop can resolve the conflict and retry. If outbound SMTP is
configured (see [doc 2 — Sysop daily ops](02-sysop-daily-ops.md)), the
applicant is emailed their new credentials automatically; otherwise the
sysop is told to relay them manually. Denials can include a reason,
also emailed if SMTP is working.

**Credentials email delivery is tracked per request** (v1.0.35+) — the
Join Requests list and each request's detail page show whether the
last send attempt actually succeeded (an SMTP-relay-side bounce,
greylist, or quota issue on the recipient's end can silently fail a
send with no other trace). A **Resend** button next to that status
re-sends the exact same already-generated credentials — it never
regenerates a new password, since that would invalidate whatever the
applicant may have already received or configured their own mailer
with from an earlier, partially-successful attempt.

This feature isn't hardcoded to ANotherNetwork — it's meant to be
usable by any sysop running their own ANetBBS hub for their own
network. ANotherNetwork is simply the first real network to use it.

If your install is the hub for *more than one* real network at once —
own zone:net, own QWK hub ID, separate downstream node pools — see the
**Hub Identities** admin page (`/admin/echomail/hub/identities/`),
which gives each network its own `/join/<slug>/` and node forms grow
an identity picker once a second one exists. Almost no install needs
this; single-network installs never see it. Full writeup: the
[[Sysop Guide]] wiki page, "Running more than one hub identity."
