You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
217 lines
4.9 KiB
217 lines
4.9 KiB
// Copyright (c) 2022 Proton Technologies AG |
|
// |
|
// This file is part of ProtonMail Bridge. |
|
// |
|
// ProtonMail Bridge is free software: you can redistribute it and/or modify |
|
// it under the terms of the GNU General Public License as published by |
|
// the Free Software Foundation, either version 3 of the License, or |
|
// (at your option) any later version. |
|
// |
|
// ProtonMail Bridge is distributed in the hope that it will be useful, |
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
// GNU General Public License for more details. |
|
// |
|
// You should have received a copy of the GNU General Public License |
|
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. |
|
|
|
package pmapi |
|
|
|
import ( |
|
"context" |
|
"strings" |
|
|
|
"github.com/ProtonMail/gopenpgp/v2/crypto" |
|
"github.com/go-resty/resty/v2" |
|
"github.com/pkg/errors" |
|
) |
|
|
|
// Address statuses. |
|
const ( |
|
DisabledAddress = iota |
|
EnabledAddress |
|
) |
|
|
|
// Address HasKeys values. |
|
const ( |
|
MissingKeys = iota |
|
KeysPresent |
|
) |
|
|
|
// Address types. |
|
const ( |
|
_ = iota // Skip first. |
|
OriginalAddress |
|
AliasAddress |
|
CustomAddress |
|
PremiumAddress |
|
) |
|
|
|
// Address Send values. |
|
const ( |
|
NoSendAddress = iota |
|
MainSendAddress |
|
SecondarySendAddress |
|
) |
|
|
|
// Address represents a user's address. |
|
type Address struct { |
|
ID string |
|
DomainID string |
|
Email string |
|
Send int |
|
Receive Boolean |
|
Status int |
|
Order int `json:",omitempty"` |
|
Type int |
|
DisplayName string |
|
Signature string |
|
MemberID string `json:",omitempty"` |
|
MemberName string `json:",omitempty"` |
|
|
|
HasKeys int |
|
Keys PMKeys |
|
} |
|
|
|
// AddressList is a list of addresses. |
|
type AddressList []*Address |
|
|
|
// ByID returns an address by id. Returns nil if no address is found. |
|
func (l AddressList) ByID(id string) *Address { |
|
for _, addr := range l { |
|
if addr.ID == id { |
|
return addr |
|
} |
|
} |
|
return nil |
|
} |
|
|
|
// AllEmails returns all emails. |
|
func (l AddressList) AllEmails() (addresses []string) { |
|
for _, a := range l { |
|
addresses = append(addresses, a.Email) |
|
} |
|
return |
|
} |
|
|
|
// ActiveEmails returns only active emails. |
|
func (l AddressList) ActiveEmails() (addresses []string) { |
|
for _, a := range l { |
|
if a.Receive { |
|
addresses = append(addresses, a.Email) |
|
} |
|
} |
|
return |
|
} |
|
|
|
// Main gets the main address. |
|
func (l AddressList) Main() *Address { |
|
for _, addr := range l { |
|
if addr.Order == 1 { |
|
return addr |
|
} |
|
} |
|
return nil |
|
} |
|
|
|
// ByEmail gets an address by email. Returns nil if no address is found. |
|
func (l AddressList) ByEmail(email string) *Address { |
|
email = SanitizeEmail(email) |
|
for _, addr := range l { |
|
if strings.EqualFold(addr.Email, email) { |
|
return addr |
|
} |
|
} |
|
return nil |
|
} |
|
|
|
func SanitizeEmail(email string) string { |
|
splitAt := strings.Split(email, "@") |
|
if len(splitAt) != 2 { |
|
return email |
|
} |
|
splitPlus := strings.Split(splitAt[0], "+") |
|
email = splitPlus[0] + "@" + splitAt[1] |
|
return email |
|
} |
|
|
|
func ConstructAddress(headerEmail string, addressEmail string) string { |
|
splitAtHeader := strings.Split(headerEmail, "@") |
|
if len(splitAtHeader) != 2 { |
|
return addressEmail |
|
} |
|
|
|
splitPlus := strings.Split(splitAtHeader[0], "+") |
|
if len(splitPlus) != 2 { |
|
return addressEmail |
|
} |
|
|
|
splitAtAddress := strings.Split(addressEmail, "@") |
|
if len(splitAtAddress) != 2 { |
|
return addressEmail |
|
} |
|
|
|
return splitAtAddress[0] + "+" + splitPlus[1] + "@" + splitAtAddress[1] |
|
} |
|
|
|
// GetAddresses requests all of current user addresses (without pagination). |
|
func (c *client) GetAddresses(ctx context.Context) (addresses AddressList, err error) { |
|
var res struct { |
|
Addresses []*Address |
|
} |
|
|
|
if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { |
|
return r.SetResult(&res).Get("/addresses") |
|
}); err != nil { |
|
return nil, err |
|
} |
|
|
|
return res.Addresses, nil |
|
} |
|
|
|
func (c *client) ReorderAddresses(ctx context.Context, addressIDs []string) error { |
|
if _, err := c.do(ctx, func(r *resty.Request) (*resty.Response, error) { |
|
return r.SetBody(&struct { |
|
AddressIDs []string |
|
}{ |
|
AddressIDs: addressIDs, |
|
}).Put("/addresses/order") |
|
}); err != nil { |
|
return err |
|
} |
|
|
|
_, err := c.UpdateUser(ctx) |
|
return err |
|
} |
|
|
|
// Addresses returns the addresses stored in the client object itself rather than fetching from the API. |
|
func (c *client) Addresses() AddressList { |
|
return c.addresses |
|
} |
|
|
|
// unlockAddresses unlocks all keys for all addresses of current user. |
|
func (c *client) unlockAddress(passphrase []byte, address *Address) error { |
|
if address == nil { |
|
return errors.New("address data is missing") |
|
} |
|
|
|
if address.HasKeys == MissingKeys { |
|
return nil |
|
} |
|
|
|
kr, err := address.Keys.UnlockAll(passphrase, c.userKeyRing) |
|
if err != nil { |
|
return errors.Wrap(err, "cannot unlock address keys for "+address.ID) |
|
} |
|
|
|
c.addrKeyRing[address.ID] = kr |
|
return nil |
|
} |
|
|
|
func (c *client) KeyRingForAddressID(addrID string) (*crypto.KeyRing, error) { |
|
if kr, ok := c.addrKeyRing[addrID]; ok { |
|
return kr, nil |
|
} |
|
|
|
return nil, errors.New("no keyring available") |
|
}
|
|
|