├── spf_result.go ├── errors.go ├── handlers.go ├── go.mod ├── .gitignore ├── README.md ├── backend.go ├── helpers.go ├── LICENSE ├── session.go ├── context.go ├── server.go ├── go.sum └── parser.go /spf_result.go: -------------------------------------------------------------------------------- 1 | package smtpsrv 2 | 3 | import "github.com/zaccone/spf" 4 | 5 | type SPFResult = spf.Result 6 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package smtpsrv 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrAuthDisabled = errors.New("auth is disabled") 7 | ) 8 | -------------------------------------------------------------------------------- /handlers.go: -------------------------------------------------------------------------------- 1 | package smtpsrv 2 | 3 | type HandlerFunc func(*Context) error 4 | type AuthFunc func(username, password string) error 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/alash3al/go-smtpsrv 2 | 3 | require ( 4 | github.com/emersion/go-smtp v0.13.0 5 | github.com/miekg/dns v1.1.50 // indirect 6 | github.com/zaccone/spf v0.0.0-20170817004109-76747b8658d9 7 | golang.org/x/text v0.3.7 // indirect 8 | ) 9 | 10 | go 1.13 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | vendor 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A SMTP Server Package [![](https://img.shields.io/badge/godoc-reference-5272B4.svg?style=flat-square)](https://godoc.org/github.com/alash3al/go-smtpsrv) 2 | ============================= 3 | a simple smtp server library for writing email servers like a boss. 4 | 5 | Quick Start 6 | =========== 7 | > `go get github.com/alash3al/go-smtpsrv` 8 | 9 | ```go 10 | package main 11 | 12 | import ( 13 | "fmt" 14 | 15 | "github.com/alash3al/go-smtpsrv/v3" 16 | ) 17 | 18 | func main() { 19 | handler := func(c smtpsrv.Context) error { 20 | // ... 21 | return nil 22 | } 23 | 24 | cfg := smtpsrv.ServerConfig{ 25 | BannerDomain: "mail.my.server", 26 | ListenAddr: ":25025", 27 | MaxMessageBytes: 5 * 1024, 28 | Handler: handler, 29 | } 30 | 31 | fmt.Println(smtpsrv.ListenAndServe(&cfg)) 32 | } 33 | 34 | ``` 35 | 36 | Thanks 37 | ======= 38 | - [parsemail](https://github.com/DusanKasan/parsemail) 39 | - [go-smtp](github.com/emersion/go-smtp) 40 | -------------------------------------------------------------------------------- /backend.go: -------------------------------------------------------------------------------- 1 | package smtpsrv 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/emersion/go-smtp" 7 | ) 8 | 9 | // The Backend implements SMTP server methods. 10 | type Backend struct { 11 | handler HandlerFunc 12 | auther AuthFunc 13 | } 14 | 15 | func NewBackend(auther AuthFunc, handler HandlerFunc) *Backend { 16 | return &Backend{ 17 | handler: handler, 18 | auther: auther, 19 | } 20 | } 21 | 22 | // Login handles a login command with username and password. 23 | func (bkd *Backend) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) { 24 | if nil == bkd.auther { 25 | return nil, errors.New("invalid command specified") 26 | } 27 | 28 | return NewSession(state, bkd.handler, &username, &password), nil 29 | } 30 | 31 | // AnonymousLogin requires clients to authenticate using SMTP AUTH before sending emails 32 | func (bkd *Backend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) { 33 | return NewSession(state, bkd.handler, nil, nil), nil 34 | } 35 | -------------------------------------------------------------------------------- /helpers.go: -------------------------------------------------------------------------------- 1 | package smtpsrv 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | // SplitAddress split the email@addre.ss to @ 10 | func SplitAddress(address string) (string, string, error) { 11 | sepInd := strings.LastIndex(address, "@") 12 | if sepInd == -1 { 13 | return "", "", errors.New("Invalid Address:" + address) 14 | } 15 | localPart := address[:sepInd] 16 | domainPart := address[sepInd+1:] 17 | return localPart, domainPart, nil 18 | } 19 | 20 | func SetDefaultServerConfig(cfg *ServerConfig) { 21 | if cfg == nil { 22 | *cfg = ServerConfig{} 23 | } 24 | 25 | if cfg.ListenAddr == "" { 26 | cfg.ListenAddr = "[::]:25025" 27 | } 28 | 29 | if cfg.BannerDomain == "" { 30 | cfg.BannerDomain = "localhost" 31 | } 32 | 33 | if cfg.ReadTimeout < 1 { 34 | cfg.ReadTimeout = 2 * time.Second 35 | } 36 | 37 | if cfg.WriteTimeout < 1 { 38 | cfg.WriteTimeout = 2 * time.Second 39 | } 40 | 41 | if cfg.MaxMessageBytes < 1 { 42 | cfg.MaxMessageBytes = 1024 * 1024 * 2 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Sean Murphy 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 | 23 | -------------------------------------------------------------------------------- /session.go: -------------------------------------------------------------------------------- 1 | package smtpsrv 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "net/mail" 7 | 8 | "github.com/emersion/go-smtp" 9 | ) 10 | 11 | // A Session is returned after successful login. 12 | type Session struct { 13 | connState *smtp.ConnectionState 14 | From *mail.Address 15 | To *mail.Address 16 | handler HandlerFunc 17 | body io.Reader 18 | username *string 19 | password *string 20 | } 21 | 22 | // NewSession initialize a new session 23 | func NewSession(state *smtp.ConnectionState, handler HandlerFunc, username, password *string) *Session { 24 | return &Session{ 25 | connState: state, 26 | handler: handler, 27 | } 28 | } 29 | 30 | func (s *Session) Mail(from string, opts smtp.MailOptions) (err error) { 31 | s.From, err = mail.ParseAddress(from) 32 | return 33 | } 34 | 35 | func (s *Session) Rcpt(to string) (err error) { 36 | s.To, err = mail.ParseAddress(to) 37 | return 38 | } 39 | 40 | func (s *Session) Data(r io.Reader) error { 41 | if s.handler == nil { 42 | return errors.New("internal error: no handler") 43 | } 44 | 45 | s.body = r 46 | 47 | c := Context{ 48 | session: s, 49 | } 50 | 51 | return s.handler(&c) 52 | } 53 | 54 | func (s *Session) Reset() { 55 | } 56 | 57 | func (s *Session) Logout() error { 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package smtpsrv 2 | 3 | import ( 4 | "crypto/tls" 5 | "net" 6 | "net/mail" 7 | 8 | "github.com/zaccone/spf" 9 | ) 10 | 11 | type Context struct { 12 | session *Session 13 | } 14 | 15 | func (c Context) From() *mail.Address { 16 | return c.session.From 17 | } 18 | 19 | func (c Context) To() *mail.Address { 20 | return c.session.To 21 | } 22 | 23 | func (c Context) User() (string, string, error) { 24 | if c.session.username == nil || c.session.password == nil { 25 | return "", "", ErrAuthDisabled 26 | } 27 | 28 | return *c.session.username, *c.session.password, nil 29 | } 30 | 31 | func (c Context) RemoteAddr() net.Addr { 32 | return c.session.connState.RemoteAddr 33 | } 34 | 35 | func (c Context) TLS() *tls.ConnectionState { 36 | return &c.session.connState.TLS 37 | } 38 | 39 | func (c Context) Read(p []byte) (int, error) { 40 | return c.session.body.Read(p) 41 | } 42 | 43 | func (c Context) Parse() (*Email, error) { 44 | return ParseEmail(c.session.body) 45 | } 46 | 47 | func (c Context) Mailable() (bool, error) { 48 | _, host, err := SplitAddress(c.From().Address) 49 | if err != nil { 50 | return false, err 51 | } 52 | 53 | mxhosts, err := net.LookupMX(host) 54 | if err != nil { 55 | return false, err 56 | } 57 | 58 | return len(mxhosts) > 0, nil 59 | } 60 | func (c Context) SPF() (SPFResult, string, error) { 61 | _, host, err := SplitAddress(c.From().Address) 62 | if err != nil { 63 | return spf.None, "", err 64 | } 65 | 66 | return spf.CheckHost(net.ParseIP(c.RemoteAddr().String()), host, c.From().Address) 67 | } 68 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package smtpsrv 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/emersion/go-smtp" 9 | ) 10 | 11 | type ServerConfig struct { 12 | ListenAddr string 13 | BannerDomain string 14 | ReadTimeout time.Duration 15 | WriteTimeout time.Duration 16 | Handler HandlerFunc 17 | Auther AuthFunc 18 | MaxMessageBytes int 19 | TLSConfig *tls.Config 20 | } 21 | 22 | func ListenAndServe(cfg *ServerConfig) error { 23 | s := smtp.NewServer(NewBackend(cfg.Auther, cfg.Handler)) 24 | 25 | SetDefaultServerConfig(cfg) 26 | 27 | s.Addr = cfg.ListenAddr 28 | s.Domain = cfg.BannerDomain 29 | s.ReadTimeout = cfg.ReadTimeout 30 | s.WriteTimeout = cfg.WriteTimeout 31 | s.MaxMessageBytes = cfg.MaxMessageBytes 32 | s.AllowInsecureAuth = true 33 | s.AuthDisabled = true 34 | s.EnableSMTPUTF8 = false 35 | 36 | fmt.Println("⇨ smtp server started on", s.Addr) 37 | 38 | return s.ListenAndServe() 39 | } 40 | 41 | func ListenAndServeTLS(cfg *ServerConfig) error { 42 | s := smtp.NewServer(NewBackend(cfg.Auther, cfg.Handler)) 43 | 44 | SetDefaultServerConfig(cfg) 45 | 46 | s.Addr = cfg.ListenAddr 47 | s.Domain = cfg.BannerDomain 48 | s.ReadTimeout = cfg.ReadTimeout 49 | s.WriteTimeout = cfg.WriteTimeout 50 | s.MaxMessageBytes = cfg.MaxMessageBytes 51 | s.AllowInsecureAuth = true 52 | s.AuthDisabled = true 53 | s.EnableSMTPUTF8 = false 54 | s.EnableREQUIRETLS = true 55 | s.TLSConfig = cfg.TLSConfig 56 | 57 | fmt.Println("⇨ smtp server started on", s.Addr) 58 | 59 | return s.ListenAndServeTLS() 60 | } 61 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ= 2 | github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= 3 | github.com/emersion/go-smtp v0.13.0 h1:aC3Kc21TdfvXnuJXCQXuhnDXUldhc12qME/S7Y3Y94g= 4 | github.com/emersion/go-smtp v0.13.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= 5 | github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg= 6 | github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= 7 | github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= 8 | github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= 9 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 10 | github.com/zaccone/spf v0.0.0-20170817004109-76747b8658d9 h1:NugUf62Z6Yzn//u/MT+cuaFX1AFzfuIR9QVywUQX18E= 11 | github.com/zaccone/spf v0.0.0-20170817004109-76747b8658d9/go.mod h1:AL91TJsHKIaWR16S1IaxTSZfBRMr3/dOdiN1OZ1m9RM= 12 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 13 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= 14 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 15 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 16 | golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= 17 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 18 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 19 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 20 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g= 21 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 22 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 23 | golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 h1:4CSI6oo7cOjJKajidEljs9h+uP0rRZBPPPhcCbj5mw8= 24 | golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 25 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= 26 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 27 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 28 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 29 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 30 | golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M= 31 | golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 32 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 33 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 34 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 35 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 36 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= 37 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 38 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 39 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 40 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 41 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 42 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 43 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 44 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 45 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 46 | golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 47 | golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 h1:BonxutuHCTL0rBDnZlKjpGIQFTjyUVTexFOdWkB6Fg0= 48 | golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 49 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 50 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 51 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 52 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 53 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package smtpsrv 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "mime" 10 | "mime/multipart" 11 | "mime/quotedprintable" 12 | "net/mail" 13 | "strings" 14 | "time" 15 | 16 | "golang.org/x/text/encoding/charmap" 17 | ) 18 | 19 | const contentTypeMultipartMixed = "multipart/mixed" 20 | const contentTypeMultipartAlternative = "multipart/alternative" 21 | const contentTypeMultipartRelated = "multipart/related" 22 | const contentTypeTextHtml = "text/html" 23 | const contentTypeTextPlain = "text/plain" 24 | 25 | // Parse an email message read from io.Reader into parsemail.Email struct 26 | func ParseEmail(r io.Reader) (email *Email, err error) { 27 | msg, err := mail.ReadMessage(r) 28 | if err != nil { 29 | return 30 | } 31 | 32 | email, err = createEmailFromHeader(msg.Header) 33 | if err != nil { 34 | return 35 | } 36 | 37 | email.ContentType = msg.Header.Get("Content-Type") 38 | contentType, params, err := parseContentType(email.ContentType) 39 | if err != nil { 40 | return 41 | } 42 | 43 | 44 | switch contentType { 45 | case contentTypeMultipartMixed: 46 | email.TextBody, email.HTMLBody, email.Attachments, email.EmbeddedFiles, err = parseMultipartMixed(msg.Body, params["boundary"]) 47 | case contentTypeMultipartAlternative: 48 | email.TextBody, email.HTMLBody, email.EmbeddedFiles, err = parseMultipartAlternative(msg.Body, params["boundary"]) 49 | case contentTypeMultipartRelated: 50 | email.TextBody, email.HTMLBody, email.EmbeddedFiles, err = parseMultipartRelated(msg.Body, params["boundary"]) 51 | case contentTypeTextPlain: 52 | newPart, err := decodeContent(msg.Body, msg.Header.Get("Content-Transfer-Encoding"), msg.Header.Get("Content-Type")) 53 | if err != nil { 54 | return email, err 55 | } 56 | 57 | message, _ := ioutil.ReadAll(newPart) 58 | email.TextBody = strings.TrimSuffix(string(message[:]), "\n") 59 | case contentTypeTextHtml: 60 | newPart, err := decodeContent(msg.Body, msg.Header.Get("Content-Transfer-Encoding"), msg.Header.Get("Content-Type")) 61 | if err != nil { 62 | return email, err 63 | } 64 | 65 | message, err := ioutil.ReadAll(newPart) 66 | if err != nil { 67 | return email, err 68 | } 69 | 70 | email.HTMLBody = strings.TrimSuffix(string(message[:]), "\n") 71 | default: 72 | email.Content, err = decodeContent(msg.Body, msg.Header.Get("Content-Transfer-Encoding"), msg.Header.Get("Content-Type")) 73 | } 74 | 75 | return 76 | } 77 | 78 | func createEmailFromHeader(header mail.Header) (email *Email, err error) { 79 | hp := headerParser{header: &header} 80 | 81 | email = &Email{} 82 | email.Subject = decodeMimeSentence(header.Get("Subject")) 83 | email.From = hp.parseAddressList(header.Get("From")) 84 | email.Sender = hp.parseAddress(header.Get("Sender")) 85 | email.ReplyTo = hp.parseAddressList(header.Get("Reply-To")) 86 | email.To = hp.parseAddressList(header.Get("To")) 87 | email.Cc = hp.parseAddressList(header.Get("Cc")) 88 | email.Bcc = hp.parseAddressList(header.Get("Bcc")) 89 | email.Date = hp.parseTime(header.Get("Date")) 90 | email.ResentFrom = hp.parseAddressList(header.Get("Resent-From")) 91 | email.ResentSender = hp.parseAddress(header.Get("Resent-Sender")) 92 | email.ResentTo = hp.parseAddressList(header.Get("Resent-To")) 93 | email.ResentCc = hp.parseAddressList(header.Get("Resent-Cc")) 94 | email.ResentBcc = hp.parseAddressList(header.Get("Resent-Bcc")) 95 | email.ResentMessageID = hp.parseMessageId(header.Get("Resent-Message-ID")) 96 | email.MessageID = hp.parseMessageId(header.Get("Message-ID")) 97 | email.InReplyTo = hp.parseMessageIdList(header.Get("In-Reply-To")) 98 | email.References = hp.parseMessageIdList(header.Get("References")) 99 | email.ResentDate = hp.parseTime(header.Get("Resent-Date")) 100 | 101 | if hp.err != nil { 102 | err = hp.err 103 | return 104 | } 105 | 106 | //decode whole header for easier access to extra fields 107 | //todo: should we decode? aren't only standard fields mime encoded? 108 | email.Header, err = decodeHeaderMime(header) 109 | if err != nil { 110 | return 111 | } 112 | 113 | return 114 | } 115 | 116 | func parseContentType(contentTypeHeader string) (contentType string, params map[string]string, err error) { 117 | if contentTypeHeader == "" { 118 | contentType = contentTypeTextPlain 119 | return 120 | } 121 | 122 | return mime.ParseMediaType(contentTypeHeader) 123 | } 124 | 125 | func parseMultipartRelated(msg io.Reader, boundary string) (textBody, htmlBody string, embeddedFiles []EmbeddedFile, err error) { 126 | pmr := multipart.NewReader(msg, boundary) 127 | for { 128 | part, err := pmr.NextPart() 129 | 130 | if err == io.EOF { 131 | break 132 | } else if err != nil { 133 | return textBody, htmlBody, embeddedFiles, err 134 | } 135 | 136 | contentType, params, err := mime.ParseMediaType(part.Header.Get("Content-Type")) 137 | if err != nil { 138 | return textBody, htmlBody, embeddedFiles, err 139 | } 140 | 141 | switch contentType { 142 | case contentTypeTextPlain: 143 | ppContent, err := ioutil.ReadAll(part) 144 | if err != nil { 145 | return textBody, htmlBody, embeddedFiles, err 146 | } 147 | 148 | textBody += strings.TrimSuffix(string(ppContent[:]), "\n") 149 | case contentTypeTextHtml: 150 | ppContent, err := ioutil.ReadAll(part) 151 | if err != nil { 152 | return textBody, htmlBody, embeddedFiles, err 153 | } 154 | 155 | htmlBody += strings.TrimSuffix(string(ppContent[:]), "\n") 156 | case contentTypeMultipartAlternative: 157 | tb, hb, ef, err := parseMultipartAlternative(part, params["boundary"]) 158 | if err != nil { 159 | return textBody, htmlBody, embeddedFiles, err 160 | } 161 | 162 | htmlBody += hb 163 | textBody += tb 164 | embeddedFiles = append(embeddedFiles, ef...) 165 | default: 166 | if isEmbeddedFile(part) { 167 | ef, err := decodeEmbeddedFile(part) 168 | if err != nil { 169 | return textBody, htmlBody, embeddedFiles, err 170 | } 171 | 172 | embeddedFiles = append(embeddedFiles, ef) 173 | } else { 174 | return textBody, htmlBody, embeddedFiles, fmt.Errorf("Can't process multipart/related inner mime type: %s", contentType) 175 | } 176 | } 177 | } 178 | 179 | return textBody, htmlBody, embeddedFiles, err 180 | } 181 | 182 | func decodeCharset(content io.Reader, contentTypeWithCharset string) (io.Reader) { 183 | 184 | charset := "default" 185 | if strings.Contains(contentTypeWithCharset, "; charset=") { 186 | split := strings.Split(contentTypeWithCharset, "; charset=") 187 | charset = strings.Trim(split[1], " \"'\n\r") 188 | } 189 | 190 | tr := content 191 | if charset != "default" { 192 | switch charset { 193 | case "Windows-1252": 194 | tr = charmap.Windows1252.NewDecoder().Reader(content) 195 | case "iso-8859-1", "ISO-8859-1": 196 | tr = charmap.ISO8859_1.NewDecoder().Reader(content) 197 | default: 198 | } 199 | } 200 | 201 | return tr 202 | } 203 | 204 | func parseMultipartAlternative(msg io.Reader, boundary string) (textBody, htmlBody string, embeddedFiles []EmbeddedFile, err error) { 205 | pmr := multipart.NewReader(msg, boundary) 206 | for { 207 | part, err := pmr.NextPart() 208 | 209 | if err == io.EOF { 210 | break 211 | } else if err != nil { 212 | return textBody, htmlBody, embeddedFiles, err 213 | } 214 | 215 | contentType, params, err := mime.ParseMediaType(part.Header.Get("Content-Type")) 216 | if err != nil { 217 | return textBody, htmlBody, embeddedFiles, err 218 | } 219 | 220 | switch contentType { 221 | case contentTypeTextPlain: 222 | newPart, err := decodeContent(part, part.Header.Get("Content-Transfer-Encoding"), part.Header.Get("Content-Type")) 223 | if err != nil { 224 | return textBody, htmlBody, embeddedFiles, err 225 | } 226 | 227 | ppContent, err := ioutil.ReadAll(newPart) 228 | if err != nil { 229 | return textBody, htmlBody, embeddedFiles, err 230 | } 231 | 232 | textBody += strings.TrimSuffix(string(ppContent[:]), "\n") 233 | 234 | case contentTypeTextHtml: 235 | newPart, err := decodeContent(part, part.Header.Get("Content-Transfer-Encoding"), part.Header.Get("Content-Type")) 236 | if err != nil { 237 | return textBody, htmlBody, embeddedFiles, err 238 | } 239 | 240 | ppContent, err := ioutil.ReadAll(newPart) 241 | if err != nil { 242 | return textBody, htmlBody, embeddedFiles, err 243 | } 244 | 245 | htmlBody += strings.TrimSuffix(string(ppContent[:]), "\n") 246 | 247 | case contentTypeMultipartRelated: 248 | tb, hb, ef, err := parseMultipartRelated(part, params["boundary"]) 249 | if err != nil { 250 | return textBody, htmlBody, embeddedFiles, err 251 | } 252 | 253 | htmlBody += hb 254 | textBody += tb 255 | embeddedFiles = append(embeddedFiles, ef...) 256 | 257 | default: 258 | if isEmbeddedFile(part) { 259 | ef, err := decodeEmbeddedFile(part) 260 | if err != nil { 261 | return textBody, htmlBody, embeddedFiles, err 262 | } 263 | 264 | embeddedFiles = append(embeddedFiles, ef) 265 | } else { 266 | return textBody, htmlBody, embeddedFiles, fmt.Errorf("Can't process multipart/alternative inner mime type: %s", contentType) 267 | } 268 | } 269 | } 270 | 271 | return textBody, htmlBody, embeddedFiles, err 272 | } 273 | 274 | func parseMultipartMixed(msg io.Reader, boundary string) (textBody, htmlBody string, attachments []Attachment, embeddedFiles []EmbeddedFile, err error) { 275 | mr := multipart.NewReader(msg, boundary) 276 | for { 277 | part, err := mr.NextPart() 278 | if err == io.EOF { 279 | break 280 | } else if err != nil { 281 | return textBody, htmlBody, attachments, embeddedFiles, err 282 | } 283 | 284 | contentType, params, err := mime.ParseMediaType(part.Header.Get("Content-Type")) 285 | if err != nil { 286 | return textBody, htmlBody, attachments, embeddedFiles, err 287 | } 288 | 289 | if contentType == contentTypeMultipartAlternative { 290 | textBody, htmlBody, embeddedFiles, err = parseMultipartAlternative(part, params["boundary"]) 291 | if err != nil { 292 | return textBody, htmlBody, attachments, embeddedFiles, err 293 | } 294 | } else if contentType == contentTypeMultipartRelated { 295 | textBody, htmlBody, embeddedFiles, err = parseMultipartRelated(part, params["boundary"]) 296 | if err != nil { 297 | return textBody, htmlBody, attachments, embeddedFiles, err 298 | } 299 | } else if contentType == contentTypeTextPlain { 300 | newPart, err := decodeContent(part, part.Header.Get("Content-Transfer-Encoding"), part.Header.Get("Content-Type")) 301 | if err != nil { 302 | return textBody, htmlBody, attachments, embeddedFiles, err 303 | } 304 | 305 | ppContent, err := ioutil.ReadAll(newPart) 306 | if err != nil { 307 | return textBody, htmlBody, attachments, embeddedFiles, err 308 | } 309 | 310 | textBody += strings.TrimSuffix(string(ppContent[:]), "\n") 311 | } else if contentType == contentTypeTextHtml { 312 | newPart, err := decodeContent(part, part.Header.Get("Content-Transfer-Encoding"), part.Header.Get("Content-Type")) 313 | if err != nil { 314 | return textBody, htmlBody, attachments, embeddedFiles, err 315 | } 316 | 317 | ppContent, err := ioutil.ReadAll(newPart) 318 | if err != nil { 319 | return textBody, htmlBody, attachments, embeddedFiles, err 320 | } 321 | 322 | htmlBody += strings.TrimSuffix(string(ppContent[:]), "\n") 323 | } else if isAttachment(part) { 324 | at, err := decodeAttachment(part) 325 | if err != nil { 326 | return textBody, htmlBody, attachments, embeddedFiles, err 327 | } 328 | 329 | attachments = append(attachments, at) 330 | } else { 331 | return textBody, htmlBody, attachments, embeddedFiles, fmt.Errorf("Unknown multipart/mixed nested mime type: %s", contentType) 332 | } 333 | } 334 | 335 | return textBody, htmlBody, attachments, embeddedFiles, err 336 | } 337 | 338 | func decodeMimeSentence(s string) string { 339 | result := []string{} 340 | ss := strings.Split(s, " ") 341 | 342 | for _, word := range ss { 343 | dec := new(mime.WordDecoder) 344 | w, err := dec.Decode(word) 345 | if err != nil { 346 | if len(result) == 0 { 347 | w = word 348 | } else { 349 | w = " " + word 350 | } 351 | } 352 | 353 | result = append(result, w) 354 | } 355 | 356 | return strings.Join(result, "") 357 | } 358 | 359 | func decodeHeaderMime(header mail.Header) (mail.Header, error) { 360 | parsedHeader := map[string][]string{} 361 | 362 | for headerName, headerData := range header { 363 | 364 | parsedHeaderData := []string{} 365 | for _, headerValue := range headerData { 366 | parsedHeaderData = append(parsedHeaderData, decodeMimeSentence(headerValue)) 367 | } 368 | 369 | parsedHeader[headerName] = parsedHeaderData 370 | } 371 | 372 | return mail.Header(parsedHeader), nil 373 | } 374 | 375 | func isEmbeddedFile(part *multipart.Part) bool { 376 | return part.Header.Get("Content-Transfer-Encoding") != "" 377 | } 378 | 379 | func decodeEmbeddedFile(part *multipart.Part) (ef EmbeddedFile, err error) { 380 | cid := decodeMimeSentence(part.Header.Get("Content-Id")) 381 | decoded, err := decodeContent(part, part.Header.Get("Content-Transfer-Encoding"), part.Header.Get("Content-Type")) 382 | if err != nil { 383 | return 384 | } 385 | 386 | ef.CID = strings.Trim(cid, "<>") 387 | ef.Data = decoded 388 | ef.ContentType = part.Header.Get("Content-Type") 389 | 390 | return 391 | } 392 | 393 | func isAttachment(part *multipart.Part) bool { 394 | return part.FileName() != "" 395 | } 396 | 397 | func decodeAttachment(part *multipart.Part) (at Attachment, err error) { 398 | filename := decodeMimeSentence(part.FileName()) 399 | decoded, err := decodeContent(part, part.Header.Get("Content-Transfer-Encoding"), part.Header.Get("Content-Type")) 400 | if err != nil { 401 | return 402 | } 403 | 404 | at.Filename = filename 405 | at.Data = decoded 406 | at.ContentType = strings.Split(part.Header.Get("Content-Type"), ";")[0] 407 | 408 | return 409 | } 410 | 411 | func decodeContent(content io.Reader, encoding string, contentTypeWithCharset string) (io.Reader, error) { 412 | 413 | switch encoding { 414 | case "base64": 415 | decoded := base64.NewDecoder(base64.StdEncoding, content) 416 | b, err := ioutil.ReadAll(decoded) 417 | if err != nil { 418 | return nil, err 419 | } 420 | 421 | return decodeCharset(bytes.NewReader(b), contentTypeWithCharset), nil 422 | 423 | case "7bit": 424 | dd, err := ioutil.ReadAll(content) 425 | if err != nil { 426 | return nil, err 427 | } 428 | 429 | return decodeCharset(bytes.NewReader(dd), contentTypeWithCharset), nil 430 | 431 | case "quoted-printable": 432 | decoded := quotedprintable.NewReader(content) 433 | b, err := ioutil.ReadAll(decoded) 434 | if err != nil { 435 | return nil, err 436 | } 437 | 438 | return decodeCharset(bytes.NewReader(b), contentTypeWithCharset), nil 439 | 440 | case "": 441 | return decodeCharset(content, contentTypeWithCharset), nil 442 | 443 | default: 444 | return nil, fmt.Errorf("unknown encoding: %s", encoding) 445 | } 446 | } 447 | 448 | type headerParser struct { 449 | header *mail.Header 450 | err error 451 | } 452 | 453 | func (hp headerParser) parseAddress(s string) (ma *mail.Address) { 454 | if hp.err != nil { 455 | return nil 456 | } 457 | 458 | if strings.Trim(s, " \n") != "" { 459 | ma, hp.err = mail.ParseAddress(s) 460 | 461 | return ma 462 | } 463 | 464 | return nil 465 | } 466 | 467 | func (hp headerParser) parseAddressList(s string) (ma []*mail.Address) { 468 | if hp.err != nil { 469 | return 470 | } 471 | 472 | if strings.Trim(s, " \n") != "" { 473 | ma, hp.err = mail.ParseAddressList(s) 474 | return 475 | } 476 | 477 | return 478 | } 479 | 480 | func (hp headerParser) parseTime(s string) (t time.Time) { 481 | if hp.err != nil || s == "" { 482 | return 483 | } 484 | 485 | formats := []string{ 486 | time.RFC1123Z, 487 | "Mon, 2 Jan 2006 15:04:05 -0700", 488 | time.RFC1123Z + " (MST)", 489 | "Mon, 2 Jan 2006 15:04:05 -0700 (MST)", 490 | } 491 | 492 | for _, format := range formats { 493 | t, hp.err = time.Parse(format, s) 494 | if hp.err == nil { 495 | return 496 | } 497 | } 498 | 499 | return 500 | } 501 | 502 | func (hp headerParser) parseMessageId(s string) string { 503 | if hp.err != nil { 504 | return "" 505 | } 506 | 507 | return strings.Trim(s, "<> ") 508 | } 509 | 510 | func (hp headerParser) parseMessageIdList(s string) (result []string) { 511 | if hp.err != nil { 512 | return 513 | } 514 | 515 | for _, p := range strings.Split(s, " ") { 516 | if strings.Trim(p, " \n") != "" { 517 | result = append(result, hp.parseMessageId(p)) 518 | } 519 | } 520 | 521 | return 522 | } 523 | 524 | // Attachment with filename, content type and data (as a io.Reader) 525 | type Attachment struct { 526 | Filename string 527 | ContentType string 528 | Data io.Reader 529 | } 530 | 531 | // EmbeddedFile with content id, content type and data (as a io.Reader) 532 | type EmbeddedFile struct { 533 | CID string 534 | ContentType string 535 | Data io.Reader 536 | } 537 | 538 | // Email with fields for all the headers defined in RFC5322 with it's attachments and 539 | type Email struct { 540 | Header mail.Header 541 | 542 | Subject string 543 | Sender *mail.Address 544 | From []*mail.Address 545 | ReplyTo []*mail.Address 546 | To []*mail.Address 547 | Cc []*mail.Address 548 | Bcc []*mail.Address 549 | Date time.Time 550 | MessageID string 551 | InReplyTo []string 552 | References []string 553 | 554 | ResentFrom []*mail.Address 555 | ResentSender *mail.Address 556 | ResentTo []*mail.Address 557 | ResentDate time.Time 558 | ResentCc []*mail.Address 559 | ResentBcc []*mail.Address 560 | ResentMessageID string 561 | 562 | ContentType string 563 | Content io.Reader 564 | 565 | HTMLBody string 566 | TextBody string 567 | 568 | Attachments []Attachment 569 | EmbeddedFiles []EmbeddedFile 570 | } 571 | --------------------------------------------------------------------------------