-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsession.go
More file actions
174 lines (154 loc) · 3.99 KB
/
session.go
File metadata and controls
174 lines (154 loc) · 3.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
package smtp
import (
"bytes"
"io"
"time"
"github.com/emersion/go-smtp"
"go.uber.org/zap"
)
// Session represents an SMTP session (one connection)
type Session struct {
backend *Backend
conn *smtp.Conn
uuid string
remoteAddr string
log *zap.Logger
// Authentication data (captured but not verified)
authenticated bool
authUsername string
authPassword string
authMechanism string
// SMTP envelope data
from string
to []string
heloName string
// Email data (accumulated during DATA command)
emailData bytes.Buffer
// Connection control
shouldClose bool // Set to true when worker requests connection close
}
// Mail is called for MAIL FROM command
func (s *Session) Mail(from string, opts *smtp.MailOptions) error {
s.from = from
s.log.Debug("MAIL FROM",
zap.String("uuid", s.uuid),
zap.String("from", from),
)
return nil
}
// Rcpt is called for RCPT TO command
func (s *Session) Rcpt(to string, opts *smtp.RcptOptions) error {
s.to = append(s.to, to)
s.log.Debug("RCPT TO",
zap.String("uuid", s.uuid),
zap.String("to", to),
)
return nil
}
// Data is called when DATA command is received
// Returns error after reading complete email
func (s *Session) Data(r io.Reader) error {
s.log.Debug("DATA command received", zap.String("uuid", s.uuid))
// 1. Read email data
s.emailData.Reset()
n, err := io.Copy(&s.emailData, r)
if err != nil {
s.log.Error("failed to read email data", zap.Error(err))
return &smtp.SMTPError{
Code: 451,
Message: "Failed to read message",
}
}
s.log.Info("email received",
zap.String("uuid", s.uuid),
zap.String("from", s.from),
zap.Strings("to", s.to),
zap.Int64("size", n),
)
// 2. Parse email
parsedMessage, err := s.parseEmail(s.emailData.Bytes())
if err != nil {
s.log.Error("failed to parse email", zap.Error(err))
return &smtp.SMTPError{
Code: 554,
Message: "Failed to parse message",
}
}
// 3. Build EmailData for Jobs
var authData *AuthData
if s.authenticated {
authData = &AuthData{
Attempted: true,
Mechanism: s.authMechanism,
Username: s.authUsername,
Password: s.authPassword,
}
}
// Convert attachments
attachments := make([]AttachmentData, 0, len(parsedMessage.Attachments))
for _, att := range parsedMessage.Attachments {
attachments = append(attachments, AttachmentData{
Filename: att.Filename,
ContentType: att.Type,
Content: att.Content,
})
}
emailData := &EmailData{
Event: "EMAIL_RECEIVED",
UUID: s.uuid,
RemoteAddr: s.remoteAddr,
ReceivedAt: time.Now(),
Envelope: EnvelopeData{
From: parsedMessage.Sender,
To: parsedMessage.Recipients,
Ccs: parsedMessage.CCs,
ReplyTo: parsedMessage.ReplyTo,
AllRecipients: parsedMessage.AllRecipients,
Helo: s.heloName,
},
Auth: authData,
Message: MessageData{
Id: parsedMessage.ID,
Headers: map[string][]string{
"Subject": {parsedMessage.Subject},
},
Body: parsedMessage.TextBody,
HTMLBody: parsedMessage.HTMLBody,
Raw: parsedMessage.Raw,
Subject: parsedMessage.Subject,
},
Attachments: attachments,
}
// 4. Push to Jobs
err = s.backend.plugin.pushToJobs(emailData)
if err != nil {
s.log.Error("failed to push email to jobs",
zap.Error(err),
zap.String("uuid", s.uuid),
)
return &smtp.SMTPError{
Code: 451,
EnhancedCode: smtp.EnhancedCode{4, 3, 0},
Message: "Temporary failure, try again later",
}
}
// Always return nil to send 250 OK to client
return nil
}
// Reset is called for RSET command
func (s *Session) Reset() {
s.from = ""
s.to = nil
s.emailData.Reset()
s.log.Debug("session reset", zap.String("uuid", s.uuid))
}
// Logout is called when connection closes
func (s *Session) Logout() error {
if s.shouldClose {
s.log.Debug("closing connection as requested by worker", zap.String("uuid", s.uuid))
} else {
s.log.Debug("connection closed", zap.String("uuid", s.uuid))
}
s.backend.plugin.connections.Delete(s.uuid)
return nil
}