Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,10 @@ jobs:
go-version-file: go.mod
cache: ${{ github.repository == 'kenn-io/msgvault' && (github.event_name == 'push' && github.ref == 'refs/heads/main' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.base.repo.full_name == github.repository)) && 'false' || 'true' }}

- name: Install golangci-lint
run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2
- name: Install lint tools
run: |
make lint-tools
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"

- name: Test
run: make test
Expand All @@ -93,11 +95,8 @@ jobs:
- name: Lint
run: make lint-ci

- name: Install govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@v1.1.4

- name: Vulnerability check
run: govulncheck -tags "fts5 sqlite_vec" ./...
run: make vulncheck

test-macos-15:
if: github.repository == 'kenn-io/msgvault' && github.event_name == 'push' && github.ref == 'refs/heads/main'
Expand Down
3 changes: 2 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
version: "2"

run:
go: "1.27"
# Lint the build-tagged code, not just the untagged subset. The primary
# build uses `fts5 sqlite_vec` (Makefile BUILD_TAGS) and the pgvector
# backend is gated behind `pgvector`; without these tags golangci-lint
Expand Down Expand Up @@ -47,7 +48,7 @@ linters:
- godot
- goheader
- gomoddirectives
- gomodguard
- gomodguard_v2
- goprintffuncname
- gosec
- govet
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ RUN cd web && bun run generate && bun run build

# Go build stage.
# Pin by digest for reproducibility; update periodically.
FROM golang:1.26.6-bookworm@sha256:116d58cbd88c1297624acc6e967a060012422bacf9930927e23fb719189c6f36 AS builder
FROM golang:1.27.0-bookworm@sha256:484ef6066fa69acb059fdfeda7ba2b8f7391f2ef6abc6f9b8411e669ebd56466 AS builder

# Install build dependencies for CGO (SQLite, DuckDB).
# libsqlite3-dev provides sqlite3.h, required to compile the sqlite-vec
Expand Down
37 changes: 30 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ LDFLAGS_RELEASE := $(LDFLAGS) -s -w
# - sqlite_vec: enable the sqlite-vec extension for vector search
BUILD_TAGS := fts5 sqlite_vec
TEST_TIMEOUT := 60m
GOLANGCI_LINT_VERSION ?= v2.13.1
GOVULNCHECK_VERSION ?= v1.7.0
GO_INSTALL_BIN := $(shell go env GOBIN)
ifeq ($(strip $(GO_INSTALL_BIN)),)
GO_INSTALL_BIN := $(shell go env GOPATH)/bin
endif
GOLANGCI_LINT_BIN := $(GO_INSTALL_BIN)/golangci-lint
CI_TOOLS_BIN := $(shell git rev-parse --path-format=absolute --git-path ci-tools/bin)
GOVULNCHECK_BIN := $(CI_TOOLS_BIN)/govulncheck

# Build tags for the PostgreSQL test lane (test-pg). Must be the full build set:
# pgvector gates the vector-on-PG code paths (//go:build pgvector), and sqlite_vec
Expand Down Expand Up @@ -53,7 +62,7 @@ export GOLANGCI_LINT_CACHE
# serialize one another while duplicate runners in one worktree can wait.
GOLANGCI_LINT_TMP ?= $(GOLANGCI_LINT_CACHE)/tmp

.PHONY: build build-release install clean test test-v test-pg test-pg-shipped test-pg-both pg-shipped-only-check require-test-db fmt lint lint-ci testify-helper-check tidy openapi api-generate openapi-check api-check web-install web-generate web-check web-test web-test-browser web-e2e web-build web-embed web-assets-check smoke-web-release shootout run-shootout install-hooks bench vcard-registry-check vcard-registry-update docs-install docs-build docs-serve docs-check docs-fixture-test docs-fixture-check docs-fixture-smoke docs-web-screenshots docs-screenshots docs-assets-branch docs-generated-assets-branch docs-deploy-staging docs-deploy help
.PHONY: build build-release install clean test test-v test-pg test-pg-shipped test-pg-both pg-shipped-only-check require-test-db fmt lint-tools lint lint-ci vuln-tools vulncheck testify-helper-check tidy openapi api-generate openapi-check api-check web-install web-generate web-check web-test web-test-browser web-e2e web-build web-embed web-assets-check smoke-web-release shootout run-shootout install-hooks bench vcard-registry-check vcard-registry-update docs-install docs-build docs-serve docs-check docs-fixture-test docs-fixture-check docs-fixture-smoke docs-web-screenshots docs-screenshots docs-assets-branch docs-generated-assets-branch docs-deploy-staging docs-deploy help

