
A mountable Rails 8 engine for **drafting, approving, scheduling, and batch-sending HTML newsletters**. Compose with Action Text, gate sends behind an approval step, schedule deliberately, and let a background job deliver in batches — all from a backstage area inside your own app.

---

## What Is This?

Maquina Newsletters is a Rails engine you mount under a backstage path (e.g. `/backstage/newsletters`). It gives you a complete newsletter lifecycle without bringing in an external service:

- **Draft and edit** with the Action Text rich-text editor — Trix or Lexxy, your choice
- **Image attachments and embeds** via Active Storage
- **Approval workflow** — a newsletter can't be sent straight from a draft
- **Deliberate scheduling** — pick the date, time, and batch size in one explicit step
- **Batch sending** — split delivery across days, or send to everyone at once
- **Test sends** to any address, plus a **send-now** override
- **Per-issue exclusion list** to drop specific recipients

The engine renders its own UI (themed with `maquina_components`) and resolves recipients from a model and scope you configure. Authentication stays in your hands — the engine inherits from a base controller you point it at.

---

## Requirements

- **Rails 8**
- **Active Storage** and **Action Text** configured in the host app
- **`image_processing` (~> 2.0)** plus an image processor — `ruby-vips` (recommended) or `mini_magick`
- A system image library: `libvips` (recommended) or ImageMagick
  - macOS: `brew install vips` (or `brew install imagemagick`)
  - Debian/Ubuntu: `apt-get install libvips` (or `apt-get install imagemagick`)
- **`tailwindcss-rails`** (engine UI theming) and **`maquina_components`** (host theming)
- **`lexxy`** — optional, for the Lexical-based editor (Rails 8.1+ can auto-configure it)

---

## Quick Start

### 1. Add the Gem

```ruby
# Gemfile
gem "maquina_newsletters", "~> 1.5"
```

```bash
bundle install
```

### 2. Mount the Engine

```ruby
# config/routes.rb
mount MaquinaNewsletters::Engine => "/backstage/newsletters"
```

### 3. Run the Installer

```bash
bin/rails generate maquina_newsletters:install
bin/rails db:migrate
```

The installer sets up the engine's migrations and, if they aren't already present, runs `active_storage:install` and `action_text:install` for you.

### 4. Add Image Processing

```ruby
# Gemfile
gem "image_processing", "~> 2.0"
gem "ruby-vips" # or: gem "mini_magick"
```

