Portal Community

Node Configuration

{
  "nodeType": "SendEmail",
  "name": "sendApprovalEmail",
  "config": {
    "credentialId": 7,
    "provider": "smtp",
    "from": "noreply@acme.com",
    "to": "$output.fetchEmployee.email",
    "cc": [],
    "bcc": [],
    "subject": "Invoice $output.createInvoice.invoiceNumber requires your approval",
    "bodyHtml": "<h2>Hello {{$output.fetchEmployee.firstName}},</h2><p>Please review invoice ...</p>",
    "bodyText": "Hello $output.fetchEmployee.firstName, please review invoice ...",
    "attachments": [
      {
        "filename": "invoice-$output.createInvoice.invoiceNumber.pdf",
        "contentBase64": "$output.generatePdf.base64Content",
        "contentType": "application/pdf"
      }
    ],
    "retryCount": 2,
    "retryDelayMs": 2000
  }
}

Configuration Fields

FieldTypeDescription
credentialIdintSMTP password or SendGrid API key via ICredentialResolver
providersmtp | sendgridEmail delivery provider. Determines which adapter implementation is used.
fromstring / exprSender address. SMTP: must match authenticated mailbox. SendGrid: must be a verified sender.
tostring / string[] / exprRecipient(s). Comma-separated string or array. Expressions evaluated per recipient.
ccstring[]CC recipients (optional).
bccstring[]BCC recipients (optional).
subjectstring / exprEmail subject line. Expression syntax supported.
bodyHtmlstring / exprHTML body. Template syntax: {{expression}} interpolation.
bodyTextstring / exprPlain-text fallback body.
attachmentsarrayArray of attachment objects with filename, contentBase64, contentType.

Provider Differences

AspectSMTPSendGrid
CredentialSMTP password (STARTTLS)SendGrid API key
Rate limitingServer-side throttlePlan-based daily limit
TrackingNo built-inOpens, clicks, bounces via webhook
AttachmentsUp to 25 MB totalUp to 30 MB total
TemplatingBFAI expression engineBFAI expression engine OR SendGrid dynamic templates

EmailChannelAdapter

public class EmailChannelAdapter : IMessagingChannelAdapter
{
    public string ChannelType => "Email";

    public async Task<MessagingResult> SendAsync(
        MessagingMessage message,
        MessagingConfig config,
        CancellationToken ct)
    {
        if (config.Provider == "sendgrid")
            return await SendViaSendGrid(message, config, ct);
        else
            return await SendViaSmtp(message, config, ct);
    }

    private async Task<MessagingResult> SendViaSmtp(
        MessagingMessage message, MessagingConfig config, CancellationToken ct)
    {
        var password = await _credentials.GetPasswordAsync(config.CredentialId, ct);
        using var client = new SmtpClient();
        await client.ConnectAsync(config.SmtpHost, config.SmtpPort, SecureSocketOptions.StartTls, ct);
        await client.AuthenticateAsync(config.SmtpUsername, password, ct);

        var email = new MimeMessage();
        email.From.Add(MailboxAddress.Parse(config.From));
        foreach (var recipient in config.To)
            email.To.Add(MailboxAddress.Parse(recipient));
        email.Subject = message.Subject;

        var builder = new BodyBuilder { HtmlBody = message.BodyHtml, TextBody = message.BodyText };
        foreach (var att in message.Attachments ?? [])
            builder.Attachments.Add(att.Filename,
                Convert.FromBase64String(att.ContentBase64),
                ContentType.Parse(att.ContentType));
        email.Body = builder.ToMessageBody();

        await client.SendAsync(email, ct);
        await client.DisconnectAsync(true, ct);

        return new MessagingResult
        {
            MessageId = email.MessageId,
            Channel = "email",
            Recipient = string.Join(", ", config.To),
            SentAt = DateTimeOffset.UtcNow
        };
    }
}
Multiple recipients: When to is an expression returning an array (e.g., $output.fetchApprovers.emails), the executor sends one email with all addresses in the To field. To send individual emails per recipient, use a ForEach node wrapping a single-recipient SendEmail node.