# PRD — contacts (FTI Consulting Intranet Contacts v2.2)

Evidence date: 2026-10-18.

Source: `git@bitbucket.org:overdrive/contacts.git`, `master` at `c1b9e91` (2020-12-11, "Initial commit").

There is no prior PRD in the source repository or in Silicon Ark. Requirements below are recovered from the Classic ASP implementation. Provenance is labelled. Connection passwords are not reproduced.

## Product purpose

**Implemented.** `title.htm` names the product **FTI Consulting Intranet - Contacts - v2.2**. The running behaviour is an intranet for browsing and maintaining journalist contacts, the publications and shows they belong to, and for searching that directory and emailing a selected set.

**Inferred.** The directory supports a media-relations workflow: categories of publications, beats, distribution lists, a shared email disclaimer, and a per-user signature field.

**Unknown / Requires Confirmation:** the business owner, and whether the product is still used.

## Users and roles

| Actor | Evidence | Status |
|---|---|---|
| Person at the login screen | `index.asp` | **Implemented** |
| Admin | `tblUser.lngType = 47`. The user form labels option value 1 as "Admin" and stores 47 | **Implemented** |
| Viewer | `tblUser.lngType = 33`. The user form labels option value 2 as "Viewer" and stores 33 | **Implemented** |
| Any other `lngType` | `topmenu.asp` sends the parent window to `logout.asp` | **Implemented** |
| Deploy / IIS operator | `web.config`, filesystem paths under `c:\inetpub\wwwroot\` | **Inferred**. No operator UI |

Cookie `ckyUserID` stores `UserID|lngType` after a successful login (`index.asp`). `function.asp` splits that value in `getUserID` and `getUserType`.

Both Admin and Viewer see Home, Search, and Admin in `topmenu.asp`. Users, System Settings, and Tools are written into `admin_leftmenu.asp` only when `lngType` is 47. A Viewer sees "My Details" instead, which opens `admin_user_lite.asp`.

`admin.asp` contains a commented check that would have sent non-47 users back to `home.asp`. That check does not run. Lookup pages set `blnAdmin` from the same type test and refuse Save and Delete when it is false. A Viewer can open the lists.

## Systems the product talks to

1. The signed-in user's browser.
2. SQL Server database `Contacts` on host `reddb03`, through ADODB and ODBC Driver 17 (`sql_connect.asp`). A commented connection string names host `reddb02`.
3. SMTP on `localhost` port 25, through CDO (`contact_email.asp`).
4. The IIS filesystem, for `tmp_upload` attachments. The path is `c:\inetpub\wwwroot\contacts\` unless `SERVER_NAME` is `sql-server`, in which case it is `c:\inetpub\wwwroot\bmsa\contacts\`.
5. The recipient's mail system, plus the hard-coded copy address `fdsouthafrica@fd.com` on the sender's summary message.

There is no identity provider, queue, or HTTP API in the tree.

## Major workflows

### WF-1 — Sign in

`index.asp` shows the FTI logo, a username field, a password field, Login, and Reset.

Client script `doLogin`:

- Blocks an empty username or empty password with an alert.
- Sets `hash` to `MD5(password)`, then `combin` to `MD5(hash + SeedVal)`, then clears the password and hash fields.
- Submits the form with `method="get"` to `index.asp`.

Server, when `btnLogin=Login`:

- Reads `tblUser` where `strUserName` equals the submitted username, concatenated into SQL.
- Treats the stored `strPW` as an MD5 digest. Compares `MD5(stored digest + SeedVal)` with `combin`.
- On a match, and when the digest is not empty, sets cookie `ckyUserID` to `UserID|lngType` and redirects to `home.asp`.
- Before the comparison, writes `MD5(username)`, the submitted `combin`, and the server digest to the response.

`Seed` is `md5(time)` only in the branch where `btnLogin` is not `Login`. A failed attempt re-renders the form without assigning a new seed in that branch.

Provenance: **Implemented**.

### WF-2 — Browse contacts by category

`home.asp` requires a non-empty `ckyUserID` cookie, then opens a frameset:

- Top: `topmenu.asp`
- Left: `leftmenu.asp` (categories from `tblCategory`, ordered by `strDescription`)
- Body: `contactlist.asp?CATID=0`

`contactlist.asp`:

- `CATID` missing or not greater than 0: "Select a Category on the left to view the Publications and Contacts".
- `LB=none`: category name and "Click the starting letter or 'All'".
- Otherwise lists publications in that category. `LB=All` returns every publication; any other `LB` filters `strName Like '<letter>%'`.
- Clicking a publication name expands its contacts (`tblContact` left-joined to `tblShow`). An empty publication says "There are no Contacts for this Publication".
- A contact name opens `contactdetail.asp?CID=<id>&lngTab=1` in a new window.

Provenance: **Implemented**.

### WF-3 — Read a contact card

`contactdetail.asp` requires the cookie. `lngTab=1` is the Journalist tab. `lngTab=2` is the Publication tab. Any other value, including the Printer Friendly submit which sets `lngTab=0`, renders a printer layout.

Journalist tab shows title, name, designation, publication, show/section, beats, distribution lists, switchboard and fax from the **publication**, direct phone numbers and emails from the **contact**, notes, PA name/tel/email, Twitter Handle (column `strCarReg`), and "Date Card Updated" (`strDateEdited`). `memInterests` is selected and not written to the page.

Publication tab shows show fields (name, appearing, deadline, host, details, reach) and publication fields (name, LSM, age, URL, postal address, login, physical address, suburb, city, country, footprint, directions, circulation, switchboard, fax, description, category, language, reach, newsdesk email).

If the joins return no row, the page says "Contact details not found" or "Publication details not found".

The printer layout (`lngTab=0`) reads switchboard and fax from `tblContact`, not from `tblPublication`. Those contact columns are no longer written by `admin_contact.asp` (the assignments are commented out).

Provenance: **Implemented**. The printer-versus-screen difference is a discrepancy inside the same file.

### WF-4 — Search, print, and email

`search.asp` opens the same top menu, an almost empty left frame (`search_leftmenu.asp` only prints the heading "Search"), and `rpt_custom.asp`.

`rpt_custom.asp` lists every row in `tblQuery` joined to `tblUser` (not filtered to the current user). Sort options are User/Query/Date, Query/User/Date, or Date/Query/User. Private queries (`blnPrivate = 1`) are styled red; others black. The date shown is `strDateLastRun`.

Actions:

- Create New Query and Edit Query go to `rpt_custom_1.asp`.
- Run Query opens `runQuery.asp` in a new window.
- Delete Query removes `tblQueryFields` and `tblQuery` only when `refUserID` is the current user. Otherwise it alerts that only the named owner can delete it.

For a Viewer (`lngType` 33), `setPermission` disables Edit and Delete in the browser unless the query is not private (`blnPrivate = 0`) or the Viewer owns it. That script does not run the same disable for Admin (`lngType` 47). The server-side delete check still applies to everyone.

`rpt_custom_1.asp` requires a description and at least one selected field. Field choices come from `tblFields`. A query name must be unique for that user. Saving stores `tblQuery` (`refUserID`, `strDescription`, `blnPrivate`) and replaces `tblQueryFields` (`refFieldsID`, `intOrder`, `strSort`, `strCriteria`). Edit Criteria (`editCriteria.asp`) updates order, sort (`ASC`, `DESC`, or empty), and criteria text.

`runQuery.asp` stamps `strDateLastRun` as `YYYY/MM/DD`, builds a contact select from the saved fields, and lists rows with checkboxes. Select All / De-Select All are client-side. Email Selected posts to `contact_email.asp` and alerts if nothing is selected. Printer Friendly posts the selection to `printer_friendly.asp` and alerts if nothing is selected.

Provenance: **Implemented**.

### WF-5 — Send mail to selected contacts

`contact_email.asp` requires the cookie. It loads the current user's `strEmail` (read-only "My Email Address") and `strSignature`. The signature is not placed in the editor and is not appended in `sendmail`.

The form has Cc, Bcc, a selected/removed recipient list, subject, an FCKeditor body (toolbar set `BMSA`), and an attachment list. Add/Remove Files opens `fAttach.asp`.

On Send:

- Appends `tblSystem.strDisclaimer` under a horizontal rule.
- Sends one summary message to the sender, with Bcc `fdsouthafrica@fd.com`, listing Cc, Bcc, and each selected contact name and email.
- Sends one message per selected contact to `strName <strEmail>`, with no Cc or Bcc.
- Sends one further message to the sender with the form's Cc and Bcc.
- Deletes uploaded files named `<UserID>_<filename>` from `tmp_upload`.
- Writes "The emails have been sent, you will receive a copy of each" and a Close link.
- On CDO error, writes `err.description`. `on error resume next` is set inside `sendmail`.

Cancel returns to `runQuery.asp?QID=...`.

Attachments are stored by aspSmartUpload as `tmp_upload/<UserID>_<original filename>` (`fAttach.cls.asp`). `fAttach.asp` and `fAttach.cls.asp` do not repeat the cookie redirect used by the main pages.

Provenance: **Implemented**.

### WF-6 — Maintain lookups, publications, shows, and contacts

`admin.asp` requires the cookie and opens:

- Top: `topmenu.asp`
- Left: `admin_leftmenu.asp`
- Letters frame: `admin_letters.asp` (empty until `letters.js` rewrites it)
- Body: `admin_lookup.asp`, which tells the user to select an item

The left menu links Beat, Category, City, Country, Designation, Distribution List, Language, Suburb, Title, Publication, Show/Section, and Contact. `letters.js` then fills the letters frame with A–Z and All.

Each lookup page lists records, and for an Admin offers Add, Edit, and Delete. A description (or name) must be non-empty in the browser, and must be unique in the table. Delete is blocked, with an alert, when child rows exist. The child check and the alert text are listed in `business-rules.md`. One alert does not match its SQL: suburb delete checks `tblPublication` and the message says "linked to Contacts".

Contact maintenance (`admin_contact.asp`) requires Title, Contact Name, Designation, and Publication in the browser. The same name on the same publication is rejected. Save writes `strDateEdited` as `DD/MM/YYYY` and `strLastUser` as the current username. If the contact has no beats, the browser asks whether to add beats now. Separate actions open `admin_contact_beat.asp` and `admin_contact_distributionlist.asp`. Deleting a contact also deletes its beat links and distribution-list links.

Publication maintenance requires name, category, language, suburb, city, and country. Empty reach becomes 0. A non-numeric reach is rejected in the browser. A URL that starts with `http://` has that prefix removed in the browser before submit. `https://` is left as typed. Save writes `strLastUser` and `strLastEdit` (`DD/MM/YYYY`).

