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
2 changes: 1 addition & 1 deletion .custom-gcl.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version: v2.13.1
version: v2.14.0
plugins:
- module: "go.kenn.io/kit"
import: "go.kenn.io/kit/lint/gclplugin"
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ RUN cd web && bun run generate && bun run build

# Go build stage.
# Pin by digest for reproducibility; update periodically.
FROM golang:1.27.1-bookworm@sha256:69a7b9788769bec032d238959b61854e9ae87f57be9029ec04e9885fabf99195 AS builder
FROM golang:1.27.2-bookworm@sha256:5cf287a799e6b94384bad13d16b14904c531f51ba65792237e122ce42b392f61 AS builder

# Install build dependencies for CGO (SQLite, DuckDB).
# libsqlite3-dev provides sqlite3.h, required to compile the sqlite-vec
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ TEST_PROFILE ?= auto
# Only the automatic SQLite profile adds query and overlaps package jobs.
SQLITE_SHARDED_TEST_PKGS := $(sort $(SHARDED_TEST_PKGS) ./internal/query)
SQLITE_SHARD_TARGETS := $(addprefix test-sqlite-shard/,$(SQLITE_SHARDED_TEST_PKGS))
GOLANGCI_LINT_VERSION ?= v2.13.1
GOLANGCI_LINT_VERSION ?= v2.14.0
GOVULNCHECK_VERSION ?= v1.7.0
HUMA_CHECK_VERSION := 3e1f59e9011e878ec595aa04aebc8a77c5292c4d
GO_INSTALL_BIN := $(shell go env GOBIN)
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.27.1-bookworm AS builder
FROM golang:1.27.2-bookworm AS builder

ARG MSGVAULT_VERSION=dev

Expand Down
10 changes: 5 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module go.kenn.io/msgvault

go 1.27.1
go 1.27.2

replace github.com/emersion/go-imap/v2 => github.com/rodboev/go-imap/v2 v2.0.0-beta.8.0.20260916140841-7dc6eaf3b23f

