Skip to content

feat: enable email delivery for KPI reports #61

Description

@2ndtlmining

The KPI report currently delivers to Discord only. The email option is present in the dialog but disabled, and POST /api/kpi-report rejects medium: "email", because this instance has no mail transport configured.

What's needed

No separate application — the existing Express server can send directly.

  1. An SMTP account. Any mailbox provider works (Gmail app password, Fastmail, self-hosted). A transactional service (Resend, SendGrid, SES) is preferable for deliverability, since a report sent from a Flux node IP is prime spam-filter material — but it isn't a hard requirement.
  2. Two dependencies: nodemailer for transport, exceljs for the XLSX attachment.
  3. Five environment variables: SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_FROM.

Behaviour

Follow the pattern the R2 backup already uses: the feature is a no-op unless every required variable is set. GET /api/kpi/availability should report whether email is configured so the dialog can enable the option automatically rather than hard-coding it.

Scope

The KPI computation is already delivery-agnostic (buildKpiReport() returns pure data; sendToDiscord() is the only delivery path). So this is:

  • emailService.js — transport, config detection, send
  • HTML email body with the same section/table structure as the Discord embed, inline CSS, no emoji
  • XLSX attachment: overview sheet plus one sheet per section, named fluxtracker-kpi-{timeframe}-{YYYY-MM-DD}.xlsx
  • Server-side email validation (never trust the client); consider a lightweight MX check
  • Reuse the existing rate limiter, keyed on the email address as the destination
  • Mask email addresses in logs
  • README: document the env vars and the enabled/disabled behaviour

Notes

The numbers in the email must come from the same buildKpiReport() call as the Discord path — no second computation, or the two can drift.

See the "KPI Report" section of the README for the current design.


Choosing a relay (added 2026-08-22)

nodemailer and exceljs are both MIT-licensed and free — there is no paid dependency in the
code. The cost question is entirely about the relay we send through, not the library.

Self-hosting a mail server (Postfix) on a Flux node is free and open source but not viable in
practice: a cloud IP with no reverse DNS, SPF, DKIM or sending reputation goes to spam or is
rejected outright. Discounted.

Free-tier relays that speak plain SMTP, so the five SMTP_* variables above work unchanged:

Relay Free tier Own domain required Notes
Brevo 300/day No Easiest start — SMTP creds without domain verification
SMTP2GO 1,000/month No Similar, lower volume
Resend 3,000/month, 100/day Yes Best deliverability of the three; needs a verified sending domain
Gmail app password ~500/day No Works, but sends from a personal mailbox and Google keeps tightening app passwords

Recommendation: start on Brevo (no domain needed, nothing to buy). Move to Resend if
FluxTracker gets its own domain, since a verified domain is what actually keeps reports out of
spam folders.

Design decision needed before building: who can be emailed

The Discord path is self-limiting — possessing the webhook URL is itself proof the requester
controls the destination. Email has no equivalent. An open form that mails an arbitrary
user-supplied address turns FluxTracker into a spam relay wearing our sending domain's
reputation, and the 5-minute per-destination rate limit does not protect against that: a
handful of reports mailed to unwitting strangers is enough to get the domain blocklisted, which
is slow and painful to reverse.

Three options, in order of preference:

  1. Email only for scheduled reports (feat: scheduled KPI reports #63) — destinations are configured by the operator, not
    typed by a visitor. Removes the problem entirely and is the most useful form of the feature
    anyway.
  2. Confirm the address once — first send to a new address delivers a confirmation link
    instead of a report; the address is remembered after that.
  3. Operator allowlist — an KPI_EMAIL_ALLOWLIST env var, simplest to build, least flexible.

Keep the open, unauthenticated form Discord-only regardless of which is chosen.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestkpiKPI report feature

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions