Reply Pilot Google: Gmail

Tato stranka popisuje modul reply-pilot-google/.

Role modulu

  • vlastni technicky Gmail OAuth i per-user Google OAuth a Calendar/Gmail API integraci
  • vystavuje interni HTTP API pro reply-pilot-be
  • vytvari mailbox snapshot payloady a zapisuje Gmail-owned mailbox cache
  • vlastni emailovou runtime cache pro inbox read model, attachment bytes a weekly-count reporty pouzivane Jira Reports
  • umi inkrementalni sync pres Gmail historyId pro vychozi inbox/sent cache
  • umi udrzovat Gmail watch stav a prijmout Pub/Sub notifikaci o zmene mailboxu
  • vytvari Gmail reply drafty

Runtime

  • bezi jako samostatny Java 21 Spring Boot/Maven kontejner
  • zapisuje runtime stav do reply-pilot-google/data; loguje jen na stdout/stderr
  • conf/ zustava read-only mount
  • na interni siti ma jediny podporovany alias reply-pilot-google
  • reply-pilot-be ho vola pres http://reply-pilot-google:5000
  • reply-pilot-jira-reports pouziva http://reply-pilot-google:5000/api/reports/email-weekly-counts
  • Gmail token se drzi v data/gmail_token.json, aby refresh flow mohl zapisovat
  • GMAIL_TOKEN_FILE zustava token technicke sdilene schranky; per-user Google granty maji samostatnou identitu, Calendar a Gmail scopes a pouzivaji sifrovane credential envelope ulozene backendem
  • watch stav se drzi v data/gmail-watch.json, aby se dal bezpecne obnovovat mezi restarty
  • Gmail mailbox cache se drzi pod data/accounts/<GMAIL_CACHE_ACCOUNT_ID>/emails/; vychozi account id je default po safe path sanitizaci
  • reply-pilot-google/data/ je runtime-only adresar a nesmi byt verzovany v gitu

Endpointy

  • GET /healthz
  • kontroluje jen lokalni runtime modulu a pritomnost/citelnost Gmail token souboru
  • neprovadi zadny primy call do Gmail API; dostupnost Gmailu se projevi az pri syncu nebo tvorbe draftu
  • GET /api/meta
  • GET /api/rate-limit/status
  • GET /api/gmail/rate-limit/status
  • GET /api/watch
  • POST /api/watch/ensure
  • POST /api/watch/stop
  • POST /api/watch/notify
  • POST /api/watch/pull
  • POST /api/watch/ack
  • POST /api/mailbox/snapshot
  • POST /api/mailbox/history
  • GET /api/reports/email-weekly-counts
  • GET /api/cache/status
  • GET /api/cache/inbox
  • GET /api/cache/emails
  • GET /api/cache/emails/<email_id>
  • GET /api/cache/emails/<email_id>/attachments
  • GET /api/cache/emails/<email_id>/attachments/<filename>
  • GET /api/emails/<email_id>
  • GET /api/emails/<email_id>/attachments
  • POST /api/emails/<email_id>/drafts/reply
  • POST /api/emails/drafts/outbound
  • POST /api/emails/<email_id>/send/reply
  • POST /api/emails/send/outbound
  • POST /api/emails/<email_id>/send/forward
  • Calendar adapter pro backend: POST /api/google-calendar/oauth/status, /oauth/authorization-url, /oauth/exchange, /oauth/disconnect, /api/google-calendar/calendars, /week a /events/{get,create,update,delete}
  • Per-user Gmail adapter pro backend: POST /api/user-gmail/profile, /api/user-gmail/snapshot, /api/user-gmail/history, /api/user-gmail/watch, /api/user-gmail/watch/ensure, /api/user-gmail/watch/initial-sync, /api/user-gmail/watch/processed, /api/user-gmail/watch/pull a /api/user-gmail/watch/ack; plaintext access token se v odpovedi nevraci

Reply draft/send endpointy a outbound send endpoint podporuji JSON bez priloh a multipart/form-data s textovymi poli recipient, subject, body_text, volitelne reply_to_message_id a opakovanym file polem attachments. Limit je 10 souboru, 10 MB na soubor a 20 MB celkem.

Import podle spolecnosti

