Emails¶
| Rails | |
|---|---|
| Status | ✅ |
| Code | app/mailers/, app/views/{auth_mailer,account_mailer,trip_created_mailer,beta_mailer,support_request_mailer,reply_mailer,mailers}/, config/locales/{en,de}.yml (mailers.*) |
Verified against code: 2026-10-20
Purpose¶
SailEver sends a few transactional emails: sign-in links, email-address changes, one email per new trip, the beta invitation, the admin notices about beta sign-ups and support requests, and the support reply. There is no newsletter and no per-track email.
General rules¶
- Sender:
SailEver <no-reply@sailever.com>for every email. The support reply addsReply-To: support@sailever.com; the admin notification replies to the requester. - Links point to the first web host (
SAILEVER_WEB_HOSTS, beta:https://beta.sailever.com); without one, tohttps://sailever.comin production (README.md). - Delivery: production sends over SMTP to OCI Email Delivery (
no-reply@sailever.comis its approved sender); a delivery error raises. Development opens mails in the browser (letter_opener), tests collect them. - Every email has an HTML and a plain-text part.
- User emails are rendered in the user's preferred language (
users.locale), falling back to English. Links carry that locale. Missing texts fall back to English (i18n.md). - Links that sign the user in are magic links: valid 30 minutes, usable once. Opening one shows a confirm page; its "Sign in" button signs in (auth.md).
- The HTML part uses the shared layout
layouts/mailer.html.erb(with awidth=device-widthviewport): a centred 600 px column with the SailEver logo on top (120 × 50 px, alt text "SailEver"), and a blue button (mailers/_button). Styles are inline. - The logo is
https://assets.sailever.com/email/logo.png(SAILEVER_ASSETS_URL+/email/logo.png; orange on transparent, readable in light and dark mail clients). That URL never changes, so sent emails keep their logo through the move frombetatowww(infra/assets). WithoutSAILEVER_ASSETS_URL(development, tests) the logo comes from the app.
Overview¶
| Mailer | Trigger | To | |
|---|---|---|---|
| Magic link | AuthMailer#magic_link |
Sign-in requested | User |
| Confirm new address | AccountMailer#confirm_email |
New address saved on the account page | New address |
| Address change notice | AccountMailer#email_change_requested |
Same | Current address |
| Account deleted | AccountMailer#deleted |
AccountErasureJob, before the account is erased |
The account's address |
| New trip | TripCreatedMailer#created |
A new trip's first track is complete | User |
| Beta invitation | BetaMailer#invitation_email |
Admin clicks "Invite" | Applicant |
| Beta sign-up notification | BetaMailer#application_received |
A new email address signs up on /beta |
Every admin |
| Support request notification | SupportRequestMailer#support_request_created |
Support form sent | Oldest admin |
| Support reply | ReplyMailer#reply_sent |
Admin sends a reply | Requester |
Magic link¶
| Part | Content |
|---|---|
| Subject | "Your SailEver Sign-in Link" |
| Heading | Logo / "Your Sign-in Link" |
| Body | "Click the button below to sign in to your SailEver account. This link will expire in 30 minutes." |
| Button | "Sign in to SailEver" → /<locale>/auth/magic_link/callback?token=… |
| Fallback | "Or copy and paste this URL into your browser:" + the URL |
| Footer | "If you did not request this email, you can safely ignore it." |
Sent immediately (not queued). The link's redirect target is kept in the token, not in the URL.
Email change¶
Both are queued when a new address is saved (auth.md).
Confirm new address (to the new address):
| Part | Content |
|---|---|
| Subject | "Confirm your new SailEver address" |
| Body | "Click the button to sign in to SailEver with |
| Button | "Confirm this address" → /<locale>/my/account/email/confirm?token=… (valid 24 hours) |
| Footer | "The link works for 24 hours. If you didn't ask for this, ignore this email." |
Notice (to the current address):
| Part | Content |
|---|---|
| Subject | "Your SailEver address is about to change" |
| Body | "Someone asked to change the sign-in address of your SailEver account to |
| Footer | "Wasn't you? Sign in and change the address back, or reply to this email." |
Account deleted¶
Sent by AccountErasureJob right before it erases the account (auth.md, admin.md), at once (not queued again). The mailer gets the address and language, not the user, because the row is gone right after. English and German texts.
| Part | Content |
|---|---|
| Subject | "Your SailEver account is deleted" |
| Body | "As you asked, we are deleting your SailEver account now: your trips, tracks, boats and devices, your passkeys, and your support requests and beta application. Watches paired with the account no longer upload. You won't hear from us again." |
| Footer | "Didn't ask for this? Reply to this email. We can't bring the data back, but we want to know." |
New trip¶
One email per trip, never one per track.
| Rule | Value |
|---|---|
| Sent when | The trip's first track is complete: a FIT file is processed, or an SLTB track is fully synced. Checked after every import, upload and recompute. |
| Sent once | Only while trips.import_notified_at is empty; it is set in the same atomic update, so concurrent jobs send one mail. Later tracks, trims and recomputes send nothing. |
| Queued | Yes (deliver_later) |
| Part | Content |
|---|---|
| Subject | " |
| Heading | Logo / "New trip in your logbook" |
| Card | Trip title, then three tiles in one row: distance "#ef4444) "Engine". The row fits a 320 px phone screen: fixed table layout, 6 px between tiles, 12 px / 4 px tile padding, 20 px values whose unit wraps under the number when it has to, 16 px card padding. |
| Button | "Open the trip" → magic link that signs the user in and opens /<locale>/my/trips/:id |
| Under the button | "The button signs you in and opens the trip." |
| Footer | "Recorded with SailEver" |
The values are the stored numbers as they are (e.g. 42.53, 5.5), not the web formats.
Beta invitation¶
| Part | Content |
|---|---|
| Subject | "Your Invite to the SailEver Beta ⛵" |
| Body | "Welcome aboard the SailEver Beta! ⛵", "Hi |
| Button | "Activate Your Account & Pair Watch" → magic link that signs in and opens /pair |
| Fallback | The URL as text |
| Signature | "Fair winds, Jakob & The SailEver Team" |
Sent immediately. The body is English on every locale (not in the locale files).
Beta sign-up notification¶
| Part | Content |
|---|---|
| To | Every user with admin (one email, all admins in To, oldest account first); not sent when there is no admin |
| From / Reply-To | no-reply@sailever.com / the applicant's email |
| Subject | "New beta sign-up: |
| Body | "New beta sign-up", then Name, Email, Watch, Signed up (date %B %e, %Y %H:%M) and Why ("—" when watch or reason is empty) |
| Button | "Review and invite" → the applicant in the admin area (/admin/beta_applicants/<id>) |
Sent in the background after the sign-up is saved, only for a new email address: sending the form again with the same address updates the applicant without a second email. English, like the admin area (not in the locale files).
Support request notification¶
| Part | Content |
|---|---|
| To | The email of the oldest user with admin; not sent when there is no admin |
| From / Reply-To | no-reply@sailever.com / the requester's email |
| Subject | "New Support Request - SailEver" |
| Body | "A new support request has been submitted on the SailEver website:", then Name, Email, Topic, Message, Submitted (date %B %e, %Y %H:%M), "Please respond to this request at your earliest convenience." |
| Footer | "This email was automatically generated by the SailEver support system." |
Sent immediately. A delivery error is logged and doesn't stop the request from being saved.
Support reply¶
| Part | Content |
|---|---|
| To / From / Reply-To | The requester / no-reply@sailever.com / support@sailever.com |
| Subject | The subject typed by the admin (default "# |
| Body | Header "SailEver Support Response", "Hello |
| Footer | "SailEver - Your Ultimate Sailing Companion", support email, website https://www.sailever.com |
Sent immediately when the admin replies (admin.md). Rendered in the admin's locale (English).
States and edge cases¶
- Requesting several sign-in links sends several emails; each link works once (rate limit in auth.md).
- A user without a locale gets English.
Known deviations¶
- The magic links in the new-trip email and the beta invitation expire after 30 minutes, like a sign-in link. A user who opens the email later lands on the sign-in page with "This link has expired. Enter your email and we'll send you a new one." and the target kept, so signing in again opens the trip (auth.md); already signed in, the link opens the trip directly.
- The beta invitation has no WatchApp store link, although the install guide says "Your invite email has the install link". Its copy says "link your Garmin watch" and "Web Platform".
- The new-trip email shows raw decimals and decimal hours instead of the web formats.
- The new-trip engine tile uses
#ef4444, not the website's--enginetoken (vessel-state.md). - Support mails use their own styles (
#0066cc,#0d6efd), not the SailEver look.