mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-02-05 20:01:57 -05:00
groupware: refactor the JMAP integration tests
This commit is contained in:
@@ -2,13 +2,23 @@ package jmap
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"slices"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
"github.com/brianvoe/gofakeit/v7"
|
||||
"github.com/opencloud-eu/opencloud/pkg/jscontact"
|
||||
"github.com/opencloud-eu/opencloud/pkg/structs"
|
||||
)
|
||||
|
||||
func TestContacts(t *testing.T) {
|
||||
@@ -53,19 +63,422 @@ func TestContacts(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func allTrue[S any](t *testing.T, s S, exceptions ...string) {
|
||||
v := reflect.ValueOf(s)
|
||||
typ := v.Type()
|
||||
for i := range v.NumField() {
|
||||
name := typ.Field(i).Name
|
||||
if slices.Contains(exceptions, name) {
|
||||
continue
|
||||
}
|
||||
value := v.Field(i).Bool()
|
||||
require.True(t, value, "should be true: %v", name)
|
||||
}
|
||||
}
|
||||
|
||||
func matchContact(t *testing.T, actual jscontact.ContactCard, expected jscontact.ContactCard) {
|
||||
require.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
type ContactsBoxes struct {
|
||||
nicknames bool
|
||||
secondaryEmails bool
|
||||
secondaryAddress bool
|
||||
phones bool
|
||||
onlineService bool
|
||||
preferredLanguage bool
|
||||
mediaWithBlobId bool
|
||||
mediaWithDataUri bool
|
||||
mediaWithExternalUri bool
|
||||
organization bool
|
||||
cryptoKey bool
|
||||
link bool
|
||||
}
|
||||
|
||||
var streetNumberRegex = regexp.MustCompile(`^(\d+)\s+(.+)$`)
|
||||
|
||||
func (s *StalwartTest) fillContacts(
|
||||
t *testing.T,
|
||||
count uint,
|
||||
) (string, string, map[string]jscontact.ContactCard, ContactsBoxes, error) {
|
||||
require := require.New(t)
|
||||
c, err := NewTestJmapClient(s.session, s.username, s.password, true, true)
|
||||
require.NoError(err)
|
||||
defer c.Close()
|
||||
|
||||
boxes := ContactsBoxes{}
|
||||
|
||||
printer := func(s string) { log.Println(s) }
|
||||
|
||||
accountId := c.session.PrimaryAccounts.Contacts
|
||||
require.NotEmpty(accountId, "no primary account for contacts in session")
|
||||
|
||||
addressbookId := ""
|
||||
{
|
||||
addressBooksById, err := c.objectsById(accountId, AddressBookType, JmapContacts)
|
||||
require.NoError(err)
|
||||
|
||||
for id, addressbook := range addressBooksById {
|
||||
if isDefault, ok := addressbook["isDefault"]; ok {
|
||||
if isDefault.(bool) {
|
||||
addressbookId = id
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
require.NotEmpty(addressbookId)
|
||||
|
||||
u := true
|
||||
|
||||
filled := map[string]jscontact.ContactCard{}
|
||||
for i := range count {
|
||||
person := gofakeit.Person()
|
||||
nameMap, nameObj := createName(person, u)
|
||||
contact := map[string]any{
|
||||
"@type": "Card",
|
||||
"version": "1.0",
|
||||
"addressBookIds": toBoolMap([]string{addressbookId}),
|
||||
"prodId": productName,
|
||||
"language": pickLanguage(),
|
||||
"kind": "individual",
|
||||
"name": nameMap,
|
||||
}
|
||||
card := jscontact.ContactCard{
|
||||
//Type: jscontact.ContactCardType,
|
||||
Version: "1.0",
|
||||
AddressBookIds: toBoolMap([]string{addressbookId}),
|
||||
ProdId: productName,
|
||||
Language: contact["language"].(string),
|
||||
Kind: jscontact.ContactCardKindIndividual,
|
||||
Name: &nameObj,
|
||||
}
|
||||
|
||||
if i%3 == 0 {
|
||||
nicknameMap, nicknameObj := createNickName(person, u)
|
||||
id := id()
|
||||
contact["nicknames"] = map[string]map[string]any{id: nicknameMap}
|
||||
card.Nicknames = map[string]jscontact.Nickname{id: nicknameObj}
|
||||
boxes.nicknames = true
|
||||
}
|
||||
|
||||
{
|
||||
emailMaps := map[string]map[string]any{}
|
||||
emailObjs := map[string]jscontact.EmailAddress{}
|
||||
emailId := id()
|
||||
emailMap, emailObj := createEmail(person, 10, u)
|
||||
emailMaps[emailId] = emailMap
|
||||
emailObjs[emailId] = emailObj
|
||||
|
||||
for i := range rand.Intn(3) {
|
||||
id := id()
|
||||
m, o := createSecondaryEmail(gofakeit.Email(), i*100, u)
|
||||
emailMaps[id] = m
|
||||
emailObjs[id] = o
|
||||
boxes.secondaryEmails = true
|
||||
}
|
||||
if len(emailMaps) > 0 {
|
||||
contact["emails"] = emailMaps
|
||||
card.Emails = emailObjs
|
||||
}
|
||||
}
|
||||
if err := propmap(i%2 == 0, 1, 2, contact, "phones", &card.Phones, func(i int, id string) (map[string]any, jscontact.Phone, error) {
|
||||
boxes.phones = true
|
||||
num := person.Contact.Phone
|
||||
if i > 0 {
|
||||
num = gofakeit.Phone()
|
||||
}
|
||||
var features map[jscontact.PhoneFeature]bool = nil
|
||||
if rand.Intn(3) < 2 {
|
||||
features = toBoolMapS(jscontact.PhoneFeatureMobile, jscontact.PhoneFeatureVoice, jscontact.PhoneFeatureVideo, jscontact.PhoneFeatureText)
|
||||
} else {
|
||||
features = toBoolMapS(jscontact.PhoneFeatureVoice, jscontact.PhoneFeatureMainNumber)
|
||||
}
|
||||
|
||||
contexts := map[jscontact.PhoneContext]bool{jscontact.PhoneContextWork: true}
|
||||
if rand.Intn(2) < 1 {
|
||||
contexts[jscontact.PhoneContextPrivate] = true
|
||||
}
|
||||
tel := "tel:" + "+1" + num
|
||||
return map[string]any{
|
||||
"@type": "Phone",
|
||||
"number": tel,
|
||||
"features": structs.MapKeys(features, func(f jscontact.PhoneFeature) string { return string(f) }),
|
||||
"contexts": structs.MapKeys(contexts, func(c jscontact.PhoneContext) string { return string(c) }),
|
||||
}, untype(jscontact.Phone{
|
||||
Type: jscontact.PhoneType,
|
||||
Number: tel,
|
||||
Features: features,
|
||||
Contexts: contexts,
|
||||
}, u), nil
|
||||
}); err != nil {
|
||||
return "", "", nil, boxes, err
|
||||
}
|
||||
if err := propmap(i%5 < 4, 1, 2, contact, "addresses", &card.Addresses, func(i int, id string) (map[string]any, jscontact.Address, error) {
|
||||
var source *gofakeit.AddressInfo
|
||||
if i == 0 {
|
||||
source = person.Address
|
||||
} else {
|
||||
source = gofakeit.Address()
|
||||
boxes.secondaryAddress = true
|
||||
}
|
||||
components := []jscontact.AddressComponent{}
|
||||
m := streetNumberRegex.FindAllStringSubmatch(source.Street, -1)
|
||||
if m != nil {
|
||||
components = append(components, jscontact.AddressComponent{ /*Type: jscontact.AddressComponentType,*/ Kind: jscontact.AddressComponentKindName, Value: m[0][2]})
|
||||
components = append(components, jscontact.AddressComponent{ /*Type: jscontact.AddressComponentType,*/ Kind: jscontact.AddressComponentKindNumber, Value: m[0][1]})
|
||||
} else {
|
||||
components = append(components, jscontact.AddressComponent{ /*Type: jscontact.AddressComponentType,*/ Kind: jscontact.AddressComponentKindName, Value: source.Street})
|
||||
}
|
||||
components = append(components,
|
||||
jscontact.AddressComponent{ /*Type: jscontact.AddressComponentType,*/ Kind: jscontact.AddressComponentKindLocality, Value: source.City},
|
||||
jscontact.AddressComponent{ /*Type: jscontact.AddressComponentType,*/ Kind: jscontact.AddressComponentKindCountry, Value: source.Country},
|
||||
jscontact.AddressComponent{ /*Type: jscontact.AddressComponentType,*/ Kind: jscontact.AddressComponentKindRegion, Value: source.State},
|
||||
jscontact.AddressComponent{ /*Type: jscontact.AddressComponentType,*/ Kind: jscontact.AddressComponentKindPostcode, Value: source.Zip},
|
||||
)
|
||||
tz := pickRandom(timezones...)
|
||||
return map[string]any{
|
||||
"@type": "Address",
|
||||
"components": structs.Map(components, func(c jscontact.AddressComponent) map[string]string {
|
||||
return map[string]string{"kind": string(c.Kind), "value": c.Value}
|
||||
}),
|
||||
"defaultSeparator": ", ",
|
||||
"isOrdered": true,
|
||||
"timeZone": tz,
|
||||
}, untype(jscontact.Address{
|
||||
Type: jscontact.AddressType,
|
||||
Components: components,
|
||||
DefaultSeparator: ", ",
|
||||
IsOrdered: true,
|
||||
TimeZone: tz,
|
||||
}, u), nil
|
||||
}); err != nil {
|
||||
return "", "", nil, boxes, err
|
||||
}
|
||||
if err := propmap(i%2 == 0, 1, 2, contact, "onlineServices", &card.OnlineServices, func(i int, id string) (map[string]any, jscontact.OnlineService, error) {
|
||||
boxes.onlineService = true
|
||||
switch rand.Intn(3) {
|
||||
case 0:
|
||||
return map[string]any{
|
||||
"@type": "OnlineService",
|
||||
"service": "Mastodon",
|
||||
"user": "@" + person.Contact.Email,
|
||||
"uri": "https://mastodon.example.com/@" + strings.ToLower(person.FirstName),
|
||||
}, untype(jscontact.OnlineService{
|
||||
Type: jscontact.OnlineServiceType,
|
||||
Service: "Mastodon",
|
||||
User: "@" + person.Contact.Email,
|
||||
Uri: "https://mastodon.example.com/@" + strings.ToLower(person.FirstName),
|
||||
}, u), nil
|
||||
case 1:
|
||||
return map[string]any{
|
||||
"@type": "OnlineService",
|
||||
"uri": "xmpp:" + person.Contact.Email,
|
||||
}, untype(jscontact.OnlineService{
|
||||
Type: jscontact.OnlineServiceType,
|
||||
Uri: "xmpp:" + person.Contact.Email,
|
||||
}, u), nil
|
||||
default:
|
||||
return map[string]any{
|
||||
"@type": "OnlineService",
|
||||
"service": "Discord",
|
||||
"user": person.Contact.Email,
|
||||
"uri": "https://discord.example.com/user/" + person.Contact.Email,
|
||||
}, untype(jscontact.OnlineService{
|
||||
Type: jscontact.OnlineServiceType,
|
||||
Service: "Discord",
|
||||
User: person.Contact.Email,
|
||||
Uri: "https://discord.example.com/user/" + person.Contact.Email,
|
||||
}, u), nil
|
||||
}
|
||||
}); err != nil {
|
||||
return "", "", nil, boxes, err
|
||||
}
|
||||
|
||||
if err := propmap(i%3 == 0, 1, 2, contact, "preferredLanguages", &card.PreferredLanguages, func(i int, id string) (map[string]any, jscontact.LanguagePref, error) {
|
||||
boxes.preferredLanguage = true
|
||||
lang := pickRandom("en", "fr", "de", "es", "it")
|
||||
contexts := pickRandoms1("work", "private")
|
||||
return map[string]any{
|
||||
"@type": "LanguagePref",
|
||||
"language": lang,
|
||||
"contexts": toBoolMap(contexts),
|
||||
"pref": i + 1,
|
||||
}, untype(jscontact.LanguagePref{
|
||||
Type: jscontact.LanguagePrefType,
|
||||
Language: lang,
|
||||
Contexts: toBoolMap(structs.Map(contexts, func(s string) jscontact.LanguagePrefContext { return jscontact.LanguagePrefContext(s) })),
|
||||
Pref: uint(i + 1),
|
||||
}, u), nil
|
||||
}); err != nil {
|
||||
return "", "", nil, boxes, err
|
||||
}
|
||||
|
||||
if i%2 == 0 {
|
||||
organizationMaps := map[string]map[string]any{}
|
||||
organizationObjs := map[string]jscontact.Organization{}
|
||||
titleMaps := map[string]map[string]any{}
|
||||
titleObjs := map[string]jscontact.Title{}
|
||||
for range 1 + rand.Intn(2) {
|
||||
boxes.organization = true
|
||||
orgId := id()
|
||||
titleId := id()
|
||||
organizationMaps[orgId] = map[string]any{
|
||||
"@type": "Organization",
|
||||
"name": person.Job.Company,
|
||||
"contexts": toBoolMapS("work"),
|
||||
}
|
||||
organizationObjs[orgId] = untype(jscontact.Organization{
|
||||
Type: jscontact.OrganizationType,
|
||||
Name: person.Job.Company,
|
||||
Contexts: toBoolMapS(jscontact.OrganizationContextWork),
|
||||
}, u)
|
||||
titleMaps[titleId] = map[string]any{
|
||||
"@type": "Title",
|
||||
"kind": "title",
|
||||
"name": person.Job.Title,
|
||||
"organizationId": orgId,
|
||||
}
|
||||
titleObjs[titleId] = untype(jscontact.Title{
|
||||
Type: jscontact.TitleType,
|
||||
Kind: jscontact.TitleKindTitle,
|
||||
Name: person.Job.Title,
|
||||
OrganizationId: orgId,
|
||||
}, u)
|
||||
}
|
||||
contact["organizations"] = organizationMaps
|
||||
contact["titles"] = titleMaps
|
||||
card.Organizations = organizationObjs
|
||||
card.Titles = titleObjs
|
||||
}
|
||||
|
||||
if err := propmap(i%2 == 0, 1, 1, contact, "cryptoKeys", &card.CryptoKeys, func(i int, id string) (map[string]any, jscontact.CryptoKey, error) {
|
||||
boxes.cryptoKey = true
|
||||
entity, err := openpgp.NewEntity(person.FirstName+" "+person.LastName, "test", person.Contact.Email, nil)
|
||||
if err != nil {
|
||||
return nil, jscontact.CryptoKey{}, err
|
||||
}
|
||||
var b bytes.Buffer
|
||||
err = entity.PrimaryKey.Serialize(&b)
|
||||
if err != nil {
|
||||
return nil, jscontact.CryptoKey{}, err
|
||||
}
|
||||
encoded := base64.RawStdEncoding.EncodeToString(b.Bytes())
|
||||
return map[string]any{
|
||||
"@type": "CryptoKey",
|
||||
"uri": "data:application/pgp-keys;base64," + encoded,
|
||||
}, untype(jscontact.CryptoKey{
|
||||
Type: jscontact.CryptoKeyType,
|
||||
Uri: "data:application/pgp-keys;base64," + encoded,
|
||||
}, u), nil
|
||||
}); err != nil {
|
||||
return "", "", nil, boxes, err
|
||||
}
|
||||
|
||||
if err := propmap(i%2 == 0, 1, 2, contact, "media", &card.Media, func(i int, id string) (map[string]any, jscontact.Media, error) {
|
||||
label := fmt.Sprintf("photo-%d", 1000+rand.Intn(9000))
|
||||
switch rand.Intn(3) {
|
||||
case 0:
|
||||
boxes.mediaWithDataUri = true
|
||||
// use data uri
|
||||
//size := 16 + rand.Intn(512-16+1) // <- let's not do that right now, makes debugging errors very difficult due to the ASCII wall noise
|
||||
size := pickRandom(16, 24, 32, 48, 64)
|
||||
img := gofakeit.ImagePng(size, size)
|
||||
mime := "image/png"
|
||||
uri := "data:" + mime + ";base64," + base64.StdEncoding.EncodeToString(img)
|
||||
contexts := toBoolMapS(jscontact.MediaContextPrivate)
|
||||
return map[string]any{
|
||||
"@type": "Media",
|
||||
"kind": string(jscontact.MediaKindPhoto),
|
||||
"uri": uri,
|
||||
"mediaType": mime,
|
||||
"contexts": structs.MapKeys(contexts, func(c jscontact.MediaContext) string { return string(c) }),
|
||||
"label": label,
|
||||
}, untype(jscontact.Media{
|
||||
Type: jscontact.MediaType,
|
||||
Kind: jscontact.MediaKindPhoto,
|
||||
Uri: uri,
|
||||
MediaType: mime,
|
||||
Contexts: contexts,
|
||||
Label: label,
|
||||
}, u), nil
|
||||
// currently not supported, reported as https://github.com/stalwartlabs/stalwart/issues/2431
|
||||
case -1: // change this to 1 to enable it again
|
||||
boxes.mediaWithBlobId = true
|
||||
size := pickRandom(16, 24, 32, 48, 64)
|
||||
img := gofakeit.ImageJpeg(size, size)
|
||||
blob, err := c.uploadBlob(accountId, img, "image/jpeg")
|
||||
if err != nil {
|
||||
return nil, jscontact.Media{}, err
|
||||
}
|
||||
contexts := toBoolMapS(jscontact.MediaContextPrivate)
|
||||
return map[string]any{
|
||||
"@type": "Media",
|
||||
"kind": string(jscontact.MediaKindPhoto),
|
||||
"blobId": blob.BlobId,
|
||||
"contexts": structs.MapKeys(contexts, func(c jscontact.MediaContext) string { return string(c) }),
|
||||
"label": label,
|
||||
}, untype(jscontact.Media{
|
||||
Type: jscontact.MediaType,
|
||||
Kind: jscontact.MediaKindPhoto,
|
||||
BlobId: blob.BlobId,
|
||||
MediaType: blob.Type,
|
||||
Contexts: contexts,
|
||||
Label: label,
|
||||
}, u), nil
|
||||
|
||||
default:
|
||||
boxes.mediaWithExternalUri = true
|
||||
// use external uri
|
||||
uri := picsum(128, 128)
|
||||
contexts := toBoolMapS(jscontact.MediaContextWork)
|
||||
return map[string]any{
|
||||
"@type": "Media",
|
||||
"kind": string(jscontact.MediaKindPhoto),
|
||||
"uri": uri,
|
||||
"contexts": structs.MapKeys(contexts, func(c jscontact.MediaContext) string { return string(c) }),
|
||||
"label": label,
|
||||
}, untype(jscontact.Media{
|
||||
Type: jscontact.MediaType,
|
||||
Kind: jscontact.MediaKindPhoto,
|
||||
Uri: uri,
|
||||
Contexts: contexts,
|
||||
Label: label,
|
||||
}, u), nil
|
||||
}
|
||||
}); err != nil {
|
||||
return "", "", nil, boxes, err
|
||||
}
|
||||
if err := propmap(i%2 == 0, 1, 1, contact, "links", &card.Links, func(i int, id string) (map[string]any, jscontact.Link, error) {
|
||||
boxes.link = true
|
||||
return map[string]any{
|
||||
"@type": "Link",
|
||||
"kind": "contact",
|
||||
"uri": "mailto:" + person.Contact.Email,
|
||||
"pref": (i + 1) * 10,
|
||||
}, untype(jscontact.Link{
|
||||
Type: jscontact.LinkType,
|
||||
Kind: jscontact.LinkKindContact,
|
||||
Uri: "mailto:" + person.Contact.Email,
|
||||
Pref: uint((i + 1) * 10),
|
||||
}, u), nil
|
||||
}); err != nil {
|
||||
return "", "", nil, boxes, err
|
||||
}
|
||||
|
||||
id, err := s.CreateContact(c, accountId, contact)
|
||||
if err != nil {
|
||||
return "", "", nil, boxes, err
|
||||
}
|
||||
card.Id = id
|
||||
filled[id] = card
|
||||
printer(fmt.Sprintf("🧑🏻 created %*s/%v uid=%v", int(math.Log10(float64(count))+1), strconv.Itoa(int(i+1)), count, id))
|
||||
}
|
||||
return accountId, addressbookId, filled, boxes, nil
|
||||
}
|
||||
|
||||
func (s *StalwartTest) CreateContact(j *TestJmapClient, accountId string, contact map[string]any) (string, error) {
|
||||
body := map[string]any{
|
||||
"using": []string{JmapCore, JmapContacts},
|
||||
"methodCalls": []any{
|
||||
[]any{
|
||||
ContactCardType + "/set",
|
||||
map[string]any{
|
||||
"accountId": accountId,
|
||||
"create": map[string]any{
|
||||
"c": contact,
|
||||
},
|
||||
},
|
||||
"0",
|
||||
},
|
||||
},
|
||||
}
|
||||
return j.create("c", ContactCardType, body)
|
||||
}
|
||||
|
||||
@@ -2,14 +2,27 @@ package jmap
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"math/rand/v2"
|
||||
"math/rand"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/mail"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/brianvoe/gofakeit/v7"
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/imapclient"
|
||||
"github.com/jhillyerd/enmime/v2"
|
||||
"github.com/opencloud-eu/opencloud/pkg/structs"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEmails(t *testing.T) {
|
||||
@@ -17,7 +30,7 @@ func TestEmails(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
count := 15 + rand.IntN(20)
|
||||
count := 15 + rand.Intn(20)
|
||||
|
||||
require := require.New(t)
|
||||
|
||||
@@ -153,3 +166,375 @@ func matchEmail(t *testing.T, actual Email, expected filledMail, hasBodies bool)
|
||||
require.ElementsMatch(list, expected.attachments)
|
||||
}
|
||||
}
|
||||
|
||||
func htmlJoin(parts []string) []string {
|
||||
var result []string
|
||||
for i := range parts {
|
||||
result = append(result, fmt.Sprintf("<p>%v</p>", parts[i]))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
var paraSplitter = regexp.MustCompile("[\r\n]+")
|
||||
var emailSplitter = regexp.MustCompile("(.+)@(.+)$")
|
||||
|
||||
func htmlFormat(body string, msg enmime.MailBuilder) enmime.MailBuilder {
|
||||
return msg.HTML([]byte(strings.Join(htmlJoin(paraSplitter.Split(body, -1)), "\n")))
|
||||
}
|
||||
|
||||
func textFormat(body string, msg enmime.MailBuilder) enmime.MailBuilder {
|
||||
return msg.Text([]byte(body))
|
||||
}
|
||||
|
||||
func bothFormat(body string, msg enmime.MailBuilder) enmime.MailBuilder {
|
||||
msg = htmlFormat(body, msg)
|
||||
msg = textFormat(body, msg)
|
||||
return msg
|
||||
}
|
||||
|
||||
var formats = []func(string, enmime.MailBuilder) enmime.MailBuilder{
|
||||
htmlFormat,
|
||||
textFormat,
|
||||
bothFormat,
|
||||
}
|
||||
|
||||
type sender struct {
|
||||
first string
|
||||
last string
|
||||
from string
|
||||
sender string
|
||||
}
|
||||
|
||||
func (s sender) inject(b enmime.MailBuilder) enmime.MailBuilder {
|
||||
return b.From(s.first+" "+s.last, s.from).Header("Sender", s.sender)
|
||||
}
|
||||
|
||||
type senderGenerator struct {
|
||||
senders []sender
|
||||
}
|
||||
|
||||
func newSenderGenerator(numSenders int) senderGenerator {
|
||||
senders := make([]sender, numSenders)
|
||||
for i := range numSenders {
|
||||
person := gofakeit.Person()
|
||||
senders[i] = sender{
|
||||
first: person.FirstName,
|
||||
last: person.LastName,
|
||||
from: person.Contact.Email,
|
||||
sender: person.FirstName + " " + person.LastName + "<" + person.Contact.Email + ">",
|
||||
}
|
||||
}
|
||||
return senderGenerator{
|
||||
senders: senders,
|
||||
}
|
||||
}
|
||||
|
||||
func (s senderGenerator) nextSender() *sender {
|
||||
if len(s.senders) < 1 {
|
||||
panic("failed to determine a sender to use")
|
||||
} else {
|
||||
return &s.senders[rand.Intn(len(s.senders))]
|
||||
}
|
||||
}
|
||||
|
||||
func fakeFilename(extension string) string {
|
||||
return strings.ReplaceAll(gofakeit.Product().Name, " ", "_") + extension
|
||||
}
|
||||
|
||||
func mailboxId(role string, mailboxes []Mailbox) string {
|
||||
for _, m := range mailboxes {
|
||||
if m.Role == role {
|
||||
return m.Id
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type filledAttachment struct {
|
||||
name string
|
||||
size int
|
||||
mimeType string
|
||||
disposition string
|
||||
}
|
||||
|
||||
type filledMail struct {
|
||||
uid int
|
||||
attachments []filledAttachment
|
||||
subject string
|
||||
testId string
|
||||
messageId string
|
||||
keywords []string
|
||||
}
|
||||
|
||||
var allKeywords = map[string]imap.Flag{
|
||||
JmapKeywordAnswered: imap.FlagAnswered,
|
||||
JmapKeywordDraft: imap.FlagDraft,
|
||||
JmapKeywordFlagged: imap.FlagFlagged,
|
||||
JmapKeywordForwarded: imap.FlagForwarded,
|
||||
JmapKeywordJunk: imap.FlagJunk,
|
||||
JmapKeywordMdnSent: imap.FlagMDNSent,
|
||||
JmapKeywordNotJunk: imap.FlagNotJunk,
|
||||
JmapKeywordPhishing: imap.FlagPhishing,
|
||||
JmapKeywordSeen: imap.FlagSeen,
|
||||
}
|
||||
|
||||
func (s *StalwartTest) fillEmailsWithImap(folder string, count int) ([]filledMail, int, error) {
|
||||
to := fmt.Sprintf("%s <%s>", s.userPersonName, s.userEmail)
|
||||
ccEvery := 2
|
||||
bccEvery := 3
|
||||
attachmentEvery := 2
|
||||
senders := max(count/4, 1)
|
||||
maxThreadSize := 6
|
||||
maxAttachments := 4
|
||||
|
||||
tlsConfig := &tls.Config{InsecureSkipVerify: true}
|
||||
|
||||
c, err := imapclient.DialTLS(net.JoinHostPort(s.ip, strconv.Itoa(s.imapPort)), &imapclient.Options{TLSConfig: tlsConfig})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
defer func(imap *imapclient.Client) {
|
||||
err := imap.Close()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}(c)
|
||||
|
||||
if err = c.Login(s.username, s.password).Wait(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if _, err = c.Select(folder, &imap.SelectOptions{ReadOnly: false}).Wait(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if ids, err := c.Search(&imap.SearchCriteria{}, nil).Wait(); err != nil {
|
||||
return nil, 0, err
|
||||
} else {
|
||||
if len(ids.AllSeqNums()) > 0 {
|
||||
storeFlags := imap.StoreFlags{
|
||||
Op: imap.StoreFlagsAdd,
|
||||
Flags: []imap.Flag{imap.FlagDeleted},
|
||||
Silent: true,
|
||||
}
|
||||
if err = c.Store(ids.All, &storeFlags, nil).Close(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if err = c.Expunge().Close(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
log.Printf("🗑️ deleted %d messages in %s", len(ids.AllSeqNums()), folder)
|
||||
} else {
|
||||
log.Printf("ℹ️ did not delete any messages, %s is empty", folder)
|
||||
}
|
||||
}
|
||||
|
||||
address, err := mail.ParseAddress(to)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
displayName := address.Name
|
||||
|
||||
addressParts := emailSplitter.FindAllStringSubmatch(address.Address, 3)
|
||||
if len(addressParts) != 1 {
|
||||
return nil, 0, fmt.Errorf("address does not have one part: '%v' -> %v", address.Address, addressParts)
|
||||
}
|
||||
if len(addressParts[0]) != 3 {
|
||||
return nil, 0, fmt.Errorf("first address part does not have a size of 3: '%v'", addressParts[0])
|
||||
}
|
||||
|
||||
domain := addressParts[0][2]
|
||||
|
||||
toName := displayName
|
||||
toAddress := fmt.Sprintf("%s@%s", s.username, domain)
|
||||
ccName1 := "Team Lead"
|
||||
ccAddress1 := fmt.Sprintf("lead@%s", domain)
|
||||
ccName2 := "Coworker"
|
||||
ccAddress2 := fmt.Sprintf("coworker@%s", domain)
|
||||
bccName := "HR"
|
||||
bccAddress := fmt.Sprintf("corporate@%s", domain)
|
||||
|
||||
sg := newSenderGenerator(senders)
|
||||
thread := 0
|
||||
mails := make([]filledMail, count)
|
||||
for i := 0; i < count; thread++ {
|
||||
threadMessageId := fmt.Sprintf("%d.%d@%s", time.Now().Unix(), 1000000+rand.Intn(8999999), domain)
|
||||
threadSubject := strings.Trim(gofakeit.SentenceSimple(), ".") // remove the . at the end, looks weird
|
||||
threadSize := 1 + rand.Intn(maxThreadSize)
|
||||
lastMessageId := ""
|
||||
lastSubject := ""
|
||||
for t := 0; i < count && t < threadSize; t++ {
|
||||
sender := sg.nextSender()
|
||||
|
||||
format := formats[i%len(formats)]
|
||||
text := gofakeit.Paragraph(2+rand.Intn(9), 1+rand.Intn(4), 1+rand.Intn(32), "\n")
|
||||
|
||||
msg := sender.inject(enmime.Builder().To(toName, toAddress))
|
||||
|
||||
messageId := ""
|
||||
if lastMessageId == "" {
|
||||
// start a new thread
|
||||
msg = msg.Header("Message-ID", threadMessageId).Subject(threadSubject)
|
||||
lastMessageId = threadMessageId
|
||||
lastSubject = threadSubject
|
||||
messageId = threadMessageId
|
||||
} else {
|
||||
// we're continuing a thread
|
||||
messageId = fmt.Sprintf("%d.%d@%s", time.Now().Unix(), 1000000+rand.Intn(8999999), domain)
|
||||
inReplyTo := ""
|
||||
subject := ""
|
||||
switch rand.Intn(2) {
|
||||
case 0:
|
||||
// reply to first post in thread
|
||||
subject = "Re: " + threadSubject
|
||||
inReplyTo = threadMessageId
|
||||
default:
|
||||
// reply to last addition to thread
|
||||
subject = "Re: " + lastSubject
|
||||
inReplyTo = lastMessageId
|
||||
}
|
||||
msg = msg.Header("Message-ID", messageId).Header("In-Reply-To", inReplyTo).Subject(subject)
|
||||
lastMessageId = messageId
|
||||
lastSubject = subject
|
||||
}
|
||||
|
||||
if i%ccEvery == 0 {
|
||||
msg = msg.CCAddrs([]mail.Address{{Name: ccName1, Address: ccAddress1}, {Name: ccName2, Address: ccAddress2}})
|
||||
}
|
||||
if i%bccEvery == 0 {
|
||||
msg = msg.BCC(bccName, bccAddress)
|
||||
}
|
||||
|
||||
numAttachments := 0
|
||||
attachments := []filledAttachment{}
|
||||
if maxAttachments > 0 && i%attachmentEvery == 0 {
|
||||
numAttachments = rand.Intn(maxAttachments)
|
||||
for a := range numAttachments {
|
||||
switch rand.Intn(2) {
|
||||
case 0:
|
||||
filename := fakeFilename(".txt")
|
||||
attachment := gofakeit.Paragraph(2+rand.Intn(4), 1+rand.Intn(4), 1+rand.Intn(32), "\n")
|
||||
data := []byte(attachment)
|
||||
msg = msg.AddAttachment(data, "text/plain", filename)
|
||||
attachments = append(attachments, filledAttachment{
|
||||
name: filename,
|
||||
size: len(data),
|
||||
mimeType: "text/plain",
|
||||
disposition: "attachment",
|
||||
})
|
||||
default:
|
||||
filename := ""
|
||||
mimetype := ""
|
||||
var image []byte = nil
|
||||
switch rand.Intn(2) {
|
||||
case 0:
|
||||
filename = fakeFilename(".png")
|
||||
mimetype = "image/png"
|
||||
image = gofakeit.ImagePng(512, 512)
|
||||
default:
|
||||
filename = fakeFilename(".jpg")
|
||||
mimetype = "image/jpeg"
|
||||
image = gofakeit.ImageJpeg(400, 200)
|
||||
}
|
||||
disposition := ""
|
||||
switch rand.Intn(2) {
|
||||
case 0:
|
||||
msg = msg.AddAttachment(image, mimetype, filename)
|
||||
disposition = "attachment"
|
||||
default:
|
||||
msg = msg.AddInline(image, mimetype, filename, "c"+strconv.Itoa(a))
|
||||
disposition = "inline"
|
||||
}
|
||||
attachments = append(attachments, filledAttachment{
|
||||
name: filename,
|
||||
size: len(image),
|
||||
mimeType: mimetype,
|
||||
disposition: disposition,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
msg = format(text, msg)
|
||||
|
||||
flags := []imap.Flag{}
|
||||
keywords := pickRandomlyFromMap(allKeywords, 0, len(allKeywords))
|
||||
for _, f := range keywords {
|
||||
flags = append(flags, f)
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
part, _ := msg.Build()
|
||||
part.Encode(buf)
|
||||
mail := buf.String()
|
||||
|
||||
var options *imap.AppendOptions = nil
|
||||
if len(flags) > 0 {
|
||||
options = &imap.AppendOptions{Flags: flags}
|
||||
}
|
||||
|
||||
size := int64(len(mail))
|
||||
appendCmd := c.Append(folder, size, options)
|
||||
if _, err := appendCmd.Write([]byte(mail)); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if err := appendCmd.Close(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if appendData, err := appendCmd.Wait(); err != nil {
|
||||
return nil, 0, err
|
||||
} else {
|
||||
attachmentStr := ""
|
||||
if numAttachments > 0 {
|
||||
attachmentStr = " " + strings.Repeat("📎", numAttachments)
|
||||
}
|
||||
log.Printf("➕ appended %v/%v [in thread %v] uid=%v%s", i+1, count, thread+1, appendData.UID, attachmentStr)
|
||||
|
||||
mails[i] = filledMail{
|
||||
uid: int(appendData.UID),
|
||||
attachments: attachments,
|
||||
subject: msg.GetSubject(),
|
||||
messageId: messageId,
|
||||
keywords: slices.Collect(maps.Keys(keywords)),
|
||||
}
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
listCmd := c.List("", "%", &imap.ListOptions{
|
||||
ReturnStatus: &imap.StatusOptions{
|
||||
NumMessages: true,
|
||||
NumUnseen: true,
|
||||
},
|
||||
})
|
||||
countMap := map[string]int{}
|
||||
for {
|
||||
mbox := listCmd.Next()
|
||||
if mbox == nil {
|
||||
break
|
||||
}
|
||||
countMap[mbox.Mailbox] = int(*mbox.Status.NumMessages)
|
||||
}
|
||||
|
||||
inboxCount := -1
|
||||
for f, i := range countMap {
|
||||
if strings.Compare(strings.ToLower(f), strings.ToLower(folder)) == 0 {
|
||||
inboxCount = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if err = listCmd.Close(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if inboxCount == -1 {
|
||||
return nil, 0, fmt.Errorf("failed to find folder '%v' via IMAP", folder)
|
||||
}
|
||||
if count != inboxCount {
|
||||
return nil, 0, fmt.Errorf("wrong number of emails in the inbox after filling, expecting %v, has %v", count, inboxCount)
|
||||
}
|
||||
|
||||
return mails, thread, nil
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user