Expand Down Expand Up @@ -52,13 +52,13 @@ require (
go.opentelemetry.io/otel/trace v1.45.0
golang.org/x/crypto v0.57.0
golang.org/x/mod v0.41.0
golang.org/x/net v0.59.0
golang.org/x/net v0.60.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sync v0.23.0
golang.org/x/sys v0.48.0
golang.org/x/text v0.42.0
golang.org/x/time v0.15.0
golang.org/x/tools v0.49.0
golang.org/x/tools v0.50.0
google.golang.org/api v0.287.0
howett.net/plist v1.0.1
)
Expand Down Expand Up @@ -165,10 +165,10 @@ require (
go.yaml.in/yaml/v4 v4.0.0-rc.6 // indirect
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
golang.org/x/image v0.45.0 // indirect
golang.org/x/telemetry v0.0.0-20260811182544-a038080d80e5 // indirect
golang.org/x/telemetry v0.0.0-20260908163034-4bcc4b2ee518 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect
google.golang.org/grpc v1.83.1 // indirect
google.golang.org/grpc v1.83.2 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.73.4 // indirect
Expand Down
16 changes: 8 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -436,8 +436,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand All @@ -457,8 +457,8 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/telemetry v0.0.0-20260811182544-a038080d80e5 h1:ZUSxONxc981v7AW7QUg+I9WwZzSTTJ019ENBYr5pV/Q=
golang.org/x/telemetry v0.0.0-20260811182544-a038080d80e5/go.mod h1:LVehoXe41cL5SCVQilsV7Gg6BNG+Js6P9PhSbYTIUkQ=
golang.org/x/telemetry v0.0.0-20260908163034-4bcc4b2ee518 h1:F5BWKvW126NXR74uxkxuc1jQHhm/rwm/J3rSiFyuRs4=
golang.org/x/telemetry v0.0.0-20260908163034-4bcc4b2ee518/go.mod h1:i+ivNqjDnTF3WTElsdk5g9V5DTSBYgdNo7xTU9SDwYA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
Expand All @@ -475,8 +475,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU=
golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
Expand All @@ -490,8 +490,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800 h1:
google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800/go.mod h1:FPk7EXUKMtImne7AmknoYjT4QXqKIzzRbeQIXzLk6fQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 h1:qEHAMpSaUhtD0p3NbEEI83HwNGFxEwaSJ1G9PLnCBZE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y=
google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
4 changes: 2 additions & 2 deletions internal/api/settings_metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -406,8 +406,8 @@ func metadataForSetting(key string) settingMetadata {
return metadata
}
last := key
if dot := strings.LastIndexByte(key, '.'); dot >= 0 {
last = key[dot+1:]
if _, suffix, ok := strings.CutLast(key, "."); ok {
last = suffix
}
words := strings.Fields(strings.ReplaceAll(last, "_", " "))
for index := range words {
Expand Down
6 changes: 2 additions & 4 deletions internal/circleback/importer.go
Original file line number Diff line number Diff line change
Expand Up @@ -1136,8 +1136,6 @@ func normalizeEmail(email string) string {
}

func emailDomain(email string) string {
if i := strings.LastIndex(email, "@"); i >= 0 {
return email[i+1:]
}
return ""
_, domain, _ := strings.CutLast(email, "@")
return domain
}
6 changes: 2 additions & 4 deletions internal/granola/importer.go
Original file line number Diff line number Diff line change
Expand Up @@ -491,8 +491,6 @@ func normalizeEmail(email string) string {
}

func emailDomain(email string) string {
if i := strings.LastIndex(email, "@"); i >= 0 {
return email[i+1:]
}
return ""
_, domain, _ := strings.CutLast(email, "@")
return domain
}
8 changes: 4 additions & 4 deletions internal/imap/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -1718,15 +1718,15 @@ func compositeID(mailbox string, uid imap.UID) string {

// parseCompositeID splits a composite message ID into mailbox and UID.
func parseCompositeID(id string) (mailbox string, uid imap.UID, err error) {
idx := strings.LastIndexByte(id, '|')
if idx < 0 {
mailbox, rawUID, ok := strings.CutLast(id, "|")
if !ok {
return "", 0, fmt.Errorf("invalid IMAP message ID %q (expected mailbox|uid)", id)
}
n, parseErr := strconv.ParseUint(id[idx+1:], 10, 32)
n, parseErr := strconv.ParseUint(rawUID, 10, 32)
if parseErr != nil {
return "", 0, fmt.Errorf("invalid UID in message ID %q: %w", id, parseErr)
}
return id[:idx], imap.UID(n), nil
return mailbox, imap.UID(n), nil
}

// GetProfile returns the IMAP account profile.
Expand Down
4 changes: 2 additions & 2 deletions internal/importer/pst_import.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ func importPstWithBatchSize(
if err := json.Unmarshal([]byte(active.CursorBefore.String), &saved); err == nil {
sameFile := saved.File == absPath || saved.File == cpFile
if !sameFile && saved.File != "" {
if curInfo, err := os.Stat(absPath); err == nil {
if curInfo, err := os.Stat(absPath); err == nil { // #nosec G703 -- The local CLI caller selects the PST file to import.
if cpInfo, err := os.Stat(saved.File); err == nil && os.SameFile(curInfo, cpInfo) {
sameFile = true
}
Expand Down Expand Up @@ -657,7 +657,7 @@ func finishPstThreads(ctx context.Context, st *store.Store, sourceID int64) erro
// distinguishes archives. Re-importing the same bytes yields the same
// fingerprint regardless of path, preserving idempotence.
func pstArchiveFingerprint(path string) (string, error) {
f, err := os.Open(path)
f, err := os.Open(path) // #nosec G703 -- The local CLI caller selects the PST file to fingerprint.
if err != nil {
return "", err
}
Expand Down
5 changes: 1 addition & 4 deletions internal/maildir/discover.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,7 @@ func Discover(root string) ([]Mailbox, error) {
// flags are ignored. Absence of the seen flag denotes an unread message.
func Flags(path string) []string {
name := filepath.Base(path)
flags := ""
if i := strings.LastIndex(name, ":2,"); i >= 0 {
flags = name[i+3:]
}
_, flags, _ := strings.CutLast(name, ":2,")
var labels []string
if !strings.Contains(flags, "S") {
labels = append(labels, "UNREAD")
Expand Down
6 changes: 3 additions & 3 deletions internal/mime/date.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ func PlausibleDateBounds(now time.Time) (time.Time, time.Time) {
func ParseReceivedChain(headers []string) []time.Time {
dates := make([]time.Time, 0, len(headers))
for _, header := range headers {
semicolon := strings.LastIndex(header, ";")
if semicolon < 0 {
_, date, ok := strings.CutLast(header, ";")
if !ok {
continue
}
if parsed := parseDate(strings.TrimSpace(header[semicolon+1:])); !parsed.IsZero() {
if parsed := parseDate(strings.TrimSpace(date)); !parsed.IsZero() {
dates = append(dates, parsed)
}
}
Expand Down
4 changes: 2 additions & 2 deletions internal/mime/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -553,8 +553,8 @@ func stripAddressDecorations(value string) string {

// extractDomain extracts the domain from an email address.
func extractDomain(email string) string {
if idx := strings.LastIndex(email, "@"); idx >= 0 {
return strings.ToLower(email[idx+1:])
if _, domain, ok := strings.CutLast(email, "@"); ok {
return strings.ToLower(domain)
}
return ""
}
Expand Down
8 changes: 4 additions & 4 deletions internal/oauth/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -919,12 +919,12 @@ func SameGoogleAccount(expected, canonical string) bool {
// from the local part, and mapping googlemail.com → gmail.com.
// Returns "" for non-Gmail addresses.
func normalizeGmailAddress(email string) string {
at := strings.LastIndex(email, "@")
if at < 0 {
local, domain, ok := strings.CutLast(email, "@")
if !ok {
return ""
}
local := strings.ToLower(email[:at])
domain := strings.ToLower(email[at+1:])
local = strings.ToLower(local)
domain = strings.ToLower(domain)

if domain != "gmail.com" && domain != "googlemail.com" {
return ""
Expand Down
4 changes: 2 additions & 2 deletions internal/store/sender_repair.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,8 @@ func normalizeRepairSender(sender mime.Address) (repairSender, error) {
func lenientRepairAddress(addr mime.Address) repairSender {
email := strings.ToLower(strings.TrimSpace(addr.Email))
domain := strings.ToLower(strings.TrimSpace(addr.Domain))
if at := strings.LastIndex(email, "@"); at >= 0 {
domain = email[at+1:]
if _, suffix, ok := strings.CutLast(email, "@"); ok {
domain = suffix
}
return repairSender{
email: email,
Expand Down
6 changes: 2 additions & 4 deletions internal/teams/participants.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,8 @@ func (r *participantResolver) resolve(ctx context.Context, id *Identity) (int64,
}

func (r *participantResolver) byEmail(email, displayName string) (int64, error) {
domain := ""
if at := strings.LastIndex(email, "@"); at >= 0 {
domain = strings.ToLower(email[at+1:])
}
_, domain, _ := strings.CutLast(email, "@")
domain = strings.ToLower(domain)
return r.store.EnsureParticipant(strings.ToLower(email), displayName, domain)
}

Expand Down
Loading