Commit Graph
31 Commits
Author SHA1 Message Date
joao-crm afdeb9622e feat: release the selected IMAP mailbox before the LIST-STATUS poll so Dovecot servers stop reporting a frozen HIGHESTMODSEQ and live sync keeps detecting new mail after the first fetch 2026-08-24 23:43:51 +00:00
Matthew Meszaros 9874ff1291 feat: make the worker answer every SEND_EMAIL it acks with exactly one per-task EMAIL_FAILED or EMAIL_SENT, raising the typed account events (auth, disabled, rate limited, server error) in addition with the EmailErrorEvent body the consumer expects instead of a SendEmailResult it could not parse; a mailbox that is not loaded yet or a storage blip is left for a few bus redeliveries before being reported, using the new Message.Attempt and Redelivers fields the NATS and Kafka buses now fill in, and an SMTP INVALID_CREDENTIALS send failure is classified as an auth error 2026-08-23 10:18:24 -07:00
Matthew Meszaros 94cf21d95e Fix truncated, unformatted and mis-encoded email content in the unibox (#137)
* feat: add internal/pkg/mailhtml, a mail-oriented HTML sanitizer and text flattener, because rendering a received message body means rendering the sender's markup: Sanitize builds on bluemonday's UGC policy but keeps what real email is made of (table layout attributes, inline CSS through the property-allowlisted style sanitizer, legacy font/center, data: and https: images) while dropping script, iframe, object and the text content of style/head blocks so a marketing email's stylesheet cannot render as body copy, forcing target=_blank plus nofollow/noreferrer on links and allowing only http, https, mailto and tel; ToText flattens the same input for previews, turning block boundaries into newlines and decoding entities back to the characters they stand for so an already-escaped body does not surface as literal &; LooksLikeHTML reports whether a stored body is actually markup, which is how a body recorded as HTML by an older sync but containing no tag at all can be recognised as the plain text it really is

* feat: add internal/pkg/mailhdr for RFC 5322 header values, since headers are ASCII on the wire and every transport was writing raw UTF-8 into Subject and display names: Subject and AddressList RFC 2047-encode non-ASCII (a no-op on plain ASCII, and a bare address stays bare rather than being wrapped in angle brackets), DecodeWords reverses encoded-words with a charset hook wired to go-message so legacy encodings Go does not handle natively still decode, and Bare/BareList strip a display name down to the routable address for SMTP envelope commands where 'Ana <a@b.com>' in RCPT TO is a syntax error, promoting go-message from an indirect to a direct dependency

* feat: encode outbound Subject and address headers on all three transports, so a subject or sender name containing an accent, a currency sign or an emoji reaches the recipient as the characters the user typed instead of mojibake: SMTP and Graph were writing the raw string into Subject (only the Gmail transport encoded it) and Graph built its From by fmt.Sprintf rather than mail.Address, so a non-ASCII display name went out unencoded there too, and all three joined To/Cc/Bcc entries verbatim so an encoded display name never appeared even when the caller supplied one; additionally the SMTP envelope now takes bare addresses through mailhdr.BareList, because an API caller may pass 'Name <addr>' (the compose handler has a bareAddress helper precisely because that arrives) and passing that to RCPT TO gets the recipient rejected by the server

* feat: rewrite the IMAP body reader, which was the reason received mail from SMTP/IMAP mailboxes came back corrupted: it built one FetchItemBodySection with a hardcoded Part []int{1} and a comment saying it would adjust when recursing, which it never did, so on a multipart/alternative the text/plain bytes were fetched twice and the second copy was stored as the HTML body (plain text rendered as markup loses every line break, shows & as an entity and swallows anything inside angle brackets), and decodeIfNeeded never reversed Content-Transfer-Encoding at all, leaving quoted-printable bodies full of =E2=80=99 runs and = soft breaks and base64 bodies unreadable, while its charset detection parsed params off a media-type string that never carried any and its mail.ReadMessage call could silently eat leading body lines as headers; the reader now walks the body structure for real part paths, fetches every text leaf in a single FETCH with a server-side Partial size cap, decodes quoted-printable and base64 (tolerating a tail cut mid-quantum by the cap) then converts the part's charset to UTF-8 with go-message, skips attachment-disposition parts so a .txt attachment cannot stand in for the body, takes one part per type inside a multipart/alternative but treats sibling inline parts in mixed/related as additive, and is bounded at five text parts per message; the stored body cap also goes from 200 KB to 512 KB because 200 KB cuts real HTML newsletters mid-document

* feat: decode Gmail's raw headers and entity-escaped snippets, because the Gmail API hands header values back exactly as they arrived on the wire, so a message from a sender whose subject or display name was RFC 2047-encoded showed in the dashboard as =?utf-8?q?caf=C3=A9?= rather than as the text it stands for, and the API's own snippet field is HTML-escaped, so a preview containing an apostrophe surfaced as &#39; in the conversation list and, until the thread reader stopped rendering snippets as message bodies, inside the message itself; getSingleHeader now runs values through mailhdr.DecodeWords (a no-op unless the value actually contains an encoded-word, so Message-ID and the warmup token header are untouched), the comma-split fallback in getAddressList does the same for display names net/mail could not parse, and the snippet is unescaped once on the way in

* feat: fix the conversation-list snippet, which collapsed whitespace before splitting on newlines so the quoted-line and signature filters below it could never match a thing, stripped HTML with bluemonday's strict policy and then showed the escaped output verbatim so an ampersand in an HTML-only message read as &amp; and a marketing email's stylesheet text rendered as body copy, and cut at 100 bytes with text[:100] so a multi-byte character or emoji at the boundary became a replacement glyph; it now flattens HTML through mailhtml.ToText (entities decoded, style and script content dropped) including when a sender puts markup in their text/plain part, filters quoted history and everything past the RFC 3676 signature delimiter while the text still has lines, collapses whitespace afterwards, and truncates on a rune boundary at 200 characters

* feat: make GET /unibox/:id serve a display-safe body and stop it failing outright, sanitizing body_html through mailhtml before it leaves the API so every consumer gets markup that cannot execute rather than each call site having to defend itself, degrading a body blob that cannot be read to the message's preview text with a new body_truncated flag instead of returning 500 (which made a message with a missing blob unopenable, and hit every seed, sandbox and dev-history fixture row since only the '<seed-' prefix was recognised while the sandbox uses '<sbx-' and dev history '<dev-'), and treating a stored HTML body that contains no tag at all as the plain text it really is, because mail synced before the IMAP reader addressed parts individually recorded the plain part under both bodies and serving that as HTML is exactly what collapsed a ten-line message onto one line

* feat: escape composer text before turning it into the HTML part of an outgoing email, replacing body_html: trimmedBody.replace(/\n/g, '<br />') in both the compose window and the reply composer with a shared plainToHtml that escapes the five markup characters first, so an email containing 'Terms & conditions' no longer ships a broken entity and one containing anything in angle brackets ('<see attached>', 'a < b', a pasted tag) no longer has the rest of the paragraph swallowed by the recipient's mail client as an unclosed tag, while runs of spaces survive as non-breaking spaces and bare URLs become links without eating the sentence punctuation after them; the same unescaped plain-to-HTML pattern in the campaign step editor's applyTemplate now goes through promptToHtml, which escapes as it paragraph-wraps

* feat: render the real message body in the unibox thread reader instead of the list preview, which is the whole of the reported bug: ThreadView mapped each thread row to a UniboxEmail whose body was '<p>' + escapeHtml(m.snippet) + '</p>' and MessageBubble rendered that as the message, but a snippet is a preview capped at 100 characters with every run of whitespace collapsed to one space, so a ten-line email displayed as roughly two lines on a single continuous line, and Gmail's already-escaped snippet was escaped a second time so an apostrophe read as &#39;; each expanded message now loads its own body from GET /unibox/:id (the newest message and anything unread open on mount, older messages collapse to their preview line so a long thread does not fetch every body at once) and renders it in a sandboxed iframe carrying no allow-scripts, which keeps a sender's stylesheet from restyling the dashboard and means nothing in the message can run even though the API already sanitized it, sizing itself from the inner document as images load, with the preview kept as the fallback when a body cannot be fetched and a notice when only a preview is stored

* feat: document how a message body is read and returned, adding a 'Reading a message' section to the unibox guide covering the expand-on-open behaviour, that formatting and special characters are preserved as sent, that the conversation list preview is a summary and not the message, and that HTML mail renders in an isolated frame with links opening in a new tab, plus a paragraph in the API endpoint reference stating that GET /unibox and GET /unibox/thread return previews carrying snippet while GET /unibox/:id returns body_plain and a sanitized body_html, and what body_truncated means

* feat: add email_accounts.save_to_sent, the per-mailbox switch for filing a copy of outbound mail in the Sent folder, defaulting on because plain SMTP submission leaves nothing behind in the sender's account while Gmail and Outlook file their own copy through their APIs, making it a per-mailbox choice rather than a global one since a submission server that files the copy itself (Gmail's SMTP, Fastmail, Zoho) would otherwise end up with two of everything, which is exactly why every desktop mail client ships the same switch, and wiring the column through the Email model, the mailbox read paths and UpdateEmail so it is readable and writable from the dashboard and the API

