tripit: Gmail ingest (12-month) + vbarzin owner + plans@ forward-to-parse

Reconciles the tripit stack source with live state and adds the forward
flow. Ingest now polls vbarzin@gmail.com [Gmail]/All Mail read-only over a
rolling 12-month X-GM-RAW travel-sender window (Croatia Jet2 refs excluded),
filing trips under MAIL_DEFAULT_OWNER_EMAIL=vbarzin@gmail.com (Viktor's
Authentik login identity). Adds an ingest-plans CronJob that polls spam@
filtered to To:plans@viktorbarzin.me (the @viktorbarzin.me catch-all target)
so forwarded bookings are extracted and attached to the matching trip;
IMAP_PASSWORD is overridden per-job to spam@'s creds (PLANS_IMAP_PASSWORD).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Viktor Barzin 2026-06-02 23:16:12 +00:00
parent fd35c4f303
commit 0c7ec3d470
6 changed files with 352 additions and 11 deletions

View file

@ -97,6 +97,9 @@ resource "kubernetes_manifest" "external_secret" {
{ secretKey = "VAPID_SUBJECT", remoteRef = { key = "tripit", property = "VAPID_SUBJECT" } },
{ secretKey = "CALENDAR_TOKEN_SECRET", remoteRef = { key = "tripit", property = "CALENDAR_TOKEN_SECRET" } },
{ secretKey = "IMAP_PASSWORD", remoteRef = { key = "tripit", property = "IMAP_PASSWORD" } },
# spam@viktorbarzin.me password used only by the ingest-plans CronJob
# (forward-to-parse via the @viktorbarzin.me -> spam@ catch-all).
{ secretKey = "PLANS_IMAP_PASSWORD", remoteRef = { key = "tripit", property = "PLANS_IMAP_PASSWORD" } },
]
}
}
@ -312,12 +315,17 @@ locals {
suspend = false
extra_env = {}
}
# Ongoing forward-to-parse ingest of me@viktorbarzin.me's mailbox. Uses the
# real local LLM (qwen3vl-4b on llama-swap qwen3-8b OOMs the shared T4).
# Read-only IMAP (BODY.PEEK), bounded to the 30 most-recent messages/run;
# the pipeline is idempotent (skips message_ids already in inbound_email),
# so re-reading the recent window is a no-op for already-seen mail.
# IMAP_PASSWORD is injected from secret/tripit via the tripit-secrets ES.
# Ongoing forward-to-parse ingest of vbarzin@gmail.com Viktor's real
# travel mailbox (the self-hosted me@ box receives no booking mail). LLM =
# qwen3vl-4b on llama-swap (qwen3-8b OOMs the shared T4). Read-only
# IMAP_SEARCH over [Gmail]/All Mail (BODY.PEEK, never sets \Seen), bounded
# to a rolling 12-month window of travel-sender mail via Gmail X-GM-RAW; the
# two Croatia Jet2 refs (33W6Y3/33W7L2) are excluded so the hand-curated
# Croatia trip isn't duplicated. Idempotent (skips message_ids already in
# inbound_email). Trips land under MAIL_DEFAULT_OWNER_EMAIL (vbarzin@gmail.com
# Viktor's Authentik login identity, so trips show up in his account).
# IMAP_PASSWORD (the vbarzin@gmail.com app-password) comes from secret/tripit
# via the tripit-secrets ES.
ingest-mail = {
schedule = "*/30 * * * *"
command = ["python", "-m", "tripit_api", "ingest-mail"]
@ -327,13 +335,41 @@ locals {
LLM_ENDPOINT = "http://llama-swap.llama-cpp.svc.cluster.local:8080"
LLM_MODEL = "qwen3vl-4b"
MAIL_INGEST_ENABLED = "true"
MAIL_DEFAULT_OWNER_EMAIL = "me@viktorbarzin.me"
MAIL_DEFAULT_OWNER_EMAIL = "vbarzin@gmail.com"
IMAP_HOST = "imap.gmail.com"
IMAP_PORT = "993"
IMAP_USER = "vbarzin@gmail.com"
IMAP_FOLDER = "[Gmail]/All Mail"
IMAP_USE_SSL = "true"
IMAP_SEARCH = "X-GM-RAW \"newer_than:12m -33W6Y3 -33W7L2 (from:jet2.com OR from:ryanair.com OR from:easyjet.com OR from:wizzair.com OR from:booking.com OR from:airbnb.com OR from:expedia.com OR from:croatiaairlines.com OR from:vueling.com OR from:lufthansa.com OR from:trainline)\""
}
}
# Forward-to-parse: forward any booking confirmation to plans@viktorbarzin.me
# (which the @viktorbarzin.me catch-all delivers into the spam@ mailbox), and
# this job ingests it. Polls spam@ read-only, filtered by IMAP SEARCH to mail
# addressed To plans@ so only deliberate forwards are processed, not the
# rest of the catch-all junk. The LLM extracts segments and the pipeline
# attaches them to the date-overlapping trip (or creates one) under
# MAIL_DEFAULT_OWNER_EMAIL. IMAP_PASSWORD is overridden for this job to
# spam@'s password via imap_password_key (secret/tripit PLANS_IMAP_PASSWORD),
# because env_from otherwise injects the Gmail app-password.
ingest-plans = {
schedule = "*/15 * * * *"
command = ["python", "-m", "tripit_api", "ingest-mail"]
suspend = false
imap_pw_secret_key = "PLANS_IMAP_PASSWORD"
extra_env = {
LLM_MODE = "llamacpp"
LLM_ENDPOINT = "http://llama-swap.llama-cpp.svc.cluster.local:8080"
LLM_MODEL = "qwen3vl-4b"
MAIL_INGEST_ENABLED = "true"
MAIL_DEFAULT_OWNER_EMAIL = "vbarzin@gmail.com"
IMAP_HOST = "mailserver.mailserver.svc.cluster.local"
IMAP_PORT = "993"
IMAP_USER = "me@viktorbarzin.me"
IMAP_USER = "spam@viktorbarzin.me"
IMAP_FOLDER = "INBOX"
IMAP_USE_SSL = "true"
IMAP_RECENT_N = "30"
IMAP_SEARCH = "TO \"plans@viktorbarzin.me\""
}
}
}
@ -391,6 +427,23 @@ resource "kubernetes_cron_job_v1" "tripit_worker" {
}
}
# Per-job IMAP_PASSWORD override from a secret key. An explicit env
# takes precedence over env_from, so a job that polls a different
# mailbox (ingest-plans -> spam@) gets its own password instead of
# the default IMAP_PASSWORD (vbarzin@gmail.com) from tripit-secrets.
dynamic "env" {
for_each = lookup(each.value, "imap_pw_secret_key", null) != null ? [1] : []
content {
name = "IMAP_PASSWORD"
value_from {
secret_key_ref {
name = "tripit-secrets"
key = each.value.imap_pw_secret_key
}
}
}
}
volume_mount {
name = "documents"
mount_path = "/data/documents"