# Build the binary (debug)
build: web-embed
Expand Down Expand Up @@ -272,6 +281,10 @@ smoke-web-release:
fmt:
go fmt ./...

# Install the pinned linter used by CI.
lint-tools:
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)

# Run linter (auto-fix)
lint:
@if ! command -v golangci-lint >/dev/null 2>&1; then \
Expand All @@ -282,13 +295,22 @@ lint:
TMPDIR="$(GOLANGCI_LINT_TMP)" golangci-lint run --fix ./...

# Run linter (CI, no auto-fix)
lint-ci: testify-helper-check
@if ! command -v golangci-lint >/dev/null 2>&1; then \
echo "golangci-lint not found. Install: https://golangci-lint.run/usage/install/" >&2; \
exit 1; \
fi
lint-ci: lint-tools testify-helper-check
@mkdir -p "$(GOLANGCI_LINT_TMP)"
TMPDIR="$(GOLANGCI_LINT_TMP)" golangci-lint run ./...
TMPDIR="$(GOLANGCI_LINT_TMP)" "$(GOLANGCI_LINT_BIN)" run ./...
@if [ -n "$$GITHUB_PATH" ]; then \
$(MAKE) --no-print-directory vuln-tools; \
printf '%s\n' "$(CI_TOOLS_BIN)" >> "$$GITHUB_PATH"; \
fi

# Install and run the scanner from a repository-owned path so a stale tool
# installed by the base branch's pull-request workflow cannot replace it.
vuln-tools:
@mkdir -p "$(CI_TOOLS_BIN)"
GOBIN="$(CI_TOOLS_BIN)" go install golang.org/x/vuln/cmd/govulncheck@$(GOVULNCHECK_VERSION)

vulncheck: vuln-tools
"$(GOVULNCHECK_BIN)" -tags "$(BUILD_TAGS)" ./...

# Enforce testify helper usage in assertion-heavy tests
testify-helper-check:
Expand Down Expand Up @@ -398,6 +420,7 @@ help:
@echo " fmt - Format code"
@echo " lint - Run linter (auto-fix)"
@echo " lint-ci - Run linter (CI, no auto-fix; also runs testify-helper-check)"
@echo " vulncheck - Run the pinned Go vulnerability scanner"
@echo " testify-helper-check - Enforce testify helper usage in assertion-heavy tests"
@echo " tidy - Tidy go.mod"
@echo " vcard-registry-check - Network-check IANA registry drift (manual; not CI)"
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<h1 align="center">msgvault</h1>

<p align="center">
<a href="https://go.dev"><img src="https://img.shields.io/badge/Go-1.26+-00ADD8?logo=go" alt="Go 1.26+"></a>
<a href="https://go.dev"><img src="https://img.shields.io/badge/Go-1.27+-00ADD8?logo=go" alt="Go 1.27+"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
<a href="https://msgvault.io"><img src="https://img.shields.io/badge/Docs-msgvault.io-blue" alt="Docs"></a>
<a href="https://discord.gg/fDnmxB8Wkq"><img src="https://img.shields.io/badge/Discord-Join-5865F2?logo=discord&amp;logoColor=white" alt="Discord"></a>
Expand Down Expand Up @@ -73,7 +73,7 @@ powershell -ExecutionPolicy ByPass -c "irm https://msgvault.io/install.ps1 | iex