* feat: teach the IMAP client to APPEND a sent message and the SMTP client to hand back the exact bytes it submitted, the two transport pieces the Sent-folder copy needs: AppendToSent resolves the folder from the RFC 6154 \\Sent special-use attribute first (requesting it only when the server advertises SPECIAL-USE) and falls back to matching the known names against both the full mailbox name and its leaf, since servers namespace as INBOX.Sent and localize the label, caches the result for the life of the connection, files the message flagged \\Seen and dated when it was sent, and returns a sentinel rather than an error when the account has no Sent folder at all; APPEND addresses its mailbox by argument and never touches the selected mailbox, so unlike the warmup MOVE/STORE actions it is safe to run while the sync loop is mid-fetch on the same connection

* feat: file a copy of every SMTP send in the mailbox's Sent folder, closing the gap where a message sent from Warmbly through an SMTP/IMAP mailbox existed only in the recipient's inbox: nothing appeared in the customer's own mail client, and nothing appeared in the unibox either, whose thread reader can only show messages the sync found in a folder, so a user who sent from the dashboard and then went looking for what they sent found no record of it at all; the worker now APPENDs the exact bytes the SMTP client submitted after a successful send, best effort so a failed append never turns a delivered message into a failed task, skipping warmup traffic because filing dozens of machine-generated messages a day would bury the customer's real sent mail, and skipping Gmail and Graph mailboxes entirely since their APIs file their own copy; the per-mailbox setting rides along on the add-email worker payload as a pointer so an older control plane that does not send the field is read as unset and takes the default rather than as an explicit no

