Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
# CHANGELOG.md

## unreleased

- `sqlpage.send_mail` : add the ability to send HTML-formatted emails
- `sqlpage.send_mail` : add an optional `body_md` argument that renders Markdown to HTML and sends the message as a `multipart/alternative` with the raw Markdown as the plain-text body and the rendered HTML as the HTML body. When `body_md` is provided, `body` becomes optional.

## v0.45

- **SQLPage can now send emails** Configure the relay and optional authentication with `smtp_host`, `smtp_port`, `smtp_username`, `smtp_password`, `smtp_from`, and `smtp_tls_mode`, then call `sqlpage.send_mail` with a JSON message. It returns `{"status":"accepted"}` on SMTP acceptance or `{"status":"error","error_code":"...","error":"..."}` without stopping the request; SQL `NULL` is passed through without sending. Messages support multiple `to` and `cc` recipients, reply-to addresses, and data-URL attachments with a configurable combined decoded-size limit. SMTP passwords are redacted from startup debug logs.
- **SQLPage can now send emails** Configure the relay and optional authentication with `smtp_host`, `smtp_port`, `smtp_username`, `smtp_password`, `smtp_from`, and `smtp_tls_mode`, then call `sqlpage.send_mail` with a JSON message. It returns `{"status":"accepted"}` on SMTP acceptance or `{"status":"error","error_code":"...","error":"..."}` without stopping the request; SQL `NULL` is passed through without sending. Messages support multiple `to` and `cc` recipients, reply-to addresses, an optional HTML `body_html` alternative, and data-URL attachments with a configurable combined decoded-size limit. SMTP passwords are redacted from startup debug logs.
- **Release builds are slightly smaller and faster.** Unused dependency features have been removed. SQLPage now uses the maintained AWS Lambda HTTP runtime and avoids unused SQLx macros, configuration parsers, multipart derives, CSV serialization support, and build dependencies.
- **Configuration loading now includes only the documented JSON, JSON5, TOML, and YAML formats.** The `config` dependency previously enabled its default INI and RON parsers even though SQLPage never documented those formats. Undocumented `.ini` and `.ron` configuration files are no longer loaded; migrate them to a supported format before upgrading.
- **SQLPage functions can now be composed with database results.** Direct calls such as `SELECT sqlpage.url_encode(url) FROM links` already ran once per row. Per-row evaluation now also works through parentheses, concatenation, `COALESCE`, JSON constructors, and nested SQLPage functions. The database first decides which rows exist, then SQLPage evaluates the selected expression for each row. This enables patterns that were not previously possible, such as fetching only missing cached values or rendering a reusable SQL file with parameters from each row:
Expand Down
46 changes: 40 additions & 6 deletions examples/official-site/sqlpage/migrations/75_send_mail.sql
Original file line number Diff line number Diff line change
Expand Up @@ -103,17 +103,19 @@ All SMTP options can also be set with uppercase environment variables such as `S

### Message fields

The function takes one JSON object with three required fields:
The function takes one JSON object with two required fields:

- `to`: the recipient email address;
- `subject`: the email subject;
- `body`: the plain-text email body.
- `body`: the plain-text email body. Required unless `body_md` is provided.

It also accepts:

- `from`: overrides `smtp_from` for this message;
- `reply_to`: the address that receives replies;
- `cc`: a recipient who receives a visible copy;
- `body_html`: an HTML version of the body;
- `body_md`: a [Markdown](https://daringfireball.net/projects/markdown/) version of the body, rendered to HTML automatically;
- `attachments`: files to include with the message.

`to` and `cc` can each be either one address or an array of addresses. Addresses can include a display name, for example `"Jane Doe <jane@example.com>"`.
Expand Down Expand Up @@ -141,6 +143,37 @@ set result = sqlpage.send_mail(json_object(

The combined decoded size of all attachments is limited by `max_email_attachment_size`, which defaults to 10 MiB. This is separate from `max_uploaded_file_size` because attachments do not have to come from form uploads.

### HTML email

Set `body_html` to send an HTML version of the message alongside the plain-text `body`. The message is sent as a `multipart/alternative`: mail clients that prefer HTML show the HTML body, and clients that prefer text show the plain-text body.

```sql
set result = sqlpage.send_mail(json_object(
''to'', ''alice@example.com'',
''subject'', ''Welcome'',
''body'', ''Welcome to our service.'',
''body_html'', ''<p>Welcome to <strong>our service</strong>.</p>''
));
```

At least one of `body` or `body_md` is required. Include a meaningful plain-text alternative for deliverability and accessibility. SQLPage does not sanitize `body_html`: the SQL author is responsible for the HTML content. Email clients ignore scripts, and styles are often stripped or sandboxed.

### Markdown email

Set `body_md` to send a [Markdown](https://daringfireball.net/projects/markdown/) version of the body. SQLPage renders the Markdown to HTML and sends the message as a `multipart/alternative` with two parts: the raw Markdown as the plain-text body, and the rendered HTML as the HTML body. When `body_md` is provided, `body` becomes optional.

```sql
set result = sqlpage.send_mail(json_object(
''to'', ''alice@example.com'',
''subject'', ''Welcome'',
''body_md'', ''# Welcome\n\nWelcome to **our service**.''
));
```

When both `body` and `body_md` are provided, `body` is used as the plain-text alternative and the rendered `body_md` is used as the HTML alternative. `body_md` cannot be combined with `body_html`; use one or the other.

SQLPage renders Markdown using the same [GFM](https://github.github.com/gfm/) options as the `markdown` template helper, honoring the `markdown_allow_dangerous_html` and `markdown_allow_dangerous_protocol` configuration options.

### Contact form

```sql
Expand Down Expand Up @@ -201,7 +234,7 @@ SQLPage does not currently support:
- OAuth or XOAUTH2 authentication. If a provider only allows OAuth, it is not compatible with this function;
- CRAM-MD5, DIGEST-MD5, client-certificate authentication, or a per-server custom CA file;
- opportunistic STARTTLS, direct delivery to recipient mail servers, or receiving email;
- HTML email, a text/HTML alternative body, BCC, multiple reply-to addresses, or custom email headers;
- BCC, multiple reply-to addresses, or custom email headers;
- provider-specific headers for templates, tags, tracking, scheduling, idempotency, or metadata;
- DKIM signing inside SQLPage, S/MIME, or end-to-end encryption. The SMTP provider may add DKIM signatures;
- connection pooling, automatic retries, a persistent queue, scheduled sending, or a bulk-send API;
Expand All @@ -212,8 +245,9 @@ SQLPage does not currently support:

