gmailctl is a Go CLI tool for declarative Gmail filter management. It allows users to write filters in Jsonnet, validates them, generates diffs, and applies changes via Gmail API without manual XML imports.
Key Components:
- Config Layer (
internal/engine/config/): Jsonnet parsing → v1alpha3.Config struct - Parser Layer (
internal/engine/parser/): Config → AST → Simplified criteria (query optimization) - Filter Engine (
internal/engine/filter/): AST → Gmail API filter format with diff computation - Apply Layer (
internal/engine/apply/): Orchestrates config parsing, diff generation, and API updates - Commands (
cmd/gmailctl/cmd/): CLI interface using cobra (init, apply, diff, download, test, etc.)
- Current version:
v1alpha3(ininternal/engine/config/v1alpha3/config.go) - Jsonnet files must specify
version: 'v1alpha3'in the root object - Migration path: Use
cmd/gmailctl-config-migrate/to convert between versions - YAML configs are explicitly not supported (returns error with help message)
The parser performs multi-pass query simplification (internal/engine/parser/ast.go):
- Flattens nested AND/OR operations (e.g.,
and[and[a,b],c]→and[a,b,c]) - Groups same-function queries (e.g.,
from:a OR from:b→from:{a b}) - Gmail has a 1500-char limit per filter, so simplification is critical
Example simplification:
// Before: {or: [{from: "a"}, {from: "b"}]}
// After: Single leaf with Function=FunctionFrom, Args=["a", "b"]cmd/gmailctl/cmd/api_provider.go defines the GmailAPIProvider interface. The main binary injects localcred.Provider{} for OAuth2 flows. Tests inject fakegmail (see Testing section).
Located at internal/data/gmailctl.libsonnet, embedded via //go:embed:
chainFilters(fs): Creates if-elsif chains by ANDing negations of previous filtersdirectlyTo(recipient): Matches TO field only (excludes CC/BCC)- Import in configs:
local lib = import 'gmailctl.libsonnet';
# Build the binary
go build ./cmd/gmailctl
# Run unit tests
go test ./...
# Run integration tests with golden file updates
go test -v -update ./...
# Generate coverage report
./hack/coverage.sh # Creates HTML coverage in temp fileintegration_test.go uses:
- Golden files in
testdata/valid/:.jsonnet(input),.json(expected config),.xml(expected Gmail export),.diff(expected diff) - fakegmail (
internal/fakegmail/): In-memory Gmail API server for hermetic testing - Update flag:
go test -updateregenerates golden files (review changes carefully!)
Example test flow:
- Parse
.jsonnetconfig - Apply to fake Gmail server
- Export to XML, compare with
.xmlgolden - Import back, compare with
.jsongolden - Diff against upstream, compare with
.diffgolden
- Use
testify/requirefor assertions (fails fast) - Test files use
require.Nil(t, err)pattern - Parser tests (
internal/engine/parser/ast_test.go) verify simplification logic - Use
reporting.Prettify(obj, colorize)for debug output
Use internal/errors package for wrapped errors:
errors.WithCause(err, ErrNotFound) // Wraps with sentinel
errors.WithDetails(err, "help text") // Adds user-facing details- Commands support
--color=always|auto|neverflag shouldUseColorDiff()checks flag, TERM env, and TTY status- Use
reporting.ColorizeDiff()for diff output (fatih/color package) - Recent change: Color diff feature was added to the
color-diffsbranch
FilterNode (config) → CriteriaAST (parser) → Gmail query string (filter)
- Leaf nodes:
{from: "x"}→ Leaf with Function=FunctionFrom, Args=["x"] - Operation nodes:
{and: [...]}→ Node with Operation=OperationAnd, Children=[...] - IsEscaped flag: When true, skip quoting/escaping (used for downloaded filters)
- Labels are opt-in: Only managed if
labelsfield is present in config - Supports nested labels with
/separator (e.g.,work/important) - Parent labels are auto-created during apply
- Deletion removes labels from all messages (irreversible warning shown)
cmd/gmailctl/ # Main binary
cmd/ # Cobra commands (apply, diff, etc.)
localcred/ # OAuth2 provider implementation
internal/engine/ # Core logic (no imports from cmd/)
config/ # Jsonnet → Config parsing
parser/ # Config → AST transformation
filter/ # AST → Gmail API filters + diffing
apply/ # Orchestration layer
api/ # Gmail API client wrapper
label/ # Label management
export/ # XML export for manual import
rimport/ # Import from Gmail (download command)
internal/data/ # Embedded Jsonnet library
testdata/valid/ # Integration test golden files
- Add field to
FilterNodeinconfig/v1alpha3/config.go - Add
FunctionTypeconstant inparser/ast.go - Implement parsing in
parser/parser.go(parseFunction) - Add query string generation in
filter/filter.go - Update tests and documentation
Use gmailctl debug -f config.jsonnet to see:
- Parsed config structure
- Simplified AST
- Generated Gmail query strings
- Jsonnet imports resolved relative to config file directory
- VM is created fresh for each parse (no persistent state)
- Standard library is always available via
import 'gmailctl.libsonnet'
google.golang.org/api/gmail/v1: Gmail API clientgithub.com/google/go-jsonnet: Jsonnet interpretergithub.com/spf13/cobra: CLI frameworkgithub.com/fatih/color: Terminal color outputgithub.com/pmezard/go-difflib: Diff computationinternal/graph/: Minimal fork of gosl graph package for munkres algorithm (label matching)
Users can add tests array to config for validation:
- Tests run via
gmailctl testcommand - Limitations: Cannot test
isEscapedexpressions or raw queries - See
cmd/gmailctl/cmd/test_cmd.goandinternal/engine/cfgtest/