go-jmap icon indicating copy to clipboard operation
go-jmap copied to clipboard

JMAP Core server implementation

Open foxcpp opened this issue 6 years ago • 2 comments

Things to consider

Below are things I believe would make library more useful.

  • go-jmap should avoid enforcing certain storage design and definitely must not provide storage implementation itself. I think it is best to simply translate API requests into corresponding method calls on some interface called "Backend" (see go-imap for a good example of how this could be done).

  • go-jmap should avoid enforcing certain authentication design and definitely must not provide authentication implementation itself. As with previous point, authentication should be implemented by calling out into user-provided interface implementation.

  • go-jmap should rely only on net/http for HTTP server. Use of more advanced frameworks will make it easier to violate the first point.

  • JMAP Core, JMAP Mail, etc implementations should be kept independent of each other for extensibility purposes. JMAP is not only about email.

foxcpp avatar Sep 05 '19 19:09 foxcpp

Here is some basic outline for the sake of discussion:

JMAP Core server logic:

// implements http.Handler so it could be simply attached to /.well-known/jmap endpoint on net/http server.
type Server struct {}

type SessionManager interface {
  // value is the value of Authentication header field.
  CheckAuth(value string) (bool, error)
  Accounts(value string) (allAccts map[ID]Account, primaryAccts map[string]ID, err error)
}

type FuncHandler func(Invocation) (Invocation, error)

type CapabilityBackend struct {
  Handlers map[string]FuncHandler
  ArgUnmarshallers map[string]FuncArgsUnmarshal
}

func NewServer(auth SessionManager, cb ...CapabilityBackend) (Server, error) {}

Here is the typical flow:

  1. Server receives the HTTP request. Authentication header is checked using SessionManager object. Request is then deserialized from JSON, inner Invocation objects are deserialized using callbacks provided by CapabilityBackend's. Additionally, JSON Pointers ("back references") are resolved at this point too (I have no thoughts on how to implement it though, any ideas are welcome). Side note: This way we free backend implementations from having to implement JSON-related boilerplate. They receive arguments as concrete structures.
  2. Each Invocation is passed to callbacks provided by CapabilityBackend's. Returned Invocations are added to the Response object and then serialized to JSON and returned as HTTP response.

JMAP Mail, JMAP Calendar, etc implementations provide factory functions that return CapabilityBackend with handlers bound to user-provided backend (database) implementation.

Here is the example of how hypothetical JMAP Todo implementation would look like (some parts are left out for brevity):

// Implementation is provided by user.
type Backend interface {
  Query(args *TodoQueryArgs) (*TodoQueryResp, error)
}

func NewTodoServer(be Backend) jmap.CapabilityBackend {
  return jmap.CapabilityBackend {
    Handlers: map[string]jmap.FuncHandler{
      "Todo/query": func(i jmap.Invocation) (Invocation, error) {
        resp, err := be.Query(i.Args.(*TodoQueryArgs))
        return Invocation{Name: i.Name, CallID: i.Name, Args: resp}, err
      },
    },
    Unmarshallers: map[string]jmap.FuncArgsUnmarshal{
      "Todo/query": unmarshalTodoQueryArgs,
    },
  },
}

foxcpp avatar Sep 05 '19 20:09 foxcpp

Here is the example of main function implementation using interfaces proposed above:

func main() {
  sessionMngr := SessionThingy{} // perhaps something OAuth-based, whatever
  todoBackend := TodoBackend{} // Our implementation of Todo DB.

  jmapSrv := jmap.NewServer(&sessionMngr, todo.NewTodoServer(todoBackend))
  http.Handle("/.well-known/jmap", &jmapSrv)
  return http.ListenAndServeTLS(...);
}

foxcpp avatar Sep 05 '19 20:09 foxcpp