- SQL `NULL` is passed through: `sqlpage.send_mail(NULL)` returns SQL `NULL`, sends nothing, and does not log a warning.
- A JSON value other than an object and unknown or invalid message fields produce a JSON result with `status`, `error_code`, and `error` fields.
- `to`, `subject`, and `body` are required and cannot be JSON `null`.
- `from`, `reply_to`, and `cc` treat JSON `null` like an omitted field. If `from` is omitted, `smtp_from` must be configured.
- `to` and `subject` are required and cannot be JSON `null`. At least one of `body` or `body_md` must be provided.
- `from`, `reply_to`, `cc`, `body_html`, and `body_md` treat JSON `null` like an omitted field. If `from` is omitted, `smtp_from` must be configured. When `body_html` and `body_md` are both omitted, the message is plain text only.
- `body_md` cannot be combined with `body_html`.
- `attachments` can be omitted or an empty array. JSON `null` is not accepted for `attachments`.
- Empty recipient arrays, JSON `null` inside recipient arrays, invalid addresses, an empty attachment file name, invalid data URLs, and unknown attachment fields produce an error result with `status`, `error_code`, and `error`.
- Empty strings are allowed for `subject` and `body`, although an SMTP server may reject them.
Expand Down Expand Up @@ -250,6 +284,6 @@ VALUES (
'send_mail',
1,
'message',
'A JSON object containing the email to send. Required properties are `to` (an address or non-empty address array), `subject`, and `body`. Optional properties are `from` (required unless `smtp_from` or `SMTP_FROM` is configured), `reply_to`, `cc` (an address or non-empty address array), and `attachments` (an array of `{ "filename": "...", "data_url": "data:..." }` objects). Invalid JSON and invalid or unknown properties return `{ "status": "error", "error_code": "...", "error": "..." }`. SQL `NULL` returns SQL `NULL` without sending.',
'A JSON object containing the email to send. Required properties are `to` (an address or non-empty address array), `subject`, and either `body` or `body_md`. Optional properties are `from` (required unless `smtp_from` or `SMTP_FROM` is configured), `reply_to`, `cc` (an address or non-empty address array), `body` (the plain-text body; required unless `body_md` is provided), `body_html` (an HTML alternative body sent as `multipart/alternative` alongside `body`), `body_md` (a Markdown body rendered to HTML and sent as `multipart/alternative` with the raw Markdown as the plain-text alternative; cannot be combined with `body_html`), and `attachments` (an array of `{ "filename": "...", "data_url": "data:..." }` objects). Invalid JSON and invalid or unknown properties return `{ "status": "error", "error_code": "...", "error": "..." }`. SQL `NULL` returns SQL `NULL` without sending.',
'JSON'
);
2 changes: 1 addition & 1 deletion examples/sending emails/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ This builds SQLPage from the current repository checkout before starting the exa
Open http://localhost:8080 and choose one of two flows, then inspect the message in the Mailpit inbox at http://localhost:8025:

