Initial commit

This commit is contained in:
Neil Alexander
2021-07-07 18:15:07 +01:00
commit ceffe7612d
26 changed files with 2130 additions and 0 deletions

View File

@@ -0,0 +1,79 @@
package smtpserver
import (
"encoding/hex"
"fmt"
"log"
"github.com/emersion/go-smtp"
"github.com/neilalexander/yggmail/internal/config"
"github.com/neilalexander/yggmail/internal/smtpsender"
"github.com/neilalexander/yggmail/internal/storage"
)
type BackendMode int
const (
BackendModeInternal BackendMode = iota
BackendModeExternal
)
type Backend struct {
Mode BackendMode
Log *log.Logger
Config *config.Config
Queues *smtpsender.Queues
Storage storage.Storage
}
func (b *Backend) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) {
switch b.Mode {
case BackendModeInternal:
// The connection came from our local listener
if authed, err := b.Storage.TryAuthenticate(username, password); err != nil {
b.Log.Printf("Failed to authenticate SMTP user %q due to error: %s", username, err)
return nil, fmt.Errorf("failed to authenticate: %w", err)
} else if !authed {
b.Log.Printf("Failed to authenticate SMTP user %q\n", username)
return nil, smtp.ErrAuthRequired
}
defer b.Log.Printf("Authenticated SMTP user %q\n", username)
return &SessionLocal{
backend: b,
state: state,
}, nil
case BackendModeExternal:
return nil, fmt.Errorf("Not expecting authenticated connection on external backend")
}
return nil, fmt.Errorf("Authenticated login failed")
}
func (b *Backend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) {
switch b.Mode {
case BackendModeInternal:
return nil, fmt.Errorf("Not expecting anonymous connection on internal backend")
case BackendModeExternal:
// The connection came from our overlay listener, so we should check
// that they are who they claim to be
if state.Hostname != state.RemoteAddr.String() {
return nil, fmt.Errorf("You are not who you claim to be")
}
pks, err := hex.DecodeString(state.RemoteAddr.String())
if err != nil {
return nil, fmt.Errorf("hex.DecodeString: %w", err)
}
b.Log.Println("Incoming SMTP session from", state.RemoteAddr.String())
return &SessionRemote{
backend: b,
state: state,
public: pks[:],
}, nil
}
return nil, fmt.Errorf("Anonymous login failed")
}

View File

@@ -0,0 +1,14 @@
package smtpserver
import (
"fmt"
"strings"
)
func parseAddress(email string) (string, string, error) {
at := strings.LastIndex(email, "@")
if at == 0 {
return "", "", fmt.Errorf("invalid email address")
}
return email[:at], email[at+1:], nil
}

View File

