├── .github └── workflows │ ├── go.yml │ └── golangci-lint.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── server ├── auth_backend.go ├── blacklist.go ├── server.go └── server_test.go └── smtp ├── address.go ├── address_test.go ├── datareader_test.go ├── message.go ├── parser.go ├── parser_test.go ├── protocol.go ├── smtp_error.go ├── state.go └── state_test.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.17 20 | 21 | - name: Build 22 | run: go build -v ./... 23 | 24 | - name: Test 25 | run: go test -v ./... 26 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | pull_request: 5 | 6 | permissions: 7 | contents: read 8 | # Optional: allow read access to pull request. Use with `only-new-issues` option. 9 | # pull-requests: read 10 | 11 | jobs: 12 | golangci: 13 | name: lint 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions/setup-go@v4 18 | with: 19 | go-version: '1.21' 20 | cache: false 21 | - name: golangci-lint 22 | uses: golangci/golangci-lint-action@v3 23 | with: 24 | # Require: The version of golangci-lint to use. 25 | # When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version. 26 | # When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit. 27 | version: v1.54 28 | 29 | # Optional: working directory, useful for monorepos 30 | # working-directory: somedir 31 | 32 | # Optional: golangci-lint command line arguments. 33 | # 34 | # Note: By default, the `.golangci.yml` file should be at the root of the repository. 35 | # The location of the configuration file can be changed by using `--config=` 36 | # args: --timeout=30m --config=/my/path/.golangci.yml --issues-exit-code=0 37 | 38 | # Optional: show only new issues if it's a pull request. The default value is `false`. 39 | # only-new-issues: true 40 | 41 | # Optional: if set to true, then all caching functionality will be completely disabled, 42 | # takes precedence over all other caching options. 43 | # skip-cache: true 44 | 45 | # Optional: if set to true, then the action won't cache or restore ~/go/pkg. 46 | # skip-pkg-cache: true 47 | 48 | # Optional: if set to true, then the action won't cache or restore ~/.cache/go-build. 49 | # skip-build-cache: true 50 | 51 | # Optional: The mode to install golangci-lint. It can be 'binary' or 'goinstall'. 52 | # install-mode: "goinstall" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | mistralmail 2 | config.json 3 | maildir 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.5 5 | - 1.6 6 | - 1.7 7 | 8 | os: 9 | - linux 10 | - osx 11 | 12 | install: 13 | - go get github.com/smartystreets/goconvey/convey 14 | - go get github.com/mistralmail/mistralmail/log 15 | - go get github.com/mistralmail/mistralmail/helpers 16 | 17 | script: 18 | - go test -v ./... 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Mathias Beke, Timo Truyts 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MistralMail SMTP 2 | =============== 3 | 4 | SMTP ([RFC 5321](https://tools.ietf.org/html/rfc5321)) implementation in Go. 5 | 6 | 7 | Acknowledgements 8 | ----------------- 9 | 10 | * [GoConvey](https://github.com/smartystreets/goconvey) 11 | * [Logrus](https://github.com/Sirupsen/logrus) 12 | 13 | Authors 14 | ------- 15 | 16 | Mathias Beke - [denbeke.be](http://denbeke.be) 17 | Timo Truyts 18 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mistralmail/smtp 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/sirupsen/logrus v1.9.3 7 | github.com/smartystreets/goconvey v1.6.4 8 | ) 9 | 10 | require ( 11 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect 12 | github.com/jtolds/gls v4.20.0+incompatible // indirect 13 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect 14 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 5 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 6 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 7 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 8 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 9 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 10 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 11 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 12 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 13 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 14 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 15 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 16 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 17 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 18 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 19 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 20 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 21 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 22 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= 23 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 24 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 25 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 26 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 27 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 28 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 29 | -------------------------------------------------------------------------------- /server/auth_backend.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/mistralmail/smtp/smtp" 8 | ) 9 | 10 | // AuthBackend represents a pluggable authentication backend for the MTA 11 | type AuthBackend interface { 12 | // Login checks whether the credentials of a user are valid. 13 | // returns ErrInvalidCredentials if credentials not valid. 14 | Login(state *smtp.State, username string, password string) (User, error) 15 | } 16 | 17 | // User denotes an authenticated SMTP user. 18 | type User interface { 19 | Username() string 20 | } 21 | 22 | // SMTPUser is a quick implementation of the User interface 23 | type SMTPUser struct { 24 | // Username is the username / email address of the user. 25 | username string 26 | } 27 | 28 | // Username returns the username 29 | func (u *SMTPUser) Username() string { 30 | return u.username 31 | } 32 | 33 | // ErrInvalidCredentials denotes incorrect credentials. 34 | var ErrInvalidCredentials = errors.New("InvalidCredentialsError") 35 | 36 | // AuthBackendMemory is a simple in-memory implementation of AuthBackend for testing purpose. 37 | type AuthBackendMemory struct { 38 | Credentials map[string]string 39 | } 40 | 41 | // Login checks whether the credentials of a user are valid 42 | func (auth *AuthBackendMemory) Login(state *smtp.State, username string, password string) (User, error) { 43 | if auth.Credentials == nil { 44 | return nil, fmt.Errorf("auth backend not initialized") 45 | } 46 | if passwordToMatch, ok := auth.Credentials[username]; ok { 47 | if passwordToMatch == password { 48 | return &SMTPUser{username: username}, nil 49 | } 50 | return nil, ErrInvalidCredentials 51 | } 52 | return nil, ErrInvalidCredentials 53 | } 54 | 55 | // NewAuthBackendMemory creates a new in-memory AuthBackend 56 | func NewAuthBackendMemory(credentials map[string]string) *AuthBackendMemory { 57 | return &AuthBackendMemory{ 58 | Credentials: credentials, 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /server/blacklist.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | // Interface for handling blaclists 4 | // it is meant to be replaced by your own implementation 5 | type Blacklist interface { 6 | // CheckIp will return true if the IP is blacklisted and false if the IP was not found in a blacklist 7 | CheckIp(ip string) bool 8 | } 9 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "io" 7 | "net" 8 | "sync" 9 | "time" 10 | 11 | "github.com/mistralmail/smtp/smtp" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | type Config struct { 16 | Ip string 17 | Hostname string 18 | Port uint32 19 | Blacklist Blacklist 20 | DisableAuth bool 21 | TLSConfig *tls.Config 22 | } 23 | 24 | // Session id 25 | 26 | var globalCounter uint32 = 0 27 | var globalCounterLock = &sync.Mutex{} 28 | 29 | func generateSessionId() smtp.Id { 30 | globalCounterLock.Lock() 31 | defer globalCounterLock.Unlock() 32 | globalCounter++ 33 | return smtp.Id{Timestamp: time.Now().Unix(), Counter: globalCounter} 34 | 35 | } 36 | 37 | // Handler is the interface that will be used when a mail was received. 38 | type Handler interface { 39 | Handle(*smtp.State) error 40 | } 41 | 42 | // HandlerFunc is a wrapper to allow normal functions to be used as a handler. 43 | type HandlerFunc func(*smtp.State) error 44 | 45 | func (h HandlerFunc) Handle(state *smtp.State) error { 46 | return h(state) 47 | } 48 | 49 | // Server Represents an SMTP server 50 | type Server struct { 51 | config Config 52 | // The handler to be called when a mail is received. 53 | MailHandler Handler 54 | // The config for tls connection. Nil if not supported. 55 | TlsConfig *tls.Config 56 | AuthBackend AuthBackend 57 | // When shutting down this channel is closed, no new connections should be handled then. 58 | // But existing connections can continue untill quitC is closed. 59 | shutDownC chan bool 60 | // When this is closed existing connections should stop. 61 | quitC chan bool 62 | wg sync.WaitGroup 63 | } 64 | 65 | // New Create a new SMTP server that doesn't handle the protocol. 66 | func New(c Config, h Handler) *Server { 67 | mta := &Server{ 68 | config: c, 69 | MailHandler: h, 70 | quitC: make(chan bool), 71 | shutDownC: make(chan bool), 72 | TlsConfig: c.TLSConfig, 73 | } 74 | 75 | // TODO what if authbackend is nil? 76 | 77 | return mta 78 | } 79 | 80 | func (s *Server) Stop() { 81 | log.Printf("Received stop command. Sending shutdown event...") 82 | close(s.shutDownC) 83 | // Give existing connections some time to finish. 84 | t := time.Duration(10) 85 | log.Printf("Waiting for a maximum of %d seconds...", t) 86 | time.Sleep(t * time.Second) 87 | log.Printf("Sending force quit event...") 88 | close(s.quitC) 89 | } 90 | 91 | func (s *Server) hasTls() bool { 92 | return s.TlsConfig != nil 93 | } 94 | 95 | // Same as the Mta struct but has methods for handling socket connections. 96 | type DefaultMta struct { 97 | Server *Server 98 | } 99 | 100 | // NewDefault Create a new SMTP server with a 101 | // socket protocol implementation. 102 | func NewDefault(c Config, h Handler) *DefaultMta { 103 | mta := &DefaultMta{ 104 | Server: New(c, h), 105 | } 106 | return mta 107 | } 108 | 109 | func (s *DefaultMta) Stop() { 110 | s.Server.Stop() 111 | } 112 | 113 | func (s *DefaultMta) ListenAndServe() error { 114 | log.Printf("Starting SMTP server at port %d", s.Server.config.Port) 115 | ln, err := net.Listen("tcp", fmt.Sprintf("%s:%d", s.Server.config.Ip, s.Server.config.Port)) 116 | if err != nil { 117 | log.Errorf("Could not start listening: %v", err) 118 | return err 119 | } 120 | 121 | // Close the listener so that listen well return from ln.Accept(). 122 | go func() { 123 | _, ok := <-s.Server.shutDownC 124 | if !ok { 125 | ln.Close() 126 | } 127 | }() 128 | 129 | err = s.listen(ln) 130 | log.Printf("Waiting for connections to close...") 131 | s.Server.wg.Wait() 132 | return err 133 | } 134 | 135 | func (s *DefaultMta) listen(ln net.Listener) error { 136 | defer ln.Close() 137 | for { 138 | c, err := ln.Accept() 139 | if err != nil { 140 | // Assume this means listener was closed. 141 | if noe, ok := err.(*net.OpError); ok && !noe.Temporary() { 142 | log.Printf("Listener is closed, stopping listen loop...") 143 | return nil 144 | } 145 | return err 146 | } 147 | 148 | s.Server.wg.Add(1) 149 | go s.serve(c) 150 | } 151 | 152 | } 153 | 154 | func (s *DefaultMta) serve(c net.Conn) { 155 | defer s.Server.wg.Done() 156 | 157 | proto := smtp.NewMtaProtocol(c) 158 | if proto == nil { 159 | log.Errorf("Could not create Mta protocol") 160 | c.Close() 161 | return 162 | } 163 | s.Server.HandleClient(proto) 164 | } 165 | 166 | // HandleClient Start communicating with a client 167 | func (s *Server) HandleClient(proto smtp.Protocol) { 168 | //log.Printf("Received connection") 169 | 170 | // Hold state for this client connection 171 | state := proto.GetState() 172 | state.Reset() 173 | state.SessionId = generateSessionId() 174 | state.Ip = proto.GetIP() 175 | 176 | log.WithFields(log.Fields{ 177 | "SessionId": state.SessionId.String(), 178 | "Ip": state.Ip.String(), 179 | }).Debug("Received connection") 180 | 181 | if s.config.Blacklist != nil { 182 | if s.config.Blacklist.CheckIp(state.Ip.String()) { 183 | log.WithFields(log.Fields{ 184 | "SessionId": state.SessionId.String(), 185 | "Ip": state.Ip.String(), 186 | }).Warn("IP found in Blacklist, closing handler") 187 | proto.Close() 188 | } else { 189 | log.WithFields(log.Fields{ 190 | "SessionId": state.SessionId.String(), 191 | "Ip": state.Ip.String(), 192 | }).Debug("IP not found in Blacklist") 193 | } 194 | } 195 | 196 | // Start with welcome message 197 | proto.Send(smtp.Answer{ 198 | Status: smtp.Ready, 199 | Message: s.config.Hostname + " Service Ready", 200 | }) 201 | 202 | var c *smtp.Cmd 203 | var err error 204 | 205 | quit := false 206 | cmdC := make(chan bool) 207 | 208 | nextCmd := func() bool { 209 | go func() { 210 | for { 211 | c, err = proto.GetCmd() 212 | 213 | if err != nil { 214 | if err == smtp.ErrLtl { 215 | proto.Send(smtp.Answer{ 216 | Status: smtp.SyntaxError, 217 | Message: "Line too long.", 218 | }) 219 | } else { 220 | // Not a line too long error. What to do? 221 | cmdC <- true 222 | return 223 | } 224 | } else { 225 | break 226 | } 227 | } 228 | cmdC <- false 229 | }() 230 | 231 | select { 232 | case _, ok := <-s.quitC: 233 | if !ok { 234 | proto.Send(smtp.Answer{ 235 | Status: smtp.ShuttingDown, 236 | Message: "Server is going down.", 237 | }) 238 | return true 239 | } 240 | case q := <-cmdC: 241 | return q 242 | 243 | } 244 | 245 | return false 246 | } 247 | 248 | quit = nextCmd() 249 | 250 | for !quit { 251 | 252 | //log.Printf("Received cmd: %#v", *c) 253 | 254 | switch cmd := (*c).(type) { 255 | case smtp.HeloCmd: 256 | state.Hostname = cmd.Domain 257 | proto.Send(smtp.Answer{ 258 | Status: smtp.Ok, 259 | Message: s.config.Hostname, 260 | }) 261 | 262 | case smtp.EhloCmd: 263 | state.Reset() 264 | state.Hostname = cmd.Domain 265 | 266 | messages := []string{s.config.Hostname, "8BITMIME"} 267 | if s.hasTls() && !state.Secure { 268 | messages = append(messages, "STARTTLS") 269 | } 270 | 271 | if !s.config.DisableAuth && s.AuthBackend != nil { 272 | messages = append(messages, "AUTH PLAIN") 273 | } 274 | 275 | messages = append(messages, "OK") 276 | 277 | proto.Send(smtp.MultiAnswer{ 278 | Status: smtp.Ok, 279 | Messages: messages, 280 | }) 281 | 282 | case smtp.QuitCmd: 283 | proto.Send(smtp.Answer{ 284 | Status: smtp.Closing, 285 | Message: "Bye!", 286 | }) 287 | quit = true 288 | 289 | case smtp.MailCmd: 290 | if ok, reason := state.CanReceiveMail(); !ok { 291 | proto.Send(smtp.Answer{ 292 | Status: smtp.BadSequence, 293 | Message: reason, 294 | }) 295 | break 296 | } 297 | if !s.config.DisableAuth && !state.Authenticated { 298 | proto.Send(smtp.Answer{ 299 | Status: smtp.AuthenticationRequired, 300 | Message: "Authentication Required", 301 | }) 302 | break 303 | } 304 | 305 | state.From = cmd.From 306 | state.EightBitMIME = cmd.EightBitMIME 307 | message := "Sender" 308 | if state.EightBitMIME { 309 | message += " and 8BITMIME" 310 | } 311 | message += " ok" 312 | 313 | proto.Send(smtp.Answer{ 314 | Status: smtp.Ok, 315 | Message: message, 316 | }) 317 | 318 | case smtp.RcptCmd: 319 | if ok, reason := state.CanReceiveRcpt(); !ok { 320 | proto.Send(smtp.Answer{ 321 | Status: smtp.BadSequence, 322 | Message: reason, 323 | }) 324 | break 325 | } 326 | 327 | state.To = append(state.To, cmd.To) 328 | 329 | if !s.config.DisableAuth { 330 | // TODO check if to/from email address allowed 331 | if ok, reason := state.AuthMatchesRcptAndMail(); !ok { 332 | proto.Send(smtp.Answer{ 333 | Status: smtp.SMTPErrorPermanentMailboxNameNotAllowed.Status, 334 | Message: reason, 335 | }) 336 | state.Reset() 337 | break 338 | } 339 | } 340 | 341 | proto.Send(smtp.Answer{ 342 | Status: smtp.Ok, 343 | Message: "OK", 344 | }) 345 | 346 | case smtp.DataCmd: 347 | if ok, reason := state.CanReceiveData(); !ok { 348 | /* 349 | RFC 5321 3.3 350 | 351 | If there was no MAIL, or no RCPT, command, or all such commands were 352 | rejected, the server MAY return a "command out of sequence" (503) or 353 | "no valid recipients" (554) reply in response to the DATA command. 354 | If one of those replies (or any other 5yz reply) is received, the 355 | client MUST NOT send the message data; more generally, message data 356 | MUST NOT be sent unless a 354 reply is received. 357 | */ 358 | proto.Send(smtp.Answer{ 359 | Status: smtp.BadSequence, 360 | Message: reason, 361 | }) 362 | break 363 | } 364 | 365 | message := "Start" 366 | if state.EightBitMIME { 367 | message += " 8BITMIME" 368 | } 369 | message += " mail input; end with ." 370 | proto.Send(smtp.Answer{ 371 | Status: smtp.StartData, 372 | Message: message, 373 | }) 374 | 375 | tryAgain: 376 | tmpData, err := io.ReadAll(&cmd.R) 377 | state.Data = append(state.Data, tmpData...) 378 | if err == smtp.ErrLtl { 379 | proto.Send(smtp.Answer{ 380 | // SyntaxError or 552 error? or something else? 381 | Status: smtp.SyntaxError, 382 | Message: "Line too long", 383 | }) 384 | goto tryAgain 385 | } else if err == smtp.ErrIncomplete { 386 | // I think this can only happen on a socket if it gets closed before receiving the full data. 387 | proto.Send(smtp.Answer{ 388 | Status: smtp.SyntaxError, 389 | Message: "Could not parse mail data", 390 | }) 391 | state.Reset() 392 | break 393 | 394 | } else if err != nil { 395 | //panic(err) 396 | log.WithFields(log.Fields{ 397 | "SessionId": state.SessionId.String(), 398 | }).Panic(err) 399 | } 400 | 401 | // Handle mail 402 | err = s.MailHandler.Handle(state) 403 | if err != nil { 404 | smtpErr, ok := err.(smtp.SMTPError) 405 | if ok { 406 | // known SMTP error, just return it 407 | proto.Send(smtp.Answer(smtpErr)) 408 | } else { 409 | // unknown internal server error 410 | proto.Send(smtp.Answer{Status: 451, Message: "local error: something went wrong"}) 411 | log.WithFields(log.Fields{ 412 | "SessionId": state.SessionId.String(), 413 | "Ip": state.Ip.String(), 414 | }).Errorf("couldn't handle mail: %v", err) 415 | } 416 | } else { 417 | // mail successfully handled! 418 | proto.Send(smtp.Answer{ 419 | Status: smtp.Ok, 420 | Message: "Mail delivered", 421 | }) 422 | } 423 | 424 | // Reset state after mail was handled so we can start from a clean slate. 425 | state.Reset() 426 | 427 | case smtp.RsetCmd: 428 | state.Reset() 429 | proto.Send(smtp.Answer{ 430 | Status: smtp.Ok, 431 | Message: "OK", 432 | }) 433 | 434 | case smtp.StartTlsCmd: 435 | if !s.hasTls() { 436 | proto.Send(smtp.Answer{ 437 | Status: smtp.NotImplemented, 438 | Message: "STARTTLS is not implemented", 439 | }) 440 | break 441 | } 442 | 443 | if state.Secure { 444 | proto.Send(smtp.Answer{ 445 | Status: smtp.NotImplemented, 446 | Message: "Already in TLS mode", 447 | }) 448 | break 449 | } 450 | 451 | proto.Send(smtp.Answer{ 452 | Status: smtp.Ready, 453 | Message: "Ready for TLS handshake", 454 | }) 455 | 456 | err := proto.StartTls(s.TlsConfig) 457 | if err != nil { 458 | log.WithFields(log.Fields{ 459 | "Ip": state.Ip.String(), 460 | "SessionId": state.SessionId.String(), 461 | }).Warningf("Could not enable TLS: %v", err) 462 | break 463 | } 464 | 465 | log.WithFields(log.Fields{ 466 | "Ip": state.Ip.String(), 467 | "SessionId": state.SessionId.String(), 468 | }).Debug("TLS enabled") 469 | state.Reset() 470 | state.Secure = true 471 | 472 | case smtp.NoopCmd: 473 | proto.Send(smtp.Answer{ 474 | Status: smtp.Ok, 475 | Message: "OK", 476 | }) 477 | 478 | case smtp.VrfyCmd, smtp.ExpnCmd, smtp.SendCmd, smtp.SomlCmd, smtp.SamlCmd: 479 | proto.Send(smtp.Answer{ 480 | Status: smtp.NotImplemented, 481 | Message: "Command not implemented", 482 | }) 483 | 484 | case smtp.InvalidCmd: 485 | // TODO: Is this correct? An InvalidCmd is a known command with 486 | // invalid arguments. So we should send smtp.SyntaxErrorParam? 487 | // Is InvalidCmd a good name for this kind of error? 488 | proto.Send(smtp.Answer{ 489 | Status: smtp.SyntaxErrorParam, 490 | Message: cmd.Info, 491 | }) 492 | 493 | case smtp.UnknownCmd: 494 | proto.Send(smtp.Answer{ 495 | Status: smtp.SyntaxError, 496 | Message: "Command not recognized", 497 | }) 498 | 499 | case smtp.AuthCmd: 500 | 501 | // check whether the connection is secure 502 | if !state.Secure { 503 | proto.Send(smtp.Answer{ 504 | Status: smtp.EncryptionRequiredForRequestedAuthenticationMechanism, 505 | Message: "5.7.0 Must issue a STARTTLS command first.", 506 | }) 507 | break 508 | } 509 | 510 | // make sure to add auth mechanisms to the EHLO command 511 | if cmd.Mechanism != "PLAIN" { 512 | proto.Send(smtp.Answer{ 513 | Status: smtp.UnrecognizedAuthenticationType, 514 | Message: "5.7.4 Unrecognized authentication type", 515 | }) 516 | break 517 | } 518 | 519 | initialResponse := "" 520 | 521 | // If no credentials are not present in AUTH command, prompt the client for them. 522 | if cmd.InitialResponse == "" { 523 | //tmpData, err := ioutil.ReadAll(&cmd.R) 524 | tmpData, err := smtp.ReadUntill('\n', smtp.MAX_CMD_LINE, &cmd.R) 525 | initialResponse = string(tmpData) 526 | if err != nil { 527 | // I think this can only happen on a socket if it gets closed before receiving the full data. 528 | log.WithFields(log.Fields{ 529 | "SessionId": state.SessionId.String(), 530 | }).Warnln(err) 531 | proto.Send(smtp.Answer{ 532 | Status: smtp.MalformedAuthInput, 533 | Message: "Could not parse auth data", 534 | }) 535 | break 536 | 537 | } 538 | } else { 539 | initialResponse = cmd.InitialResponse 540 | } 541 | 542 | authorizationIdentity, authenticationIdenity, password, err := smtp.ParseAuthPlainInitialRespone(initialResponse) 543 | if err != nil { 544 | 545 | log.WithFields(log.Fields{ 546 | "Ip": state.Ip.String(), 547 | "SessionId": state.SessionId.String(), 548 | }).Warningf("Could not decode base64: %v", err) 549 | 550 | proto.Send(smtp.Answer{ 551 | Status: smtp.SyntaxErrorParam, 552 | Message: "Invalid initial response for PLAIN auth", 553 | }) 554 | 555 | break 556 | 557 | } 558 | 559 | log.WithFields(log.Fields{ 560 | "authorization-identity": authorizationIdentity, 561 | "authentication-identity": authenticationIdenity, 562 | // "password": "password", 563 | // let's not log user passwords.... 564 | }).Debugln("received auth") 565 | 566 | // Check if AuthBackend is initialized 567 | if s.AuthBackend == nil { 568 | log.Errorln("AuthBackend not initialized") 569 | proto.Send(smtp.Answer{ 570 | Status: smtp.TemporaryAuthenticationFailure, 571 | Message: "4.7.0 Temporary authentication failure", 572 | }) 573 | break 574 | } 575 | 576 | user, err := s.AuthBackend.Login(state, authenticationIdenity, password) 577 | if err == ErrInvalidCredentials { 578 | // Invalid credentials 579 | state.Authenticated = false 580 | 581 | log.WithFields(log.Fields{ 582 | "Ip": state.Ip.String(), 583 | "SessionId": state.SessionId.String(), 584 | }).Printf("invalid auth for user: %s", authenticationIdenity) 585 | 586 | proto.Send(smtp.Answer{ 587 | Status: smtp.AuthenticationCredentialsInvalid, 588 | Message: "5.7.8 Authentication credentials invalid", 589 | }) 590 | 591 | break 592 | } 593 | if err != nil { 594 | // Other error 595 | state.Authenticated = false 596 | 597 | log.WithFields(log.Fields{ 598 | "Ip": state.Ip.String(), 599 | "SessionId": state.SessionId.String(), 600 | }).Printf("authentication failed for user: %s with error: %v", authenticationIdenity, err) 601 | 602 | proto.Send(smtp.Answer{ 603 | Status: smtp.TemporaryAuthenticationFailure, 604 | Message: "4.7.0 Temporary authentication failure", 605 | }) 606 | 607 | break 608 | } 609 | 610 | // Valid auth 611 | 612 | state.Authenticated = true 613 | state.User = user 614 | 615 | log.WithFields(log.Fields{ 616 | "Ip": state.Ip.String(), 617 | "SessionId": state.SessionId.String(), 618 | }).Printf("valid auth for user: %s", authenticationIdenity) 619 | 620 | proto.Send(smtp.Answer{ 621 | Status: smtp.AuthenticationSucceeded, 622 | Message: "2.7.0 Authentication successful", 623 | }) 624 | 625 | //initialResponseText := string(initialResponseByte) 626 | //log.Fatalf("initial-response: %v", initialResponseText) 627 | 628 | default: 629 | // TODO: We get here if the switch does not handle all Cmd's defined 630 | // in protocol.go. That means we forgot to add it here. This should ideally 631 | // be checked at compile time. But if we get here anyway we probably shouldn't 632 | // crash... 633 | log.Fatalf("Command not implemented: %#v", cmd) 634 | } 635 | 636 | if quit { 637 | break 638 | } 639 | 640 | quit = nextCmd() 641 | } 642 | 643 | proto.Close() 644 | log.WithFields(log.Fields{ 645 | "SessionId": state.SessionId.String(), 646 | "Ip": state.Ip.String(), 647 | }).Debug("Closed connection") 648 | } 649 | -------------------------------------------------------------------------------- /server/server_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "crypto/tls" 7 | "errors" 8 | "io" 9 | "net" 10 | "testing" 11 | 12 | "github.com/mistralmail/smtp/smtp" 13 | c "github.com/smartystreets/goconvey/convey" 14 | ) 15 | 16 | // Dummy mail handler 17 | func dummyHandler(*smtp.State) error { 18 | return nil 19 | } 20 | 21 | // Dummy mail handler which returns error 22 | func dummyHandlerError(*smtp.State) error { 23 | return smtp.SMTPErrorPermanentMailboxNotAvailable 24 | } 25 | 26 | type testProtocol struct { 27 | t *testing.T 28 | // Goconvey context so it works in a different goroutine 29 | ctx c.C 30 | cmds []smtp.Cmd 31 | answers []interface{} 32 | expectTLS bool 33 | state smtp.State 34 | } 35 | 36 | func getMailWithoutError(a string) *smtp.MailAddress { 37 | addr, _ := smtp.ParseAddress(a) 38 | return &addr 39 | } 40 | 41 | func (p *testProtocol) Send(cmd smtp.Cmd) { 42 | p.ctx.So(len(p.answers), c.ShouldBeGreaterThan, 0) 43 | 44 | //c.Printf("RECEIVED: %#v\n", cmd) 45 | 46 | answer := p.answers[0] 47 | p.answers = p.answers[1:] 48 | 49 | if cmdA, ok := cmd.(smtp.Answer); ok { 50 | p.ctx.So(cmdA, c.ShouldHaveSameTypeAs, answer) 51 | cmdE, _ := answer.(smtp.Answer) 52 | p.ctx.So(cmdA.Status, c.ShouldEqual, cmdE.Status) 53 | } else if cmdA, ok := cmd.(smtp.MultiAnswer); ok { 54 | p.ctx.So(cmdA, c.ShouldHaveSameTypeAs, answer) 55 | cmdE, _ := answer.(smtp.MultiAnswer) 56 | p.ctx.So(cmdA.Status, c.ShouldEqual, cmdE.Status) 57 | } else { 58 | p.t.Fatalf("Answer should be Answer or MultiAnswer") 59 | } 60 | } 61 | 62 | func (p *testProtocol) GetCmd() (*smtp.Cmd, error) { 63 | p.ctx.So(len(p.cmds), c.ShouldBeGreaterThan, 0) 64 | 65 | cmd := p.cmds[0] 66 | p.cmds = p.cmds[1:] 67 | 68 | if cmd == nil { 69 | return nil, io.EOF 70 | } 71 | 72 | //c.Printf("SENDING: %#v\n", cmd) 73 | return &cmd, nil 74 | } 75 | 76 | func (p *testProtocol) Close() { 77 | // Did not expect connection to be closed, got more commands 78 | p.ctx.So(len(p.cmds), c.ShouldBeLessThanOrEqualTo, 0) 79 | 80 | // Did not expect connection to be closed, need more answers 81 | p.ctx.So(len(p.answers), c.ShouldBeLessThanOrEqualTo, 0) 82 | } 83 | 84 | func (p *testProtocol) StartTls(c *tls.Config) error { 85 | if !p.expectTLS { 86 | p.t.Fatalf("Did not expect StartTls") 87 | return errors.New("NOT IMPLEMENTED") 88 | } 89 | 90 | return nil 91 | } 92 | 93 | func (p *testProtocol) GetIP() net.IP { 94 | return net.ParseIP("127.0.0.1") 95 | } 96 | 97 | func (p *testProtocol) GetState() *smtp.State { 98 | return &p.state 99 | } 100 | 101 | // Tests answers for HELO,EHLO and QUIT 102 | func TestAnswersHeloQuit(t *testing.T) { 103 | cfg := Config{ 104 | Hostname: "home.sweet.home", 105 | DisableAuth: true, 106 | } 107 | 108 | mta := New(cfg, HandlerFunc(dummyHandler)) 109 | if mta == nil { 110 | t.Fatal("Could not create mta server") 111 | } 112 | 113 | c.Convey("Testing answers for HELO and QUIT.", t, func(ctx c.C) { 114 | 115 | // Test connection with HELO and QUIT 116 | proto := &testProtocol{ 117 | t: t, 118 | ctx: ctx, 119 | cmds: []smtp.Cmd{ 120 | smtp.HeloCmd{ 121 | Domain: "some.sender", 122 | }, 123 | smtp.QuitCmd{}, 124 | }, 125 | answers: []interface{}{ 126 | smtp.Answer{ 127 | Status: smtp.Ready, 128 | Message: cfg.Hostname + " Service Ready", 129 | }, 130 | smtp.Answer{ 131 | Status: smtp.Ok, 132 | Message: cfg.Hostname, 133 | }, 134 | smtp.Answer{ 135 | Status: smtp.Closing, 136 | Message: "Bye!", 137 | }, 138 | }, 139 | } 140 | mta.HandleClient(proto) 141 | c.So(proto.GetState().Hostname, c.ShouldEqual, "some.sender") 142 | }) 143 | 144 | c.Convey("Testing answers for HELO and close connection.", t, func(ctx c.C) { 145 | proto := &testProtocol{ 146 | t: t, 147 | ctx: ctx, 148 | cmds: []smtp.Cmd{ 149 | smtp.HeloCmd{ 150 | Domain: "some.sender", 151 | }, 152 | nil, 153 | }, 154 | answers: []interface{}{ 155 | smtp.Answer{ 156 | Status: smtp.Ready, 157 | Message: cfg.Hostname + " Service Ready", 158 | }, 159 | smtp.Answer{ 160 | Status: smtp.Ok, 161 | Message: cfg.Hostname, 162 | }, 163 | }, 164 | } 165 | mta.HandleClient(proto) 166 | 167 | }) 168 | 169 | c.Convey("Testing answers for EHLO and QUIT.", t, func(ctx c.C) { 170 | 171 | // Test connection with EHLO and QUIT 172 | proto := &testProtocol{ 173 | t: t, 174 | ctx: ctx, 175 | cmds: []smtp.Cmd{ 176 | smtp.EhloCmd{ 177 | Domain: "some.sender", 178 | }, 179 | smtp.QuitCmd{}, 180 | }, 181 | answers: []interface{}{ 182 | smtp.Answer{ 183 | Status: smtp.Ready, 184 | Message: cfg.Hostname + " Service Ready", 185 | }, 186 | smtp.MultiAnswer{ 187 | Status: smtp.Ok, 188 | }, 189 | smtp.Answer{ 190 | Status: smtp.Closing, 191 | Message: "Bye!", 192 | }, 193 | }, 194 | } 195 | mta.HandleClient(proto) 196 | }) 197 | 198 | c.Convey("Testing answers for EHLO and close connection.", t, func(ctx c.C) { 199 | proto := &testProtocol{ 200 | t: t, 201 | ctx: ctx, 202 | cmds: []smtp.Cmd{ 203 | smtp.EhloCmd{ 204 | Domain: "some.sender.ehlo", 205 | }, 206 | nil, 207 | }, 208 | answers: []interface{}{ 209 | smtp.Answer{ 210 | Status: smtp.Ready, 211 | Message: cfg.Hostname + " Service Ready", 212 | }, 213 | smtp.MultiAnswer{ 214 | Status: smtp.Ok, 215 | }, 216 | }, 217 | } 218 | mta.HandleClient(proto) 219 | c.So(proto.GetState().Hostname, c.ShouldEqual, "some.sender.ehlo") 220 | }) 221 | } 222 | 223 | // Test answers if we are given a sequence of MAIL,RCPT,DATA commands. 224 | func TestMailAnswersCorrectSequence(t *testing.T) { 225 | cfg := Config{ 226 | Hostname: "home.sweet.home", 227 | DisableAuth: true, 228 | } 229 | 230 | mta := New(cfg, HandlerFunc(dummyHandler)) 231 | if mta == nil { 232 | t.Fatal("Could not create mta server") 233 | } 234 | 235 | c.Convey("Testing correct sequence of MAIL,RCPT,DATA commands.", t, func(ctx c.C) { 236 | 237 | proto := &testProtocol{ 238 | t: t, 239 | ctx: ctx, 240 | cmds: []smtp.Cmd{ 241 | smtp.HeloCmd{ 242 | Domain: "some.sender", 243 | }, 244 | smtp.MailCmd{ 245 | From: getMailWithoutError("someone@somewhere.test"), 246 | }, 247 | smtp.RcptCmd{ 248 | To: getMailWithoutError("guy1@somewhere.test"), 249 | }, 250 | smtp.RcptCmd{ 251 | To: getMailWithoutError("guy2@somewhere.test"), 252 | }, 253 | smtp.DataCmd{ 254 | R: *smtp.NewDataReader(bufio.NewReader(bytes.NewReader([]byte("Some test email\n.\n")))), 255 | }, 256 | smtp.QuitCmd{}, 257 | }, 258 | answers: []interface{}{ 259 | smtp.Answer{ 260 | Status: smtp.Ready, 261 | Message: cfg.Hostname + " Service Ready", 262 | }, 263 | smtp.Answer{ 264 | Status: smtp.Ok, 265 | Message: cfg.Hostname, 266 | }, 267 | smtp.Answer{ 268 | Status: smtp.Ok, 269 | Message: "OK", 270 | }, 271 | smtp.Answer{ 272 | Status: smtp.Ok, 273 | Message: "OK", 274 | }, 275 | smtp.Answer{ 276 | Status: smtp.Ok, 277 | Message: "OK", 278 | }, 279 | smtp.Answer{ 280 | Status: smtp.StartData, 281 | Message: "OK", 282 | }, 283 | smtp.Answer{ 284 | Status: smtp.Ok, 285 | Message: "OK", 286 | }, 287 | smtp.Answer{ 288 | Status: smtp.Closing, 289 | Message: "Bye!", 290 | }, 291 | }, 292 | } 293 | mta.HandleClient(proto) 294 | }) 295 | 296 | c.Convey("Testing wrong sequence of MAIL,RCPT,DATA commands.", t, func(ctx c.C) { 297 | c.Convey("RCPT before MAIL", func() { 298 | proto := &testProtocol{ 299 | t: t, 300 | ctx: ctx, 301 | cmds: []smtp.Cmd{ 302 | smtp.HeloCmd{ 303 | Domain: "some.sender", 304 | }, 305 | smtp.RcptCmd{ 306 | To: getMailWithoutError("guy1@somewhere.test"), 307 | }, 308 | smtp.QuitCmd{}, 309 | }, 310 | answers: []interface{}{ 311 | smtp.Answer{ 312 | Status: smtp.Ready, 313 | Message: cfg.Hostname + " Service Ready", 314 | }, 315 | smtp.Answer{ 316 | Status: smtp.Ok, 317 | Message: cfg.Hostname, 318 | }, 319 | smtp.Answer{ 320 | Status: smtp.BadSequence, 321 | Message: "Need mail before RCPT", 322 | }, 323 | smtp.Answer{ 324 | Status: smtp.Closing, 325 | Message: "Bye!", 326 | }, 327 | }, 328 | } 329 | mta.HandleClient(proto) 330 | }) 331 | 332 | c.Convey("DATA before MAIL", func() { 333 | proto := &testProtocol{ 334 | t: t, 335 | ctx: ctx, 336 | cmds: []smtp.Cmd{ 337 | smtp.HeloCmd{ 338 | Domain: "some.sender", 339 | }, 340 | smtp.DataCmd{}, 341 | smtp.QuitCmd{}, 342 | }, 343 | answers: []interface{}{ 344 | smtp.Answer{ 345 | Status: smtp.Ready, 346 | Message: cfg.Hostname + " Service Ready", 347 | }, 348 | smtp.Answer{ 349 | Status: smtp.Ok, 350 | Message: cfg.Hostname, 351 | }, 352 | smtp.Answer{ 353 | Status: smtp.BadSequence, 354 | Message: "Need mail before DATA", 355 | }, 356 | smtp.Answer{ 357 | Status: smtp.Closing, 358 | Message: "Bye!", 359 | }, 360 | }, 361 | } 362 | mta.HandleClient(proto) 363 | }) 364 | 365 | c.Convey("DATA before RCPT", func() { 366 | proto := &testProtocol{ 367 | t: t, 368 | ctx: ctx, 369 | cmds: []smtp.Cmd{ 370 | smtp.HeloCmd{ 371 | Domain: "some.sender", 372 | }, 373 | smtp.MailCmd{ 374 | From: getMailWithoutError("guy@somewhere.test"), 375 | }, 376 | smtp.DataCmd{}, 377 | smtp.QuitCmd{}, 378 | }, 379 | answers: []interface{}{ 380 | smtp.Answer{ 381 | Status: smtp.Ready, 382 | Message: cfg.Hostname + " Service Ready", 383 | }, 384 | smtp.Answer{ 385 | Status: smtp.Ok, 386 | Message: cfg.Hostname, 387 | }, 388 | smtp.Answer{ 389 | Status: smtp.Ok, 390 | Message: "OK", 391 | }, 392 | smtp.Answer{ 393 | Status: smtp.BadSequence, 394 | Message: "Need RCPT before DATA", 395 | }, 396 | smtp.Answer{ 397 | Status: smtp.Closing, 398 | Message: "Bye!", 399 | }, 400 | }, 401 | } 402 | mta.HandleClient(proto) 403 | }) 404 | 405 | c.Convey("Multiple MAIL commands.", func() { 406 | proto := &testProtocol{ 407 | t: t, 408 | ctx: ctx, 409 | cmds: []smtp.Cmd{ 410 | smtp.HeloCmd{ 411 | Domain: "some.sender", 412 | }, 413 | smtp.MailCmd{ 414 | From: getMailWithoutError("guy@somewhere.test"), 415 | }, 416 | smtp.RcptCmd{ 417 | To: getMailWithoutError("someone@somewhere.test"), 418 | }, 419 | smtp.MailCmd{ 420 | From: getMailWithoutError("someguy@somewhere.test"), 421 | }, 422 | smtp.QuitCmd{}, 423 | }, 424 | answers: []interface{}{ 425 | smtp.Answer{ 426 | Status: smtp.Ready, 427 | Message: cfg.Hostname + " Service Ready", 428 | }, 429 | smtp.Answer{ 430 | Status: smtp.Ok, 431 | Message: cfg.Hostname, 432 | }, 433 | smtp.Answer{ 434 | Status: smtp.Ok, 435 | Message: "OK", 436 | }, 437 | smtp.Answer{ 438 | Status: smtp.Ok, 439 | Message: "OK", 440 | }, 441 | smtp.Answer{ 442 | Status: smtp.BadSequence, 443 | Message: "Sender already specified", 444 | }, 445 | smtp.Answer{ 446 | Status: smtp.Closing, 447 | Message: "Bye!", 448 | }, 449 | }, 450 | } 451 | mta.HandleClient(proto) 452 | }) 453 | 454 | }) 455 | } 456 | 457 | // Tests if our state gets reset correctly. 458 | func TestReset(t *testing.T) { 459 | cfg := Config{ 460 | Hostname: "home.sweet.home", 461 | DisableAuth: true, 462 | } 463 | 464 | mta := New(cfg, HandlerFunc(dummyHandler)) 465 | if mta == nil { 466 | t.Fatal("Could not create mta server") 467 | } 468 | 469 | c.Convey("Testing reset", t, func(ctx c.C) { 470 | 471 | c.Convey("Test reset after sending mail.", func() { 472 | proto := &testProtocol{ 473 | t: t, 474 | ctx: ctx, 475 | cmds: []smtp.Cmd{ 476 | smtp.HeloCmd{ 477 | Domain: "some.sender", 478 | }, 479 | smtp.MailCmd{ 480 | From: getMailWithoutError("someone@somewhere.test"), 481 | }, 482 | smtp.RcptCmd{ 483 | To: getMailWithoutError("guy1@somewhere.test"), 484 | }, 485 | smtp.DataCmd{ 486 | R: *smtp.NewDataReader(bufio.NewReader(bytes.NewReader([]byte("Some email content\n.\n")))), 487 | }, 488 | smtp.RcptCmd{ 489 | To: getMailWithoutError("someguy@somewhere.test"), 490 | }, 491 | smtp.QuitCmd{}, 492 | }, 493 | answers: []interface{}{ 494 | smtp.Answer{ 495 | Status: smtp.Ready, 496 | Message: cfg.Hostname + " Service Ready", 497 | }, 498 | smtp.Answer{ 499 | Status: smtp.Ok, 500 | Message: cfg.Hostname, 501 | }, 502 | smtp.Answer{ 503 | Status: smtp.Ok, 504 | Message: "OK", 505 | }, 506 | smtp.Answer{ 507 | Status: smtp.Ok, 508 | Message: "OK", 509 | }, 510 | smtp.Answer{ 511 | Status: smtp.StartData, 512 | Message: "OK", 513 | }, 514 | smtp.Answer{ 515 | Status: smtp.Ok, 516 | Message: "OK", 517 | }, 518 | smtp.Answer{ 519 | Status: smtp.BadSequence, 520 | Message: "Need mail before RCPT", 521 | }, 522 | smtp.Answer{ 523 | Status: smtp.Closing, 524 | Message: "Bye!", 525 | }, 526 | }, 527 | } 528 | mta.HandleClient(proto) 529 | }) 530 | 531 | c.Convey("Manually reset", func() { 532 | proto := &testProtocol{ 533 | t: t, 534 | ctx: ctx, 535 | cmds: []smtp.Cmd{ 536 | smtp.HeloCmd{ 537 | Domain: "some.sender", 538 | }, 539 | smtp.MailCmd{ 540 | From: getMailWithoutError("someone@somewhere.test"), 541 | }, 542 | smtp.RcptCmd{ 543 | To: getMailWithoutError("guy1@somewhere.test"), 544 | }, 545 | smtp.RsetCmd{}, 546 | smtp.MailCmd{ 547 | From: getMailWithoutError("someone@somewhere.test"), 548 | }, 549 | smtp.RcptCmd{ 550 | To: getMailWithoutError("guy1@somewhere.test"), 551 | }, 552 | smtp.DataCmd{ 553 | R: *smtp.NewDataReader(bufio.NewReader(bytes.NewReader([]byte("Some email\n.\n")))), 554 | }, 555 | smtp.QuitCmd{}, 556 | }, 557 | answers: []interface{}{ 558 | smtp.Answer{ 559 | Status: smtp.Ready, 560 | Message: cfg.Hostname + " Service Ready", 561 | }, 562 | smtp.Answer{ 563 | Status: smtp.Ok, 564 | Message: cfg.Hostname, 565 | }, 566 | smtp.Answer{ 567 | Status: smtp.Ok, 568 | Message: "OK", 569 | }, 570 | smtp.Answer{ 571 | Status: smtp.Ok, 572 | Message: "OK", 573 | }, 574 | smtp.Answer{ 575 | Status: smtp.Ok, 576 | Message: "OK", 577 | }, 578 | smtp.Answer{ 579 | Status: smtp.Ok, 580 | Message: "OK", 581 | }, 582 | smtp.Answer{ 583 | Status: smtp.Ok, 584 | Message: "OK", 585 | }, 586 | smtp.Answer{ 587 | Status: smtp.StartData, 588 | Message: "OK", 589 | }, 590 | smtp.Answer{ 591 | Status: smtp.Ok, 592 | Message: "OK", 593 | }, 594 | smtp.Answer{ 595 | Status: smtp.Closing, 596 | Message: "Bye!", 597 | }, 598 | }, 599 | } 600 | mta.HandleClient(proto) 601 | }) 602 | 603 | // EHLO should reset state. 604 | c.Convey("Reset with EHLO", func() { 605 | proto := &testProtocol{ 606 | t: t, 607 | ctx: ctx, 608 | cmds: []smtp.Cmd{ 609 | smtp.EhloCmd{ 610 | Domain: "some.sender", 611 | }, 612 | smtp.MailCmd{ 613 | From: getMailWithoutError("someone@somewhere.test"), 614 | }, 615 | smtp.RcptCmd{ 616 | To: getMailWithoutError("guy1@somewhere.test"), 617 | }, 618 | smtp.EhloCmd{ 619 | Domain: "some.sender", 620 | }, 621 | smtp.MailCmd{ 622 | From: getMailWithoutError("someone@somewhere.test"), 623 | }, 624 | smtp.RcptCmd{ 625 | To: getMailWithoutError("guy1@somewhere.test"), 626 | }, 627 | smtp.DataCmd{ 628 | R: *smtp.NewDataReader(bufio.NewReader(bytes.NewReader([]byte("Some email\n.\n")))), 629 | }, 630 | smtp.QuitCmd{}, 631 | }, 632 | answers: []interface{}{ 633 | smtp.Answer{ 634 | Status: smtp.Ready, 635 | Message: cfg.Hostname + " Service Ready", 636 | }, 637 | smtp.MultiAnswer{ 638 | Status: smtp.Ok, 639 | }, 640 | smtp.Answer{ 641 | Status: smtp.Ok, 642 | Message: "OK", 643 | }, 644 | smtp.Answer{ 645 | Status: smtp.Ok, 646 | Message: "OK", 647 | }, 648 | smtp.MultiAnswer{ 649 | Status: smtp.Ok, 650 | }, 651 | smtp.Answer{ 652 | Status: smtp.Ok, 653 | Message: "OK", 654 | }, 655 | smtp.Answer{ 656 | Status: smtp.Ok, 657 | Message: "OK", 658 | }, 659 | smtp.Answer{ 660 | Status: smtp.StartData, 661 | Message: "OK", 662 | }, 663 | smtp.Answer{ 664 | Status: smtp.Ok, 665 | Message: "OK", 666 | }, 667 | smtp.Answer{ 668 | Status: smtp.Closing, 669 | Message: "Bye!", 670 | }, 671 | }, 672 | } 673 | mta.HandleClient(proto) 674 | }) 675 | 676 | }) 677 | } 678 | 679 | // Tests answers if we send an unknown command. 680 | func TestAnswersUnknownCmd(t *testing.T) { 681 | cfg := Config{ 682 | Hostname: "home.sweet.home", 683 | DisableAuth: true, 684 | } 685 | 686 | mta := New(cfg, HandlerFunc(dummyHandler)) 687 | if mta == nil { 688 | t.Fatal("Could not create mta server") 689 | } 690 | 691 | c.Convey("Testing answers for unknown cmds.", t, func(ctx c.C) { 692 | proto := &testProtocol{ 693 | t: t, 694 | ctx: ctx, 695 | cmds: []smtp.Cmd{ 696 | smtp.HeloCmd{ 697 | Domain: "some.sender", 698 | }, 699 | smtp.UnknownCmd{ 700 | Cmd: "someinvalidcmd", 701 | }, 702 | smtp.QuitCmd{}, 703 | }, 704 | answers: []interface{}{ 705 | smtp.Answer{ 706 | Status: smtp.Ready, 707 | Message: cfg.Hostname + " Service Ready", 708 | }, 709 | smtp.Answer{ 710 | Status: smtp.Ok, 711 | Message: "OK", 712 | }, 713 | smtp.Answer{ 714 | Status: smtp.SyntaxError, 715 | Message: cfg.Hostname, 716 | }, 717 | smtp.Answer{ 718 | Status: smtp.Closing, 719 | Message: "Bye!", 720 | }, 721 | }, 722 | } 723 | mta.HandleClient(proto) 724 | }) 725 | } 726 | 727 | // Tests STARTTLS 728 | func TestStartTls(t *testing.T) { 729 | cfg := Config{ 730 | Hostname: "home.sweet.home", 731 | DisableAuth: true, 732 | } 733 | 734 | mta := New(cfg, HandlerFunc(dummyHandler)) 735 | if mta == nil { 736 | t.Fatal("Could not create mta server") 737 | } 738 | mta.TlsConfig = &tls.Config{} 739 | 740 | c.Convey("Testing STARTTLS", t, func(ctx c.C) { 741 | proto := &testProtocol{ 742 | t: t, 743 | ctx: ctx, 744 | cmds: []smtp.Cmd{ 745 | smtp.EhloCmd{ 746 | Domain: "some.sender", 747 | }, 748 | smtp.StartTlsCmd{}, 749 | smtp.QuitCmd{}, 750 | }, 751 | answers: []interface{}{ 752 | smtp.Answer{ 753 | Status: smtp.Ready, 754 | Message: cfg.Hostname + " Service Ready", 755 | }, 756 | smtp.MultiAnswer{ 757 | Status: smtp.Ok, 758 | }, 759 | smtp.Answer{ 760 | Status: smtp.Ready, 761 | }, 762 | smtp.Answer{ 763 | Status: smtp.Closing, 764 | Message: "Bye!", 765 | }, 766 | }, 767 | } 768 | proto.expectTLS = true 769 | mta.HandleClient(proto) 770 | }) 771 | 772 | c.Convey("Testing if STARTTLS resets state", t, func(ctx c.C) { 773 | proto := &testProtocol{ 774 | t: t, 775 | ctx: ctx, 776 | cmds: []smtp.Cmd{ 777 | smtp.EhloCmd{ 778 | Domain: "some.sender", 779 | }, 780 | smtp.MailCmd{ 781 | From: getMailWithoutError("someone@somewhere.test"), 782 | }, 783 | smtp.StartTlsCmd{}, 784 | smtp.MailCmd{ 785 | From: getMailWithoutError("someone@somewhere.test"), 786 | }, 787 | smtp.QuitCmd{}, 788 | }, 789 | answers: []interface{}{ 790 | smtp.Answer{ 791 | Status: smtp.Ready, 792 | Message: cfg.Hostname + " Service Ready", 793 | }, 794 | smtp.MultiAnswer{ 795 | Status: smtp.Ok, 796 | }, 797 | smtp.Answer{ 798 | Status: smtp.Ok, 799 | }, 800 | smtp.Answer{ 801 | Status: smtp.Ready, 802 | }, 803 | smtp.Answer{ 804 | Status: smtp.Ok, 805 | }, 806 | smtp.Answer{ 807 | Status: smtp.Closing, 808 | Message: "Bye!", 809 | }, 810 | }, 811 | } 812 | proto.expectTLS = true 813 | mta.HandleClient(proto) 814 | }) 815 | 816 | c.Convey("Testing if we can STARTTLS twice", t, func(ctx c.C) { 817 | proto := &testProtocol{ 818 | t: t, 819 | ctx: ctx, 820 | cmds: []smtp.Cmd{ 821 | smtp.EhloCmd{ 822 | Domain: "some.sender", 823 | }, 824 | smtp.StartTlsCmd{}, 825 | smtp.StartTlsCmd{}, 826 | smtp.QuitCmd{}, 827 | }, 828 | answers: []interface{}{ 829 | smtp.Answer{ 830 | Status: smtp.Ready, 831 | Message: cfg.Hostname + " Service Ready", 832 | }, 833 | smtp.MultiAnswer{ 834 | Status: smtp.Ok, 835 | }, 836 | smtp.Answer{ 837 | Status: smtp.Ready, 838 | }, 839 | smtp.Answer{ 840 | Status: smtp.NotImplemented, 841 | }, 842 | smtp.Answer{ 843 | Status: smtp.Closing, 844 | Message: "Bye!", 845 | }, 846 | }, 847 | } 848 | proto.expectTLS = true 849 | mta.HandleClient(proto) 850 | }) 851 | } 852 | 853 | // Simple test for representation of SessionId 854 | func TestSessionId(t *testing.T) { 855 | c.Convey("Testing Session ID String()", t, func() { 856 | id := smtp.Id{Timestamp: 1446302030, Counter: 42} 857 | c.So(id.String(), c.ShouldEqual, "5634d14e2a") 858 | 859 | id = smtp.Id{Timestamp: 2147483648, Counter: 4294967295} 860 | c.So(id.String(), c.ShouldEqual, "80000000ffffffff") 861 | }) 862 | } 863 | 864 | // Test whether error in handle() is correctly handled in the DATA command 865 | func TestErrorInHandler(t *testing.T) { 866 | cfg := Config{ 867 | Hostname: "home.sweet.home", 868 | DisableAuth: true, 869 | } 870 | 871 | mta := New(cfg, HandlerFunc(dummyHandlerError)) 872 | if mta == nil { 873 | t.Fatal("Could not create mta server") 874 | } 875 | 876 | c.Convey("Testing with error in handle()", t, func(ctx c.C) { 877 | 878 | proto := &testProtocol{ 879 | t: t, 880 | ctx: ctx, 881 | cmds: []smtp.Cmd{ 882 | smtp.HeloCmd{ 883 | Domain: "some.sender", 884 | }, 885 | smtp.MailCmd{ 886 | From: getMailWithoutError("someone@somewhere.test"), 887 | }, 888 | smtp.RcptCmd{ 889 | To: getMailWithoutError("guy1@somewhere.test"), 890 | }, 891 | smtp.DataCmd{ 892 | R: *smtp.NewDataReader(bufio.NewReader(bytes.NewReader([]byte("Some test email\n.\n")))), 893 | }, 894 | smtp.QuitCmd{}, 895 | }, 896 | answers: []interface{}{ 897 | smtp.Answer{ 898 | Status: smtp.Ready, 899 | Message: cfg.Hostname + " Service Ready", 900 | }, 901 | smtp.Answer{ 902 | Status: smtp.Ok, 903 | Message: cfg.Hostname, 904 | }, 905 | smtp.Answer{ 906 | Status: smtp.Ok, 907 | Message: "OK", 908 | }, 909 | smtp.Answer{ 910 | Status: smtp.Ok, 911 | Message: "OK", 912 | }, 913 | smtp.Answer{ 914 | Status: smtp.StartData, 915 | Message: "OK", 916 | }, 917 | smtp.Answer{ 918 | Status: smtp.SMTPErrorPermanentMailboxNotAvailable.Status, 919 | Message: smtp.SMTPErrorPermanentMailboxNotAvailable.Message, 920 | }, 921 | smtp.Answer{ 922 | Status: smtp.Closing, 923 | Message: "Bye!", 924 | }, 925 | }, 926 | } 927 | mta.HandleClient(proto) 928 | }) 929 | } 930 | 931 | func TestAuth(t *testing.T) { 932 | cfg := Config{ 933 | Hostname: "home.sweet.home", 934 | } 935 | 936 | mta := New(cfg, HandlerFunc(dummyHandler)) 937 | if mta == nil { 938 | t.Fatal("Could not create mta server") 939 | } 940 | mta.TlsConfig = &tls.Config{} 941 | 942 | mta.AuthBackend = NewAuthBackendMemory(map[string]string{"some-username@example.com": "password1234"}) 943 | 944 | c.Convey("Testing AUTH with correct credentials", t, func(ctx c.C) { 945 | 946 | proto := &testProtocol{ 947 | t: t, 948 | ctx: ctx, 949 | expectTLS: true, 950 | cmds: []smtp.Cmd{ 951 | smtp.HeloCmd{ 952 | Domain: "some.sender", 953 | }, 954 | smtp.StartTlsCmd{}, 955 | smtp.AuthCmd{ 956 | Mechanism: "PLAIN", 957 | InitialResponse: "AHNvbWUtdXNlcm5hbWVAZXhhbXBsZS5jb20AcGFzc3dvcmQxMjM0", 958 | }, 959 | smtp.QuitCmd{}, 960 | }, 961 | answers: []interface{}{ 962 | smtp.Answer{ 963 | Status: smtp.Ready, 964 | Message: cfg.Hostname + " Service Ready", 965 | }, 966 | smtp.Answer{ 967 | Status: smtp.Ok, 968 | Message: cfg.Hostname, 969 | }, 970 | smtp.Answer{ 971 | Status: smtp.Ready, 972 | }, 973 | smtp.Answer{ 974 | Status: smtp.AuthenticationSucceeded, 975 | Message: "2.7.0 Authentication successful", 976 | }, 977 | smtp.Answer{ 978 | Status: smtp.Closing, 979 | Message: "Bye!", 980 | }, 981 | }, 982 | } 983 | 984 | c.So(proto.GetState().Authenticated, c.ShouldEqual, false) 985 | 986 | mta.HandleClient(proto) 987 | c.So(proto.GetState().Hostname, c.ShouldEqual, "some.sender") 988 | c.So(proto.GetState().Authenticated, c.ShouldEqual, true) 989 | }) 990 | 991 | c.Convey("Testing AUTH without STARTTLS", t, func(ctx c.C) { 992 | 993 | proto := &testProtocol{ 994 | t: t, 995 | ctx: ctx, 996 | cmds: []smtp.Cmd{ 997 | smtp.HeloCmd{ 998 | Domain: "some.sender", 999 | }, 1000 | smtp.AuthCmd{ 1001 | Mechanism: "PLAIN", 1002 | InitialResponse: "AHNvbWUtdXNlcm5hbWVAZXhhbXBsZS5jb20AcGFzc3dvcmQxMjM0", 1003 | }, 1004 | smtp.QuitCmd{}, 1005 | }, 1006 | answers: []interface{}{ 1007 | smtp.Answer{ 1008 | Status: smtp.Ready, 1009 | Message: cfg.Hostname + " Service Ready", 1010 | }, 1011 | smtp.Answer{ 1012 | Status: smtp.Ok, 1013 | Message: cfg.Hostname, 1014 | }, 1015 | smtp.Answer{ 1016 | Status: smtp.EncryptionRequiredForRequestedAuthenticationMechanism, 1017 | }, 1018 | smtp.Answer{ 1019 | Status: smtp.Closing, 1020 | Message: "Bye!", 1021 | }, 1022 | }, 1023 | } 1024 | 1025 | c.So(proto.GetState().Authenticated, c.ShouldEqual, false) 1026 | 1027 | mta.HandleClient(proto) 1028 | c.So(proto.GetState().Hostname, c.ShouldEqual, "some.sender") 1029 | c.So(proto.GetState().Authenticated, c.ShouldEqual, false) 1030 | }) 1031 | 1032 | c.Convey("Testing AUTH with incorrect credentials", t, func(ctx c.C) { 1033 | 1034 | proto := &testProtocol{ 1035 | t: t, 1036 | ctx: ctx, 1037 | expectTLS: true, 1038 | cmds: []smtp.Cmd{ 1039 | smtp.HeloCmd{ 1040 | Domain: "some.sender", 1041 | }, 1042 | smtp.StartTlsCmd{}, 1043 | smtp.AuthCmd{ 1044 | Mechanism: "PLAIN", 1045 | InitialResponse: "AHNvbWUtdXNlcm5hbWUAc29tZS1pbmNvcnJlY3QtcGFzc3dvcmQ=", 1046 | }, 1047 | smtp.QuitCmd{}, 1048 | }, 1049 | answers: []interface{}{ 1050 | smtp.Answer{ 1051 | Status: smtp.Ready, 1052 | Message: cfg.Hostname + " Service Ready", 1053 | }, 1054 | smtp.Answer{ 1055 | Status: smtp.Ok, 1056 | Message: cfg.Hostname, 1057 | }, 1058 | smtp.Answer{ 1059 | Status: smtp.Ready, 1060 | }, 1061 | smtp.Answer{ 1062 | Status: smtp.AuthenticationCredentialsInvalid, 1063 | Message: "5.7.8 Authentication credentials invalid", 1064 | }, 1065 | smtp.Answer{ 1066 | Status: smtp.Closing, 1067 | Message: "Bye!", 1068 | }, 1069 | }, 1070 | } 1071 | 1072 | c.So(proto.GetState().Authenticated, c.ShouldEqual, false) 1073 | 1074 | mta.HandleClient(proto) 1075 | c.So(proto.GetState().Hostname, c.ShouldEqual, "some.sender") 1076 | c.So(proto.GetState().Authenticated, c.ShouldEqual, false) 1077 | }) 1078 | 1079 | c.Convey("Testing AUTH with credentials in different command", t, func(ctx c.C) { 1080 | 1081 | proto := &testProtocol{ 1082 | t: t, 1083 | ctx: ctx, 1084 | expectTLS: true, 1085 | cmds: []smtp.Cmd{ 1086 | smtp.HeloCmd{ 1087 | Domain: "some.sender", 1088 | }, 1089 | smtp.StartTlsCmd{}, 1090 | smtp.AuthCmd{ 1091 | Mechanism: "PLAIN", 1092 | InitialResponse: "", 1093 | R: *bufio.NewReader(bytes.NewReader([]byte("AHNvbWUtdXNlcm5hbWVAZXhhbXBsZS5jb20AcGFzc3dvcmQxMjM0\r\n"))), 1094 | }, 1095 | smtp.QuitCmd{}, 1096 | }, 1097 | answers: []interface{}{ 1098 | smtp.Answer{ 1099 | Status: smtp.Ready, 1100 | Message: cfg.Hostname + " Service Ready", 1101 | }, 1102 | smtp.Answer{ 1103 | Status: smtp.Ok, 1104 | Message: cfg.Hostname, 1105 | }, 1106 | smtp.Answer{ 1107 | Status: smtp.Ready, 1108 | }, 1109 | smtp.Answer{ 1110 | Status: smtp.AuthenticationSucceeded, 1111 | Message: "2.7.0 Authentication successful", 1112 | }, 1113 | smtp.Answer{ 1114 | Status: smtp.Closing, 1115 | Message: "Bye!", 1116 | }, 1117 | }, 1118 | } 1119 | 1120 | c.So(proto.GetState().Authenticated, c.ShouldEqual, false) 1121 | 1122 | mta.HandleClient(proto) 1123 | c.So(proto.GetState().Hostname, c.ShouldEqual, "some.sender") 1124 | c.So(proto.GetState().Authenticated, c.ShouldEqual, true) 1125 | }) 1126 | 1127 | c.Convey("Testing AUTH with unknown mechanism", t, func(ctx c.C) { 1128 | 1129 | proto := &testProtocol{ 1130 | t: t, 1131 | ctx: ctx, 1132 | expectTLS: true, 1133 | cmds: []smtp.Cmd{ 1134 | smtp.HeloCmd{ 1135 | Domain: "some.sender", 1136 | }, 1137 | smtp.StartTlsCmd{}, 1138 | smtp.AuthCmd{ 1139 | Mechanism: "SOME_UNKNOWN_MECHANISM", 1140 | InitialResponse: "", 1141 | }, 1142 | smtp.QuitCmd{}, 1143 | }, 1144 | answers: []interface{}{ 1145 | smtp.Answer{ 1146 | Status: smtp.Ready, 1147 | Message: cfg.Hostname + " Service Ready", 1148 | }, 1149 | smtp.Answer{ 1150 | Status: smtp.Ok, 1151 | Message: cfg.Hostname, 1152 | }, 1153 | smtp.Answer{ 1154 | Status: smtp.Ready, 1155 | }, 1156 | smtp.Answer{ 1157 | Status: smtp.UnrecognizedAuthenticationType, 1158 | Message: "5.7.4 Unrecognized authentication type", 1159 | }, 1160 | smtp.Answer{ 1161 | Status: smtp.Closing, 1162 | Message: "Bye!", 1163 | }, 1164 | }, 1165 | } 1166 | 1167 | c.So(proto.GetState().Authenticated, c.ShouldEqual, false) 1168 | 1169 | mta.HandleClient(proto) 1170 | c.So(proto.GetState().Hostname, c.ShouldEqual, "some.sender") 1171 | c.So(proto.GetState().Authenticated, c.ShouldEqual, false) 1172 | }) 1173 | 1174 | c.Convey("Testing MAIL FROM when not authenticated and again after authentication", t, func(ctx c.C) { 1175 | 1176 | proto := &testProtocol{ 1177 | t: t, 1178 | ctx: ctx, 1179 | expectTLS: true, 1180 | cmds: []smtp.Cmd{ 1181 | smtp.HeloCmd{ 1182 | Domain: "some.sender", 1183 | }, 1184 | smtp.StartTlsCmd{}, 1185 | smtp.MailCmd{ 1186 | From: getMailWithoutError("test@test.com"), 1187 | }, 1188 | smtp.AuthCmd{ 1189 | Mechanism: "PLAIN", 1190 | InitialResponse: "AHNvbWUtdXNlcm5hbWVAZXhhbXBsZS5jb20AcGFzc3dvcmQxMjM0", 1191 | }, 1192 | smtp.MailCmd{ 1193 | From: getMailWithoutError("test@test.com"), 1194 | }, 1195 | smtp.QuitCmd{}, 1196 | }, 1197 | answers: []interface{}{ 1198 | smtp.Answer{ 1199 | Status: smtp.Ready, 1200 | Message: cfg.Hostname + " Service Ready", 1201 | }, 1202 | smtp.Answer{ 1203 | Status: smtp.Ok, 1204 | Message: cfg.Hostname, 1205 | }, 1206 | smtp.Answer{ 1207 | Status: smtp.Ready, 1208 | }, 1209 | smtp.Answer{ 1210 | Status: smtp.AuthenticationRequired, 1211 | Message: "Authentication Required", 1212 | }, 1213 | smtp.Answer{ 1214 | Status: smtp.AuthenticationSucceeded, 1215 | Message: "2.7.0 Authentication successful", 1216 | }, 1217 | smtp.Answer{ 1218 | Status: smtp.Ok, 1219 | Message: "OK", 1220 | }, 1221 | smtp.Answer{ 1222 | Status: smtp.Closing, 1223 | Message: "Bye!", 1224 | }, 1225 | }, 1226 | } 1227 | 1228 | c.So(proto.GetState().Authenticated, c.ShouldEqual, false) 1229 | 1230 | mta.HandleClient(proto) 1231 | c.So(proto.GetState().Hostname, c.ShouldEqual, "some.sender") 1232 | c.So(proto.GetState().Authenticated, c.ShouldEqual, true) 1233 | }) 1234 | 1235 | c.Convey("Testing AUTH when sending from a wrong email address", t, func(ctx c.C) { 1236 | 1237 | proto := &testProtocol{ 1238 | t: t, 1239 | ctx: ctx, 1240 | expectTLS: true, 1241 | cmds: []smtp.Cmd{ 1242 | smtp.HeloCmd{ 1243 | Domain: "some.sender", 1244 | }, 1245 | smtp.StartTlsCmd{}, 1246 | smtp.AuthCmd{ 1247 | Mechanism: "PLAIN", 1248 | InitialResponse: "AHNvbWUtdXNlcm5hbWVAZXhhbXBsZS5jb20AcGFzc3dvcmQxMjM0", 1249 | }, 1250 | smtp.MailCmd{ 1251 | From: getMailWithoutError("some.addres.that.is.not.mine@example.com"), 1252 | }, 1253 | smtp.RcptCmd{ 1254 | To: getMailWithoutError("test@example.com"), 1255 | }, 1256 | // Try again with correct username 1257 | smtp.MailCmd{ 1258 | From: getMailWithoutError("some-username@example.com"), 1259 | }, 1260 | smtp.RcptCmd{ 1261 | To: getMailWithoutError("test@example.com"), 1262 | }, 1263 | smtp.QuitCmd{}, 1264 | }, 1265 | answers: []interface{}{ 1266 | smtp.Answer{ 1267 | Status: smtp.Ready, 1268 | Message: cfg.Hostname + " Service Ready", 1269 | }, 1270 | smtp.Answer{ 1271 | Status: smtp.Ok, 1272 | Message: cfg.Hostname, 1273 | }, 1274 | smtp.Answer{ 1275 | Status: smtp.Ready, 1276 | }, 1277 | smtp.Answer{ 1278 | Status: smtp.AuthenticationSucceeded, 1279 | Message: "2.7.0 Authentication successful", 1280 | }, 1281 | smtp.Answer{ 1282 | Status: smtp.Ok, 1283 | Message: "OK", 1284 | }, 1285 | smtp.Answer{ 1286 | Status: smtp.SMTPErrorPermanentMailboxNameNotAllowed.Status, 1287 | Message: "5.7.1 Sender address rejected: not owned by user some.addres.that.is.not.mine@example.com", 1288 | }, 1289 | smtp.Answer{ 1290 | Status: smtp.Ok, 1291 | Message: "OK", 1292 | }, 1293 | smtp.Answer{ 1294 | Status: smtp.Ok, 1295 | Message: "OK", 1296 | }, 1297 | smtp.Answer{ 1298 | Status: smtp.Closing, 1299 | Message: "Bye!", 1300 | }, 1301 | }, 1302 | } 1303 | 1304 | c.So(proto.GetState().Authenticated, c.ShouldEqual, false) 1305 | 1306 | mta.HandleClient(proto) 1307 | c.So(proto.GetState().Hostname, c.ShouldEqual, "some.sender") 1308 | c.So(proto.GetState().Authenticated, c.ShouldEqual, true) 1309 | }) 1310 | 1311 | } 1312 | -------------------------------------------------------------------------------- /smtp/address.go: -------------------------------------------------------------------------------- 1 | package smtp 2 | 3 | import ( 4 | "errors" 5 | "net/mail" 6 | "strings" 7 | ) 8 | 9 | type MailAddress mail.Address 10 | 11 | // GetLocal gets the local part of a mail address. E.g the part before the @. 12 | func (address *MailAddress) GetLocal() string { 13 | index := strings.LastIndex(address.Address, "@") 14 | local := address.Address[:index] 15 | return local 16 | } 17 | 18 | // GetDomain gets the domain part of a mail address. E.g the part after the @. 19 | func (address *MailAddress) GetDomain() string { 20 | index := strings.LastIndex(address.Address, "@") 21 | domain := address.Address[index+1:] 22 | return domain 23 | } 24 | 25 | // GetAddress gets the full mail address. 26 | func (address *MailAddress) GetAddress() string { 27 | return address.Address 28 | } 29 | 30 | func (address *MailAddress) String() string { 31 | a := mail.Address(*address) 32 | return a.String() 33 | } 34 | 35 | // ParseAddress parses a string into a MailAddress. 36 | func ParseAddress(rawAddress string) (MailAddress, error) { 37 | 38 | /* 39 | RFC 5321 40 | 41 | 4.5.3.1.1. Local-part 42 | 43 | The maximum total length of a user name or other local-part is 64 44 | octets. 45 | 46 | 4.5.3.1.2. Domain 47 | 48 | The maximum total length of a domain name or number is 255 octets. 49 | */ 50 | index := strings.LastIndex(rawAddress, "@") 51 | if index == -1 { 52 | return MailAddress{}, errors.New("expected @ in mail address") 53 | } 54 | rawLocal := rawAddress[:index] 55 | if len(rawLocal) > 64 { 56 | return MailAddress{}, errors.New("length of local part exceeds 64") 57 | } 58 | rawDomain := rawAddress[index+1:] 59 | if len(rawDomain) > 255 { 60 | return MailAddress{}, errors.New("length of domain name part exceeds 255") 61 | } 62 | 63 | // Try to parse the mail address using Go's built-in functions: 64 | address, err := mail.ParseAddress(rawAddress) 65 | if err != nil { 66 | return MailAddress{}, err 67 | } 68 | 69 | return MailAddress(*address), nil 70 | } 71 | -------------------------------------------------------------------------------- /smtp/address_test.go: -------------------------------------------------------------------------------- 1 | package smtp 2 | 3 | import ( 4 | _ "fmt" 5 | . "github.com/smartystreets/goconvey/convey" 6 | "testing" 7 | ) 8 | 9 | func TestParseAddress(t *testing.T) { 10 | 11 | Convey("Testing ParseAddress()", t, func() { 12 | 13 | mails := []struct { 14 | str string 15 | parsed struct { 16 | Local string 17 | Domain string 18 | Address string 19 | } 20 | }{ 21 | { 22 | str: `<"bob"@example.com>`, 23 | parsed: struct { 24 | Local string 25 | Domain string 26 | Address string 27 | }{ 28 | Local: `bob`, 29 | Domain: `example.com`, 30 | Address: `bob@example.com`, 31 | }, 32 | }, 33 | { 34 | str: ` `, 35 | parsed: struct { 36 | Local string 37 | Domain string 38 | Address string 39 | }{ 40 | Local: `bob`, 41 | Domain: `example.com`, 42 | Address: `bob@example.com`, 43 | }, 44 | }, 45 | { 46 | str: `<" "@example.com>`, 47 | parsed: struct { 48 | Local string 49 | Domain string 50 | Address string 51 | }{ 52 | Local: ` `, 53 | Domain: `example.com`, 54 | Address: ` @example.com`, 55 | }, 56 | }, 57 | { 58 | str: `<"test@test2"@example.com>`, 59 | parsed: struct { 60 | Local string 61 | Domain string 62 | Address string 63 | }{ 64 | Local: `test@test2`, 65 | Domain: `example.com`, 66 | Address: `test@test2@example.com`, 67 | }, 68 | }, 69 | } 70 | 71 | for _, mail := range mails { 72 | address, err := ParseAddress(mail.str) 73 | So(err, ShouldEqual, nil) 74 | So(address.GetLocal(), ShouldEqual, mail.parsed.Local) 75 | So(address.GetDomain(), ShouldEqual, mail.parsed.Domain) 76 | So(address.GetAddress(), ShouldEqual, mail.parsed.Address) 77 | 78 | // Cyclic check: 79 | // string -> parsed address -> string -> parsed address 80 | str := address.String() 81 | address, err = ParseAddress(str) 82 | So(err, ShouldEqual, nil) 83 | So(address.GetLocal(), ShouldEqual, mail.parsed.Local) 84 | So(address.GetDomain(), ShouldEqual, mail.parsed.Domain) 85 | So(address.GetAddress(), ShouldEqual, mail.parsed.Address) 86 | } 87 | 88 | Convey("Testing ParseAddress() with invalid mail", func() { 89 | 90 | _, err := ParseAddress("some mail address without at sign") 91 | So(err, ShouldNotEqual, nil) 92 | 93 | }) 94 | 95 | }) 96 | 97 | } 98 | 99 | /* 100 | func TestValidate(t *testing.T) { 101 | Convey("Testing Validate()", t, func() { 102 | 103 | valid_locals := []string{ 104 | "mathias", 105 | "foo,!#", 106 | "!def!xyz%abc", 107 | "$A12345", 108 | //"Fred Bloggs", 109 | "customer/department=shipping", 110 | } 111 | 112 | for _, m := range valid_locals { 113 | m := MailAddress{Local: m, Domain: "example.com"} 114 | valid, _ := m.Validate() 115 | So(valid, ShouldEqual, true) 116 | } 117 | 118 | }) 119 | 120 | } 121 | */ 122 | -------------------------------------------------------------------------------- /smtp/datareader_test.go: -------------------------------------------------------------------------------- 1 | package smtp 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io" 7 | "testing" 8 | ) 9 | 10 | func compare(t *testing.T, data []byte, expected []byte) { 11 | br := bufio.NewReader(bytes.NewReader(data)) 12 | 13 | dataReader := NewDataReader(br) 14 | output, err := io.ReadAll(dataReader) 15 | if !bytes.Equal(output, expected) { 16 | t.Errorf("Expected %v\ngot %v\n", expected, output) 17 | } 18 | if err != nil { 19 | t.Errorf("Did not expect error: %v", err) 20 | } 21 | 22 | } 23 | 24 | func expectError(t *testing.T, data []byte, expected error) { 25 | br := bufio.NewReader(bytes.NewReader(data)) 26 | dataReader := NewDataReader(br) 27 | _, err := io.ReadAll(dataReader) 28 | if err != expected { 29 | t.Errorf("Expected error: %v, got: %v", expected, err) 30 | } 31 | 32 | } 33 | 34 | func TestDataReaderValid(t *testing.T) { 35 | data := []byte("Some test mail\nblablabla\n.\n") 36 | expected := []byte("Some test mail\nblablabla\n") 37 | compare(t, data, expected) 38 | 39 | data = []byte("Some test mail\nblablabla\n.\nshould not read this") 40 | expected = []byte("Some test mail\nblablabla\n") 41 | compare(t, data, expected) 42 | 43 | data = []byte("Some test mail\n..blablabla\n.\n") 44 | expected = []byte("Some test mail\n.blablabla\n") 45 | compare(t, data, expected) 46 | 47 | data = []byte("Some test mail\n.blablabla\n.\n") 48 | expected = []byte("Some test mail\nblablabla\n") 49 | compare(t, data, expected) 50 | 51 | // first line is 1000 chars 52 | data = []byte("aafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddd\n.\n") 53 | expected = []byte("aafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddd\n") 54 | compare(t, data, expected) 55 | 56 | // first line is 1001 chars but starts with a dot, so server should see it as 1000 57 | data = []byte(".aafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsdddddd\n.\n") 58 | expected = []byte("aafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsdddddd\n") 59 | compare(t, data, expected) 60 | 61 | // first line is 1000 chars, second 10, third 1000 62 | data = []byte("aafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddd\naj ge je a t\naafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddd\n.\n") 63 | expected = []byte("aafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddd\naj ge je a t\naafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddd\n") 64 | compare(t, data, expected) 65 | } 66 | 67 | func TestDataReaderInvalid(t *testing.T) { 68 | data := []byte("Some test mail\nblablabla\nno ending dot") 69 | expectError(t, data, ErrIncomplete) 70 | 71 | data = []byte("Some test mail\r\nDot on invalid place\n.test") 72 | expectError(t, data, ErrIncomplete) 73 | 74 | data = []byte("") 75 | expectError(t, data, ErrIncomplete) 76 | } 77 | 78 | func TestDataReaderTooLong(t *testing.T) { 79 | // length === 1001 80 | data := []byte("aafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddd3\n") 81 | expectError(t, data, ErrLtl) 82 | 83 | // first line is small, second is 1003 84 | data = []byte("Some text :)\naafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddddddddfsdaafsddddddd321\n") 85 | expectError(t, data, ErrLtl) 86 | } 87 | -------------------------------------------------------------------------------- /smtp/message.go: -------------------------------------------------------------------------------- 1 | package smtp 2 | 3 | import "net/mail" 4 | import "io" 5 | 6 | type MailMessage mail.Message 7 | 8 | func ReadMessage(r io.Reader) (*MailMessage, error) { 9 | m, err := mail.ReadMessage(r) 10 | if err != nil { 11 | return nil, err 12 | } 13 | msg := MailMessage(*m) 14 | return &msg, err 15 | } 16 | -------------------------------------------------------------------------------- /smtp/parser.go: -------------------------------------------------------------------------------- 1 | package smtp 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/base64" 7 | "errors" 8 | "fmt" 9 | "strings" 10 | ) 11 | 12 | type parser struct { 13 | } 14 | 15 | func (p *parser) ParseCommand(br *bufio.Reader) (command Cmd, err error) { 16 | /* 17 | RFC 5321 2.3.8 18 | 19 | Lines consist of zero or more data characters terminated by the 20 | sequence ASCII character "CR" (hex value 0D) followed immediately by 21 | ASCII character "LF" (hex value 0A). This termination sequence is 22 | denoted as in this document. Conforming implementations MUST 23 | NOT recognize or generate any other character or character sequence 24 | as a line terminator. Limits MAY be imposed on line lengths by 25 | servers (see Section 4). 26 | */ 27 | 28 | var address *MailAddress 29 | verb, args, err := parseLine(br) 30 | if err != nil { 31 | return nil, err 32 | } 33 | //conn.write(500, err.Error()) 34 | //conn.c.Close() 35 | 36 | switch verb { 37 | 38 | case "HELO": 39 | { 40 | if len(args) != 1 { 41 | command = InvalidCmd{Cmd: "HELO", Info: "HELO requires exactly one valid domain"} 42 | break 43 | } 44 | domain := "" 45 | for _, arg := range args { 46 | domain = arg.Key 47 | } 48 | command = HeloCmd{Domain: domain} 49 | } 50 | 51 | case "EHLO": 52 | { 53 | if len(args) != 1 { 54 | command = InvalidCmd{Cmd: "EHLO", Info: "EHLO requires exactly one valid address"} 55 | break 56 | } 57 | domain := "" 58 | for _, arg := range args { 59 | domain = arg.Key 60 | } 61 | command = EhloCmd{Domain: domain} 62 | } 63 | 64 | case "MAIL": 65 | { 66 | fromArg := args["FROM"] 67 | address, err = parseFROM(fromArg.Key + fromArg.Operator + fromArg.Value) 68 | if err != nil { 69 | command = InvalidCmd{Cmd: verb, Info: err.Error()} 70 | err = nil 71 | break 72 | } 73 | 74 | eightBitMIME := false 75 | bodyArg, ok := args["BODY"] 76 | if ok { 77 | bodyArg.Value = strings.ToUpper(bodyArg.Value) 78 | if bodyArg.Operator != "=" || (bodyArg.Value != "8BITMIME" && bodyArg.Value != "7BIT") { 79 | command = InvalidCmd{Cmd: verb, Info: "Syntax is BODY=8BITMIME|7BIT"} 80 | break 81 | } 82 | 83 | if bodyArg.Value == "8BITMIME" { 84 | eightBitMIME = true 85 | } 86 | } 87 | 88 | command = MailCmd{From: address, EightBitMIME: eightBitMIME} 89 | } 90 | 91 | case "RCPT": 92 | { 93 | toArg := args["TO"] 94 | address, err = parseTO(toArg.Key + toArg.Operator + toArg.Value) 95 | if err != nil { 96 | command = InvalidCmd{Cmd: verb, Info: err.Error()} 97 | err = nil 98 | } else { 99 | command = RcptCmd{To: address} 100 | } 101 | } 102 | 103 | case "DATA": 104 | { 105 | // TODO: write tests for this 106 | command = DataCmd{ 107 | R: *NewDataReader(br), 108 | } 109 | } 110 | 111 | case "RSET": 112 | { 113 | command = RsetCmd{} 114 | } 115 | 116 | case "SEND": 117 | { 118 | command = SendCmd{} 119 | } 120 | 121 | case "SOML": 122 | { 123 | command = SomlCmd{} 124 | } 125 | 126 | case "SAML": 127 | { 128 | command = SamlCmd{} 129 | } 130 | 131 | case "VRFY": 132 | { 133 | //conn.write(502, "Command not implemented") 134 | /* 135 | RFC 821 136 | SMTP provides as additional features, commands to verify a user 137 | name or expand a mailing list. This is done with the VRFY and 138 | EXPN commands 139 | RFC 5321 140 | As discussed in Section 3.5, individual sites may want to disable 141 | either or both of VRFY or EXPN for security reasons (see below). As 142 | a corollary to the above, implementations that permit this MUST NOT 143 | appear to have verified addresses that are not, in fact, verified. 144 | If a site disables these commands for security reasons, the SMTP 145 | server MUST return a 252 response, rather than a code that could be 146 | confused with successful or unsuccessful verification. 147 | Returning a 250 reply code with the address listed in the VRFY 148 | command after having checked it only for syntax violates this rule. 149 | Of course, an implementation that "supports" VRFY by always returning 150 | 550 whether or not the address is valid is equally not in 151 | conformance. 152 | From what I have read, 502 is better than 252... 153 | */ 154 | user := "" 155 | for _, arg := range args { 156 | user = arg.Key 157 | } 158 | command = VrfyCmd{Param: user} 159 | } 160 | 161 | case "EXPN": 162 | { 163 | listName := "" 164 | for _, arg := range args { 165 | listName = arg.Key 166 | } 167 | command = ExpnCmd{ListName: listName} 168 | } 169 | 170 | case "NOOP": 171 | { 172 | command = NoopCmd{} 173 | } 174 | 175 | case "QUIT": 176 | { 177 | command = QuitCmd{} 178 | } 179 | 180 | case "STARTTLS": 181 | { 182 | command = StartTlsCmd{} 183 | } 184 | case "AUTH": 185 | { 186 | mechanism := "" 187 | initialResponse := "" 188 | // TODO: make this better 189 | count := 0 190 | for _, arg := range args { 191 | if count == 0 { 192 | mechanism = arg.Key 193 | } 194 | if count == 1 { 195 | initialResponse = arg.Key + arg.Operator + arg.Value 196 | } 197 | count++ 198 | 199 | } 200 | command = AuthCmd{ 201 | Mechanism: mechanism, 202 | InitialResponse: initialResponse, 203 | R: *br, 204 | } 205 | } 206 | 207 | default: 208 | { 209 | // TODO: CLEAN THIS UP 210 | command = UnknownCmd{Cmd: verb, Line: strings.TrimSuffix(verb, "\n")} 211 | } 212 | 213 | } 214 | 215 | return 216 | } 217 | 218 | type Argument struct { 219 | Key string 220 | Value string 221 | Operator string 222 | } 223 | 224 | // parseLine returns the verb of the line and a list of all comma separated arguments 225 | func parseLine(br *bufio.Reader) (string, map[string]Argument, error) { 226 | /* 227 | RFC 5321 228 | 4.5.3.1.4. Command Line 229 | 230 | The maximum total length of a command line including the command word 231 | and the is 512 octets. SMTP extensions may be used to 232 | increase this limit. 233 | */ 234 | buffer, err := ReadUntill('\n', MAX_CMD_LINE, br) 235 | if err != nil { 236 | if err == ErrLtl { 237 | _ = SkipTillNewline(br) 238 | return string(buffer), map[string]Argument{}, err 239 | } 240 | 241 | return string(buffer), map[string]Argument{}, err 242 | } 243 | line := string(buffer) 244 | verb := "" 245 | argMap := map[string]Argument{} 246 | 247 | // Strip \n and \r 248 | line = strings.TrimSuffix(line, "\n") 249 | line = strings.TrimSuffix(line, "\r") 250 | 251 | i := strings.Index(line, " ") 252 | if i == -1 { 253 | verb = strings.ToUpper(line) 254 | return verb, map[string]Argument{}, nil 255 | } 256 | 257 | verb = strings.ToUpper(line[:i]) 258 | line = line[i+1:] 259 | 260 | tmpArgs := strings.Split(line, " ") 261 | for _, arg := range tmpArgs { 262 | argument := Argument{} 263 | i = strings.IndexAny(arg, ":=") 264 | if i == -1 { 265 | argument.Key = strings.TrimSpace(arg) 266 | } else { 267 | argument.Key = strings.TrimSpace(arg[:i]) 268 | argument.Value = strings.TrimSpace(arg[i+1:]) 269 | argument.Operator = arg[i : i+1] 270 | } 271 | 272 | if len(argument.Key) == 0 { 273 | continue 274 | } 275 | 276 | // Put key in the arguments map in uppercase to make sure 277 | // we are case insensitive 278 | argMap[strings.ToUpper(argument.Key)] = argument 279 | } 280 | 281 | return verb, argMap, nil 282 | } 283 | 284 | func parseFROM(from string) (*MailAddress, error) { 285 | index := strings.Index(from, ":") 286 | if index == -1 { 287 | return nil, errors.New("no FROM given (didn't find ':')") 288 | } 289 | if strings.ToLower(from[0:index]) != "from" { 290 | return nil, errors.New("no FROM given") 291 | } 292 | 293 | address_str := from[index+1:] 294 | 295 | address, err := ParseAddress(address_str) 296 | if err != nil { 297 | return nil, err 298 | } 299 | return &address, nil 300 | } 301 | 302 | func parseTO(to string) (*MailAddress, error) { 303 | index := strings.Index(to, ":") 304 | if index == -1 { 305 | return nil, errors.New("no TO given (didn't find ':')") 306 | } 307 | if strings.ToLower(to[0:index]) != "to" { 308 | return nil, errors.New("no TO given") 309 | } 310 | 311 | address_str := to[index+1:] 312 | 313 | address, err := ParseAddress(address_str) 314 | if err != nil { 315 | return nil, err 316 | } 317 | return &address, nil 318 | } 319 | 320 | // ParseAuthPlainInitialRespone parses the base64 encoded initial response of an Auth PLAIN request 321 | // 322 | // "The mechanism consists of a single message from the client to the server. The 323 | // client sends the authorization identity (identity to login as), followed by a 324 | // US-ASCII NulL character, followed by the authentication identity (identity whose 325 | // password will be used), followed by a US-ASCII NulL character, followed by the 326 | // clear-text password. The client may leave the authorization identity empty to indicate 327 | // that it is the same as the authentication identity." 328 | func ParseAuthPlainInitialRespone(initialResponse string) (authorizationIdentity string, authenticationIdenity string, password string, err error) { 329 | initialResponseByte, err := base64.StdEncoding.DecodeString(initialResponse) 330 | if err != nil { 331 | err = fmt.Errorf("couldn't decode base64 %v", err) 332 | return 333 | } 334 | initialResponseByteSplit := bytes.Split(initialResponseByte, []byte("\x00")) 335 | if len(initialResponseByteSplit) != 3 { 336 | err = fmt.Errorf("couldn't parse initial response: expected exactly 3 arguments") 337 | return 338 | } 339 | 340 | authorizationIdentity = string(initialResponseByteSplit[0]) 341 | authenticationIdenity = string(initialResponseByteSplit[1]) 342 | password = string(initialResponseByteSplit[2]) 343 | return 344 | } 345 | -------------------------------------------------------------------------------- /smtp/parser_test.go: -------------------------------------------------------------------------------- 1 | package smtp 2 | 3 | import ( 4 | "bufio" 5 | _ "fmt" 6 | "strings" 7 | "testing" 8 | 9 | . "github.com/smartystreets/goconvey/convey" 10 | ) 11 | 12 | func TestParser(t *testing.T) { 13 | 14 | Convey("Testing parser", t, func() { 15 | commands := "" 16 | commands += "HELO relay.example.org\r\n" 17 | commands += "HeLo relay.example.org\r\n" 18 | commands += "helo relay.example.org\r\n" 19 | commands += "helO relay.example.org\r\n" 20 | commands += "EHLO other.example.org\r\n" 21 | commands += "MAIL FROM:\r\n" 22 | commands += "MAIL FROM:\r\n" 23 | commands += "mail FROM:\r\n" 24 | commands += "MAIL FROM: body=8BITMIME\r\n" 25 | commands += "MAIL FROM: BODY=8bitmime\r\n" 26 | commands += "MAIL FROM: BODY=7bit\r\n" 27 | commands += "RCPT TO:\r\n" 28 | commands += "RCPT TO:\r\n" 29 | commands += "RCPT to:\r\n" 30 | commands += "rcpt to:\r\n" 31 | commands += "SEND\r\n" 32 | commands += "SOML\r\n" 33 | commands += "SAML\r\n" 34 | commands += "RSET\r\n" 35 | commands += "VRFY jones\r\n" 36 | commands += "EXPN staff\r\n" 37 | commands += "NOOP\r\n" 38 | commands += "QUIT\r\n" 39 | // commands += "AUTH PLAIN\r\n" 40 | // commands += "AUTH PLAIN dGVzdAB0ZXN0ADEyMzQ=\r\n" 41 | 42 | br := bufio.NewReader(strings.NewReader(commands)) 43 | 44 | p := parser{} 45 | 46 | expectedCommands := []Cmd{ 47 | HeloCmd{Domain: "relay.example.org"}, 48 | HeloCmd{Domain: "relay.example.org"}, 49 | HeloCmd{Domain: "relay.example.org"}, 50 | HeloCmd{Domain: "relay.example.org"}, 51 | EhloCmd{Domain: "other.example.org"}, 52 | MailCmd{From: &MailAddress{Address: "bob@example.org"}}, 53 | MailCmd{From: &MailAddress{Address: "BOB@example.org"}}, 54 | MailCmd{From: &MailAddress{Address: "bob@example.org"}}, 55 | MailCmd{From: &MailAddress{Address: "bob@example.org"}, EightBitMIME: true}, 56 | MailCmd{From: &MailAddress{Address: "bob@example.org"}, EightBitMIME: true}, 57 | MailCmd{From: &MailAddress{Address: "bob@example.org"}}, 58 | RcptCmd{To: &MailAddress{Address: "alice@example.com"}}, 59 | RcptCmd{To: &MailAddress{Address: "theboss@example.com"}}, 60 | RcptCmd{To: &MailAddress{Address: "theboss@example.com"}}, 61 | RcptCmd{To: &MailAddress{Address: "Theboss@example.com"}}, 62 | SendCmd{}, 63 | SomlCmd{}, 64 | SamlCmd{}, 65 | RsetCmd{}, 66 | VrfyCmd{Param: "jones"}, 67 | ExpnCmd{ListName: "staff"}, 68 | NoopCmd{}, 69 | QuitCmd{}, 70 | // AuthCmd{Mechanism: "PLAIN"}, 71 | // AuthCmd{Mechanism: "PLAIN", InitialResponse: "dGVzdAB0ZXN0ADEyMzQ="}, 72 | } 73 | 74 | for _, expectedCommand := range expectedCommands { 75 | command, err := p.ParseCommand(br) 76 | So(err, ShouldEqual, nil) 77 | So(command, ShouldResemble, expectedCommand) 78 | } 79 | 80 | }) 81 | 82 | Convey("Testing parser DATA cmd", t, func() { 83 | commands := "" 84 | commands += "DATA\r\n" 85 | commands += "quit\r\n" 86 | 87 | br := bufio.NewReader(strings.NewReader(commands)) 88 | p := parser{} 89 | 90 | command, err := p.ParseCommand(br) 91 | So(err, ShouldEqual, nil) 92 | So(command, ShouldHaveSameTypeAs, DataCmd{}) 93 | 94 | command, err = p.ParseCommand(br) 95 | So(err, ShouldEqual, nil) 96 | So(command, ShouldHaveSameTypeAs, QuitCmd{}) 97 | 98 | }) 99 | 100 | Convey("Testing parser with invalid commands", t, func() { 101 | 102 | commands := "" 103 | commands += "RCPT\r\n" 104 | commands += "helo\r\n" 105 | commands += "ehlo\r\n" 106 | commands += "\r\n" 107 | commands += " \r\n" 108 | commands += "RCPT TO:some invalid email\r\n" 109 | commands += "rcpt :valid@mail.be\r\n" 110 | commands += "RCPT :valid@mail.be\r\n" 111 | commands += "RCPT TA:valid@mail.be\r\n" 112 | commands += "MAIL\r\n" 113 | commands += "MAIL from:some invalid email\r\n" 114 | commands += "MAIL :valid@mail.be\r\n" 115 | commands += "MAIL FROA:valid@mail.be\r\n" 116 | commands += "MAIL To some@invalid\r\n" 117 | commands += "MAIL FROM:some@valid.be BODY:8bitmime\r\n" 118 | commands += "UNKN some unknown command\r\n" 119 | 120 | br := bufio.NewReader(strings.NewReader(commands)) 121 | 122 | p := parser{} 123 | 124 | expectedCommands := []Cmd{ 125 | InvalidCmd{}, 126 | InvalidCmd{}, 127 | InvalidCmd{}, 128 | UnknownCmd{}, 129 | UnknownCmd{}, 130 | InvalidCmd{}, 131 | InvalidCmd{}, 132 | InvalidCmd{}, 133 | InvalidCmd{}, 134 | InvalidCmd{}, 135 | InvalidCmd{}, 136 | InvalidCmd{}, 137 | InvalidCmd{}, 138 | InvalidCmd{}, 139 | InvalidCmd{}, 140 | UnknownCmd{}, 141 | } 142 | 143 | for _, expectedCommand := range expectedCommands { 144 | command, err := p.ParseCommand(br) 145 | So(err, ShouldEqual, nil) 146 | So(command, ShouldHaveSameTypeAs, expectedCommand) 147 | } 148 | 149 | }) 150 | 151 | Convey("Testing parseLine()", t, func() { 152 | 153 | tests := []struct { 154 | line string 155 | verb string 156 | args map[string]Argument 157 | }{ 158 | { 159 | line: "HELO\r\n", 160 | verb: "HELO", 161 | args: map[string]Argument{}, 162 | }, 163 | { 164 | line: "HELO relay.example.org\r\n", 165 | verb: "HELO", 166 | args: map[string]Argument{"RELAY.EXAMPLE.ORG": Argument{Key: "relay.example.org"}}, 167 | }, 168 | { 169 | line: "MAIL FROM:\r\n", 170 | verb: "MAIL", 171 | args: map[string]Argument{"FROM": Argument{Key: "FROM", Value: "", Operator: ":"}}, 172 | }, 173 | { 174 | line: "HELO some_ctrl_char\r\n", 175 | verb: "HELO", 176 | args: map[string]Argument{"SOME_CTRL_CHAR": Argument{Key: "some_ctrl_char"}}, 177 | }, 178 | { 179 | line: "HELO some_ctrl_char\n", 180 | verb: "HELO", 181 | args: map[string]Argument{"SOME_CTRL_CHAR": Argument{Key: "some_ctrl_char"}}, 182 | }, 183 | { 184 | line: "AUTH PLAIN\r\n", 185 | verb: "AUTH", 186 | args: map[string]Argument{"PLAIN": {Key: "PLAIN"}}, 187 | }, 188 | { 189 | line: "AUTH PLAIN dGVzdAB0ZXN0ADEyMzQ=\r\n", 190 | verb: "AUTH", 191 | args: map[string]Argument{"PLAIN": {Key: "PLAIN"}, "DGVZDAB0ZXN0ADEYMZQ": {Key: "dGVzdAB0ZXN0ADEyMzQ", Operator: "="}}, 192 | }, 193 | { 194 | line: "SOME_verb a b c test1=value1 test2:value2\n", 195 | verb: "SOME_VERB", 196 | args: map[string]Argument{ 197 | "A\tB": Argument{Key: "a\tb"}, 198 | "C": Argument{Key: "c"}, 199 | "TEST1": Argument{Key: "test1", Value: "value1", Operator: "="}, 200 | "TEST2": Argument{Key: "test2", Value: "value2", Operator: ":"}, 201 | }, 202 | }, 203 | } 204 | 205 | for _, test := range tests { 206 | br := bufio.NewReader(strings.NewReader(test.line)) 207 | verb, args, err := parseLine(br) 208 | So(err, ShouldEqual, nil) 209 | So(verb, ShouldEqual, test.verb) 210 | So(args, ShouldResemble, test.args) 211 | } 212 | 213 | }) 214 | 215 | Convey("Testing parseTo()", t, func() { 216 | 217 | tests := []struct { 218 | line string 219 | addressString string 220 | }{ 221 | { 222 | line: "RCPT TO:\r\n", 223 | addressString: "alice@example.com", 224 | }, 225 | } 226 | 227 | for _, test := range tests { 228 | br := bufio.NewReader(strings.NewReader(test.line)) 229 | _, args, err := parseLine(br) 230 | So(err, ShouldEqual, nil) 231 | 232 | toArg := args["TO"] 233 | addr, err := parseTO(toArg.Key + toArg.Operator + toArg.Value) 234 | So(err, ShouldEqual, nil) 235 | So(addr.GetAddress(), ShouldEqual, test.addressString) 236 | } 237 | 238 | }) 239 | 240 | Convey("Testing parseFROM()", t, func() { 241 | 242 | tests := []struct { 243 | line string 244 | addressString string 245 | }{ 246 | { 247 | line: "MAIL from:\r\n", 248 | addressString: "alice@example.com", 249 | }, 250 | } 251 | 252 | for _, test := range tests { 253 | br := bufio.NewReader(strings.NewReader(test.line)) 254 | _, args, err := parseLine(br) 255 | So(err, ShouldEqual, nil) 256 | 257 | fromArg := args["FROM"] 258 | addr, err := parseFROM(fromArg.Key + fromArg.Operator + fromArg.Value) 259 | So(err, ShouldEqual, nil) 260 | So(addr.GetAddress(), ShouldEqual, test.addressString) 261 | } 262 | 263 | }) 264 | 265 | Convey("Testing parseAuthPlainInitialRespone()", t, func() { 266 | 267 | tests := []struct { 268 | initialResponse string 269 | authenticationIdenity string 270 | authorizationIdentity string 271 | password string 272 | }{ 273 | { 274 | initialResponse: "dGVzdAB0ZXN0ADEyMzQ=", 275 | authenticationIdenity: "test", 276 | authorizationIdentity: "test", 277 | password: "1234", 278 | }, 279 | { 280 | initialResponse: "dGVzdAB0ZXN0AHRlc3RwYXNz", 281 | authenticationIdenity: "test", 282 | authorizationIdentity: "test", 283 | password: "testpass", 284 | }, 285 | { 286 | initialResponse: "YXV0aHoAYXV0aG4AcGFzcw==", 287 | authenticationIdenity: "authn", 288 | authorizationIdentity: "authz", 289 | password: "pass", 290 | }, 291 | { 292 | initialResponse: "AGF1dGhuAHBhc3M=", 293 | authenticationIdenity: "authn", 294 | authorizationIdentity: "", // empty authorization identity 295 | password: "pass", 296 | }, 297 | } 298 | 299 | for _, test := range tests { 300 | authorizationIdentity, authenticationIdenity, password, err := ParseAuthPlainInitialRespone(test.initialResponse) 301 | So(err, ShouldEqual, nil) 302 | So(authenticationIdenity, ShouldEqual, test.authenticationIdenity) 303 | So(authorizationIdentity, ShouldEqual, test.authorizationIdentity) 304 | So(password, ShouldEqual, test.password) 305 | } 306 | 307 | _, _, _, err := ParseAuthPlainInitialRespone("test") 308 | So(err, ShouldBeError) 309 | 310 | _, _, _, err = ParseAuthPlainInitialRespone("YXV0aHoAYXV0aG4=") // "authz\0authn" 311 | So(err, ShouldBeError) 312 | }) 313 | } 314 | -------------------------------------------------------------------------------- /smtp/protocol.go: -------------------------------------------------------------------------------- 1 | package smtp 2 | 3 | import ( 4 | "bufio" 5 | "crypto/tls" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net" 10 | "strconv" 11 | 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | type StatusCode uint32 16 | 17 | // SMTP status codes 18 | const ( 19 | Ready StatusCode = 220 20 | Closing StatusCode = 221 21 | Ok StatusCode = 250 22 | StartData StatusCode = 354 23 | ShuttingDown StatusCode = 421 24 | SyntaxError StatusCode = 500 25 | SyntaxErrorParam StatusCode = 501 26 | NotImplemented StatusCode = 502 27 | BadSequence StatusCode = 503 28 | AbortMail StatusCode = 552 29 | NoValidRecipients StatusCode = 554 30 | ) 31 | 32 | // SMTP status codes for AUTH extension (RFC 4954) 33 | const ( 34 | AuthenticationSucceeded StatusCode = 235 35 | EncodedString StatusCode = 334 36 | PasswordTransitionNeeded StatusCode = 432 37 | TemporaryAuthenticationFailure StatusCode = 454 38 | AuthenticationExchangeLineTooLong StatusCode = 500 39 | MalformedAuthInput StatusCode = 501 40 | AuthCommandNotPermittedDuringMailTransaction StatusCode = 503 41 | UnrecognizedAuthenticationType StatusCode = 504 42 | AuthenticationRequired StatusCode = 530 43 | AuthenticationMechanismTooWeak StatusCode = 534 44 | AuthenticationCredentialsInvalid StatusCode = 535 45 | EncryptionRequiredForRequestedAuthenticationMechanism StatusCode = 538 46 | ) 47 | 48 | // ErrLtl Line too long error 49 | var ErrLtl = errors.New("line too long") 50 | 51 | // ErrIncomplete Incomplete data error 52 | var ErrIncomplete = errors.New("incomplete data") 53 | 54 | const ( 55 | MAX_DATA_LINE = 1000 56 | MAX_CMD_LINE = 512 57 | ) 58 | 59 | // ReadUntill reads untill delim is found or max bytes are read. 60 | // If delim was found it returns nil as error. If delim wasn't found after max bytes, 61 | // it returns ErrLtl. 62 | func ReadUntill(delim byte, max int, r io.Reader) ([]byte, error) { 63 | buffer := make([]byte, max) 64 | 65 | n := 0 66 | for n < max { 67 | read, err := r.Read(buffer[n : n+1]) 68 | if read == 0 || err != nil { 69 | return buffer[0:n], err 70 | } 71 | 72 | if read > 1 { 73 | panic("Should read 1 byte at a time.") 74 | } 75 | 76 | if buffer[n] == delim { 77 | return buffer[0 : n+1], nil 78 | } 79 | 80 | n++ 81 | 82 | } 83 | 84 | return buffer[0:n], ErrLtl 85 | } 86 | 87 | // SkipTillNewline removes all data untill a newline is found. 88 | func SkipTillNewline(r io.Reader) error { 89 | var err error 90 | for { 91 | _, err = ReadUntill('\n', 1000, r) 92 | if err != nil { 93 | if err == ErrLtl { 94 | continue 95 | } 96 | 97 | break 98 | } 99 | 100 | break 101 | } 102 | 103 | return err 104 | } 105 | 106 | // DataReader implements the reader that will read the data from a MAIL cmd 107 | type DataReader struct { 108 | br *bufio.Reader 109 | state int 110 | bytesInLine int 111 | } 112 | 113 | func NewDataReader(br *bufio.Reader) *DataReader { 114 | dr := &DataReader{ 115 | br: br, 116 | } 117 | 118 | return dr 119 | } 120 | 121 | // Implementation from textproto.DotReader.Read 122 | func (r *DataReader) Read(b []byte) (n int, err error) { 123 | // Run data through a simple state machine to 124 | // elide leading dots, rewrite trailing \r\n into \n, 125 | // and detect ending .\r\n line. 126 | const ( 127 | stateBeginLine = iota // beginning of line; initial state; must be zero 128 | stateDot // read . at beginning of line 129 | stateDotCR // read .\r at beginning of line 130 | stateCR // read \r (possibly at end of line) 131 | stateData // reading data in middle of line 132 | stateEOF // reached .\r\n end marker line 133 | ) 134 | 135 | br := r.br 136 | for n < len(b) && r.state != stateEOF { 137 | var c byte 138 | c, err = br.ReadByte() 139 | if err != nil { 140 | err = ErrIncomplete 141 | break 142 | } 143 | r.bytesInLine++ 144 | if r.bytesInLine > MAX_DATA_LINE { 145 | err = ErrLtl 146 | _ = SkipTillNewline(br) 147 | r.bytesInLine = 0 148 | r.state = stateBeginLine 149 | break 150 | } 151 | switch r.state { 152 | case stateBeginLine: 153 | if c == '.' { 154 | r.state = stateDot 155 | continue 156 | } 157 | if c == '\r' { 158 | r.state = stateCR 159 | continue 160 | } 161 | r.state = stateData 162 | 163 | case stateDot: 164 | if c == '\r' { 165 | r.state = stateDotCR 166 | continue 167 | } 168 | if c == '\n' { 169 | r.state = stateEOF 170 | continue 171 | } 172 | r.state = stateData 173 | 174 | case stateDotCR: 175 | if c == '\n' { 176 | r.state = stateEOF 177 | continue 178 | } 179 | // Not part of .\r\n. 180 | // Consume leading dot and emit saved \r. 181 | err := br.UnreadByte() 182 | if err != nil { 183 | return n, fmt.Errorf("couldn't unread byte: %w", err) 184 | } 185 | c = '\r' 186 | r.state = stateData 187 | 188 | case stateCR: 189 | if c == '\n' { 190 | r.state = stateBeginLine 191 | r.bytesInLine = 0 192 | break 193 | } 194 | // Not part of \r\n. Emit saved \r 195 | err := br.UnreadByte() 196 | if err != nil { 197 | return n, fmt.Errorf("couldn't unread byte: %w", err) 198 | } 199 | c = '\r' 200 | r.state = stateData 201 | 202 | case stateData: 203 | if c == '\r' { 204 | r.state = stateCR 205 | continue 206 | } 207 | if c == '\n' { 208 | r.state = stateBeginLine 209 | r.bytesInLine = 0 210 | } 211 | } 212 | b[n] = c 213 | n++ 214 | } 215 | 216 | if err == nil && r.state == stateEOF { 217 | err = io.EOF 218 | } 219 | 220 | return 221 | } 222 | 223 | // Cmd All SMTP answers/commands should implement this interface. 224 | type Cmd interface { 225 | fmt.Stringer 226 | } 227 | 228 | // Answer A raw SMTP answer. Used to send a status code + message. 229 | type Answer struct { 230 | Status StatusCode 231 | Message string 232 | } 233 | 234 | func (c Answer) String() string { 235 | return fmt.Sprintf("%d %s", c.Status, c.Message) 236 | } 237 | 238 | // MultiAnswer A multiline answer. 239 | type MultiAnswer struct { 240 | Status StatusCode 241 | Messages []string 242 | } 243 | 244 | func (c MultiAnswer) String() string { 245 | if len(c.Messages) == 0 { 246 | return fmt.Sprintf("%d", c.Status) 247 | } 248 | 249 | result := "" 250 | for i := 0; i < len(c.Messages)-1; i++ { 251 | result += fmt.Sprintf("%d-%s", c.Status, c.Messages[i]) 252 | result += "\r\n" 253 | } 254 | 255 | result += fmt.Sprintf("%d %s", c.Status, c.Messages[len(c.Messages)-1]) 256 | 257 | return result 258 | } 259 | 260 | // InvalidCmd is a known command with invalid arguments or syntax 261 | type InvalidCmd struct { 262 | // The command 263 | Cmd string 264 | Info string 265 | } 266 | 267 | func (c InvalidCmd) String() string { 268 | return fmt.Sprintf("%s %s", c.Cmd, c.Info) 269 | } 270 | 271 | // UnknownCmd is a command that is none of the other commands. i.e. not implemented 272 | type UnknownCmd struct { 273 | // The command 274 | Cmd string 275 | Line string 276 | } 277 | 278 | func (c UnknownCmd) String() string { 279 | return c.Cmd 280 | } 281 | 282 | type HeloCmd struct { 283 | Domain string 284 | } 285 | 286 | func (c HeloCmd) String() string { 287 | return "" 288 | } 289 | 290 | type EhloCmd struct { 291 | Domain string 292 | } 293 | 294 | func (c EhloCmd) String() string { 295 | return "" 296 | } 297 | 298 | type QuitCmd struct { 299 | } 300 | 301 | func (c QuitCmd) String() string { 302 | return "" 303 | } 304 | 305 | type MailCmd struct { 306 | From *MailAddress 307 | EightBitMIME bool 308 | } 309 | 310 | func (c MailCmd) String() string { 311 | return "" 312 | } 313 | 314 | type RcptCmd struct { 315 | To *MailAddress 316 | } 317 | 318 | func (c RcptCmd) String() string { 319 | return "" 320 | } 321 | 322 | type DataCmd struct { 323 | Data []byte 324 | R DataReader 325 | } 326 | 327 | func (c DataCmd) String() string { 328 | return "" 329 | } 330 | 331 | type RsetCmd struct { 332 | } 333 | 334 | func (c RsetCmd) String() string { 335 | return "" 336 | } 337 | 338 | type StartTlsCmd struct { 339 | } 340 | 341 | func (c StartTlsCmd) String() string { 342 | return "" 343 | } 344 | 345 | type NoopCmd struct{} 346 | 347 | func (c NoopCmd) String() string { 348 | return "" 349 | } 350 | 351 | // Not implemented because of security concerns 352 | type VrfyCmd struct { 353 | Param string 354 | } 355 | 356 | func (c VrfyCmd) String() string { 357 | return "" 358 | } 359 | 360 | type ExpnCmd struct { 361 | ListName string 362 | } 363 | 364 | func (c ExpnCmd) String() string { 365 | return "" 366 | } 367 | 368 | type SendCmd struct{} 369 | 370 | func (c SendCmd) String() string { 371 | return "" 372 | } 373 | 374 | type SomlCmd struct{} 375 | 376 | func (c SomlCmd) String() string { 377 | return "" 378 | } 379 | 380 | type SamlCmd struct{} 381 | 382 | func (c SamlCmd) String() string { 383 | return "" 384 | } 385 | 386 | type AuthCmd struct { 387 | Mechanism string 388 | InitialResponse string 389 | R bufio.Reader 390 | } 391 | 392 | func (c AuthCmd) String() string { 393 | return fmt.Sprintf("AUTH %s", c.Mechanism) 394 | } 395 | 396 | type Id struct { 397 | Timestamp int64 398 | Counter uint32 399 | } 400 | 401 | func (id *Id) String() string { 402 | return strconv.FormatInt(id.Timestamp, 16) + strconv.FormatInt(int64(id.Counter), 16) 403 | } 404 | 405 | // Protocol Used as communication layer so we can easily switch between a real socket 406 | // and a test implementation. 407 | type Protocol interface { 408 | // Send a SMTP command. 409 | Send(Cmd) 410 | // Receive a command(will block while waiting for it). 411 | // Returns an error if something wen't wrong. E.g line was too long. 412 | GetCmd() (*Cmd, error) 413 | // Close the connection. 414 | Close() 415 | // StartTls starts the tls handshake. 416 | StartTls(*tls.Config) error 417 | // GetIP gets the ip of the client. 418 | GetIP() net.IP 419 | // Get the state that belongs to this connection. 420 | GetState() *State 421 | } 422 | 423 | type MtaProtocol struct { 424 | c net.Conn 425 | br *bufio.Reader 426 | parser parser 427 | state *State 428 | } 429 | 430 | // NewMtaProtocol Creates a protocol that works over a socket. 431 | // the net.Conn parameter will be closed when done. 432 | func NewMtaProtocol(c net.Conn) *MtaProtocol { 433 | proto := &MtaProtocol{ 434 | c: c, 435 | br: bufio.NewReader(c), 436 | parser: parser{}, 437 | state: &State{}, 438 | } 439 | 440 | return proto 441 | } 442 | 443 | func (p *MtaProtocol) Send(c Cmd) { 444 | log.WithFields(log.Fields{ 445 | "Cmd": fmt.Sprintf("%#v", c), 446 | "SessionId": p.state.SessionId.String(), 447 | "Ip": p.state.Ip.String(), 448 | }).Debug("Sending cmd") 449 | fmt.Fprintf(p.c, "%s\r\n", c) 450 | } 451 | 452 | func (p *MtaProtocol) GetCmd() (*Cmd, error) { 453 | cmd, err := p.parser.ParseCommand(p.br) 454 | if err != nil { 455 | log.WithFields(log.Fields{ 456 | "err": err, 457 | }).Debug("MtaProtocol.GetCmd could not parse command") 458 | return nil, err 459 | } 460 | 461 | log.WithFields(log.Fields{ 462 | "Cmd": fmt.Sprintf("%#v", cmd), 463 | "SessionId": p.state.SessionId.String(), 464 | "Ip": p.state.Ip.String(), 465 | }).Debug("Received cmd") 466 | return &cmd, nil 467 | } 468 | 469 | func (p *MtaProtocol) Close() { 470 | err := p.c.Close() 471 | if err != nil { 472 | log.Printf("Error while closing protocol: %v", err) 473 | } 474 | } 475 | 476 | func (p *MtaProtocol) StartTls(c *tls.Config) error { 477 | tlsCon := tls.Server(p.c, c) 478 | err := tlsCon.Handshake() 479 | if err != nil { 480 | return err 481 | } 482 | 483 | p.c = tlsCon 484 | p.br.Reset(p.c) 485 | return nil 486 | } 487 | 488 | func (p *MtaProtocol) GetIP() net.IP { 489 | ip, _, err := net.SplitHostPort(p.c.RemoteAddr().String()) 490 | if err != nil { 491 | log.Printf("Could not get ip: %v", p.c.RemoteAddr().String()) 492 | return nil 493 | } 494 | 495 | return net.ParseIP(ip) 496 | } 497 | 498 | func (p *MtaProtocol) GetState() *State { 499 | return p.state 500 | } 501 | -------------------------------------------------------------------------------- /smtp/smtp_error.go: -------------------------------------------------------------------------------- 1 | package smtp 2 | 3 | import "fmt" 4 | 5 | // SMTPError describes an SMTP error with a Status and a Message 6 | // list of SMTP errors: https://datatracker.ietf.org/doc/html/rfc5321#section-4.2.3 7 | type SMTPError Answer 8 | 9 | func (err SMTPError) Error() string { 10 | return fmt.Sprintf("smtp error %d %q", err.Status, err.Message) 11 | } 12 | 13 | var ( 14 | // 4yz Transient Negative Completion reply 15 | // The command was not accepted, and the requested action did not 16 | // occur. However, the error condition is temporary, and the action 17 | // may be requested again. The sender should return to the beginning 18 | // of the command sequence (if any). It is difficult to assign a 19 | // meaning to "transient" when two different sites (receiver- and 20 | // sender-SMTP agents) must agree on the interpretation. Each reply 21 | // in this category might have a different time value, but the SMTP 22 | // client SHOULD try again. A rule of thumb to determine whether a 23 | // reply fits into the 4yz or the 5yz category (see below) is that 24 | // replies are 4yz if they can be successful if repeated without any 25 | // change in command form or in properties of the sender or receiver 26 | // (that is, the command is repeated identically and the receiver 27 | // does not put up a new implementation). 28 | SMTPErrorTransientServiceNotAvailable = SMTPError{Status: 421, Message: "Service not available, closing transmission channel"} 29 | SMTPErrorTransientMailboxNotAvailable = SMTPError{Status: 450, Message: "Requested mail action not taken: mailbox unavailable"} 30 | SMTPErrorTransientLocalError = SMTPError{Status: 451, Message: "Requested action aborted: local error in processing"} 31 | SMTPErrorTransientInsufficientSystemStorage = SMTPError{Status: 452, Message: "Requested action not taken: insufficient system storage"} 32 | SMTPErrorTransientUnableToAccommodateParameters = SMTPError{Status: 455, Message: "Server unable to accommodate parameters"} 33 | 34 | // 5yz Permanent Negative Completion reply 35 | // The command was not accepted and the requested action did not 36 | // occur. The SMTP client SHOULD NOT repeat the exact request (in 37 | // the same sequence). Even some "permanent" error conditions can be 38 | // corrected, so the human user may want to direct the SMTP client to 39 | // reinitiate the command sequence by direct action at some point in 40 | // the future (e.g., after the spelling has been changed, or the user 41 | // has altered the account status). 42 | SMTPErrorPermanentSyntaxError = SMTPError{Status: 500, Message: "Syntax error, command unrecognized"} 43 | SMTPErrorPermanentSyntaxErrorInParameters = SMTPError{Status: 501, Message: "Syntax error in parameters or arguments"} 44 | SMTPErrorPermanentCommandNotImplemented = SMTPError{Status: 502, Message: "Command not implemented"} 45 | SMTPErrorPermanentBadSequence = SMTPError{Status: 503, Message: "Bad sequence of commands"} 46 | SMTPErrorPermanentParameterNotImplemented = SMTPError{Status: 504, Message: "Command parameter not implemented"} 47 | SMTPErrorPermanentMailboxNotAvailable = SMTPError{Status: 550, Message: "Requested action not taken: mailbox unavailable"} 48 | SMTPErrorPermanentUserNotLocal = SMTPError{Status: 551, Message: "User not local"} 49 | SMTPErrorPermanentExceededStorage = SMTPError{Status: 552, Message: "Requested mail action aborted: exceeded storage allocation"} 50 | SMTPErrorPermanentMailboxNameNotAllowed = SMTPError{Status: 553, Message: "Requested action not taken: mailbox name not allowed"} 51 | SMTPErrorPermanentTransactionFailed = SMTPError{Status: 554, Message: "Transaction failed"} 52 | SMTPErrorMailParametersNotImplemented = SMTPError{Status: 555, Message: "MAIL FROM/RCPT TO parameters not recognized or not implemented"} 53 | ) 54 | -------------------------------------------------------------------------------- /smtp/state.go: -------------------------------------------------------------------------------- 1 | package smtp 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "net" 8 | "strings" 9 | ) 10 | 11 | // State contains all the state for a single client 12 | type State struct { 13 | From *MailAddress 14 | To []*MailAddress 15 | Data []byte 16 | EightBitMIME bool 17 | Secure bool 18 | SessionId Id 19 | Ip net.IP 20 | Hostname string 21 | Authenticated bool 22 | User User 23 | } 24 | 25 | // User denotes an authenticated SMTP user. 26 | type User interface { 27 | // Username returns the username / email address of the user. 28 | Username() string 29 | } 30 | 31 | // reset the state 32 | func (s *State) Reset() { 33 | s.From = nil 34 | s.To = []*MailAddress{} 35 | s.Data = []byte{} 36 | s.EightBitMIME = false 37 | } 38 | 39 | // Checks the state if the client can send a MAIL command. 40 | func (s *State) CanReceiveMail() (bool, string) { 41 | if s.From != nil { 42 | return false, "Sender already specified" 43 | } 44 | 45 | return true, "" 46 | } 47 | 48 | // Checks the state if the client can send a RCPT command. 49 | func (s *State) CanReceiveRcpt() (bool, string) { 50 | if s.From == nil { 51 | return false, "Need mail before RCPT" 52 | } 53 | 54 | return true, "" 55 | } 56 | 57 | // Checks the state if the client can send a DATA command. 58 | func (s *State) CanReceiveData() (bool, string) { 59 | if s.From == nil { 60 | return false, "Need mail before DATA" 61 | } 62 | 63 | if len(s.To) == 0 { 64 | return false, "Need RCPT before DATA" 65 | } 66 | 67 | return true, "" 68 | } 69 | 70 | // Check whether the auth user is allowed to send from the MAIL FROM email address and to the RCPT TO address. 71 | func (s *State) AuthMatchesRcptAndMail() (bool, string) { 72 | 73 | // TODO: what if one of those variables is nil? 74 | 75 | // TODO: handle if user can send from multiple email addresses 76 | if s.From.Address != s.User.Username() { 77 | return false, fmt.Sprintf("5.7.1 Sender address rejected: not owned by user %s", s.User.Username()) 78 | } 79 | 80 | // TODO: check for recipient? 81 | 82 | return true, "" 83 | } 84 | 85 | // AddHeader prepends the given header to the state. 86 | func (s *State) AddHeader(headerKey string, headerValue string) { 87 | header := fmt.Sprintf("%s: %s\r\n", headerKey, headerValue) 88 | s.Data = append([]byte(header), s.Data...) 89 | } 90 | 91 | // GetHeader gets a header from the state. 92 | func (s *State) GetHeader(headerKey string) (headerValue string, ok bool) { 93 | reader := bufio.NewReader(bytes.NewReader(s.Data)) 94 | for { 95 | line, err := reader.ReadString('\n') 96 | if err != nil { 97 | break 98 | } 99 | 100 | // Headers end with an empty line 101 | if len(strings.TrimSpace(line)) == 0 { 102 | break 103 | } 104 | 105 | if strings.HasPrefix(strings.ToLower(line), strings.ToLower(headerKey)+":") { 106 | // Found the header, extract the value 107 | headerValue = strings.TrimSpace(line[len(headerKey)+1:]) 108 | return headerValue, true 109 | } 110 | } 111 | 112 | return "", false 113 | } 114 | -------------------------------------------------------------------------------- /smtp/state_test.go: -------------------------------------------------------------------------------- 1 | package smtp 2 | 3 | import ( 4 | "net/mail" 5 | "strings" 6 | "testing" 7 | 8 | . "github.com/smartystreets/goconvey/convey" 9 | ) 10 | 11 | func TestState(t *testing.T) { 12 | 13 | Convey("AddHeader()", t, func() { 14 | 15 | // Create a state object with a message 16 | message := `From: sender@example.com 17 | To: recipient@example.com 18 | Subject: Test Subject 19 | 20 | This is the body of the email.` 21 | 22 | state := &State{ 23 | Data: []byte(message), 24 | } 25 | 26 | // Add the header 27 | state.AddHeader("MessageId", "some-value@localhost") 28 | 29 | // If we now parse the state data again, it should be valid and the header should be present 30 | parsedMessage, err := mail.ReadMessage(strings.NewReader(string(state.Data))) 31 | So(err, ShouldBeNil) 32 | So(parsedMessage.Header.Get("MessageId"), ShouldEqual, "some-value@localhost") 33 | 34 | // and make sure the rest is also still there... 35 | So(parsedMessage.Header.Get("From"), ShouldEqual, "sender@example.com") 36 | 37 | }) 38 | 39 | Convey("GetHeader()", t, func() { 40 | // Create a state object with a message 41 | message := `From: sender@example.com 42 | To: recipient@example.com 43 | X-Spam-Score: -5.1 44 | Subject: Test Subject 45 | 46 | This is the body of the email.` 47 | 48 | state := &State{ 49 | Data: []byte(message), 50 | } 51 | 52 | headerValue, ok := state.GetHeader("To") 53 | So(ok, ShouldBeTrue) 54 | So(headerValue, ShouldEqual, "recipient@example.com") 55 | 56 | headerValue, ok = state.GetHeader("to") 57 | So(ok, ShouldBeTrue) 58 | So(headerValue, ShouldEqual, "recipient@example.com") 59 | 60 | headerValue, ok = state.GetHeader("TO") 61 | So(ok, ShouldBeTrue) 62 | So(headerValue, ShouldEqual, "recipient@example.com") 63 | 64 | headerValue, ok = state.GetHeader("From") 65 | So(ok, ShouldBeTrue) 66 | So(headerValue, ShouldEqual, "sender@example.com") 67 | 68 | headerValue, ok = state.GetHeader("X-Spam-Score") 69 | So(ok, ShouldBeTrue) 70 | So(headerValue, ShouldEqual, "-5.1") 71 | 72 | _, ok = state.GetHeader("Date") 73 | So(ok, ShouldBeFalse) 74 | }) 75 | } 76 | --------------------------------------------------------------------------------