party_organization.import_email_communication ma default TRUE a edituje se na formulari firmy se stejnymi opravnenimi jako ostatni firemni udaje. Backend preskoci cele vlakno, pokud vsechny jeho kanonicky navazane firmy maji FALSE; jakakoli firma s TRUE povoluje cele vlakno. Import policy zahrnuje i skryte firmy; osobni Gmail dal vyzaduje aspon jednu viditelnou firemni vazbu jako dosud. Sdilene i povolene osobni schranky pouzivaji stejnou podminku. Gmail snapshot/history dal stahuje a cachuje data. Dosavadni DB historie i prilohy zustavaji dostupne, odesilani se tim nezakazuje.

Backend eviduje preskocena vlakna v email_import_skipped_thread. Samostatny worker job email_import_backfill vola POST /api/mailbox/import/backfill a po opetovnem zapnuti importu firmy prehrava cele cached vlakno, vcetne chybějících zprav existujiciho vlakna. Jedna davka fronty obsahuje nejvyse 10 vlaken; chyba se odlozi o pet minut a zaznam zustava ve fronte. Opakovany import nezdvojuje zpravy.

Pro osobni cache slouzi bearer-protected POST /api/user-gmail/cache/thread:

{"app_user_id": 7, "mailbox_email": "owner@example.test", "thread_id": "thread-id"}

Odpoved obsahuje gmail_page.emails (jedno cele vlakno) a gmail_page.attachments_by_email (ulozena metadata). Endpoint nevola Google API, nevyzaduje OAuth envelope a kontroluje shodu mailbox_email s account cache. Backend pred volanim vyzaduje sync_inbox. Sdilena cache pouziva stavajici GET /api/cache/emails/<email_id> a attachment endpoint. Nekompletni cache se neimportuje; replay funguje i po ztrate Gmail pristupu, pokud cache obsahuje cele vlakno. Backend nedostava filesystem mount ani plaintext Google token.

Stari osobnich konverzaci a opakovany import

BE nastaveni PERSONAL_GMAIL_IMPORT_DAYS je spolecne pro vsechny osobni Google ucty. Default je 15; 0 vypina limit stari. Zaporne nebo necelociselne hodnoty jsou chyba konfigurace. Source of truth je secrets/local/reply-pilot-be.env a secrets/prod/reply-pilot-be.env; po zmene je nutny restart/redeploy BE. Sdilena schranka nema tento limit.

Rozhoduje datum nejnovejsi zpravy celeho vlakna, prevedene do APP_TIMEZONE (default Europe/Prague). Prijatelne datum je dnesni lokalni datum minus pocet dni nebo novejsi, vcetne celeho hranicniho dne. Napriklad 15. 9. 2026 pri limitu 15 projde vlakno s nejnovejsi zpravou 31. 8. 2026 v libovolny cas; 30. 8. uz ne. Jde o kalendarni dny, ne nasobky 24 hodin; zmena letniho casu hranici neposune.

Osobni Gmail snapshot i incremental sync cachuji plnou historii vybranou pres {in:inbox in:sent}, bez limitu stari. Samostatna archivovana, Spam a Trash vlakna se nevybiraji. Vybrane vlakno se uchova cele, vcetne starsich zprav a priloh. DB import navic vyzaduje aktualni kanonickou firemni vazbu alespon na jednu viditelnou firmu a firemni import policy popsanou vyse. Vazby se hledaji pres from/to/cc/bcc ve vsech zpravach: presna aktivni firemni emailova adresa, kontakt osoby pres CONTACT_FOR nebo presna rucne ulozena firemni DOMAIN. Skryte firmy zustavaji soucasti rozhodnuti o import_email_communication. Projde-li cele vlakno, importuji se vsechny jeho zpravy a prilohy, i starsi nez limit. Stejna kontrola plati pro snapshot, incremental import, replay i aktualizaci existujiciho DB vlakna. Zkraceni okna, plynuti casu, vypnuti firmy nebo odpojeni Gmailu dosavadni DB historii ani prilohy nemaze.

