மாற்று பதிவேடு
என்ன புதிது.
அது கப்பல்கள் போல், தளத்தில் ஒவ்வொரு மேம்பாடு.
· EmailService
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 its low-volume days. A day that ends with no attempts is now repeated: it and every later day slide forward by the whole days that went unused, any number at once, while days that were used keep their windows and history. Completion moves with them, and the warm-up state reports idleDaysSkipped. The run is settled before the send path counts anything, so a send is never counted against a day that is about to be repeated; and when the counters cannot be read the run is left as it stands rather than closing a day on zeros.
· kamo-internal
A campaign the backend cannot send says why and how to fix it
EmailService now pauses a campaign instead of failing its recipients one by one when it cannot send it: its From is off the platform relay's domain, or there is no bulk sender at all. It refuses send, schedule and resume while the cause stands, and reports it on the campaign as sendBlocker. - the editor and the progress view show a banner with the cause, the server's own sentence and the fix as two steps: change the From (or use the provider default) and resume, or open the bulk sender settings and resume. Resume stays disabled while the draft still has the cause - Resume now saves the draft first, so a corrected From reaches the backend instead of the campaign being resumed as stored and refused again - a refused send, schedule or resume is shown in place with the server's message, not only in a toast, and the campaign is re-read so its status and blocker are current - the campaigns grid marks a blocked campaign beside its status, with the reason on hover - errors from the campaign API prefer the human message over a bare machine code, and carry the status and code
· kamo-internal
Warm-up says idle days don't count and draws the dates that slid
A warm-up day in which nothing is sent no longer counts: the run slides forward and the day repeats when sending resumes, which makes switching warm-up on before the first campaign safe. The panel now says so, beside the switch and again in the idle and not-started notes. When days have been skipped, the day ring says how many, with the schedule moved forward by that many. Every schedule tile shows its date, and the chart's tooltip names it, read from each day as the backend has it now rather than counted on from the first, so the slid dates are the ones drawn. On /marketing/email, an idle warm-up with nothing waiting reads as "on, starts with your next campaign" rather than claiming delivery is being paced.
· EmailService
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 times, each retry a warm-up attempt, before anyone learned the From was wrong. When the org's provider is the relay's host and port and the From's domain is not the relay's sending domain, the recipient is now marked FAILED with the fix in its error: no SMTP, no retry, no suppression, and nothing counted against the warm-up or its slot.
· EmailService
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 campaigns, never past a campaign's own hourly cap and never past the day's warm-up cap. It runs every second under the dispatcher's own lease, so the two can never send the same recipient from different pods. The settings, decided days and safety brake live in the bulk provider's encrypted blob; counters live in Redis and are rebuilt from recipient rows if lost. GET, PUT and reset endpoints under /api/email/bulk-provider/warmup report the state, schedule, queue and health. Both paths now send through one per-recipient method, which claims the row SENDING before the relay sees it. Campaign counters are written without saving the campaign, the interrupted-send sweep included; saving it used to put a campaign paused mid-batch back to SENDING.
· kamo-internal
Recipient rows carry contacted, and already-sent rows keep their mark beside the status
EmailService's recipient DTO now reports whether the campaign has already emailed each address. The mapper carries it through, together with lastError, and fills the campaign id the DTO leaves out. An already-contacted row is never held by warm-up and never emailed again, so the progress grid no longer gives it the warm-up note, and when its last outcome is anything but Sent the status chip keeps a small "Already sent" mark. In the recipients step, a row marked "Already sent" also shows how that send ended when it did not simply go out.
· kamo-internal
The campaign From field says when the bulk relay would refuse it
The campaign's own From field, defaulted from the bulk provider, warns when the address is off the provider's domain, as mail that may land in spam. On the platform relay that is not a risk but a certainty: EmailService fails every recipient of a campaign whose From domain is not the relay's sending domain ("The bulk relay only sends as @domain"). So when the bulk provider carries a platform relay, an off-domain From is an error: the field is marked, an alert says what will happen and offers the provider default when that is on the relay's domain, and preflight blocks sending until it is fixed. Organizations on their own SMTP provider still get the softer spam warning.
· EmailService
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's tests also pin the rest of what a campaign message leaves with: the campaign's From address and name, each falling back to the bulk provider's; a display name that cannot start a header of its own; the unsubscribe mailbox on the domain the mail is actually from; and a Reply-To only when the campaign sets one.
· EmailService
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 latest row sent to the address in the last seven days. A hard bounce (5.1.x, 5.2.1, 5.4.4) marks the row BOUNCED and suppresses the address. A block (5.7.x) marks it FAILED but keeps the address, since the refusal was of the sender, not the mailbox. Anything else permanent is a plain failure. A deferral's reason is kept on the row until delivery. Only rows still SENT are moved, and the warm-up counts each report once, against the day the message was sent, so the reporter can resend a batch safely.
· EmailService
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 by the pace's percentage only if the day before was healthy by its bounce, block and deferral rates, and otherwise holds. Each day is decided once and kept with the reason. Messages are paced from what is left of the day, with mean-one jitter and the occasional break, never faster than four times the steady rate and never so late that the rest of the day's budget no longer fits. The safety brake stops a day that receivers are blocking or bouncing.
· kamo-internal
Warm-up pacing banner on /marketing/email
While the new host warm-up is pacing, campaigns go out far more slowly than their own settings say, and nothing on the campaign said why. The campaigns workspace and a campaign's progress view now carry a slim banner with the day, today's sends against the cap, the next send and what is waiting, linking to the warm-up settings, and a queued recipient explains that it is being held. The banner is hidden while warm-up is off or complete, and whenever the warm-up cannot be read.
· kamo-internal
Typed bulk sender client with field errors and the warm-up endpoints
The client knew a provider status called PENDING that the backend never sends, so a sender that was saved but not yet tested fell through to "Not configured". It now reads PENDING_SETUP (still accepting PENDING) and normalises the whole state in one place: the connection summary, when the password last changed, the platform relay and whether the member may manage it. Failures come back as BulkEmailApiError, carrying the HTTP status and, for a 400, the form field the backend named, so the form can put the message on that field and a 403 can be shown as an access state rather than an error. Empty 204s are tolerated, and a save or warm-up write that answers with a bare acknowledgement is followed by a GET instead of being adopted as the state, which is what blanked the form after every save. Adds the warm-up calls: GET and PUT /bulk-provider/warmup and POST /bulk-provider/warmup/reset.
· kamo-internal
Connection rule, presets and warm-up view model behind the bulk sender
The pure pieces the new screens are built on, each with tests: - the backend's port-first connection rule, mirrored so the form can say how KamoCRM will connect before anything is saved: 465 is implicit TLS with sign-in, 587 is STARTTLS with sign-in whatever Security says, any other port with STARTTLS upgrades and signs in, and anything else is plaintext with no sign-in - one-click presets for the platform relay and common providers - the form as a value: what is dirty, the save body, and validation that only asks for a username and password when the connection signs in - setup readiness and the stages of a test send - the warm-up ramp, countdown, send rhythm and schedule chart numbers useBulkWarmup polls the warm-up every 15 seconds while the tab is visible, pauses while it is hidden, stops on a 403, and applies writes optimistically with rollback.
· kamo-internal
Rebuilt bulk sender tab with a new host warm-up panel
The Bulk Email Sender tab is rebuilt from one long form into a set of cards over a draft of the saved state: - a masthead with the live status (a saved but untested sender now reads "Needs a test"), who it sends as, the stored connection in one line and when it was last tested - a four-step readiness rail: connection, sender, test passed, warm-up - the connection, with presets, a None / STARTTLS / SSL-TLS control, a suggestion when a well-known port is typed, and a live account of how KamoCRM will actually connect, warning when the port overrides the chosen Security - sign-in that is only interactive when the connection signs in, and a password row that says whether one is saved and when, with Replace and Remove staged until save - the sender identity with an inbox preview, warning when the platform relay would refuse the From domain - a sticky save bar, errors from the backend placed on their field, and the saved state taken from the save's own answer - an inline test send that draws where the message got to, marking the stage that refused it The New Host Warm Up panel sits below: on/off, pace, the current day as a fourteen-segment ring, today's sends against the cap with a live countdown to the next send, totals, delivery health and how much of it the relay reports, the 14-day schedule with each day's decision and a cap against actual chart, today's hourly sends and the uneven gaps between recent sends, the queue, the safety brake with Resume now, and Reset. A member without the right to change the sender gets a read-only tab, and one refused the read gets an access state rather than an error.
· EmailService
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 it with a message saying where to set the sender up. A configured host and port is enough; it need not have been verified.
· EmailService
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 {error, field}; a login is required only when the connection will actually sign in. A blank password keeps the stored one, clearPassword removes it, and passwordUpdatedAt records when one was set. Status goes back to PENDING_SETUP only when a delivery-relevant field changed. Every write to the encrypted blob now goes through one locked read-modify-write that keeps keys it does not own, because the blob is about to carry the warm-up as well. The state shows how EmailService will connect, and the platform org is shown its internal relay's settings. The test send says where it failed (connect, TLS, login, sender, recipient, data) with a short fix for each.
· kamo-internal
Preview the email as sent, a Custom / No Template audience, and a real From field
Preview: a dialog that renders the editor's own buildBodyHtml() output - the string Save stores and the dispatcher sends - so the audience's header, signature and footer and the compliance footer are all there and it cannot drift from the real email. Desktop and mobile views, "preview as" any recipient or a sample, merge tokens filled the way EmailService fills them, an inbox-style envelope, and a sandboxed frame. Reachable from the compose step, the footer bar, and a sent campaign's progress view. Audience: "Custom / No Template" wraps nothing around the body. It is saved as COMPANY scope with signatureScope NONE (which the server stores and never reads), and the campaigns grid labels it as Custom. From: its own From name and address fields, defaulted from the bulk email provider and editable per campaign, with a reset to the default and a warning when the address is off the provider's domain. This replaces the sender derived from the audience and the no-reply radio. Recipients: addresses the campaign has already emailed are marked "Already sent", with a notice that they will never be emailed by it again; EmailService now reports this per recipient.
· KlusterServices
Sign hello.kamocrm.com with its own customer key, by the From domain itself
Campaign mail leaves as hello.kamocrm.com through postfix-bulk. It gets its own DKIM key (selector customer, published as customer._domainkey.hello), and use_esld = false so a hello.kamocrm.com message is signed d=hello rather than falling back to the parent's mail key, which fails the strict DKIM alignment (adkim=s) the domain publishes.
· KlusterServices
Postfix-bulk, a second outbound relay for campaign mail, with delivery results fed back
Campaign mail now leaves through its own relay on k3m1 (egress 47.181.8.87) as hello.kamocrm.com, so complaints about bulk mail never touch the reputation of mail.kamocrm.com (47.181.8.84). - Plaintext relay on port 25 for EmailService only (NetworkPolicy); it accepts only @hello.kamocrm.com senders and refuses anything else with a 4xx, never a 5xx, because the campaign dispatcher suppresses on any 5xx. - An egress guard holds outbound delivery unless the pod's public address is 47.181.8.87, so bulk can never quietly leave as another IP. - A delivery-events sidecar reads the mail log and reports each campaign message's delivered / deferred / bounced / expired result to EmailService, joined by the X-Kamo-Ref header, which is stripped before delivery. Applied by hand for now: deploy-services.yml applies mail/ files by name and does not list these yet.
· EmailService
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 permanent record of that address: never deleted, reset or re-queued. - materializeRecipients used to delete every row and rebuild the list on each save, so editing a paused campaign put already-emailed addresses back in the queue. It now keeps history, refreshes rows not yet contacted in place, removes only uncontacted rows that left the list, and never re-adds an address the campaign has contacted, however many times it is added. - The dispatcher claims a row SENDING before the relay sees it. A crash between the relay accepting a message and the row being marked SENT used to leave it QUEUED and send it again; now it stays SENDING and a sweep closes it out as FAILED without retrying. - A recipient added while a campaign was paused never got a release time, so it was never sent and the campaign never finished. Release times now go to any queued row that lacks one. - A startup unique index on (campaign_id, lower(trim(email))) backs the rule at the table. The recipient list endpoint now returns each row's name, merge values, source and whether it was contacted, so reopening and re-saving a draft no longer erases personalization. The From address falls back to the bulk provider's, and the From name is actually sent (it was stored and dropped).
· kamo-internal
NoReply Outbound, a god-only audit of mail sent as the platform NoReply address
A third section under Mail, shown only to god-eligible members, opens /messages/outbound/noreply: every message EmailService handed to the relay as the platform NoReply address, newest first, with search, a Sent/Failed filter and a drawer showing the full message. The page turns away anyone who is not god-eligible. That is the courtesy, not the lock: the data comes from EmailService, which refuses any session without GD.
· EmailService
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 are all covered without opting in. Refusals are recorded with the relay's reason and still rethrown. The write runs in its own transaction and never throws: a failed audit row cannot fail or roll back a send. GET /api/email/noreply-outbound lists the ledger (search, status filter, paging, bodies excluded); /{id} returns one message with its body. Both require GD on the session and nothing else: the ledger spans every organization, so no org-scoped right can open it. The table is created on startup by NoReplyOutboundSchema, as the article-share ledger's is.
· kamo-internal
A tracking link can land on the white-label CRM fees article
Adds **************** to the destinations the tracking-link picker offers on /marketing/tracking, with its own icon. The path is locale-free like its siblings: the marketing site 308s it to **************** and keeps ?discount= across the hop, the same as /pricing (checked against the live site). The row's label and hint are in kamo-translation-dictionary 7a6fd4e2.
· kamo-marketing
Let a reader send the seat-fee article to someone they know
A "pass it on" form at the foot of **************** reached from a chip at the top and a line after the seven questions. The reader names up to three people and we email each of them the article once, with the reader's name on it; they can also open it in their own mail client or copy the link instead. POST /api/share-article gates it before EmailService sees it: same-origin only, a *** verified server-side against the *** service (the other forms here forward the payload without anyone checking it), and self-send only for visitors in the EU/EEA, the UK and Switzerland. It forwards a body built from bounded fields, with the visitor address our ingress recorded, to EmailService's /api/internal/article-share, which enforces the limits and the platform org's opt-out list. Deployment reads INTERNAL_AUTH_SECRET from the existing mlos-internal-auth secret (optional, so the site still starts without it) and gains EMAIL_SERVICE_URL and CAPCHA_SERVICE_URL.
· EmailService
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/internal endpoint) sends each of them one email as the top-level organization, from "<sender> via KamoCRM" with Reply-To the sender. Reuses what exists instead of adding a second copy of it: - Opt-outs live in email_suppressions, keyed to the platform org's id, so an opt-out here never touches a hosted org's list and an org's list never blocks this. The link is a signed token (orgId|email, HMAC) that the existing UnsubscribeController and page now accept alongside campaign tokens, with the same RFC 8058 List-Unsubscribe headers campaigns send. - Delivery goes through **************** which gains DeliveryOptions (display name and extra headers) so From, the SPF decision and the relay fallback stay decided in one place. A caller-built letter has no template row, so the usage counter is skipped for it. Limits, counted from a hashed ledger **************** created on startup) so every pod agrees: three people per request, preset notes only, 10 a day per sender and per visitor address, 500 a day overall, one email per address every 90 days, never to a suppressed address. Also sets bulk-email.unsubscribe-base-url in the prod configmap. The code default is blank, which sends campaign mail without its unsubscribe link.
· kamo-marketing
Put every page in /sitemap.xml itself, and list pages only in the languages they have
/sitemap.xml was an index, so opening it showed 79 file names rather than the site's pages. - /sitemap.xml is now one <urlset> with every page: 18,700 of them, each at its English address, with every language version it exists in as an hreflang alternate. That is within the protocol's 50,000-URL / 50 MB limit for a single file; past it, the file is rejected whole. Changelog entries go in without their alternates, which would put the file near 60 MB. Each entry page declares its languages itself. - The per-language files, where every language version is a URL of its own, move to /sitemap-index.xml. robots.txt names both files. - A page whose copy has its own message namespace is listed, and names alternates, only in the locales that namespace is really translated into: at least 90% of its strings present, fewer than 30% identical to the English (app/lib/message-locales.ts). The new **************** article and /resources have no translations yet. They were in all 21 locales, 20 of them the English page under another prefix. - Collection pages read the same live measure for `collections` in place of the hard-coded COLLECTION_CONTENT_LOCALES. Swahili collection copy has been translated since that list was written, so 132 real Swahili pages were pointing their canonical at English.
· kamo-internal
A department's Select checks the members it is showing
The Directory's per-department Select used to switch the whole list into select mode with nothing checked, and read "Done" until pressed again. It now checks every member that department is showing (the search and the All / Online / Offline strip decide who) and lets go of anyone checked outside it. It always reads Select, and is disabled for that department alone while the selection is exactly those rows; unchecking one, checking someone elsewhere, or a filter that shows different rows brings it back. The viewer's own row is left out: a group chat or meeting they start includes them anyway, and a meeting would otherwise send them a chat invite to itself. Disabled with aria-disabled so a keyboard user keeps focus inside the panel's trap, and dimmed with opacity because .operator-label's color is !important under tailwind's `important: true`.
· kamo-marketing
Public service announcement on who keeps software seat fees
New page at **************** explaining, for non-technical owners, how resold white-label platforms work: a flat agency license, per-seat fees the agency keeps, and metered usage billed on top. Ends with the questions to ask before signing and how KamoCRM differs. Server-rendered article with Article and BreadcrumbList JSON-LD, its own OG card, a seat-fee calculator, and CTAs for free signup, demo, contact and phone. Linked from the Resources menu and hub, from the site banner as a PSA to the left of the change log, and listed in the sitemap.
· kamo-marketing
List every page in every language it exists in, and make each page agree
/sitemap.xml listed each page once, as its English URL. The 17,843 changelog entries had no language alternates at all, and every page's own hreflang named all 21 locales whether or not the page existed in them. - Every language version is now a <url> of its own, carrying the page's full, reciprocal hreflang cluster and x-default: Google's recommended form, and the one that gets a localized page found without waiting for a crawler to follow an alternate link to it. - A version is listed only where it is its own canonical: SITE_LOCALES for hand-written pages, blog posts and changelog list pages; COLLECTION_CONTENT_LOCALES for comparisons, solutions, integrations and the glossary; and for KB articles and changelog entries, the locales KBService and SecurityService report a real translation for. An echo of the English does not count. Listing a version that canonicals to English submits a URL that cannot be indexed. - Each page's <head> names exactly the same set. localeAlternates() takes the locales a page exists in, and a locale outside them canonicals to English with no noindex. Collection pages in locales without collection copy used to be noindex while every other page still named them as alternates. - The index is split by section (pages, collections, each KB section, changelog list pages, changelog entries), each file with its newest lastmod and well inside the 50,000-URL / 50 MB limits. Changelog files run oldest first, so a full file never changes again. - Changelog list pages 2..N are listed; each is its own canonical. - /sitemap.json is one streamed document holding every page and every language URL. /api/sitemap serves the same document. - A source that cannot be read answers 503 rather than a partial sitemap.
· KBService
Say which locales each public article is really translated into
The marketing site's sitemap now lists every language version of every help-center, documentation and API-reference article, and each article page names the others as its hreflang alternates. Neither could tell a translation from an echo: TranslateService answers directions it has no provider for with the English source, so the sw and ta rows repeat the English title and body word for word (measured 2026-09-11). Naming those would point search engines at pages that canonical back to English. Tree nodes and single reads now carry `locales`: the locales whose title or body differs from the English article. The body alone is enough - an Arabic row can keep the English title over a translated body. Computed in one query per request by KbTranslatedLocalesService, which reads through the EntityManager so no shared-library release is needed.

உங்கள் உலகம் காத்திருக்கிறது.
நிமிடங்களில் இலவசமாகத் தொடங்கவும். நீங்கள் தயாராக இருக்கும் போது பொருட்களை சேர்க்கவும் - எந்த மாற்றமும் இல்லை, புதிய உள்நுழைவுகள் இல்லை.
எப்பொழுதும் இலவசம் — கடன் அட்டை தேவையில்லை