Commit e2e78ad8 authored by ale's avatar ale
Browse files

Add vendor dir

parent 6faade0b
Pipeline #22125 passed with stages
in 30 seconds
Copyright (c) 2014 Christian Joergensen (christian@technobabble.dk)
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
Go smtpd [![GoDoc](https://godoc.org/github.com/chrj/smtpd?status.png)](https://godoc.org/github.com/chrj/smtpd) [![Go Report Card](https://goreportcard.com/badge/github.com/chrj/smtpd)](https://goreportcard.com/report/github.com/chrj/smtpd)
========
Package smtpd implements an SMTP server in golang.
Features
--------
* STARTTLS (using `crypto/tls`)
* Authentication (PLAIN/LOGIN, only after STARTTLS)
* [XCLIENT](http://www.postfix.org/XCLIENT_README.html) and [PROXY protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) (for running behind a proxy)
* Connection, HELO, sender and recipient checks for rejecting e-mails using callbacks
* Configurable limits for: connection count, message size and recipient count
* Hands incoming e-mail off to a configured callback function
Version numbers
---------------
The package is tagged with semantic version numbers, making it suitable for use in a [Go Module](https://github.com/golang/go/wiki/Modules).
Feedback
--------
If you end up using this package or have any feedback, I'd very much like to hear about it. You can reach me by [email](mailto:christian@technobabble.dk).
package smtpd
import (
"fmt"
"net/mail"
)
func parseAddress(src string) (string, error) {
// While a RFC5321 mailbox specification is not the same as an RFC5322
// email address specification, it is better to accept that format and
// parse it down to the actual address, as there are a lot of badly
// behaving MTAs and MUAs that do it wrongly. It therefore makes sense
// to rely on Go's built-in address parser. This does have the benefit
// of allowing "email@example.com" as input as thats commonly used,
// though not RFC compliant.
addr, err := mail.ParseAddress(src)
if err != nil {
return "", fmt.Errorf("malformed e-mail address: %s", src)
}
return addr.Address, nil
}
package smtpd
import (
"crypto/tls"
"fmt"
"net"
"time"
)
// Envelope holds a message
type Envelope struct {
Sender string
Recipients []string
Data []byte
}
// AddReceivedLine prepends a Received header to the Data
func (env *Envelope) AddReceivedLine(peer Peer) {
tlsDetails := ""
tlsVersions := map[uint16]string{
tls.VersionSSL30: "SSL3.0",
tls.VersionTLS10: "TLS1.0",
tls.VersionTLS11: "TLS1.1",
tls.VersionTLS12: "TLS1.2",
tls.VersionTLS13: "TLS1.3",
}
if peer.TLS != nil {
version := "unknown"
if val, ok := tlsVersions[peer.TLS.Version]; ok {
version = val
}
cipher := tls.CipherSuiteName(peer.TLS.CipherSuite)
tlsDetails = fmt.Sprintf(
"\r\n\t(version=%s cipher=%s);",
version,
cipher,
)
}
peerIP := ""
if addr, ok := peer.Addr.(*net.TCPAddr); ok {
peerIP = addr.IP.String()
}
line := wrap([]byte(fmt.Sprintf(
"Received: from %s ([%s]) by %s with %s;%s\r\n\t%s\r\n",
peer.HeloName,
peerIP,
peer.ServerName,
peer.Protocol,
tlsDetails,
time.Now().Format("Mon, 02 Jan 2006 15:04:05 -0700 (MST)"),
)))
env.Data = append(env.Data, line...)
// Move the new Received line up front
copy(env.Data[len(line):], env.Data[0:len(env.Data)-len(line)])
copy(env.Data, line)
}
module github.com/chrj/smtpd
go 1.14
require github.com/eaigner/dkim v0.0.0-20150301120808-6fe4a7ee9cfb
github.com/eaigner/dkim v0.0.0-20150301120808-6fe4a7ee9cfb h1:17kQ+7S0aEyRhZd9KCAofvKlL1N1/w+zUZKaxpLFpM0=
github.com/eaigner/dkim v0.0.0-20150301120808-6fe4a7ee9cfb/go.mod h1:FSCIHbrqk7D01Mj8y/jW+NS1uoCerr+ad+IckTHTFf4=
package smtpd
import (
"bufio"
"bytes"
"crypto/tls"
"encoding/base64"
"fmt"
"io"
"io/ioutil"
"net"
"net/textproto"
"strconv"
"strings"
"time"
)
type command struct {
line string
action string
fields []string
params []string
}
func parseLine(line string) (cmd command) {
cmd.line = line
cmd.fields = strings.Fields(line)
if len(cmd.fields) > 0 {
cmd.action = strings.ToUpper(cmd.fields[0])
if len(cmd.fields) > 1 {
// Account for some clients breaking the standard and having
// an extra whitespace after the ':' character. Example:
//
// MAIL FROM: <test@example.org>
//
// Should be:
//
// MAIL FROM:<test@example.org>
//
// Thus, we add a check if the second field ends with ':'
// and appends the rest of the third field.
if cmd.fields[1][len(cmd.fields[1])-1] == ':' && len(cmd.fields) > 2 {
cmd.fields[1] = cmd.fields[1] + cmd.fields[2]
cmd.fields = cmd.fields[0:2]
}
cmd.params = strings.Split(cmd.fields[1], ":")
}
}
return
}
func (session *session) handle(line string) {
cmd := parseLine(line)
// Commands are dispatched to the appropriate handler functions.
// If a network error occurs during handling, the handler should
// just return and let the error be handled on the next read.
switch cmd.action {
case "PROXY":
session.handlePROXY(cmd)
return
case "HELO":
session.handleHELO(cmd)
return
case "EHLO":
session.handleEHLO(cmd)
return
case "MAIL":
session.handleMAIL(cmd)
return
case "RCPT":
session.handleRCPT(cmd)
return
case "STARTTLS":
session.handleSTARTTLS(cmd)
return
case "DATA":
session.handleDATA(cmd)
return
case "RSET":
session.handleRSET(cmd)
return
case "NOOP":
session.handleNOOP(cmd)
return
case "QUIT":
session.handleQUIT(cmd)
return
case "AUTH":
session.handleAUTH(cmd)
return
case "XCLIENT":
session.handleXCLIENT(cmd)
return
}
session.reply(502, "Unsupported command.")
}
func (session *session) handleHELO(cmd command) {
if len(cmd.fields) < 2 {
session.reply(502, "Missing parameter")
return
}
if session.peer.HeloName != "" {
// Reset envelope in case of duplicate HELO
session.reset()
}
if session.server.HeloChecker != nil {
err := session.server.HeloChecker(session.peer, cmd.fields[1])
if err != nil {
session.error(err)
return
}
}
session.peer.HeloName = cmd.fields[1]
session.peer.Protocol = SMTP
session.reply(250, "Go ahead")
return
}
func (session *session) handleEHLO(cmd command) {
if len(cmd.fields) < 2 {
session.reply(502, "Missing parameter")
return
}
if session.peer.HeloName != "" {
// Reset envelope in case of duplicate EHLO
session.reset()
}
if session.server.HeloChecker != nil {
err := session.server.HeloChecker(session.peer, cmd.fields[1])
if err != nil {
session.error(err)
return
}
}
session.peer.HeloName = cmd.fields[1]
session.peer.Protocol = ESMTP
fmt.Fprintf(session.writer, "250-%s\r\n", session.server.Hostname)
extensions := session.extensions()
if len(extensions) > 1 {
for _, ext := range extensions[:len(extensions)-1] {
fmt.Fprintf(session.writer, "250-%s\r\n", ext)
}
}
session.reply(250, extensions[len(extensions)-1])
return
}
func (session *session) handleMAIL(cmd command) {
if len(cmd.params) != 2 || strings.ToUpper(cmd.params[0]) != "FROM" {
session.reply(502, "Invalid syntax.")
return
}
if session.peer.HeloName == "" {
session.reply(502, "Please introduce yourself first.")
return
}
if session.server.Authenticator != nil && session.peer.Username == "" {
session.reply(530, "Authentication Required.")
return
}
if !session.tls && session.server.ForceTLS {
session.reply(502, "Please turn on TLS by issuing a STARTTLS command.")
return
}
if session.envelope != nil {
session.reply(502, "Duplicate MAIL")
return
}
var err error
addr := "" // null sender
// We must accept a null sender as per rfc5321 section-6.1.
if cmd.params[1] != "<>" {
addr, err = parseAddress(cmd.params[1])
if err != nil {
session.reply(502, "Malformed e-mail address")
return
}
}
if session.server.SenderChecker != nil {
err = session.server.SenderChecker(session.peer, addr)
if err != nil {
session.error(err)
return
}
}
session.envelope = &Envelope{
Sender: addr,
}
session.reply(250, "Go ahead")
return
}
func (session *session) handleRCPT(cmd command) {
if len(cmd.params) != 2 || strings.ToUpper(cmd.params[0]) != "TO" {
session.reply(502, "Invalid syntax.")
return
}
if session.envelope == nil {
session.reply(502, "Missing MAIL FROM command.")
return
}
if len(session.envelope.Recipients) >= session.server.MaxRecipients {
session.reply(452, "Too many recipients")
return
}
addr, err := parseAddress(cmd.params[1])
if err != nil {
session.reply(502, "Malformed e-mail address")
return
}
if session.server.RecipientChecker != nil {
err = session.server.RecipientChecker(session.peer, addr)
if err != nil {
session.error(err)
return
}
}
session.envelope.Recipients = append(session.envelope.Recipients, addr)
session.reply(250, "Go ahead")
return
}
func (session *session) handleSTARTTLS(cmd command) {
if session.tls {
session.reply(502, "Already running in TLS")
return
}
if session.server.TLSConfig == nil {
session.reply(502, "TLS not supported")
return
}
tlsConn := tls.Server(session.conn, session.server.TLSConfig)
session.reply(220, "Go ahead")
if err := tlsConn.Handshake(); err != nil {
session.logError(err, "couldn't perform handshake")
session.reply(550, "Handshake error")
return
}
// Reset envelope as a new EHLO/HELO is required after STARTTLS
session.reset()
// Reset deadlines on the underlying connection before I replace it
// with a TLS connection
session.conn.SetDeadline(time.Time{})
// Replace connection with a TLS connection
session.conn = tlsConn
session.reader = bufio.NewReader(tlsConn)
session.writer = bufio.NewWriter(tlsConn)
session.scanner = bufio.NewScanner(session.reader)
session.tls = true
// Save connection state on peer
state := tlsConn.ConnectionState()
session.peer.TLS = &state
// Flush the connection to set new timeout deadlines
session.flush()
return
}
func (session *session) handleDATA(cmd command) {
if session.envelope == nil || len(session.envelope.Recipients) == 0 {
session.reply(502, "Missing RCPT TO command.")
return
}
session.reply(354, "Go ahead. End your data with <CR><LF>.<CR><LF>")
session.conn.SetDeadline(time.Now().Add(session.server.DataTimeout))
data := &bytes.Buffer{}
reader := textproto.NewReader(session.reader).DotReader()
_, err := io.CopyN(data, reader, int64(session.server.MaxMessageSize))
if err == io.EOF {
// EOF was reached before MaxMessageSize
// Accept and deliver message
session.envelope.Data = data.Bytes()
if err := session.deliver(); err != nil {
session.error(err)
} else {
session.reply(250, "Thank you.")
}
session.reset()
}
if err != nil {
// Network error, ignore
return
}
// Discard the rest and report an error.
_, err = io.Copy(ioutil.Discard, reader)
if err != nil {
// Network error, ignore
return
}
session.reply(552, fmt.Sprintf(
"Message exceeded max message size of %d bytes",
session.server.MaxMessageSize,
))
session.reset()
return
}
func (session *session) handleRSET(cmd command) {
session.reset()
session.reply(250, "Go ahead")
return
}
func (session *session) handleNOOP(cmd command) {
session.reply(250, "Go ahead")
return
}
func (session *session) handleQUIT(cmd command) {
session.reply(221, "OK, bye")
session.close()
return
}
func (session *session) handleAUTH(cmd command) {
if len(cmd.fields) < 2 {
session.reply(502, "Invalid syntax.")
return
}
if session.server.Authenticator == nil {
session.reply(502, "AUTH not supported.")
return
}
if session.peer.HeloName == "" {
session.reply(502, "Please introduce yourself first.")
return
}
if !session.tls {
session.reply(502, "Cannot AUTH in plain text mode. Use STARTTLS.")
return
}
mechanism := strings.ToUpper(cmd.fields[1])
username := ""
password := ""
switch mechanism {
case "PLAIN":
auth := ""
if len(cmd.fields) < 3 {
session.reply(334, "Give me your credentials")
if !session.scanner.Scan() {
return
}
auth = session.scanner.Text()
} else {
auth = cmd.fields[2]
}
data, err := base64.StdEncoding.DecodeString(auth)
if err != nil {
session.reply(502, "Couldn't decode your credentials")
return
}
parts := bytes.Split(data, []byte{0})
if len(parts) != 3 {
session.reply(502, "Couldn't decode your credentials")
return
}
username = string(parts[1])
password = string(parts[2])
case "LOGIN":
encodedUsername := ""