The installer detects your OS and architecture, downloads the latest release from [GitHub Releases](https://github.com/kenn-io/msgvault/releases), verifies the SHA-256 checksum, and installs the binary. You can review the script ([bash](https://msgvault.io/install.sh), [PowerShell](https://msgvault.io/install.ps1)) before running, or download a release binary directly from GitHub.

To build from source instead (requires **Go 1.26+**, **Bun 1.3.14+**, and a
To build from source instead (requires **Go 1.27+**, **Bun 1.3.14+**, and a
C/C++ compiler for CGO and to statically link DuckDB):

```bash
Expand Down
3 changes: 1 addition & 2 deletions cmd/msgvault/cmd/add_discord.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,7 @@ func messageContentUnavailable(message discord.Message) bool {
}

func discordDiagnostic(err error) string {
var apiErr *discord.APIError
if errors.As(err, &apiErr) {
if apiErr, ok := errors.AsType[*discord.APIError](err); ok {
switch apiErr.StatusCode {
case http.StatusUnauthorized:
return "authentication failed"
Expand Down
3 changes: 1 addition & 2 deletions cmd/msgvault/cmd/addaccount.go
Original file line number Diff line number Diff line change
Expand Up @@ -435,8 +435,7 @@ func runAddAccountLocal(cmd *cobra.Command, args []string) error {
return fmt.Errorf("service account token for %s: %w", email, saErr)
}
if saErr := oauth.ValidateTokenEmail(cmd.Context(), ts, email); saErr != nil {
var mismatch *oauth.TokenMismatchError
if errors.As(saErr, &mismatch) {
if mismatch, ok := errors.AsType[*oauth.TokenMismatchError](saErr); ok {
existing, lookupErr := findGmailSource(s, email)
if lookupErr != nil && !errors.Is(lookupErr, errGmailSourceNotFound) {
return fmt.Errorf("service account validation failed: %w (also: %w)", saErr, lookupErr)
Expand Down
12 changes: 5 additions & 7 deletions cmd/msgvault/cmd/repair_dates.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,13 +322,11 @@ func scanAndPlanDateRepairs(
continue
}
plan.repairs = append(plan.repairs, plannedDateRepair{
MessageDateRepair: store.MessageDateRepair{
ID: candidate.ID,
SentAt: resolved,
ExpectedLastModifiedAt: candidate.LastModifiedAt,
},
source: source,
oldSentAt: candidate.SentAt,
ID: candidate.ID,
SentAt: resolved,
ExpectedLastModifiedAt: candidate.LastModifiedAt,
source: source,
oldSentAt: candidate.SentAt,
})
}
return plan, nil
Expand Down
6 changes: 2 additions & 4 deletions cmd/msgvault/cmd/repair_dates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,10 +328,8 @@ func TestApplyPlannedDateRepairsRecordsGuardFailure(t *testing.T) {
plan := &dateRepairPlan{
candidateCount: 1,
repairs: []plannedDateRepair{{
MessageDateRepair: store.MessageDateRepair{
ID: messageID,
SentAt: time.Date(2007, 1, 2, 15, 4, 5, 0, time.UTC),
},
ID: messageID,
SentAt: time.Date(2007, 1, 2, 15, 4, 5, 0, time.UTC),
source: mime.DateSourceReceived,
oldSentAt: sql.NullTime{
Time: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC),
Expand Down
6 changes: 2 additions & 4 deletions cmd/msgvault/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -444,8 +444,7 @@ func wrapOAuthError(err error) error {
// permanently invalid (expired or revoked), as opposed to a transient failure
// like a network error or context cancellation.
func isAuthInvalidError(err error) bool {
var retrieveErr *oauth2.RetrieveError
if errors.As(err, &retrieveErr) {
if retrieveErr, ok := errors.AsType[*oauth2.RetrieveError](err); ok {
// Google returns "invalid_grant" when refresh tokens are expired or revoked
return retrieveErr.ErrorCode == "invalid_grant"
}
Expand Down Expand Up @@ -586,8 +585,7 @@ func getTokenSourceWithReauth(
// AuthorizeManual validates the token and atomically saves it,
// so the old token is only overwritten after validation succeeds.
if authErr := authorizeManualForReauth(ctx, mgr, email); authErr != nil {
var mismatch *oauth.TokenMismatchError
if errors.As(authErr, &mismatch) {
if mismatch, ok := errors.AsType[*oauth.TokenMismatchError](authErr); ok {
return nil, fmt.Errorf(
"re-authorize %s: %w\n"+
"If this account uses an alias, remove "+
Expand Down
2 changes: 0 additions & 2 deletions cmd/msgvault/cmd/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,6 @@ func runHTTPSearch(cmd *cobra.Command, queryStr string) error {

// nil error return mirrors outputSearchResultsJSON so callers can return
// either uniformly; tabwriter output never fails.
//
//nolint:unparam // symmetry with error-returning outputSearchResultsJSON sibling
func outputSearchResultsTable(results []query.MessageSummary) error {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
_, _ = fmt.Fprintln(w, "ID\tDATE\tFROM\tSUBJECT\tSIZE")
Expand Down
2 changes: 0 additions & 2 deletions cmd/msgvault/cmd/show_message.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,6 @@ func showHTTPMessage(cmd *cobra.Command, idStr string) error {

// nil error return mirrors outputMessageJSON so callers can return either
// uniformly; text printing never fails.
//
//nolint:unparam // symmetry with error-returning outputMessageJSON sibling
func outputMessageText(msg *query.MessageDetail) error {
// Header section
fmt.Println("═══════════════════════════════════════════════════════════════════════════════")
Expand Down
2 changes: 1 addition & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Build, test, lint, and code conventions.

## Build

Source builds require Go 1.26+, Bun 1.3.14+, and a C/C++ compiler. The Make
Source builds require Go 1.27+, Bun 1.3.14+, and a C/C++ compiler. The Make
targets install the pinned browser dependencies when `web/package.json` or
`web/bun.lock` changes, then embed the production UI in the Go binary.

Expand Down
2 changes: 1 addition & 1 deletion docs/screenshots/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1.4
# Stage 1: Build msgvault binary
FROM golang:1.26-bookworm AS builder
FROM golang:1.27.0-bookworm AS builder

ARG MSGVAULT_VERSION=dev

Expand Down
2 changes: 1 addition & 1 deletion docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ conda install -c conda-forge msgvault

## Build From Source

Requires Go 1.26+, Bun 1.3.14+, and a C/C++ compiler (GCC or Clang). Bun builds
Requires Go 1.27+, Bun 1.3.14+, and a C/C++ compiler (GCC or Clang). Bun builds
the browser application embedded in the binary. CGO is required because
msgvault uses `mattn/go-sqlite3` (SQLite with FTS5) and `duckdb-go/v2` (Parquet
analytics), both of which compile native extensions.
Expand Down
66 changes: 58 additions & 8 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,69 @@
let
pkgs = nixpkgs.legacyPackages.${system};

# Pin Go 1.26.6 until nixpkgs-unstable ships it.
# Pin Go 1.27.0 until the locked nixpkgs revision ships it.
# Scoped to msgvault only — do NOT export via overlay, that would
# invalidate every Go derivation in the transitive closure.
goPinned = pkgs.go_1_26.overrideAttrs (_: rec {
version = "1.26.6";
goPinned = pkgs.go_1_26.overrideAttrs (old: rec {
version = "1.27.0";
src = pkgs.fetchurl {
url = "https://go.dev/dl/go${version}.src.tar.gz";
hash = "sha256-oHIcVMaIkBRI13rZs+x+p8R0cwdV/4kTgukuy5P/LLE=";
hash = "sha256-cAJAPXzERSnvbSb2mkSBgmM5Xq18FsBaWAiuBH6+sOU=";
};
patches =
builtins.filter (
patch: !(pkgs.lib.hasSuffix "go_no_vendor_checks-1.26.patch" (toString patch))
) old.patches
++ [
(pkgs.fetchurl {
url = "https://raw.githubusercontent.com/NixOS/nixpkgs/67a70befab1966b026701e8e94cf19b1543075c5/pkgs/development/compilers/go/go_no_vendor_checks-1.27.patch";
hash = "sha256-aTpc6kAX9bAyMMAHqzldcb2EEseCpke7QlJuQ1Bk6jc=";
})
];
});

buildGoModule = pkgs.buildGoModule.override { go = goPinned; };

goplsPinned = (pkgs.gopls.override { buildGoLatestModule = buildGoModule; }).overrideAttrs (_: rec {
version = "0.23.0";
src = pkgs.fetchFromGitHub {
owner = "golang";
repo = "tools";
tag = "gopls/v${version}";
hash = "sha256-GTRZ0tS2a7Cx4qRf6PfxhkGVPYRoLYOmE+W/2x9Pttk=";
};
vendorHash = "sha256-rvm33C3z3T6moeEQ4C7aG+dT8ROqmpBFehIpwGFZMrU=";
});

golangciLintPinned =
(pkgs.golangci-lint.override { buildGo126Module = buildGoModule; }).overrideAttrs
(_: rec {
version = "2.13.1";
src = pkgs.fetchFromGitHub {
owner = "golangci";
repo = "golangci-lint";
tag = "v${version}";
hash = "sha256-8nWHSMAwIILfKMPfxWKMimxWt9N+kUsZEAaoAOPbRBE=";
};
vendorHash = "sha256-yZRqfht5rY2yyoZNtYttE57sB7EYjk71yrKw8dLYzNk=";
});

delvePinned = (pkgs.delve.override { inherit buildGoModule; }).overrideAttrs (_: rec {
version = "1.27.1";
src = pkgs.fetchFromGitHub {
owner = "go-delve";
repo = "delve";
tag = "v${version}";
hash = "sha256-H91QnLyqywgoc3zdTaclzzUxVPagNnxLzKub2gnL25w=";
};
checkFlags = [ "-skip=TestGeneratedDoc|TestTypecheckRPC" ];
});

gotoolsPinned = pkgs.gotools.override {
inherit buildGoModule;
go = goPinned;
};

bunPinned = pkgs.bun.overrideAttrs (old: rec {
version = "1.3.14";
src = passthru.sources.${system};
Expand Down Expand Up @@ -91,10 +141,10 @@
devShells.default = pkgs.mkShell {
packages = [
goPinned
pkgs.gopls
pkgs.gotools
pkgs.golangci-lint
pkgs.delve
goplsPinned
gotoolsPinned
golangciLintPinned
delvePinned
pkgs.gcc
pkgs.prek
pkgs.sqlite-interactive
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module go.kenn.io/msgvault

go 1.26.6
go 1.27.0

replace github.com/emersion/go-imap/v2 => github.com/hstern/go-imap/v2 v2.0.0-beta.8.0.20260621192506-dabdeca47dc7

Expand Down Expand Up @@ -30,7 +30,7 @@ require (
github.com/jhillyerd/enmime v1.3.0
github.com/mattn/go-isatty v0.0.22
github.com/mattn/go-runewidth v0.0.24
github.com/mattn/go-sqlite3 v1.14.47
github.com/mattn/go-sqlite3 v1.14.50
github.com/modelcontextprotocol/go-sdk v1.7.0
github.com/mooijtech/go-pst/v6 v6.0.2
github.com/robfig/cron/v3 v3.0.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,8 @@ github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJ
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-sqlite3 v1.14.47 h1:jOBI62gS7nKeZv+as1oGEy0+1qISgXwH/QBlR6KbfIo=
github.com/mattn/go-sqlite3 v1.14.47/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
github.com/mattn/go-sqlite3 v1.14.50 h1:dmdFvo1XG4MPzA4IkAmE9upVz/Nj31uRoM5+jC8hYbY=
github.com/mattn/go-sqlite3 v1.14.50/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs=
Expand Down
Loading