#8: implement ManageSieve protocol server (RFC 5804)
This commit is contained in:
1 parent
5a7dcc002a
commit
0e6fc27bc1
13 files changed
+1634
No files matched your search
@@ -0,0 +1,243 @@
|
||||
package managesievepresentation
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/alexedwards/argon2id"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"lambdamail/protocols/internal/application/port"
|
||||
appusecase "lambdamail/protocols/internal/application/usecase"
|
||||
)
|
||||
|
||||
type fakeAuthRepository struct {
|
||||
byAddress map[string]port.MailboxAuth
|
||||
}
|
||||
|
||||
func (f *fakeAuthRepository) FindByAddress(_ context.Context, address string) (*port.MailboxAuth, error) {
|
||||
rec, ok := f.byAddress[address]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
return &rec, nil
|
||||
}
|
||||
|
||||
type fakeSieveRepository struct {
|
||||
scripts map[string]map[string]port.SieveScriptRecord
|
||||
}
|
||||
|
||||
func newFakeSieveRepository() *fakeSieveRepository {
|
||||
return &fakeSieveRepository{scripts: make(map[string]map[string]port.SieveScriptRecord)}
|
||||
}
|
||||
|
||||
func (f *fakeSieveRepository) GetScript(_ context.Context, mailboxID, name string) (*port.SieveScriptRecord, error) {
|
||||
mb, ok := f.scripts[mailboxID]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
rec, ok := mb[name]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
return &rec, nil
|
||||
}
|
||||
|
||||
func (f *fakeSieveRepository) PutScript(_ context.Context, mailboxID, name, script string) error {
|
||||
mb, ok := f.scripts[mailboxID]
|
||||
if !ok {
|
||||
mb = make(map[string]port.SieveScriptRecord)
|
||||
f.scripts[mailboxID] = mb
|
||||
}
|
||||
existing, exists := mb[name]
|
||||
isActive := false
|
||||
if exists {
|
||||
isActive = existing.IsActive
|
||||
}
|
||||
mb[name] = port.SieveScriptRecord{
|
||||
ID: uuid.New().String(),
|
||||
MailboxID: mailboxID,
|
||||
Name: name,
|
||||
Script: script,
|
||||
IsActive: isActive,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeSieveRepository) ListScripts(_ context.Context, mailboxID string) ([]port.SieveScriptRecord, error) {
|
||||
mb, ok := f.scripts[mailboxID]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
var out []port.SieveScriptRecord
|
||||
for _, rec := range mb {
|
||||
out = append(out, rec)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (f *fakeSieveRepository) SetActiveScript(_ context.Context, mailboxID, name string) error {
|
||||
mb, ok := f.scripts[mailboxID]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
for k, rec := range mb {
|
||||
rec.IsActive = (k == name)
|
||||
mb[k] = rec
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeSieveRepository) DeleteScript(_ context.Context, mailboxID, name string) error {
|
||||
mb, ok := f.scripts[mailboxID]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
delete(mb, name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeSieveRepository) RenameScript(_ context.Context, mailboxID, oldName, newName string) error {
|
||||
mb, ok := f.scripts[mailboxID]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
rec := mb[oldName]
|
||||
delete(mb, oldName)
|
||||
rec.Name = newName
|
||||
mb[newName] = rec
|
||||
return nil
|
||||
}
|
||||
|
||||
func setupManageSieveServer(t *testing.T) (net.Listener, func()) {
|
||||
t.Helper()
|
||||
password := "correct horse battery staple"
|
||||
hash, err := argon2id.CreateHash(password, argon2id.DefaultParams)
|
||||
if err != nil {
|
||||
t.Fatalf("hash: %v", err)
|
||||
}
|
||||
mailboxID := uuid.New()
|
||||
|
||||
auth := &fakeAuthRepository{byAddress: map[string]port.MailboxAuth{
|
||||
"user@example.test": {ID: mailboxID, PasswordHash: hash},
|
||||
}}
|
||||
sieveRepo := newFakeSieveRepository()
|
||||
|
||||
uc := appusecase.NewManageSieveSessionUseCase(auth, sieveRepo)
|
||||
srv := NewServer("127.0.0.1:0", uc, nil)
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
go srv.Serve(ln)
|
||||
|
||||
return ln, func() {
|
||||
ln.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func TestManageSieveServer_FullLifecycle(t *testing.T) {
|
||||
ln, cleanup := setupManageSieveServer(t)
|
||||
defer cleanup()
|
||||
|
||||
c, err := net.Dial("tcp", ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatalf("dial: %v", err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
reader := bufio.NewReader(c)
|
||||
writer := bufio.NewWriter(c)
|
||||
|
||||
readLine := func() string {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
t.Fatalf("read: %v", err)
|
||||
}
|
||||
return strings.TrimRight(line, "\r\n")
|
||||
}
|
||||
|
||||
sendCmd := func(cmd string) {
|
||||
writer.WriteString(cmd + "\r\n")
|
||||
writer.Flush()
|
||||
}
|
||||
|
||||
// 1. Read greeting capabilities
|
||||
for {
|
||||
l := readLine()
|
||||
if strings.HasPrefix(l, "OK") {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 2. CAPABILITY
|
||||
sendCmd("CAPABILITY")
|
||||
for {
|
||||
l := readLine()
|
||||
if strings.HasPrefix(l, "OK") {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 3. AUTHENTICATE PLAIN
|
||||
saslPayload := base64.StdEncoding.EncodeToString([]byte("\x00user@example.test\x00correct horse battery staple"))
|
||||
sendCmd(fmt.Sprintf("AUTHENTICATE \"PLAIN\" %s", saslPayload))
|
||||
if authResp := readLine(); !strings.HasPrefix(authResp, "OK") {
|
||||
t.Fatalf("AUTH resp = %q, want OK", authResp)
|
||||
}
|
||||
|
||||
// 4. CHECKSCRIPT
|
||||
scriptContent := `require ["fileinto"]; if header :is "Subject" "Spam" { fileinto "Junk"; }`
|
||||
sendCmd(fmt.Sprintf("CHECKSCRIPT {%d+}\r\n%s", len(scriptContent), scriptContent))
|
||||
if checkResp := readLine(); !strings.HasPrefix(checkResp, "OK") {
|
||||
t.Fatalf("CHECKSCRIPT resp = %q", checkResp)
|
||||
}
|
||||
|
||||
// 5. PUTSCRIPT
|
||||
sendCmd(fmt.Sprintf("PUTSCRIPT \"my_rules\" {%d+}\r\n%s", len(scriptContent), scriptContent))
|
||||
if putResp := readLine(); !strings.HasPrefix(putResp, "OK") {
|
||||
t.Fatalf("PUTSCRIPT resp = %q", putResp)
|
||||
}
|
||||
|
||||
// 6. GETSCRIPT
|
||||
sendCmd("GETSCRIPT \"my_rules\"")
|
||||
getLiteralHead := readLine()
|
||||
if !strings.HasPrefix(getLiteralHead, "{") {
|
||||
t.Fatalf("GETSCRIPT literal head = %q", getLiteralHead)
|
||||
}
|
||||
getScriptContent := readLine()
|
||||
if getScriptContent != scriptContent {
|
||||
t.Errorf("GETSCRIPT content = %q, want %q", getScriptContent, scriptContent)
|
||||
}
|
||||
if getOk := readLine(); !strings.HasPrefix(getOk, "OK") {
|
||||
t.Fatalf("GETSCRIPT term = %q", getOk)
|
||||
}
|
||||
|
||||
// 7. LISTSCRIPTS
|
||||
sendCmd("LISTSCRIPTS")
|
||||
listLine := readLine()
|
||||
if !strings.Contains(listLine, `"my_rules"`) {
|
||||
t.Fatalf("LISTSCRIPTS line = %q", listLine)
|
||||
}
|
||||
if listOk := readLine(); !strings.HasPrefix(listOk, "OK") {
|
||||
t.Fatalf("LISTSCRIPTS term = %q", listOk)
|
||||
}
|
||||
|
||||
// 8. SETACTIVE
|
||||
sendCmd("SETACTIVE \"my_rules\"")
|
||||
if setActiveOk := readLine(); !strings.HasPrefix(setActiveOk, "OK") {
|
||||
t.Fatalf("SETACTIVE term = %q", setActiveOk)
|
||||
}
|
||||
|
||||
// 9. LOGOUT
|
||||
sendCmd("LOGOUT")
|
||||
if logoutOk := readLine(); !strings.HasPrefix(logoutOk, "OK") {
|
||||
t.Fatalf("LOGOUT term = %q", logoutOk)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user