* feat: expose the Sent folder copy as a mailbox setting in the dashboard, adding a 'Keep a copy of sent mail' toggle to the Settings tab of the mailbox drawer that only renders for SMTP/IMAP mailboxes (Gmail and Outlook file their own copy, so the control would be a lie there), tracked by the drawer's save bar alongside the other editable fields, and worded so the one case where it should be turned off is obvious: a provider that already saves its own copy, where leaving it on means seeing every sent message twice

* feat: document the Sent folder copy in the mailboxes guide and the API reference, explaining why the toggle exists at all (SMTP submission leaves nothing in the sender's own account, so without it a sent message shows in neither the customer's mail client nor the unibox thread), when to turn it off (a provider such as Gmail, Fastmail or Zoho that already files its own copy of anything submitted over SMTP, where leaving it on doubles every message), that OAuth Gmail and Outlook mailboxes never show the control because their APIs file the copy themselves, that warmup traffic is deliberately excluded, and that PATCH /emails/:id takes save_to_sent

* feat: add unibox_emails.body_text and its search index, because unibox search ran against search_tsv, a generated column built from subject and snippet, and a snippet is a truncated one-line preview, so searching for a phrase that appears in the third paragraph of an email returned nothing at all and read as broken search rather than as search that only covers the first line; message bodies stay in object storage where they belong, and what lands in Postgres is a bounded 16 KB plain-text rendering carried on the new-email worker event, indexed with a GIN expression index rather than a second stored generated column since adding one of those rewrites the whole table while this builds against a column that is empty on every existing row