- **Simple email** sends to one recipient with the `SMTP_FROM` sender configured in Docker Compose.
- **Advanced email** demonstrates multiple recipients, Cc, Reply-To, a per-message sender override, and an uploaded attachment.
- **Advanced email** demonstrates multiple recipients, Cc, Reply-To, a per-message sender override, an HTML alternative body, and an uploaded attachment.

The SMTP server is configured in [`docker-compose.yml`](./docker-compose.yml) with `SMTP_HOST=mailpit`, `SMTP_PORT=1025`, `SMTP_TLS_MODE=none`, and a default `SMTP_FROM`. Plaintext mode is intended only for trusted local SMTP servers such as Mailpit.

Expand Down
1 change: 1 addition & 0 deletions examples/sending emails/advanced.sql
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ select 'email' as type, 'sender' as name, 'From override' as label, 'advanced@ex
select 'email' as type, 'reply_to' as name, 'Reply-To' as label, 'replies@example.com' as value, true as required;
select 'subject' as name, 'Subject' as label, 'Advanced SMTP demo' as value, true as required;
select 'textarea' as type, 'body' as name, 'Message' as label, 'Sent to a team with an attachment' as value, true as required;
select 'textarea' as type, 'body_html' as name, 'HTML body (optional)' as label, '<p>Sent to a team with an <strong>attachment</strong></p>' as value;
select 'file' as type, 'attachment' as name, 'Attachment' as label, true as required;

select 'button' as component;
Expand Down
2 changes: 1 addition & 1 deletion examples/sending emails/index.sql
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ select
'simple.sql' as link;
select
'Advanced email' as title,
'Add multiple recipients, Cc, Reply-To, a sender override, and an uploaded attachment.' as description,
'Multiple recipients, Cc, Reply-To, a sender override, an HTML body, and an uploaded attachment.' as description,
'advanced.sql' as link;
1 change: 1 addition & 0 deletions examples/sending emails/send-advanced.sql
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ set message = json_object(
'reply_to', :reply_to,
'subject', :subject,
'body', :body,
'body_html', NULLIF(:body_html, ''),
'attachments', json_array(json_object(
'filename', $attachment_name,
'data_url', $attachment_data_url
Expand Down
2 changes: 2 additions & 0 deletions examples/sending emails/test.hurl
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ sender: advanced@example.com
reply_to: replies@example.com
subject: Advanced SMTP demo
body: Sent to a team with an attachment
body_html: <p>Sent to a team with an <strong>attachment</strong></p>
attachment: file,test-attachment.txt; text/plain
HTTP 200
[Asserts]
Expand All @@ -89,6 +90,7 @@ jsonpath "$.To[1].Address" == "second@example.com"
jsonpath "$.Cc[0].Address" == "team@example.com"
jsonpath "$.ReplyTo[0].Address" == "replies@example.com"
jsonpath "$.Text" contains "Sent to a team with an attachment"
jsonpath "$.HTML" contains "Sent to a team with an"
jsonpath "$.Attachments[0].FileName" == "test-attachment.txt"
jsonpath "$.Attachments[0].ContentType" == "text/plain"

Expand Down
13 changes: 13 additions & 0 deletions src/template_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,19 @@ impl MarkdownConfig for AppConfig {
}
}

/// Renders a markdown source string to HTML using the application's markdown configuration.
/// Uses the same GFM options as the `markdown` Handlebars helper with the "default" preset.
pub fn render_markdown_to_html(
config: &impl MarkdownConfig,
markdown_src: &str,
) -> Result<String, String> {
let mut options = markdown::Options::gfm();
options.compile.allow_dangerous_html = config.allow_dangerous_html();
options.compile.allow_dangerous_protocol = config.allow_dangerous_protocol();
options.compile.allow_any_img_src = true;
markdown::to_html_with_options(markdown_src, &options).map_err(|e| e.to_string())
}

/// Helper to render markdown with configurable options
#[derive(Default)]
struct MarkdownHelper {
Expand Down
Loading