2017-09-03 21:11:01 +03:00
|
|
|
package carddav
|
|
|
|
|
|
|
|
import (
|
2017-09-12 23:45:13 +03:00
|
|
|
"bytes"
|
2022-05-24 22:06:25 +03:00
|
|
|
"context"
|
2017-09-12 23:45:13 +03:00
|
|
|
"errors"
|
2020-02-04 22:28:25 +02:00
|
|
|
"fmt"
|
2017-09-13 19:53:40 +03:00
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
2017-09-03 21:11:01 +03:00
|
|
|
"net/http"
|
2020-02-04 22:28:25 +02:00
|
|
|
"path"
|
2017-09-13 19:53:40 +03:00
|
|
|
"strconv"
|
2020-02-04 22:28:25 +02:00
|
|
|
"strings"
|
2017-09-09 19:23:14 +03:00
|
|
|
"sync"
|
2017-09-03 21:11:01 +03:00
|
|
|
|
2020-12-29 19:47:20 +02:00
|
|
|
"github.com/ProtonMail/go-crypto/openpgp"
|
2017-09-03 21:11:01 +03:00
|
|
|
"github.com/emersion/go-vcard"
|
|
|
|
"github.com/emersion/go-webdav/carddav"
|
2017-09-13 12:43:12 +03:00
|
|
|
"github.com/emersion/hydroxide/protonmail"
|
2017-09-03 21:11:01 +03:00
|
|
|
)
|
|
|
|
|
2020-02-04 22:28:25 +02:00
|
|
|
// TODO: use a HTTP error
|
|
|
|
var errNotFound = errors.New("carddav: not found")
|
2017-09-09 16:37:03 +03:00
|
|
|
|
2017-09-13 19:53:40 +03:00
|
|
|
var (
|
|
|
|
cleartextCardProps = []string{vcard.FieldVersion, vcard.FieldProductID, "X-PM-LABEL", "X-PM-GROUP"}
|
2017-09-13 20:03:05 +03:00
|
|
|
signedCardProps = []string{vcard.FieldVersion, vcard.FieldProductID, vcard.FieldFormattedName, vcard.FieldUID, vcard.FieldEmail}
|
2017-09-13 19:53:40 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
func formatCard(card vcard.Card, privateKey *openpgp.Entity) (*protonmail.ContactImport, error) {
|
|
|
|
vcard.ToV4(card)
|
|
|
|
|
|
|
|
// Add groups to emails
|
|
|
|
i := 1
|
|
|
|
for _, email := range card[vcard.FieldEmail] {
|
|
|
|
if email.Group == "" {
|
2017-09-14 11:32:38 +03:00
|
|
|
email.Group = "item" + strconv.Itoa(i)
|
2017-09-13 19:53:40 +03:00
|
|
|
i++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
toEncrypt := card
|
|
|
|
toSign := make(vcard.Card)
|
|
|
|
for _, k := range signedCardProps {
|
|
|
|
if fields, ok := toEncrypt[k]; ok {
|
|
|
|
toSign[k] = fields
|
|
|
|
if k != vcard.FieldVersion {
|
|
|
|
delete(toEncrypt, k)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-09-13 11:11:14 +03:00
|
|
|
|
2017-09-13 19:53:40 +03:00
|
|
|
var contactImport protonmail.ContactImport
|
2017-09-13 11:11:14 +03:00
|
|
|
var b bytes.Buffer
|
2017-09-13 19:53:40 +03:00
|
|
|
|
|
|
|
if len(toSign) > 0 {
|
|
|
|
if err := vcard.NewEncoder(&b).Encode(toSign); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
signed, err := protonmail.NewSignedContactCard(&b, privateKey)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
contactImport.Cards = append(contactImport.Cards, signed)
|
|
|
|
b.Reset()
|
2017-09-13 11:11:14 +03:00
|
|
|
}
|
|
|
|
|
2017-09-13 19:53:40 +03:00
|
|
|
if len(toEncrypt) > 0 {
|
|
|
|
if err := vcard.NewEncoder(&b).Encode(toEncrypt); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
to := []*openpgp.Entity{privateKey}
|
|
|
|
encrypted, err := protonmail.NewEncryptedContactCard(&b, to, privateKey)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
contactImport.Cards = append(contactImport.Cards, encrypted)
|
|
|
|
b.Reset()
|
|
|
|
}
|
|
|
|
|
|
|
|
return &contactImport, nil
|
2017-09-13 11:11:14 +03:00
|
|
|
}
|
|
|
|
|
2020-02-04 22:28:25 +02:00
|
|
|
func parseAddressObjectPath(p string) (string, error) {
|
|
|
|
dirname, filename := path.Split(p)
|
|
|
|
ext := path.Ext(filename)
|
2022-05-24 22:06:25 +03:00
|
|
|
if dirname != "/contacts/default/" || ext != ".vcf" {
|
2020-02-04 22:28:25 +02:00
|
|
|
return "", errNotFound
|
|
|
|
}
|
|
|
|
return strings.TrimSuffix(filename, ext), nil
|
2017-09-03 21:11:01 +03:00
|
|
|
}
|
|
|
|
|
2020-02-04 22:28:25 +02:00
|
|
|
func formatAddressObjectPath(id string) string {
|
2022-05-24 22:06:25 +03:00
|
|
|
return "/contacts/default/" + id + ".vcf"
|
2017-09-03 21:11:01 +03:00
|
|
|
}
|
|
|
|
|
2020-02-04 22:28:25 +02:00
|
|
|
func (b *backend) toAddressObject(contact *protonmail.Contact, req *carddav.AddressDataRequest) (*carddav.AddressObject, error) {
|
|
|
|
// TODO: handle req
|
2017-09-12 23:45:13 +03:00
|
|
|
|
2017-09-03 21:11:01 +03:00
|
|
|
card := make(vcard.Card)
|
2020-02-04 22:28:25 +02:00
|
|
|
for _, c := range contact.Cards {
|
|
|
|
md, err := c.Read(b.privateKeys)
|
2017-09-13 19:53:40 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2017-09-03 21:11:01 +03:00
|
|
|
}
|
|
|
|
|
2017-09-13 19:53:40 +03:00
|
|
|
decoded, err := vcard.NewDecoder(md.UnverifiedBody).Decode()
|
2017-09-03 21:11:01 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2017-09-13 19:53:40 +03:00
|
|
|
// The signature can be checked only if md.UnverifiedBody is consumed until
|
|
|
|
// EOF
|
2017-09-14 16:35:40 +03:00
|
|
|
io.Copy(ioutil.Discard, md.UnverifiedBody)
|
2017-09-13 19:53:40 +03:00
|
|
|
if err := md.SignatureError; err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2017-09-03 21:11:01 +03:00
|
|
|
for k, fields := range decoded {
|
|
|
|
for _, f := range fields {
|
|
|
|
card.Add(k, f)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-04 22:28:25 +02:00
|
|
|
return &carddav.AddressObject{
|
|
|
|
Path: formatAddressObjectPath(contact.ID),
|
2020-09-14 13:09:50 +03:00
|
|
|
ModTime: contact.ModifyTime.Time(),
|
2020-02-04 22:28:25 +02:00
|
|
|
// TODO: stronger ETag
|
|
|
|
ETag: fmt.Sprintf("%x%x", contact.ModifyTime, contact.Size),
|
|
|
|
Card: card,
|
|
|
|
}, nil
|
2017-09-14 12:52:19 +03:00
|
|
|
}
|
|
|
|
|
2020-02-04 22:28:25 +02:00
|
|
|
type backend struct {
|
2017-09-13 20:03:05 +03:00
|
|
|
c *protonmail.Client
|
2020-02-04 22:28:25 +02:00
|
|
|
cache map[string]*protonmail.Contact
|
2017-09-13 20:03:05 +03:00
|
|
|
locker sync.Mutex
|
|
|
|
total int
|
2017-09-13 19:53:40 +03:00
|
|
|
privateKeys openpgp.EntityList
|
2017-09-04 12:41:26 +03:00
|
|
|
}
|
|
|
|
|
2022-05-24 22:06:25 +03:00
|
|
|
func (b *backend) CurrentUserPrincipal(ctx context.Context) (string, error) {
|
|
|
|
return "/", nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *backend) AddressbookHomeSetPath(ctx context.Context) (string, error) {
|
|
|
|
return "/contacts", nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *backend) AddressBook(ctx context.Context) (*carddav.AddressBook, error) {
|
2020-02-04 22:28:25 +02:00
|
|
|
return &carddav.AddressBook{
|
2022-05-24 22:06:25 +03:00
|
|
|
Path: "/contacts/default",
|
2017-09-13 12:43:12 +03:00
|
|
|
Name: "ProtonMail",
|
|
|
|
Description: "ProtonMail contacts",
|
2017-09-09 17:45:05 +03:00
|
|
|
MaxResourceSize: 100 * 1024,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2020-02-04 22:28:25 +02:00
|
|
|
func (b *backend) cacheComplete() bool {
|
|
|
|
b.locker.Lock()
|
|
|
|
defer b.locker.Unlock()
|
|
|
|
return b.total >= 0 && len(b.cache) == b.total
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *backend) getCache(id string) (*protonmail.Contact, bool) {
|
|
|
|
b.locker.Lock()
|
|
|
|
contact, ok := b.cache[id]
|
|
|
|
b.locker.Unlock()
|
|
|
|
return contact, ok
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *backend) putCache(contact *protonmail.Contact) {
|
|
|
|
b.locker.Lock()
|
|
|
|
b.cache[contact.ID] = contact
|
|
|
|
b.locker.Unlock()
|
2017-09-03 21:11:01 +03:00
|
|
|
}
|
|
|
|
|
2020-02-04 22:28:25 +02:00
|
|
|
func (b *backend) deleteCache(id string) {
|
|
|
|
b.locker.Lock()
|
|
|
|
delete(b.cache, id)
|
|
|
|
b.locker.Unlock()
|
2017-09-09 19:23:14 +03:00
|
|
|
}
|
|
|
|
|
2022-05-24 22:06:25 +03:00
|
|
|
func (b *backend) GetAddressObject(ctx context.Context, path string, req *carddav.AddressDataRequest) (*carddav.AddressObject, error) {
|
2020-02-04 22:28:25 +02:00
|
|
|
id, err := parseAddressObjectPath(path)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
contact, ok := b.getCache(id)
|
|
|
|
if !ok {
|
|
|
|
if b.cacheComplete() {
|
|
|
|
return nil, errNotFound
|
|
|
|
}
|
|
|
|
|
|
|
|
contact, err = b.c.GetContact(id)
|
|
|
|
if apiErr, ok := err.(*protonmail.APIError); ok && apiErr.Code == 13051 {
|
|
|
|
return nil, errNotFound
|
|
|
|
} else if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
b.putCache(contact)
|
|
|
|
}
|
|
|
|
|
|
|
|
return b.toAddressObject(contact, req)
|
2017-09-09 19:23:14 +03:00
|
|
|
}
|
|
|
|
|
2022-05-24 22:06:25 +03:00
|
|
|
func (b *backend) ListAddressObjects(ctx context.Context, req *carddav.AddressDataRequest) ([]carddav.AddressObject, error) {
|
2020-02-04 22:28:25 +02:00
|
|
|
if b.cacheComplete() {
|
|
|
|
b.locker.Lock()
|
|
|
|
defer b.locker.Unlock()
|
2017-09-09 19:23:14 +03:00
|
|
|
|
2020-02-04 22:28:25 +02:00
|
|
|
aos := make([]carddav.AddressObject, 0, len(b.cache))
|
|
|
|
for _, contact := range b.cache {
|
|
|
|
ao, err := b.toAddressObject(contact, req)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
aos = append(aos, *ao)
|
2017-09-04 12:41:26 +03:00
|
|
|
}
|
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
|
2020-02-04 22:28:25 +02:00
|
|
|
total, contacts, err := b.c.ListContacts(0, 0)
|
2017-09-09 17:37:14 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2020-02-04 22:28:25 +02:00
|
|
|
b.locker.Lock()
|
|
|
|
b.total = total
|
|
|
|
b.locker.Unlock()
|
2017-09-09 17:37:14 +03:00
|
|
|
|
2020-02-04 22:28:25 +02:00
|
|
|
m := make(map[string]*protonmail.Contact, total)
|
2017-09-09 17:37:14 +03:00
|
|
|
for _, contact := range contacts {
|
2020-02-04 22:28:25 +02:00
|
|
|
m[contact.ID] = contact
|
2017-09-09 17:37:14 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Get all contacts cards
|
2020-02-04 22:28:25 +02:00
|
|
|
aos := make([]carddav.AddressObject, 0, total)
|
2017-09-04 12:46:14 +03:00
|
|
|
page := 0
|
|
|
|
for {
|
2020-02-04 22:28:25 +02:00
|
|
|
_, contacts, err := b.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
|
|
|
|
2020-02-04 22:28:25 +02:00
|
|
|
for _, contactExport := range contacts {
|
|
|
|
contact, ok := m[contactExport.ID]
|
2017-09-09 17:37:14 +03:00
|
|
|
if !ok {
|
2020-02-04 22:28:25 +02:00
|
|
|
continue
|
2017-09-09 17:37:14 +03:00
|
|
|
}
|
2020-02-04 22:28:25 +02:00
|
|
|
contact.Cards = contactExport.Cards
|
|
|
|
b.putCache(contact)
|
2017-09-09 17:37:14 +03:00
|
|
|
|
2020-02-04 22:28:25 +02:00
|
|
|
ao, err := b.toAddressObject(contact, req)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
aos = append(aos, *ao)
|
2017-09-04 12:46:14 +03:00
|
|
|
}
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-05-24 22:06:25 +03:00
|
|
|
func (b *backend) QueryAddressObjects(ctx context.Context, query *carddav.AddressBookQuery) ([]carddav.AddressObject, error) {
|
2020-02-04 22:28:25 +02:00
|
|
|
panic("TODO")
|
|
|
|
}
|
|
|
|
|
2022-05-24 22:06:25 +03:00
|
|
|
func (b *backend) PutAddressObject(ctx context.Context, path string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (loc string, err error) {
|
2020-02-04 22:28:25 +02:00
|
|
|
id, err := parseAddressObjectPath(path)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
2017-09-04 12:41:26 +03:00
|
|
|
}
|
|
|
|
|
2020-02-04 22:28:25 +02:00
|
|
|
contactImport, err := formatCard(card, b.privateKeys[0])
|
2017-09-03 21:11:01 +03:00
|
|
|
if err != nil {
|
2020-02-04 22:28:25 +02:00
|
|
|
return "", err
|
2017-09-03 21:11:01 +03:00
|
|
|
}
|
|
|
|
|
2020-02-04 22:28:25 +02:00
|
|
|
var contact *protonmail.Contact
|
|
|
|
|
|
|
|
var req carddav.AddressDataRequest
|
2022-05-24 22:06:25 +03:00
|
|
|
if _, getErr := b.GetAddressObject(ctx, path, &req); getErr == nil {
|
2020-02-04 22:28:25 +02:00
|
|
|
contact, err = b.c.UpdateContact(id, contactImport)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
resps, err := b.c.CreateContacts([]*protonmail.ContactImport{contactImport})
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
if len(resps) != 1 {
|
|
|
|
return "", errors.New("hydroxide/carddav: expected exactly one response when creating contact")
|
|
|
|
}
|
|
|
|
resp := resps[0]
|
|
|
|
if err := resp.Err(); err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
contact = resp.Response.Contact
|
2017-09-12 23:45:13 +03:00
|
|
|
}
|
2020-02-04 22:28:25 +02:00
|
|
|
contact.Cards = contactImport.Cards // Not returned by the server
|
|
|
|
|
|
|
|
// TODO: increment b.total if necessary
|
|
|
|
b.putCache(contact)
|
|
|
|
return formatAddressObjectPath(contact.ID), nil
|
2017-09-12 23:45:13 +03:00
|
|
|
}
|
|
|
|
|
2022-05-24 22:06:25 +03:00
|
|
|
func (b *backend) DeleteAddressObject(ctx context.Context, path string) error {
|
2020-02-04 22:28:25 +02:00
|
|
|
id, err := parseAddressObjectPath(path)
|
2017-09-13 11:11:14 +03:00
|
|
|
if err != nil {
|
2020-02-04 22:28:25 +02:00
|
|
|
return err
|
2017-09-12 23:45:13 +03:00
|
|
|
}
|
2020-02-04 22:28:25 +02:00
|
|
|
resps, err := b.c.DeleteContacts([]string{id})
|
2017-09-12 23:45:13 +03:00
|
|
|
if err != nil {
|
2020-02-04 22:28:25 +02:00
|
|
|
return err
|
2017-09-12 23:45:13 +03:00
|
|
|
}
|
|
|
|
if len(resps) != 1 {
|
2020-02-04 22:28:25 +02:00
|
|
|
return errors.New("hydroxide/carddav: expected exactly one response when deleting contact")
|
2017-09-12 23:45:13 +03:00
|
|
|
}
|
|
|
|
resp := resps[0]
|
2020-02-04 22:28:25 +02:00
|
|
|
// TODO: decrement b.total if necessary
|
|
|
|
b.deleteCache(id)
|
|
|
|
return resp.Err()
|
2017-09-03 21:11:01 +03:00
|
|
|
}
|
|
|
|
|
2020-02-04 22:28:25 +02:00
|
|
|
func (b *backend) receiveEvents(events <-chan *protonmail.Event) {
|
2017-09-09 19:23:14 +03:00
|
|
|
for event := range events {
|
2020-02-04 22:28:25 +02:00
|
|
|
b.locker.Lock()
|
2018-01-11 13:39:32 +02:00
|
|
|
if event.Refresh&protonmail.EventRefreshContacts != 0 {
|
2020-02-04 22:28:25 +02:00
|
|
|
b.cache = make(map[string]*protonmail.Contact)
|
|
|
|
b.total = -1
|
2017-09-09 19:23:14 +03:00
|
|
|
} else if len(event.Contacts) > 0 {
|
|
|
|
for _, eventContact := range event.Contacts {
|
|
|
|
switch eventContact.Action {
|
|
|
|
case protonmail.EventCreate:
|
2020-02-04 22:28:25 +02:00
|
|
|
if b.total >= 0 {
|
|
|
|
b.total++
|
|
|
|
}
|
2017-09-09 19:23:14 +03:00
|
|
|
fallthrough
|
|
|
|
case protonmail.EventUpdate:
|
2020-02-04 22:28:25 +02:00
|
|
|
b.cache[eventContact.ID] = eventContact.Contact
|
2017-09-09 19:23:14 +03:00
|
|
|
case protonmail.EventDelete:
|
2020-02-04 22:28:25 +02:00
|
|
|
delete(b.cache, eventContact.ID)
|
|
|
|
if b.total >= 0 {
|
|
|
|
b.total--
|
|
|
|
}
|
2017-09-09 19:23:14 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-02-04 22:28:25 +02:00
|
|
|
b.locker.Unlock()
|
2017-09-09 19:23:14 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-13 19:53:40 +03:00
|
|
|
func NewHandler(c *protonmail.Client, privateKeys openpgp.EntityList, events <-chan *protonmail.Event) http.Handler {
|
|
|
|
if len(privateKeys) == 0 {
|
|
|
|
panic("hydroxide/carddav: no private key available")
|
|
|
|
}
|
|
|
|
|
2020-02-04 22:28:25 +02:00
|
|
|
b := &backend{
|
2017-09-13 20:03:05 +03:00
|
|
|
c: c,
|
2020-02-04 22:28:25 +02:00
|
|
|
cache: make(map[string]*protonmail.Contact),
|
2017-09-13 20:03:05 +03:00
|
|
|
total: -1,
|
2017-09-13 19:53:40 +03:00
|
|
|
privateKeys: privateKeys,
|
2017-09-09 19:23:14 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if events != nil {
|
2020-02-04 22:28:25 +02:00
|
|
|
go b.receiveEvents(events)
|
2017-09-09 19:23:14 +03:00
|
|
|
}
|
|
|
|
|
2020-02-04 22:28:25 +02:00
|
|
|
return &carddav.Handler{b}
|
2017-09-03 21:11:01 +03:00
|
|
|
}
|