Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added send message API endpoint #279

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 201 additions & 0 deletions server/apiv1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import (
"bytes"
"encoding/json"
"fmt"
"net"
"net/http"
"net/mail"
"strconv"
"strings"
"text/template"
"time"

"github.com/axllent/mailpit/config"
Expand All @@ -23,6 +25,205 @@ import (
"github.com/lithammer/shortuuid/v4"
)

// SendMessage sends a new message
func SendMessage(w http.ResponseWriter, r *http.Request) {
// swagger:route POST /api/v1/messages message SendMessage
//
// # Send message
//
// Sends a message to the mailbox
//
// Consumes:
// - application/json
//
// Produces:
// - text/plain
//
// Schemes: http, https
//
// Responses:
// 200: OKResponse
// default: ErrorResponse

decoder := json.NewDecoder(r.Body)

data := sendMessageRequestBody{}

if err := decoder.Decode(&data); err != nil {
httpError(w, err.Error())
return
}

fromEmail := data.From.Email
if fromEmail == "" {
httpError(w, "No valid sender address found")
return
}
_, err := mail.ParseAddress(fromEmail)
if err != nil {
httpError(w, "Invalid sender email address: "+fromEmail)
return
}
from := fromEmail
if data.From.Name != "" {
from = fmt.Sprintf("%s <%s>", data.From.Name, data.From.Email)
}

if len(data.To) == 0 || data.To[0].Email == "" {
httpError(w, "No valid recipient addresses found")
return
}
toEmails := []string{}
toRecipients := []string{}
for _, to := range data.To {
if _, err := mail.ParseAddress(to.Email); err != nil {
httpError(w, "Invalid 'to' recipient email address: "+to.Email)
return
}
toEmails = append(toEmails, to.Email)
if to.Name != "" {
toRecipients = append(toRecipients, fmt.Sprintf("%s <%s>", to.Name, to.Email))
} else {
toRecipients = append(toRecipients, to.Email)
}
}

ccRecipients := []string{}
for _, cc := range data.CC {
if _, err := mail.ParseAddress(cc.Email); err != nil {
httpError(w, "Invalid 'cc' email address: "+cc.Email)
return
}
if cc.Name != "" {
ccRecipients = append(ccRecipients, fmt.Sprintf("%s <%s>", cc.Name, cc.Email))
} else {
ccRecipients = append(ccRecipients, cc.Email)
}
}

bccRecipients := []string{}
for _, bcc := range data.BCC {
if _, err := mail.ParseAddress(bcc.Email); err != nil {
httpError(w, "Invalid 'bcc' email address: "+bcc.Email)
return
}
if bcc.Name != "" {
bccRecipients = append(bccRecipients, fmt.Sprintf("%s <%s>", bcc.Name, bcc.Email))
} else {
bccRecipients = append(bccRecipients, bcc.Email)
}
}

replyToRecipients := []string{}
for _, replyTo := range data.ReplyTo {
if _, err := mail.ParseAddress(replyTo.Email); err != nil {
httpError(w, "Invalid 'replyTo' email address: "+replyTo.Email)
return
}
if replyTo.Name != "" {
replyToRecipients = append(replyToRecipients, fmt.Sprintf("%s <%s>", replyTo.Name, replyTo.Email))
} else {
replyToRecipients = append(replyToRecipients, replyTo.Email)
}
}

if data.Subject == "" {
httpError(w, "No valid subject found")
return
}

if data.HTML == "" {
httpError(w, "No valid HTML body found")
return
}

if data.Text == "" {
httpError(w, "No valid text body found")
return
}

clientIP, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
fmt.Fprintf(w, "Error parsing request RemoteAddr: %s", err)
return
}

templateData := map[string]interface{}{
"From": from,
"To": strings.Join(toRecipients, ", "),
"CC": strings.Join(ccRecipients, ", "),
"BCC": strings.Join(bccRecipients, ", "),
"ReplyTo": strings.Join(replyToRecipients, ", "),
"Tags": data.Tags,
"Subject": data.Subject,
"Headers": data.Headers,
"Text": data.Text,
"HTML": data.HTML,
"Attachments": data.Attachments,
}
messageTemplate, err := template.New("message").Parse(
"" +
"Content-Type: multipart/mixed; boundary=boundary\r\n" +
"{{range $key, $value := .Headers}}{{$key}}: {{$value}}\r\n{{end}}" +
"X-Tags:{{range $index, $tag := .Tags}} {{$tag}}\r\n{{end}}" +
"From: {{.From}}\r\n" +
"To: {{.To}}\r\n" +
"{{if .CC}}Cc: {{.CC}}\r\n{{end}}" +
"{{if .BCC}}Bcc: {{.BCC}}\r\n{{end}}" +
"{{if .ReplyTo}}Reply-To: {{.ReplyTo}}\r\n{{end}}" +
"Subject: {{.Subject}}\r\n" +
"\r\n" +
"{{if .Text }}--boundary\r\n" +
"Content-Type: text/plain; charset=\"UTF-8\"\r\n\r\n" +
"{{.Text}}" +
"\r\n{{ end }}" +
"{{if .HTML }}--boundary\r\n" +
"Content-Type: text/html; charset=\"UTF-8\"\r\n\r\n" +
"{{.HTML}}" +
"\r\n{{ end}}" +
"{{range .Attachments}}" +
"--boundary\r\n" +
"Content-Type: application/octet-stream\r\n" +
"Content-Disposition: attachment; filename=\"{{.Filename}}\"\r\n" +
"Content-Transfer-Encoding: base64\r\n\r\n" +
"{{.Content}}\r\n" +
"{{end}}" +
"--boundary--\r\n",
)
if err != nil {
fmt.Fprintf(w, "Error initialising email body template: %s", err)
return
}
message := bytes.NewBuffer(nil)
messageTemplate.Execute(message, templateData)
msg := message.Bytes()

// update message date
msg, err = tools.UpdateMessageHeader(msg, "Date", time.Now().Format(time.RFC1123Z))
if err != nil {
httpError(w, err.Error())
return
}

// generate unique ID
uid := shortuuid.New() + "@mailpit"
// update Message-Id with unique ID
msg, err = tools.UpdateMessageHeader(msg, "Message-Id", "<"+uid+">")
if err != nil {
httpError(w, err.Error())
return
}

if err := smtpd.MailHandler(&net.IPAddr{IP: net.ParseIP(clientIP)}, fromEmail, toEmails, msg); err != nil {
logger.Log().Errorf("[smtp] error sending message: %s", err.Error())
httpError(w, "SMTP error: "+err.Error())
return
}

w.Header().Add("Content-Type", "text/plain")
_, _ = w.Write([]byte("ok"))
}

