hydroxide-push/carddav/carddav.go

382 lines
8.1 KiB
Go
Raw Normal View History

2017-09-03 21:11:01 +03:00
package carddav
import (
"bytes"
"errors"
"io"
"io/ioutil"
2017-09-03 21:11:01 +03:00
"net/http"
2017-09-04 13:06:36 +03:00
"os"
"strconv"
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/go-vcard"
"github.com/emersion/go-webdav/carddav"
2017-09-13 12:43:12 +03:00
"github.com/emersion/hydroxide/protonmail"
"golang.org/x/crypto/openpgp"
2017-09-03 21:11:01 +03:00
)
2017-09-09 16:37:03 +03:00
type contextKey string
const ClientContextKey = contextKey("client")
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}
)
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 == "" {
email.Group = "item" + strconv.Itoa(i)
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)
}
}
}
var contactImport protonmail.ContactImport
var b bytes.Buffer
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()
}
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-09 17:37:14 +03:00
type addressFileInfo struct {
contact *protonmail.Contact
}
func (fi *addressFileInfo) Name() string {
2017-09-13 21:09:38 +03:00
return fi.contact.ID
2017-09-09 17:37:14 +03:00
}
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 {
2017-09-13 20:03:05 +03:00
ab *addressBook
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
}
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 {
md, err := c.Read(ao.ab.privateKeys)
if err != nil {
return nil, err
2017-09-03 21:11:01 +03:00
}
decoded, err := vcard.NewDecoder(md.UnverifiedBody).Decode()
2017-09-03 21:11:01 +03:00
if err != nil {
return nil, err
}
// The signature can be checked only if md.UnverifiedBody is consumed until
// EOF
if _, err := io.Copy(ioutil.Discard, md.UnverifiedBody); err != nil {
return nil, err
}
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)
}
}
}
return card, nil
}
func (ao *addressObject) SetCard(card vcard.Card) error {
contactImport, err := formatCard(card, ao.ab.privateKeys[0])
if err != nil {
return err
}
contact, err := ao.ab.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-14 12:52:19 +03:00
func (ao *addressObject) Remove() error {
resps, err := ao.ab.c.DeleteContacts([]string{ao.contact.ID})
if err != nil {
return err
}
if len(resps) != 1 {
return errors.New("hydroxide/carddav: expected exactly one response when deleting contact")
}
resp := resps[0]
return resp.Err()
}
2017-09-03 21:11:01 +03:00
type addressBook struct {
2017-09-13 20:03:05 +03:00
c *protonmail.Client
cache map[string]*addressObject
locker sync.Mutex
total int
privateKeys openpgp.EntityList
2017-09-04 12:41:26 +03:00
}
2017-09-09 17:45:05 +03:00
func (ab *addressBook) Info() (*carddav.AddressBookInfo, error) {
return &carddav.AddressBookInfo{
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
}
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-13 20:03:05 +03:00
ab: ab,
2017-09-09 17:37:14 +03:00
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{
2017-09-13 20:03:05 +03:00
ab: ab,
2017-09-09 17:37:14 +03:00
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 {
// 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-13 20:03:05 +03:00
ab: ab,
contact: contact,
}
ab.cacheAddressObject(ao)
return ao, nil
}
func (ab *addressBook) CreateAddressObject(card vcard.Card) (carddav.AddressObject, error) {
contactImport, err := formatCard(card, ab.privateKeys[0])
if err != nil {
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-13 20:03:05 +03:00
ab: ab,
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{
2017-09-13 20:03:05 +03:00
ab: ab,
2017-09-09 19:23:14 +03:00
contact: eventContact.Contact,
}
case protonmail.EventDelete:
delete(ab.cache, eventContact.ID)
ab.total--
}
}
}
ab.locker.Unlock()
}
}
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")
}
2017-09-09 19:23:14 +03:00
ab := &addressBook{
2017-09-13 20:03:05 +03:00
c: c,
cache: make(map[string]*addressObject),
total: -1,
privateKeys: privateKeys,
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
}