Show maintenance requires a name. Reach follows the same empty-becomes-0 rule.

Provenance: **Implemented**. Uniqueness and delete guards are enforced in the ASP pages, not by a schema file (no schema file is in the repository).

### WF-7 — Users, disclaimer, and the 2011 tool

Admin only (`lngType` 47), or the page redirects to `home.asp`:

- `admin_user.asp` — add, edit, delete users. Required in the browser: username, password, email, user type. Username must be unique. A new or changed password is stored as `md5(plaintext)`. The edit form shows `***************` and keeps the old digest if that placeholder is saved. The last remaining Admin cannot be saved as Viewer; the type is forced back to 47 and an alert is shown. A user cannot delete their own row. Deleting a user also deletes that user's `tblQuery` rows. A counter that would have blocked deletion when queries exist is commented out, so the "could not be deleted since they have Queries" alert does not fire.
- `admin_system.asp` — one textarea, Email Disclaimer, saved to `tblSystem.strDisclaimer`. Inserts a row when none exists.
- `admin_tools.asp` — if the `tblFields` row labelled `Notes` is not already `CAST(tblContact.memNotes as varchar) AS conNotes`, shows "Update database 2011/09/12". That button adds `tblContact.strLastUser`, `tblPublication.strLastUser`, and `tblPublication.strLastEdit` when `INFORMATION_SCHEMA` says they are missing. A second handler updates the Notes field expression when `btnNotesUpdate=Update Notes Field`, but the page does not render a control with that name.

