RFC 984

PCMAIL: A distributed mail system for personal computers

๐Ÿ“… published May 1986, from ietf

1. Status of this Document โœจ

RFC 984: PCMAIL: A Distributed Mail System for Personal Computers ๐Ÿ’–

Authors: David D. Clark and Mark L. Lambert ๐ŸŽ€ Affiliation: M. I. T. Laboratory for Computer Science ๐Ÿฆ‹ Date: May 1986 ๐ŸŒธ Obsoletes / Updates: The original header doesn't list any RFCs that this one obsoletes or updates. ๐Ÿ™ˆ Network Working Group ๐Ÿ‘‘

okay so bestie, first things first: this doc is a preliminary discussion of how to design a mail system built on personal computers ๐Ÿ’ปโœจ. It's published so people can discuss it and comment, and it is not a standard ๐Ÿ™…โ€โ™€๏ธ. The proposal may change, so implementing it is not advised ๐Ÿ˜ฌ. Distribution of this memo is unlimited ๐ŸŒˆ.


2. Introduction ๐Ÿงš

Okay, so PCMAIL is a distributed mail system that gives mail service to an arbitrary number of users ๐Ÿ’…. Each user owns one or more personal computers (PCs) ๐Ÿ’ป. The system is split into two halves, and it's giving very clean architecture ๐Ÿ˜ญ๐Ÿ”ฅ.

The first half is a single entity called the "repository" ๐Ÿ›๏ธ. The repository is the storage center for incoming mail ๐Ÿ“ฅ. Mail for a PCMAIL user can come from outside, from the Internet, or from other repository users ๐Ÿ’Œ. The repository also keeps a stable copy of each user's mail state, which the doc calls the user's "global mail state" ๐Ÿ”’. Because of that, the repository is typically a computer with a huge amount of disk storage ๐Ÿ’พ๐Ÿ‘‘.

The second half is one or more "clients" ๐Ÿ“ฑ. Each PCMAIL user can have any number of clients, and they're usually PCs ๐Ÿ–ฅ๏ธ. Clients give the user a friendly way to reach their global mail state over a network ๐Ÿซถ. To make things faster between the repository and a user's clients, each client keeps a local copy of that user's global mail state, called the "local mail state" ๐Ÿš€. But here's the catch: clients are PCs, so they won't always have network access, which means the local and global states won't always match ๐Ÿ™ˆ. So synchronizing them is necessary ๐Ÿ”„โœจ.

Clients talk to the repository using the Distributed Mail System Protocol (DMSP) ๐Ÿฆ‹. The spec for DMSP is in appendix A ๐Ÿ“œ. So the repository is a DMSP server, and it's also a mail end-site and storage facility ๐Ÿ’œ. DMSP gives you a full set of mail operations, like "send a message", "delete a message", "print a message", and so on ๐Ÿ’Œ. It also has special operations for syncing a user's global mail state with their clients' local mail states ๐Ÿ”„. The designers paid really close attention to how DMSP operations act on a user's mail state ๐Ÿง๐Ÿ’…. Every DMSP operation is atomic, meaning it either succeeds completely or fails completely ๐ŸŽฏ. So a client can get abruptly disconnected from the repository without leaving the mail state inconsistent or damaged ๐Ÿ›ก๏ธ๐Ÿ˜ญ.


PCMAIL is built specifically for PCs, so the design is heavily shaped by three PC traits ๐ŸŽ€. First, PCs are relatively inexpensive ๐Ÿ’ธ. That means people might own more than one, maybe one at the office and one at home ๐Ÿข๐Ÿ . Second, PCs are portable ๐Ÿš—. Most can be packed up and moved in the back seat of a car, and a few are truly portable, about the size of a briefcase and battery-powered ๐Ÿงณ๐Ÿ”‹. Third, PCs are resource-poor ๐Ÿฅบ. A typical PC has a small amount of main memory (typically less than one megabyte), and very little mass storage, like floppy-disk drives that can access maybe 360 kilobytes of data ๐Ÿ’พ๐Ÿ™ƒ.

Since PCs are cheap and people may own several, PCMAIL lets users have multiple access points to their mail state ๐Ÿ”‘โœจ. Each user can have several client PCs, and each one can reach the user's mail by talking to the repository over a network ๐ŸŒ. All the client PCs keep local copies of the user's global mail state, and they sync the local and global states using DMSP ๐Ÿ”๐Ÿ’–.

Here's the thing: many PCs will only be connected to a network every once in a while, so they can't always reach the repository ๐Ÿ“ด๐Ÿ˜ฌ. So PCMAIL allows two communication modes between repository and client ๐Ÿค. Interactive mode is for clients that are always connected to the network ๐ŸŸข. Any change to the client's local mail state is immediately made to the repository's global mail state, and any incoming mail is immediately sent from the repository to the client โšก. Batch mode is for clients that only access the repository now and then ๐Ÿ—“๏ธ. Users change the client's local mail state, and those changes get queued up as "actions" ๐Ÿ“. When the client next connects to the repository, the actions get sent over, and the local mail state is synced with the global one ๐Ÿ”„๐ŸŽ‰.

Last but not least, PCMAIL is designed so that using a resource-poor PC as a client doesn't hurt too much ๐Ÿฅน๐Ÿซถ. Mail messages are split into two parts: a "descriptor" and a "body" ๐Ÿ“ฆ. The descriptor is a capsule summary of the message, about 100 bytes long, and its length doesn't depend on how long the actual message is ๐Ÿ’…. The body is the actual message text, including an RFC-822 standard message header ๐Ÿ“„. Even if the client doesn't have room for every message, it can always hold every descriptor, so the user at least gets a summary of their whole mail state ๐Ÿ‘€โœจ. Message bodies can then be pulled down from the repository whenever the client has storage space free ๐Ÿ“ฅ๐ŸŒธ.


The rest of the document is organized like this ๐Ÿ—บ๏ธ. First, there's a detailed description of the repository architecture ๐Ÿ›๏ธ. Next comes DMSP: its operations and why it was designed the way it was ๐Ÿฆ‹. The third section covers client architecture ๐Ÿ“ฑ. After that, there's a walkthrough of a typical DMSP session between the repository and a client ๐ŸŽฌ. The final section talks about the current PCMAIL implementation ๐Ÿ› ๏ธ๐Ÿ’–.

Up next: the repository architecture ๐Ÿ›๏ธ๐Ÿ‘€ more to come, bestie!

