diff --git a/carddav/carddav.go b/carddav/carddav.go index 77b61df..9777aca 100644 --- a/carddav/carddav.go +++ b/carddav/carddav.go @@ -4,6 +4,7 @@ import ( "net/http" "os" "strings" + "sync" "time" "github.com/emersion/hydroxide/protonmail" @@ -86,6 +87,7 @@ func (ao *addressObject) Stat() (os.FileInfo, error) { type addressBook struct { c *protonmail.Client cache map[string]*addressObject + locker sync.Mutex total int } @@ -98,15 +100,34 @@ func (ab *addressBook) Info() (*carddav.AddressBookInfo, error) { } func (ab *addressBook) cacheComplete() bool { + ab.locker.Lock() + defer ab.locker.Unlock() return ab.total >= 0 && len(ab.cache) == ab.total } +func (ab *addressBook) addressObject(id string) (*addressObject, bool) { + ab.locker.Lock() + defer ab.locker.Unlock() + ao, ok := ab.cache[id] + return ao, ok +} + +func (ab *addressBook) cacheAddressObject(ao *addressObject) { + ab.locker.Lock() + defer ab.locker.Unlock() + ab.cache[ao.contact.ID] = ao +} + func (ab *addressBook) ListAddressObjects() ([]carddav.AddressObject, error) { if ab.cacheComplete() { + ab.locker.Lock() + defer ab.locker.Unlock() + aos := make([]carddav.AddressObject, 0, len(ab.cache)) for _, ao := range ab.cache { aos = append(aos, ao) } + return aos, nil } @@ -116,14 +137,16 @@ func (ab *addressBook) ListAddressObjects() ([]carddav.AddressObject, error) { if err != nil { return nil, err } + ab.locker.Lock() ab.total = total + ab.locker.Unlock() for _, contact := range contacts { - if _, ok := ab.cache[contact.ID]; !ok { - ab.cache[contact.ID] = &addressObject{ + if _, ok := ab.addressObject(contact.ID); !ok { + ab.cacheAddressObject(&addressObject{ c: ab.c, contact: contact, - } + }) } } @@ -131,31 +154,30 @@ func (ab *addressBook) ListAddressObjects() ([]carddav.AddressObject, error) { var aos []carddav.AddressObject page := 0 for { - total, contacts, err := ab.c.ListContactsExport(page, 0) + _, contacts, err := ab.c.ListContactsExport(page, 0) if err != nil { return nil, err } - ab.total = total if aos == nil { aos = make([]carddav.AddressObject, 0, total) } for _, contact := range contacts { - ao, ok := ab.cache[contact.ID] + ao, ok := ab.addressObject(contact.ID) if !ok { ao = &addressObject{ c: ab.c, contact: &protonmail.Contact{ID: contact.ID}, } - ab.cache[contact.ID] = ao + ab.cacheAddressObject(ao) } ao.contact.Cards = contact.Cards aos = append(aos, ao) } - if len(aos) == total || len(contacts) == 0 { + if len(aos) >= total || len(contacts) == 0 { break } page++ @@ -165,7 +187,7 @@ func (ab *addressBook) ListAddressObjects() ([]carddav.AddressObject, error) { } func (ab *addressBook) GetAddressObject(id string) (carddav.AddressObject, error) { - if ao, ok := ab.cache[id]; ok { + if ao, ok := ab.addressObject(id); ok { return ao, nil } else if ab.cacheComplete() { return nil, carddav.ErrNotFound @@ -180,14 +202,47 @@ func (ab *addressBook) GetAddressObject(id string) (carddav.AddressObject, error c: ab.c, contact: contact, } - ab.cache[id] = ao + ab.cacheAddressObject(ao) return ao, nil } -func NewHandler(c *protonmail.Client) http.Handler { - return carddav.NewHandler(&addressBook{ +func (ab *addressBook) receiveEvents(events <-chan *protonmail.Event) { + for event := range events { + ab.locker.Lock() + if event.Refresh == 1 { + ab.cache = make(map[string]*addressObject) + ab.total = -1 + } else if len(event.Contacts) > 0 { + for _, eventContact := range event.Contacts { + switch eventContact.Action { + case protonmail.EventCreate: + ab.total++ + fallthrough + case protonmail.EventUpdate: + ab.cache[eventContact.ID] = &addressObject{ + c: ab.c, + contact: eventContact.Contact, + } + case protonmail.EventDelete: + delete(ab.cache, eventContact.ID) + ab.total-- + } + } + } + ab.locker.Unlock() + } +} + +func NewHandler(c *protonmail.Client, events <-chan *protonmail.Event) http.Handler { + ab := &addressBook{ c: c, cache: make(map[string]*addressObject), total: -1, - }) + } + + if events != nil { + go ab.receiveEvents(events) + } + + return carddav.NewHandler(ab) } diff --git a/cmd/hydroxide/hydroxide.go b/cmd/hydroxide/hydroxide.go index 8290cea..7b5c464 100644 --- a/cmd/hydroxide/hydroxide.go +++ b/cmd/hydroxide/hydroxide.go @@ -12,6 +12,7 @@ import ( "log" "net/http" "os" + "time" "github.com/emersion/hydroxide/carddav" "github.com/emersion/hydroxide/protonmail" @@ -122,6 +123,26 @@ func authenticate(c *protonmail.Client, cachedAuth *cachedAuth) error { return err } +func receiveEvents(c *protonmail.Client, last string, ch chan<- *protonmail.Event) { + t := time.NewTicker(time.Minute) + defer t.Stop() + + for range t.C { + event, err := c.GetEvent(last) + if err != nil { + log.Println("Cannot receive event:", err) + continue + } + + if event.ID == last { + continue + } + last = event.ID + + ch <- event + } +} + type session struct { h http.Handler hashedSecretKey []byte @@ -286,7 +307,9 @@ func main() { return } - h = carddav.NewHandler(c) + events := make(chan *protonmail.Event) + go receiveEvents(c, cachedAuth.EventID, events) + h = carddav.NewHandler(c, events) sessions[username] = &session{ h: h, diff --git a/protonmail/events.go b/protonmail/events.go new file mode 100644 index 0000000..f5e9527 --- /dev/null +++ b/protonmail/events.go @@ -0,0 +1,57 @@ +package protonmail + +import ( + "net/http" +) + +type Event struct { + ID string `json:"EventID"` + Refresh int + //Messages + Contacts []*EventContact + //ContactEmails + //Labels + //User + //Members + //Domains + //Organization + //MessageCounts + //ConversationCounts + //UsedSpace + Notices []string +} + +type EventAction int + +const ( + EventDelete EventAction = iota + EventCreate + EventUpdate +) + +type EventContact struct { + ID string + Action EventAction + Contact *Contact +} + +func (c *Client) GetEvent(last string) (*Event, error) { + if last == "" { + last = "latest" + } + + req, err := c.newRequest(http.MethodGet, "/events/"+last, nil) + if err != nil { + return nil, err + } + + var respData struct { + resp + *Event + } + if err := c.doJSON(req, &respData); err != nil { + return nil, err + } + + return respData.Event, nil +}