feat(GODT-2799): SMTP Service

Refactor code to isolate the SMTP functionality in a dedicated SMTP
service for each user as discussed in the Bridge Service Architecture
RFC.

Some shared types have been moved from `user` to `usertypes` so that
they can be shared with Service and User Code.

Finally due to lack of recursive imports, the user data SMTP needs
access to is hidden behind an interface until the User Identity service
is implemented.
This commit is contained in:
Leander Beernaert
2023-07-18 10:08:13 +02:00
parent f454f1a74f
commit 3ef526333a
21 changed files with 351 additions and 184 deletions

View File

@ -40,7 +40,10 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/services/sendrecorder"
"github.com/ProtonMail/proton-bridge/v3/internal/services/smtp"
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
"github.com/ProtonMail/proton-bridge/v3/internal/usertypes"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/algo"
"github.com/bradenaw/juniper/xslices"
@ -65,7 +68,7 @@ type User struct {
vault *vault.User
client *proton.Client
reporter reporter.Reporter
sendHash *sendRecorder
sendHash *sendrecorder.SendRecorder
eventCh *async.QueuedChannel[events.Event]
eventLock safe.RWMutex
@ -100,6 +103,8 @@ type User struct {
telemetryManager telemetry.Availability
// goStatusProgress triggers a check/sending if progress is needed.
goStatusProgress func()
smtpService *smtp.Service
}
// New returns a new user.
@ -148,7 +153,7 @@ func New(
vault: encVault,
client: client,
reporter: reporter,
sendHash: newSendRecorder(sendEntryExpiry),
sendHash: sendrecorder.NewSendRecorder(sendrecorder.SendEntryExpiry),
eventCh: async.NewQueuedChannel[events.Event](0, 0, crashHandler),
eventLock: safe.NewRWMutex(),
@ -156,10 +161,10 @@ func New(
apiUser: apiUser,
apiUserLock: safe.NewRWMutex(),
apiAddrs: groupBy(apiAddrs, func(addr proton.Address) string { return addr.ID }),
apiAddrs: usertypes.GroupBy(apiAddrs, func(addr proton.Address) string { return addr.ID }),
apiAddrsLock: safe.NewRWMutex(),
apiLabels: groupBy(apiLabels, func(label proton.Label) string { return label.ID }),
apiLabels: usertypes.GroupBy(apiLabels, func(label proton.Label) string { return label.ID }),
apiLabelsLock: safe.NewRWMutex(),
updateCh: make(map[string]*async.QueuedChannel[imap.Update]),
@ -178,6 +183,8 @@ func New(
telemetryManager: telemetryManager,
}
user.smtpService = smtp.NewService(user, client, user.sendHash, user.panicHandler, user.reporter)
// Check for status_progress when triggered.
user.goStatusProgress = user.tasks.PeriodicOrTrigger(configstatus.ProgressCheckInterval, 0, func(ctx context.Context) {
user.SendConfigStatusProgress(ctx)
@ -264,6 +271,9 @@ func New(
}
})
// Start SMTP Service
user.smtpService.Start(user.tasks)
return user, nil
}
@ -461,7 +471,7 @@ func (user *User) NewIMAPConnectors() (map[string]connector.Connector, error) {
switch user.vault.AddressMode() {
case vault.CombinedMode:
primAddr, err := getAddrIdx(user.apiAddrs, 0)
primAddr, err := usertypes.GetAddrIdx(user.apiAddrs, 0)
if err != nil {
return nil, fmt.Errorf("failed to get primary address: %w", err)
}
@ -485,10 +495,10 @@ func (user *User) SendMail(authID string, from string, to []string, r io.Reader)
}
if len(to) == 0 {
return ErrInvalidRecipient
return smtp.ErrInvalidRecipient
}
if err := user.sendMail(authID, from, to, r); err != nil {
if err := user.smtpService.SendMail(context.Background(), authID, from, to, r); err != nil {
if apiErr := new(proton.APIError); errors.As(err, &apiErr) {
logrus.WithError(apiErr).WithField("Details", apiErr.DetailsToString()).Error("failed to send message")
}
@ -664,6 +674,12 @@ func (user *User) SendTelemetry(ctx context.Context, data []byte) error {
return nil
}
func (user *User) WithSMTPData(ctx context.Context, op func(context.Context, map[string]proton.Address, proton.User, *vault.User) error) error {
return safe.RLockRet(func() error {
return op(ctx, user.apiAddrs, user.apiUser, user.vault)
}, user.apiUserLock, user.apiAddrsLock, user.eventLock)
}
// initUpdateCh initializes the user's update channels in the given address mode.
// It is assumed that user.apiAddrs and user.updateCh are already locked.
func (user *User) initUpdateCh(mode vault.AddressMode) {