diff --git a/carddav/backend.go b/carddav/backend.go
new file mode 100644
index 0000000..516088a
--- /dev/null
+++ b/carddav/backend.go
@@ -0,0 +1,23 @@
+package carddav
+
+// TODO: add context support
+
+import (
+ "errors"
+
+ "github.com/emersion/go-vcard"
+)
+
+var (
+ ErrNotFound = errors.New("carddav: not found")
+)
+
+type AddressObject interface {
+ ID() string
+ Card() (vcard.Card, error)
+}
+
+type AddressBook interface {
+ GetAddressObject(id string) (AddressObject, error)
+ ListAddressObjects() ([]AddressObject, error)
+}
diff --git a/carddav/carddav.go b/carddav/carddav.go
new file mode 100644
index 0000000..3cc4870
--- /dev/null
+++ b/carddav/carddav.go
@@ -0,0 +1,316 @@
+package carddav
+
+import (
+ "bytes"
+ "encoding/xml"
+ "errors"
+ "net/http"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/emersion/go-vcard"
+ "github.com/emersion/go-webdav"
+ "golang.org/x/net/context"
+
+ "log"
+)
+
+var (
+ errNotYetImplemented = errors.New("not yet implemented")
+ errUnsupported = errors.New("unsupported")
+)
+
+const nsDAV = "DAV:"
+
+var (
+ resourcetype = xml.Name{Space: nsDAV, Local: "resourcetype"}
+ displayname = xml.Name{Space: nsDAV, Local: "displayname"}
+ getcontenttype = xml.Name{Space: nsDAV, Local: "getcontenttype"}
+)
+
+const nsCardDAV = "urn:ietf:params:xml:ns:carddav"
+
+var (
+ addressBookDescription = xml.Name{Space: nsCardDAV, Local: "addressbook-description"}
+ addressBookSupportedAddressData = xml.Name{Space: nsCardDAV, Local: "supported-address-data"}
+ addressBookMaxResourceSize = xml.Name{Space: nsCardDAV, Local: "max-resource-size"}
+ addressBookHomeSet = xml.Name{Space: nsCardDAV, Local: "addressbook-home-set"}
+)
+
+type fileInfo struct {
+ name string
+ size int64
+ mode os.FileMode
+ modTime time.Time
+}
+
+func (fi *fileInfo) Name() string {
+ return fi.name
+}
+
+func (fi *fileInfo) Size() int64 {
+ return fi.size
+}
+
+func (fi *fileInfo) Mode() os.FileMode {
+ return fi.mode
+}
+
+func (fi *fileInfo) ModTime() time.Time {
+ return fi.modTime
+}
+
+func (fi *fileInfo) IsDir() bool {
+ return fi.mode.IsDir()
+}
+
+func (fi *fileInfo) Sys() interface{} {
+ return nil
+}
+
+type file struct {
+ *bytes.Reader
+ fs *fileSystem
+ name string
+ ao AddressObject
+}
+
+func (f *file) Close() error {
+ return nil
+}
+
+func (f *file) Read(b []byte) (int, error) {
+ if f.Reader == nil {
+ card, err := f.ao.Card()
+ if err != nil {
+ return 0, err
+ }
+
+ var b bytes.Buffer
+ if err := vcard.NewEncoder(&b).Encode(card); err != nil {
+ return 0, err
+ }
+
+ f.Reader = bytes.NewReader(b.Bytes())
+ }
+
+ return f.Reader.Read(b)
+}
+
+func (f *file) Write(b []byte) (int, error) {
+ return 0, errUnsupported
+}
+
+func (f *file) Seek(offset int64, whence int) (int64, error) {
+ if f.Reader == nil {
+ if _, err := f.Read(nil); err != nil {
+ return 0, err
+ }
+ }
+
+ return f.Reader.Seek(offset, whence)
+}
+
+func (f *file) Readdir(count int) ([]os.FileInfo, error) {
+ return nil, errUnsupported
+}
+
+func (f *file) Stat() (os.FileInfo, error) {
+ return &fileInfo{
+ name: f.name,
+ mode: os.ModePerm,
+ }, nil
+}
+
+// TODO: getcontenttype for file
+
+type dir struct {
+ fs *fileSystem
+ name string
+ files []os.FileInfo
+
+ n int
+}
+
+func (d *dir) Close() error {
+ return nil
+}
+
+func (d *dir) Read(b []byte) (int, error) {
+ return 0, errUnsupported
+}
+
+func (d *dir) Write(b []byte) (int, error) {
+ return 0, errUnsupported
+}
+
+func (d *dir) Seek(offset int64, whence int) (int64, error) {
+ return 0, errUnsupported
+}
+
+func (d *dir) Readdir(count int) ([]os.FileInfo, error) {
+ if d.files == nil {
+ aos, err := d.fs.ab.ListAddressObjects()
+ if err != nil {
+ return nil, err
+ }
+
+ d.files = make([]os.FileInfo, len(aos))
+ for i, ao := range aos {
+ d.files[i] = &fileInfo{
+ name: ao.ID() + ".vcf",
+ mode: os.ModePerm,
+ }
+ }
+ }
+
+ if count == 0 {
+ count = len(d.files) - d.n
+ }
+ if d.n >= len(d.files) {
+ return nil, nil
+ }
+
+ from := d.n
+ d.n += count
+ if d.n > len(d.files) {
+ d.n = len(d.files)
+ }
+
+ return d.files[from:d.n], nil
+}
+
+func (d *dir) Stat() (os.FileInfo, error) {
+ return &fileInfo{
+ name: d.name,
+ mode: os.ModeDir | os.ModePerm,
+ }, nil
+}
+
+func (d *dir) DeadProps() (map[xml.Name]webdav.Property, error) {
+ return map[xml.Name]webdav.Property{
+ resourcetype: webdav.Property{
+ XMLName: resourcetype,
+ InnerXML: []byte(``),
+ },
+ displayname: webdav.Property{
+ XMLName: displayname,
+ InnerXML: []byte("Test"),
+ },
+ addressBookDescription: webdav.Property{
+ XMLName: addressBookDescription,
+ InnerXML: []byte("C'est juste un test mdr."),
+ },
+ addressBookSupportedAddressData: webdav.Property{
+ XMLName: addressBookSupportedAddressData,
+ InnerXML: []byte(``),
+ },
+ addressBookMaxResourceSize: webdav.Property{
+ XMLName: addressBookMaxResourceSize,
+ InnerXML: []byte("102400"),
+ },
+ addressBookHomeSet: webdav.Property{
+ XMLName: addressBookHomeSet,
+ InnerXML: []byte(`/`),
+ },
+ }, nil
+}
+
+func (d *dir) Patch([]webdav.Proppatch) ([]webdav.Propstat, error) {
+ return nil, errUnsupported
+}
+
+type fileSystem struct {
+ ab AddressBook
+}
+
+func (fs *fileSystem) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
+ return errNotYetImplemented
+}
+
+func (fs *fileSystem) addressObjectID(name string) string {
+ return strings.TrimRight(strings.TrimLeft(name, "/"), ".vcf")
+}
+
+func (fs *fileSystem) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
+ if name == "/" {
+ return &dir{
+ fs: fs,
+ name: name,
+ }, nil
+ }
+
+ id := fs.addressObjectID(name)
+ ao, err := fs.ab.GetAddressObject(id)
+ if err != nil {
+ return nil, err
+ }
+
+ return &file{
+ fs: fs,
+ name: name,
+ ao: ao,
+ }, nil
+}
+
+func (fs *fileSystem) RemoveAll(ctx context.Context, name string) error {
+ return errNotYetImplemented
+}
+
+func (fs *fileSystem) Rename(ctx context.Context, oldName, newName string) error {
+ return errNotYetImplemented
+}
+
+func (fs *fileSystem) Stat(ctx context.Context, name string) (os.FileInfo, error) {
+ if name == "/" {
+ return &fileInfo{
+ name: name,
+ mode: os.ModeDir | os.ModePerm,
+ }, nil
+ }
+
+ id := fs.addressObjectID(name)
+ _, err := fs.ab.GetAddressObject(id)
+ if err != nil {
+ return nil, err
+ }
+
+ return &fileInfo{
+ name: name,
+ mode: os.ModePerm,
+ }, nil
+}
+
+type Handler struct {
+ webdav *webdav.Handler
+}
+
+func NewHandler(ab AddressBook) *Handler {
+ return &Handler{&webdav.Handler{
+ FileSystem: &fileSystem{ab},
+ Logger: func(req *http.Request, err error) {
+ if err != nil {
+ log.Println("ERROR", req, err)
+ }
+ },
+ }}
+}
+
+type responseWriter struct {
+ http.ResponseWriter
+}
+
+func (w responseWriter) Write(b []byte) (int, error) {
+ os.Stdout.Write(b)
+ return w.ResponseWriter.Write(b)
+}
+
+func (h *Handler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
+ log.Printf("%+v\n", req)
+ if req.Method == http.MethodOptions {
+ resp.Header().Add("DAV", "addressbook")
+ }
+ //h.webdav.ServeHTTP(resp, req)
+ h.webdav.ServeHTTP(responseWriter{resp}, req)
+}