3. Repository Architecture โœจ

okay so the repository is basically the big boss machine ๐Ÿ–ฅ๏ธ๐Ÿ’…. it's usually a medium-to-large computer with a huge pile of disk storage. it also has to be a permanent network site, bestie, because client PCs talk to it over the network and they rely on it being available at any time ๐Ÿซถ

the repository has a few jobs. the most important one is that it has to efficiently manage a potentially large number of users and their mail states ๐Ÿ‘‘. mail has to be stored reliably, in a way that makes it easy for multiple clients to reach the global mail state and sync their local mail states with it ๐Ÿ’–. second, the repository has to talk to its clients efficiently ๐Ÿ“ก. the protocol between repository and client must be reliable and must offer operations that (1) allow typical mail manipulation, and (2) support Pcmail's distributed nature by allowing efficient synchronization between local and global mail states. it's giving teamwork ๐Ÿค. third, the repository must be able to process mail from sources outside its own user community (a primary outside source is the Internet ๐ŸŒ). Internet mail arrives with a NIC RFC-822 standard message header, and the recipient names in the message must be properly translated from the RFC-822 namespace into the repository's namespace. this is lowkey the trickiest bit ๐Ÿ™ˆ

3.1. Management of user mail state

Pcmail splits the world into a community of users ๐ŸŒธ. each user is referred to by a user object. a user object has a unique name, a password (which the user's clients use to authenticate themselves to the repository before they manipulate a global mail state ๐Ÿ”), a list of "client objects" describing the clients that belong to the user, and a list of "mailbox objects".

a client object has a unique name and a status. a user has one client object for every client they own, and a client cannot communicate with the repository unless it has a matching client object in a user's client list. so client objects are basically the repository's guest list ๐ŸŽ€ and they're how it identifies valid clients. client objects also help the repository manage local and global mail state synchronization. for every global state change, the repository keeps a list of client objects for those clients that haven't recorded the global change locally ๐Ÿงš

a client's status is either "active" or "inactive". the repository defines inactive clients as clients that haven't connected to it within a set time period (one week in the current Pcmail implementation) ๐Ÿ—“๏ธ. when an inactive client does connect, the repository tells it that it has been "reset" ๐Ÿ”„. the repository resets a client by marking all messages in the user's mail state as having changed since that client last logged in. then, when the client next syncs with the repository, it gets a complete copy of the repository's global mail state. this forced reset is based on the assumption that enough global state changes happen in a week that the client would spend way too much time doing an ordinary local state-global state synchronization ๐Ÿ˜ญ

messages live in mailboxes ๐Ÿ“ฌ. users can have an arbitrary number of mailboxes, and they serve both to store and to categorize messages. since there can be any number of mailboxes, messages can be categorized to an arbitrarily fine degree ๐Ÿ’…. a mailbox object both names a mailbox and describes its contents. mailboxes are identified by a unique name, and their contents are described by three numeric values. the first is the total number of messages in the mailbox. the second is the total number of unseen messages in the mailbox (messages that have never been seen by the user via any client). the third is the next available message unique identifier (UID). this info goes in the mailbox object so clients can get a summary of a mailbox's contents without having to read every message inside it ๐Ÿฆ‹

associated with each mailbox is an arbitrary number of message objects. each message is split into two parts: a "descriptor", which holds a summary of useful info about the message, and a "body", which is the message text itself, including the NIC RFC-822 message header ๐Ÿ“. each message gets a monotonically increasing UID based on the owning mailbox's next available UID. every mailbox has its own set of UIDs which, together with the mailbox name and user name, uniquely identify the message within the repository ๐Ÿ”‘

a descriptor holds the following info: the message UID, the message size in bytes and lines, four "useful" message header fields (the date:, to:, from:, and subject: fields), and two groups of eight flags each. the first group of flags is system defined. these flags mark whether the message has never been seen, whether it has been deleted, whether it is a forwarded message, and whether the message has been expunged. the remaining four flags are reserved for future use ๐Ÿ”ฎ. the second group of flags is user defined. the repository never looks at these flags internally. instead, application programs running on the clients can use them ๐ŸŽจ. descriptors are an efficient way for clients to get message info without wasting time pulling the whole message from the repository โœจ

3.2. Repository-to-RFC-822 name translation

"Address objects" give the repository a way to translate the RFC-822-style mail addresses in Internet messages into repository names ๐ŸŒ. the repository has its own namespace for message identification. any message is uniquely identified by the triple (user-name, mailbox-name, message-UID). any mailbox is uniquely identified by the pair (user-name, mailbox-name). so, to send a message between two repository users, a user addresses the message to (user-name, mailbox-name). the repository delivers the message to the named user and mailbox, and gives it a UID based on the requested mailbox's next available UID ๐Ÿ’Œ

to translate between RFC-822-style mail addresses and repository names, the repository keeps a list of address objects. each address object is an association between an RFC-822-style address and a (user-name, mailbox-name) pair. when mail arrives from the Internet, the repository can use the address object list to translate the recipients into (user-name, mailbox-name) pairs and route the message correctly ๐Ÿš€


4. Communication Between Repository and Client: DMSP ๐Ÿ’ฌ

the Distributed Mail System Protocol (DMSP) is a block-stream protocol that defines and manipulates the objects from the previous section ๐Ÿงฉ. it was designed for Pcmail's single-repository/multiple-client model of the world. on top of typical mail manipulation functions, DMSP also has functions that make it easy to sync global and local mail states ๐Ÿซถ

DMSP is built on top of the Unified Stream Protocol (USP), which is specified in MIT-LCS Technical Memo 255. USP gives you a reliable virtual circuit block-stream connection between two machines ๐Ÿ”Œ. it defines a basic set of data types ("strings", "integers", "booleans", etc.). instances of these data types are grouped in an application-defined order to form USP blocks. each USP block is defined by a numeric "block type", so a USP application can interpret a block's contents based on knowing its type ๐Ÿง . DMSP is a set of operations, and each operation is made up of one or more different USP blocks that get sent between repository and client ๐ŸŽ€

here's how a DMSP session goes: a client starts the session by opening a USP connection to the repository's machine. then the client authenticates both itself and its user to the repository with a "login" operation ๐Ÿ”. if authentication succeeds, the user does an arbitrary number of DMSP operations and then ends the session with a "logout" operation (at which point the repository closes the connection) ๐Ÿ‘‹

because DMSP can manipulate a pair of mail states (local and global) at once, it is extremely important that all DMSP operations are atomic ๐Ÿ˜ญ. a failure in any DMSP operation must leave both states in a consistent, known state. for this reason, a DMSP operation counts as failed unless the operation initiator receives an explicit acknowledgement. this acknowledgement comes in one of two basic forms, based on the two broad categories all DMSP operations fall into ๐Ÿ—‚๏ธ. first, an operation can be a request to modify mail state. in that case the repository acknowledges with either an "ok" or a "failure" (and if it's a failure, the reason is returned too). second, an operation can be a request for information. then the repository acknowledges by handing the client the information ๐Ÿ“ฆ. things like "delete a message" are in the first category, and things like "send a list of mailboxes" are in the second ๐Ÿ’…

here's a list of DMSP operations by object type, their block types and arguments, and their expected acknowledgement block types ๐Ÿ“‹. each DMSP block has a different number. the first digit of each block type tells you which object is being manipulated: operations numbered 5xx are general, operations numbered 6xx are user operations, operations numbered 7xx are client operations, operations numbered 8xx are mailbox and address operations, and operations numbered 11xx are message operations ๐ŸŒˆ

blocks marked "=>" flow from client to repository, and blocks marked "<=" flow from repository to client. if more than one block can be sent, the choices are separated by "or" ("|") characters ๐Ÿฆ‹

for clarity, each block type is written in a human-understandable form. the block number is followed by an operation name, and this name is never sent as part of a USP block ๐Ÿ™ˆ. block arguments are identified by name and type, and enclosed in square brackets. "Record" data types are described by a list of "field-name:field-type" pairs in square brackets. "Choice" data types are described by a list of "tag:tag-name" pairs in square brackets. USP data types get abbreviated like this:

Primitive data types:

  • string: str
  • cardinal: card
  • long-cardinal: Lcard
  • integer: int
  • long-integer: Lint
  • boolean: bool

Compound data types:

  • sequence: SEQ
  • array: AR
  • record: REC
  • choice: CH

4.1. General operations

the first group of DMSP operations does general stuff that doesn't work on any one particular class of object ๐ŸŒŸ. DMSP has six general operations, which provide the following services:

if either a client or the repository thinks the other is malfunctioning, it can send an "abort-request" ๐Ÿšจ. an abort-request is never acknowledged. after the request is sent, the sender immediately closes the USP connection and hands control back to its application ๐Ÿ˜ค

=> 503 (abort-request) [why:str]

DMSP has a lowkey limited remote debugging situation ๐Ÿ” through the "start-debug" and "end-debug" ops. When a client sends start-debug, the repo switches on its own version of remote debugging โœจ. What remote debugging actually is depends on the implementation, and the current repo just writes debug info to a special file ๐Ÿ“„. end-debug turns it back off ๐Ÿ’….

=> 504 (start-debug) []

<= 500 (ok) [] |
   501 (failure) [why:str]

or

=> 505 (end-debug) []

<= 500 (ok) []

Next up is version skew, which is a real vibe killer ๐Ÿ˜ญ. To stop clients and the repo from running different DMSP versions, there's a "send-version" op. The client sends its DMSP version number, and it only succeeds if that matches the repo's DMSP version number. No match, no success ๐Ÿ™…โ€โ™€๏ธ.

=> 506 (send-version) [version-number:card]

<= 500 (ok) [] |
   501 (failure) [why:str]

DMSP also lets clients send an arbitrary text message to the repo ๐Ÿ’ฌ. The "log-message" op takes a string of any length, the repo accepts it, and what happens to it after that is up to the implementation ๐Ÿคทโ€โ™€๏ธ.

=> 507 log-message[message:str]

<= 500 (ok) [] |
   501 (failure) [why:str]

And finally, users can send mail to each other with the "send-message" op ๐Ÿ“ฌ. The message has to come with an Internet-style header as defined by NIC RFC-822. The repo takes the message and delivers it to the mailboxes listed in the "to:", "cc:", and "bcc:" fields of that header. If any of those mailboxes live outside the repo's user community, the repo is responsible for handing the message to a local SMTP server ๐Ÿซถ.

An OK block only gets sent back if the entire message was successfully transmitted ๐Ÿ™Œ. For messages going out to the Internet, send-message counts as a success once the message has made it to the local SMTP server.

=> 508 (send-message) [message:SEQ[str]]

<= 500 (ok) [] |
   501 (failure) [why:str]

4.2. User operations

Next up are the user object ops ๐Ÿ‘‘. The big ones are "login" and "logout". A client has to do a login before it can touch a user's mail state ๐Ÿ”. A DMSP login block has five items: (1) the user's name, (2) the user's password, (3) the name of the client doing the login, (4) a flag telling the repo to create a client object for that client if one doesn't already exist, and (5) a flag that's TRUE if the client wants "batch mode" or FALSE if it wants "interactive" mode. That flag lets the repo tune its internal settings for whichever mode is in play ๐ŸŽ›๏ธ.

The repo can answer with an OK block (auth worked โœ…), a FAILURE block (auth didn't work โŒ), or a FORCE-RESET block. FORCE-RESET shows up when the repo has marked the client as "inactive", which happens to clients that haven't connected in over a week ๐Ÿ˜ณ. It tells the client to wipe its local mail state and pull down a full copy of the repo's mail state. The reasoning is that so many changes would have piled up in a week that a normal sync would be super inefficient ๐Ÿฅฒ.

=> 600 (login) [user:str, password:str, client:str,
                create-client-object?:bool,
                batch-mode-flag:bool]

<= 500 (ok) [] |
   501 (failure) [why:str] |
   705 (force-client-reset) []

When a client is done talking to the repo, it sends a logout ๐Ÿ‘‹. That gives the repo a chance to do any cleanup it needs before it closes the USP connection.

=> 601 (logout) []

<= 500 (ok) []

DMSP also has "add-user" and "remove-user" ops, which let system admins add new users to the repo and remove them remotely ๐Ÿง‘โ€๐Ÿ’ป. These ops are privileged, so the repo checks who's asking and authenticates them before doing either one ๐Ÿ”’. Both need the name of the user being added or removed, and add-user also needs a default password to give the new user.

=> 602 (add-user) [user:str, password:str]

<= 500 (ok) [] |
   501 (failure) [why:str]

=> 603 (remove-user) [user:str]

<= 500 (ok) [] |
   501 (failure) [why:str]

Users can change their own password with the "set-password" op ๐Ÿ”‘. It works pretty much like the UNIX change-password command, and it takes the user's current password and the new one they want. If the current password you send matches the user's actual current password, it gets swapped for the new one ๐Ÿ’….

=> 604 (set-password) [old-password:str,
                       new-password:str]

<= 500 (ok) [] |
   501 (failure) [why:str]

4.3. Client operations

DMSP has four ops for messing with client objects ๐Ÿงš. The first one, "list-clients", tells the repo to send the user's client list back to the client that asked. The list is a series of (name, status) pairs ๐Ÿ“‹.

=> 700 (list-clients) []

<= 701 (client-list) [client-list:SEQ[
                      REC[name:str, status:card]]]

"add-client" lets a user add a client object to their client list โž•. Login can also do this through the "create-this-client?" flag, but add-client is handy when you want to make a bunch of new client objects while already logged in through an existing client ๐Ÿ™Œ. It needs the name of the client to add.

=> 702 (add-client) [client:str]

<= 500 (ok) [] |
   501 (failure) [why:str]

The most common way this one fails is trying to add a client that already exists ๐Ÿ™ƒ.

"remove-client" takes an existing client object off a user's client list ๐Ÿ—‘๏ธ. The client being removed can even be the one making the request ๐Ÿ‘€. It needs the name of the client to remove.

=> 703 (remove-client) [client:str]

<= 500 (ok) [] |
   501 (failure) [why:str]

The most common failure here is trying to remove a client that doesn't exist ๐Ÿ’€. That's the usual failure for any DMSP op that works on a named object.

The last client op is "reset-client" ๐Ÿ”„. It makes the repo mark every message in the user's mail state as changed since the client last logged in. The next time the client syncs, it'll get a full copy of the repo's global mail state. This is useful for two reasons. First, a client's local mail state can easily get lost or damaged, especially if it lives on a floppy disk ๐Ÿ’พ. Second, if the repo has marked a client as inactive, reset-client is a quick way to resync when there are so many differences between local and global mail state that a normal sync would take forever ๐Ÿข.

=> 704 (reset-client) [client:str]

<= 500 (ok) [] |
   501 (failure) [why:str]

4.4. Mailbox operations

DMSP supports five ops that work on mailbox objects ๐Ÿ“ซ. First, "list-mailboxes" has the repo send each mailbox's info to the client that asked. That info is the mailbox name, the total message count, the unseen message count, and the "next available UID" ๐Ÿ”ข. It's super useful for syncing local and global mail state, since a client can compare the user's global mailbox list against its own local one ๐Ÿ‘ฏ. It also gives you a quick summary of each mailbox without actually sending the contents โœจ.

=> 800 (list-mailboxes) []

<= 801 (mailbox-list) [mailbox-list:SEQ[
                       REC[mailbox:str,
                           next-UID:Lcard,
                           num-msgs:card,
                           num-unseen-msgs:card]]]

"add-mailbox" has the repo create a new mailbox and attach it to the user's mailbox list ๐Ÿ“ฅ. An address object gets made automatically to bind the (user-name, mailbox-name) pair to an RFC-822-style address, and it goes into the repo's address object list. That's what lets mail from the Internet find its way to the new mailbox ๐ŸŒˆ.

=> 802 (add-mailbox) [mailbox:str]

<= 500 (ok) [] |
   501 (failure) [why:str]

"Remove-mailbox" takes a mailbox off the user's list ๐Ÿงน. Every message inside it gets deleted and permanently removed from the system ๐Ÿ˜ฑ. Any address objects tying that mailbox name to RFC-822-style addresses get removed too.

=> 803 (remove-mailbox) [mailbox:str]

<= 500 (ok) [] |
   501 (failure) [why:str]

DMSP also has an "expunge-mailbox" op ๐Ÿ—‘๏ธ. Messages can be marked "deleted" and "undeleted" whenever you want, but the deletions only become permanent once you run expunge-mailbox. That makes the repo go through a named mailbox and remove any messages marked "deleted" from the system ๐Ÿ’ฅ.

=> 808 expunge-mailbox[mailbox:str]

<= 500 (ok) [] |
   501 (failure) [why:str]

Last but not least, "reset-mailbox" makes the repo mark every message in a named mailbox as changed since the current client last logged in ๐Ÿ”. When the client syncs next, it gets a full copy of that mailbox's mail state. It's basically a more specific version of reset-client, which pulls down the user's entire global mail state. Its main job is fixing mailboxes whose contents got accidentally destroyed locally ๐Ÿš‘.

=> 809 (reset-mailbox) [mailbox:str]

<= 500 (ok) [] |
   501 (failure) [why:str]

4.5. Address operations

DMSP has three ops that let users manipulate address objects ๐Ÿ“ฎ. First up, "list-address" returns a list of address objects tied to a particular (user-name, mailbox-name) pair ๐Ÿ“‡.

=> 804 (list-addresses) [mailbox:str]

<= 501 (failure) [why:str] |
   805 (address-list) [address-list:SEQ[str]]

"add-address" adds a new address object that links a (user-name, mailbox-name) pair to a given RFC-822-style mailbox address ๐ŸŽ€.

=> 806 (add-address) [mailbox:str,
                      RFC-822-mail-address:str]

okay so the remove-address response is the last bit of that section ๐Ÿ’… โœจ

         <= 500 (ok) [] |
            501 (failure) [why:str]

4. Communication Between Repository and Client: DMSP โœจ

4.6. Message operations ๐Ÿฆ‹

Okay bestie, messages are the stuff clients touch the most, so DMSP gives them special message ops to make syncing fast ๐Ÿ”ฅ and a bunch of normal message-manipulation moves too ๐Ÿ’–. Heads up: in this part, "message" and "descriptor" mean the exact same thing, so don't get confused when I switch between them ๐Ÿ™ˆ

To check a message's flag values, a client uses the get-descriptor-flags operation ๐ŸŽ€. The repository sends back an array of booleans. Eight of those are system defined, and eight are user defined, which the repository just ignores ๐Ÿงš

         => 1100 (get-descriptor-flags) [mailbox:str,
                                         uid:Lcard]

         <= 1101 (descriptor-flags) [flags:SEQ[bool]] |
            501 (failure) [why:str]

Next up is get-descriptors ๐Ÿ’œ. A user asks for a whole series of descriptors by giving a pair of message UIDs, one for the lower bound and one for the upper bound. UIDs are defined to keep going up monotonically, so that pair is enough to pin down the exact series, no cap ๐Ÿ‘‘. The repository replies with a sequence of "choices". Each element is either a descriptor, and then the choice is tagged as a descriptor, or it's a notice that the requested message got expunged after the client's last connection to the repository ๐Ÿ’€

         => 1102 (get-descriptors) [mailbox:str,
                                    low-UID:Lcard,
                                    high-UID:Lcard]

         <= 501 (failure) [why:str] |
            1103 (descriptor-list) [descriptor-list:SEQ[ CH[
                                    expunged[uid:Lcard]
                                    descriptor[REC[UID:Lcard,
                                                   flags:SEQ[bool],
                                                   from-field:str,
                                                   to-field:str,
                                                   date-field:str,
                                                   subject-field:str,
                                                   num-bytes:Lcard,
                                                   num-lines:Lcard]
                                                ]]]]

Then there's get-changed-descriptors ๐ŸŒธ, which is meant for state synchronization. Whenever a descriptor changes state, like getting deleted for example, the repository keeps track of which clients haven't recorded that change locally yet ๐Ÿ‘€. Get-changed-descriptors makes the repository send over a given number of descriptors that changed since the client's last synchronization. The list starts with the earliest-changed descriptor, so it's giving chronological vibes โญ

         => 1105 (get-changed-descriptors) [mailbox:str,
                                            max-to-send:card]

         <= 501 (failure) why:str] |
            1103 (descriptor-list) [descriptor-list:SEQ[
                  CH[
                    expunged[uid:Lcard]
                    descriptor[REC[UID:Lcard,
                                   flags:SEQ[bool],
                                   from-field:str,
                                   to-field:str,
                                   date-field:str,
                                   subject-field:str,
                                   num-bytes:Lcard,
                                   num-lines:Lcard]
                                ]]]]

Once the user has looked at the changed descriptors, they'll want to tell the repository that the current client has recorded the change locally ๐Ÿซถ. That's what reset-changed-descriptors is for ๐Ÿ“. It marks a given number of changed descriptors as "seen by current client", starting with the changed descriptor that has the lowest UID โœจ

         => 1106 (reset-changed-descriptors) [
                  mailbox:str,
                  number-to-reset:card]

         <= 500 (ok) [] |
            501 (failure) [why:str]

Message bodies come over from repository to user through get-message-text ๐Ÿ“ฉ. Splitting "get-descriptors" and "get-message-text" into separate ops is lowkey genius, because clients with tiny amounts of disk storage can grab a small message summary (through "get-descriptors" or "get-changed-descriptors") without pulling the entire message ๐Ÿงšโ€โ™€๏ธ ๐Ÿ’–

         => 1107 (get-message-text)[mailbox:str,
                                    uid:Lcard]

         <= 501 (failure) [why:str] |
            1110 (message) [message:SEQ[str]]

Sometimes a message is just too big for some clients to store locally ๐Ÿ˜ญ. Users can still look at the contents with print-message ๐Ÿ–จ๏ธ. This one has the repository send a copy of the message to a named printer. The printer name only has to mean something to that specific repository implementation, and DMSP just sends the name as a way of identifying it ๐Ÿฆ‹

         => 1108 (print-message) [mailbox:str,
                                  uid:Lcard,
                                  printer-name:str]

         <= 500 (ok) [] |
            501 (failure) [why:str]

The user can set and clear any of the 16 descriptor flags using set-flag ๐Ÿšฉ. The flag gets set or cleared depending on the operation arguments, and that's literally all there is to it ๐Ÿ’…

         => 1109 (set-flag) [mailbox:str,
                             uid:Lcard,
                             flag-number:card,
                             flag-setting:bool]

         <= 500 (ok) [] |
            501 (failure) [why:str]

And last but definitely not least, copying a message into another mailbox is done with copy-message ๐ŸŒˆโœจ

         => 1111 (copy-message) [source-mailbox:str,
                                 target-mailbox:str,
                                 source-uid:Lcard]

         <= 500 (ok) [] |
            501 (failure) [why:str]

5. Client Architecture ๐ŸŽ€

Okay bestie, clients are usually PCs, so Pcmail's architecture has to vibe with three PC traits ๐Ÿ’ป. One, PCs are cheap, so a user might own more than one ๐Ÿ’ธ. Two, they're portable, so they aren't expected to stay plugged into a network ๐Ÿ”Œ. Three, they're resource-poor, so they can't be expected to store a ton of state locally ๐Ÿฅฒ. The subsections below show how Pcmail handles all three ๐Ÿ’….

5.1. Multiple clients ๐Ÿ‘ฏโ€โ™€๏ธ

Pcmail users can own more than one PC, and that's the whole reason Pcmail uses a multiple client model โœจ. A user might have a PC client at home, another at the office, and maybe even a third portable PC ๐Ÿงณ. Each client keeps its own separate copy of the user's mail state, which is why Pcmail is distributed in nature ๐ŸŒ. Having separate clients means users can get to their mail state from several different locations, and that's honestly iconic ๐Ÿฆ‹.

5.2. Synchronization ๐Ÿ”„

PCs are pretty portable, so a PC being always connected to a network is relatively unlikely ๐Ÿ™ˆ. That's another reason each client keeps a local copy of the user's mail state ๐Ÿ’œ. The user can then change the local mail state while they're not connected to the network (or the repository). Obviously this brings up the problem of keeping the local and global mail states in sync, and ngl that's the tricky part ๐Ÿ˜ญ.

The repository is always in a position to receive global mail state updates, whether that's incoming mail or changes from other clients ๐Ÿ“ฌ. A client that isn't always connected can't receive those global changes right away ๐Ÿ˜ฌ. On top of that, the client's user can make their own changes to the local mail state ๐ŸŽ€.

Pcmail's architecture allows efficient synchronization between each client's local mail state and the repository's global mail state ๐Ÿซถ. Every client is identified in the repository by a client object attached to the user, and that object is the basis for synchronization between local and global mail states ๐Ÿ”ฅ. Some of the less common state changes are adding and deleting user mailboxes, and adding and deleting address objects ๐Ÿ—‚๏ธ. These changes get synced through DMSP list operations, which let clients compare their local versions of the mailbox and address object lists with the repository's global version and make any appropriate changes ๐Ÿ’–. Most possible changes to a user's mail state come in the form of changed descriptors ๐Ÿ“‹. Since most users have a large number of messages, and message states change pretty often, message synchronization needs special attention ๐Ÿ‘‘.

An existing descriptor can change in one of two ways. First, one of its sixteen flags can change (this covers reading an unseen message, deleting a message, and expunging a message) โœ…. Second, a descriptor changes when incoming mail arrives or when a message gets copied from one mailbox to another ๐Ÿ“ฅ. Both of those result in a new message being added to a mailbox ๐Ÿ“.

In both of those cases, the repository and every client that hasn't previously noted a change need to sync up ๐ŸŒˆ. To keep track of which clients have noticed a global mail state change and updated their local states, each descriptor has a (potentially empty) "update list" of client objects attached to it ๐Ÿงš. That list identifies the clients that haven't recorded a change to that descriptor's state yet ๐Ÿ‘€.

When a client connects to the repository, it runs the DMSP "get-changed-descriptors" operation ๐Ÿ’ซ. This makes the repository return a list of all descriptor objects that have the requesting client on their update list ๐Ÿ“œ. As the client receives the changed descriptors, it can store them locally, which updates the local mail state ๐Ÿ’พ. Once a changed descriptor has been recorded, the client uses the DMSP "reset-descriptors" operation to remove itself from that descriptor's update list ๐Ÿงน. After that, the descriptor won't be sent to the client again unless (1) it's explicitly requested, or (2) it changes again ๐Ÿ™…โ€โ™€๏ธ.

So a client can run through its user's mailboxes, grab all the changed descriptors, fold them into the local mail state, and mark each change as recorded ๐ŸŽ‰.

5.3. Batch operation versus interactive operation ๐Ÿ“ฆ

Because PCs are so portable, they may not always be connected to the repository ๐Ÿงณ. Since each client keeps a local mail state, Pcmail users can manipulate that local state while they aren't connected ๐Ÿ’…. This is called "batch" operation, because all changes get recorded by the client and then made to the repository's global state as one batch, the next time the client connects ๐Ÿ“ฎ. Interactive operation happens when a client is always connected to the repository ๐Ÿ“ถ. In interactive mode, changes to the local mail state get propagated to the global state right away through DMSP operations โšก.

In batch mode, the client connects to the repository and sends over all the changes the user made to the local mail state ๐Ÿ“ค. The repository updates its global mail state to match ๐Ÿ—„๏ธ. Once all the changes have been processed, the client starts synchronization so it can pull in newly arrived mail and the mail state changes made by other clients ๐ŸŒธ.

In interactive mode, local changes go straight to the repository, so the first part of batch-type operation is gone ๐Ÿ™Œ. The synchronization process changes too, because interactive clients can periodically poll the repository for a list of changes and sync a small amount at a time ๐Ÿ•ฐ๏ธ.

5.4. Message summaries ๐Ÿ“

PCs are assumed to have very little disk storage, so a given client may never have enough room for a complete local copy of the user's global mail state ๐Ÿฅบ. That means Pcmail's client architecture has to let users get a clear picture of their mail state without having all their messages on hand ๐Ÿง .

Descriptors do exactly that, giving message information without taking up big chunks of storage ๐Ÿ’–. Each descriptor contains a summary of information about one message โœจ. That information includes the message UID, its length in bytes and lines, and its status (encoded in the eight system-defined and eight user-defined flags) ๐Ÿท๏ธ. It also includes portions of the message's RFC-822 header (the "to:", "from:", "subject:" and "date:" fields) ๐Ÿ’Œ. All of this fits into a small data structure (around 100 bytes) whose length doesn't depend on the size of the message it describes ๐Ÿฆ‹.

Any client should be able to store a complete list of message descriptors without much trouble ๐ŸŽ€. That lets a user see their whole mail state without having every message stored locally ๐Ÿ‘‘. Short messages can live on the client alongside the descriptors ๐Ÿ“ฑ. Long messages can either be printed through the DMSP "print-message" operation, or pulled down on their own with the "fetch-message-text" operation ๐Ÿ–จ๏ธ๐Ÿ’….


6. Typical Client-Repository Interaction โœจ

okay so this example walks through a typical chat session between the repository and a client ๐Ÿ’– The client is one of three belonging to user "Fred" ๐Ÿงš. It's called "office-client", and since Fred uses it regularly to check his mail, it's marked as "active" ๐ŸŒธ. Fred has two mailboxes. "main" is where all his current mail lives, and "archive" is where messages of lasting importance get kept ๐ŸŽ€. The example runs through a simple synchronization and then a bunch of typical mail state manipulations ๐Ÿ’…. Usually the sync is handled by an application program that connects to the repository, logs in, syncs, and logs out ๐Ÿซถ.

For the example, all DMSP operations are shown in a user-readable format ๐Ÿ™ˆ. In real life they'd be sent as a stream of USP blocks, each one a block-type number followed by a stream of bytes for the block's arguments ๐Ÿค“. The block name and its number are both included just for convenience โœจ.

To get at his global mail state, the client software has to authenticate Fred to the repository. It does this with the DMSP login operation ๐Ÿ”:

600 (login) ["fred", "ajyr63ywg", "office-client",
             FALSE, FALSE]

This tells the repository that Fred is logging in via "office-client", and that "office-client" is identified by an existing client object attached to Fred's user object ๐Ÿ‘‘. The second login argument is an encrypted version of Fred's password ๐Ÿ™ˆ. The final argument tells the repository that Fred's client isn't running in batch mode but in interactive mode ๐Ÿ’œ.

Fred's authentication checks out, so the repository logs him in and acknowledges the login request with an OK block โœ…

Now that Fred is logged in, he wants to bring "office-client"'s local mail state up to date ๐Ÿฆ‹. To do that, the client program asks for an up-to-date list of mailboxes:

800 (list-mailboxes) []

The repository replies with:

801 (mailbox-list) [["main", 10, 1, 253],
                    ["archive", 100, 0, 101]]

This tells the client there are two mailboxes, "main" and "archive" ๐Ÿ“ฌ. "Main" has 10 messages, one of which is unseen, and the next incoming message will be assigned a UID of 253 ๐ŸŒˆ. "Archive", on the other hand, has 100 messages, none of which are unseen, and the next message sent to "archive" will get the UID 101 ๐Ÿ’€. There are no new mailboxes in the list. (If there were, the client program would create them. On the other hand, if some mailboxes in the client's local list weren't in the repository's list, the program would assume another client deleted them and delete them locally as well ๐Ÿซ .)

To sync, the client only needs to look at each mailbox's contents to see if (1) any new mail has arrived, or (2) Fred changed any messages on one of his other two clients after "office-client"'s last connection to the repository ๐Ÿ”.

The client asks for any changed descriptors with the "get-changed-descriptors" operation. It requests at most ten of them, since storage is super tight on "office-client" ๐Ÿฅบ

1105 (get-changed-descriptors) ["main", 10]

The repository responds with:

1103 (descriptor-list) [[descriptor[
                         6,
                         [T T F F F F F F F F F F F F F F],
                         "Fred@borax",
                         "Joe@fab",
                         "Wed, 23 Jan 86 11:11 EST",
                         "tomorrow's meeting",
                         621,
                         10]]
                        [descriptor[
                         10,
                         [F T F F F F F F F F F F F F F F],
                         "Fred",
                         "Freds-secretary",
                         "Fri, 25 Jan 86 11:11 EST",
                         "Monthly progress report",
                         13211,
                         350]]
                       ]

The first descriptor is a message Fred deleted on another client yesterday, so "office-client" marks its local copy as deleted ๐Ÿ—‘๏ธ. The second one is a brand new message, so "office-client" adds the descriptor to its local list โœจ. Both changes are now recorded locally, which means the descriptors can be reset:

1106 (reset-descriptors) ["main", 2]

The repository clears each descriptor's update vector bit that corresponds to "office-client"'s client object ๐Ÿงน. "Main" is now synced! ๐ŸŽ‰ The client then turns to Fred's "archive" mailbox and asks for the first ten changed descriptors:

1105 (get-changed-descriptors) ["archive", 10]

The repository responds with:

1103 (descriptor-list) []

An empty list tells "office-client" that nothing in "archive" has changed since its last sync, so no new sync is needed for that mailbox ๐Ÿ˜Œ

Fred's client is now ready to pull down the new message so Fred can read it ๐Ÿ“–. The message is 320 lines long, and there might not be enough storage on "office-client" to hold it. The client tries anyway:

1107 (fetch-message-text) ["main", 10]

The repository starts transmitting the message:

1110 (message) ["From: Fred's-secretary",
                "To: Fred",
                "Subject: Monthly progress report",
                "Date: Fri, 25 Jan 86 11:11 EST",
                "",
                "Dear Fred,",
                "Here is this month's progress report",
                ...
                ]

Halfway through, "office-client" runs out of disk space ๐Ÿ˜ฑ Because all DMSP operations are defined to be atomic, the part of the message that already arrived is thrown away locally and the operation fails ๐Ÿ’”. "Office-client" lets Fred know the message can't be pulled over because there isn't enough disk space. The sync is now finished, so Fred's client logs out:

601 (logout) []

The repository does any housecleaning it needs, acknowledges the logout request, and closes the USP connection ๐Ÿ‘‹


7. A Current Pcmail Implementation ๐Ÿ’…

Next up is a quick look at a real Pcmail implementation that serves a small community of users ๐ŸŒŸ. The Pcmail repository runs under UNIX on a DEC VAX-750 that's connected to the Internet ๐Ÿ–ฅ๏ธ. The clients are IBM PCs, XTs, and ATs ๐Ÿ’พ. The network software that talks to the repository only allows "batch-mode" operation. Users make local state changes, which get queued until the client connects to the repository. Then the changes are performed and the local and global states are synchronized, and the client disconnects ๐Ÿ”„.

Users access and modify their local mail state through a user interface program ๐ŸชŸ. The program uses windows and a full-screen mode, and gives users a rich variety of commands for working on individual messages as well as mailboxes ๐Ÿฆ„. The interface lets you use any text editor to compose messages, and it adds some features of its own to make RFC-822-style header composition easier โœ๏ธ.

Synchronization and processing of queued changes is handled by a separate program that the user runs whenever they want ๐Ÿƒโ€โ™€๏ธ. It takes any actions queued while using the user interface and converts them into DMSP operations. All queued changes are made before any synchronization happens ๐Ÿ™Œ.

Limiting the client to batch mode was a deliberate choice, and there are two big reasons ๐Ÿ’œ. First, the implementation is geared toward portable computers as clients. These machines are rarely connected to the network, so interactive mode isn't really needed ๐Ÿ’ป. Clients that are constantly connected run a bit less efficiently than they could, since users have to make changes locally and then run the action-processing/synchronization program, instead of just making changes interactively ๐Ÿข.

The second big reason is that batch mode allows a very simple locking scheme to prevent problems from concurrent state updates ๐Ÿ”’. A user may have several clients, so the repository could easily end up in all kinds of inconsistent states as different clients try to change its global mail state at the same time ๐Ÿ˜ตโ€๐Ÿ’ซ. To prevent that, a user's mail state is locked as soon as a client connects to the repository, and the lock is released when that client disconnects ๐Ÿ”“. This scheme is simple to implement, but it makes interactive-mode operation really cumbersome. If a user stays constantly connected (that is, in interactive mode), the repository would be unavailable to the user's other clients for an unacceptable length of time ๐Ÿ™…โ€โ™€๏ธ.


8. Conclusions ๐ŸŽ€

Pcmail is now used by a small community of people at the MIT Laboratory for Computer Science ๐Ÿซ. The repository design works well, giving a fairly efficient way to store and maintain mail state for several users ๐Ÿ“ฆ. Members of another research group at LCS are working on a replicated, scaleable version of the repository, built to support a very large community of users with high availability ๐ŸŒ. That repository also uses DMSP and has successfully communicated with clients that use the current repository implementation ๐Ÿค. So DMSP seems to be useable over several flavors of repository design ๐Ÿฆ.

The clients are PCs, and they're unfortunately very limited in resources, which makes local mail state manipulation hard at times ๐Ÿ˜ฉ. Synchronization is also pretty time consuming because the PCs perform slowly ๐ŸŒ. Batch-mode client operation is super useful for portable computers that spend a big chunk of their time unplugged and away from a network ๐Ÿ”‹. It's somewhat less useful for most of the clients, though. Those are always connected to the network and could make really good use of "interactive-mode" state manipulation โœจ

I. DMSP Protocol Specification โœจ

Okay bestie, this is the final part ๐ŸŽ€ Here's the full DMSP block list, sorted by operation type. Same as before: "=>" means a block flowing from the client to the repository, and "<=" means a block flowing from the repository to the client ๐Ÿ’–

      General operations:

         => or <= 503 (abort-request) [why:str]
         (no acknowledgement)

         => 504 (start-debug) []
         <= 500 (ok) [] |
            501 (failure) [why:str]

         => 505 (end-debug) []
         <= 500 (ok) []

         => 506 (send-version) [version:card]
         <= 500 (ok) [] |
            501 (failure) [why:str]

         => 507 (log-message) [message:str]
         <= 500 (ok) [] |
            501 (failure) [why:str]

         => 508 (send-message) [message:seq[str]]
         <= 500 (ok) [] |
            501 (failure) [why:str]

      User operations:

         => 600 (login) [name:str, password:str,
                         client:str, create-client-object?:bool
                         batch-mode-flag:bool]
         <= 500 (ok) [] |
            501 (failure) [why:str] |
            705 (force-client-reset) []

         => 601 (logout) []
         <= 500 (ok) []

         => 602 (add-user) [name:str, password:str]
         <= 500 (ok) [] |
            501 (failure) [why:str]

         => 603 (remove-user) [user:str]
         <= 500 (ok) [] |
            501 (failure) [why:str]

         => 604 (set-password) [old:str, new:str]
         <= 500 (ok) [] |
            501 (failure) [why:str]

      Client operations:

         => 700 (list-clients) []
         <= 701 (client-list) [client-list:seq[
                               rec[name:str], status:card]]

         => 702 (add-client) [client:str]
         <= 500 (ok) [] |
            501 (failure) [why:str]

         => 703 (remove-client) [client:str]
         <= 500 (ok) [] |
            501 (failure) [why:str]

         => 704 (reset-client) [client:str]
         <= 500 (ok) [] |
            501 (failure) [why:str]

      Mailbox operations:

         => 800 (list-mailboxes) []
         <= 801 (mailbox-list) [mailbox-list:seq[
                                rec[mailbox:str,
                                    next-uid:lcard,
                                    num-msgs:card,
                                    num-unseen-msgs:card]]]

         => 802 (add-mailbox) [mailbox:str]
         <= 500 (ok) [] |
            501 (failure) [why:str]

         => 803 (remove-mailbox) [mailbox:str]
         <= 500 (ok) [] |
            501 (failure) [why:str]

         => 808 (expunge-mailbox) [mailbox:str]
         <= 500 (ok) [] |
            501 (failure) [why:str]

         => 809 (reset-mailbox) [mailbox:str]
         <= 500 (ok) [] |
            501 (failure) [why:str]

      Address operations:

         => 804 (list-addresses) [mailbox:str]
         <= 501 (failure) [why:str] |
            805 (address-list) [address-list:seq[str]]

         => 806 (add-address) [mailbox:str, address:str]
         <= 500 (ok) [] |
            501 (failure) [why:str]

         => 807 (remove-address) [mailbox:str, address:str]
         <= 500 (ok) [] |
            501 (failure) [why:str]

      Message operations:

         => 1100 (get-descriptor-flags) [mailbox:str, uid:lcard]
         <= 1101 (descriptor-flags) [flags:seq[bool]] |
            501 (failure) [why:str]

         => 1102 (get-descriptors) [mailbox:str,
                                    low-uid:lcard,
                                    high-uid:lcard]
         <= 501 (failure) [why:str] |
            1103 (descriptor-list) [descriptor-list:seq[
                   ch[
                     expunged[uid:lcard],
                     descriptor[rec[uid:lcard,
                                    flags:seq[bool],
                                    from-field:str,
                                    to-field:str,
                                    date-field:str,
                                    subject-field:str,
                                    nun-bytes:lcard,
                                    num-lines:lcard]
                          ]]]]

         => 1105 (get-changed-descriptors) [mailbox:str,
                                            max-to-send:card]
         <= 501 (failure) [why:str] |
            1103 (descriptor-list) [descriptor-list:seq[
                   ch[
                     expunged[uid:lcard],
                     descriptor[rec[uid:lcard,
                                    flags:seq[bool],
                                    from-field:str,
                                    to-field:str,
                                    date-field:str,
                                    subject-field:str,
                                    num-bytes:lcard,
                                    num-lines:lcard]
                         ]]]]

         => 1106 (reset-changed-descriptors) [
                         mailbox:str,
                         start-uid:lcard,
                         end-uid:lcard]
         <= 500 (ok) [] |
            501 (failure) [why:str]

         => 1107 (get-message-text) [mailbox:str,
                                     uid:lcard]
         <= 501 (failure) [why:str] |
            1110 (message) [message:seq[str]]

         => 1108 (print-message) [mailbox:str,
                                  uid:lcard,
                                  printer-name:str]
         <= 500 (ok) [] |
            501 (failure) [why:str]

         => 1109 (set-flag) [mailbox:str,
                             uid:lcard,
                             flag-number:card,
                             flag-setting:bool]
         <= 500 (ok) [] |
            501 (failure) [why:str]

         => 1111 copy-message[source-mailbox:str,
                              target-mailbox:str,
                              source-uid:lcard]
         <= 500 (ok) [] |
            501 (failure) [why:str]

DMSP block types by number ๐Ÿ”ข

      General block types

         ok                        500
         failure                   501
         abort-request             503
         start-debug               504
         end-debug                 505
         send-version              506
         log-message               507
         send-message              508

      User operation block types

         login                     600
         logout                    601
         add-user                  602
         remove-user               603
         set-password              604

      Client operation block types

         list-clients              700
         client-list               701
         add-clien                 702
         remove-client             703
         reset-client              704
         force-client-reset        705

      Mailbox operation block types

         list-mailboxes            800
         mailbox-list              801
         add-mailbox               802
         remove-mailbox            803
         expunge-mailbox           808
         reset-mailbox             809

      Address operation block types

         list-addresses            804
         address-list              805
         add-address               806
         remove-address            807

      Message operation block types

         get-descriptor-flags      1100
         descriptor-flags          1101
         get-descriptors           1102
         descriptor-list           1103
         get-changed-descriptors   1105
         reset-changed-descriptors 1106
         get-message-text          1107
         print-message             1108
         set-flag                  1109
         message                   1110
         copy-message              1111

That's the whole thing, bestie! ๐ŸŒˆ๐Ÿ‘‘ You made it through all of RFC 984 ๐Ÿ’…๐Ÿฆ‹

๐Ÿ“ฆ where this page comes from