Skip to main content

Basic Email Sending

EmailEngine simplifies sending emails through registered accounts' SMTP servers. This guide covers the fundamentals of sending emails using the submit API.

Why It Matters

When your SaaS needs to send email on behalf of a customer, direct SMTP is brittle: every provider has its own auth, rate limits, retries, and error codes. EmailEngine shields you from that complexity by exposing a single REST endpoint that proxies the customer's mailbox. You get consistent JSON responses and robust retry logic.

Step-by-Step Guide

1. Register the Account

Before sending, register an email account in EmailEngine using the account registration API.

Endpoint: POST /v1/account

curl -XPOST "http://127.0.0.1:3000/v1/account" \
-H "Authorization: Bearer <your-token>" \
-H "Content-Type: application/json" \
-d '{
"account": "example",
"name": "Andris Reinman",
"email": "andris@example.com",
"imap": {
"auth": { "user": "andris", "pass": "secretpass" },
"host": "mail.example.com",
"port": 993,
"secure": true
},
"smtp": {
"auth": { "user": "andris", "pass": "secretpass" },
"host": "mail.example.com",
"port": 465,
"secure": true
}
}'

Expected response:

{
"account": "example",
"state": "new"
}

Important: If you use an SMTP port other than 465, set "secure": false.

2. Wait for Connection

Before sending, ensure the account is connected. Poll the account status:

curl "http://127.0.0.1:3000/v1/account/example" \
-H "Authorization: Bearer <your-token>"

Wait until state becomes "connected". Submission is rejected while EmailEngine is performing the initial sync.

3. Submit a Simple Email

Endpoint: POST /v1/account/:id/submit

curl -XPOST "http://127.0.0.1:3000/v1/account/example/submit" \
-H "Authorization: Bearer <your-token>" \
-H "Content-Type: application/json" \
-d '{
"to": [
{
"name": "Recipient Name",
"address": "recipient@example.com"
}
],
"subject": "Test message",
"text": "Hello from myself!",
"html": "<p>Hello from myself!</p>"
}'

Expected response (queued, not yet delivered):

{
"response": "Queued for delivery",
"messageId": "<99f7f0ec-90a1-caaf-698b-18e096c7679e@example.com>",
"sendAt": "2025-05-14T10:22:31.312Z",
"queueId": "4646ac53857fd2b2"
}

The message is now queued for delivery. EmailEngine will handle the actual SMTP transmission.

Message Components

Recipients

You can specify multiple recipients in to, cc, and bcc fields:

{
"to": [
{ "name": "Alice", "address": "alice@example.com" },
{ "name": "Bob", "address": "bob@example.com" }
],
"cc": [
{ "address": "manager@example.com" }
],
"bcc": [
{ "address": "archive@example.com" }
]
}

The name field is optional but recommended for a better recipient experience.

Content

Plain Text and HTML

Always provide both plain text and HTML versions for best compatibility:

{
"subject": "Welcome to our service",
"text": "Welcome! Visit https://example.com to get started.",
"html": "<p>Welcome!</p><p>Visit <a href='https://example.com'>our site</a> to get started.</p>"
}

HTML Only

If you only provide HTML, EmailEngine can auto-generate a plain text version:

{
"subject": "HTML Newsletter",
"html": "<h1>Hello!</h1><p>This is an HTML email.</p>"
}

Attachments

Add attachments using the attachments array:

{
"to": [{ "address": "recipient@example.com" }],
"subject": "Document attached",
"text": "Please find the document attached.",
"attachments": [
{
"filename": "document.pdf",
"content": "base64-encoded-content-here",
"contentType": "application/pdf"
}
]
}

Attachment options:

  • filename - Attachment filename (optional, recommended)
  • content - Base64 encoded content (required unless using reference)
  • contentType - MIME type (optional, auto-detected if omitted)
  • cid - Content ID for inline images (optional)
  • reference - Reference an existing attachment by ID instead of providing content (optional)

Inline Images

Reference inline images in HTML using Content ID:

{
"html": "<p>Logo: <img src='cid:logo' /></p>",
"attachments": [
{
"filename": "logo.png",
"content": "iVBORw0KGgoAAAANS...",
"contentType": "image/png",
"cid": "logo"
}
]
}

Custom Headers

Add custom email headers:

{
"to": [{ "address": "recipient@example.com" }],
"subject": "Test",
"text": "Test message",
"headers": {
"X-Custom-Header": "value",
"X-Campaign-ID": "campaign-123",
"List-Unsubscribe": "<mailto:unsubscribe@example.com>"
}
}

Common custom headers:

  • X-Custom-* - Your custom headers
  • Reply-To - Set reply address
  • List-Unsubscribe - Unsubscribe link for bulk email
  • X-Priority - Set message priority (1-5)

Sender Information

Override default sender information:

{
"from": {
"name": "Support Team",
"address": "support@example.com"
},
"replyTo": {
"name": "No Reply",
"address": "noreply@example.com"
},
"to": [{ "address": "recipient@example.com" }],
"subject": "Support Ticket Response",
"text": "Your ticket has been updated."
}

If omitted, EmailEngine uses the account's configured email and name.

Advanced Options

Scheduled Sending

Schedule an email for future delivery:

{
"to": [{ "address": "recipient@example.com" }],
"subject": "Scheduled message",
"text": "This will be sent at the specified time",
"sendAt": "2025-12-25T09:00:00.000Z"
}

The message stays in the queue until the specified time.

Skip Sent Folder

Prevent saving a copy to the Sent Mail folder:

{
"to": [{ "address": "recipient@example.com" }],
"subject": "No copy saved",
"text": "This won't appear in Sent Mail",
"copy": false
}

Useful for bulk sending to avoid cluttering the Sent folder.

Custom Message ID

Specify your own Message-ID for threading:

{
"to": [{ "address": "recipient@example.com" }],
"subject": "Custom ID",
"text": "Message with custom ID",
"messageId": "<custom-id-12345@example.com>"
}

Important for maintaining email threads (see Threading).

Delivery Status Notifications

Request delivery status notifications (DSN):

{
"to": [{ "address": "recipient@example.com" }],
"subject": "With DSN",
"text": "Request delivery notification",
"dsn": {
"return": "headers",
"notify": ["success", "failure", "delay"]
}
}

Note: DSN support varies by email provider.

Email Tracking

Enable open and click tracking for your emails:

{
"to": [{ "address": "recipient@example.com" }],
"subject": "Tracked Email",
"html": "<p>Check out <a href='https://example.com'>our website</a>!</p>",
"trackOpens": true,
"trackClicks": true
}

When enabled, EmailEngine will:

  • Insert a tracking pixel to detect email opens (trackOpens)
  • Rewrite links to track clicks (trackClicks)
  • Send trackOpen and trackClick webhook events when detected

Optional: Override the base URL for tracking links:

{
"trackOpens": true,
"trackClicks": true,
"baseUrl": "https://yourdomain.com"
}

Learn more about tracking events →

Preview Mode (Dry Run)

Generate email preview without actually sending:

{
"to": [{ "address": "recipient@example.com" }],
"subject": "Test Email",
"html": "<p>Preview this email</p>",
"dryRun": true
}

Response:

{
"response": "Dry run",
"messageId": "<generated-message-id@example.com>",
"preview": "BASE64_ENCODED_RFC822_MESSAGE"
}

The preview field contains the complete RFC822 formatted email (base64 encoded). Decode it to see exactly what would be sent. Perfect for testing templates and rendering.

Network Configuration

Proxy Routing

Route SMTP connection through a proxy server:

{
"to": [{ "address": "recipient@example.com" }],
"subject": "Via Proxy",
"text": "Sent through proxy",
"proxy": "http://proxy.company.com:8080"
}

Supports HTTP, HTTPS, SOCKS4, and SOCKS5 proxies.

Local Address Binding

Bind to a specific local IP address:

{
"to": [{ "address": "recipient@example.com" }],
"subject": "From Specific IP",
"text": "Sent from specific interface",
"localAddress": "192.168.1.100"
}

Useful for multi-interface systems or IP-based routing.

Delivery Control

Custom Retry Attempts

Override the default retry count for this message:

{
"to": [{ "address": "recipient@example.com" }],
"subject": "High Priority",
"text": "Will retry up to 15 times",
"deliveryAttempts": 15
}

Default is usually 10 attempts. Use higher values for critical emails.

SMTP Gateway

Route through a specific SMTP gateway:

{
"to": [{ "address": "recipient@example.com" }],
"subject": "Via Gateway",
"text": "Sent through custom gateway",
"gateway": "gateway-id-123"
}

Gateways must be configured in EmailEngine settings first. Learn more →

SMTP Envelope

Specify SMTP envelope separately from message headers:

{
"to": [{ "address": "recipient@example.com" }],
"from": { "address": "noreply@example.com" },
"subject": "Envelope Example",
"text": "Header From and SMTP MAIL FROM can differ",
"envelope": {
"from": "bounce@example.com",
"to": ["actualrecipient@example.com"]
}
}

Useful for bounce handling and advanced email routing.

Idempotency

Prevent duplicate message submission with idempotency keys:

Request:

curl -XPOST "http://127.0.0.1:3000/v1/account/example/submit" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: unique-key-12345" \
-d '{
"to": [{ "address": "recipient@example.com" }],
"subject": "Important",
"text": "This will only send once even if request is retried"
}'

