A self-contained compose dialog. Recipients come from a directory you supply, addresses are de-duplicated and cleaned, the note is capped and escaped, and every field is handed back to you in OnSend. It owns no connector, so you choose Outlook, a flow, or SMTP.
This component requires Modern controls to be enabled. Settings → Updates → Preview → Modern controls and themes
Title / ContextLine / DefaultSubject; the toggles map to Config.A compose-email dialog for Canvas apps: directory-backed To and CC pickers, priority tags, a lockable subject, a note with a character counter, and a finished HTML email body ready to send.
Owns no connector. The component queries nothing and sends nothing. It
builds every output and raises OnSend, and the host decides what sending
means. That is what lets it drop into any app and work immediately against the
sample directory.
OnSend hands you ToAddresses, Subject and Body.Directory is an input. The component exposes what the
sender typed as SearchText and leaves the query, the connector and the
minimum-character rule to you.Busy while your send runs,
because only you know when it finished.cmpSendEmail.yamlVisible to your open flagThree things a paste does not carry, all Power Apps behaviour rather than anything about this component:
Width by hand. A pasted instance arrives at 600
regardless of the property. Height survives; Width does not.Config explicitly if you are customising it. A Record input's
default does not reach a pasted instance.ModernCombobox.Directory is an ordinary table input, so both patterns work with no change to
the component. Pick based on how big your tenant is.
// App.OnStart
ClearCollect(
colDirectory,
Office365Users.SearchUser({ searchTerm: "", top: 500 })
);
// Instance
Directory: =colDirectory
One call at startup, instant filtering after that, and the picker works offline once loaded. Right for a few hundred people.
Directory: |-
=Office365Users.SearchUser(
{ searchTerm: Trim(cmpSendEmail_1.SearchText), top: 50 }
)
SearchUser returns DisplayName, Mail and JobTitle among others, so it is
a superset of what Directory expects and drops straight in with no projection.
A blank searchTerm returns a default set rather than erroring, so the picker
opens populated and narrows as the sender types. If you would rather not spend a
call on an empty box:
Directory: |-
=If(
Len(Trim(cmpSendEmail_1.SearchText)) >= 3,
Office365Users.SearchUser(
{ searchTerm: Trim(cmpSendEmail_1.SearchText), top: 50 }
)
)
The version worth building for a real app:
Directory: |-
=If(
IsBlank(Trim(cmpSendEmail_1.SearchText)),
colRecentRecipients,
Office365Users.SearchUser(
{ searchTerm: Trim(cmpSendEmail_1.SearchText), top: 50 }
)
)
Build colRecentRecipients from your own audit rows. The common case becomes one
tap with no connector call at all.
DelayOutput is already set on both pickers, so SearchText updates when
the sender pauses rather than on every keystroke. Without it you would fire one
call per character against a connector that throttles per user per connection.Mail. The
dialog counts these and warns before the send, and drops them from
ToAddresses. You will see them far more against a real tenant than against
the sample rows.top is the only cap that matters. There is nothing to delegate here:
SearchUser is an action, not a tabular source, so the server-side filtering
is the searchTerm parameter and no delegation warning will ever appear.=Set(gblSendBusy, true);
Set(gblSendFailed, false);
IfError(
Office365Outlook.SendEmailV2(
cmpSendEmail_1.ToAddresses,
cmpSendEmail_1.Subject,
cmpSendEmail_1.Body,
{
Cc: cmpSendEmail_1.CcAddresses,
Importance: If(cmpSendEmail_1.Priority = "Urgent", "High", "Normal")
}
),
Set(gblSendFailed, true);
Notify("Could not send. " & FirstError.Message, NotificationType.Error)
);
Set(gblSendBusy, false);
If(
!gblSendFailed,
Notify(
"Sent to " & cmpSendEmail_1.RecipientCount & " recipient(s).",
NotificationType.Success
);
Set(gblSendOpen, false);
Reset(cmpSendEmail_1)
)
// OnCancel
=Set(gblSendOpen, false);
Reset(cmpSendEmail_1)
Reset() only on the success path. A failed send that also clears the dialog
throws away recipients and a note the sender just typed, with no way back. On
failure the dialog stays open, still populated, and Busy is cleared so they can
try again.
Audit off Note, not Body. Body is escaped markup and will read as
< soup in an audit trail.
| Property | Type | Default | Notes |
|---|---|---|---|
Busy | Boolean | false | Set this true from the host while your send is running and false when it finishes. The dialog disables every control and shows a sending state. |
Config | Record | see below | Look and behaviour. Theme is light or dark. |
ContextHtml | Text | "" | HTML placed above the note in the body. Use it for a record summary. |
ContextLine | Text | "" | One short line shown under the dialog title, so the sender can see what the mail is about. Not sent. |
DefaultSubject | Text | "" | Prefills the subject box. With Config.LockSubject true it becomes read only, which is what you want when a reply has to be traceable back to a record. |
Directory | Table | see below | The people the pickers offer. Needs DisplayName and Mail columns; JobTitle is shown when present. |
Signature | Text | "" | Plain text appended to the bottom of the body. Blank omits the line entirely. |
Title | Text | "New message" | Dialog heading. |
Config defaults:
{
Theme: "light",
Accent: "",
Radius: 16,
Width: 480,
MaxNote: 2000,
ShowPriority: true,
ShowCc: true,
LockSubject: false
}
Accent is a hex string; blank falls back to blue. Every key is Coalesced
against these values, so a partial record is safe.
| Output | Type | Notes |
|---|---|---|
Body | Text | The finished HTML body. Read this in OnSend and hand it to your mail action. |
CcAddresses | Text | Semicolon separated. Blank addresses dropped, duplicates collapsed, and anyone already on the To line removed. |
Note | Text | The plain text the sender typed, trimmed and unescaped. Log this rather than Body, which is markup. |
Priority | Text | Urgent, FYI, Reminder, or blank. Only a label; deciding what Urgent means to your mail action is the host's job. |
RecipientCount | Number | How many usable addresses ended up on the To line, after blanks and duplicates were removed. |
SearchText | Text | What the sender has typed into whichever picker they are using, Cc taking precedence when it is not blank. Bind Directory to a formula that reads this to get a live directory search. |
Subject | Text | The subject as sent, priority prefix included. |
ToAddresses | Text | Semicolon separated. Blank addresses dropped and duplicates collapsed. |
| Event | Fires when | Read in handler |
|---|---|---|
OnSend | Send is pressed and every output is populated | ToAddresses, CcAddresses, Subject, Body, Note, Priority, RecipientCount |
OnCancel | Dismissed by the scrim, the close button or Cancel | — |
Body is a complete table-based HTML email, not a string of <b> tags. Accent
rule across the top, a priority pill when one is set, the subject as a heading,
your ContextHtml in a tinted box, the note, then a rule and the signature.
Tables and inline styles only, because Outlook on Windows renders mail through
Word. No flexbox, no grid, no <style> block, no web fonts. border-radius
degrades to square corners there, which is the intended floor.
| Slot | Treatment | Why |
|---|---|---|
| The note | escaped | typed by a sender |
Signature | escaped | documented as plain text |
ContextHtml | raw | your own markup |
That asymmetry is what makes a record summary possible. It also means any
user-typed text you interpolate into ContextHtml is yours to escape. A
request title pulled from a list is the usual candidate.
The one to copy first. A label/value table survives every mail client without a single div.
<table role='presentation' cellpadding='0' cellspacing='0' border='0' width='100%'>
<tr><td style='padding:2px 0;color:#64748B;width:90px;'>Request</td><td style='padding:2px 0;color:#0F172A;font-weight:600;'>REQ-2026-000123</td></tr>
<tr><td style='padding:2px 0;color:#64748B;'>Status</td><td style='padding:2px 0;color:#0F172A;'>In review</td></tr>
<tr><td style='padding:2px 0;color:#64748B;'>Owner</td><td style='padding:2px 0;color:#0F172A;'>Ava Chen</td></tr>
</table>
The link is wrapped in a one-cell table rather than styled directly, because
Outlook will not paint a background or a radius on a bare <a>. It degrades to a
plain blue rectangle with square corners, which is still a button.
<table role='presentation' cellpadding='0' cellspacing='0' border='0'><tr>
<td bgcolor='#3B82F6' style='background-color:#3B82F6;border-radius:6px;'>
<a href='https://example.com/item/123' style='display:inline-block;padding:10px 18px;color:#FFFFFF;font-size:13px;font-weight:600;text-decoration:none;'>Open the request</a>
</td>
</tr></table>
<div style='margin-bottom:10px;'>
<span style='display:inline-block;padding:3px 10px;border-radius:999px;background-color:#FEF3C7;color:#92400E;font-size:11px;font-weight:600;'>ON HOLD</span>
<span style='color:#64748B;font-size:12px;'>was In review</span>
</div>
<div style='color:#334155;'>Paused pending budget confirmation.</div>
Attributes are single-quoted throughout. Double quotes each need doubling inside a Power Fx string and a template this size becomes unreadable; every mail client accepts single quotes.
AccessibleLabel on modern controls and
by Tooltip on classic ones, which have no AccessibleLabel property.| Recipients | No cap in the component. Your mail action will have one. |
| Note length | Config.MaxNote, default 2000, with a counter that warms amber at 90%. |
| Instances per screen | One. Dialog state lives in component-scoped globals. |
| Directory size | Whatever your Directory formula returns. Live search is capped by top. |
One email when something new ships. Unsubscribe anytime.
Use the toolbar to format · or type markdown directly