* feat: index what a message actually says, adding mailhtml.SearchText (HTML flattened, entities decoded, whitespace collapsed, quoted history deliberately kept because a phrase someone quoted back at you should still find the conversation, truncated on a rune boundary) and computing it on all three sync paths so IMAP, Gmail and Graph mail all arrive with searchable text, writing it on insert, and widening the unibox search filter to match either the existing subject-and-preview vector or the body expression, written exactly as the new index declares it so the index is actually used

* feat: backfill the searchable text of messages that were synced before bodies were indexed, so search covers the archive a customer already has instead of only mail that arrives from now on, which would have made the feature useless on day one for exactly the people who need it; the sweep pages through unibox_emails by id, reads each body from object storage under the mailbox owner's key, renders it with the same helper the sync path uses and writes it back, at 100 rows per 30 seconds because nothing waits on it, and returns for good once a pass finds nothing left to visit, with rows whose stored body really is empty simply revisited after the next restart rather than needing a tried-and-failed marker in the schema

* feat: document that unibox search now covers message text and not just subjects and previews, in the search paragraph of the unibox guide where the old wording only promised that search stays inside the current scope

* feat: add generation.RenderThread, the shared way to put a conversation in front of a model, because every AI surface was grounding on preview snippets and a draft written from the first hundred characters of each email answers the greeting rather than the question; it strips quoted history and signatures (the earlier messages are already in the prompt on their own, so quoting them again spends the budget twice, though a reply written underneath the quote is kept rather than thrown away when there is nothing meaningful above the attribution line), spends a bounded character budget newest-message-first since the message being replied to matters most, degrades older messages to their preview line instead of dropping them once the budget runs low, and renders oldest-first so the transcript reads in order

* feat: add grounding reads to the unibox service and repository, returning message text (the stored body, falling back to the preview for mail synced before bodies were indexed) for a thread or for all correspondence with one address, kept deliberately separate from the preview queries and given their own result type so a 16 KB body can never leak into a list response by accident, capped at twenty messages whatever a caller asks for, and paired with a RenderGrounding helper so every AI surface formats a conversation the same way instead of each one rolling its own transcript loop

* feat: ground every AI writing surface in what the messages actually say, switching the unibox reply draft, the compose draft's correspondence history, the inbox agent's thread history and the assistant's read-thread tool from preview snippets to real message text through the new grounding reads, which is what makes a drafted reply answer the question that was asked rather than the first sentence of the email; the inbox agent's triviality gate also reads the reply's full text now, since a preview line cannot tell a one-word ack apart from a long message that happens to open with one, and the assistant tool returns a bounded body per message with quoted history stripped instead of a snippet field

* feat: say in the docs that AI drafting reads the messages and not their previews, in both the unibox reply-draft section (adding that quoted history is stripped and the newest messages get the most room, so a draft answers what was asked rather than the opening sentence) and the inbox agent's grounding section, where 'the full thread so far' was true of the message list but not of how much of each message the model actually saw

* feat: renumber the two new migrations to 000087 and 000088 after rebasing onto main, which landed its own 000085 (org data transfer) and 000086 (email sync state) in the meantime, so the sequence has no duplicate versions

