// Package protonmail implements a ProtonMail API client. package protonmail import ( "bytes" "encoding/json" "io" "io/ioutil" "net/http" "strconv" "golang.org/x/crypto/openpgp" ) const Version = 3 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 { RootURL string AppVersion string ClientID string ClientSecret string HTTPClient *http.Client ReAuth func() error uid string accessToken string keyRing openpgp.EntityList } 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) } } func (c *Client) newRequest(method, path string, body io.Reader) (*http.Request, error) { req, err := http.NewRequest(method, c.RootURL+path, body) if err != nil { return nil, err } req.Header.Set("X-Pm-Appversion", c.AppVersion) req.Header.Set(headerAPIVersion, strconv.Itoa(Version)) c.setRequestAuthorization(req) return req, nil } func (c *Client) newJSONRequest(method, path string, body interface{}) (*http.Request, error) { var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(body); err != nil { return nil, err } b := buf.Bytes() //log.Printf(">> %v %v\n%v", method, path, string(b)) req, err := c.newRequest(method, path, bytes.NewReader(b)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") req.GetBody = func() (io.ReadCloser, error) { return ioutil.NopCloser(bytes.NewReader(b)), nil } return req, nil } func (c *Client) do(req *http.Request) (*http.Response, error) { httpClient := c.HTTPClient if httpClient == nil { httpClient = http.DefaultClient } 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 } func (c *Client) doJSON(req *http.Request, respData interface{}) error { req.Header.Set("Accept", "application/json") if respData == nil { respData = new(resp) } 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 }