Skip to main content

Report Mailing: sending renderings by mail

A rendered report usually has somewhere to go. Until now VeloxFactory could hand itgo: back to the caller as Base64,Base64 or as a URL orURL, straight into a print task.task, Reportor Mailing adds the fourth destination:into the mailbox of whoever needs the document. Report Mailing is that last destination. A render request carries an optional mailing segment, and once the render succeeded VeloxFactory sends an HTML mail through a configured mailer, using a configured template, with the PDF attached and, if you want it, an xlsx export of the same data.

NothingMailing aboutis renderingan changes.addition to a render, never a condition of it. A request without a mailing segment behavesis exactlya asplain it always did,render, and a mail that fails never turns a successful render into an error.

Screenshot placeholder: mailing.history-record-modal.png
The mail dialog of a Report History Record with mailer, template, recipients, attachments and the background switch.

The three parts

Mailing is built from three things you configure once and one thing you send per render.

Part What it holds
Mailer The SMTP account: host, port, credentials, sender address, and the dispatch rate limits your provider allows. Master data under Configuration.
Mail Template Subject and body, written in a WYSIWYG editor, with placeholders and optional whitelabeling. Master data under Configuration.
Mailing segment Per request: which mailer, which template, who receives it, what is attached, and whether it goes out immediately or through the queue.
Report Mail Task The record of one send attempt: recipients, the rendered subject and body, the attachments, the status and, if something went wrong, the error.

Mailing from a render request

The mailing segment is optional and sits next to the fields you already know:

{
    "outputType": "base64",
    "parameters": { "P_ORDER_NO": "4711" },
    "createPrintTask": false,
    "createHistoryRecord": true,
    "mailing": {
        "mailer": "Office SMTP",
        "mailTemplate": "Report Delivery",
        "to": ["disposition@example.com", "[data.first.customerEmail]"],
        "cc": [],
        "bcc": ["archive@example.com"],
        "contactName": "[data.first.customerName]",
        "includePdf": true,
        "pdfFileName": "Order_[parameters.P_ORDER_NO]",
        "includeExcel": true,
        "includeParameters": true,
        "excelFileName": "Data_[traceId]",
        "sendAsync": false
    }
}

mailer and mailTemplate accept either the numeric ID or the unique name of the record, so a request stays readable. Only active records are resolved.

Field Required Description
mailer yes ID or unique name of an active Mailer.
mailTemplate yes ID or unique name of an active Mail Template.
to, cc, bcc to: yes Address lists. Literal addresses are validated before the render is spent, addresses containing a placeholder are resolved afterwards.
contactName no Free text for a personal greeting, available in the template as [contactName]. May itself be a placeholder.
includePdf no Default true. Set it to false for a notification mail that only carries the xlsx, or nothing at all.
pdfFileName no Base name the PDF carries as an attachment of the mail, without extension. The stored file keeps its own name, nothing is copied.
includeExcel no Default false. Builds an xlsx from the rows that were actually rendered.
includeParameters no Prints the parameters as a block above the data table in the xlsx.
excelFileName no Base name the xlsx carries as an attachment, same rules as pdfFileName.
sendAsync no Default false. true hands the mail to the queue and answers immediately.
ℹ️ A failed mail never fails the render. Whatever goes wrong with the mailing, the render response stays a success and the detail lands in meta.mailing and on the mail task. Your integration keeps its PDF.

The one combination that is refused up front is outputType: "preview" together with mailing: a preview render deletes its own file immediately, so there would be nothing to attach.


The Excel attachment

includeExcel turns the data of the render into a real xlsx, not a CSV with a different name. The rows come from what was actually rendered, which matters for reports fed by an SQL adapter: there the rows only exist in the render result, never in your request.

The sheet has a header row in the report's accent colour, frozen below the header, with an auto filter across the table. Columns are typed from the report's field definitions, so an integer is a number, a date is a real Excel date and a decimal keeps its format. For a dynamic array without field definitions the type is inferred from the values. Rows with differing keys are merged into one header, in first-seen order, and missing values stay empty.

With includeParameters the parameters are printed above the table, so the recipient can see what the report was asked for without opening the mail again.