Viewer only (`lngType` 33): `admin_user_lite.asp` edits that user's `strSignature` and nothing else. An Admin who opens it is redirected to `admin_lookup.asp`.

Provenance: **Implemented**.

### WF-8 — Log out

`logout.asp` sets `ckyUserID` to an empty string and shows "You have been logged out successfully" with a link back to `index.asp`. It does not check the cookie first.

Provenance: **Implemented**.

## Functional requirements

### FR-001 — Authenticate with a seeded MD5 password

Requirement: A person signs in with a username and password. The browser hashes the password with MD5, then hashes that digest with a server seed. The server accepts the login only when that value matches `MD5(tblUser.strPW + seed)` and the stored digest is not empty.

Who: any `tblUser` row. Trigger: Login on `index.asp`. Inputs: `strUserName`, `combin`, `SeedVal`. Output: cookie and redirect to `home.asp`, or the login form again. Validation: both fields required in the browser. Error: no alert on a wrong password; the form is shown again, and the three hash lines are written above it.

Evidence: `index.asp`, `md5.js`, `md5.asp`. Implementation: **Implemented**. Confidence: High.

### FR-002 — Keep a session cookie

Requirement: Pages that include the cookie test redirect to `index.asp` when `ckyUserID` is null or empty.

Who: every later request. The cookie is set without an explicit expiry in `index.asp`.

