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:
@@ -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 {
|
||||||
|
|||||||
@@ -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}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user