pax_global_header 0000666 0000000 0000000 00000000064 14476422733 0014526 g ustar 00root root 0000000 0000000 52 comment=e563407504ce5a1c63fe732e550ae0de75266967
go-proton-api-1.0.0/ 0000775 0000000 0000000 00000000000 14476422733 0014217 5 ustar 00root root 0000000 0000000 go-proton-api-1.0.0/.github/ 0000775 0000000 0000000 00000000000 14476422733 0015557 5 ustar 00root root 0000000 0000000 go-proton-api-1.0.0/.github/workflows/ 0000775 0000000 0000000 00000000000 14476422733 0017614 5 ustar 00root root 0000000 0000000 go-proton-api-1.0.0/.github/workflows/check.yml 0000664 0000000 0000000 00000001062 14476422733 0021413 0 ustar 00root root 0000000 0000000 name: Lint and Test
on: push
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Get sources
uses: actions/checkout@v3
- name: Set up Go 1.18
uses: actions/setup-go@v3
with:
go-version: '1.18'
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.50.0
args: --timeout=180s
skip-cache: true
- name: Run tests
run: go test -v ./...
- name: Run tests with race check
run: go test -v -race ./...
go-proton-api-1.0.0/.gitignore 0000664 0000000 0000000 00000000047 14476422733 0016210 0 ustar 00root root 0000000 0000000 # Editor files
.*.sw?
*~
.idea
.vscode
go-proton-api-1.0.0/CONTRIBUTING.md 0000664 0000000 0000000 00000000753 14476422733 0016455 0 ustar 00root root 0000000 0000000 # Contribution Policy
By making a contribution to this project:
1. I assign any and all copyright related to the contribution to Proton AG;
2. I certify that the contribution was created in whole by me;
3. I understand and agree that this project and the contribution are public
and that a record of the contribution (including all personal information I
submit with it) is maintained indefinitely and may be redistributed with
this project or the open source license(s) involved. go-proton-api-1.0.0/COPYING_NOTES.md 0000664 0000000 0000000 00000022052 14476422733 0016622 0 ustar 00root root 0000000 0000000 # Copying
The MIT License (MIT)
Copyright (c) 2020 James Houlahan
Copyright (c) 2022 Proton AG
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# Dependencies
Go Proton API includes the following 3rd party software:
* [The Go Project libraries](https://golang.org/project/) | Available under [BSD license](https://golang.org/LICENSE)
* [semver](https://github.com/Masterminds/semver/v3) available under [license](https://github.com/Masterminds/semver/v3/blob/master/LICENSE)
* [gluon](https://github.com/ProtonMail/gluon) available under [license](https://github.com/ProtonMail/gluon/blob/master/LICENSE)
* [go-crypto](https://github.com/ProtonMail/go-crypto) available under [license](https://github.com/ProtonMail/go-crypto/blob/master/LICENSE)
* [go-srp](https://github.com/ProtonMail/go-srp) available under [license](https://github.com/ProtonMail/go-srp/blob/master/LICENSE)
* [gopenpgp](https://github.com/ProtonMail/gopenpgp/v2) available under [license](https://github.com/ProtonMail/gopenpgp/v2/blob/master/LICENSE)
* [goquery](https://github.com/PuerkitoBio/goquery) available under [license](https://github.com/PuerkitoBio/goquery/blob/master/LICENSE)
* [juniper](https://github.com/bradenaw/juniper) available under [license](https://github.com/bradenaw/juniper/blob/master/LICENSE)
* [go-message](https://github.com/emersion/go-message) available under [license](https://github.com/emersion/go-message/blob/master/LICENSE)
* [go-vcard](https://github.com/emersion/go-vcard) available under [license](https://github.com/emersion/go-vcard/blob/master/LICENSE)
* [gin](https://github.com/gin-gonic/gin) available under [license](https://github.com/gin-gonic/gin/blob/master/LICENSE)
* [resty](https://github.com/go-resty/resty/v2) available under [license](https://github.com/go-resty/resty/v2/blob/master/LICENSE)
* [uuid](https://github.com/google/uuid) available under [license](https://github.com/google/uuid/blob/master/LICENSE)
* [logrus](https://github.com/sirupsen/logrus) available under [license](https://github.com/sirupsen/logrus/blob/master/LICENSE)
* [testify](https://github.com/stretchr/testify) available under [license](https://github.com/stretchr/testify/blob/master/LICENSE)
* [cli](https://github.com/urfave/cli/v2) available under [license](https://github.com/urfave/cli/v2/blob/master/LICENSE)
* [goleak](https://go.uber.org/goleak) available under [license](https://pkg.go.dev/go.uber.org/goleak?tab=licenses)
* [exp](https://golang.org/x/exp) available under [license](https://cs.opensource.google/go/x/exp/+/master:LICENSE)
* [net](https://golang.org/x/net) available under [license](https://cs.opensource.google/go/x/net/+/master:LICENSE)
* [text](https://golang.org/x/text) available under [license](https://cs.opensource.google/go/x/text/+/master:LICENSE)
* [grpc](https://google.golang.org/grpc) available under [license](https://github.com/grpc/grpc-go/blob/master/LICENSE)
* [protobuf](https://google.golang.org/protobuf) available under [license](https://github.com/protocolbuffers/protobuf/blob/main/LICENSE)
* [bcrypt](https://github.com/ProtonMail/bcrypt) available under [license](https://github.com/ProtonMail/bcrypt/blob/master/LICENSE)
* [go-mime](https://github.com/ProtonMail/go-mime) available under [license](https://github.com/ProtonMail/go-mime/blob/master/LICENSE)
* [cascadia](https://github.com/andybalholm/cascadia) available under [license](https://github.com/andybalholm/cascadia/blob/master/LICENSE)
* [sonic](https://github.com/bytedance/sonic) available under [license](https://github.com/bytedance/sonic/blob/master/LICENSE)
* [base64x](https://github.com/chenzhuoyu/base64x) available under [license](https://github.com/chenzhuoyu/base64x/blob/master/LICENSE)
* [circl](https://github.com/cloudflare/circl) available under [license](https://github.com/cloudflare/circl/blob/master/LICENSE)
* [go-md2man](https://github.com/cpuguy83/go-md2man/v2) available under [license](https://github.com/cpuguy83/go-md2man/v2/blob/master/LICENSE)
* [saferith](https://github.com/cronokirby/saferith) available under [license](https://github.com/cronokirby/saferith/blob/master/LICENSE)
* [go-spew](https://github.com/davecgh/go-spew) available under [license](https://github.com/davecgh/go-spew/blob/master/LICENSE)
* [go-textwrapper](https://github.com/emersion/go-textwrapper) available under [license](https://github.com/emersion/go-textwrapper/blob/master/LICENSE)
* [mimetype](https://github.com/gabriel-vasile/mimetype) available under [license](https://github.com/gabriel-vasile/mimetype/blob/master/LICENSE)
* [sse](https://github.com/gin-contrib/sse) available under [license](https://github.com/gin-contrib/sse/blob/master/LICENSE)
* [locales](https://github.com/go-playground/locales) available under [license](https://github.com/go-playground/locales/blob/master/LICENSE)
* [universal-translator](https://github.com/go-playground/universal-translator) available under [license](https://github.com/go-playground/universal-translator/blob/master/LICENSE)
* [validator](https://github.com/go-playground/validator/v10) available under [license](https://github.com/go-playground/validator/v10/blob/master/LICENSE)
* [go-json](https://github.com/goccy/go-json) available under [license](https://github.com/goccy/go-json/blob/master/LICENSE)
* [protobuf](https://github.com/golang/protobuf) available under [license](https://github.com/golang/protobuf/blob/master/LICENSE)
* [go](https://github.com/json-iterator/go) available under [license](https://github.com/json-iterator/go/blob/master/LICENSE)
* [cpuid](https://github.com/klauspost/cpuid/v2) available under [license](https://github.com/klauspost/cpuid/v2/blob/master/LICENSE)
* [text](https://github.com/kr/text) available under [license](https://github.com/kr/text/blob/master/LICENSE)
* [go-urn](https://github.com/leodido/go-urn) available under [license](https://github.com/leodido/go-urn/blob/master/LICENSE)
* [go-isatty](https://github.com/mattn/go-isatty) available under [license](https://github.com/mattn/go-isatty/blob/master/LICENSE)
* [concurrent](https://github.com/modern-go/concurrent) available under [license](https://github.com/modern-go/concurrent/blob/master/LICENSE)
* [reflect2](https://github.com/modern-go/reflect2) available under [license](https://github.com/modern-go/reflect2/blob/master/LICENSE)
* [go-toml](https://github.com/pelletier/go-toml/v2) available under [license](https://github.com/pelletier/go-toml/v2/blob/master/LICENSE)
* [errors](https://github.com/pkg/errors) available under [license](https://github.com/pkg/errors/blob/master/LICENSE)
* [go-difflib](https://github.com/pmezard/go-difflib) available under [license](https://github.com/pmezard/go-difflib/blob/master/LICENSE)
* [go-internal](https://github.com/rogpeppe/go-internal) available under [license](https://github.com/rogpeppe/go-internal/blob/master/LICENSE)
* [blackfriday](https://github.com/russross/blackfriday/v2) available under [license](https://github.com/russross/blackfriday/v2/blob/master/LICENSE)
* [golang-asm](https://github.com/twitchyliquid64/golang-asm) available under [license](https://github.com/twitchyliquid64/golang-asm/blob/master/LICENSE)
* [codec](https://github.com/ugorji/go/codec) available under [license](https://github.com/ugorji/go/codec/blob/master/LICENSE)
* [smetrics](https://github.com/xrash/smetrics) available under [license](https://github.com/xrash/smetrics/blob/master/LICENSE)
* [arch](https://golang.org/x/arch) available under [license](https://cs.opensource.google/go/x/arch/+/master:LICENSE)
* [crypto](https://golang.org/x/crypto) available under [license](https://cs.opensource.google/go/x/crypto/+/master:LICENSE)
* [sync](https://golang.org/x/sync) available under [license](https://cs.opensource.google/go/x/sync/+/master:LICENSE)
* [sys](https://golang.org/x/sys) available under [license](https://cs.opensource.google/go/x/sys/+/master:LICENSE)
* [genproto](https://google.golang.org/genproto) available under [license](https://pkg.go.dev/google.golang.org/genproto?tab=licenses)
* [yaml](https://gopkg.in/yaml.v3) available under [license](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE)
go-proton-api-1.0.0/LICENSE 0000664 0000000 0000000 00000002126 14476422733 0015225 0 ustar 00root root 0000000 0000000 The MIT License (MIT)
Copyright (c) 2020 James Houlahan
Copyright (c) 2022 Proton AG
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
go-proton-api-1.0.0/README.md 0000664 0000000 0000000 00000002773 14476422733 0015507 0 ustar 00root root 0000000 0000000 # Go Proton API
This repository holds Go Proton API, a Go library implementing a client and development server for (a subset of) the Proton REST API.
The license can be found in the [LICENSE](./LICENSE) file.
For the contribution policy, see [CONTRIBUTING](./CONTRIBUTING.md).
## Environment variables
Most of the integration tests run locally. The ones that interact with Proton servers require the following environment variables set:
- ```GO_PROTON_API_TEST_USERNAME```
- ```GO_PROTON_API_TEST_PASSWORD```
## Contribution
This library is forked from [go-proton-api](https://github.com/ProtonMail/go-proton-api) in order to support the [Proton API Bridge](https://github.com/henrybear327/Proton-API-Bridge) project.
Contribution is welcomed!
The intention to upstream the changes are planned, once the changes to the codebase has stabalized.
go-proton-api-1.0.0/address.go 0000664 0000000 0000000 00000003227 14476422733 0016177 0 ustar 00root root 0000000 0000000 package proton
import (
"context"
"github.com/go-resty/resty/v2"
"golang.org/x/exp/slices"
)
func (c *Client) GetAddresses(ctx context.Context) ([]Address, error) {
var res struct {
Addresses []Address
}
if err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetResult(&res).Get("/core/v4/addresses")
}); err != nil {
return nil, err
}
slices.SortFunc(res.Addresses, func(a, b Address) int {
return a.Order - b.Order
})
return res.Addresses, nil
}
func (c *Client) GetAddress(ctx context.Context, addressID string) (Address, error) {
var res struct {
Address Address
}
if err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetResult(&res).Get("/core/v4/addresses/" + addressID)
}); err != nil {
return Address{}, err
}
return res.Address, nil
}
func (c *Client) OrderAddresses(ctx context.Context, req OrderAddressesReq) error {
return c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetBody(req).Put("/core/v4/addresses/order")
})
}
func (c *Client) EnableAddress(ctx context.Context, addressID string) error {
return c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.Put("/core/v4/addresses/" + addressID + "/enable")
})
}
func (c *Client) DisableAddress(ctx context.Context, addressID string) error {
return c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.Put("/core/v4/addresses/" + addressID + "/disable")
})
}
func (c *Client) DeleteAddress(ctx context.Context, addressID string) error {
return c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.Delete("/core/v4/addresses/" + addressID)
})
}
go-proton-api-1.0.0/address_test.go 0000664 0000000 0000000 00000004202 14476422733 0017230 0 ustar 00root root 0000000 0000000 package proton_test
import (
"context"
"testing"
"github.com/henrybear327/go-proton-api"
"github.com/henrybear327/go-proton-api/server"
"github.com/stretchr/testify/require"
)
func TestAddress_Types(t *testing.T) {
s := server.New()
defer s.Close()
// Create a user on the server.
userID, _, err := s.CreateUser("user", []byte("pass"))
require.NoError(t, err)
id2, err := s.CreateAddress(userID, "user@alias.com", []byte("pass"))
require.NoError(t, err)
require.NoError(t, s.ChangeAddressType(userID, id2, proton.AddressTypeAlias))
id3, err := s.CreateAddress(userID, "user@custom.com", []byte("pass"))
require.NoError(t, err)
require.NoError(t, s.ChangeAddressType(userID, id3, proton.AddressTypeCustom))
id4, err := s.CreateAddress(userID, "user@premium.com", []byte("pass"))
require.NoError(t, err)
require.NoError(t, s.ChangeAddressType(userID, id4, proton.AddressTypePremium))
id5, err := s.CreateAddress(userID, "user@external.com", []byte("pass"))
require.NoError(t, err)
require.NoError(t, s.ChangeAddressType(userID, id5, proton.AddressTypeExternal))
m := proton.New(
proton.WithHostURL(s.GetHostURL()),
proton.WithTransport(proton.InsecureTransport()),
)
defer m.Close()
// Create one session for the user.
c, auth, err := m.NewClientWithLogin(context.Background(), "user", []byte("pass"))
require.NoError(t, err)
require.Equal(t, userID, auth.UserID)
// Get addresses for the user.
addrs, err := c.GetAddresses(context.Background())
require.NoError(t, err)
for _, addr := range addrs {
switch addr.ID {
case id2:
require.Equal(t, addr.Email, "user@alias.com")
require.Equal(t, addr.Type, proton.AddressTypeAlias)
case id3:
require.Equal(t, addr.Email, "user@custom.com")
require.Equal(t, addr.Type, proton.AddressTypeCustom)
case id4:
require.Equal(t, addr.Email, "user@premium.com")
require.Equal(t, addr.Type, proton.AddressTypePremium)
case id5:
require.Equal(t, addr.Email, "user@external.com")
require.Equal(t, addr.Type, proton.AddressTypeExternal)
default:
require.Equal(t, addr.Email, "user@proton.local")
require.Equal(t, addr.Type, proton.AddressTypeOriginal)
}
}
}
go-proton-api-1.0.0/address_types.go 0000664 0000000 0000000 00000001016 14476422733 0017415 0 ustar 00root root 0000000 0000000 package proton
type Address struct {
ID string
Email string
Send Bool
Receive Bool
Status AddressStatus
Type AddressType
Order int
DisplayName string
Keys Keys
}
type OrderAddressesReq struct {
AddressIDs []string
}
type AddressStatus int
const (
AddressStatusDisabled AddressStatus = iota
AddressStatusEnabled
AddressStatusDeleting
)
type AddressType int
const (
AddressTypeOriginal AddressType = iota + 1
AddressTypeAlias
AddressTypeCustom
AddressTypePremium
AddressTypeExternal
)
go-proton-api-1.0.0/attachment.go 0000664 0000000 0000000 00000005233 14476422733 0016701 0 ustar 00root root 0000000 0000000 package proton
import (
"bytes"
"context"
"fmt"
"io"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/go-resty/resty/v2"
)
func (c *Client) GetAttachment(ctx context.Context, attachmentID string) ([]byte, error) {
var buffer bytes.Buffer
if err := c.getAttachment(ctx, attachmentID, &buffer); err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
func (c *Client) GetAttachmentInto(ctx context.Context, attachmentID string, reader io.ReaderFrom) error {
return c.getAttachment(ctx, attachmentID, reader)
}
func (c *Client) UploadAttachment(ctx context.Context, addrKR *crypto.KeyRing, req CreateAttachmentReq) (Attachment, error) {
var res struct {
Attachment Attachment
}
kr, err := addrKR.FirstKey()
if err != nil {
return res.Attachment, fmt.Errorf("failed to get first key: %w", err)
}
sig, err := kr.SignDetached(crypto.NewPlainMessage(req.Body))
if err != nil {
return Attachment{}, fmt.Errorf("failed to sign attachment: %w", err)
}
enc, err := kr.EncryptAttachment(crypto.NewPlainMessage(req.Body), req.Filename)
if err != nil {
return Attachment{}, fmt.Errorf("failed to encrypt attachment: %w", err)
}
if err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetResult(&res).
SetMultipartFormData(map[string]string{
"MessageID": req.MessageID,
"Filename": req.Filename,
"MIMEType": string(req.MIMEType),
"Disposition": string(req.Disposition),
"ContentID": req.ContentID,
}).
SetMultipartFields(
&resty.MultipartField{
Param: "KeyPackets",
FileName: "blob",
ContentType: "application/octet-stream",
Reader: bytes.NewReader(enc.KeyPacket),
},
&resty.MultipartField{
Param: "DataPacket",
FileName: "blob",
ContentType: "application/octet-stream",
Reader: bytes.NewReader(enc.DataPacket),
},
&resty.MultipartField{
Param: "Signature",
FileName: "blob",
ContentType: "application/octet-stream",
Reader: bytes.NewReader(sig.GetBinary()),
},
).
Post("/mail/v4/attachments")
}); err != nil {
return Attachment{}, err
}
return res.Attachment, nil
}
func (c *Client) getAttachment(ctx context.Context, attachmentID string, reader io.ReaderFrom) error {
res, err := c.doRes(ctx, func(req *resty.Request) (*resty.Response, error) {
res, err := req.SetDoNotParseResponse(true).Get("/mail/v4/attachments/" + attachmentID)
return parseResponse(res, err)
})
if err != nil {
return fmt.Errorf("failed to request attachment: %w", err)
}
defer res.RawBody().Close()
if _, err = reader.ReadFrom(res.RawBody()); err != nil {
return err
}
return nil
}
go-proton-api-1.0.0/attachment_interfaces.go 0000664 0000000 0000000 00000005113 14476422733 0021101 0 ustar 00root root 0000000 0000000 package proton
import (
"bytes"
"context"
"github.com/ProtonMail/gluon/async"
"github.com/bradenaw/juniper/parallel"
)
// AttachmentAllocator abstract the attachment download buffer creation.
type AttachmentAllocator interface {
// NewBuffer should return a new byte buffer for use. Note that this function may be called from multiple go-routines.
NewBuffer() *bytes.Buffer
}
type DefaultAttachmentAllocator struct{}
func NewDefaultAttachmentAllocator() *DefaultAttachmentAllocator {
return &DefaultAttachmentAllocator{}
}
func (DefaultAttachmentAllocator) NewBuffer() *bytes.Buffer {
return bytes.NewBuffer(nil)
}
// Scheduler allows the user to specify how the attachment data for the message should be downloaded.
type Scheduler interface {
Schedule(ctx context.Context, attachmentIDs []string, storageProvider AttachmentAllocator, downloader func(context.Context, string, *bytes.Buffer) error) ([]*bytes.Buffer, error)
}
// SequentialScheduler downloads the attachments one by one.
type SequentialScheduler struct{}
func NewSequentialScheduler() *SequentialScheduler {
return &SequentialScheduler{}
}
func (SequentialScheduler) Schedule(ctx context.Context, attachmentIDs []string, storageProvider AttachmentAllocator, downloader func(context.Context, string, *bytes.Buffer) error) ([]*bytes.Buffer, error) {
result := make([]*bytes.Buffer, len(attachmentIDs))
for i, v := range attachmentIDs {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
buffer := storageProvider.NewBuffer()
if err := downloader(ctx, v, buffer); err != nil {
return nil, err
}
result[i] = buffer
}
return result, nil
}
type ParallelScheduler struct {
workers int
panicHandler async.PanicHandler
}
func NewParallelScheduler(workers int, panicHandler async.PanicHandler) *ParallelScheduler {
if workers == 0 {
workers = 1
}
return &ParallelScheduler{workers: workers}
}
func (p ParallelScheduler) Schedule(ctx context.Context, attachmentIDs []string, storageProvider AttachmentAllocator, downloader func(context.Context, string, *bytes.Buffer) error) ([]*bytes.Buffer, error) {
// If we have less attachments than the maximum works, reduce worker count to match attachment count.
workers := p.workers
if len(attachmentIDs) < workers {
workers = len(attachmentIDs)
}
return parallel.MapContext(ctx, workers, attachmentIDs, func(ctx context.Context, id string) (*bytes.Buffer, error) {
defer async.HandlePanic(p.panicHandler)
buffer := storageProvider.NewBuffer()
if err := downloader(ctx, id, buffer); err != nil {
return nil, err
}
return buffer, nil
})
}
go-proton-api-1.0.0/attachment_test.go 0000664 0000000 0000000 00000003442 14476422733 0017740 0 ustar 00root root 0000000 0000000 package proton_test
import (
"context"
"errors"
"net/http"
"sync"
"testing"
"github.com/henrybear327/go-proton-api"
"github.com/henrybear327/go-proton-api/server"
"github.com/stretchr/testify/require"
)
func TestAttachment_429Response(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
s := server.New()
defer s.Close()
m := proton.New(
proton.WithHostURL(s.GetHostURL()),
proton.WithTransport(proton.InsecureTransport()),
)
_, _, err := s.CreateUser("user", []byte("pass"))
require.NoError(t, err)
c, _, err := m.NewClientWithLogin(ctx, "user", []byte("pass"))
require.NoError(t, err)
s.AddStatusHook(func(r *http.Request) (int, bool) {
return http.StatusTooManyRequests, true
})
_, err = c.GetAttachment(ctx, "someID")
require.Error(t, err)
apiErr := new(proton.APIError)
require.True(t, errors.As(err, &apiErr), "expected to be API error")
require.Equal(t, 429, apiErr.Status)
require.Equal(t, proton.InvalidValue, apiErr.Code)
require.Equal(t, "Request failed with status 429", apiErr.Message)
}
func TestAttachment_ContextCancelled(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
s := server.New()
defer s.Close()
m := proton.New(
proton.WithHostURL(s.GetHostURL()),
proton.WithTransport(proton.InsecureTransport()),
)
_, _, err := s.CreateUser("user", []byte("pass"))
require.NoError(t, err)
c, _, err := m.NewClientWithLogin(ctx, "user", []byte("pass"))
require.NoError(t, err)
wg := sync.WaitGroup{}
wg.Add(1)
s.AddStatusHook(func(r *http.Request) (int, bool) {
wg.Wait()
return http.StatusTooManyRequests, true
})
go func() {
_, err = c.GetAttachment(ctx, "someID")
wg.Done()
}()
cancel()
wg.Wait()
require.Error(t, err)
require.True(t, errors.Is(err, context.Canceled))
}
go-proton-api-1.0.0/attachment_types.go 0000664 0000000 0000000 00000001052 14476422733 0020120 0 ustar 00root root 0000000 0000000 package proton
import (
"github.com/ProtonMail/gluon/rfc822"
)
type Attachment struct {
ID string
Name string
Size int64
MIMEType rfc822.MIMEType
Disposition Disposition
Headers Headers
KeyPackets string
Signature string
}
type Disposition string
const (
InlineDisposition Disposition = "inline"
AttachmentDisposition Disposition = "attachment"
)
type CreateAttachmentReq struct {
MessageID string
Filename string
MIMEType rfc822.MIMEType
Disposition Disposition
ContentID string
Body []byte
}
go-proton-api-1.0.0/auth.go 0000664 0000000 0000000 00000002126 14476422733 0015510 0 ustar 00root root 0000000 0000000 package proton
import (
"context"
"github.com/go-resty/resty/v2"
)
func (c *Client) Auth2FA(ctx context.Context, req Auth2FAReq) error {
return c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetBody(req).Post("/auth/v4/2fa")
})
}
func (c *Client) AuthDelete(ctx context.Context) error {
return c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.Delete("/auth/v4")
})
}
func (c *Client) AuthSessions(ctx context.Context) ([]AuthSession, error) {
var res struct {
Sessions []AuthSession
}
if err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetResult(&res).Get("/auth/v4/sessions")
}); err != nil {
return nil, err
}
return res.Sessions, nil
}
func (c *Client) AuthRevoke(ctx context.Context, authUID string) error {
return c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.Delete("/auth/v4/sessions/" + authUID)
})
}
func (c *Client) AuthRevokeAll(ctx context.Context) error {
return c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.Delete("/auth/v4/sessions")
})
}
go-proton-api-1.0.0/auth_test.go 0000664 0000000 0000000 00000011437 14476422733 0016554 0 ustar 00root root 0000000 0000000 package proton_test
import (
"context"
"runtime"
"testing"
"time"
"github.com/bradenaw/juniper/parallel"
"github.com/henrybear327/go-proton-api"
"github.com/henrybear327/go-proton-api/server"
"github.com/stretchr/testify/require"
)
func TestAuth(t *testing.T) {
s := server.New()
defer s.Close()
_, _, err := s.CreateUser("user", []byte("pass"))
require.NoError(t, err)
m := proton.New(
proton.WithHostURL(s.GetHostURL()),
proton.WithTransport(proton.InsecureTransport()),
)
defer m.Close()
// Create one session.
c1, auth1, err := m.NewClientWithLogin(context.Background(), "user", []byte("pass"))
require.NoError(t, err)
// Revoke all other sessions.
require.NoError(t, c1.AuthRevokeAll(context.Background()))
// Create another session.
c2, _, err := m.NewClientWithLogin(context.Background(), "user", []byte("pass"))
require.NoError(t, err)
// There should be two sessions.
sessions, err := c1.AuthSessions(context.Background())
require.NoError(t, err)
require.Len(t, sessions, 2)
// Revoke the first session.
require.NoError(t, c2.AuthRevoke(context.Background(), auth1.UID))
// The first session should no longer work.
require.Error(t, c1.AuthDelete(context.Background()))
// There should be one session remaining.
remaining, err := c2.AuthSessions(context.Background())
require.NoError(t, err)
require.Len(t, remaining, 1)
// Delete the last session.
require.NoError(t, c2.AuthDelete(context.Background()))
}
func TestAuth_Refresh(t *testing.T) {
s := server.New()
defer s.Close()
// Create a user on the server.
userID, _, err := s.CreateUser("user", []byte("pass"))
require.NoError(t, err)
// The auth is valid for 4 seconds.
s.SetAuthLife(4 * time.Second)
m := proton.New(
proton.WithHostURL(s.GetHostURL()),
proton.WithTransport(proton.InsecureTransport()),
)
defer m.Close()
// Create one session for the user.
c, auth, err := m.NewClientWithLogin(context.Background(), "user", []byte("pass"))
require.NoError(t, err)
require.Equal(t, userID, auth.UserID)
// Wait for 2 seconds.
time.Sleep(2 * time.Second)
// The client should still be authenticated.
{
user, err := c.GetUser(context.Background())
require.NoError(t, err)
require.Equal(t, "user", user.Name)
require.Equal(t, userID, user.ID)
}
// Wait for 2 more seconds.
time.Sleep(2 * time.Second)
// The client's auth token should have expired, but will be refreshed on the next request.
{
user, err := c.GetUser(context.Background())
require.NoError(t, err)
require.Equal(t, "user", user.Name)
require.Equal(t, userID, user.ID)
}
}
func TestAuth_Refresh_Multi(t *testing.T) {
s := server.New()
defer s.Close()
// Create a user on the server.
userID, _, err := s.CreateUser("user", []byte("pass"))
require.NoError(t, err)
// The auth is valid for 4 seconds.
s.SetAuthLife(4 * time.Second)
m := proton.New(
proton.WithHostURL(s.GetHostURL()),
proton.WithTransport(proton.InsecureTransport()),
)
defer m.Close()
c, auth, err := m.NewClientWithLogin(context.Background(), "user", []byte("pass"))
require.NoError(t, err)
require.Equal(t, userID, auth.UserID)
time.Sleep(2 * time.Second)
// The client should still be authenticated.
parallel.Do(runtime.NumCPU(), 100, func(idx int) {
user, err := c.GetUser(context.Background())
require.NoError(t, err)
require.Equal(t, "user", user.Name)
require.Equal(t, userID, user.ID)
})
// Wait for the auth to expire.
time.Sleep(2 * time.Second)
// Client auth token should have expired, but will be refreshed on the next request.
parallel.Do(runtime.NumCPU(), 100, func(idx int) {
user, err := c.GetUser(context.Background())
require.NoError(t, err)
require.Equal(t, "user", user.Name)
require.Equal(t, userID, user.ID)
})
}
func TestAuth_Refresh_Deauth(t *testing.T) {
s := server.New()
defer s.Close()
// Create a user on the server.
userID, _, err := s.CreateUser("user", []byte("pass"))
require.NoError(t, err)
m := proton.New(
proton.WithHostURL(s.GetHostURL()),
proton.WithTransport(proton.InsecureTransport()),
)
defer m.Close()
// Create one session for the user.
c, auth, err := m.NewClientWithLogin(context.Background(), "user", []byte("pass"))
require.NoError(t, err)
require.Equal(t, userID, auth.UserID)
deauth := false
c.AddDeauthHandler(func() {
deauth = true
})
// The client should still be authenticated.
{
user, err := c.GetUser(context.Background())
require.NoError(t, err)
require.Equal(t, "user", user.Name)
require.Equal(t, userID, user.ID)
}
require.NoError(t, s.RevokeUser(userID))
// The client's auth token should have expired, and should not be refreshed
{
_, err := c.GetUser(context.Background())
require.Error(t, err)
}
// The client shuold call de-auth handlers.
require.Eventually(t, func() bool { return deauth }, time.Second, 300*time.Millisecond)
}
go-proton-api-1.0.0/block.go 0000664 0000000 0000000 00000002104 14476422733 0015635 0 ustar 00root root 0000000 0000000 package proton
import (
"context"
"io"
"github.com/go-resty/resty/v2"
)
func (c *Client) GetBlock(ctx context.Context, bareURL, token string) (io.ReadCloser, error) {
res, err := c.doRes(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetHeader("pm-storage-token", token).SetDoNotParseResponse(true).Get(bareURL)
})
if err != nil {
return nil, err
}
return res.RawBody(), nil
}
func (c *Client) RequestBlockUpload(ctx context.Context, req BlockUploadReq) ([]BlockUploadLink, error) {
var res struct {
UploadLinks []BlockUploadLink
}
if err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetResult(&res).SetBody(req).Post("/drive/blocks")
}); err != nil {
return nil, err
}
return res.UploadLinks, nil
}
func (c *Client) UploadBlock(ctx context.Context, bareURL, token string, block io.Reader) error {
return c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.
SetHeader("pm-storage-token", token).
SetMultipartField("Block", "blob", "application/octet-stream", block).
Post(bareURL)
})
}
go-proton-api-1.0.0/block_types.go 0000664 0000000 0000000 00000001575 14476422733 0017074 0 ustar 00root root 0000000 0000000 package proton
// Block is a block of file contents. They are split in 4MB blocks although this number may change in the future.
// Each block is its own data packet separated from the key packet which is held by the node,
// which means the sessionKey is the same for every block.
type Block struct {
Index int
BareURL string // URL to the block
Token string // Token for download URL
Hash string // Encrypted block's sha256 hash, in base64
EncSignature string // Encrypted signature of the block
SignatureEmail string // Email used to sign the block
}
type BlockUploadReq struct {
AddressID string
ShareID string
LinkID string
RevisionID string
BlockList []BlockUploadInfo
}
type BlockUploadInfo struct {
Index int
Size int64
EncSignature string
Hash string
}
type BlockUploadLink struct {
Token string
BareURL string
}
go-proton-api-1.0.0/boolean.go 0000664 0000000 0000000 00000001351 14476422733 0016165 0 ustar 00root root 0000000 0000000 package proton
import "encoding/json"
// Bool is a convenience type for boolean values; it converts from APIBool to Go's builtin bool type.
type Bool bool
// APIBool is the boolean type used by the API (0 or 1).
type APIBool int
const (
APIFalse APIBool = iota
APITrue
)
func (b *Bool) UnmarshalJSON(data []byte) error {
var v APIBool
if err := json.Unmarshal(data, &v); err != nil {
return err
}
*b = Bool(v == APITrue)
return nil
}
func (b Bool) MarshalJSON() ([]byte, error) {
var v APIBool
if b {
v = APITrue
} else {
v = APIFalse
}
return json.Marshal(v)
}
func (b Bool) String() string {
if b {
return "true"
}
return "false"
}
func (b Bool) FormatURL() string {
if b {
return "1"
}
return "0"
}
go-proton-api-1.0.0/calendar.go 0000664 0000000 0000000 00000003443 14476422733 0016323 0 ustar 00root root 0000000 0000000 package proton
import (
"context"
"github.com/go-resty/resty/v2"
)
func (c *Client) GetCalendars(ctx context.Context) ([]Calendar, error) {
var res struct {
Calendars []Calendar
}
if err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetResult(&res).Get("/calendar/v1")
}); err != nil {
return nil, err
}
return res.Calendars, nil
}
func (c *Client) GetCalendar(ctx context.Context, calendarID string) (Calendar, error) {
var res struct {
Calendar Calendar
}
if err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetResult(&res).Get("/calendar/v1/" + calendarID)
}); err != nil {
return Calendar{}, err
}
return res.Calendar, nil
}
func (c *Client) GetCalendarKeys(ctx context.Context, calendarID string) (CalendarKeys, error) {
var res struct {
Keys CalendarKeys
}
if err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetResult(&res).Get("/calendar/v1/" + calendarID + "/keys")
}); err != nil {
return nil, err
}
return res.Keys, nil
}
func (c *Client) GetCalendarMembers(ctx context.Context, calendarID string) ([]CalendarMember, error) {
var res struct {
Members []CalendarMember
}
if err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetResult(&res).Get("/calendar/v1/" + calendarID + "/members")
}); err != nil {
return nil, err
}
return res.Members, nil
}
func (c *Client) GetCalendarPassphrase(ctx context.Context, calendarID string) (CalendarPassphrase, error) {
var res struct {
Passphrase CalendarPassphrase
}
if err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetResult(&res).Get("/calendar/v1/" + calendarID + "/passphrase")
}); err != nil {
return CalendarPassphrase{}, err
}
return res.Passphrase, nil
}
go-proton-api-1.0.0/calendar_event.go 0000664 0000000 0000000 00000003505 14476422733 0017523 0 ustar 00root root 0000000 0000000 package proton
import (
"context"
"net/url"
"strconv"
"github.com/go-resty/resty/v2"
)
func (c *Client) CountCalendarEvents(ctx context.Context, calendarID string) (int, error) {
var res struct {
Total int
}
if err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetResult(&res).Get("/calendar/v1/" + calendarID + "/events")
}); err != nil {
return 0, err
}
return res.Total, nil
}
// TODO: For now, the query params are partially constant -- should they be configurable?
func (c *Client) GetCalendarEvents(ctx context.Context, calendarID string, page, pageSize int, filter url.Values) ([]CalendarEvent, error) {
var res struct {
Events []CalendarEvent
}
if err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetQueryParams(map[string]string{
"Page": strconv.Itoa(page),
"PageSize": strconv.Itoa(pageSize),
}).SetQueryParamsFromValues(filter).SetResult(&res).Get("/calendar/v1/" + calendarID + "/events")
}); err != nil {
return nil, err
}
return res.Events, nil
}
func (c *Client) GetAllCalendarEvents(ctx context.Context, calendarID string, filter url.Values) ([]CalendarEvent, error) {
total, err := c.CountCalendarEvents(ctx, calendarID)
if err != nil {
return nil, err
}
return fetchPaged(ctx, total, maxPageSize, c, func(ctx context.Context, page, pageSize int) ([]CalendarEvent, error) {
return c.GetCalendarEvents(ctx, calendarID, page, pageSize, filter)
})
}
func (c *Client) GetCalendarEvent(ctx context.Context, calendarID, eventID string) (CalendarEvent, error) {
var res struct {
Event CalendarEvent
}
if err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetResult(&res).Get("/calendar/v1/" + calendarID + "/events/" + eventID)
}); err != nil {
return CalendarEvent{}, err
}
return res.Event, nil
}
go-proton-api-1.0.0/calendar_event_types.go 0000664 0000000 0000000 00000004352 14476422733 0020750 0 ustar 00root root 0000000 0000000 package proton
import (
"encoding/base64"
"github.com/ProtonMail/gopenpgp/v2/crypto"
)
type CalendarEvent struct {
ID string
UID string
CalendarID string
SharedEventID string
CreateTime int64
LastEditTime int64
StartTime int64
StartTimezone string
EndTime int64
EndTimezone string
FullDay Bool
Author string
Permissions CalendarPermissions
Attendees []CalendarAttendee
SharedKeyPacket string
CalendarKeyPacket string
SharedEvents []CalendarEventPart
CalendarEvents []CalendarEventPart
AttendeesEvents []CalendarEventPart
PersonalEvents []CalendarEventPart
}
// TODO: Only personal events have MemberID; should we have a different type for that?
type CalendarEventPart struct {
MemberID string
Type CalendarEventType
Data string
Signature string
Author string
}
func (part CalendarEventPart) Decode(calKR *crypto.KeyRing, addrKR *crypto.KeyRing, kp []byte) error {
if part.Type&CalendarEventTypeEncrypted != 0 {
var enc *crypto.PGPMessage
if kp != nil {
raw, err := base64.StdEncoding.DecodeString(part.Data)
if err != nil {
return err
}
enc = crypto.NewPGPSplitMessage(kp, raw).GetPGPMessage()
} else {
var err error
if enc, err = crypto.NewPGPMessageFromArmored(part.Data); err != nil {
return err
}
}
dec, err := calKR.Decrypt(enc, nil, crypto.GetUnixTime())
if err != nil {
return err
}
part.Data = dec.GetString()
}
if part.Type&CalendarEventTypeSigned != 0 {
sig, err := crypto.NewPGPSignatureFromArmored(part.Signature)
if err != nil {
return err
}
if err := addrKR.VerifyDetached(crypto.NewPlainMessageFromString(part.Data), sig, crypto.GetUnixTime()); err != nil {
return err
}
}
return nil
}
type CalendarEventType int
const (
CalendarEventTypeClear CalendarEventType = iota
CalendarEventTypeEncrypted
CalendarEventTypeSigned
)
type CalendarAttendee struct {
ID string
Token string
Status CalendarAttendeeStatus
Permissions CalendarPermissions
}
// TODO: What is this?
type CalendarAttendeeStatus int
const (
CalendarAttendeeStatusPending CalendarAttendeeStatus = iota
CalendarAttendeeStatusMaybe
CalendarAttendeeStatusNo
CalendarAttendeeStatusYes
)
go-proton-api-1.0.0/calendar_types.go 0000664 0000000 0000000 00000005316 14476422733 0017550 0 ustar 00root root 0000000 0000000 package proton
import (
"errors"
"github.com/ProtonMail/gopenpgp/v2/crypto"
)
type Calendar struct {
ID string
Name string
Description string
Color string
Display Bool
Type CalendarType
Flags CalendarFlag
}
type CalendarFlag int64
const (
CalendarFlagActive CalendarFlag = 1 << iota
CalendarFlagUpdatePassphrase
CalendarFlagResetNeeded
CalendarFlagIncompleteSetup
CalendarFlagLostAccess
)
type CalendarType int
const (
CalendarTypeNormal CalendarType = iota
CalendarTypeSubscribed
)
type CalendarKey struct {
ID string
CalendarID string
PassphraseID string
PrivateKey string
Flags CalendarKeyFlag
}
func (key CalendarKey) Unlock(passphrase []byte) (*crypto.Key, error) {
lockedKey, err := crypto.NewKeyFromArmored(key.PrivateKey)
if err != nil {
return nil, err
}
return lockedKey.Unlock(passphrase)
}
type CalendarKeys []CalendarKey
func (keys CalendarKeys) Unlock(passphrase []byte) (*crypto.KeyRing, error) {
kr, err := crypto.NewKeyRing(nil)
if err != nil {
return nil, err
}
for _, key := range keys {
if k, err := key.Unlock(passphrase); err != nil {
continue
} else if err := kr.AddKey(k); err != nil {
return nil, err
}
}
return kr, nil
}
// TODO: What is this?
type CalendarKeyFlag int64
const (
CalendarKeyFlagActive CalendarKeyFlag = 1 << iota
CalendarKeyFlagPrimary
)
type CalendarMember struct {
ID string
Permissions CalendarPermissions
Email string
Color string
Display Bool
CalendarID string
}
// TODO: What is this?
type CalendarPermissions int
// TODO: Support invitations.
type CalendarPassphrase struct {
ID string
Flags CalendarPassphraseFlag
MemberPassphrases []MemberPassphrase
}
func (passphrase CalendarPassphrase) Decrypt(memberID string, addrKR *crypto.KeyRing) ([]byte, error) {
for _, passphrase := range passphrase.MemberPassphrases {
if passphrase.MemberID == memberID {
return passphrase.decrypt(addrKR)
}
}
return nil, errors.New("no such member passphrase")
}
// TODO: What is this?
type CalendarPassphraseFlag int64
type MemberPassphrase struct {
MemberID string
Passphrase string
Signature string
}
func (passphrase MemberPassphrase) decrypt(addrKR *crypto.KeyRing) ([]byte, error) {
msg, err := crypto.NewPGPMessageFromArmored(passphrase.Passphrase)
if err != nil {
return nil, err
}
sig, err := crypto.NewPGPSignatureFromArmored(passphrase.Signature)
if err != nil {
return nil, err
}
dec, err := addrKR.Decrypt(msg, nil, crypto.GetUnixTime())
if err != nil {
return nil, err
}
if err := addrKR.VerifyDetached(dec, sig, crypto.GetUnixTime()); err != nil {
return nil, err
}
return dec.GetBinary(), nil
}
go-proton-api-1.0.0/client.go 0000664 0000000 0000000 00000011061 14476422733 0016023 0 ustar 00root root 0000000 0000000 package proton
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"sync"
"sync/atomic"
"github.com/go-resty/resty/v2"
)
// clientID is a unique identifier for a client.
var clientID uint64
// AuthHandler is given any new auths that are returned from the API due to an unexpected auth refresh.
type AuthHandler func(Auth)
// Handler is a generic function that can be registered for a certain event (e.g. deauth, API code).
type Handler func()
// Client is the proton client.
type Client struct {
m *Manager
// clientID is this client's unique ID.
clientID uint64
uid string
acc string
ref string
authLock sync.RWMutex
authHandlers []AuthHandler
deauthHandlers []Handler
hookLock sync.RWMutex
deauthOnce sync.Once
}
func newClient(m *Manager, uid string) *Client {
c := &Client{
m: m,
uid: uid,
clientID: atomic.AddUint64(&clientID, 1),
}
return c
}
func (c *Client) AddAuthHandler(handler AuthHandler) {
c.hookLock.Lock()
defer c.hookLock.Unlock()
c.authHandlers = append(c.authHandlers, handler)
}
func (c *Client) AddDeauthHandler(handler Handler) {
c.hookLock.Lock()
defer c.hookLock.Unlock()
c.deauthHandlers = append(c.deauthHandlers, handler)
}
func (c *Client) AddPreRequestHook(hook resty.RequestMiddleware) {
c.hookLock.Lock()
defer c.hookLock.Unlock()
c.m.rc.OnBeforeRequest(func(rc *resty.Client, r *resty.Request) error {
if clientID, ok := ClientIDFromContext(r.Context()); !ok || clientID != c.clientID {
return nil
}
return hook(rc, r)
})
}
func (c *Client) AddPostRequestHook(hook resty.ResponseMiddleware) {
c.hookLock.Lock()
defer c.hookLock.Unlock()
c.m.rc.OnAfterResponse(func(rc *resty.Client, r *resty.Response) error {
if clientID, ok := ClientIDFromContext(r.Request.Context()); !ok || clientID != c.clientID {
return nil
}
return hook(rc, r)
})
}
func (c *Client) Close() {
c.authLock.Lock()
defer c.authLock.Unlock()
c.uid = ""
c.acc = ""
c.ref = ""
c.hookLock.Lock()
defer c.hookLock.Unlock()
c.authHandlers = nil
c.deauthHandlers = nil
}
func (c *Client) withAuth(acc, ref string) *Client {
c.acc = acc
c.ref = ref
return c
}
func (c *Client) do(ctx context.Context, fn func(*resty.Request) (*resty.Response, error)) error {
if _, err := c.doRes(ctx, fn); err != nil {
return err
}
return nil
}
func (c *Client) doRes(ctx context.Context, fn func(*resty.Request) (*resty.Response, error)) (*resty.Response, error) {
c.hookLock.RLock()
defer c.hookLock.RUnlock()
res, err := c.exec(ctx, fn)
if res != nil {
// If we receive no response, we can't do anything.
if res.RawResponse == nil {
return nil, newNetError(err, "received no response from API")
}
// If we receive a net error, we can't do anything.
if resErr, ok := err.(*resty.ResponseError); ok {
if netErr := new(net.OpError); errors.As(resErr.Err, &netErr) {
return nil, newNetError(netErr, "network error while communicating with API")
}
}
// If we receive a 401, we need to refresh the auth.
if res.StatusCode() == http.StatusUnauthorized {
if err := c.authRefresh(ctx); err != nil {
return nil, fmt.Errorf("failed to refresh auth: %w", err)
}
if res, err = c.exec(ctx, fn); err != nil {
return nil, fmt.Errorf("failed to retry request: %w", err)
}
}
}
return res, err
}
func (c *Client) exec(ctx context.Context, fn func(*resty.Request) (*resty.Response, error)) (*resty.Response, error) {
c.authLock.RLock()
defer c.authLock.RUnlock()
r := c.m.r(WithClient(ctx, c.clientID))
if c.uid != "" {
r.SetHeader("x-pm-uid", c.uid)
}
if c.acc != "" {
r.SetAuthToken(c.acc)
}
return fn(r)
}
func (c *Client) authRefresh(ctx context.Context) error {
c.authLock.Lock()
defer c.authLock.Unlock()
c.hookLock.RLock()
defer c.hookLock.RUnlock()
auth, err := c.m.authRefresh(ctx, c.uid, c.ref)
if err != nil {
if respErr, ok := err.(*resty.ResponseError); ok {
switch respErr.Response.StatusCode() {
case http.StatusBadRequest, http.StatusUnprocessableEntity:
c.deauthOnce.Do(func() {
for _, handler := range c.deauthHandlers {
handler()
}
})
return fmt.Errorf("failed to refresh auth, de-auth: %w", err)
case http.StatusConflict, http.StatusTooManyRequests, http.StatusInternalServerError, http.StatusServiceUnavailable:
return fmt.Errorf("failed to refresh auth, server issues: %w", err)
default:
//
}
}
return fmt.Errorf("failed to refresh auth: %w", err)
}
c.acc = auth.AccessToken
c.ref = auth.RefreshToken
for _, handler := range c.authHandlers {
handler(auth)
}
return nil
}
go-proton-api-1.0.0/contact.go 0000664 0000000 0000000 00000007012 14476422733 0016201 0 ustar 00root root 0000000 0000000 package proton
import (
"context"
"strconv"
"github.com/go-resty/resty/v2"
)
func (c *Client) GetContact(ctx context.Context, contactID string) (Contact, error) {
var res struct {
Contact Contact
}
if err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetResult(&res).Get("/contacts/v4/" + contactID)
}); err != nil {
return Contact{}, err
}
return res.Contact, nil
}
func (c *Client) CountContacts(ctx context.Context) (int, error) {
var res struct {
Total int
}
if err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetResult(&res).Get("/contacts/v4")
}); err != nil {
return 0, err
}
return res.Total, nil
}
func (c *Client) CountContactEmails(ctx context.Context, email string) (int, error) {
var res struct {
Total int
}
if err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetResult(&res).SetQueryParam("Email", email).Get("/contacts/v4/emails")
}); err != nil {
return 0, err
}
return res.Total, nil
}
func (c *Client) GetContacts(ctx context.Context, page, pageSize int) ([]Contact, error) {
var res struct {
Contacts []Contact
}
if err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetQueryParams(map[string]string{
"Page": strconv.Itoa(page),
"PageSize": strconv.Itoa(pageSize),
}).SetResult(&res).Get("/contacts/v4")
}); err != nil {
return nil, err
}
return res.Contacts, nil
}
func (c *Client) GetAllContacts(ctx context.Context) ([]Contact, error) {
total, err := c.CountContacts(ctx)
if err != nil {
return nil, err
}
return fetchPaged(ctx, total, maxPageSize, c, func(ctx context.Context, page, pageSize int) ([]Contact, error) {
return c.GetContacts(ctx, page, pageSize)
})
}
func (c *Client) GetContactEmails(ctx context.Context, email string, page, pageSize int) ([]ContactEmail, error) {
var res struct {
ContactEmails []ContactEmail
}
if err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetQueryParams(map[string]string{
"Page": strconv.Itoa(page),
"PageSize": strconv.Itoa(pageSize),
"Email": email,
}).SetResult(&res).Get("/contacts/v4/emails")
}); err != nil {
return nil, err
}
return res.ContactEmails, nil
}
func (c *Client) GetAllContactEmails(ctx context.Context, email string) ([]ContactEmail, error) {
total, err := c.CountContactEmails(ctx, email)
if err != nil {
return nil, err
}
return fetchPaged(ctx, total, maxPageSize, c, func(ctx context.Context, page, pageSize int) ([]ContactEmail, error) {
return c.GetContactEmails(ctx, email, page, pageSize)
})
}
func (c *Client) CreateContacts(ctx context.Context, req CreateContactsReq) ([]CreateContactsRes, error) {
var res struct {
Responses []CreateContactsRes
}
if err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetBody(req).SetResult(&res).Post("/contacts/v4")
}); err != nil {
return nil, err
}
return res.Responses, nil
}
func (c *Client) UpdateContact(ctx context.Context, contactID string, req UpdateContactReq) (Contact, error) {
var res struct {
Contact Contact
}
if err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetBody(req).SetResult(&res).Put("/contacts/v4/" + contactID)
}); err != nil {
return Contact{}, err
}
return res.Contact, nil
}
func (c *Client) DeleteContacts(ctx context.Context, req DeleteContactsReq) error {
return c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetBody(req).Put("/contacts/v4/delete")
})
}
go-proton-api-1.0.0/contact_card.go 0000664 0000000 0000000 00000015250 14476422733 0017175 0 ustar 00root root 0000000 0000000 package proton
import (
"bytes"
"errors"
"strings"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/bradenaw/juniper/xslices"
"github.com/emersion/go-vcard"
)
const (
FieldPMScheme = "X-PM-SCHEME"
FieldPMSign = "X-PM-SIGN"
FieldPMEncrypt = "X-PM-ENCRYPT"
FieldPMMIMEType = "X-PM-MIMETYPE"
)
type Cards []*Card
func (c *Cards) Merge(kr *crypto.KeyRing) (vcard.Card, error) {
merged := newVCard()
for _, card := range *c {
dec, err := card.decode(kr)
if err != nil {
return nil, err
}
for k, fields := range dec {
for _, f := range fields {
merged.Add(k, f)
}
}
}
return merged, nil
}
func (c *Cards) Get(cardType CardType) (*Card, bool) {
for _, card := range *c {
if card.Type == cardType {
return card, true
}
}
return nil, false
}
type Card struct {
Type CardType
Data string
Signature string
}
type CardType int
const (
CardTypeClear CardType = iota
CardTypeEncrypted
CardTypeSigned
)
func NewCard(kr *crypto.KeyRing, cardType CardType) (*Card, error) {
card := &Card{Type: cardType}
if err := card.encode(kr, newVCard()); err != nil {
return nil, err
}
return card, nil
}
func newVCard() vcard.Card {
card := make(vcard.Card)
card.AddValue(vcard.FieldVersion, "4.0")
return card
}
func (c Card) Get(kr *crypto.KeyRing, key string) ([]*vcard.Field, error) {
dec, err := c.decode(kr)
if err != nil {
return nil, err
}
return dec[key], nil
}
func (c *Card) Set(kr *crypto.KeyRing, key, value string) error {
dec, err := c.decode(kr)
if err != nil {
return err
}
if field := dec.Get(key); field != nil {
field.Value = value
return c.encode(kr, dec)
}
dec.AddValue(key, value)
return c.encode(kr, dec)
}
func (c *Card) ChangeType(kr *crypto.KeyRing, cardType CardType) error {
dec, err := c.decode(kr)
if err != nil {
return err
}
c.Type = cardType
return c.encode(kr, dec)
}
// GetGroup returns a type to manipulate the group defined by the given key/value pair.
func (c Card) GetGroup(kr *crypto.KeyRing, groupKey, groupValue string) (CardGroup, error) {
group, err := c.getGroup(kr, groupKey, groupValue)
if err != nil {
return CardGroup{}, err
}
return CardGroup{Card: c, kr: kr, group: group}, nil
}
// DeleteGroup removes all values in the group defined by the given key/value pair.
func (c *Card) DeleteGroup(kr *crypto.KeyRing, groupKey, groupValue string) error {
group, err := c.getGroup(kr, groupKey, groupValue)
if err != nil {
return err
}
return c.deleteGroup(kr, group)
}
type CardGroup struct {
Card
kr *crypto.KeyRing
group string
}
// Get returns the values in the group with the given key.
func (g CardGroup) Get(key string) ([]string, error) {
dec, err := g.decode(g.kr)
if err != nil {
return nil, err
}
var fields []*vcard.Field
for _, field := range dec[key] {
if field.Group != g.group {
continue
}
fields = append(fields, field)
}
return xslices.Map(fields, func(field *vcard.Field) string {
return field.Value
}), nil
}
// Set sets the value in the group.
func (g *CardGroup) Set(key, value string, params vcard.Params) error {
dec, err := g.decode(g.kr)
if err != nil {
return err
}
for _, field := range dec[key] {
if field.Group != g.group {
continue
}
field.Value = value
return g.encode(g.kr, dec)
}
dec.Add(key, &vcard.Field{
Value: value,
Group: g.group,
Params: params,
})
return g.encode(g.kr, dec)
}
// Add adds a value to the group.
func (g *CardGroup) Add(key, value string, params vcard.Params) error {
dec, err := g.decode(g.kr)
if err != nil {
return err
}
dec.Add(key, &vcard.Field{
Value: value,
Group: g.group,
Params: params,
})
return g.encode(g.kr, dec)
}
// Remove removes the value in the group with the given key/value.
func (g *CardGroup) Remove(key, value string) error {
dec, err := g.decode(g.kr)
if err != nil {
return err
}
fields, ok := dec[key]
if !ok {
return errors.New("no such key")
}
var rest []*vcard.Field
for _, field := range fields {
if field.Group != g.group {
rest = append(rest, field)
} else if field.Value != value {
rest = append(rest, field)
}
}
if len(rest) > 0 {
dec[key] = rest
} else {
delete(dec, key)
}
return g.encode(g.kr, dec)
}
// RemoveAll removes all values in the group with the given key.
func (g *CardGroup) RemoveAll(key string) error {
dec, err := g.decode(g.kr)
if err != nil {
return err
}
fields, ok := dec[key]
if !ok {
return errors.New("no such key")
}
var rest []*vcard.Field
for _, field := range fields {
if field.Group != g.group {
rest = append(rest, field)
}
}
if len(rest) > 0 {
dec[key] = rest
} else {
delete(dec, key)
}
return g.encode(g.kr, dec)
}
func (c Card) getGroup(kr *crypto.KeyRing, groupKey, groupValue string) (string, error) {
fields, err := c.Get(kr, groupKey)
if err != nil {
return "", err
}
for _, field := range fields {
if field.Value != groupValue {
continue
}
return field.Group, nil
}
return "", errors.New("no such field")
}
func (c *Card) deleteGroup(kr *crypto.KeyRing, group string) error {
dec, err := c.decode(kr)
if err != nil {
return err
}
for key, fields := range dec {
var rest []*vcard.Field
for _, field := range fields {
if field.Group != group {
rest = append(rest, field)
}
}
if len(rest) > 0 {
dec[key] = rest
} else {
delete(dec, key)
}
}
return c.encode(kr, dec)
}
func (c Card) decode(kr *crypto.KeyRing) (vcard.Card, error) {
if c.Type&CardTypeEncrypted != 0 {
enc, err := crypto.NewPGPMessageFromArmored(c.Data)
if err != nil {
return nil, err
}
dec, err := kr.Decrypt(enc, nil, crypto.GetUnixTime())
if err != nil {
return nil, err
}
c.Data = dec.GetString()
}
if c.Type&CardTypeSigned != 0 {
sig, err := crypto.NewPGPSignatureFromArmored(c.Signature)
if err != nil {
return nil, err
}
if err := kr.VerifyDetached(crypto.NewPlainMessageFromString(c.Data), sig, crypto.GetUnixTime()); err != nil {
return nil, err
}
}
return vcard.NewDecoder(strings.NewReader(c.Data)).Decode()
}
func (c *Card) encode(kr *crypto.KeyRing, card vcard.Card) error {
buf := new(bytes.Buffer)
if err := vcard.NewEncoder(buf).Encode(card); err != nil {
return err
}
if c.Type&CardTypeSigned != 0 {
sig, err := kr.SignDetached(crypto.NewPlainMessageFromString(buf.String()))
if err != nil {
return err
}
if c.Signature, err = sig.GetArmored(); err != nil {
return err
}
}
if c.Type&CardTypeEncrypted != 0 {
enc, err := kr.Encrypt(crypto.NewPlainMessageFromString(buf.String()), nil)
if err != nil {
return err
}
if c.Data, err = enc.GetArmored(); err != nil {
return err
}
} else {
c.Data = buf.String()
}
return nil
}
go-proton-api-1.0.0/contact_types.go 0000664 0000000 0000000 00000005636 14476422733 0017437 0 ustar 00root root 0000000 0000000 package proton
import (
"encoding/base64"
"strconv"
"strings"
"github.com/ProtonMail/gluon/rfc822"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/emersion/go-vcard"
)
type RecipientType int
const (
RecipientTypeInternal RecipientType = iota + 1
RecipientTypeExternal
)
type ContactSettings struct {
MIMEType *rfc822.MIMEType
Scheme *EncryptionScheme
Sign *bool
Encrypt *bool
Keys []*crypto.Key
}
type Contact struct {
ContactMetadata
ContactCards
}
func (c *Contact) GetSettings(kr *crypto.KeyRing, email string) (ContactSettings, error) {
signedCard, ok := c.Cards.Get(CardTypeSigned)
if !ok {
return ContactSettings{}, nil
}
group, err := signedCard.GetGroup(kr, vcard.FieldEmail, email)
if err != nil {
return ContactSettings{}, nil
}
var settings ContactSettings
scheme, err := group.Get(FieldPMScheme)
if err != nil {
return ContactSettings{}, err
}
if len(scheme) > 0 {
switch scheme[0] {
case "pgp-inline":
settings.Scheme = newPtr(PGPInlineScheme)
case "pgp-mime":
settings.Scheme = newPtr(PGPMIMEScheme)
}
}
mimeType, err := group.Get(FieldPMMIMEType)
if err != nil {
return ContactSettings{}, err
}
if len(mimeType) > 0 {
settings.MIMEType = newPtr(rfc822.MIMEType(mimeType[0]))
}
sign, err := group.Get(FieldPMSign)
if err != nil {
return ContactSettings{}, err
}
if len(sign) > 0 {
sign, err := strconv.ParseBool(sign[0])
if err != nil {
return ContactSettings{}, err
}
settings.Sign = newPtr(sign)
}
encrypt, err := group.Get(FieldPMEncrypt)
if err != nil {
return ContactSettings{}, err
}
if len(encrypt) > 0 {
encrypt, err := strconv.ParseBool(encrypt[0])
if err != nil {
return ContactSettings{}, err
}
settings.Encrypt = newPtr(encrypt)
}
keys, err := group.Get(vcard.FieldKey)
if err != nil {
return ContactSettings{}, err
}
if len(keys) > 0 {
for _, key := range keys {
dec, err := base64.StdEncoding.DecodeString(strings.SplitN(key, ",", 2)[1])
if err != nil {
return ContactSettings{}, err
}
pubKey, err := crypto.NewKey(dec)
if err != nil {
return ContactSettings{}, err
}
settings.Keys = append(settings.Keys, pubKey)
}
}
return settings, nil
}
type ContactMetadata struct {
ID string
Name string
UID string
Size int64
CreateTime int64
ModifyTime int64
ContactEmails []ContactEmail
LabelIDs []string
}
type ContactCards struct {
Cards Cards
}
type ContactEmail struct {
ID string
Name string
Email string
Type []string
ContactID string
LabelIDs []string
}
type CreateContactsReq struct {
Contacts []ContactCards
Overwrite int
Labels int
}
type CreateContactsRes struct {
Index int
Response struct {
APIError
Contact Contact
}
}
type UpdateContactReq struct {
Cards Cards
}
type DeleteContactsReq struct {
IDs []string
}
func newPtr[T any](v T) *T {
return &v
}
go-proton-api-1.0.0/contexts.go 0000664 0000000 0000000 00000001114 14476422733 0016412 0 ustar 00root root 0000000 0000000 package proton
import "context"
type withClientKeyType struct{}
var withClientKey withClientKeyType
// WithClient marks this context as originating from the client with the given ID.
func WithClient(parent context.Context, clientID uint64) context.Context {
return context.WithValue(parent, withClientKey, clientID)
}
// ClientIDFromContext returns true if this context was marked as originating from a client.
func ClientIDFromContext(ctx context.Context) (uint64, bool) {
clientID, ok := ctx.Value(withClientKey).(uint64)
if !ok {
return 0, false
}
return clientID, true
}
go-proton-api-1.0.0/core_settings.go 0000664 0000000 0000000 00000002246 14476422733 0017422 0 ustar 00root root 0000000 0000000 package proton
import (
"context"
"github.com/go-resty/resty/v2"
)
func (c *Client) GetUserSettings(ctx context.Context) (UserSettings, error) {
var res struct {
UserSettings UserSettings
}
if err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetResult(&res).Get("/core/v4/settings")
}); err != nil {
return UserSettings{}, err
}
return res.UserSettings, nil
}
func (c *Client) SetUserSettingsTelemetry(ctx context.Context, req SetTelemetryReq) (UserSettings, error) {
var res struct {
UserSettings UserSettings
}
if err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetBody(req).SetResult(&res).Put("/core/v4/settings/telemetry")
}); err != nil {
return UserSettings{}, err
}
return res.UserSettings, nil
}
func (c *Client) SetUserSettingsCrashReports(ctx context.Context, req SetCrashReportReq) (UserSettings, error) {
var res struct {
UserSettings UserSettings
}
if err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetBody(req).SetResult(&res).Put("/core/v4/settings/crashreports")
}); err != nil {
return UserSettings{}, err
}
return res.UserSettings, nil
}
go-proton-api-1.0.0/core_settings_type.go 0000664 0000000 0000000 00000000460 14476422733 0020457 0 ustar 00root root 0000000 0000000 package proton
type UserSettings struct {
Telemetry SettingsBool
CrashReports SettingsBool
}
type SetTelemetryReq struct {
Telemetry SettingsBool
}
type SetCrashReportReq struct {
CrashReports SettingsBool
}
type SettingsBool int
const (
SettingDisabled SettingsBool = iota
SettingEnabled
)
go-proton-api-1.0.0/data.go 0000664 0000000 0000000 00000000754 14476422733 0015465 0 ustar 00root root 0000000 0000000 package proton
import (
"context"
"github.com/go-resty/resty/v2"
)
func (c *Client) SendDataEvent(ctx context.Context, req SendStatsReq) error {
return c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetBody(req).Post("/data/v1/stats")
})
}
func (c *Client) SendDataEventMultiple(ctx context.Context, req SendStatsMultiReq) error {
return c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetBody(req).Post("/data/v1/stats/multiple")
})
}
go-proton-api-1.0.0/data_type.go 0000664 0000000 0000000 00000000336 14476422733 0016522 0 ustar 00root root 0000000 0000000 package proton
type SendStatsReq struct {
MeasurementGroup string
Event string
Values map[string]any
Dimensions map[string]any
}
type SendStatsMultiReq struct {
EventInfo []SendStatsReq
}
go-proton-api-1.0.0/event.go 0000664 0000000 0000000 00000004340 14476422733 0015670 0 ustar 00root root 0000000 0000000 package proton
import (
"context"
"time"
"github.com/ProtonMail/gluon/async"
"github.com/go-resty/resty/v2"
)
func (c *Client) GetLatestEventID(ctx context.Context) (string, error) {
var res struct {
Event
}
if err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetResult(&res).Get("/core/v4/events/latest")
}); err != nil {
return "", err
}
return res.EventID, nil
}
// maxCollectedEvents limits the number of events which are collected per one GetEvent
// call.
const maxCollectedEvents = 50
func (c *Client) GetEvent(ctx context.Context, eventID string) ([]Event, bool, error) {
var events []Event
event, more, err := c.getEvent(ctx, eventID)
if err != nil {
return nil, more, err
}
events = append(events, event)
nCollected := 0
for more {
nCollected++
if nCollected >= maxCollectedEvents {
break
}
event, more, err = c.getEvent(ctx, event.EventID)
if err != nil {
return nil, false, err
}
events = append(events, event)
}
return events, more, nil
}
// NewEventStreamer returns a new event stream.
// It polls the API for new events at random intervals between `period` and `period+jitter`.
func (c *Client) NewEventStream(ctx context.Context, period, jitter time.Duration, lastEventID string) <-chan Event {
eventCh := make(chan Event)
go func() {
defer async.HandlePanic(c.m.panicHandler)
defer close(eventCh)
ticker := NewTicker(period, jitter, c.m.panicHandler)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
// ...
}
events, _, err := c.GetEvent(ctx, lastEventID)
if err != nil {
continue
}
if events[len(events)-1].EventID == lastEventID {
continue
}
for _, evt := range events {
select {
case <-ctx.Done():
return
case eventCh <- evt:
lastEventID = evt.EventID
}
}
}
}()
return eventCh
}
func (c *Client) getEvent(ctx context.Context, eventID string) (Event, bool, error) {
var res struct {
Event
More Bool
}
if err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetResult(&res).Get("/core/v4/events/" + eventID)
}); err != nil {
return Event{}, false, err
}
return res.Event, bool(res.More), nil
}
go-proton-api-1.0.0/event_drive.go 0000664 0000000 0000000 00000004655 14476422733 0017072 0 ustar 00root root 0000000 0000000 package proton
import (
"context"
"github.com/go-resty/resty/v2"
)
func (c *Client) GetLatestVolumeEventID(ctx context.Context, volumeID string) (string, error) {
var res struct {
EventID string
}
if err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetResult(&res).Get("/drive/volumes/" + volumeID + "/events/latest")
}); err != nil {
return "", err
}
return res.EventID, nil
}
func (c *Client) GetLatestShareEventID(ctx context.Context, shareID string) (string, error) {
var res struct {
EventID string
}
if err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetResult(&res).Get("/drive/shares/" + shareID + "/events/latest")
}); err != nil {
return "", err
}
return res.EventID, nil
}
func (c *Client) GetVolumeEvent(ctx context.Context, volumeID, eventID string) (DriveEvent, error) {
event, more, err := c.getVolumeEvent(ctx, volumeID, eventID)
if err != nil {
return DriveEvent{}, err
}
for more {
var next DriveEvent
next, more, err = c.getVolumeEvent(ctx, volumeID, event.EventID)
if err != nil {
return DriveEvent{}, err
}
event.Events = append(event.Events, next.Events...)
}
return event, nil
}
func (c *Client) GetShareEvent(ctx context.Context, shareID, eventID string) (DriveEvent, error) {
event, more, err := c.getShareEvent(ctx, shareID, eventID)
if err != nil {
return DriveEvent{}, err
}
for more {
var next DriveEvent
next, more, err = c.getShareEvent(ctx, shareID, event.EventID)
if err != nil {
return DriveEvent{}, err
}
event.Events = append(event.Events, next.Events...)
}
return event, nil
}
func (c *Client) getVolumeEvent(ctx context.Context, volumeID, eventID string) (DriveEvent, bool, error) {
var res struct {
DriveEvent
More Bool
}
if err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetResult(&res).Get("/drive/volumes/" + volumeID + "/events/" + eventID)
}); err != nil {
return DriveEvent{}, false, err
}
return res.DriveEvent, bool(res.More), nil
}
func (c *Client) getShareEvent(ctx context.Context, shareID, eventID string) (DriveEvent, bool, error) {
var res struct {
DriveEvent
More Bool
}
if err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) {
return r.SetResult(&res).Get("/drive/shares/" + shareID + "/events/" + eventID)
}); err != nil {
return DriveEvent{}, false, err
}
return res.DriveEvent, bool(res.More), nil
}
go-proton-api-1.0.0/event_drive_types.go 0000664 0000000 0000000 00000000520 14476422733 0020301 0 ustar 00root root 0000000 0000000 package proton
type DriveEvent struct {
EventID string
Events []LinkEvent
Refresh Bool
}
type LinkEvent struct {
EventID string
EventType LinkEventType
CreateTime int
Link Link
Data any
}
type LinkEventType int
const (
LinkEventDelete LinkEventType = iota
LinkEventCreate
LinkEventUpdate
LinkEventUpdateMetadata
)
go-proton-api-1.0.0/event_test.go 0000664 0000000 0000000 00000005513 14476422733 0016732 0 ustar 00root root 0000000 0000000 package proton_test
import (
"context"
"testing"
"time"
"github.com/google/uuid"
"github.com/henrybear327/go-proton-api"
"github.com/henrybear327/go-proton-api/server"
"github.com/stretchr/testify/require"
)
func TestEventStreamer(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
s := server.New()
defer s.Close()
m := proton.New(
proton.WithHostURL(s.GetHostURL()),
proton.WithTransport(proton.InsecureTransport()),
)
_, _, err := s.CreateUser("user", []byte("pass"))
require.NoError(t, err)
c, _, err := m.NewClientWithLogin(ctx, "user", []byte("pass"))
require.NoError(t, err)
createTestMessages(t, c, "pass", 10)
latestEventID, err := c.GetLatestEventID(ctx)
require.NoError(t, err)
eventCh := make(chan proton.Event)
go func() {
for event := range c.NewEventStream(ctx, time.Second, 0, latestEventID) {
eventCh <- event
}
}()
// Perform some action to generate an event.
metadata, err := c.GetMessageMetadata(ctx, proton.MessageFilter{})
require.NoError(t, err)
require.NoError(t, c.LabelMessages(ctx, []string{metadata[0].ID}, proton.TrashLabel))
// Wait for the first event.
<-eventCh
// Close the client; this should stop the client's event streamer.
c.Close()
// Create a new client and perform some actions with it to generate more events.
cc, _, err := m.NewClientWithLogin(ctx, "user", []byte("pass"))
require.NoError(t, err)
defer cc.Close()
require.NoError(t, cc.LabelMessages(ctx, []string{metadata[1].ID}, proton.TrashLabel))
// We should not receive any more events from the original client.
select {
case <-eventCh:
require.Fail(t, "received unexpected event")
default:
// ...
}
}
func TestMaxEventMerge(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
s := server.New()
defer s.Close()
s.SetMaxUpdatesPerEvent(1)
m := proton.New(
proton.WithHostURL(s.GetHostURL()),
proton.WithTransport(proton.InsecureTransport()),
)
_, _, err := s.CreateUser("user", []byte("pass"))
require.NoError(t, err)
c, _, err := m.NewClientWithLogin(ctx, "user", []byte("pass"))
require.NoError(t, err)
latestID, err := c.GetLatestEventID(ctx)
require.NoError(t, err)
label, err := c.CreateLabel(context.Background(), proton.CreateLabelReq{
Name: uuid.NewString(),
Color: "#f66",
Type: proton.LabelTypeFolder,
})
require.NoError(t, err)
for i := 0; i < 75; i++ {
_, err := c.UpdateLabel(ctx, label.ID, proton.UpdateLabelReq{Name: uuid.NewString()})
require.NoError(t, err)
}
events, more, err := c.GetEvent(ctx, latestID)
require.NoError(t, err)
require.True(t, more)
require.Equal(t, 50, len(events))
events2, more, err := c.GetEvent(ctx, events[len(events)-1].EventID)
require.NotEqual(t, events, events2)
require.NoError(t, err)
require.False(t, more)
require.Equal(t, 26, len(events2))
}
go-proton-api-1.0.0/event_types.go 0000664 0000000 0000000 00000005556 14476422733 0017126 0 ustar 00root root 0000000 0000000 package proton
import (
"fmt"
"strings"
"github.com/bradenaw/juniper/xslices"
)
type Event struct {
EventID string
Refresh RefreshFlag
User *User
UserSettings *UserSettings
MailSettings *MailSettings
Messages []MessageEvent
Labels []LabelEvent
Addresses []AddressEvent
UsedSpace *int
}
func (event Event) String() string {
var parts []string
if event.Refresh != 0 {
parts = append(parts, fmt.Sprintf("refresh: %v", event.Refresh))
}
if event.User != nil {
parts = append(parts, "user: [modified]")
}
if event.MailSettings != nil {
parts = append(parts, "mail-settings: [modified]")
}
if len(event.Messages) > 0 {
parts = append(parts, fmt.Sprintf(
"messages: created=%d, updated=%d, deleted=%d",
xslices.CountFunc(event.Messages, func(e MessageEvent) bool { return e.Action == EventCreate }),
xslices.CountFunc(event.Messages, func(e MessageEvent) bool { return e.Action == EventUpdate || e.Action == EventUpdateFlags }),
xslices.CountFunc(event.Messages, func(e MessageEvent) bool { return e.Action == EventDelete }),
))
}
if len(event.Labels) > 0 {
parts = append(parts, fmt.Sprintf(
"labels: created=%d, updated=%d, deleted=%d",
xslices.CountFunc(event.Labels, func(e LabelEvent) bool { return e.Action == EventCreate }),
xslices.CountFunc(event.Labels, func(e LabelEvent) bool { return e.Action == EventUpdate || e.Action == EventUpdateFlags }),
xslices.CountFunc(event.Labels, func(e LabelEvent) bool { return e.Action == EventDelete }),
))
}
if len(event.Addresses) > 0 {
parts = append(parts, fmt.Sprintf(
"addresses: created=%d, updated=%d, deleted=%d",
xslices.CountFunc(event.Addresses, func(e AddressEvent) bool { return e.Action == EventCreate }),
xslices.CountFunc(event.Addresses, func(e AddressEvent) bool { return e.Action == EventUpdate || e.Action == EventUpdateFlags }),
xslices.CountFunc(event.Addresses, func(e AddressEvent) bool { return e.Action == EventDelete }),
))
}
return fmt.Sprintf("Event %s: %s", event.EventID, strings.Join(parts, ", "))
}
type RefreshFlag uint8
const (
RefreshMail RefreshFlag = 1 << iota // 1<<0 = 1
_ // 1<<1 = 2
_ // 1<<2 = 4
_ // 1<<3 = 8
_ // 1<<4 = 16
_ // 1<<5 = 32
_ // 1<<6 = 64
_ // 1<<7 = 128
RefreshAll RefreshFlag = 1<