{
  "name": "email",
  "title": "Email - Sending & Formatting",
  "description": "Sending email: gipity email send (from gipity@gipity.ai), gipity gmail (from your own Gmail), and the app email() service - plus HTML style rules, image handling, and deliverability tips",
  "guid": "sk_plat_emgd",
  "category": "Agent Tools",
  "requiredTools": [],
  "content": "# Email - Sending & Formatting\n\n## Two distinct email channels\n\nGipity has two separate email paths; use the one matching who the recipient should see as the sender.\n\n| Dimension | Platform email | User's Gmail |\n|---|---|---|\n| CLI | `gipity email send` | `gipity gmail send|reply|search|read` |\n| Sender address | `gipity@gipity.ai` | the user's own Gmail address |\n| Quota / deliverability | Gipity platform quota | User's Gmail quota |\n| Requires | Nothing extra | A connected Google account |\n| Use when | The platform owns the message (reminders, heartbeat alerts, platform notifications) | The message should come from the user personally (replies to Gmail threads, outreach from their own address) |\n\n**Default is `gipity email send`.** Only use `gipity gmail` when the user explicitly asks to send from their own Gmail, or when you're replying to an existing Gmail thread.\n\n**These are CLI commands, for you while you build.** A *deployed app* sends email from a function with the injected **`email()`** service: declare `services: ['email']` and call `await email({ to, subject, text })`. The platform brokers delivery - owner-billed, one credit per recipient actually sent. The From **address** is platform-controlled (`gipity@gipity.ai`) for deliverability and anti-spoofing; the app may set only the display **name** (`fromName`) and `replyTo` (so replies land in the owner's inbox). Unsubscribed/blocked recipients are skipped and not charged. Full reference: the [app-development](app-development.md) skill.\n\n```js\n// functions/send-welcome.js   (gipity.yaml → services: ['email'])\nexport default async function (ctx, { email }) {\n  const r = await email({\n    to: 'lead@example.com',      // string or array (max 50)\n    subject: 'Thanks for signing up',\n    text: 'We\\'ll be in touch shortly.',   // and/or html\n    replyTo: 'you@yourco.com',   // optional - replies thread to you\n    fromName: 'Acme Sales',      // optional display name\n  });\n  return r; // { sent: 1, skipped: 0, results: [{ to, status: 'sent' }] }\n}\n```\n\nUnder `gipity test`, `email()` returns `{ sent: 0, skipped: 0, results: [], test: true }` without sending. Verify the real send path after deploy with `gipity email test <to>` (skipped recipients say why: unsubscribed, blocked test/internal address, or server not configured); `gipity email log` lists recent sends AND skipped attempts.\n\n**Prefer a workflow `notify` step when the email is event- or schedule-driven** - \"email me when someone submits the form\" (a *record insert*) or a *scheduled digest* - so no function has to run. The complete record-triggered recipe:\n\n```yaml\n# workflows/contact-notify.yaml - email the owner on each new contact-form row\nname: contact-notify\ntrigger: record.after_insert\ntable: contact_messages\nsteps:\n  - name: email_owner\n    step_type: notify\n    config:\n      channel: email\n      to: owner@example.com\n      subject: \"New contact message from {{trigger.record.name}}\"\n      body: \"{{trigger.record.email}} wrote:\\n\\n{{trigger.record.message}}\"\n```\n\nplus a `workflows` phase in `gipity.yaml` (`type: workflows`, `source: workflows`) so `gipity deploy` reconciles it. Three requirements make the record trigger fire: the workflow needs `table:`, the row must be written through the Records API (not raw SQL in a function), and an anonymous-visitor form needs the table set to `auth_level: public`. For a scheduled email instead of a record-triggered one, swap the trigger for `trigger: scheduled` + `cron:`. Details (step types, template variables, limits): [workflow](workflow.md).\n\n- Omit `--to` on `gipity email send` to send to the user's own email.\n\n## Plain characters only\nUse plain ASCII characters only. No em dashes, en dashes, smart/curly quotes, ellipsis, non-breaking spaces, or other fancy Unicode punctuation. Use a hyphen (-), straight double quote (\"), straight apostrophe ('), and three periods (...). Applies to everything you write for the user: chat responses, email drafts and bodies, file content, memory, commit messages, code comments. Exception: if the user explicitly asks for specific characters or a particular style (for example \"use em dashes in this poem\", \"make it fancy\"), follow their lead - they are in charge of their own content.\n\n## Replying to a Gmail thread\n- Always `gipity gmail read <message-id>` the message you're replying to first - you need its sender, date, and full body to build the quote.\n- The body you pass to `gipity gmail reply` MUST be: the new reply text, a blank line, then a standard quote header `On <date>, <sender> wrote:`, then the prior message body line-prefixed with `> `. Example:\n\n  ```\n  Sounds good - Tuesday at 2pm works for me.\n\n  On Wed, Apr 17, 2026 at 9:14 AM, Alice <alice@example.com> wrote:\n  > Can we move the sync to Tuesday?\n  > Let me know what time works.\n  ```\n\n- Quote the message you're directly replying to (the most recent one), and leave any quoted chain it already contains intact - don't strip history.\n- Default to including the quote. Skip it only if the user explicitly says so (e.g. \"just send 'yes', don't quote\").\n- For `subject`, prefix the original subject with `Re: ` if it isn't already.\n\n## How gipity email send works\n- It sends as soon as you run it. Only send to someone other than the user when the user asked for that email.\n- Every external email includes an unsubscribe link; recipients can block future emails from the sender.\n- Self-sends (omit `--to`) go to the user's own address.\n- A platform footer is appended to every email.\n\n### Rate limits\n- **Max 10 emails per 1 hour** per account\n- Plan batch sends accordingly - space them out if sending to multiple recipients\n\n### Options\n- `--to` - recipient (repeatable; omit for self-send), `--cc`, `--bcc`, `--reply-to`\n- `--subject` - subject line (required)\n- `--body` - plain text body (required)\n- `--html` - optional HTML body, used as the rich version alongside plain text\n\n## HTML Style Rules\n- **Inline styles only** - email clients strip `<style>` tags. Every element needs `style=\"...\"`.\n- **System fonts**: `font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif`\n- **Single-column layout** - centered `<div>` or `<table>` with `max-width: 600px`. No flexbox/grid.\n- **Minimal colors** - dark text (#333) on white (#fff). One accent color max.\n- **Font sizes**: 14-16px body, 20-24px headings, never below 12px.\n\n## Images\n- **Default: transcode before emailing.** For a user-supplied image (photo, screenshot, render), in the sandbox resize to 800px max on any side and convert to JPG at quality 85. `convert input.png -resize '800x800>' -quality 85 output.jpg` (the `>` only shrinks, never enlarges). A 1.6MB phone photo typically drops to ~100–200KB. Then get a public URL for the transcoded version with `gipity file url <path>` and link/embed that.\n- **Send the original only if the user asks** - \"send the original\", \"don't compress\", \"full resolution\", etc. If the input is already small and reasonable (under ~500KB and ~1200px), you can skip the transcode.\n- **Never SVG** - email clients strip it. Convert to PNG/JPG first: `cairosvg` (Python) or ImageMagick.\n- **Never base64** - email clients strip `data:` URIs. Always host it (`gipity file url <path>`) and link: `<img src=\"https://...\" width=\"...\" height=\"...\">`.\n- **HTML display width**: use `width=\"600\"` (or less) on the `<img>` - 600px is the standard single-column email layout width.\n\n## Preview\n- Preview rich/styled HTML emails before sending: save the HTML into the project, get its URL with `gipity file url <path>`, and check it with `gipity page screenshot <url>`.\n- Simple text-only emails don't need a preview step.\n- Same hosted URLs work in both preview and email.\n- 3MB preview limit - compress images in sandbox if needed."
}
