// Copyright (c) 2022 Proton AG // // This file is part of Proton Mail Bridge.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 mocks import ( "bufio" "bytes" "fmt" "io" "net" "os" "strings" "sync" "time" "github.com/ProtonMail/go-rfc5322" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/text/cases" "golang.org/x/text/language" ) type SMTPClient struct { lock *sync.Mutex debug *debug t TestingT conn net.Conn response *bufio.Reader address string } func NewSMTPClient(t TestingT, tag, smtpAddr string) *SMTPClient { conn, err := net.Dial("tcp", smtpAddr) require.NoError(t, err) if err != nil { return &SMTPClient{} } logrus.WithField("addr", conn.LocalAddr().String()). WithField("tag", tag). Debug("SMTP Dialed") response := bufio.NewReader(conn) // Read first response to opening connection. _, err = response.ReadString('\n') assert.NoError(t, err) return &SMTPClient{ lock: &sync.Mutex{}, debug: newDebug(tag), t: t, conn: conn, response: response, } } func (c *SMTPClient) Close() { c.lock.Lock() defer c.lock.Unlock() _ = c.conn.Close() } func (c *SMTPClient) SendCommands(commands ...string) *SMTPResponse { c.lock.Lock() defer c.lock.Unlock() smtpResponse := &SMTPResponse{t: c.t} for _, command := range commands { command = strings.ReplaceAll(command, "[userAddress]", c.address) command = strings.ReplaceAll(command, "[userAddress|capitalize]", cases.Title(language.Und).String(c.address)) tstart := time.Now() c.debug.printReq(command) fmt.Fprintf(c.conn, "%s\r\n", command) message, err := c.response.ReadString('\n') if err != nil { smtpResponse.err = fmt.Errorf("read response failed: %v", err) c.debug.printErr(smtpResponse.err.Error() + "\n") return smtpResponse } // Message contains code and message. Codes 4xx and 5xx are bad ones, except "500 Speak up". if strings.HasPrefix(message, "4") || strings.HasPrefix(message, "5") { c.debug.printErr(message) err := errors.New(strings.Trim(message, "\r\n")) smtpResponse.err = errors.Wrap(err, "SMTP error") return smtpResponse } else if command != "" && len(message) == 0 { err := errors.New("empty answer") smtpResponse.err = errors.Wrap(err, "SMTP error") return smtpResponse } c.debug.printRes(message) smtpResponse.result = message c.debug.printTime(time.Since(tstart)) } return smtpResponse } // Auth func (c *SMTPClient) Login(account, password string) *SMTPResponse { c.address = account return c.SendCommands( "HELO ATEIST.TEST", "AUTH LOGIN", base64(account), base64(password), ) } func (c *SMTPClient) Logout() *SMTPResponse { return c.SendCommands("QUIT") } // Sending func (c *SMTPClient) EML(fileName, bcc string) *SMTPResponse { f, err := os.Open(fileName) //nolint:gosec if err != nil { panic(fmt.Errorf("smtp eml open: %s", err)) } defer f.Close() //nolint:errcheck,gosec return c.SendMail(f, bcc) } func (c *SMTPClient) SendMail(r io.Reader, bcc string) *SMTPResponse { var message, from string var tos []string if bcc != "" { tos = append(tos, bcc) } scanner := bufio.NewScanner(r) for scanner.Scan() { line := string(bytes.Trim(scanner.Bytes(), "\r\n")) // Make sure no line ending is there. message += line + "\r\n" from = c.address if from == "" && strings.HasPrefix(line, "From: ") { if addrs, err := rfc5322.ParseAddressList(line[6:]); err == nil { from = addrs[0].Address } } if strings.HasPrefix(line, "To: ") || strings.HasPrefix(line, "CC: ") { if addrs, err := rfc5322.ParseAddressList(line[4:]); err == nil { for _, addr := range addrs { tos = append(tos, addr.Address) } } } } if err := scanner.Err(); err != nil { panic(fmt.Errorf("smtp eml scan: %s", err)) } if from == "" { panic(fmt.Errorf("smtp eml no from")) } if len(tos) == 0 { panic(fmt.Errorf("smtp eml no to")) } commands := []string{ fmt.Sprintf("MAIL FROM:<%s>", from), } for _, to := range tos { commands = append(commands, fmt.Sprintf("RCPT TO:<%s>", to)) } commands = append(commands, "DATA", message+"\r\n.") // Message ending. return c.SendCommands(commands...) }