jmap-service-libs
Shared Go libraries for the jmap-service-* family of repositories.
Installation
go get github.com/jarrod-lowe/jmap-service-libs
Available Packages
tracing
OpenTelemetry/X-Ray instrumentation for JMAP services running on AWS Lambda.
import "github.com/jarrod-lowe/jmap-service-libs/tracing"
Features:
- Automatic X-Ray and W3C Trace Context propagation
- Standardized attribute helpers:
RequestID, AccountID, BlobID, ParentBlobID, ContentType, Function, JMAPMethod, JMAPClientID, JMAPCallIndex
- Span creation helpers:
StartHandlerSpan, StartMethodSpan, StartColdStartSpan
- Error recording with proper status codes:
RecordError
- Convenience tracer wrapper:
Tracer
logging
Structured JSON logging for Lambda environments using slog.
import "github.com/jarrod-lowe/jmap-service-libs/logging"
// Simple usage - reads LOG_LEVEL from environment (DEBUG, INFO, WARN, ERROR)
var logger = logging.New()
// Override log level programmatically
var debugLogger = logging.New(logging.WithLevel(slog.LevelDebug))
// Capture output for testing
var buf bytes.Buffer
testLogger := logging.New(logging.WithOutput(&buf))
Features:
- JSON output format for CloudWatch Logs
- Environment-based log level via
LOG_LEVEL (defaults to INFO)
- Option pattern for overriding level or output
- Zero dependencies beyond standard library
awsinit
Lambda initialization with OpenTelemetry tracing for AWS Lambda handlers.
import "github.com/jarrod-lowe/jmap-service-libs/awsinit"
// HTTP handler (API Gateway) - creates cold start span
result, err := awsinit.Init(context.Background(),
awsinit.WithHTTPHandler("jmap-api"),
)
if err != nil {
panic(err)
}
defer result.Cleanup()
ddb := dynamodb.NewFromConfig(result.Config)
handler := NewHandler(ddb)
result.Start(handler.Handle)
// Event-driven handler (SQS, SNS, etc.)
result, err := awsinit.Init(context.Background())
if err != nil {
panic(err)
}
sqsClient := sqs.NewFromConfig(result.Config)
result.Start(handler.Handle)
Features:
- Encapsulates Lambda bootstrap boilerplate (~15 lines → ~3 lines)
- Automatic OTel tracing initialization with X-Ray
- AWS config loading with OTel middleware
- Cold start span for HTTP handlers
- Handler instrumentation via
otellambda
- Distinct error types for debugging
jmaperror
Type-safe JMAP error handling per RFC 8620.
import "github.com/jarrod-lowe/jmap-service-libs/jmaperror"
// Method-level errors (returned in methodResponses)
err := jmaperror.InvalidArguments("mailboxId must be provided")
err := jmaperror.ServerFail("database error", underlyingErr)
// Set errors (per-object failures in Foo/set)
err := jmaperror.NotFound("object not found")
err := jmaperror.InvalidProperties("invalid values", []string{"name", "email"})
// HTTP problems (request-level failures as application/problem+json)
err := jmaperror.NotJSON("request body is not valid JSON")
err := jmaperror.Limit("maxSizeRequest", "request exceeds 10MB")
Features:
- MethodError:
UnknownMethod, InvalidArguments, ServerFail, AccountNotFound, InvalidResultReference, StateMismatch, Forbidden
- SetError:
NotFound, InvalidProperties, TooLarge, OverQuota, TooManyPending, BlobNotFound, InvalidMailboxId, InvalidEmail
- HTTPProblem:
UnknownCapability, NotJSON, NotRequest, Limit
- Common
JMAPError interface with Type() and ToMap() methods
- Proper Go error wrapping with
Unwrap() for ServerFail
dbclient
Shared DynamoDB client utilities for JMAP services.
import "github.com/jarrod-lowe/jmap-service-libs/dbclient"
// Create client from AWS config (integrates with awsinit)
result, _ := awsinit.Init(ctx, awsinit.WithHTTPHandler("my-handler"))
ddb := dbclient.NewClient(result.Config)
repo := myrepo.New(ddb, os.Getenv("TABLE_NAME"))
// Use key helpers
pk := dbclient.AccountPK("account-123") // "ACCOUNT#account-123"
pk := dbclient.UserPK("user-456") // "USER#user-456"
// Error handling
if dbclient.IsConditionalCheckFailed(err) {
return ErrNotFound
}
if idx := dbclient.GetConditionalCheckFailureIndex(err); idx >= 0 {
// Handle specific item failure in transaction
}
Features:
DynamoDBClient interface for testable repository dependencies
NewClient(cfg aws.Config) helper integrating with awsinit
- Key constants:
AttrPK, AttrSK, PrefixAccount, PrefixUser, SKMeta
- Key helpers:
AccountPK(id), UserPK(id)
- Error helpers:
IsConditionalCheckFailed, IsTransactionCanceled, GetTransactionCancellationReasons, HasConditionalCheckFailure, GetConditionalCheckFailureIndex
plugincontract
JMAP plugin communication types for the core-to-plugin invocation contract.
import "github.com/jarrod-lowe/jmap-service-libs/plugincontract"
func handler(ctx context.Context, req plugincontract.PluginInvocationRequest) (plugincontract.PluginInvocationResponse, error) {
// Extract arguments using type-safe Args helpers
accountID, _ := req.Args.String("accountId")
ids, _ := req.Args.StringSlice("ids")
limit := req.Args.IntOr("limit", 100)
// Process request...
return plugincontract.PluginInvocationResponse{
MethodResponse: plugincontract.MethodResponse{
Name: req.Method,
Args: plugincontract.Args{"accountId": accountID, "list": results},
ClientID: req.ClientID,
},
}, nil
}
Features:
PluginInvocationRequest - Request payload sent from core to plugin
PluginInvocationResponse - Response wrapper from plugin to core
MethodResponse - JMAP method response structure
EventPayload - System event payload delivered via SQS
Args type with helper methods: String, StringOr, Int, IntOr, Float, Bool, BoolOr, StringSlice, Object, Has
See docs/plugin-interface.md for the full plugin author guide.
Planned Migrations
The following code patterns have been identified across jmap-service-core and jmap-service-email as candidates for migration to this shared library.
High Priority — Identical or Near-Identical Code
These patterns exist in both repositories with minimal variation:
| Package |
Description |
Source Locations |
logging |
Structured JSON logging setup with slog.NewJSONHandler |
Done - see logging package |
awsinit |
AWS SDK config loading with OTel middleware instrumentation |
Done - see awsinit package |
jmaperror |
JMAP protocol error response formatting, standard error type constants (unknownMethod, invalidArguments, serverFail, etc.) |
Done - see jmaperror package |
dbclient |
DynamoDB client interface definition, key prefix constants (ACCOUNT#, META#, etc.), conditional check error handling helpers |
Done - see dbclient package |
plugincontract |
JMAP plugin invocation request/response types (PluginInvocationRequest, PluginInvocationResponse, MethodResponse) |
Done - see plugincontract package |
apiresponse |
API Gateway proxy response formatting, HTTP error response helpers |
jmap-service-core/cmd/blob-upload/main.go, jmap-service-core/cmd/blob-download/main.go |
Medium Priority — Similar Patterns Requiring Abstraction
These patterns are similar but need some generalization:
| Package |
Description |
Source Locations |
validation |
Media type (MIME) validation, AWS resource tag validation |
jmap-service-core/internal/bloballocate/allocate.go, jmap-service-core/cmd/blob-upload/main.go |
arnutil |
ARN parsing, SQS ARN to Queue URL conversion |
jmap-service-core/cmd/account-init/main.go |
httpclient |
HTTP client wrapper with exponential backoff retry |
jmap-service-email/internal/blob/client.go |
sqspublish |
Generic SQS message publisher with JSON serialization |
jmap-service-email/internal/blobdelete/publisher.go, jmap-service-email/internal/mailboxcleanup/publisher.go |
txerror |
DynamoDB TransactionCanceledException handling, conditional check failure detection and categorization |
Done - merged into dbclient package |
Future Candidates — Currently in One Repository
These patterns exist in only one repo but are likely needed as more services are added:
| Package |
Description |
Current Location |
Rationale |
auth |
Account ID extraction from JWT claims and IAM path parameters, IAM authentication detection, ARN normalization, principal authorization |
jmap-service-core/cmd/*/main.go, jmap-service-core/internal/plugin/authorization.go |
Any service needing direct client authentication |
resultref |
JMAP result reference resolution (RFC 8620 §3.7), JSON pointer evaluation with wildcard support |
jmap-service-core/internal/resultref/ |
Any service processing JMAP method calls |
plugininvoke |
Lambda-based plugin invocation interface and implementation |
jmap-service-core/internal/plugin/invoker.go |
Core pattern for JMAP method dispatch |
pluginregistry |
Plugin metadata registry, method-to-Lambda routing, capability management |
jmap-service-core/internal/plugin/registry.go |
Central to JMAP multi-service architecture |
emailparse |
RFC 5322 email parsing, MIME structure extraction, body part handling |
jmap-service-email/internal/email/parser.go |
Any email-related service |
headers |
Email header parsing (RFC 2047 decoding, address list parsing, date parsing) |
jmap-service-email/internal/headers/ |
Any email-related service |
charset |
Character set detection and decoding for email body content |
jmap-service-email/internal/charset/ |
Any email-related service |
keywords |
JMAP Email keyword validation per RFC 8621 |
jmap-service-email/internal/email/keywords.go |
Any Email/* method handler |
Architectural Patterns (Document Only)
These patterns should be documented as best practices but don't require code extraction:
- Lambda Dependency Injection — Handler struct with interface dependencies, initialized in
main(), enables testability
- Single-Table Design Keys —
PK() and SK() methods on domain types with consistent prefix constants
- Repository Builder Pattern — Methods returning
[]types.TransactWriteItem for composable atomic transactions
- Functional Mocks — Test mocks with optional function pointers for flexible per-test stubbing
- SQS/DynamoDB Streams Consumer Pattern — Standard event loop with batch failure tracking
Development
Using Local Changes in Sibling Services
When developing changes to this library alongside a consuming service, use a replace directive in the consuming service's go.mod:
replace github.com/jarrod-lowe/jmap-service-libs => ../jmap-service-libs
Remember to remove the replace directive before committing.
Development Commands
make help # Show all available targets
make deps # Tidy dependencies
make test # Run all tests
make lint # Run golangci-lint
make fmt # Format code
make clean # Remove build artifacts
Contributing
When adding a new package:
- Create a directory at the root level (e.g.,
tracing/)
- Include comprehensive tests
- Add package documentation in a
doc.go file
- Update this README with the package description