@@ -0,0 +1,110 @@
package smtpserver
import (
"bytes"
"encoding/hex"
"fmt"
"io"
"time"
"github.com/emersion/go-message"
"github.com/emersion/go-smtp"
"github.com/neilalexander/yggmail/internal/smtpsender"
)
type SessionLocal struct {
backend *Backend
state *smtp.ConnectionState
from string
rcpt []string
}
func (s *SessionLocal) Mail(from string, opts smtp.MailOptions) error {
_, host, err := parseAddress(from)
if err != nil {
return fmt.Errorf("parseAddress: %w", err)
}
if host != hex.EncodeToString(s.backend.Config.PublicKey) {
return fmt.Errorf("not allowed to send outgoing mail as %s", from)
}
s.from = from
return nil
}
func (s *SessionLocal) Rcpt(to string) error {
s.rcpt = append(s.rcpt, to)
return nil
}
func (s *SessionLocal) Data(r io.Reader) error {
m, err := message.Read(r)
if err != nil {
return fmt.Errorf("message.Read: %w", err)
}
m.Header.Add(
"Received", fmt.Sprintf("from %s by Yggmail %s; %s",
s.state.RemoteAddr.String(),
hex.EncodeToString(s.backend.Config.PublicKey),
time.Now().String(),
),
)
servers := make(map[string]struct{})
for _, rcpt := range s.rcpt {
localpart, host, err := parseAddress(rcpt)
if err != nil {
return fmt.Errorf("parseAddress: %w", err)
}
if _, ok := servers[host]; ok {
continue
}
servers[host] = struct{}{}
if host == hex.EncodeToString(s.backend.Config.PublicKey) {
var b bytes.Buffer
if err := m.WriteTo(&b); err != nil {
return fmt.Errorf("m.WriteTo: %w", err)
}
if _, err := s.backend.Storage.MailCreate(localpart, "INBOX", b.Bytes()); err != nil {
return fmt.Errorf("s.backend.Storage.StoreMessageFor: %w", err)
}
continue
}
queue, err := s.backend.Queues.QueueFor(host)
if err != nil {
return fmt.Errorf("s.backend.Queues.QueueFor: %w", err)
}
mail := &smtpsender.QueuedMail{
From: s.from,
Rcpt: rcpt,
Destination: host,
}
var b bytes.Buffer
if err := m.WriteTo(&b); err != nil {
return fmt.Errorf("m.WriteTo: %w", err)
}
mail.Content = b.Bytes()
if err := queue.Queue(mail); err != nil {
return fmt.Errorf("queue.Queue: %w", err)
}
s.backend.Log.Println("Queued mail for", mail.Destination)
}
return nil
}
func (s *SessionLocal) Reset() {}
func (s *SessionLocal) Logout() error {
return nil
}

View File

@@ -0,0 +1,82 @@
package smtpserver
import (
"bytes"
"crypto/ed25519"
"encoding/hex"
"fmt"
"io"
"time"
"github.com/emersion/go-message"
"github.com/emersion/go-smtp"
)
type SessionRemote struct {
backend *Backend
state *smtp.ConnectionState
public ed25519.PublicKey
from string
localparts []string
}
func (s *SessionRemote) Mail(from string, opts smtp.MailOptions) error {
_, host, err := parseAddress(from)
if err != nil {
return fmt.Errorf("mail.ParseAddress: %w", err)
}
if local := s.state.RemoteAddr.String(); local != host {
return fmt.Errorf("not allowed to send incoming mail as %s", from)
}
s.from = from
return nil
}
func (s *SessionRemote) Rcpt(to string) error {
user, host, err := parseAddress(to)
if err != nil {
return fmt.Errorf("mail.ParseAddress: %w", err)
}
if local := hex.EncodeToString(s.backend.Config.PublicKey); host != local {
return fmt.Errorf("not allowed to send mail to %q", host)
}
s.localparts = append(s.localparts, user)
return nil
}
func (s *SessionRemote) Data(r io.Reader) error {
m, err := message.Read(r)
if err != nil {
return fmt.Errorf("message.Read: %w", err)
}
m.Header.Add(
"Received", fmt.Sprintf("from Yggmail %s; %s",
hex.EncodeToString(s.public),
time.Now().String(),
),
)
var b bytes.Buffer
if err := m.WriteTo(&b); err != nil {
return fmt.Errorf("m.WriteTo: %w", err)
}
for _, localpart := range s.localparts {
if _, err := s.backend.Storage.MailCreate(localpart, "INBOX", b.Bytes()); err != nil {
return fmt.Errorf("s.backend.Storage.StoreMessageFor: %w", err)
}
s.backend.Log.Printf("Stored new mail for local user %q from %s", localpart, s.from)
}
return nil
}
func (s *SessionRemote) Reset() {}
func (s *SessionRemote) Logout() error {
return nil
}

View File

@@ -0,0 +1,18 @@
package smtpserver
import (
"github.com/emersion/go-smtp"
)
type SMTPServer struct {
server *smtp.Server
backend smtp.Backend
}
func NewSMTPServer(backend smtp.Backend) *SMTPServer {
s := &SMTPServer{
server: smtp.NewServer(backend),
backend: backend,
}
return s
}