// GetMessages returns a paginated list of messages as JSON
func GetMessages(w http.ResponseWriter, r *http.Request) {
// swagger:route GET /api/v1/messages messages GetMessages
Expand Down
115 changes: 115 additions & 0 deletions server/apiv1/swagger.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,121 @@ import "github.com/axllent/mailpit/internal/stats"

// These structs are for the purpose of defining swagger HTTP parameters & responses

// Send request
// swagger:model sendMessageRequestBody
type sendMessageRequestBody struct {
// Email+name pair to send the message from
//
// required: true
// example: {"email": "[email protected]", "name": "Sender"}
From struct {
Email string `json:"email"`
Name string `json:"name"`
} `json:"from"`

// Array of email+name pairs to send the message to
//
// required: true
// example: [{
// "email": "[email protected]",
// "name": "user1",
// },
// {
// "email": "[email protected]"
// }],
To []struct {
Email string `json:"email"`
Name string `json:"name"`
} `json:"to"`

// Array of email+name pairs to CC the message to
//
// required: true
// example: [{
// "email": "[email protected]",
// "name": "user1",
// },
// {
// "email": "[email protected]"
// }],
CC []struct {
Email string `json:"email"`
Name string `json:"name"`
} `json:"cc"`

// Array of email+name pairs to BCC the message to
//
// required: true
// example: [{
// "email": "[email protected]",
// "name": "user1",
// },
// {
// "email": "[email protected]"
// }],
BCC []struct {
Email string `json:"email"`
Name string `json:"name"`
} `json:"bcc"`

// Array of email+name pairs to add in the Reply-To header
//
// required: true
// example: [{
// "email": "[email protected]",
// "name": "user1",
// },
// {
// "email": "[email protected]"
// }],
ReplyTo []struct {
Email string `json:"email"`
Name string `json:"name"`
} `json:"replyTo"`

// Array of strings to add as tags
//
// required: false
// example: ["Tag 1", "Tag 2"]
Tags []string `json:"tags"`

// String of email subject
//
// required: true
// example: "Hello"
Subject string `json:"subject"`

// Map of headers
//
// required: false
// example: {"X-IP": "1.2.3.4"}
Headers map[string]string `json:"headers"`

// String of email text body
//
// required: true
// example: "Hello"
Text string `json:"text"`

// String of email HTML body
//
// required: true
// example: "<html><body>Hello</body></html>"
HTML string `json:"html"`

// Array of content+filename pairs to add as attachments
//
// required: false
// example: [{
// "content": "VGhpcyBpcyBhIHBsYWluIHRleHQgYXRhY2htZW50Lg==",
// "filename": "AttachedFile.txt",
// }],
Attachments []struct {
Content string `json:"content"`
Filename string `json:"filename"`
} `json:"attachments"`
}

// Application information
// swagger:response InfoResponse
type infoResponse struct {
Expand Down
1 change: 1 addition & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ func apiRoutes() *mux.Router {
r := mux.NewRouter()

// API V1
r.HandleFunc(config.Webroot+"api/v1/messages", middleWareFunc(apiv1.SendMessage)).Methods("POST")
r.HandleFunc(config.Webroot+"api/v1/messages", middleWareFunc(apiv1.GetMessages)).Methods("GET")
r.HandleFunc(config.Webroot+"api/v1/messages", middleWareFunc(apiv1.SetReadStatus)).Methods("PUT")
r.HandleFunc(config.Webroot+"api/v1/messages", middleWareFunc(apiv1.DeleteMessages)).Methods("DELETE")
Expand Down
4 changes: 2 additions & 2 deletions server/smtpd/smtpd.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ var (
DisableReverseDNS bool
)

func mailHandler(origin net.Addr, from string, to []string, data []byte) error {
func MailHandler(origin net.Addr, from string, to []string, data []byte) error {
if !config.SMTPStrictRFCHeaders {
// replace all <CR><CR><LF> (\r\r\n) with <CR><LF> (\r\n)
// @see https://github.com/axllent/mailpit/issues/87 & https://github.com/axllent/mailpit/issues/153
Expand Down Expand Up @@ -207,7 +207,7 @@ func Listen() error {

logger.Log().Infof("[smtpd] starting on %s (%s)", config.SMTPListen, smtpType)

return listenAndServe(config.SMTPListen, mailHandler, authHandler)
return listenAndServe(config.SMTPListen, MailHandler, authHandler)
}

func listenAndServe(addr string, handler smtpd.Handler, authHandler smtpd.AuthHandler) error {
Expand Down