Mail System Internals
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:
toandfrom– 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.nextandprev– 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
@mailwith no arguments lists messages in the current folder.@mail <number>reads a specific message and marks it as read.@mail/nextreads the first unread message.@mail/listshows messages with timestamps and status flags: N (new/unread), C (cleared), U (urgent), F (forwarded), + (tagged).@mail/clearmarks messages for deletion;@mail/purgeperforms the actual deletion. Cleared messages are also purged automatically on disconnect.@mail/tagand@mail/untagallow batch operations: tag messages from specific senders, then act on all tagged messages at once.@mail/safeprotects a message from automatic expiration.@mail/review <player>lists messages you have sent to a player;@mail/retractdeletes unread sent messages.@mail/replyand@mail/replyallstart 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.