Stavajici worker job email_import_backfill navic pri kazdem behu prehodnoti nejvyse 10 vlaken z inventory jedne osobni cache. Cyklicky strida uzivatele s sync_inbox a po konci jejich inventory zacina znovu. Tim zachyti rozsireni okna, 0, nove kontakty/domeny i opetovne povoleni firem bez nove Gmail udalosti. OAuth grant neni potreba, pokud je cela cache stale dostupna. Opakovani nezdvojuje zpravy ani prilohy; chybejici/nekompletni vlakno se neimportuje, ostatni pokracuji a chybne vlakno se zkusi pri dalsim pruchodu.

Vychozi interval workeru je 30 sekund (WORKER_IMPORT_POLL_INTERVAL_SECONDS), job bezi pri WORKER_IMPORT_ENABLED nebo WORKER_GMAIL_WATCH_PULL_ENABLED. Prehodnoceni neni okamzite: jedna schranka s 1 000 vlakny potrebuje pri tomto intervalu priblizne 50 minut plus cas zpracovani; ostatni povolene schranky pruchod prodluzuji. BE drzi kurzory v pameti; restart zacne pruchod od zacatku. Predchozi DB fronta pro firemni zakazy zustava zachovana, ale vekem nebo chybejici vazbou odmitnuta vlakna se najdou primo v cache inventory.

Bearer-protected inventory endpoint POST /api/user-gmail/cache/threads:

{"app_user_id": 7, "mailbox_email": "owner@example.test", "after_thread_id": "", "limit": 10}
{"status": "ok", "gmail_cache": {"mailbox_email": "owner@example.test", "thread_ids": ["thread-id"], "next_after_thread_id": ""}}

limit ma default 10 a maximum 50. Stabilni lexikograficke razeni ID zabranuje posouvani stranek pri nove zprave ve starem vlakne. Prazdny dalsi kurzor znamena konec lokalni inventory, nezavisle na vzdalenem Gmail strankovani. BE predava aktualni Google adresu jako ocekavanou identitu; pokud uzivatel grant odpojil, adresu vynecha a pouzije identitu svazanou s jeho app-user-<id> cache. Neshoda neprazdne ocekavane identity vraci 409. Nasledne cteni celeho vlakna vzdy predava vracenou mailbox identitu. Google API ani OAuth se pri inventory nevola.

Gmail-Owned Mailbox Cache

Detail importovaneho osobniho vlakna v aplikaci cte ulozene zpravy z PostgreSQL pres autentizovany GET /api/email-threads/<thread_id>/messages, ne ze sdilene Gmail cache. Odkazy z detailu firmy a zpravy predavaji activity_id, podle ktereho se vybere stejny provider/mailbox. Bez tohoto parametru lze precist jen jednoznacne vlakno; vice provideru se stejnym thread ID vraci 409. Zpravy si zachovaji vlastni activity detail a prilohy pouzivaji activity-scoped endpointy Google modulu. Tento archivni pohled nezobrazuje shared-mailbox reply/forward akce. Sdilena vlakna pouzivaji dosavadni cache pohled, pri chybejici cache lze zobrazit importovanou DB historii. Pri nasazeni teto zmeny nasad nejprve BE, potom web app; DB migrace ani novy download schranky nejsou potreba.

reply-pilot-google je jediny modul, ktery smi drzet Gmail OAuth material, Google client dependencies, Gmail watch/rate-limit stav a fyzicke mailbox cache soubory. reply-pilot-be pouziva jen interni HTTP endpointy Gmail modulu.

Stejna hranice plati pro per-user Google ucet: OAuth client secret, envelope encryption key, token exchange/refresh/revoke a Google Calendar/Gmail HTTP volani patri pouze do reply-pilot-google. Backend smi drzet jen opaque sifrovany envelope a volat interni adapter pres GOOGLE_API_BASE_URL. Osobni Gmail cache pouziva account id app-user-<id> a backend volani navic strazi dovednosti sync_inbox.

