// Copyright (c) 2024 Proton AG // // This file is part of Proton Mail Bridge. // // Proton Mail 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. // // Proton Mail 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 Proton Mail Bridge. If not, see . package smtp import ( "context" "io" "sync" "time" ) type Accounts struct { accountsLock sync.RWMutex accounts map[string]*smtpAccountState } const maxFailedCommands = 3 const defaultErrTimeout = 20 * time.Second const successiveErrInterval = time.Second func NewAccounts() *Accounts { return &Accounts{ accounts: make(map[string]*smtpAccountState), } } func (s *Accounts) AddAccount(account *Service) { s.accountsLock.Lock() defer s.accountsLock.Unlock() s.accounts[account.UserID()] = &smtpAccountState{ service: account, errTimeout: defaultErrTimeout, } } func (s *Accounts) RemoveAccount(account *Service) { s.accountsLock.Lock() defer s.accountsLock.Unlock() delete(s.accounts, account.UserID()) } func (s *Accounts) CheckAuth(user string, password []byte) (string, string, error) { s.accountsLock.RLock() defer s.accountsLock.RUnlock() for id, account := range s.accounts { addrID, err := account.service.checkAuth(context.Background(), user, password) if err != nil { continue } return id, addrID, nil } return "", "", ErrNoSuchUser } func (s *Accounts) SendMail(ctx context.Context, userID, addrID, from string, to []string, r io.Reader) error { if len(to) == 0 { return ErrInvalidRecipient } s.accountsLock.RLock() defer s.accountsLock.RUnlock() requestTime := time.Now() account, ok := s.accounts[userID] if !ok { return ErrNoSuchUser } if err := account.canMakeRequest(requestTime); err != nil { return err } err := account.service.SendMail(ctx, addrID, from, to, r) account.handleSMTPErr(requestTime, err) return err } type smtpAccountState struct { service *Service errTimeout time.Duration errLock sync.Mutex errCounter int lastRequest time.Time } func (s *smtpAccountState) canMakeRequest(requestTime time.Time) error { s.errLock.Lock() defer s.errLock.Unlock() if s.errCounter >= maxFailedCommands { if requestTime.Sub(s.lastRequest) >= s.errTimeout { s.errCounter = 0 return nil } return ErrTooManyErrors } return nil } func (s *smtpAccountState) handleSMTPErr(requestTime time.Time, err error) { s.errLock.Lock() defer s.errLock.Unlock() if err == nil || requestTime.Sub(s.lastRequest) > successiveErrInterval { s.errCounter = 0 } else { s.errCounter++ } s.lastRequest = requestTime }