outbound_test.go raw

   1  package bridge
   2  
   3  import (
   4  	"context"
   5  	"fmt"
   6  	"net"
   7  	"strings"
   8  	"testing"
   9  	"time"
  10  
  11  	gosmtp "github.com/emersion/go-smtp"
  12  	bridgesmtp "next.orly.dev/pkg/bridge/smtp"
  13  )
  14  
  15  func TestOutboundProcessor_NotSubscribed(t *testing.T) {
  16  	var replies []string
  17  	sendDM := func(pubkey, content string) error {
  18  		replies = append(replies, content)
  19  		return nil
  20  	}
  21  
  22  	store := NewMemorySubscriptionStore()
  23  	handler := NewSubscriptionHandler(store, nil, sendDM, 2100, nil, 0)
  24  	op := NewOutboundProcessor(nil, nil, handler, "bridge.example.com", sendDM, nil)
  25  
  26  	err := op.ProcessOutbound("user1", "To: alice@example.com\n\nHello")
  27  	if err != nil {
  28  		t.Fatalf("unexpected error: %v", err)
  29  	}
  30  	if len(replies) == 0 {
  31  		t.Fatal("expected subscription required reply")
  32  	}
  33  	if !strings.Contains(replies[0], "subscription") {
  34  		t.Errorf("reply = %q, want subscription message", replies[0])
  35  	}
  36  }
  37  
  38  func TestOutboundProcessor_RateLimited(t *testing.T) {
  39  	var replies []string
  40  	sendDM := func(pubkey, content string) error {
  41  		replies = append(replies, content)
  42  		return nil
  43  	}
  44  
  45  	rl := NewRateLimiter(RateLimitConfig{
  46  		PerUserPerHour: 1,
  47  		MinInterval:    0,
  48  	})
  49  	rl.Record("user1")
  50  
  51  	op := NewOutboundProcessor(nil, rl, nil, "bridge.example.com", sendDM, nil)
  52  
  53  	err := op.ProcessOutbound("user1", "To: alice@example.com\n\nHello")
  54  	if err != nil {
  55  		t.Fatalf("unexpected error: %v", err)
  56  	}
  57  	if len(replies) == 0 {
  58  		t.Fatal("expected rate limit reply")
  59  	}
  60  	if !strings.Contains(replies[0], "Rate limit") {
  61  		t.Errorf("reply = %q, want rate limit message", replies[0])
  62  	}
  63  }
  64  
  65  func TestOutboundProcessor_NoRecipients(t *testing.T) {
  66  	var replies []string
  67  	sendDM := func(pubkey, content string) error {
  68  		replies = append(replies, content)
  69  		return nil
  70  	}
  71  
  72  	op := NewOutboundProcessor(nil, nil, nil, "bridge.example.com", sendDM, nil)
  73  
  74  	err := op.ProcessOutbound("user1", "Subject: No recipient\n\nBody")
  75  	if err != nil {
  76  		t.Fatalf("unexpected error: %v", err)
  77  	}
  78  	if len(replies) == 0 {
  79  		t.Fatal("expected no-recipient reply")
  80  	}
  81  	if !strings.Contains(replies[0], "No recipients") {
  82  		t.Errorf("reply = %q, want no-recipients message", replies[0])
  83  	}
  84  }
  85  
  86  func TestOutboundProcessor_EmptyContent(t *testing.T) {
  87  	var replies []string
  88  	sendDM := func(pubkey, content string) error {
  89  		replies = append(replies, content)
  90  		return nil
  91  	}
  92  
  93  	op := NewOutboundProcessor(nil, nil, nil, "bridge.example.com", sendDM, nil)
  94  
  95  	err := op.ProcessOutbound("user1", "")
  96  	if err != nil {
  97  		t.Fatalf("unexpected error: %v", err)
  98  	}
  99  	if len(replies) == 0 {
 100  		t.Fatal("expected reply")
 101  	}
 102  }
 103  
 104  func TestOutboundProcessor_WithSubscription_SMTPNil(t *testing.T) {
 105  	var replies []string
 106  	sendDM := func(pubkey, content string) error {
 107  		replies = append(replies, content)
 108  		return nil
 109  	}
 110  
 111  	store := NewMemorySubscriptionStore()
 112  	store.Save(&Subscription{
 113  		PubkeyHex: "user1",
 114  		ExpiresAt: time.Now().Add(24 * time.Hour),
 115  	})
 116  	handler := NewSubscriptionHandler(store, nil, sendDM, 2100, nil, 0)
 117  
 118  	op := NewOutboundProcessor(nil, nil, handler, "bridge.example.com", sendDM, nil)
 119  
 120  	// Will panic because smtpClient is nil when calling Send
 121  	func() {
 122  		defer func() {
 123  			r := recover()
 124  			if r == nil {
 125  				t.Fatal("expected panic from nil smtpClient")
 126  			}
 127  		}()
 128  		op.ProcessOutbound("user1", "To: alice@example.com\n\nHello")
 129  	}()
 130  }
 131  
 132  func TestOutboundProcessor_Reply_NilSendDM(t *testing.T) {
 133  	op := &OutboundProcessor{sendDM: nil}
 134  	op.reply("user1", "test") // should not panic
 135  }
 136  
 137  func TestOutboundProcessor_Reply_Error(t *testing.T) {
 138  	op := &OutboundProcessor{
 139  		sendDM: func(pubkey, content string) error {
 140  			return fmt.Errorf("send failed")
 141  		},
 142  	}
 143  	op.reply("user1", "test") // should not panic, just log
 144  }
 145  
 146  func TestOutboundProcessor_Reply_Success(t *testing.T) {
 147  	var sent string
 148  	op := &OutboundProcessor{
 149  		sendDM: func(pubkey, content string) error {
 150  			sent = content
 151  			return nil
 152  		},
 153  	}
 154  	op.reply("user1", "hello")
 155  	if sent != "hello" {
 156  		t.Errorf("sent = %q, want hello", sent)
 157  	}
 158  }
 159  
 160  func TestOutboundProcessor_FullFlow(t *testing.T) {
 161  	var received *bridgesmtp.InboundEmail
 162  	server := bridgesmtp.NewServer(bridgesmtp.ServerConfig{
 163  		Domain:     "test.example.com",
 164  		ListenAddr: "127.0.0.1:0",
 165  	}, func(email *bridgesmtp.InboundEmail) error {
 166  		received = email
 167  		return nil
 168  	})
 169  
 170  	if err := server.Start(); err != nil {
 171  		t.Fatalf("start server: %v", err)
 172  	}
 173  	defer server.Stop(context.Background())
 174  
 175  	addr := server.Addr().String()
 176  	_, port, _ := net.SplitHostPort(addr)
 177  
 178  	smtpClient := bridgesmtp.NewClient(bridgesmtp.ClientConfig{
 179  		FromDomain: "bridge.example.com",
 180  	})
 181  	smtpClient.SetResolver(func(domain string) ([]*net.MX, error) {
 182  		return []*net.MX{{Host: "127.0.0.1", Pref: 10}}, nil
 183  	})
 184  	smtpClient.SetDialer(func(a string) (*gosmtp.Client, error) {
 185  		return gosmtp.Dial("127.0.0.1:" + port)
 186  	})
 187  
 188  	var replies []string
 189  	sendDM := func(pubkey, content string) error {
 190  		replies = append(replies, content)
 191  		return nil
 192  	}
 193  
 194  	store := NewMemorySubscriptionStore()
 195  	store.Save(&Subscription{
 196  		PubkeyHex: "user1pubkeyhex0123456789abcdef0123456789abcdef0123456789abcdef01",
 197  		ExpiresAt: time.Now().Add(24 * time.Hour),
 198  	})
 199  	handler := NewSubscriptionHandler(store, nil, sendDM, 2100, nil, 0)
 200  
 201  	op := NewOutboundProcessor(smtpClient, nil, handler, "bridge.example.com", sendDM, nil)
 202  
 203  	err := op.ProcessOutbound(
 204  		"user1pubkeyhex0123456789abcdef0123456789abcdef0123456789abcdef01",
 205  		"To: alice@test.example.com\nSubject: Hello\n\nThis is a test.",
 206  	)
 207  	if err != nil {
 208  		t.Fatalf("ProcessOutbound failed: %v", err)
 209  	}
 210  
 211  	time.Sleep(100 * time.Millisecond)
 212  
 213  	if received == nil {
 214  		t.Fatal("expected server to receive email")
 215  	}
 216  	if !strings.Contains(received.From, "bridge.example.com") {
 217  		t.Errorf("From = %q, want bridge.example.com domain", received.From)
 218  	}
 219  	if len(replies) == 0 {
 220  		t.Fatal("expected confirmation reply")
 221  	}
 222  	if !strings.Contains(replies[len(replies)-1], "Email sent to") {
 223  		t.Errorf("expected confirmation, got: %s", replies[len(replies)-1])
 224  	}
 225  }
 226  
 227  func TestOutboundProcessor_SendFails(t *testing.T) {
 228  	smtpClient := bridgesmtp.NewClient(bridgesmtp.ClientConfig{
 229  		FromDomain: "bridge.example.com",
 230  	})
 231  	smtpClient.SetResolver(func(domain string) ([]*net.MX, error) {
 232  		return []*net.MX{{Host: "127.0.0.1", Pref: 10}}, nil
 233  	})
 234  	smtpClient.SetDialer(func(a string) (*gosmtp.Client, error) {
 235  		return nil, net.ErrClosed
 236  	})
 237  
 238  	var replies []string
 239  	sendDM := func(pubkey, content string) error {
 240  		replies = append(replies, content)
 241  		return nil
 242  	}
 243  
 244  	op := NewOutboundProcessor(smtpClient, nil, nil, "bridge.example.com", sendDM, nil)
 245  
 246  	err := op.ProcessOutbound("user1", "To: alice@example.com\n\nHello")
 247  	if err == nil {
 248  		t.Fatal("expected error from SMTP failure")
 249  	}
 250  	if len(replies) == 0 {
 251  		t.Fatal("expected failure reply")
 252  	}
 253  	if !strings.Contains(replies[0], "delivery failed") {
 254  		t.Errorf("expected delivery failure message, got: %s", replies[0])
 255  	}
 256  }
 257  
 258  func TestOutboundProcessor_ParseError(t *testing.T) {
 259  	var replies []string
 260  	sendDM := func(pubkey, content string) error {
 261  		replies = append(replies, content)
 262  		return nil
 263  	}
 264  
 265  	op := NewOutboundProcessor(nil, nil, nil, "bridge.example.com", sendDM, nil)
 266  
 267  	// Content without "To:" header but starting with something that looks like a header
 268  	err := op.ProcessOutbound("user1", "Subject: Test\n\nNo recipient")
 269  	if err != nil {
 270  		t.Fatalf("unexpected error: %v", err)
 271  	}
 272  	if len(replies) == 0 {
 273  		t.Fatal("expected reply about no recipients")
 274  	}
 275  	if !strings.Contains(replies[0], "No recipients") {
 276  		t.Errorf("expected no-recipients message, got: %s", replies[0])
 277  	}
 278  }
 279  
 280  func TestOutboundProcessor_WithCcRecipients(t *testing.T) {
 281  	var received *bridgesmtp.InboundEmail
 282  	server := bridgesmtp.NewServer(bridgesmtp.ServerConfig{
 283  		Domain:     "test.example.com",
 284  		ListenAddr: "127.0.0.1:0",
 285  	}, func(email *bridgesmtp.InboundEmail) error {
 286  		received = email
 287  		return nil
 288  	})
 289  
 290  	if err := server.Start(); err != nil {
 291  		t.Fatalf("start server: %v", err)
 292  	}
 293  	defer server.Stop(context.Background())
 294  
 295  	addr := server.Addr().String()
 296  	_, port, _ := net.SplitHostPort(addr)
 297  
 298  	smtpClient := bridgesmtp.NewClient(bridgesmtp.ClientConfig{
 299  		FromDomain: "bridge.example.com",
 300  	})
 301  	smtpClient.SetResolver(func(domain string) ([]*net.MX, error) {
 302  		return []*net.MX{{Host: "127.0.0.1", Pref: 10}}, nil
 303  	})
 304  	smtpClient.SetDialer(func(a string) (*gosmtp.Client, error) {
 305  		return gosmtp.Dial("127.0.0.1:" + port)
 306  	})
 307  
 308  	var replies []string
 309  	sendDM := func(pubkey, content string) error {
 310  		replies = append(replies, content)
 311  		return nil
 312  	}
 313  
 314  	op := NewOutboundProcessor(smtpClient, nil, nil, "bridge.example.com", sendDM, nil)
 315  
 316  	err := op.ProcessOutbound("user1", "To: alice@test.example.com\nCc: bob@test.example.com\nSubject: CC Test\n\nHello with CC")
 317  	if err != nil {
 318  		t.Fatalf("ProcessOutbound failed: %v", err)
 319  	}
 320  
 321  	time.Sleep(100 * time.Millisecond)
 322  
 323  	if received == nil {
 324  		t.Fatal("expected server to receive email")
 325  	}
 326  	// Confirmation should include both To and Cc
 327  	if len(replies) == 0 {
 328  		t.Fatal("expected confirmation reply")
 329  	}
 330  	last := replies[len(replies)-1]
 331  	if !strings.Contains(last, "alice@test.example.com") {
 332  		t.Errorf("confirmation missing To recipient: %s", last)
 333  	}
 334  	if !strings.Contains(last, "bob@test.example.com") {
 335  		t.Errorf("confirmation missing Cc recipient: %s", last)
 336  	}
 337  }
 338  
 339  func TestOutboundProcessor_ShortPubkey(t *testing.T) {
 340  	// Test with pubkey shorter than 16 chars
 341  	var received *bridgesmtp.InboundEmail
 342  	server := bridgesmtp.NewServer(bridgesmtp.ServerConfig{
 343  		Domain:     "test.example.com",
 344  		ListenAddr: "127.0.0.1:0",
 345  	}, func(email *bridgesmtp.InboundEmail) error {
 346  		received = email
 347  		return nil
 348  	})
 349  
 350  	if err := server.Start(); err != nil {
 351  		t.Fatalf("start server: %v", err)
 352  	}
 353  	defer server.Stop(context.Background())
 354  
 355  	addr := server.Addr().String()
 356  	_, port, _ := net.SplitHostPort(addr)
 357  
 358  	smtpClient := bridgesmtp.NewClient(bridgesmtp.ClientConfig{
 359  		FromDomain: "bridge.example.com",
 360  	})
 361  	smtpClient.SetResolver(func(domain string) ([]*net.MX, error) {
 362  		return []*net.MX{{Host: "127.0.0.1", Pref: 10}}, nil
 363  	})
 364  	smtpClient.SetDialer(func(a string) (*gosmtp.Client, error) {
 365  		return gosmtp.Dial("127.0.0.1:" + port)
 366  	})
 367  
 368  	var replies []string
 369  	sendDM := func(pubkey, content string) error {
 370  		replies = append(replies, content)
 371  		return nil
 372  	}
 373  
 374  	op := NewOutboundProcessor(smtpClient, nil, nil, "bridge.example.com", sendDM, nil)
 375  
 376  	// Use a short pubkey (< 16 chars)
 377  	err := op.ProcessOutbound("abc", "To: alice@test.example.com\n\nHello")
 378  	if err != nil {
 379  		t.Fatalf("ProcessOutbound failed: %v", err)
 380  	}
 381  
 382  	time.Sleep(100 * time.Millisecond)
 383  
 384  	if received == nil {
 385  		t.Fatal("expected server to receive email")
 386  	}
 387  	// From address should use the full short pubkey
 388  	if !strings.Contains(received.From, "abc@bridge.example.com") {
 389  		t.Errorf("From = %q, expected abc@bridge.example.com", received.From)
 390  	}
 391  }
 392  
 393  func TestOutboundProcessor_WithRateLimiter(t *testing.T) {
 394  	var received *bridgesmtp.InboundEmail
 395  	server := bridgesmtp.NewServer(bridgesmtp.ServerConfig{
 396  		Domain:     "test.example.com",
 397  		ListenAddr: "127.0.0.1:0",
 398  	}, func(email *bridgesmtp.InboundEmail) error {
 399  		received = email
 400  		return nil
 401  	})
 402  
 403  	if err := server.Start(); err != nil {
 404  		t.Fatalf("start server: %v", err)
 405  	}
 406  	defer server.Stop(context.Background())
 407  
 408  	addr := server.Addr().String()
 409  	_, port, _ := net.SplitHostPort(addr)
 410  
 411  	smtpClient := bridgesmtp.NewClient(bridgesmtp.ClientConfig{
 412  		FromDomain: "bridge.example.com",
 413  	})
 414  	smtpClient.SetResolver(func(domain string) ([]*net.MX, error) {
 415  		return []*net.MX{{Host: "127.0.0.1", Pref: 10}}, nil
 416  	})
 417  	smtpClient.SetDialer(func(a string) (*gosmtp.Client, error) {
 418  		return gosmtp.Dial("127.0.0.1:" + port)
 419  	})
 420  
 421  	var replies []string
 422  	sendDM := func(pubkey, content string) error {
 423  		replies = append(replies, content)
 424  		return nil
 425  	}
 426  
 427  	rl := NewRateLimiter(RateLimitConfig{
 428  		PerUserPerHour: 100,
 429  		MinInterval:    0,
 430  	})
 431  
 432  	op := NewOutboundProcessor(smtpClient, rl, nil, "bridge.example.com", sendDM, nil)
 433  
 434  	err := op.ProcessOutbound("user1", "To: alice@test.example.com\n\nHello")
 435  	if err != nil {
 436  		t.Fatalf("ProcessOutbound failed: %v", err)
 437  	}
 438  
 439  	time.Sleep(100 * time.Millisecond)
 440  
 441  	if received == nil {
 442  		t.Fatal("expected server to receive email")
 443  	}
 444  
 445  	_ = received
 446  }
 447