From e3897edc148db59b70ed05743ec5210452e47e29 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=A2=96=E9=80=B8?=
 <49649786+Zuoqiu-Yingyi@users.noreply.github.com>
Date: Fri, 25 Oct 2024 01:03:51 +0800
Subject: [PATCH 1/9] :art: add CardDAV server

---
 kernel/go.mod           |  2 ++
 kernel/go.sum           |  7 ++++
 kernel/model/carddav.go | 74 +++++++++++++++++++++++++++++++++++++++++
 kernel/server/serve.go  | 28 +++++++++++++++-
 4 files changed, 110 insertions(+), 1 deletion(-)
 create mode 100644 kernel/model/carddav.go

diff --git a/kernel/go.mod b/kernel/go.mod
index f4ef57ab75d..1856a389034 100644
--- a/kernel/go.mod
+++ b/kernel/go.mod
@@ -98,6 +98,8 @@ require (
 	github.com/dlclark/regexp2 v1.11.4 // indirect
 	github.com/dsnet/compress v0.0.1 // indirect
 	github.com/dustin/go-humanize v1.0.1 // indirect
+	github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9 // indirect
+	github.com/emersion/go-webdav v0.5.0 // indirect
 	github.com/fatih/set v0.2.1 // indirect
 	github.com/gigawattio/window v0.0.0-20180317192513-0f5467e35573 // indirect
 	github.com/gin-contrib/sse v0.1.0 // indirect
diff --git a/kernel/go.sum b/kernel/go.sum
index a852cf4911a..949add86596 100644
--- a/kernel/go.sum
+++ b/kernel/go.sum
@@ -89,6 +89,11 @@ github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5Jflh
 github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f/go.mod h1:2MKFUgfNMULRxqZkadG1Vh44we3y5gJAtTBlVsx1BKQ=
+github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9 h1:ATgqloALX6cHCranzkLb8/zjivwQ9DWWDCQRnxTPfaA=
+github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
+github.com/emersion/go-webdav v0.5.0 h1:Ak/BQLgAihJt/UxJbCsEXDPxS5Uw4nZzgIMOq3rkKjc=
+github.com/emersion/go-webdav v0.5.0/go.mod h1:ycyIzTelG5pHln4t+Y32/zBvmrM7+mV7x+V+Gx4ZQno=
 github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
 github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
 github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:IT4JYU7k4ikYg1SCxNI1/Tieq/NFvh6dzLdgi7eu0tM=
@@ -367,6 +372,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/studio-b12/gowebdav v0.9.0 h1:1j1sc9gQnNxbXXM4M/CebPOX4aXYtr7MojAVcN4dHjU=
 github.com/studio-b12/gowebdav v0.9.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
+github.com/teambition/rrule-go v1.7.2/go.mod h1:mBJ1Ht5uboJ6jexKdNUJg2NcwP8uUMNvStWXlJD3MvU=
+github.com/teambition/rrule-go v1.8.2/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4=
 github.com/tetratelabs/wazero v1.7.3 h1:PBH5KVahrt3S2AHgEjKu4u+LlDbbk+nsGE3KLucy6Rw=
 github.com/tetratelabs/wazero v1.7.3/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
 github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
diff --git a/kernel/model/carddav.go b/kernel/model/carddav.go
new file mode 100644
index 00000000000..249d8f32ada
--- /dev/null
+++ b/kernel/model/carddav.go
@@ -0,0 +1,74 @@
+// SiYuan - Refactor your thinking
+// Copyright (c) 2020-present, b3log.org
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+package model
+
+import (
+	"context"
+
+	"github.com/emersion/go-vcard"
+	"github.com/emersion/go-webdav/carddav"
+)
+
+const (
+	// REF: https://developers.google.com/people/carddav#resources
+	CardDavPrincipalPath   = "/carddav"
+	CardDavHomeSetPath     = CardDavPrincipalPath + "/contacts"
+	CardDavAddressBookPath = CardDavHomeSetPath + "/default"
+)
+
+type CardDavBackend struct{}
+
+func (b *CardDavBackend) CurrentUserPrincipal(ctx context.Context) (string, error) {
+	return CardDavPrincipalPath, nil
+}
+
+func (b *CardDavBackend) AddressbookHomeSetPath(ctx context.Context) (string, error) {
+	return CardDavHomeSetPath, nil
+}
+
+func (b *CardDavBackend) AddressBook(ctx context.Context) (*carddav.AddressBook, error) {
+	return &carddav.AddressBook{
+		Path:        CardDavAddressBookPath,
+		Name:        "SiYuan",
+		Description: "SiYuan default contacts",
+	}, nil
+}
+
+func (b *CardDavBackend) GetAddressObject(ctx context.Context, path string, req *carddav.AddressDataRequest) (addressObject *carddav.AddressObject, err error) {
+	// TODO
+	return
+}
+
+func (b *CardDavBackend) ListAddressObjects(ctx context.Context, req *carddav.AddressDataRequest) (addressObjects []carddav.AddressObject, err error) {
+	// TODO
+	return
+}
+
+func (b *CardDavBackend) QueryAddressObjects(ctx context.Context, query *carddav.AddressBookQuery) (addressObjects []carddav.AddressObject, err error) {
+	// TODO
+	return
+}
+
+func (b *CardDavBackend) PutAddressObject(ctx context.Context, path string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (loc string, err error) {
+	// TODO
+	return
+}
+
+func (b *CardDavBackend) DeleteAddressObject(ctx context.Context, path string) (err error) {
+	// TODO
+	return
+}
diff --git a/kernel/server/serve.go b/kernel/server/serve.go
index 4a42d169132..d4b3cfb377c 100644
--- a/kernel/server/serve.go
+++ b/kernel/server/serve.go
@@ -32,6 +32,7 @@ import (
 	"time"
 
 	"github.com/88250/gulu"
+	"github.com/emersion/go-webdav/carddav"
 	"github.com/gin-contrib/gzip"
 	"github.com/gin-contrib/sessions"
 	"github.com/gin-contrib/sessions/cookie"
@@ -88,6 +89,7 @@ func Serve(fastMode bool) {
 	serveAppearance(ginServer)
 	serveWebSocket(ginServer)
 	serveWebDAV(ginServer)
+	serveCardDAV(ginServer)
 	serveExport(ginServer)
 	serveWidgets(ginServer)
 	servePlugins(ginServer)
@@ -616,7 +618,31 @@ func serveWebDAV(ginServer *gin.Engine) {
 	}
 
 	ginGroup := ginServer.Group("/webdav", model.CheckAuth, model.CheckAdminRole)
-	ginGroup.Match(WebDavMethod, "/*path", func(c *gin.Context) {
+	ginGroup.Any("/*path", func(c *gin.Context) {
+		if util.ReadOnly {
+			switch c.Request.Method {
+			case "POST", "PUT", "DELETE", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", "PROPPATCH":
+				c.AbortWithError(http.StatusForbidden, fmt.Errorf(model.Conf.Language(34)))
+				return
+			}
+		}
+		handler.ServeHTTP(c.Writer, c.Request)
+	})
+}
+
+func serveCardDAV(ginServer *gin.Engine) {
+	// REF: https://github.com/emersion/hydroxide/blob/master/carddav/carddav.go
+	handler := carddav.Handler{
+		Backend: &model.CardDavBackend{},
+		Prefix:  "",
+	}
+
+	ginServer.Any("/.well-known/caldav", func(c *gin.Context) {
+		handler.ServeHTTP(c.Writer, c.Request)
+	})
+
+	ginGroup := ginServer.Group("/carddav", model.CheckAuth, model.CheckAdminRole)
+	ginGroup.Any("/*path", func(c *gin.Context) {
 		if util.ReadOnly {
 			switch c.Request.Method {
 			case "POST", "PUT", "DELETE", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", "PROPPATCH":

From d8d76cd32c503404a6efc40ac37aa535c33110a3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=A2=96=E9=80=B8?=
 <49649786+Zuoqiu-Yingyi@users.noreply.github.com>
Date: Fri, 25 Oct 2024 10:36:39 +0800
Subject: [PATCH 2/9] :art: change CardDAV principals path

---
 kernel/model/carddav.go | 10 ++++++----
 kernel/server/serve.go  |  4 ++--
 2 files changed, 8 insertions(+), 6 deletions(-)

diff --git a/kernel/model/carddav.go b/kernel/model/carddav.go
index 249d8f32ada..765b45972c9 100644
--- a/kernel/model/carddav.go
+++ b/kernel/model/carddav.go
@@ -25,15 +25,17 @@ import (
 
 const (
 	// REF: https://developers.google.com/people/carddav#resources
-	CardDavPrincipalPath   = "/carddav"
-	CardDavHomeSetPath     = CardDavPrincipalPath + "/contacts"
-	CardDavAddressBookPath = CardDavHomeSetPath + "/default"
+	CardDavPrefixPath               = "/carddav"
+	CardDavPrincipalsPath           = CardDavPrefixPath + "/principals"
+	CardDavCurrentUserPrincipalPath = CardDavPrincipalsPath + "/main"
+	CardDavHomeSetPath              = CardDavCurrentUserPrincipalPath + "/contacts"
+	CardDavAddressBookPath          = CardDavHomeSetPath + "/default"
 )
 
 type CardDavBackend struct{}
 
 func (b *CardDavBackend) CurrentUserPrincipal(ctx context.Context) (string, error) {
-	return CardDavPrincipalPath, nil
+	return CardDavCurrentUserPrincipalPath, nil
 }
 
 func (b *CardDavBackend) AddressbookHomeSetPath(ctx context.Context) (string, error) {
diff --git a/kernel/server/serve.go b/kernel/server/serve.go
index d4b3cfb377c..dae5bb096a0 100644
--- a/kernel/server/serve.go
+++ b/kernel/server/serve.go
@@ -634,14 +634,14 @@ func serveCardDAV(ginServer *gin.Engine) {
 	// REF: https://github.com/emersion/hydroxide/blob/master/carddav/carddav.go
 	handler := carddav.Handler{
 		Backend: &model.CardDavBackend{},
-		Prefix:  "",
+		Prefix:  model.CardDavPrefixPath,
 	}
 
 	ginServer.Any("/.well-known/caldav", func(c *gin.Context) {
 		handler.ServeHTTP(c.Writer, c.Request)
 	})
 
-	ginGroup := ginServer.Group("/carddav", model.CheckAuth, model.CheckAdminRole)
+	ginGroup := ginServer.Group(model.CardDavPrefixPath, model.CheckAuth, model.CheckAdminRole)
 	ginGroup.Any("/*path", func(c *gin.Context) {
 		if util.ReadOnly {
 			switch c.Request.Method {

From 734c799aa59e5acb958e50e371d50a206d681e92 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=A2=96=E9=80=B8?=
 <49649786+Zuoqiu-Yingyi@users.noreply.github.com>
Date: Sat, 26 Oct 2024 02:17:22 +0800
Subject: [PATCH 3/9] :art: implement load contacts feature

---
 kernel/go.mod           |   4 +-
 kernel/go.sum           |   7 +-
 kernel/model/carddav.go | 198 ++++++++++++++++++++++++++++++++++++----
 kernel/model/session.go |   2 +-
 kernel/server/serve.go  |  56 +++++++++---
 5 files changed, 230 insertions(+), 37 deletions(-)

diff --git a/kernel/go.mod b/kernel/go.mod
index 1856a389034..99d3402ec15 100644
--- a/kernel/go.mod
+++ b/kernel/go.mod
@@ -23,6 +23,8 @@ require (
 	github.com/denisbrodbeck/machineid v1.0.1
 	github.com/dgraph-io/ristretto v1.0.0
 	github.com/djherbis/times v1.6.0
+	github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9
+	github.com/emersion/go-webdav v0.5.1-0.20240713135526-7f8c17ad7135
 	github.com/emirpasic/gods v1.18.1
 	github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb
 	github.com/flopp/go-findfont v0.1.0
@@ -98,8 +100,6 @@ require (
 	github.com/dlclark/regexp2 v1.11.4 // indirect
 	github.com/dsnet/compress v0.0.1 // indirect
 	github.com/dustin/go-humanize v1.0.1 // indirect
-	github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9 // indirect
-	github.com/emersion/go-webdav v0.5.0 // indirect
 	github.com/fatih/set v0.2.1 // indirect
 	github.com/gigawattio/window v0.0.0-20180317192513-0f5467e35573 // indirect
 	github.com/gin-contrib/sse v0.1.0 // indirect
diff --git a/kernel/go.sum b/kernel/go.sum
index 949add86596..3c0ab3a949d 100644
--- a/kernel/go.sum
+++ b/kernel/go.sum
@@ -89,11 +89,11 @@ github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5Jflh
 github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
-github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f/go.mod h1:2MKFUgfNMULRxqZkadG1Vh44we3y5gJAtTBlVsx1BKQ=
+github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6/go.mod h1:BEksegNspIkjCQfmzWgsgbu6KdeJ/4LwUZs7DMBzjzw=
 github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9 h1:ATgqloALX6cHCranzkLb8/zjivwQ9DWWDCQRnxTPfaA=
 github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
-github.com/emersion/go-webdav v0.5.0 h1:Ak/BQLgAihJt/UxJbCsEXDPxS5Uw4nZzgIMOq3rkKjc=
-github.com/emersion/go-webdav v0.5.0/go.mod h1:ycyIzTelG5pHln4t+Y32/zBvmrM7+mV7x+V+Gx4ZQno=
+github.com/emersion/go-webdav v0.5.1-0.20240713135526-7f8c17ad7135 h1:Ssk00uh7jhctJ23eclGxhhGqplSQB+wCt6fmbjhnOS8=
+github.com/emersion/go-webdav v0.5.1-0.20240713135526-7f8c17ad7135/go.mod h1:mI8iBx3RAODwX7PJJ7qzsKAKs/vY429YfS2/9wKnDbQ=
 github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
 github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
 github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:IT4JYU7k4ikYg1SCxNI1/Tieq/NFvh6dzLdgi7eu0tM=
@@ -372,7 +372,6 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/studio-b12/gowebdav v0.9.0 h1:1j1sc9gQnNxbXXM4M/CebPOX4aXYtr7MojAVcN4dHjU=
 github.com/studio-b12/gowebdav v0.9.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
-github.com/teambition/rrule-go v1.7.2/go.mod h1:mBJ1Ht5uboJ6jexKdNUJg2NcwP8uUMNvStWXlJD3MvU=
 github.com/teambition/rrule-go v1.8.2/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4=
 github.com/tetratelabs/wazero v1.7.3 h1:PBH5KVahrt3S2AHgEjKu4u+LlDbbk+nsGE3KLucy6Rw=
 github.com/tetratelabs/wazero v1.7.3/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
diff --git a/kernel/model/carddav.go b/kernel/model/carddav.go
index 765b45972c9..03501463001 100644
--- a/kernel/model/carddav.go
+++ b/kernel/model/carddav.go
@@ -17,60 +17,226 @@
 package model
 
 import (
+	"bytes"
 	"context"
+	"fmt"
+	"os"
+	"path"
+	"path/filepath"
+	"strings"
+	"sync"
 
+	"github.com/88250/gulu"
 	"github.com/emersion/go-vcard"
 	"github.com/emersion/go-webdav/carddav"
+	"github.com/siyuan-note/logging"
+	"github.com/siyuan-note/siyuan/kernel/util"
 )
 
 const (
 	// REF: https://developers.google.com/people/carddav#resources
-	CardDavPrefixPath               = "/carddav"
-	CardDavPrincipalsPath           = CardDavPrefixPath + "/principals"
-	CardDavCurrentUserPrincipalPath = CardDavPrincipalsPath + "/main"
-	CardDavHomeSetPath              = CardDavCurrentUserPrincipalPath + "/contacts"
-	CardDavAddressBookPath          = CardDavHomeSetPath + "/default"
+	CardDavPrefixPath        = "/carddav"
+	CardDavRootPath          = CardDavPrefixPath + "/principals"      // 0 resourceTypeRoot
+	CardDavUserPrincipalPath = CardDavRootPath + "/main"              // 1 resourceTypeUserPrincipal
+	CardDavHomeSetPath       = CardDavUserPrincipalPath + "/contacts" // 2 resourceTypeAddressBookHomeSet
+
+	CardDavDefaultAddressBookPath = CardDavHomeSetPath + "/default" // 3 resourceTypeAddressBook
+	CardDavDefaultAddressBookName = "default"
+
+	CardDavAddressBooksMetaDataFilePath = CardDavHomeSetPath + "/address-books.json"
 )
 
+var (
+	defaultAddressBook = carddav.AddressBook{
+		Path:        CardDavDefaultAddressBookPath,
+		Name:        CardDavDefaultAddressBookName,
+		Description: "Default address book",
+		// MaxResourceSize: math.MaxInt32,
+	}
+	contacts = Contacts{
+		M: sync.Map{},
+	}
+)
+
+type Contacts struct {
+	M sync.Map // Path -> *AddressBook
+}
+
+func (c *Contacts) reload() error {
+	c.M.Clear()
+	addressBooksMetaDataFilePath := filepath.Join(util.DataDir, "storage", strings.TrimPrefix(CardDavAddressBooksMetaDataFilePath, "/"))
+	metaData, err := os.ReadFile(addressBooksMetaDataFilePath)
+	if os.IsNotExist(err) {
+		// create meta data file
+		if err = os.MkdirAll(CardDavDefaultAddressBookPath, 0755); err != nil {
+			logging.LogErrorf("make dir [%s] failed: %s", CardDavDefaultAddressBookPath, err)
+			return err
+		}
+
+		addressBooksMetaData := []*carddav.AddressBook{&defaultAddressBook}
+		data, err := gulu.JSON.MarshalIndentJSON(addressBooksMetaData, "", "  ")
+		if err != nil {
+			logging.LogErrorf("marshal address books meta data failed: %s", err)
+			return err
+		}
+
+		if err := os.WriteFile(addressBooksMetaDataFilePath, data, 0755); err != nil {
+			logging.LogErrorf("write file [%s] failed: %s", addressBooksMetaDataFilePath, err)
+			return err
+		}
+	} else {
+		// load meta data file
+		addressBooksMetaData := []*carddav.AddressBook{}
+		if err = gulu.JSON.UnmarshalJSON(metaData, &addressBooksMetaData); err != nil {
+			logging.LogErrorf("unmarshal address books meta data failed: %s", err)
+			return err
+		}
+
+		wg := &sync.WaitGroup{}
+		wg.Add(len(addressBooksMetaData))
+		for _, addressBookMetaData := range addressBooksMetaData {
+			addressBook := &AddressBook{
+				MetaData:  addressBookMetaData,
+				Addresses: sync.Map{},
+			}
+			c.M.Store(addressBookMetaData.Path, addressBook)
+			go addressBook.load(wg)
+		}
+		wg.Wait()
+	}
+	return nil
+}
+
+type AddressBook struct {
+	MetaData  *carddav.AddressBook
+	Addresses sync.Map // id -> *carddav.AddressObject
+}
+
+func (b *AddressBook) load(wg *sync.WaitGroup) {
+	defer wg.Done()
+	addressBookPath := filepath.Join(util.DataDir, "storage", strings.TrimPrefix(b.MetaData.Path, "/"))
+	entries, err := os.ReadDir(addressBookPath)
+	if err != nil {
+		logging.LogErrorf("read dir [%s] failed: %s", addressBookPath, err)
+	} else {
+		for _, entry := range entries {
+			if !entry.IsDir() {
+				filename := entry.Name()
+				ext := path.Ext(filename)
+				if ext == ".vcf" {
+					wg.Add(1)
+					go func() {
+						defer wg.Done()
+
+						addressFilePath := path.Join(addressBookPath, filename)
+
+						addressFileInfo, err := entry.Info()
+						if err != nil {
+							logging.LogErrorf("get file [%s] info failed: %s", addressFilePath, err)
+							return
+						}
+
+						// read file
+						addressData, err := os.ReadFile(addressFilePath)
+						if err != nil {
+							logging.LogErrorf("read file [%s] failed: %s", addressFilePath, err)
+							return
+						}
+
+						// decode file
+						reader := bytes.NewReader(addressData)
+						decoder := vcard.NewDecoder(reader)
+						card, err := decoder.Decode()
+						if err != nil {
+							logging.LogErrorf("decode file [%s] failed: %s", addressFilePath, err)
+							return
+						}
+
+						// load data
+						address := &carddav.AddressObject{
+							Path:          b.MetaData.Path + "/" + filename,
+							ModTime:       addressFileInfo.ModTime(),
+							ContentLength: addressFileInfo.Size(),
+							ETag:          fmt.Sprintf("%x-%x", addressFileInfo.ModTime(), addressFileInfo.Size()),
+							Card:          card,
+						}
+
+						id := path.Base(filename)
+						b.Addresses.Store(id, address)
+					}()
+				}
+			}
+		}
+	}
+}
+
+func (b *AddressBook) save(wg *sync.WaitGroup) {
+	defer wg.Done()
+	// TODO: save addresses data to files
+}
+
 type CardDavBackend struct{}
 
 func (b *CardDavBackend) CurrentUserPrincipal(ctx context.Context) (string, error) {
-	return CardDavCurrentUserPrincipalPath, nil
+	logging.LogInfof("CardDAV CurrentUserPrincipal")
+	return CardDavUserPrincipalPath, nil
 }
 
-func (b *CardDavBackend) AddressbookHomeSetPath(ctx context.Context) (string, error) {
+func (b *CardDavBackend) AddressBookHomeSetPath(ctx context.Context) (string, error) {
+	logging.LogInfof("CardDAV AddressBookHomeSetPath")
 	return CardDavHomeSetPath, nil
 }
 
-func (b *CardDavBackend) AddressBook(ctx context.Context) (*carddav.AddressBook, error) {
-	return &carddav.AddressBook{
-		Path:        CardDavAddressBookPath,
-		Name:        "SiYuan",
-		Description: "SiYuan default contacts",
-	}, nil
+func (b *CardDavBackend) ListAddressBooks(ctx context.Context) (addressBooks []carddav.AddressBook, err error) {
+	logging.LogInfof("CardDAV ListAddressBooks")
+	// TODO
+	return
+}
+
+func (b *CardDavBackend) GetAddressBook(ctx context.Context, path string) (addressBook *carddav.AddressBook, err error) {
+	logging.LogInfof("CardDAV GetAddressBook")
+	// TODO
+	return
+}
+
+func (b *CardDavBackend) CreateAddressBook(ctx context.Context, addressBook *carddav.AddressBook) (err error) {
+	logging.LogInfof("CardDAV CreateAddressBook")
+	// TODO
+	return
+}
+
+func (b *CardDavBackend) DeleteAddressBook(ctx context.Context, path string) (err error) {
+	logging.LogInfof("CardDAV DeleteAddressBook")
+	// TODO
+	return
 }
 
 func (b *CardDavBackend) GetAddressObject(ctx context.Context, path string, req *carddav.AddressDataRequest) (addressObject *carddav.AddressObject, err error) {
+	logging.LogInfof("CardDAV GetAddressObject: %s", path)
 	// TODO
 	return
 }
 
-func (b *CardDavBackend) ListAddressObjects(ctx context.Context, req *carddav.AddressDataRequest) (addressObjects []carddav.AddressObject, err error) {
+func (b *CardDavBackend) ListAddressObjects(ctx context.Context, path string, req *carddav.AddressDataRequest) (addressObjects []carddav.AddressObject, err error) {
+	logging.LogInfof("CardDAV ListAddressObjects")
 	// TODO
 	return
 }
 
-func (b *CardDavBackend) QueryAddressObjects(ctx context.Context, query *carddav.AddressBookQuery) (addressObjects []carddav.AddressObject, err error) {
+func (b *CardDavBackend) QueryAddressObjects(ctx context.Context, path string, query *carddav.AddressBookQuery) (addressObjects []carddav.AddressObject, err error) {
+	logging.LogInfof("CardDAV QueryAddressObjects: %v", query)
 	// TODO
 	return
 }
 
-func (b *CardDavBackend) PutAddressObject(ctx context.Context, path string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (loc string, err error) {
+func (b *CardDavBackend) PutAddressObject(ctx context.Context, path string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (addressObject *carddav.AddressObject, err error) {
+	logging.LogInfof("CardDAV PutAddressObject: %s", path)
 	// TODO
 	return
 }
 
 func (b *CardDavBackend) DeleteAddressObject(ctx context.Context, path string) (err error) {
+	logging.LogInfof("CardDAV DeleteAddressObject: %s", path)
 	// TODO
 	return
 }
diff --git a/kernel/model/session.go b/kernel/model/session.go
index ef0d4244026..3dc3bca8efa 100644
--- a/kernel/model/session.go
+++ b/kernel/model/session.go
@@ -300,7 +300,7 @@ func CheckAuth(c *gin.Context) {
 	}
 
 	// WebDAV BasicAuth Authenticate
-	if strings.HasPrefix(c.Request.RequestURI, "/webdav") {
+	if strings.HasPrefix(c.Request.RequestURI, "/webdav") || strings.HasPrefix(c.Request.RequestURI, "/carddav") {
 		c.Header("WWW-Authenticate", "Basic realm=Authorization Required")
 		c.AbortWithStatus(http.StatusUnauthorized)
 		return
diff --git a/kernel/server/serve.go b/kernel/server/serve.go
index dae5bb096a0..8be8a63f437 100644
--- a/kernel/server/serve.go
+++ b/kernel/server/serve.go
@@ -48,17 +48,42 @@ import (
 	"golang.org/x/net/webdav"
 )
 
+const (
+	MethodMkcol     = "MKCOL"
+	MethodCopy      = "COPY"
+	MethodMove      = "MOVE"
+	MethodLock      = "LOCK"
+	MethodUnlock    = "UNLOCK"
+	MethodPropfind  = "PROPFIND"
+	MethodProppatch = "PROPPATCH"
+)
+
 var (
-	cookieStore  = cookie.NewStore([]byte("ATN51UlxVq1Gcvdf"))
-	WebDavMethod = []string{
-		"OPTIONS",
-		"GET", "HEAD",
-		"POST", "PUT",
-		"DELETE",
-		"MKCOL",
-		"COPY", "MOVE",
-		"LOCK", "UNLOCK",
-		"PROPFIND", "PROPPATCH",
+	cookieStore   = cookie.NewStore([]byte("ATN51UlxVq1Gcvdf"))
+	WebDavMethods = []string{
+		http.MethodOptions,
+		http.MethodHead,
+		http.MethodGet,
+		http.MethodPost,
+		http.MethodPut,
+		http.MethodDelete,
+
+		MethodMkcol,
+		MethodCopy,
+		MethodMove,
+		MethodLock,
+		MethodUnlock,
+		MethodPropfind,
+		MethodProppatch,
+	}
+	CardDavMethods = []string{
+		http.MethodOptions,
+		http.MethodHead,
+		http.MethodGet,
+		http.MethodPut,
+		http.MethodDelete,
+
+		MethodPropfind,
 	}
 )
 
@@ -618,7 +643,8 @@ func serveWebDAV(ginServer *gin.Engine) {
 	}
 
 	ginGroup := ginServer.Group("/webdav", model.CheckAuth, model.CheckAdminRole)
-	ginGroup.Any("/*path", func(c *gin.Context) {
+	// ginGroup.Any NOT support extension methods (PROPFIND etc.)
+	ginGroup.Match(WebDavMethods, "/*path", func(c *gin.Context) {
 		if util.ReadOnly {
 			switch c.Request.Method {
 			case "POST", "PUT", "DELETE", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", "PROPPATCH":
@@ -634,15 +660,17 @@ func serveCardDAV(ginServer *gin.Engine) {
 	// REF: https://github.com/emersion/hydroxide/blob/master/carddav/carddav.go
 	handler := carddav.Handler{
 		Backend: &model.CardDavBackend{},
-		Prefix:  model.CardDavPrefixPath,
+		Prefix:  model.CardDavRootPath,
 	}
 
-	ginServer.Any("/.well-known/caldav", func(c *gin.Context) {
+	ginServer.Match(CardDavMethods, "/.well-known/carddav", func(c *gin.Context) {
+		logging.LogInfof("CardDAV %s: /.well-known/carddav", c.Request.Method)
 		handler.ServeHTTP(c.Writer, c.Request)
 	})
 
 	ginGroup := ginServer.Group(model.CardDavPrefixPath, model.CheckAuth, model.CheckAdminRole)
-	ginGroup.Any("/*path", func(c *gin.Context) {
+	ginGroup.Match(CardDavMethods, "/*path", func(c *gin.Context) {
+		logging.LogInfof("CardDAV %s: %s", c.Request.Method, c.Request.URL.Path)
 		if util.ReadOnly {
 			switch c.Request.Method {
 			case "POST", "PUT", "DELETE", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", "PROPPATCH":

From 173c1b09dba4dc0b77ce30513fefb8eaba950fe6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=A2=96=E9=80=B8?=
 <49649786+Zuoqiu-Yingyi@users.noreply.github.com>
Date: Tue, 5 Nov 2024 00:56:08 +0800
Subject: [PATCH 4/9] :art: implement save contacts feature

---
 kernel/go.mod           |   2 +-
 kernel/go.sum           |   2 +
 kernel/model/carddav.go | 161 ++++++++++++++++++++++++++++++----------
 3 files changed, 124 insertions(+), 41 deletions(-)

diff --git a/kernel/go.mod b/kernel/go.mod
index 99d3402ec15..f898835a4e0 100644
--- a/kernel/go.mod
+++ b/kernel/go.mod
@@ -23,7 +23,7 @@ require (
 	github.com/denisbrodbeck/machineid v1.0.1
 	github.com/dgraph-io/ristretto v1.0.0
 	github.com/djherbis/times v1.6.0
-	github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9
+	github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff
 	github.com/emersion/go-webdav v0.5.1-0.20240713135526-7f8c17ad7135
 	github.com/emirpasic/gods v1.18.1
 	github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb
diff --git a/kernel/go.sum b/kernel/go.sum
index 3c0ab3a949d..18285d54769 100644
--- a/kernel/go.sum
+++ b/kernel/go.sum
@@ -92,6 +92,8 @@ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+m
 github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6/go.mod h1:BEksegNspIkjCQfmzWgsgbu6KdeJ/4LwUZs7DMBzjzw=
 github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9 h1:ATgqloALX6cHCranzkLb8/zjivwQ9DWWDCQRnxTPfaA=
 github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
+github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff h1:4N8wnS3f1hNHSmFD5zgFkWCyA4L1kCDkImPAtK7D6tg=
+github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
 github.com/emersion/go-webdav v0.5.1-0.20240713135526-7f8c17ad7135 h1:Ssk00uh7jhctJ23eclGxhhGqplSQB+wCt6fmbjhnOS8=
 github.com/emersion/go-webdav v0.5.1-0.20240713135526-7f8c17ad7135/go.mod h1:mI8iBx3RAODwX7PJJ7qzsKAKs/vY429YfS2/9wKnDbQ=
 github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
diff --git a/kernel/model/carddav.go b/kernel/model/carddav.go
index 03501463001..3d6b3c0fff0 100644
--- a/kernel/model/carddav.go
+++ b/kernel/model/carddav.go
@@ -54,16 +54,28 @@ var (
 		// MaxResourceSize: math.MaxInt32,
 	}
 	contacts = Contacts{
-		M: sync.Map{},
+		Loaded:        false,
+		Changed:       false,
+		Lock:          sync.Mutex{},
+		Books:         sync.Map{},
+		BooksMetaData: []*carddav.AddressBook{},
 	}
 )
 
 type Contacts struct {
-	M sync.Map // Path -> *AddressBook
+	Loaded        bool
+	Changed       bool
+	Lock          sync.Mutex // load & save
+	Books         sync.Map   // Path -> *AddressBook
+	BooksMetaData []*carddav.AddressBook
 }
 
+// reload all contacts
 func (c *Contacts) reload() error {
-	c.M.Clear()
+	c.Lock.Lock()
+	defer c.Lock.Unlock()
+
+	c.Books.Clear()
 	addressBooksMetaDataFilePath := filepath.Join(util.DataDir, "storage", strings.TrimPrefix(CardDavAddressBooksMetaDataFilePath, "/"))
 	metaData, err := os.ReadFile(addressBooksMetaDataFilePath)
 	if os.IsNotExist(err) {
@@ -73,8 +85,8 @@ func (c *Contacts) reload() error {
 			return err
 		}
 
-		addressBooksMetaData := []*carddav.AddressBook{&defaultAddressBook}
-		data, err := gulu.JSON.MarshalIndentJSON(addressBooksMetaData, "", "  ")
+		c.BooksMetaData = []*carddav.AddressBook{&defaultAddressBook}
+		data, err := gulu.JSON.MarshalIndentJSON(c.BooksMetaData, "", "  ")
 		if err != nil {
 			logging.LogErrorf("marshal address books meta data failed: %s", err)
 			return err
@@ -86,32 +98,37 @@ func (c *Contacts) reload() error {
 		}
 	} else {
 		// load meta data file
-		addressBooksMetaData := []*carddav.AddressBook{}
-		if err = gulu.JSON.UnmarshalJSON(metaData, &addressBooksMetaData); err != nil {
+		c.BooksMetaData = []*carddav.AddressBook{}
+		if err = gulu.JSON.UnmarshalJSON(metaData, &c.BooksMetaData); err != nil {
 			logging.LogErrorf("unmarshal address books meta data failed: %s", err)
 			return err
 		}
 
 		wg := &sync.WaitGroup{}
-		wg.Add(len(addressBooksMetaData))
-		for _, addressBookMetaData := range addressBooksMetaData {
+		wg.Add(len(c.BooksMetaData))
+		for _, addressBookMetaData := range c.BooksMetaData {
 			addressBook := &AddressBook{
+				Changed:   false,
 				MetaData:  addressBookMetaData,
 				Addresses: sync.Map{},
 			}
-			c.M.Store(addressBookMetaData.Path, addressBook)
+			c.Books.Store(addressBookMetaData.Path, addressBook)
 			go addressBook.load(wg)
 		}
 		wg.Wait()
 	}
+	c.Loaded = true
+	c.Changed = false
 	return nil
 }
 
 type AddressBook struct {
+	Changed   bool
 	MetaData  *carddav.AddressBook
-	Addresses sync.Map // id -> *carddav.AddressObject
+	Addresses sync.Map // id -> *AddressObject
 }
 
+// load an address book from multiple *.vcf files
 func (b *AddressBook) load(wg *sync.WaitGroup) {
 	defer wg.Done()
 	addressBookPath := filepath.Join(util.DataDir, "storage", strings.TrimPrefix(b.MetaData.Path, "/"))
@@ -130,37 +147,15 @@ func (b *AddressBook) load(wg *sync.WaitGroup) {
 
 						addressFilePath := path.Join(addressBookPath, filename)
 
-						addressFileInfo, err := entry.Info()
-						if err != nil {
-							logging.LogErrorf("get file [%s] info failed: %s", addressFilePath, err)
-							return
+						// load data
+						address := &AddressObject{
+							FilePath: addressFilePath,
+							BookPath: b.MetaData.Path,
 						}
-
-						// read file
-						addressData, err := os.ReadFile(addressFilePath)
-						if err != nil {
-							logging.LogErrorf("read file [%s] failed: %s", addressFilePath, err)
+						if err := address.load(); err != nil {
 							return
 						}
 
-						// decode file
-						reader := bytes.NewReader(addressData)
-						decoder := vcard.NewDecoder(reader)
-						card, err := decoder.Decode()
-						if err != nil {
-							logging.LogErrorf("decode file [%s] failed: %s", addressFilePath, err)
-							return
-						}
-
-						// load data
-						address := &carddav.AddressObject{
-							Path:          b.MetaData.Path + "/" + filename,
-							ModTime:       addressFileInfo.ModTime(),
-							ContentLength: addressFileInfo.Size(),
-							ETag:          fmt.Sprintf("%x-%x", addressFileInfo.ModTime(), addressFileInfo.Size()),
-							Card:          card,
-						}
-
 						id := path.Base(filename)
 						b.Addresses.Store(id, address)
 					}()
@@ -170,9 +165,95 @@ func (b *AddressBook) load(wg *sync.WaitGroup) {
 	}
 }
 
-func (b *AddressBook) save(wg *sync.WaitGroup) {
+// save an address book to multiple *.vcf files
+func (b *AddressBook) save(force bool, wg *sync.WaitGroup) {
 	defer wg.Done()
-	// TODO: save addresses data to files
+
+	b.Addresses.Range(func(id any, address any) bool {
+		// id_ := id.(string)
+		address_ := address.(*AddressObject)
+		address_.save(force)
+		return true
+	})
+}
+
+type AddressObject struct {
+	Changed  bool
+	FilePath string
+	BookPath string
+	Data     *carddav.AddressObject
+}
+
+// load an address from *.vcf file
+func (o *AddressObject) load() error {
+	// get file info
+	addressFileInfo, err := os.Stat(o.FilePath)
+	if err != nil {
+		logging.LogErrorf("get file [%s] info failed: %s", o.FilePath, err)
+		return err
+	}
+
+	// read file
+	addressData, err := os.ReadFile(o.FilePath)
+	if err != nil {
+		logging.LogErrorf("read file [%s] failed: %s", o.FilePath, err)
+		return err
+	}
+
+	// decode file
+	reader := bytes.NewReader(addressData)
+	decoder := vcard.NewDecoder(reader)
+	card, err := decoder.Decode()
+	if err != nil {
+		logging.LogErrorf("decode file [%s] failed: %s", o.FilePath, err)
+		return err
+	}
+
+	// load data
+	o.Changed = false
+	o.Data = &carddav.AddressObject{
+		Path:          o.BookPath + "/" + addressFileInfo.Name(),
+		ModTime:       addressFileInfo.ModTime(),
+		ContentLength: addressFileInfo.Size(),
+		ETag:          fmt.Sprintf("%x-%x", addressFileInfo.ModTime(), addressFileInfo.Size()),
+		Card:          card,
+	}
+	return nil
+}
+
+// save an address to *.vcf file
+func (o *AddressObject) save(force bool) error {
+	if force || o.Changed {
+		var addressData bytes.Buffer
+
+		// encode data
+		encoder := vcard.NewEncoder(&addressData)
+		if err := encoder.Encode(o.Data.Card); err != nil {
+			logging.LogErrorf("encode card [%s] failed: %s", o.Data.Path, err)
+			return err
+		}
+
+		// write file
+		if err := os.WriteFile(o.FilePath, addressData.Bytes(), 0755); err != nil {
+			logging.LogErrorf("write file [%s] failed: %s", o.FilePath, err)
+			return err
+		}
+
+		// update file info
+		addressFileInfo, err := os.Stat(o.FilePath)
+		if err != nil {
+			logging.LogErrorf("get file [%s] info failed: %s", o.FilePath, err)
+			return err
+		}
+
+		o.Data.Path = o.BookPath + "/" + addressFileInfo.Name()
+		o.Data.ModTime = addressFileInfo.ModTime()
+		o.Data.ContentLength = addressFileInfo.Size()
+		o.Data.ETag = fmt.Sprintf("%x-%x", addressFileInfo.ModTime(), addressFileInfo.Size())
+
+		o.Changed = false
+	}
+	return nil
 }
 
 type CardDavBackend struct{}

From c24a8779a62ffb06d634c5bffba8fc862b466a62 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=A2=96=E9=80=B8?=
 <49649786+Zuoqiu-Yingyi@users.noreply.github.com>
Date: Tue, 5 Nov 2024 17:17:52 +0800
Subject: [PATCH 5/9] :art: implement address books CURD

---
 kernel/model/carddav.go | 383 +++++++++++++++++++++++++++++++---------
 1 file changed, 296 insertions(+), 87 deletions(-)

diff --git a/kernel/model/carddav.go b/kernel/model/carddav.go
index 3d6b3c0fff0..aefd3e48d32 100644
--- a/kernel/model/carddav.go
+++ b/kernel/model/carddav.go
@@ -54,29 +54,34 @@ var (
 		// MaxResourceSize: math.MaxInt32,
 	}
 	contacts = Contacts{
-		Loaded:        false,
-		Changed:       false,
-		Lock:          sync.Mutex{},
-		Books:         sync.Map{},
-		BooksMetaData: []*carddav.AddressBook{},
+		loaded:        false,
+		changed:       false,
+		lock:          sync.Mutex{},
+		books:         sync.Map{},
+		booksMetaData: []*carddav.AddressBook{},
 	}
 )
 
-type Contacts struct {
-	Loaded        bool
-	Changed       bool
-	Lock          sync.Mutex // load & save
-	Books         sync.Map   // Path -> *AddressBook
-	BooksMetaData []*carddav.AddressBook
+func AddressBookPath2DirectoryPath(addressBookPath string) string {
+	return filepath.Join(util.DataDir, "storage", strings.TrimPrefix(addressBookPath, "/"))
+}
+
+func AddressBooksMetaDataFilePath() string {
+	return filepath.Join(util.DataDir, "storage", strings.TrimPrefix(CardDavAddressBooksMetaDataFilePath, "/"))
 }
 
-// reload all contacts
-func (c *Contacts) reload() error {
-	c.Lock.Lock()
-	defer c.Lock.Unlock()
+type Contacts struct {
+	loaded        bool
+	changed       bool
+	lock          sync.Mutex // load & save
+	books         sync.Map   // Path -> *AddressBook
+	booksMetaData []*carddav.AddressBook
+}
 
-	c.Books.Clear()
-	addressBooksMetaDataFilePath := filepath.Join(util.DataDir, "storage", strings.TrimPrefix(CardDavAddressBooksMetaDataFilePath, "/"))
+// load all contacts
+func (c *Contacts) load() error {
+	c.books.Clear()
+	addressBooksMetaDataFilePath := AddressBooksMetaDataFilePath()
 	metaData, err := os.ReadFile(addressBooksMetaDataFilePath)
 	if os.IsNotExist(err) {
 		// create meta data file
@@ -85,96 +90,270 @@ func (c *Contacts) reload() error {
 			return err
 		}
 
-		c.BooksMetaData = []*carddav.AddressBook{&defaultAddressBook}
-		data, err := gulu.JSON.MarshalIndentJSON(c.BooksMetaData, "", "  ")
-		if err != nil {
-			logging.LogErrorf("marshal address books meta data failed: %s", err)
-			return err
-		}
-
-		if err := os.WriteFile(addressBooksMetaDataFilePath, data, 0755); err != nil {
-			logging.LogErrorf("write file [%s] failed: %s", addressBooksMetaDataFilePath, err)
+		c.booksMetaData = []*carddav.AddressBook{&defaultAddressBook}
+		if err := c.saveAddressBooksMetaData(); err != nil {
 			return err
 		}
 	} else {
 		// load meta data file
-		c.BooksMetaData = []*carddav.AddressBook{}
-		if err = gulu.JSON.UnmarshalJSON(metaData, &c.BooksMetaData); err != nil {
+		c.booksMetaData = []*carddav.AddressBook{}
+		if err = gulu.JSON.UnmarshalJSON(metaData, &c.booksMetaData); err != nil {
 			logging.LogErrorf("unmarshal address books meta data failed: %s", err)
 			return err
 		}
 
 		wg := &sync.WaitGroup{}
-		wg.Add(len(c.BooksMetaData))
-		for _, addressBookMetaData := range c.BooksMetaData {
+		wg.Add(len(c.booksMetaData))
+		for _, addressBookMetaData := range c.booksMetaData {
 			addressBook := &AddressBook{
-				Changed:   false,
-				MetaData:  addressBookMetaData,
-				Addresses: sync.Map{},
+				Changed:       false,
+				DirectoryPath: AddressBookPath2DirectoryPath(addressBookMetaData.Path),
+				MetaData:      addressBookMetaData,
+				Addresses:     sync.Map{},
 			}
-			c.Books.Store(addressBookMetaData.Path, addressBook)
-			go addressBook.load(wg)
+			c.books.Store(addressBookMetaData.Path, addressBook)
+			go func() {
+				defer wg.Done()
+				addressBook.load()
+			}()
 		}
 		wg.Wait()
 	}
-	c.Loaded = true
-	c.Changed = false
+
+	c.loaded = true
+	c.changed = false
+	return nil
+}
+
+// save all contacts
+func (c *Contacts) save(force bool) error {
+	if force || c.changed {
+		// save address books meta data
+		if err := c.saveAddressBooksMetaData(); err != nil {
+			return err
+		}
+
+		// save all address to *.vbf files
+		wg := &sync.WaitGroup{}
+		c.books.Range(func(path any, book any) bool {
+			wg.Add(1)
+			go func() {
+				defer wg.Done()
+				// path_ := path.(string)
+				book_ := book.(*AddressBook)
+				book_.save(force)
+			}()
+			return true
+		})
+		wg.Wait()
+		c.changed = false
+	}
+	return nil
+}
+
+// save all contacts
+func (c *Contacts) saveAddressBooksMetaData() error {
+	data, err := gulu.JSON.MarshalIndentJSON(c.booksMetaData, "", "  ")
+	if err != nil {
+		logging.LogErrorf("marshal address books meta data failed: %s", err)
+		return err
+	}
+
+	filePath := AddressBooksMetaDataFilePath()
+	if err := os.WriteFile(filePath, data, 0755); err != nil {
+		logging.LogErrorf("write file [%s] failed: %s", filePath, err)
+		return err
+	}
+
+	return nil
+}
+
+func (c *Contacts) Load() error {
+	c.lock.Lock()
+	defer c.lock.Unlock()
+
+	if !c.loaded {
+		return c.load()
+	}
+	return nil
+}
+
+func (c *Contacts) ListAddressBooks() (addressBooks []carddav.AddressBook, err error) {
+	c.lock.Lock()
+	defer c.lock.Unlock()
+
+	for _, addressBook := range contacts.booksMetaData {
+		addressBooks = append(addressBooks, *addressBook)
+	}
+	return
+}
+
+func (c *Contacts) GetAddressBook(path string) (addressBook *carddav.AddressBook, err error) {
+	c.lock.Lock()
+	defer c.lock.Unlock()
+
+	if book, ok := contacts.books.Load(path); ok {
+		addressBook = book.(*AddressBook).MetaData
+	}
+	return
+}
+
+func (c *Contacts) CreateAddressBook(addressBookMetaData *carddav.AddressBook) (err error) {
+	c.lock.Lock()
+	defer c.lock.Unlock()
+
+	var addressBook *AddressBook
+
+	// update map
+	if value, ok := c.books.Load(addressBookMetaData.Path); ok {
+		// update map item
+		addressBook = value.(*AddressBook)
+		addressBook.MetaData = addressBookMetaData
+	} else {
+		// insert map item
+		addressBook = &AddressBook{
+			Changed:       false,
+			DirectoryPath: AddressBookPath2DirectoryPath(addressBookMetaData.Path),
+			MetaData:      addressBookMetaData,
+			Addresses:     sync.Map{},
+		}
+		c.books.Store(addressBookMetaData.Path, addressBook)
+	}
+
+	var index = -1
+	for i, item := range c.booksMetaData {
+		if item.Path == addressBookMetaData.Path {
+			index = i
+			break
+		}
+	}
+
+	if index >= 0 {
+		// update list
+		c.booksMetaData[index] = addressBookMetaData
+	} else {
+		// insert list
+		c.booksMetaData = append(c.booksMetaData, addressBookMetaData)
+	}
+
+	// create address book directory
+	if err = os.MkdirAll(addressBook.DirectoryPath, 0755); err != nil {
+		logging.LogErrorf("create directory [%s] failed: %s", addressBook, err)
+		return
+	}
+
+	// save meta data
+	if err = c.saveAddressBooksMetaData(); err != nil {
+		return
+	}
+
+	return
+}
+
+func (c *Contacts) DeleteAddressBook(path string) (err error) {
+	c.lock.Lock()
+	defer c.lock.Unlock()
+
+	var addressBook *AddressBook
+
+	// delete map item
+	if value, loaded := c.books.LoadAndDelete(path); loaded {
+		addressBook = value.(*AddressBook)
+	}
+
+	// delete list item
+	for i, item := range c.booksMetaData {
+		if item.Path == path {
+			c.booksMetaData = append(c.booksMetaData[:i], c.booksMetaData[i+1:]...)
+			break
+		}
+	}
+
+	// remove address book directory
+	if err = os.RemoveAll(addressBook.DirectoryPath); err != nil {
+		logging.LogErrorf("remove directory [%s] failed: %s", addressBook, err)
+		return
+	}
+
+	// save meta data
+	if err = c.saveAddressBooksMetaData(); err != nil {
+		return
+	}
+
 	return nil
 }
 
 type AddressBook struct {
-	Changed   bool
-	MetaData  *carddav.AddressBook
-	Addresses sync.Map // id -> *AddressObject
+	Changed       bool
+	DirectoryPath string
+	MetaData      *carddav.AddressBook
+	Addresses     sync.Map // id -> *AddressObject
 }
 
 // load an address book from multiple *.vcf files
-func (b *AddressBook) load(wg *sync.WaitGroup) {
-	defer wg.Done()
-	addressBookPath := filepath.Join(util.DataDir, "storage", strings.TrimPrefix(b.MetaData.Path, "/"))
-	entries, err := os.ReadDir(addressBookPath)
+func (b *AddressBook) load() error {
+	entries, err := os.ReadDir(b.DirectoryPath)
 	if err != nil {
-		logging.LogErrorf("read dir [%s] failed: %s", addressBookPath, err)
-	} else {
-		for _, entry := range entries {
-			if !entry.IsDir() {
-				filename := entry.Name()
-				ext := path.Ext(filename)
-				if ext == ".vcf" {
-					wg.Add(1)
-					go func() {
-						defer wg.Done()
-
-						addressFilePath := path.Join(addressBookPath, filename)
-
-						// load data
-						address := &AddressObject{
-							FilePath: addressFilePath,
-							BookPath: b.MetaData.Path,
-						}
-						if err := address.load(); err != nil {
-							return
-						}
-
-						id := path.Base(filename)
-						b.Addresses.Store(id, address)
-					}()
-				}
+		logging.LogErrorf("read dir [%s] failed: %s", b.DirectoryPath, err)
+		return err
+	}
+
+	wg := &sync.WaitGroup{}
+	for _, entry := range entries {
+		if !entry.IsDir() {
+			filename := entry.Name()
+			ext := path.Ext(filename)
+			if ext == ".vcf" {
+				wg.Add(1)
+				go func() {
+					defer wg.Done()
+
+					addressFilePath := path.Join(b.DirectoryPath, filename)
+
+					// load data
+					address := &AddressObject{
+						FilePath: addressFilePath,
+						BookPath: b.MetaData.Path,
+					}
+					if err := address.load(); err != nil {
+						return
+					}
+
+					id := path.Base(filename)
+					b.Addresses.Store(id, address)
+				}()
 			}
 		}
 	}
+	wg.Wait()
+	return nil
 }
 
 // save an address book to multiple *.vcf files
-func (b *AddressBook) save(force bool, wg *sync.WaitGroup) {
-	defer wg.Done()
-
-	b.Addresses.Range(func(id any, address any) bool {
-		// id_ := id.(string)
-		address_ := address.(*AddressObject)
-		address_.save(force)
-		return true
-	})
+func (b *AddressBook) save(force bool) error {
+	if force || b.Changed {
+		// create directory
+		if err := os.MkdirAll(b.DirectoryPath, 0755); err != nil {
+			logging.LogErrorf("create directory [%s] failed: %s", b.DirectoryPath, err)
+			return err
+		}
+
+		wg := &sync.WaitGroup{}
+		b.Addresses.Range(func(id any, address any) bool {
+			wg.Add(1)
+			go func() {
+				defer wg.Done()
+				// id_ := id.(string)
+				address_ := address.(*AddressObject)
+				address_.save(force)
+			}()
+			return true
+		})
+		wg.Wait()
+		b.Changed = false
+	}
+
+	return nil
 }
 
 type AddressObject struct {
@@ -270,54 +449,84 @@ func (b *CardDavBackend) AddressBookHomeSetPath(ctx context.Context) (string, er
 
 func (b *CardDavBackend) ListAddressBooks(ctx context.Context) (addressBooks []carddav.AddressBook, err error) {
 	logging.LogInfof("CardDAV ListAddressBooks")
-	// TODO
-	return
+	if err = contacts.Load(); err != nil {
+		return
+	}
+
+	return contacts.ListAddressBooks()
 }
 
 func (b *CardDavBackend) GetAddressBook(ctx context.Context, path string) (addressBook *carddav.AddressBook, err error) {
 	logging.LogInfof("CardDAV GetAddressBook")
-	// TODO
-	return
+	if err = contacts.Load(); err != nil {
+		return
+	}
+
+	return contacts.GetAddressBook(path)
 }
 
 func (b *CardDavBackend) CreateAddressBook(ctx context.Context, addressBook *carddav.AddressBook) (err error) {
 	logging.LogInfof("CardDAV CreateAddressBook")
-	// TODO
-	return
+	if err = contacts.Load(); err != nil {
+		return
+	}
+	return contacts.CreateAddressBook(addressBook)
 }
 
 func (b *CardDavBackend) DeleteAddressBook(ctx context.Context, path string) (err error) {
 	logging.LogInfof("CardDAV DeleteAddressBook")
-	// TODO
-	return
+	if err = contacts.Load(); err != nil {
+		return
+	}
+	return contacts.DeleteAddressBook(path)
 }
 
 func (b *CardDavBackend) GetAddressObject(ctx context.Context, path string, req *carddav.AddressDataRequest) (addressObject *carddav.AddressObject, err error) {
 	logging.LogInfof("CardDAV GetAddressObject: %s", path)
+	if err = contacts.Load(); err != nil {
+		return
+	}
+
 	// TODO
 	return
 }
 
 func (b *CardDavBackend) ListAddressObjects(ctx context.Context, path string, req *carddav.AddressDataRequest) (addressObjects []carddav.AddressObject, err error) {
 	logging.LogInfof("CardDAV ListAddressObjects")
+	if err = contacts.Load(); err != nil {
+		return
+	}
+
 	// TODO
 	return
 }
 
 func (b *CardDavBackend) QueryAddressObjects(ctx context.Context, path string, query *carddav.AddressBookQuery) (addressObjects []carddav.AddressObject, err error) {
 	logging.LogInfof("CardDAV QueryAddressObjects: %v", query)
+	if err = contacts.Load(); err != nil {
+		return
+	}
+
 	// TODO
 	return
 }
 
 func (b *CardDavBackend) PutAddressObject(ctx context.Context, path string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (addressObject *carddav.AddressObject, err error) {
 	logging.LogInfof("CardDAV PutAddressObject: %s", path)
+	if err = contacts.Load(); err != nil {
+		return
+	}
+
 	// TODO
 	return
 }
 
 func (b *CardDavBackend) DeleteAddressObject(ctx context.Context, path string) (err error) {
 	logging.LogInfof("CardDAV DeleteAddressObject: %s", path)
+	if err = contacts.Load(); err != nil {
+		return
+	}
+
 	// TODO
 	return
 }

From 743e24b954a2d7e1d72a9569132b5dd7eaa85b54 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=A2=96=E9=80=B8?=
 <49649786+Zuoqiu-Yingyi@users.noreply.github.com>
Date: Wed, 6 Nov 2024 01:35:00 +0800
Subject: [PATCH 6/9] :bug: fix CardDAV method `OPTIONS`

---
 kernel/go.sum           |   1 -
 kernel/model/carddav.go | 115 +++++++++++++++++++++++++---------------
 kernel/server/serve.go  |  53 ++++++++++++++----
 3 files changed, 115 insertions(+), 54 deletions(-)

diff --git a/kernel/go.sum b/kernel/go.sum
index 18285d54769..7dd3a058f6c 100644
--- a/kernel/go.sum
+++ b/kernel/go.sum
@@ -90,7 +90,6 @@ github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdf
 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6/go.mod h1:BEksegNspIkjCQfmzWgsgbu6KdeJ/4LwUZs7DMBzjzw=
-github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9 h1:ATgqloALX6cHCranzkLb8/zjivwQ9DWWDCQRnxTPfaA=
 github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
 github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff h1:4N8wnS3f1hNHSmFD5zgFkWCyA4L1kCDkImPAtK7D6tg=
 github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
diff --git a/kernel/model/carddav.go b/kernel/model/carddav.go
index aefd3e48d32..25b08a8be98 100644
--- a/kernel/model/carddav.go
+++ b/kernel/model/carddav.go
@@ -19,6 +19,7 @@ package model
 import (
 	"bytes"
 	"context"
+	"errors"
 	"fmt"
 	"os"
 	"path"
@@ -48,10 +49,10 @@ const (
 
 var (
 	defaultAddressBook = carddav.AddressBook{
-		Path:        CardDavDefaultAddressBookPath,
-		Name:        CardDavDefaultAddressBookName,
-		Description: "Default address book",
-		// MaxResourceSize: math.MaxInt32,
+		Path:            CardDavDefaultAddressBookPath,
+		Name:            CardDavDefaultAddressBookName,
+		Description:     "Default address book",
+		MaxResourceSize: 0,
 	}
 	contacts = Contacts{
 		loaded:        false,
@@ -60,14 +61,19 @@ var (
 		books:         sync.Map{},
 		booksMetaData: []*carddav.AddressBook{},
 	}
+	ErrorNotFound = errors.New("carddav: not found")
 )
 
 func AddressBookPath2DirectoryPath(addressBookPath string) string {
 	return filepath.Join(util.DataDir, "storage", strings.TrimPrefix(addressBookPath, "/"))
 }
 
+func HomeSetPathPath() string {
+	return filepath.Join(util.DataDir, "storage", CardDavHomeSetPath)
+}
+
 func AddressBooksMetaDataFilePath() string {
-	return filepath.Join(util.DataDir, "storage", strings.TrimPrefix(CardDavAddressBooksMetaDataFilePath, "/"))
+	return filepath.Join(util.DataDir, "storage", CardDavAddressBooksMetaDataFilePath)
 }
 
 type Contacts struct {
@@ -84,12 +90,6 @@ func (c *Contacts) load() error {
 	addressBooksMetaDataFilePath := AddressBooksMetaDataFilePath()
 	metaData, err := os.ReadFile(addressBooksMetaDataFilePath)
 	if os.IsNotExist(err) {
-		// create meta data file
-		if err = os.MkdirAll(CardDavDefaultAddressBookPath, 0755); err != nil {
-			logging.LogErrorf("make dir [%s] failed: %s", CardDavDefaultAddressBookPath, err)
-			return err
-		}
-
 		c.booksMetaData = []*carddav.AddressBook{&defaultAddressBook}
 		if err := c.saveAddressBooksMetaData(); err != nil {
 			return err
@@ -101,24 +101,24 @@ func (c *Contacts) load() error {
 			logging.LogErrorf("unmarshal address books meta data failed: %s", err)
 			return err
 		}
+	}
 
-		wg := &sync.WaitGroup{}
-		wg.Add(len(c.booksMetaData))
-		for _, addressBookMetaData := range c.booksMetaData {
-			addressBook := &AddressBook{
-				Changed:       false,
-				DirectoryPath: AddressBookPath2DirectoryPath(addressBookMetaData.Path),
-				MetaData:      addressBookMetaData,
-				Addresses:     sync.Map{},
-			}
-			c.books.Store(addressBookMetaData.Path, addressBook)
-			go func() {
-				defer wg.Done()
-				addressBook.load()
-			}()
+	wg := &sync.WaitGroup{}
+	wg.Add(len(c.booksMetaData))
+	for _, addressBookMetaData := range c.booksMetaData {
+		addressBook := &AddressBook{
+			Changed:       false,
+			DirectoryPath: AddressBookPath2DirectoryPath(addressBookMetaData.Path),
+			MetaData:      addressBookMetaData,
+			Addresses:     sync.Map{},
 		}
-		wg.Wait()
+		c.books.Store(addressBookMetaData.Path, addressBook)
+		go func() {
+			defer wg.Done()
+			addressBook.load()
+		}()
 	}
+	wg.Wait()
 
 	c.loaded = true
 	c.changed = false
@@ -159,6 +159,12 @@ func (c *Contacts) saveAddressBooksMetaData() error {
 		return err
 	}
 
+	dirPath := HomeSetPathPath()
+	if err := os.MkdirAll(dirPath, 0755); err != nil {
+		logging.LogErrorf("create directory [%s] failed: %s", dirPath, err)
+		return err
+	}
+
 	filePath := AddressBooksMetaDataFilePath()
 	if err := os.WriteFile(filePath, data, 0755); err != nil {
 		logging.LogErrorf("write file [%s] failed: %s", filePath, err)
@@ -194,7 +200,10 @@ func (c *Contacts) GetAddressBook(path string) (addressBook *carddav.AddressBook
 
 	if book, ok := contacts.books.Load(path); ok {
 		addressBook = book.(*AddressBook).MetaData
+		return
 	}
+
+	err = ErrorNotFound
 	return
 }
 
@@ -292,6 +301,11 @@ type AddressBook struct {
 
 // load an address book from multiple *.vcf files
 func (b *AddressBook) load() error {
+	if err := os.MkdirAll(b.DirectoryPath, 0755); err != nil {
+		logging.LogErrorf("create directory [%s] failed: %s", b.DirectoryPath, err)
+		return err
+	}
+
 	entries, err := os.ReadDir(b.DirectoryPath)
 	if err != nil {
 		logging.LogErrorf("read dir [%s] failed: %s", b.DirectoryPath, err)
@@ -412,6 +426,13 @@ func (o *AddressObject) save(force bool) error {
 			return err
 		}
 
+		// create directory
+		dirPath := path.Dir(o.FilePath)
+		if err := os.MkdirAll(dirPath, 0755); err != nil {
+			logging.LogErrorf("create directory [%s] failed: %s", dirPath, err)
+			return err
+		}
+
 		// write file
 		if err := os.WriteFile(o.FilePath, addressData.Bytes(), 0755); err != nil {
 			logging.LogErrorf("write file [%s] failed: %s", o.FilePath, err)
@@ -453,16 +474,18 @@ func (b *CardDavBackend) ListAddressBooks(ctx context.Context) (addressBooks []c
 		return
 	}
 
-	return contacts.ListAddressBooks()
+	addressBooks, err = contacts.ListAddressBooks()
+	return
 }
 
-func (b *CardDavBackend) GetAddressBook(ctx context.Context, path string) (addressBook *carddav.AddressBook, err error) {
-	logging.LogInfof("CardDAV GetAddressBook")
+func (b *CardDavBackend) GetAddressBook(ctx context.Context, bookPath string) (addressBook *carddav.AddressBook, err error) {
+	logging.LogInfof("CardDAV GetAddressBook: %s", bookPath)
 	if err = contacts.Load(); err != nil {
 		return
 	}
 
-	return contacts.GetAddressBook(path)
+	addressBook, err = contacts.GetAddressBook(bookPath)
+	return
 }
 
 func (b *CardDavBackend) CreateAddressBook(ctx context.Context, addressBook *carddav.AddressBook) (err error) {
@@ -470,19 +493,23 @@ func (b *CardDavBackend) CreateAddressBook(ctx context.Context, addressBook *car
 	if err = contacts.Load(); err != nil {
 		return
 	}
-	return contacts.CreateAddressBook(addressBook)
+
+	err = contacts.CreateAddressBook(addressBook)
+	return
 }
 
-func (b *CardDavBackend) DeleteAddressBook(ctx context.Context, path string) (err error) {
-	logging.LogInfof("CardDAV DeleteAddressBook")
+func (b *CardDavBackend) DeleteAddressBook(ctx context.Context, bookPath string) (err error) {
+	logging.LogInfof("CardDAV DeleteAddressBook: %s", bookPath)
 	if err = contacts.Load(); err != nil {
 		return
 	}
-	return contacts.DeleteAddressBook(path)
+
+	err = contacts.DeleteAddressBook(bookPath)
+	return
 }
 
-func (b *CardDavBackend) GetAddressObject(ctx context.Context, path string, req *carddav.AddressDataRequest) (addressObject *carddav.AddressObject, err error) {
-	logging.LogInfof("CardDAV GetAddressObject: %s", path)
+func (b *CardDavBackend) GetAddressObject(ctx context.Context, urlPath string, req *carddav.AddressDataRequest) (addressObject *carddav.AddressObject, err error) {
+	logging.LogInfof("CardDAV GetAddressObject: %s", urlPath)
 	if err = contacts.Load(); err != nil {
 		return
 	}
@@ -491,8 +518,8 @@ func (b *CardDavBackend) GetAddressObject(ctx context.Context, path string, req
 	return
 }
 
-func (b *CardDavBackend) ListAddressObjects(ctx context.Context, path string, req *carddav.AddressDataRequest) (addressObjects []carddav.AddressObject, err error) {
-	logging.LogInfof("CardDAV ListAddressObjects")
+func (b *CardDavBackend) ListAddressObjects(ctx context.Context, bookPath string, req *carddav.AddressDataRequest) (addressObjects []carddav.AddressObject, err error) {
+	logging.LogInfof("CardDAV ListAddressObjects: %s", bookPath)
 	if err = contacts.Load(); err != nil {
 		return
 	}
@@ -501,8 +528,8 @@ func (b *CardDavBackend) ListAddressObjects(ctx context.Context, path string, re
 	return
 }
 
-func (b *CardDavBackend) QueryAddressObjects(ctx context.Context, path string, query *carddav.AddressBookQuery) (addressObjects []carddav.AddressObject, err error) {
-	logging.LogInfof("CardDAV QueryAddressObjects: %v", query)
+func (b *CardDavBackend) QueryAddressObjects(ctx context.Context, urlPath string, query *carddav.AddressBookQuery) (addressObjects []carddav.AddressObject, err error) {
+	logging.LogInfof("CardDAV QueryAddressObjects: %s %v", urlPath, query)
 	if err = contacts.Load(); err != nil {
 		return
 	}
@@ -511,8 +538,8 @@ func (b *CardDavBackend) QueryAddressObjects(ctx context.Context, path string, q
 	return
 }
 
-func (b *CardDavBackend) PutAddressObject(ctx context.Context, path string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (addressObject *carddav.AddressObject, err error) {
-	logging.LogInfof("CardDAV PutAddressObject: %s", path)
+func (b *CardDavBackend) PutAddressObject(ctx context.Context, urlPath string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (addressObject *carddav.AddressObject, err error) {
+	logging.LogInfof("CardDAV PutAddressObject: %s %v", urlPath, card)
 	if err = contacts.Load(); err != nil {
 		return
 	}
@@ -521,8 +548,8 @@ func (b *CardDavBackend) PutAddressObject(ctx context.Context, path string, card
 	return
 }
 
-func (b *CardDavBackend) DeleteAddressObject(ctx context.Context, path string) (err error) {
-	logging.LogInfof("CardDAV DeleteAddressObject: %s", path)
+func (b *CardDavBackend) DeleteAddressObject(ctx context.Context, urlPath string) (err error) {
+	logging.LogInfof("CardDAV DeleteAddressObject: %s", urlPath)
 	if err = contacts.Load(); err != nil {
 		return
 	}
diff --git a/kernel/server/serve.go b/kernel/server/serve.go
index 8be8a63f437..3f782e61e82 100644
--- a/kernel/server/serve.go
+++ b/kernel/server/serve.go
@@ -54,12 +54,23 @@ const (
 	MethodMove      = "MOVE"
 	MethodLock      = "LOCK"
 	MethodUnlock    = "UNLOCK"
-	MethodPropfind  = "PROPFIND"
-	MethodProppatch = "PROPPATCH"
+	MethodPropFind  = "PROPFIND"
+	MethodPropPatch = "PROPPATCH"
 )
 
 var (
-	cookieStore   = cookie.NewStore([]byte("ATN51UlxVq1Gcvdf"))
+	cookieStore = cookie.NewStore([]byte("ATN51UlxVq1Gcvdf"))
+	HttpMethods = []string{
+		http.MethodGet,
+		http.MethodHead,
+		http.MethodPost,
+		http.MethodPut,
+		http.MethodPatch,
+		http.MethodDelete,
+		http.MethodConnect,
+		http.MethodOptions,
+		http.MethodTrace,
+	}
 	WebDavMethods = []string{
 		http.MethodOptions,
 		http.MethodHead,
@@ -73,17 +84,24 @@ var (
 		MethodMove,
 		MethodLock,
 		MethodUnlock,
-		MethodPropfind,
-		MethodProppatch,
+		MethodPropFind,
+		MethodPropPatch,
 	}
 	CardDavMethods = []string{
 		http.MethodOptions,
 		http.MethodHead,
 		http.MethodGet,
+		http.MethodPost,
 		http.MethodPut,
 		http.MethodDelete,
 
-		MethodPropfind,
+		MethodMkcol,
+		// MethodCopy,
+		// MethodMove,
+		// MethodLock,
+		// MethodUnlock,
+		MethodPropFind,
+		MethodPropPatch,
 	}
 )
 
@@ -698,15 +716,32 @@ func shortReqMsg(msg []byte) []byte {
 }
 
 func corsMiddleware() gin.HandlerFunc {
-	return func(c *gin.Context) {
+	allowMethods := strings.Join(HttpMethods, ", ")
+	allowWebDavMethods := strings.Join(WebDavMethods, ", ")
+	allowCardDavMethods := strings.Join(CardDavMethods, ", ")
 
+	return func(c *gin.Context) {
 		c.Header("Access-Control-Allow-Origin", "*")
 		c.Header("Access-Control-Allow-Credentials", "true")
 		c.Header("Access-Control-Allow-Headers", "origin, Content-Length, Content-Type, Authorization")
-		c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS")
 		c.Header("Access-Control-Allow-Private-Network", "true")
 
-		if c.Request.Method == "OPTIONS" {
+		if strings.HasPrefix(c.Request.RequestURI, "/webdav/") {
+			c.Header("Access-Control-Allow-Methods", allowWebDavMethods)
+			c.Next()
+			return
+		}
+
+		if strings.HasPrefix(c.Request.RequestURI, "/carddav/") {
+			c.Header("Access-Control-Allow-Methods", allowCardDavMethods)
+			c.Next()
+			return
+		}
+
+		c.Header("Access-Control-Allow-Methods", allowMethods)
+
+		switch c.Request.Method {
+		case http.MethodOptions:
 			c.Header("Access-Control-Max-Age", "600")
 			c.AbortWithStatus(204)
 			return

From 61cbba14517fe4211ac39ebddf109fb6d7a7c167 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=A2=96=E9=80=B8?=
 <49649786+Zuoqiu-Yingyi@users.noreply.github.com>
Date: Wed, 6 Nov 2024 03:39:19 +0800
Subject: [PATCH 7/9] :art: implement addresses CURD

---
 kernel/model/carddav.go | 268 ++++++++++++++++++++++++++++++++++------
 kernel/server/serve.go  |   2 -
 2 files changed, 228 insertions(+), 42 deletions(-)

diff --git a/kernel/model/carddav.go b/kernel/model/carddav.go
index 25b08a8be98..3891152a4d8 100644
--- a/kernel/model/carddav.go
+++ b/kernel/model/carddav.go
@@ -61,19 +61,72 @@ var (
 		books:         sync.Map{},
 		booksMetaData: []*carddav.AddressBook{},
 	}
-	ErrorNotFound = errors.New("carddav: not found")
+
+	ErrorNotFound = errors.New("CardDAV: not found")
+
+	ErrorBookNotFound    = errors.New("CardDAV: address book not found")
+	ErrorBookPathInvalid = errors.New("CardDAV: address book path is invalid")
+
+	ErrorAddressNotFound                 = errors.New("CardDAV: address not found")
+	ErrorAddressFileExtensionNameInvalid = errors.New("CardDAV: address file extension name is invalid")
 )
 
-func AddressBookPath2DirectoryPath(addressBookPath string) string {
-	return filepath.Join(util.DataDir, "storage", strings.TrimPrefix(addressBookPath, "/"))
+// CardDavPath2DirectoryPath converts CardDAV path to absolute path of the file system
+func CardDavPath2DirectoryPath(cardDavPath string) string {
+	return filepath.Join(util.DataDir, "storage", strings.TrimPrefix(cardDavPath, "/"))
 }
 
+// HomeSetPathPath returns the absolute path of the address book home set directory
 func HomeSetPathPath() string {
-	return filepath.Join(util.DataDir, "storage", CardDavHomeSetPath)
+	return CardDavPath2DirectoryPath(CardDavHomeSetPath)
 }
 
+// AddressBooksMetaDataFilePath returns the absolute path of the address books meta data file
 func AddressBooksMetaDataFilePath() string {
-	return filepath.Join(util.DataDir, "storage", CardDavAddressBooksMetaDataFilePath)
+	return CardDavPath2DirectoryPath(CardDavAddressBooksMetaDataFilePath)
+}
+
+// ParseAddressPath parses address path to address book path and address ID
+func ParseAddressPath(addressParh string) (addressBookPath string, addressID string, err error) {
+	addressBookPath, addressFileName := path.Split(addressParh)
+	addressID = path.Base(addressFileName)
+	addressFileExt := path.Ext(addressFileName)
+
+	if len(strings.Split(addressBookPath, "/")) != 6 {
+		err = ErrorBookPathInvalid
+		return
+	}
+
+	if addressFileExt != ".vcf" {
+		err = ErrorAddressFileExtensionNameInvalid
+		return
+	}
+
+	return
+}
+
+func AddressPropsFilter(address *carddav.AddressObject, req *carddav.AddressDataRequest) *carddav.AddressObject {
+	var card *vcard.Card
+	if req.AllProp {
+		card = &address.Card
+	} else {
+		card = &vcard.Card{}
+		for _, prop := range req.Props {
+			fields := address.Card[prop]
+			if fields != nil {
+				for _, field := range fields {
+					card.Add(prop, field)
+				}
+			}
+		}
+	}
+	return &carddav.AddressObject{
+		Path:          address.Path,
+		ModTime:       address.ModTime,
+		ContentLength: address.ContentLength,
+		ETag:          address.ETag,
+		Card:          *card,
+	}
 }
 
 type Contacts struct {
@@ -108,7 +161,7 @@ func (c *Contacts) load() error {
 	for _, addressBookMetaData := range c.booksMetaData {
 		addressBook := &AddressBook{
 			Changed:       false,
-			DirectoryPath: AddressBookPath2DirectoryPath(addressBookMetaData.Path),
+			DirectoryPath: CardDavPath2DirectoryPath(addressBookMetaData.Path),
 			MetaData:      addressBookMetaData,
 			Addresses:     sync.Map{},
 		}
@@ -203,7 +256,7 @@ func (c *Contacts) GetAddressBook(path string) (addressBook *carddav.AddressBook
 		return
 	}
 
-	err = ErrorNotFound
+	err = ErrorBookNotFound
 	return
 }
 
@@ -222,7 +275,7 @@ func (c *Contacts) CreateAddressBook(addressBookMetaData *carddav.AddressBook) (
 		// insert map item
 		addressBook = &AddressBook{
 			Changed:       false,
-			DirectoryPath: AddressBookPath2DirectoryPath(addressBookMetaData.Path),
+			DirectoryPath: CardDavPath2DirectoryPath(addressBookMetaData.Path),
 			MetaData:      addressBookMetaData,
 			Addresses:     sync.Map{},
 		}
@@ -292,6 +345,146 @@ func (c *Contacts) DeleteAddressBook(path string) (err error) {
 	return nil
 }
 
+func (c *Contacts) GetAddressObject(addressPath string, req *carddav.AddressDataRequest) (addressObject *carddav.AddressObject, err error) {
+	c.lock.Lock()
+	defer c.lock.Unlock()
+
+	bookPath, addressID, err := ParseAddressPath(addressPath)
+	if err != nil {
+		logging.LogErrorf("parse address path [%s] failed: %s", addressPath, err)
+		return
+	}
+
+	var addressBook *AddressBook
+	if value, ok := c.books.Load(bookPath); ok {
+		addressBook = value.(*AddressBook)
+	} else {
+		err = ErrorBookNotFound
+		return
+	}
+
+	if value, ok := addressBook.Addresses.Load(addressID); ok {
+		addressObject = AddressPropsFilter(value.(*AddressObject).Data, req)
+	} else {
+		err = ErrorAddressNotFound
+		return
+	}
+
+	return
+}
+
+func (c *Contacts) ListAddressObjects(bookPath string, req *carddav.AddressDataRequest) (addressObjects []carddav.AddressObject, err error) {
+	c.lock.Lock()
+	defer c.lock.Unlock()
+
+	var addressBook *AddressBook
+	if value, ok := c.books.Load(bookPath); ok {
+		addressBook = value.(*AddressBook)
+	} else {
+		err = ErrorBookNotFound
+		return
+	}
+
+	addressBook.Addresses.Range(func(id any, address any) bool {
+		addressObjects = append(addressObjects, *AddressPropsFilter(address.(*AddressObject).Data, req))
+		return true
+	})
+
+	return
+}
+
+func (c *Contacts) QueryAddressObjects(addressPath string, query *carddav.AddressBookQuery) (addressObjects []carddav.AddressObject, err error) {
+	c.lock.Lock()
+	defer c.lock.Unlock()
+	// TODO
+	return
+}
+
+func (c *Contacts) PutAddressObject(addressPath string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (addressObject *carddav.AddressObject, err error) {
+	c.lock.Lock()
+	defer c.lock.Unlock()
+
+	bookPath, addressID, err := ParseAddressPath(addressPath)
+	if err != nil {
+		logging.LogErrorf("parse address path [%s] failed: %s", addressPath, err)
+		return
+	}
+
+	var addressBook *AddressBook
+	if value, ok := c.books.Load(bookPath); ok {
+		addressBook = value.(*AddressBook)
+	} else {
+		err = ErrorBookNotFound
+		return
+	}
+
+	var address *AddressObject
+	if value, ok := addressBook.Addresses.Load(addressID); ok {
+		address = value.(*AddressObject)
+
+		if opts.IfNoneMatch.IsSet() {
+			addressObject = address.Data
+			return
+		}
+
+		address.Data.Card = card
+		address.Changed = true
+	} else {
+		address = &AddressObject{
+			Changed:  true,
+			FilePath: CardDavPath2DirectoryPath(addressPath),
+			BookPath: bookPath,
+			Data: &carddav.AddressObject{
+				Card: card,
+			},
+		}
+	}
+
+	err = address.save(true)
+	if err != nil {
+		return
+	}
+
+	err = address.update()
+	if err != nil {
+		return
+	}
+
+	addressBook.Addresses.Store(addressID, address)
+	addressObject = address.Data
+	return
+}
+
+func (c *Contacts) DeleteAddressObject(addressPath string) (err error) {
+	c.lock.Lock()
+	defer c.lock.Unlock()
+
+	bookPath, addressID, err := ParseAddressPath(addressPath)
+	if err != nil {
+		logging.LogErrorf("parse address path [%s] failed: %s", addressPath, err)
+		return
+	}
+
+	var addressBook *AddressBook
+	if value, ok := c.books.Load(bookPath); ok {
+		addressBook = value.(*AddressBook)
+	} else {
+		err = ErrorBookNotFound
+		return
+	}
+
+	if value, loaded := addressBook.Addresses.LoadAndDelete(addressID); loaded {
+		address := value.(*AddressObject)
+
+		if err = os.Remove(address.FilePath); err != nil {
+			logging.LogErrorf("remove file [%s] failed: %s", address.FilePath, err)
+			return
+		}
+	}
+
+	return
+}
+
 type AddressBook struct {
 	Changed       bool
 	DirectoryPath string
@@ -360,6 +553,7 @@ func (b *AddressBook) save(force bool) error {
 				// id_ := id.(string)
 				address_ := address.(*AddressObject)
 				address_.save(force)
+				address_.update()
 			}()
 			return true
 		})
@@ -439,37 +633,39 @@ func (o *AddressObject) save(force bool) error {
 			return err
 		}
 
-		// update file info
-		addressFileInfo, err := os.Stat(o.FilePath)
-		if err != nil {
-			logging.LogErrorf("get file [%s] info failed: %s", o.FilePath, err)
-			return err
-		}
-
-		o.Data.Path = o.BookPath + "/" + addressFileInfo.Name()
-		o.Data.ModTime = addressFileInfo.ModTime()
-		o.Data.ContentLength = addressFileInfo.Size()
-		o.Data.ETag = fmt.Sprintf("%x-%x", addressFileInfo.ModTime(), addressFileInfo.Size())
-
 		o.Changed = false
 	}
 	return nil
 }
 
+// update file info
+func (o *AddressObject) update() error {
+	// update file info
+	addressFileInfo, err := os.Stat(o.FilePath)
+	if err != nil {
+		logging.LogErrorf("get file [%s] info failed: %s", o.FilePath, err)
+		return err
+	}
+
+	o.Data.Path = o.BookPath + "/" + addressFileInfo.Name()
+	o.Data.ModTime = addressFileInfo.ModTime()
+	o.Data.ContentLength = addressFileInfo.Size()
+	o.Data.ETag = fmt.Sprintf("%x-%x", addressFileInfo.ModTime(), addressFileInfo.Size())
+
+	return nil
+}
+
 type CardDavBackend struct{}
 
 func (b *CardDavBackend) CurrentUserPrincipal(ctx context.Context) (string, error) {
-	logging.LogInfof("CardDAV CurrentUserPrincipal")
 	return CardDavUserPrincipalPath, nil
 }
 
 func (b *CardDavBackend) AddressBookHomeSetPath(ctx context.Context) (string, error) {
-	logging.LogInfof("CardDAV AddressBookHomeSetPath")
 	return CardDavHomeSetPath, nil
 }
 
 func (b *CardDavBackend) ListAddressBooks(ctx context.Context) (addressBooks []carddav.AddressBook, err error) {
-	logging.LogInfof("CardDAV ListAddressBooks")
 	if err = contacts.Load(); err != nil {
 		return
 	}
@@ -479,7 +675,6 @@ func (b *CardDavBackend) ListAddressBooks(ctx context.Context) (addressBooks []c
 }
 
 func (b *CardDavBackend) GetAddressBook(ctx context.Context, bookPath string) (addressBook *carddav.AddressBook, err error) {
-	logging.LogInfof("CardDAV GetAddressBook: %s", bookPath)
 	if err = contacts.Load(); err != nil {
 		return
 	}
@@ -489,7 +684,6 @@ func (b *CardDavBackend) GetAddressBook(ctx context.Context, bookPath string) (a
 }
 
 func (b *CardDavBackend) CreateAddressBook(ctx context.Context, addressBook *carddav.AddressBook) (err error) {
-	logging.LogInfof("CardDAV CreateAddressBook")
 	if err = contacts.Load(); err != nil {
 		return
 	}
@@ -499,7 +693,6 @@ func (b *CardDavBackend) CreateAddressBook(ctx context.Context, addressBook *car
 }
 
 func (b *CardDavBackend) DeleteAddressBook(ctx context.Context, bookPath string) (err error) {
-	logging.LogInfof("CardDAV DeleteAddressBook: %s", bookPath)
 	if err = contacts.Load(); err != nil {
 		return
 	}
@@ -508,52 +701,47 @@ func (b *CardDavBackend) DeleteAddressBook(ctx context.Context, bookPath string)
 	return
 }
 
-func (b *CardDavBackend) GetAddressObject(ctx context.Context, urlPath string, req *carddav.AddressDataRequest) (addressObject *carddav.AddressObject, err error) {
-	logging.LogInfof("CardDAV GetAddressObject: %s", urlPath)
+func (b *CardDavBackend) GetAddressObject(ctx context.Context, addressPath string, req *carddav.AddressDataRequest) (addressObject *carddav.AddressObject, err error) {
 	if err = contacts.Load(); err != nil {
 		return
 	}
 
-	// TODO
+	addressObject, err = contacts.GetAddressObject(addressPath, req)
 	return
 }
 
 func (b *CardDavBackend) ListAddressObjects(ctx context.Context, bookPath string, req *carddav.AddressDataRequest) (addressObjects []carddav.AddressObject, err error) {
-	logging.LogInfof("CardDAV ListAddressObjects: %s", bookPath)
 	if err = contacts.Load(); err != nil {
 		return
 	}
 
-	// TODO
+	addressObjects, err = contacts.ListAddressObjects(bookPath, req)
 	return
 }
 
-func (b *CardDavBackend) QueryAddressObjects(ctx context.Context, urlPath string, query *carddav.AddressBookQuery) (addressObjects []carddav.AddressObject, err error) {
-	logging.LogInfof("CardDAV QueryAddressObjects: %s %v", urlPath, query)
+func (b *CardDavBackend) QueryAddressObjects(ctx context.Context, addressPath string, query *carddav.AddressBookQuery) (addressObjects []carddav.AddressObject, err error) {
 	if err = contacts.Load(); err != nil {
 		return
 	}
 
-	// TODO
+	addressObjects, err = contacts.QueryAddressObjects(addressPath, query)
 	return
 }
 
-func (b *CardDavBackend) PutAddressObject(ctx context.Context, urlPath string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (addressObject *carddav.AddressObject, err error) {
-	logging.LogInfof("CardDAV PutAddressObject: %s %v", urlPath, card)
+func (b *CardDavBackend) PutAddressObject(ctx context.Context, addressPath string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (addressObject *carddav.AddressObject, err error) {
 	if err = contacts.Load(); err != nil {
 		return
 	}
 
-	// TODO
+	addressObject, err = contacts.PutAddressObject(addressPath, card, opts)
 	return
 }
 
-func (b *CardDavBackend) DeleteAddressObject(ctx context.Context, urlPath string) (err error) {
-	logging.LogInfof("CardDAV DeleteAddressObject: %s", urlPath)
+func (b *CardDavBackend) DeleteAddressObject(ctx context.Context, addressPath string) (err error) {
 	if err = contacts.Load(); err != nil {
 		return
 	}
 
-	// TODO
+	err = contacts.DeleteAddressObject(addressPath)
 	return
 }
diff --git a/kernel/server/serve.go b/kernel/server/serve.go
index 3f782e61e82..4f3dc3cc42b 100644
--- a/kernel/server/serve.go
+++ b/kernel/server/serve.go
@@ -682,13 +682,11 @@ func serveCardDAV(ginServer *gin.Engine) {
 	}
 
 	ginServer.Match(CardDavMethods, "/.well-known/carddav", func(c *gin.Context) {
-		logging.LogInfof("CardDAV %s: /.well-known/carddav", c.Request.Method)
 		handler.ServeHTTP(c.Writer, c.Request)
 	})
 
 	ginGroup := ginServer.Group(model.CardDavPrefixPath, model.CheckAuth, model.CheckAdminRole)
 	ginGroup.Match(CardDavMethods, "/*path", func(c *gin.Context) {
-		logging.LogInfof("CardDAV %s: %s", c.Request.Method, c.Request.URL.Path)
 		if util.ReadOnly {
 			switch c.Request.Method {
 			case "POST", "PUT", "DELETE", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", "PROPPATCH":

From dea1b910c618bbc5935198e8a9be251035dfd914 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=A2=96=E9=80=B8?=
 <49649786+Zuoqiu-Yingyi@users.noreply.github.com>
Date: Wed, 6 Nov 2024 20:53:22 +0800
Subject: [PATCH 8/9] :art:  implement CardDAV `REPORT` method

---
 kernel/model/carddav.go        | 182 ++++++++++++++++++++++-----------
 kernel/model/session.go        |   7 +-
 kernel/server/proxy/publish.go |   3 +-
 kernel/server/serve.go         |  27 ++++-
 4 files changed, 154 insertions(+), 65 deletions(-)

diff --git a/kernel/model/carddav.go b/kernel/model/carddav.go
index 3891152a4d8..fd381182918 100644
--- a/kernel/model/carddav.go
+++ b/kernel/model/carddav.go
@@ -37,8 +37,8 @@ import (
 const (
 	// REF: https://developers.google.com/people/carddav#resources
 	CardDavPrefixPath        = "/carddav"
-	CardDavRootPath          = CardDavPrefixPath + "/principals"      // 0 resourceTypeRoot
-	CardDavUserPrincipalPath = CardDavRootPath + "/main"              // 1 resourceTypeUserPrincipal
+	CardDavPrincipalsPath    = CardDavPrefixPath + "/principals"      // 0 resourceTypeRoot
+	CardDavUserPrincipalPath = CardDavPrincipalsPath + "/main"        // 1 resourceTypeUserPrincipal
 	CardDavHomeSetPath       = CardDavUserPrincipalPath + "/contacts" // 2 resourceTypeAddressBookHomeSet
 
 	CardDavDefaultAddressBookPath = CardDavHomeSetPath + "/default" // 3 resourceTypeAddressBook
@@ -47,6 +47,17 @@ const (
 	CardDavAddressBooksMetaDataFilePath = CardDavHomeSetPath + "/address-books.json"
 )
 
+type PathDepth int
+
+const (
+	pathDepth_Root          PathDepth = 1 + iota // /carddav
+	pathDepth_Principals                         // /carddav/principals
+	pathDepth_UserPrincipal                      // /carddav/principals/main
+	pathDepth_HomeSet                            // /carddav/principals/main/contacts
+	pathDepth_AddressBook                        // /carddav/principals/main/contacts/default
+	pathDepth_Address                            // /carddav/principals/main/contacts/default/id.vcf
+)
+
 var (
 	defaultAddressBook = carddav.AddressBook{
 		Path:            CardDavDefaultAddressBookPath,
@@ -62,7 +73,8 @@ var (
 		booksMetaData: []*carddav.AddressBook{},
 	}
 
-	ErrorNotFound = errors.New("CardDAV: not found")
+	ErrorNotFound    = errors.New("CardDAV: not found")
+	ErrorPathInvalid = errors.New("CardDAV: path is invalid")
 
 	ErrorBookNotFound    = errors.New("CardDAV: address book not found")
 	ErrorBookPathInvalid = errors.New("CardDAV: address book path is invalid")
@@ -86,13 +98,18 @@ func AddressBooksMetaDataFilePath() string {
 	return CardDavPath2DirectoryPath(CardDavAddressBooksMetaDataFilePath)
 }
 
+func GetPathDepth(urlPath string) PathDepth {
+	urlPath = path.Clean(urlPath)
+	return PathDepth(len(strings.Split(urlPath, "/")) - 1)
+}
+
 // ParseAddressPath parses address path to address book path and address ID
-func ParseAddressPath(addressParh string) (addressBookPath string, addressID string, err error) {
-	addressBookPath, addressFileName := path.Split(addressParh)
+func ParseAddressPath(addressPath string) (addressBookPath string, addressID string, err error) {
+	addressBookPath, addressFileName := path.Split(addressPath)
 	addressID = path.Base(addressFileName)
 	addressFileExt := path.Ext(addressFileName)
 
-	if len(strings.Split(addressBookPath, "/")) != 6 {
+	if GetPathDepth(addressBookPath) != pathDepth_AddressBook {
 		err = ErrorBookPathInvalid
 		return
 	}
@@ -107,19 +124,22 @@ func ParseAddressPath(addressParh string) (addressBookPath string, addressID str
 
 func AddressPropsFilter(address *carddav.AddressObject, req *carddav.AddressDataRequest) *carddav.AddressObject {
 	var card *vcard.Card
-	if req.AllProp {
-		card = &address.Card
-	} else {
-		card = &vcard.Card{}
-		for _, prop := range req.Props {
-			fields := address.Card[prop]
-			if fields != nil {
-				for _, field := range fields {
-					card.Add(prop, field)
-				}
-			}
-		}
-	}
+	card = &address.Card
+
+	// if req.AllProp {
+	// 	card = &address.Card
+	// } else {
+	// 	card = &vcard.Card{}
+	// 	for _, prop := range req.Props {
+	// 		fields := address.Card[prop]
+	// 		if fields != nil {
+	// 			for _, field := range fields {
+	// 				card.Add(prop, field)
+	// 			}
+	// 		}
+	// 	}
+	// }
+
 	return &carddav.AddressObject{
 		Path:          address.Path,
 		ModTime:       address.ModTime,
@@ -237,6 +257,30 @@ func (c *Contacts) Load() error {
 	return nil
 }
 
+func (c *Contacts) GetAddress(addressPath string) (addressBook *AddressBook, addressObject *AddressObject, err error) {
+	bookPath, addressID, err := ParseAddressPath(addressPath)
+	if err != nil {
+		logging.LogErrorf("parse address path [%s] failed: %s", addressPath, err)
+		return
+	}
+
+	if value, ok := c.books.Load(bookPath); ok {
+		addressBook = value.(*AddressBook)
+	} else {
+		err = ErrorBookNotFound
+		return
+	}
+
+	if value, ok := addressBook.Addresses.Load(addressID); ok {
+		addressObject = value.(*AddressObject)
+	} else {
+		err = ErrorAddressNotFound
+		return
+	}
+
+	return
+}
+
 func (c *Contacts) ListAddressBooks() (addressBooks []carddav.AddressBook, err error) {
 	c.lock.Lock()
 	defer c.lock.Unlock()
@@ -349,27 +393,12 @@ func (c *Contacts) GetAddressObject(addressPath string, req *carddav.AddressData
 	c.lock.Lock()
 	defer c.lock.Unlock()
 
-	bookPath, addressID, err := ParseAddressPath(addressPath)
+	_, address, err := c.GetAddress(addressPath)
 	if err != nil {
-		logging.LogErrorf("parse address path [%s] failed: %s", addressPath, err)
-		return
-	}
-
-	var addressBook *AddressBook
-	if value, ok := c.books.Load(bookPath); ok {
-		addressBook = value.(*AddressBook)
-	} else {
-		err = ErrorBookNotFound
-		return
-	}
-
-	if value, ok := addressBook.Addresses.Load(addressID); ok {
-		addressObject = AddressPropsFilter(value.(*AddressObject).Data, req)
-	} else {
-		err = ErrorAddressNotFound
 		return
 	}
 
+	addressObject = AddressPropsFilter(address.Data, req)
 	return
 }
 
@@ -393,10 +422,38 @@ func (c *Contacts) ListAddressObjects(bookPath string, req *carddav.AddressDataR
 	return
 }
 
-func (c *Contacts) QueryAddressObjects(addressPath string, query *carddav.AddressBookQuery) (addressObjects []carddav.AddressObject, err error) {
+func (c *Contacts) QueryAddressObjects(urlPath string, query *carddav.AddressBookQuery) (addressObjects []carddav.AddressObject, err error) {
 	c.lock.Lock()
 	defer c.lock.Unlock()
-	// TODO
+
+	switch GetPathDepth(urlPath) {
+	case pathDepth_Root, pathDepth_Principals, pathDepth_UserPrincipal, pathDepth_HomeSet:
+		c.books.Range(func(path any, book any) bool {
+			addressBook := book.(*AddressBook)
+			addressBook.Addresses.Range(func(id any, address any) bool {
+				addressObjects = append(addressObjects, *address.(*AddressObject).Data)
+				return true
+			})
+			return true
+		})
+	case pathDepth_AddressBook:
+		if value, ok := c.books.Load(urlPath); ok {
+			addressBook := value.(*AddressBook)
+			addressBook.Addresses.Range(func(id any, address any) bool {
+				addressObjects = append(addressObjects, *address.(*AddressObject).Data)
+				return true
+			})
+		}
+	case pathDepth_Address:
+		if _, address, _ := c.GetAddress(urlPath); address != nil {
+			addressObjects = append(addressObjects, *address.Data)
+		}
+	default:
+		err = ErrorPathInvalid
+		return
+	}
+
+	addressObjects, err = carddav.Filter(query, addressObjects)
 	return
 }
 
@@ -459,29 +516,16 @@ func (c *Contacts) DeleteAddressObject(addressPath string) (err error) {
 	c.lock.Lock()
 	defer c.lock.Unlock()
 
-	bookPath, addressID, err := ParseAddressPath(addressPath)
-	if err != nil {
-		logging.LogErrorf("parse address path [%s] failed: %s", addressPath, err)
+	_, address, err := c.GetAddress(addressPath)
+	if err != nil && err != ErrorAddressNotFound {
 		return
 	}
 
-	var addressBook *AddressBook
-	if value, ok := c.books.Load(bookPath); ok {
-		addressBook = value.(*AddressBook)
-	} else {
-		err = ErrorBookNotFound
+	if err = os.Remove(address.FilePath); err != nil {
+		logging.LogErrorf("remove file [%s] failed: %s", address.FilePath, err)
 		return
 	}
 
-	if value, loaded := addressBook.Addresses.LoadAndDelete(addressID); loaded {
-		address := value.(*AddressObject)
-
-		if err = os.Remove(address.FilePath); err != nil {
-			logging.LogErrorf("remove file [%s] failed: %s", address.FilePath, err)
-			return
-		}
-	}
-
 	return
 }
 
@@ -599,7 +643,7 @@ func (o *AddressObject) load() error {
 	// load data
 	o.Changed = false
 	o.Data = &carddav.AddressObject{
-		Path:          o.BookPath + "/" + addressFileInfo.Name(),
+		Path:          path.Join(o.BookPath, addressFileInfo.Name()),
 		ModTime:       addressFileInfo.ModTime(),
 		ContentLength: addressFileInfo.Size(),
 		ETag:          fmt.Sprintf("%x-%x", addressFileInfo.ModTime(), addressFileInfo.Size()),
@@ -647,7 +691,7 @@ func (o *AddressObject) update() error {
 		return err
 	}
 
-	o.Data.Path = o.BookPath + "/" + addressFileInfo.Name()
+	o.Data.Path = path.Join(o.BookPath, addressFileInfo.Name())
 	o.Data.ModTime = addressFileInfo.ModTime()
 	o.Data.ContentLength = addressFileInfo.Size()
 	o.Data.ETag = fmt.Sprintf("%x-%x", addressFileInfo.ModTime(), addressFileInfo.Size())
@@ -658,90 +702,110 @@ func (o *AddressObject) update() error {
 type CardDavBackend struct{}
 
 func (b *CardDavBackend) CurrentUserPrincipal(ctx context.Context) (string, error) {
+	// logging.LogDebugf("CardDAV CurrentUserPrincipal")
 	return CardDavUserPrincipalPath, nil
 }
 
 func (b *CardDavBackend) AddressBookHomeSetPath(ctx context.Context) (string, error) {
+	// logging.LogDebugf("CardDAV AddressBookHomeSetPath")
 	return CardDavHomeSetPath, nil
 }
 
 func (b *CardDavBackend) ListAddressBooks(ctx context.Context) (addressBooks []carddav.AddressBook, err error) {
+	// logging.LogDebugf("CardDAV ListAddressBooks")
 	if err = contacts.Load(); err != nil {
 		return
 	}
 
 	addressBooks, err = contacts.ListAddressBooks()
+	// logging.LogDebugf("CardDAV ListAddressBooks <- addressBooks: %#v, err: %s", addressBooks, err)
 	return
 }
 
 func (b *CardDavBackend) GetAddressBook(ctx context.Context, bookPath string) (addressBook *carddav.AddressBook, err error) {
+	// logging.LogDebugf("CardDAV GetAddressBook -> bookPath: %s", bookPath)
 	if err = contacts.Load(); err != nil {
 		return
 	}
 
 	addressBook, err = contacts.GetAddressBook(bookPath)
+	// logging.LogDebugf("CardDAV GetAddressBook <- addressBook: %#v, err: %s", addressBook, err)
 	return
 }
 
 func (b *CardDavBackend) CreateAddressBook(ctx context.Context, addressBook *carddav.AddressBook) (err error) {
+	// logging.LogDebugf("CardDAV CreateAddressBook -> addressBook: %#v", addressBook)
 	if err = contacts.Load(); err != nil {
 		return
 	}
 
 	err = contacts.CreateAddressBook(addressBook)
+	// logging.LogDebugf("CardDAV CreateAddressBook <- err: %s", err)
 	return
 }
 
 func (b *CardDavBackend) DeleteAddressBook(ctx context.Context, bookPath string) (err error) {
+	// logging.LogDebugf("CardDAV DeleteAddressBook -> bookPath: %s", bookPath)
 	if err = contacts.Load(); err != nil {
 		return
 	}
 
 	err = contacts.DeleteAddressBook(bookPath)
+	// logging.LogDebugf("CardDAV DeleteAddressBook <- err: %s", err)
 	return
 }
 
 func (b *CardDavBackend) GetAddressObject(ctx context.Context, addressPath string, req *carddav.AddressDataRequest) (addressObject *carddav.AddressObject, err error) {
+	// logging.LogDebugf("CardDAV GetAddressObject -> addressPath: %s, req: %#v", addressPath, req)
 	if err = contacts.Load(); err != nil {
 		return
 	}
 
 	addressObject, err = contacts.GetAddressObject(addressPath, req)
+	// logging.LogDebugf("CardDAV GetAddressObject <- addressObject: %#v, err: %s", addressObject, err)
 	return
 }
 
 func (b *CardDavBackend) ListAddressObjects(ctx context.Context, bookPath string, req *carddav.AddressDataRequest) (addressObjects []carddav.AddressObject, err error) {
+	// logging.LogDebugf("CardDAV ListAddressObjects -> bookPath: %s, req: %#v", bookPath, req)
 	if err = contacts.Load(); err != nil {
 		return
 	}
 
 	addressObjects, err = contacts.ListAddressObjects(bookPath, req)
+	// logging.LogDebugf("CardDAV ListAddressObjects <- addressObjects: %#v, err: %s", addressObjects, err)
 	return
 }
 
-func (b *CardDavBackend) QueryAddressObjects(ctx context.Context, addressPath string, query *carddav.AddressBookQuery) (addressObjects []carddav.AddressObject, err error) {
+func (b *CardDavBackend) QueryAddressObjects(ctx context.Context, urlPath string, query *carddav.AddressBookQuery) (addressObjects []carddav.AddressObject, err error) {
+	// logging.LogDebugf("CardDAV QueryAddressObjects -> urlPath: %s, query: %#v", urlPath, query)
 	if err = contacts.Load(); err != nil {
 		return
 	}
 
-	addressObjects, err = contacts.QueryAddressObjects(addressPath, query)
+	addressObjects, err = contacts.QueryAddressObjects(urlPath, query)
+	// logging.LogDebugf("CardDAV QueryAddressObjects <- addressObjects: %#v, err: %s", addressObjects, err)
 	return
 }
 
 func (b *CardDavBackend) PutAddressObject(ctx context.Context, addressPath string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (addressObject *carddav.AddressObject, err error) {
+	// logging.LogDebugf("CardDAV PutAddressObject -> addressPath: %s, card: %#v, opts: %#v", addressPath, card, opts)
 	if err = contacts.Load(); err != nil {
 		return
 	}
 
 	addressObject, err = contacts.PutAddressObject(addressPath, card, opts)
+	// logging.LogDebugf("CardDAV PutAddressObject <- addressObject: %#v, err: %s", addressObject, err)
 	return
 }
 
 func (b *CardDavBackend) DeleteAddressObject(ctx context.Context, addressPath string) (err error) {
+	// logging.LogDebugf("CardDAV DeleteAddressObject -> addressPath: %s", addressPath)
 	if err = contacts.Load(); err != nil {
 		return
 	}
 
 	err = contacts.DeleteAddressObject(addressPath)
+	// logging.LogDebugf("CardDAV DeleteAddressObject <- err: %s", err)
 	return
 }
diff --git a/kernel/model/session.go b/kernel/model/session.go
index 3dc3bca8efa..a86d3268a16 100644
--- a/kernel/model/session.go
+++ b/kernel/model/session.go
@@ -34,6 +34,11 @@ import (
 	"github.com/steambap/captcha"
 )
 
+var (
+	BasicAuthHeaderKey   = "WWW-Authenticate"
+	BasicAuthHeaderValue = "Basic realm=\"SiYuan Authorization Require\", charset=\"UTF-8\""
+)
+
 func LogoutAuth(c *gin.Context) {
 	ret := gulu.Ret.NewResult()
 	defer c.JSON(http.StatusOK, ret)
@@ -301,7 +306,7 @@ func CheckAuth(c *gin.Context) {
 
 	// WebDAV BasicAuth Authenticate
 	if strings.HasPrefix(c.Request.RequestURI, "/webdav") || strings.HasPrefix(c.Request.RequestURI, "/carddav") {
-		c.Header("WWW-Authenticate", "Basic realm=Authorization Required")
+		c.Header(BasicAuthHeaderKey, BasicAuthHeaderValue)
 		c.AbortWithStatus(http.StatusUnauthorized)
 		return
 	}
diff --git a/kernel/server/proxy/publish.go b/kernel/server/proxy/publish.go
index 52506270e87..65d9a766637 100644
--- a/kernel/server/proxy/publish.go
+++ b/kernel/server/proxy/publish.go
@@ -21,7 +21,6 @@ import (
 	"net"
 	"net/http"
 	"net/http/httputil"
-	"strconv"
 
 	"github.com/siyuan-note/logging"
 	"github.com/siyuan-note/siyuan/kernel/model"
@@ -143,7 +142,7 @@ func (PublishServiceTransport) RoundTrip(request *http.Request) (response *http.
 				ProtoMinor: request.ProtoMinor,
 				Request:    request,
 				Header: http.Header{
-					"WWW-Authenticate": {"Basic realm=" + strconv.Quote("Authorization Required")},
+					model.BasicAuthHeaderKey: {model.BasicAuthHeaderValue},
 				},
 				Close:         false,
 				ContentLength: -1,
diff --git a/kernel/server/serve.go b/kernel/server/serve.go
index 4f3dc3cc42b..3d1bb676fe0 100644
--- a/kernel/server/serve.go
+++ b/kernel/server/serve.go
@@ -56,6 +56,7 @@ const (
 	MethodUnlock    = "UNLOCK"
 	MethodPropFind  = "PROPFIND"
 	MethodPropPatch = "PROPPATCH"
+	MethodReport    = "REPORT"
 )
 
 var (
@@ -102,6 +103,8 @@ var (
 		// MethodUnlock,
 		MethodPropFind,
 		MethodPropPatch,
+
+		MethodReport,
 	}
 )
 
@@ -665,7 +668,15 @@ func serveWebDAV(ginServer *gin.Engine) {
 	ginGroup.Match(WebDavMethods, "/*path", func(c *gin.Context) {
 		if util.ReadOnly {
 			switch c.Request.Method {
-			case "POST", "PUT", "DELETE", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", "PROPPATCH":
+			case http.MethodPost,
+				http.MethodPut,
+				http.MethodDelete,
+				MethodMkcol,
+				MethodCopy,
+				MethodMove,
+				MethodLock,
+				MethodUnlock,
+				MethodPropPatch:
 				c.AbortWithError(http.StatusForbidden, fmt.Errorf(model.Conf.Language(34)))
 				return
 			}
@@ -678,7 +689,7 @@ func serveCardDAV(ginServer *gin.Engine) {
 	// REF: https://github.com/emersion/hydroxide/blob/master/carddav/carddav.go
 	handler := carddav.Handler{
 		Backend: &model.CardDavBackend{},
-		Prefix:  model.CardDavRootPath,
+		Prefix:  model.CardDavPrincipalsPath,
 	}
 
 	ginServer.Match(CardDavMethods, "/.well-known/carddav", func(c *gin.Context) {
@@ -687,14 +698,24 @@ func serveCardDAV(ginServer *gin.Engine) {
 
 	ginGroup := ginServer.Group(model.CardDavPrefixPath, model.CheckAuth, model.CheckAdminRole)
 	ginGroup.Match(CardDavMethods, "/*path", func(c *gin.Context) {
+		// logging.LogDebugf("CardDAV -> [%s] %s", c.Request.Method, c.Request.URL.String())
 		if util.ReadOnly {
 			switch c.Request.Method {
-			case "POST", "PUT", "DELETE", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", "PROPPATCH":
+			case http.MethodPost,
+				http.MethodPut,
+				http.MethodDelete,
+				MethodMkcol,
+				MethodCopy,
+				MethodMove,
+				MethodLock,
+				MethodUnlock,
+				MethodPropPatch:
 				c.AbortWithError(http.StatusForbidden, fmt.Errorf(model.Conf.Language(34)))
 				return
 			}
 		}
 		handler.ServeHTTP(c.Writer, c.Request)
+		// logging.LogDebugf("CardDAV <- [%s] %v", c.Request.Method, c.Writer.Status())
 	})
 }
 

From 3d81ab41e093b11a2e6f1be608efb276497b549a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=A2=96=E9=80=B8?=
 <49649786+Zuoqiu-Yingyi@users.noreply.github.com>
Date: Wed, 6 Nov 2024 22:10:17 +0800
Subject: [PATCH 9/9] :art: parse *.vcf file with multiple vCard

---
 kernel/model/carddav.go | 87 ++++++++++++++++++++++++++++++++++++-----
 1 file changed, 78 insertions(+), 9 deletions(-)

diff --git a/kernel/model/carddav.go b/kernel/model/carddav.go
index fd381182918..8b2aad21a69 100644
--- a/kernel/model/carddav.go
+++ b/kernel/model/carddav.go
@@ -21,6 +21,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"io"
 	"os"
 	"path"
 	"path/filepath"
@@ -122,6 +123,7 @@ func ParseAddressPath(addressPath string) (addressBookPath string, addressID str
 	return
 }
 
+// AddressPropsFilter filters address properties
 func AddressPropsFilter(address *carddav.AddressObject, req *carddav.AddressDataRequest) *carddav.AddressObject {
 	var card *vcard.Card
 	card = &address.Card
@@ -149,6 +151,29 @@ func AddressPropsFilter(address *carddav.AddressObject, req *carddav.AddressData
 	}
 }
 
+func LoadCards(filePath string) (cards []*vcard.Card, err error) {
+	data, err := os.ReadFile(filePath)
+	if err != nil {
+		logging.LogErrorf("read vCard file [%s] failed: %s", filePath, err)
+		return
+	}
+
+	decoder := vcard.NewDecoder(bytes.NewReader(data))
+	for {
+		card, err := decoder.Decode()
+		if err != nil {
+			if err == io.EOF {
+				break
+			}
+			logging.LogErrorf("decode vCard file [%s] failed: %s", filePath, err)
+			return nil, err
+		}
+		cards = append(cards, &card)
+	}
+
+	return
+}
+
 type Contacts struct {
 	loaded        bool
 	changed       bool
@@ -559,19 +584,63 @@ func (b *AddressBook) load() error {
 				go func() {
 					defer wg.Done()
 
+					// load cards
 					addressFilePath := path.Join(b.DirectoryPath, filename)
-
-					// load data
-					address := &AddressObject{
-						FilePath: addressFilePath,
-						BookPath: b.MetaData.Path,
-					}
-					if err := address.load(); err != nil {
+					vCards, err := LoadCards(addressFilePath)
+					if err != nil {
 						return
 					}
 
-					id := path.Base(filename)
-					b.Addresses.Store(id, address)
+					switch len(vCards) {
+					case 0: // invalid file
+					case 1: // file contain 1 card
+						address := &AddressObject{
+							FilePath: addressFilePath,
+							BookPath: b.MetaData.Path,
+							Data: &carddav.AddressObject{
+								Card: *vCards[0],
+							},
+						}
+						if err := address.update(); err != nil {
+							return
+						}
+
+						id := path.Base(filename)
+						b.Addresses.Store(id, address)
+					default: // file contain multiple cards
+						// Create a file for each card
+						addressesWaitGroup := &sync.WaitGroup{}
+						for _, vCard := range vCards {
+							addressesWaitGroup.Add(1)
+							go func() {
+								defer addressesWaitGroup.Done()
+								filename_ := util.AssetName(filename)
+								address := &AddressObject{
+									FilePath: path.Join(b.DirectoryPath, filename_),
+									BookPath: b.MetaData.Path,
+									Data: &carddav.AddressObject{
+										Card: *vCard,
+									},
+								}
+								if err := address.save(true); err != nil {
+									return
+								}
+								if err := address.update(); err != nil {
+									return
+								}
+
+								id := path.Base(filename)
+								b.Addresses.Store(id, address)
+							}()
+						}
+
+						addressesWaitGroup.Wait()
+						// Delete original file with multiple cards
+						if err := os.Remove(addressFilePath); err != nil {
+							logging.LogErrorf("remove file [%s] failed: %s", addressFilePath, err)
+							return
+						}
+					}
 				}()
 			}
 		}