diff --git a/.gitignore b/.gitignore index 5c14d2e..1fa8218 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -jmap +server.crt +server.key +TODO.md +/jmapd diff --git a/Dockerfile b/Dockerfile index 9823ee0..4ae87c5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,6 +11,8 @@ RUN CGO_ENABLED=0 go build -o /jmap ./cmd/jmapd FROM scratch COPY --from=builder /jmap /jmap +COPY --from=builder /app/server.crt /server.crt +COPY --from=builder /app/server.key /server.key EXPOSE 8080 diff --git a/Makefile b/Makefile index 48582dc..322a0a5 100644 --- a/Makefile +++ b/Makefile @@ -13,13 +13,24 @@ lint: fmt vet test @echo "Code passed fmt, vet, and tests" build: lint - go build . + go build ./cmd/jmapd/ +#GOEXPERIMENT=jsonv2 + +dbuild: + docker build -t jmap . + +drun: dbuild + docker run --rm --name jmap \ + -p 8080:8080 \ + -v "$(PWD)/server.crt:/certs/server.crt:ro" \ + -v "$(PWD)/server.key:/certs/server.key:ro" \ + jmap linux: GOOS=linux GOARCH=amd64 go build -o jmap run: build - ./jmap + ./jmapd clean: rm -f jmap diff --git a/README.md b/README.md index b1e8648..d4f424f 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,16 @@ JMAP (JSON Meta Application Protocol) is a modern, efficient protocol for synchr - cmd/jmapd: contains all code required for a jmap server. - internal: contains reusable code that isn't jmap specific. + + +# Dev Setup + +- Clone the repository using +``` +git clone github.com/maneeshaxyz/jmap +``` + +- generate a .cert and .key for testing using +``` +openssl req -x509 -newkey rsa:4096 -nodes -keyout server.key -out server.crt -days 365 -subj "/CN=localhost" +``` diff --git a/cmd/jmapd/account.go b/cmd/jmapd/account.go new file mode 100644 index 0000000..c1d3fb8 --- /dev/null +++ b/cmd/jmapd/account.go @@ -0,0 +1,12 @@ +package main + +type Accounts map[string]Account + +type Account struct { + Name string `json:"name"` + IsPersonal bool `json:"isPersonal"` + IsReadOnly bool `json:"isReadOnly"` + AccountCapabilities AccountCapabilities `json:"accountCapabilities"` +} + +type AccountCapabilities map[string]any diff --git a/cmd/jmapd/handlers.go b/cmd/jmapd/handlers.go index 94a1ff6..4e35a7c 100644 --- a/cmd/jmapd/handlers.go +++ b/cmd/jmapd/handlers.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "fmt" "net/http" ) @@ -44,5 +45,45 @@ func (app *application) healthCheck(w http.ResponseWriter, r *http.Request) { app.serverError(w, err) return } +} + +func (app *application) wellKnownHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode("/jmap/session"); err != nil { + app.serverError(w, err) + return + } +} +func (app *application) sessionHandler(w http.ResponseWriter, r *http.Request) { + session := Session{ + Capabilities: Capabilities{ + "urn:ietf:params:jmap:core": CoreCapability{ + MaxSizeUpload: 50000000, + MaxConcurrentUpload: 4, + MaxSizeRequest: 10000000, + MaxConcurrentRequests: 4, + MaxCallsInRequest: 16, + MaxObjectsInGet: 500, + MaxObjectsInSet: 500, + CollationAlgorithms: []string{"i;ascii-numeric"}, + }, + }, + PrimaryAccounts: map[string]string{ + "urn:ietf:params:jmap:mail": "account1", + }, + Username: "user@example.com", + APIURL: "https://api.example.com/jmap/", + DownloadURL: "https://api.example.com/jmap/download/{accountId}/{blobId}/{type}/{name}", + UploadURL: "https://api.example.com/jmap/upload/{accountId}", + EventSourceURL: "https://api.example.com/jmap/events?types={types}&closeafter={closeafter}&ping={ping}", + State: "some_state_string", + Accounts: make(Accounts), // Fill with actual accounts + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(session); err != nil { + app.serverError(w, err) + return + } } diff --git a/cmd/jmapd/main.go b/cmd/jmapd/main.go index 1549fc8..152c955 100644 --- a/cmd/jmapd/main.go +++ b/cmd/jmapd/main.go @@ -10,7 +10,9 @@ import ( ) type config struct { - port int + port int + certfile string + keyfile string } type application struct { @@ -26,12 +28,19 @@ type application struct { func main() { var cfg config + flag.IntVar(&cfg.port, "port", 8080, "HTTP port") + flag.StringVar(&cfg.certfile, "certfile", "server.crt", "Cert File") + flag.StringVar(&cfg.keyfile, "keyfile", "server.key", "Key File") flag.Parse() infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime) errorLog := log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile) + if cfg.certfile == "" || cfg.keyfile == "" { + errorLog.Fatal("TLS enabled: certfile and keyfile must be provided") + } + app := &application{ config: cfg, errorLog: errorLog, @@ -49,6 +58,6 @@ func main() { infoLog.Printf("Starting server on %s", srv.Addr) - err := srv.ListenAndServe() + err := srv.ListenAndServeTLS(cfg.certfile, cfg.keyfile) errorLog.Fatal(err) } diff --git a/cmd/jmapd/routes.go b/cmd/jmapd/routes.go index 2912f95..af9f7d8 100644 --- a/cmd/jmapd/routes.go +++ b/cmd/jmapd/routes.go @@ -9,6 +9,8 @@ func (a *application) routes() *http.ServeMux { mux.HandleFunc("/get/resource", a.getHandler) mux.HandleFunc("/post/resource", a.postHandler) mux.HandleFunc("/healthcheck", a.healthCheck) + mux.HandleFunc("/.well-known/jmap", a.wellKnownHandler) + mux.HandleFunc("/jmap/session", a.sessionHandler) return mux } diff --git a/cmd/jmapd/session.go b/cmd/jmapd/session.go new file mode 100644 index 0000000..3fc87ce --- /dev/null +++ b/cmd/jmapd/session.go @@ -0,0 +1,27 @@ +package main + +type Session struct { + Capabilities Capabilities `json:"capabilities"` + Accounts Accounts `json:"acounts"` + PrimaryAccounts map[string]string `json:"primaryAccounts,omitempty"` + Username string `json:"username"` + APIURL string `json:"apiUrl"` + DownloadURL string `json:"downloadUrl"` + UploadURL string `json:"uploadUrl"` + EventSourceURL string `json:"eventSourceUrl"` + State string `json:"state"` +} + +// set to map[string]any so clients can ignore unknown Capabilities +type Capabilities map[string]any + +type CoreCapability struct { + MaxSizeUpload uint64 `json:"maxSizeUpload"` + MaxConcurrentUpload uint64 `json:"maxConcurrentUpload"` + MaxSizeRequest uint64 `json:"maxSizeRequest"` + MaxConcurrentRequests uint64 `json:"maxConcurrentRequests"` + MaxCallsInRequest uint64 `json:"maxCallsInRequest"` + MaxObjectsInGet uint64 `json:"maxObjectsInGet"` + MaxObjectsInSet uint64 `json:"maxObjectsInSet"` + CollationAlgorithms []string `json:"collationAlgorithms"` +}