If the render produced a history record, the xlsx is stored on it and stays downloadable from the history record page. It is cleaned up by the same retention jobs as the PDF.


Placeholders in recipients and attachment names

Subject and body have always beenare placeholder-driven.driven, Inand theso final implementationare the recipients, the contact name and the attachment names. They all accept the same catalog, which is what makes mailing useful for data that only exists after the render.

"to": ["[data.first.customerEmail]"],
"contactName": "[data.first.customerName]",
"pdfFileName": "Lieferschein_[parameters.P_ORDER_NO]_[now.date]"

For a report with an SQL adapter, [data.first.<column>] addresses the rows the query fetched. The recipient list is therefore a result of the render, not an input to it.

A few rules are worth knowing:

Situation Behaviour
A placeholder holds several addresses Comma or semicolon separated values are split, trimmed and de-duplicated. One column can address a whole distribution list.
A cc or bcc placeholder resolves to nothing Dropped silently. An unknown placeholder is reported in meta.mailing.unresolvedPlaceholders.
No valid to address is left No mail is sent, the reason is in meta.mailing.errors. The render is unaffected.
Column names Matched exactly as the adapter returns them. [data.first.EMAIL] is not [data.first.email].
Attachment names Resolved, then made file-system safe with case and underscores preserved. Umlauts are transliterated, invalid characters collapse to a single hyphen, the extension is forced.
ℹ️ The name is a label, not a file. pdfFileName and excelFileName only change what the attachment is called in the mail. On disk the PDF and the xlsx are always stored under the trace id, they hang on the history record, stay downloadable from it and are deleted with it. Two mails may carry the same attachment name without anything being overwritten.

Without a requested name the attachment is called like the stored file, which is the trace id plus its extension.


Immediately or through the queue

By default the mail is sent while the request is still open. The response then already tells you whether it went out. That is the right mode for a single document and a fast relay.

sendAsync: true creates the mail task, hands it to the queue and answers immediately. The mail is sent by a Horizon worker, the task moves from Pending to Sent, and a failure is retried with backoff before it ends as Error. Use it for bulk runs and for slow or unreliable SMTP servers, where waiting for the transport would block the render.

⚠️ Asynchronous sending needs a running queue. Without php artisan horizon the mail task stays Pending forever. It is visible in the Mail Queue, so nothing is lost, but nothing is sent either.

There is a third case you do not ask for: if the mailer's rate limit is exhausted, a synchronous send is converted into a delayed queued send instead of failing. The mail is not lost, the response says so, and the task shows when it will go out. See the Mailers page for the windows.


Mailing from the frontend

The same settings are available in threefour places in the UI, built from one shared block of fields, so they look and behave identically everywhere.

Place How it works
Generate PDF A Send results via Mail toggle in the Output Settings. Configure it once, and every render from that page mails its result.
Scan2Print The same toggle in the station configuration. Every scan then prints and mails.
Report History Record A Send Mail button that mails a document which was rendered earlier, with a freely chosen mailer, template and recipients.
Job Scheduler The same toggle on a render job. Every run of the schedule mails its result.

In the two configuration modals the mailing settings live in the session alongside the rest of the output settings, and the values are kept even while the toggle is off, so switching mailing off for a moment loses nothing. The file name fields appear only while their attachment switch is on.

After a render the notification says what happened to the mail. A mail that did not go out as configured turns the success notification into a warning instead of hiding the problem.

The block is only rendered at all when the current user may create mail tasks and at least one active mailer and one active template exist. Nobody is offered a button that cannot work.

Screenshot placeholder: mailing.generate-pdf-settings.png
Output Settings of the Generate PDF page with the mailing block expanded.

On a schedule

A mail does not need a caller. A render job in the Job Scheduler carries the same mailing settings as a render request, so a schedule puts the shift report in a mailbox at six every morning without anybody asking for it.

Two things work differently from an interactive send. The schedule's owner and API token decide what may go out: the token needs report-mail-task:create, and a revoked token or a deactivated owner parks the schedule rather than mailing. And placeholders are resolved at the moment the run fires, so [now.date] in a subject line or an attachment name carries the date of that run, not the date the schedule was saved.

Every run is logged with its trace id. A mail task that ends in Error, or anything reported in meta.mailing, marks the run as a warning, so a schedule whose mail is not arriving is visible in the run log without opening the Mail Queue.

