Skip to content

Board invites: SMTP on port 2525 with a calendar part, API fallback - #107

Merged
getvindicated2day merged 1 commit into
mainfrom
board-invites-smtp
Oct 14, 2026
Merged

getvindicated2day merged 1 commit into
mainfrom
board-invites-smtp

Conversation

@getvindicated2day

@getvindicated2day getvindicated2day commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

The bug

Since #106, board meeting invites go through the Brevo HTTP API with invite.ics as a plain attachment. The API can't send a text/calendar; method=REQUEST MIME part, so Gmail doesn't treat them as calendar invites: no Yes / Maybe / No and no "Add to calendar". We'd moved off SMTP because Railway blocks ports 587 and 465.

The fix

SMTP on port 2525 first.

  • lib/board/invites.ts sends with nodemailer to smtp-relay.brevo.com:2525, with STARTTLS required and BREVO_SMTP_LOGIN / BREVO_SMTP_KEY as the login.
  • It uses nodemailer's icalEvent ({ method: "REQUEST" | "CANCEL", content: ics }). That adds a text/calendar; method=… part and attaches the same .ics as invite.ics, so I didn't add a second attachment.
  • The .ics is unchanged (UID, SEQUENCE, RRULE, RECURRENCE-ID, VTIMEZONE).
  • Each SMTP attempt gives up after 15 seconds, using nodemailer's connection, greeting and socket timeouts plus an overall cap. A stuck connection is closed rather than reused.

The Brevo API as the fallback.

  • If the SMTP variables aren't set, or SMTP fails or times out, the invite goes through the API path from Board invites: send through the Brevo API, 15s timeout on all email #106 (invite.ics attachment).
  • After one SMTP failure, the rest of that batch goes straight to the API, so a blocked port costs one 15-second wait, not one per person.
  • The log records which path each invite took, for example 3 sent (0 over SMTP, 3 through the Brevo API), and why SMTP was skipped.

Organizer test warning. The "Send a test invite to" box shows: "Gmail won't show RSVP buttons on an invite sent to its own organizer. Use a different address to test." It shows whenever the address matches the organizer's in any capitalization. The box is still pre-filled with the organizer's address, so the warning shows until it's changed.

Setup. inviteSetupProblem needs a sender and either the SMTP login and key or BREVO_API_KEY. BOARD_SETUP.md and .env.example explain that BREVO_SMTP_LOGIN and BREVO_SMTP_KEY are needed for full calendar invites, and that without them invites fall back to an attachment.

Outbox mode. BOARD_INVITE_OUTBOX now writes each invite as an .eml, the SMTP version, with the .ics beside it.

nodemailer and @types/nodemailer are back in package.json, at the same version as before #106.

Tested (production build)

Outbox: each .eml was parsed with Python's email package and the .ics with icalendar. Every email is text/plain + text/html + text/calendar + one application/ics invite.ics attachment, and the attachment matches the calendar part.

Step Calendar part method SEQUENCE Other
Test invite REQUEST 0
New weekly meeting REQUEST 0
Edit REQUEST 1
Cancel one date CANCEL 2 RECURRENCE-ID for that date
Cancel the series CANCEL 3

In every case the part's method matches the .ics METHOD.

Each send path, using a test-only preload that sends smtp-relay.brevo.com:2525 to a local stand-in:

SMTP Result
Works (local server requiring STARTTLS and login) 3 sent over SMTP in 0.5s, each with text/calendar; method=REQUEST and the attachment
Hangs SMTP gave up at 15s; all 3 sent through the API (15.1s total); logged
Refused Immediate fallback; 3 sent through the API
Host not found Immediate fallback; 3 sent through the API
No SMTP variables Straight to the API; 3 sent
SMTP and the API both hang 502 "The invites didn't go through. Try again." at 30s
Working again after a hang Next batch went over SMTP

Browser: the organizer warning shows for the organizer's address (any capitalization) and not for other addresses. No sideways scrolling at 390px.

Lint (no errors; 1 existing warning), typecheck and build pass.

Deploy notes

  • Add BREVO_SMTP_LOGIN and BREVO_SMTP_KEY back in Railway (Brevo → SMTP & API → SMTP). Without them invites still go out, as attachments.
  • After deploying, send a test invite to a non-organizer Gmail address and check the server log: "over SMTP" means port 2525 got through Railway. If it says the invites went through the Brevo API, the port is blocked too and invites are still attachments.

Generated by Claude Code

Invites sent through the Brevo API only carried invite.ics as a plain
attachment, so Gmail didn't show them as invites (no Yes / Maybe / No).

- Invites go out over Brevo SMTP on port 2525 (STARTTLS, BREVO_SMTP_LOGIN
  / BREVO_SMTP_KEY) with nodemailer's icalEvent: a "text/calendar;
  method=REQUEST" (or CANCEL) part, and the same .ics attached as
  invite.ics. Each SMTP attempt gives up after 15 seconds.
- If SMTP isn't set up, fails or times out, the invite goes through the
  Brevo API as before. After one SMTP failure the rest of the batch goes
  straight to the API. The log says how many went each way.
- inviteSetupProblem needs a sender plus SMTP or the API key.
- The test invite box warns when the address is the organizer's: Gmail
  doesn't show RSVP buttons on an invite sent to its own organizer.
- BOARD_INVITE_OUTBOX writes each invite as an .eml (the SMTP version)
  with the .ics next to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@getvindicated2day
getvindicated2day merged commit 4664d14 into main Oct 14, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants