# Send Email - Your SMTP, Resend or SendGrid, With Attachments (`feedsmith/send-email`) Actor

Send transactional notifications and reports through your own SMTP, Resend or SendGrid account - your From address, your deliverability, your provider logs. Attach the run's dataset as CSV/JSON.

- **URL**: https://apify.com/feedsmith/send-email.md
- **Developed by:** [TRUONG VAN HOA](https://apify.com/feedsmith) (community)
- **Categories:** Integrations, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 email sents

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Send Email — your own SMTP, Resend or SendGrid, with attachments

Send transactional notifications and reports through **your own** email account — SMTP (Gmail,
Outlook, Zoho, Brevo, Mailgun, Amazon SES...), [Resend](https://resend.com) or
[SendGrid](https://sendgrid.com). Because it's your account, the From name and address, your
domain's deliverability (SPF/DKIM) and every provider-side delivery log are yours, not a shared
sender's. Attach a run's dataset as a CSV or JSON file, or show a quick HTML preview table of the
first rows, right in the email.

This actor is built for **notifications and reports** - "email me when my scraper finishes", a
daily digest, an alert - not for bulk or marketing mail. See [Acceptable use](#acceptable-use) below.

### Setup recipes

#### 1. Gmail (or any SMTP account) with an App Password

1. Turn on 2-Step Verification on your Google Account, if it isn't already.
2. Go to [myaccount.google.com/apppasswords](https://myaccount.google.com/apppasswords), create an
   app password (any name), and copy the 16-character password shown.
3. In the input: `provider: "smtp"`, `smtpHost: "smtp.gmail.com"`, `smtpPort: 587`,
   `smtpSecurity: "starttls"`, `smtpUser` = your Gmail address, `smtpPassword` = the app password
   (not your normal Google password - Gmail rejects that over SMTP).

Other SMTP providers, same idea (host/port only, your address + a normal or app password):

| Provider | Host | Port |
| --- | --- | --- |
| Outlook / Office 365 | `smtp.office365.com` | 587 |
| Zoho Mail | `smtp.zoho.com` | 587 |
| Brevo (Sendinblue) | `smtp-relay.brevo.com` | 587 |
| Mailgun | `smtp.mailgun.org` | 587 |
| Amazon SES | `email-smtp.<region>.amazonaws.com` | 587 |

#### 2. Resend

1. Sign up at [resend.com](https://resend.com), verify a sending domain under **Domains** (or use
   `onboarding@resend.dev` as `fromEmail` for a quick test, no domain needed).
2. Create an API key under **API Keys** with send permission.
3. In the input: `provider: "resend"`, `apiKey` = the key, `fromEmail` = an address on your verified
   domain (or `onboarding@resend.dev` for testing).

#### 3. SendGrid

1. Sign up at [sendgrid.com](https://sendgrid.com), verify a sender identity or domain under
   **Settings > Sender Authentication**.
2. Create an API key under **Settings > API Keys** with **Mail Send** permission.
3. In the input: `provider: "sendgrid"`, `apiKey` = the key, `fromEmail` = the verified sender/domain
   address.

### "Email me when my scraper finishes" (Integrations)

Chain this actor after any other Actor via **Integrations**, triggered on that Actor's run
succeeding, and pass its dataset straight through:

```json
{
  "provider": "resend",
  "apiKey": "{{secrets.RESEND_API_KEY}}",
  "fromEmail": "alerts@yourdomain.com",
  "to": "you@yourdomain.com",
  "subject": "{{resource.actorName}} finished - {{itemCount}} items",
  "text": "Your run is done. {{itemCount}} items were scraped on {{date}}.",
  "datasetId": "{{resource.defaultDatasetId}}",
  "attachmentFormat": "csv",
  "includeTablePreview": true
}
```

`{{resource.defaultDatasetId}}` and `{{resource.actorName}}` are filled in by Integrations; `{{date}}`
and `{{itemCount}}` are filled in by this actor from the dataset it just read.

### Example: from the API

```bash
curl "https://api.apify.com/v2/acts/YOUR_USERNAME~send-email/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "smtp",
    "smtpHost": "smtp.gmail.com",
    "smtpUser": "you@gmail.com",
    "smtpPassword": "your-app-password",
    "fromEmail": "you@gmail.com",
    "to": "customer@example.com",
    "subject": "Your order shipped",
    "text": "Order #1042 is on its way."
  }'
```

### Example: from an AI agent (Apify MCP)

An agent that already has a result to report can send it directly, with no dataset involved:

```json
{
  "provider": "sendgrid",
  "apiKey": "...",
  "fromEmail": "bot@yourdomain.com",
  "to": "you@yourdomain.com",
  "subject": "Task complete",
  "text": "The agent finished the requested task successfully."
}
```

### Input reference

| Field | Notes |
| --- | --- |
| `provider` | `smtp` | `resend` | `sendgrid` |
| `smtpHost`, `smtpPort`, `smtpSecurity`, `smtpUser`, `smtpPassword` | SMTP only |
| `apiKey` | Resend/SendGrid only, secret input |
| `fromEmail`, `fromName`, `replyTo` | Sender identity |
| `to`, `cc`, `bcc` | Comma, semicolon or newline-separated string. **Max 20 unique recipients combined** - see [Acceptable use](#acceptable-use) |
| `subject`, `text`, `html` | At least one of `text`/`html` is required. Support `{{runId}}`, `{{datasetId}}`, `{{itemCount}}`, `{{date}}` |
| `datasetId` | Attach and/or preview a dataset, e.g. `{{resource.defaultDatasetId}}` |
| `attachmentFormat` | `csv` | `json` | `none` |
| `attachmentFileName`, `maxAttachmentRows` | Attachment naming/size controls |
| `includeTablePreview`, `previewRows` | Append an HTML table of the first rows to the body |
| `extraAttachments` | `[{fileName, url}]` (https only) or `[{fileName, contentBase64, contentType}]` |
| `dryRun` | Validate and build the message without sending, not charged |

Attachments (dataset + `extraAttachments` combined) are capped at **10 MB** after encoding - a run
over that fails before sending, with a message telling you which input to lower.

### Output

`OUTPUT` (key-value store record), also pushed as one dataset item:

```json
{
  "status": "sent",
  "provider": "resend",
  "messageId": "4ef9...",
  "from": "Acme Alerts <alerts@yourdomain.com>",
  "recipientsCount": 2,
  "subject": "Weekly report - 128 items",
  "attachment": { "fileName": "dataset-abc123-2026-09-27.csv", "rows": 128, "bytes": 9421 },
  "previewRows": 10,
  "sentAt": "2026-09-27T08:00:00.000Z",
  "warnings": []
}
```

`status` is `"sent"`, `"dry-run"`, or `"setup-required"`. A run started with **no login details at
all** (no SMTP host/username/password and no API key - e.g. the untouched prefilled form) does not
fail: it finishes with `status: "setup-required"`, **sends nothing**, pushes no dataset item and is
not charged. The status message and `OUTPUT.message` say what to fill in. Partial or wrong details
(host but no password, a rejected API key, ...) still fail the run with an actionable message.

### Acceptable use

This actor is for **transactional notifications and reports** to recipients who expect them - run
alerts, digests, order/status updates. It is not a bulk or marketing mailer:

- Recipients come only from `to`/`cc`/`bcc` — never from a dataset. There is no mail-merge mode.
- A hard cap of **20 unique recipients per run** (`to`+`cc`+`bcc` combined) - a run over the cap fails
  before sending.
- One message per run to every recipient on it (`bcc` hides the extra recipients from each other) -
  no per-recipient loops.

Cold outreach, purchased lists, or scraped contact data are forbidden by every provider this actor
can use — quoting Resend's Acceptable Use Policy: *"You are prohibited from sending unsolicited
messages of any kind, including cold outreach, purchased lists, or scraped contact data."* The same
applies here. Runs that look like bulk/marketing sending may be blocked by your own SMTP/Resend/
SendGrid account regardless of what this actor allows.

### Pricing

Pay per event, charged only once the provider actually accepts the message — **a failed run or a
validation error (bad address, over a cap) costs nothing**:

| Event | Price | When |
| --- | --- | --- |
| `email-sent` | $0.002 | Once per message the provider accepted |
| `attachment-rows-1000` | $0.001 | Per started block of 1,000 dataset rows attached or previewed |

A single notification with no attachment costs $0.002. A report with a 2,000-row CSV attached costs
$0.002 + $0.002 = $0.004. If **Max total charge** can't cover even one `email-sent` event, the run
fails before attempting to send.

### FAQ

**Why does this land in the inbox instead of spam?** Because it's sent through *your* SMTP account or
*your* verified Resend/SendGrid domain, it inherits your domain's own SPF/DKIM/reputation - the same
signals mailbox providers use to trust any other mail from you. A shared sender used by many
unrelated actors can't offer that.

**Is my password/API key safe?** `smtpPassword` and `apiKey` are Apify secret inputs: encrypted at
rest, never shown in logs, used only in memory for the run's duration. This actor never logs a
password, API key, or full message body, and redacts anything that looks like a secret from error
messages before they're shown. Nothing is stored beyond the run itself.

**Can I send to more than 20 people?** Split into multiple runs, or use your provider's own
mailing-list/broadcast feature - this actor is deliberately capped for notifications/reports (see
[Acceptable use](#acceptable-use)).

**What happens if my dataset is huge?** `maxAttachmentRows` (default 50,000) caps how many rows are
attached; the combined attachment size (dataset + `extraAttachments`) is capped at 10 MB after
encoding. Over either limit, the run fails before sending with the exact input to lower.

**Gmail says "Invalid login" / "Application-specific password required".** Use an App Password
(Google Account > Security > App passwords), not your normal Gmail password - Gmail rejects normal
passwords over SMTP for accounts with 2-Step Verification on.

**Is this affiliated with Google, Resend or Twilio SendGrid?** No. This is an independent tool that
uses each provider's official public API/SMTP interface with your own credentials in your own
account.

**Something wrong or missing?** Open an issue on the Actor's Issues tab.

# Actor input Schema

## `provider` (type: `string`):

Which service actually sends the message. All three use your own account, so the From address and deliverability are yours, not a shared sender's.

## `smtpHost` (type: `string`):

Provider only when Provider = SMTP. Common presets: Gmail smtp.gmail.com, Outlook/Office 365 smtp.office365.com, Zoho smtp.zoho.com, Brevo smtp-relay.brevo.com, Mailgun smtp.mailgun.org, Amazon SES email-smtp.<region>.amazonaws.com. See the README for step-by-step setup.

## `smtpPort` (type: `integer`):

587 for STARTTLS (most providers), 465 for implicit TLS, 25 is often blocked by hosting providers - avoid it.

## `smtpSecurity` (type: `string`):

How the connection is encrypted. Match this to your provider's documented port (587 = STARTTLS, 465 = implicit TLS).

## `smtpUser` (type: `string`):

Usually your full email address.

## `smtpPassword` (type: `string`):

For Gmail, this must be an App Password (Google Account > Security > App passwords), not your normal login password.

## `apiKey` (type: `string`):

Required when Provider = Resend or SendGrid. Create one in your Resend or SendGrid dashboard with send permission.

## `fromEmail` (type: `string`):

The sender address. Must be a verified domain/sender in Resend or SendGrid, or a mailbox you can authenticate as over SMTP.

## `fromName` (type: `string`):

Display name shown next to the From address, e.g. "Acme Alerts".

## `replyTo` (type: `string`):

Optional. Where replies should go, if different from the From address.

## `to` (type: `string`):

Recipient(s), required. Comma, semicolon or newline separated (e.g. "a@x.com, b@y.com"), or "Name <a@x.com>". Max 20 unique recipients across To+Cc+Bcc combined - this Actor sends notifications/reports, not bulk mail.

## `cc` (type: `string`):

Optional. Same format as To.

## `bcc` (type: `string`):

Optional. Same format as To. Bcc addresses are never shown in the run's OUTPUT, only counted.

## `subject` (type: `string`):

Required. Supports {{runId}}, {{datasetId}}, {{itemCount}}, {{date}} placeholders.

## `text` (type: `string`):

Plain-text body. Provide Text and/or HTML - at least one is required. Supports the same {{...}} placeholders as Subject.

## `html` (type: `string`):

HTML body. If only HTML is given, a plain-text fallback is generated automatically by stripping tags. Placeholder values are HTML-escaped before insertion.

## `datasetId` (type: `string`):

Optional. Attach and/or preview a run's dataset, e.g. {{resource.defaultDatasetId}} when chaining after another Actor via Integrations.

## `attachmentFormat` (type: `string`):

Format of the dataset attachment, if "Apify dataset ID" is set.

## `attachmentFileName` (type: `string`):

Optional. Default: "dataset-<id>-<date>.csv" or ".json".

## `maxAttachmentRows` (type: `integer`):

Stop attaching dataset rows after this many. Lower it if the attachment would exceed the 10 MB total size cap.

## `includeTablePreview` (type: `boolean`):

Append an HTML table of the first rows of the dataset (see "Preview rows" below) to the HTML body, in addition to any attachment.

## `previewRows` (type: `integer`):

How many dataset rows to show in the table preview (1-100, up to 8 columns each), when "Include table preview" is on.

## `extraAttachments` (type: `array`):

Optional. Array of {"fileName", "url"} (https only, fetched at run time) or {"fileName", "contentBase64", "contentType"} (inline). Counted in the 10 MB total attachment cap.

## `dryRun` (type: `boolean`):

Validate everything and build the message (recipients, attachment size) without sending. Written to OUTPUT only. Not charged.

## Actor input object example

```json
{
  "provider": "smtp",
  "smtpPort": 587,
  "smtpSecurity": "starttls",
  "attachmentFormat": "csv",
  "maxAttachmentRows": 50000,
  "includeTablePreview": false,
  "previewRows": 10,
  "extraAttachments": [],
  "dryRun": false
}
```

# Actor output Schema

## `results` (type: `string`):

Summary of the message that was sent (or would be sent, for a dry run).

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "extraAttachments": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("feedsmith/send-email").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "extraAttachments": [] }

# Run the Actor and wait for it to finish
run = client.actor("feedsmith/send-email").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "extraAttachments": []
}' |
apify call feedsmith/send-email --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,feedsmith/send-email"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/4cWV0GUq8ZXUJvIxb/builds/TSTNOB5Qo2I09bWyg/openapi.json