Evidence: the same `if` at the top of `home.asp`, `search.asp`, `admin.asp`, `contactlist.asp`, `contactdetail.asp`, the admin pages, and the query pages. Implementation: **Implemented**. Confidence: High.

`default.asp`, `new.asp`, `db_connect.asp`, `email.asp`, `testemail.asp`, `fAttach.asp`, and `fAttach.cls.asp` do not use that redirect.

### FR-003 — Limit the shell to types 47 and 33

Requirement: After login, `topmenu.asp` shows Home, Search, and Admin for types 47 and 33. Any other type is sent to `logout.asp`.

Evidence: `topmenu.asp`. Implementation: **Implemented**. Confidence: High.

### FR-004 — Browse publications and contacts by category and initial

Requirement: The left menu lists categories. The body lists publications for the selected category, filtered by initial or All, and expands to contacts and show names.

Evidence: `leftmenu.asp`, `contactlist.asp`. Implementation: **Implemented**. Confidence: High.

### FR-005 — Show a journalist card and a publication card

Requirement: A contact opens on the Journalist tab and can switch to the Publication tab. Printer Friendly renders a single layout in the same page.

Evidence: `contactdetail.asp`. Implementation: **Implemented**. Confidence: High.

Interests are stored and edited, and are not shown on the card. Provenance: **Implemented** gap between `admin_contact.asp` and `contactdetail.asp`.

### FR-006 — Maintain reference data

Requirement: An Admin can add, edit, and delete beats, categories, cities, countries, designations, distribution lists, languages, suburbs, titles, publications, shows, and contacts, with the uniqueness and delete rules in `business-rules.md`. A Viewer can open the screens; Save and Delete are ignored on the server when `blnAdmin` is false.

Evidence: `admin_leftmenu.asp` and each `admin_*.asp` page. Implementation: **Implemented**. Confidence: High.

### FR-007 — Link beats and distribution lists to a contact

Requirement: A contact can have many beats and many distribution lists. Adding a beat or list that is already linked alerts and does not insert a second row. Removing a link deletes only the junction row.

Evidence: `admin_contact_beat.asp`, `admin_contact_distributionlist.asp`. Implementation: **Implemented**. Confidence: High.

### FR-008 — Save and run a personal query over the directory

Requirement: A signed-in user can create a named query, mark it private, choose fields from `tblFields`, set sort and criteria, run it, and print the selected contacts. Query names are unique per owner. Only the owner can delete the query on the server.

Evidence: `rpt_custom.asp`, `rpt_custom_1.asp`, `editCriteria.asp`, `runQuery.asp`, `printer_friendly.asp`. Implementation: **Implemented**. Confidence: High.

The list of queries includes every user's queries. Privacy is a colour and a client-side edit/delete lock for Viewers, not a filter that hides the row. Provenance: **Implemented**.

### FR-009 — Email the selected query rows

Requirement: From a query result, the user composes one HTML message and the system sends it to each selected contact, sends the user a copy, and sends a summary that is also Bcc'd to `fdsouthafrica@fd.com`. The system disclaimer is appended. Optional files are attached from `tmp_upload` and deleted after send.

Evidence: `contact_email.asp`, `fAttach.asp`, `fAttach.cls.asp`, `admin_system.asp`. Implementation: **Implemented**. Confidence: High.

### FR-010 — Administer users

Requirement: An Admin manages usernames, passwords, type, email, and signature. A Viewer can edit only their own signature.

Evidence: `admin_user.asp`, `admin_user_lite.asp`. Implementation: **Implemented**. Confidence: High.

### FR-011 — Store one email disclaimer

Requirement: An Admin can save a single disclaimer. Outbound contact mail appends it.

Evidence: `admin_system.asp`, `contact_email.asp`. Implementation: **Implemented**. Confidence: High.

If `tblSystem` has more than one row, the page displays the first row and an update has no `WHERE` clause, so every row would be updated. Provenance: **Implemented** SQL. Whether more than one row exists is **Unknown**.

