feat(GODT-3185): report cases which leads to wrong address key used

This commit is contained in:
Jakub Cuth
2024-03-13 07:49:25 +00:00
parent d2fbbc3e25
commit 6fadbde4a6
18 changed files with 358 additions and 23 deletions

View File

@ -750,7 +750,7 @@ func TestBridge_SendAddressDisabled(t *testing.T) {
strings.NewReader("Subject: Test 1\r\n\r\nHello world!"),
)
smtpErr := smtpservice.NewErrCanNotSendOnAddress(senderInfo.Addresses[0])
smtpErr := smtpservice.NewErrCannotSendFromAddress(senderInfo.Addresses[0])
require.Equal(t, fmt.Sprintf("Error: %v", smtpErr.Error()), err.Error())
})
})

View File

@ -23,12 +23,15 @@ import (
"errors"
"fmt"
"net/mail"
"strings"
"sync/atomic"
"time"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/gluon/connector"
"github.com/ProtonMail/gluon/imap"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/gluon/rfc5322"
"github.com/ProtonMail/gluon/rfc822"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/gopenpgp/v2/crypto"
@ -54,6 +57,7 @@ type Connector struct {
identityState sharedIdentity
client APIClient
telemetry Telemetry
reporter reporter.Reporter
panicHandler async.PanicHandler
sendRecorder *sendrecorder.SendRecorder
@ -75,6 +79,7 @@ func NewConnector(
sendRecorder *sendrecorder.SendRecorder,
panicHandler async.PanicHandler,
telemetry Telemetry,
reporter reporter.Reporter,
showAllMail bool,
syncState *SyncState,
) *Connector {
@ -90,6 +95,7 @@ func NewConnector(
client: apiClient,
telemetry: telemetry,
reporter: reporter,
panicHandler: panicHandler,
sendRecorder: sendRecorder,
@ -279,7 +285,7 @@ func (s *Connector) CreateMessage(ctx context.Context, _ connector.IMAPStateWrit
if messageID, ok, err := s.sendRecorder.HasEntryWait(ctx, hash, time.Now().Add(90*time.Second), toList); err != nil {
return imap.Message{}, nil, fmt.Errorf("failed to check send hash: %w", err)
} else if ok {
s.log.WithField("messageID", messageID).Warn("Message already sent")
s.log.WithField("messageID", messageID).Warn("Message already in sent mailbox")
// Query the server-side message.
full, err := s.client.GetFullMessage(ctx, messageID, usertypes.NewProtonAPIScheduler(s.panicHandler), proton.NewDefaultAttachmentAllocator())
@ -671,11 +677,21 @@ func (s *Connector) importMessage(
) (imap.Message, []byte, error) {
var full proton.FullMessage
// addr is primary for combined mode or active for split mode
addr, ok := s.identityState.GetAddress(s.addrID)
if !ok {
return imap.Message{}, nil, fmt.Errorf("could not find address")
}
p, err2 := parser.New(bytes.NewReader(literal))
if err2 != nil {
return imap.Message{}, nil, fmt.Errorf("failed to parse literal: %w", err2)
}
isDraft := slices.Contains(labelIDs, proton.DraftsLabel)
s.reportGODT3185(isDraft, addr.Email, p, s.addressMode == usertypes.AddressModeCombined)
if err := s.identityState.WithAddrKR(s.addrID, func(_, addrKR *crypto.KeyRing) error {
primaryKey, errKey := addrKR.FirstKey()
if errKey != nil {
@ -683,11 +699,8 @@ func (s *Connector) importMessage(
}
var messageID string
p, err2 := parser.New(bytes.NewReader(literal))
if err2 != nil {
return fmt.Errorf("failed to parse literal: %w", err2)
}
if slices.Contains(labelIDs, proton.DraftsLabel) {
if isDraft {
msg, err := s.createDraftWithParser(ctx, p, primaryKey, addr)
if err != nil {
return fmt.Errorf("failed to create draft: %w", err)
@ -850,3 +863,94 @@ func defaultMailboxPermanentFlags() imap.FlagSet {
func defaultMailboxAttributes() imap.FlagSet {
return imap.NewFlagSet()
}
func stripPlusAlias(a string) string {
iPlus := strings.Index(a, "+")
iAt := strings.Index(a, "@")
if iPlus <= 0 || iAt <= 0 || iPlus >= iAt {
return a
}
return a[:iPlus] + a[iAt:]
}
func equalAddresses(a, b string) bool {
return strings.EqualFold(stripPlusAlias(a), stripPlusAlias(b))
}
func (s *Connector) reportGODT3185(isDraft bool, defaultAddr string, p *parser.Parser, isCombinedMode bool) {
reportAction := "draft"
if !isDraft {
reportAction = "import"
}
reportMode := "combined"
if isCombinedMode {
reportMode = "split"
}
senderAddr := ""
if p != nil && p.Root() != nil && p.Root().Header.Len() != 0 {
addrField := p.Root().Header.Get("From")
if addrField == "" {
addrField = p.Root().Header.Get("Sender")
}
if addrField != "" {
sender, err := rfc5322.ParseAddressList(addrField)
if err == nil && len(sender) > 0 {
senderAddr = sender[0].Address
} else {
s.log.WithError(err).Warn("Invalid sender address in reporter")
}
}
}
if equalAddresses(defaultAddr, senderAddr) {
return
}
isDisabled := false
isUserAddress := false
for _, a := range s.identityState.GetAddresses() {
if !equalAddresses(a.Email, senderAddr) {
continue
}
isUserAddress = true
isDisabled = !bool(a.Send) || (a.Status != proton.AddressStatusEnabled)
break
}
if !isUserAddress && senderAddr != "" {
return
}
reportResult := "using sender address"
if !isCombinedMode {
reportResult = "error address not match"
}
reportAddress := ""
if senderAddr == "" {
reportAddress = " invalid"
reportResult = "error import/draft"
}
if isDisabled {
reportAddress = " disabled"
if isDraft {
reportResult = "error draft"
}
}
report := fmt.Sprintf(
"GODT-3185: %s with non-default%s address in %s mode: %s",
reportAction, reportAddress, reportMode, reportResult,
)
s.log.Warn(report)
if s.reporter != nil {
_ = s.reporter.ReportMessage(report)
}
}

View File

@ -203,3 +203,35 @@ func TestFixGODT3003Labels_Noop(t *testing.T) {
require.NoError(t, err)
require.False(t, applied)
}
func TestStripPlusAlias(t *testing.T) {
cases := map[string]string{
"one@three.com": "one@three.com",
"one+two@three.com": "one@three.com",
"one@three+two.com": "one@three+two.com",
"+one@three.com": "+one@three.com",
"@three.com": "@three.com",
}
for given, want := range cases {
require.Equal(t, want, stripPlusAlias(given), "input was %q", given)
}
}
func TestEqualAddresse(t *testing.T) {
cases := []struct {
a, b string
want bool
}{
{"one@three.com", "one@three.com", true},
{"one@three.com", "one+two@three.com", true},
{"OnE@thReE.com", "One@THree.com", true},
{"one@three.com", "two@three.com", false},
{"one+two@three.com", "two@three.com", false},
{"one@three.com", "one@three+two.com", false},
}
for _, c := range cases {
require.Equal(t, c.want, equalAddresses(c.a, c.b), "input was %q and %q", c.a, c.b)
}
}

View File

@ -508,6 +508,7 @@ func (s *Service) buildConnectors() (map[string]*Connector, error) {
s.sendRecorder,
s.panicHandler,
s.telemetry,
s.reporter,
s.showAllMail,
s.syncStateProvider,
)
@ -525,6 +526,7 @@ func (s *Service) buildConnectors() (map[string]*Connector, error) {
s.sendRecorder,
s.panicHandler,
s.telemetry,
s.reporter,
s.showAllMail,
s.syncStateProvider,
)

View File

@ -155,6 +155,7 @@ func addNewAddressSplitMode(ctx context.Context, s *Service, addrID string) erro
s.sendRecorder,
s.panicHandler,
s.telemetry,
s.reporter,
s.showAllMail,
s.syncStateProvider,
)

View File

@ -27,14 +27,14 @@ var ErrInvalidReturnPath = errors.New("invalid return path")
var ErrNoSuchUser = errors.New("no such user")
var ErrTooManyErrors = errors.New("too many failed requests, please try again later")
type ErrCanNotSendOnAddress struct {
type ErrCannotSendFromAddress struct {
address string
}
func NewErrCanNotSendOnAddress(address string) *ErrCanNotSendOnAddress {
return &ErrCanNotSendOnAddress{address: address}
func NewErrCannotSendFromAddress(address string) *ErrCannotSendFromAddress {
return &ErrCannotSendFromAddress{address: address}
}
func (e ErrCanNotSendOnAddress) Error() string {
return fmt.Sprintf("can't send on address: %v", e.address)
func (e ErrCannotSendFromAddress) Error() string {
return fmt.Sprintf("cannot send from address: %v", e.address)
}

View File

@ -103,8 +103,8 @@ func (s *Service) smtpSendMail(ctx context.Context, authID string, from string,
}
if !fromAddr.Send || fromAddr.Status != proton.AddressStatusEnabled {
s.log.Errorf("Can't send emails on address: %v", fromAddr.Email)
return &ErrCanNotSendOnAddress{address: fromAddr.Email}
s.log.Errorf("Cannot send emails from address: %v", fromAddr.Email)
return &ErrCannotSendFromAddress{address: fromAddr.Email}
}
// Load the user's mail settings.