A campaign can go to every lead, account or application a member can see
"All leads / accounts / applications" is a rule, not a list. It is stored with the campaign (the bulk-provider blob, campaignAudiences β no DDL), runs on the ac...
The preference page in every language the product speaks
The page behind every unsubscribe link only spoke English. It now speaks all 22 languages the product UI ships: English, Chinese, Hindi, Spanish, French, Arabic...
An unsubscribe by email is honoured, not dropped
Every bulk and shared-article message offered unsubscribe@<sender's domain> in its List-Unsubscribe header, but nothing read that mailbox - for a tenant it was ...
Opt-out screens refresh live when anyone changes the list
The ledger now tells OptOutChangePublisher after every committed change - a recipient on the preference page, a bounce, a member in Settings - and it publishes ...
The organization owner can open the opt-out list
A brand-new right is granted to nobody on the day it deploys, so a list gated only on MANAGE_EMAIL_OPT_OUTS would ship visible to no organization. The owner hol...
Name the sender once at the top of the preference page
With a logo, the name is its alt text and is not written out again beneath it; without one, the name is the header. It was shown, and read aloud, twice.
A real preference page behind every unsubscribe link
The unsubscribe link opened a bare confirm-or-leave card. Someone who only wanted fewer emails had no way to say so, and nothing told them why they were being m...
The opt-out list only names contacts from organization books
A member's personal address book is theirs; the org-wide opt-out list must not become a way to see whose private contacts include someone.
An org admin can see and change the whole opt-out list
Settings -> Email -> Opt Out now has its API under /api/email/opt-outs, behind MANAGE_EMAIL_OPT_OUTS (god bypasses): a summary with the last 30 days, the list f...
Every suppression check and bounce goes through the opt-out ledger
SuppressionService now asks EmailOptOutLedger, so a check sees the do-not-email list, pauses and topics alike, and a bounce lands in the same history as an unsu...
Several outbound hosts per org, sent round-robin, each warmed on its own, inside a send window
An organization's campaign mail now leaves through any number of outbound hosts, one message at a time in rotation, instead of one bulk provider. - Hosts: an o...
The NoReply ledger can be filtered by subsystem, pruned, and told what to record
Each row now names the subsystem that sent it and, for a template send, the template key. EmailTemplateService and the campaign sender open a NoReplyOutboundSou...
Every campaign is sent as the org's bulk sender, and its From never pauses it
A campaign's own from_email/from_name are no longer read: the From is always the address and name on Settings > Email > Bulk Email Sender (else the org's NoRepl...
Startup checks for the one-address-per-campaign index instead of creating it
EmailService ran CREATE UNIQUE INDEX on every startup. DDL on this cluster costs every service about fifteen minutes of "schema version mismatch", too much to r...
Review fixes to the provider blob, delivery reports and test send
The provider blob is always written in a transaction of its own, so its lock query stays the first statement and a lost lock race can be retried even when a cal...
A warm-up day nothing was sent on does not count
A warm-up day used to be the N-th 24 hours from the start whether or not anything went, so turning the warm-up on days before the first campaign silently spent ...
A from the platform relay would refuse pauses the campaign instead of failing its recipients
Failing the recipients made every one of them contacted, so once the From was fixed the campaign could never send to those addresses. The send path now checks t...
The platform relay is recognised by one host-and-port check everywhere
Refusing an off-domain From matched the relay by host and port, while the warm-up's delivery tracking and the test send's advice matched by host alone, so a pro...
Live campaign progress reaches browsers on every replica
The STOMP broker is in-heap per pod and progress is raised on whichever pod holds the dispatch lease or received a delivery report, so browsers on the other pod...
Campaign mail the platform relay would refuse fails at once instead of being retried
The relay sends only as its own domain and answers anything else with a 451, so a campaign whose From is on another domain used to retry every recipient three t...
Spring can build the bulk-email, warm-up and delivery beans
Several of them have a second constructor taking a clock for tests, and with two constructors Spring needs to be told which to use or the whole context fails at...
New host warm-up holds an org's campaign mail and releases it at the warm-up's pace
While an org's warm-up is on, the dispatcher leaves its campaigns alone and a releaser sends them one message at a time, the earliest due first across the org's...
Every campaign message names its recipient row in X-Kamo-Ref
The bulk relay logs the header against its queue id and strips it before delivery, so delivery results can be matched to the exact recipient row. The composer'...
The smtp envelope sender is set to the bare from address
Campaign mail now carries a display name in its From, and the relay it goes through accepts only its own sender domain, judged by MAIL FROM. mail.smtp.from is n...
The relay's delivery reports bounce, block and count campaign mail after hand-off
POST **************** takes the platform relay's reports of what became of each message, matched to the recipient row by the X-Kamo-Ref header or, failing that,...
The warm-up's schedule, pacing and health rules
A new sending IP is ramped over fourteen warm-up days. Days 1-7 follow a fixed schedule per pace (conservative, balanced, aggressive); from day 8 each day grows...
A campaign cannot be sent or scheduled without a bulk email sender
The dispatcher used to accept such a campaign and then skip it on every tick, leaving it at SENDING with nothing but a log line. sendNow and schedule now refuse...
A missing mailing address no longer blocks sending
assertSendable refused to schedule or send a campaign without a physical address. The org's address still goes in the footer when it has one on file; when it ha...
The smtp connection rule is stated once and shared
SmtpConnectionRule holds the port convention createSmtpSession applies (465 implicit TLS, 587 STARTTLS, STARTTLS elsewhere only when asked for, anything else pl...
The provider settings api answers with the saved state and names the field it refuses
PUT now returns the full settings state instead of {success: true}, which blanked the form after every save. Fields are validated server-side and refused as 400...
A hold'em invitation is gated on the right to play hold'em
HOLDEM_INVITE was in neither the gated nor the ungated map, so it shipped ungated and failed the test that exists to catch exactly that.
A line break in a sender's display name can no longer start a header
Jakarta Mail encodes commas, quotes and non-ASCII in a display name but passes a raw CR/LF through, so displayedAs now flattens control characters to spaces bef...
The NoReply recorder names the constructor Spring should use
It has two, the second for tests, and with neither marked Spring picks neither: the context failed with "No default constructor found" and took EmailSmtpService...
A campaign emails an address at most once, ever
Addresses are matched trimmed and case-insensitively, and a row the campaign has handed to the relay (sent, in flight, bounced, failed, or retried) is now its p...
A ledger of every message sent as the platform NoReply address, readable by god only
Recorded in EmailSmtpService at the one place mail reaches the relay, and decided by the From address, so transactional templates, campaigns and article shares ...
Email an article to someone, as the platform org, with a working opt-out
A visitor on the marketing site can send the seat-fee article to up to three people. POST /api/internal/article-share (X-Internal-Auth, like every other /api/in...
Template logos come from a live theme host
resolveLogoUrls built the logo URL from the org's first domain whatever its state, so an org still setting up its domain got a logo URL that could not be fetche...
A notification raised on the wrong pod reached nobody
The broker here is enableSimpleBroker β in-heap, per pod, no relay β and this deployment runs two replicas. MemberNotificationService published straight into it...
The last two durable consumers become queues, and contact sync reaches every pod
The mail relay was one of four NATS subscriptions on a plain durable consumer. A plain durable admits exactly ONE subscriber, so at two replicas the second is r...
A bulk mark-read announces itself, so the nav badge follows it
The single-message endpoints have always published the mailbox events that move the nav badge and reload the other tabs. The three batch endpoints β flags, move...
Every pod relays the mailbox push, so the unread badge moves again
The browser's email relay bound a DURABLE NATS consumer named after the member ("email-ws-<id>"). A durable push consumer admits exactly one subscriber, so with...
A service may change a meeting's link, and the room is told
updateEvent restated the title, description, location, colour, timezone and deep link, and dropped meetingUrl β so a link added to a callback that had already b...
A preference PUT was resetting every setting it did not mention
Reported as "when I enable repeating sound it does not save". It saved. Then the next preference the member touched turned it off again β and the settings page ...
A meeting link can be set, not only read
meetingUrl has been on the entity, in EventDto and in meetingSignature for as long as the dialog has existed, and nothing could ever write it. EventRequest did ...
The join link reaches the guest's calendar, not just their inbox
An invitation carried its meeting link in the html body and nowhere else, so a guest who accepted it on Monday and opened their calendar on Friday had no way in...
A guest a service put on an event is actually told about it
InternalCalendarService wrote the attendee rows and stopped. It never set sendInvites and never called EventInviteService, so a guest arriving through the servi...
Let a browser report the two conditions only it can see
Every notification so far is raised by the service that watched something happen. These two cannot be: whether a member looked at a conversation within five sec...
Watch a mailbox for the phone, not only for the browser
An IMAP IDLE listener is the only thing that notices a message arriving; it publishes email.inbox.<addr>, which is what eventually asks MediaService to buzz a p...
Preview variables for SYSTEM_BUG_COMMENT
Without them the template editor renders the new letter with holes where the report number, the sender and the button go, which reads as a broken template rathe...
Give the bug-status preview the denial block it now renders
SystemBugStatusEmail pre-renders two conditional blocks, and the admin preview has to supply both. Present and empty rather than absent: an unsupplied placehold...
Like what you see shipping?
Every one of these updates lands in your workspace automatically. Start free and watch it grow week after week.