1
0
mirror of https://github.com/jhillyerd/inbucket.git synced 2025-12-17 17:47:03 +00:00

Successfully accepted an ESMTP message from Mac Mail!

This commit is contained in:
James Hillyerd
2012-10-12 14:05:55 -07:00
parent f5b7aca712
commit ca66280f74
2 changed files with 61 additions and 12 deletions

View File

@@ -2,10 +2,12 @@ package smtpd
import ( import (
"bufio" "bufio"
"bytes"
"container/list" "container/list"
"fmt" "fmt"
"github.com/jhillyerd/inbucket/app/inbucket" "github.com/jhillyerd/inbucket/app/inbucket"
"net" "net"
"regexp"
"strings" "strings"
"time" "time"
) )
@@ -38,6 +40,7 @@ func (s State) String() string {
var commands = map[string]bool{ var commands = map[string]bool{
"HELO": true, "HELO": true,
"EHLO": true,
"MAIL": true, "MAIL": true,
"RCPT": true, "RCPT": true,
"DATA": true, "DATA": true,
@@ -111,11 +114,14 @@ func (s *Server) startSession(id int, conn net.Conn) {
// Commands we handle in any state // Commands we handle in any state
switch cmd { switch cmd {
case "SEND", "SOML", "SAML", "VRFY", "EXPN", "HELP", "TURN": case "SEND", "SOML", "SAML", "EXPN", "HELP", "TURN":
// These commands are not implemented in any state // These commands are not implemented in any state
ss.send(fmt.Sprintf("502 %v command not implemented", cmd)) ss.send(fmt.Sprintf("502 %v command not implemented", cmd))
ss.warn("Command %v not implemented by Inbucket", cmd) ss.warn("Command %v not implemented by Inbucket", cmd)
continue continue
case "VRFY":
ss.send("252 Cannot VRFY user, but will accept message")
continue
case "NOOP": case "NOOP":
ss.send("250 I have sucessfully done nothing") ss.send("250 I have sucessfully done nothing")
continue continue
@@ -167,10 +173,15 @@ func (s *Server) startSession(id int, conn net.Conn) {
// GREET state -> waiting for HELO // GREET state -> waiting for HELO
func (ss *Session) greetHandler(cmd string, arg string) { func (ss *Session) greetHandler(cmd string, arg string) {
if cmd == "HELO" { switch cmd {
case "HELO":
ss.send("250 Great, let's get this show on the road") ss.send("250 Great, let's get this show on the road")
ss.enterState(READY) ss.enterState(READY)
} else { case "EHLO":
ss.send("250-Great, let's get this show on the road")
ss.send("250 8BITMIME")
ss.enterState(READY)
default:
ss.ooSeq(cmd) ss.ooSeq(cmd)
} }
} }
@@ -178,13 +189,20 @@ func (ss *Session) greetHandler(cmd string, arg string) {
// READY state -> waiting for MAIL // READY state -> waiting for MAIL
func (ss *Session) readyHandler(cmd string, arg string) { func (ss *Session) readyHandler(cmd string, arg string) {
if cmd == "MAIL" { if cmd == "MAIL" {
if (len(arg) < 6) || (strings.ToUpper(arg[0:5]) != "FROM:") { // (?i) makes the regex case insensitive
re := regexp.MustCompile("(?i)^FROM:<([^>]+)>( (\\w+)=(\\w+))?$")
m := re.FindStringSubmatch(arg)
if m == nil {
ss.send("501 Was expecting MAIL arg syntax of FROM:<address>") ss.send("501 Was expecting MAIL arg syntax of FROM:<address>")
ss.warn("Bad MAIL argument: \"%v\"", arg) ss.warn("Bad MAIL argument: \"%v\"", arg)
return return
} }
// This trim is probably too forgiving from := m[1]
from := strings.Trim(arg[5:], "<> ") // This is where the client may put BODY=8BITMIME, but we already
// ready the DATA as bytes, so it does not effect our processing.
pkey := m[3]
pval := m[4]
ss.trace("MAIL param: %v = %v", pkey, pval)
ss.from = from ss.from = from
ss.recipients = list.New() ss.recipients = list.New()
ss.info("Mail from: %v", from) ss.info("Mail from: %v", from)
@@ -247,7 +265,7 @@ func (ss *Session) dataHandler() {
mb, err := ss.server.dataStore.MailboxFor(recip) mb, err := ss.server.dataStore.MailboxFor(recip)
if err != nil { if err != nil {
ss.error("Failed to open mailbox for %v", recip) ss.error("Failed to open mailbox for %v", recip)
ss.send(fmt.Sprintf("554 Failed to open mailbox for %v", recip)) ss.send(fmt.Sprintf("451 Failed to open mailbox for %v", recip))
ss.enterState(READY) ss.enterState(READY)
return return
} }
@@ -257,8 +275,10 @@ func (ss *Session) dataHandler() {
} }
ss.send("354 Start mail input; end with <CRLF>.<CRLF>") ss.send("354 Start mail input; end with <CRLF>.<CRLF>")
var buf bytes.Buffer
for { for {
line, err := ss.readLine() buf.Reset()
err := ss.readByteLine(&buf)
if err != nil { if err != nil {
if netErr, ok := err.(net.Error); ok { if netErr, ok := err.(net.Error); ok {
if netErr.Timeout() { if netErr.Timeout() {
@@ -269,7 +289,8 @@ func (ss *Session) dataHandler() {
ss.enterState(QUIT) ss.enterState(QUIT)
return return
} }
if line == ".\r\n" || line == ".\n" { line := buf.Bytes()
if string(line) == ".\r\n" {
// Mail data complete // Mail data complete
for _, m := range messages { for _, m := range messages {
m.Close() m.Close()
@@ -280,13 +301,13 @@ func (ss *Session) dataHandler() {
return return
} }
// SMTP RFC says remove leading periods from input // SMTP RFC says remove leading periods from input
if line != "" && line[0] == '.' { if len(line) > 0 && line[0] == '.' {
line = line[1:] line = line[1:]
} }
msgSize += uint64(len(line)) msgSize += uint64(len(line))
// Append to message objects // Append to message objects
for i, m := range messages { for i, m := range messages {
if err := m.Append([]byte(line)); err != nil { if err := m.Append(line); err != nil {
ss.error("Failed to append to mailbox %v: %v", mailboxes[i], err) ss.error("Failed to append to mailbox %v: %v", mailboxes[i], err)
ss.send("554 Something went wrong") ss.send("554 Something went wrong")
ss.enterState(READY) ss.enterState(READY)
@@ -325,6 +346,34 @@ func (ss *Session) send(msg string) {
ss.trace("Sent: \"%v\"", msg) ss.trace("Sent: \"%v\"", msg)
} }
// readByteLine reads a line of input into the provided buffer. Does
// not reset the Buffer - please do so prior to calling.
func (ss *Session) readByteLine(buf *bytes.Buffer) error {
if err := ss.conn.SetReadDeadline(ss.nextDeadline()); err != nil {
return err
}
for {
line, err := ss.reader.ReadBytes('\r')
if err != nil {
return err
}
buf.Write(line)
// Read the next byte looking for '\n'
c, err := ss.reader.ReadByte()
if err != nil {
return err
}
buf.WriteByte(c)
if c == '\n' {
// We've reached the end of the line, return
return nil
}
// Else, keep looking
}
// Should be unreachable
return nil
}
// Reads a line of input // Reads a line of input
func (ss *Session) readLine() (line string, err error) { func (ss *Session) readLine() (line string, err error) {
if err = ss.conn.SetReadDeadline(ss.nextDeadline()); err != nil { if err = ss.conn.SetReadDeadline(ss.nextDeadline()); err != nil {

View File

@@ -19,7 +19,7 @@ type Server struct {
// Init a new Server object // Init a new Server object
func New(domain string, port int) *Server { func New(domain string, port int) *Server {
ds := inbucket.NewDataStore() ds := inbucket.NewDataStore()
return &Server{domain: domain, port: port, maxRecips: 100, maxIdleSeconds: 60, return &Server{domain: domain, port: port, maxRecips: 100, maxIdleSeconds: 300,
dataStore: ds} dataStore: ds}
} }