> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bosonware.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Using BosonMail with Bun

> Send email from Bun scripts and HTTP servers using the BosonMail SDK, with native file reading and environment loading.

This guide shows you how to use the BosonMail SDK with Bun. You will initialize a project, install the SDK, load environment variables automatically, send emails from scripts and HTTP servers, read file attachments with `Bun.file`, and write tests with mocked clients.

> [!NOTE]
>
> BosonWare builds the BosonMail package with Bun (`prepublishOnly: bun run build`) and documents `bun add` in the README. Bun loads `.env` files automatically, so no extra dotenv package is required.

## Prerequisites

* Bun 1.0 or later
* A BosonMail API key

## Project setup

**1. Initialize the project**

Create a new Bun project with TypeScript support.

```bash theme={null}
mkdir bosonmail-bun && cd bosonmail-bun
bun init
```

**2. Install the SDK**

Add the BosonMail SDK from the npm registry.

```bash theme={null}
bun add @bosonware-ent/bosonmail
```

**3. Add environment variables**

Create a `.env` file in the project root. Bun loads it automatically on startup.

```bash .env theme={null}
BOSONMAIL_API_KEY=your_api_key
```

## Sending email from a script

Create a standalone script that reads the API key from `Bun.env` or `process.env` and sends a simple email.

```typescript send.ts theme={null}
import BosonMailClient from "@bosonware-ent/bosonmail";

const apiKey = Bun.env.BOSONMAIL_API_KEY ?? process.env.BOSONMAIL_API_KEY;
if (!apiKey) {
  throw new Error("BOSONMAIL_API_KEY is not set");
}

const client = new BosonMailClient({ apiKey });

const result = await client.sendEmail(
  "customer@example.com",
  "Welcome to BosonMail",
  {
    text: "Thanks for signing up. Your account is ready.",
    html: "<p>Thanks for signing up. Your account is ready.</p>",
  }
);

if (!result.success) {
  console.error("Failed to send:", result.errorMessage);
  process.exit(1);
}

console.log("Email accepted. ID:", result.id, "Message ID:", result.messageId, "Status:", result.status);
```

Run the script:

```bash theme={null}
bun run send.ts
```

## Sending attachments with Bun.file

Read files directly into `ArrayBuffer` and pass them to the SDK.

```typescript send-with-attachment.ts theme={null}
import BosonMailClient from "@bosonware-ent/bosonmail";

const apiKey = Bun.env.BOSONMAIL_API_KEY ?? process.env.BOSONMAIL_API_KEY;
if (!apiKey) {
  throw new Error("BOSONMAIL_API_KEY is not set");
}

const client = new BosonMailClient({ apiKey });

const file = Bun.file("./invoice.pdf");
const buffer = await file.arrayBuffer();

const result = await client.sendEmail(
  "customer@example.com",
  "Your invoice",
  {
    text: "Please find your invoice attached.",
    html: "<p>Please find your invoice attached.</p>",
    attachments: [
      {
        filename: "invoice.pdf",
        content: buffer,
        contentType: "application/pdf",
      },
    ],
  }
);

if (!result.success) {
  console.error("Failed to send:", result.errorMessage);
  process.exit(1);
}

console.log("Email accepted. ID:", result.id, "Message ID:", result.messageId);
```

> [!NOTE]
>
> `Bun.file` returns a `BunFile` that implements `Blob`. Calling `.arrayBuffer()` gives you an `ArrayBuffer`, which the SDK accepts directly. You can also pass the `BunFile` itself as the `content` because it is a `Blob` subclass.

## HTTP server with Bun.serve

Expose a minimal HTTP endpoint that accepts JSON and sends email.

