/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * SPDX-FileCopyrightText: 2026 Coffey Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only * * Modified by Coffey Labs in 2026 for INBUXA. */ const RFC = (n, section) => `https://www.rfc-editor.org/rfc/rfc${n}${section ? `#section-${section}` : ""}`; export const CONTROL = { require: { sig: "require ", doc: "Declares the extensions the script uses. Every extension must be required before its commands, tests or tags appear.", rfc: RFC(5228, "3.2") }, if: { sig: "if ", doc: "Runs the block when the test is true. Can be followed by `elsif` and `else`.", rfc: RFC(5228, "3.1") }, elsif: { sig: "elsif ", doc: "Runs the block when all previous tests in the chain were false and this test is true.", rfc: RFC(5228, "3.1") }, else: { sig: "else ", doc: "Runs the block when every previous test in the chain was false.", rfc: RFC(5228, "3.1") }, stop: { sig: "stop", doc: "Ends the script. The implicit keep still applies unless another action cancelled it.", rfc: RFC(5228, "3.3") }, include: { sig: 'include [:personal / :global] [:once] [:optional] ', doc: "Runs another script. In the playground, add a script tab with that name.", rfc: RFC(6609, "3.2"), capability: "include" }, return: { sig: "return", doc: "Stops the current included script and continues in the script that included it.", rfc: RFC(6609, "3.3"), capability: "include" }, global: { sig: "global ", doc: "Shares variables between the including and included scripts.", rfc: RFC(6609, "3.4"), capability: "include" }, foreverypart: { sig: "foreverypart [:name ] ", doc: "Runs the block once for each MIME part of the message. Tests with `:mime` and commands such as `replace` act on the current part.", rfc: RFC(5703, "3"), capability: "foreverypart" }, break: { sig: "break [:name ]", doc: "Leaves the innermost (or the named) `foreverypart` or `while` loop.", rfc: RFC(5703, "3"), capability: "foreverypart" }, while: { sig: "while ", doc: "inbuxa extension. Repeats the block while the expression is true. Bounded by the CPU limit.", capability: "vnd.inbuxa.while" }, continue: { sig: "continue", doc: "inbuxa extension. Skips to the next iteration of the enclosing loop.", capability: "vnd.inbuxa.while" }, }; export const ACTIONS = { keep: { sig: "keep [:flags ]", doc: "Files the message into the default mailbox (INBOX). This is what happens implicitly when no other action cancels it.", rfc: RFC(5228, "4.3") }, discard: { sig: "discard", doc: "Silently throws the message away and cancels the implicit keep.", rfc: RFC(5228, "4.4") }, fileinto: { sig: "fileinto [:copy] [:flags ] [:create] [:specialuse ] [:mailboxid ] ", doc: "Delivers the message into a mailbox and cancels the implicit keep (unless `:copy`).", rfc: RFC(5228, "4.1"), capability: "fileinto" }, redirect: { sig: "redirect [:copy] [:notify ] [:ret ] [:bytimerelative / :bytimeabsolute ] ", doc: "Forwards the message to another address and cancels the implicit keep (unless `:copy`). Limited by the maximum redirects setting.", rfc: RFC(5228, "4.2") }, reject: { sig: "reject ", doc: "Refuses the message and sends the reason back to the sender.", rfc: RFC(5429, "2.2"), capability: "reject" }, ereject: { sig: "ereject ", doc: "Refuses the message at the SMTP level when possible.", rfc: RFC(5429, "2.1"), capability: "ereject" }, vacation: { sig: "vacation [:days / :seconds ] [:subject ] [:from ] [:addresses ] [:mime] [:handle ] ", doc: "Sends an automatic reply, at most once per sender in the given period. Tracked with duplicate ids: try Run again.", rfc: RFC(5230, "4"), capability: "vacation" }, notify: { sig: "notify [:from ] [:importance <1|2|3>] [:options ] [:message ] ", doc: "Sends a notification, for example to a `mailto:` URI.", rfc: RFC(5435, "3"), capability: "enotify" }, set: { sig: "set [:lower / :upper / :lowerfirst / :upperfirst / :quotewildcard / :quoteregex / :encodeurl / :length] ", doc: "Stores a value in a variable. Use `${name}` to read it back inside strings.", rfc: RFC(5229, "4"), capability: "variables" }, setflag: { sig: "setflag [] ", doc: "Replaces the flags that will be stored with the message.", rfc: RFC(5232, "3.1"), capability: "imap4flags" }, addflag: { sig: "addflag [] ", doc: "Adds flags that will be stored with the message, such as `\\Seen` or `\\Flagged`.", rfc: RFC(5232, "3.2"), capability: "imap4flags" }, removeflag: { sig: "removeflag [] ", doc: "Removes flags from the set stored with the message.", rfc: RFC(5232, "3.3"), capability: "imap4flags" }, addheader: { sig: "addheader [:last] ", doc: "Adds a header field, at the top unless `:last` is given.", rfc: RFC(5293, "4"), capability: "editheader" }, deleteheader: { sig: 'deleteheader [:index [:last]] [] [] []', doc: "Removes header fields, optionally only those matching the values.", rfc: RFC(5293, "5"), capability: "editheader" }, replace: { sig: "replace [:mime] [:subject ] [:from ] ", doc: "Replaces the message body, or the current part inside `foreverypart`.", rfc: RFC(5703, "5"), capability: "replace" }, enclose: { sig: "enclose [:subject ] [:headers ] ", doc: "Wraps the original message as an attachment of a new message.", rfc: RFC(5703, "6"), capability: "enclose" }, extracttext: { sig: "extracttext [:first ] [] ", doc: "Stores the text of the current MIME part in a variable. Only valid inside `foreverypart`.", rfc: RFC(5703, "7"), capability: "extracttext" }, convert: { sig: "convert ", doc: "Converts MIME parts from one media type to another.", rfc: RFC(6558, "3"), capability: "convert" }, error: { sig: "error ", doc: "Aborts the script with an error. The implicit keep applies.", rfc: RFC(5463, "5"), capability: "ihave" }, let: { sig: "let ", doc: "inbuxa extension. Evaluates an expression and stores the result, for example `let \"n\" \"len(header.subject) * 2\"`.", capability: "vnd.inbuxa.expressions" }, }; export const TESTS = { address: { sig: "address [] [] [] [:index [:last]] ", doc: "Compares addresses in header fields such as From or To. Address parts: `:all`, `:localpart`, `:domain`, `:user`, `:detail`.", rfc: RFC(5228, "5.1") }, allof: { sig: "allof ()", doc: "True when every test in the list is true.", rfc: RFC(5228, "5.2") }, anyof: { sig: "anyof ()", doc: "True when at least one test in the list is true.", rfc: RFC(5228, "5.3") }, envelope: { sig: "envelope [] [] [] ", doc: "Compares the SMTP envelope. Set the envelope below the message.", rfc: RFC(5228, "5.4"), capability: "envelope" }, exists: { sig: "exists ", doc: "True when every listed header field is present.", rfc: RFC(5228, "5.5") }, false: { sig: "false", doc: "Always false.", rfc: RFC(5228, "5.6") }, header: { sig: "header [:mime] [] [] [:index [:last]] ", doc: "Compares header field values. With `:mime` it looks at MIME part headers.", rfc: RFC(5228, "5.7") }, not: { sig: "not ", doc: "Inverts a test.", rfc: RFC(5228, "5.8") }, size: { sig: "size <:over / :under> ", doc: "Compares the message size. Numbers accept K, M and G suffixes.", rfc: RFC(5228, "5.9") }, true: { sig: "true", doc: "Always true.", rfc: RFC(5228, "5.10") }, body: { sig: "body [] [] [:raw / :content / :text] ", doc: "Compares the message body.", rfc: RFC(5173, "4"), capability: "body" }, currentdate: { sig: "currentdate [:zone ] [] [] ", doc: "Compares the current date. Date parts include `year`, `month`, `day`, `date`, `hour`, `weekday`, `iso8601`. The clock can be fixed in Settings.", rfc: RFC(5260, "5"), capability: "date" }, date: { sig: "date [:zone / :originalzone] [] []
", doc: "Compares a date taken from a header field.", rfc: RFC(5260, "4"), capability: "date" }, duplicate: { sig: "duplicate [:handle ] [:header / :uniqueid ] [:seconds ] [:last]", doc: "True when the same message (by Message-ID or the given id) was seen before. Use Run again to simulate a redelivery.", rfc: RFC(7352, "3"), capability: "duplicate" }, environment: { sig: "environment [] [] ", doc: "Compares environment items such as `domain`, `host`, `phase` or `remote-ip`. Configure them in Settings.", rfc: RFC(5183, "4"), capability: "environment" }, hasflag: { sig: "hasflag [] [] [] ", doc: "Tests the flags set by the script so far.", rfc: RFC(5232, "4"), capability: "imap4flags" }, ihave: { sig: "ihave ", doc: "True when the listed extensions are available.", rfc: RFC(5463, "4"), capability: "ihave" }, mailboxexists: { sig: "mailboxexists ", doc: "True when every mailbox exists. Mailboxes are listed in Settings.", rfc: RFC(5490, "3.1"), capability: "mailbox" }, mailboxidexists: { sig: "mailboxidexists ", doc: "True when every mailbox id exists.", rfc: RFC(9042, "4.2"), capability: "mailboxid" }, metadata: { sig: "metadata [] [] ", doc: "Compares a mailbox annotation.", rfc: RFC(5490, "3.2"), capability: "mboxmetadata" }, metadataexists: { sig: "metadataexists ", doc: "True when the mailbox has the annotations.", rfc: RFC(5490, "3.3"), capability: "mboxmetadata" }, servermetadata: { sig: "servermetadata [] [] ", doc: "Compares a server annotation. Configure annotations in Settings.", rfc: RFC(5490, "3.4"), capability: "servermetadata" }, servermetadataexists: { sig: "servermetadataexists ", doc: "True when the server has the annotations.", rfc: RFC(5490, "3.5"), capability: "servermetadata" }, spamtest: { sig: "spamtest [:percent] [] [] ", doc: "Compares the spam score (0 to 10, or a percentage with `:percent`). Set the score in Settings.", rfc: RFC(5235, "3.2"), capability: "spamtest" }, virustest: { sig: "virustest [] [] ", doc: "Compares the virus status (0 unknown, 1 clean, 5 infected). Set it in Settings.", rfc: RFC(5235, "3.3"), capability: "virustest" }, specialuse_exists: { sig: "specialuse_exists [] ", doc: "True when a mailbox with the special-use attributes exists.", rfc: RFC(8579, "3"), capability: "special-use" }, string: { sig: "string [] [] ", doc: "Compares arbitrary strings, usually containing variables.", rfc: RFC(5229, "5"), capability: "variables" }, valid_notify_method: { sig: "valid_notify_method ", doc: "True when the notification URIs are valid. Allowed schemes are configured in Settings.", rfc: RFC(5435, "5"), capability: "enotify" }, notify_method_capability: { sig: "notify_method_capability [] [] ", doc: "Queries a capability of a notification method.", rfc: RFC(5435, "6"), capability: "enotify" }, valid_ext_list: { sig: "valid_ext_list ", doc: "True when the external lists exist. Lists are configured in Settings.", rfc: RFC(6134, "2.6"), capability: "extlists" }, eval: { sig: "eval ", doc: "inbuxa extension. True when the expression evaluates to a non-zero, non-empty value.", capability: "vnd.inbuxa.expressions" }, }; export const TAGS = { is: "Exact match.", contains: "Substring match.", matches: "Wildcard match: `*` matches any sequence, `?` one character. Captures go to `${1}`, `${2}`...", regex: "Regular expression match. Captures go to `${1}`, `${2}`... Requires \"regex\".", value: "Relational comparison: \"gt\", \"ge\", \"lt\", \"le\", \"eq\" or \"ne\". Requires \"relational\".", count: "Compares the number of values. Requires \"relational\".", list: "Matches against an external list, such as \"addrbook:default\". Requires \"extlists\".", comparator: "Sets the comparator: \"i;ascii-casemap\" (default), \"i;octet\" or \"i;ascii-numeric\".", all: "Compares the whole address.", localpart: "Compares the part before the @.", domain: "Compares the part after the @.", user: "Compares the user part of a subaddress (before +). Requires \"subaddress\".", detail: "Compares the detail part of a subaddress (after +). Requires \"subaddress\".", over: "True when the size is larger than the limit.", under: "True when the size is smaller than the limit.", copy: "Keeps the implicit keep in place. Requires \"copy\".", flags: "Flags to store with the message. Requires \"imap4flags\".", create: "Creates the mailbox if it does not exist. Requires \"mailbox\".", specialuse: "Selects the mailbox by its special use, such as \"\\\\Junk\". Requires \"special-use\".", mailboxid: "Selects the mailbox by id. Requires \"mailboxid\".", days: "Minimum days between replies to the same sender.", seconds: "Minimum seconds between replies or duplicates.", subject: "Subject of the generated message.", from: "From address of the generated message.", addresses: "Your own addresses, so the reply is not sent to yourself.", handle: "Name used to track replies or duplicates independently.", mime: "For tests: look at MIME part headers. For vacation and replace: the text is a full MIME entity.", anychild: "With `:mime`, look at every child part.", type: "With `:mime`, compare only the media type.", subtype: "With `:mime`, compare only the media subtype.", contenttype: "With `:mime`, compare type/subtype.", param: "With `:mime`, compare Content-Type parameters.", index: "Selects the nth occurrence of a header field. Requires \"index\".", last: "Counts `:index` from the end.", lower: "Lowercases the value.", upper: "Uppercases the value.", lowerfirst: "Lowercases the first character.", upperfirst: "Uppercases the first character.", quotewildcard: "Escapes `*`, `?` and `\\` for use in :matches.", quoteregex: "Escapes regex special characters.", encodeurl: "Percent-encodes the value.", length: "Replaces the value with its length.", percent: "Uses the percentage spam score. Requires \"spamtestplus\".", zone: "Time zone offset such as \"+0100\".", originalzone: "Keeps the time zone from the header.", raw: "Compares the raw body.", content: "Compares body parts of the given media types.", text: "Compares the decoded text body.", importance: "\"1\" high, \"2\" normal, \"3\" low.", message: "Notification text.", options: "Method-specific notification options.", personal: "Includes a personal script.", global: "Includes a global script.", once: "Includes the script only once.", optional: "Does not fail when the script is missing.", notify: "DSN notification: \"never\" or a list of \"success\", \"failure\", \"delay\". Requires \"redirect-dsn\".", ret: "DSN return of content: \"full\" or \"hdrs\". Requires \"redirect-dsn\".", bytimerelative: "Deliver-by time in seconds. Requires \"redirect-deliverby\".", bytimeabsolute: "Deliver-by date. Requires \"redirect-deliverby\".", bymode: "\"notify\" or \"return\" when the deliver-by time passes.", bytrace: "Requests deliver-by trace information.", uniqueid: "Custom id for the duplicate test.", header: "Header field used as the duplicate id.", first: "Maximum number of characters to extract.", name: "Names a foreverypart loop.", fcc: "Stores a copy of the generated message in a mailbox. Requires \"fcc\".", }; export const DATE_PARTS = ["year", "month", "day", "date", "julian", "hour", "minute", "second", "time", "iso8601", "std11", "zone", "weekday"]; export const ENVIRONMENT_ITEMS = ["domain", "host", "location", "name", "phase", "remote-host", "remote-ip", "version"]; export const FUNCTIONS = { trim: 1, trim_start: 1, trim_end: 1, len: 1, count: 1, is_empty: 1, is_number: 1, is_ascii: 1, to_lowercase: 1, to_uppercase: 1, is_email: 1, thread_name: 1, html_to_text: 1, is_uppercase: 1, is_lowercase: 1, has_digits: 1, count_spaces: 1, count_uppercase: 1, count_lowercase: 1, count_chars: 1, dedup: 1, lines: 1, is_ip_addr: 1, is_ipv4_addr: 1, is_ipv6_addr: 1, winnow: 1, sort: 2, email_part: 2, eq_ignore_case: 2, contains: 2, contains_ignore_case: 2, starts_with: 2, ends_with: 2, uri_part: 2, split: 2, rsplit: 2, split_once: 2, rsplit_once: 2, strip_prefix: 2, strip_suffix: 2, is_intersect: 2, substring: 3, split_n: 3, llm_prompt: 3, }; export function lookup(word) { const key = word.toLowerCase(); if (CONTROL[key]) return { kind: "control", ...CONTROL[key] }; if (ACTIONS[key]) return { kind: "action", ...ACTIONS[key] }; if (TESTS[key]) return { kind: "test", ...TESTS[key] }; return null; }