If the same request is sent multiple times with the same Idempotency-Key header, EmailEngine will:

  • Process it only once
  • Return the same response for duplicate requests
  • Prevent accidental double-sends

The idempotency key can be any string (0-1024 characters). Use UUIDs or request-specific identifiers.

Webhook Notifications

EmailEngine sends webhook notifications for delivery status updates. Configure your webhook URL under Settings → Webhooks.

messageSent

Delivered to the outbound MTA (SMTP server accepted the message):

{
"account": "example",
"date": "2025-05-14T10:32:39.499Z",
"event": "messageSent",
"data": {
"messageId": "<a00576bd-f757-10c7-26b8-885d7bbd9e83@example.com>",
"response": "250 2.0.0 Ok: queued as 5755482356",
"envelope": {
"from": "andris@example.com",
"to": ["recipient@example.com"]
}
}
}

messageDeliveryError

Emitted after every failed delivery attempt. EmailEngine retries automatically until delivery succeeds or the maximum number of attempts is reached:

{
"serviceUrl": "http://127.0.0.1:3000",
"account": "example",
"date": "2025-05-14T15:07:35.832Z",
"event": "messageDeliveryError",
"data": {
"queueId": "1833c8a88a86109a1bf",
"envelope": {
"from": "andris@example.com",
"to": ["recipient@example.com"]
},
"messageId": "<29e26263-7125-ff56-4f80-83a5cf737d5e@example.com>",
"error": "400 Message Not Accepted",
"errorCode": "EPROTOCOL",
"smtpResponseCode": 400,
"job": {
"attemptsMade": 1,
"attempts": 10,
"nextAttempt": "2025-05-14T15:07:45.465Z"
}
}
}

messageFailed

Raised once EmailEngine gives up retrying (max attempts reached):

{
"account": "example",
"date": "2025-05-14T11:58:50.181Z",
"event": "messageFailed",
"data": {
"messageId": "<97ac5d9a-93c7-104b-8d26-6b25f8d644ec@example.com>",
"queueId": "610c2c93e608bd37",
"error": "Error: Invalid login: 535 5.7.8 Error: authentication failed: "
}
}

Testing Sent Emails

Using Ethereal Email

For testing, use Ethereal Email to create temporary test accounts:

# Send to your Ethereal test address
curl -XPOST "http://127.0.0.1:3000/v1/account/example/submit" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"to": [{ "address": "test@ethereal.email" }],
"subject": "Test Email",
"text": "Testing EmailEngine sending"
}'

Check Sent Folder

Verify the email was saved to the Sent folder:

curl "http://127.0.0.1:3000/v1/account/example/messages?path=Sent" \
-H "Authorization: Bearer <token>"

Monitor Queue

Check the outbox queue status:

curl "http://127.0.0.1:3000/v1/account/example/outbox" \
-H "Authorization: Bearer <token>"

Common Pitfalls

Authentication Issues

Problem: Gmail, Outlook, and Yahoo may refuse SMTP logins that look like bots.

Solution:

  • Gmail: Use OAuth2 or app-specific passwords (account passwords no longer work)
  • Outlook: Use OAuth2 (password authentication completely disabled)
  • Yahoo: Use OAuth2 or app-specific passwords

See Gmail Setup and Outlook Setup.

Timeout Errors

Problem: Heroku dynos cut idle sockets.

Solution:

  • Move off Heroku or increase dyno size
  • Configure longer timeouts
  • Use a different deployment platform

Account Not Connected

Problem: Submission rejected with "Account not ready" error.

Solution: Wait for initial IMAP sync to complete. Poll /v1/account/:id until state becomes "connected".

Rate Limiting

Problem: Too many emails sent too quickly.

Solution:

  • Implement throttling in your application
  • Use mail merge for bulk sending
  • Spread sends over time
  • Check provider rate limits

Large Attachments

Problem: Email rejected due to size limits.

Solution:

  • Reduce attachment size
  • Use external file hosting with links
  • Compress attachments
  • Check provider size limits (typically 25-50MB)

Spam Filters

Problem: Emails flagged as spam.

Solution:

  • Provide both HTML and plain text versions
  • Avoid spam trigger words
  • Include unsubscribe links for bulk email
  • Authenticate domain with SPF/DKIM
  • Warm up new accounts gradually

Performance Considerations

Optimize for Bulk Sending

For sending multiple emails:

  • Use Mail Merge instead of individual calls
  • Enable copy: false to skip Sent folder storage
  • Implement rate limiting
  • Monitor the outbox queue

Connection Pooling

EmailEngine maintains SMTP connection pools automatically. For high-volume sending:

  • Keep accounts connected
  • Avoid frequent reconnections
  • Monitor connection status

Webhook Processing

Handle webhooks asynchronously:

  • Return 200 OK quickly from webhook endpoint
  • Process delivery status in background jobs
  • Implement webhook retry logic