#25: deliver mail addressed to this server without going out to the internet
Every message was resolved through MX and delivered over SMTP, including one addressed to a mailbox on this very host. Two users on the same server could not write to each other without a working outbound port 25 - which the provider blocks, so on this deployment they could not write to each other at all. It also made internal mail take a pointless round trip through the public internet. The worker now checks whether the recipient resolves to a local mailbox and, if so, hands the message to the inbound path directly. The check runs ahead of the relay branch on purpose: sending local mail out to a smarthost only to have it delivered back is slower and has more ways to fail. The shortcut is narrow. It relies on ResolveDeliveryTargets, which only resolves addresses on an active local domain, and a test covers that a remote recipient still goes out over SMTP - otherwise local delivery would swallow mail meant for the internet. A resolver error is treated as retry rather than as not-local: falling through to SMTP on an unknown answer would send to the internet a message that may belong here. Local delivery is still content-scanned; a virus between two accounts on one host is no less a virus. This also surfaced a permissive test double: fakeAliasFanOutRepository claimed every address was local, which was harmless while nothing consulted it while routing and wrong the moment something did. It now hosts one address, as the real repository does, and the DSN fan-out test names it explicitly. Verified: the local-delivery test fails without the change (the recipient triggers an MX lookup) and passes with it; 22 Go packages green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0fcdd15b58
commit
c83f2f75b2
5 files changed
+198
-3
No files matched your search
@@ -0,0 +1,114 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"lambdamail/protocols/internal/application/port"
|
||||
"lambdamail/protocols/internal/domain/entity"
|
||||
)
|
||||
|
||||
// countingMXResolver fails the test if delivery ever tries to leave the host.
|
||||
type countingMXResolver struct{ lookups int }
|
||||
|
||||
func (c *countingMXResolver) LookupMX(_ context.Context, _ string) ([]string, error) {
|
||||
c.lookups++
|
||||
// An address that cannot be dialled, so a test that does reach here fails
|
||||
// on the assertion rather than on a timeout.
|
||||
return []string{"127.0.0.1:9"}, nil
|
||||
}
|
||||
|
||||
func newLocalDeliveryJob(to string) *entity.OutboundJob {
|
||||
mbID := uuid.New()
|
||||
return &entity.OutboundJob{
|
||||
ID: uuid.New(),
|
||||
MailboxID: &mbID,
|
||||
BlobID: uuid.New(),
|
||||
EnvelopeFrom: "me@example.test",
|
||||
EnvelopeTo: to,
|
||||
DestinationDomain: "example.test",
|
||||
Status: entity.OutboundJobStatusQueued,
|
||||
NextAttemptAt: time.Now(),
|
||||
ExpiresAt: time.Now().Add(time.Hour),
|
||||
}
|
||||
}
|
||||
|
||||
// Mail between two accounts on this server must not depend on outbound port
|
||||
// 25. Every message used to be resolved through MX and delivered over SMTP,
|
||||
// including one addressed to a mailbox on this very host - so on a provider
|
||||
// that blocks port 25, users could not write to each other on their own
|
||||
// server.
|
||||
func TestLocalRecipientIsDeliveredWithoutLeavingTheHost(t *testing.T) {
|
||||
recipient := "colleague@example.test"
|
||||
mailboxes := &fakeMailboxRepository{byAddress: map[string]port.MailboxRecord{
|
||||
recipient: {ID: uuid.New(), MaxMessageBytes: 1_000_000},
|
||||
}}
|
||||
messages := &recordingMessageRepository{}
|
||||
inbound := NewProcessInboundEmailUseCase(
|
||||
mailboxes, &stubBlobStorage{ref: port.BlobRef{ID: uuid.New(), SHA256: "s", SizeBytes: 4}}, messages,
|
||||
)
|
||||
|
||||
mx := &countingMXResolver{}
|
||||
repo := &fakeOutboundRepo{}
|
||||
job := newLocalDeliveryJob(recipient)
|
||||
repo.jobs = append(repo.jobs, job)
|
||||
|
||||
worker := NewOutboundWorkerUseCase(
|
||||
repo, mx,
|
||||
&fakeBlobReader{payload: []byte("From: me@example.test\r\nTo: " + recipient + "\r\n\r\nhello\r\n")},
|
||||
inbound, mailboxes, "mail.example.test",
|
||||
)
|
||||
|
||||
if _, err := worker.ProcessBatch(context.Background(), "w1", 10); err != nil {
|
||||
t.Fatalf("ProcessBatch: %v", err)
|
||||
}
|
||||
|
||||
if mx.lookups != 0 {
|
||||
t.Errorf("a local recipient triggered %d MX lookups; delivery left the host", mx.lookups)
|
||||
}
|
||||
if len(messages.persisted) != 1 {
|
||||
t.Fatalf("want the message stored once for the local recipient, got %d", len(messages.persisted))
|
||||
}
|
||||
if job.Status != entity.OutboundJobStatusDelivered {
|
||||
t.Errorf("job status = %q, want delivered", job.Status)
|
||||
}
|
||||
if job.TlsPolicyUsed != entity.TLSModeLocal {
|
||||
t.Errorf("tls policy = %q, want %q - nothing crossed a network",
|
||||
job.TlsPolicyUsed, entity.TLSModeLocal)
|
||||
}
|
||||
}
|
||||
|
||||
// The shortcut must be narrow: an address this server does not host still goes
|
||||
// out over SMTP, or local delivery would swallow the internet's mail.
|
||||
func TestRemoteRecipientStillGoesOutOverSmtp(t *testing.T) {
|
||||
mailboxes := &fakeMailboxRepository{byAddress: map[string]port.MailboxRecord{}}
|
||||
messages := &recordingMessageRepository{}
|
||||
inbound := NewProcessInboundEmailUseCase(
|
||||
mailboxes, &stubBlobStorage{ref: port.BlobRef{ID: uuid.New()}}, messages,
|
||||
)
|
||||
|
||||
mx := &countingMXResolver{}
|
||||
repo := &fakeOutboundRepo{}
|
||||
job := newLocalDeliveryJob("stranger@remote.test")
|
||||
job.DestinationDomain = "remote.test"
|
||||
repo.jobs = append(repo.jobs, job)
|
||||
|
||||
worker := NewOutboundWorkerUseCase(
|
||||
repo, mx, &fakeBlobReader{payload: []byte("From: me\r\n\r\nhi\r\n")},
|
||||
inbound, mailboxes, "mail.example.test",
|
||||
)
|
||||
|
||||
if _, err := worker.ProcessBatch(context.Background(), "w1", 10); err != nil {
|
||||
t.Fatalf("ProcessBatch: %v", err)
|
||||
}
|
||||
|
||||
if mx.lookups == 0 {
|
||||
t.Error("a remote recipient was not resolved through MX")
|
||||
}
|
||||
if len(messages.persisted) != 0 {
|
||||
t.Errorf("a remote message was filed into a local mailbox: %+v", messages.persisted)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user