diff --git a/internal/tools/utils.go b/internal/tools/utils.go
index f152c6e..2064f6f 100644
--- a/internal/tools/utils.go
+++ b/internal/tools/utils.go
@@ -1,6 +1,9 @@
package tools
-import "fmt"
+import (
+ "fmt"
+ "strings"
+)
// Plural returns a singular or plural of a word together with the total
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)
}
+
+// 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
+}
diff --git a/server/apiv1/api.go b/server/apiv1/api.go
index 24a3a66..e562876 100644
--- a/server/apiv1/api.go
+++ b/server/apiv1/api.go
@@ -900,6 +900,19 @@ func httpError(w http.ResponseWriter, msg string) {
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
func getStartLimit(req *http.Request) (start int, limit int) {
start = 0
diff --git a/server/apiv1/send.go b/server/apiv1/send.go
new file mode 100644
index 0000000..7733380
--- /dev/null
+++ b/server/apiv1/send.go
@@ -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:
Mailpit is awesome!
+ 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,
+ 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())
+}
diff --git a/server/apiv1/swagger.go b/server/apiv1/swagger.go
index 8dffcf0..96bfe81 100644
--- a/server/apiv1/swagger.go
+++ b/server/apiv1/swagger.go
@@ -170,6 +170,7 @@ type htmlResponse string
// HTTP error response will return with a >= 400 response code
// swagger:response ErrorResponse
+// example: invalid request
type errorResponse string
// Plain text "ok" response
@@ -179,3 +180,21 @@ type okResponse string
// Plain JSON array response
// swagger:response ArrayResponse
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
+}
diff --git a/server/server.go b/server/server.go
index f659165..bc0e058 100644
--- a/server/server.go
+++ b/server/server.go
@@ -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.SetReadStatus)).Methods("PUT")
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.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}/thumb", middleWareFunc(apiv1.Thumbnail)).Methods("GET")
r.HandleFunc(config.Webroot+"api/v1/message/{id}/headers", middleWareFunc(apiv1.GetHeaders)).Methods("GET")
diff --git a/server/server_test.go b/server/server_test.go
index 206a1c4..3646e31 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -202,6 +202,106 @@ func TestAPIv1Search(t *testing.T) {
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": "Mailpit is awesome!
",
+ "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, `Mailpit is awesome!
`, msg.HTML, "wrong HTML")
+ assertEqual(t, `"John Doe" `, msg.From.String(), "wrong HTML")
+ assertEqual(t, 1, len(msg.To), "wrong To count")
+ assertEqual(t, `"Jane Doe" `, msg.To[0].String(), "wrong To address")
+ assertEqual(t, 2, len(msg.Cc), "wrong Cc count")
+ assertEqual(t, `"Manager 1" `, msg.Cc[0].String(), "wrong Cc address")
+ assertEqual(t, `"Manager 2" `, msg.Cc[1].String(), "wrong Cc address")
+ assertEqual(t, 1, len(msg.Bcc), "wrong Bcc count")
+ assertEqual(t, ``, msg.Bcc[0].String(), "wrong Bcc address")
+ assertEqual(t, 1, len(msg.ReplyTo), "wrong Reply-To count")
+ assertEqual(t, `"Secretary" `, 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() {
logger.NoLogging = true
config.MaxMessages = 0
@@ -288,7 +388,21 @@ func insertEmailData(t *testing.T) {
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) {
@@ -372,6 +486,31 @@ func clientPut(url, body string) ([]byte, error) {
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) {
if a == b {
return
diff --git a/server/smtpd/smtpd.go b/server/smtpd/smtpd.go
index 169b5e5..95580df 100644
--- a/server/smtpd/smtpd.go
+++ b/server/smtpd/smtpd.go
@@ -23,7 +23,15 @@ var (
DisableReverseDNS bool
)
+// MailHandler handles the incoming message to store in the database
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 {
// replace all (\r\r\n) with (\r\n)
// @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 {
logger.Log().Errorf("[smtpd] error parsing message: %s", err.Error())
stats.LogSMTPRejected()
- return err
+ return "", err
}
// 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) {
logger.Log().Debugf("[smtpd] duplicate message found, ignoring %s", messageID)
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, ", "))
}
- _, err = storage.Store(&data)
+ id, err := storage.Store(&data)
if err != nil {
logger.Log().Errorf("[db] error storing message: %s", err.Error())
- return err
+ return "", err
}
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")
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) {
diff --git a/server/ui/api/v1/swagger.json b/server/ui/api/v1/swagger.json
index 984850e..7a461a4 100644
--- a/server/ui/api/v1/swagger.json
+++ b/server/ui/api/v1/swagger.json
@@ -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": {
"get": {
"description": "Returns a JSON array of all unique message tags.",
@@ -1083,6 +1120,18 @@
"x-go-name": "Warning",
"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": {
"description": "Link struct",
"type": "object",
@@ -1375,6 +1424,182 @@
},
"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": {
"description": "Result is a SpamAssassin result",
"type": "object",
@@ -1582,6 +1807,18 @@
"schema": {
"$ref": "#/definitions/WebUIConfiguration"
}
+ },
+ "jsonErrorResponse": {
+ "description": "JSON error response",
+ "schema": {
+ "$ref": "#/definitions/JSONErrorMessage"
+ }
+ },
+ "sendMessageResponse": {
+ "description": "Confirmation message for HTTP send API",
+ "schema": {
+ "$ref": "#/definitions/SendMessageConfirmation"
+ }
}
}
}
\ No newline at end of file