Skip to content

Sending Emails

PlaceOS provides two primary drivers for sending email:

  • SMTP Mailer — sends direct emails
  • Template Mailer — resolves templates and delegates delivery to another mailer (typically SMTP)

Email delivery is exposed through the shared:

PlaceOS::Driver::Interface::Mailer

Drivers that want to publish available template fields implement:

PlaceOS::Driver::Interface::MailerTemplates

Typical deployment:

Custom Driver
│ send_template(...)
Template Mailer
│ send_mail(...)
SMTP Mailer
SMTP Server
Custom Driver → SMTP Mailer → SMTP Server
Custom Driver → Template Mailer → SMTP Mailer → SMTP Server

Driver:

drivers/place/smtp.cr

Interface:

PlaceOS::Driver::Interface::Mailer

The SMTP mailer sends standard email messages with:

  • Plain text content
  • HTML content
  • Attachments
  • Inline resources
  • CC / BCC
  • Custom sender and reply-to

Example settings:

---
sender: "support@example.com"
host: "smtp.example.com"
port: 587
tls_mode: "STARTTLS"
username: "smtp-user"
password: "smtp-password"
ssl_verify_ignore: false
SettingDescription
senderDefault from address
hostSMTP server hostname
portSMTP port
tls_modeNONE, SSL, or STARTTLS
usernameSMTP username (optional)
passwordSMTP password (optional)
ssl_verify_ignoreDisable certificate verification

Use when subject and content are known at runtime.

require "placeos-driver"
require "placeos-driver/interface/mailer"
class Example::AlertMailer < PlaceOS::Driver
descriptive_name "Example Alert Mailer"
generic_name :AlertMailer
def mailer
system.implementing(
PlaceOS::Driver::Interface::Mailer
)[0]
end
def send_alert(
email : String,
room_name : String,
fault : String
)
mailer.send_mail(
to: email,
subject: "Fault detected in #{room_name}",
message_plaintext:
"Fault detected: #{fault}",
message_html: <<-HTML
<h1>Fault detected</h1>
<p>Room: <strong>#{room_name}</strong></p>
<p>Fault: #{fault}</p>
HTML
)
end
end

send_mail(
to : String | Array(String),
subject : String,
message_plaintext : String? = nil,
message_html : String? = nil,
resource_attachments :
Array(ResourceAttachment) = [] of ResourceAttachment,
attachments :
Array(Attachment) = [] of Attachment,
cc : String | Array(String) = [] of String,
bcc : String | Array(String) = [] of String,
from : String | Array(String) | Nil = nil,
reply_to : String | Array(String) | Nil = nil,
)

Attachments must be Base64 encoded.

require "base64"
def send_report(
email : String,
pdf_bytes : Bytes
)
mailer.send_mail(
to: email,
subject: "Daily report",
message_plaintext:
"Attached is your report.",
attachments: [
{
file_name: "report.pdf",
content: Base64.strict_encode(pdf_bytes),
}
]
)
end

Used for embedded images in HTML.

require "base64"
def send_badge(
email : String,
png_bytes : Bytes
)
mailer.send_mail(
to: email,
subject: "Badge",
message_html:
%(<img src="cid:badge_image" />),
resource_attachments: [
{
file_name: "badge.png",
content: Base64.strict_encode(png_bytes),
content_id: "badge_image",
}
]
)
end

Driver:

drivers/place/template_mailer.cr

Interface:

PlaceOS::Driver::Interface::MailerTemplates

The Template Mailer:

  1. Receives send_template(...)
  2. Finds matching template
  3. Expands fields
  4. Sends via downstream mailer

Typically:

Template Mailer → SMTP Mailer

Templates are resolved in order:

  1. Level
  2. Building
  3. Region
  4. Organisation

The most specific match is used.


Custom drivers define available fields using:

PlaceOS::Driver::Interface::MailerTemplates

This enables UI configuration of templates.


require "placeos-driver"
require "placeos-driver/interface/mailer"
require "placeos-driver/interface/mailer_templates"
class Example::BookingMailer < PlaceOS::Driver
include PlaceOS::Driver::Interface::MailerTemplates
descriptive_name "Booking Mailer"
generic_name :BookingMailer
def mailer
system.implementing(
PlaceOS::Driver::Interface::Mailer
)[0]
end
def template_fields
[
TemplateFields.new(
trigger: {"bookings", "approved"},
name: "Booking approved",
description:
"Sent when booking approved",
fields: [
{
name: "user_name",
description: "Recipient name"
},
{
name: "room_name",
description: "Room name"
},
{
name: "start_time",
description: "Booking start"
}
]
)
]
end
end

Use when subject and content are externally managed.

def send_booking_email(
email : String,
user_name : String,
room_name : String,
start_time : String
)
mailer.send_template(
to: email,
template:
{"bookings", "approved"},
args: {
"user_name" => user_name,
"room_name" => room_name,
"start_time" => start_time,
}
)
end

send_template(
to : String | Array(String),
template : Tuple(String, String),
args : TemplateItems,
resource_attachments :
Array(ResourceAttachment) = [] of ResourceAttachment,
attachments :
Array(Attachment) = [] of Attachment,
cc : String | Array(String) = [] of String,
bcc : String | Array(String) = [] of String,
from : String | Array(String) | Nil = nil,
reply_to : String | Array(String) | Nil = nil,
)

Templates use:

%{field_name}

Example metadata:

[
{
"trigger": "bookings.approved",
"subject":
"Booking approved for %{room_name}",
"text":
"Hi %{user_name}, your booking starts at %{start_time}.",
"html":
"<p>Hi %{user_name}</p><p>Your booking starts at %{start_time}</p>",
"from":
"bookings@example.com",
"reply_to":
"support@example.com"
}
]

If the Template Mailer:

  • cannot find metadata template

then:

send_template(...)

is forwarded to the downstream mailer.

This allows fallback templates defined in:

email_templates:

---
email_templates:
bookings:
approved:
subject:
"Booking approved for %{room_name}"
text:
"Hi %{user_name}, your booking starts at %{start_time}"
html:
"<p>Hi %{user_name}</p><p>Your booking starts at %{start_time}</p>"

  • content generated in driver
  • no UI-editable templates required
  • simple delivery logic
mailer.send_mail(...)

  • content managed outside driver
  • templates editable in UI
  • reusable email formats required
mailer.send_template(...)

  • Always resolve the mailer using:
system.implementing(
PlaceOS::Driver::Interface::Mailer
)
  • Prefer templates for user-facing emails
  • Use direct mail for operational alerts
  • Base64 encode all attachments
  • Provide descriptive template fields
  • Keep template triggers consistent