mirror of
https://github.com/axllent/mailpit.git
synced 2025-08-15 20:13:16 +02:00
Feature: API endpoint for sending (#278)
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
package tools
|
package tools
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
// Plural returns a singular or plural of a word together with the total
|
// Plural returns a singular or plural of a word together with the total
|
||||||
func Plural(total int, singular, plural string) string {
|
func Plural(total int, singular, plural string) string {
|
||||||
@@ -9,3 +12,15 @@ func Plural(total int, singular, plural string) string {
|
|||||||
}
|
}
|
||||||
return fmt.Sprintf("%d %s", total, plural)
|
return fmt.Sprintf("%d %s", total, plural)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InArray tests if a string is within an array. It is not case sensitive.
|
||||||
|
func InArray(k string, arr []string) bool {
|
||||||
|
k = strings.ToLower(k)
|
||||||
|
for _, v := range arr {
|
||||||
|
if strings.ToLower(v) == k {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
@@ -900,6 +900,19 @@ func httpError(w http.ResponseWriter, msg string) {
|
|||||||
fmt.Fprint(w, msg)
|
fmt.Fprint(w, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// httpJSONError returns a basic error message (400 response) in JSON format
|
||||||
|
func httpJSONError(w http.ResponseWriter, msg string) {
|
||||||
|
w.Header().Set("Referrer-Policy", "no-referrer")
|
||||||
|
w.Header().Set("Content-Security-Policy", config.ContentSecurityPolicy)
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
e := JSONErrorMessage{
|
||||||
|
Error: msg,
|
||||||
|
}
|
||||||
|
bytes, _ := json.Marshal(e)
|
||||||
|
w.Header().Add("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
// Get the start and limit based on query params. Defaults to 0, 50
|
// Get the start and limit based on query params. Defaults to 0, 50
|
||||||
func getStartLimit(req *http.Request) (start int, limit int) {
|
func getStartLimit(req *http.Request) (start int, limit int) {
|
||||||
start = 0
|
start = 0
|
||||||
|
275
server/apiv1/send.go
Normal file
275
server/apiv1/send.go
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
package apiv1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/mail"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/axllent/mailpit/internal/tools"
|
||||||
|
"github.com/axllent/mailpit/server/smtpd"
|
||||||
|
"github.com/jhillyerd/enmime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// swagger:parameters SendMessage
|
||||||
|
type sendMessageParams struct {
|
||||||
|
// in: body
|
||||||
|
Body *SendRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendRequest to send a message via HTTP
|
||||||
|
// swagger:model SendRequest
|
||||||
|
type SendRequest struct {
|
||||||
|
// "From" recipient
|
||||||
|
// required: true
|
||||||
|
From struct {
|
||||||
|
// Optional name
|
||||||
|
// example: John Doe
|
||||||
|
Name string
|
||||||
|
// Email address
|
||||||
|
// example: john@example.com
|
||||||
|
// required: true
|
||||||
|
Email string
|
||||||
|
}
|
||||||
|
|
||||||
|
// "To" recipients
|
||||||
|
To []struct {
|
||||||
|
// Optional name
|
||||||
|
// example: Jane Doe
|
||||||
|
Name string
|
||||||
|
// Email address
|
||||||
|
// example: jane@example.com
|
||||||
|
// required: true
|
||||||
|
Email string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cc recipients
|
||||||
|
Cc []struct {
|
||||||
|
// Optional name
|
||||||
|
// example: Manager
|
||||||
|
Name string
|
||||||
|
// Email address
|
||||||
|
// example: manager@example.com
|
||||||
|
// required: true
|
||||||
|
Email string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bcc recipients email addresses only
|
||||||
|
// example: ["jack@example.com"]
|
||||||
|
Bcc []string
|
||||||
|
|
||||||
|
// Optional Reply-To recipients
|
||||||
|
ReplyTo []struct {
|
||||||
|
// Optional name
|
||||||
|
// example: Secretary
|
||||||
|
Name string
|
||||||
|
// Email address
|
||||||
|
// example: secretary@example.com
|
||||||
|
// required: true
|
||||||
|
Email string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subject
|
||||||
|
// example: Mailpit message via the HTTP API
|
||||||
|
Subject string
|
||||||
|
|
||||||
|
// Message body (text)
|
||||||
|
// example: This is the text body
|
||||||
|
Text string
|
||||||
|
|
||||||
|
// Message body (HTML)
|
||||||
|
// example: <p style="font-family: arial">Mailpit is <b>awesome</b>!</p>
|
||||||
|
HTML string
|
||||||
|
|
||||||
|
// Attachments
|
||||||
|
Attachments []struct {
|
||||||
|
// Base64-encoded string of the file content
|
||||||
|
// required: true
|
||||||
|
// example: VGhpcyBpcyBhIHBsYWluIHRleHQgYXR0YWNobWVudA==
|
||||||
|
Content string
|
||||||
|
// Filename
|
||||||
|
// required: true
|
||||||
|
// example: AttachedFile.txt
|
||||||
|
Filename string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mailpit tags
|
||||||
|
// example: ["Tag 1","Tag 2"]
|
||||||
|
Tags []string
|
||||||
|
|
||||||
|
// Optional headers in {"key":"value"} format
|
||||||
|
// example: {"X-IP":"1.2.3.4"}
|
||||||
|
Headers map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendMessageConfirmation struct
|
||||||
|
type SendMessageConfirmation struct {
|
||||||
|
// Database ID
|
||||||
|
// example: iAfZVVe2UQFNSG5BAjgYwa
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONErrorMessage struct
|
||||||
|
type JSONErrorMessage struct {
|
||||||
|
// Error message
|
||||||
|
// example: invalid format
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendMessageHandler handles HTTP requests to send a new message
|
||||||
|
func SendMessageHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// swagger:route POST /api/v1/send message SendMessage
|
||||||
|
//
|
||||||
|
// # Send a message
|
||||||
|
//
|
||||||
|
// Send a message via the HTTP API.
|
||||||
|
//
|
||||||
|
// Consumes:
|
||||||
|
// - application/json
|
||||||
|
//
|
||||||
|
// Produces:
|
||||||
|
// - application/json
|
||||||
|
//
|
||||||
|
// Schemes: http, https
|
||||||
|
//
|
||||||
|
// Responses:
|
||||||
|
// 200: sendMessageResponse
|
||||||
|
// default: jsonErrorResponse
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(r.Body)
|
||||||
|
|
||||||
|
data := SendRequest{}
|
||||||
|
|
||||||
|
if err := decoder.Decode(&data); err != nil {
|
||||||
|
httpJSONError(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := data.Send(r.RemoteAddr)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
httpJSONError(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes, _ := json.Marshal(SendMessageConfirmation{ID: id})
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send will validate the message structure and attempt to send to Mailpit.
|
||||||
|
// It returns a sending summary or an error.
|
||||||
|
func (d SendRequest) Send(remoteAddr string) (string, error) {
|
||||||
|
ip, _, err := net.SplitHostPort(remoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error parsing request RemoteAddr: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
ipAddr := &net.IPAddr{IP: net.ParseIP(ip)}
|
||||||
|
|
||||||
|
addresses := []string{}
|
||||||
|
|
||||||
|
msg := enmime.Builder().
|
||||||
|
From(d.From.Name, d.From.Email).
|
||||||
|
Subject(d.Subject).
|
||||||
|
Text([]byte(d.Text))
|
||||||
|
|
||||||
|
if d.HTML != "" {
|
||||||
|
msg = msg.HTML([]byte(d.HTML))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(d.To) > 0 {
|
||||||
|
for _, a := range d.To {
|
||||||
|
if _, err := mail.ParseAddress(a.Email); err == nil {
|
||||||
|
msg = msg.To(a.Name, a.Email)
|
||||||
|
addresses = append(addresses, a.Email)
|
||||||
|
} else {
|
||||||
|
return "", fmt.Errorf("invalid To address: %s", a.Email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(d.Cc) > 0 {
|
||||||
|
for _, a := range d.Cc {
|
||||||
|
if _, err := mail.ParseAddress(a.Email); err == nil {
|
||||||
|
msg = msg.CC(a.Name, a.Email)
|
||||||
|
addresses = append(addresses, a.Email)
|
||||||
|
} else {
|
||||||
|
return "", fmt.Errorf("invalid Cc address: %s", a.Email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(d.Bcc) > 0 {
|
||||||
|
for _, e := range d.Bcc {
|
||||||
|
if _, err := mail.ParseAddress(e); err == nil {
|
||||||
|
msg = msg.BCC("", e)
|
||||||
|
addresses = append(addresses, e)
|
||||||
|
} else {
|
||||||
|
return "", fmt.Errorf("invalid Bcc address: %s", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(d.ReplyTo) > 0 {
|
||||||
|
for _, a := range d.ReplyTo {
|
||||||
|
if _, err := mail.ParseAddress(a.Email); err == nil {
|
||||||
|
msg = msg.ReplyTo(a.Name, a.Email)
|
||||||
|
} else {
|
||||||
|
return "", fmt.Errorf("invalid Reply-To address: %s", a.Email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
restrictedHeaders := []string{"To", "From", "Cc", "Bcc", "Reply-To", "Date", "Subject", "Content-Type", "Mime-Version"}
|
||||||
|
|
||||||
|
if len(d.Tags) > 0 {
|
||||||
|
msg = msg.Header("X-Tags", strings.Join(d.Tags, ", "))
|
||||||
|
restrictedHeaders = append(restrictedHeaders, "X-Tags")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(d.Headers) > 0 {
|
||||||
|
for k, v := range d.Headers {
|
||||||
|
// check header isn't in "restricted" headers
|
||||||
|
if tools.InArray(k, restrictedHeaders) {
|
||||||
|
return "", fmt.Errorf("cannot overwrite header: \"%s\"", k)
|
||||||
|
}
|
||||||
|
msg = msg.Header(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(d.Attachments) > 0 {
|
||||||
|
for _, a := range d.Attachments {
|
||||||
|
// workaround: split string because JS readAsDataURL() returns the base64 string
|
||||||
|
// with the mime type prefix eg: data:image/png;base64,<base64String>
|
||||||
|
parts := strings.Split(a.Content, ",")
|
||||||
|
content := parts[len(parts)-1]
|
||||||
|
b, err := base64.StdEncoding.DecodeString(content)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error decoding base64 attachment \"%s\": %s", a.Filename, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
mimeType := http.DetectContentType(b)
|
||||||
|
msg = msg.AddAttachment(b, mimeType, a.Filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
part, err := msg.Build()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error building message: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
var buff bytes.Buffer
|
||||||
|
|
||||||
|
if err := part.Encode(io.Writer(&buff)); err != nil {
|
||||||
|
return "", fmt.Errorf("error building message: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return smtpd.Store(ipAddr, d.From.Email, addresses, buff.Bytes())
|
||||||
|
}
|
@@ -170,6 +170,7 @@ type htmlResponse string
|
|||||||
|
|
||||||
// HTTP error response will return with a >= 400 response code
|
// HTTP error response will return with a >= 400 response code
|
||||||
// swagger:response ErrorResponse
|
// swagger:response ErrorResponse
|
||||||
|
// example: invalid request
|
||||||
type errorResponse string
|
type errorResponse string
|
||||||
|
|
||||||
// Plain text "ok" response
|
// Plain text "ok" response
|
||||||
@@ -179,3 +180,21 @@ type okResponse string
|
|||||||
// Plain JSON array response
|
// Plain JSON array response
|
||||||
// swagger:response ArrayResponse
|
// swagger:response ArrayResponse
|
||||||
type arrayResponse []string
|
type arrayResponse []string
|
||||||
|
|
||||||
|
// Confirmation message for HTTP send API
|
||||||
|
// swagger:response sendMessageResponse
|
||||||
|
type sendMessageResponse struct {
|
||||||
|
// Response for sending messages via the HTTP API
|
||||||
|
//
|
||||||
|
// in: body
|
||||||
|
Body SendMessageConfirmation
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON error response
|
||||||
|
// swagger:response jsonErrorResponse
|
||||||
|
type jsonErrorResponse struct {
|
||||||
|
// A JSON-encoded error response
|
||||||
|
//
|
||||||
|
// in: body
|
||||||
|
Body JSONErrorMessage
|
||||||
|
}
|
||||||
|
@@ -127,10 +127,11 @@ func apiRoutes() *mux.Router {
|
|||||||
r.HandleFunc(config.Webroot+"api/v1/messages", middleWareFunc(apiv1.GetMessages)).Methods("GET")
|
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.SetReadStatus)).Methods("PUT")
|
||||||
r.HandleFunc(config.Webroot+"api/v1/messages", middleWareFunc(apiv1.DeleteMessages)).Methods("DELETE")
|
r.HandleFunc(config.Webroot+"api/v1/messages", middleWareFunc(apiv1.DeleteMessages)).Methods("DELETE")
|
||||||
r.HandleFunc(config.Webroot+"api/v1/tags", middleWareFunc(apiv1.GetAllTags)).Methods("GET")
|
|
||||||
r.HandleFunc(config.Webroot+"api/v1/tags", middleWareFunc(apiv1.SetMessageTags)).Methods("PUT")
|
|
||||||
r.HandleFunc(config.Webroot+"api/v1/search", middleWareFunc(apiv1.Search)).Methods("GET")
|
r.HandleFunc(config.Webroot+"api/v1/search", middleWareFunc(apiv1.Search)).Methods("GET")
|
||||||
r.HandleFunc(config.Webroot+"api/v1/search", middleWareFunc(apiv1.DeleteSearch)).Methods("DELETE")
|
r.HandleFunc(config.Webroot+"api/v1/search", middleWareFunc(apiv1.DeleteSearch)).Methods("DELETE")
|
||||||
|
r.HandleFunc(config.Webroot+"api/v1/send", middleWareFunc(apiv1.SendMessageHandler)).Methods("POST")
|
||||||
|
r.HandleFunc(config.Webroot+"api/v1/tags", middleWareFunc(apiv1.GetAllTags)).Methods("GET")
|
||||||
|
r.HandleFunc(config.Webroot+"api/v1/tags", middleWareFunc(apiv1.SetMessageTags)).Methods("PUT")
|
||||||
r.HandleFunc(config.Webroot+"api/v1/message/{id}/part/{partID}", middleWareFunc(apiv1.DownloadAttachment)).Methods("GET")
|
r.HandleFunc(config.Webroot+"api/v1/message/{id}/part/{partID}", middleWareFunc(apiv1.DownloadAttachment)).Methods("GET")
|
||||||
r.HandleFunc(config.Webroot+"api/v1/message/{id}/part/{partID}/thumb", middleWareFunc(apiv1.Thumbnail)).Methods("GET")
|
r.HandleFunc(config.Webroot+"api/v1/message/{id}/part/{partID}/thumb", middleWareFunc(apiv1.Thumbnail)).Methods("GET")
|
||||||
r.HandleFunc(config.Webroot+"api/v1/message/{id}/headers", middleWareFunc(apiv1.GetHeaders)).Methods("GET")
|
r.HandleFunc(config.Webroot+"api/v1/message/{id}/headers", middleWareFunc(apiv1.GetHeaders)).Methods("GET")
|
||||||
|
@@ -202,6 +202,106 @@ func TestAPIv1Search(t *testing.T) {
|
|||||||
assertSearchEqual(t, ts.URL+"/api/v1/search", "!tag:\"Test tag 023\"", 99)
|
assertSearchEqual(t, ts.URL+"/api/v1/search", "!tag:\"Test tag 023\"", 99)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAPIv1Send(t *testing.T) {
|
||||||
|
setup()
|
||||||
|
defer storage.Close()
|
||||||
|
|
||||||
|
r := apiRoutes()
|
||||||
|
|
||||||
|
ts := httptest.NewServer(r)
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
jsonData := `{
|
||||||
|
"From": {
|
||||||
|
"Email": "john@example.com",
|
||||||
|
"Name": "John Doe"
|
||||||
|
},
|
||||||
|
"To": [
|
||||||
|
{
|
||||||
|
"Email": "jane@example.com",
|
||||||
|
"Name": "Jane Doe"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Cc": [
|
||||||
|
{
|
||||||
|
"Email": "manager1@example.com",
|
||||||
|
"Name": "Manager 1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Email": "manager2@example.com",
|
||||||
|
"Name": "Manager 2"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Bcc": ["jack@example.com"],
|
||||||
|
"Headers": {
|
||||||
|
"X-IP": "1.2.3.4"
|
||||||
|
},
|
||||||
|
"Subject": "Mailpit message via the HTTP API",
|
||||||
|
"Text": "This is the text body",
|
||||||
|
"HTML": "<p style=\"font-family: arial\">Mailpit is <b>awesome</b>!</p>",
|
||||||
|
"Attachments": [
|
||||||
|
{
|
||||||
|
"Content": "VGhpcyBpcyBhIHBsYWluIHRleHQgYXR0YWNobWVudA==",
|
||||||
|
"Filename": "Attached File.txt"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ReplyTo": [
|
||||||
|
{
|
||||||
|
"Email": "secretary@example.com",
|
||||||
|
"Name": "Secretary"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Tags": [
|
||||||
|
"Tag 1",
|
||||||
|
"Tag 2"
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
t.Log("Sending message via HTTP API")
|
||||||
|
b, err := clientPost(ts.URL+"/api/v1/send", jsonData)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected nil, received %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := apiv1.SendMessageConfirmation{}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(b, &resp); err != nil {
|
||||||
|
t.Errorf(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Fetching response for message %s", resp.ID)
|
||||||
|
msg, err := fetchMessage(ts.URL + "/api/v1/message/" + resp.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Testing response for message %s", resp.ID)
|
||||||
|
assertEqual(t, `Mailpit message via the HTTP API`, msg.Subject, "wrong subject")
|
||||||
|
assertEqual(t, `This is the text body`, msg.Text, "wrong text")
|
||||||
|
assertEqual(t, `<p style="font-family: arial">Mailpit is <b>awesome</b>!</p>`, msg.HTML, "wrong HTML")
|
||||||
|
assertEqual(t, `"John Doe" <john@example.com>`, msg.From.String(), "wrong HTML")
|
||||||
|
assertEqual(t, 1, len(msg.To), "wrong To count")
|
||||||
|
assertEqual(t, `"Jane Doe" <jane@example.com>`, msg.To[0].String(), "wrong To address")
|
||||||
|
assertEqual(t, 2, len(msg.Cc), "wrong Cc count")
|
||||||
|
assertEqual(t, `"Manager 1" <manager1@example.com>`, msg.Cc[0].String(), "wrong Cc address")
|
||||||
|
assertEqual(t, `"Manager 2" <manager2@example.com>`, msg.Cc[1].String(), "wrong Cc address")
|
||||||
|
assertEqual(t, 1, len(msg.Bcc), "wrong Bcc count")
|
||||||
|
assertEqual(t, `<jack@example.com>`, msg.Bcc[0].String(), "wrong Bcc address")
|
||||||
|
assertEqual(t, 1, len(msg.ReplyTo), "wrong Reply-To count")
|
||||||
|
assertEqual(t, `"Secretary" <secretary@example.com>`, msg.ReplyTo[0].String(), "wrong Reply-To address")
|
||||||
|
assertEqual(t, 2, len(msg.Tags), "wrong Tags count")
|
||||||
|
assertEqual(t, `Tag 1,Tag 2`, strings.Join(msg.Tags, ","), "wrong Tags")
|
||||||
|
assertEqual(t, 1, len(msg.Attachments), "wrong Attachment count")
|
||||||
|
assertEqual(t, `Attached File.txt`, msg.Attachments[0].FileName, "wrong Attachment name")
|
||||||
|
|
||||||
|
attachmentBytes, err := clientGet(ts.URL + "/api/v1/message/" + resp.ID + "/part/" + msg.Attachments[0].PartID)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf(err.Error())
|
||||||
|
}
|
||||||
|
assertEqual(t, `This is a plain text attachment`, string(attachmentBytes), "wrong Attachment content")
|
||||||
|
}
|
||||||
|
|
||||||
func setup() {
|
func setup() {
|
||||||
logger.NoLogging = true
|
logger.NoLogging = true
|
||||||
config.MaxMessages = 0
|
config.MaxMessages = 0
|
||||||
@@ -288,7 +388,21 @@ func insertEmailData(t *testing.T) {
|
|||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchMessage(url string) (storage.Message, error) {
|
||||||
|
m := storage.Message{}
|
||||||
|
|
||||||
|
data, err := clientGet(url)
|
||||||
|
if err != nil {
|
||||||
|
return m, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(data, &m); err != nil {
|
||||||
|
return m, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchMessages(url string) (apiv1.MessagesSummary, error) {
|
func fetchMessages(url string) (apiv1.MessagesSummary, error) {
|
||||||
@@ -372,6 +486,31 @@ func clientPut(url, body string) ([]byte, error) {
|
|||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func clientPost(url, body string) ([]byte, error) {
|
||||||
|
client := new(http.Client)
|
||||||
|
|
||||||
|
b := strings.NewReader(body)
|
||||||
|
req, err := http.NewRequest("POST", url, b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("%s returned status %d", url, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := io.ReadAll(resp.Body)
|
||||||
|
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
|
||||||
func assertEqual(t *testing.T, a interface{}, b interface{}, message string) {
|
func assertEqual(t *testing.T, a interface{}, b interface{}, message string) {
|
||||||
if a == b {
|
if a == b {
|
||||||
return
|
return
|
||||||
|
@@ -23,7 +23,15 @@ var (
|
|||||||
DisableReverseDNS bool
|
DisableReverseDNS bool
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// MailHandler handles the incoming message to store in the database
|
||||||
func mailHandler(origin net.Addr, from string, to []string, data []byte) error {
|
func mailHandler(origin net.Addr, from string, to []string, data []byte) error {
|
||||||
|
_, err := Store(origin, from, to, data)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store will attempt to save a message to the database
|
||||||
|
func Store(origin net.Addr, from string, to []string, data []byte) (string, error) {
|
||||||
if !config.SMTPStrictRFCHeaders {
|
if !config.SMTPStrictRFCHeaders {
|
||||||
// replace all <CR><CR><LF> (\r\r\n) with <CR><LF> (\r\n)
|
// 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
|
// @see https://github.com/axllent/mailpit/issues/87 & https://github.com/axllent/mailpit/issues/153
|
||||||
@@ -34,7 +42,7 @@ func mailHandler(origin net.Addr, from string, to []string, data []byte) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Log().Errorf("[smtpd] error parsing message: %s", err.Error())
|
logger.Log().Errorf("[smtpd] error parsing message: %s", err.Error())
|
||||||
stats.LogSMTPRejected()
|
stats.LogSMTPRejected()
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// check / set the Return-Path based on SMTP from
|
// check / set the Return-Path based on SMTP from
|
||||||
@@ -70,7 +78,7 @@ func mailHandler(origin net.Addr, from string, to []string, data []byte) error {
|
|||||||
if storage.MessageIDExists(messageID) {
|
if storage.MessageIDExists(messageID) {
|
||||||
logger.Log().Debugf("[smtpd] duplicate message found, ignoring %s", messageID)
|
logger.Log().Debugf("[smtpd] duplicate message found, ignoring %s", messageID)
|
||||||
stats.LogSMTPIgnored()
|
stats.LogSMTPIgnored()
|
||||||
return nil
|
return "", nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,10 +125,10 @@ func mailHandler(origin net.Addr, from string, to []string, data []byte) error {
|
|||||||
logger.Log().Debugf("[smtpd] added missing addresses to Bcc header: %s", strings.Join(missingAddresses, ", "))
|
logger.Log().Debugf("[smtpd] added missing addresses to Bcc header: %s", strings.Join(missingAddresses, ", "))
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = storage.Store(&data)
|
id, err := storage.Store(&data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Log().Errorf("[db] error storing message: %s", err.Error())
|
logger.Log().Errorf("[db] error storing message: %s", err.Error())
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
stats.LogSMTPAccepted(len(data))
|
stats.LogSMTPAccepted(len(data))
|
||||||
@@ -130,7 +138,7 @@ func mailHandler(origin net.Addr, from string, to []string, data []byte) error {
|
|||||||
subject := msg.Header.Get("Subject")
|
subject := msg.Header.Get("Subject")
|
||||||
logger.Log().Debugf("[smtpd] received (%s) from:%s subject:%q", cleanIP(origin), from, subject)
|
logger.Log().Debugf("[smtpd] received (%s) from:%s subject:%q", cleanIP(origin), from, subject)
|
||||||
|
|
||||||
return nil
|
return id, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func authHandler(remoteAddr net.Addr, mechanism string, username []byte, password []byte, _ []byte) (bool, error) {
|
func authHandler(remoteAddr net.Addr, mechanism string, username []byte, password []byte, _ []byte) (bool, error) {
|
||||||
|
@@ -606,6 +606,43 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/send": {
|
||||||
|
"post": {
|
||||||
|
"description": "Send a message via the HTTP API.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"schemes": [
|
||||||
|
"http",
|
||||||
|
"https"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"message"
|
||||||
|
],
|
||||||
|
"summary": "Send a message",
|
||||||
|
"operationId": "SendMessage",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Body",
|
||||||
|
"in": "body",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/SendRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/responses/sendMessageResponse"
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"$ref": "#/responses/jsonErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/tags": {
|
"/api/v1/tags": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Returns a JSON array of all unique message tags.",
|
"description": "Returns a JSON array of all unique message tags.",
|
||||||
@@ -1083,6 +1120,18 @@
|
|||||||
"x-go-name": "Warning",
|
"x-go-name": "Warning",
|
||||||
"x-go-package": "github.com/axllent/mailpit/internal/htmlcheck"
|
"x-go-package": "github.com/axllent/mailpit/internal/htmlcheck"
|
||||||
},
|
},
|
||||||
|
"JSONErrorMessage": {
|
||||||
|
"description": "JSONErrorMessage struct",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"Error": {
|
||||||
|
"description": "Error message",
|
||||||
|
"type": "string",
|
||||||
|
"example": "invalid format"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
|
||||||
|
},
|
||||||
"Link": {
|
"Link": {
|
||||||
"description": "Link struct",
|
"description": "Link struct",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -1375,6 +1424,182 @@
|
|||||||
},
|
},
|
||||||
"x-go-package": "github.com/axllent/mailpit/internal/spamassassin"
|
"x-go-package": "github.com/axllent/mailpit/internal/spamassassin"
|
||||||
},
|
},
|
||||||
|
"SendMessageConfirmation": {
|
||||||
|
"description": "SendMessageConfirmation struct",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"ID": {
|
||||||
|
"description": "Database ID",
|
||||||
|
"type": "string",
|
||||||
|
"example": "iAfZVVe2UQFNSG5BAjgYwa"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
|
||||||
|
},
|
||||||
|
"SendRequest": {
|
||||||
|
"description": "SendRequest to send a message via HTTP",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"From"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Attachments": {
|
||||||
|
"description": "Attachments",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"Content",
|
||||||
|
"Filename"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Content": {
|
||||||
|
"description": "Base64-encoded string of the file content",
|
||||||
|
"type": "string",
|
||||||
|
"example": "VGhpcyBpcyBhIHBsYWluIHRleHQgYXR0YWNobWVudA=="
|
||||||
|
},
|
||||||
|
"Filename": {
|
||||||
|
"description": "Filename",
|
||||||
|
"type": "string",
|
||||||
|
"example": "AttachedFile.txt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Bcc": {
|
||||||
|
"description": "Bcc recipients email addresses only",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"example": [
|
||||||
|
"jack@example.com"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Cc": {
|
||||||
|
"description": "Cc recipients",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"Email"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Email": {
|
||||||
|
"description": "Email address",
|
||||||
|
"type": "string",
|
||||||
|
"example": "manager@example.com"
|
||||||
|
},
|
||||||
|
"Name": {
|
||||||
|
"description": "Optional name",
|
||||||
|
"type": "string",
|
||||||
|
"example": "Manager"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"From": {
|
||||||
|
"description": "\"From\" recipient",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"Email"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Email": {
|
||||||
|
"description": "Email address",
|
||||||
|
"type": "string",
|
||||||
|
"example": "john@example.com"
|
||||||
|
},
|
||||||
|
"Name": {
|
||||||
|
"description": "Optional name",
|
||||||
|
"type": "string",
|
||||||
|
"example": "John Doe"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"HTML": {
|
||||||
|
"description": "Message body (HTML)",
|
||||||
|
"type": "string",
|
||||||
|
"example": "\u003cp style=\"font-family: arial\"\u003eMailpit is \u003cb\u003eawesome\u003c/b\u003e!\u003c/p\u003e"
|
||||||
|
},
|
||||||
|
"Headers": {
|
||||||
|
"description": "Optional headers in {\"key\":\"value\"} format",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"X-IP": "1.2.3.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ReplyTo": {
|
||||||
|
"description": "Optional Reply-To recipients",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"Email"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Email": {
|
||||||
|
"description": "Email address",
|
||||||
|
"type": "string",
|
||||||
|
"example": "secretary@example.com"
|
||||||
|
},
|
||||||
|
"Name": {
|
||||||
|
"description": "Optional name",
|
||||||
|
"type": "string",
|
||||||
|
"example": "Secretary"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Subject": {
|
||||||
|
"description": "Subject",
|
||||||
|
"type": "string",
|
||||||
|
"example": "Mailpit message via the HTTP API"
|
||||||
|
},
|
||||||
|
"Tags": {
|
||||||
|
"description": "Mailpit tags",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"example": [
|
||||||
|
"Tag 1",
|
||||||
|
"Tag 2"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Text": {
|
||||||
|
"description": "Message body (text)",
|
||||||
|
"type": "string",
|
||||||
|
"example": "This is the text body"
|
||||||
|
},
|
||||||
|
"To": {
|
||||||
|
"description": "\"To\" recipients",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"Email"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Email": {
|
||||||
|
"description": "Email address",
|
||||||
|
"type": "string",
|
||||||
|
"example": "jane@example.com"
|
||||||
|
},
|
||||||
|
"Name": {
|
||||||
|
"description": "Optional name",
|
||||||
|
"type": "string",
|
||||||
|
"example": "Jane Doe"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
|
||||||
|
},
|
||||||
"SpamAssassinResponse": {
|
"SpamAssassinResponse": {
|
||||||
"description": "Result is a SpamAssassin result",
|
"description": "Result is a SpamAssassin result",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -1582,6 +1807,18 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/WebUIConfiguration"
|
"$ref": "#/definitions/WebUIConfiguration"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"jsonErrorResponse": {
|
||||||
|
"description": "JSON error response",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/JSONErrorMessage"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sendMessageResponse": {
|
||||||
|
"description": "Confirmation message for HTTP send API",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/SendMessageConfirmation"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user