Documentation

Mail & Documents

Send Nimbus Mail, manage domain addresses, and issue paper documents from another resource.

Nimbus Mail is the mail app on Facade, Lumen and Solace. Every Nimbus account has an address such as [email protected], and players who own or rent a domain on Namestead make addresses on it, such as [email protected], that several characters share. Other resources send mail to characters or addresses, such as receipts and notices, read a character's addresses, and manage addresses on a domain, such as giving every officer access to [email protected].

Documents are paper. A printout, a photocopy or an issued original is a laptop_document item that only holds the document's serial, and the server keeps the page itself. Other resources hand out official originals, such as an impound notice or a prescription, and check whether a serial is a real one.

To open Mail on the player's screen from the client, see Open Mail.

The mail exports only exist while Config.Mail.enabled is true in shared/unlocked/mail.lua. Mail needs the Nimbus cloud: with Config.Settings.cloud.mode set to "drive" there are no Nimbus accounts, so characters have no address. Domain addresses also need Config.Domains.enabled and Config.Mail.domains.enabled.

The document exports only exist while Config.Documents.enabled is true in shared/unlocked/documents.lua.

With either off, calling its exports fails, so a resource that works without them wraps its calls in pcall.

General Information

Every export on this page is a server export.

A character is passed by their identifier. A recipient can be a character identifier, which becomes their Nimbus address, or any address. Addresses are turned to lower case.

Exports marked Yields wait on the database. Call them from a thread, an event handler or a command, not while your script loads.

Types / Classes

MailResourceMail is in types/mail.lua, and LaptopDocument, DocumentBlock, DocumentRun, LaptopDocumentView and DocumentKind are in types/documents.lua inside zyke_laptop. LaptopDocumentIssue and LaptopDocumentCheck are in server/locked/documents/exports.lua.

#Mail

#Send Mail

