2017-08-22 13:40:27 +03:00
|
|
|
package protonmail
|
|
|
|
|
|
|
|
import (
|
2017-09-13 12:42:19 +03:00
|
|
|
"bytes"
|
|
|
|
"io"
|
2017-08-22 13:40:27 +03:00
|
|
|
"net/http"
|
2017-09-03 21:11:01 +03:00
|
|
|
"net/url"
|
|
|
|
"strconv"
|
2017-09-13 12:42:19 +03:00
|
|
|
"strings"
|
|
|
|
|
|
|
|
"golang.org/x/crypto/openpgp"
|
|
|
|
"golang.org/x/crypto/openpgp/armor"
|
|
|
|
"golang.org/x/crypto/openpgp/packet"
|
2017-08-22 13:40:27 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
type Contact struct {
|
2017-09-13 12:42:19 +03:00
|
|
|
ID string
|
|
|
|
Name string
|
|
|
|
UID string
|
|
|
|
Size int
|
2017-09-09 17:37:14 +03:00
|
|
|
CreateTime int64
|
|
|
|
ModifyTime int64
|
2017-09-13 12:42:19 +03:00
|
|
|
LabelIDs []string
|
2017-08-22 13:40:27 +03:00
|
|
|
|
2017-09-03 21:11:01 +03:00
|
|
|
// Not when using ListContacts
|
|
|
|
ContactEmails []*ContactEmail
|
|
|
|
Cards []*ContactCard
|
2017-08-22 13:40:27 +03:00
|
|
|
}
|
|
|
|
|
2017-09-03 21:11:01 +03:00
|
|
|
type ContactEmailDefaults int
|
|
|
|
|
2017-08-22 13:40:27 +03:00
|
|
|
type ContactEmail struct {
|
2017-08-22 14:22:27 +03:00
|
|
|
ID string
|
|
|
|
Email string
|
2017-09-03 21:11:01 +03:00
|
|
|
Type []string
|
|
|
|
Defaults ContactEmailDefaults
|
2017-08-22 14:22:27 +03:00
|
|
|
Order int
|
2017-08-22 13:40:27 +03:00
|
|
|
ContactID string
|
2017-08-22 14:22:27 +03:00
|
|
|
LabelIDs []string
|
2017-09-03 21:11:01 +03:00
|
|
|
|
|
|
|
// Only when using ListContactsEmails
|
|
|
|
Name string
|
2017-08-22 13:40:27 +03:00
|
|
|
}
|
|
|
|
|
2017-09-03 21:11:01 +03:00
|
|
|
type ContactCardType int
|
2017-08-22 13:40:27 +03:00
|
|
|
|
2017-08-22 14:22:27 +03:00
|
|
|
const (
|
2017-09-03 21:11:01 +03:00
|
|
|
ContactCardCleartext ContactCardType = iota
|
|
|
|
ContactCardEncrypted
|
|
|
|
ContactCardSigned
|
|
|
|
ContactCardEncryptedAndSigned
|
2017-08-22 14:22:27 +03:00
|
|
|
)
|
|
|
|
|
2017-09-03 21:11:01 +03:00
|
|
|
func (t ContactCardType) Signed() bool {
|
|
|
|
switch t {
|
|
|
|
case ContactCardSigned, ContactCardEncryptedAndSigned:
|
|
|
|
return true
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t ContactCardType) Encrypted() bool {
|
|
|
|
switch t {
|
|
|
|
case ContactCardEncrypted, ContactCardEncryptedAndSigned:
|
|
|
|
return true
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type ContactCard struct {
|
|
|
|
Type ContactCardType
|
|
|
|
Data string
|
|
|
|
Signature string
|
|
|
|
}
|
|
|
|
|
2017-09-13 12:42:19 +03:00
|
|
|
func NewEncryptedContactCard(r io.Reader, to []*openpgp.Entity, signer *openpgp.Entity) (*ContactCard, error) {
|
|
|
|
// TODO: sign and encrypt at the same time
|
|
|
|
|
|
|
|
var msg, armored bytes.Buffer
|
|
|
|
if signer != nil {
|
|
|
|
// We'll sign the message later, keep a copy of it
|
|
|
|
r = io.TeeReader(r, &msg)
|
|
|
|
}
|
|
|
|
|
|
|
|
ciphertext, err := armor.Encode(&armored, "PGP MESSAGE", nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
cleartext, err := openpgp.Encrypt(ciphertext, to, nil, nil, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if _, err := io.Copy(cleartext, r); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if err := cleartext.Close(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := ciphertext.Close(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
card := &ContactCard{
|
|
|
|
Type: ContactCardEncrypted,
|
|
|
|
Data: armored.String(),
|
|
|
|
}
|
|
|
|
|
|
|
|
if signer != nil {
|
|
|
|
var sig bytes.Buffer
|
|
|
|
if err := openpgp.ArmoredDetachSignText(&sig, signer, &msg, nil); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
card.Type = ContactCardEncryptedAndSigned
|
|
|
|
card.Signature = sig.String()
|
|
|
|
}
|
|
|
|
|
|
|
|
return card, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewSignedContactCard(r io.Reader, signer *openpgp.Entity) (*ContactCard, error) {
|
|
|
|
var msg, sig bytes.Buffer
|
|
|
|
r = io.TeeReader(r, &msg)
|
|
|
|
if err := openpgp.ArmoredDetachSignText(&sig, signer, r, nil); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &ContactCard{
|
|
|
|
Type: ContactCardSigned,
|
|
|
|
Data: msg.String(),
|
|
|
|
Signature: sig.String(),
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func entityPrimaryKey(e *openpgp.Entity) *openpgp.Key {
|
|
|
|
var selfSig *packet.Signature
|
|
|
|
for _, ident := range e.Identities {
|
|
|
|
if selfSig == nil {
|
|
|
|
selfSig = ident.SelfSignature
|
|
|
|
} else if ident.SelfSignature.IsPrimaryId != nil && *ident.SelfSignature.IsPrimaryId {
|
|
|
|
selfSig = ident.SelfSignature
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return &openpgp.Key{e, e.PrimaryKey, e.PrivateKey, selfSig}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (card *ContactCard) Read(keyring openpgp.KeyRing) (*openpgp.MessageDetails, error) {
|
|
|
|
if !card.Type.Encrypted() {
|
|
|
|
md := &openpgp.MessageDetails{
|
|
|
|
IsEncrypted: false,
|
|
|
|
IsSigned: false,
|
|
|
|
UnverifiedBody: strings.NewReader(card.Data),
|
|
|
|
}
|
|
|
|
|
|
|
|
if !card.Type.Signed() {
|
|
|
|
return md, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
signed := strings.NewReader(card.Data)
|
|
|
|
signature := strings.NewReader(card.Signature)
|
|
|
|
signer, err := openpgp.CheckArmoredDetachedSignature(keyring, signed, signature)
|
|
|
|
md.IsSigned = true
|
|
|
|
md.SignatureError = err
|
|
|
|
if signer != nil {
|
|
|
|
md.SignedByKeyId = signer.PrimaryKey.KeyId
|
|
|
|
md.SignedBy = entityPrimaryKey(signer)
|
|
|
|
}
|
|
|
|
return md, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
ciphertextBlock, err := armor.Decode(strings.NewReader(card.Data))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var r io.Reader = ciphertextBlock.Body
|
|
|
|
|
|
|
|
if card.Type.Signed() {
|
|
|
|
sigBlock, err := armor.Decode(strings.NewReader(card.Signature))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
r = io.MultiReader(r, sigBlock.Body)
|
|
|
|
}
|
|
|
|
|
|
|
|
return openpgp.ReadMessage(r, keyring, nil, nil)
|
|
|
|
}
|
|
|
|
|
2017-09-03 21:11:01 +03:00
|
|
|
type ContactExport struct {
|
|
|
|
ID string
|
|
|
|
Cards []*ContactCard
|
2017-08-22 13:40:27 +03:00
|
|
|
}
|
|
|
|
|
2017-09-12 23:45:13 +03:00
|
|
|
type ContactImport struct {
|
|
|
|
Cards []*ContactCard
|
|
|
|
}
|
|
|
|
|
2017-09-09 17:37:14 +03:00
|
|
|
func (c *Client) ListContacts(page, pageSize int) (total int, contacts []*Contact, err error) {
|
|
|
|
v := url.Values{}
|
|
|
|
v.Set("Page", strconv.Itoa(page))
|
|
|
|
if pageSize > 0 {
|
|
|
|
v.Set("PageSize", strconv.Itoa(pageSize))
|
|
|
|
}
|
|
|
|
|
|
|
|
req, err := c.newRequest(http.MethodGet, "/contacts?"+v.Encode(), nil)
|
2017-08-22 14:22:27 +03:00
|
|
|
if err != nil {
|
2017-09-09 17:37:14 +03:00
|
|
|
return 0, nil, err
|
2017-08-22 14:22:27 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
var respData struct {
|
|
|
|
resp
|
|
|
|
Contacts []*Contact
|
2017-09-09 17:37:14 +03:00
|
|
|
Total int
|
2017-08-22 14:22:27 +03:00
|
|
|
}
|
|
|
|
if err := c.doJSON(req, &respData); err != nil {
|
2017-09-09 17:37:14 +03:00
|
|
|
return 0, nil, err
|
2017-08-22 14:22:27 +03:00
|
|
|
}
|
|
|
|
|
2017-09-09 17:37:14 +03:00
|
|
|
return respData.Total, respData.Contacts, nil
|
2017-08-22 13:40:27 +03:00
|
|
|
}
|
|
|
|
|
2017-09-03 21:11:01 +03:00
|
|
|
func (c *Client) ListContactsEmails(page, pageSize int) (total int, emails []*ContactEmail, err error) {
|
|
|
|
v := url.Values{}
|
|
|
|
v.Set("Page", strconv.Itoa(page))
|
|
|
|
if pageSize > 0 {
|
|
|
|
v.Set("PageSize", strconv.Itoa(pageSize))
|
|
|
|
}
|
|
|
|
|
|
|
|
req, err := c.newRequest(http.MethodGet, "/contacts/emails?"+v.Encode(), nil)
|
2017-08-22 13:40:27 +03:00
|
|
|
if err != nil {
|
2017-09-03 21:11:01 +03:00
|
|
|
return 0, nil, err
|
2017-08-22 13:40:27 +03:00
|
|
|
}
|
|
|
|
|
2017-08-22 14:22:27 +03:00
|
|
|
var respData struct {
|
|
|
|
resp
|
2017-09-03 21:11:01 +03:00
|
|
|
ContactEmails []*ContactEmail
|
|
|
|
Total int
|
2017-08-22 14:22:27 +03:00
|
|
|
}
|
2017-08-22 13:40:27 +03:00
|
|
|
if err := c.doJSON(req, &respData); err != nil {
|
2017-09-03 21:11:01 +03:00
|
|
|
return 0, nil, err
|
2017-08-22 13:40:27 +03:00
|
|
|
}
|
|
|
|
|
2017-09-03 21:11:01 +03:00
|
|
|
return respData.Total, respData.ContactEmails, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) ListContactsExport(page, pageSize int) (total int, contacts []*ContactExport, err error) {
|
|
|
|
v := url.Values{}
|
|
|
|
v.Set("Page", strconv.Itoa(page))
|
|
|
|
if pageSize > 0 {
|
|
|
|
v.Set("PageSize", strconv.Itoa(pageSize))
|
|
|
|
}
|
|
|
|
|
|
|
|
req, err := c.newRequest(http.MethodGet, "/contacts/export?"+v.Encode(), nil)
|
|
|
|
if err != nil {
|
|
|
|
return 0, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var respData struct {
|
|
|
|
resp
|
|
|
|
Contacts []*ContactExport
|
|
|
|
Total int
|
|
|
|
}
|
|
|
|
if err := c.doJSON(req, &respData); err != nil {
|
|
|
|
return 0, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return respData.Total, respData.Contacts, nil
|
2017-08-22 13:40:27 +03:00
|
|
|
}
|
2017-08-22 14:22:27 +03:00
|
|
|
|
|
|
|
func (c *Client) GetContact(id string) (*Contact, error) {
|
|
|
|
req, err := c.newRequest(http.MethodGet, "/contacts/"+id, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var respData struct {
|
|
|
|
resp
|
|
|
|
Contact *Contact
|
|
|
|
}
|
|
|
|
if err := c.doJSON(req, &respData); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return respData.Contact, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type CreateContactResp struct {
|
2017-09-13 12:42:19 +03:00
|
|
|
Index int
|
2017-08-22 14:22:27 +03:00
|
|
|
Response struct {
|
|
|
|
resp
|
|
|
|
Contact *Contact
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-12 23:20:25 +03:00
|
|
|
func (resp *CreateContactResp) Err() error {
|
|
|
|
return resp.Response.Err()
|
|
|
|
}
|
|
|
|
|
2017-09-12 23:45:13 +03:00
|
|
|
func (c *Client) CreateContacts(contacts []*ContactImport) ([]*CreateContactResp, error) {
|
2017-08-22 14:22:27 +03:00
|
|
|
reqData := struct {
|
2017-09-13 12:42:19 +03:00
|
|
|
Contacts []*ContactImport
|
2017-09-12 23:45:13 +03:00
|
|
|
Overwrite, Groups, Labels int
|
|
|
|
}{contacts, 0, 0, 0}
|
2017-08-22 14:22:27 +03:00
|
|
|
req, err := c.newJSONRequest(http.MethodPost, "/contacts", &reqData)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var respData struct {
|
|
|
|
resp
|
|
|
|
Responses []*CreateContactResp
|
|
|
|
}
|
|
|
|
if err := c.doJSON(req, &respData); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return respData.Responses, nil
|
|
|
|
}
|
2017-09-12 23:20:25 +03:00
|
|
|
|
2017-09-13 11:11:14 +03:00
|
|
|
func (c *Client) UpdateContact(id string, contact *ContactImport) (*Contact, error) {
|
|
|
|
req, err := c.newJSONRequest(http.MethodPut, "/contacts/"+id, contact)
|
2017-09-12 23:20:25 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var respData struct {
|
|
|
|
resp
|
|
|
|
Contact *Contact
|
|
|
|
}
|
|
|
|
if err := c.doJSON(req, &respData); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return respData.Contact, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type DeleteContactResp struct {
|
2017-09-13 12:42:19 +03:00
|
|
|
ID string
|
2017-09-12 23:20:25 +03:00
|
|
|
Response struct {
|
|
|
|
resp
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (resp *DeleteContactResp) Err() error {
|
|
|
|
return resp.Response.Err()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) DeleteContacts(ids []string) ([]*DeleteContactResp, error) {
|
|
|
|
reqData := struct {
|
|
|
|
IDs []string
|
|
|
|
}{ids}
|
|
|
|
req, err := c.newJSONRequest(http.MethodPut, "/contacts/delete", &reqData)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var respData struct {
|
|
|
|
resp
|
|
|
|
Responses []*DeleteContactResp
|
|
|
|
}
|
|
|
|
if err := c.doJSON(req, &respData); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return respData.Responses, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) DeleteAllContacts() error {
|
|
|
|
req, err := c.newRequest(http.MethodDelete, "/contacts", nil)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
var respData resp
|
|
|
|
if err := c.doJSON(req, &respData); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|