Initial checkin.
This commit is contained in:
commit
cbb043f2e6
5 changed files with 478 additions and 0 deletions
46
cmd/smtpd/main.go
Normal file
46
cmd/smtpd/main.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bitbucket.org/chrj/smtpd"
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"log"
|
||||
)
|
||||
|
||||
func dumpMessage(peer smtpd.Peer, env smtpd.Envelope) error {
|
||||
log.Printf("New mail from: %s", env.MailFrom)
|
||||
return nil
|
||||
}
|
||||
|
||||
var tlsCert = flag.String("tlscert", "", "TLS: Certificate file")
|
||||
var tlsKey = flag.String("tlskey", "", "TLS: Private key")
|
||||
|
||||
func main() {
|
||||
|
||||
flag.Parse()
|
||||
|
||||
var tlsConfig *tls.Config
|
||||
|
||||
if *tlsCert != "" {
|
||||
cert, err := tls.LoadX509KeyPair(*tlsCert, *tlsKey)
|
||||
if err != nil {
|
||||
log.Fatal("certificate error:", err)
|
||||
}
|
||||
tlsConfig = &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
}
|
||||
|
||||
server := &smtpd.Server{
|
||||
Addr: "127.0.0.1:10025",
|
||||
WelcomeMessage: "localhost ESMTP ready.",
|
||||
Handler: dumpMessage,
|
||||
TLSConfig: tlsConfig,
|
||||
ForceTLS: true,
|
||||
}
|
||||
|
||||
server.ListenAndServe()
|
||||
|
||||
return
|
||||
|
||||
}
|
20
cmd/smtpd/smtpd.crt
Normal file
20
cmd/smtpd/smtpd.crt
Normal file
|
@ -0,0 +1,20 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDLjCCAhYCCQD7wib+be6ipjANBgkqhkiG9w0BAQUFADBZMQswCQYDVQQGEwJE
|
||||
SzETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0
|
||||
cyBQdHkgTHRkMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTQwNzEzMjA0MTE2WhcN
|
||||
MTUwNzEzMjA0MTE2WjBZMQswCQYDVQQGEwJESzETMBEGA1UECAwKU29tZS1TdGF0
|
||||
ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRIwEAYDVQQDDAls
|
||||
b2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDK41RNmjLD
|
||||
NVs3ZOX1IpCfWITMZ8kx0TB9BXh86XhgaH47DNoOnSeDvawGfmKXYF7ISuFRacbc
|
||||
C1xeiN+hah0CAJQJXpzYO8dpyXrPVIiZ/mKFRAnz/Kp/PApDjkpJ13VnLkuZLbJg
|
||||
dQ0dtsb2BW+T/jEHDpyCOwR2g1AdlnsjuP+V1WxZvCKYvv5awv5AWwmCbKGjA1Jv
|
||||
8j54WZzK7bFxp19Eyg2WVXhf7ZB+zs8RbliYzUqgT7GnUEBQkofaxb5j+n/PR7AU
|
||||
/U1dFSVM7i1mn58SjsDx5v6GIh/Z3ekdEKbBiJJSvyhPV6K1b7IOWo9tQGeEMQP4
|
||||
tvkLDPPgPXOZAgMBAAEwDQYJKoZIhvcNAQEFBQADggEBAAniCpu2zPujExDMp36l
|
||||
3VKtMZBbbOn8rwAcGOUjeSTZT62VQJX4CSsXJGuSHLV9fKPO8K3pob9mZ/CGL3Xj
|
||||
JnLKDMgAQEiLq9IZPZg0/vYJjP96Hlgf0sOT6Q4dX36kDvGsWKJZilPEOKFvZh+R
|
||||
acwWmN8bEGhFThijvTfY7sxEnTem1R2qs5cqCRfc4vCammTCRpLSWcD4p/WVZc5K
|
||||
MCv8N2/JDg9plaBiQZyzaaiXI4X90IZQlWzIT6E2+i3V6bRwLioTituxu1r6Pwx9
|
||||
lniOQrA1+5waqottyMWQGHzmrFFg93HDX0WmP4IXHkXWhAcR611DLIw3NQuqt7Q4
|
||||
ecM=
|
||||
-----END CERTIFICATE-----
|
17
cmd/smtpd/smtpd.csr
Normal file
17
cmd/smtpd/smtpd.csr
Normal file
|
@ -0,0 +1,17 @@
|
|||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIICnjCCAYYCAQAwWTELMAkGA1UEBhMCREsxEzARBgNVBAgMClNvbWUtU3RhdGUx
|
||||
ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9j
|
||||
YWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyuNUTZoywzVb
|
||||
N2Tl9SKQn1iEzGfJMdEwfQV4fOl4YGh+OwzaDp0ng72sBn5il2BeyErhUWnG3Atc
|
||||
XojfoWodAgCUCV6c2DvHacl6z1SImf5ihUQJ8/yqfzwKQ45KSdd1Zy5LmS2yYHUN
|
||||
HbbG9gVvk/4xBw6cgjsEdoNQHZZ7I7j/ldVsWbwimL7+WsL+QFsJgmyhowNSb/I+
|
||||
eFmcyu2xcadfRMoNllV4X+2Qfs7PEW5YmM1KoE+xp1BAUJKH2sW+Y/p/z0ewFP1N
|
||||
XRUlTO4tZp+fEo7A8eb+hiIf2d3pHRCmwYiSUr8oT1eitW+yDlqPbUBnhDED+Lb5
|
||||
Cwzz4D1zmQIDAQABoAAwDQYJKoZIhvcNAQEFBQADggEBALkJ6moQnDeT91Y37nQP
|
||||
pXmcbiL/bj34v3MnUYArmxtZcfMJ3B9qxe5/0psq4r6hjxPWNaW92NkkE1aJZwuO
|
||||
cAqGWcPBVFH309siq5J0NGkjArdtd84NBewoBZVqpcqwrfVAI6adINlF2dGLeeJW
|
||||
SAlVEKCt3SLz3X+lVgKIzZTEsMuYmTaUrr490ecDWsh2eey0pbhtSqXkPkQOVUla
|
||||
8QqysE5DuaES8ysTIuAh28uIxWmLXIWnVqia2+eltEgiuaiAZVH3CYH136/FTEL1
|
||||
a5toCmQFWv9rAc+EfVxIh1CgUNsWx5ARPVuSRZaBjH4qXwIg8V138eC482MNtMfM
|
||||
fDs=
|
||||
-----END CERTIFICATE REQUEST-----
|
27
cmd/smtpd/smtpd.key
Normal file
27
cmd/smtpd/smtpd.key
Normal file
|
@ -0,0 +1,27 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAyuNUTZoywzVbN2Tl9SKQn1iEzGfJMdEwfQV4fOl4YGh+Owza
|
||||
Dp0ng72sBn5il2BeyErhUWnG3AtcXojfoWodAgCUCV6c2DvHacl6z1SImf5ihUQJ
|
||||
8/yqfzwKQ45KSdd1Zy5LmS2yYHUNHbbG9gVvk/4xBw6cgjsEdoNQHZZ7I7j/ldVs
|
||||
WbwimL7+WsL+QFsJgmyhowNSb/I+eFmcyu2xcadfRMoNllV4X+2Qfs7PEW5YmM1K
|
||||
oE+xp1BAUJKH2sW+Y/p/z0ewFP1NXRUlTO4tZp+fEo7A8eb+hiIf2d3pHRCmwYiS
|
||||
Ur8oT1eitW+yDlqPbUBnhDED+Lb5Cwzz4D1zmQIDAQABAoIBAQCoterncP8fRqIo
|
||||
aRW0B18dsj0TwIYUj/BzNfZgYMCB4sJ9Fg3JszMloLaI29XeLPwEMAg3a+86EZRo
|
||||
5AaaMiQXAyYWuH9SbDtBo5IlEBVbgKaqTM69/fBFR0b9sDfkOW9eMqgYo2A+R3d1
|
||||
qwS9lf2Xoftg8+x/etYWOtGHGRgitflirlW3uLvgCo/gP5gcb+HbtQNVRyKnqR1n
|
||||
hNUeDCxTMLuhkvS2NxMUcAkuNYSLRiM3bXtER5RfatPHgvmFEmtKoB3TbQsw4Z6e
|
||||
E+xlgFEnTVLkPoblhQjcUpiDqaZCfRUxmFvgfB+/0zrCZh3TtmMxSnUm1uFcNyul
|
||||
dBICKx2lAoGBAP1e8GZKJ2hB5MIIG8TyZvmv1EnNfgAAhSbhWWXufoKflEAIBihg
|
||||
7NBQAuHdq76e+G1F8GJzsHtNZquQhrIwo0U0/eBycrLBkQgIAeZJEZx7EH3EsqL3
|
||||
7RuJIaOQBy5LBnnnxjwlcNQvS7FiZheqEsN7RYScGE1RFAREj86B54bLAoGBAMz+
|
||||
SUpCTXHzpgOLhN6KBTmgr0fk4SKVLSdyFjNJbd7bokPoy2aO8IkBKn/jWrxNOZij
|
||||
5XU0NryYuMq1dsJViZ1kRzF8Q3xw1IjKOUeWBp1221FrA+nouinIYNdtoNmIOLXO
|
||||
1IOF0jInLqjBHC0MdaZDaupEJ0ZbFV+8EQCka/6rAoGANPkSffBnCM8uCrszQxwD
|
||||
F5UBZ2TFQS7ap+RZkowoexruHe0PjIWnPW5dC+gSrkoCWqZSueLCNSVbn+cZoku0
|
||||
9xU7Nx/2hxUdQ3aZHxKL0hGQwxrK1nPLaQRkuhO0zKL2+anRsmWJj3NL+gw+mBgA
|
||||
0EoHoNAZ7KBU9Qd4oY5bX70CgYBCdWJPZ+VxvxsgZRgjib2d7EFHXqW6r4BfHHak
|
||||
E/dB3BTkTVG8IzVKRY2AvrXI/IRivygB8naYeC7Y0TH6WP7vfvYxzeaXLoFJA77E
|
||||
PZhRbpo18Crpp6DLMQJsdUdDnw07rB1rsnPt/JP88/ZtiG+QAqVj48qT3a21RuSA
|
||||
P84fVwKBgQCFdUzNpwsDVZ0L51yk7D9LwsA9jwzBxc5Jtd8CIDVylAlj1BM7hkiG
|
||||
durZfNVtkhi+RXgD3SjZXWtCCprvrrjl8T52+deOCx2qM/5qhtJRKIHEkqndx4e5
|
||||
lmt3J5alekerwijR/F8+qnrrEsvtp6rozMDCNSGa6ir4HWYQUJ2C4g==
|
||||
-----END RSA PRIVATE KEY-----
|
368
smtpd.go
Normal file
368
smtpd.go
Normal file
|
@ -0,0 +1,368 @@
|
|||
package smtpd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
Addr string // Address to listen on
|
||||
WelcomeMessage string // Initial server banner
|
||||
|
||||
ReadTimeout time.Duration // Socket timeout for read operations (default: 60s)
|
||||
WriteTimeout time.Duration // Socket timeout for write operations (default: 60s)
|
||||
|
||||
// New e-mails are handed off to this function.
|
||||
// If an error is returned, it will be reported in the SMTP session
|
||||
Handler func(peer Peer, env Envelope) error
|
||||
|
||||
// Enable PLAIN/LOGIN authentication
|
||||
Authenticator func(peer Peer, username, password string) error
|
||||
|
||||
TLSConfig *tls.Config // Enable STARTTLS support
|
||||
ForceTLS bool // Force STARTTLS usage
|
||||
|
||||
MaxMessageSize int // Max message size in bytes (default: 10240000)
|
||||
}
|
||||
|
||||
type sessionState int
|
||||
|
||||
const (
|
||||
_STATE_HELO sessionState = iota
|
||||
_STATE_AUTH
|
||||
_STATE_MAIL
|
||||
_STATE_RCPT
|
||||
_STATE_DATA
|
||||
)
|
||||
|
||||
type session struct {
|
||||
server *Server
|
||||
conn net.Conn
|
||||
reader *bufio.Reader
|
||||
writer *bufio.Writer
|
||||
peer Peer
|
||||
state sessionState
|
||||
tls bool
|
||||
}
|
||||
|
||||
type Peer struct {
|
||||
HeloName string // Server name used in HELO/EHLO command
|
||||
UserName string // Username from authentication
|
||||
Addr net.Addr // Network address
|
||||
}
|
||||
|
||||
type MailAddress string
|
||||
|
||||
type Envelope struct {
|
||||
MailFrom MailAddress
|
||||
Recipients []MailAddress
|
||||
Data []byte
|
||||
Peer *Peer
|
||||
}
|
||||
|
||||
func (srv *Server) newConnection(c net.Conn) (s *session, err error) {
|
||||
|
||||
log.Printf("New connection from: %s", c.RemoteAddr())
|
||||
|
||||
s = &session{
|
||||
server: srv,
|
||||
conn: c,
|
||||
reader: bufio.NewReader(c),
|
||||
writer: bufio.NewWriter(c),
|
||||
peer: Peer{Addr: c.RemoteAddr()},
|
||||
}
|
||||
|
||||
return s, nil
|
||||
|
||||
}
|
||||
|
||||
func (srv *Server) ListenAndServe() error {
|
||||
l, err := net.Listen("tcp", srv.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return srv.Serve(l)
|
||||
}
|
||||
|
||||
func (srv *Server) Serve(l net.Listener) error {
|
||||
|
||||
srv.configureDefaults()
|
||||
|
||||
defer l.Close()
|
||||
|
||||
for {
|
||||
|
||||
conn, e := l.Accept()
|
||||
if e != nil {
|
||||
if ne, ok := e.(net.Error); ok && ne.Temporary() {
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
session, err := srv.newConnection(conn)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
session.state = _STATE_HELO
|
||||
|
||||
go session.serve()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (srv *Server) configureDefaults() {
|
||||
|
||||
if srv.MaxMessageSize == 0 {
|
||||
srv.MaxMessageSize = 10240000
|
||||
}
|
||||
|
||||
if srv.ReadTimeout == 0 {
|
||||
srv.ReadTimeout = time.Second * 60
|
||||
}
|
||||
|
||||
if srv.WriteTimeout == 0 {
|
||||
srv.WriteTimeout = time.Second * 60
|
||||
}
|
||||
|
||||
if srv.ForceTLS && srv.TLSConfig == nil {
|
||||
log.Fatal("Cannot use ForceTLS with no TLSConfig")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (session *session) serve() {
|
||||
|
||||
log.Print("Serving")
|
||||
|
||||
defer func() {
|
||||
session.writer.Flush()
|
||||
session.conn.Close()
|
||||
}()
|
||||
|
||||
session.reply(220, session.server.WelcomeMessage)
|
||||
|
||||
scanner := bufio.NewScanner(session.reader)
|
||||
|
||||
var env Envelope
|
||||
var data *bytes.Buffer
|
||||
|
||||
for scanner.Scan() {
|
||||
|
||||
line := scanner.Text()
|
||||
command := ""
|
||||
fields := []string{}
|
||||
params := []string{}
|
||||
|
||||
if session.state != _STATE_DATA {
|
||||
fields = strings.Fields(line)
|
||||
command = strings.ToUpper(fields[0])
|
||||
if len(fields) > 1 {
|
||||
params = strings.Split(fields[1], ":")
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Line: %s, fields: %#v, params: %#v", line, fields, params)
|
||||
|
||||
if command == "QUIT" {
|
||||
session.reply(250, "Ok, bye")
|
||||
return
|
||||
}
|
||||
|
||||
switch session.state {
|
||||
|
||||
case _STATE_HELO:
|
||||
|
||||
if command == "HELO" || command == "EHLO" {
|
||||
if len(fields) < 2 {
|
||||
session.reply(502, "Missing parameter")
|
||||
continue
|
||||
} else {
|
||||
session.peer.HeloName = fields[1]
|
||||
}
|
||||
} else {
|
||||
session.reply(502, "Command not recognized, expected HELO/EHLO")
|
||||
continue
|
||||
}
|
||||
|
||||
if command == "EHLO" {
|
||||
session.WriteExtensions()
|
||||
} else {
|
||||
session.reply(250, "Go ahead")
|
||||
}
|
||||
|
||||
if session.server.Authenticator == nil {
|
||||
session.state = _STATE_MAIL
|
||||
} else {
|
||||
session.state = _STATE_AUTH
|
||||
}
|
||||
|
||||
continue
|
||||
|
||||
case _STATE_MAIL:
|
||||
|
||||
if !session.tls && command == "STARTTLS" && session.server.TLSConfig != nil {
|
||||
|
||||
tls_conn := tls.Server(session.conn, session.server.TLSConfig)
|
||||
session.reply(250, "Go ahead")
|
||||
|
||||
if err := tls_conn.Handshake(); err != nil {
|
||||
log.Printf("TLS Handshake error:", err)
|
||||
session.reply(550, "Handshake error")
|
||||
continue
|
||||
}
|
||||
|
||||
session.conn = tls_conn
|
||||
|
||||
session.reader = bufio.NewReader(tls_conn)
|
||||
session.writer = bufio.NewWriter(tls_conn)
|
||||
|
||||
scanner = bufio.NewScanner(session.reader)
|
||||
|
||||
session.tls = true
|
||||
session.state = _STATE_HELO
|
||||
|
||||
continue
|
||||
|
||||
}
|
||||
|
||||
if !session.tls && session.server.ForceTLS {
|
||||
session.reply(550, "Must run STARTTLS first")
|
||||
continue
|
||||
}
|
||||
|
||||
if command == "MAIL" && strings.ToUpper(params[0]) == "FROM" {
|
||||
|
||||
addr, err := parseMailAddress(params[1])
|
||||
|
||||
if err != nil {
|
||||
session.reply(502, "Ill-formatted e-mail address")
|
||||
continue
|
||||
}
|
||||
|
||||
env = Envelope{
|
||||
Peer: &session.peer,
|
||||
MailFrom: addr,
|
||||
}
|
||||
|
||||
session.reply(250, "Go ahead")
|
||||
session.state = _STATE_RCPT
|
||||
continue
|
||||
|
||||
} else {
|
||||
session.reply(502, "Command not recognized, expected MAIL FROM")
|
||||
continue
|
||||
}
|
||||
|
||||
case _STATE_RCPT:
|
||||
|
||||
if command == "RCPT" && strings.ToUpper(params[0]) == "TO" {
|
||||
|
||||
addr, err := parseMailAddress(params[1])
|
||||
|
||||
if err != nil {
|
||||
session.reply(502, "Ill-formatted e-mail address")
|
||||
continue
|
||||
}
|
||||
|
||||
env.Recipients = append(env.Recipients, addr)
|
||||
|
||||
session.reply(250, "Go ahead")
|
||||
continue
|
||||
|
||||
} else if command == "DATA" && len(env.Recipients) > 0 {
|
||||
session.reply(250, "Go ahead. End your data with <CR><LF>.<CR><LF>")
|
||||
data = &bytes.Buffer{}
|
||||
session.state = _STATE_DATA
|
||||
continue
|
||||
}
|
||||
|
||||
if len(env.Recipients) == 0 {
|
||||
session.reply(502, "Command not recognized, expected RCPT")
|
||||
} else {
|
||||
session.reply(502, "Command not recognized, expected RCPT or DATA")
|
||||
}
|
||||
|
||||
continue
|
||||
|
||||
case _STATE_DATA:
|
||||
|
||||
if line == "." {
|
||||
env.Data = data.Bytes()
|
||||
data.Reset()
|
||||
err := session.handle(env)
|
||||
|
||||
if err != nil {
|
||||
session.reply(502, fmt.Sprintf("%s", err))
|
||||
} else {
|
||||
session.reply(200, "Thank you.")
|
||||
}
|
||||
|
||||
session.state = _STATE_MAIL
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (session *session) reply(code int, message string) {
|
||||
|
||||
fmt.Fprintf(session.writer, "%d %s\r\n", code, message)
|
||||
|
||||
session.conn.SetWriteDeadline(time.Now().Add(session.server.WriteTimeout))
|
||||
session.writer.Flush()
|
||||
|
||||
session.conn.SetReadDeadline(time.Now().Add(session.server.ReadTimeout))
|
||||
|
||||
}
|
||||
|
||||
func (session *session) WriteExtensions() {
|
||||
|
||||
extensions := []string{
|
||||
"SIZE 10240000",
|
||||
}
|
||||
|
||||
if session.server.TLSConfig != nil && !session.tls {
|
||||
extensions = append(extensions, "STARTTLS")
|
||||
}
|
||||
|
||||
if session.tls {
|
||||
extensions = append(extensions, "AUTH PLAIN LOGIN")
|
||||
}
|
||||
|
||||
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])
|
||||
|
||||
}
|
||||
|
||||
func (session *session) handle(env Envelope) error {
|
||||
if session.server.Handler != nil {
|
||||
return session.server.Handler(session.peer, env)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func parseMailAddress(src string) (MailAddress, error) {
|
||||
if src[0] != '<' || src[len(src)-1] != '>' || strings.Count(src, "@") != 1 {
|
||||
return MailAddress(""), fmt.Errorf("Ill-formatted e-mail address: %s", src)
|
||||
}
|
||||
return MailAddress(src[1 : len(src)-1]), nil
|
||||
}
|
Loading…
Add table
Reference in a new issue