TinyMUX

Mail System Internals

Hardcode

The TinyMUX @mail system provides persistent, in-game messaging between players. It descends from the PennMUSH 1.50p10 mail system, which was adapted by Kalkin for DarkZone and later integrated into TinyMUX, where it has been heavily modified over the years. The original PennMUSH code traces back to Langston (Andrew Molitor), who wrote the Strstringstrom mail system (often called “branstroms” or “Langston mail”) that became the standard MUSH mail implementation.

Data Structures

Each mail message is represented by a struct mail containing header metadata:

  • to and from – dbref of recipient and sender.
  • number – index into the message body table (not a per-player sequence number).
  • time – timestamp string recording when the message was sent.
  • subject – the subject line.
  • tolist – the original recipient list as entered by the sender.
  • read – a bitmask encoding read/unread status, folder assignment, and flags such as cleared, urgent, mass, safe, forwarded, tagged, and reply.
  • next and prev – pointers forming a circular doubly-linked list per recipient.
  • sqlite_id – the SQLite row ID used for write-through persistence.

The per-player mail chains are indexed by a hash table (mail_htab) keyed on the recipient’s dbref. Each entry points to the head of that player’s circular linked list.

Message Body Storage

Message bodies are stored separately from headers in a global array (mail_list) of MAILBODY entries. Each entry holds the message text, its byte length, and a reference count. When the same message is sent to multiple recipients, all of their struct mail headers point to the same body slot, and the reference count tracks how many headers share it. When a header is deleted, the reference count is decremented; when it reaches zero, the body text is freed. This deduplication keeps memory usage proportional to the number of distinct messages rather than the number of deliveries.

New bodies are assigned the first free slot in the array (a linear scan for a null pointer), and the array grows in increments of 100 slots as needed.

Sending Mail

The @mail <player-list> = <subject> command initiates composition. The message body is built interactively: lines prefixed with - append text, lines prefixed with ~ prepend text, and @mail/edit performs search-and-replace on the draft. The draft is stored in player attributes (A_MAILMSG, A_MAILTO, A_MAILSUB) until @mail/send or -- finalizes delivery. The player’s @signature attribute (A_SIGNATURE) is evaluated and appended to the body at send time.

For each recipient, send_mail() allocates a new struct mail header, links it into the recipient’s circular list, increments the body’s reference count, and fires the recipient’s @amail attribute if set. The @mail/quick shorthand sends a complete message in one command. The @mail/cc and @mail/bcc switches add visible and hidden recipients, respectively; BCC recipients are stripped from the tolist seen by other recipients but retained in the sender’s own copy (prefixed with !).

Folders

Each player has 16 folders numbered 0 through 15. Folder 0 is the inbox where new mail arrives. The current folder is stored in the A_MAILCURF attribute, and folder names are stored in A_MAILFOLDERS as a space-separated list of number:NAME:number records. Most @mail commands operate on the current folder. The @mail/file command moves messages between folders. The folder a message belongs to is encoded in the upper bits of the read flag field using the FolderBit() macro.

Reading and Managing Mail

  • @mail with no arguments lists messages in the current folder.
  • @mail <number> reads a specific message and marks it as read.
  • @mail/next reads the first unread message.
  • @mail/list shows messages with timestamps and status flags: N (new/unread), C (cleared), U (urgent), F (forwarded), + (tagged).
  • @mail/clear marks messages for deletion; @mail/purge performs the actual deletion. Cleared messages are also purged automatically on disconnect.
  • @mail/tag and @mail/untag allow batch operations: tag messages from specific senders, then act on all tagged messages at once.
  • @mail/safe protects a message from automatic expiration.
  • @mail/review <player> lists messages you have sent to a player; @mail/retract deletes unread sent messages.
  • @mail/reply and @mail/replyall start a reply, optionally quoting the original with /quote.

Mail Aliases (@malias)

Mail aliases allow sending to named groups. Each alias is a malias_t structure containing the alias name, description, owner dbref, and an array of up to 100 member dbrefs (MAX_MALIAS_MEMBERSHIP). Aliases owned by GOD are global; others are personal. Administrative switches include @malias/add, @malias/remove, @malias/desc, @malias/chown, @malias/rename, and @malias/delete. Alias data is synchronized to SQLite via sqlite_wt_sync_all_aliases().

Mail Database Persistence

Historically, mail was stored in a flat file (mail.db) using a versioned format. The current format is V6 (introduced 2007-03-13; V5 used Latin-1 encoding and is converted on load). The flat file contains: the version tag, the body table size, all mail headers (one per line: to, from, number, tolist, time, subject, read), an end-of-headers marker, all body entries (number and text), an end-of-bodies marker, and finally the alias table.

Current TinyMUX uses SQLite as the primary persistence layer with write-through semantics. Every mutation—inserting a header, updating flags, deleting a header, writing a body—is mirrored to SQLite in real time via helper functions (sqlite_wt_insert_mail, sqlite_wt_update_mail_flags, sqlite_wt_delete_mail, sqlite_wt_mail_body). On startup, sqlite_load_mail() loads all mail from SQLite; if no SQLite data is found, the flat file is read as a fallback and the data is migrated forward.

Mail Expiration

The mail_expiration configuration parameter sets the number of days after which messages are automatically deleted. A negative value disables expiration entirely. The check_mail_expiration() function iterates over every player’s mailbox, comparing each message’s timestamp against the current time. Messages marked safe (M_SAFE) and players with the No_Mail_Expire flag are skipped. Expiration runs as part of the server’s periodic maintenance cycle.

Administrative Commands

Wizards have access to several administrative mail commands:

  • @mail/stats, @mail/dstats, @mail/fstats – progressively detailed mail statistics (total counts, read/unread breakdown, space usage).
  • @mail/debug sanity – checks the mail database for inconsistencies.
  • @mail/debug fix – attempts to repair problems found by the sanity check.
  • @mail/debug clear=<player> – wipes all mail for a specific player.
  • @mail/nuke – destroys all mail in the entire database.

Softcode Functions

Several functions expose the mail system to softcode:

  • mail() – returns message text, message counts (read/unread/cleared), or a specific player’s message.
  • mailfrom(<msg>) – returns the dbref of the sender.
  • mailsubj(<msg>) – returns the subject line.
  • mailsize(<player>) – returns total mailbox size in bytes.
  • mailreview(<player>) – counts or reads messages sent to a player.
  • mailsend(<recipients>, <subject>, <message>) – sends mail from softcode, subject to normal permission checks and the mail throttle.

Performance Considerations

The per-player circular linked list means that listing or searching a single player’s mail is proportional to that player’s message count, not the total database size. The hash table lookup to find a player’s mail chain is O(1). However, message body allocation scans the body array linearly for a free slot, which can become slow if the array is large and fragmented. The expiration check iterates the entire database (all players, all messages) and should be scheduled during off-peak hours on large games. The SQLite write-through adds per-mutation I/O overhead but eliminates the need for periodic full dumps of the mail database.

Source Files

The primary implementation is in mail.cpp. The struct mail and struct mail_body types along with flag definitions and switch keys are declared in the mail module header. Softcode mail functions are implemented in funceval.cpp.