Compare commits

..

193 Commits

Author SHA1 Message Date
f843166147 chore: Release
Some checks failed
Continuous integration / Check (push) Successful in 1m44s
Continuous integration / Test Suite (push) Successful in 2m49s
Continuous integration / Trunk (push) Successful in 1m53s
Continuous integration / Rustfmt (push) Failing after 1m35s
Continuous integration / build (push) Successful in 2m57s
Continuous integration / Disallow unused dependencies (push) Successful in 5m39s
2026-01-20 09:56:53 -08:00
943dd5c142 server: add Updated invitation parsing support 2026-01-20 09:56:16 -08:00
a1cf16350b server: big improvements for parsing all day events 2026-01-20 09:39:40 -08:00
626eca5619 Merge pull request 'chore(deps): update all non-major dependencies' (#233) from renovate/all-minor-patch into master
All checks were successful
Continuous integration / Check (push) Successful in 2m55s
Continuous integration / Test Suite (push) Successful in 4m20s
Continuous integration / Rustfmt (push) Successful in 42s
Continuous integration / Trunk (push) Successful in 2m11s
Continuous integration / build (push) Successful in 2m40s
Continuous integration / Disallow unused dependencies (push) Successful in 5m38s
2026-01-20 07:46:17 -08:00
cb77f83607 chore(deps): update all non-major dependencies
All checks were successful
Continuous integration / Check (push) Successful in 3m40s
Continuous integration / Test Suite (push) Successful in 6m30s
Continuous integration / Trunk (push) Successful in 21m18s
Continuous integration / Rustfmt (push) Successful in 1m38s
Continuous integration / build (push) Successful in 6m31s
Continuous integration / Disallow unused dependencies (push) Successful in 5m42s
2026-01-20 14:46:29 +00:00
b2f1431664 Merge pull request 'chore(deps): update rust crate html2text to v0.16.6' (#232) from renovate/all-minor-patch into master
All checks were successful
Continuous integration / Check (push) Successful in 2m54s
Continuous integration / Test Suite (push) Successful in 3m6s
Continuous integration / Rustfmt (push) Successful in 45s
Continuous integration / build (push) Successful in 2m28s
Continuous integration / Disallow unused dependencies (push) Successful in 2m27s
Continuous integration / Trunk (push) Successful in 21m7s
2026-01-18 02:31:16 -08:00
c541f4bd00 chore(deps): update rust crate html2text to v0.16.6
All checks were successful
Continuous integration / Check (push) Successful in 1m45s
Continuous integration / Test Suite (push) Successful in 6m44s
Continuous integration / Rustfmt (push) Successful in 1m37s
Continuous integration / Trunk (push) Successful in 7m38s
Continuous integration / Disallow unused dependencies (push) Successful in 2m31s
Continuous integration / build (push) Successful in 4m50s
2026-01-18 10:16:22 +00:00
4a20b1d4ba Merge pull request 'chore(deps): update rust crate human_format to v1.2.1' (#231) from renovate/all-minor-patch into master
All checks were successful
Continuous integration / Test Suite (push) Successful in 2m38s
Continuous integration / Check (push) Successful in 2m52s
Continuous integration / Rustfmt (push) Successful in 1m36s
Continuous integration / Trunk (push) Successful in 7m59s
Continuous integration / build (push) Successful in 7m12s
Continuous integration / Disallow unused dependencies (push) Successful in 2m35s
2026-01-16 20:46:17 -08:00
d54c90dc7b chore(deps): update rust crate human_format to v1.2.1
All checks were successful
Continuous integration / Check (push) Successful in 2m53s
Continuous integration / Test Suite (push) Successful in 2m58s
Continuous integration / Rustfmt (push) Successful in 59s
Continuous integration / build (push) Successful in 2m28s
Continuous integration / Disallow unused dependencies (push) Successful in 2m26s
Continuous integration / Trunk (push) Successful in 20m57s
2026-01-17 04:16:21 +00:00
f8a4a5d5b4 Merge pull request 'chore(deps): update all non-major dependencies to v7.2.0' (#230) from renovate/all-minor-patch into master
All checks were successful
Continuous integration / Check (push) Successful in 3m13s
Continuous integration / Test Suite (push) Successful in 2m20s
Continuous integration / Rustfmt (push) Successful in 1m7s
Continuous integration / Trunk (push) Successful in 2m11s
Continuous integration / build (push) Successful in 2m50s
Continuous integration / Disallow unused dependencies (push) Successful in 5m42s
2026-01-16 14:46:29 -08:00
8c99bb3ba1 chore(deps): update all non-major dependencies to v7.2.0
All checks were successful
Continuous integration / Check (push) Successful in 1m44s
Continuous integration / Test Suite (push) Successful in 3m28s
Continuous integration / Rustfmt (push) Successful in 49s
Continuous integration / build (push) Successful in 2m57s
Continuous integration / Disallow unused dependencies (push) Successful in 3m42s
Continuous integration / Trunk (push) Successful in 20m56s
2026-01-16 22:02:28 +00:00
c1a66fc548 Merge pull request 'fix(deps): update rust crate graphql_client to 0.16.0' (#229) from renovate/all-minor-patch into master
All checks were successful
Continuous integration / Check (push) Successful in 1m35s
Continuous integration / Test Suite (push) Successful in 2m59s
Continuous integration / Trunk (push) Successful in 1m23s
Continuous integration / Rustfmt (push) Successful in 44s
Continuous integration / build (push) Successful in 2m31s
Continuous integration / Disallow unused dependencies (push) Successful in 3m18s
2026-01-15 11:01:37 -08:00
f45bf002ba fix(deps): update rust crate graphql_client to 0.16.0
All checks were successful
Continuous integration / Test Suite (push) Successful in 2m36s
Continuous integration / Check (push) Successful in 3m29s
Continuous integration / Trunk (push) Successful in 1m12s
Continuous integration / Rustfmt (push) Successful in 1m35s
Continuous integration / build (push) Successful in 2m34s
Continuous integration / Disallow unused dependencies (push) Successful in 5m37s
2026-01-15 18:46:24 +00:00
630e9b68b7 Merge pull request 'chore(deps): update rust crate chrono to v0.4.43' (#228) from renovate/all-minor-patch into master
All checks were successful
Continuous integration / Check (push) Successful in 3m16s
Continuous integration / Test Suite (push) Successful in 2m30s
Continuous integration / Rustfmt (push) Successful in 57s
Continuous integration / Trunk (push) Successful in 2m8s
Continuous integration / build (push) Successful in 2m22s
Continuous integration / Disallow unused dependencies (push) Successful in 5m47s
2026-01-14 15:02:57 -08:00
787e10a989 chore(deps): update rust crate chrono to v0.4.43
All checks were successful
Continuous integration / Check (push) Successful in 1m43s
Continuous integration / Test Suite (push) Successful in 3m35s
Continuous integration / Trunk (push) Successful in 1m20s
Continuous integration / Rustfmt (push) Successful in 1m34s
Continuous integration / build (push) Successful in 3m21s
Continuous integration / Disallow unused dependencies (push) Successful in 5m51s
2026-01-14 21:47:44 +00:00
7de6a5f0fc chore: Release
All checks were successful
Continuous integration / Check (push) Successful in 2m12s
Continuous integration / Test Suite (push) Successful in 2m25s
Continuous integration / Trunk (push) Successful in 2m10s
Continuous integration / Rustfmt (push) Successful in 49s
Continuous integration / Disallow unused dependencies (push) Successful in 2m39s
Continuous integration / build (push) Successful in 4m35s
2026-01-14 13:37:20 -08:00
83230ba962 Merge branch 'renovate/lock-file-maintenance'
Some checks failed
Continuous integration / Test Suite (push) Successful in 3m47s
Continuous integration / Check (push) Successful in 5m46s
Continuous integration / Trunk (push) Successful in 2m12s
Continuous integration / Rustfmt (push) Successful in 1m38s
Continuous integration / build (push) Successful in 4m32s
Continuous integration / Disallow unused dependencies (push) Has been cancelled
2026-01-14 13:27:00 -08:00
6ef786c4e7 Manually update deps to a buildable rev 2026-01-14 13:26:30 -08:00
d4c29d7b98 chore(deps): lock file maintenance
Some checks failed
Continuous integration / Check (push) Failing after 1m34s
Continuous integration / Test Suite (push) Failing after 3m56s
Continuous integration / Rustfmt (push) Successful in 45s
Continuous integration / build (push) Failing after 3m11s
Continuous integration / Disallow unused dependencies (push) Failing after 3m7s
Continuous integration / Trunk (push) Successful in 21m26s
2026-01-14 18:31:51 +00:00
95a4f2e630 Merge pull request 'chore(deps): update all non-major dependencies' (#227) from renovate/all-minor-patch into master
All checks were successful
Continuous integration / Check (push) Successful in 1m38s
Continuous integration / Trunk (push) Successful in 1m53s
Continuous integration / Rustfmt (push) Successful in 53s
Continuous integration / Test Suite (push) Successful in 4m26s
Continuous integration / build (push) Successful in 2m20s
Continuous integration / Disallow unused dependencies (push) Successful in 5m42s
2026-01-14 10:31:19 -08:00
faa35dd65a chore(deps): update all non-major dependencies
All checks were successful
Continuous integration / Test Suite (push) Successful in 2m36s
Continuous integration / Check (push) Successful in 2m44s
Continuous integration / Trunk (push) Successful in 1m27s
Continuous integration / Rustfmt (push) Successful in 1m36s
Continuous integration / build (push) Successful in 2m48s
Continuous integration / Disallow unused dependencies (push) Successful in 5m37s
2026-01-14 13:16:29 +00:00
7c54c6b9c3 Merge pull request 'fix(deps): update rust crate quick-xml to 0.39.0' (#225) from renovate/all-minor-patch into master
All checks were successful
Continuous integration / Test Suite (push) Successful in 2m29s
Continuous integration / Check (push) Successful in 2m38s
Continuous integration / Trunk (push) Successful in 1m19s
Continuous integration / Rustfmt (push) Successful in 1m40s
Continuous integration / build (push) Successful in 2m16s
Continuous integration / Disallow unused dependencies (push) Successful in 5m35s
2026-01-11 11:31:18 -08:00
2d1e38bdff fix(deps): update rust crate quick-xml to 0.39.0
All checks were successful
Continuous integration / Check (push) Successful in 2m33s
Continuous integration / Test Suite (push) Successful in 4m6s
Continuous integration / Rustfmt (push) Successful in 1m38s
Continuous integration / build (push) Successful in 4m17s
Continuous integration / Trunk (push) Successful in 7m52s
Continuous integration / Disallow unused dependencies (push) Successful in 5m40s
2026-01-11 19:01:21 +00:00
33bb355975 chore: Release
All checks were successful
Continuous integration / Check (push) Successful in 1m48s
Continuous integration / Test Suite (push) Successful in 2m4s
Continuous integration / Rustfmt (push) Successful in 58s
Continuous integration / Trunk (push) Successful in 2m11s
Continuous integration / build (push) Successful in 3m33s
Continuous integration / Disallow unused dependencies (push) Successful in 5m42s
2026-01-08 16:13:19 -08:00
2f20a3a8ed server: set user-agent when slurping, fixes /. bug 2026-01-08 16:13:00 -08:00
8b3bfe253f Merge pull request 'chore(deps): update actions/checkout action to v6' (#197) from renovate/actions-checkout-6.x into master
All checks were successful
Continuous integration / Check (push) Successful in 3m43s
Continuous integration / Test Suite (push) Successful in 3m52s
Continuous integration / Rustfmt (push) Successful in 46s
Continuous integration / Trunk (push) Successful in 2m27s
Continuous integration / build (push) Successful in 2m20s
Continuous integration / Disallow unused dependencies (push) Successful in 5m39s
Reviewed-on: #197
2026-01-08 12:23:57 -08:00
22b9646ac4 Merge pull request 'fix(deps): update rust crate zip to v7' (#217) from renovate/zip-7.x into master
Some checks failed
Continuous integration / Check (push) Has been cancelled
Continuous integration / Test Suite (push) Has been cancelled
Continuous integration / Trunk (push) Has been cancelled
Continuous integration / Rustfmt (push) Has been cancelled
Continuous integration / build (push) Has been cancelled
Continuous integration / Disallow unused dependencies (push) Has been cancelled
Reviewed-on: #217
2026-01-08 12:23:42 -08:00
1df8ad8a0c Merge branch 'renovate/all-minor-patch'
All checks were successful
Continuous integration / Check (push) Successful in 1m26s
Continuous integration / Trunk (push) Successful in 59s
Continuous integration / Rustfmt (push) Successful in 49s
Continuous integration / Test Suite (push) Successful in 4m6s
Continuous integration / build (push) Successful in 2m43s
Continuous integration / Disallow unused dependencies (push) Successful in 5m49s
2026-01-08 10:51:25 -08:00
bb7721dbc6 Updates for askama 2026-01-08 10:51:09 -08:00
475c552e3a fix(deps): update all non-major dependencies
Some checks failed
Continuous integration / Check (push) Failing after 1m54s
Continuous integration / Trunk (push) Successful in 1m34s
Continuous integration / Rustfmt (push) Successful in 45s
Continuous integration / Test Suite (push) Failing after 5m28s
Continuous integration / build (push) Failing after 3m29s
Continuous integration / Disallow unused dependencies (push) Failing after 5m41s
2026-01-08 17:03:02 +00:00
c85832c93b chore: Release
All checks were successful
Continuous integration / Check (push) Successful in 1m28s
Continuous integration / Test Suite (push) Successful in 2m30s
Continuous integration / Rustfmt (push) Successful in 1m4s
Continuous integration / Trunk (push) Successful in 2m5s
Continuous integration / build (push) Successful in 2m24s
Continuous integration / Disallow unused dependencies (push) Successful in 5m0s
2026-01-08 08:54:01 -08:00
7e991186fe server: add indexes that should help with bulk read 2026-01-08 08:53:42 -08:00
95d06ec669 Merge pull request 'chore(deps): lock file maintenance' (#224) from renovate/lock-file-maintenance into master
All checks were successful
Continuous integration / Check (push) Successful in 1m18s
Continuous integration / Test Suite (push) Successful in 4m27s
Continuous integration / Rustfmt (push) Successful in 40s
Continuous integration / Trunk (push) Successful in 3m56s
Continuous integration / build (push) Successful in 1m56s
Continuous integration / Disallow unused dependencies (push) Successful in 5m9s
2026-01-04 16:32:18 -08:00
84810d8644 chore(deps): lock file maintenance
All checks were successful
Continuous integration / Check (push) Successful in 2m19s
Continuous integration / Trunk (push) Successful in 1m37s
Continuous integration / Rustfmt (push) Successful in 46s
Continuous integration / build (push) Successful in 4m33s
Continuous integration / Test Suite (push) Successful in 9m6s
Continuous integration / Disallow unused dependencies (push) Successful in 2m21s
2026-01-05 00:02:23 +00:00
8a86f0d0b2 chore: Release
All checks were successful
Continuous integration / Check (push) Successful in 1m8s
Continuous integration / Test Suite (push) Successful in 3m35s
Continuous integration / Rustfmt (push) Successful in 55s
Continuous integration / Trunk (push) Successful in 2m4s
Continuous integration / build (push) Successful in 1m51s
Continuous integration / Disallow unused dependencies (push) Successful in 5m1s
2026-01-04 11:08:22 -08:00
eab4986fd3 server: fix date extraction and unit tests 2026-01-04 11:08:03 -08:00
3c644c570e Merge pull request 'chore(deps): lock file maintenance' (#223) from renovate/lock-file-maintenance into master
All checks were successful
Continuous integration / Check (push) Successful in 1m25s
Continuous integration / Test Suite (push) Successful in 2m17s
Continuous integration / Trunk (push) Successful in 2m5s
Continuous integration / Rustfmt (push) Successful in 42s
Continuous integration / Disallow unused dependencies (push) Successful in 2m26s
Continuous integration / build (push) Successful in 9m7s
2025-12-28 18:46:57 -08:00
7a9df3c15c chore(deps): lock file maintenance
All checks were successful
Continuous integration / Check (push) Successful in 2m28s
Continuous integration / Test Suite (push) Successful in 2m42s
Continuous integration / Rustfmt (push) Successful in 42s
Continuous integration / build (push) Successful in 2m41s
Continuous integration / Disallow unused dependencies (push) Successful in 2m14s
Continuous integration / Trunk (push) Successful in 20m53s
2025-12-29 01:47:41 +00:00
f9d8acf744 Merge pull request 'chore(deps): lock file maintenance' (#222) from renovate/lock-file-maintenance into master
All checks were successful
Continuous integration / Check (push) Successful in 2m14s
Continuous integration / Test Suite (push) Successful in 1m51s
Continuous integration / Rustfmt (push) Successful in 1m2s
Continuous integration / Trunk (push) Successful in 2m7s
Continuous integration / build (push) Successful in 2m7s
Continuous integration / Disallow unused dependencies (push) Successful in 5m4s
2025-12-28 16:48:08 -08:00
75f3770f3e chore(deps): lock file maintenance
All checks were successful
Continuous integration / Check (push) Successful in 4m26s
Continuous integration / Test Suite (push) Successful in 4m25s
Continuous integration / Rustfmt (push) Successful in 40s
Continuous integration / build (push) Successful in 4m10s
Continuous integration / Disallow unused dependencies (push) Successful in 2m18s
Continuous integration / Trunk (push) Successful in 20m54s
2025-12-29 00:02:21 +00:00
85dd61a272 Merge pull request 'chore(deps): update rust crate serde_json to v1.0.146' (#220) from renovate/all-minor-patch into master
All checks were successful
Continuous integration / Check (push) Successful in 1m29s
Continuous integration / Test Suite (push) Successful in 6m57s
Continuous integration / Trunk (push) Successful in 7m51s
Continuous integration / Rustfmt (push) Successful in 1m30s
Continuous integration / build (push) Successful in 2m18s
Continuous integration / Disallow unused dependencies (push) Successful in 5m1s
2025-12-22 05:01:52 -08:00
1c5412de14 chore(deps): update rust crate serde_json to v1.0.146
All checks were successful
Continuous integration / Check (push) Successful in 2m41s
Continuous integration / Test Suite (push) Successful in 2m46s
Continuous integration / Rustfmt (push) Successful in 54s
Continuous integration / build (push) Successful in 2m51s
Continuous integration / Disallow unused dependencies (push) Successful in 2m16s
Continuous integration / Trunk (push) Successful in 21m20s
2025-12-22 12:31:26 +00:00
034027ddd5 Merge pull request 'chore(deps): lock file maintenance' (#219) from renovate/lock-file-maintenance into master
All checks were successful
Continuous integration / Check (push) Successful in 1m39s
Continuous integration / Test Suite (push) Successful in 2m37s
Continuous integration / Trunk (push) Successful in 2m5s
Continuous integration / Rustfmt (push) Successful in 53s
Continuous integration / Disallow unused dependencies (push) Successful in 2m30s
Continuous integration / build (push) Successful in 7m6s
2025-12-21 16:47:07 -08:00
81a07a8172 chore(deps): lock file maintenance
All checks were successful
Continuous integration / Check (push) Successful in 3m28s
Continuous integration / Test Suite (push) Successful in 3m40s
Continuous integration / Rustfmt (push) Successful in 47s
Continuous integration / build (push) Successful in 3m30s
Continuous integration / Disallow unused dependencies (push) Successful in 2m22s
Continuous integration / Trunk (push) Successful in 21m0s
2025-12-22 00:02:30 +00:00
d9e8c2133e Merge pull request 'chore(deps): update rust crate axum to v0.8.8' (#218) from renovate/axum-monorepo into master
All checks were successful
Continuous integration / Check (push) Successful in 1m10s
Continuous integration / Trunk (push) Successful in 1m12s
Continuous integration / Rustfmt (push) Successful in 50s
Continuous integration / Test Suite (push) Successful in 3m2s
Continuous integration / build (push) Successful in 1m54s
Continuous integration / Disallow unused dependencies (push) Successful in 5m0s
2025-12-20 09:02:03 -08:00
99aa7a7071 chore(deps): update rust crate axum to v0.8.8
All checks were successful
Continuous integration / Check (push) Successful in 1m11s
Continuous integration / Test Suite (push) Successful in 3m36s
Continuous integration / Rustfmt (push) Successful in 1m30s
Continuous integration / build (push) Successful in 4m12s
Continuous integration / Trunk (push) Successful in 8m35s
Continuous integration / Disallow unused dependencies (push) Successful in 5m2s
2025-12-20 16:46:32 +00:00
bf7418339e fix(deps): update rust crate zip to v7
All checks were successful
Continuous integration / Check (push) Successful in 1m36s
Continuous integration / Test Suite (push) Successful in 2m46s
Continuous integration / Rustfmt (push) Successful in 40s
Continuous integration / build (push) Successful in 2m46s
Continuous integration / Disallow unused dependencies (push) Successful in 2m19s
Continuous integration / Trunk (push) Successful in 21m7s
2025-12-20 00:01:57 +00:00
cc585cc63f Merge pull request 'chore(deps): lock file maintenance' (#214) from renovate/lock-file-maintenance into master
All checks were successful
Continuous integration / Check (push) Successful in 55s
Continuous integration / Test Suite (push) Successful in 1m17s
Continuous integration / Trunk (push) Successful in 1m3s
Continuous integration / Rustfmt (push) Successful in 1m29s
Continuous integration / build (push) Successful in 1m28s
Continuous integration / Disallow unused dependencies (push) Successful in 5m3s
2025-12-15 09:47:04 -08:00
293f90fde5 chore(deps): lock file maintenance
All checks were successful
Continuous integration / Check (push) Successful in 1m3s
Continuous integration / Test Suite (push) Successful in 1m38s
Continuous integration / Trunk (push) Successful in 1m13s
Continuous integration / Rustfmt (push) Successful in 41s
Continuous integration / build (push) Successful in 1m36s
Continuous integration / Disallow unused dependencies (push) Successful in 2m17s
2025-12-15 17:17:40 +00:00
a06e4b3454 Merge pull request 'chore(deps): update rust crate reqwest to v0.12.26' (#215) from renovate/all-minor-patch into master
All checks were successful
Continuous integration / Check (push) Successful in 56s
Continuous integration / Test Suite (push) Successful in 1m22s
Continuous integration / Rustfmt (push) Successful in 40s
Continuous integration / build (push) Successful in 1m36s
Continuous integration / Disallow unused dependencies (push) Successful in 2m27s
Continuous integration / Trunk (push) Successful in 21m2s
2025-12-15 09:16:55 -08:00
6e5145e21b chore(deps): update rust crate reqwest to v0.12.26
All checks were successful
Continuous integration / Check (push) Successful in 56s
Continuous integration / Test Suite (push) Successful in 1m38s
Continuous integration / Trunk (push) Successful in 1m5s
Continuous integration / Rustfmt (push) Successful in 1m30s
Continuous integration / build (push) Successful in 2m18s
Continuous integration / Disallow unused dependencies (push) Successful in 5m6s
2025-12-15 16:46:42 +00:00
d41f3e9fd1 Merge pull request 'chore(deps): lock file maintenance' (#213) from renovate/lock-file-maintenance into master
All checks were successful
Continuous integration / Check (push) Successful in 58s
Continuous integration / Test Suite (push) Successful in 1m37s
Continuous integration / Rustfmt (push) Successful in 54s
Continuous integration / Trunk (push) Successful in 2m3s
Continuous integration / build (push) Successful in 1m42s
Continuous integration / Disallow unused dependencies (push) Successful in 5m4s
2025-12-14 16:47:42 -08:00
5519018043 chore(deps): lock file maintenance
All checks were successful
Continuous integration / Check (push) Successful in 1m15s
Continuous integration / Test Suite (push) Successful in 2m23s
Continuous integration / Rustfmt (push) Successful in 1m16s
Continuous integration / build (push) Successful in 2m42s
Continuous integration / Disallow unused dependencies (push) Successful in 2m24s
Continuous integration / Trunk (push) Successful in 21m15s
2025-12-15 00:02:24 +00:00
e3121219b6 chore: Release
All checks were successful
Continuous integration / Check (push) Successful in 1m3s
Continuous integration / Test Suite (push) Successful in 1m19s
Continuous integration / Trunk (push) Successful in 1m2s
Continuous integration / Rustfmt (push) Successful in 49s
Continuous integration / Disallow unused dependencies (push) Successful in 2m26s
Continuous integration / build (push) Successful in 3m12s
2025-12-14 09:14:13 -08:00
7272bbb6b0 web: text overflow at 2 lines for subject and authors 2025-12-14 09:13:48 -08:00
dc741f421b Merge pull request 'fix(deps): update all non-major dependencies' (#212) from renovate/all-minor-patch into master
All checks were successful
Continuous integration / Check (push) Successful in 1m26s
Continuous integration / Trunk (push) Successful in 1m11s
Continuous integration / Test Suite (push) Successful in 3m17s
Continuous integration / Rustfmt (push) Successful in 42s
Continuous integration / Disallow unused dependencies (push) Successful in 2m25s
Continuous integration / build (push) Successful in 7m21s
2025-12-08 13:17:00 -08:00
69d3b8a210 fix(deps): update all non-major dependencies
All checks were successful
Continuous integration / Check (push) Successful in 2m9s
Continuous integration / Test Suite (push) Successful in 2m50s
Continuous integration / Rustfmt (push) Successful in 1m27s
Continuous integration / build (push) Successful in 3m50s
Continuous integration / Disallow unused dependencies (push) Successful in 2m33s
Continuous integration / Trunk (push) Successful in 20m56s
2025-12-08 20:32:37 +00:00
f5c4067291 Merge pull request 'chore(deps): lock file maintenance' (#211) from renovate/lock-file-maintenance into master
All checks were successful
Continuous integration / Check (push) Successful in 1m28s
Continuous integration / Test Suite (push) Successful in 2m17s
Continuous integration / Trunk (push) Successful in 7m39s
Continuous integration / Rustfmt (push) Successful in 1m29s
Continuous integration / build (push) Successful in 3m25s
Continuous integration / Disallow unused dependencies (push) Successful in 5m7s
2025-12-07 16:32:06 -08:00
930a45cbad chore(deps): lock file maintenance
All checks were successful
Continuous integration / Check (push) Successful in 2m21s
Continuous integration / Trunk (push) Successful in 2m41s
Continuous integration / Test Suite (push) Successful in 4m28s
Continuous integration / Rustfmt (push) Successful in 1m30s
Continuous integration / build (push) Successful in 4m42s
Continuous integration / Disallow unused dependencies (push) Successful in 5m7s
2025-12-08 00:02:19 +00:00
ef612c0d4f Merge pull request 'fix(deps): update rust crate scraper to 0.25.0' (#210) from renovate/all-minor-patch into master
All checks were successful
Continuous integration / Check (push) Successful in 1m59s
Continuous integration / Trunk (push) Successful in 2m8s
Continuous integration / Test Suite (push) Successful in 4m51s
Continuous integration / Rustfmt (push) Successful in 1m30s
Continuous integration / build (push) Successful in 2m37s
Continuous integration / Disallow unused dependencies (push) Successful in 5m5s
2025-12-07 14:46:27 -08:00
723e9c5ff5 fix(deps): update rust crate scraper to 0.25.0
All checks were successful
Continuous integration / Check (push) Successful in 2m57s
Continuous integration / Test Suite (push) Successful in 2m13s
Continuous integration / Rustfmt (push) Successful in 43s
Continuous integration / build (push) Successful in 2m15s
Continuous integration / Disallow unused dependencies (push) Successful in 2m44s
Continuous integration / Trunk (push) Successful in 21m9s
2025-12-07 22:16:57 +00:00
0fdcfabfbe Merge pull request 'chore(deps): update rust crate html2text to v0.16.5' (#209) from renovate/all-minor-patch into master
All checks were successful
Continuous integration / Check (push) Successful in 1m57s
Continuous integration / Test Suite (push) Successful in 2m50s
Continuous integration / Trunk (push) Successful in 1m17s
Continuous integration / Rustfmt (push) Successful in 1m28s
Continuous integration / build (push) Successful in 2m16s
Continuous integration / Disallow unused dependencies (push) Successful in 5m1s
2025-12-06 00:31:45 -08:00
b6c3f014cb chore(deps): update rust crate html2text to v0.16.5
All checks were successful
Continuous integration / Check (push) Successful in 1m30s
Continuous integration / Test Suite (push) Successful in 2m50s
Continuous integration / Rustfmt (push) Successful in 1m31s
Continuous integration / Trunk (push) Successful in 8m56s
Continuous integration / build (push) Successful in 6m26s
Continuous integration / Disallow unused dependencies (push) Successful in 2m52s
2025-12-06 08:17:07 +00:00
1937bb4c99 Merge pull request 'chore(deps): update rust crate flate2 to v1.1.7' (#208) from renovate/all-minor-patch into master
Some checks failed
Continuous integration / Check (push) Failing after 12s
Continuous integration / Trunk (push) Failing after 11s
Continuous integration / Rustfmt (push) Failing after 12s
Continuous integration / build (push) Failing after 11s
Continuous integration / Disallow unused dependencies (push) Failing after 11s
Continuous integration / Test Suite (push) Successful in 2m19s
2025-12-05 02:31:21 -08:00
cdd5d9befc chore(deps): update rust crate flate2 to v1.1.7
All checks were successful
Continuous integration / Check (push) Successful in 1m43s
Continuous integration / Trunk (push) Successful in 1m9s
Continuous integration / Rustfmt (push) Successful in 44s
Continuous integration / build (push) Successful in 2m32s
Continuous integration / Disallow unused dependencies (push) Successful in 2m18s
Continuous integration / Test Suite (push) Successful in 9m35s
2025-12-05 10:16:24 +00:00
232a14fd96 Merge pull request 'chore(deps): update rust crate log to v0.4.29' (#206) from renovate/all-minor-patch into master
All checks were successful
Continuous integration / Check (push) Successful in 1m19s
Continuous integration / Test Suite (push) Successful in 2m6s
Continuous integration / Trunk (push) Successful in 1m4s
Continuous integration / Rustfmt (push) Successful in 43s
Continuous integration / build (push) Successful in 2m23s
Continuous integration / Disallow unused dependencies (push) Successful in 2m28s
2025-12-02 14:46:36 -08:00
3038c98a7a chore(deps): update rust crate log to v0.4.29
All checks were successful
Continuous integration / Check (push) Successful in 1m31s
Continuous integration / Test Suite (push) Successful in 3m40s
Continuous integration / Trunk (push) Successful in 1m32s
Continuous integration / Rustfmt (push) Successful in 46s
Continuous integration / build (push) Successful in 4m34s
Continuous integration / Disallow unused dependencies (push) Successful in 2m33s
2025-12-02 22:16:32 +00:00
4dd240c358 Merge pull request 'chore(deps): update rust crate uuid to v1.19.0' (#205) from renovate/all-minor-patch into master
All checks were successful
Continuous integration / Check (push) Successful in 1m12s
Continuous integration / Test Suite (push) Successful in 2m58s
Continuous integration / Trunk (push) Successful in 1m13s
Continuous integration / Rustfmt (push) Successful in 41s
Continuous integration / build (push) Successful in 3m4s
Continuous integration / Disallow unused dependencies (push) Successful in 2m22s
2025-12-01 19:31:17 -08:00
c66e876ab7 chore(deps): update rust crate uuid to v1.19.0
All checks were successful
Continuous integration / Check (push) Successful in 1m28s
Continuous integration / Test Suite (push) Successful in 2m13s
Continuous integration / Trunk (push) Successful in 7m37s
Continuous integration / build (push) Successful in 2m3s
Continuous integration / Disallow unused dependencies (push) Successful in 2m35s
Continuous integration / Rustfmt (push) Successful in 43s
2025-12-02 02:46:26 +00:00
a7762595fa Merge pull request 'chore(deps): lock file maintenance' (#204) from renovate/lock-file-maintenance into master
All checks were successful
Continuous integration / Check (push) Successful in 1m20s
Continuous integration / Test Suite (push) Successful in 2m30s
Continuous integration / Trunk (push) Successful in 7m27s
Continuous integration / Rustfmt (push) Successful in 42s
Continuous integration / build (push) Successful in 2m28s
Continuous integration / Disallow unused dependencies (push) Successful in 2m24s
2025-11-30 16:46:57 -08:00
1ac471dfe7 chore(deps): lock file maintenance
All checks were successful
Continuous integration / Check (push) Successful in 1m46s
Continuous integration / Test Suite (push) Successful in 3m26s
Continuous integration / Trunk (push) Successful in 7m52s
Continuous integration / Rustfmt (push) Successful in 42s
Continuous integration / build (push) Successful in 3m26s
Continuous integration / Disallow unused dependencies (push) Successful in 2m20s
2025-12-01 00:01:57 +00:00
72a549ea0f Merge pull request 'chore(deps): update rust crate tracing to v0.1.43' (#203) from renovate/tokio-tracing-monorepo into master
All checks were successful
Continuous integration / Check (push) Successful in 1m46s
Continuous integration / Test Suite (push) Successful in 2m49s
Continuous integration / Trunk (push) Successful in 1m8s
Continuous integration / Rustfmt (push) Successful in 56s
Continuous integration / build (push) Successful in 2m18s
Continuous integration / Disallow unused dependencies (push) Successful in 2m58s
2025-11-28 01:31:57 -08:00
878afd695f chore(deps): update rust crate tracing to v0.1.43
All checks were successful
Continuous integration / Check (push) Successful in 1m29s
Continuous integration / Test Suite (push) Successful in 3m3s
Continuous integration / Trunk (push) Successful in 7m58s
Continuous integration / Rustfmt (push) Successful in 53s
Continuous integration / build (push) Successful in 2m53s
Continuous integration / Disallow unused dependencies (push) Successful in 2m20s
2025-11-28 09:01:41 +00:00
50b23731df Merge pull request 'chore(deps): update rust crate xtracing to v0.3.3' (#202) from renovate/all-minor-patch into master
All checks were successful
Continuous integration / Check (push) Successful in 1m14s
Continuous integration / Test Suite (push) Successful in 1m57s
Continuous integration / Trunk (push) Successful in 2m7s
Continuous integration / Rustfmt (push) Successful in 46s
Continuous integration / build (push) Successful in 2m17s
Continuous integration / Disallow unused dependencies (push) Successful in 2m50s
2025-11-27 16:18:03 -08:00
95df6b54ea chore(deps): update rust crate xtracing to v0.3.3
All checks were successful
Continuous integration / Check (push) Successful in 1m13s
Continuous integration / Test Suite (push) Successful in 2m29s
Continuous integration / Trunk (push) Successful in 1m8s
Continuous integration / Rustfmt (push) Successful in 44s
Continuous integration / build (push) Successful in 3m0s
Continuous integration / Disallow unused dependencies (push) Successful in 2m50s
2025-11-27 23:32:15 +00:00
ee626eb631 chore: Release
All checks were successful
Continuous integration / Check (push) Successful in 1m21s
Continuous integration / Test Suite (push) Successful in 2m5s
Continuous integration / Trunk (push) Successful in 58s
Continuous integration / Rustfmt (push) Successful in 43s
Continuous integration / build (push) Successful in 1m57s
Continuous integration / Disallow unused dependencies (push) Successful in 2m26s
2025-11-27 10:11:04 -08:00
26f805738d web: fix progress on news + catchup mode
Some checks failed
Continuous integration / Check (push) Has been cancelled
Continuous integration / Test Suite (push) Has been cancelled
Continuous integration / Trunk (push) Has been cancelled
Continuous integration / Rustfmt (push) Has been cancelled
Continuous integration / build (push) Has been cancelled
Continuous integration / Disallow unused dependencies (push) Has been cancelled
2025-11-27 10:10:42 -08:00
30b89c2418 chore: Release
All checks were successful
Continuous integration / Check (push) Successful in 1m5s
Continuous integration / Test Suite (push) Successful in 2m2s
Continuous integration / Trunk (push) Successful in 1m12s
Continuous integration / Rustfmt (push) Successful in 44s
Continuous integration / build (push) Successful in 2m52s
Continuous integration / Disallow unused dependencies (push) Successful in 2m15s
2025-11-27 09:58:29 -08:00
b933b2a113 Normalize all letterbox dependency versions 2025-11-27 09:58:07 -08:00
dfbe6d67aa chore: Release 2025-11-27 09:55:40 -08:00
8cca562a33 cargo update 2025-11-27 09:55:13 -08:00
b1e207765f chore: Release 2025-11-27 09:53:22 -08:00
b140c15fc8 chore: Release 2025-11-27 09:46:23 -08:00
859564c476 web: change up progress bar behavior 2025-11-27 09:45:01 -08:00
3c48076996 web: add spinner when loading next page in catchup mode 2025-11-26 13:46:19 -08:00
01fd53e467 Merge pull request 'chore(deps): update rust crate tracing to v0.1.42' (#201) from renovate/tokio-tracing-monorepo into master
All checks were successful
Continuous integration / Check (push) Successful in 1m15s
Continuous integration / Test Suite (push) Successful in 2m42s
Continuous integration / Trunk (push) Successful in 1m21s
Continuous integration / Rustfmt (push) Successful in 51s
Continuous integration / build (push) Successful in 1m57s
Continuous integration / Disallow unused dependencies (push) Successful in 2m25s
2025-11-26 06:01:53 -08:00
8001c37c91 chore(deps): update rust crate tracing to v0.1.42
All checks were successful
Continuous integration / Check (push) Successful in 1m15s
Continuous integration / Test Suite (push) Successful in 2m30s
Continuous integration / Trunk (push) Successful in 1m56s
Continuous integration / Rustfmt (push) Successful in 53s
Continuous integration / build (push) Successful in 3m22s
Continuous integration / Disallow unused dependencies (push) Successful in 2m42s
2025-11-26 13:31:40 +00:00
f139dd391a Merge pull request 'chore(deps): update rust crate cacher to v0.2.1' (#200) from renovate/all-minor-patch into master
All checks were successful
Continuous integration / Check (push) Successful in 1m9s
Continuous integration / Test Suite (push) Successful in 1m40s
Continuous integration / Trunk (push) Successful in 1m5s
Continuous integration / Rustfmt (push) Successful in 43s
Continuous integration / build (push) Successful in 2m6s
Continuous integration / Disallow unused dependencies (push) Successful in 2m31s
2025-11-25 18:46:52 -08:00
27588b44c3 chore(deps): update rust crate cacher to v0.2.1
All checks were successful
Continuous integration / Check (push) Successful in 1m11s
Continuous integration / Test Suite (push) Successful in 2m16s
Continuous integration / Trunk (push) Successful in 7m41s
Continuous integration / Rustfmt (push) Successful in 42s
Continuous integration / build (push) Successful in 2m14s
Continuous integration / Disallow unused dependencies (push) Successful in 2m38s
2025-11-26 00:47:40 +00:00
79c78615f5 Merge pull request 'fix(deps): update rust crate zip to v6' (#178) from renovate/zip-6.x into master
All checks were successful
Continuous integration / Check (push) Successful in 2m26s
Continuous integration / Test Suite (push) Successful in 2m2s
Continuous integration / Trunk (push) Successful in 7m59s
Continuous integration / Rustfmt (push) Successful in 45s
Continuous integration / build (push) Successful in 2m6s
Continuous integration / Disallow unused dependencies (push) Successful in 2m39s
Reviewed-on: #178
2025-11-25 16:28:49 -08:00
3971228fc3 Merge pull request 'chore(deps): update rust crate tower-http to v0.6.7' (#199) from renovate/all-minor-patch into master
All checks were successful
Continuous integration / Check (push) Successful in 1m36s
Continuous integration / Test Suite (push) Successful in 1m55s
Continuous integration / Trunk (push) Successful in 57s
Continuous integration / Rustfmt (push) Successful in 41s
Continuous integration / build (push) Successful in 2m11s
Continuous integration / Disallow unused dependencies (push) Successful in 2m31s
2025-11-24 10:46:57 -08:00
5dbce7642e chore(deps): update rust crate tower-http to v0.6.7
All checks were successful
Continuous integration / Check (push) Successful in 2m4s
Continuous integration / Test Suite (push) Successful in 2m37s
Continuous integration / Trunk (push) Successful in 1m14s
Continuous integration / Rustfmt (push) Successful in 42s
Continuous integration / build (push) Successful in 2m12s
Continuous integration / Disallow unused dependencies (push) Successful in 2m59s
2025-11-24 18:31:35 +00:00
f0d14f6bdc Merge pull request 'chore(deps): lock file maintenance' (#198) from renovate/lock-file-maintenance into master
All checks were successful
Continuous integration / Check (push) Successful in 1m7s
Continuous integration / Test Suite (push) Successful in 1m52s
Continuous integration / Trunk (push) Successful in 8m0s
Continuous integration / Rustfmt (push) Successful in 48s
Continuous integration / build (push) Successful in 1m53s
Continuous integration / Disallow unused dependencies (push) Successful in 2m25s
2025-11-23 16:47:27 -08:00
18e8802299 chore(deps): lock file maintenance
All checks were successful
Continuous integration / Check (push) Successful in 2m1s
Continuous integration / Test Suite (push) Successful in 4m28s
Continuous integration / Trunk (push) Successful in 8m3s
Continuous integration / Rustfmt (push) Successful in 47s
Continuous integration / build (push) Successful in 4m8s
Continuous integration / Disallow unused dependencies (push) Successful in 2m31s
2025-11-24 00:02:37 +00:00
51ff0b8e14 chore(deps): update actions/checkout action to v6
All checks were successful
Continuous integration / Check (push) Successful in 58s
Continuous integration / Test Suite (push) Successful in 1m51s
Continuous integration / Rustfmt (push) Successful in 41s
Continuous integration / build (push) Successful in 1m53s
Continuous integration / Disallow unused dependencies (push) Successful in 2m19s
Continuous integration / Trunk (push) Successful in 1m41s
2025-11-20 16:46:50 +00:00
8ea3ac2745 Merge pull request 'chore(deps): update rust crate clap to v4.5.53' (#196) from renovate/all-minor-patch into master
All checks were successful
Continuous integration / Check (push) Successful in 1m32s
Continuous integration / Test Suite (push) Successful in 2m10s
Continuous integration / Trunk (push) Successful in 56s
Continuous integration / Rustfmt (push) Successful in 40s
Continuous integration / build (push) Successful in 1m40s
Continuous integration / Disallow unused dependencies (push) Successful in 2m19s
2025-11-19 13:31:31 -08:00
a4c9850d8e chore(deps): update rust crate clap to v4.5.53
All checks were successful
Continuous integration / Check (push) Successful in 1m32s
Continuous integration / Test Suite (push) Successful in 1m55s
Continuous integration / Trunk (push) Successful in 7m31s
Continuous integration / Rustfmt (push) Successful in 48s
Continuous integration / build (push) Successful in 1m38s
Continuous integration / Disallow unused dependencies (push) Successful in 2m22s
2025-11-19 21:01:54 +00:00
9e7522951c Merge pull request 'chore(deps): update rust crate clap to v4.5.52' (#195) from renovate/all-minor-patch into master
All checks were successful
Continuous integration / Check (push) Successful in 1m8s
Continuous integration / Test Suite (push) Successful in 1m58s
Continuous integration / Trunk (push) Successful in 1m0s
Continuous integration / Rustfmt (push) Successful in 59s
Continuous integration / build (push) Successful in 1m39s
Continuous integration / Disallow unused dependencies (push) Successful in 2m38s
2025-11-17 09:31:40 -08:00
b43a025d1a chore(deps): update rust crate clap to v4.5.52
All checks were successful
Continuous integration / Check (push) Successful in 1m16s
Continuous integration / Test Suite (push) Successful in 1m44s
Continuous integration / Trunk (push) Successful in 1m15s
Continuous integration / Rustfmt (push) Successful in 38s
Continuous integration / build (push) Successful in 1m32s
Continuous integration / Disallow unused dependencies (push) Successful in 2m37s
2025-11-17 17:16:44 +00:00
2aa82f09c6 Merge pull request 'chore(deps): lock file maintenance' (#194) from renovate/lock-file-maintenance into master
All checks were successful
Continuous integration / Check (push) Successful in 1m12s
Continuous integration / Test Suite (push) Successful in 1m28s
Continuous integration / Trunk (push) Successful in 7m47s
Continuous integration / Rustfmt (push) Successful in 52s
Continuous integration / build (push) Successful in 1m49s
Continuous integration / Disallow unused dependencies (push) Successful in 2m24s
2025-11-16 16:47:15 -08:00
27f800356c chore(deps): lock file maintenance
All checks were successful
Continuous integration / Check (push) Successful in 1m30s
Continuous integration / Test Suite (push) Successful in 3m22s
Continuous integration / Trunk (push) Successful in 1m47s
Continuous integration / Rustfmt (push) Successful in 55s
Continuous integration / build (push) Successful in 3m28s
Continuous integration / Disallow unused dependencies (push) Successful in 2m15s
2025-11-17 00:02:49 +00:00
20ddf25605 Merge pull request 'chore(deps): update rust crate html2text to v0.16.4' (#193) from renovate/all-minor-patch into master
All checks were successful
Continuous integration / Check (push) Successful in 1m7s
Continuous integration / Test Suite (push) Successful in 1m37s
Continuous integration / Trunk (push) Successful in 1m3s
Continuous integration / Rustfmt (push) Successful in 40s
Continuous integration / build (push) Successful in 1m36s
Continuous integration / Disallow unused dependencies (push) Successful in 2m16s
2025-11-16 07:46:40 -08:00
c79ffee6e4 chore(deps): update rust crate html2text to v0.16.4
All checks were successful
Continuous integration / Check (push) Successful in 1m2s
Continuous integration / Test Suite (push) Successful in 1m23s
Continuous integration / Trunk (push) Successful in 59s
Continuous integration / Rustfmt (push) Successful in 41s
Continuous integration / build (push) Successful in 1m38s
Continuous integration / Disallow unused dependencies (push) Successful in 2m15s
2025-11-16 15:31:42 +00:00
1972e9ec20 Merge pull request 'chore(deps): update rust crate html2text to v0.16.3' (#192) from renovate/all-minor-patch into master
All checks were successful
Continuous integration / Check (push) Successful in 1m8s
Continuous integration / Test Suite (push) Successful in 1m32s
Continuous integration / Trunk (push) Successful in 1m5s
Continuous integration / Rustfmt (push) Successful in 51s
Continuous integration / build (push) Successful in 1m46s
Continuous integration / Disallow unused dependencies (push) Successful in 2m51s
2025-11-16 04:31:53 -08:00
2a238b0b02 chore(deps): update rust crate html2text to v0.16.3
All checks were successful
Continuous integration / Check (push) Successful in 1m15s
Continuous integration / Test Suite (push) Successful in 1m49s
Continuous integration / Trunk (push) Successful in 8m43s
Continuous integration / Rustfmt (push) Successful in 49s
Continuous integration / build (push) Successful in 1m53s
Continuous integration / Disallow unused dependencies (push) Successful in 2m49s
2025-11-16 12:01:57 +00:00
639b6c9f0a Merge pull request 'chore(deps): update rust crate quick-xml to v0.38.4' (#190) from renovate/all-minor-patch into master
All checks were successful
Continuous integration / Check (push) Successful in 1m11s
Continuous integration / Test Suite (push) Successful in 1m28s
Continuous integration / Trunk (push) Successful in 1m10s
Continuous integration / Rustfmt (push) Successful in 48s
Continuous integration / build (push) Successful in 1m44s
Continuous integration / Disallow unused dependencies (push) Successful in 2m16s
2025-11-14 14:46:37 -08:00
2d6bf544da chore(deps): update rust crate quick-xml to v0.38.4
All checks were successful
Continuous integration / Check (push) Successful in 52s
Continuous integration / Test Suite (push) Successful in 1m16s
Continuous integration / Trunk (push) Successful in 58s
Continuous integration / Rustfmt (push) Successful in 48s
Continuous integration / build (push) Successful in 1m45s
Continuous integration / Disallow unused dependencies (push) Successful in 2m30s
2025-11-14 22:16:56 +00:00
a56b6d72f6 Merge pull request 'chore(deps): update rust crate axum to v0.8.7' (#191) from renovate/axum-monorepo into master
All checks were successful
Continuous integration / Check (push) Successful in 1m2s
Continuous integration / Test Suite (push) Successful in 1m58s
Continuous integration / Trunk (push) Successful in 1m9s
Continuous integration / Rustfmt (push) Successful in 52s
Continuous integration / build (push) Successful in 1m28s
Continuous integration / Disallow unused dependencies (push) Successful in 2m36s
2025-11-14 14:16:38 -08:00
c03de14b79 chore(deps): update rust crate axum to v0.8.7
All checks were successful
Continuous integration / Check (push) Successful in 1m4s
Continuous integration / Test Suite (push) Successful in 1m59s
Continuous integration / Trunk (push) Successful in 8m4s
Continuous integration / Rustfmt (push) Successful in 43s
Continuous integration / build (push) Successful in 1m47s
Continuous integration / Disallow unused dependencies (push) Successful in 2m43s
2025-11-14 21:46:44 +00:00
44fc422aa3 Merge pull request 'chore(deps): lock file maintenance' (#189) from renovate/lock-file-maintenance into master
All checks were successful
Continuous integration / Check (push) Successful in 1m18s
Continuous integration / Test Suite (push) Successful in 1m57s
Continuous integration / Trunk (push) Successful in 1m1s
Continuous integration / Rustfmt (push) Successful in 40s
Continuous integration / build (push) Successful in 2m25s
Continuous integration / Disallow unused dependencies (push) Successful in 2m55s
2025-11-09 19:47:33 -08:00
58880c17aa chore(deps): lock file maintenance
All checks were successful
Continuous integration / Check (push) Successful in 1m48s
Continuous integration / Test Suite (push) Successful in 3m41s
Continuous integration / Trunk (push) Successful in 1m27s
Continuous integration / Rustfmt (push) Successful in 46s
Continuous integration / build (push) Successful in 4m18s
Continuous integration / Disallow unused dependencies (push) Successful in 2m18s
2025-11-10 03:02:35 +00:00
2be3b9ed07 Merge pull request 'chore(deps): lock file maintenance' (#188) from renovate/lock-file-maintenance into master
All checks were successful
Continuous integration / Check (push) Successful in 1m8s
Continuous integration / Test Suite (push) Successful in 1m46s
Continuous integration / Trunk (push) Successful in 7m51s
Continuous integration / Rustfmt (push) Successful in 42s
Continuous integration / build (push) Successful in 2m11s
Continuous integration / Disallow unused dependencies (push) Successful in 2m12s
2025-11-09 16:47:25 -08:00
9dfaa11cc6 chore(deps): lock file maintenance
All checks were successful
Continuous integration / Check (push) Successful in 2m2s
Continuous integration / Test Suite (push) Successful in 4m18s
Continuous integration / Trunk (push) Successful in 1m52s
Continuous integration / Rustfmt (push) Successful in 42s
Continuous integration / build (push) Successful in 4m21s
Continuous integration / Disallow unused dependencies (push) Successful in 2m32s
2025-11-10 00:02:49 +00:00
49492f9f21 Merge pull request 'chore(deps): update rust crate html2text to v0.16.2' (#187) from renovate/all-minor-patch into master
All checks were successful
Continuous integration / Check (push) Successful in 1m18s
Continuous integration / Test Suite (push) Successful in 2m12s
Continuous integration / Trunk (push) Successful in 1m3s
Continuous integration / Rustfmt (push) Successful in 37s
Continuous integration / build (push) Successful in 2m25s
Continuous integration / Disallow unused dependencies (push) Successful in 2m13s
2025-11-08 23:31:37 -08:00
84e120df95 chore(deps): update rust crate html2text to v0.16.2
All checks were successful
Continuous integration / Check (push) Successful in 1m7s
Continuous integration / Test Suite (push) Successful in 1m35s
Continuous integration / Trunk (push) Successful in 7m48s
Continuous integration / Rustfmt (push) Successful in 40s
Continuous integration / build (push) Successful in 2m13s
Continuous integration / Disallow unused dependencies (push) Successful in 2m36s
2025-11-09 07:01:43 +00:00
b21f0bc398 Merge pull request 'chore(deps): update rust crate html2text to v0.16.1' (#186) from renovate/all-minor-patch into master
All checks were successful
Continuous integration / Check (push) Successful in 1m2s
Continuous integration / Test Suite (push) Successful in 1m28s
Continuous integration / Trunk (push) Successful in 58s
Continuous integration / Rustfmt (push) Successful in 41s
Continuous integration / build (push) Successful in 1m35s
Continuous integration / Disallow unused dependencies (push) Successful in 2m14s
2025-11-08 03:02:16 -08:00
d8463c3178 chore(deps): update rust crate html2text to v0.16.1
All checks were successful
Continuous integration / Check (push) Successful in 1m7s
Continuous integration / Test Suite (push) Successful in 1m38s
Continuous integration / Trunk (push) Successful in 7m33s
Continuous integration / Rustfmt (push) Successful in 41s
Continuous integration / build (push) Successful in 1m35s
Continuous integration / Disallow unused dependencies (push) Successful in 2m21s
2025-11-08 10:46:41 +00:00
e1681edda3 chore: Release
All checks were successful
Continuous integration / Check (push) Successful in 1m1s
Continuous integration / Test Suite (push) Successful in 1m32s
Continuous integration / Trunk (push) Successful in 1m0s
Continuous integration / Rustfmt (push) Successful in 39s
Continuous integration / build (push) Successful in 1m41s
Continuous integration / Disallow unused dependencies (push) Successful in 2m12s
2025-11-05 21:12:09 -08:00
25ee8522ad cargo sqlx prepare 2025-11-05 21:12:08 -08:00
df356e8711 server: add label_unprocessed method, and implement wake 2025-11-05 21:11:26 -08:00
2e43700cd7 chore: Release
All checks were successful
Continuous integration / Check (push) Successful in 1m0s
Continuous integration / Test Suite (push) Successful in 1m31s
Continuous integration / Trunk (push) Successful in 1m24s
Continuous integration / Rustfmt (push) Successful in 42s
Continuous integration / build (push) Successful in 1m37s
Continuous integration / Disallow unused dependencies (push) Successful in 2m25s
2025-11-05 15:47:21 -08:00
b3769d99bf web: fix styling for second layer tags 2025-11-05 15:46:37 -08:00
2aa85a03f8 web: make +6 month button work from post date 2025-11-05 15:01:56 -08:00
c0982e82c6 chore: Release
All checks were successful
Continuous integration / Check (push) Successful in 58s
Continuous integration / Test Suite (push) Successful in 1m26s
Continuous integration / Trunk (push) Successful in 1m1s
Continuous integration / Rustfmt (push) Successful in 45s
Continuous integration / build (push) Successful in 1m48s
Continuous integration / Disallow unused dependencies (push) Successful in 2m33s
2025-11-03 17:19:22 -08:00
8971fe3b6b web: fix lint 2025-11-03 17:19:08 -08:00
243e35ec15 chore: Release 2025-11-03 15:43:26 -08:00
4cf1f882b8 cargo sqlx prepare 2025-11-03 15:43:25 -08:00
a8129e4685 Upsert snoozes and mark snoozed messages as read 2025-11-03 15:42:44 -08:00
50a4bfcac7 More implementation 2025-11-03 15:42:44 -08:00
90ac9a1e43 snooze: add UI elements and DB for snooze functionality 2025-11-03 15:42:44 -08:00
52b19365d7 Regen Cargo.lock 2025-11-03 15:42:44 -08:00
399865f5f7 snooze: add UI elements and DB for snooze functionality 2025-11-03 15:42:44 -08:00
2eb4784e83 WIP snooze feature 2025-11-03 15:42:44 -08:00
be2085b397 Merge pull request 'chore(deps): lock file maintenance' (#185) from renovate/lock-file-maintenance into master
All checks were successful
Continuous integration / Check (push) Successful in 1m4s
Continuous integration / Test Suite (push) Successful in 1m29s
Continuous integration / Trunk (push) Successful in 7m47s
Continuous integration / Rustfmt (push) Successful in 40s
Continuous integration / build (push) Successful in 1m50s
Continuous integration / Disallow unused dependencies (push) Successful in 2m12s
2025-11-02 17:02:02 -08:00
2837ea835a chore(deps): lock file maintenance
All checks were successful
Continuous integration / Check (push) Successful in 2m12s
Continuous integration / Test Suite (push) Successful in 4m19s
Continuous integration / Trunk (push) Successful in 7m50s
Continuous integration / Rustfmt (push) Successful in 43s
Continuous integration / build (push) Successful in 4m24s
Continuous integration / Disallow unused dependencies (push) Successful in 2m38s
2025-11-03 00:02:52 +00:00
a84e673d88 Merge pull request 'fix(deps): update all non-major dependencies' (#184) from renovate/all-minor-patch into master
All checks were successful
Continuous integration / Check (push) Successful in 55s
Continuous integration / Test Suite (push) Successful in 1m19s
Continuous integration / Trunk (push) Successful in 56s
Continuous integration / Rustfmt (push) Successful in 43s
Continuous integration / build (push) Successful in 1m32s
Continuous integration / Disallow unused dependencies (push) Successful in 2m11s
2025-11-01 05:46:41 -07:00
2bc840a4e2 fix(deps): update all non-major dependencies
All checks were successful
Continuous integration / Check (push) Successful in 2m22s
Continuous integration / Test Suite (push) Successful in 4m22s
Continuous integration / Trunk (push) Successful in 7m37s
Continuous integration / Rustfmt (push) Successful in 41s
Continuous integration / build (push) Successful in 5m3s
Continuous integration / Disallow unused dependencies (push) Successful in 2m16s
2025-11-01 12:16:43 +00:00
dd2062f719 Merge pull request 'chore(deps): lock file maintenance' (#183) from renovate/lock-file-maintenance into master
All checks were successful
Continuous integration / Check (push) Successful in 1m55s
Continuous integration / Test Suite (push) Successful in 3m44s
Continuous integration / Trunk (push) Successful in 8m15s
Continuous integration / Rustfmt (push) Successful in 42s
Continuous integration / build (push) Successful in 4m44s
Continuous integration / Disallow unused dependencies (push) Successful in 3m0s
2025-10-26 18:02:44 -07:00
616623e477 chore(deps): lock file maintenance
All checks were successful
Continuous integration / Check (push) Successful in 2m46s
Continuous integration / Test Suite (push) Successful in 5m45s
Continuous integration / Trunk (push) Successful in 7m43s
Continuous integration / Rustfmt (push) Successful in 52s
Continuous integration / build (push) Successful in 6m14s
Continuous integration / Disallow unused dependencies (push) Successful in 2m20s
2025-10-27 00:02:30 +00:00
593a20f621 Merge pull request 'chore(deps): update all non-major dependencies' (#181) from renovate/all-minor-patch into master
All checks were successful
Continuous integration / Check (push) Successful in 1m35s
Continuous integration / Test Suite (push) Successful in 3m8s
Continuous integration / Trunk (push) Successful in 1m24s
Continuous integration / Rustfmt (push) Successful in 40s
Continuous integration / build (push) Successful in 2m55s
Continuous integration / Disallow unused dependencies (push) Successful in 2m30s
Reviewed-on: #181
2025-10-24 07:35:10 -07:00
584ccba5bd chore(deps): update all non-major dependencies
Some checks failed
renovate/artifacts Artifact file update failure
Continuous integration / Check (push) Successful in 1m38s
Continuous integration / Test Suite (push) Successful in 2m56s
Continuous integration / Trunk (push) Successful in 7m43s
Continuous integration / Rustfmt (push) Successful in 40s
Continuous integration / build (push) Successful in 2m55s
Continuous integration / Disallow unused dependencies (push) Successful in 2m28s
2025-10-24 05:46:43 +00:00
e7a01e9d70 Merge pull request 'fix(deps): update all non-major dependencies' (#163) from renovate/all-minor-patch into master
All checks were successful
Continuous integration / Check (push) Successful in 1m43s
Continuous integration / Test Suite (push) Successful in 3m25s
Continuous integration / Trunk (push) Successful in 1m41s
Continuous integration / Rustfmt (push) Successful in 43s
Continuous integration / build (push) Successful in 3m8s
Continuous integration / Disallow unused dependencies (push) Successful in 2m56s
Reviewed-on: #163
2025-10-22 13:39:16 -07:00
727599c12c fix(deps): update all non-major dependencies
Some checks failed
renovate/artifacts Artifact file update failure
Continuous integration / Check (push) Successful in 2m16s
Continuous integration / Test Suite (push) Successful in 4m21s
Continuous integration / Trunk (push) Successful in 7m59s
Continuous integration / Rustfmt (push) Successful in 46s
Continuous integration / build (push) Successful in 5m3s
Continuous integration / Disallow unused dependencies (push) Successful in 2m22s
2025-10-22 08:17:31 +00:00
17ad5b3b0b chore: Release
All checks were successful
Continuous integration / Check (push) Successful in 1m47s
Continuous integration / Test Suite (push) Successful in 2m58s
Continuous integration / Trunk (push) Successful in 1m31s
Continuous integration / Rustfmt (push) Successful in 42s
Continuous integration / build (push) Successful in 3m18s
Continuous integration / Disallow unused dependencies (push) Successful in 3m4s
2025-10-20 19:50:27 -07:00
285b2f1591 server: handle message/delivery-status 2025-10-20 19:50:01 -07:00
1537333e76 server: add handling for multipart/report w/ ract_rfc822 subpart 2025-10-20 19:50:01 -07:00
285ff1d098 Merge pull request 'chore(deps): lock file maintenance' (#180) from renovate/lock-file-maintenance into master
All checks were successful
Continuous integration / Check (push) Successful in 2m3s
Continuous integration / Test Suite (push) Successful in 3m1s
Continuous integration / Trunk (push) Successful in 8m7s
Continuous integration / Rustfmt (push) Successful in 42s
Continuous integration / build (push) Successful in 3m13s
Continuous integration / Disallow unused dependencies (push) Successful in 2m28s
2025-10-19 18:02:57 -07:00
1563bf05a3 chore(deps): lock file maintenance
All checks were successful
Continuous integration / Check (push) Successful in 2m42s
Continuous integration / Test Suite (push) Successful in 6m17s
Continuous integration / Trunk (push) Successful in 7m54s
Continuous integration / Rustfmt (push) Successful in 50s
Continuous integration / build (push) Successful in 5m58s
Continuous integration / Disallow unused dependencies (push) Successful in 2m26s
2025-10-20 00:02:39 +00:00
458aab3167 Merge pull request 'chore(deps): lock file maintenance' (#179) from renovate/lock-file-maintenance into master
All checks were successful
Continuous integration / Check (push) Successful in 1m40s
Continuous integration / Test Suite (push) Successful in 3m51s
Continuous integration / Trunk (push) Successful in 9m4s
Continuous integration / Rustfmt (push) Successful in 46s
Continuous integration / build (push) Successful in 4m7s
Continuous integration / Disallow unused dependencies (push) Successful in 2m41s
2025-10-12 18:02:38 -07:00
492e420337 chore(deps): lock file maintenance
All checks were successful
Continuous integration / Check (push) Successful in 2m20s
Continuous integration / Test Suite (push) Successful in 6m20s
Continuous integration / Trunk (push) Successful in 8m17s
Continuous integration / Rustfmt (push) Successful in 41s
Continuous integration / build (push) Successful in 6m8s
Continuous integration / Disallow unused dependencies (push) Successful in 2m44s
2025-10-13 00:02:43 +00:00
aa6f99f32e fix(deps): update rust crate zip to v6
All checks were successful
Continuous integration / Check (push) Successful in 1m43s
Continuous integration / Test Suite (push) Successful in 2m45s
Continuous integration / Trunk (push) Successful in 8m16s
Continuous integration / Rustfmt (push) Successful in 45s
Continuous integration / build (push) Successful in 5m13s
Continuous integration / Disallow unused dependencies (push) Successful in 2m43s
2025-10-09 11:46:54 +00:00
330f9b1763 Merge pull request 'chore(deps): lock file maintenance' (#177) from renovate/lock-file-maintenance into master
All checks were successful
Continuous integration / Check (push) Successful in 1m29s
Continuous integration / Test Suite (push) Successful in 2m33s
Continuous integration / Trunk (push) Successful in 8m10s
Continuous integration / Rustfmt (push) Successful in 42s
Continuous integration / build (push) Successful in 3m30s
Continuous integration / Disallow unused dependencies (push) Successful in 2m32s
2025-10-05 17:47:37 -07:00
ad904ac1c0 chore(deps): lock file maintenance
All checks were successful
Continuous integration / Check (push) Successful in 2m0s
Continuous integration / Test Suite (push) Successful in 3m53s
Continuous integration / Trunk (push) Successful in 1m39s
Continuous integration / Rustfmt (push) Successful in 51s
Continuous integration / build (push) Successful in 4m35s
Continuous integration / Disallow unused dependencies (push) Successful in 2m23s
2025-10-06 00:02:48 +00:00
20f125bda5 Merge pull request 'chore(deps): update rust crate axum to v0.8.6' (#176) from renovate/axum-monorepo into master
All checks were successful
Continuous integration / Check (push) Successful in 1m36s
Continuous integration / Test Suite (push) Successful in 2m37s
Continuous integration / Trunk (push) Successful in 1m23s
Continuous integration / Rustfmt (push) Successful in 1m15s
Continuous integration / build (push) Successful in 3m40s
Continuous integration / Disallow unused dependencies (push) Successful in 2m16s
2025-09-30 06:46:31 -07:00
cf99e75ab8 chore(deps): update rust crate axum to v0.8.6
All checks were successful
Continuous integration / Check (push) Successful in 1m47s
Continuous integration / Test Suite (push) Successful in 3m37s
Continuous integration / Trunk (push) Successful in 8m1s
Continuous integration / Rustfmt (push) Successful in 48s
Continuous integration / build (push) Successful in 3m40s
Continuous integration / Disallow unused dependencies (push) Successful in 2m39s
2025-09-30 13:16:34 +00:00
54fc1e7962 Merge pull request 'chore(deps): lock file maintenance' (#175) from renovate/lock-file-maintenance into master
All checks were successful
Continuous integration / Check (push) Successful in 1m27s
Continuous integration / Test Suite (push) Successful in 2m54s
Continuous integration / Trunk (push) Successful in 1m18s
Continuous integration / Rustfmt (push) Successful in 51s
Continuous integration / build (push) Successful in 2m52s
Continuous integration / Disallow unused dependencies (push) Successful in 2m42s
2025-09-28 21:02:01 -07:00
b187edc23b chore(deps): lock file maintenance
All checks were successful
Continuous integration / Check (push) Successful in 2m4s
Continuous integration / Test Suite (push) Successful in 4m59s
Continuous integration / Trunk (push) Successful in 2m8s
Continuous integration / Rustfmt (push) Successful in 1m1s
Continuous integration / build (push) Successful in 5m7s
Continuous integration / Disallow unused dependencies (push) Successful in 2m32s
2025-09-29 01:33:22 +00:00
fdafba3eeb Merge pull request 'chore(deps): lock file maintenance' (#174) from renovate/lock-file-maintenance into master
All checks were successful
Continuous integration / Check (push) Successful in 1m25s
Continuous integration / Test Suite (push) Successful in 2m48s
Continuous integration / Trunk (push) Successful in 8m45s
Continuous integration / Rustfmt (push) Successful in 48s
Continuous integration / build (push) Successful in 3m1s
Continuous integration / Disallow unused dependencies (push) Successful in 2m32s
2025-09-28 18:32:44 -07:00
c5fe9f67d2 chore(deps): lock file maintenance
All checks were successful
Continuous integration / Check (push) Successful in 2m2s
Continuous integration / Test Suite (push) Successful in 4m21s
Continuous integration / Trunk (push) Successful in 1m58s
Continuous integration / Rustfmt (push) Successful in 41s
Continuous integration / build (push) Successful in 4m47s
Continuous integration / Disallow unused dependencies (push) Successful in 2m19s
2025-09-29 00:02:59 +00:00
ff970acf79 Merge pull request 'chore(deps): update rust crate axum to v0.8.5' (#173) from renovate/axum-monorepo into master
All checks were successful
Continuous integration / Check (push) Successful in 1m18s
Continuous integration / Test Suite (push) Successful in 2m1s
Continuous integration / Trunk (push) Successful in 1m22s
Continuous integration / Rustfmt (push) Successful in 38s
Continuous integration / build (push) Successful in 1m57s
Continuous integration / Disallow unused dependencies (push) Successful in 2m42s
2025-09-28 13:01:36 -07:00
2f9bc17873 chore(deps): update rust crate axum to v0.8.5
All checks were successful
Continuous integration / Check (push) Successful in 2m4s
Continuous integration / Test Suite (push) Successful in 3m30s
Continuous integration / Trunk (push) Successful in 7m57s
Continuous integration / Rustfmt (push) Successful in 54s
Continuous integration / build (push) Successful in 4m12s
Continuous integration / Disallow unused dependencies (push) Successful in 2m36s
2025-09-28 19:31:55 +00:00
7e82f4ce97 Merge pull request 'chore(deps): update rust crate serde to v1.0.228' (#172) from renovate/serde-monorepo into master
All checks were successful
Continuous integration / Check (push) Successful in 1m17s
Continuous integration / Test Suite (push) Successful in 1m47s
Continuous integration / Trunk (push) Successful in 1m28s
Continuous integration / Rustfmt (push) Successful in 1m17s
Continuous integration / build (push) Successful in 2m15s
Continuous integration / Disallow unused dependencies (push) Successful in 2m17s
2025-09-27 11:17:19 -07:00
5bb4f010d3 chore(deps): update rust crate serde to v1.0.228
All checks were successful
Continuous integration / Check (push) Successful in 1m39s
Continuous integration / Test Suite (push) Successful in 3m33s
Continuous integration / Trunk (push) Successful in 8m35s
Continuous integration / Rustfmt (push) Successful in 41s
Continuous integration / build (push) Successful in 3m40s
Continuous integration / Disallow unused dependencies (push) Successful in 2m32s
2025-09-27 17:32:25 +00:00
0af630acbe Merge pull request 'chore(deps): update rust crate serde to v1.0.227' (#171) from renovate/serde-monorepo into master
All checks were successful
Continuous integration / Check (push) Successful in 1m2s
Continuous integration / Test Suite (push) Successful in 1m30s
Continuous integration / Trunk (push) Successful in 1m22s
Continuous integration / Rustfmt (push) Successful in 1m9s
Continuous integration / build (push) Successful in 1m55s
Continuous integration / Disallow unused dependencies (push) Successful in 2m41s
2025-09-25 17:48:24 -07:00
d3d350e159 chore(deps): update rust crate serde to v1.0.227
All checks were successful
Continuous integration / Check (push) Successful in 1m45s
Continuous integration / Test Suite (push) Successful in 3m17s
Continuous integration / Trunk (push) Successful in 1m11s
Continuous integration / Rustfmt (push) Successful in 51s
Continuous integration / build (push) Successful in 3m31s
Continuous integration / Disallow unused dependencies (push) Successful in 3m35s
2025-09-26 00:17:20 +00:00
4013e4a7bf Merge pull request 'chore(deps): lock file maintenance' (#170) from renovate/lock-file-maintenance into master
All checks were successful
Continuous integration / Check (push) Successful in 1m24s
Continuous integration / Test Suite (push) Successful in 1m56s
Continuous integration / Trunk (push) Successful in 1m47s
Continuous integration / Rustfmt (push) Successful in 54s
Continuous integration / build (push) Successful in 2m47s
Continuous integration / Disallow unused dependencies (push) Successful in 2m48s
2025-09-21 19:47:12 -07:00
b63171ea98 chore(deps): lock file maintenance
All checks were successful
Continuous integration / Check (push) Successful in 1m26s
Continuous integration / Test Suite (push) Successful in 2m9s
Continuous integration / Trunk (push) Successful in 1m56s
Continuous integration / Rustfmt (push) Successful in 1m21s
Continuous integration / build (push) Successful in 2m8s
Continuous integration / Disallow unused dependencies (push) Successful in 2m38s
2025-09-22 01:03:58 +00:00
1c6ef02d11 Merge pull request 'chore(deps): lock file maintenance' (#169) from renovate/lock-file-maintenance into master
All checks were successful
Continuous integration / Check (push) Successful in 1m48s
Continuous integration / Test Suite (push) Successful in 2m35s
Continuous integration / Trunk (push) Successful in 8m22s
Continuous integration / Rustfmt (push) Successful in 53s
Continuous integration / build (push) Successful in 2m4s
Continuous integration / Disallow unused dependencies (push) Successful in 2m43s
2025-09-21 18:03:09 -07:00
32e5837dbf chore(deps): lock file maintenance
All checks were successful
Continuous integration / Check (push) Successful in 2m10s
Continuous integration / Test Suite (push) Successful in 4m11s
Continuous integration / Trunk (push) Successful in 8m17s
Continuous integration / Rustfmt (push) Successful in 58s
Continuous integration / build (push) Successful in 4m10s
Continuous integration / Disallow unused dependencies (push) Successful in 2m49s
2025-09-22 00:02:29 +00:00
38234d4d18 Merge pull request 'chore(deps): update rust crate serde to v1.0.226' (#168) from renovate/serde-monorepo into master
All checks were successful
Continuous integration / Check (push) Successful in 1m3s
Continuous integration / Test Suite (push) Successful in 1m31s
Continuous integration / Trunk (push) Successful in 1m10s
Continuous integration / Rustfmt (push) Successful in 48s
Continuous integration / build (push) Successful in 1m56s
Continuous integration / Disallow unused dependencies (push) Successful in 2m41s
2025-09-20 17:17:26 -07:00
f609a3c122 chore(deps): update rust crate serde to v1.0.226
All checks were successful
Continuous integration / Check (push) Successful in 1m39s
Continuous integration / Test Suite (push) Successful in 3m41s
Continuous integration / Trunk (push) Successful in 8m13s
Continuous integration / Rustfmt (push) Successful in 48s
Continuous integration / build (push) Successful in 3m29s
Continuous integration / Disallow unused dependencies (push) Successful in 2m28s
2025-09-20 23:47:09 +00:00
440a630414 Merge pull request 'chore(deps): update rust crate serde to v1.0.225' (#167) from renovate/serde-monorepo into master
All checks were successful
Continuous integration / Check (push) Successful in 1m13s
Continuous integration / Test Suite (push) Successful in 1m27s
Continuous integration / Trunk (push) Successful in 1m11s
Continuous integration / Rustfmt (push) Successful in 1m9s
Continuous integration / build (push) Successful in 2m8s
Continuous integration / Disallow unused dependencies (push) Successful in 3m0s
2025-09-15 21:46:51 -07:00
ebda258750 chore(deps): update rust crate serde to v1.0.225
All checks were successful
Continuous integration / Check (push) Successful in 1m53s
Continuous integration / Test Suite (push) Successful in 4m12s
Continuous integration / Trunk (push) Successful in 8m40s
Continuous integration / Rustfmt (push) Successful in 50s
Continuous integration / build (push) Successful in 4m40s
Continuous integration / Disallow unused dependencies (push) Successful in 2m38s
2025-09-16 04:02:50 +00:00
f766b3d529 Merge pull request 'chore(deps): update rust crate serde to v1.0.224' (#166) from renovate/serde-monorepo into master
All checks were successful
Continuous integration / Check (push) Successful in 1m23s
Continuous integration / Test Suite (push) Successful in 1m26s
Continuous integration / Trunk (push) Successful in 1m13s
Continuous integration / Rustfmt (push) Successful in 1m0s
Continuous integration / build (push) Successful in 2m14s
Continuous integration / Disallow unused dependencies (push) Successful in 2m40s
2025-09-15 10:17:33 -07:00
96d927d416 chore(deps): update rust crate serde to v1.0.224
All checks were successful
Continuous integration / Check (push) Successful in 2m51s
Continuous integration / Test Suite (push) Successful in 4m43s
Continuous integration / Trunk (push) Successful in 8m0s
Continuous integration / Rustfmt (push) Successful in 44s
Continuous integration / build (push) Successful in 4m47s
Continuous integration / Disallow unused dependencies (push) Successful in 2m25s
2025-09-15 16:17:25 +00:00
60543b7e5d Merge pull request 'chore(deps): lock file maintenance' (#165) from renovate/lock-file-maintenance into master
All checks were successful
Continuous integration / Check (push) Successful in 2m6s
Continuous integration / Test Suite (push) Successful in 4m26s
Continuous integration / Trunk (push) Successful in 7m26s
Continuous integration / Rustfmt (push) Successful in 33s
Continuous integration / build (push) Successful in 4m19s
Continuous integration / Disallow unused dependencies (push) Successful in 2m22s
2025-09-14 19:16:51 -07:00
97a7bb6083 chore(deps): lock file maintenance
All checks were successful
Continuous integration / Check (push) Successful in 2m6s
Continuous integration / Test Suite (push) Successful in 4m15s
Continuous integration / Trunk (push) Successful in 7m8s
Continuous integration / Rustfmt (push) Successful in 33s
Continuous integration / build (push) Successful in 4m52s
Continuous integration / Disallow unused dependencies (push) Successful in 2m7s
2025-09-15 00:02:42 +00:00
c493857188 web: remove wasm-bindgen-test
All checks were successful
Continuous integration / Check (push) Successful in 1m55s
Continuous integration / Test Suite (push) Successful in 4m4s
Continuous integration / Trunk (push) Successful in 7m18s
Continuous integration / Rustfmt (push) Successful in 28s
Continuous integration / build (push) Successful in 4m26s
Continuous integration / Disallow unused dependencies (push) Successful in 2m2s
2025-09-14 15:25:15 -07:00
21f344b01c Merge pull request 'chore(deps): update rust crate serde to v1.0.223' (#164) from renovate/serde-monorepo into master
Some checks failed
Continuous integration / Check (push) Has been cancelled
Continuous integration / Test Suite (push) Has been cancelled
Continuous integration / Trunk (push) Has been cancelled
Continuous integration / Rustfmt (push) Has been cancelled
Continuous integration / build (push) Has been cancelled
Continuous integration / Disallow unused dependencies (push) Has been cancelled
2025-09-14 15:16:51 -07:00
78f6d87c03 chore(deps): update rust crate serde to v1.0.223
All checks were successful
Continuous integration / Check (push) Successful in 2m2s
Continuous integration / Test Suite (push) Successful in 4m11s
Continuous integration / Trunk (push) Successful in 7m26s
Continuous integration / Rustfmt (push) Successful in 33s
Continuous integration / build (push) Successful in 4m21s
Continuous integration / Disallow unused dependencies (push) Successful in 2m5s
2025-09-14 20:47:14 +00:00
6edad4e8f2 Merge branch 'renovate/zip-5.x'
All checks were successful
Continuous integration / Check (push) Successful in 2m2s
Continuous integration / Test Suite (push) Successful in 4m4s
Continuous integration / Trunk (push) Successful in 7m30s
Continuous integration / Rustfmt (push) Successful in 33s
Continuous integration / build (push) Successful in 4m21s
Continuous integration / Disallow unused dependencies (push) Successful in 2m2s
2025-09-14 11:47:08 -07:00
8b06950cb8 Merge branch 'renovate/all-minor-patch'
Some checks failed
Continuous integration / Check (push) Has been cancelled
Continuous integration / Test Suite (push) Has been cancelled
Continuous integration / Trunk (push) Has been cancelled
Continuous integration / Rustfmt (push) Has been cancelled
Continuous integration / build (push) Has been cancelled
Continuous integration / Disallow unused dependencies (push) Has been cancelled
2025-09-14 11:44:24 -07:00
34417131b0 chore(deps): update all non-major dependencies
Some checks failed
renovate/artifacts Artifact file update failure
Continuous integration / Check (push) Has been cancelled
Continuous integration / Test Suite (push) Has been cancelled
Continuous integration / Trunk (push) Has been cancelled
Continuous integration / Rustfmt (push) Has been cancelled
Continuous integration / build (push) Has been cancelled
Continuous integration / Disallow unused dependencies (push) Has been cancelled
2025-09-14 18:17:23 +00:00
d63e72ad35 chore: Release
All checks were successful
Continuous integration / Check (push) Successful in 2m14s
Continuous integration / Test Suite (push) Successful in 4m25s
Continuous integration / Trunk (push) Successful in 7m10s
Continuous integration / Rustfmt (push) Successful in 28s
Continuous integration / build (push) Successful in 4m42s
Continuous integration / Disallow unused dependencies (push) Successful in 2m12s
2025-09-14 11:00:40 -07:00
33c0a106b7 server: fix date parsing w/ TZ and cal widget highlight 2025-09-14 11:00:21 -07:00
030d1c2ebe chore: Release
Some checks failed
Continuous integration / Check (push) Successful in 2m10s
Continuous integration / Test Suite (push) Failing after 4m4s
Continuous integration / Trunk (push) Successful in 7m13s
Continuous integration / Rustfmt (push) Successful in 28s
Continuous integration / build (push) Successful in 4m36s
Continuous integration / Disallow unused dependencies (push) Successful in 2m8s
2025-09-11 17:41:08 -07:00
e386d7e74e server: address lint
Some checks failed
Continuous integration / Check (push) Has been cancelled
Continuous integration / Test Suite (push) Has been cancelled
Continuous integration / Trunk (push) Has been cancelled
Continuous integration / Rustfmt (push) Has been cancelled
Continuous integration / build (push) Has been cancelled
Continuous integration / Disallow unused dependencies (push) Has been cancelled
2025-09-11 17:40:53 -07:00
55e38e96a0 chore: Release 2025-09-11 17:39:28 -07:00
7f47fe8de6 web: test plain text jammed in HTML more like plain-text 2025-09-11 17:39:04 -07:00
3889b855a5 server: fix date parsing w/ TZ and cal widget highlight 2025-09-11 16:31:43 -07:00
2c1c7abf0a Generated AI helper 2025-09-11 16:13:02 -07:00
9452a2b014 renovate: disable updating wasm-bindgen
Some checks failed
Continuous integration / Check (push) Successful in 2m6s
Continuous integration / Test Suite (push) Successful in 4m38s
Continuous integration / Rustfmt (push) Has been cancelled
Continuous integration / build (push) Has been cancelled
Continuous integration / Disallow unused dependencies (push) Has been cancelled
Continuous integration / Trunk (push) Has been cancelled
2025-09-11 15:45:21 -07:00
0df97a7b76 fix(deps): update rust crate zip to v5
All checks were successful
Continuous integration / Check (push) Successful in 1m19s
Continuous integration / Test Suite (push) Successful in 1m42s
Continuous integration / Trunk (push) Successful in 7m12s
Continuous integration / Rustfmt (push) Successful in 39s
Continuous integration / build (push) Successful in 2m2s
Continuous integration / Disallow unused dependencies (push) Successful in 2m20s
2025-09-05 21:16:29 +00:00
33 changed files with 3995 additions and 1496 deletions

View File

@@ -7,7 +7,7 @@ jobs:
name: Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
- run: cargo check
@@ -15,7 +15,7 @@ jobs:
name: Test Suite
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
- run: cargo test
@@ -23,7 +23,7 @@ jobs:
name: Trunk
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: nightly
@@ -35,7 +35,7 @@ jobs:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
components: rustfmt
@@ -46,7 +46,7 @@ jobs:
name: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
- run: cargo build
@@ -54,7 +54,7 @@ jobs:
name: Disallow unused dependencies
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: nightly

40
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,40 @@
# Copilot/AI Agent Instructions for Letterbox
## Project Overview
- **Letterbox** is a Rust monorepo for a mail/newsreader system with a web frontend and a Rocket/GraphQL backend.
- Major crates: `server` (backend, Rocket+async-graphql), `web` (Seed-based WASM frontend), `notmuch` (mail integration), `shared` (common types), `procmail2notmuch` (migration/utility).
- Data flows: Email/news data is indexed and queried via the backend, exposed to the frontend via GraphQL. SQLx/Postgres is used for persistence. Notmuch and custom SQL are both used for mail storage/search.
## Key Workflows
- **Development**: Use `dev.sh` to launch a tmux session with live-reloading for both frontend (`trunk serve`) and backend (`cargo watch ... run`).
- **Build/Release**: Use `just patch|minor|major` for versioned releases (runs SQLx prepare, bumps versions, pushes). `Makefile`'s `release` target does similar steps.
- **Frontend**: In `web/`, use `cargo make serve` and `cargo make watch` for local dev. See `web/README.md` for Seed-specific details.
- **Backend**: In `server/`, run with `cargo run` or via the tmux/dev.sh workflow. SQL migrations are in `server/migrations/`.
## Project Conventions & Patterns
- **GraphQL**: All API boundaries are defined in `server/src/graphql.rs`. Use the `Query`, `Mutation`, and `Subscription` roots. Types are defined with `async-graphql` derive macros.
- **HTML Sanitization**: See `server/src/lib.rs` for custom HTML/CSS sanitization and transformation logic (e.g., `Transformer` trait, `sanitize_html`).
- **Tag/Query Parsing**: The `Query` struct in `server/src/lib.rs` parses user queries into filters for notmuch/newsreader/tantivy.
- **Shared Types**: Use the `shared` crate for types and helpers shared between frontend and backend.
- **Custom SQL**: Raw SQL queries are in `server/sql/`. Use these for complex queries not handled by SQLx macros.
- **Feature Flags**: The `tantivy` feature enables full-text search via Tantivy. Check for `#[cfg(feature = "tantivy")]` in backend code.
## Integration Points
- **Notmuch**: Integrated via the `notmuch` crate for mail indexing/search.
- **Postgres**: Used for newsreader and other persistent data (see `server/migrations/`).
- **GraphQL**: All client-server communication is via GraphQL endpoints defined in the backend.
- **Seed/Trunk**: Frontend is built with Seed (Rust/WASM) and served via Trunk.
## Examples
- To add a new GraphQL query, update `server/src/graphql.rs` and expose it in the `QueryRoot`.
- To add a new frontend page, add a module in `web/src/` and register it in the Seed app's router.
- To run the full dev environment: `./dev.sh` (requires tmux, trunk, cargo-watch, etc.).
## References
- See `web/README.md` for frontend/Seed workflow details.
- See `Justfile` and `Makefile` for release/versioning automation.
- See `server/src/lib.rs` and `server/src/graphql.rs` for backend architecture and conventions.
- See `server/sql/` for custom SQL queries.
---
If any conventions or workflows are unclear, please ask for clarification or check the referenced files for examples.

2693
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ authors = ["Bill Thiede <git@xinu.tv>"]
edition = "2021"
license = "UNLICENSED"
publish = ["xinu"]
version = "0.17.41"
version = "0.17.60"
repository = "https://git.z.xinu.tv/wathiede/letterbox"
[profile.dev]

View File

@@ -13,8 +13,8 @@ version.workspace = true
[dependencies]
anyhow = "1.0.98"
clap = { version = "4.5.37", features = ["derive", "env"] }
letterbox-notmuch = { version = "0.17.9", registry = "xinu" }
letterbox-shared = { version = "0.17.9", registry = "xinu" }
letterbox-notmuch = { version = "0.17", registry = "xinu" }
letterbox-shared = { version = "0.17", registry = "xinu" }
serde = { version = "1.0.219", features = ["derive"] }
sqlx = { version = "0.8.5", features = ["postgres", "runtime-tokio"] }
tokio = { version = "1.44.2", features = ["rt", "macros", "rt-multi-thread"] }

View File

@@ -3,4 +3,11 @@
"extends": [
"config:recommended"
]
,
"packageRules": [
{
"matchPackageNames": ["wasm-bindgen"],
"enabled": false
}
]
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM snooze WHERE id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4"
]
},
"nullable": []
},
"hash": "77f79f981a9736d18ffd4b87d3aec34d6a048162154a3aba833370c58a860795"
}

View File

@@ -0,0 +1,26 @@
{
"db_name": "PostgreSQL",
"query": "\nSELECT id, message_id\nFROM snooze\nWHERE wake < NOW();\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "message_id",
"type_info": "Text"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false
]
},
"hash": "c8383663124a5cc5912b54553f18f7064d33087ebfdf3c0c1c43cbe6d3577084"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n p.id,\n link,\n clean_summary\nFROM\n post AS p\nINNER JOIN feed AS f ON p.site = f.slug -- necessary to weed out nzb posts\nWHERE\n search_summary IS NULL\n -- TODO remove AND link ~ '^<'\nORDER BY\n ROW_NUMBER() OVER (PARTITION BY site ORDER BY date DESC)\nLIMIT 100;\n",
"query": "SELECT\n p.id,\n link,\n clean_summary\nFROM\n post AS p\nINNER JOIN feed AS f ON p.site = f.slug -- necessary to weed out nzb posts\nWHERE\n search_summary IS NULL\n -- TODO remove AND link ~ '^<'\nORDER BY\n ROW_NUMBER() OVER (PARTITION BY site ORDER BY date DESC)\nLIMIT 1000;\n",
"describe": {
"columns": [
{
@@ -28,5 +28,5 @@
true
]
},
"hash": "3d271b404f06497a5dcde68cf6bf07291d70fa56058ea736ac24e91d33050c04"
"hash": "cf369e3d5547f400cb54004dd03783ef6998a000aec91c50a79405dcf1c53b17"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO snooze (message_id, wake)\n VALUES ($1, $2)\n ON CONFLICT (message_id) DO UPDATE\n SET wake = $2\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Timestamptz"
]
},
"nullable": []
},
"hash": "effd0d0d91e6ad84546f7177f1fd39d4fad736b471eb5e55fd5ac74f7adff664"
}

View File

@@ -13,39 +13,39 @@ version.workspace = true
[dependencies]
chrono-tz = "0.10"
html2text = "0.15"
html2text = "0.16"
ammonia = "4.1.0"
anyhow = "1.0.98"
askama = { version = "0.14.0", features = ["derive"] }
async-graphql = { version = "7", features = ["log"] }
askama = { version = "0.15.0", features = ["derive"] }
async-graphql = { version = "7", features = ["log", "chrono"] }
async-graphql-axum = "7.0.16"
async-trait = "0.1.88"
axum = { version = "0.8.3", features = ["ws"] }
axum-macros = "0.5.0"
build-info = "0.0.41"
build-info = "0.0.42"
cacher = { version = "0.2.0", registry = "xinu" }
chrono = "0.4.40"
clap = { version = "4.5.37", features = ["derive"] }
css-inline = "0.17.0"
css-inline = "0.19.0"
flate2 = "1.1.2"
futures = "0.3.31"
headers = "0.4.0"
html-escape = "0.2.13"
ical = "0.11"
letterbox-notmuch = { path = "../notmuch", version = "0.17.41", registry = "xinu" }
letterbox-shared = { path = "../shared", version = "0.17.41", registry = "xinu" }
letterbox-notmuch = { path = "../notmuch", version = "0.17", registry = "xinu" }
letterbox-shared = { path = "../shared", version = "0.17", registry = "xinu" }
linkify = "0.10.0"
lol_html = "2.3.0"
mailparse = "0.16.1"
maplit = "1.0.2"
memmap = "0.7.0"
quick-xml = { version = "0.38.1", features = ["serialize"] }
quick-xml = { version = "0.39.0", features = ["serialize"] }
regex = "1.11.1"
reqwest = { version = "0.12.15", features = ["blocking"] }
scraper = "0.24.0"
reqwest = { version = "0.13.0", features = ["blocking"] }
scraper = "0.25.0"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
sqlx = { version = "0.8.5", features = ["postgres", "runtime-tokio", "time"] }
sqlx = { version = "0.8.5", features = ["postgres", "runtime-tokio", "chrono"] }
tantivy = { version = "0.25.0", optional = true }
thiserror = "2.0.12"
tokio = "1.44.2"
@@ -56,11 +56,11 @@ urlencoding = "2.1.3"
#xtracing = { git = "http://git-private.h.xinu.tv/wathiede/xtracing.git" }
#xtracing = { path = "../../xtracing" }
xtracing = { version = "0.3.2", registry = "xinu" }
zip = "4.3.0"
zip = "7.0.0"
[build-dependencies]
build-info-build = "0.0.41"
build-info-build = "0.0.42"
[features]
#default = [ "tantivy" ]

View File

@@ -0,0 +1,2 @@
-- Add down migration script here
DROP TABLE IF EXISTS snooze;

View File

@@ -0,0 +1,6 @@
-- Add up migration script here
CREATE TABLE IF NOT EXISTS snooze (
id integer NOT NULL GENERATED ALWAYS AS IDENTITY,
message_id text NOT NULL UNIQUE,
wake timestamptz NOT NULL
);

View File

@@ -0,0 +1,2 @@
DROP INDEX IF EXISTS movie_sets_year_id_idx;
DROP INDEX IF EXISTS movie_sets_year_idx;

View File

@@ -0,0 +1,6 @@
-- Add index on movie_sets.year to speed up year-based queries
CREATE INDEX movie_sets_year_idx ON movie_sets(year);
-- Composite index for queries that filter by year and return id
-- This can make the subquery in UPDATE statements even faster
CREATE INDEX movie_sets_year_id_idx ON movie_sets(year, id);

View File

@@ -10,4 +10,4 @@ WHERE
-- TODO remove AND link ~ '^<'
ORDER BY
ROW_NUMBER() OVER (PARTITION BY site ORDER BY date DESC)
LIMIT 100;
LIMIT 1000;

View File

@@ -1,7 +1,7 @@
use std::io::{Cursor, Read};
use askama::Template;
use chrono::{Datelike, Local, LocalResult, TimeZone, Utc};
use chrono::{Datelike, LocalResult, TimeZone, Utc};
use chrono_tz::Tz;
use mailparse::{parse_content_type, parse_mail, MailHeader, MailHeaderMap, ParsedMail};
use quick_xml::de::from_str as xml_from_str;
@@ -17,9 +17,11 @@ use crate::{
const APPLICATION_GZIP: &'static str = "application/gzip";
const APPLICATION_ZIP: &'static str = "application/zip";
const APPLICATION_TLSRPT_GZIP: &'static str = "application/tlsrpt+gzip";
const IMAGE_JPEG: &'static str = "image/jpeg";
const IMAGE_PJPEG: &'static str = "image/pjpeg";
const IMAGE_PNG: &'static str = "image/png";
const MESSAGE_DELIVERY_STATUS: &'static str = "message/delivery-status";
const MESSAGE_RFC822: &'static str = "message/rfc822";
const MULTIPART_ALTERNATIVE: &'static str = "multipart/alternative";
const MULTIPART_MIXED: &'static str = "multipart/mixed";
@@ -32,6 +34,7 @@ const TEXT_PLAIN: &'static str = "text/plain";
// Inline Askama filters module for template use
mod filters {
// Usage: {{ items|batch(7) }}
#[askama::filter_fn]
pub fn batch<T: Clone>(
items: &[T],
_: &dyn ::askama::Values,
@@ -146,7 +149,7 @@ pub fn extract_calendar_metadata_from_mail(
// Fallback extraction: if iCal did not provide metadata, extract from subject/body before generating fallback HTML
if body_html.is_none() {
// Try to extract summary from subject (e.g., "New event: <summary> @ ...")
// Try to extract summary from subject (e.g., "New event: <summary> @ ..." or "Updated invitation: <summary> @ ...")
if summary.is_none() {
if let Some(subject) = m.headers.get_first_value("Subject") {
if let Some(caps) = regex::Regex::new(r"New event: ([^@]+) @")
@@ -159,14 +162,19 @@ pub fn extract_calendar_metadata_from_mail(
.and_then(|re| re.captures(&subject))
{
summary = Some(caps[1].trim().to_string());
} else if let Some(caps) = regex::Regex::new(r"Updated invitation: ([^@]+) @")
.ok()
.and_then(|re| re.captures(&subject))
{
summary = Some(caps[1].trim().to_string());
}
}
}
// Try to extract start/end dates from subject
if start_date.is_none() || end_date.is_none() {
if let Some(subject) = m.headers.get_first_value("Subject") {
// Pattern: New event: Dentist appt @ Tue Sep 23, 2025 3pm - 4pm (PDT) (tconvertino@gmail.com)
if let Some(caps) = regex::Regex::new(r"New event: [^@]+@ ([A-Za-z]{3}) ([A-Za-z]{3}) (\d{1,2}), (\d{4}) (\d{1,2})(?::(\d{2}))? ?([ap]m) ?- ?(\d{1,2})(?::(\d{2}))? ?([ap]m)").ok().and_then(|re| re.captures(&subject)) {
// Pattern: @ Tue Sep 23, 2025 3pm - 4pm (works for New event, Invitation, Updated invitation, etc.)
if let Some(caps) = regex::Regex::new(r"@ ([A-Za-z]{3}) ([A-Za-z]{3}) (\d{1,2}), (\d{4}) (\d{1,2})(?::(\d{2}))? ?([ap]m) ?- ?(\d{1,2})(?::(\d{2}))? ?([ap]m)").ok().and_then(|re| re.captures(&subject)) {
let month = &caps[2];
let day = &caps[3];
let year = &caps[4];
@@ -193,8 +201,16 @@ pub fn extract_calendar_metadata_from_mail(
}
}
if let (Some(sm), Some(em)) = (month_num(start_month), month_num(end_month)) {
let current_year = chrono::Local::now().year().to_string();
let start = format!("{}{}{}", current_year, sm, format!("{:0>2}", start_day));
// If start month is later in calendar year than end month, start is in previous year
let sm_num: u32 = sm.parse().unwrap_or(1);
let em_num: u32 = em.parse().unwrap_or(1);
let end_year = year.parse::<i32>().unwrap_or_else(|_| chrono::Local::now().year());
let start_year: i32 = if sm_num > em_num {
end_year - 1
} else {
end_year
};
let start = format!("{}{}{}", start_year, sm, format!("{:0>2}", start_day));
let mut end_date_val = chrono::NaiveDate::parse_from_str(&format!("{}-{}-{}", year, em, format!("{:0>2}", end_day)), "%Y-%m-%d").ok();
if let Some(d) = end_date_val.as_mut() {
*d = d.succ_opt().unwrap_or(*d);
@@ -230,6 +246,31 @@ pub fn extract_calendar_metadata_from_mail(
if end_date.is_none() { end_date = Some(end); }
}
}
// Pattern: single all-day event: @ Sun Jan 18, 2026 (no time range)
if start_date.is_none() {
if let Some(caps) = regex::Regex::new(r"@ [A-Za-z]{3} ([A-Za-z]{3}) (\d{1,2}), (\d{4})(?:\s*\(|$)").ok().and_then(|re| re.captures(&subject)) {
let month = &caps[1];
let day = &caps[2];
let year = &caps[3];
fn month_num(mon: &str) -> Option<&'static str> {
match mon {
"Jan" => Some("01"), "Feb" => Some("02"), "Mar" => Some("03"), "Apr" => Some("04"),
"May" => Some("05"), "Jun" => Some("06"), "Jul" => Some("07"), "Aug" => Some("08"),
"Sep" => Some("09"), "Oct" => Some("10"), "Nov" => Some("11"), "Dec" => Some("12"),
_ => None
}
}
if let Some(mm) = month_num(month) {
let start = format!("{}{}{:0>2}", year, mm, day);
// For all-day events, end date is the next day (exclusive)
if let Ok(d) = chrono::NaiveDate::parse_from_str(&format!("{}-{}-{:0>2}", year, mm, day), "%Y-%m-%d") {
let end = d.succ_opt().unwrap_or(d).format("%Y%m%d").to_string();
start_date = Some(start);
end_date = Some(end);
}
}
}
}
}
}
}
@@ -317,6 +358,8 @@ pub fn extract_calendar_metadata_from_mail(
let needs_ical_flex =
summary.is_some() || start_date.is_some() || end_date.is_some() || has_recurrence;
if needs_ical_flex {
use chrono::{Datelike, NaiveDate};
let summary_val = summary.clone().unwrap_or_default();
let organizer_val = organizer.clone().unwrap_or_default();
let start_val = start_date.clone().unwrap_or_default();
@@ -326,15 +369,101 @@ pub fn extract_calendar_metadata_from_mail(
} else {
String::new()
};
// Compute event_days and all_days for calendar grid rendering
let mut event_days: Vec<NaiveDate> = Vec::new();
let (local_fmt_start, local_fmt_end) = if let (Some(ref start_str), Some(ref end_str)) =
(&start_date, &end_date)
{
// Parse YYYYMMDD format dates
let start_d = NaiveDate::parse_from_str(start_str, "%Y%m%d").ok();
let end_d = NaiveDate::parse_from_str(end_str, "%Y%m%d").ok();
if let (Some(start), Some(end)) = (start_d, end_d) {
// For all-day events, end date is exclusive, so we need to subtract one day
let display_end = if end > start {
end.pred_opt().unwrap_or(end)
} else {
end
};
// Add all days from start to display_end (inclusive) to event_days
let mut day_iter = start;
while day_iter <= display_end {
event_days.push(day_iter);
day_iter = day_iter.succ_opt().unwrap_or(day_iter);
if day_iter == display_end && day_iter == start {
// Single day event
break;
}
}
// Format dates for display
let fmt_start = start.format("%a %b %e, %Y").to_string();
let fmt_end = display_end.format("%a %b %e, %Y").to_string();
(fmt_start, fmt_end)
} else {
(start_val.clone(), end_val.clone())
}
} else {
(start_val.clone(), end_val.clone())
};
// Compute calendar grid (all_days) from event_days
let (all_days, caption) = if !event_days.is_empty() {
let first_event = event_days.first().unwrap();
let last_event = event_days.last().unwrap();
let first_of_month =
NaiveDate::from_ymd_opt(first_event.year(), first_event.month(), 1).unwrap();
let last_of_month = {
let next_month = if last_event.month() == 12 {
NaiveDate::from_ymd_opt(last_event.year() + 1, 1, 1).unwrap()
} else {
NaiveDate::from_ymd_opt(last_event.year(), last_event.month() + 1, 1).unwrap()
};
next_month.pred_opt().unwrap()
};
// Start from Sunday of the week containing first_of_month
let mut cal_start = first_of_month;
while cal_start.weekday() != chrono::Weekday::Sun {
cal_start = cal_start.pred_opt().unwrap();
}
// End on Saturday of the week containing last_of_month
let mut cal_end = last_of_month;
while cal_end.weekday() != chrono::Weekday::Sat {
cal_end = cal_end.succ_opt().unwrap();
}
let mut all_days = vec![];
let mut d = cal_start;
while d <= cal_end {
all_days.push(d);
d = d.succ_opt().unwrap();
}
let start_month = first_event.format("%B %Y");
let end_month = last_event.format("%B %Y");
let caption = if start_month.to_string() == end_month.to_string() {
start_month.to_string()
} else {
format!("{} {}", start_month, end_month)
};
(all_days, caption)
} else {
(vec![], String::new())
};
let template = IcalSummaryTemplate {
summary: &summary_val,
local_fmt_start: &start_val,
local_fmt_end: &end_val,
local_fmt_start: &local_fmt_start,
local_fmt_end: &local_fmt_end,
organizer: &organizer_val,
organizer_cn: "",
all_days: vec![],
event_days: vec![],
caption: String::new(),
all_days,
event_days,
caption,
description_paragraphs: &[],
today: Some(chrono::Local::now().date_naive()),
recurrence_display,
@@ -641,115 +770,186 @@ pub fn extract_gzip(m: &ParsedMail) -> Result<(Body, Option<String>), ServerErro
Ok((extract_unhandled(m)?, None))
}
pub fn extract_report(m: &ParsedMail, _part_addr: &mut Vec<String>) -> Result<Body, ServerError> {
let mut html_part = None;
let mut tlsrpt_part = None;
pub fn extract_report(m: &ParsedMail, part_addr: &mut Vec<String>) -> Result<Body, ServerError> {
let mut parts = Vec::new();
for (idx, sp) in m.subparts.iter().enumerate() {
part_addr.push(idx.to_string());
for sp in &m.subparts {
match sp.ctype.mimetype.as_str() {
TEXT_HTML => html_part = Some(sp.get_body()?),
"application/tlsrpt+gzip" => tlsrpt_part = Some(sp.get_body_raw()?),
_ => {} // Ignore other parts for now
}
}
let tlsrpt_summary_html = if let Some(gz_bytes) = tlsrpt_part {
let mut decoder = flate2::read::GzDecoder::new(&gz_bytes[..]);
let mut buffer = Vec::new();
if decoder.read_to_end(&mut buffer).is_ok() {
if let Ok(json_str) = String::from_utf8(buffer) {
match serde_json::from_str::<TlsRpt>(&json_str) {
Ok(tlsrpt) => {
let formatted_tlsrpt = FormattedTlsRpt {
organization_name: tlsrpt.organization_name,
date_range: FormattedTlsRptDateRange {
start_datetime: tlsrpt.date_range.start_datetime,
end_datetime: tlsrpt.date_range.end_datetime,
},
contact_info: tlsrpt.contact_info.unwrap_or_else(|| "".to_string()),
report_id: tlsrpt.report_id,
policies: tlsrpt
.policies
.into_iter()
.map(|policy| FormattedTlsRptPolicy {
policy: FormattedTlsRptPolicyDetails {
policy_type: policy.policy.policy_type,
policy_string: policy.policy.policy_string,
policy_domain: policy.policy.policy_domain,
mx_host: policy
.policy
.mx_host
.unwrap_or_else(|| Vec::new())
.into_iter()
.map(|mx| match mx {
MxHost::String(s) => FormattedTlsRptMxHost {
hostname: s,
failure_count: 0,
result_type: "".to_string(),
},
MxHost::Object(o) => FormattedTlsRptMxHost {
hostname: o.hostname,
failure_count: o.failure_count,
result_type: o.result_type,
},
})
.collect(),
APPLICATION_TLSRPT_GZIP => {
let gz_bytes = sp.get_body_raw()?;
let mut decoder = flate2::read::GzDecoder::new(&gz_bytes[..]);
let mut buffer = Vec::new();
if decoder.read_to_end(&mut buffer).is_ok() {
if let Ok(json_str) = String::from_utf8(buffer) {
match serde_json::from_str::<TlsRpt>(&json_str) {
Ok(tlsrpt) => {
let formatted_tlsrpt = FormattedTlsRpt {
organization_name: tlsrpt.organization_name,
date_range: FormattedTlsRptDateRange {
start_datetime: tlsrpt.date_range.start_datetime,
end_datetime: tlsrpt.date_range.end_datetime,
},
summary: policy.summary,
failure_details: policy
.failure_details
.unwrap_or_else(|| Vec::new())
contact_info: tlsrpt
.contact_info
.unwrap_or_else(|| "".to_string()),
report_id: tlsrpt.report_id,
policies: tlsrpt
.policies
.into_iter()
.map(|detail| FormattedTlsRptFailureDetails {
result_type: detail.result_type,
sending_mta_ip: detail
.sending_mta_ip
.unwrap_or_else(|| "".to_string()),
receiving_ip: detail
.receiving_ip
.unwrap_or_else(|| "".to_string()),
receiving_mx_hostname: detail
.receiving_mx_hostname
.unwrap_or_else(|| "".to_string()),
failed_session_count: detail.failed_session_count,
additional_info: detail
.additional_info
.unwrap_or_else(|| "".to_string()),
failure_reason_code: detail
.failure_reason_code
.unwrap_or_else(|| "".to_string()),
.map(|policy| FormattedTlsRptPolicy {
policy: FormattedTlsRptPolicyDetails {
policy_type: policy.policy.policy_type,
policy_string: policy.policy.policy_string,
policy_domain: policy.policy.policy_domain,
mx_host: policy
.policy
.mx_host
.unwrap_or_else(|| Vec::new())
.into_iter()
.map(|mx| match mx {
MxHost::String(s) => {
FormattedTlsRptMxHost {
hostname: s,
failure_count: 0,
result_type: "".to_string(),
}
}
MxHost::Object(o) => {
FormattedTlsRptMxHost {
hostname: o.hostname,
failure_count: o.failure_count,
result_type: o.result_type,
}
}
})
.collect(),
},
summary: policy.summary,
failure_details: policy
.failure_details
.unwrap_or_else(|| Vec::new())
.into_iter()
.map(|detail| FormattedTlsRptFailureDetails {
result_type: detail.result_type,
sending_mta_ip: detail
.sending_mta_ip
.unwrap_or_else(|| "".to_string()),
receiving_ip: detail
.receiving_ip
.unwrap_or_else(|| "".to_string()),
receiving_mx_hostname: detail
.receiving_mx_hostname
.unwrap_or_else(|| "".to_string()),
failed_session_count: detail
.failed_session_count,
additional_info: detail
.additional_info
.unwrap_or_else(|| "".to_string()),
failure_reason_code: detail
.failure_reason_code
.unwrap_or_else(|| "".to_string()),
})
.collect(),
})
.collect(),
})
.collect(),
};
let template = TlsReportTemplate {
report: &formatted_tlsrpt,
};
template.render().unwrap_or_else(|e| format!("<div class=\"tlsrpt-error\">Failed to render TLS report template: {}</div>", e))
};
let template = TlsReportTemplate {
report: &formatted_tlsrpt,
};
let html = template.render().unwrap_or_else(|e| format!("<div class=\"tlsrpt-error\">Failed to render TLS report template: {}</div>", e));
parts.push(Body::html(html));
}
Err(e) => {
let html = format!(
"<div class=\"tlsrpt-error\">Failed to parse TLS report JSON: {}</div>",
e
);
parts.push(Body::html(html));
}
}
} else {
let html = format!("<div class=\"tlsrpt-error\">Failed to convert decompressed data to UTF-8.</div>");
parts.push(Body::html(html));
}
} else {
let html =
format!("<div class=\"tlsrpt-error\">Failed to decompress data.</div>");
parts.push(Body::html(html));
}
}
MESSAGE_RFC822 => {
parts.push(extract_rfc822(&sp, part_addr)?);
}
TEXT_HTML => {
let body = sp.get_body()?;
parts.push(Body::html(body));
}
MESSAGE_DELIVERY_STATUS => {
let body = extract_delivery_status(sp)?;
parts.push(body);
}
TEXT_PLAIN => {
let body = sp.get_body()?;
parts.push(Body::text(body));
}
_ => {
// For any other content type, try to extract the body using the general extract_body function
match extract_body(sp, part_addr) {
Ok(body) => parts.push(body),
Err(_) => {
// If extraction fails, create an unhandled content type body
let msg = format!(
"Unhandled report subpart content type: {}\n{}",
sp.ctype.mimetype,
sp.get_body()
.unwrap_or_else(|_| "Failed to get body".to_string())
);
parts.push(Body::UnhandledContentType(UnhandledContentType {
text: msg,
content_tree: render_content_type_tree(sp),
}));
}
Err(e) => format!(
"<div class=\"tlsrpt-error\">Failed to parse TLS report JSON: {}</div>",
e
),
}
} else {
format!("<div class=\"tlsrpt-error\">Failed to convert decompressed data to UTF-8.</div>")
}
} else {
format!("<div class=\"tlsrpt-error\">Failed to decompressed data.</div>")
}
} else {
"".to_string()
};
let final_html = if let Some(html) = html_part {
format!("{}<hr>{} ", html, tlsrpt_summary_html)
} else {
tlsrpt_summary_html
};
part_addr.pop();
}
Ok(Body::html(final_html))
if parts.is_empty() {
return Ok(Body::html(
"<div class=\"report-error\">No report content found</div>".to_string(),
));
}
// Add <hr> tags between subparts for better visual separation
let html = parts
.iter()
.map(|p| match p {
Body::PlainText(PlainText { text, .. }) => {
format!(
r#"<p class="view-part-text-plain font-mono whitespace-pre-line">{}</p>"#,
linkify_html(&html_escape::encode_text(text).trim_matches('\n'))
)
}
Body::Html(Html { html, .. }) => html.clone(),
Body::UnhandledContentType(UnhandledContentType { text, .. }) => {
format!(
r#"<p class="view-part-unhandled">{}</p>"#,
linkify_html(&html_escape::encode_text(text).trim_matches('\n'))
)
}
})
.collect::<Vec<_>>()
.join("<hr>\n");
Ok(Body::html(html))
}
pub fn extract_delivery_status(m: &ParsedMail) -> Result<Body, ServerError> {
Ok(Body::text(m.get_body()?))
}
pub fn extract_unhandled(m: &ParsedMail) -> Result<Body, ServerError> {
@@ -1837,39 +2037,42 @@ pub fn render_ical_summary(ical_data: &str) -> Result<String, ServerError> {
}
}
// Always use America/Los_Angeles for Google Calendar events if no TZID is present
let event_tz: Tz = tzid
.as_deref()
.unwrap_or("America/Los_Angeles")
.parse()
.unwrap_or(chrono_tz::America::Los_Angeles);
// Parse start/end as chrono DateTime
let (local_fmt_start, local_fmt_end, event_days, recurrence_display) =
if let Some(dtstart) = dtstart {
let tz: Tz = tzid
.as_deref()
.unwrap_or("UTC")
.parse()
.unwrap_or(chrono_tz::UTC);
let fallback = chrono::DateTime::<chrono::Utc>::from_timestamp(0, 0)
.map(|dt| dt.with_timezone(&tz))
.map(|dt| dt.with_timezone(&event_tz))
.unwrap_or_else(|| {
tz.with_ymd_and_hms(1970, 1, 1, 0, 0, 0)
event_tz
.with_ymd_and_hms(1970, 1, 1, 0, 0, 0)
.single()
.unwrap_or_else(|| tz.timestamp_opt(0, 0).single().unwrap())
.unwrap_or_else(|| event_tz.timestamp_opt(0, 0).single().unwrap())
});
let start = parse_ical_datetime_tz(dtstart, tz).unwrap_or(fallback);
let start = parse_ical_datetime_tz(dtstart, event_tz).unwrap_or(fallback);
let end = dtend
.and_then(|d| parse_ical_datetime_tz(d, tz))
.and_then(|d| parse_ical_datetime_tz(d, event_tz))
.unwrap_or(start);
let local_start = start.with_timezone(&Local);
let local_end = end.with_timezone(&Local);
// Use the event's TZ for all calendar grid/highlighting logic
let allday =
dtstart.len() == 8 && (dtend.map(|s| s.len() == 8).unwrap_or(false));
let fmt_start = if allday {
local_start.format("%a %b %e, %Y").to_string()
start.format("%a %b %e, %Y").to_string()
} else {
local_start.format("%-I:%M %p %a %b %e, %Y").to_string()
start.format("%-I:%M %p %a %b %e, %Y").to_string()
};
let fmt_end = if allday {
local_end.format("%a %b %e, %Y").to_string()
end.format("%a %b %e, %Y").to_string()
} else {
local_end.format("%-I:%M %p %a %b %e, %Y").to_string()
end.format("%-I:%M %p %a %b %e, %Y").to_string()
};
// All calendar grid and event_days logic below uses start/end in event's TZ
// Recurrence support: parse RRULE and generate event_days accordingly
let mut days = vec![];
@@ -2144,6 +2347,39 @@ fn parse_ical_datetime_tz(dt: &str, tz: Tz) -> Option<chrono::DateTime<Tz>> {
#[cfg(test)]
mod tests {
#[test]
fn google_calendar_email_thursday_highlights_thursday() {
use mailparse::parse_mail;
let raw_email = include_str!("../../server/testdata/google-calendar-example-thursday.eml");
let parsed = parse_mail(raw_email.as_bytes()).expect("parse_mail");
let mut part_addr = vec![];
let body = extract_body(&parsed, &mut part_addr).expect("extract_body");
let meta = extract_calendar_metadata_from_mail(&parsed, &body);
// Assert detection as Google Calendar
assert!(meta.is_google_calendar_event);
let html = meta.body_html.expect("body_html");
// Print event date info for debugging
for part in parsed.subparts.iter() {
if part.ctype.mimetype == TEXT_CALENDAR {
if let Ok(ical) = part.get_body() {
println!("ICAL data: {}", ical);
if let Some(start) = ical.lines().find(|l| l.starts_with("DTSTART:")) {
println!("Start date: {}", start);
}
}
}
}
println!("Rendered HTML: {}", html);
// Look for September 11 (Thursday) being highlighted
// The calendar should show Sept 11 highlighted with background:#ffd700 and the correct data-event-day
assert!(html.contains(r#"data-event-day="2025-09-11""#));
assert!(html.contains(r#"background:#ffd700"#));
// Since 1:00 AM UTC on Friday 9/12 is 6:00 PM PDT on Thursday 9/11, verify times are correct
assert!(html.contains("6:00 PM Thu Sep 11, 2025"));
}
use super::*;
#[test]
fn google_calendar_email_3_single_event_metadata() {
@@ -2168,8 +2404,9 @@ mod tests {
html.contains("Dentist appt"),
"HTML should contain the summary"
);
// Date is now formatted as human-readable "Tue Sep 23, 2025"
assert!(
html.contains("20250923"),
html.contains("Sep 23, 2025") || html.contains("20250923"),
"HTML should contain the event date"
);
assert!(
@@ -2198,8 +2435,8 @@ mod tests {
Some("calendar-notification@google.com".to_string())
);
// Dates: from subject, Thu Sep 11 to Fri Jan 30, 2026
let current_year = chrono::Local::now().year();
assert_eq!(meta.start_date, Some(format!("{}0911", current_year)));
// Start date is Sep 11, 2025 (one year before end since Sep > Jan)
assert_eq!(meta.start_date, Some("20250911".to_string()));
assert_eq!(meta.end_date, Some("20260131".to_string()));
}
#[test]
@@ -2223,12 +2460,13 @@ mod tests {
html.contains("<b>Organizer:</b> calendar-notification@google.com"),
"HTML should contain the labeled organizer"
);
// Dates are now formatted as human-readable
assert!(
html.contains("<b>Start:</b> 20250911"),
html.contains("<b>Start:</b> Thu Sep 11, 2025") || html.contains("<b>Start:</b> 20250911"),
"HTML should contain the labeled start time"
);
assert!(
html.contains("<b>End:</b> 20260131"),
html.contains("<b>End:</b> Fri Jan 30, 2026") || html.contains("<b>End:</b> 20260131"),
"HTML should contain the labeled end time"
);
if !html.contains("ical-flex") {
@@ -2299,8 +2537,8 @@ mod tests {
Some("calendar-notification@google.com".to_string())
);
// Dates: from subject, Thu Sep 11 to Fri Jan 30, 2026
let current_year = chrono::Local::now().year();
assert_eq!(meta.start_date, Some(format!("{}0911", current_year)));
// Start date is Sep 11, 2025 (one year before end since Sep > Jan)
assert_eq!(meta.start_date, Some("20250911".to_string()));
assert_eq!(meta.end_date, Some("20260131".to_string()));
// Debug: print the rendered HTML for inspection
if let Some(ref html) = meta.body_html {
@@ -2333,8 +2571,8 @@ mod tests {
Some("calendar-notification@google.com".to_string())
);
// Assert that the start and end dates are present
let current_year = chrono::Local::now().year();
assert_eq!(meta.start_date, Some(format!("{}0911", current_year)));
// Start date is Sep 11, 2025 (one year before end since Sep > Jan)
assert_eq!(meta.start_date, Some("20250911".to_string()));
assert_eq!(meta.end_date, Some("20260131".to_string()));
// Assert that the HTML body contains recurrence info
if let Some(ref html) = meta.body_html {
@@ -2350,6 +2588,52 @@ mod tests {
}
}
#[test]
fn google_calendar_email_4_single_allday_event() {
use mailparse::parse_mail;
let raw_email = include_str!("../../server/testdata/google-calendar-example-4.eml");
let parsed = parse_mail(raw_email.as_bytes()).expect("parse_mail");
let mut part_addr = vec![];
let body = extract_body(&parsed, &mut part_addr).expect("extract_body");
let meta = extract_calendar_metadata_from_mail(&parsed, &body);
// Assert detection as Google Calendar
assert!(meta.is_google_calendar_event);
// Assert metadata extraction for single all-day event
assert_eq!(meta.summary, Some("Emery Sleeps Over".to_string()));
assert_eq!(meta.organizer, Some("tconvertino@gmail.com".to_string()));
// Dates: Sunday Jan 18, 2026 (all-day event)
assert_eq!(meta.start_date, Some("20260118".to_string()));
assert_eq!(meta.end_date, Some("20260119".to_string())); // All-day events end next day
// Assert ical summary is rendered and shows Jan 18 highlighted
let html = meta.body_html.expect("body_html");
println!("Rendered HTML: {}", html);
assert!(html.contains("ical-flex"), "Calendar widget should be rendered");
assert!(html.contains(r#"data-event-day="2026-01-18""#), "Jan 18 should be highlighted");
}
#[test]
fn google_calendar_email_5_updated_invitation() {
use mailparse::parse_mail;
let raw_email = include_str!("../../server/testdata/google-calendar-example-5.eml");
let parsed = parse_mail(raw_email.as_bytes()).expect("parse_mail");
let mut part_addr = vec![];
let body = extract_body(&parsed, &mut part_addr).expect("extract_body");
let meta = extract_calendar_metadata_from_mail(&parsed, &body);
// Assert detection as Google Calendar
assert!(meta.is_google_calendar_event);
// Assert metadata extraction for updated invitation
assert_eq!(meta.summary, Some("painting class".to_string()));
assert_eq!(meta.organizer, Some("tconvertino@gmail.com".to_string()));
// Dates: Thursday Feb 12, 2026 7pm - 9pm (same day event with time)
assert_eq!(meta.start_date, Some("20260212".to_string()));
assert_eq!(meta.end_date, Some("20260212".to_string()));
// Assert ical summary is rendered and shows Feb 12 highlighted
let html = meta.body_html.expect("body_html");
println!("Rendered HTML: {}", html);
assert!(html.contains("ical-flex"), "Calendar widget should be rendered");
assert!(html.contains(r#"data-event-day="2026-02-12""#), "Feb 12 should be highlighted");
}
#[test]
fn recurring_event_rrule_metadata_and_highlight() {
use super::render_ical_summary;

View File

@@ -7,6 +7,7 @@ use async_graphql::{
Union,
};
use cacher::FilesystemCacher;
use chrono::{DateTime, Utc};
use futures::stream;
use letterbox_notmuch::Notmuch;
use serde::{Deserialize, Serialize};
@@ -628,6 +629,42 @@ impl MutationRoot {
nm.tag_remove(&tag, &query)?;
Ok(true)
}
#[instrument(skip_all, fields(query=query, wake_time=wake_time.to_string(), rid=request_id()))]
async fn snooze<'ctx>(
&self,
ctx: &Context<'ctx>,
query: String,
wake_time: DateTime<Utc>,
) -> Result<bool, Error> {
info!("TODO snooze {query} until {wake_time})");
let pool = ctx.data_unchecked::<PgPool>();
sqlx::query!(
r#"
INSERT INTO snooze (message_id, wake)
VALUES ($1, $2)
ON CONFLICT (message_id) DO UPDATE
SET wake = $2
"#,
query,
wake_time
)
.execute(pool)
.await?;
let nm = ctx.data_unchecked::<Notmuch>();
let pool = ctx.data_unchecked::<PgPool>();
#[cfg(feature = "tantivy")]
let tantivy = ctx.data_unchecked::<TantivyConnection>();
let unread = false;
let query: Query = query.parse()?;
newsreader::set_read_status(pool, &query, unread).await?;
#[cfg(feature = "tantivy")]
tantivy.reindex_thread(pool, &query).await?;
nm::set_read_status(nm, &query, unread).await?;
Ok(true)
}
/// Drop and recreate tantivy index. Warning this is slow
#[cfg(feature = "tantivy")]
async fn drop_and_load_index<'ctx>(&self, ctx: &Context<'ctx>) -> Result<bool, Error> {
@@ -639,6 +676,18 @@ impl MutationRoot {
Ok(true)
}
#[instrument(skip_all, fields(rid=request_id()))]
async fn label_unprocessed<'ctx>(
&self,
ctx: &Context<'ctx>,
limit: Option<usize>,
) -> Result<bool, Error> {
let nm = ctx.data_unchecked::<Notmuch>();
let pool = ctx.data_unchecked::<PgPool>();
label_unprocessed(&nm, &pool, false, limit, "tag:unprocessed").await?;
Ok(true)
}
#[instrument(skip_all, fields(rid=request_id()))]
async fn refresh<'ctx>(&self, ctx: &Context<'ctx>) -> Result<bool, Error> {
let nm = ctx.data_unchecked::<Notmuch>();
@@ -648,7 +697,10 @@ impl MutationRoot {
newsreader::refresh(pool, cacher).await?;
// Process email labels
label_unprocessed(&nm, &pool, false, Some(10), "tag:unprocessed").await?;
label_unprocessed(&nm, &pool, false, Some(1000), "tag:unprocessed").await?;
// Look for snoozed messages and mark unread
wakeup(&nm, &pool).await?;
#[cfg(feature = "tantivy")]
{
@@ -670,6 +722,33 @@ impl SubscriptionRoot {
pub type GraphqlSchema = Schema<QueryRoot, MutationRoot, SubscriptionRoot>;
#[instrument(name = "wakeup", skip_all)]
pub async fn wakeup(nm: &Notmuch, pool: &PgPool) -> Result<(), Error> {
for row in sqlx::query!(
r#"
SELECT id, message_id
FROM snooze
WHERE wake < NOW();
"#
)
.fetch_all(pool)
.await?
{
let query: Query = row.message_id.parse()?;
info!("need to wake {query}");
let unread = true;
newsreader::set_read_status(pool, &query, unread).await?;
#[cfg(feature = "tantivy")]
tantivy.reindex_thread(pool, &query).await?;
nm::set_read_status(nm, &query, unread).await?;
sqlx::query!("DELETE FROM snooze WHERE id = $1", row.id)
.execute(pool)
.await?;
}
Ok(())
}
#[instrument(skip_all, fields(query=query))]
pub async fn compute_catchup_ids(
nm: &Notmuch,

View File

@@ -19,6 +19,7 @@ use std::{
use async_trait::async_trait;
use cacher::{Cacher, FilesystemCacher};
use chrono::NaiveDateTime;
use css_inline::{CSSInliner, InlineError, InlineOptions};
pub use error::ServerError;
use linkify::{LinkFinder, LinkKind};
@@ -30,7 +31,6 @@ use maplit::{hashmap, hashset};
use regex::Regex;
use reqwest::StatusCode;
use scraper::{Html, Selector};
use sqlx::types::time::PrimitiveDateTime;
use thiserror::Error;
use tracing::{debug, error, info, warn};
use url::Url;
@@ -44,6 +44,8 @@ use crate::{
const NEWSREADER_TAG_PREFIX: &'static str = "News/";
const NEWSREADER_THREAD_PREFIX: &'static str = "news:";
const USER_AGENT: &'static str = "letterbox news reader (letterbox-ua@xinu.tv)";
// TODO: figure out how to use Cow
#[async_trait]
trait Transformer: Send + Sync {
@@ -318,7 +320,8 @@ impl<'c> Transformer for SlurpContents<'c> {
let body = if let Some(body) = cacher.get(link.as_str()) {
String::from_utf8_lossy(&body).to_string()
} else {
let resp = reqwest::get(link.as_str()).await?;
let client = reqwest::Client::builder().user_agent(USER_AGENT).build()?;
let resp = client.get(link.as_str()).send().await?;
let status = resp.status();
if status.is_server_error() {
error!("status error for {link}: {status}");
@@ -754,6 +757,7 @@ pub struct Query {
pub is_notmuch: bool,
pub is_newsreader: bool,
pub is_tantivy: bool,
pub is_snoozed: bool,
pub corpus: Option<Corpus>,
}
@@ -777,6 +781,9 @@ impl fmt::Display for Query {
if self.is_newsreader {
write!(f, "is:news ")?;
}
if self.is_snoozed {
write!(f, "is:snoozed ")?;
}
match self.corpus {
Some(c) => write!(f, "corpus:{c:?}")?,
_ => (),
@@ -833,6 +840,7 @@ impl FromStr for Query {
let mut is_notmuch = false;
let mut is_newsreader = false;
let mut is_tantivy = false;
let mut is_snoozed = false;
let mut corpus = None;
for word in s.split_whitespace() {
if word == "is:unread" {
@@ -872,6 +880,8 @@ impl FromStr for Query {
is_newsreader = true;
} else if word == "is:newsreader" {
is_newsreader = true;
} else if word == "is:snoozed" {
is_snoozed = true;
} else {
remainder.push(word.to_string());
}
@@ -890,13 +900,14 @@ impl FromStr for Query {
is_notmuch,
is_newsreader,
is_tantivy,
is_snoozed,
corpus,
})
}
}
pub struct ThreadSummaryRecord {
pub site: Option<String>,
pub date: Option<PrimitiveDateTime>,
pub date: Option<NaiveDateTime>,
pub is_read: Option<bool>,
pub title: Option<String>,
pub uid: String,
@@ -914,11 +925,7 @@ async fn thread_summary_from_row(r: ThreadSummaryRecord) -> ThreadSummary {
title = clean_title(&title).await.expect("failed to clean title");
ThreadSummary {
thread: format!("{NEWSREADER_THREAD_PREFIX}{}", r.uid),
timestamp: r
.date
.expect("post missing date")
.assume_utc()
.unix_timestamp() as isize,
timestamp: r.date.expect("post missing date").and_utc().timestamp() as isize,
date_relative: format!("{:?}", r.date),
//date_relative: "TODO date_relative".to_string(),
matched: 0,

View File

@@ -6,7 +6,7 @@ use letterbox_shared::compute_color;
use maplit::hashmap;
use scraper::Selector;
use sqlx::postgres::PgPool;
use tracing::{error, info, instrument};
use tracing::{error, info, instrument, warn};
use url::Url;
use crate::{
@@ -86,6 +86,10 @@ pub async fn search(
query: &Query,
) -> Result<Vec<(i32, ThreadSummary)>, async_graphql::Error> {
info!("search({after:?} {before:?} {first:?} {last:?} {query:?}");
if query.is_snoozed {
warn!("TODO implement snooze for newsreader::search");
return Ok(Vec::new());
}
if !is_newsreader_query(query) {
return Ok(Vec::new());
}
@@ -211,11 +215,7 @@ pub async fn thread(
}
let title = clean_title(&r.title.unwrap_or("NO TITLE".to_string())).await?;
let is_read = r.is_read.unwrap_or(false);
let timestamp = r
.date
.expect("post missing date")
.assume_utc()
.unix_timestamp();
let timestamp = r.date.expect("post missing date").and_utc().timestamp();
Ok(Thread::News(NewsPost {
thread_id,
is_read,

View File

@@ -64,6 +64,10 @@ pub async fn search(
last: Option<i32>,
query: &Query,
) -> Result<Vec<(i32, ThreadSummary)>, async_graphql::Error> {
if query.is_snoozed {
warn!("TODO implement snooze for nm::search");
return Ok(Vec::new());
}
if !is_notmuch_query(query) {
return Ok(Vec::new());
}

View File

@@ -74,13 +74,7 @@
{% for week in all_days|batch(7) %}
<tr>
{% for day in week %}
{% if event_days.contains(day) && today.is_some() && today.unwrap() == day %}
<td
data-event-day="{{ day.format("%Y-%m-%d") }}"
style="background:#ffd700; color:#222; font-weight:bold; border:2px solid #2196f3; border-radius:4px; text-align:center; box-shadow:0 0 0 2px #2196f3;">
{{ day.day() }}
</td>
{% elif event_days.contains(day) %}
{% if event_days.contains(day) %}
<td
data-event-day="{{ day.format("%Y-%m-%d") }}"
style="background:#ffd700; color:#222; font-weight:bold; border:1px solid #aaa; border-radius:4px; text-align:center;">

View File

@@ -0,0 +1,728 @@
Return-Path: <couchmoney+caf_=gmail=xinu.tv@gmail.com>
Delivered-To: bill@xinu.tv
Received: from phx.xinu.tv [74.207.253.222]
by nixos-01.h.xinu.tv with IMAP (fetchmail-6.5.6)
for <wathiede@localhost> (single-drop); Sat, 17 Jan 2026 09:25:58 -0800 (PST)
Received: from phx.xinu.tv
by phx.xinu.tv with LMTP
id YIkZKyXGa2k93xMAJR8clQ
(envelope-from <couchmoney+caf_=gmail=xinu.tv@gmail.com>)
for <bill@xinu.tv>; Sat, 17 Jan 2026 09:25:57 -0800
X-Original-To: gmail@xinu.tv
Received: from mail-lf1-f48.google.com (mail-lf1-f48.google.com [209.85.167.48])
by phx.xinu.tv (Postfix) with ESMTPS id B744880023
for <gmail@xinu.tv>; Sat, 17 Jan 2026 09:25:56 -0800 (PST)
Received: by mail-lf1-f48.google.com with SMTP id 2adb3069b0e04-59b78886454so3800941e87.2
for <gmail@xinu.tv>; Sat, 17 Jan 2026 09:25:56 -0800 (PST)
ARC-Seal: i=2; a=rsa-sha256; t=1768670755; cv=pass;
d=google.com; s=arc-20240605;
b=UCMG36NoEclyVlwzV5KDOA6Fq75afR1kZ6QZQ8A0CR9RJMMEnPEpiuhheiGH7csZWs
HEZJmrLtTX/e5qiZ0k5njtm8694d+44YtpWRS54bwcAvwWBeCnHstTFkuOB4J2GWvT6G
R9MwX2lwlaGj118bn6aIQTWLB6KyWzUmGdq9AO52fvWTkzlPFDN54/AUYdhx4r+dG5k3
tqmDhE87DYIPTtNwYeUZpyEvcKuXYqlRmkHEL+qkixmj6yFX9jReNcHypO3QOj8StGqu
H/WKwOSnM5Yupv4EblgGPF8ib8tczyxoi+q73sv7iRtQy8wgyAC1gG6T6/qXuY/+1V1K
lyfw==
ARC-Message-Signature: i=2; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20240605;
h=to:from:subject:date:message-id:auto-submitted:sender:reply-to
:mime-version:dkim-signature:dkim-signature:delivered-to;
bh=Hw6hGQbUlNGCz02STp2P+s244T5EBOLlXrfxnO0S/+U=;
fh=mUux9OA2+hLns/mBVi/4Nr5W8MsoxjQs+3G2LAg1TZo=;
b=V+AKgzj8GN/DZNHWPE/MY0blPHHM1Kp85OCTTacCIk/G6dNhx+WmLnIyExrC3i4wmU
i62upyA0a18rhHZRhV1FB4oMMhQVroYLKwh5dFuqFtTARua9DgwYeN6YALL9+rr84n2b
eZe0txkO5dyJgxByumgOymYFgbevrEtd1GWfK2v1BxtQXzqNZ0SKj5PhVCc5WD+toeHu
OEqUuCoHRWpeXYD19OUqv/+MwhPC4t5R5fz8nlPcjxa/fYINuI5+iLhSP7Ki4gzAZFRK
T3zMitsxIv/8zKMrhG5K0cm7Nntn2XBT5zrIDURZW9HEKYLww0yJ8qKXPNL+RtbfGgMq
TiRw==;
darn=xinu.tv
ARC-Authentication-Results: i=2; mx.google.com;
dkim=pass header.i=@google.com header.s=20230601 header.b=bNGW+EgT;
dkim=pass header.i=@gmail.com header.s=20230601 header.b=QfHaLAXu;
spf=pass (google.com: domain of tconvertino@gmail.com designates 209.85.220.73 as permitted sender) smtp.mailfrom=tconvertino@gmail.com;
dmarc=pass (p=NONE sp=QUARANTINE dis=NONE) header.from=gmail.com;
dara=pass header.i=@gmail.com
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=1e100.net; s=20230601; t=1768670755; x=1769275555;
h=to:from:subject:date:message-id:auto-submitted:sender:reply-to
:mime-version:dkim-signature:dkim-signature:delivered-to
:x-forwarded-for:x-forwarded-to:x-gm-message-state:from:to:cc
:subject:date:message-id:reply-to;
bh=Hw6hGQbUlNGCz02STp2P+s244T5EBOLlXrfxnO0S/+U=;
b=eTx/TzBvNH8F83uUTAONl5k6vTRn2Id8TE91Mnl0cbJd6GaM9J4DgWVxmBoqfep8nA
hN3H5r01YLOdQNWTWmAV2RUrylBtLMQRqW7xeVnIIUKQNXfavZZKaFpwsuudvjDWGKBo
zesMFCOaEstK+nCpo/bPurb9kprcOh5y/WZjgL7OqtpnnyzhN6DhkhaYbGetIwW4osj5
aCFPOcoMcCYYW8OxEZC6bv2hJzehnV93g8IDY8tBQ88TIf1Kl3uDM8v3oLwMGgmEX+te
PYznnWbJF0vG00cWauIsTnjzUt8SSpnaUXw6PXbHlZxn5Roa/l6hg/tuhs699btYOJm/
izbQ==
X-Forwarded-Encrypted: i=2; AJvYcCUWtYAXLj/f6NFhD1jVOvyY1Jd5fsiQkXHwDFfYixYixyUvud2GXNENdLwj08ultHSVt74PwA==@xinu.tv
X-Gm-Message-State: AOJu0Yx6pI6AZXNq1lGFocBmt39kF0MuPDwo3WPcPrcCg8s1e8EF0iF0
0jOlOq3z4d3WKZqbpCpMIczBtHf5wHUzS1TFiPfYcoHfhnoxLm+dVYhdf0B5b39G2NSwnHIRAcZ
HPVGwj7Cl8dNJOMBLPOevH4CYTDEubbDxDmQOvWE0bhVDk2P+UIU53lYzGkLCnQ==
X-Received: by 2002:a05:6512:4016:b0:59b:7888:62c8 with SMTP id 2adb3069b0e04-59baeed63a1mr2407719e87.33.1768670754691;
Sat, 17 Jan 2026 09:25:54 -0800 (PST)
X-Forwarded-To: gmail@xinu.tv
X-Forwarded-For: couchmoney@gmail.com gmail@xinu.tv
Delivered-To: couchmoney@gmail.com
Received: by 2002:a05:6504:2382:b0:2d3:710a:2457 with SMTP id h2csp2362813lty;
Sat, 17 Jan 2026 09:25:53 -0800 (PST)
X-Received: by 2002:a05:6808:4f0e:b0:450:ac57:48a7 with SMTP id 5614622812f47-45c9c14fc0fmr2557499b6e.59.1768670753179;
Sat, 17 Jan 2026 09:25:53 -0800 (PST)
ARC-Seal: i=1; a=rsa-sha256; t=1768670753; cv=none;
d=google.com; s=arc-20240605;
b=V+T4U8NWyAR1p4yC5XY/I8vxXwtdkLXkIEO6gNBVvJyYi4XbjMMEnoRPAqOULwONFT
7q1V9vArMoZrvS4GNL3dg05tLr0Ug+Frm39+Vp1Wp3UxhQ/yxiby8jhRYkMyaKLZxhR3
2kihw8UgFjdUteHHwKoTDnIkTeKrMKZK8N4bTEzf9LoIXHMZcVaeC5XItuuOUdX6TPXr
xEQKfzCfz3UHY1piusFov9YIr8iBLGnNp6bXJqbRKmnLhOGkt9HQOT9rBl1nmBg5bqQj
4qxTu8Le/CE5qljInXX5iXNYXp1eMD1G6PZ9Hah1hr/wen1VPM6ysNynBlDzlNQUyEMJ
8lNw==
ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20240605;
h=to:from:subject:date:message-id:auto-submitted:sender:reply-to
:mime-version:dkim-signature:dkim-signature;
bh=Hw6hGQbUlNGCz02STp2P+s244T5EBOLlXrfxnO0S/+U=;
fh=mbzrMIWIgWMC0ni1xEx+ViW4J0RLAdLdPT2cX81nTlk=;
b=UhcC2/PG/1T8wb5AzN1QNoaEXR7rs82O/P2CXN7vMVR9JoE3selJIzwIpyWxuKDPK3
GQtEmc8Bqcvcqu//9mWJxsklCkxSXrYnJ0UvykvbmZT7xPhM4r2mpWPluvfxLfEEdbqg
aNgJM1bn4QoYvnjmIF638/SN9dK5TI9seZ04BzbqQxd7Vw5OeccovSpPerSP7ya7l+4k
wOHhvP4mAlB/0bUae8xN/bqS0SIgy+V+cRr3tYEsRb21gJgTT757rHIV0aQu5LSO9t2N
UilB/hh4qvPhaCWmj6I+30ZYD02m9WKPYkwteLA9NXtggMw9WGeywxPZ//pHazzbq7iQ
im+A==;
dara=google.com
ARC-Authentication-Results: i=1; mx.google.com;
dkim=pass header.i=@google.com header.s=20230601 header.b=bNGW+EgT;
dkim=pass header.i=@gmail.com header.s=20230601 header.b=QfHaLAXu;
spf=pass (google.com: domain of tconvertino@gmail.com designates 209.85.220.73 as permitted sender) smtp.mailfrom=tconvertino@gmail.com;
dmarc=pass (p=NONE sp=QUARANTINE dis=NONE) header.from=gmail.com;
dara=pass header.i=@gmail.com
Received: from mail-sor-f73.google.com (mail-sor-f73.google.com. [209.85.220.73])
by mx.google.com with SMTPS id 5614622812f47-45c9e03dfa5sor2738469b6e.10.2026.01.17.09.25.53
for <couchmoney@gmail.com>
(Google Transport Security);
Sat, 17 Jan 2026 09:25:53 -0800 (PST)
Received-SPF: pass (google.com: domain of tconvertino@gmail.com designates 209.85.220.73 as permitted sender) client-ip=209.85.220.73;
Authentication-Results: mx.google.com;
dkim=pass header.i=@google.com header.s=20230601 header.b=bNGW+EgT;
dkim=pass header.i=@gmail.com header.s=20230601 header.b=QfHaLAXu;
spf=pass (google.com: domain of tconvertino@gmail.com designates 209.85.220.73 as permitted sender) smtp.mailfrom=tconvertino@gmail.com;
dmarc=pass (p=NONE sp=QUARANTINE dis=NONE) header.from=gmail.com;
dara=pass header.i=@gmail.com
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=google.com; s=20230601; t=1768670752; x=1769275552; dara=google.com;
h=to:from:subject:date:message-id:auto-submitted:sender:reply-to
:mime-version:from:to:cc:subject:date:message-id:reply-to;
bh=Hw6hGQbUlNGCz02STp2P+s244T5EBOLlXrfxnO0S/+U=;
b=bNGW+EgTmg1v7auBVBEmmFGyg6QDqI536axgkCb2SIiknIljcxZLx2KR0hrFlA3lSz
Z89Q1JdMU37Tx8upXXkAQYBe0A42UgQjXEYYfjykMl/PNg7XppVWzevLwkKLmmr/dZ7f
YMcE1DQogEr3RNXJeD92NfJxyOQGskvnzb4rhy22QonzF2UyGy/QX2UtFSz1cZi+35Yq
vTkaernNWU3hf5pAXigHisJTtoJeTRgVNY4ch+gru1X1LmZZzrTgWt6e7hGtsbvlV7cZ
CBM8gqf1LrVLV0Y1PdvS50yack5EFKbyKtmQWAwHBlOABVDwPHbPD9/6N4973C9juedx
HnNg==
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=gmail.com; s=20230601; t=1768670753; x=1769275553; dara=google.com;
h=to:from:subject:date:message-id:auto-submitted:sender:reply-to
:mime-version:from:to:cc:subject:date:message-id:reply-to;
bh=Hw6hGQbUlNGCz02STp2P+s244T5EBOLlXrfxnO0S/+U=;
b=QfHaLAXuHdf594PTfEIjd3XOTBUaUaqHXtEArT1QZBEi1Vpf8NyYD6cPKPbjnln3CZ
q5s/sBjI6bmtszfVecPTEv3SEnFgQqQS/PCw3YbMNteemsw4rDNccwV6DSiX/BYMRZIM
4HEgMoLbXrlMlFnjEpWkfb7Kon5Y39C2DNx3sZ3TX/s8fLgYC8JpdUXdZ+LRlr8QzoNH
VLvVwW2iBYOzX9QdBtLdghvnmgvuSxIq1xB0zQNvDixOuG/egq1nDjHna4T75W8qzNEq
+hl8Rng1G2oqWAWUASkwSRvrUvV/NJA3gE+tGD2Isj9d/r4Ppll4jBWOu7KVRPM8Yrld
MYcg==
MIME-Version: 1.0
X-Received: by 2002:a05:6820:1993:b0:65f:67b7:95c2 with SMTP id
006d021491bc7-661179f382fmr2914920eaf.55.1768670752809; Sat, 17 Jan 2026
09:25:52 -0800 (PST)
Reply-To: tconvertino@gmail.com
Sender: Google Calendar <calendar-notification@google.com>
Auto-Submitted: auto-generated
Message-ID: <calendar-c95e83d6-7062-41ea-8c57-5b7bc2a79c76@google.com>
Date: Sat, 17 Jan 2026 17:25:52 +0000
Subject: New event: Emery Sleeps Over @ Sun Jan 18, 2026 (tconvertino@gmail.com)
From: tconvertino@gmail.com
To: couchmoney@gmail.com
Content-Type: multipart/alternative; boundary="000000000000f22916064898bf45"
X-Rspamd-Queue-Id: B744880023
X-Rspamd-Server: phx
X-Spamd-Result: default: False [-0.90 / 15.00];
URI_COUNT_ODD(1.00)[1];
ARC_ALLOW(-1.00)[google.com:s=arc-20240605:i=2];
DMARC_POLICY_ALLOW(-0.50)[gmail.com,none];
R_DKIM_ALLOW(-0.20)[google.com:s=20230601,gmail.com:s=20230601];
R_SPF_ALLOW(-0.20)[+ip4:209.85.128.0/17];
MANY_INVISIBLE_PARTS(0.10)[2];
MIME_GOOD(-0.10)[multipart/alternative,text/plain];
FREEMAIL_TO(0.00)[gmail.com];
RCVD_COUNT_THREE(0.00)[3];
RCVD_TLS_LAST(0.00)[];
FORGED_SENDER(0.00)[tconvertino@gmail.com,couchmoney@gmail.com];
RCPT_COUNT_ONE(0.00)[1];
RCVD_IN_DNSWL_NONE(0.00)[209.85.220.73:received];
TAGGED_FROM(0.00)[caf_=gmail=xinutv];
FREEMAIL_REPLYTO(0.00)[gmail.com];
MIME_TRACE(0.00)[0:+,1:+,2:~];
FREEMAIL_FROM(0.00)[gmail.com];
FROM_NEQ_ENVFROM(0.00)[tconvertino@gmail.com,couchmoney@gmail.com];
MISSING_XM_UA(0.00)[];
HAS_REPLYTO(0.00)[tconvertino@gmail.com];
DNSWL_BLOCKED(0.00)[209.85.167.48:from];
DWL_DNSWL_NONE(0.00)[gmail.com:dkim];
TO_DN_NONE(0.00)[];
FREEMAIL_ENVFROM(0.00)[gmail.com];
FORGED_SENDER_FORWARDING(0.00)[];
DKIM_TRACE(0.00)[google.com:+,gmail.com:+];
DWL_DNSWL_BLOCKED(0.00)[google.com:dkim];
TO_DOM_EQ_FROM_DOM(0.00)[];
FROM_NO_DN(0.00)[];
FWD_GOOGLE(0.00)[couchmoney@gmail.com];
ASN(0.00)[asn:15169, ipnet:209.85.128.0/17, country:US];
RWL_MAILSPIKE_POSSIBLE(0.00)[209.85.167.48:from];
REPLYTO_EQ_FROM(0.00)[]
X-Rspamd-Action: no action
X-TUID: GNj+V6W3PxE3
--000000000000f22916064898bf45
Content-Type: text/plain; charset="UTF-8"; format=flowed; delsp=yes
Emery Sleeps Over
Sunday Jan 18, 2026
Organizer
tconvertino@gmail.com
tconvertino@gmail.com
~~//~~
Invitation from Google Calendar: https://calendar.google.com/calendar/
You are receiving this email because you are subscribed to calendar
notifications. To stop receiving these emails, go to
https://calendar.google.com/calendar/r/settings, select this calendar, and
change "Other notifications".
Forwarding this invitation could allow any recipient to send a response to
the organizer, be added to the guest list, invite others regardless of
their own invitation status, or modify your RSVP.
Learn more https://support.google.com/calendar/answer/37135#forwarding
--000000000000f22916064898bf45
Content-Type: text/html; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable
<!doctype html><html xmlns=3D"http://www.w3.org/1999/xhtml" xmlns:v=3D"urn:=
schemas-microsoft-com:vml" xmlns:o=3D"urn:schemas-microsoft-com:office:offi=
ce"><head><title></title><!--[if !mso]><meta http-equiv=3D"X-UA-Compatible"=
content=3D"IE=3Dedge"><![endif]--><meta http-equiv=3D"Content-Type" conten=
t=3D"text/html; charset=3DUTF-8"><meta name=3D"viewport" content=3D"width=
=3Ddevice-width,initial-scale=3D1"><meta name=3D"color-scheme" content=3D"l=
ight dark"><meta name=3D"supported-color-schemes" content=3D"light dark">
<style>
body, html {
font-family: Roboto, Helvetica, Arial, sans-serif;
}
body {
margin: 0;
padding: 0;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
#outlook a {
padding: 0;
}
.ReadMsgBody {
width: 100%;
}
.ExternalClass {
width: 100%;
}
.ExternalClass * {
line-height: 100%;
}
table,
td {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if !mso]><!-->
<style>
@media only screen and (max-width:580px) {
@-ms-viewport {
width: 320px;
}
@viewport {
width: 320px;
}
}
</style>
<!--<![endif]-->
<!--[if mso]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!--[if lte mso 11]>
<style>
.outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<!--[if !mso]><!-- -->
<style>body, html {font-family:Roboto,Helvetica,Arial,sans-serif;}@font-f=
ace {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-stretch: normal;
src: url(//fonts.gstatic.com/s/roboto/v48/KFOMCnqEu92Fr1ME7kSn66aGLdTylUA=
MQXC89YmC2DPNWubEbVmUiA8.ttf) format('truetype');
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-stretch: normal;
src: url(//fonts.gstatic.com/s/roboto/v48/KFOMCnqEu92Fr1ME7kSn66aGLdTylUA=
MQXC89YmC2DPNWub2bVmUiA8.ttf) format('truetype');
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
font-stretch: normal;
src: url(//fonts.gstatic.com/s/roboto/v48/KFOMCnqEu92Fr1ME7kSn66aGLdTylUA=
MQXC89YmC2DPNWuYjalmUiA8.ttf) format('truetype');
}
@font-face {
font-family: 'Material Icons Extended';
font-style: normal;
font-weight: 400;
src: url(//fonts.gstatic.com/s/materialiconsextended/v154/kJEjBvgX7BgnkSr=
UwT8UnLVc38YydejYY-oE_LvM.ttf) format('truetype');
}
@font-face {
font-family: 'Google Material Icons';
font-style: normal;
font-weight: 400;
src: url(//fonts.gstatic.com/s/googlematerialicons/v144/Gw6kwdfw6UnXLJCcm=
afZyFRXb3BL9rvi0QZG3g.otf) format('opentype');
}
.google-material-icons {
font-family: 'Google Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
}
@font-face {
font-family: 'Google Material Icons Filled';
font-style: normal;
font-weight: 400;
src: url(//fonts.gstatic.com/s/googlematerialiconsfilled/v118/WWXFlimHYg6=
HKI3TavMkbKdhBmDvgach8TVpeGsuueSZJH4.otf) format('opentype');
}
.google-material-icons-filled {
font-family: 'Google Material Icons Filled';
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
}
@font-face {
font-family: 'Google Sans';
font-style: normal;
font-weight: 400;
src: url(//fonts.gstatic.com/s/googlesans/v14/4UaGrENHsxJlGDuGo1OIlL3Owps=
.ttf) format('truetype');
}
@font-face {
font-family: 'Google Sans';
font-style: normal;
font-weight: 500;
src: url(//fonts.gstatic.com/s/googlesans/v14/4UabrENHsxJlGDuGo1OIlLU94Yt=
zCwM.ttf) format('truetype');
}
@font-face {
font-family: 'Google Sans';
font-style: normal;
font-weight: 700;
src: url(//fonts.gstatic.com/s/googlesans/v14/4UabrENHsxJlGDuGo1OIlLV154t=
zCwM.ttf) format('truetype');
}
</style><!--<![endif]-->
<style>
.body-container {
padding-left: 16px;
padding-right: 16px;
}
</style>
=20
<style>
u+.body .body-container,
body[data-outlook-cycle] .body-container,
#MessageViewBody .body-container {
padding-left: 0;
padding-right: 0;
}
</style>
=20
<style>
@media only screen and (min-width:580px) {
.column-per-37 {
width: 37% !important;
max-width: 37%;
}
.column-per-63 {
width: 63% !important;
max-width: 63%;
}
}
</style>
=20
<style>
.appointment-buttons th {
display: block;
clear: both;
float: left;
margin-top: 12px;
}
.appointment-buttons th a {
float: left;
}
#MessageViewBody .appointment-buttons th {
margin-top: 24px;
}
</style>
=20
<style>
@media only screen and (max-width:580px) {
table.full-width-mobile {
width: 100% !important;
}
td.full-width-mobile {
width: auto !important;
}
}
</style>
<style>
.main-container-inner,
.info-bar-inner {
padding: 12px 16px !important;
}
.main-column-table-ltr {
padding-right: 0 !important;
}
.main-column-table-rtl {
padding-left: 0 !important;
}
@media only screen and (min-width:580px) {
.main-container-inner {
padding: 24px 32px !important;
}
.info-bar-inner {
padding: 12px 32px !important;
}
.main-column-table-ltr {
padding-right: 32px !important;
}
.main-column-table-rtl {
padding-left: 32px !important;
}
.appointment-buttons th {
display: table-cell;
clear: none;
}
}
.primary-text {
color: #3c4043 !important;
}
.secondary-text,
.phone-number a {
color: #70757a !important;
}
.accent-text {
color: #1a73e8 !important;
}
.accent-text-dark {
color: #185abc !important;
}
.grey-button-text,
.attachment-chip a {
color: #5f6368 !important;
}
.primary-button {
background-color: #1a73e8 !important;
}
.primary-button-text {
color: #fff !important;
}
.underline-on-hover:hover {
text-decoration: underline !important;
}
.grey-infobar-text {
color: #202124 !important;
}
@media (prefers-color-scheme: dark) {
.primary-text:not([class^=3D"x_"]) {
color: #e8eaed !important;
}
.secondary-text:not([class^=3D"x_"]),
.phone-number:not([class^=3D"x_"]) a {
color: #9aa0a6 !important;
}
.grey-button-text:not([class^=3D"x_"]),
.attachment-chip:not([class^=3D"x_"]) a {
color: #bdc1c6 !important;
}
.accent-text:not([class^=3D"x_"]),
.hairline-button-text:not([class^=3D"x_"]) {
color: #8ab4f8 !important;
}
.primary-button:not([class^=3D"x_"]) {
background-color: #8ab4f8 !important;
}
.primary-button-text:not([class^=3D"x_"]) {
color: #202124 !important;
}
}
</style>
<style>
@media (prefers-color-scheme: dark) {
.cse-banner:not([class^=3D"x_"]) {
background-color: #3c4043 !important; /* Google Grey 800 */
}
.encryption-icon:not([class^=3D"x_"]) {
/* WARNING: This causes the whole style tag to get stripped in Gm=
ail. */
background-image: url('https://fonts.gstatic.com/s/i/googlemateri=
aliconsfilled/encrypted/v3/gm_grey200-24dp/2x/gm_filled_encrypted_gm_grey20=
0_24dp.png') !important;
}
}
</style>
<!--[if !mso]><!-->
<style>
.prevent-link a {
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
</style>
<!--<![endif]-->
<!--[if mso | IE]>
<style>
.main-container-inner {
padding: 24px 32px !important;
}
.info-bar-inner {
padding: 12px 32px !important;
}
.cse-banner .encryption-icon {
/* We use the IE workaround instead. */
background-image: none !important;
}
.cse-banner .encryption-icon .ms-fallback {
display: block !important;
}
/* NB: Some MS clients ignore dark-scheme styling and apply their o=
wn, so there's nothing we can do to help there. */
@media (prefers-color-scheme: dark) {
.cse-banner:not([class^=3D"x_"]) .encryption-icon .ms-fallback {
display: none !important;
}
.cse-banner:not([class^=3D"x_"]) .encryption-icon .ms-fallback-da=
rk {
display: block !important;
}
}
</style>
<![endif]-->
</head><body class=3D"body"><span itemscope itemtype=3D"http://schema.org=
/InformAction"><span style=3D"display:none" itemprop=3D"about" itemscope it=
emtype=3D"http://schema.org/Person"><meta itemprop=3D"description" content=
=3D"Invitation from tconvertino@gmail.com"/></span><span itemprop=3D"object=
" itemscope itemtype=3D"http://schema.org/Event"><meta itemprop=3D"eventSta=
tus" content=3D"http://schema.org/EventScheduled"/><span itemprop=3D"publis=
her" itemscope itemtype=3D"http://schema.org/Organization"><meta itemprop=
=3D"name" content=3D"Google Calendar"/></span><meta itemprop=3D"eventId/goo=
gleCalendar" content=3D"7bsl09rb144q35feflbl0q2v58"/><span style=3D"display=
: none; font-size: 1px; color: #fff; line-height: 1px; height: 0; max-heigh=
t: 0; width: 0; max-width: 0; opacity: 0; overflow: hidden;" itemprop=3D"na=
me">Emery Sleeps Over</span><meta itemprop=3D"url" content=3D"https://calen=
dar.google.com/calendar/r?eid=3DN2JzbDA5cmIxNDRxMzVmZWZsYmwwcTJ2NTggdGNvbnZ=
lcnRpbm9AbQ&amp;es=3D1"/><span aria-hidden=3D"true"><time itemprop=3D"start=
Date" datetime=3D"20260118"></time><time itemprop=3D"endDate" datetime=3D"2=
0260119"></time></span><div style=3D"display: none; font-size: 1px; color: =
#fff; line-height: 1px; height: 0; max-height: 0; width: 0; max-width: 0; o=
pacity: 0; overflow: hidden;">You have been invited by tconvertino@gmail.co=
m to attend an event named Emery Sleeps Over on Sunday Jan 18, 2026.</div><=
table border=3D"0" cellpadding=3D"0" cellspacing=3D"0" role=3D"presentation=
" align=3D"center" style=3D"width:100%;" class=3D"body-container"><tbody><t=
r><td style=3D"" class=3D"" align=3D"left"><!--[if mso | IE]><table border=
=3D"0" cellpadding=3D"0" cellspacing=3D"0" role=3D"presentation"><tr><td he=
ight=3D"16" style=3D"height:16px;"><![endif]--><div style=3D"height:16px;" =
aria-hidden=3D"true"> &nbsp; </div><!--[if mso | IE]></td></tr></table><![e=
ndif]--><table border=3D"0" cellpadding=3D"0" cellspacing=3D"0" role=3D"pre=
sentation" align=3D"center" style=3D"width:100%;" class=3D""><tbody><tr><td=
style=3D"border: solid 1px #dadce0; border-radius: 8px; direction: rtl; fo=
nt-size: 0; padding: 24px 32px; text-align: left; vertical-align: top;" cla=
ss=3D"main-container-inner"><!--[if mso | IE]><table border=3D"0" cellpaddi=
ng=3D"0" cellspacing=3D"0" role=3D"presentation"><tr><![endif]--><div class=
=3D"" style=3D"font-size: 13px; text-align: left; direction: ltr; display: =
inline-block; vertical-align: top; width: 100%;overflow: hidden; word-wrap:=
break-word;"><table border=3D"0" cellpadding=3D"0" cellspacing=3D"0" role=
=3D"presentation" width=3D"100%" class=3D"main-column-table-ltr" style=3D"p=
adding-right: 32px; padding-left: 0;;table-layout: fixed;"><tbody><tr><td c=
lass=3D"main-column-td" style=3D"padding:0; vertical-align:top;"><table bor=
der=3D"0" cellpadding=3D"0" cellspacing=3D"0" role=3D"presentation" width=
=3D"100%" style=3D"table-layout: fixed;"><tr><td style=3D"font-size: 0; pad=
ding: 0; text-align: left; word-break: break-word;;padding-bottom:24px;"><d=
iv style=3D"font-family: Roboto, sans-serif;font-style: normal; font-weight=
: 400; font-size: 14px; line-height: 20px; letter-spacing: 0.2px;color: #3c=
4043; text-decoration: none;" class=3D"primary-text" role=3D"presentation">=
<span aria-hidden=3D"true"><time itemprop=3D"startDate" datetime=3D"2026011=
8"></time><time itemprop=3D"endDate" datetime=3D"20260119"></time></span><t=
able border=3D"0" cellpadding=3D"0" cellspacing=3D"0" role=3D"presentation"=
style=3D"padding-bottom: 4px;"><tr><td><h2 class=3D"primary-text" style=3D=
"font-size: 14px;color: #3c4043; text-decoration: none;font-weight: 700;-we=
bkit-font-smoothing: antialiased;margin: 0; padding: 0;">When</h2></td></tr=
></table><span>Sunday Jan 18, 2026</span></div></td></tr><tr><td style=3D"f=
ont-size: 0; padding: 0; text-align: left; word-break: break-word;;padding-=
bottom:24px;"><div style=3D"font-family: Roboto, sans-serif;font-style: nor=
mal; font-weight: 400; font-size: 14px; line-height: 20px; letter-spacing: =
0.2px;color: #3c4043; text-decoration: none;" class=3D"primary-text" role=
=3D"presentation"><table border=3D"0" cellpadding=3D"0" cellspacing=3D"0" r=
ole=3D"presentation" style=3D"padding-bottom: 4px;"><tr><td><h2 class=3D"pr=
imary-text" style=3D"font-size: 14px;color: #3c4043; text-decoration: none;=
font-weight: 700;-webkit-font-smoothing: antialiased;margin: 0; padding: 0;=
">Calendar</h2></td></tr></table>tconvertino@gmail.com</div></td></tr><tr><=
td style=3D"font-size: 0; padding: 0; text-align: left; word-break: break-w=
ord;;padding-bottom:24px;"><div style=3D"font-family: Roboto, sans-serif;fo=
nt-style: normal; font-weight: 400; font-size: 14px; line-height: 20px; let=
ter-spacing: 0.2px;color: #3c4043; text-decoration: none;" class=3D"primary=
-text" role=3D"presentation"><table border=3D"0" cellpadding=3D"0" cellspac=
ing=3D"0" role=3D"presentation" style=3D"padding-bottom: 4px;"><tr><td><h2 =
class=3D"primary-text" style=3D"font-size: 14px;color: #3c4043; text-decora=
tion: none;font-weight: 700;-webkit-font-smoothing: antialiased;margin: 0; =
padding: 0;">Organizer</h2></td></tr></table><div style=3D"color: #3c4042;"=
><span class=3D"notranslate"><a class=3D"primary-text underline-on-hover" s=
tyle=3D"display: inline-block;;color: #3c4043; text-decoration: none;" href=
=3D"mailto:tconvertino@gmail.com">tconvertino@gmail.com</a></span><span ite=
mprop=3D"organizer" itemscope itemtype=3D"http://schema.org/Person"><meta i=
temprop=3D"name" content=3D"tconvertino@gmail.com"/><meta itemprop=3D"email=
" content=3D"tconvertino@gmail.com"/></span></div></div></td></tr><tr><td s=
tyle=3D"font-size: 0; padding: 0; text-align: left; word-break: break-word;=
;padding-bottom:24px;"><div style=3D"font-family: Roboto, sans-serif;font-s=
tyle: normal; font-weight: 400; font-size: 14px; line-height: 20px; letter-=
spacing: 0.2px;color: #3c4043; text-decoration: none;" class=3D"primary-tex=
t" role=3D"presentation"><table border=3D"0" cellpadding=3D"0" cellspacing=
=3D"0" role=3D"presentation" style=3D"padding-bottom: 4px;"><tr><td><h2 cla=
ss=3D"primary-text" style=3D"font-size: 14px;color: #3c4043; text-decoratio=
n: none;font-weight: 700;-webkit-font-smoothing: antialiased;margin: 0; pad=
ding: 0;">Guests</h2></td></tr></table><div style=3D"padding-bottom: 4px; t=
ext-align: left;;color: #3c4042;"></div><a href=3D"https://calendar.google.=
com/calendar/r?eid=3DN2JzbDA5cmIxNDRxMzVmZWZsYmwwcTJ2NTggdGNvbnZlcnRpbm9AbQ=
&amp;es=3D1" style=3D"display: inline-block;;color: #1a73e8; text-decoratio=
n: none;font-weight: 700;" target=3D"_blank" class=3D"accent-text underline=
-on-hover">View all guest info</a></div></td></tr></table></td></tr></tbody=
></table></div><!--[if mso | IE]></tr></table><![endif]--></td></tr></tbody=
></table><table border=3D"0" cellpadding=3D"0" cellspacing=3D"0" role=3D"pr=
esentation" align=3D"center" style=3D"width:100%;" class=3D""><tbody><tr><t=
d style=3D"font-size: 0; padding: 0; text-align: left; word-break: break-wo=
rd;;padding:4px 12px;" class=3D"" align=3D"left"><div class=3D"secondary-te=
xt" style=3D"color: #70757a; text-decoration: none;font-family: Roboto, san=
s-serif;font-size: 12px; line-height: 16px; mso-line-height-rule: exactly; =
text-align: left;"><p>Invitation from <a href=3D"https://calendar.google.co=
m/calendar/" class=3D"accent-text underline-on-hover" style=3D"font-family:=
Roboto, sans-serif;font-size: 12px; line-height: 16px; mso-line-height-rul=
e: exactly;;color: #1a73e8; text-decoration: none;" target=3D"_blank">Googl=
e Calendar</a></p><p>You are receiving this email because you are subscribe=
d to calendar notifications. To stop receiving these emails, go to <a href=
=3D"https://calendar.google.com/calendar/r/settings" class=3D"accent-text u=
nderline-on-hover" style=3D"font-family: Roboto, sans-serif;font-size: 12px=
; line-height: 16px; mso-line-height-rule: exactly;;color: #1a73e8; text-de=
coration: none;" target=3D"_blank">Calendar settings</a>, select this calen=
dar, and change "Other notifications".</p><p>Forwarding this invitation cou=
ld allow any recipient to send a response to the organizer, be added to the=
guest list, invite others regardless of their own invitation status, or mo=
dify your RSVP. <a class=3D"accent-text underline-on-hover" style=3D"font-f=
amily: Roboto, sans-serif;font-size: 12px; line-height: 16px; mso-line-heig=
ht-rule: exactly;;color: #1a73e8; text-decoration: none;" href=3D"https://s=
upport.google.com/calendar/answer/37135#forwarding">Learn more</a></p></div=
></td></tr></tbody></table></td></tr></tbody></table></span></span></body><=
/html>
--000000000000f22916064898bf45--

View File

@@ -0,0 +1,733 @@
Return-Path: <couchmoney+caf_=gmail=xinu.tv@gmail.com>
Delivered-To: bill@xinu.tv
Received: from phx.xinu.tv [74.207.253.222]
by nixos-01.h.xinu.tv with IMAP (fetchmail-6.5.6)
for <wathiede@localhost> (single-drop); Sat, 17 Jan 2026 07:15:51 -0800 (PST)
Received: from phx.xinu.tv
by phx.xinu.tv with LMTP
id EBhFKaana2lD2BMAJR8clQ
(envelope-from <couchmoney+caf_=gmail=xinu.tv@gmail.com>)
for <bill@xinu.tv>; Sat, 17 Jan 2026 07:15:50 -0800
X-Original-To: gmail@xinu.tv
Received: from mail-lf1-f47.google.com (mail-lf1-f47.google.com [209.85.167.47])
by phx.xinu.tv (Postfix) with ESMTPS id 9ED9E80023
for <gmail@xinu.tv>; Sat, 17 Jan 2026 07:15:49 -0800 (PST)
Received: by mail-lf1-f47.google.com with SMTP id 2adb3069b0e04-59b72a1e2f0so3298670e87.0
for <gmail@xinu.tv>; Sat, 17 Jan 2026 07:15:49 -0800 (PST)
ARC-Seal: i=2; a=rsa-sha256; t=1768662948; cv=pass;
d=google.com; s=arc-20240605;
b=P8ukIhKVppSoOtIG2772uR9xtbgbpQjTdNpTjI3WHjWj7WKqgmV4ndotO/y7GwqvOX
OwYJbwY9cIR/PPOGf0/UfUSfPTp6iWwfapN3B5YDoSrOk3ef8e0HXRcSMeIG9D0JWfKB
YTFLsg6dqxdrPLsyDhFeJQQFK4+qb4gndM7SEw9UZtvvEbzGmGro9QFcO+uXsPZ6jQsb
UNn7VJhOqi3sc+mNAmPSNN1KSjeZ5wQoJDKHAhc39P7HlCI6Upm3UgyejjToTNa5pFUH
YfqgrPEU0NsJWZNUfnItYhChHXZyHE9iUcK8eDO7qRyvVAwkEdxmc34EC4hOJsHxdLli
ik4A==
ARC-Message-Signature: i=2; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20240605;
h=to:from:subject:date:message-id:auto-submitted:sender:reply-to
:mime-version:dkim-signature:dkim-signature:delivered-to;
bh=DSYDWRwqF5i+Yosvco75LpPB3hH2JqH2JZElsHalDcM=;
fh=opiyoKXgWpm8kcAa3Nw4m5S9DBRIdfMLcfsQwVK5zKs=;
b=hyfgpNzlhW6aPYNzOd5CkaqQ2qYr/Sb25Qti1ez7y+n0SxJM/QbpyMCafAFv1s6vSR
jdbmeaofRjmDkL0uDdoQb0E5TnycNKP3r4FWEKc2KZkfnKEtWWOivFDIvQDPF4NpYNn3
9SFZkfmVa2TSfrnH4wRG37gCQgc6gzDpl6a2oZRmM4T44xbIXCIseFSalR+oUTFCJU3M
XSBCNjV7w/V8S2hiH72Ace8enjUG+DRbeE8aYiNwu0XkECojPxGgBuM9OJPSmYzNt3LW
7M+weqkx0Byoi6UDaI2NQqXH4wkSrjmIFcX+84jglfl2zOsPGwgehJYdo/QcPMXtQohY
sMZA==;
darn=xinu.tv
ARC-Authentication-Results: i=2; mx.google.com;
dkim=pass header.i=@google.com header.s=20230601 header.b=UV12BjMR;
dkim=pass header.i=@gmail.com header.s=20230601 header.b=HTQ+XWtf;
spf=pass (google.com: domain of tconvertino@gmail.com designates 209.85.220.73 as permitted sender) smtp.mailfrom=tconvertino@gmail.com;
dmarc=pass (p=NONE sp=QUARANTINE dis=NONE) header.from=gmail.com;
dara=pass header.i=@gmail.com
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=1e100.net; s=20230601; t=1768662948; x=1769267748;
h=to:from:subject:date:message-id:auto-submitted:sender:reply-to
:mime-version:dkim-signature:dkim-signature:delivered-to
:x-forwarded-for:x-forwarded-to:x-gm-message-state:from:to:cc
:subject:date:message-id:reply-to;
bh=DSYDWRwqF5i+Yosvco75LpPB3hH2JqH2JZElsHalDcM=;
b=wCE8iOXwi+1PwnSvMIbzsg5Pdo1jXyrqQjqZN3UN9bXyLJmoKV+a5l4cE7La2TkWIe
zdw/qcgetdMWPrJncmnLYptNMOMhHf8OmgDbtKbZLo1WVnY6OcnTTCWljhm9l3p1sNCJ
NNvrE79CNsi4mB30/gh5GUSFldNs4CKXLoX5U1JyaR0URBNSdgTtWHQEk9Zip6lzSJ/W
3myHdqXjlFIBn8JdN2F+Uvpe4SOcpUYivDzRzGgKBVBybgL+7/B2FVxu1DSqImvvdnew
G45//ZKZLk1NZTwB9b3fVu1ZRrtVwXykyTsLWQs/L+h2RatzY5cbRknOUEg+Sh12glbJ
Mvzg==
X-Forwarded-Encrypted: i=2; AJvYcCWTlZh7K3d3elcvXPqK9OnJxLWgTqppYJqLWqVOMFJDYDcmSN8i4AmT2kLKruApNvLk5mchcQ==@xinu.tv
X-Gm-Message-State: AOJu0YzRCKajwo80HG2ZJYkIh92JNKbnNkbFqaG5Dye25AAdsFY1p/Sa
HUpoeDQQxTSXezOEygAE9wR8sFTjlB4uRgVqtHJ5s0ml5G9sQVG9Ir31aJU5vBs4OpeeAa01KHq
OqoyqsCDybPJBW43d/qWOxpcpIORkUKMmPkNGNzU1+hFlox6fAt4TasNy8lbiLw==
X-Received: by 2002:a05:6512:2507:b0:59b:b55a:a293 with SMTP id 2adb3069b0e04-59bb55aa3b5mr1328824e87.34.1768662947779;
Sat, 17 Jan 2026 07:15:47 -0800 (PST)
X-Forwarded-To: gmail@xinu.tv
X-Forwarded-For: couchmoney@gmail.com gmail@xinu.tv
Delivered-To: couchmoney@gmail.com
Received: by 2002:a05:6504:2382:b0:2d3:710a:2457 with SMTP id h2csp2302771lty;
Sat, 17 Jan 2026 07:15:46 -0800 (PST)
X-Received: by 2002:a05:6808:221e:b0:45a:6ef9:79 with SMTP id 5614622812f47-45c9d86c3acmr2292013b6e.52.1768662946398;
Sat, 17 Jan 2026 07:15:46 -0800 (PST)
ARC-Seal: i=1; a=rsa-sha256; t=1768662946; cv=none;
d=google.com; s=arc-20240605;
b=KdiLKIN7OQmPF5O3bozzB9t0JL/To2npnCMuMVg4Iawwtni3NbkM+Y4nXB3Wtm5kAj
Sa6HsrnbgNz3D8lp3nJB8bXexvVTc73ZQFDKtKnDRj7sv/8eK0qMpCkRb3Rhgu2cQEfa
lYv7E0pEj0qcMxDM5osGhdhrwsV8BnEboHL38bCfhDxSmcmGsmPgiC5IWiyhcWq8G2W6
XwwPDdcHXk4wLwy4AQ1/NR5q130ELNP1f2e1Xq+xxLhfuV7GzhgOCTxYBIzrzJdM0kC/
Sxd1ejz+WLbowjVP2s4rStRtCvHU/g1LlPGmdnmyN9wQ+Tkya+Q7LjRWWB99e04qENOE
3E9g==
ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20240605;
h=to:from:subject:date:message-id:auto-submitted:sender:reply-to
:mime-version:dkim-signature:dkim-signature;
bh=DSYDWRwqF5i+Yosvco75LpPB3hH2JqH2JZElsHalDcM=;
fh=mbzrMIWIgWMC0ni1xEx+ViW4J0RLAdLdPT2cX81nTlk=;
b=R24D+rdvNBJO+lAlMBX/5KkpnryHlsj/t5x1rJvWsKG/yrKJe81Epa9aDOWh5pljV4
Cg7IH6KrYLzYYMNQ83stCE7zIBs7sjtBl81bd/OpWCoXgJEAnE2bYs958fk3z8jINzBv
huNTL3ijCNiZeI/y5Ye/S8avkRfWf1tNn7nNc7oVf6Xc+ujGIk28KL3EGerj29au519E
MqL6BVYlSAx9VcBXY2vRZft2xDJo+JiKIw2n+XJT7+Ax0LubBX5BCfcRinBFT/XsVFTD
zxNaxxq9DbbAtr78NgacXMOxmo8bJYi/MD0wo0BVZBxd3kXhKF367iEKBreXu7tYPp5i
d7tg==;
dara=google.com
ARC-Authentication-Results: i=1; mx.google.com;
dkim=pass header.i=@google.com header.s=20230601 header.b=UV12BjMR;
dkim=pass header.i=@gmail.com header.s=20230601 header.b=HTQ+XWtf;
spf=pass (google.com: domain of tconvertino@gmail.com designates 209.85.220.73 as permitted sender) smtp.mailfrom=tconvertino@gmail.com;
dmarc=pass (p=NONE sp=QUARANTINE dis=NONE) header.from=gmail.com;
dara=pass header.i=@gmail.com
Received: from mail-sor-f73.google.com (mail-sor-f73.google.com. [209.85.220.73])
by mx.google.com with SMTPS id 5614622812f47-45ca0e0d20dsor1067583b6e.18.2026.01.17.07.15.46
for <couchmoney@gmail.com>
(Google Transport Security);
Sat, 17 Jan 2026 07:15:46 -0800 (PST)
Received-SPF: pass (google.com: domain of tconvertino@gmail.com designates 209.85.220.73 as permitted sender) client-ip=209.85.220.73;
Authentication-Results: mx.google.com;
dkim=pass header.i=@google.com header.s=20230601 header.b=UV12BjMR;
dkim=pass header.i=@gmail.com header.s=20230601 header.b=HTQ+XWtf;
spf=pass (google.com: domain of tconvertino@gmail.com designates 209.85.220.73 as permitted sender) smtp.mailfrom=tconvertino@gmail.com;
dmarc=pass (p=NONE sp=QUARANTINE dis=NONE) header.from=gmail.com;
dara=pass header.i=@gmail.com
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=google.com; s=20230601; t=1768662946; x=1769267746; dara=google.com;
h=to:from:subject:date:message-id:auto-submitted:sender:reply-to
:mime-version:from:to:cc:subject:date:message-id:reply-to;
bh=DSYDWRwqF5i+Yosvco75LpPB3hH2JqH2JZElsHalDcM=;
b=UV12BjMRpTIHG34HTOnEB4ApBu4DnXlft6fU+aIV2TzL1HFmpDf9rz1riEoHyaWSGU
P7ydXwhdNhl1b9SVRu+jhtsd0Wt7vLGTb/ru1OZEnxC8P2A9jCdqjT3J9GwtpZxcv4nh
ipdJF6LYZ6yt1H2AftMJf59/9L1HbiwMs3MceHqj+R0AlU4KNtuevB/ImTump5XiSH/8
iDgvVfqyvm2kYwp9yV4cMXWUIQG6bWHB/No01kLG2PtgOy45chmfp/P22/ZyY3wEFd/U
044fReZdGmqwqGfhM81y2+WVwuzQltjv8dZ2yf1SeSVOLmrc8Uc5lYFvzQJK7NiaVsyv
sYWQ==
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=gmail.com; s=20230601; t=1768662946; x=1769267746; dara=google.com;
h=to:from:subject:date:message-id:auto-submitted:sender:reply-to
:mime-version:from:to:cc:subject:date:message-id:reply-to;
bh=DSYDWRwqF5i+Yosvco75LpPB3hH2JqH2JZElsHalDcM=;
b=HTQ+XWtfciYuHpwOHCqsF6ctCQc5eRWblgRxVdEfjGFYy2TmgSb/jA5+HldQ7hxk5S
mfppkAhF82R1zQM96e2QMnoamqslIpum/+pvdIfxbvXiAytCNOmQZoc1xhlTz5CCkpni
g23H0O3kiXSdzhG4uLJgsGMaNR/rAkslbMW7ARNTluvuWM60d9At9ZCazZCVLq2C0nhS
0HvaBa5uxi19A6l/55NmEbhPvAHXSw7V9SEUF8TTBuGLfZEkrjUF+RxfAgN/+NuZR5o7
yOAlNI/iEi+F8T7Is+ZmjbHYysZu01/nzldZnZEQamYuUQBeKpbNHSEGbVrpD3+AuT6f
/t8w==
MIME-Version: 1.0
X-Received: by 2002:a05:6820:1ca1:b0:65f:7009:d6e5 with SMTP id
006d021491bc7-661188b9d4bmr1995718eaf.22.1768662945924; Sat, 17 Jan 2026
07:15:45 -0800 (PST)
Reply-To: tconvertino@gmail.com
Sender: Google Calendar <calendar-notification@google.com>
Auto-Submitted: auto-generated
Message-ID: <calendar-3c0029f2-74ee-41a1-b1f0-0716665ce0d3@google.com>
Date: Sat, 17 Jan 2026 15:15:45 +0000
Subject: Updated invitation: painting class @ Thu Feb 12, 2026 7pm - 9pm (PST) (tconvertino@gmail.com)
From: tconvertino@gmail.com
To: couchmoney@gmail.com
Content-Type: multipart/alternative; boundary="0000000000009e8d3e064896eeed"
X-Rspamd-Queue-Id: 9ED9E80023
X-Rspamd-Server: phx
X-Spamd-Result: default: False [-0.70 / 15.00];
URI_COUNT_ODD(1.00)[1];
ARC_ALLOW(-1.00)[google.com:s=arc-20240605:i=2];
DMARC_POLICY_ALLOW(-0.50)[gmail.com,none];
MANY_INVISIBLE_PARTS(0.20)[3];
R_DKIM_ALLOW(-0.20)[google.com:s=20230601,gmail.com:s=20230601];
R_SPF_ALLOW(-0.20)[+ip4:209.85.128.0/17:c];
MIME_GOOD(-0.10)[multipart/alternative,text/plain];
MIME_BASE64_TEXT(0.10)[];
FREEMAIL_ENVFROM(0.00)[gmail.com];
RCVD_TLS_LAST(0.00)[];
RCVD_COUNT_THREE(0.00)[3];
FREEMAIL_TO(0.00)[gmail.com];
TAGGED_FROM(0.00)[caf_=gmail=xinutv];
FREEMAIL_FROM(0.00)[gmail.com];
FREEMAIL_REPLYTO(0.00)[gmail.com];
MIME_TRACE(0.00)[0:+,1:+,2:~];
RCPT_COUNT_ONE(0.00)[1];
FORGED_SENDER(0.00)[tconvertino@gmail.com,couchmoney@gmail.com];
RCVD_IN_DNSWL_NONE(0.00)[209.85.220.73:received];
MISSING_XM_UA(0.00)[];
HAS_REPLYTO(0.00)[tconvertino@gmail.com];
TO_DN_NONE(0.00)[];
DNSWL_BLOCKED(0.00)[209.85.167.47:from];
FORGED_SENDER_FORWARDING(0.00)[];
FROM_NEQ_ENVFROM(0.00)[tconvertino@gmail.com,couchmoney@gmail.com];
DWL_DNSWL_NONE(0.00)[google.com:dkim];
DKIM_TRACE(0.00)[google.com:+,gmail.com:+];
DWL_DNSWL_BLOCKED(0.00)[gmail.com:dkim];
TO_DOM_EQ_FROM_DOM(0.00)[];
FROM_NO_DN(0.00)[];
FWD_GOOGLE(0.00)[couchmoney@gmail.com];
ASN(0.00)[asn:15169, ipnet:209.85.128.0/17, country:US];
RWL_MAILSPIKE_POSSIBLE(0.00)[209.85.167.47:from];
REPLYTO_EQ_FROM(0.00)[]
X-Rspamd-Action: no action
X-TUID: SJoEMVYLjYPK
--0000000000009e8d3e064896eeed
Content-Type: text/plain; charset="UTF-8"; format=flowed; delsp=yes
Content-Transfer-Encoding: base64
VGhpcyBldmVudCBoYXMgYmVlbiB1cGRhdGVkDQpDaGFuZ2VkOiB0aW1lDQoNCg0KcGFpbnRpbmcg
Y2xhc3MNClRodXJzZGF5IEZlYiAxMiwgMjAyNiDii4UgN3BtIOKAkyA5cG0NClBhY2lmaWMgVGlt
ZSAtIExvcyBBbmdlbGVzDQoNCg0KDQpPcmdhbml6ZXINCnRjb252ZXJ0aW5vQGdtYWlsLmNvbQ0K
dGNvbnZlcnRpbm9AZ21haWwuY29tDQoNCn5+Ly9+fg0KSW52aXRhdGlvbiBmcm9tIEdvb2dsZSBD
YWxlbmRhcjogaHR0cHM6Ly9jYWxlbmRhci5nb29nbGUuY29tL2NhbGVuZGFyLw0KDQpZb3UgYXJl
IHJlY2VpdmluZyB0aGlzIGVtYWlsIGJlY2F1c2UgeW91IGFyZSBzdWJzY3JpYmVkIHRvIGNhbGVu
ZGFyICANCm5vdGlmaWNhdGlvbnMuIFRvIHN0b3AgcmVjZWl2aW5nIHRoZXNlIGVtYWlscywgZ28g
dG8gIA0KaHR0cHM6Ly9jYWxlbmRhci5nb29nbGUuY29tL2NhbGVuZGFyL3Ivc2V0dGluZ3MsIHNl
bGVjdCB0aGlzIGNhbGVuZGFyLCBhbmQgIA0KY2hhbmdlICJPdGhlciBub3RpZmljYXRpb25zIi4N
Cg0KRm9yd2FyZGluZyB0aGlzIGludml0YXRpb24gY291bGQgYWxsb3cgYW55IHJlY2lwaWVudCB0
byBzZW5kIGEgcmVzcG9uc2UgdG8gIA0KdGhlIG9yZ2FuaXplciwgYmUgYWRkZWQgdG8gdGhlIGd1
ZXN0IGxpc3QsIGludml0ZSBvdGhlcnMgcmVnYXJkbGVzcyBvZiAgDQp0aGVpciBvd24gaW52aXRh
dGlvbiBzdGF0dXMsIG9yIG1vZGlmeSB5b3VyIFJTVlAuDQoNCkxlYXJuIG1vcmUgaHR0cHM6Ly9z
dXBwb3J0Lmdvb2dsZS5jb20vY2FsZW5kYXIvYW5zd2VyLzM3MTM1I2ZvcndhcmRpbmcNCg==
--0000000000009e8d3e064896eeed
Content-Type: text/html; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable
<!doctype html><html xmlns=3D"http://www.w3.org/1999/xhtml" xmlns:v=3D"urn:=
schemas-microsoft-com:vml" xmlns:o=3D"urn:schemas-microsoft-com:office:offi=
ce"><head><title></title><!--[if !mso]><meta http-equiv=3D"X-UA-Compatible"=
content=3D"IE=3Dedge"><![endif]--><meta http-equiv=3D"Content-Type" conten=
t=3D"text/html; charset=3DUTF-8"><meta name=3D"viewport" content=3D"width=
=3Ddevice-width,initial-scale=3D1"><meta name=3D"color-scheme" content=3D"l=
ight dark"><meta name=3D"supported-color-schemes" content=3D"light dark">
<style>
body, html {
font-family: Roboto, Helvetica, Arial, sans-serif;
}
body {
margin: 0;
padding: 0;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
#outlook a {
padding: 0;
}
.ReadMsgBody {
width: 100%;
}
.ExternalClass {
width: 100%;
}
.ExternalClass * {
line-height: 100%;
}
table,
td {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if !mso]><!-->
<style>
@media only screen and (max-width:580px) {
@-ms-viewport {
width: 320px;
}
@viewport {
width: 320px;
}
}
</style>
<!--<![endif]-->
<!--[if mso]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!--[if lte mso 11]>
<style>
.outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<!--[if !mso]><!-- -->
<style>body, html {font-family:Roboto,Helvetica,Arial,sans-serif;}@font-f=
ace {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-stretch: normal;
src: url(//fonts.gstatic.com/s/roboto/v48/KFOMCnqEu92Fr1ME7kSn66aGLdTylUA=
MQXC89YmC2DPNWubEbVmUiA8.ttf) format('truetype');
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-stretch: normal;
src: url(//fonts.gstatic.com/s/roboto/v48/KFOMCnqEu92Fr1ME7kSn66aGLdTylUA=
MQXC89YmC2DPNWub2bVmUiA8.ttf) format('truetype');
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
font-stretch: normal;
src: url(//fonts.gstatic.com/s/roboto/v48/KFOMCnqEu92Fr1ME7kSn66aGLdTylUA=
MQXC89YmC2DPNWuYjalmUiA8.ttf) format('truetype');
}
@font-face {
font-family: 'Material Icons Extended';
font-style: normal;
font-weight: 400;
src: url(//fonts.gstatic.com/s/materialiconsextended/v154/kJEjBvgX7BgnkSr=
UwT8UnLVc38YydejYY-oE_LvM.ttf) format('truetype');
}
@font-face {
font-family: 'Google Material Icons';
font-style: normal;
font-weight: 400;
src: url(//fonts.gstatic.com/s/googlematerialicons/v144/Gw6kwdfw6UnXLJCcm=
afZyFRXb3BL9rvi0QZG3g.otf) format('opentype');
}
.google-material-icons {
font-family: 'Google Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
}
@font-face {
font-family: 'Google Material Icons Filled';
font-style: normal;
font-weight: 400;
src: url(//fonts.gstatic.com/s/googlematerialiconsfilled/v118/WWXFlimHYg6=
HKI3TavMkbKdhBmDvgach8TVpeGsuueSZJH4.otf) format('opentype');
}
.google-material-icons-filled {
font-family: 'Google Material Icons Filled';
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
}
@font-face {
font-family: 'Google Sans';
font-style: normal;
font-weight: 400;
src: url(//fonts.gstatic.com/s/googlesans/v14/4UaGrENHsxJlGDuGo1OIlL3Owps=
.ttf) format('truetype');
}
@font-face {
font-family: 'Google Sans';
font-style: normal;
font-weight: 500;
src: url(//fonts.gstatic.com/s/googlesans/v14/4UabrENHsxJlGDuGo1OIlLU94Yt=
zCwM.ttf) format('truetype');
}
@font-face {
font-family: 'Google Sans';
font-style: normal;
font-weight: 700;
src: url(//fonts.gstatic.com/s/googlesans/v14/4UabrENHsxJlGDuGo1OIlLV154t=
zCwM.ttf) format('truetype');
}
</style><!--<![endif]-->
<style>
.body-container {
padding-left: 16px;
padding-right: 16px;
}
</style>
=20
<style>
u+.body .body-container,
body[data-outlook-cycle] .body-container,
#MessageViewBody .body-container {
padding-left: 0;
padding-right: 0;
}
</style>
=20
<style>
@media only screen and (min-width:580px) {
.column-per-37 {
width: 37% !important;
max-width: 37%;
}
.column-per-63 {
width: 63% !important;
max-width: 63%;
}
}
</style>
=20
<style>
.appointment-buttons th {
display: block;
clear: both;
float: left;
margin-top: 12px;
}
.appointment-buttons th a {
float: left;
}
#MessageViewBody .appointment-buttons th {
margin-top: 24px;
}
</style>
=20
<style>
@media only screen and (max-width:580px) {
table.full-width-mobile {
width: 100% !important;
}
td.full-width-mobile {
width: auto !important;
}
}
</style>
<style>
.main-container-inner,
.info-bar-inner {
padding: 12px 16px !important;
}
.main-column-table-ltr {
padding-right: 0 !important;
}
.main-column-table-rtl {
padding-left: 0 !important;
}
@media only screen and (min-width:580px) {
.main-container-inner {
padding: 24px 32px !important;
}
.info-bar-inner {
padding: 12px 32px !important;
}
.main-column-table-ltr {
padding-right: 32px !important;
}
.main-column-table-rtl {
padding-left: 32px !important;
}
.appointment-buttons th {
display: table-cell;
clear: none;
}
}
.primary-text {
color: #3c4043 !important;
}
.secondary-text,
.phone-number a {
color: #70757a !important;
}
.accent-text {
color: #1a73e8 !important;
}
.accent-text-dark {
color: #185abc !important;
}
.grey-button-text,
.attachment-chip a {
color: #5f6368 !important;
}
.primary-button {
background-color: #1a73e8 !important;
}
.primary-button-text {
color: #fff !important;
}
.underline-on-hover:hover {
text-decoration: underline !important;
}
.grey-infobar-text {
color: #202124 !important;
}
@media (prefers-color-scheme: dark) {
.primary-text:not([class^=3D"x_"]) {
color: #e8eaed !important;
}
.secondary-text:not([class^=3D"x_"]),
.phone-number:not([class^=3D"x_"]) a {
color: #9aa0a6 !important;
}
.grey-button-text:not([class^=3D"x_"]),
.attachment-chip:not([class^=3D"x_"]) a {
color: #bdc1c6 !important;
}
.accent-text:not([class^=3D"x_"]),
.hairline-button-text:not([class^=3D"x_"]) {
color: #8ab4f8 !important;
}
.primary-button:not([class^=3D"x_"]) {
background-color: #8ab4f8 !important;
}
.primary-button-text:not([class^=3D"x_"]) {
color: #202124 !important;
}
}
</style>
<style>
@media (prefers-color-scheme: dark) {
.cse-banner:not([class^=3D"x_"]) {
background-color: #3c4043 !important; /* Google Grey 800 */
}
.encryption-icon:not([class^=3D"x_"]) {
/* WARNING: This causes the whole style tag to get stripped in Gm=
ail. */
background-image: url('https://fonts.gstatic.com/s/i/googlemateri=
aliconsfilled/encrypted/v3/gm_grey200-24dp/2x/gm_filled_encrypted_gm_grey20=
0_24dp.png') !important;
}
}
</style>
<!--[if !mso]><!-->
<style>
.prevent-link a {
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
</style>
<!--<![endif]-->
<!--[if mso | IE]>
<style>
.main-container-inner {
padding: 24px 32px !important;
}
.info-bar-inner {
padding: 12px 32px !important;
}
.cse-banner .encryption-icon {
/* We use the IE workaround instead. */
background-image: none !important;
}
.cse-banner .encryption-icon .ms-fallback {
display: block !important;
}
/* NB: Some MS clients ignore dark-scheme styling and apply their o=
wn, so there's nothing we can do to help there. */
@media (prefers-color-scheme: dark) {
.cse-banner:not([class^=3D"x_"]) .encryption-icon .ms-fallback {
display: none !important;
}
.cse-banner:not([class^=3D"x_"]) .encryption-icon .ms-fallback-da=
rk {
display: block !important;
}
}
</style>
<![endif]-->
</head><body class=3D"body"><span itemscope itemtype=3D"http://schema.org=
/InformAction"><span itemprop=3D"object" itemscope itemtype=3D"http://schem=
a.org/Event"><meta itemprop=3D"eventStatus" content=3D"http://schema.org/Ev=
entRescheduled"/><span itemprop=3D"publisher" itemscope itemtype=3D"http://=
schema.org/Organization"><meta itemprop=3D"name" content=3D"Google Calendar=
"/></span><meta itemprop=3D"eventId/googleCalendar" content=3D"4octcgvhijqj=
m45h8d9dr4iq1m"/><span style=3D"display: none; font-size: 1px; color: #fff;=
line-height: 1px; height: 0; max-height: 0; width: 0; max-width: 0; opacit=
y: 0; overflow: hidden;" itemprop=3D"name">painting class</span><meta itemp=
rop=3D"url" content=3D"https://calendar.google.com/calendar/r?eid=3DNG9jdGN=
ndmhpanFqbTQ1aDhkOWRyNGlxMW0gdGNvbnZlcnRpbm9AbQ&amp;es=3D1"/><span aria-hid=
den=3D"true"><time itemprop=3D"startDate" datetime=3D"20260213T030000Z"></t=
ime><time itemprop=3D"endDate" datetime=3D"20260213T050000Z"></time></span>=
<div style=3D"display: none; font-size: 1px; color: #fff; line-height: 1px;=
height: 0; max-height: 0; width: 0; max-width: 0; opacity: 0; overflow: hi=
dden;">You have been invited by tconvertino@gmail.com to attend an event na=
med painting class on Thursday Feb 12, 2026 =E2=8B=85 7pm =E2=80=93 9pm (Pa=
cific Time - Los Angeles).</div><table border=3D"0" cellpadding=3D"0" cells=
pacing=3D"0" role=3D"presentation" align=3D"center" style=3D"width:100%;" c=
lass=3D"body-container"><tbody><tr><td style=3D"" class=3D"" align=3D"left"=
><!--[if mso | IE]><table border=3D"0" cellpadding=3D"0" cellspacing=3D"0" =
role=3D"presentation"><tr><td height=3D"16" style=3D"height:16px;"><![endif=
]--><div style=3D"height:16px;" aria-hidden=3D"true"> &nbsp; </div><!--[if =
mso | IE]></td></tr></table><![endif]--><table border=3D"0" cellpadding=3D"=
0" cellspacing=3D"0" role=3D"presentation" align=3D"center" style=3D"width:=
100%;" class=3D""><tbody><tr><td style=3D"background-color: #e6f4ea;color: =
#0d5327;padding: 12px 32px; border-radius: 8px;font-family: Roboto, sans-se=
rif;font-size: 14px; line-height: 20px;text-align: left;" class=3D"info-bar=
-inner"><span style=3D"font-weight: 700;">This event has been updated</span=
><br/><span style=3D"display:none" itemprop=3D"about" itemscope itemtype=3D=
"http://schema.org/Thing/Clock"><meta itemprop=3D"description" content=3D"T=
ime updated"/></span><div style=3D""><span style=3D"font-weight: 700;">Chan=
ged:</span> time</div></td></tr></tbody></table><!--[if mso | IE]><table bo=
rder=3D"0" cellpadding=3D"0" cellspacing=3D"0" role=3D"presentation"><tr><t=
d height=3D"12" style=3D"height:12px;"><![endif]--><div style=3D"height:12p=
x;" aria-hidden=3D"true"> &nbsp; </div><!--[if mso | IE]></td></tr></table>=
<![endif]--><table border=3D"0" cellpadding=3D"0" cellspacing=3D"0" role=3D=
"presentation" align=3D"center" style=3D"width:100%;" class=3D""><tbody><tr=
><td style=3D"border: solid 1px #dadce0; border-radius: 8px; direction: rtl=
; font-size: 0; padding: 24px 32px; text-align: left; vertical-align: top;"=
class=3D"main-container-inner"><!--[if mso | IE]><table border=3D"0" cellp=
adding=3D"0" cellspacing=3D"0" role=3D"presentation"><tr><![endif]--><div c=
lass=3D"" style=3D"font-size: 13px; text-align: left; direction: ltr; displ=
ay: inline-block; vertical-align: top; width: 100%;overflow: hidden; word-w=
rap: break-word;"><table border=3D"0" cellpadding=3D"0" cellspacing=3D"0" r=
ole=3D"presentation" width=3D"100%" class=3D"main-column-table-ltr" style=
=3D"padding-right: 32px; padding-left: 0;;table-layout: fixed;"><tbody><tr>=
<td class=3D"main-column-td" style=3D"padding:0; vertical-align:top;"><tabl=
e border=3D"0" cellpadding=3D"0" cellspacing=3D"0" role=3D"presentation" wi=
dth=3D"100%" style=3D"table-layout: fixed;"><tr><td style=3D"font-size: 0; =
padding: 0; text-align: left; word-break: break-word;;padding-bottom:24px;"=
><div style=3D"font-family: Roboto, sans-serif;font-style: normal; font-wei=
ght: 400; font-size: 14px; line-height: 20px; letter-spacing: 0.2px;color: =
#3c4043; text-decoration: none;" class=3D"primary-text" role=3D"presentatio=
n"><span aria-hidden=3D"true"><time itemprop=3D"startDate" datetime=3D"2026=
0213T030000Z"></time><time itemprop=3D"endDate" datetime=3D"20260213T050000=
Z"></time></span><table border=3D"0" cellpadding=3D"0" cellspacing=3D"0" ro=
le=3D"presentation" style=3D"padding-bottom: 4px;"><tr><td><h2 class=3D"pri=
mary-text" style=3D"font-size: 14px;color: #3c4043; text-decoration: none;f=
ont-weight: 700;-webkit-font-smoothing: antialiased;margin: 0; padding: 0;"=
>When</h2></td><td style=3D"width: 8px;"></td><td style=3D"padding-top: 2px=
; padding-bottom: 3px;"><div style=3D"background-color: #1e8e3e; border-rad=
ius: 10px; padding: 1px 5px; line-height: 13px;"><span style=3D"color: whit=
e; font-size: 11px; font-weight: 700; letter-spacing: 0.8px; text-transform=
: uppercase; vertical-align: top;">CHANGED</span></div></td></tr></table><s=
pan>Thursday Feb 12, 2026 =E2=8B=85 7pm =E2=80=93 9pm (Pacific Time - Los A=
ngeles)<br/><span style=3D"text-decoration: line-through;"><del><span style=
=3D"display: none; font-size: 1px; color: #fff; line-height: 1px; height: 0=
; max-height: 0; width: 0; max-width: 0; opacity: 0; overflow: hidden;font-=
size: 0; display: block;">Old: </span>Thursday Jan 22, 2026 =E2=8B=85 7pm =
=E2=80=93 9pm (Pacific Time - Los Angeles)</del></span></span></div></td></=
tr><tr><td style=3D"font-size: 0; padding: 0; text-align: left; word-break:=
break-word;;padding-bottom:24px;"><div style=3D"font-family: Roboto, sans-=
serif;font-style: normal; font-weight: 400; font-size: 14px; line-height: 2=
0px; letter-spacing: 0.2px;color: #3c4043; text-decoration: none;" class=3D=
"primary-text" role=3D"presentation"><table border=3D"0" cellpadding=3D"0" =
cellspacing=3D"0" role=3D"presentation" style=3D"padding-bottom: 4px;"><tr>=
<td><h2 class=3D"primary-text" style=3D"font-size: 14px;color: #3c4043; tex=
t-decoration: none;font-weight: 700;-webkit-font-smoothing: antialiased;mar=
gin: 0; padding: 0;">Organizer</h2></td></tr></table><div style=3D"color: #=
3c4042;"><span class=3D"notranslate"><a class=3D"primary-text underline-on-=
hover" style=3D"display: inline-block;;color: #3c4043; text-decoration: non=
e;" href=3D"mailto:tconvertino@gmail.com">tconvertino@gmail.com</a></span><=
span itemprop=3D"organizer" itemscope itemtype=3D"http://schema.org/Person"=
><meta itemprop=3D"name" content=3D"tconvertino@gmail.com"/><meta itemprop=
=3D"email" content=3D"tconvertino@gmail.com"/></span></div></div></td></tr>=
<tr><td style=3D"font-size: 0; padding: 0; text-align: left; word-break: br=
eak-word;;padding-bottom:24px;"><div style=3D"font-family: Roboto, sans-ser=
if;font-style: normal; font-weight: 400; font-size: 14px; line-height: 20px=
; letter-spacing: 0.2px;color: #3c4043; text-decoration: none;" class=3D"pr=
imary-text" role=3D"presentation"><table border=3D"0" cellpadding=3D"0" cel=
lspacing=3D"0" role=3D"presentation" style=3D"padding-bottom: 4px;"><tr><td=
><h2 class=3D"primary-text" style=3D"font-size: 14px;color: #3c4043; text-d=
ecoration: none;font-weight: 700;-webkit-font-smoothing: antialiased;margin=
: 0; padding: 0;">Guests</h2></td></tr></table><div style=3D"padding-bottom=
: 4px; text-align: left;;color: #3c4042;"></div><a href=3D"https://calendar=
.google.com/calendar/r?eid=3DNG9jdGNndmhpanFqbTQ1aDhkOWRyNGlxMW0gdGNvbnZlcn=
Rpbm9AbQ&amp;es=3D1" style=3D"display: inline-block;;color: #1a73e8; text-d=
ecoration: none;font-weight: 700;" target=3D"_blank" class=3D"accent-text u=
nderline-on-hover">View all guest info</a></div></td></tr></table></td></tr=
></tbody></table></div><!--[if mso | IE]></tr></table><![endif]--></td></tr=
></tbody></table><table border=3D"0" cellpadding=3D"0" cellspacing=3D"0" ro=
le=3D"presentation" align=3D"center" style=3D"width:100%;" class=3D""><tbod=
y><tr><td style=3D"font-size: 0; padding: 0; text-align: left; word-break: =
break-word;;padding:4px 12px;" class=3D"" align=3D"left"><div class=3D"seco=
ndary-text" style=3D"color: #70757a; text-decoration: none;font-family: Rob=
oto, sans-serif;font-size: 12px; line-height: 16px; mso-line-height-rule: e=
xactly; text-align: left;"><p>Invitation from <a href=3D"https://calendar.g=
oogle.com/calendar/" class=3D"accent-text underline-on-hover" style=3D"font=
-family: Roboto, sans-serif;font-size: 12px; line-height: 16px; mso-line-he=
ight-rule: exactly;;color: #1a73e8; text-decoration: none;" target=3D"_blan=
k">Google Calendar</a></p><p>You are receiving this email because you are s=
ubscribed to calendar notifications. To stop receiving these emails, go to =
<a href=3D"https://calendar.google.com/calendar/r/settings" class=3D"accent=
-text underline-on-hover" style=3D"font-family: Roboto, sans-serif;font-siz=
e: 12px; line-height: 16px; mso-line-height-rule: exactly;;color: #1a73e8; =
text-decoration: none;" target=3D"_blank">Calendar settings</a>, select thi=
s calendar, and change "Other notifications".</p><p>Forwarding this invitat=
ion could allow any recipient to send a response to the organizer, be added=
to the guest list, invite others regardless of their own invitation status=
, or modify your RSVP. <a class=3D"accent-text underline-on-hover" style=3D=
"font-family: Roboto, sans-serif;font-size: 12px; line-height: 16px; mso-li=
ne-height-rule: exactly;;color: #1a73e8; text-decoration: none;" href=3D"ht=
tps://support.google.com/calendar/answer/37135#forwarding">Learn more</a></=
p></div></td></tr></tbody></table></td></tr></tbody></table></span></span><=
/body></html>
--0000000000009e8d3e064896eeed--

View File

@@ -0,0 +1,175 @@
Return-Path: <couchmoney+caf_=gmail=xinu.tv@gmail.com>
Delivered-To: bill@xinu.tv
Received: from phx.xinu.tv [74.207.253.222]
by nixos-01.h.xinu.tv with IMAP (fetchmail-6.5.1)
for <wathiede@localhost> (single-drop); Thu, 11 Sep 2025 12:27:35 -0700 (PDT)
Received: from phx.xinu.tv
by phx.xinu.tv with LMTP
id CqRrBqciw2hiKicAJR8clQ
(envelope-from <couchmoney+caf_=gmail=xinu.tv@gmail.com>)
for <bill@xinu.tv>; Thu, 11 Sep 2025 12:27:35 -0700
X-Original-To: gmail@xinu.tv
Received-SPF: Pass (mailfrom) identity=mailfrom; client-ip=2a00:1450:4864:20::130; helo=mail-lf1-x130.google.com; envelope-from=couchmoney+caf_=gmail=xinu.tv@gmail.com; receiver=xinu.tv
Authentication-Results: phx.xinu.tv;
dkim=pass (2048-bit key; unprotected) header.d=google.com header.i=@google.com header.a=rsa-sha256 header.s=20230601 header.b=dc+iKaXd;
dkim=pass (2048-bit key; unprotected) header.d=gmail.com header.i=@gmail.com header.a=rsa-sha256 header.s=20230601 header.b=kf8o8wAd
Received: from mail-lf1-x130.google.com (mail-lf1-x130.google.com [IPv6:2a00:1450:4864:20::130])
by phx.xinu.tv (Postfix) with ESMTPS id D7E2D80037
for <gmail@xinu.tv>; Thu, 11 Sep 2025 12:27:33 -0700 (PDT)
Received: by mail-lf1-x130.google.com with SMTP id 2adb3069b0e04-55f716e25d9so1141446e87.1
for <gmail@xinu.tv>; Thu, 11 Sep 2025 12:27:33 -0700 (PDT)
ARC-Seal: i=2; a=rsa-sha256; t=1757618852; cv=pass;
d=google.com; s=arc-20240605;
b=MZ+1JfQuPR9luCCxiZNUeqSEpjt1vLuM3bTRCaal/W0NBxkCH0y5v9WfPR0KJ2BPb1
Rtnt/5ayDtmsLf8l6yTTVsBlFYW70ehqXWMD10MMcDEMvnib4KKDAacGaSmijAK4cYGq
FOU9CGNY986OMXMk54TD9NF3fkKDIKcAoh81D6at5/DE3Puuxofq0vZmtmVqQBNKG169
REkhcDpkXTMs/4rJpmZwXp2HbjD84avusBwSlYIQUWsBgO4g7THHjoR4Uk56cek9aEds
ip8IkTO6KRFe6u8FebQsZ/Q9sSAK3pheMExWFVMha9Y0XhACVOZiV600zRCPS9MNHhYw
XEaA==
ARC-Message-Signature: i=2; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20240605;
h=to:from:subject:date:message-id:auto-submitted:sender:reply-to
:mime-version:dkim-signature:dkim-signature:delivered-to;
bh=mVNsDGUAhSGrAIoTy8PIfvCBxBB4yaBy/VZH8i3gPl4=;
fh=WnbwIlqFRbBot/H7TyqablNBDgXRuegsgjC3piothTI=;
b=aYMo5f7VI2b4CiAvLELRJ9zM3dF7ZH8FEqmoAtCcfPHrT9kLLCnriuyXG1R6sC3eoR
++boT29xoScVroIlfcI77Ty7N5X1fawOABkVDWWt7z5w4WhiesT0klxw5nINj9hnLBiK
22nrMevpRpFtmuDO7cle78lSAFZoZuyv+aXCK9RnLKvIm2JuXRrvU8LivxbbpNB4gNl0
hE1jsGuZm1SOJ54SRLwwa4HpSiOJV2x2txTtPCzmvE/LZvNESPjfi3Y2u7gaR87OzkNs
gNi5Xoc+D908zBsmcYKpUYiQcPL79s3DfNwYFIs/rR8Z2xgaHbFD/YmqRUmCEeNLv7o2
RR8g==;
darn=xinu.tv
ARC-Authentication-Results: i=2; mx.google.com;
dkim=pass header.i=@google.com header.s=20230601 header.b=dc+iKaXd;
dkim=pass header.i=@gmail.com header.s=20230601 header.b=kf8o8wAd;
spf=pass (google.com: domain of tconvertino@gmail.com designates 209.85.220.73 as permitted sender) smtp.mailfrom=tconvertino@gmail.com;
dmarc=pass (p=NONE sp=QUARANTINE dis=NONE) header.from=gmail.com;
dara=pass header.i=@gmail.com
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=1e100.net; s=20230601; t=1757618852; x=1758223652;
h=to:from:subject:date:message-id:auto-submitted:sender:reply-to
:mime-version:dkim-signature:dkim-signature:delivered-to
:x-forwarded-for:x-forwarded-to:x-gm-message-state:from:to:cc
:subject:date:message-id:reply-to;
bh=mVNsDGUAhSGrAIoTy8PIfvCBxBB4yaBy/VZH8i3gPl4=;
b=GKJkb+LmE79XIMEhHRvoCodKS+GBTOCShzMe06Q+zKxUZFHi6XMg8GqteuXQO9LVbw
nPUVN4QO2Hvqch0xzjbc0ryyMOD0u7HqpDUAEZCzamFXIfsX6hZXKLhFqy4YomtsG3os
TCOWBGLqwu7KalfOVg2p+csOR68i0mGyBII1sKcL9vUv9kIQJZxQKHGkuIc48cf6tbUB
L+mkVbMwXLSbpuTJszPmIVZV5o0K52KN+2QoLcmXGfw0mUOnjNI0oSovdbPg4SSDZ3cw
iIsC9vjvtCSFS3pf+Fp807s+Zjh5P6xeSxGU57qhC+HT9kTzIioh5EqKnGqcskDTqrI1
uCiQ==
X-Forwarded-Encrypted: i=2; AJvYcCUfSSA2sT31daRt2+W7dAD9YPx1gqa4JFpVuqCtxVtjqbKfKhOX/EcDQiECQ4BEWjmAP+IqTQ==@xinu.tv
X-Gm-Message-State: AOJu0Ywn7D0BjTaGiM/UFG0WhGuyYGfpLijg+ouhrOaGZzSREyTcRa37
XA3bzQ/LKTpzWhhh01GMwnigmELbWdIVr/BeRLVCuJdh+m+JBMgnAjBTIDs9RF3/xfR7rpG7VOB
6k+ugF+8QRKB4BcL2t8MvfJD03CkrzuhhvUtFTRHopcSZrkqzh8GOJayq42VveQ==
X-Received: by 2002:a05:6512:3b24:b0:55f:6580:818c with SMTP id 2adb3069b0e04-57050fe2fa3mr165340e87.46.1757618851553;
Thu, 11 Sep 2025 12:27:31 -0700 (PDT)
X-Forwarded-To: gmail@xinu.tv
X-Forwarded-For: couchmoney@gmail.com gmail@xinu.tv
Delivered-To: couchmoney@gmail.com
Received: by 2002:a05:6504:d09:b0:2c3:f6c4:ad72 with SMTP id c9csp3388833lty;
Thu, 11 Sep 2025 12:27:29 -0700 (PDT)
X-Received: by 2002:a05:6602:36ce:b0:889:b536:779b with SMTP id ca18e2360f4ac-8903378d714mr78653239f.7.1757618849269;
Thu, 11 Sep 2025 12:27:29 -0700 (PDT)
ARC-Seal: i=1; a=rsa-sha256; t=1757618849; cv=none;
d=google.com; s=arc-20240605;
b=Ln2bufZfSNhR/NmMPrG2QFdtvupjJtLDQnFvsL8HTPn+Dlrt5ff+6k6Wpupab/5mS7
hXjtVD0jnryGUiM5h+SNjxwzNPM3PBoueTpAzzBkjHQqMxJVpspgsGJUVOWAVRBWtWo
39qFyoP0vhzGRWDAuAFV+4VDhsvH7GL8lTrZCSMzrngTadmEdJ5haUIQOa50KFUn5HrK
1r12gayb+TaGaWfQfDo0Me689T8MQnS0ITUuzgvFxfgHZBz3h+IPnC0hrlhdziGovETo
GvHzgCCtiVzu6rop6VMLjLuAYmmT9+jZ3GjSRb+078C9cJR17YpguOC14Cyv4od1Tf7y
RFiQ==;
dara=google.com
ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20240605;
h=to:from:subject:date:message-id:auto-submitted:sender:reply-to
:mime-version:dkim-signature:dkim-signature;
bh=mVNsDGUAhSGrAIoTy8PIfvCBxBB4yaBy/VZH8i3gPl4=;
fh=mbzrMIWIgWMC0ni1xEx+ViW4J0RLAdLdPT2cX81nTlk=;
b=JRkHr3CKSkCrafdLzBRtaBOGNl3/0ZSTtgubaNXtvhAiIqRqiQYocfLnVM6N/9sH7O
byTXYaRoaRLw/35WM+QTFGP3zUGRkM3eO4UVS/utVIss1IVLDjfmZHalqLYl8RokW5br
89Z/xYIyjTE7WUdy6uMSrExCNm5VWjO/qcMKsE5s5oDbXdSLaUYxLTurICM3LQksGkCY
wiAWaDDqK14+uhEhW5AyEnebDSYhL9U8UadIv+eK6Ng9q1kwOUzxICRQXEyUtnKhaDKJ
eZ1Qe1mp1CjCulr+I15fz3VwUJ6W1cv6cytcxPbu4p5GPn2gb2hS1eR81HVTL6V1Sp5G
NdDQ==;
dara=google.com
ARC-Authentication-Results: i=1; mx.google.com;
dkim=pass header.i=@google.com header.s=20230601 header.b=dc+iKaXd;
dkim=pass header.i=@gmail.com header.s=20230601 header.b=kf8o8wAd;
spf=pass (google.com: domain of tconvertino@gmail.com designates 209.85.220.73 as permitted sender) smtp.mailfrom=tconvertino@gmail.com;
dmarc=pass (p=NONE sp=QUARANTINE dis=NONE) header.from=gmail.com;
dara=pass header.i=@gmail.com
Received: from mail-sor-f73.google.com (mail-sor-f73.google.com. [209.85.220.73])
by mx.google.com with SMTPS id ca18e2360f4ac-88f2ea1122asor117632339f.3.2025.09.11.12.27.29
for <couchmoney@gmail.com>
(Google Transport Security);
Thu, 11 Sep 2025 12:27:29 -0700 (PDT)
Received-SPF: pass (google.com: domain of tconvertino@gmail.com designates 209.85.220.73 as permitted sender) client-ip=209.85.220.73;
Authentication-Results: mx.google.com;
dkim=pass header.i=@google.com header.s=20230601 header.b=dc+iKaXd;
dkim=pass header.i=@gmail.com header.s=20230601 header.b=kf8o8wAd;
spf=pass (google.com: domain of tconvertino@gmail.com designates 209.85.220.73 as permitted sender) smtp.mailfrom=tconvertino@gmail.com;
dmarc=pass (p=NONE sp=QUARANTINE dis=NONE) header.from=gmail.com;
dara=pass header.i=@gmail.com
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=google.com; s=20230601; t=1757618849; x=1758223649; dara=google.com;
h=to:from:subject:date:message-id:auto-submitted:sender:reply-to
:mime-version:from:to:cc:subject:date:message-id:reply-to;
bh=mVNsDGUAhSGrAIoTy8PIfvCBxBB4yaBy/VZH8i3gPl4=;
b=dc+iKaXdFyqu6K0MIgk848QuwpQXvwzwlEVkxmjuCWvn9DzanMbYn5QJRyRTKilRna
BZ7gJSPriHUHcJd4fVKgGuCaQg0TxenCwm+0R64oB1xcDLfonayo/nCrFqEcCLHNmi7x
lTyWGJ0rLw6nKazxtcCdIbDhVgiE7/fXNI89w6XFp6pcKLl48yFIoCG1f6uY4iQ7QqNU
hLHzjmlzjTi58xFLao7SizZ0lr7E5cHXKHp1Ls/hkDzzcY0Y+O5+3r+NQw4MtpHTcY6/
kQlg6OhyMx8PTu4cuepQKXLHV4aFaNJbDQTp8wew4xPIgi7pm2p6hb6C3GgwY6ptOvLd
wuag==
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=gmail.com; s=20230601; t=1757618849; x=1758223649; dara=google.com;
h=to:from:subject:date:message-id:auto-submitted:sender:reply-to
:mime-version:from:to:cc:subject:date:message-id:reply-to;
bh=mVNsDGUAhSGrAIoTy8PIfvCBxBB4yaBy/VZH8i3gPl4=;
b=kf8o8wAd5DSU/NC7SDiuIoohCu+/7wTjWyQqDYbBjUFGaBaYdj6aD5JWNQ1KEA2W8o
E+Qy2ymyrzodKa1eOsQX2UDAYKOKpdxMWvx1u19+SC3Dp8DP4puRMrL2ObiSEMLCuOvz
Mxmkd+ZUP72EhVuQwK1iSm04/cjQaMsSiPhvSBaxXMaaarwlKeOoCoIo+qC/Z9emiBBv
Gk0sQcLA+CByvsxuvD9GInSA0rdoZ0ijhSb0Y475Hieam1QQqy/fhe8lgujzhXNFoIbR
5EA9GE0VV9PDoNanaT+u954YeOFBL2YZ5gm2gHltw8tBI98LKnC42Pa3qyMznBa2dI2Q
A0RQ==
X-Google-Smtp-Source: AGHT+IGmC5/03nTVMeYJBoq1R/BiA19iH0DFaZyyImB3W8mtgjdn+XqIFK1fC8aTwWRXQmsr71Xo0cmkgx6hjPvicQ/d
MIME-Version: 1.0
X-Received: by 2002:a05:6602:380d:b0:887:4c93:f12c with SMTP id
ca18e2360f4ac-8903596aca3mr58994639f.17.1757618848817; Thu, 11 Sep 2025
12:27:28 -0700 (PDT)
Reply-To: tconvertino@gmail.com
Sender: Google Calendar <calendar-notification@google.com>
Auto-Submitted: auto-generated
Message-ID: <calendar-01d5e8a0-fad7-450b-9758-a16472bf2aa8@google.com>
Date: Thu, 11 Sep 2025 19:27:28 +0000
Subject: Canceled event: Scout Babysits @ Thu Sep 11, 2025 6pm - 9pm (PDT) (Family)
From: tconvertino@gmail.com
To: couchmoney@gmail.com
Content-Type: multipart/mixed; boundary="000000000000226b77063e8b878d"
--000000000000226b77063e8b878d
Content-Type: text/calendar; charset="UTF-8"; method=CANCEL
Content-Transfer-Encoding: 7bit
BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:CANCEL
X-GOOGLE-CALID:g66m0feuqsao8l1c767pvvcg4k@group.calendar.google.com
BEGIN:VEVENT
DTSTART:20250912T010000Z
DTEND:20250912T040000Z
DTSTAMP:20250911T192728Z
UID:4ang6172d1t7782sn2hmi30fgi@google.com
CREATED:20250901T224707Z
DESCRIPTION:
LAST-MODIFIED:20250911T192728Z
LOCATION:
SEQUENCE:1
STATUS:CANCELLED
SUMMARY:Scout Babysits
TRANSP:OPAQUE
END:VEVENT
END:VCALENDAR
--000000000000226b77063e8b878d--

View File

@@ -11,8 +11,8 @@ version.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
build-info = "0.0.41"
letterbox-notmuch = { path = "../notmuch", version = "0.17.41", registry = "xinu" }
build-info = "0.0.42"
letterbox-notmuch = { path = "../notmuch", version = "0.17", registry = "xinu" }
regex = "1.11.1"
serde = { version = "1.0.219", features = ["derive"] }
sqlx = "0.8.5"

View File

@@ -9,10 +9,10 @@ repository.workspace = true
version.workspace = true
[build-dependencies]
build-info-build = "0.0.41"
build-info-build = "0.0.42"
[dev-dependencies]
wasm-bindgen-test = "0.3.50"
#wasm-bindgen-test = "0.3.50"
[dependencies]
console_error_panic_hook = "0.1.7"
@@ -24,16 +24,16 @@ serde = { version = "1.0.219", features = ["derive"] }
itertools = "0.14.0"
serde_json = { version = "1.0.140", features = ["unbounded_depth"] }
chrono = "0.4.40"
graphql_client = "0.14.0"
graphql_client = "0.16.0"
thiserror = "2.0.12"
gloo-net = { version = "0.6.0", features = ["json", "serde_json"] }
human_format = "1.1.0"
build-info = "0.0.41"
build-info = "0.0.42"
wasm-bindgen = "=0.2.100"
uuid = { version = "1.16.0", features = [
"js",
] } # direct dep to set js feature, prevents Rng issues
letterbox-shared = { path = "../shared/", version = "0.17.41", registry = "xinu" }
letterbox-shared = { path = "../shared/", version = "0.17", registry = "xinu" }
seed_hooks = { version = "0.4.1", registry = "xinu" }
strum_macros = "0.27.1"
gloo-console = "0.3.0"

View File

@@ -51,7 +51,7 @@
},
{
"args": [],
"description": "Indicates that an Input Object is a OneOf Input Object (and thus requires\n exactly one of its field be provided)",
"description": "Indicates that an Input Object is a OneOf Input Object (and thus requires exactly one of its field be provided)",
"locations": [
"INPUT_OBJECT"
],
@@ -107,12 +107,14 @@
}
],
"mutationType": {
"name": "Mutation"
"name": "MutationRoot"
},
"queryType": {
"name": "QueryRoot"
},
"subscriptionType": null,
"subscriptionType": {
"name": "SubscriptionRoot"
},
"types": [
{
"description": null,
@@ -314,6 +316,16 @@
"name": "Corpus",
"possibleTypes": null
},
{
"description": "Implement the DateTime<Utc> scalar\n\nThe input/output is a string in RFC3339 format.",
"enumValues": null,
"fields": null,
"inputFields": null,
"interfaces": null,
"kind": "SCALAR",
"name": "DateTime",
"possibleTypes": null
},
{
"description": null,
"enumValues": [
@@ -969,6 +981,51 @@
}
}
},
{
"args": [
{
"defaultValue": null,
"description": null,
"name": "query",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
{
"defaultValue": null,
"description": null,
"name": "wakeTime",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "DateTime",
"ofType": null
}
}
}
],
"deprecationReason": null,
"description": null,
"isDeprecated": false,
"name": "snooze",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
}
},
{
"args": [],
"deprecationReason": null,
@@ -989,7 +1046,7 @@
"inputFields": null,
"interfaces": [],
"kind": "OBJECT",
"name": "Mutation",
"name": "MutationRoot",
"possibleTypes": null
},
{
@@ -1474,6 +1531,33 @@
"name": "String",
"possibleTypes": null
},
{
"description": null,
"enumValues": null,
"fields": [
{
"args": [],
"deprecationReason": null,
"description": null,
"isDeprecated": false,
"name": "values",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
}
}
],
"inputFields": null,
"interfaces": [],
"kind": "OBJECT",
"name": "SubscriptionRoot",
"possibleTypes": null
},
{
"description": null,
"enumValues": null,

View File

@@ -0,0 +1,4 @@
mutation SnoozeMutation($query: String!, $wakeTime: DateTime!) {
snooze(query: $query, wakeTime: $wakeTime)
}

View File

@@ -1,4 +1,4 @@
DEV_HOST=localhost
DEV_PORT=9345
graphql-client introspect-schema http://${DEV_HOST:?}:${DEV_PORT:?}/api/graphql --output schema.json
graphql-client introspect-schema http://${DEV_HOST:?}:${DEV_PORT:?}/api/graphql/ --output schema.json
git diff schema.json

View File

@@ -1,7 +1,9 @@
use chrono::Utc;
use gloo_net::{http::Request, Error};
use graphql_client::GraphQLQuery;
use serde::{de::DeserializeOwned, Serialize};
type DateTime = chrono::DateTime<Utc>;
// The paths are relative to the directory where your `Cargo.toml` is located.
// Both json and the GraphQL schema language are supported as sources for the schema
#[derive(GraphQLQuery)]
@@ -52,6 +54,14 @@ pub struct AddTagMutation;
)]
pub struct RemoveTagMutation;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "graphql/schema.json",
query_path = "graphql/snooze.graphql",
response_derives = "Debug"
)]
pub struct SnoozeMutation;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "graphql/schema.json",

View File

@@ -1,5 +1,6 @@
use std::collections::HashSet;
use chrono::{DateTime, Utc};
use graphql_client::GraphQLQuery;
use letterbox_shared::WebsocketMessage;
use log::{debug, error, info, warn};
@@ -259,6 +260,29 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
Msg::GoToSearchResults
});
}
Msg::Snooze(query, wake_time) => {
let is_catchup = model.catchup.is_some();
orders.skip().perform_cmd(async move {
let res: Result<
graphql_client::Response<graphql::snooze_mutation::ResponseData>,
gloo_net::Error,
> = send_graphql(graphql::SnoozeMutation::build_query(
graphql::snooze_mutation::Variables {
query: query.clone(),
wake_time,
},
))
.await;
if let Err(e) = res {
error!("Failed to snooze {query} until {wake_time}: {e}");
}
if is_catchup {
Msg::CatchupMarkAsRead
} else {
Msg::GoToSearchResults
}
});
}
Msg::FrontPageRequest {
query,
@@ -267,6 +291,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
first,
last,
} => {
model.refreshing_state = RefreshingState::Loading;
let (after, before, first, last) = match (after.as_ref(), before.as_ref(), first, last)
{
// If no pagination set, set reasonable defaults
@@ -292,25 +317,32 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
});
}
Msg::FrontPageResult(Err(e)) => {
error!("error FrontPageResult: {e:?}");
let msg = format!("error FrontPageResult: {e:?}");
error!("{msg}");
model.refreshing_state = RefreshingState::Error(msg);
}
Msg::FrontPageResult(Ok(graphql_client::Response {
data: None,
errors: None,
..
})) => {
error!("FrontPageResult no data or errors, should not happen");
let msg = format!("FrontPageResult no data or errors, should not happen");
error!("{msg}");
model.refreshing_state = RefreshingState::Error(msg);
}
Msg::FrontPageResult(Ok(graphql_client::Response {
data: None,
errors: Some(e),
..
})) => {
error!("FrontPageResult error: {e:?}");
let msg = format!("FrontPageResult error: {e:?}");
error!("{msg}");
model.refreshing_state = RefreshingState::Error(msg);
}
Msg::FrontPageResult(Ok(graphql_client::Response {
data: Some(data), ..
})) => {
model.refreshing_state = RefreshingState::None;
model.tags = Some(
data.tags
.into_iter()
@@ -350,6 +382,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
}
Msg::ShowThreadRequest { thread_id } => {
model.refreshing_state = RefreshingState::Loading;
orders.skip().perform_cmd(async move {
Msg::ShowThreadResult(
send_graphql(graphql::ShowThreadQuery::build_query(
@@ -362,6 +395,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
Msg::ShowThreadResult(Ok(graphql_client::Response {
data: Some(data), ..
})) => {
model.refreshing_state = RefreshingState::None;
model.tags = Some(
data.tags
.into_iter()
@@ -401,9 +435,12 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
orders.send_msg(Msg::WindowScrolled);
}
Msg::ShowThreadResult(bad) => {
error!("show_thread_query error: {bad:#?}");
let msg = format!("show_thread_query error: {bad:#?}");
error!("{msg}");
model.refreshing_state = RefreshingState::Error(msg);
}
Msg::CatchupRequest { query } => {
model.refreshing_state = RefreshingState::Loading;
orders.perform_cmd(async move {
Msg::CatchupResult(
send_graphql::<_, graphql::catchup_query::ResponseData>(
@@ -418,6 +455,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
Msg::CatchupResult(Ok(graphql_client::Response {
data: Some(data), ..
})) => {
model.refreshing_state = RefreshingState::None;
let items = data.catchup;
if items.is_empty() {
orders.send_msg(Msg::GoToSearchResults);
@@ -433,7 +471,9 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
}
}
Msg::CatchupResult(bad) => {
error!("catchup_query error: {bad:#?}");
let msg = format!("catchup_query error: {bad:#?}");
error!("{msg}");
model.refreshing_state = RefreshingState::Error(msg);
}
Msg::SelectionSetNone => {
if let Context::SearchResult {
@@ -813,6 +853,7 @@ pub enum Msg {
SetUnread(String, bool),
AddTag(String, String),
RemoveTag(String, String),
Snooze(String, DateTime<Utc>),
FrontPageRequest {
query: String,

View File

@@ -78,13 +78,16 @@ mod tw_classes {
}
pub fn view(model: &Model) -> Node<Msg> {
let is_loading = match model.refreshing_state {
RefreshingState::Loading => true,
_ => false,
};
match &model.context {
Context::None => normal_view(
div![h1!["Loading"]],
&model.versions,
&model.query,
&model.refreshing_state,
model.read_completion_ratio,
&model.tags,
),
Context::ThreadResult {
@@ -93,17 +96,23 @@ pub fn view(model: &Model) -> Node<Msg> {
} => {
if let Some(catchup) = &model.catchup {
catchup_view(
thread(thread_data, open_messages, &model.content_el, true),
thread(thread_data, open_messages, &model.content_el, true, 0.),
&catchup.items,
is_loading,
model.read_completion_ratio,
)
} else {
normal_view(
thread(thread_data, open_messages, &model.content_el, false),
thread(
thread_data,
open_messages,
&model.content_el,
false,
model.read_completion_ratio,
),
&model.versions,
&model.query,
&model.refreshing_state,
model.read_completion_ratio,
&model.tags,
)
}
@@ -114,17 +123,17 @@ pub fn view(model: &Model) -> Node<Msg> {
} => {
if let Some(catchup) = &model.catchup {
catchup_view(
news_post(post, &model.content_el, true),
news_post(post, &model.content_el, true, 0.),
&catchup.items,
is_loading,
model.read_completion_ratio,
)
} else {
normal_view(
news_post(post, &model.content_el, false),
news_post(post, &model.content_el, false, model.read_completion_ratio),
&model.versions,
&model.query,
&model.refreshing_state,
model.read_completion_ratio,
&model.tags,
)
}
@@ -140,7 +149,6 @@ pub fn view(model: &Model) -> Node<Msg> {
&model.versions,
&model.query,
&model.refreshing_state,
model.read_completion_ratio,
&model.tags,
),
}
@@ -151,7 +159,6 @@ fn normal_view(
versions: &Version,
query: &str,
refreshing_state: &RefreshingState,
read_completion_ratio: f64,
tags: &Option<Vec<Tag>>,
) -> Node<Msg> {
div![
@@ -178,13 +185,13 @@ fn normal_view(
content,
view_header(query, refreshing_state, false),
],
reading_progress(read_completion_ratio),
]
}
fn catchup_view(
content: Node<Msg>,
items: &[CatchupItem],
is_loading: bool,
read_completion_ratio: f64,
) -> Node<Msg> {
div![
@@ -201,13 +208,34 @@ fn catchup_view(
"bg-black/50",
],
div![
C!["absolute", "top-0", "right-4", "text-gray-500", "p-4"],
span![i![C!["fas", "fa-x"]]],
ev(Ev::Click, move |_| Msg::CatchupExit)
C!["absolute", "top-0", "left-4", "text-green-200", "p-4"],
IF!(is_loading=>span![i![C!["animate-spin", "fas", "fa-spinner"]]])
],
h1![
C!["text-center"],
format!("{} left ", items.iter().filter(|i| !i.seen).count(),)
],
div![
C!["absolute", "top-0", "right-4", "text-gray-500", "p-4"],
span![i![C!["fas", "fa-x"]]],
ev(Ev::Click, move |_| Msg::CatchupExit)
],
div![
C![
"absolute",
"left-0",
"right-0",
"bottom-0",
"w-full",
"h-1",
"bg-gray-200"
],
div![
C!["h-1", "bg-green-500"],
style! {
St::Width => format!("{}%", read_completion_ratio*100.)
}
]
]
],
div![C!["mt-12", "mb-20"], content],
@@ -247,7 +275,6 @@ fn catchup_view(
ev(Ev::Click, |_| Msg::CatchupMarkAsRead)
]
],
reading_progress(read_completion_ratio)
]
}
@@ -324,8 +351,8 @@ fn search_results(
attrs! {
At::Href => urls::thread(&tid)
},
div![title_break, &r.subject],
span![C!["text-xs"], pretty_authors(&r.authors)],
div![C!["line-clamp-2"], title_break, &r.subject],
span![C!["line-clamp-2", "text-xs"], pretty_authors(&r.authors)],
div![
C!["flex", "flex-wrap", "justify-between"],
span![tags_chiclet(&tags)],
@@ -727,15 +754,19 @@ fn render_open_header(msg: &ShowThreadQueryThreadOnEmailThreadMessages) -> Node<
C!["flex", "p-4", "bg-neutral-800"],
div![avatar],
div![
C!["px-4", "mr-auto"],
span![
C!["font-semibold", "text-sm"],
from_detail.as_ref().map(|addr| attrs! {
At::Title => addr
}),
&from,
" ",
from_detail.as_ref().map(|text| copy_text_widget(&text))
C!["px-4", "flex-1"],
div![
C!["flex"],
div![
C!["font-semibold", "text-sm", "flex-1"],
from_detail.as_ref().map(|addr| attrs! {
At::Title => addr
}),
&from,
" ",
from_detail.as_ref().map(|text| copy_text_widget(&text))
],
snooze_buttons(msg.timestamp, &id),
],
IF!(!msg.to.is_empty() =>div![
C!["text-xs"],
@@ -1058,6 +1089,8 @@ fn message_render(msg: &ShowThreadQueryThreadOnEmailThreadMessages, open: bool)
},
) => div![
C!["view-part-text-html"],
// If there isn't any HTML tags, treat more like plain text
IF!(!(contents.contains('<') && contents.contains('>')) => C!["whitespace-pre-line"]),
raw![contents],
IF!(!msg.attachments.is_empty() => render_attachements(&msg.attachments)),
view_content_tree(&content_tree),
@@ -1142,6 +1175,7 @@ fn thread(
open_messages: &HashSet<String>,
content_el: &ElRef<HtmlElement>,
catchup_mode: bool,
read_completion_ratio: f64,
) -> Node<Msg> {
// TODO(wathiede): show per-message subject if it changes significantly from top-level subject
let subject = if thread.subject.is_empty() {
@@ -1226,7 +1260,8 @@ fn thread(
el_ref(content_el),
messages,
IF!(!catchup_mode => click_to_top())
]
],
reading_progress(read_completion_ratio)
]
}
@@ -1369,7 +1404,7 @@ pub fn view_tags(tags: &Option<Vec<Tag>>) -> Node<Msg> {
},
],
a![
C!["grow", "truncate"],
C![indent_cls, "grow", "truncate"],
attrs! {
At::Href => href
},
@@ -1470,6 +1505,7 @@ fn news_post(
post: &ShowThreadQueryThreadOnNewsPost,
content_el: &ElRef<HtmlElement>,
catchup_mode: bool,
read_completion_ratio: f64,
) -> Node<Msg> {
let subject = &post.title;
set_title(subject);
@@ -1557,6 +1593,7 @@ fn news_post(
]
],
IF!(!catchup_mode => click_to_top()),
reading_progress(read_completion_ratio)
]
}
fn render_news_post_header(post: &ShowThreadQueryThreadOnNewsPost) -> Node<Msg> {
@@ -1592,9 +1629,13 @@ fn render_news_post_header(post: &ShowThreadQueryThreadOnNewsPost) -> Node<Msg>
C!["flex", "p-4", "bg-neutral-800"],
div![favicon],
div![
C!["px-4", "mr-auto"],
C!["px-4", "mr-auto", "flex-1"],
div![
div![C!["font-semibold", "text-sm"], from],
div![
C!["flex"],
div![C!["font-semibold", "text-sm", "flex-1"], from],
snooze_buttons(Some(post.timestamp), &id),
],
div![
C!["flex", "gap-2", "pt-2", "text-sm"],
a![
@@ -1689,3 +1730,47 @@ fn click_to_top() -> Node<Msg> {
ev(Ev::Click, |_| Msg::ScrollToTop)
]
}
fn snooze_buttons(timestamp: Option<i64>, id: &str) -> Node<Msg> {
div![
span![C!["px-2"], ""],
button![
tw_classes::button(),
C!["rounded-r-none"],
"1d",
ev(Ev::Click, {
let id = id.to_string();
move |e| {
e.stop_propagation();
Msg::Snooze(id, Utc::now() + chrono::Days::new(1))
}
})
],
button![
tw_classes::button(),
C!["rounded-none"],
"7d",
ev(Ev::Click, {
let id = id.to_string();
move |e| {
e.stop_propagation();
Msg::Snooze(id, Utc::now() + chrono::Days::new(7))
}
})
],
timestamp.map(
|ts| chrono::DateTime::from_timestamp(ts, 0).map(|ts| button![
tw_classes::button(),
C!["rounded-l-none"],
"+6m",
ev(Ev::Click, {
let id = id.to_string();
move |e| {
e.stop_propagation();
Msg::Snooze(id, ts + chrono::Days::new(180))
}
})
])
),
]
}