2017-08-22 01:04:16 +03:00
|
|
|
// Package protonmail implements a ProtonMail API client.
|
|
|
|
package protonmail
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/json"
|
|
|
|
"io"
|
2017-09-26 19:29:13 +03:00
|
|
|
"io/ioutil"
|
2017-08-22 01:04:16 +03:00
|
|
|
"net/http"
|
|
|
|
"strconv"
|
2017-08-22 11:36:43 +03:00
|
|
|
|
|
|
|
"golang.org/x/crypto/openpgp"
|
2017-08-22 01:04:16 +03:00
|
|
|
)
|
|
|
|
|
2017-08-22 11:16:20 +03:00
|
|
|
const Version = 3
|
2017-08-22 01:04:16 +03:00
|
|
|
|
|
|
|
const headerAPIVersion = "X-Pm-Apiversion"
|
|
|
|
|
|
|
|
type resp struct {
|
|
|
|
Code int
|
|
|
|
*apiError
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *resp) Err() error {
|
|
|
|
if err := r.apiError; err != nil {
|
|
|
|
return r.apiError
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type maybeError interface {
|
|
|
|
Err() error
|
|
|
|
}
|
|
|
|
|
|
|
|
type apiError struct {
|
|
|
|
Message string `json:"Error"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (err apiError) Error() string {
|
|
|
|
return err.Message
|
|
|
|
}
|
|
|
|
|
|
|
|
// Client is a ProtonMail API client.
|
|
|
|
type Client struct {
|
2017-08-22 11:16:20 +03:00
|
|
|
RootURL string
|
2017-08-22 01:04:16 +03:00
|
|
|
AppVersion string
|
2017-08-22 11:16:20 +03:00
|
|
|
|
|
|
|
ClientID string
|
2017-08-22 01:04:16 +03:00
|
|
|
ClientSecret string
|
2017-08-22 11:16:20 +03:00
|
|
|
|
|
|
|
HTTPClient *http.Client
|
2017-09-26 19:29:13 +03:00
|
|
|
ReAuth func() error
|
2017-08-22 11:36:43 +03:00
|
|
|
|
2017-08-22 14:22:27 +03:00
|
|
|
uid string
|
2017-08-22 11:36:43 +03:00
|
|
|
accessToken string
|
2017-08-22 14:22:27 +03:00
|
|
|
keyRing openpgp.EntityList
|
2017-08-22 01:04:16 +03:00
|
|
|
}
|
|
|
|
|
2017-09-26 19:29:13 +03:00
|
|
|
func (c *Client) setRequestAuthorization(req *http.Request) {
|
|
|
|
if c.uid != "" && c.accessToken != "" {
|
|
|
|
req.Header.Set("X-Pm-Uid", c.uid)
|
|
|
|
req.Header.Set("Authorization", "Bearer "+c.accessToken)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-08-22 01:04:16 +03:00
|
|
|
func (c *Client) newRequest(method, path string, body io.Reader) (*http.Request, error) {
|
2017-08-22 11:16:20 +03:00
|
|
|
req, err := http.NewRequest(method, c.RootURL+path, body)
|
2017-08-22 01:04:16 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
req.Header.Set("X-Pm-Appversion", c.AppVersion)
|
|
|
|
req.Header.Set(headerAPIVersion, strconv.Itoa(Version))
|
2017-09-26 19:29:13 +03:00
|
|
|
c.setRequestAuthorization(req)
|
2017-08-22 01:04:16 +03:00
|
|
|
return req, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) newJSONRequest(method, path string, body interface{}) (*http.Request, error) {
|
2017-09-26 19:29:13 +03:00
|
|
|
var buf bytes.Buffer
|
|
|
|
if err := json.NewEncoder(&buf).Encode(body); err != nil {
|
2017-08-22 01:04:16 +03:00
|
|
|
return nil, err
|
|
|
|
}
|
2017-09-26 19:29:13 +03:00
|
|
|
b := buf.Bytes()
|
|
|
|
|
|
|
|
req, err := c.newRequest(method, path, bytes.NewReader(b))
|
2017-08-22 10:41:47 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2017-09-26 19:29:13 +03:00
|
|
|
|
2017-08-22 10:41:47 +03:00
|
|
|
req.Header.Set("Content-Type", "application/json")
|
2017-09-26 19:29:13 +03:00
|
|
|
req.GetBody = func() (io.ReadCloser, error) {
|
|
|
|
return ioutil.NopCloser(bytes.NewReader(b)), nil
|
|
|
|
}
|
2017-08-22 10:41:47 +03:00
|
|
|
return req, nil
|
2017-08-22 01:04:16 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) do(req *http.Request) (*http.Response, error) {
|
|
|
|
httpClient := c.HTTPClient
|
|
|
|
if httpClient == nil {
|
|
|
|
httpClient = http.DefaultClient
|
|
|
|
}
|
2017-09-26 19:29:13 +03:00
|
|
|
|
|
|
|
resp, err := httpClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return resp, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if access token has expired
|
|
|
|
_, hasAuth := req.Header["Authorization"]
|
|
|
|
canRetry := req.Body == nil || req.GetBody != nil
|
|
|
|
if resp.StatusCode == http.StatusUnauthorized && hasAuth && c.ReAuth != nil && canRetry {
|
|
|
|
resp.Body.Close()
|
|
|
|
c.accessToken = ""
|
|
|
|
if err := c.ReAuth(); err != nil {
|
|
|
|
return resp, err
|
|
|
|
}
|
|
|
|
c.setRequestAuthorization(req) // Access token has changed
|
|
|
|
if req.Body != nil {
|
|
|
|
body, err := req.GetBody()
|
|
|
|
if err != nil {
|
|
|
|
return resp, err
|
|
|
|
}
|
|
|
|
req.Body = body
|
|
|
|
}
|
|
|
|
return c.do(req)
|
|
|
|
}
|
|
|
|
|
|
|
|
return resp, nil
|
2017-08-22 01:04:16 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) doJSON(req *http.Request, respData interface{}) error {
|
2017-08-22 10:41:47 +03:00
|
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
|
2017-08-22 12:19:21 +03:00
|
|
|
if respData == nil {
|
|
|
|
respData = new(resp)
|
|
|
|
}
|
|
|
|
|
2017-08-22 01:04:16 +03:00
|
|
|
resp, err := c.do(req)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(respData); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if maybeError, ok := respData.(maybeError); ok {
|
|
|
|
if err := maybeError.Err(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|