forked from Silverfish/proton-bridge
GODT-1650: Send extras
This commit is contained in:
@ -1,6 +1,8 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"net/mail"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gluon/rfc822"
|
||||
"gitlab.protontech.ch/go/liteapi"
|
||||
@ -13,16 +15,16 @@ type API interface {
|
||||
GetHostURL() string
|
||||
AddCallWatcher(func(server.Call), ...string)
|
||||
|
||||
AddUser(username, password, address string) (string, string, error)
|
||||
AddAddress(userID, address, password string) (string, error)
|
||||
CreateUser(username, password, address string) (string, string, error)
|
||||
CreateAddress(userID, address, password string) (string, error)
|
||||
RemoveAddress(userID, addrID string) error
|
||||
RevokeUser(userID string) error
|
||||
|
||||
GetLabels(userID string) ([]liteapi.Label, error)
|
||||
AddLabel(userID, name string, labelType liteapi.LabelType) (string, error)
|
||||
CreateLabel(userID, name string, labelType liteapi.LabelType) (string, error)
|
||||
|
||||
GetMessages(userID string) ([]liteapi.Message, error)
|
||||
AddMessage(userID, addrID string, labelIDs []string, sender, recipient, subject, body string, mimeType rfc822.MIMEType, read, starred bool) (string, error)
|
||||
CreateMessage(userID, addrID string, labelIDs []string, subject string, sender *mail.Address, toList, ccList, bccList []*mail.Address, decBody string, mimeType rfc822.MIMEType, read, starred bool) (string, error)
|
||||
|
||||
Close()
|
||||
}
|
||||
|
||||
@ -74,7 +74,8 @@ func TestFeatures(testingT *testing.T) {
|
||||
ctx.Step(`^the internet is turned off$`, s.internetIsTurnedOff)
|
||||
ctx.Step(`^the internet is turned on$`, s.internetIsTurnedOn)
|
||||
ctx.Step(`^the user agent is "([^"]*)"$`, s.theUserAgentIs)
|
||||
ctx.Step(`^the value of the "([^"]*)" header in the request to "([^"]*)" is "([^"]*)"$`, s.theValueOfTheHeaderInTheRequestToIs)
|
||||
ctx.Step(`^the header in the "([^"]*)" request to "([^"]*)" has "([^"]*)" set to "([^"]*)"$`, s.theHeaderInTheRequestToHasSetTo)
|
||||
ctx.Step(`^the body in the "([^"]*)" request to "([^"]*)" is:$`, s.theBodyInTheRequestToIs)
|
||||
|
||||
// ==== SETUP ====
|
||||
ctx.Step(`^there exists an account with username "([^"]*)" and password "([^"]*)"$`, s.thereExistsAnAccountWithUsernameAndPassword)
|
||||
@ -167,8 +168,9 @@ func TestFeatures(testingT *testing.T) {
|
||||
ctx.Step(`^SMTP client "([^"]*)" cannot authenticate with incorrect password$`, s.smtpClientCannotAuthenticateWithIncorrectPassword)
|
||||
ctx.Step(`^SMTP client "([^"]*)" sends MAIL FROM "([^"]*)"$`, s.smtpClientSendsMailFrom)
|
||||
ctx.Step(`^SMTP client "([^"]*)" sends RCPT TO "([^"]*)"$`, s.smtpClientSendsRcptTo)
|
||||
ctx.Step(`^SMTP client "([^"]*)" sends DATA "([^"]*)"$`, s.smtpClientSendsData)
|
||||
ctx.Step(`^SMTP client "([^"]*)" sends DATA:$`, s.smtpClientSendsData)
|
||||
ctx.Step(`^SMTP client "([^"]*)" sends RSET$`, s.smtpClientSendsReset)
|
||||
ctx.Step(`^SMTP client "([^"]*)" sends the following message from "([^"]*)" to "([^"]*)":$`, s.smtpClientSendsTheFollowingMessageFromTo)
|
||||
},
|
||||
Options: &godog.Options{
|
||||
Format: "pretty",
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
@ -172,20 +173,24 @@ func (t *testCtx) getMBoxID(userID string, name string) string {
|
||||
return labels[idx].ID
|
||||
}
|
||||
|
||||
func (t *testCtx) getLastCall(path string) (server.Call, error) {
|
||||
calls := t.calls[len(t.calls)-2]
|
||||
func (t *testCtx) getLastCall(method, path string) (server.Call, error) {
|
||||
var allCalls []server.Call
|
||||
|
||||
if len(calls) == 0 {
|
||||
for _, calls := range t.calls {
|
||||
allCalls = append(allCalls, calls...)
|
||||
}
|
||||
|
||||
if len(allCalls) == 0 {
|
||||
return server.Call{}, fmt.Errorf("no calls made")
|
||||
}
|
||||
|
||||
for _, call := range calls {
|
||||
if call.URL.Path == path {
|
||||
for idx := len(allCalls) - 1; idx >= 0; idx-- {
|
||||
if call := allCalls[idx]; call.Method == method && regexp.MustCompile("^"+path+"$").MatchString(call.URL.Path) {
|
||||
return call, nil
|
||||
}
|
||||
}
|
||||
|
||||
return calls[len(calls)-1], nil
|
||||
return server.Call{}, fmt.Errorf("no call with method %q and path %q was made", method, path)
|
||||
}
|
||||
|
||||
func (t *testCtx) pushError(err error) {
|
||||
|
||||
81
tests/diff.go
Normal file
81
tests/diff.go
Normal file
@ -0,0 +1,81 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
)
|
||||
|
||||
func IsSub(outer, inner any) bool {
|
||||
if outer == nil && inner != nil {
|
||||
return IsSub(reflect.Zero(reflect.TypeOf(inner)).Interface(), inner)
|
||||
}
|
||||
|
||||
if outer != nil && inner == nil {
|
||||
return IsSub(reflect.Zero(reflect.TypeOf(outer)).Interface(), outer)
|
||||
}
|
||||
|
||||
switch inner := inner.(type) {
|
||||
case map[string]any:
|
||||
outer, ok := outer.(map[string]any)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return isSubMap(outer, inner)
|
||||
|
||||
case []any:
|
||||
outer, ok := outer.([]any)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(inner) != len(outer) {
|
||||
return false
|
||||
}
|
||||
|
||||
return isSubSlice(outer, inner)
|
||||
|
||||
default:
|
||||
if reflect.TypeOf(outer) != reflect.TypeOf(inner) {
|
||||
return false
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(outer, inner) {
|
||||
return true
|
||||
}
|
||||
|
||||
return reflect.DeepEqual(reflect.Zero(reflect.TypeOf(inner)).Interface(), inner)
|
||||
}
|
||||
}
|
||||
|
||||
func isSubMap(outer, inner map[string]any) bool {
|
||||
for k, v := range inner {
|
||||
w, ok := outer[k]
|
||||
if !ok {
|
||||
for _, w := range outer {
|
||||
if IsSub(w, inner) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !IsSub(w, v) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func isSubSlice(outer, inner []any) bool {
|
||||
for _, v := range inner {
|
||||
if xslices.IndexFunc(outer, func(outer any) bool {
|
||||
return IsSub(outer, v)
|
||||
}) < 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
125
tests/diff_test.go
Normal file
125
tests/diff_test.go
Normal file
@ -0,0 +1,125 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
func Test_IsSub(t *testing.T) {
|
||||
tests := []struct {
|
||||
outer string
|
||||
inner string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
outer: `{}`,
|
||||
inner: `{}`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
outer: `{"a": 1}`,
|
||||
inner: `{"a": 1}`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
outer: `{"a": 1, "b": 2}`,
|
||||
inner: `{"a": 1}`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
outer: `{"a": 1, "b": 2}`,
|
||||
inner: `{"a": 1, "c": 3}`,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
outer: `{"a": 1, "b": {"c": 2}}`,
|
||||
inner: `{"c": 2}`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
outer: `{"a": 1, "b": {"c": 2, "d": 3}}`,
|
||||
inner: `{"c": 2}`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
outer: `{"a": 1, "b": {"c": 2, "d": 3}}`,
|
||||
inner: `{"c": 2, "d": 3}`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
outer: `{"a": 1, "b": {"c": 2, "d": 3}}`,
|
||||
inner: `{"c": 2, "e": 3}`,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
outer: `{"a": 1, "b": {"c": 2, "d": "ignore"}}`,
|
||||
inner: `{"a": 1, "b": {"c": 2, "d": ""}}`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
outer: `{"a": 1, "b": {"c": 2, "d": null}}`,
|
||||
inner: `{"a": 1, "b": {"c": 2, "d": null}}`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
outer: `{"a": 1, "b": {"c": 2, "d": ["1"]}}`,
|
||||
inner: `{"a": 1, "b": {"c": 2, "d": []}}`,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
outer: `{"a": 1, "b": {"c": 2, "d": []}}`,
|
||||
inner: `{"a": 1, "b": {"c": 2, "d": null}}`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
outer: `{"a": []}`,
|
||||
inner: `{"a": []}`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
outer: `{"a": [1, 2]}`,
|
||||
inner: `{"a": [1, 2]}`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
outer: `{"a": [1, 3]}`,
|
||||
inner: `{"a": [1, 2]}`,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
outer: `{"a": [1, 2, 3]}`,
|
||||
inner: `{"a": [1, 2]}`,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
outer: `{"a": null}`,
|
||||
inner: `{"a": []}`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
outer: `{"a": []}`,
|
||||
inner: `{"a": null}`,
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(fmt.Sprintf("%v vs %v", tt.inner, tt.outer), func(t *testing.T) {
|
||||
var outerMap, innerMap map[string]any
|
||||
|
||||
if err := json.Unmarshal([]byte(tt.outer), &outerMap); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(tt.inner), &innerMap); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got := IsSub(outerMap, innerMap); got != tt.want {
|
||||
t.Errorf("isSub() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,11 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cucumber/godog"
|
||||
)
|
||||
|
||||
func (s *scenario) itSucceeds() error {
|
||||
@ -52,15 +55,38 @@ func (s *scenario) theUserAgentIs(userAgent string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenario) theValueOfTheHeaderInTheRequestToIs(key, path, value string) error {
|
||||
call, err := s.t.getLastCall(path)
|
||||
func (s *scenario) theHeaderInTheRequestToHasSetTo(method, path, key, value string) error {
|
||||
call, err := s.t.getLastCall(method, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if haveKey := call.Request.Header.Get(key); haveKey != value {
|
||||
if haveKey := call.Header.Get(key); haveKey != value {
|
||||
return fmt.Errorf("have header %q, want %q", haveKey, value)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenario) theBodyInTheRequestToIs(method, path string, value *godog.DocString) error {
|
||||
call, err := s.t.getLastCall(method, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var body, want map[string]any
|
||||
|
||||
if err := json.Unmarshal(call.Body, &body); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(value.Content), &want); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !IsSub(body, want) {
|
||||
return fmt.Errorf("have body %v, want %v", body, want)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -17,4 +17,4 @@ Feature: The IMAP ID is propagated to bridge
|
||||
When user "user@pm.me" connects IMAP client "1"
|
||||
And IMAP client "1" announces its ID with name "name" and version "version"
|
||||
When the user reports a bug
|
||||
Then the value of the "User-Agent" header in the request to "/core/v4/reports/bug" is "name/version ([GOOS])"
|
||||
Then the header in the "POST" request to "/core/v4/reports/bug" has "User-Agent" set to "name/version ([GOOS])"
|
||||
@ -6,7 +6,10 @@ Feature: SMTP initiation
|
||||
When user "user@pm.me" connects and authenticates SMTP client "1"
|
||||
|
||||
Scenario: Send without first announcing FROM and TO
|
||||
When SMTP client "1" sends DATA "Subject: test"
|
||||
When SMTP client "1" sends DATA:
|
||||
"""
|
||||
Subject: test
|
||||
"""
|
||||
Then it fails with error "Missing RCPT TO command"
|
||||
|
||||
Scenario: Reset is the same as without FROM and TO
|
||||
@ -16,7 +19,10 @@ Feature: SMTP initiation
|
||||
Then it succeeds
|
||||
When SMTP client "1" sends RSET
|
||||
Then it succeeds
|
||||
When SMTP client "1" sends DATA "Subject: test"
|
||||
When SMTP client "1" sends DATA:
|
||||
"""
|
||||
Subject: test
|
||||
"""
|
||||
Then it fails with error "Missing RCPT TO command"
|
||||
|
||||
Scenario: Send without FROM
|
||||
@ -26,7 +32,10 @@ Feature: SMTP initiation
|
||||
Scenario: Send without TO
|
||||
When SMTP client "1" sends MAIL FROM "<user@pm.me>"
|
||||
Then it succeeds
|
||||
When SMTP client "1" sends DATA "Subject: test"
|
||||
When SMTP client "1" sends DATA:
|
||||
"""
|
||||
Subject: test
|
||||
"""
|
||||
Then it fails with error "Missing RCPT TO command"
|
||||
|
||||
Scenario: Send with empty FROM
|
||||
|
||||
332
tests/features/smtp/send/plain.feature
Normal file
332
tests/features/smtp/send/plain.feature
Normal file
@ -0,0 +1,332 @@
|
||||
Feature: SMTP sending of plain messages
|
||||
Background:
|
||||
Given there exists an account with username "user@pm.me" and password "password"
|
||||
And there exists an account with username "bridgetest@protonmail.com" and password "password"
|
||||
And there exists an account with username "bridgetest2@protonmail.com" and password "password"
|
||||
And bridge starts
|
||||
And the user logs in with username "user@pm.me" and password "password"
|
||||
And user "user@pm.me" connects and authenticates SMTP client "1"
|
||||
|
||||
Scenario: Only from and to headers to internal account
|
||||
When SMTP client "1" sends the following message from "user@pm.me" to "bridgetest@protonmail.com":
|
||||
"""
|
||||
From: Bridge Test <user@pm.me>
|
||||
To: Internal Bridge <bridgetest@protonmail.com>
|
||||
|
||||
hello
|
||||
|
||||
"""
|
||||
Then it succeeds
|
||||
When user "user@pm.me" connects and authenticates IMAP client "1"
|
||||
Then IMAP client "1" eventually sees the following messages in "Sent":
|
||||
| sender | recipient | subject | unread |
|
||||
| user@pm.me | bridgetest@protonmail.com | | false |
|
||||
And the body in the "POST" request to "/mail/v4/messages" is:
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
"Subject": "",
|
||||
"Sender": {
|
||||
"Name": "Bridge Test"
|
||||
},
|
||||
"ToList": [
|
||||
{
|
||||
"Address": "bridgetest@protonmail.com",
|
||||
"Name": "Internal Bridge"
|
||||
}
|
||||
],
|
||||
"CCList": [],
|
||||
"BCCList": [],
|
||||
"MIMEType": "text/plain"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
Scenario: Only from and to headers to external account
|
||||
When SMTP client "1" sends the following message from "user@pm.me" to "pm.bridge.qa@gmail.com":
|
||||
"""
|
||||
From: Bridge Test <user@pm.me>
|
||||
To: External Bridge <pm.bridge.qa@gmail.com>
|
||||
|
||||
hello
|
||||
|
||||
"""
|
||||
Then it succeeds
|
||||
When user "user@pm.me" connects and authenticates IMAP client "1"
|
||||
Then IMAP client "1" eventually sees the following messages in "Sent":
|
||||
| sender | recipient | subject | unread |
|
||||
| user@pm.me | pm.bridge.qa@gmail.com | | false |
|
||||
And the body in the "POST" request to "/mail/v4/messages" is:
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
"Subject": "",
|
||||
"Sender": {
|
||||
"Name": "Bridge Test"
|
||||
},
|
||||
"ToList": [
|
||||
{
|
||||
"Address": "pm.bridge.qa@gmail.com",
|
||||
"Name": "External Bridge"
|
||||
}
|
||||
],
|
||||
"CCList": [],
|
||||
"BCCList": [],
|
||||
"MIMEType": "text/plain"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
Scenario: Basic message to internal account
|
||||
When SMTP client "1" sends the following message from "user@pm.me" to "bridgetest@protonmail.com":
|
||||
"""
|
||||
From: Bridge Test <user@pm.me>
|
||||
To: Internal Bridge <bridgetest@protonmail.com>
|
||||
Subject: Plain text internal
|
||||
Content-Disposition: inline
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
This is body of mail 👋
|
||||
|
||||
"""
|
||||
Then it succeeds
|
||||
When user "user@pm.me" connects and authenticates IMAP client "1"
|
||||
Then IMAP client "1" eventually sees the following messages in "Sent":
|
||||
| sender | recipient | subject | unread |
|
||||
| user@pm.me | bridgetest@protonmail.com | Plain text internal | false |
|
||||
And the body in the "POST" request to "/mail/v4/messages" is:
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
"Subject": "Plain text internal",
|
||||
"Sender": {
|
||||
"Name": "Bridge Test"
|
||||
},
|
||||
"ToList": [
|
||||
{
|
||||
"Address": "bridgetest@protonmail.com",
|
||||
"Name": "Internal Bridge"
|
||||
}
|
||||
],
|
||||
"CCList": [],
|
||||
"BCCList": [],
|
||||
"MIMEType": "text/plain"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
Scenario: Basic message to external account
|
||||
When SMTP client "1" sends the following message from "user@pm.me" to "pm.bridge.qa@gmail.com":
|
||||
"""
|
||||
From: Bridge Test <user@pm.me>
|
||||
To: External Bridge <pm.bridge.qa@gmail.com>
|
||||
Subject: Plain text external
|
||||
Content-Disposition: inline
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
This is body of mail 👋
|
||||
|
||||
"""
|
||||
Then it succeeds
|
||||
When user "user@pm.me" connects and authenticates IMAP client "1"
|
||||
Then IMAP client "1" eventually sees the following messages in "Sent":
|
||||
| sender | recipient | subject | unread |
|
||||
| user@pm.me | pm.bridge.qa@gmail.com | Plain text external | false |
|
||||
And the body in the "POST" request to "/mail/v4/messages" is:
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
"Subject": "Plain text external",
|
||||
"Sender": {
|
||||
"Name": "Bridge Test"
|
||||
},
|
||||
"ToList": [
|
||||
{
|
||||
"Address": "pm.bridge.qa@gmail.com",
|
||||
"Name": "External Bridge"
|
||||
}
|
||||
],
|
||||
"CCList": [],
|
||||
"BCCList": [],
|
||||
"MIMEType": "text/plain"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
Scenario: Message without charset is utf8
|
||||
When SMTP client "1" sends the following message from "user@pm.me" to "pm.bridge.qa@gmail.com":
|
||||
"""
|
||||
From: Bridge Test <user@pm.me>
|
||||
To: External Bridge <pm.bridge.qa@gmail.com>
|
||||
Subject: Plain text no charset external
|
||||
Content-Disposition: inline
|
||||
Content-Type: text/plain;
|
||||
|
||||
This is body of mail without charset. Please assume utf8
|
||||
|
||||
"""
|
||||
Then it succeeds
|
||||
When user "user@pm.me" connects and authenticates IMAP client "1"
|
||||
Then IMAP client "1" eventually sees the following messages in "Sent":
|
||||
| sender | recipient | subject | unread |
|
||||
| user@pm.me | pm.bridge.qa@gmail.com | Plain text no charset external | false |
|
||||
And the body in the "POST" request to "/mail/v4/messages" is:
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
"Subject": "Plain text no charset external",
|
||||
"Sender": {
|
||||
"Name": "Bridge Test"
|
||||
},
|
||||
"ToList": [
|
||||
{
|
||||
"Address": "pm.bridge.qa@gmail.com",
|
||||
"Name": "External Bridge"
|
||||
}
|
||||
],
|
||||
"CCList": [],
|
||||
"BCCList": [],
|
||||
"MIMEType": "text/plain"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
Scenario: Message without charset is base64-encoded latin1
|
||||
When SMTP client "1" sends the following message from "user@pm.me" to "pm.bridge.qa@gmail.com":
|
||||
"""
|
||||
From: Bridge Test <user@pm.me>
|
||||
To: External Bridge <pm.bridge.qa@gmail.com>
|
||||
Subject: Plain text no charset external
|
||||
Content-Disposition: inline
|
||||
Content-Type: text/plain;
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
dGhpcyBpcyBpbiBsYXRpbjEgYW5kIHRoZXJlIGFyZSBsb3RzIG9mIGVzIHdpdGggYWNjZW50czog
|
||||
6enp6enp6enp6enp6enp
|
||||
|
||||
|
||||
"""
|
||||
Then it succeeds
|
||||
When user "user@pm.me" connects and authenticates IMAP client "1"
|
||||
Then IMAP client "1" eventually sees the following messages in "Sent":
|
||||
| sender | recipient | subject | unread |
|
||||
| user@pm.me | pm.bridge.qa@gmail.com | Plain text no charset external | false |
|
||||
And the body in the "POST" request to "/mail/v4/messages" is:
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
"Subject": "Plain text no charset external",
|
||||
"Sender": {
|
||||
"Name": "Bridge Test"
|
||||
},
|
||||
"ToList": [
|
||||
{
|
||||
"Address": "pm.bridge.qa@gmail.com",
|
||||
"Name": "External Bridge"
|
||||
}
|
||||
],
|
||||
"CCList": [],
|
||||
"BCCList": [],
|
||||
"MIMEType": "text/plain"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
Scenario: Message without charset and content is detected as HTML
|
||||
When SMTP client "1" sends the following message from "user@pm.me" to "pm.bridge.qa@gmail.com":
|
||||
"""
|
||||
From: Bridge Test <user@pm.me>
|
||||
To: External Bridge <pm.bridge.qa@gmail.com>
|
||||
Subject: Plain, no charset, no content, external
|
||||
Content-Disposition: inline
|
||||
Content-Type: text/plain;
|
||||
|
||||
"""
|
||||
Then it succeeds
|
||||
When user "user@pm.me" connects and authenticates IMAP client "1"
|
||||
Then IMAP client "1" eventually sees the following messages in "Sent":
|
||||
| sender | recipient | subject | unread |
|
||||
| user@pm.me | pm.bridge.qa@gmail.com | Plain, no charset, no content, external | false |
|
||||
And the body in the "POST" request to "/mail/v4/messages" is:
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
"Subject": "Plain, no charset, no content, external",
|
||||
"Sender": {
|
||||
"Name": "Bridge Test"
|
||||
},
|
||||
"ToList": [
|
||||
{
|
||||
"Address": "pm.bridge.qa@gmail.com",
|
||||
"Name": "External Bridge"
|
||||
}
|
||||
],
|
||||
"CCList": [],
|
||||
"BCCList": [],
|
||||
"MIMEType": "text/plain"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
Scenario: RCPT does not contain all CC
|
||||
When SMTP client "1" sends MAIL FROM "<user@pm.me>"
|
||||
And SMTP client "1" sends RCPT TO "<bridgetest@protonmail.com>"
|
||||
And SMTP client "1" sends DATA:
|
||||
"""
|
||||
From: Bridge Test <user@pm.me>
|
||||
To: Internal Bridge <bridgetest@protonmail.com>
|
||||
CC: Internal Bridge 2 <bridgetest2@protonmail.com>
|
||||
Content-Type: text/plain
|
||||
Subject: RCPT-CC test
|
||||
|
||||
This is CC missing in RCPT test. Have a nice day!
|
||||
.
|
||||
"""
|
||||
Then it succeeds
|
||||
When user "user@pm.me" connects and authenticates IMAP client "1"
|
||||
Then IMAP client "1" eventually sees the following messages in "Sent":
|
||||
| sender | recipient | cc | subject | unread |
|
||||
| user@pm.me | bridgetest@protonmail.com | bridgetest2@protonmail.com | RCPT-CC test | false |
|
||||
And the body in the "POST" request to "/mail/v4/messages" is:
|
||||
"""
|
||||
{
|
||||
"Message": {
|
||||
"Subject": "RCPT-CC test",
|
||||
"Sender": {
|
||||
"Name": "Bridge Test"
|
||||
},
|
||||
"ToList": [
|
||||
{
|
||||
"Address": "bridgetest@protonmail.com",
|
||||
"Name": "Internal Bridge"
|
||||
}
|
||||
],
|
||||
"CCList": [
|
||||
{
|
||||
"Address": "bridgetest2@protonmail.com",
|
||||
"Name": "Internal Bridge 2"
|
||||
}
|
||||
],
|
||||
"BCCList": []
|
||||
}
|
||||
}
|
||||
"""
|
||||
And the body in the "POST" request to "/mail/v4/messages/.*" is:
|
||||
"""
|
||||
{
|
||||
"Packages":[
|
||||
{
|
||||
"Addresses":{
|
||||
"bridgetest@protonmail.com":{
|
||||
"Type":1
|
||||
},
|
||||
"bridgetest2@protonmail.com":{
|
||||
"Type":1
|
||||
}
|
||||
},
|
||||
"Type":1,
|
||||
"MIMEType":"text/plain"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
@ -280,12 +280,28 @@ func (s *scenario) imapClientSeesTheFollowingMessagesInMailbox(clientID, mailbox
|
||||
}
|
||||
|
||||
haveMessages := xslices.Map(fetch, func(msg *imap.Message) Message {
|
||||
return Message{
|
||||
Sender: msg.Envelope.Sender[0].Address(),
|
||||
Recipient: msg.Envelope.To[0].Address(),
|
||||
Subject: msg.Envelope.Subject,
|
||||
Unread: slices.Contains(msg.Flags, imap.SeenFlag),
|
||||
message := Message{
|
||||
Subject: msg.Envelope.Subject,
|
||||
Unread: slices.Contains(msg.Flags, imap.SeenFlag),
|
||||
}
|
||||
|
||||
if len(msg.Envelope.From) > 0 {
|
||||
message.From = msg.Envelope.From[0].Address()
|
||||
}
|
||||
|
||||
if len(msg.Envelope.To) > 0 {
|
||||
message.To = msg.Envelope.To[0].Address()
|
||||
}
|
||||
|
||||
if len(msg.Envelope.Cc) > 0 {
|
||||
message.CC = msg.Envelope.Cc[0].Address()
|
||||
}
|
||||
|
||||
if len(msg.Envelope.Bcc) > 0 {
|
||||
message.BCC = msg.Envelope.Bcc[0].Address()
|
||||
}
|
||||
|
||||
return message
|
||||
})
|
||||
|
||||
return matchMessages(haveMessages, table)
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
"net/smtp"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/constants"
|
||||
"github.com/cucumber/godog"
|
||||
)
|
||||
|
||||
func (s *scenario) userConnectsSMTPClient(username, clientID string) error {
|
||||
@ -87,13 +88,13 @@ func (s *scenario) smtpClientSendsRcptTo(clientID, to string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenario) smtpClientSendsData(clientID, data string) error {
|
||||
func (s *scenario) smtpClientSendsData(clientID string, data *godog.DocString) error {
|
||||
_, client := s.t.getSMTPClient(clientID)
|
||||
|
||||
rc, err := client.Data()
|
||||
if err != nil {
|
||||
s.t.pushError(err)
|
||||
} else if _, err := rc.Write([]byte(data)); err != nil {
|
||||
} else if _, err := rc.Write([]byte(data.Content)); err != nil {
|
||||
s.t.pushError(err)
|
||||
} else if err := rc.Close(); err != nil {
|
||||
s.t.pushError(err)
|
||||
@ -109,3 +110,30 @@ func (s *scenario) smtpClientSendsReset(clientID string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenario) smtpClientSendsTheFollowingMessageFromTo(clientID, from, to string, message *godog.DocString) error {
|
||||
_, client := s.t.getSMTPClient(clientID)
|
||||
|
||||
s.t.pushError(func() error {
|
||||
if err := client.Mail(from); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := client.Rcpt(to); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wc, err := client.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := wc.Write([]byte(message.Content)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return wc.Close()
|
||||
}())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@ -13,10 +14,24 @@ import (
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Sender string
|
||||
Recipient string
|
||||
Subject string
|
||||
Unread bool
|
||||
Subject string `bdd:"subject"`
|
||||
|
||||
From string `bdd:"sender"`
|
||||
To string `bdd:"recipient"`
|
||||
CC string `bdd:"cc"`
|
||||
BCC string `bdd:"bcc"`
|
||||
|
||||
Unread bool `bdd:"unread"`
|
||||
}
|
||||
|
||||
func newMessageFromRow(header, row *messages.PickleTableRow) Message {
|
||||
var msg Message
|
||||
|
||||
if err := unmarshalRow(header, row, &msg); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
func matchMessages(have []Message, want *godog.Table) error {
|
||||
@ -28,20 +43,27 @@ func matchMessages(have []Message, want *godog.Table) error {
|
||||
}
|
||||
|
||||
func parseMessages(table *godog.Table) []Message {
|
||||
header := table.Rows[0]
|
||||
|
||||
return xslices.Map(table.Rows[1:], func(row *messages.PickleTableRow) Message {
|
||||
return Message{
|
||||
Sender: row.Cells[0].Value,
|
||||
Recipient: row.Cells[1].Value,
|
||||
Subject: row.Cells[2].Value,
|
||||
Unread: mustParseBool(row.Cells[3].Value),
|
||||
}
|
||||
return newMessageFromRow(header, row)
|
||||
})
|
||||
}
|
||||
|
||||
type Mailbox struct {
|
||||
Name string
|
||||
Total int
|
||||
Unread int
|
||||
Name string `bdd:"name"`
|
||||
Total int `bdd:"total"`
|
||||
Unread int `bdd:"unread"`
|
||||
}
|
||||
|
||||
func newMailboxFromRow(header, row *messages.PickleTableRow) Mailbox {
|
||||
var mbox Mailbox
|
||||
|
||||
if err := unmarshalRow(header, row, &mbox); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return mbox
|
||||
}
|
||||
|
||||
func matchMailboxes(have []Mailbox, want *godog.Table) error {
|
||||
@ -53,33 +75,13 @@ func matchMailboxes(have []Mailbox, want *godog.Table) error {
|
||||
}
|
||||
|
||||
func parseMailboxes(table *godog.Table) []Mailbox {
|
||||
mustParseInt := func(s string) int {
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return i
|
||||
}
|
||||
header := table.Rows[0]
|
||||
|
||||
return xslices.Map(table.Rows[1:], func(row *messages.PickleTableRow) Mailbox {
|
||||
return Mailbox{
|
||||
Name: row.Cells[0].Value,
|
||||
Total: mustParseInt(row.Cells[1].Value),
|
||||
Unread: mustParseInt(row.Cells[2].Value),
|
||||
}
|
||||
return newMailboxFromRow(header, row)
|
||||
})
|
||||
}
|
||||
|
||||
func mustParseBool(s string) bool {
|
||||
v, err := strconv.ParseBool(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func eventually(condition func() error, waitFor, tick time.Duration) error {
|
||||
ch := make(chan error, 1)
|
||||
|
||||
@ -108,3 +110,62 @@ func eventually(condition func() error, waitFor, tick time.Duration) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getCellValue(header, row *messages.PickleTableRow, name string) (string, bool) {
|
||||
for idx, cell := range header.Cells {
|
||||
if cell.Value == name {
|
||||
return row.Cells[idx].Value, true
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
func unmarshalRow(header, row *messages.PickleTableRow, v any) error {
|
||||
typ := reflect.TypeOf(v).Elem()
|
||||
|
||||
for idx := 0; idx < typ.NumField(); idx++ {
|
||||
field := typ.Field(idx)
|
||||
|
||||
if tag, ok := field.Tag.Lookup("bdd"); ok {
|
||||
cell, ok := getCellValue(header, row, tag)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
switch field.Type.Kind() {
|
||||
case reflect.String:
|
||||
reflect.ValueOf(v).Elem().Field(idx).SetString(cell)
|
||||
|
||||
case reflect.Int:
|
||||
reflect.ValueOf(v).Elem().Field(idx).SetInt(int64(mustParseInt(cell)))
|
||||
|
||||
case reflect.Bool:
|
||||
reflect.ValueOf(v).Elem().Field(idx).SetBool(mustParseBool(cell))
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported type %q", field.Type.Kind())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mustParseInt(s string) int {
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
func mustParseBool(s string) bool {
|
||||
v, err := strconv.ParseBool(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gluon/rfc822"
|
||||
@ -17,7 +18,7 @@ import (
|
||||
|
||||
func (s *scenario) thereExistsAnAccountWithUsernameAndPassword(username, password string) error {
|
||||
// Create the user.
|
||||
userID, addrID, err := s.t.api.AddUser(username, password, username)
|
||||
userID, addrID, err := s.t.api.CreateUser(username, password, username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -37,7 +38,7 @@ func (s *scenario) thereExistsAnAccountWithUsernameAndPassword(username, passwor
|
||||
func (s *scenario) theAccountHasAdditionalAddress(username, address string) error {
|
||||
userID := s.t.getUserID(username)
|
||||
|
||||
addrID, err := s.t.api.AddAddress(userID, address, s.t.getUserPass(userID))
|
||||
addrID, err := s.t.api.CreateAddress(userID, address, s.t.getUserPass(userID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -62,7 +63,7 @@ func (s *scenario) theAccountNoLongerHasAdditionalAddress(username, address stri
|
||||
|
||||
func (s *scenario) theAccountHasCustomFolders(username string, count int) error {
|
||||
for idx := 0; idx < count; idx++ {
|
||||
if _, err := s.t.api.AddLabel(s.t.getUserID(username), uuid.NewString(), liteapi.LabelTypeFolder); err != nil {
|
||||
if _, err := s.t.api.CreateLabel(s.t.getUserID(username), uuid.NewString(), liteapi.LabelTypeFolder); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -72,7 +73,7 @@ func (s *scenario) theAccountHasCustomFolders(username string, count int) error
|
||||
|
||||
func (s *scenario) theAccountHasCustomLabels(username string, count int) error {
|
||||
for idx := 0; idx < count; idx++ {
|
||||
if _, err := s.t.api.AddLabel(s.t.getUserID(username), uuid.NewString(), liteapi.LabelTypeLabel); err != nil {
|
||||
if _, err := s.t.api.CreateLabel(s.t.getUserID(username), uuid.NewString(), liteapi.LabelTypeLabel); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -103,7 +104,7 @@ func (s *scenario) theAccountHasTheFollowingCustomMailboxes(username string, tab
|
||||
})
|
||||
|
||||
for _, wantMailbox := range wantMailboxes {
|
||||
if _, err := s.t.api.AddLabel(s.t.getUserID(username), wantMailbox.name, wantMailbox.typ); err != nil {
|
||||
if _, err := s.t.api.CreateLabel(s.t.getUserID(username), wantMailbox.name, wantMailbox.typ); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -117,13 +118,15 @@ func (s *scenario) theAddressOfAccountHasTheFollowingMessagesInMailbox(address,
|
||||
mboxID := s.t.getMBoxID(userID, mailbox)
|
||||
|
||||
for _, wantMessage := range parseMessages(table) {
|
||||
if _, err := s.t.api.AddMessage(
|
||||
if _, err := s.t.api.CreateMessage(
|
||||
userID,
|
||||
addrID,
|
||||
[]string{mboxID},
|
||||
wantMessage.Sender,
|
||||
wantMessage.Recipient,
|
||||
wantMessage.Subject,
|
||||
&mail.Address{Address: wantMessage.From},
|
||||
[]*mail.Address{{Address: wantMessage.To}},
|
||||
[]*mail.Address{},
|
||||
[]*mail.Address{},
|
||||
"some body goes here",
|
||||
rfc822.TextPlain,
|
||||
wantMessage.Unread,
|
||||
@ -142,16 +145,18 @@ func (s *scenario) theAddressOfAccountHasMessagesInMailbox(address, username str
|
||||
mboxID := s.t.getMBoxID(userID, mailbox)
|
||||
|
||||
for idx := 0; idx < count; idx++ {
|
||||
if _, err := s.t.api.AddMessage(
|
||||
if _, err := s.t.api.CreateMessage(
|
||||
userID,
|
||||
addrID,
|
||||
[]string{mboxID},
|
||||
fmt.Sprintf("sender%v@pm.me", idx),
|
||||
fmt.Sprintf("recipient%v@pm.me", idx),
|
||||
fmt.Sprintf("subject %v", idx),
|
||||
fmt.Sprintf("body %v", idx),
|
||||
fmt.Sprintf("subject %d", idx),
|
||||
&mail.Address{Address: fmt.Sprintf("sender %d", idx)},
|
||||
[]*mail.Address{{Address: fmt.Sprintf("recipient %d", idx)}},
|
||||
[]*mail.Address{},
|
||||
[]*mail.Address{},
|
||||
fmt.Sprintf("body %d", idx),
|
||||
rfc822.TextPlain,
|
||||
false,
|
||||
idx%2 == 0,
|
||||
false,
|
||||
); err != nil {
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user