Grammar
On this page
This page is the exact rule for an OPVS message. It is written in ABNF (RFC 5234), with a few rules in plain words where ABNF cannot say them. It matches both reference parsers. Where a rule says "refused", a parser returns no sender and the server answers HTTP 400.
Two principles shaped it: strict about the address, generous about labels. A wrong address is refused, so mail never goes somewhere it was not meant to go. A label (a tag, a flag, an id) may use - inside it, and a tag may use letters from any language.
The message
message = routing-line [ LF body ] [ LF context-block ]
routing-line = address *( 1*WSP mark ) *WSPIn plain words:
Blank lines and spaces around the whole message are ignored.
Line 1 is the routing line. Everything after it is the body, then an optional context block.
A blank line between the routing line and the body is optional.
CRLFline ends are accepted.A message with no valid address is refused.
The address
address = "@" sender ">" destination ; then a space or the end of the line
sender = name [ "+" name ] [ "::" domain ]
destination = ( "*" / "|" name / "team:" name / name [ "+" name ] ) [ "::" domain ]
name = ( ALPHA / DIGIT ) *( ALPHA / DIGIT / "_" / "-" )
domain = 1*( ALPHA / DIGIT / "_" / "." / "-" )ALPHAandDIGITare ASCII only, as in RFC 5234. Upper case is kept.The
+ nameafter a name is a sub-address. It is never a flag.The address must be followed by a space or the end of the line. If anything is glued to it (
bob@opvs.run,bob?,team:*,bob::a.io::b), the line is refused.team:with no name and a bare|are refused.Domains are compared without regard to case or a trailing dot.
The marks
After the address, the routing line is split into tokens on spaces and tabs. Each token is read once.
mark = kind / urgency / thread / reply-to / tag / negated-tag
/ flag / cause / replaces / inline-value
kind = "?" / "!" / "~" / "^" / "." / ">" / "<"
urgency = "!!!" / "!!"
thread = "&" id [ reply-to ]
reply-to = "<m_" id
tag = "#" tag-label
negated-tag = "/#" tag-label
flag = "+" label
cause = "<-" label
replaces = "!=" [ 1*WSP ] label
inline-value = "$" key ":" 1*VALUE-CHAR
id = 1*( ALPHA / DIGIT / "_" / "-" )
label = 1*WORD *( "-" 1*WORD )
tag-label = 1*TAG-WORD *( "-" 1*TAG-WORD )
key = ( ALPHA / "_" ) *( ALPHA / DIGIT / "_" / "." / "-" )
WORD = ALPHA / DIGIT / "_"
TAG-WORD = <any Unicode letter or digit> / "_"
VALUE-CHAR = <any character except a space or tab>Rules the ABNF does not show:
Rule | |
|---|---|
Order | Marks may come in any order. The order the formatters write (kind, urgency, thread, cause, replaces, tags, negated tags, flags) is a style. |
Kind | The first standalone kind token counts, including one at the end of the line. A second one is ignored. No kind means |
| A lone |
Urgency | Only a whole |
Thread | The first thread wins. |
Whole tokens | A tag, negated tag or flag must start its token: |
Values are opaque | Nothing inside an |
Unknown tokens | A token that is none of the above is ignored. Senders should not write one. |
The body
The body is everything after the routing line and before the context block, with blank lines at its start and end removed. It is plain text or markdown and is kept as written. A --- line inside the body is just text (a markdown rule).
The context block
context-block = "---" LF 1*( context-line / blank-line ) [ "---" ]
context-line = *WSP "$" key *WSP ":" value LFIn plain words: a --- section is a context block only if it is the final section of the message and every non-blank line in it is $key:value. The closing --- is optional. If even one line in the final section is something else, the whole section is body, kept as written. Nothing is ever thrown away.
@ops>team:backend . #release
Release notes for 4.2 are below.
---
This line sits below a markdown rule, and it is still body.
---
$version:4.2
$hotfix:false
$owners:["ann-lee","sam"]
---Here the first --- is a markdown rule inside the body. Only the final section is the context block: version = 4.2, hotfix = false, owners = a list.
Value types
The same rules apply to inline values and to context block values.
Written | Read as |
|---|---|
starts with | that JSON value ( |
ASCII digits with no leading zero, an optional | a number: |
| a boolean |
anything else | text |
So +5, .5, 3., 1e3 and 1_000 are text, and a value with a leading zero (02134, 007) is text. Postcodes and ids survive.
When the same key appears more than once, the last one wins. When a key appears both inline and in the context block, the block wins.
Round trip
A conforming formatter writes a parsed message so that parsing it again gives the same message. The OPVS server stores the message as its formatter writes it. The stored text can differ in spelling from what was sent: the marks are written in the formatter's order, and a number is written back as a number (299.00 may come back as 299.0 or 299). Keep exact spellings in the body or as text values.
Refused messages
plain text with no address@alice ? no destination@_bot>alice . a name may not start with _@jürgen>alice . names are ASCIIRelated
Addresses and Message kinds, urgency, topics and flags: the same rules, explained
Parser libraries: the two implementations