```typescript server.ts theme={null}
import BosonMailClient from "@bosonware-ent/bosonmail";

const apiKey = Bun.env.BOSONMAIL_API_KEY ?? process.env.BOSONMAIL_API_KEY;
if (!apiKey) {
  throw new Error("BOSONMAIL_API_KEY is not set");
}

const client = new BosonMailClient({ apiKey });

const server = Bun.serve({
  port: 3000,
  async fetch(request) {
    const url = new URL(request.url);

    if (url.pathname === "/health") {
      return new Response(JSON.stringify({ ok: true }), {
        headers: { "Content-Type": "application/json" },
      });
    }

    if (url.pathname === "/api/send" && request.method === "POST") {
      let body: Record<string, unknown>;
      try {
        body = await request.json();
      } catch {
        return new Response(
          JSON.stringify({ error: "Invalid JSON body" }),
          { status: 400, headers: { "Content-Type": "application/json" } }
        );
      }

      const to = typeof body.to === "string" ? body.to.trim() : "";
      const subject =
        typeof body.subject === "string" ? body.subject.trim() : "";
      const text =
        typeof body.text === "string" ? body.text.trim() : "";
      const html =
        typeof body.html === "string" ? body.html.trim() : undefined;

      if (!to) {
        return new Response(
          JSON.stringify({ error: "'to' is required" }),
          { status: 400, headers: { "Content-Type": "application/json" } }
        );
      }
      if (!subject) {
        return new Response(
          JSON.stringify({ error: "'subject' is required" }),
          { status: 400, headers: { "Content-Type": "application/json" } }
        );
      }
      if (!text && !html) {
        return new Response(
          JSON.stringify({ error: "Email must contain text or HTML content" }),
          { status: 400, headers: { "Content-Type": "application/json" } }
        );
      }

      const result = await client.sendEmail(to, subject, { text, html });

      if (!result.success) {
        const status = result.statusCode ?? 502;
        return new Response(
          JSON.stringify({
            error: result.errorMessage,
            statusCode: result.statusCode,
          }),
          { status, headers: { "Content-Type": "application/json" } }
        );
      }

      return new Response(
        JSON.stringify({
          success: true,
          id: result.id,
          messageId: result.messageId,
          status: result.status,
        }),
        {
          status: result.statusCode ?? 200,
          headers: { "Content-Type": "application/json" },
        }
      );
    }

    if (
      url.pathname.startsWith("/api/emails/") &&
      url.pathname.endsWith("/status") &&
      request.method === "GET"
    ) {
      const parts = url.pathname.split("/");
      const id = parts[parts.length - 2]?.trim();
      if (!id) {
        return new Response(
          JSON.stringify({ error: "'messageId' is required" }),
          { status: 400, headers: { "Content-Type": "application/json" } }
        );
      }

      const result = await client.getEmailStatus(id);

      if (!result.success) {
        const status = result.statusCode ?? 502;
        return new Response(
          JSON.stringify({
            error: result.errorMessage,
            errorCode: result.errorCode,
          }),
          { status, headers: { "Content-Type": "application/json" } }
        );
      }

      return new Response(
        JSON.stringify({
          success: true,
          id: result.data?.id,
          uuid: result.data?.uuid,
          messageId: result.data?.message_id,
          status: result.data?.status,
          deliveryStatus: result.data?.delivery_status,
          recipient: result.data?.recipient,
          subject: result.data?.subject,
          createdAt: result.data?.created_at,
        }),
        {
          status: result.statusCode ?? 200,
          headers: { "Content-Type": "application/json" },
        }
      );
    }

    return new Response(JSON.stringify({ error: "Not found" }), {
      status: 404,
      headers: { "Content-Type": "application/json" },
    });
  },
});

console.log(`Server listening on http://localhost:${server.port}`);
```

Run the server:

```bash theme={null}
bun run server.ts
```

## Testing with Bun test

Bun has a built-in test runner. Mock the client by injecting a stub or by replacing the module import.

```typescript send.test.ts theme={null}
import { describe, it, expect, mock } from "bun:test";
import BosonMailClient from "@bosonware-ent/bosonmail";

// Example: stub the client method directly
describe("email sending", () => {
  it("returns success when the SDK accepts the email", async () => {
    const client = new BosonMailClient({
      apiKey: "test_key",
      baseUrl: "http://localhost:9999",
    });

    const original = client.sendEmail;
    client.sendEmail = mock(async () => ({
      success: true,
      statusCode: 200,
      id: "123",
      messageId: "msg-456",
      status: "queued",
    }));

    const result = await client.sendEmail(
      "test@example.com",
      "Subject",
      { text: "Body" }
    );

    expect(result.success).toBe(true);
    expect(result.messageId).toBe("msg-456");

    client.sendEmail = original;
  });

  it("returns failure when the SDK rejects validation", async () => {
    const client = new BosonMailClient({
      apiKey: "test_key",
      baseUrl: "http://localhost:9999",
    });

    const result = await client.sendEmail("", "Subject", { text: "Body" });

    expect(result.success).toBe(false);
    expect(result.errorMessage).toContain("recipient");
  });
});
```

Run tests:

```bash theme={null}
bun test
```

> [!TIP]
>
> For integration tests, start a local HTTP interceptor or a mock server on a custom `baseUrl`. The SDK never throws for API or network failures, so you can assert on `result.success` and `result.statusCode` directly.

## Related

> [card] [Sending emails](https://docs.bosonware.com/sdk/sending-emails.md)
>
> SDK reference for sendEmail and its options.

> [card] [Email status](https://docs.bosonware.com/sdk/email-status.md)
>
> How to check delivery status with getEmailStatus.

> [card] [Production best practices](https://docs.bosonware.com/guides/production-best-practices.md)
>
> Key management, retries, logging, and monitoring guidance.

> [card] [Attachments](https://docs.bosonware.com/sdk/attachments.md)
>
> SDK reference for sending file attachments.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.