Sends mail from a resource. Yields. Returns the mail's message key, or nil and "invalid" or "noRecipients". A refused mail also prints a warning in the server console.

  • to and cc are a character identifier or address, or a list of them, each cut to limits.recipients (20 by default). There is no BCC.
  • A character without a Nimbus account is skipped. When nobody is left, it returns "noRecipients".
  • "invalid" is returned for an invalid from address, a subject or body that isn't a string or is over limits.subject or limits.body characters, or a file that is invalid or over limits.attachmentBytes. Files past limits.attachments (5 by default) are dropped.
  • Mail to an address nobody has still returns a message key. It is not delivered, and no bounce is sent.
  • from is Config.Mail.systemSender.address ([email protected]) when left out, and fromName is the system sender's name only for that address. Replies to an address nobody has bounce, so a noreply@ sender stays one. Sent from a domain address, a copy goes into that address's Sent.
  • The body is plain text, shown exactly as written, unless markup is true. Markup uses **bold**, *italic*, __underline__, ~~strike~~ and [text](https://...), with - , 1. and > at the start of a line for lists and quotes, and a backslash before a character to show it as it is.

Example:

lua
---@param to string | string[] @ Character identifiers or addresses
---@param mail MailResourceMail
---@return string? messageKey
---@return string? err @ "invalid" or "noRecipients"
local messageKey, err = exports["zyke_laptop"]:SendMail(identifier, {
    from = "[email protected]",
    fromName = "Benny's Original Motor Works",
    subject = "Your car is ready",
    body = "Hi,\n\nYour **Sultan RS** is ready to pick up.",
    markup = true,
    cc = "[email protected]",
    files = {{name = "invoice.txt", mime = "text/plain", data = "Paint job: $4,500"}},
})
FieldDefaultDescription
fromConfig.Mail.systemSender.addressThe sender's address. It doesn't have to exist.
fromNameThe system sender's name, or noneThe name the mail is shown from, up to limits.displayName characters.
subject""One line, up to limits.subject characters.
body""Up to limits.body characters.
markupfalsetrue keeps the body's formatting marks.
ccNoneA character identifier or address, or a list of them.
filesNone{name, mime?, data}. data is text, or a data URL for a picture. mime is "application/octet-stream" when left out or invalid.

#Get Mail Address

Returns the character's Nimbus address, or nil when they have no Nimbus account. Yields the first time a character is looked up.

Example:

lua
---@param identifier string
---@return string? address
local address = exports["zyke_laptop"]:GetMailAddress(identifier)

#Get Mail Addresses

Returns every address the character reads and sends from: their Nimbus address first, then every domain address they have access to whose domain is still registered, by address. Returns an empty table when they have no Nimbus account. Yields.

Example:

lua
---@param identifier string
---@return string[] addresses
local addresses = exports["zyke_laptop"]:GetMailAddresses(identifier)

#Create Mailbox

Makes an address on a domain registered on Namestead and gives members access to it at once. It doesn't check who owns the domain or the reserved names, so a script can make [email protected] on a domain it set up. The address counts towards the domain's maxMailboxes. Yields.

Returns true, or false and an error: "invalid" for an invalid address, "notFound" when the domain isn't registered or domain addresses are off, "taken" when the address exists, and "limit" when the domain has domains.maxMailboxes addresses (25 by default).

The address lives as long as the domain. A rental that runs out suspends it, and a released domain deletes it with its mail.

Example:

lua
---@param address string
---@param members? string[] @ Character identifiers
---@param name? string @ The name mail from it goes out with
---@return boolean created
---@return string? err @ "invalid", "notFound", "taken" or "limit"
local created, err = exports["zyke_laptop"]:CreateMailbox("[email protected]", {identifier}, "LSPD Dispatch")

#Delete Mailbox

Deletes a domain address with its members, settings, auto-reply and every mail it held. Returns false for an address that isn't a domain address. Yields.

Example:

lua
---@param address string
---@return boolean deleted
local deleted = exports["zyke_laptop"]:DeleteMailbox("[email protected]")

#Set Mailbox Access

Gives a character access to a domain address, or takes it away, and updates their open Mail. Returns true when it changed, and false for an address that isn't a domain address or a character who already had, or didn't have, access. Yields.

Example:

lua
---@param address string
---@param identifier string
---@param hasAccess boolean
---@return boolean changed
exports["zyke_laptop"]:SetMailboxAccess("[email protected]", identifier, true)

#Get Mailbox Access

Returns the identifiers of every character with access to a domain address, or nil for an address that isn't a domain address.

Example:

lua
---@param address string
---@return string[]? members
local members = exports["zyke_laptop"]:GetMailboxAccess("[email protected]")

#Documents

A document's serial looks like DC-7F3K-92QD. Each one has a kind: "original" from IssueDocument, "print" from a laptop's printer, or "copy" from a photocopy. A copy has its own serial and shows its original's content. Documents nobody looked at for retentionDays (120 by default) are deleted, and their paper then reads blank.

#Issue Document

Writes an original and hands its paper to the player as copies papers of the same original. Without plyId it only writes the document. Yields.

Returns the serial, or nil and an error:

  • "invalid": issuer is missing, or nothing readable is left in blocks, text and fields.
  • "tooLarge": the document is over limits.bytes (49152 by default) as JSON.
  • "noItem": the paper item Config.Documents.item is missing from the inventory.
  • "noRoom": the player can't carry the papers. When the document was already written, the serial is returned with "noRoom".
  • "failed": the document could not be written.

The issuer and serial are printed on the paper, with the issuing character's name when issuedBy is given. metadata is added to the item's metadata, and may replace its label and description but never the document. Photocopies never get it.

Example:

lua
---@param plyId? integer
---@param data LaptopDocumentIssue
---@return string? serial
---@return string? err @ "invalid", "tooLarge", "noItem", "noRoom" or "failed"
local serial, err = exports["zyke_laptop"]:IssueDocument(plyId, {
    issuer = "Los Santos Police Department",
    issuerId = "police",
    issuedBy = identifier,
    title = "Vehicle impound notice",
    blocks = {
        {k = "p", s = "title", r = {{t = "Impound notice"}}},
        {k = "p", r = {{t = "Your vehicle was impounded for "}, {t = "parking on the sidewalk", b = true}, {t = "."}}},
    },
    fields = {{"Plate", "46EEK572"}, {"Fee", "$500"}},
    size = "letter",
    footer = "Mission Row Police Station",
    numbers = "fr",
    copies = 1,
    metadata = {plate = "46EEK572"},
})
FieldDefaultDescription
issuerRequiredThe organisation printed on it, up to 100 characters.
issuerIdNoneYour own key for the issuer, such as a job name, up to 64 characters. VerifyDocument returns it.
issuedByNoneIdentifier of the character who issued it. Their name is shown on it.
title""Up to 120 characters. The paper item's label.
blocksNoneThe document's blocks.
textNonePlain text instead of blocks. A blank line starts a new paragraph.
fieldsNone{{label, value}, ...}, a two-column table with bold labels after the text. Up to 60 rows.
size"letter""letter", "a4" or "legal".
oPortrait"l" for landscape.
mThe paper's own{top, right, bottom, left} margins in pixels at 96 per inch, 216 at most.
bgWhitePage colour as #rrggbb.
header / footerNoneText in the page margins, up to 160 characters.
numbersNonePage numbers: "h" header or "f" footer, then "l" left, "c" centre or "r" right, such as "fr".
copies1Papers handed to the player, 1 to 10.
pagesCounted from break blocksPages it fills, up to limits.pages.
metadataNoneMore item metadata for your own use, such as a prescription id.

#Blocks

A block is a table with k, and up to 400 blocks are kept. Blocks and fields that aren't valid are dropped.

kFields
"p"A paragraph. r its runs, s its style ("title", "h1", "h2" or "small"), a its alignment ("c", "r" or "j"), ls its line spacing (1, 1.5 or 2, 1.15 when left out) and il its indent level, 0 to 8.
"ul" / "ol"A bulleted or numbered list. items is a list of runs, lv each item's nesting level, 0 to 8, plus a and ls.
"table"rows = {{runs, runs}, ...}, up to 60 rows of 8 cells.
"img"A picture. src is an https address, w its width in percent of the line, and a its alignment. Up to limits.images (8 by default), from the imageHosts the config allows.
"break"A page break.

A run is {t = "text"} with any of b bold, i italic, u underline, s strikethrough set to true, z the size in points (8 to 48), f the font ("inter", "ubuntu", "serif" or "mono", a sans when left out), and c and h the text and highlight colours as #rrggbb.

#Verify Document

Tells whether a serial is an original from IssueDocument or a photocopy of one, so a police check or a pharmacy can trust it. A laptop's own printout, or a copy of one, is never valid. Returns {valid = false} for an unknown serial. Yields when the document isn't cached.

For a valid document, serial, title, issuer, issuerId, issuedByName and issuedAt (Unix seconds) are the original's, and copy is true for a photocopy. An invalid one only has kind and title.

Example:

lua
---@param serial string
---@return LaptopDocumentCheck check @ {valid, kind?, serial?, copy?, title?, issuer?, issuerId?, issuedByName?, issuedAt?}
local check = exports["zyke_laptop"]:VerifyDocument("DC-7F3K-92QD")

if (check.valid and check.issuerId == "police" and not check.copy) then
    -- An original from the police
end

#Get Document

Returns what a paper says: {id, kind, title, document, issuer?, issuedByName?, createdAt, sourceId?}, where document is the LaptopDocument and sourceId the original's serial on a copy. Returns nil for an unknown serial. Yields when the document isn't cached.

Example:

lua
---@param serial string
---@return LaptopDocumentView? view
local view = exports["zyke_laptop"]:GetDocument("DC-7F3K-92QD")

#Show Document

Shows a document on the player's screen, outside the laptop, as if they used its paper. Counts as the document being looked at, which keeps it from the retention sweep. Returns false for an unknown serial. Yields when the document isn't cached.

Example:

lua
---@param plyId integer
---@param serial string
---@return boolean shown
exports["zyke_laptop"]:ShowDocument(plyId, "DC-7F3K-92QD")

A paper item's serial is in its metadata as document, so a resource reads the paper a player hands over from the item and checks it with VerifyDocument.

#Server Local Events

These are local server events. Listen with AddEventHandler on the server. They cannot block an action.

#Mail Sent

Fires when mail is sent, by a player, a resource, an auto-reply or a bounce. author is the character who wrote it, nil for a resource. delivered holds the addresses it reached. sentAt is in milliseconds.

Example:

lua
---@param data {from: string, author?: string, to: string[], cc: string[], bcc: string[], subject: string, kind: MailKind, messageKey: string, delivered: string[], sentAt: integer}
AddEventHandler("zyke_laptop:mail:Sent", function(data)
    print(data.from, data.subject, #data.delivered)
end)

#Mail Received

Fires once for every address a mail reached. identifiers are the characters who read that address. folder is "inbox" or "spam".

Example:

lua
---@param data {address: string, identifiers: string[], from: string, subject: string, folder: MailFolder, kind: MailKind, messageKey: string, sentAt: integer}
AddEventHandler("zyke_laptop:mail:Received", function(data)
    print(data.address, data.from, data.subject)
end)

#Document Issued

Fires when IssueDocument handed out an original. plyId is nil when no player was given one.

Example:

lua
---@param plyId? integer
---@param serial string
---@param issuer string
---@param issuerId? string
AddEventHandler("zyke_laptop:documents:Issued", function(plyId, serial, issuer, issuerId) end)

#Document Printed

Fires when a player sends a print from a laptop to a printer. The paper is taken from the tray then, and the printout waits at the printer until the player collects it.

Example:

lua
---@param plyId integer
---@param serial string
---@param printerId integer
---@param copies integer
AddEventHandler("zyke_laptop:documents:Printed", function(plyId, serial, printerId, copies) end)

#Document Copied

Fires when a player photocopies a paper at a printer.

Example:

lua
---@param plyId integer
---@param copySerial string
---@param originalSerial string
---@param copies integer
AddEventHandler("zyke_laptop:documents:Copied", function(plyId, copySerial, originalSerial, copies) end)

#Printer Placed

Fires when a player sets a printer down.

Example:

lua
---@param plyId integer
---@param printerId integer
AddEventHandler("zyke_laptop:documents:PrinterPlaced", function(plyId, printerId) end)