ℹ️ Two different mails. The mail configured here is the document itself. A schedule that could not be executed at all sends the Job Scheduler's own failure mail to its own recipients. See Schedule your jobs.

The Mail Queue

Every send attempt, synchronous or queued, is recorded as a Report Mail Task. The overview lives in the main menu under Mail Queue and is a regular VeloxFactory lookup with the usual filters and the usual session-persisted state.

A mail task keeps what was actually sent, not just what was requested: the resolved recipients, the rendered subject, the rendered body and the names of the attachments. The detail page shows the mail exactly as the recipient received it, rendered in an iframe from the stored HTML, whitelabeling included.

Status Meaning
Pending Queued, or waiting for a free rate limit slot. The task shows when it is due.
Sent The transport accepted the message, with the timestamp. Delivery itself is not tracked.
Error The send failed after its retries. The transport's own message is on the task.

Repeat resends a task. It opens a dialog in which mailer and recipients can be changed before sending, so a mail that went to the wrong address is corrected rather than cloned. Subject and body are re-rendered from the current template when the history record still exists, otherwise the stored snapshot is reused. The repeated task gets its own trace id, derived from the original.

The same table also appears on the Report History Record page, so the mails belonging to one document are visible where the document is.

Screenshot placeholder: mailing.mail-queue.png
Mail Queue overview with status badges, recipients and the repeat action.

Retention

Mail tasks are purgedcleaned likeup by the Purge Job, the consolidated cleanup schedule of the Job Scheduler. It removes print tasks, on their own scheduletasks and mail tasks before the history records they reference:reference, so nothing is deleted out from under a reference. How long mail tasks are kept is a retention on that schedule, editable in the frontend or through the API like everything else; an empty value switches the step off entirely.

The same work is available as a standalone command:

php artisan report-mail-tasks:purge --days=30

The retention is configured with PURGE_MAILTASKS_DAYS and the job runs nightly. Attachments are not owned by the mail task;task. theThe PDF and the xlsx belong to the history record and are removed by its ownretention retention,step, or, if no history record was created, by the orphaned-file purge.step.


The API

Besides the mailing segment on POST /report-config/{id}/render, mailing has two endpoints of its own.

Endpoint Description
POST /report-history-record/{id}/mail Mail a document that was rendered earlier. Takes the same fields as the mailing segment, including placeholders in recipients and attachment names. Builds the xlsx on the fly if the record does not have one yet.
GET /report-mail-task List mail tasks, filterable by status.
GET /report-mail-task/{id} A single mail task, with its relations and its audit segment on request.
POST /report-mail-task Create and dispatch a mail task directly.
POST /report-mail-task/{id}/repeat Resend a task. Optional mailer, to, cc and bcc override the original values, anything left out is reused.
DELETE /report-mail-task/{id} Delete a mail task. The attachments belong to the history record and are not touched.

A render that mailed something returns the mail task in its response, and anything noteworthy about the mailing in meta.mailing:

Key Meaning
errors The mail was not sent, with the reason.
skipped The render failed, so there was nothing to send.
excelError The xlsx could not be built. The mail goes out with the PDF only.
unresolvedPlaceholders Placeholders in recipients, contact name or attachment names that resolved to nothing.

AI assistants

The MCP server exposes mailing the same way it exposes printing: the mailing segment is part of the render tool, and mail tasks, mailers and templates have their own tools. Because mailer and template are resolved by name, an assistant can pass what a person said. Render the delivery note for order 4711 and mail it to dispatch is one call.


Permissions

Mailing addshas three permission scopes, each with the usual read, create, update, delete and full.

Scope Grants
mailer:* The Mailers master data.
mail-template:* The Mail Templates master data.
report-mail-task:* Seeing the Mail Queue, and create for actually sending mail, from the API as well as from the frontend.

report-mail-task:create is the one that matters for sending. It is checked at the start of a render, before anything is rendered, so a user without it gets a clear refusal instead of a wasted render.

Mail graphics are Common Report Resources, so maintaining them uses common-report-resource:*. That permission also covers the logos printed on labels, which is the price for uploading a logo once rather than twice.