## Integrations

| System | Purpose | Trigger | Direction | Auth | Failure |
|---|---|---|---|---|---|
| SQL Server `Contacts` on `reddb03` | All product data | Every data page includes `sql_connect.asp` | App to database | SQL login embedded in `sql_connect.asp` | No application error page. A failed `cn.open` surfaces as an ASP error |
| SMTP `localhost:25` | Send contact mail | Send on `contact_email.asp` | App to SMTP | None in the CDO block | `err.description` written to the page; send continues under `on error resume next` |
| IIS filesystem `tmp_upload` | Attachment staging | Add/Remove on `fAttach.cls.asp`; delete after send | App to disk | The Windows identity of the IIS app pool / impersonated user (`web.config` `impersonate="true"`) | No dedicated message if the folder is missing. `fAttach.asp` calls `GetFolder` on that path |

`email.asp` and `testemail.asp` are separate mail probes. They are not linked from the menus. `email.asp` addresses a fixed recipient. `testemail.asp` posts an open from/to/subject/body form to itself.

## Constraints

- Classic ASP, HTML 4.01 framesets, client JavaScript, no front-end framework.
- Almost every form uses `method="get"`, so field values travel on the query string. The mail composer and the attachment form use POST.
- SQL is concatenated from request values. `fixquote` doubles single quotes and turns double quotes into single quotes. It is not used on every parameter (ids are concatenated raw).
- Login comparison depends on a time-based seed and on the password already being stored as MD5.
- Two user types are hard-coded as 47 and 33. The labels Admin and Viewer exist only in the user form.
- Deployment paths are hard-coded Windows paths, switched by `SERVER_NAME = "sql-server"`.
- FCKeditor 2.3.2 is vendored. Its base path default in `fckeditor.js` is `/fckeditor/`; the mail page overrides `BasePath` to `FCKeditor/`.

## Behaviour that is present and not part of the product navigation

| File | What it does | Provenance |
|---|---|---|
| `default.asp` | Writes `Hello` | **Implemented**. Not linked. Whether IIS serves it as the default document is **Unknown** (`web.config` enables default documents and does not list them) |
| `db_connect.asp` | Opens the Contacts database and writes a `tblUser.UserID` or `failed` | **Implemented** probe. Not included by other pages |
| `new.asp` | Opens database `bmsa` on `localhost` and writes a diagnostic line | **Implemented**. Not linked. Credentials not reproduced |
| `email.asp` | Sends one CDO message to a fixed address | **Implemented**. Not linked |
| `testemail.asp` | Open mail form to `localhost:25` | **Implemented**. Not linked. No cookie check |
| `contact_emailBCC.asp` | Mail sender similar to `contact_email.asp`, and it appends an extra Bcc address under a 2009 testing comment | **Implemented**. Not linked |
| `zcontactdetail.asp`, `zcontact_email.asp`, `zcontact_email.asp.old`, `zadmin_publication.asp` | Older copies of the live pages | **Implemented** files. Not linked |
| `printer_friendly.backup.asp`, `printer_friendly2.asp` | Alternate print layouts | **Implemented** files. `runQuery.asp` posts to `printer_friendly.asp` only |
| `contacts.201301.zip`, `contactdetail.zip`, `contacts_title.zip` | Archives in the repo root | Present. Not executed by the application |

## Assumptions

These are inferences, not requirements:

- "Beat" means a journalist's subject area, because it is a many-to-many label on a contact and is filtered with `LIKE` in the query runner.
- "Publication" means a media outlet and "Show/Section" means a programme or section inside it, because the contact card and the publication form use those words.
- Type 33 was meant as a read-only admin, because the server ignores their saves while still letting them open the screens.

## Unknown / Requires Confirmation

- Whether the application is deployed, and the public or intranet URL.
- Whether `reddb03` and the SQL account in `sql_connect.asp` are still valid.
- Whether `fdsouthafrica@fd.com` must remain on the summary message.
- Whether Viewer access to Admin is intentional.
- The full column list and keys of each table. Only columns named in SQL strings are known. No `CREATE TABLE` script is in the repository.
- The rows inside `tblFields` (the query-builder catalogue). The pages read `strLabel` and `strName` at runtime. The only label the code branches on by name is `Notes`, `Beat`, and `Distribution List`.
- Whether `memInterests` was supposed to appear on the contact card.
- Whether the printer layout should use publication switchboard and fax, as the Journalist tab does.