Then `bundle install` and install the system library (see [Requirements](#requirements)).

### 5. Wire Up Tailwind

```css
/* app/assets/tailwind/application.css */
@import "tailwindcss";
@import "../builds/tailwind/maquina_newsletters";
```

```bash
bin/rails tailwindcss:build   # or tailwindcss:watch in development
```

Keep `app/assets/builds/*` in `.gitignore` and rebuild on each machine.

### 6. Set the Mailer Host

So image URLs in delivered emails are absolute:

```ruby
# config/environments/production.rb
config.action_mailer.default_url_options = { host: "newsletters.example.com" }
```

---

## Configuration

Create an initializer to tell the engine who receives newsletters and how it's protected:

```ruby
# config/initializers/maquina_newsletters.rb
MaquinaNewsletters.configure do |config|
  # Recipient resolution — which records receive a newsletter.
  config.recipient_model      = "User"          # constantized at use-time
  config.recipient_scope      = :active         # a scope returning a relation
  config.recipient_email_attr = :email_address  # the email column

  # Base controller — see "Authentication" below.
  config.base_controller_class = "BackstageController"

  # Optional HTTP Basic Auth (off by default)
  config.http_basic_auth_enabled  = true
  config.http_basic_auth_user     = ENV["NEWSLETTERS_USER"]
  config.http_basic_auth_password = ENV["NEWSLETTERS_PASSWORD"]
end
```

If the initializer is absent, the defaults are:

| Setting | Default |
|---------|---------|
| `recipient_model` | `"User"` |
| `recipient_scope` | `:active` |
| `recipient_email_attr` | `:email_address` |
| `base_controller_class` | `"ActionController::Base"` |
| HTTP Basic Auth | disabled |

---

## Authentication

**The engine does not provide authentication — that's the host app's job.** Every engine controller inherits from a base controller you configure by name:

```ruby
config.base_controller_class = "BackstageController"
```

Point it at an already-authenticated controller in your app (session checks, etc.) and every engine route is protected automatically.

For apps whose base controller doesn't authenticate, the engine ships an optional HTTP Basic Auth fallback:

- **Enabled with credentials** — challenges with HTTP Basic Auth
- **Enabled without credentials** — fails closed (401 on every request)
- **Disabled** — no built-in auth; relies on the base controller

Don't stack both methods — pick one.

---

## The Newsletter Lifecycle

A newsletter moves through four states:

| State | What happens |
|-------|--------------|
| **Draft** | Create and edit content (subject + Action Text body). Saving creates a draft; no send time is set. |
| **Approved** | Approve a draft when it's ready. You can't send from a draft. |
| **Scheduled** | On an approved issue, set the send timing and batch size. |
| **Sending → Sent** | A background job delivers. A sending/sent issue can't be edited. |

You can move backward too: **Back to draft** (from approved/scheduled/sent) and **Unschedule** (from scheduled back to approved).

### Scheduling

The schedule form appears on an approved issue and takes three inputs:

- **Date** — date picker, today onward (no past dates)
- **Time** — 8:00 AM to 8:00 PM in 30-minute increments
- **Batch size** — recipients per batch. `0` sends to everyone at once; a positive number splits the send across days, one batch per day.

If the chosen date/time has already passed, it auto-rolls forward to the next 30-minute slot and the confirmation says so. Once scheduled, a read-only summary shows **Recipients / Scheduled at / Batch size / Sent at**.

### Send Now & Test Send

- **Send now** — an overflow (⋮) action that delivers immediately to all recipients behind a confirmation, bypassing scheduling.
- **Test send** — available while drafting/approving/scheduling. Sends exactly one email to any address you type, ignoring batch size and schedule, without changing the issue's state. Ideal for preview validation.

### Recipients

Recipients are resolved **at send time** from your configured model and scope (e.g. `User.active`), minus the per-issue exclusion list. The resulting addresses are downcased, de-duplicated, and sorted for stable batching.

---

## Editors

The host chooses the Action Text editor via `config.action_text.editor`:

- **`:trix`** — Rails default, no extra setup
- **`:lexxy`** — Lexical-based; install the `lexxy` gem and wire up its JS/CSS

For Lexxy with importmaps:

```ruby
# config/importmap.rb
pin "lexxy", to: "lexxy.js"
pin "@rails/activestorage", to: "activestorage.esm.js"
```

```javascript
// app/javascript/application.js
import * as ActiveStorage from "@rails/activestorage"
import "lexxy"
ActiveStorage.start()
```

```erb
<%%# in your layout, after the CSS build %>
<%%= stylesheet_link_tag "lexxy" %>
```

On Rails 8.1, installing the `lexxy` gem auto-sets `config.action_text.editor = :lexxy`; set it to `:trix` to override.

---

## Next Steps

<div class="not-prose mt-8 grid grid-cols-1 gap-4 sm:grid-cols-2">
  <a href="https://github.com/maquina-app/maquina_newsletters" target="_blank" rel="noopener" class="group relative rounded-2xl border border-zinc-200 p-6 hover:border-[#50B1FD]/50 dark:border-zinc-800 dark:hover:border-[#50B1FD]/50 transition">
    <h3 class="font-sora font-semibold text-zinc-900 dark:text-white group-hover:text-[#50B1FD] transition">
      GitHub Repository
    </h3>
    <p class="mt-2 text-sm text-zinc-600 dark:text-zinc-400">
      Source code, issues, and contribution guidelines.
    </p>
  </a>

  <a href="/documentation/components/" class="group relative rounded-2xl border border-zinc-200 p-6 hover:border-[#50B1FD]/50 dark:border-zinc-800 dark:hover:border-[#50B1FD]/50 transition">
    <h3 class="font-sora font-semibold text-zinc-900 dark:text-white group-hover:text-[#50B1FD] transition">
      Maquina Components
    </h3>
    <p class="mt-2 text-sm text-zinc-600 dark:text-zinc-400">
      The UI library that themes the engine's backstage views.
    </p>
  </a>

  <a href="/documentation/generators/" class="group relative rounded-2xl border border-zinc-200 p-6 hover:border-[#50B1FD]/50 dark:border-zinc-800 dark:hover:border-[#50B1FD]/50 transition">
    <h3 class="font-sora font-semibold text-zinc-900 dark:text-white group-hover:text-[#50B1FD] transition">
      Maquina Generators
    </h3>
    <p class="mt-2 text-sm text-zinc-600 dark:text-zinc-400">
      Generate the authentication that protects your backstage.
    </p>
  </a>
</div>
