├── .gitignore ├── go.mod ├── test.php └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | gomail -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/eatonphil/gomail 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /test.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "net" 7 | "strings" 8 | ) 9 | 10 | func logError(err error) { 11 | log.Printf("[ERROR] %s\n", err) 12 | } 13 | 14 | func logInfo(msg string) { 15 | log.Printf("[INFO] %s\n", msg) 16 | } 17 | 18 | type message struct { 19 | clientDomain string 20 | smtpCommands map[string]string 21 | atmHeaders map[string]string 22 | body string 23 | from string 24 | date string 25 | subject string 26 | to string 27 | } 28 | 29 | type connection struct { 30 | conn net.Conn 31 | id int 32 | buf []byte 33 | } 34 | 35 | func (c *connection) logInfo(msg string, args ...interface{}) { 36 | args = append([]interface{}{c.id, c.conn.RemoteAddr().String()}, args...) 37 | log.Printf("[INFO] [%d: %s] "+msg+"\n", args...) 38 | } 39 | 40 | func (c *connection) logError(err error) { 41 | log.Printf("[ERROR] [%d: %s] %s\n", c.id, c.conn.RemoteAddr().String(), err) 42 | } 43 | 44 | func (c *connection) readLine() (string, error) { 45 | for { 46 | b := make([]byte, 1024) 47 | n, err := c.conn.Read(b) 48 | if err != nil { 49 | return "", err 50 | } 51 | 52 | c.buf = append(c.buf, b[:n]...) 53 | for i, b := range c.buf { 54 | // If end of line 55 | if b == '\n' && i > 0 && c.buf[i-1] == '\r' { 56 | // i-1 because drop the CRLF, no one cares after this 57 | line := string(c.buf[:i-1]) 58 | c.buf = c.buf[i+1:] 59 | return line, nil 60 | } 61 | } 62 | } 63 | } 64 | 65 | func (c *connection) readMultiLine() (string, error) { 66 | for { 67 | noMoreReads := false 68 | for i, b := range c.buf { 69 | if i > 1 && 70 | b != ' ' && 71 | b != '\t' && 72 | c.buf[i-2] == '\r' && 73 | c.buf[i-1] == '\n' { 74 | // i-2 because drop the CRLF, no one cares after this 75 | line := string(c.buf[:i-2]) 76 | c.buf = c.buf[i:] 77 | return line, nil 78 | } 79 | 80 | noMoreReads = c.isBodyClose(i) 81 | } 82 | 83 | if !noMoreReads { 84 | b := make([]byte, 1024) 85 | n, err := c.conn.Read(b) 86 | if err != nil { 87 | return "", err 88 | } 89 | 90 | c.buf = append(c.buf, b[:n]...) 91 | 92 | // If this gets here more than once it's going to be an infinite loop 93 | } 94 | } 95 | } 96 | 97 | func (c *connection) isBodyClose(i int) bool { 98 | return i > 4 && 99 | c.buf[i-4] == '\r' && 100 | c.buf[i-3] == '\n' && 101 | c.buf[i-2] == '.' && 102 | c.buf[i-1] == '\r' && 103 | c.buf[i-0] == '\n' 104 | } 105 | 106 | func (c *connection) readToEndOfBody() (string, error) { 107 | for { 108 | for i := range c.buf { 109 | if c.isBodyClose(i) { 110 | return string(c.buf[:i-4]), nil 111 | } 112 | } 113 | 114 | b := make([]byte, 1024) 115 | n, err := c.conn.Read(b) 116 | if err != nil { 117 | return "", err 118 | } 119 | 120 | c.buf = append(c.buf, b[:n]...) 121 | } 122 | } 123 | 124 | func (c *connection) writeLine(msg string) error { 125 | msg += "\r\n" 126 | for len(msg) > 0 { 127 | n, err := c.conn.Write([]byte(msg)) 128 | if err != nil { 129 | return err 130 | } 131 | 132 | msg = msg[n:] 133 | } 134 | 135 | return nil 136 | } 137 | 138 | func (c *connection) handle() { 139 | defer c.conn.Close() 140 | c.logInfo("Connection accepted") 141 | 142 | err := c.writeLine("220") 143 | if err != nil { 144 | c.logError(err) 145 | return 146 | } 147 | 148 | c.logInfo("Awaiting EHLO") 149 | 150 | line, err := c.readLine() 151 | if err != nil { 152 | c.logError(err) 153 | return 154 | } 155 | 156 | if !strings.HasPrefix(line, "EHLO") { 157 | c.logError(errors.New("Expected EHLO got: " + line)) 158 | return 159 | } 160 | 161 | msg := message{ 162 | smtpCommands: map[string]string{}, 163 | atmHeaders: map[string]string{}, 164 | } 165 | msg.clientDomain = line[len("EHLO "):] 166 | 167 | c.logInfo("Received EHLO") 168 | 169 | err = c.writeLine("250 ") 170 | if err != nil { 171 | c.logError(err) 172 | return 173 | } 174 | 175 | c.logInfo("Done EHLO") 176 | 177 | for line != "" { 178 | line, err = c.readLine() 179 | if err != nil { 180 | c.logError(err) 181 | return 182 | } 183 | 184 | pieces := strings.SplitN(line, ":", 2) 185 | smtpCommand := strings.ToUpper(pieces[0]) 186 | 187 | // Special header without a value 188 | if smtpCommand == "DATA" { 189 | err = c.writeLine("354") 190 | if err != nil { 191 | c.logError(err) 192 | return 193 | } 194 | 195 | break 196 | } 197 | 198 | smtpValue := pieces[1] 199 | msg.smtpCommands[smtpCommand] = smtpValue 200 | 201 | c.logInfo("Got header: " + line) 202 | 203 | err = c.writeLine("250 OK") 204 | if err != nil { 205 | c.logError(err) 206 | return 207 | } 208 | } 209 | 210 | c.logInfo("Done SMTP headers, reading ARPA text message headers") 211 | 212 | for { 213 | line, err = c.readMultiLine() 214 | if err != nil { 215 | c.logError(err) 216 | return 217 | } 218 | 219 | if strings.TrimSpace(line) == "" { 220 | break 221 | } 222 | 223 | pieces := strings.SplitN(line, ": ", 2) 224 | atmHeader := strings.ToUpper(pieces[0]) 225 | atmValue := pieces[1] 226 | msg.atmHeaders[atmHeader] = atmValue 227 | 228 | if atmHeader == "SUBJECT" { 229 | msg.subject = atmValue 230 | } 231 | if atmHeader == "TO" { 232 | msg.to = atmValue 233 | } 234 | if atmHeader == "FROM" { 235 | msg.from = atmValue 236 | } 237 | if atmHeader == "DATE" { 238 | msg.date = atmValue 239 | } 240 | } 241 | 242 | c.logInfo("Done ARPA text message headers, reading body") 243 | 244 | msg.body, err = c.readToEndOfBody() 245 | if err != nil { 246 | c.logError(err) 247 | return 248 | } 249 | 250 | c.logInfo("Got body (%d bytes)", len(msg.body)) 251 | 252 | err = c.writeLine("250 OK") 253 | if err != nil { 254 | c.logError(err) 255 | return 256 | } 257 | 258 | c.logInfo("Message:\n%s\n", msg.body) 259 | 260 | c.logInfo("Connection closed") 261 | } 262 | 263 | func main() { 264 | l, err := net.Listen("tcp", "0.0.0.0:25") 265 | if err != nil { 266 | panic(err) 267 | } 268 | defer l.Close() 269 | 270 | logInfo("Listening") 271 | 272 | id := 0 273 | for { 274 | conn, err := l.Accept() 275 | if err != nil { 276 | logError(err) 277 | continue 278 | } 279 | 280 | id += 1 281 | c := connection{conn, id, nil} 282 | go c.handle() 283 | } 284 | } 285 | --------------------------------------------------------------------------------