---- File layout Each message is a separate file. A folder is a directory; messages within a folder are named with names of the form %d and are hardlinks to the message file. A folder also contains a file named ".lock", to which locks are applied when locking the whole folder. Operations that apply to only a particular message take a shared lock on the folder and then an appropriate lock on the message; operations that mung the folder as a whole take an exclusive lock on the folder. Deleted messages may or may not be kept around with names like #%d or ,%d or some such; see mmrm below. Dropping a message into a folder "by hand" (eg, restoring a pseudo-deleted message with mv) is not a good idea; there's a program to do that right. Using normal UNIX tools on message files is fine provided they don't _change_ anything. (Editing the _content_ of a message file is OK provided you don't care about anyone who tries to read it while the editor is writing, and provided the editor doesn't affect the directory structure at all (eg, renaming the message file to a backup). Where $HOME is used, if $HOME is not set, "." is assumed. ---- Profile (configuration) and notation conventions Profile is kept in $MM, or $HOME/.mmrc if $MM is not set. Profile takes the form of "tag: value" lines; {tag} below refers to the profile value for tag. Profile lines beginning with # are comments. Profile lines can be continued; when a newline is immediately followed by a space or tab, the newline and all consecutive following spaces, tabs, and newlines are replaced with a single space. Comment stripping happens before continuation processing. Any profile tag can be overridden in the environment. When looking up {tag}, if MMPROF_TAG exists in the environment, its value is used instead. (That is, the tag is uppercased and has MMPROF_ prefixed to it, and the result is the environment variable checked for.) When something is "{foo}, default "bar", interpreted relative to baz", this means that if {foo} is set, its value is used as follows; otherwise, the result is as if {foo} were set to "bar". If the value begins with a /, it is used unchanged; otherwise, it has baz/ prefixed to it. When this phrasing is not used, this check for leading / characters is not done - for example, see below about the file sequences are kept in. is {mmdir}, default ".mm", interpreted relative to $HOME. Newly created folders will have mode {foldermode}, default 0700. Newly created messages will have mode {messagemode}, default 0600. Existing folders' and messages' modes are never modified. A lock for the entire mail system is used; the file is {syslock}, default ".syslock", interpreted relative to . (Most programs take a shared lock on this file. A program that does massive widespread modifications, such as folder compaction, may want to take an exclusive lock on this file; if it does, it can then skip most other locking.) Folders are subdirectories of {folders}, default "mail", interpreted relative to . Sequences are kept in a file called {seqfile}, default ".seq", in each folder. A folder's lockfile is {folderlock}, default ".lock", in that folder. Global state which is mutated by programs (as opposed to profile, which is global state which is mutated only by humans), is kept in {statefile}, default "state", relative to . Global state currently means only the current folder, which is kept in {statefile} under the name "folder". ---- Specifying messages on command lines A message number, below, can be just a number, in which case it refers to the current folder, or +folder:number, which refers to a message within a specific folder. When listing message numbers, one can specify a folder name without a message number; this sets the current folder for purposes of that command, for the rest of the command or until that syntax occurs again. "first", "last", and "cur" refer to the first, last, and current message, respectively; "next" and "prev" usually refer to the message following and the message preceding "cur", but see mmrm for some exceptions. Ranges are also supported, two numbers separated by a dash; the part before the dash can also be "first" or "cur", and that after can be "last" or "cur"; omitting the first part is equivalent to "first" and the last part is equivalent to "last". As a special case, "all" is equivalent to "first-last". In addition (not as part of a range), firstN and lastN, where N is a decimal number, refer to the first or last N messages in the folder, by message count; this will always be N messages, unless N is more than the number of messages in the folder. first#N and last#N refer to all messages within N messages by message number of the first or last message; this can be from one through N messages. Similarly, nextN, prevN, next#N, and prev#N refer to the previous and next N messages, by message count or message number. prevN and prev#N can also be used as the starting component of a range, in which case they specify the first message that they would refer to when used alone as a list; similarly, nextN and next#N can be used as the ending component of a range. [XXX prevN/nextN in ranges are unimplemented.] When listing messages, a sequence name can also be given instead of a message range; this refers to all messages in the sequence. To allow sequence names beginning with "first", "last", "cur", "next", "prev", or "all" (which would otherwise be confused with other syntax), you can specify either a bare colon with a sequence name following, or an extra colon after the folder name, depending on whether you wish to specify a folder name. Such a construct always takes the part after the "extra" colon as a sequence name. Each folder's current message is kept as a one-message sequence "cur" in that folder; similarly, "next" and "prev" sequences are used to keep track of the next and prev messages. Attempts to explicitly manipulate these sequences may or may not "work", depending on what you try. If the "cur" sequence is empty, the current message is the folder's first message (if the folder contains no messages, an error occurs); if the "cur" sequence has more than one message in it, the first message in it is the one used as "the current message". Similar remarks apply to "next" and "prev" as well. When these sequences are changed by programs, they are always either entirely deleted or left with exactly one message in them. Normally, next and prev will always refer to the messages before and after the current message; sometimes, next and prev will be adjacent and cur will be the same as one of them (this happens when cur is deleted, for example). It is possible for messages to exist between prev and cur, or cur and next, but it takes explicit action; for example, if messages 10 through 20 exist, with cur at 15 (and next at 16), deleting 16 will push next to 17; using mmmv to move message 20 to 16 then will end up leaving a message between cur and next. ---- Format programs A few programs use "format programs". This is simply code in a small stack language. The locution "the program " refers to program text found by looking up {fooformat}, or if that isn't defined, the program text found in the file named by {fooform}, or if that isn't defined, some program-specific default; such programs have a -prog tag option, which if given causes {tagformat} and/or {tagform} to be looked up instead. The language understands strings and integers. Code consists of: whitespace - ignored, except as token separator (but note that most tokens are self-terminating) string of digits - numeric constant (to write a negative number, use a leading underscore as a minus sign) "..." - string constant (C-style backslash escapes accepted) 'c' - numeric constant, value of character c (C-style backslash escapes accepted) [[ ... ]] - comment; these comments do not nest. { ... } - code block, "evaluates" to a constant that can be passed to Cc to execute the code. +, -, *, /, % - basic integer arithmetic (/ or % by zero produces an error at run time) =, <, > - integer comparison !, &, |, ^ - boolean operations ?? ?| ?+ ?| ?+ ... ?| ?. - if-else-elseif-endif D - does nothing when executed; if present in the program (ie, seen by the compiler), turns on format-program debugging. L( ... L) - loop begin/end Lw, Lu, Lb, Lc - loop while/until/break/continue Cr - return from current :NAME...; word, or main routine Cc - call a code block generated with the { ... } syntax $+ - strcat $?f - strcmp $?l - stringcmp $| - strcut $_ - substr $l - strlen $*+ - rexplode $*- - explode $!+ - rimplode $!- - implode $i+ - instr $i- - rinstr $I+ - instring $I- - rinstring Note that $i+, $i-, $I+, $I- return -1 for "not found"; 0 is a successful return. $>i - atoi $). Normally PATH extends to the next close paren, but a backslash will quote any character, including close paren or backslash. It is possible for a token to overlap an EOF boundary, but it requires that the file contain no trailing newline and it almost always bad style anyway. Note that when i(PATH) occurs in code in a file, PATH is _not_ interpreted relative to the directory the file exists in! (It arguably should be.) @NAME - extension, see below A NAME, as used in the \, :, and @ syntaxes, is any string of alphanumerics or underscores; it may even begin with a digit. In addition, individual uses of the language may define additional operations; when this is done, the additional operations are listed and described with the appropriate program. They take the form of names used with the @ syntax. Here are the possible extensions; each program lists which of these it provides: @width (-- i) Width of the terminal, in columns. @height (-- i) Height of the terminal, in lines. @unixfrom (-- s, or -- 0) Returns the From_ line of the message, if it has one, or else integer zero. (If the message somehow has multiple From_ lines, only one of them is accessible.) @hdrget (s -- s s, or s -- 0) Takes TOS string as a header name and returns the name and contents of the next such header; if no such headers remain, returns a single integer zero. Each header in the message will be returned at most once by @hdrget. If the argument is "", the first remaining header is returned regardless of what its name is. Header name matching is case-insensitive. (The header name is returned even when a name is specified, for consistency and to allow preserving case.) (On success, TOS is the header value, below that is the header name.) @hdrmatch (s -- s s, or s -- 0) Just like @hdrget, except the argument is a regexp against which header names are matched. Header name matching is case-insensitive. Note that if you want the match to be anchored at the beginning and/or end of the header name, you need to explicitly make it so with the ^ and/or $ operators. @hdrreset (--) Resets the notion of "already seen" used by @hdrget and @hdrmatch, so that after @hdrreset all headers in the message are "not yet seen". @bodyline (-- s, or -- 0) Returns the next line of the message's body, or integer zero if there are no more lines in the message's body. @outstring (s --) Prints the string to stdout. @filter (s --, or s0 s1 ... sN N --) Forks and executes the specified command, such that the command's stdout is what mmread's stdout was before @filter was executed, and mmread's stdout after the @filter is executed is piped to the command's stdin, thus interposing the command into the output stream. When passed a single string, the command is invoked by execing $SHELL with the -c convention, using /bin/sh if $SHELL is not set; when TOS is a number, @filter uses execlp(s0,s1,s2,...,sN,(char *)0), loosely speaking. Note in particular that s0 (pathname) is not the same as s1 (argv 0), and that s0 is not counted into N. Multiple executions of @filter will push multiple filters onto the output stream, with the last-pushed filter processing the data first. @dumpbody (--) All remaining body lines are copied to stdout. This is purely an efficiency optimization; semantically, it is equivalent to L( @bodyline t?s Lw @outstring "\n" @outstring L) Errors detected at run time, such as pop on an empty stack or an attempt to divide two strings, produce error complaints. ---- Program descriptions mmrcv [-U] [-u] [-s seq] [+folder ...] Receive a new message. Files it in the listed folder(s). If no folders are listed, files it in {inbox}, or "inbox" if {inbox} is not defined. If {unseen-sequence} is defined, each sequence named there gets each new message added to it in its folder. -s with a sequence name may be given; if this is done, the message is added to that sequence as well as any others it may be added to. Multiple -s options may be given, and the sequence names accumulate. -U suppresses the use of {unseen-sequence}; -u specifies the converse, restoring the default behavior. If both -U and -u are given, the last one given wins. (Note that if the sequence(s) named by {unseen-sequence} are explicitly given with -s, then -U and -u make no difference.) Note that all flags must be before any folder names. For each folder, if its cur sequence has a message in it but its next sequence doesn't, the incoming message is added to the next sequence. mmread [-prog tag] [msg ...] Read mail. Arguments are message numbers. This command changes the global default folder to whatever folder was current at the end of the argument list. If no arguments are specified, reads the current message in the current folder; if only folder names are specified, just changes the global current folder, without reading any mail. If {unseen-sequence} is defined, each sequence named there gets each message removed from it as the message is read. (Note that a message is separately unseen in each folder it's delivered to.) As each message is read, its folder's current message is set to it; a folder in which no messages get read will not have its current message affected. Changes to the profile while mmread is running will not be noticed by mmread. When mmread sets a folder's current message, it also sets next to the following message and prev to the preceding message, unless there is no next or previous message, in which case the relevant sequence is emptied. "Reading" a message means running the program ; the default program amounts to "cat" on the message file. mmread defines the following program extension operations: @width, @height, @unixfrom, @hdrget, @hdrmatch, @bodyline, @outstring, @filter, @dumpbody If mmread gets a SIGPIPE while processing the message, which can happen (for example) when a long message is being fed to a pager and the user quits the pager before EOF, program execution is aborted and cleanup actions occur as if the program had exited normally. [XXX there is a problem here. mmrcv keeps the folder locked shared for its entire run, which means mmrcv can't deliver into it. Forgetting and leaving an mmread process sitting inside a pager can end up filling up the process table with mmrcv processes waiting for the folder lock. I would love to come up with a way to cure this without having to buffer output all over the place (note that a new filter can be forked for each message) or try to unlock everything every time mmread blocks trying to write to a pipe to a filter.] mmrm [msg ...] Deletes mail. Just like mmread except that instead of reading the messages, deletes them. Automatically removes each message from any sequences it may belong to in that folder. If {rmbak} is defined, it must be a printf-style format, in which %% and %s are the only %-sign escapes allowed (non-escapes are of course accepted), and exactly one %s must appear. This format is used with the old message filename to generate a new message filename that the file is renamed to. (This operation is done using only the last part of the path to the message, that is, the filename within the folder's directory; you can include slashes in {rmbak}, though it is unlikely to do anything very useful.) If {rmbak} is not defined, deleted messages are simply removed outright. mmrm does not change any folder's current message, except when deleting it; when this is done, the folder's current message changes to the undeleted message with the lowest number that's above the (now ex-)current message, or when deleting the highest-numbered message, the highest numbered remaining message. (When deleting the last message from a folder, the "cur" sequence is removed entirely, which causes the current message to revert to the lowest-numbered message in the folder at the time the current message is desired.) When deleting the "next" message, mmrm sets the "next" message to the next higher numbered message, or removes it entirely if there is no such; similarly for "prev", in the other direction. When deleting "cur", the "next" and "prev" sequences are affected only if the message being deleted is also present there. (This means that it is possible for, for example, "next" and "cur" to refer to the same message; this is not an accident.) mmpath msg|+folder [msg|+folder ...] Prints paths to messages or folders. If an argument is a folder reference, the path to that folder is printed; if a message reference, the path to that message is printed. One line of output is generated for each argument. If no arguments at all are given, the path folders are kept in is printed (ie, {folders}, default "mail", interpreted relative to ). Arguments can refer to multiple messages, with ranges or sequences; when this is done, one line of output is generated per message. Note that this command follows the usual rules about interpreting argument lists; in particular, a folder reference both prints a line of output and changes the default folder for further message references for the rest of the command (per the general description above). [XXX This is probably not the Right Thing, but until we decide what the Right Thing is, it'll have to do.] Messages do not have to exist. mmmv [-u] [-s seq] [-p] [-f] msg msg mmmv [-u] [-s seq] [-p] msg [msg ...] +folder Moves a message from one place to another (first form); if the destination message exists, mmmv normally just exits with a complaint, but if -f is given, the destination message is destroyed first. The second form takes the listed message(s) and moves them into the specified folder, assigning them new message numbers there as if they were mail newly received into that folder. Both forms take a -p option ("preserve"), which doesn't remove messages from their original locations. The second form interprets all but the final folder name as usual for a list-of-messages argument list. Note that if only two arguments are given, both forms are still possible; mmmv disambiguates based on whether the second argument is a pure folder reference or a message reference. When the first form with -f destroyes a destination message, it is removed as if with mmrm, including use of {rmbak} and sequence number adjustments. However, source messages are always simply unlinked when moved; sequence number adjustments happen, but {rmbak} is not consulted for them. Both forms can take -s with a sequence name; the message is added to that sequence in its destination folder. -u is like -s, but it makes the message be "unseen" in the same way mmrcv does, including checking {unseen-sequence}. Multiple -s options may be given, and -u may be given as well, provided they all occur before any message references; the sequence names accumulate. However, mmmv never performs the check involving the cur and next sequences described under mmrcv. mmlnfile file +folder Links the specified file into the specified folder as a new message. Does not affect current folder, current message, sequences, anything but allocating a new message number and linking the file in. (This is the correct way to resurrect a message "deleted" by mmrm with {rmbak} set.) Does not remove the original file; do that by hand if you want it done. mmls [-prog tag] [msg ...] For each message specified, produces a one-line summary of that message. For each message, mmls runs the format program . mmls defines one format extension operation, @width. (There is no requirement that the format program pay any attention to @width; mmls does not check the length of the string to be output.) The program is run on each message and the top-of-stack string on completion is output. (If the stack is empty, or if the top-of-stack value is not a string, an error indication is output instead. Note also that if the string contains a newline, the "one-line" adjective above will be incorrect.) mmls interprets its arguments differently from the general description above in one respect: if no explicit references to messages are present (ie, all arguments are folder refernces), then mmls behaves as if each folder reference had ":all" appended. mmpack [+folder ...] For each folder, "pack" the messages in it, renumbering them so that their order relative to one another is unchanged, but the message numbers are sequential from 1 upwards. All sequences in the folder are adjusted to match. If no folders are listed, the current folder is packed. mmsend [-prog tag] [-repl msg] mmsend [-prog tag] [-to] addr [[-to] addr ...] mmsend -draft msg Sends mail. The first two forms create a new message; if -repl is given, the initial message is generated by running the program on the argument to -repl; otherwise, at least one address must be given (-to may be given before any address, to disambiguate in cases where the address might otherwise be confused with an option), and the initial message is generated by putting the to address(es) on the stack (in the order specified, first-specified on the bottom of the stack), then pushing a count of the addresses, and running the program with no message. In either case, the output is the initial message, which is saved as a new message in the {drafts} folder, or "drafts" if {drafts} is not defined. Alternatively, -draft may be given, with a message spec, in which case the message is treated as if it were a draft that had just been created with one of the other two forms. The default folder for the argument to -draft is {drafts} (or "drafts"), not the current folder. A draft can also be created by moving a message into the drafts folder "by hand", with mmrcv, mmmv, or mmlnfile, or by simply using a message in another folder with -draft. Once the draft is created or located, mmsend enters a command loop, with the prompt "What now?". The commands it understands are: s, send Sends the draft to the address(es) in its header. Once the message is sent, mmsend removes the draft and exits. S, Send Sends the message as for "send", but does not destroy the draft, nor exit, once the message is sent. e, edit Invoke {editor} (default $EDITOR, default "vi") on the draft; if an argument is given, invoke that editor instead. |, filter Filter the draft through the program described by the arguments; the output replaces the draft. (Note that when using |, whitespace must appear after it.) f, file Save the draft. Takes an argument specifying where to save the draft. If this argument begins with a +, it is taken as a folder reference, and (a copy of) the draft is filed as an incoming message there; otherwise, it is taken as a filename, and the draft is appended to that file (creating it if necessary). If multiple arguments are given, each is treated separately, as if multiple file commands had been given. q, x, quit, exit, or EOF or error reading the command Leave the draft in place, for potential future use with mmsend -draft, and exit. abort Throw away the draft and exit. If -draft is used and then the abort command is used to quit, the message passed to -draft is removed from its folder. echo Print the rest of the arguments, inserting a single space between arguments if multiple arguments are given. multi Each argument in turn is taken as a command and executed as if typed (including alias checking). noerr Takes exactly one argument, which is executed as a command. But if an error occurs, instead of stopping execution of any enclosing multi commands, the noerr command completes successfully. (Errors in the enclosed command will break back to the noerr command.) fail Does nothing, unsuccessfully. It takes no arguments. For purposes of control structure, this command behaves like an error; however, it does not actually print any error message (unless it is (incorrectly) given arguments). (If you want a user-controlled message, use a multi construct around an echo and a fail.) Every time a command is issued, the profile is checked for an entry "mmsend-alias-X" where X is the first word of the command. If one is found, its value replaces the command line, but with some expansions. $ signs control introduction of arguments from the alias command line; \ serves to quote \ or $ (that is, \\ becomes \ and \$ becomes $, the latter not being a special character). Currently, \ followed by any other character is left untouched, but it is unwise to depend on this. After processing of \ and $ expansions, the result is treated as if it had been typed as a command, including re-checking for aliases. Note that it is possible to set up an alias expansion loop, which will either eventually overflow the maximum argument count or else send mmsend into an infinite loop. The various $ syntaxes are: $(nnn) (nnn is a string of one or more digits) Replaced by argument nnn from the alias command line. Argument 0 is the command verb, argument 1 the first argument after that, etc. If nnn is too large, this expands to zero characters. If nnn is only one digit long, the parentheses may be omitted. $(nnn*) (nnn is a string of zero or more digits) Replaced by arguments nnn through the last argument; a single space is provided between arguments. If nnn is so large that argument nnn does not exist, this expands to zero characters. If no digits are provided, the effect is as if $(1*) had been written. $"nnn" This is just like $(nnn) except that the replacement text is processed to quote any characters that would otherwise be argument-list syntax, such as whitespace, quotes, and backslashes. If argument nnn exists and is zero-length, a quoted zero-length string is produced; if argument nnn does not exist, nothing is produced. $"nnn*" This is just like $(nnn*) except that each argument is processed as if for the $"nnn" syntax. The spaces introduced between arguments are not quoted. If nnn is so large that argument nnn does not exist, nothing is produced. $Q< ... $Q> This processes everything between the delimiters such as to quote any argument-list syntax characters generated. For example, if you want something like $"nnn*" but with quoted spaces between arguments (so that the whole thing becomes a single argument), you can write it as $Q<$(nnn*)$Q>. These constructs nest; text inside multiple $Q<...$Q> bracketing is quoted multiple times, such that if it is run through argument parsing that many times it will come back intact. (This is most useful in conjunction with commands that treat their arguments as commands, notably the "multi" command.) $?cond: ... $?| ... $?. Conditional. If "cond" is true, processes the first part and skips the second; otherwise, vice versa. The $?| and the second piece of controlled text are optional. cond can be: eNNN (NNN is one or more digits) True if NNN is small enough that argument NNN exists. Unlike most programs, mmsend does not hold any locks during most of its operation; it re-acquires locks as necessary to implement its commands and releases them before turning control back over to the user. This means that it is possible for the draft to disappear out from under it; if this happens, a complaint is printed and it exits. Using mmpack or similar tools on the folder a draft exists in is safe only when no mmsend processes are active. In particular, if the user is in the editor when folder manipulations happen, it is (almost) certain that when the editor saves the file, it will save it to the number it had when the editor was started. Note also that when mmsend destroys a draft, it does the same sequence manipulations mmrm would do when deleting that message, but it does not check {rmbak}; it simply unlinks the file. In this respect it's similar to the way mmmv removes a source message. When mmsend decides to send the message, it runs {sendmail} and feeds it the message on its stdin. If {sendmail} is not defined, it runs _PATH_SENDMAIL from with "-t" as the only argument. In either case, the equivalent of execvp is used. If an Fcc: header is present, sending the message also means taking the Fcc: header's value as a list of folder references, and delivering the message as incoming mail to those folders. This is done by piping the message to an mmrcv process; it runs {mmsend-mmrcv}, default "mmrcv", with execvp(). Both when piping to sendmail and when piping to mmrcv, the Fcc: header is removed from the message. Other ways of saving the message, such as the "file" command or such constructs as "filter tee somefile", do not remove Fcc:. ---- Programs not yet fully specced mmfolders [-1] [-l] [+folder ...] -l - just list, otherwise give human-readable listing with info about each folder -1 - don't recurse If folder names given, give info about those folders (and without -1, any subfolders); if no folder names given, searches the folders directory and acts as though each folder found there had been listed on the command line. [XXX Need to spec what "give info" lists, probably should be configurable.]