internal/submission/submission.go (view raw)
1package submission
2
3import (
4 "bytes"
5 "context"
6 "crypto/tls"
7 "database/sql"
8 "errors"
9 "io"
10 "log/slog"
11 "sync"
12
13 "postern/internal/db"
14 "postern/internal/dkim"
15 "postern/internal/model"
16 "postern/internal/policy"
17
18 "github.com/emersion/go-sasl"
19 smtpserver "github.com/emersion/go-smtp"
20 "github.com/google/uuid"
21)
22
23type Config struct {
24 DB *db.DB
25 DKIM *dkim.DKIM
26 Listen string
27 TLS *tls.Config
28 PolicyEngine *policy.Engine
29 IsLocalSubmission bool // If set, will disable TLS and Auth and accept all local mails for delivery.
30}
31type Server struct {
32 db *db.DB
33 dkim *dkim.DKIM
34 listen string
35 tlsConfig *tls.Config
36 policyEngine *policy.Engine
37
38 mu sync.Mutex
39 smtpSrv *smtpserver.Server
40 status string
41 isLocalSubmission bool
42}
43
44func NewServer(config *Config) (*Server, error) {
45 return &Server{
46 db: config.DB,
47 dkim: config.DKIM,
48 listen: config.Listen,
49 tlsConfig: config.TLS,
50 policyEngine: config.PolicyEngine,
51 isLocalSubmission: config.IsLocalSubmission,
52 }, nil
53}
54
55func (i *Server) Start() error {
56 be := &backend{
57 db: i.db,
58 dkim: i.dkim,
59 isLocalSubmission: i.isLocalSubmission,
60 policyEngine: i.policyEngine,
61 }
62 s := smtpserver.NewServer(be)
63
64 s.Addr = i.listen
65 s.MaxMessageBytes = 20_000_000
66
67 i.mu.Lock()
68 i.smtpSrv = s
69 i.status = "running"
70 i.mu.Unlock()
71
72 if i.isLocalSubmission {
73 err := s.ListenAndServe()
74 if err != nil {
75 return err
76 }
77 } else {
78 s.TLSConfig = i.tlsConfig
79 err := s.ListenAndServeTLS()
80 if err != nil {
81 return err
82 }
83 }
84
85 return nil
86}
87
88func (i *Server) Stop() error {
89 i.mu.Lock()
90 defer i.mu.Unlock()
91 i.status = "stopped"
92 if i.smtpSrv != nil {
93 return i.smtpSrv.Close()
94 }
95 return nil
96}
97
98func (i *Server) Status() string {
99 i.mu.Lock()
100 defer i.mu.Unlock()
101 return i.status
102}
103
104type smtpSession struct {
105 db *db.DB
106 dkim *dkim.DKIM
107 conn *smtpserver.Conn
108 authenticatedUser model.User
109 sessionID string
110 mailFrom string
111 rcptTo []string
112 logger *slog.Logger
113 isLocalSubmission bool
114 policyEngine *policy.Engine
115}
116
117// The backend implements SMTP server methods.
118type backend struct {
119 db *db.DB
120 dkim *dkim.DKIM
121 isLocalSubmission bool
122 policyEngine *policy.Engine
123}
124
125// NewSession is called after client greeting (EHLO, HELO).
126func (bkd *backend) NewSession(c *smtpserver.Conn) (smtpserver.Session, error) {
127 sessionID := uuid.NewString()
128 l := slog.With("component", "submission", "remote_ip", c.Conn().RemoteAddr().String(), "session_id", sessionID)
129 l.Info("new session")
130 return &smtpSession{
131 db: bkd.db,
132 dkim: bkd.dkim,
133 conn: c,
134 authenticatedUser: model.User{},
135 sessionID: sessionID,
136 logger: l,
137 rcptTo: make([]string, 0),
138 isLocalSubmission: bkd.isLocalSubmission,
139 policyEngine: bkd.policyEngine,
140 }, nil
141}
142
143// AuthMechanisms returns a slice of available auth mechanisms
144func (s *smtpSession) AuthMechanisms() []string {
145 return []string{sasl.Plain}
146}
147
148// Auth is the handler for supported authenticators.
149func (s *smtpSession) Auth(_ string) (sasl.Server, error) {
150 return sasl.NewPlainServer(func(identity, username, password string) error {
151 u, err := s.db.GetUser(context.Background(), username)
152 if err != nil {
153 if errors.Is(err, sql.ErrNoRows) {
154 s.logger.Info("user not found")
155 } else {
156 s.logger.Error("getting user", "error", err)
157 }
158 return smtpserver.ErrAuthFailed
159 }
160
161 err = u.VerifyPassword([]byte(password))
162 if err != nil {
163 slog.Info("invalid password", "username", username)
164 return smtpserver.ErrAuthFailed
165 }
166
167 s.authenticatedUser = u
168 s.logger.Info("authenticated", "user_id", u.ID, "username", username)
169 return nil
170 }), nil
171}
172
173func (s *smtpSession) Reset() {
174 s.mailFrom = ""
175 s.rcptTo = make([]string, 0)
176}
177
178func (s *smtpSession) Logout() error {
179 s.authenticatedUser = model.User{}
180 return nil
181}
182
183func (s *smtpSession) Mail(from string, _ *smtpserver.MailOptions) error {
184 ctx := context.Background()
185 s.logger.InfoContext(ctx, "MAIL FROM", "from", from)
186
187 if !s.isLocalSubmission && s.authenticatedUser.ID == 0 {
188 s.logger.InfoContext(ctx, "no authenticated user")
189 return smtpserver.ErrAuthFailed
190 }
191
192 s.mailFrom = from
193 return nil
194}
195
196func (s *smtpSession) Rcpt(to string, _ *smtpserver.RcptOptions) error {
197 ctx := context.Background()
198 s.logger.InfoContext(ctx, "RCPT TO", "to", to)
199
200 if !s.isLocalSubmission && s.authenticatedUser.ID == 0 {
201 s.logger.InfoContext(ctx, "no authenticated user")
202 return smtpserver.ErrAuthFailed
203 }
204
205 s.rcptTo = append(s.rcptTo, to)
206 return nil
207}
208
209func (s *smtpSession) Data(r io.Reader) error {
210 ctx := context.Background()
211 s.logger.InfoContext(ctx, "DATA")
212
213 if !s.isLocalSubmission && s.authenticatedUser.ID == 0 {
214 s.logger.InfoContext(ctx, "no authenticated user")
215 return smtpserver.ErrAuthFailed
216 }
217
218 // TODO Check if this mail is for us or for the relay
219
220 var buffer bytes.Buffer
221 _, err := io.Copy(&buffer, r)
222 if err != nil {
223 s.logger.ErrorContext(ctx, "Copy", "error", err)
224 return errInternalServerError
225 }
226
227 message := buffer.Bytes()
228
229 signed, err := s.dkim.Sign(ctx, message)
230 if err != nil {
231 s.logger.ErrorContext(ctx, "dkim sign", "error", err)
232 return errInternalServerError
233 }
234
235 // Call policy hook to determine relay config
236 msgCtx := policy.MessageContextFromEnvelope(s.mailFrom, "", int64(len(signed)))
237 user := policy.UserToStarlark(s.authenticatedUser, s.db)
238 relayConfig, err := s.policyEngine.OnMessageSubmit(user, msgCtx)
239 if err != nil {
240 s.logger.ErrorContext(ctx, "policy on_message_submit", "error", err)
241 return errInternalServerError
242 }
243
244 err = shipMessage(s.mailFrom, s.rcptTo, bytes.NewReader(signed), relayConfig)
245 if err != nil {
246 s.logger.ErrorContext(ctx, "ship message", "error", err)
247 }
248 return err
249}