forked from Silverfish/proton-bridge
We build too many walls and not enough bridges
This commit is contained in:
78
test/mocks/debug.go
Normal file
78
test/mocks/debug.go
Normal file
@ -0,0 +1,78 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.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 mocks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/logrusorgru/aurora"
|
||||
)
|
||||
|
||||
type debug struct {
|
||||
verbosity int
|
||||
reqTag string
|
||||
}
|
||||
|
||||
func newDebug(reqTag string) *debug {
|
||||
return &debug{
|
||||
verbosity: verbosityLevelFromEnv(),
|
||||
reqTag: reqTag,
|
||||
}
|
||||
}
|
||||
|
||||
func verbosityLevelFromEnv() int {
|
||||
verbosityName := os.Getenv("VERBOSITY")
|
||||
switch strings.ToLower(verbosityName) {
|
||||
case "error", "fatal", "panic":
|
||||
return 0
|
||||
case "warning":
|
||||
return 1
|
||||
case "info":
|
||||
return 2
|
||||
case "debug", "trace":
|
||||
return 3
|
||||
}
|
||||
return 2
|
||||
}
|
||||
|
||||
func (d *debug) printReq(command string) {
|
||||
if d.verbosity > 0 {
|
||||
fmt.Println(aurora.Green(fmt.Sprintf("Req %s: %s", d.reqTag, command)))
|
||||
}
|
||||
}
|
||||
|
||||
func (d *debug) printRes(line string) {
|
||||
if d.verbosity > 1 {
|
||||
line = strings.ReplaceAll(line, "\n", "")
|
||||
line = strings.ReplaceAll(line, "\r", "")
|
||||
fmt.Println(aurora.Cyan(fmt.Sprintf("Res %s: %s", d.reqTag, line)))
|
||||
}
|
||||
}
|
||||
|
||||
func (d *debug) printErr(line string) {
|
||||
fmt.Print(aurora.Bold(aurora.Red(fmt.Sprintf("Res %s: %s", d.reqTag, line))))
|
||||
}
|
||||
|
||||
func (d *debug) printTime(diff time.Duration) {
|
||||
if d.verbosity > 0 {
|
||||
fmt.Println(aurora.Green(fmt.Sprintf("Time %s:%v", d.reqTag, diff)))
|
||||
}
|
||||
}
|
||||
227
test/mocks/imap.go
Normal file
227
test/mocks/imap.go
Normal file
@ -0,0 +1,227 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.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 mocks
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type IMAPClient struct {
|
||||
lock *sync.Mutex
|
||||
debug *debug
|
||||
t TestingT
|
||||
reqTag string
|
||||
reqIndex int // global request index for this client
|
||||
conn net.Conn
|
||||
response *bufio.Reader
|
||||
idling bool
|
||||
}
|
||||
|
||||
func NewIMAPClient(t TestingT, tag string, imapAddr string) *IMAPClient {
|
||||
conn, err := net.Dial("tcp", imapAddr)
|
||||
require.NoError(t, err)
|
||||
response := bufio.NewReader(conn)
|
||||
|
||||
// Read first response to opening connection.
|
||||
_, err = response.ReadString('\n')
|
||||
assert.NoError(t, err)
|
||||
|
||||
return &IMAPClient{
|
||||
lock: &sync.Mutex{},
|
||||
debug: newDebug(tag),
|
||||
t: t,
|
||||
reqTag: tag,
|
||||
reqIndex: 0,
|
||||
conn: conn,
|
||||
response: response,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *IMAPClient) Close() {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
if c.idling {
|
||||
c.StopIDLE()
|
||||
}
|
||||
_ = c.conn.Close()
|
||||
}
|
||||
|
||||
func (c *IMAPClient) SendCommand(command string) *IMAPResponse {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
imapResponse := &IMAPResponse{t: c.t}
|
||||
go imapResponse.sendCommand(c.reqTag, c.reqIndex, command, c.debug, c.conn, c.response)
|
||||
c.reqIndex++
|
||||
|
||||
return imapResponse
|
||||
}
|
||||
|
||||
// Auth
|
||||
|
||||
func (c *IMAPClient) Login(account, password string) *IMAPResponse {
|
||||
return c.SendCommand(fmt.Sprintf("LOGIN %s %s", account, password))
|
||||
}
|
||||
|
||||
func (c *IMAPClient) Logout() *IMAPResponse {
|
||||
return c.SendCommand("LOGOUT")
|
||||
}
|
||||
|
||||
// Mailboxes
|
||||
|
||||
func (c *IMAPClient) ListMailboxes() *IMAPResponse {
|
||||
return c.SendCommand("LIST \"\" *")
|
||||
}
|
||||
|
||||
func (c *IMAPClient) Select(mailboxName string) *IMAPResponse {
|
||||
return c.SendCommand(fmt.Sprintf("SELECT \"%s\"", mailboxName)) //nolint[gosec]
|
||||
}
|
||||
|
||||
func (c *IMAPClient) CreateMailbox(mailboxName string) *IMAPResponse {
|
||||
return c.SendCommand(fmt.Sprintf("CREATE \"%s\"", mailboxName))
|
||||
}
|
||||
|
||||
func (c *IMAPClient) DeleteMailbox(mailboxName string) *IMAPResponse {
|
||||
return c.SendCommand(fmt.Sprintf("DELETE \"%s\"", mailboxName)) //nolint[gosec]
|
||||
}
|
||||
|
||||
func (c *IMAPClient) RenameMailbox(mailboxName, newMailboxName string) *IMAPResponse {
|
||||
return c.SendCommand(fmt.Sprintf("RENAME \"%s\" \"%s\"", mailboxName, newMailboxName))
|
||||
}
|
||||
|
||||
func (c *IMAPClient) GetMailboxInfo(mailboxName string) *IMAPResponse {
|
||||
return c.SendCommand(fmt.Sprintf("EXAMINE \"%s\"", mailboxName))
|
||||
}
|
||||
|
||||
func (c *IMAPClient) GetMailboxStatus(mailboxName string) *IMAPResponse {
|
||||
return c.SendCommand(fmt.Sprintf("STATUS \"%s\" (MESSAGES UNSEEN UIDNEXT UIDVALIDITY)", mailboxName))
|
||||
}
|
||||
|
||||
// Messages
|
||||
|
||||
func (c *IMAPClient) FetchAllFlags() *IMAPResponse {
|
||||
return c.FetchAll("flags")
|
||||
}
|
||||
|
||||
func (c *IMAPClient) FetchAllSubjects() *IMAPResponse {
|
||||
return c.FetchAll("body.peek[header.fields (subject)]")
|
||||
}
|
||||
|
||||
func (c *IMAPClient) FetchAllHeaders() *IMAPResponse {
|
||||
return c.FetchAll("body.peek[header]")
|
||||
}
|
||||
|
||||
func (c *IMAPClient) FetchAllBodyStructures() *IMAPResponse {
|
||||
return c.FetchAll("bodystructure")
|
||||
}
|
||||
|
||||
func (c *IMAPClient) FetchAllSizes() *IMAPResponse {
|
||||
return c.FetchAll("rfc822.size")
|
||||
}
|
||||
|
||||
func (c *IMAPClient) FetchAllBodies() *IMAPResponse {
|
||||
return c.FetchAll("rfc822")
|
||||
}
|
||||
|
||||
func (c *IMAPClient) FetchAll(parts string) *IMAPResponse {
|
||||
return c.Fetch("1:*", parts)
|
||||
}
|
||||
|
||||
func (c *IMAPClient) Fetch(ids, parts string) *IMAPResponse {
|
||||
return c.SendCommand(fmt.Sprintf("FETCH %s %s", ids, parts))
|
||||
}
|
||||
|
||||
func (c *IMAPClient) FetchUID(ids, parts string) *IMAPResponse {
|
||||
return c.SendCommand(fmt.Sprintf("UID FETCH %s %s", ids, parts))
|
||||
}
|
||||
|
||||
func (c *IMAPClient) Search(query string) *IMAPResponse {
|
||||
return c.SendCommand(fmt.Sprintf("SEARCH %s", query))
|
||||
}
|
||||
|
||||
// Message
|
||||
|
||||
func (c *IMAPClient) Append(mailboxName, subject, from, to, body string) *IMAPResponse {
|
||||
msg := fmt.Sprintf("Subject: %s\r\n", subject)
|
||||
msg += fmt.Sprintf("From: %s\r\n", from)
|
||||
msg += fmt.Sprintf("To: %s\r\n", to)
|
||||
msg += "\r\n"
|
||||
msg += body
|
||||
msg += "\r\n"
|
||||
|
||||
cmd := fmt.Sprintf("APPEND \"%s\" (\\Seen) \"25-Mar-2021 00:30:00 +0100\" {%d}\r\n%s", mailboxName, len(msg), msg)
|
||||
return c.SendCommand(cmd)
|
||||
}
|
||||
|
||||
func (c *IMAPClient) Delete(ids string) *IMAPResponse {
|
||||
return c.AddFlags(ids, "\\Deleted")
|
||||
}
|
||||
|
||||
func (c *IMAPClient) Copy(ids, newMailboxName string) *IMAPResponse {
|
||||
return c.SendCommand(fmt.Sprintf("COPY %s \"%s\"", ids, newMailboxName))
|
||||
}
|
||||
|
||||
func (c *IMAPClient) Move(ids, newMailboxName string) *IMAPResponse {
|
||||
return c.SendCommand(fmt.Sprintf("MOVE %s \"%s\"", ids, newMailboxName))
|
||||
}
|
||||
|
||||
func (c *IMAPClient) MarkAsRead(ids string) *IMAPResponse {
|
||||
return c.AddFlags(ids, "\\Seen")
|
||||
}
|
||||
|
||||
func (c *IMAPClient) MarkAsUnread(ids string) *IMAPResponse {
|
||||
return c.RemoveFlags(ids, "\\Seen")
|
||||
}
|
||||
|
||||
func (c *IMAPClient) MarkAsStarred(ids string) *IMAPResponse {
|
||||
return c.AddFlags(ids, "\\Flagged")
|
||||
}
|
||||
|
||||
func (c *IMAPClient) MarkAsUnstarred(ids string) *IMAPResponse {
|
||||
return c.RemoveFlags(ids, "\\Flagged")
|
||||
}
|
||||
|
||||
func (c *IMAPClient) AddFlags(ids, flags string) *IMAPResponse {
|
||||
return c.changeFlags(ids, flags, "+")
|
||||
}
|
||||
|
||||
func (c *IMAPClient) RemoveFlags(ids, flags string) *IMAPResponse {
|
||||
return c.changeFlags(ids, flags, "-")
|
||||
}
|
||||
|
||||
func (c *IMAPClient) changeFlags(ids, flags, op string) *IMAPResponse {
|
||||
return c.SendCommand(fmt.Sprintf("STORE %s %sflags (%s)", ids, op, flags))
|
||||
}
|
||||
|
||||
// IDLE
|
||||
|
||||
func (c *IMAPClient) StartIDLE() *IMAPResponse {
|
||||
c.idling = true
|
||||
return c.SendCommand("IDLE")
|
||||
}
|
||||
|
||||
func (c *IMAPClient) StopIDLE() {
|
||||
c.idling = false
|
||||
fmt.Fprintf(c.conn, "%s\r\n", "DONE")
|
||||
}
|
||||
199
test/mocks/imap_response.go
Normal file
199
test/mocks/imap_response.go
Normal file
@ -0,0 +1,199 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.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 mocks
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
a "github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type IMAPResponse struct {
|
||||
t TestingT
|
||||
err error
|
||||
result string
|
||||
sections []string
|
||||
done bool
|
||||
}
|
||||
|
||||
func (ir *IMAPResponse) sendCommand(reqTag string, reqIndex int, command string, debug *debug, conn io.Writer, response *bufio.Reader) {
|
||||
defer func() { ir.done = true }()
|
||||
|
||||
tstart := time.Now()
|
||||
|
||||
commandID := fmt.Sprintf("%sO%0d", reqTag, reqIndex)
|
||||
command = fmt.Sprintf("%s %s", commandID, command)
|
||||
|
||||
debug.printReq(command)
|
||||
fmt.Fprintf(conn, "%s\r\n", command)
|
||||
|
||||
var section string
|
||||
for {
|
||||
line, err := response.ReadString('\n')
|
||||
|
||||
if err != nil {
|
||||
ir.err = errors.Wrap(err, "read response failed")
|
||||
debug.printErr(ir.err.Error() + "\n")
|
||||
return
|
||||
}
|
||||
|
||||
// Finishing line contains `commandID` following with status (`NO`, `BAD`, ...) and then message itself.
|
||||
lineWithoutID := strings.Replace(line, commandID+" ", "", 1)
|
||||
if strings.HasPrefix(line, commandID) && (strings.HasPrefix(lineWithoutID, "NO ") || strings.HasPrefix(lineWithoutID, "BAD ")) {
|
||||
debug.printErr(line)
|
||||
err := errors.New(strings.Trim(lineWithoutID, "\r\n"))
|
||||
ir.err = errors.Wrap(err, "IMAP error")
|
||||
return
|
||||
} else if command != "" && len(line) == 0 {
|
||||
err := errors.New("empty answer")
|
||||
ir.err = errors.Wrap(err, "IMAP error")
|
||||
debug.printErr(ir.err.Error() + "\n")
|
||||
return
|
||||
}
|
||||
debug.printRes(line)
|
||||
|
||||
if strings.HasPrefix(line, "* ") { //nolint[gocritic]
|
||||
if section != "" {
|
||||
ir.sections = append(ir.sections, section)
|
||||
}
|
||||
section = line
|
||||
} else if strings.HasPrefix(line, commandID) {
|
||||
if section != "" {
|
||||
ir.sections = append(ir.sections, section)
|
||||
}
|
||||
ir.result = line
|
||||
break
|
||||
} else {
|
||||
section += line
|
||||
}
|
||||
}
|
||||
|
||||
debug.printTime(time.Since(tstart))
|
||||
}
|
||||
|
||||
func (ir *IMAPResponse) wait() {
|
||||
for {
|
||||
if ir.done {
|
||||
break
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func (ir *IMAPResponse) AssertOK() *IMAPResponse {
|
||||
ir.wait()
|
||||
a.NoError(ir.t, ir.err)
|
||||
return ir
|
||||
}
|
||||
|
||||
func (ir *IMAPResponse) AssertError(wantErrMsg string) *IMAPResponse {
|
||||
ir.wait()
|
||||
if ir.err == nil {
|
||||
a.Fail(ir.t, "Expected error %s", wantErrMsg)
|
||||
} else {
|
||||
a.Regexp(ir.t, wantErrMsg, ir.err.Error(), "Expected error %s but got %s", wantErrMsg, ir.err)
|
||||
}
|
||||
return ir
|
||||
}
|
||||
|
||||
func (ir *IMAPResponse) AssertSectionsCount(expectedCount int) *IMAPResponse {
|
||||
ir.wait()
|
||||
a.Equal(ir.t, expectedCount, len(ir.sections))
|
||||
return ir
|
||||
}
|
||||
|
||||
// AssertSectionsInOrder checks sections against regular expression in exact order.
|
||||
// First regexp checks first section, second the second and so on. If there is
|
||||
// more responses (sections) than expected regexps, that's OK.
|
||||
func (ir *IMAPResponse) AssertSectionsInOrder(wantRegexps ...string) *IMAPResponse {
|
||||
ir.wait()
|
||||
if !a.True(ir.t,
|
||||
len(ir.sections) >= len(wantRegexps),
|
||||
"Wrong number of sections, want %v, got %v",
|
||||
len(wantRegexps),
|
||||
len(ir.sections),
|
||||
) {
|
||||
return ir
|
||||
}
|
||||
|
||||
for idx, wantRegexp := range wantRegexps {
|
||||
section := ir.sections[idx]
|
||||
match, err := regexp.MatchString(wantRegexp, section)
|
||||
if !a.NoError(ir.t, err) {
|
||||
return ir
|
||||
}
|
||||
if !a.True(ir.t, match, "Section does not match given regex", section, wantRegexp) {
|
||||
return ir
|
||||
}
|
||||
}
|
||||
return ir
|
||||
}
|
||||
|
||||
// AssertSections is similar to AssertSectionsInOrder but is not strict to the order.
|
||||
// It means it just tries to find all "regexps" in the response.
|
||||
func (ir *IMAPResponse) AssertSections(wantRegexps ...string) *IMAPResponse {
|
||||
ir.wait()
|
||||
for _, wantRegexp := range wantRegexps {
|
||||
a.NoError(ir.t, ir.hasSectionRegexp(wantRegexp), "regexp %v not found", wantRegexp)
|
||||
}
|
||||
return ir
|
||||
}
|
||||
|
||||
// WaitForSections is the same as AssertSections but waits for `timeout` before giving up.
|
||||
func (ir *IMAPResponse) WaitForSections(timeout time.Duration, wantRegexps ...string) {
|
||||
a.Eventually(ir.t, func() bool {
|
||||
return ir.HasSections(wantRegexps...)
|
||||
}, timeout, 50*time.Millisecond, "Wanted sections: %v\nSections: %v", wantRegexps, &ir.sections)
|
||||
}
|
||||
|
||||
// WaitForNotSections is the opposite of WaitForSection: waits to not have the response.
|
||||
func (ir *IMAPResponse) WaitForNotSections(timeout time.Duration, unwantedRegexps ...string) *IMAPResponse {
|
||||
time.Sleep(timeout)
|
||||
match := ir.HasSections(unwantedRegexps...)
|
||||
a.False(ir.t, match, "Unwanted sections: %v\nSections: %v", unwantedRegexps, &ir.sections)
|
||||
return ir
|
||||
}
|
||||
|
||||
// HasSections is the same as AssertSections but only returns bool (do not uses testingT).
|
||||
func (ir *IMAPResponse) HasSections(wantRegexps ...string) bool {
|
||||
for _, wantRegexp := range wantRegexps {
|
||||
if err := ir.hasSectionRegexp(wantRegexp); err != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (ir *IMAPResponse) hasSectionRegexp(wantRegexp string) error {
|
||||
for _, section := range ir.sections {
|
||||
match, err := regexp.MatchString(wantRegexp, section)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if match {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("Section matching given regex not found")
|
||||
}
|
||||
182
test/mocks/smtp.go
Normal file
182
test/mocks/smtp.go
Normal file
@ -0,0 +1,182 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.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 mocks
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/mail"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
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)
|
||||
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 {
|
||||
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)
|
||||
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]
|
||||
|
||||
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 addr, err := mail.ParseAddress(line[6:]); err == nil {
|
||||
from = addr.Address
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(line, "To: ") || strings.HasPrefix(line, "CC: ") {
|
||||
if addrs, err := mail.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...)
|
||||
}
|
||||
42
test/mocks/smtp_response.go
Normal file
42
test/mocks/smtp_response.go
Normal file
@ -0,0 +1,42 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.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 mocks
|
||||
|
||||
import (
|
||||
a "github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type SMTPResponse struct {
|
||||
t TestingT
|
||||
err error
|
||||
result string
|
||||
}
|
||||
|
||||
func (sr *SMTPResponse) AssertOK() *SMTPResponse {
|
||||
a.NoError(sr.t, sr.err)
|
||||
return sr
|
||||
}
|
||||
|
||||
func (sr *SMTPResponse) AssertError(wantErrMsg string) *SMTPResponse {
|
||||
if sr.err == nil {
|
||||
a.Fail(sr.t, "Expected error %s", wantErrMsg)
|
||||
} else {
|
||||
a.Regexp(sr.t, wantErrMsg, sr.err.Error(), "Expected error %s but got %s", wantErrMsg, sr.err)
|
||||
}
|
||||
return sr
|
||||
}
|
||||
23
test/mocks/testingt.go
Normal file
23
test/mocks/testingt.go
Normal file
@ -0,0 +1,23 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.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 mocks
|
||||
|
||||
type TestingT interface {
|
||||
Errorf(string, ...interface{})
|
||||
FailNow()
|
||||
}
|
||||
26
test/mocks/utils.go
Normal file
26
test/mocks/utils.go
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2020 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.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 mocks
|
||||
|
||||
import (
|
||||
b64 "encoding/base64"
|
||||
)
|
||||
|
||||
func base64(data string) (encoded string) {
|
||||
return b64.StdEncoding.EncodeToString([]byte(data))
|
||||
}
|
||||
Reference in New Issue
Block a user