Skip to content
Back to skills

Email

ASecurity

Something in the app must reach its owners by e-mail: an access request, a low-stock alert, a problem report. Also: the sender name / Reply-To of every e-mail the app sends. An app can NOT e-mail an arbitrary address (no EmailJS, SendGrid, Resend, nodemailer, `mailto:` tricks). A contact form → use `forms` (`skill_info('forms')`): it stores and e-mails submissions.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 29, 2026
ai-agentsgoreactnodeapi

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 10 files and shows the line behind each finding

Scanned September 29, 2026

npx -y skills add freema/drobek --skill email --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Email?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Email
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/freema-email/badge)](https://www.skillsdirectory.com/skills/freema-email)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
# email — notify the app's owners by e-mail

## 1. When to use

Something in the app must reach its owners by e-mail: an access request, a
low-stock alert, a problem report. Also: the sender name / Reply-To of every
e-mail the app sends. An app can NOT e-mail an arbitrary address (no
EmailJS, SendGrid, Resend, nodemailer, `mailto:` tricks). A contact form →
use `forms` (`skill_info('forms')`): it stores and e-mails submissions.

## 2. Minimal working code

`notifyAdmins` needs a signed-in user, so the UI sits in `<LoginGate>`
(`skill_info('auth')`).

```tsx
// src/main.tsx
import { useState } from 'react';
import { createRoot } from 'react-dom/client';
import { drobek, DrobekError } from 'drobek';
import { LoginGate } from 'drobek/auth';
import './styles.css';

function RequestAccess() {
  const [status, setStatus] = useState('');
  async function ask() {
    setStatus('Sending…');
    try {
      const { sent } = await drobek.email.notifyAdmins('Access request', 'Please give me access to the stock list.');
      setStatus(sent > 0 ? 'The owners were notified.' : 'Nobody to notify.');
    } catch (err) {
      setStatus(err instanceof DrobekError && err.code === 'limit_exceeded' ? 'Daily limit reached — try tomorrow.' : 'Could not send.');
    }
  }
  return (
    <main>
      <h1>Stock</h1>
      <button onClick={ask}>Ask the owners for access</button>
      <p role="status">{status}</p>
    </main>
  );
}

createRoot(document.getElementById('root')!).render(
  <LoginGate title="Stock">
    <RequestAccess />
  </LoginGate>
);
```

Optional sender settings:

```json
{ "app_id": "…", "module": "email", "config": { "fromName": "Acme bakery", "replyTo": "orders@acme.example" } }
```

## 3. API and types

```ts api
// drobek.email
export interface Api {
  /** E-mail the app's owners. subject 1–150 chars (one line), text 1–5000 chars plain text. */
  notifyAdmins(subject: string, text: string): Promise<{ sent: number }>;
}
```

- Recipients = the app's **owners**: editors and workspace-admins of the
  app's drobek workspace. The mail names the signed-in user who sent it.
  The subject becomes `[<app name>] <subject>`.
- Config: `fromName` (≤ 60 chars, one plain line, no `"<>@\`) — applies at
  once; `replyTo` (an address) — see rules. Failures reject with
  `DrobekError` (`code`, `status`, `message`).
- REST: `POST /__drobek/v1/email/notify-admins { subject, text }` with the
  header `X-Drobek-SDK: 1` (the SDK sends it).

## 4. Rules and limits

- A new or changed `replyTo` needs the owner's confirmation:
  `configure_module` answers `applied: false` + `confirm_url`; give the user
  the link.
- Recipients come only from the server: the owners, addresses the owner
  confirmed in a module config (e.g. `forms` notify lists), the signed-in
  user. App code never names an address.
- `EMAIL_NOTIFY_ADMINS_PER_DAY` 20 `notifyAdmins` calls per app per day;
  `EMAIL_PER_APP_PER_DAY` 50 notification e-mails per app per day
  (notifyAdmins + form notifications; sign-in codes do not count).
- An operator-wide hourly budget covers all app e-mail; past it,
  notifications pause for a while (`unavailable`), sign-in codes keep going.
- The text is escaped into drobek's layout: HTML shows as text.
  `skill_info('email').limits` has this server's values.

## 5. Errors → fix

| error | cause | fix |
|---|---|---|
| `unauthorized` (401) | nobody is signed in | wrap the UI in `<LoginGate>` |
| `limit_exceeded` (429) | the app's daily e-mail limit | show "try again tomorrow"; never retry in a loop |
| `unavailable` (503) | app e-mail paused on the server, or SMTP failed | show a message; retry later |
| `invalid_request` (400) | empty or too long subject/text | read `details[].path` |
| `csrf_rejected` (403) | raw `fetch` without the SDK header | call `drobek.email.notifyAdmins` |
| `invalid_params` | configure_module: bad `fromName` / `replyTo` | read `issues[].path` |

Files in this skill

  • SKILL.md3.8 KB
  • package.json947 B
  • src/config.ts1.9 KB
  • src/index.test.ts8.3 KB
  • src/index.ts3.1 KB
  • src/routes.ts4.5 KB
  • src/sdk.ts593 B
  • tsconfig.build.json489 B
  • tsconfig.json404 B
  • vitest.config.ts207 B

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.