2017-09-03 21:11:01 +03:00
|
|
|
package carddav
|
|
|
|
|
|
|
|
import (
|
2017-09-12 23:45:13 +03:00
|
|
|
"bytes"
|
|
|
|
"errors"
|
2017-09-03 21:11:01 +03:00
|
|
|
"net/http"
|
2017-09-04 13:06:36 +03:00
|
|
|
"os"
|
2017-09-03 21:11:01 +03:00
|
|
|
"strings"
|
2017-09-09 19:23:14 +03:00
|
|
|
"sync"
|
2017-09-09 17:37:14 +03:00
|
|
|
"time"
|
2017-09-03 21:11:01 +03:00
|
|
|
|
|
|
|
"github.com/emersion/hydroxide/protonmail"
|
|
|
|
"github.com/emersion/go-vcard"
|
|
|
|
"github.com/emersion/go-webdav/carddav"
|
|
|
|
)
|
|
|
|
|
2017-09-09 16:37:03 +03:00
|
|
|
type contextKey string
|
|
|
|
|
|
|
|
const ClientContextKey = contextKey("client")
|
|
|
|
|
2017-09-13 11:11:14 +03:00
|
|
|
func formatCard(card vcard.Card) (*protonmail.ContactImport, error) {
|
|
|
|
// TODO: sign/encrypt stuff
|
|
|
|
|
|
|
|
var b bytes.Buffer
|
|
|
|
if err := vcard.NewEncoder(&b).Encode(card); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &protonmail.ContactImport{
|
|
|
|
Cards: []*protonmail.ContactCard{
|
|
|
|
{Data: b.String()},
|
|
|
|
},
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2017-09-09 17:37:14 +03:00
|
|
|
type addressFileInfo struct {
|
|
|
|
contact *protonmail.Contact
|
|
|
|
}
|
|
|
|
|
|
|
|
func (fi *addressFileInfo) Name() string {
|
|
|
|
return fi.contact.ID + ".vcf"
|
|
|
|
}
|
|
|
|
|
|
|
|
func (fi *addressFileInfo) Size() int64 {
|
|
|
|
return int64(fi.contact.Size)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (fi *addressFileInfo) Mode() os.FileMode {
|
|
|
|
return os.ModePerm
|
|
|
|
}
|
|
|
|
|
|
|
|
func (fi *addressFileInfo) ModTime() time.Time {
|
|
|
|
return time.Unix(fi.contact.ModifyTime, 0)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (fi *addressFileInfo) IsDir() bool {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func (fi *addressFileInfo) Sys() interface{} {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-09-03 21:11:01 +03:00
|
|
|
type addressObject struct {
|
|
|
|
c *protonmail.Client
|
2017-09-09 17:37:14 +03:00
|
|
|
contact *protonmail.Contact
|
2017-09-03 21:11:01 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func (ao *addressObject) ID() string {
|
|
|
|
return ao.contact.ID
|
|
|
|
}
|
|
|
|
|
2017-09-12 23:45:13 +03:00
|
|
|
func (ao *addressObject) Stat() (os.FileInfo, error) {
|
|
|
|
return &addressFileInfo{ao.contact}, nil
|
|
|
|
}
|
|
|
|
|
2017-09-03 21:11:01 +03:00
|
|
|
func (ao *addressObject) Card() (vcard.Card, error) {
|
|
|
|
card := make(vcard.Card)
|
|
|
|
|
|
|
|
for _, c := range ao.contact.Cards {
|
|
|
|
if c.Type.Encrypted() {
|
|
|
|
// TODO: decrypt
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if c.Type.Signed() {
|
|
|
|
// TODO: check signature
|
|
|
|
}
|
|
|
|
|
|
|
|
decoded, err := vcard.NewDecoder(strings.NewReader(c.Data)).Decode()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
for k, fields := range decoded {
|
|
|
|
for _, f := range fields {
|
|
|
|
card.Add(k, f)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return card, nil
|
|
|
|
}
|
|
|
|
|
2017-09-12 23:45:13 +03:00
|
|
|
func (ao *addressObject) SetCard(card vcard.Card) error {
|
2017-09-13 11:11:14 +03:00
|
|
|
contactImport, err := formatCard(card)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
contact, err := ao.c.UpdateContact(ao.contact.ID, contactImport)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
contact.Cards = contactImport.Cards // Not returned by the server
|
|
|
|
|
|
|
|
ao.contact = contact
|
|
|
|
return nil
|
2017-09-04 13:06:36 +03:00
|
|
|
}
|
|
|
|
|
2017-09-03 21:11:01 +03:00
|
|
|
type addressBook struct {
|
2017-09-04 12:41:26 +03:00
|
|
|
c *protonmail.Client
|
2017-09-09 17:37:14 +03:00
|
|
|
cache map[string]*addressObject
|
2017-09-09 19:23:14 +03:00
|
|
|
locker sync.Mutex
|
2017-09-04 12:41:26 +03:00
|
|
|
total int
|
|
|
|
}
|
|
|
|
|
2017-09-09 17:45:05 +03:00
|
|
|
func (ab *addressBook) Info() (*carddav.AddressBookInfo, error) {
|
|
|
|
return &carddav.AddressBookInfo{
|
|
|
|
Name: "ProtonMail",
|
|
|
|
Description: "ProtonMail contacts",
|
|
|
|
MaxResourceSize: 100 * 1024,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2017-09-04 12:41:26 +03:00
|
|
|
func (ab *addressBook) cacheComplete() bool {
|
2017-09-09 19:23:14 +03:00
|
|
|
ab.locker.Lock()
|
|
|
|
defer ab.locker.Unlock()
|
2017-09-04 12:41:26 +03:00
|
|
|
return ab.total >= 0 && len(ab.cache) == ab.total
|
2017-09-03 21:11:01 +03:00
|
|
|
}
|
|
|
|
|
2017-09-09 19:23:14 +03:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2017-09-03 21:11:01 +03:00
|
|
|
func (ab *addressBook) ListAddressObjects() ([]carddav.AddressObject, error) {
|
2017-09-04 12:41:26 +03:00
|
|
|
if ab.cacheComplete() {
|
2017-09-09 19:23:14 +03:00
|
|
|
ab.locker.Lock()
|
|
|
|
defer ab.locker.Unlock()
|
|
|
|
|
2017-09-04 12:41:26 +03:00
|
|
|
aos := make([]carddav.AddressObject, 0, len(ab.cache))
|
|
|
|
for _, ao := range ab.cache {
|
|
|
|
aos = append(aos, ao)
|
|
|
|
}
|
2017-09-09 19:23:14 +03:00
|
|
|
|
2017-09-04 12:41:26 +03:00
|
|
|
return aos, nil
|
|
|
|
}
|
|
|
|
|
2017-09-09 17:37:14 +03:00
|
|
|
// Get a list of all contacts
|
|
|
|
// TODO: paging support
|
|
|
|
total, contacts, err := ab.c.ListContacts(0, 0)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2017-09-09 19:23:14 +03:00
|
|
|
ab.locker.Lock()
|
2017-09-09 17:37:14 +03:00
|
|
|
ab.total = total
|
2017-09-09 19:23:14 +03:00
|
|
|
ab.locker.Unlock()
|
2017-09-09 17:37:14 +03:00
|
|
|
|
|
|
|
for _, contact := range contacts {
|
2017-09-09 19:23:14 +03:00
|
|
|
if _, ok := ab.addressObject(contact.ID); !ok {
|
|
|
|
ab.cacheAddressObject(&addressObject{
|
2017-09-09 17:37:14 +03:00
|
|
|
c: ab.c,
|
|
|
|
contact: contact,
|
2017-09-09 19:23:14 +03:00
|
|
|
})
|
2017-09-09 17:37:14 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get all contacts cards
|
2017-09-04 12:46:14 +03:00
|
|
|
var aos []carddav.AddressObject
|
|
|
|
page := 0
|
|
|
|
for {
|
2017-09-09 19:23:14 +03:00
|
|
|
_, contacts, err := ab.c.ListContactsExport(page, 0)
|
2017-09-04 12:46:14 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2017-09-03 21:11:01 +03:00
|
|
|
|
2017-09-04 12:46:14 +03:00
|
|
|
if aos == nil {
|
|
|
|
aos = make([]carddav.AddressObject, 0, total)
|
|
|
|
}
|
2017-09-04 12:41:26 +03:00
|
|
|
|
2017-09-04 12:46:14 +03:00
|
|
|
for _, contact := range contacts {
|
2017-09-09 19:23:14 +03:00
|
|
|
ao, ok := ab.addressObject(contact.ID)
|
2017-09-09 17:37:14 +03:00
|
|
|
if !ok {
|
|
|
|
ao = &addressObject{
|
|
|
|
c: ab.c,
|
|
|
|
contact: &protonmail.Contact{ID: contact.ID},
|
|
|
|
}
|
2017-09-09 19:23:14 +03:00
|
|
|
ab.cacheAddressObject(ao)
|
2017-09-09 17:37:14 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
ao.contact.Cards = contact.Cards
|
2017-09-04 12:46:14 +03:00
|
|
|
aos = append(aos, ao)
|
|
|
|
}
|
|
|
|
|
2017-09-09 19:23:14 +03:00
|
|
|
if len(aos) >= total || len(contacts) == 0 {
|
2017-09-04 12:46:14 +03:00
|
|
|
break
|
|
|
|
}
|
|
|
|
page++
|
2017-09-03 21:11:01 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return aos, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ab *addressBook) GetAddressObject(id string) (carddav.AddressObject, error) {
|
2017-09-09 19:23:14 +03:00
|
|
|
if ao, ok := ab.addressObject(id); ok {
|
2017-09-04 12:41:26 +03:00
|
|
|
return ao, nil
|
|
|
|
} else if ab.cacheComplete() {
|
|
|
|
return nil, carddav.ErrNotFound
|
|
|
|
}
|
|
|
|
|
2017-09-03 21:11:01 +03:00
|
|
|
contact, err := ab.c.GetContact(id)
|
|
|
|
if err != nil {
|
2017-09-12 23:20:25 +03:00
|
|
|
// TODO: return carddav.ErrNotFound if appropriate
|
2017-09-03 21:11:01 +03:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2017-09-04 12:41:26 +03:00
|
|
|
ao := &addressObject{
|
2017-09-12 23:45:13 +03:00
|
|
|
c: ab.c,
|
|
|
|
contact: contact,
|
|
|
|
}
|
|
|
|
ab.cacheAddressObject(ao)
|
|
|
|
return ao, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ab *addressBook) CreateAddressObject(card vcard.Card) (carddav.AddressObject, error) {
|
2017-09-13 11:11:14 +03:00
|
|
|
contactImport, err := formatCard(card)
|
|
|
|
if err != nil {
|
2017-09-12 23:45:13 +03:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
resps, err := ab.c.CreateContacts([]*protonmail.ContactImport{contactImport})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if len(resps) != 1 {
|
|
|
|
return nil, errors.New("hydroxide/carddav: expected exactly one response when creating contact")
|
|
|
|
}
|
|
|
|
resp := resps[0]
|
|
|
|
if err := resp.Err(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
contact := resp.Response.Contact
|
|
|
|
contact.Cards = contactImport.Cards // Not returned by the server
|
|
|
|
|
|
|
|
ao := &addressObject{
|
2017-09-03 21:11:01 +03:00
|
|
|
c: ab.c,
|
2017-09-09 17:37:14 +03:00
|
|
|
contact: contact,
|
2017-09-04 12:41:26 +03:00
|
|
|
}
|
2017-09-09 19:23:14 +03:00
|
|
|
ab.cacheAddressObject(ao)
|
2017-09-04 12:41:26 +03:00
|
|
|
return ao, nil
|
2017-09-03 21:11:01 +03:00
|
|
|
}
|
|
|
|
|
2017-09-09 19:23:14 +03:00
|
|
|
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{
|
2017-09-04 12:41:26 +03:00
|
|
|
c: c,
|
2017-09-09 17:37:14 +03:00
|
|
|
cache: make(map[string]*addressObject),
|
2017-09-04 12:41:26 +03:00
|
|
|
total: -1,
|
2017-09-09 19:23:14 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if events != nil {
|
|
|
|
go ab.receiveEvents(events)
|
|
|
|
}
|
|
|
|
|
|
|
|
return carddav.NewHandler(ab)
|
2017-09-03 21:11:01 +03:00
|
|
|
}
|