From d1d71c86144317caac22a2a4c497566ccd6cc7e7 Mon Sep 17 00:00:00 2001 From: arzumify Date: Thu, 21 Nov 2024 19:34:13 +0000 Subject: [PATCH] Initial commit --- .gitignore | 1 + LICENSE.md | 157 +++++++++++ README.md | 5 + go.mod | 8 + go.sum | 4 + smtp.go | 792 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 967 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 smtp.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..969dabb --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,157 @@ +# GNU LESSER GENERAL PUBLIC LICENSE + +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +This version of the GNU Lesser General Public License incorporates the +terms and conditions of version 3 of the GNU General Public License, +supplemented by the additional permissions listed below. + +## 0. Additional Definitions. + +As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the +GNU General Public License. + +"The Library" refers to a covered work governed by this License, other +than an Application or a Combined Work as defined below. + +An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + +A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + +The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + +The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + +## 1. Exception to Section 3 of the GNU GPL. + +You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + +## 2. Conveying Modified Versions. + +If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + +- a) under this License, provided that you make a good faith effort + to ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or +- b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + +## 3. Object Code Incorporating Material from Library Header Files. + +The object code form of an Application may incorporate material from a +header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + +- a) Give prominent notice with each copy of the object code that + the Library is used in it and that the Library and its use are + covered by this License. +- b) Accompany the object code with a copy of the GNU GPL and this + license document. + +## 4. Combined Works. + +You may convey a Combined Work under terms of your choice that, taken +together, effectively do not restrict modification of the portions of +the Library contained in the Combined Work and reverse engineering for +debugging such modifications, if you also do each of the following: + +- a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. +- b) Accompany the Combined Work with a copy of the GNU GPL and this + license document. +- c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. +- d) Do one of the following: + - 0) Convey the Minimal Corresponding Source under the terms of + this License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + - 1) Use a suitable shared library mechanism for linking with + the Library. A suitable mechanism is one that (a) uses at run + time a copy of the Library already present on the user's + computer system, and (b) will operate properly with a modified + version of the Library that is interface-compatible with the + Linked Version. +- e) Provide Installation Information, but only if you would + otherwise be required to provide such information under section 6 + of the GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the Application + with a modified version of the Linked Version. (If you use option + 4d0, the Installation Information must accompany the Minimal + Corresponding Source and Corresponding Application Code. If you + use option 4d1, you must provide the Installation Information in + the manner specified by section 6 of the GNU GPL for conveying + Corresponding Source.) + +## 5. Combined Libraries. + +You may place library facilities that are a work based on the Library +side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + +- a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities, conveyed under the terms of this License. +- b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + +## 6. Revised Versions of the GNU Lesser General Public License. + +The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +as you received it specifies that a certain numbered version of the +GNU Lesser General Public License "or any later version" applies to +it, you have the option of following the terms and conditions either +of that published version or of any later version published by the +Free Software Foundation. If the Library as you received it does not +specify a version number of the GNU Lesser General Public License, you +may choose any version of the GNU Lesser General Public License ever +published by the Free Software Foundation. + +If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7617c7b --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# SMTP + +A comprehensive library for sending and receiving emails using the SMTP protocol, written in Go. + +[![Go Report Card](https://goreportcard.com/badge/git.ailur.dev/ailur/smtp)](https://goreportcard.com/report/git.ailur.dev/ailur/smtp) [![Go Reference](https://pkg.go.dev/badge/git.ailur.dev/ailur/smtp.svg)](https://pkg.go.dev/git.ailur.dev/ailur/smtp) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a98c213 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module smtp + +go 1.23 + +require ( + git.ailur.dev/ailur/spf v1.0.1 + github.com/google/uuid v1.6.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2180cf9 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +git.ailur.dev/ailur/spf v1.0.1 h1:ApkuF2YsQJgUMo0I4cmQcHBERXZ+ZspOkqMe2lyaUfk= +git.ailur.dev/ailur/spf v1.0.1/go.mod h1:j+l6sReELJT3VCyAt/DgOfNqNYU/AvzJvj5vgLt3WGo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/smtp.go b/smtp.go new file mode 100644 index 0000000..403e598 --- /dev/null +++ b/smtp.go @@ -0,0 +1,792 @@ +package smtp + +import ( + "errors" + "fmt" + "io" + "net" + "strconv" + "strings" + + "crypto/tls" + "net/textproto" + + "git.ailur.dev/ailur/spf" + "github.com/google/uuid" +) + +var ( + defaultCapabilities = []string{ + "250-8BITMIME", + "250-ENHANCEDSTATUSCODES", + "250-SMTPUTF8", + "250 BINARYMIME", + } + queue = make(map[uuid.UUID]*MailQueueItem) +) + +// MailQueueItem is a struct that represents an item in the mail queue +type MailQueueItem struct { + From *Address + To []*Address + Host string +} + +// ViewMailQueue returns the current mail queue +func ViewMailQueue() map[uuid.UUID]*MailQueueItem { + return queue +} + +// Address is a struct that represents an email address +type Address struct { + Name string + Address string +} + +// Mail is a struct that represents an email +type Mail struct { + From *Address + To []*Address + Data []byte +} + +// DatabaseBackend is a struct that represents a database backend +type DatabaseBackend struct { + CheckUser func(*Address) (bool, error) + WriteMail func(*Mail) (uuid.UUID, error) +} + +// AuthenticationBackend is a struct that represents an authentication backend +type AuthenticationBackend struct { + Authenticate func(string) (*Address, error) +} + +func readMultilineCodeResponse(conn *textproto.Conn) (int, string, error) { + var lines strings.Builder + for { + line, err := conn.ReadLine() + if err != nil { + return 0, "", err + } + + lines.WriteString(line) + + code, err := strconv.Atoi(line[:3]) + if err != nil { + return 0, "", err + } + + if line[3] != '-' { + return code, lines.String(), nil + } + } +} + +func systemError(err error, receiver *Address, database DatabaseBackend) { + _, _ = database.WriteMail(&Mail{ + From: &Address{ + Name: "EMail System", + Address: "system", + }, + To: []*Address{receiver}, + Data: []byte(fmt.Sprintf("Hello there. This is the EMail system.\n We're sorry, but an error occurred while trying to send your email. The error was: %s. The email has not been sent.", err.Error())), + }) +} + +func sendEmail(args SenderArgs, mail *Mail, database DatabaseBackend, queueID uuid.UUID) { + mxs, err := net.LookupMX(mail.To[0].Address) + if err != nil { + systemError(err, queue[queueID].From, database) + delete(queue, queueID) + return + } + + ips, err := net.LookupIP(mxs[0].Host) + if err != nil { + systemError(err, queue[queueID].From, database) + delete(queue, queueID) + return + } + + conn, err := net.Dial("tcp", ips[0].String()+":25") + if err != nil { + systemError(err, queue[queueID].From, database) + delete(queue, queueID) + return + } + + err = Send(args, mail, conn, mxs[0].Host) + if err != nil { + systemError(err, queue[queueID].From, database) + delete(queue, queueID) + return + } + + err = conn.Close() + if err != nil { + systemError(err, queue[queueID].From, database) + delete(queue, queueID) + return + } + + delete(queue, queueID) +} + +func speakMultiLine(conn *textproto.Conn, lines []string) error { + for _, line := range lines { + err := conn.PrintfLine(line) + if err != nil { + return err + } + } + + return nil +} + +// Receiver is a struct that represents an SMTP receiver +type Receiver struct { + underlyingListener net.Listener + hostname string + ownedDomains map[string]any + enforceTLS bool + tlsConfig *tls.Config + database DatabaseBackend + auth AuthenticationBackend +} + +// NewReceiver creates a new Receiver +func NewReceiver(conn net.Listener, hostname string, ownedDomains []string, enforceTLS bool, database DatabaseBackend, authentication AuthenticationBackend, tlsConfig *tls.Config) *Receiver { + var ownedDomainsMap = make(map[string]any) + for _, domain := range ownedDomains { + ownedDomainsMap[domain] = nil + } + return &Receiver{ + underlyingListener: conn, + hostname: hostname, + ownedDomains: ownedDomainsMap, + enforceTLS: enforceTLS, + tlsConfig: tlsConfig, + database: database, + auth: authentication, + } +} + +// Close closes the connection to the Receiver +func (fr *Receiver) Close() error { + return fr.underlyingListener.Close() +} + +// Serve serves the Receiver. It will always return a non-nil error +func (fr *Receiver) Serve() error { + for { + conn, err := fr.underlyingListener.Accept() + if err != nil { + return err + } + + go fr.handleConnection(conn) + } +} + +func (fr *Receiver) handleConnection(conn net.Conn) { + var state struct { + HELO bool + AUTH *Address + TLS bool + FROM *Address + RCPT []*Address + DATA []byte + } + + submissionSlice := strings.Split(conn.LocalAddr().String(), ":") + isSubmission := submissionSlice[len(submissionSlice)-1] != "25" + textProto := textproto.NewConn(conn) + + err := textProto.PrintfLine("220 %s ESMTP At your service", fr.hostname) + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + + fmt.Println("Connection from", conn.RemoteAddr().String()) + + for { + line, err := textProto.ReadLine() + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + + switch { + case strings.HasPrefix(line, "QUIT"): + err = textProto.PrintfLine("221 2.0.0 See you soon") + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + } + _ = conn.Close() + return + case strings.HasPrefix(line, "RSET"): + state.HELO = false + state.AUTH = nil + state.FROM = nil + state.RCPT = nil + state.DATA = nil + err = textProto.PrintfLine("250 2.0.0 Connection reset") + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + case strings.HasPrefix(line, "NOOP"): + err = textProto.PrintfLine("250 2.0.0 Take your time") + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + case strings.HasPrefix(line, "HELO"): + if state.HELO { + err = textProto.PrintfLine("503 5.5.1 HELO already called") + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + continue + } else { + state.HELO = true + err = textProto.PrintfLine("250 %s, ready to receive mail", fr.hostname) + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + } + case strings.HasPrefix(line, "EHLO"): + if state.HELO { + err = textProto.PrintfLine("503 5.5.1 EHLO already called") + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + continue + } else { + var capabilities []string + if fr.tlsConfig != nil { + capabilities = append(capabilities, "250-STARTTLS") + } + if fr.enforceTLS { + capabilities = append(capabilities, "250-REQUIRETLS") + } + capabilities = append(capabilities, defaultCapabilities...) + state.HELO = true + err = speakMultiLine(textProto, capabilities) + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + } + case strings.HasPrefix(line, "AUTH"): + if !isSubmission { + err = textProto.PrintfLine("503 5.5.1 AUTH only allowed on submission port") + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + continue + } + + if !state.TLS && fr.enforceTLS { + err = textProto.PrintfLine("530 5.7.0 Must issue a STARTTLS command first") + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + continue + } + + if state.AUTH != nil { + err = textProto.PrintfLine("503 5.5.1 AUTH already called") + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + continue + } else { + address, err := fr.auth.Authenticate(line) + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + + if address == nil { + err = textProto.PrintfLine("535 5.7.8 Authentication failed") + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + } else { + state.AUTH = address + err = textProto.PrintfLine("235 2.7.0 Authentication successful") + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + } + } + case strings.HasPrefix(line, "STARTTLS"): + if state.TLS { + err = textProto.PrintfLine("503 5.5.1 STARTTLS already called") + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + continue + } else { + err = textProto.PrintfLine("220 2.0.0 Ready to start TLS") + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + + tlsConn := tls.Server(conn, fr.tlsConfig) + textProto = textproto.NewConn(tlsConn) + + state.HELO = false + state.AUTH = nil + state.FROM = nil + state.RCPT = nil + state.DATA = nil + state.TLS = true + } + case strings.HasPrefix(line, "MAIL FROM"): + if !state.HELO { + err = textProto.PrintfLine("503 5.5.1 HELO required") + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + continue + } + + if !state.TLS && fr.enforceTLS { + err = textProto.PrintfLine("530 5.7.0 Must issue a STARTTLS command first") + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + continue + } + + addressSlice := strings.Split(strings.TrimPrefix(strings.TrimSuffix(line, ">"), "MAIL FROM:<"), "@") + + address := &Address{ + Name: addressSlice[0], + Address: addressSlice[1], + } + + if isSubmission { + if state.AUTH == nil { + err = textProto.PrintfLine("503 5.5.1 AUTH required") + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + continue + } + + if *address != *state.AUTH { + err = textProto.PrintfLine("535 5.7.8 Authenticated wrong user") + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + continue + } + } else { + err := spf.CheckIP(strings.Split(conn.RemoteAddr().String(), ":")[0], address.Address) + if err != nil { + if err.Type() == spf.ErrTypeInternal { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } else { + err := textProto.PrintfLine("550 5.7.1 SPF check failed") + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + continue + } + } + } + + state.FROM = address + err = textProto.PrintfLine("250 2.1.0 Sender OK") + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + case strings.HasPrefix(line, "RCPT TO"): + if !state.TLS && fr.enforceTLS { + err = textProto.PrintfLine("530 5.7.0 Must issue a STARTTLS command first") + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + continue + } + + addressSlice := strings.Split(strings.TrimPrefix(strings.TrimSuffix(line, ">"), "RCPT TO:<"), "@") + address := &Address{ + Name: addressSlice[0], + Address: addressSlice[1], + } + + if isSubmission { + if state.AUTH == nil { + err = textProto.PrintfLine("503 5.5.1 AUTH required") + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + continue + } + } else { + _, ok := fr.ownedDomains[address.Address] + if !ok { + err = textProto.PrintfLine("503 5.5.1 Relaying not allowed") + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + continue + } + + ok, err := fr.database.CheckUser(address) + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + + if !ok { + err = textProto.PrintfLine("550 5.1.1 User not found") + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + continue + + } + } + + state.RCPT = append(state.RCPT, address) + err = textProto.PrintfLine("250 2.1.5 Recipient OK") + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + case strings.HasPrefix(line, "DATA"): + if !state.TLS && fr.enforceTLS { + err = textProto.PrintfLine("530 5.7.0 Must issue a STARTTLS command first") + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + continue + } + + if state.FROM == nil { + err = textProto.PrintfLine("503 5.5.1 MAIL FROM required") + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + continue + } + + if len(state.RCPT) == 0 { + err = textProto.PrintfLine("503 5.5.1 RCPT TO required") + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + continue + } + + err = textProto.PrintfLine("354 2.0.0 Start mail input; end with .") + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + + state.DATA, err = io.ReadAll(textProto.DotReader()) + + mail := &Mail{ + From: state.FROM, + To: state.RCPT, + Data: state.DATA, + } + + if !isSubmission { + _, err := fr.database.WriteMail(mail) + if err != nil { + _ = textProto.PrintfLine(err.Error()) + _ = conn.Close() + return + } + + err = textProto.PrintfLine("250 2.0.0 Message accepted for delivery") + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + } else { + queueID := uuid.New() + + queue[queueID] = &MailQueueItem{ + From: state.FROM, + To: state.RCPT, + Host: strings.Split(conn.RemoteAddr().String(), ":")[0], + } + go sendEmail(SenderArgs{ + Hostname: fr.hostname, + EnforceTLS: fr.enforceTLS, + }, mail, fr.database, queueID) + + err = textProto.PrintfLine("250 2.0.0 Message queued for delivery") + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + + state.DATA = nil + state.FROM = nil + state.RCPT = nil + } + default: + err = textProto.PrintfLine("500 5.5.2 Command not recognized") + if err != nil { + _ = textProto.PrintfLine("421 4.7.0 Temporary server error") + _ = conn.Close() + return + } + } + } + +} + +// SenderArgs is a struct that represents the arguments for the Sender +type SenderArgs struct { + Hostname string + EnforceTLS bool +} + +// Send sends an email to another server +func Send(args SenderArgs, mail *Mail, conn net.Conn, mxHost string) (err error) { + textConn := textproto.NewConn(conn) + + err = textConn.PrintfLine("RSET") + if err != nil { + return err + } + + code, line, err := textConn.ReadCodeLine(0) + if err != nil { + return err + } + + if code != 220 { + return errors.New("unexpected RSET response - " + line) + } + + code, line, err = textConn.ReadCodeLine(0) + if err != nil { + return err + } + + if code != 250 { + return errors.New("unexpected greeting - " + line) + } + + err = textConn.PrintfLine("EHLO %s", args.Hostname) + if err != nil { + return err + } + + code, lines, err := readMultilineCodeResponse(textConn) + if err != nil { + return err + } + + if code != 250 { + return errors.New("unexpected EHLO response - " + lines) + } + + if strings.Contains(lines, "STARTTLS") { + err = textConn.PrintfLine("STARTTLS") + if err != nil { + return err + } + + code, line, err := textConn.ReadCodeLine(0) + if err != nil { + return err + } + + if code != 220 { + return errors.New("unexpected STARTTLS response - " + line) + } + + tlsConn := tls.Client(conn, &tls.Config{ + ServerName: mxHost, + InsecureSkipVerify: false, + }) + + err = tlsConn.Handshake() + if err != nil { + return err + } + + textConn = textproto.NewConn(tlsConn) + + // Just use HELO, no point using EHLO when we already have all the capabilities + // This also gets us out of using readMultilineCodeResponse + err = textConn.PrintfLine("HELO %s", args.Hostname) + if err != nil { + return err + } + + code, line, err = textConn.ReadCodeLine(0) + if err != nil { + return err + } + + if code != 250 { + return errors.New("unexpected STARTTLS HELO response - " + line) + } + } else if args.EnforceTLS { + return errors.New("STARTTLS not supported") + } + + err = textConn.PrintfLine("MAIL FROM:<%s@%s>", mail.From.Name, mail.From.Address) + if err != nil { + return err + } + + code, line, err = textConn.ReadCodeLine(0) + fmt.Println(code, line, err) + if err != nil { + // For some reason the EHLO stuff ends up here + fmt.Println("5") + return err + } + + if code != 250 { + return errors.New("unexpected MAIL FROM response - " + line) + } + + for _, recipient := range mail.To { + err = textConn.PrintfLine("RCPT TO:<%s@%s>", recipient.Name, recipient.Address) + if err != nil { + return err + } + + code, line, err = textConn.ReadCodeLine(0) + fmt.Println(code, line, err) + if err != nil { + fmt.Println("6") + return err + } + + if code != 250 { + return errors.New("unexpected RCPT TO response - " + line) + } + } + + err = textConn.PrintfLine("DATA") + if err != nil { + return err + } + + code, line, err = textConn.ReadCodeLine(0) + fmt.Println(code, line, err) + if err != nil { + fmt.Println("7") + return err + } + + if code != 354 { + return errors.New("unexpected DATA response - " + line) + } + + writer := textConn.DotWriter() + _, err = writer.Write(mail.Data) + if err != nil { + return err + } + + err = writer.Close() + if err != nil { + return errors.New("failed to close data writer - " + err.Error()) + } + + code, line, err = textConn.ReadCodeLine(0) + fmt.Println(code, line, err) + if err != nil { + fmt.Println("8") + return err + } + + if code != 250 { + return errors.New("unexpected DATA finish response - " + line + ", your message may have been sent, but it is not guaranteed") + } + + err = textConn.PrintfLine("QUIT") + if err != nil { + return err + } + + code, line, err = textConn.ReadCodeLine(0) + fmt.Println(code, line, err) + if err != nil { + fmt.Println("9") + return err + } + + if code != 221 { + return errors.New("unexpected QUIT response - " + line + ", your message may have been sent, but it is not guaranteed") + } + + return +}