Fyzicky layout:

  • /app/data/accounts/<GMAIL_CACHE_ACCOUNT_ID>/emails/index.json
  • /app/data/accounts/<GMAIL_CACHE_ACCOUNT_ID>/emails/threads/*.json
  • /app/data/accounts/<GMAIL_CACHE_ACCOUNT_ID>/emails/attachments/<thread>/<file>

GMAIL_CACHE_ACCOUNT_ID defaultuje na default a runtime jej sanitizuje pro bezpecnou filesystem komponentu. Attachment relative_path v cache a HTTP payloadu zustava logicka hodnota emails/attachments/<thread>/<file>. Pri importu osobni schranky backend ulozi do DB metadata prefix accounts/app-user-<id>/; tim se globalne unikatni cesta nesrazi se stejnym Gmail threadId a nazvem souboru v jine schrance. Fyzicke cteni osobni prilohy se presto provadi jen account-scoped Google API cestou.

Jednorazovy presun stare BE cache:

cd /Users/jan/projects/reply-pilot/reply-pilot-google
scripts/gmail-migrate-cache.sh --account-id default

Script kopiruje vychozi ../reply-pilot-be/data/emails do ${HOST_DATA_DIR}/accounts/<account-id>/emails a odmita ne-prazdny target, pokud neni explicitne zadane --force.

Osobni Gmail snapshot pouziva samostatny account id app-user-<id>. Jeho strankovani cache pouze doplnuje a pri prvni strance nerestartuje index, aby preruseny opakovany import nezneviditelnil drive ulozena vlakna. Drive ulozene osobni prilohy jsou append-only: novy snapshot je neprepise ani neodstrani. Mazani podle Gmail history se pro osobni schranky nesmi zapnout, dokud serverova kopie a attachment read path nejsou prokazatelne nezavisle na zdrojove schrance.

Autentizovany backend endpoint POST /api/google-calendar/gmail/sync-step zpracuje prave jednu snapshot stranku prihlaseneho uzivatele. Nejprve ji Google modul ulozi vcetne priloh do per-user cache, potom backend importuje odpovidajici vlakna a jejich persistovana attachment metadata v jedne DB transakci. Az po uspesnem commitu odpoved obsahuje next_page_token a history_id; stejny vstupni page_token je proto mozne po chybe bezpecne zopakovat. Endpoint sam zadny kurzor neuklada. Autentizovane backend endpointy GET /api/google-calendar/gmail/watch a POST /api/google-calendar/gmail/watch/ensure umi precist a obnovit oddeleny watch stav povoleneho uzivatele.

Worker pri zapnutem Gmail watch pullu vola take interni backend endpoint POST /api/mailbox/personal-gmail/watch/pull. Pokud nema zadny uzivatel explicitne zapnutou dovednost sync_inbox, endpoint neobnovuje watch, necte osobni subscription a nevola Gmail API. Pro kazdeho povoleneho uzivatele nejprve obnovi watch a po jedne strance importuje cely pocatecni snapshot. Resume token stranky ulozi az po uspesnem DB commitu; po chybe se proto stejna stranka idempotentne zopakuje. Pub/Sub notifikace konkretni schranky se potvrdi az po dokonceni pocatecniho snapshotu, uspesnem importu vsech stranek Gmail historie a ulozeni processed_history_id. Vypnuty znamy ucet a notifikace jineho prostredi se v dane environment-specific subscription potvrdi bez importu.

Watch stav je svazan s normalizovanou e-mailovou adresou. Pokud by stejny app_user_id zacal ukazovat na jinou Gmail schranku, automaticky sync skonci konfliktem a nepouzije stary cursor ani cache pro novy ucet.

Rate limit a pozastaveni osobni synchronizace

Google modul pri Gmail rate limitu uchova Google error.message, error.errors[].reason a puvodni HTTP status. Interni HTTP 429 odpoved vraci reason, google_http_status, google_error_reason, retry_at, retry_after_seconds a retry_source. Zprava je omezena na 1024 znaku a bez ridicich znaku; nevraci se cely Google response body ani request headers. Deadline ma prednost z Retry-After (retry_source: header), jinak z casu Retry after <ISO timestamp> v Google zprave (body). Bez pouzitelneho budouciho deadlinu zustava dosavadni 900sekundovy cooldown (default_cooldown). Efektivni deadline obsahuje uz prvni odpoved, ktera cooldown zalozila.

Google modul uklada tyto udaje do mailbox-owned gmail-rate-limit.json, takze preziji restart. Stary zaznam bez podrobnosti zustava citelny; puvodni Google zpravu k nemu nelze zpetne doplnit. Nove podrobnosti pribudou pri dalsim skutecnem odmitnuti Google API, ne pouhym ctenim aktivniho cooldownu.

BE loguje novy cooldown jednou jako WARN Personal Gmail sync paused until=... s Google duvodem a zdrojem deadlinu. Do expirace pri pravidelnem watch pullu preskakuje pouze dotceneho uzivatele; dalsi uzivatele synchronizuje dal. Odpoved POST /api/mailbox/personal-gmail/watch/pull obsahuje paused_user_count a paused_users s duvodem a retry udaji; cooldown se nepocita do failed_count. Worker pridava pocet pozastavenych uzivatelu do sveho souhrnneho logu. Po deadlinu nasledujici pruchod znovu pouzije ulozeny cursor; notifikace nedokoncene synchronizace se nepotvrzuji. BE drzi tento odklad v pameti a po restartu jej pri dalsim volani znovu prevezme z Google modulu.

Postupne obnoveni osobniho stahovani

Osobni snapshot i history download zacinaji oknem 1 (nejvyse jedno vlakno snapshotu nebo jeden zaznam history na stranku). Po kazdych trech uspesnych davkach, vcetne ulozeneho dilciho stahovani, se okno zvetsi o 1, nejvyse na 10. Novy Google rate limit, chyba stahovani nebo restart s nedokoncenym pozadavkem vrati okno na 1. Drive ulozeny Google retry deadline ma vzdy prednost.

Jedna davka stahne nejvyse 4 * okno novych priloh. Nove Gmail requesty spousti v ramci 20sekundoveho rozpoctu, s odstupem nejmene 1000 / okno ms a pripadne delsimi rozestupy podle vah metod, GMAIL_QUOTA_PER_USER_PER_MINUTE a GMAIL_QUOTA_OPERATIONAL_HEADROOM_PERCENT. HTTP read timeout pouziva zbyvajici cas davky, connect timeout nejvyse 5 sekund; vnitrni HTTP retries jsou vypnute. Jde o rozpoctovani dalsich volani, ne o tvrdy limit celkoveho casu prenosu a zapisu cache. Lokalni pacing nezna spotrebu jinych klientu stejne schranky a nemuze zarucit, ze Google dalsi pozadavek prijme.

Progress a velikost okna jsou v account-owned gmail-download-recovery.json. Rozpracovana metadata stranek/vlaken a jednotlive prilohy se atomicky ukladaji do gmail-downloads/ vedle emails/. Dalsi pokus i restart pouzije tyto soubory; velke vlakno proto nemusi znovu stahovat vsechny prilohy od zacatku. Prilohy z dokoncene cache se znovu pouzivaji podle dvojice Gmail message ID a attachment ID. Starsi cache bez attachment ID jej dostane pri pristim stazeni pouze po overeni shody zpravy a obsahu. Existujici soubory priloh se neprepisuji.

Nedokoncena davka vraci interni HTTP 409, code: gmail_sync_pending, retry_at, retry_after_seconds a retry_source: download_budget s odkladem 20 sekund. BE ji vykaze v paused_users, loguje jako INFO a pokracuje ostatnimi uzivateli; nezvysi failed_count. Pri pristim worker pruchodu po deadlinu navaze. Dokud neni cela stranka v cache a BE nepotvrdi DB import, neposouva se checkpoint ani nepotvrzuji notifikace. Potvrzeni checkpointu uklidi rozpracovane soubory; hotove emaily a prilohy zustanou v cache.

Pri nasazeni teto zmeny nasad nejprve reply-pilot-be (rozpoznani HTTP 409), pote reply-pilot-google (adaptivni stahovani). Neni potreba menit env ani SQL schema. Sdilena schranka a interaktivni operace tento download rezim nepouzivaji.

Watch workflow

  • POST /api/watch/ensure vola Gmail users.watch jen kdyz existujici watch nema dostatecnou rezervu do expirace
  • vychozi watch labels jsou INBOX,SENT, aby incremental sync zachytil prijata vlakna i sent-only odchozi vlakna
  • worker muze watch pravidelne obnovovat pres WORKER_GMAIL_WATCH_* promenne bez dalsiho full scanu mailboxu
  • POST /api/watch/pull cte Gmail Pub/Sub Pull subscription a vraci dekodovane notifikace vcetne ack_id
  • POST /api/watch/notify ulozi metadata prijate Pub/Sub notifikace a vraci, zda je potreba spustit navazujici import workflow
  • pull flow drzi ack_id v Gmail-owned gmail-watch.json a potvrdi je az po uspesnem zapisu mailbox-wide full snapshotu nebo incremental history cursoru; cilene backfill snapshoty pending watch notifikace nepotvrzuji
  • POST /api/watch/ack zustava explicitni admin operace; pri sync/cache chybe automaticky flow ack neposila, aby doslo k redelivery
  • doporuceny provoz je zapnout watch renewal a workerovy gmail_synchronizer, ktery spojuje watch pull i mailbox import do jedne pipeline

Gmail OAuth Token Lifecycle

Gmail OAuth token je uzivatelsky token pro Gmail API a je odlisny od Pub/Sub service account JSON. Runtime soubor je:

  • host: ${HOST_DATA_DIR}/gmail_token.json
  • kontejner: /app/data/gmail_token.json
  • env: GMAIL_TOKEN_FILE=/app/data/gmail_token.json

Sifrovany source of truth:

  • secrets/local/reply-pilot-google-token.json pro lokalni vyvojovy mailbox
  • secrets/prod/reply-pilot-google-token.json pro produkcni mailbox

Start/deploy workflow materializuje token do reply-pilot-google/data/, pokud odpovidajici SOPS secret existuje. Token se nema kopirovat do Docker image a reply-pilot-google/data/ se necommituje.

Vygenerovani tokenu:

cd /Users/jan/projects/reply-pilot/reply-pilot-google
scripts/gmail-generate-token.sh \
  --token-file /tmp/reply-pilot-google-token.json \
  --client-id '...apps.googleusercontent.com' \
  --client-secret 'GOCSPX-...'

Zasifrovani lokalniho tokenu:

sops encrypt \
  --input-type json \
  --output-type json \
  --filename-override ../secrets/local/reply-pilot-google-token.json \
  /tmp/reply-pilot-google-token.json \
  > ../secrets/local/reply-pilot-google-token.json
rm -f /tmp/reply-pilot-google-token.json

Pro produkci pouzij ../secrets/prod/reply-pilot-google-token.json. Lokalni vyvojovy token ma patrit osobnimu nebo testovacimu mailboxu; produkcni token se lokalne nepouziva kvuli Gmail API kvotam a riziku zasahu do produkcni schranky.

Pole expiry v JSONu je expirace kratkodobeho access tokenu. Dlouhodobe opravneni drzi refresh_token; aplikace pri expiraci vola Google token endpoint a ziska novy access token.

Pub/Sub Runtime Config

  • GMAIL_WATCH_TOPIC_NAME je plne jmeno Gmail watch topicu, napriklad projects/<project-id>/topics/gmail-watch
  • GMAIL_WATCH_SUBSCRIPTION_NAME je plne jmeno environment-specific Pull subscription, napriklad projects/<project-id>/subscriptions/reply-pilot-dev-sub
  • GMAIL_PERSONAL_WATCH_TOPIC_NAME je samostatny topic osobnich schranek; pri chybejici hodnote se osobni watch neregistruje do sdileneho topicu
  • GMAIL_PERSONAL_WATCH_SUBSCRIPTION_NAME je samostatna subscription osobnich schranek a musi byt jina pro local a production
  • GMAIL_PUBSUB_CREDENTIALS_FILE muze ukazovat na service account JSON pod /app/conf/...; kdyz chybi, modul zkusi Application Default Credentials
  • doporuceny sifrovany source of truth pro Pub/Sub service account JSON je secrets/<environment>/reply-pilot-google-pubsub.json; start/deploy workflow ho materializuje do reply-pilot-google/conf/reply-pilot-pubsub.json
  • lokalni a produkcni instance maji mit vlastni subscription; obe ale mohou cilit na stejny topic
  • lokalni a produkcni instance nemaji sdilet stejnou Pull subscription, pokud maji bezet soucasne
  • pokud lokalne netestujes Pub/Sub, vypni workerove WORKER_GMAIL_WATCH_* a WORKER_GMAIL_WATCH_PULL_* prepinace

Oznaceni vlaken jako prectenych z Reply Pilotu (RP-7598)

Po uspesnem prechodu Email Thread Reply z Reply Pilotu do Waiting for Reply nebo Done BE ihned zkusi oznacit vlakno jako prectene. Spoustecem je explicitni edit se zmenou stavu, akce presunu do cekani po odeslani/forwardu nebo uzavreni emailoveho tasku. Samotny refresh Jira cache, import zpravy a automaticka synchronizace tasku tuto akci nespousteji. Ostatni typy tasku se neoznacuji.

BE nacte (provider, external_thread_id) z vazby tasku. V Google modulu nejprve nacte RFC Message-ID hlavicky vsech zprav zdrojoveho vlakna a oznaci zdrojove vlakno. V ostatnich schrankach vyhleda skutecna Gmail thread ID pres presne rfc822msgid dotazy; Gmail thread ID se mezi ucty nekopiruje a shoda predmetu se nepouziva. Vyhledavani zahrnuje i archiv, Spam a Trash, strankuje vysledky a deduplikuje vlakna. Chybejici Message-ID nevede k sirsi heuristice.

Cilem je sdilena schranka a vsechny osobni schranky s explicitne zapnutou dovednosti sync_inbox a dostupnym Google grantem, nezavisle na autorovi zmeny nebo aktualnim assignee tasku. Google modul odstrani pouze label UNREAD pres Gmail threads.modify. Nearchivuje ani nemaze zpravy. Lokalni cache se obnovi beznym Gmail history syncem; vysledkem akce je zmena primo v Gmailu.

Interni endpointy jsou chranene GOOGLE_API_TOKEN:

  • POST /api/threads/mark-read pro sdilenou schranku
  • POST /api/user-gmail/threads/mark-read pro osobni schranku

Payload obsahuje bud thread_id pro zdrojovou schranku, nebo rfc822_message_ids pro dohledani kopii. Osobni varianta navic predava app_user_id, mailbox_email a opaque encrypted_credentials; Google modul overi vazbu identity schranky a vraci pripadny credential_update. Odpoved gmail_read obsahuje rfc822_message_ids, matched_thread_count, marked_thread_count a failed_thread_count.

Provedeni je synchronni best-effort pokus po Jira prechodu, bez nove fronty nebo periodickeho worker jobu. Selhani konkretni schranky/brany se zapise jako INFO na stdout/stderr, nezrusi Jira prechod a neblokuje pokus v dalsich schrankach. Pokud nelze nacist identifikatory zdrojoveho vlakna, nelze bezpecne vyhledat jeho kopie a zaznamena se INFO. Selhani samotneho oznaceni zdroje zachova nactene Message-ID pro ostatni schranky. Automaticky retry se neplanuje.

Nasazeni: nejprve reply-pilot-google, potom reply-pilot-be; DB migrace neni potreba. Nove OAuth autorizace vyzaduji take gmail.modify. Existujici granty se samy nerozsiri: osobni uzivatele musi znovu autorizovat Google ucet; sdileny token je potreba znovu vygenerovat pres gmail-generate-token.sh a ulozit do prislusneho SOPS token secretu podle lifecycle postupu vyse. Dosavadni read/compose pristupy dal fungují; bez gmail.modify Gmail oznaceni odmitne a chyba se zaloguje.

Jira tasky osobnich schranek

Nove zpravy importovane do aplikace spousti reply-task automatiku. Pro kazdou schranku a Gmail thread existuje samostatne vyhledavany Email Thread Reply task. Nejnovejsi importovana zprava urcuje stav Drafting Reply (prichozi) nebo Waiting for Reply (odchozi). Assignee je vzdy Jira ucet vlastnika schranky, nikoli vychozi obchodnik firmy; bez dohledatelne Jira identity zustava pozadavek ve stavajici retry fronte s chybou (nejvyse tri pokusy). Import emailu tim neselze. Queue doplni i chybejici tasky pro drive importovana osobni vlakna z DB. Existujici navazane tasky se beze zmen ve vlakne neaktualizuji. Zmena vyzaduje DB expand migraci 0087, nasledne BE, App a Search. Cteni tasku, firemni vazby a vyhledavani rozlisuji provider i thread ID. Osobni odesilani tato zmena nepridava; odpoved odeslana z Gmailu aktualizuje task po synchronizaci.