* feat: add the two new API fields to the OpenAPI spec that landed on main while this branch was open, documenting save_to_sent on the Mailbox and MailboxUpdate schemas and body_truncated on UniboxEmail, and saying on body_html that what the API returns is already sanitized so a client can render it directly
2026-08-21 18:45:09 +02:00
Matthew Meszaros a75ea012a0 feat: import a mailbox's recent history on connect and govern sync by fair use: a backfill on every provider (newest first, inside an operator-editable window and cap, resumable through a durable per-provider cursor relayed as SYNC_STATE), a per-mailbox sync governor with priority, live and backfill lanes on shared Redis windows that defers over-budget mail with the cursor held instead of dropping it and only deactivates a mailbox for a flood or chronic daily overage, sync.* budgets on the admin instance settings shipped inside ADD_EMAIL, saved IMAP folder cursors and last_synced_at finally written, a Sync card in the mailbox drawer fed by GET /emails/:id/sync, and docs 2026-08-18 09:09:52 -07:00
Matthew Meszaros ec4cd3160b feat: thread Unibox dashboard replies into the conversation they answer, by carrying the composer's thread_id all the way from email_tasks to the provider (EmailMessage had no ThreadID field at all, so the column was read from the database and silently dropped in user_email_task, and Gmail only appends to an existing thread when threadId is set on the outbound message since a matching Subject and In-Reply-To do not do it), populating the models.SendEmail.Parent field that already existed with an avro tag and that the worker already read but nothing ever set, replacing the worker's gate that required InReplyTo to be non-empty before it would look at Parent (a dashboard reply never sets that header, so a perfectly valid ThreadID was discarded and the provider opened a new conversation) with a parentReference helper shared by the Gmail and Graph send paths that resolves the two genuinely independent handles separately, and backfilling the RFC In-Reply-To header server-side in UniboxReply from the newest Message-ID in the thread via a new org-scoped LatestMessageIDInThread query, because a provider thread id is meaningless outside the mailbox that issued it and the recipient's mail client can only thread on References and In-Reply-To (#122) 2026-08-16 07:46:16 +02:00
Matthew Meszaros 2c56dc9075 feat: make the Gmail history checkpoint persist and advance, by setting UserID and EmailID on the JobEventHistoryIDUpdate that NewHistoryID publishes (email_history_ids is keyed (user_id, email_id) with a foreign key to users, so the zero UUIDs it was sending made every checkpoint write fail on email_history_ids_user_id_fkey and no Gmail mailbox ever got a row, which is why replies, opens via label changes and every other inbound signal never reached Unibox), bootstrapping a mailbox with no baseline from Users.GetProfile instead of calling history.list with startHistoryId=0 which Gmail rejects with 'Requested entity was not found' so a freshly connected mailbox could never establish one, advancing the in-memory GoogleData.LastHistoryID after a successful walk since it was only ever written at construction and a stale cursor re-walks the window just processed while a zero one re-bootstraps past everything that arrived in between, and no longer discarding a MailError when a partial history was processed in the same tick, matching the field assignment the already-correct but uncalled ImapGoogleSync has had all along (#120) 2026-08-16 07:46:11 +02:00
Matthew Meszaros 16b672e6f6 feat: wire OnTokenRefresh on the Gmail worker client so every send and sync stops panicking, since goog.Client was constructed with all four message and label callbacks but no token callback while goog.Init unconditionally wrapped the token source in stoken, whose Token() calls that callback on every single request from inside the oauth2 transport's RoundTrip, making the nil func value a guaranteed nil-pointer dereference on the first Gmail API call any mailbox made (the Outlook path immediately below it set the same field correctly, so no Microsoft mailbox was affected), additionally guarding both goog.Init and msgraph.Init so the stoken wrapper is only installed when there is somewhere to persist a refreshed token to, hardening stoken.Token itself against a nil callback because it runs inside RoundTrip where a panic takes down the caller's request rather than surfacing as an error, and adding a regression test that panics without the guard and passes with it (#118) 2026-08-16 07:46:00 +02:00
Matthew Meszaros 50f50e680d feat: make the inbound mail pipeline work end to end by never publishing the eventbus partition key as Nats-Msg-Id (JetStream deduped every event after the first per mailbox), fetching IMAP message bodies after the outer FETCH closes instead of nesting one inside it (which deadlocked sync on the first message), wrapping NEW_EMAIL in JobEventNewEmail across all three providers so the consumer stops nil-derefing, coalescing nil arrays before the NOT NULL unibox columns, sealing validation credentials on a copy so stored SMTP/IMAP passwords are not double encrypted, routing the email task type to the user email handler, and returning false instead of closing a nil conn in VerifySMTP (#88) 2026-07-31 09:41:36 +02:00
Matthew Meszaros bb646709fa feat: return the message count from the sync SELECT and skip FETCH on empty mailboxes - a 1:* sequence set against zero messages is a server error, so every fresh mailbox failed its first sync pass before any mail arrived 2026-07-11 19:47:14 +02:00
Matthew Meszaros cd00d06e8b feat: select the mailbox (read-only, CONDSTORE) before each sync FETCH - the IMAP sync loop issued FETCH against a session with no selected mailbox, so every generic-IMAP sync pass failed and inbound mail never reached the consumer 2026-07-11 19:20:29 +02:00
Matthew Meszaros 50aa0a3b3d feat: contain panics in the per-mailbox sync loop - the worker is multi-tenant, so one mailbox hitting a bad server response must not take down every other account on the process 2026-07-11 17:30:20 +02:00
Matthew Meszaros ab3c925eca feat: detect permanent NDRs worker-side from the synced bounce body across gmail/graph/imap and emit an INBOUND_BOUNCE event, giving api-sent mail its only bounce signal without adding SQL or S3 to the worker 2026-07-04 11:36:47 +02:00
Matthew Meszaros b8e4002adb feat: fetch the warmup verify token and machine-reply headers via BODY.PEEK on IMAP sync and surface them as flag pseudo-headers, and join replies to the parent's thread instead of forking a new thread at every reply depth 2026-07-04 09:30:27 +02:00
Matthew Meszaros 3e4c3e9655 feat: write the caller's Message-ID header on SMTP sends so the recorded id matches the wire message for reply and bounce correlation, classify refused recipients as recipient-rejected instead of server-unreachable, and use the resolved host for the SMTP client handshake 2026-07-04 09:30:27 +02:00
Matthew Meszaros 1d4212c725 feat: key the Gmail messageId map by the Gmail message id so remove and label lookups match the add entry, and translate Gmail label transitions into internal flag add/remove events with the UNREAD inversion 2026-07-04 09:30:27 +02:00
Matthew Meszaros 049d751a11 feat: route the outlook provider through Microsoft Graph in the wmail worker with nil-guarded send/sync/new-email dispatch and a delta-cursor event, request delegated Graph OAuth scopes instead of the legacy IMAP/SMTP scopes, and always sync Outlook mailboxes 2026-07-04 08:07:49 +02:00
Matthew Meszaros b701e4efab feat: send campaign email attachments
Carry attachment references inside the stored email body blob, resolve bytes on workers, and encode attachments for Gmail API and SMTP sends without changing the Kafka send-email contract.
2026-06-05 06:03:21 +02:00
Matthew Meszaros 1afe453eb0 feat: add deliverability controls
Add seed inbox-placement testing with admin management, placement result polling, and seed mailbox persistence.

Add pre-send email verification, invalid-recipient skipping, warmup-health campaign gating, and RFC 8058 one-click unsubscribe headers/endpoints.
2026-06-03 16:47:53 +02:00
Matthew Meszaros 0cb53a25a6 feat: improve warmup pool scheduling
Add explicit warmup participant roles so connected accounts can provide recipient-only pool capacity without becoming senders. Cap warmup scheduling by eligible recipients, avoid same-day recipient reuse, reschedule exhausted pools, and reconcile missing warmup task chains.
2026-05-31 18:07:13 +02:00
Matthew Meszaros 8347237547 merge: resolve main into feature/workers-support
Brings in PR #15 (email warmup process 4) plus its preceding commits:
customer-defined warmup routing on premium pool, free-trial warmup +
1 inbox for 14 days, customer webhook subscriptions with HMAC signing
+ retry, bumped default API rate limits to 100 req/s with flat per-
user/per-plan caps, plus dev-fixture additions.

One real conflict: internal/client/smtpimap/imap/client.go added
distinct imports on each side (this branch added 'net' for the
*net.TCPAddr BindIP field; main added 'sync' for a Mutex). Kept both.

Everything else auto-merged additively:
  cmd/backend/main.go     - imports + handler fields + DI lines
  internal/api/handler/handler.go - new fields next to existing ones
  internal/api/routes.go  - new route group next to existing ones

Full build + test suite pass (no regressions).
2026-05-27 16:29:18 +00:00
Matthew Meszaros 59b515cad4 callers: migrate blob access from raw *s3 inputs to storage.Store
Seven call sites stop reaching through the embedded *s3.Client and
instead use the high-level storage.Store methods. Same runtime behavior
on the AWS path; opens the door to the Filesystem backend for
self-hosters.

avatar.go retains an S3-specific path for public-ACL + cache-control
on uploaded avatars and falls back to ServiceUnavailable on non-S3
backends. A future PublicStore interface could clean that up.

unibox/storage.go GetBody now propagates the emsg.DecodeBinary error
that the original code dropped on the floor.
2026-05-27 14:41:53 +00:00
Matthew Meszaros e8f741dc82 feat: reduce warmup content + header fingerprint
rename outbound warmup verification header from X-Warmbly-Token to a
generic X-Mailtrace-Verify and accept both on receive during rollout.
add slot-based subject synthesis that fires ~40% of the time, yielding
thousands of unique strings on top of the static corpus so vendors
cannot trivially cluster on a fixed subject list.
2026-05-25 15:23:58 +00:00
Matthew Meszaros d227038ca0 ci: drop unused/unconvert/gosimple + shadow/nilness, run gofmt
Disable the linters that fire on legacy code without flagging real
bugs: `unused` (orphan repos kept for future feature flags),
`unconvert` (defensive type conversions), `gosimple` (style
suggestions in code we don't want to touch).

govet: disable `shadow` (idiomatic `err :=` re-decls in transaction
patterns) and `nilness` (legitimate defensive nil checks that look
tautological to the analyzer).

Ran `gofmt -w internal/ cmd/` — every Go file now passes
gofmt -l with no output.

Kept: govet, staticcheck, ineffassign, typecheck, bodyclose, noctx,
sqlclosecheck, gofmt, goimports, misspell — the real-bug checks.
2026-05-23 16:54:12 +00:00
Matthew Meszaros 2f2ccb41dd feat: add exponential backoff retry for transient email send failures 2026-04-12 11:10:28 +00:00
Matthew Meszaros e7933ab6c5 feat: publish auth error events on critical mail errors and register token update handler 2026-04-10 07:05:39 +00:00
Matthew Meszaros f9d9ed6dc0 feat: implement IMAP sync worker and unified mail sync loop for all providers 2026-04-10 07:02:25 +00:00
Matthew Meszaros 00dac953ae fix: add nil checks on SmtpImapData and avoid slice modification during iteration 2026-04-10 06:58:41 +00:00
Matthew Meszaros 58d41f3668 fix: store correct email account ID and message ID in dynamo email map 2026-04-10 06:54:31 +00:00
Máté Mészáros (Laptop) 41624a6f79 Analytics & Tracking 2026-01-29 05:59:04 +01:00
Máté Mészáros (Laptop) 81d5970ea8 Realtime, Task Handler, Worker & Consumer Setup 2026-01-26 04:42:19 +01:00
Matthew Meszaros 772c19820d New Repository: Add Backend Code 2026-01-17 14:11:14 +00:00