Skip to content

Commit

Permalink
SMTP: WIP implementation of ReceivedMessage / ReceivedPart
Browse files Browse the repository at this point in the history
  • Loading branch information
Lucas Hinderberger committed Jun 21, 2024
1 parent 2171791 commit 898c502
Show file tree
Hide file tree
Showing 2 changed files with 143 additions and 3 deletions.
145 changes: 142 additions & 3 deletions internal/smtp/message.go
Original file line number Diff line number Diff line change
@@ -1,28 +1,167 @@
package smtp

import "time"
import (
"bytes"
"errors"
"fmt"
"io"
"mime"
"mime/multipart"
"net/mail"
"net/textproto"
"strings"
"time"
)

// ReceivedMessage contains a single email message as received via SMTP.
type ReceivedMessage struct {
smtpFrom string
smtpRcptTo []string
rawMessageData []byte
receivedAt time.Time

headers mail.Header
body []byte

contentType string
contentTypeParams map[string]string

isMultipart bool
multiparts []*ReceivedPart
}

// ReceivedPart contains a single part of a multipart message as received
// via SMTP.
type ReceivedPart struct {
headers textproto.MIMEHeader
body []byte
}

func NewReceivedMessage(
from string, rcptTo []string, rawMessageData []byte, receivedAt time.Time,
) (*ReceivedMessage, error) {
parsedMsg, err := mail.ReadMessage(bytes.NewReader(rawMessageData))
if err != nil {
return nil, fmt.Errorf("could not parse message: %w", err)
}

// TODO: Limit length?
body, err := io.ReadAll(parsedMsg.Body)
if err != nil {
return nil, fmt.Errorf("could not read message body: %w", err)
}

msg := &ReceivedMessage{
smtpFrom: from,
smtpRcptTo: rcptTo,
rawMessageData: rawMessageData,
receivedAt: receivedAt,
headers: parsedMsg.Header,
body: body,
}

rawContentType := msg.headers.Get("Content-Type")
if rawContentType != "" {
msg.contentType, msg.contentTypeParams, err = mime.ParseMediaType(msg.contentType)
if err != nil {
return nil, fmt.Errorf("could not parse Content-Type: %w", err)
}

// case-sensitive comparison of the content type is permitted here,
// since mime.ParseMediaType is documented to return the media type
// in lower case.
msg.isMultipart = strings.HasPrefix(msg.contentType, "multipart/")
}

// TODO: Parse message
if msg.isMultipart {
boundary, ok := msg.contentTypeParams["boundary"]
if !ok {
return nil, fmt.Errorf("encountered multipart message without defined boundary")
}

r := multipart.NewReader(bytes.NewReader(msg.body), boundary)

for {
rawPart, err := r.NextPart()
if err != nil {
if errors.Is(err, io.EOF) {
break
} else {
return nil, fmt.Errorf("could not read multipart: %w", err)
}
}

part, err := NewReceivedPart(rawPart)
if err != nil {
return nil, fmt.Errorf("could not parse message part: %w", err)
}

msg.multiparts = append(msg.multiparts, part)
}
}

return msg, nil
}

// TODO: Getters
func NewReceivedPart(p *multipart.Part) (*ReceivedPart, error) {
// TODO: Limit length?
body, err := io.ReadAll(p)
if err != nil {
return nil, fmt.Errorf("could not read message part body: %w", err)
}

part := &ReceivedPart{
headers: p.Header,
body: body,
}

return part, nil
}

// =======
// Getters
// =======

func (m *ReceivedMessage) ContentType() string {
return m.contentType
}

func (m *ReceivedMessage) Body() []byte {
return m.body
}

func (m *ReceivedMessage) Headers() mail.Header {
return m.headers
}

func (m *ReceivedMessage) IsMultipart() bool {
return m.isMultipart
}

func (m *ReceivedMessage) Multiparts() []*ReceivedPart {
return m.multiparts
}

func (m *ReceivedMessage) RawMessageData() []byte {
return m.rawMessageData
}

func (m *ReceivedMessage) ReceivedAt() time.Time {
return m.receivedAt
}

func (m *ReceivedMessage) SmtpFrom() string {
return m.smtpFrom
}

func (m *ReceivedMessage) SmtpRcptTo() []string {
return m.smtpRcptTo
}

func (p *ReceivedPart) Body() []byte {
return p.body
}

func (p *ReceivedPart) Headers() textproto.MIMEHeader {
return p.headers
}
1 change: 1 addition & 0 deletions internal/smtp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ func newSession(server *Server, c *smtp.Conn) (smtp.Session, error) {

// Implements smtp.Session's Data method.
func (s *session) Data(r io.Reader) error {
// TODO: Limit length?
rawData, err := io.ReadAll(r)
if err != nil {
return fmt.Errorf("could not read mail data from SMTP: %w", err)
Expand Down

0 comments on commit 898c502

Please sign in to comment.