#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,428 @@
|
||||
package managesievepresentation
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"lambdamail/protocols/internal/application/port"
|
||||
appusecase "lambdamail/protocols/internal/application/usecase"
|
||||
)
|
||||
|
||||
type state int
|
||||
|
||||
const (
|
||||
stateAuth state = iota
|
||||
stateMain
|
||||
)
|
||||
|
||||
type conn struct {
|
||||
rawConn net.Conn
|
||||
reader *bufio.Reader
|
||||
writer *bufio.Writer
|
||||
tlsProvider port.CertProvider
|
||||
|
||||
useCase *appusecase.ManageSieveSessionUseCase
|
||||
|
||||
state state
|
||||
username string
|
||||
mailboxID string
|
||||
isTLS bool
|
||||
}
|
||||
|
||||
func newConn(c net.Conn, useCase *appusecase.ManageSieveSessionUseCase, tlsProvider port.CertProvider) *conn {
|
||||
return &conn{
|
||||
rawConn: c,
|
||||
reader: bufio.NewReader(c),
|
||||
writer: bufio.NewWriter(c),
|
||||
useCase: useCase,
|
||||
tlsProvider: tlsProvider,
|
||||
state: stateAuth,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *conn) serve() {
|
||||
defer c.rawConn.Close()
|
||||
|
||||
c.sendCapabilities()
|
||||
c.writeLine(`OK "LambdaMail ManageSieve ready."`)
|
||||
|
||||
for {
|
||||
line, err := c.reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
line = strings.TrimRight(line, "\r\n")
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(line, " ", 2)
|
||||
cmd := strings.ToUpper(parts[0])
|
||||
arg := ""
|
||||
if len(parts) > 1 {
|
||||
arg = parts[1]
|
||||
}
|
||||
|
||||
switch cmd {
|
||||
case "CAPABILITY":
|
||||
c.handleCapability()
|
||||
case "STARTTLS":
|
||||
if c.handleStartTLS() {
|
||||
c.sendCapabilities()
|
||||
c.writeLine(`OK "TLS negotiation complete."`)
|
||||
}
|
||||
case "AUTHENTICATE":
|
||||
c.handleAuthenticate(arg)
|
||||
case "PUTSCRIPT":
|
||||
c.handlePutScript(arg)
|
||||
case "GETSCRIPT":
|
||||
c.handleGetScript(arg)
|
||||
case "SETACTIVE":
|
||||
c.handleSetActive(arg)
|
||||
case "HAVESPACE":
|
||||
c.handleHaveSpace(arg)
|
||||
case "LISTSCRIPTS":
|
||||
c.handleListScripts()
|
||||
case "DELETESCRIPT":
|
||||
c.handleDeleteScript(arg)
|
||||
case "RENAMESCRIPT":
|
||||
c.handleRenameScript(arg)
|
||||
case "CHECKSCRIPT":
|
||||
c.handleCheckScript(arg)
|
||||
case "LOGOUT", "QUIT":
|
||||
c.writeLine(`OK "Logout complete."`)
|
||||
return
|
||||
default:
|
||||
c.writeLine(`NO "Unknown or unsupported command."`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *conn) writeLine(msg string) error {
|
||||
if _, err := c.writer.WriteString(msg + "\r\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.writer.Flush()
|
||||
}
|
||||
|
||||
func (c *conn) sendCapabilities() {
|
||||
c.writer.WriteString("\"IMPLEMENTATION\" \"LambdaMail ManageSieve\"\r\n")
|
||||
c.writer.WriteString("\"SASL\" \"PLAIN\"\r\n")
|
||||
if !c.isTLS && c.tlsProvider != nil {
|
||||
c.writer.WriteString("\"STARTTLS\"\r\n")
|
||||
}
|
||||
c.writer.WriteString("\"SIEVE\" \"fileinto reject envelope subaddress body imap4flags variables\"\r\n")
|
||||
c.writer.Flush()
|
||||
}
|
||||
|
||||
func (c *conn) handleCapability() {
|
||||
c.sendCapabilities()
|
||||
c.writeLine(`OK "Capability completed."`)
|
||||
}
|
||||
|
||||
func (c *conn) handleStartTLS() bool {
|
||||
if c.isTLS {
|
||||
c.writeLine(`NO "TLS already active."`)
|
||||
return false
|
||||
}
|
||||
if c.tlsProvider == nil {
|
||||
c.writeLine(`NO "STARTTLS not supported."`)
|
||||
return false
|
||||
}
|
||||
if err := c.writeLine(`OK "Begin TLS negotiation."`); err != nil {
|
||||
return false
|
||||
}
|
||||
tlsConfig := &tls.Config{
|
||||
GetCertificate: c.tlsProvider.GetCertificate,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
tlsConn := tls.Server(c.rawConn, tlsConfig)
|
||||
if err := tlsConn.Handshake(); err != nil {
|
||||
return false
|
||||
}
|
||||
c.rawConn = tlsConn
|
||||
c.reader = bufio.NewReader(tlsConn)
|
||||
c.writer = bufio.NewWriter(tlsConn)
|
||||
c.isTLS = true
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *conn) handleAuthenticate(arg string) {
|
||||
if c.state != stateAuth {
|
||||
c.writeLine(`NO "Already authenticated."`)
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.Fields(arg)
|
||||
if len(parts) == 0 || strings.ToUpper(parts[0]) != `"PLAIN"` && strings.ToUpper(parts[0]) != `PLAIN` {
|
||||
c.writeLine(`NO "Unsupported SASL mechanism."`)
|
||||
return
|
||||
}
|
||||
|
||||
var respB64 string
|
||||
if len(parts) > 1 {
|
||||
respB64 = parts[1]
|
||||
} else {
|
||||
// Challenge response mode
|
||||
c.writeLine(`""`)
|
||||
line, err := c.reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
respB64 = strings.TrimRight(line, "\r\n")
|
||||
}
|
||||
|
||||
decoded, err := base64.StdEncoding.DecodeString(respB64)
|
||||
if err != nil {
|
||||
c.writeLine(`NO "Invalid base64 payload."`)
|
||||
return
|
||||
}
|
||||
|
||||
// SASL PLAIN format: [authzid] \0 authcid \0 passwd
|
||||
saslParts := bytes.Split(decoded, []byte{0})
|
||||
var user, pass string
|
||||
if len(saslParts) == 3 {
|
||||
user = string(saslParts[1])
|
||||
pass = string(saslParts[2])
|
||||
} else if len(saslParts) == 2 {
|
||||
user = string(saslParts[0])
|
||||
pass = string(saslParts[1])
|
||||
} else {
|
||||
c.writeLine(`NO "Malformed SASL PLAIN authentication."`)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
mailboxID, err := c.useCase.Login(ctx, user, pass)
|
||||
if err != nil {
|
||||
c.writeLine(`NO "Authentication failed."`)
|
||||
return
|
||||
}
|
||||
|
||||
c.username = user
|
||||
c.mailboxID = mailboxID
|
||||
c.state = stateMain
|
||||
c.writeLine(`OK "Logged in."`)
|
||||
}
|
||||
|
||||
func (c *conn) handlePutScript(arg string) {
|
||||
if c.state != stateMain {
|
||||
c.writeLine(`NO "Authentication required."`)
|
||||
return
|
||||
}
|
||||
|
||||
name, content, err := c.parseNameAndContent(arg)
|
||||
if err != nil {
|
||||
c.writeLine(fmt.Sprintf(`NO "%v"`, err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
if err := c.useCase.PutScript(ctx, c.mailboxID, name, content); err != nil {
|
||||
c.writeLine(fmt.Sprintf(`NO "%v"`, err))
|
||||
return
|
||||
}
|
||||
|
||||
c.writeLine(`OK "PUTSCRIPT completed."`)
|
||||
}
|
||||
|
||||
func (c *conn) handleGetScript(arg string) {
|
||||
if c.state != stateMain {
|
||||
c.writeLine(`NO "Authentication required."`)
|
||||
return
|
||||
}
|
||||
|
||||
name := c.parseStringLiteral(arg)
|
||||
if name == "" {
|
||||
c.writeLine(`NO "Missing script name."`)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
rec, err := c.useCase.GetScript(ctx, c.mailboxID, name)
|
||||
if err != nil || rec == nil {
|
||||
c.writeLine(`NO "Script does not exist."`)
|
||||
return
|
||||
}
|
||||
|
||||
c.writer.WriteString(fmt.Sprintf("{%d+}\r\n%s\r\n", len(rec.Script), rec.Script))
|
||||
c.writeLine(`OK "GETSCRIPT completed."`)
|
||||
}
|
||||
|
||||
func (c *conn) handleSetActive(arg string) {
|
||||
if c.state != stateMain {
|
||||
c.writeLine(`NO "Authentication required."`)
|
||||
return
|
||||
}
|
||||
|
||||
name := c.parseStringLiteral(arg)
|
||||
ctx := context.Background()
|
||||
if err := c.useCase.SetActive(ctx, c.mailboxID, name); err != nil {
|
||||
c.writeLine(fmt.Sprintf(`NO "%v"`, err))
|
||||
return
|
||||
}
|
||||
|
||||
c.writeLine(`OK "SETACTIVE completed."`)
|
||||
}
|
||||
|
||||
func (c *conn) handleHaveSpace(arg string) {
|
||||
parts := strings.Fields(arg)
|
||||
if len(parts) < 2 {
|
||||
c.writeLine(`NO "Missing arguments for HAVESPACE."`)
|
||||
return
|
||||
}
|
||||
size, err := strconv.Atoi(parts[1])
|
||||
if err != nil || size < 0 || size > appusecase.MaxScriptSizeBytes {
|
||||
c.writeLine(`NO "Quota exceeded."`)
|
||||
return
|
||||
}
|
||||
c.writeLine(`OK "Storage space available."`)
|
||||
}
|
||||
|
||||
func (c *conn) handleListScripts() {
|
||||
if c.state != stateMain {
|
||||
c.writeLine(`NO "Authentication required."`)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
scripts, err := c.useCase.ListScripts(ctx, c.mailboxID)
|
||||
if err != nil {
|
||||
c.writeLine(`NO "Unable to list scripts."`)
|
||||
return
|
||||
}
|
||||
|
||||
for _, s := range scripts {
|
||||
if s.IsActive {
|
||||
c.writer.WriteString(fmt.Sprintf("%q ACTIVE\r\n", s.Name))
|
||||
} else {
|
||||
c.writer.WriteString(fmt.Sprintf("%q\r\n", s.Name))
|
||||
}
|
||||
}
|
||||
c.writeLine(`OK "LISTSCRIPTS completed."`)
|
||||
}
|
||||
|
||||
func (c *conn) handleDeleteScript(arg string) {
|
||||
if c.state != stateMain {
|
||||
c.writeLine(`NO "Authentication required."`)
|
||||
return
|
||||
}
|
||||
|
||||
name := c.parseStringLiteral(arg)
|
||||
if name == "" {
|
||||
c.writeLine(`NO "Missing script name."`)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
if err := c.useCase.DeleteScript(ctx, c.mailboxID, name); err != nil {
|
||||
c.writeLine(fmt.Sprintf(`NO "%v"`, err))
|
||||
return
|
||||
}
|
||||
|
||||
c.writeLine(`OK "DELETESCRIPT completed."`)
|
||||
}
|
||||
|
||||
func (c *conn) handleRenameScript(arg string) {
|
||||
if c.state != stateMain {
|
||||
c.writeLine(`NO "Authentication required."`)
|
||||
return
|
||||
}
|
||||
|
||||
parts := c.parseTwoStrings(arg)
|
||||
if len(parts) < 2 {
|
||||
c.writeLine(`NO "Missing script names for RENAMESCRIPT."`)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
if err := c.useCase.RenameScript(ctx, c.mailboxID, parts[0], parts[1]); err != nil {
|
||||
c.writeLine(fmt.Sprintf(`NO "%v"`, err))
|
||||
return
|
||||
}
|
||||
|
||||
c.writeLine(`OK "RENAMESCRIPT completed."`)
|
||||
}
|
||||
|
||||
func (c *conn) handleCheckScript(arg string) {
|
||||
var content string
|
||||
var err error
|
||||
|
||||
if strings.HasPrefix(arg, "{") {
|
||||
_, content, err = c.parseNameAndContent(`"" ` + arg)
|
||||
} else {
|
||||
content = c.parseStringLiteral(arg)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.writeLine(fmt.Sprintf(`NO "%v"`, err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
if err := c.useCase.CheckScript(ctx, content); err != nil {
|
||||
c.writeLine(fmt.Sprintf(`NO "%v"`, err))
|
||||
return
|
||||
}
|
||||
|
||||
c.writeLine(`OK "CHECKSCRIPT completed."`)
|
||||
}
|
||||
|
||||
func (c *conn) parseStringLiteral(input string) string {
|
||||
input = strings.TrimSpace(input)
|
||||
if strings.HasPrefix(input, `"`) && strings.HasSuffix(input, `"`) && len(input) >= 2 {
|
||||
return input[1 : len(input)-1]
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
func (c *conn) parseTwoStrings(input string) []string {
|
||||
fields := strings.Fields(input)
|
||||
var out []string
|
||||
for _, f := range fields {
|
||||
out = append(out, c.parseStringLiteral(f))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (c *conn) parseNameAndContent(arg string) (string, string, error) {
|
||||
parts := strings.SplitN(arg, " ", 2)
|
||||
if len(parts) < 2 {
|
||||
return "", "", fmt.Errorf("missing parameters")
|
||||
}
|
||||
|
||||
name := c.parseStringLiteral(parts[0])
|
||||
literalHeader := strings.TrimSpace(parts[1])
|
||||
|
||||
if !strings.HasPrefix(literalHeader, "{") || !strings.HasSuffix(literalHeader, "}") {
|
||||
// Not literal string - treat literalHeader as quoted string
|
||||
return name, c.parseStringLiteral(literalHeader), nil
|
||||
}
|
||||
|
||||
sizeStr := strings.TrimPrefix(literalHeader, "{")
|
||||
sizeStr = strings.TrimSuffix(sizeStr, "}")
|
||||
sizeStr = strings.TrimSuffix(sizeStr, "+")
|
||||
|
||||
size, err := strconv.Atoi(sizeStr)
|
||||
if err != nil || size < 0 {
|
||||
return "", "", fmt.Errorf("invalid literal size")
|
||||
}
|
||||
|
||||
buf := make([]byte, size)
|
||||
if _, err := io.ReadFull(c.reader, buf); err != nil {
|
||||
return "", "", fmt.Errorf("unable to read script content: %w", err)
|
||||
}
|
||||
|
||||
// Consume trailing CRLF
|
||||
c.reader.ReadString('\n')
|
||||
|
||||
return name, string(buf), nil
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package managesievepresentation
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"lambdamail/protocols/internal/application/port"
|
||||
appusecase "lambdamail/protocols/internal/application/usecase"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
addr string
|
||||
useCase *appusecase.ManageSieveSessionUseCase
|
||||
tlsProvider port.CertProvider
|
||||
listener net.Listener
|
||||
}
|
||||
|
||||
func NewServer(addr string, useCase *appusecase.ManageSieveSessionUseCase, tlsProvider port.CertProvider) *Server {
|
||||
return &Server{
|
||||
addr: addr,
|
||||
useCase: useCase,
|
||||
tlsProvider: tlsProvider,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) ListenAndServe() error {
|
||||
ln, err := net.Listen("tcp", s.addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.listener = ln
|
||||
return s.Serve(ln)
|
||||
}
|
||||
|
||||
func (s *Server) Serve(ln net.Listener) error {
|
||||
s.listener = ln
|
||||
for {
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func(rawConn net.Conn) {
|
||||
connHandler := newConn(rawConn, s.useCase, s.tlsProvider)
|
||||
connHandler.serve()
|
||||
}(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Close() error {
|
||||
if s.listener != nil {
|
||||
return s.listener.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user