mirror of
https://github.com/axllent/mailpit.git
synced 2025-06-25 00:37:17 +02:00
Chore: Add swagger examples & API code restructure
This commit is contained in:
@ -2,692 +2,16 @@
|
|||||||
package apiv1
|
package apiv1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/mail"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/araddon/dateparse"
|
"github.com/araddon/dateparse"
|
||||||
"github.com/axllent/mailpit/config"
|
"github.com/axllent/mailpit/config"
|
||||||
"github.com/axllent/mailpit/internal/htmlcheck"
|
|
||||||
"github.com/axllent/mailpit/internal/linkcheck"
|
|
||||||
"github.com/axllent/mailpit/internal/logger"
|
"github.com/axllent/mailpit/internal/logger"
|
||||||
"github.com/axllent/mailpit/internal/spamassassin"
|
|
||||||
"github.com/axllent/mailpit/internal/storage"
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/jhillyerd/enmime"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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
|
|
||||||
//
|
|
||||||
// # List messages
|
|
||||||
//
|
|
||||||
// Returns messages from the mailbox ordered from newest to oldest.
|
|
||||||
//
|
|
||||||
// Produces:
|
|
||||||
// - application/json
|
|
||||||
//
|
|
||||||
// Schemes: http, https
|
|
||||||
//
|
|
||||||
// Parameters:
|
|
||||||
// + name: start
|
|
||||||
// in: query
|
|
||||||
// description: Pagination offset
|
|
||||||
// required: false
|
|
||||||
// type: integer
|
|
||||||
// default: 0
|
|
||||||
// + name: limit
|
|
||||||
// in: query
|
|
||||||
// description: Limit results
|
|
||||||
// required: false
|
|
||||||
// type: integer
|
|
||||||
// default: 50
|
|
||||||
//
|
|
||||||
// Responses:
|
|
||||||
// 200: MessagesSummaryResponse
|
|
||||||
// default: ErrorResponse
|
|
||||||
start, beforeTS, limit := getStartLimit(r)
|
|
||||||
|
|
||||||
messages, err := storage.List(start, beforeTS, limit)
|
|
||||||
if err != nil {
|
|
||||||
httpError(w, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
stats := storage.StatsGet()
|
|
||||||
|
|
||||||
var res MessagesSummary
|
|
||||||
|
|
||||||
res.Start = start
|
|
||||||
res.Messages = messages
|
|
||||||
res.Count = float64(len(messages)) // legacy - now undocumented in API specs
|
|
||||||
res.Total = stats.Total
|
|
||||||
res.Unread = stats.Unread
|
|
||||||
res.Tags = stats.Tags
|
|
||||||
res.MessagesCount = stats.Total
|
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "application/json")
|
|
||||||
if err := json.NewEncoder(w).Encode(res); err != nil {
|
|
||||||
httpError(w, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search returns the latest messages as JSON
|
|
||||||
func Search(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// swagger:route GET /api/v1/search messages MessagesSummary
|
|
||||||
//
|
|
||||||
// # Search messages
|
|
||||||
//
|
|
||||||
// Returns messages matching [a search](https://mailpit.axllent.org/docs/usage/search-filters/), sorted by received date (descending).
|
|
||||||
//
|
|
||||||
// Produces:
|
|
||||||
// - application/json
|
|
||||||
//
|
|
||||||
// Schemes: http, https
|
|
||||||
//
|
|
||||||
// Parameters:
|
|
||||||
// + name: query
|
|
||||||
// in: query
|
|
||||||
// description: Search query
|
|
||||||
// required: true
|
|
||||||
// type: string
|
|
||||||
// + name: start
|
|
||||||
// in: query
|
|
||||||
// description: Pagination offset
|
|
||||||
// required: false
|
|
||||||
// type: integer
|
|
||||||
// default: 0
|
|
||||||
// + name: limit
|
|
||||||
// in: query
|
|
||||||
// description: Limit results
|
|
||||||
// required: false
|
|
||||||
// type: integer
|
|
||||||
// default: 50
|
|
||||||
// + name: tz
|
|
||||||
// in: query
|
|
||||||
// description: [Timezone identifier](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) used specifically for `before:` & `after:` searches (eg: "Pacific/Auckland").
|
|
||||||
// required: false
|
|
||||||
// type: string
|
|
||||||
//
|
|
||||||
// Responses:
|
|
||||||
// 200: MessagesSummaryResponse
|
|
||||||
// default: ErrorResponse
|
|
||||||
search := strings.TrimSpace(r.URL.Query().Get("query"))
|
|
||||||
if search == "" {
|
|
||||||
httpError(w, "Error: no search query")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
start, beforeTS, limit := getStartLimit(r)
|
|
||||||
|
|
||||||
messages, results, err := storage.Search(search, r.URL.Query().Get("tz"), start, beforeTS, limit)
|
|
||||||
if err != nil {
|
|
||||||
httpError(w, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
stats := storage.StatsGet()
|
|
||||||
|
|
||||||
var res MessagesSummary
|
|
||||||
|
|
||||||
res.Start = start
|
|
||||||
res.Messages = messages
|
|
||||||
res.Count = float64(len(messages)) // legacy - now undocumented in API specs
|
|
||||||
res.Total = stats.Total // total messages in mailbox
|
|
||||||
res.MessagesCount = float64(results)
|
|
||||||
res.Unread = stats.Unread
|
|
||||||
res.Tags = stats.Tags
|
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "application/json")
|
|
||||||
if err := json.NewEncoder(w).Encode(res); err != nil {
|
|
||||||
httpError(w, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteSearch will delete all messages matching a search
|
|
||||||
func DeleteSearch(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// swagger:route DELETE /api/v1/search messages DeleteSearch
|
|
||||||
//
|
|
||||||
// # Delete messages by search
|
|
||||||
//
|
|
||||||
// Delete all messages matching [a search](https://mailpit.axllent.org/docs/usage/search-filters/).
|
|
||||||
//
|
|
||||||
// Produces:
|
|
||||||
// - application/json
|
|
||||||
//
|
|
||||||
// Schemes: http, https
|
|
||||||
//
|
|
||||||
// Parameters:
|
|
||||||
// + name: query
|
|
||||||
// in: query
|
|
||||||
// description: Search query
|
|
||||||
// required: true
|
|
||||||
// type: string
|
|
||||||
// + name: tz
|
|
||||||
// in: query
|
|
||||||
// description: [Timezone identifier](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) used specifically for `before:` & `after:` searches (eg: "Pacific/Auckland").
|
|
||||||
// required: false
|
|
||||||
// type: string
|
|
||||||
//
|
|
||||||
// Responses:
|
|
||||||
// 200: OKResponse
|
|
||||||
// default: ErrorResponse
|
|
||||||
search := strings.TrimSpace(r.URL.Query().Get("query"))
|
|
||||||
if search == "" {
|
|
||||||
httpError(w, "Error: no search query")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := storage.DeleteSearch(search, r.URL.Query().Get("tz")); err != nil {
|
|
||||||
httpError(w, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "text/plain")
|
|
||||||
_, _ = w.Write([]byte("ok"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMessage (method: GET) returns the Message as JSON
|
|
||||||
func GetMessage(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// swagger:route GET /api/v1/message/{ID} message Message
|
|
||||||
//
|
|
||||||
// # Get message summary
|
|
||||||
//
|
|
||||||
// Returns the summary of a message, marking the message as read.
|
|
||||||
//
|
|
||||||
// The ID can be set to `latest` to return the latest message.
|
|
||||||
//
|
|
||||||
// Produces:
|
|
||||||
// - application/json
|
|
||||||
//
|
|
||||||
// Schemes: http, https
|
|
||||||
//
|
|
||||||
// Parameters:
|
|
||||||
// + name: ID
|
|
||||||
// in: path
|
|
||||||
// description: Message database ID or "latest"
|
|
||||||
// required: true
|
|
||||||
// type: string
|
|
||||||
//
|
|
||||||
// Responses:
|
|
||||||
// 200: Message
|
|
||||||
// default: ErrorResponse
|
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
|
|
||||||
id := vars["id"]
|
|
||||||
|
|
||||||
if id == "latest" {
|
|
||||||
var err error
|
|
||||||
id, err = storage.LatestID(r)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(404)
|
|
||||||
fmt.Fprint(w, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
msg, err := storage.GetMessage(id)
|
|
||||||
if err != nil {
|
|
||||||
fourOFour(w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "application/json")
|
|
||||||
if err := json.NewEncoder(w).Encode(msg); err != nil {
|
|
||||||
httpError(w, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DownloadAttachment (method: GET) returns the attachment data
|
|
||||||
func DownloadAttachment(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// swagger:route GET /api/v1/message/{ID}/part/{PartID} message Attachment
|
|
||||||
//
|
|
||||||
// # Get message attachment
|
|
||||||
//
|
|
||||||
// This will return the attachment part using the appropriate Content-Type.
|
|
||||||
//
|
|
||||||
// Produces:
|
|
||||||
// - application/*
|
|
||||||
// - image/*
|
|
||||||
// - text/*
|
|
||||||
//
|
|
||||||
// Schemes: http, https
|
|
||||||
//
|
|
||||||
// Parameters:
|
|
||||||
// + name: ID
|
|
||||||
// in: path
|
|
||||||
// description: Message database ID
|
|
||||||
// required: true
|
|
||||||
// type: string
|
|
||||||
// + name: PartID
|
|
||||||
// in: path
|
|
||||||
// description: Attachment part ID
|
|
||||||
// required: true
|
|
||||||
// type: string
|
|
||||||
//
|
|
||||||
// Responses:
|
|
||||||
// 200: BinaryResponse
|
|
||||||
// default: ErrorResponse
|
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
|
|
||||||
id := vars["id"]
|
|
||||||
partID := vars["partID"]
|
|
||||||
|
|
||||||
a, err := storage.GetAttachmentPart(id, partID)
|
|
||||||
if err != nil {
|
|
||||||
fourOFour(w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fileName := a.FileName
|
|
||||||
if fileName == "" {
|
|
||||||
fileName = a.ContentID
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Add("Content-Type", a.ContentType)
|
|
||||||
w.Header().Set("Content-Disposition", "filename=\""+fileName+"\"")
|
|
||||||
_, _ = w.Write(a.Content)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetHeaders (method: GET) returns the message headers as JSON
|
|
||||||
func GetHeaders(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// swagger:route GET /api/v1/message/{ID}/headers message Headers
|
|
||||||
//
|
|
||||||
// # Get message headers
|
|
||||||
//
|
|
||||||
// Returns the message headers as an array.
|
|
||||||
//
|
|
||||||
// The ID can be set to `latest` to return the latest message headers.
|
|
||||||
//
|
|
||||||
// Produces:
|
|
||||||
// - application/json
|
|
||||||
//
|
|
||||||
// Schemes: http, https
|
|
||||||
//
|
|
||||||
// Parameters:
|
|
||||||
// + name: ID
|
|
||||||
// in: path
|
|
||||||
// description: Message database ID or "latest"
|
|
||||||
// required: true
|
|
||||||
// type: string
|
|
||||||
//
|
|
||||||
// Responses:
|
|
||||||
// 200: MessageHeaders
|
|
||||||
// default: ErrorResponse
|
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
|
|
||||||
id := vars["id"]
|
|
||||||
|
|
||||||
if id == "latest" {
|
|
||||||
var err error
|
|
||||||
id, err = storage.LatestID(r)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(404)
|
|
||||||
fmt.Fprint(w, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := storage.GetMessageRaw(id)
|
|
||||||
if err != nil {
|
|
||||||
fourOFour(w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
reader := bytes.NewReader(data)
|
|
||||||
m, err := mail.ReadMessage(reader)
|
|
||||||
if err != nil {
|
|
||||||
httpError(w, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "application/json")
|
|
||||||
if err := json.NewEncoder(w).Encode(m.Header); err != nil {
|
|
||||||
httpError(w, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DownloadRaw (method: GET) returns the full email source as plain text
|
|
||||||
func DownloadRaw(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// swagger:route GET /api/v1/message/{ID}/raw message Raw
|
|
||||||
//
|
|
||||||
// # Get message source
|
|
||||||
//
|
|
||||||
// Returns the full email source as plain text.
|
|
||||||
//
|
|
||||||
// The ID can be set to `latest` to return the latest message source.
|
|
||||||
//
|
|
||||||
// Produces:
|
|
||||||
// - text/plain
|
|
||||||
//
|
|
||||||
// Schemes: http, https
|
|
||||||
//
|
|
||||||
// Parameters:
|
|
||||||
// + name: ID
|
|
||||||
// in: path
|
|
||||||
// description: Message database ID or "latest"
|
|
||||||
// required: true
|
|
||||||
// type: string
|
|
||||||
//
|
|
||||||
// Responses:
|
|
||||||
// 200: TextResponse
|
|
||||||
// default: ErrorResponse
|
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
|
|
||||||
id := vars["id"]
|
|
||||||
dl := r.FormValue("dl")
|
|
||||||
|
|
||||||
if id == "latest" {
|
|
||||||
var err error
|
|
||||||
id, err = storage.LatestID(r)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(404)
|
|
||||||
fmt.Fprint(w, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := storage.GetMessageRaw(id)
|
|
||||||
if err != nil {
|
|
||||||
fourOFour(w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
||||||
if dl == "1" {
|
|
||||||
w.Header().Set("Content-Disposition", "attachment; filename=\""+id+".eml\"")
|
|
||||||
}
|
|
||||||
_, _ = w.Write(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteMessages (method: DELETE) deletes all messages matching IDS.
|
|
||||||
func DeleteMessages(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// swagger:route DELETE /api/v1/messages messages DeleteMessages
|
|
||||||
//
|
|
||||||
// # Delete messages
|
|
||||||
//
|
|
||||||
// Delete individual or all messages. If no IDs are provided then all messages are deleted.
|
|
||||||
//
|
|
||||||
// Consumes:
|
|
||||||
// - application/json
|
|
||||||
//
|
|
||||||
// Produces:
|
|
||||||
// - text/plain
|
|
||||||
//
|
|
||||||
// Schemes: http, https
|
|
||||||
//
|
|
||||||
// Responses:
|
|
||||||
// 200: OKResponse
|
|
||||||
// default: ErrorResponse
|
|
||||||
|
|
||||||
decoder := json.NewDecoder(r.Body)
|
|
||||||
var data struct {
|
|
||||||
IDs []string
|
|
||||||
}
|
|
||||||
err := decoder.Decode(&data)
|
|
||||||
if err != nil || len(data.IDs) == 0 {
|
|
||||||
if err := storage.DeleteAllMessages(); err != nil {
|
|
||||||
httpError(w, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := storage.DeleteMessages(data.IDs); err != nil {
|
|
||||||
httpError(w, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "text/plain")
|
|
||||||
_, _ = w.Write([]byte("ok"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetReadStatus (method: PUT) will update the status to Read/Unread for all provided IDs
|
|
||||||
// If no IDs are provided then all messages are updated.
|
|
||||||
func SetReadStatus(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// swagger:route PUT /api/v1/messages messages SetReadStatus
|
|
||||||
//
|
|
||||||
// # Set read status
|
|
||||||
//
|
|
||||||
// If no IDs are provided then all messages are updated.
|
|
||||||
//
|
|
||||||
// Consumes:
|
|
||||||
// - application/json
|
|
||||||
//
|
|
||||||
// Produces:
|
|
||||||
// - text/plain
|
|
||||||
//
|
|
||||||
// Schemes: http, https
|
|
||||||
//
|
|
||||||
// Responses:
|
|
||||||
// 200: OKResponse
|
|
||||||
// default: ErrorResponse
|
|
||||||
|
|
||||||
decoder := json.NewDecoder(r.Body)
|
|
||||||
|
|
||||||
var data struct {
|
|
||||||
Read bool
|
|
||||||
IDs []string
|
|
||||||
}
|
|
||||||
|
|
||||||
err := decoder.Decode(&data)
|
|
||||||
if err != nil {
|
|
||||||
httpError(w, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ids := data.IDs
|
|
||||||
|
|
||||||
if len(ids) == 0 {
|
|
||||||
if data.Read {
|
|
||||||
err := storage.MarkAllRead()
|
|
||||||
if err != nil {
|
|
||||||
httpError(w, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
err := storage.MarkAllUnread()
|
|
||||||
if err != nil {
|
|
||||||
httpError(w, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if data.Read {
|
|
||||||
for _, id := range ids {
|
|
||||||
if err := storage.MarkRead(id); err != nil {
|
|
||||||
httpError(w, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for _, id := range ids {
|
|
||||||
if err := storage.MarkUnread(id); err != nil {
|
|
||||||
httpError(w, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "text/plain")
|
|
||||||
_, _ = w.Write([]byte("ok"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTMLCheck returns a summary of the HTML client support
|
|
||||||
func HTMLCheck(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// swagger:route GET /api/v1/message/{ID}/html-check Other HTMLCheck
|
|
||||||
//
|
|
||||||
// # HTML check
|
|
||||||
//
|
|
||||||
// Returns the summary of the message HTML checker.
|
|
||||||
//
|
|
||||||
// Produces:
|
|
||||||
// - application/json
|
|
||||||
//
|
|
||||||
// Schemes: http, https
|
|
||||||
//
|
|
||||||
// Responses:
|
|
||||||
// 200: HTMLCheckResponse
|
|
||||||
// default: ErrorResponse
|
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
id := vars["id"]
|
|
||||||
|
|
||||||
if id == "latest" {
|
|
||||||
var err error
|
|
||||||
id, err = storage.LatestID(r)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(404)
|
|
||||||
fmt.Fprint(w, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
raw, err := storage.GetMessageRaw(id)
|
|
||||||
if err != nil {
|
|
||||||
fourOFour(w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
e := bytes.NewReader(raw)
|
|
||||||
|
|
||||||
parser := enmime.NewParser(enmime.DisableCharacterDetection(true))
|
|
||||||
|
|
||||||
msg, err := parser.ReadEnvelope(e)
|
|
||||||
if err != nil {
|
|
||||||
httpError(w, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if msg.HTML == "" {
|
|
||||||
httpError(w, "message does not contain HTML")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
checks, err := htmlcheck.RunTests(msg.HTML)
|
|
||||||
if err != nil {
|
|
||||||
httpError(w, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "application/json")
|
|
||||||
if err := json.NewEncoder(w).Encode(checks); err != nil {
|
|
||||||
httpError(w, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// LinkCheck returns a summary of links in the email
|
|
||||||
func LinkCheck(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// swagger:route GET /api/v1/message/{ID}/link-check Other LinkCheck
|
|
||||||
//
|
|
||||||
// # Link check
|
|
||||||
//
|
|
||||||
// Returns the summary of the message Link checker.
|
|
||||||
//
|
|
||||||
// Produces:
|
|
||||||
// - application/json
|
|
||||||
//
|
|
||||||
// Schemes: http, https
|
|
||||||
//
|
|
||||||
// Responses:
|
|
||||||
// 200: LinkCheckResponse
|
|
||||||
// default: ErrorResponse
|
|
||||||
|
|
||||||
if config.DemoMode {
|
|
||||||
httpError(w, "this functionality has been disabled for demonstration purposes")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
id := vars["id"]
|
|
||||||
|
|
||||||
if id == "latest" {
|
|
||||||
var err error
|
|
||||||
id, err = storage.LatestID(r)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(404)
|
|
||||||
fmt.Fprint(w, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
msg, err := storage.GetMessage(id)
|
|
||||||
if err != nil {
|
|
||||||
fourOFour(w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
f := r.URL.Query().Get("follow")
|
|
||||||
followRedirects := f == "true" || f == "1"
|
|
||||||
|
|
||||||
summary, err := linkcheck.RunTests(msg, followRedirects)
|
|
||||||
if err != nil {
|
|
||||||
httpError(w, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "application/json")
|
|
||||||
if err := json.NewEncoder(w).Encode(summary); err != nil {
|
|
||||||
httpError(w, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SpamAssassinCheck returns a summary of SpamAssassin results (if enabled)
|
|
||||||
func SpamAssassinCheck(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// swagger:route GET /api/v1/message/{ID}/sa-check Other SpamAssassinCheck
|
|
||||||
//
|
|
||||||
// # SpamAssassin check
|
|
||||||
//
|
|
||||||
// Returns the SpamAssassin summary (if enabled) of the message.
|
|
||||||
//
|
|
||||||
// Produces:
|
|
||||||
// - application/json
|
|
||||||
//
|
|
||||||
// Schemes: http, https
|
|
||||||
//
|
|
||||||
// Responses:
|
|
||||||
// 200: SpamAssassinResponse
|
|
||||||
// default: ErrorResponse
|
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
id := vars["id"]
|
|
||||||
|
|
||||||
if id == "latest" {
|
|
||||||
var err error
|
|
||||||
id, err = storage.LatestID(r)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(404)
|
|
||||||
fmt.Fprint(w, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
msg, err := storage.GetMessageRaw(id)
|
|
||||||
if err != nil {
|
|
||||||
fourOFour(w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
summary, err := spamassassin.Check(msg)
|
|
||||||
if err != nil {
|
|
||||||
httpError(w, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "application/json")
|
|
||||||
if err := json.NewEncoder(w).Encode(summary); err != nil {
|
|
||||||
httpError(w, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FourOFour returns a basic 404 message
|
// FourOFour returns a basic 404 message
|
||||||
func fourOFour(w http.ResponseWriter) {
|
func fourOFour(w http.ResponseWriter) {
|
||||||
w.Header().Set("Referrer-Policy", "no-referrer")
|
w.Header().Set("Referrer-Policy", "no-referrer")
|
||||||
|
@ -6,8 +6,41 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/axllent/mailpit/config"
|
"github.com/axllent/mailpit/config"
|
||||||
|
"github.com/axllent/mailpit/internal/stats"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Application information
|
||||||
|
// swagger:response AppInfoResponse
|
||||||
|
type appInfoResponse struct {
|
||||||
|
// Application information
|
||||||
|
//
|
||||||
|
// in: body
|
||||||
|
Body stats.AppInformation
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppInfo returns some basic details about the running app, and latest release.
|
||||||
|
func AppInfo(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
// swagger:route GET /api/v1/info application AppInformation
|
||||||
|
//
|
||||||
|
// # Get application information
|
||||||
|
//
|
||||||
|
// Returns basic runtime information, message totals and latest release version.
|
||||||
|
//
|
||||||
|
// Produces:
|
||||||
|
// - application/json
|
||||||
|
//
|
||||||
|
// Schemes: http, https
|
||||||
|
//
|
||||||
|
// Responses:
|
||||||
|
// 200: AppInfoResponse
|
||||||
|
// 400: ErrorResponse
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "application/json")
|
||||||
|
if err := json.NewEncoder(w).Encode(stats.Load()); err != nil {
|
||||||
|
httpError(w, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Response includes global web UI settings
|
// Response includes global web UI settings
|
||||||
//
|
//
|
||||||
// swagger:model WebUIConfiguration
|
// swagger:model WebUIConfiguration
|
||||||
@ -38,6 +71,15 @@ type webUIConfiguration struct {
|
|||||||
DuplicatesIgnored bool
|
DuplicatesIgnored bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Web UI configuration response
|
||||||
|
// swagger:response WebUIConfigurationResponse
|
||||||
|
type webUIConfigurationResponse struct {
|
||||||
|
// Web UI configuration settings
|
||||||
|
//
|
||||||
|
// in: body
|
||||||
|
Body webUIConfiguration
|
||||||
|
}
|
||||||
|
|
||||||
// WebUIConfig returns configuration settings for the web UI.
|
// WebUIConfig returns configuration settings for the web UI.
|
||||||
func WebUIConfig(w http.ResponseWriter, _ *http.Request) {
|
func WebUIConfig(w http.ResponseWriter, _ *http.Request) {
|
||||||
// swagger:route GET /api/v1/webui application WebUIConfiguration
|
// swagger:route GET /api/v1/webui application WebUIConfiguration
|
||||||
@ -48,13 +90,14 @@ func WebUIConfig(w http.ResponseWriter, _ *http.Request) {
|
|||||||
// Intended for web UI only!
|
// Intended for web UI only!
|
||||||
//
|
//
|
||||||
// Produces:
|
// Produces:
|
||||||
// - application/json
|
// - application/json
|
||||||
//
|
//
|
||||||
// Schemes: http, https
|
// Schemes: http, https
|
||||||
//
|
//
|
||||||
// Responses:
|
// Responses:
|
||||||
// 200: WebUIConfigurationResponse
|
// 200: WebUIConfigurationResponse
|
||||||
// default: ErrorResponse
|
// 400: ErrorResponse
|
||||||
|
|
||||||
conf := webUIConfiguration{}
|
conf := webUIConfiguration{}
|
||||||
|
|
||||||
conf.Label = config.Label
|
conf.Label = config.Label
|
@ -1,31 +0,0 @@
|
|||||||
package apiv1
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/axllent/mailpit/internal/stats"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AppInfo returns some basic details about the running app, and latest release.
|
|
||||||
func AppInfo(w http.ResponseWriter, _ *http.Request) {
|
|
||||||
// swagger:route GET /api/v1/info application AppInformation
|
|
||||||
//
|
|
||||||
// # Get application information
|
|
||||||
//
|
|
||||||
// Returns basic runtime information, message totals and latest release version.
|
|
||||||
//
|
|
||||||
// Produces:
|
|
||||||
// - application/json
|
|
||||||
//
|
|
||||||
// Schemes: http, https
|
|
||||||
//
|
|
||||||
// Responses:
|
|
||||||
// 200: InfoResponse
|
|
||||||
// default: ErrorResponse
|
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "application/json")
|
|
||||||
if err := json.NewEncoder(w).Encode(stats.Load()); err != nil {
|
|
||||||
httpError(w, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
252
server/apiv1/message.go
Normal file
252
server/apiv1/message.go
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
package apiv1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/mail"
|
||||||
|
|
||||||
|
"github.com/axllent/mailpit/internal/storage"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
// swagger:parameters GetMessageParams
|
||||||
|
type getMessageParams struct {
|
||||||
|
// Message database ID or "latest"
|
||||||
|
//
|
||||||
|
// in: path
|
||||||
|
// required: true
|
||||||
|
// example: 4oRBnPtCXgAqZniRhzLNmS
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMessage (method: GET) returns the Message as JSON
|
||||||
|
func GetMessage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// swagger:route GET /api/v1/message/{ID} message GetMessageParams
|
||||||
|
//
|
||||||
|
// # Get message summary
|
||||||
|
//
|
||||||
|
// Returns the summary of a message, marking the message as read.
|
||||||
|
//
|
||||||
|
// The ID can be set to `latest` to return the latest message.
|
||||||
|
//
|
||||||
|
// Produces:
|
||||||
|
// - application/json
|
||||||
|
//
|
||||||
|
// Schemes: http, https
|
||||||
|
//
|
||||||
|
// Responses:
|
||||||
|
// 200: Message
|
||||||
|
// 400: ErrorResponse
|
||||||
|
// 404: NotFoundResponse
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
|
||||||
|
id := vars["id"]
|
||||||
|
|
||||||
|
if id == "latest" {
|
||||||
|
var err error
|
||||||
|
id, err = storage.LatestID(r)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(404)
|
||||||
|
fmt.Fprint(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := storage.GetMessage(id)
|
||||||
|
if err != nil {
|
||||||
|
fourOFour(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "application/json")
|
||||||
|
if err := json.NewEncoder(w).Encode(msg); err != nil {
|
||||||
|
httpError(w, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// swagger:parameters GetHeadersParams
|
||||||
|
type getHeadersParams struct {
|
||||||
|
// Message database ID or "latest"
|
||||||
|
//
|
||||||
|
// in: path
|
||||||
|
// required: true
|
||||||
|
// example: 4oRBnPtCXgAqZniRhzLNmS
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message headers
|
||||||
|
// swagger:model MessageHeadersResponse
|
||||||
|
type messageHeaders map[string][]string
|
||||||
|
|
||||||
|
// GetHeaders (method: GET) returns the message headers as JSON
|
||||||
|
func GetHeaders(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// swagger:route GET /api/v1/message/{ID}/headers message GetHeadersParams
|
||||||
|
//
|
||||||
|
// # Get message headers
|
||||||
|
//
|
||||||
|
// Returns the message headers as an array. Note that header keys are returned alphabetically.
|
||||||
|
//
|
||||||
|
// The ID can be set to `latest` to return the latest message headers.
|
||||||
|
//
|
||||||
|
// Produces:
|
||||||
|
// - application/json
|
||||||
|
//
|
||||||
|
// Schemes: http, https
|
||||||
|
//
|
||||||
|
// Responses:
|
||||||
|
// 200: MessageHeadersResponse
|
||||||
|
// 400: ErrorResponse
|
||||||
|
// 404: NotFoundResponse
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
|
||||||
|
id := vars["id"]
|
||||||
|
|
||||||
|
if id == "latest" {
|
||||||
|
var err error
|
||||||
|
id, err = storage.LatestID(r)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(404)
|
||||||
|
fmt.Fprint(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := storage.GetMessageRaw(id)
|
||||||
|
if err != nil {
|
||||||
|
fourOFour(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reader := bytes.NewReader(data)
|
||||||
|
m, err := mail.ReadMessage(reader)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "application/json")
|
||||||
|
if err := json.NewEncoder(w).Encode(m.Header); err != nil {
|
||||||
|
httpError(w, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// swagger:parameters AttachmentParams
|
||||||
|
type attachmentParams struct {
|
||||||
|
// Message database ID or "latest"
|
||||||
|
//
|
||||||
|
// in: path
|
||||||
|
// required: true
|
||||||
|
// example: 4oRBnPtCXgAqZniRhzLNmS
|
||||||
|
ID string
|
||||||
|
|
||||||
|
// Attachment part ID
|
||||||
|
//
|
||||||
|
// in: path
|
||||||
|
// required: true
|
||||||
|
// example: 2
|
||||||
|
PartID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// DownloadAttachment (method: GET) returns the attachment data
|
||||||
|
func DownloadAttachment(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// swagger:route GET /api/v1/message/{ID}/part/{PartID} message AttachmentParams
|
||||||
|
//
|
||||||
|
// # Get message attachment
|
||||||
|
//
|
||||||
|
// This will return the attachment part using the appropriate Content-Type.
|
||||||
|
//
|
||||||
|
// The ID can be set to `latest` to reference the latest message.
|
||||||
|
//
|
||||||
|
// Produces:
|
||||||
|
// - application/*
|
||||||
|
// - image/*
|
||||||
|
// - text/*
|
||||||
|
//
|
||||||
|
// Schemes: http, https
|
||||||
|
//
|
||||||
|
// Responses:
|
||||||
|
// 200: BinaryResponse
|
||||||
|
// 400: ErrorResponse
|
||||||
|
// 404: NotFoundResponse
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
|
||||||
|
id := vars["id"]
|
||||||
|
partID := vars["partID"]
|
||||||
|
|
||||||
|
a, err := storage.GetAttachmentPart(id, partID)
|
||||||
|
if err != nil {
|
||||||
|
fourOFour(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fileName := a.FileName
|
||||||
|
if fileName == "" {
|
||||||
|
fileName = a.ContentID
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", a.ContentType)
|
||||||
|
w.Header().Set("Content-Disposition", "filename=\""+fileName+"\"")
|
||||||
|
_, _ = w.Write(a.Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// swagger:parameters DownloadRawParams
|
||||||
|
type downloadRawParams struct {
|
||||||
|
// Message database ID or "latest"
|
||||||
|
//
|
||||||
|
// in: path
|
||||||
|
// required: true
|
||||||
|
// example: 4oRBnPtCXgAqZniRhzLNmS
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// DownloadRaw (method: GET) returns the full email source as plain text
|
||||||
|
func DownloadRaw(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// swagger:route GET /api/v1/message/{ID}/raw message DownloadRawParams
|
||||||
|
//
|
||||||
|
// # Get message source
|
||||||
|
//
|
||||||
|
// Returns the full email source as plain text.
|
||||||
|
//
|
||||||
|
// The ID can be set to `latest` to return the latest message source.
|
||||||
|
//
|
||||||
|
// Produces:
|
||||||
|
// - text/plain
|
||||||
|
//
|
||||||
|
// Schemes: http, https
|
||||||
|
//
|
||||||
|
// Responses:
|
||||||
|
// 200: TextResponse
|
||||||
|
// 400: ErrorResponse
|
||||||
|
// 404: NotFoundResponse
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
|
||||||
|
id := vars["id"]
|
||||||
|
dl := r.FormValue("dl")
|
||||||
|
|
||||||
|
if id == "latest" {
|
||||||
|
var err error
|
||||||
|
id, err = storage.LatestID(r)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(404)
|
||||||
|
fmt.Fprint(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := storage.GetMessageRaw(id)
|
||||||
|
if err != nil {
|
||||||
|
fourOFour(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
if dl == "1" {
|
||||||
|
w.Header().Set("Content-Disposition", "attachment; filename=\""+id+".eml\"")
|
||||||
|
}
|
||||||
|
_, _ = w.Write(data)
|
||||||
|
}
|
388
server/apiv1/messages.go
Normal file
388
server/apiv1/messages.go
Normal file
@ -0,0 +1,388 @@
|
|||||||
|
package apiv1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/axllent/mailpit/internal/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
// swagger:parameters GetMessagesParams
|
||||||
|
type getMessagesParams struct {
|
||||||
|
// Pagination offset
|
||||||
|
//
|
||||||
|
// in: query
|
||||||
|
// name: start
|
||||||
|
// required: false
|
||||||
|
// default: 0
|
||||||
|
// type: integer
|
||||||
|
// example: 100
|
||||||
|
Start int `json:"start"`
|
||||||
|
|
||||||
|
// Limit number of results
|
||||||
|
//
|
||||||
|
// in: query
|
||||||
|
// name: limit
|
||||||
|
// required: false
|
||||||
|
// default: 50
|
||||||
|
// type: integer
|
||||||
|
// example: 50
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summary of messages
|
||||||
|
// swagger:response MessagesSummaryResponse
|
||||||
|
type messagesSummaryResponse struct {
|
||||||
|
// The messages summary
|
||||||
|
// in: body
|
||||||
|
Body MessagesSummary
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessagesSummary is a summary of a list of messages
|
||||||
|
type MessagesSummary struct {
|
||||||
|
// Total number of messages in mailbox
|
||||||
|
Total float64 `json:"total"`
|
||||||
|
|
||||||
|
// Total number of unread messages in mailbox
|
||||||
|
Unread float64 `json:"unread"`
|
||||||
|
|
||||||
|
// Legacy - now undocumented in API specs but left for backwards compatibility.
|
||||||
|
// Removed from API documentation 2023-07-12
|
||||||
|
// swagger:ignore
|
||||||
|
Count float64 `json:"count"`
|
||||||
|
|
||||||
|
// Total number of messages matching current query
|
||||||
|
MessagesCount float64 `json:"messages_count"`
|
||||||
|
|
||||||
|
// Pagination offset
|
||||||
|
Start int `json:"start"`
|
||||||
|
|
||||||
|
// All current tags
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
|
||||||
|
// Messages summary
|
||||||
|
// in: body
|
||||||
|
Messages []storage.MessageSummary `json:"messages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMessages returns a paginated list of messages as JSON
|
||||||
|
func GetMessages(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// swagger:route GET /api/v1/messages messages GetMessagesParams
|
||||||
|
//
|
||||||
|
// # List messages
|
||||||
|
//
|
||||||
|
// Returns messages from the mailbox ordered from newest to oldest.
|
||||||
|
//
|
||||||
|
// Produces:
|
||||||
|
// - application/json
|
||||||
|
//
|
||||||
|
// Schemes: http, https
|
||||||
|
//
|
||||||
|
// Responses:
|
||||||
|
// 200: MessagesSummaryResponse
|
||||||
|
// 400: ErrorResponse
|
||||||
|
|
||||||
|
start, beforeTS, limit := getStartLimit(r)
|
||||||
|
|
||||||
|
messages, err := storage.List(start, beforeTS, limit)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stats := storage.StatsGet()
|
||||||
|
|
||||||
|
var res MessagesSummary
|
||||||
|
|
||||||
|
res.Start = start
|
||||||
|
res.Messages = messages
|
||||||
|
res.Count = float64(len(messages)) // legacy - now undocumented in API specs
|
||||||
|
res.Total = stats.Total
|
||||||
|
res.Unread = stats.Unread
|
||||||
|
res.Tags = stats.Tags
|
||||||
|
res.MessagesCount = stats.Total
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "application/json")
|
||||||
|
if err := json.NewEncoder(w).Encode(res); err != nil {
|
||||||
|
httpError(w, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// swagger:parameters SetReadStatusParams
|
||||||
|
type setReadStatusParams struct {
|
||||||
|
// in: body
|
||||||
|
Body struct {
|
||||||
|
// Read status
|
||||||
|
//
|
||||||
|
// required: false
|
||||||
|
// default: false
|
||||||
|
// example: true
|
||||||
|
Read bool
|
||||||
|
|
||||||
|
// Array of message database IDs
|
||||||
|
//
|
||||||
|
// required: false
|
||||||
|
// example: ["4oRBnPtCXgAqZniRhzLNmS", "hXayS6wnCgNnt6aFTvmOF6"]
|
||||||
|
IDs []string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetReadStatus (method: PUT) will update the status to Read/Unread for all provided IDs
|
||||||
|
// If no IDs are provided then all messages are updated.
|
||||||
|
func SetReadStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// swagger:route PUT /api/v1/messages messages SetReadStatusParams
|
||||||
|
//
|
||||||
|
// # Set read status
|
||||||
|
//
|
||||||
|
// If no IDs are provided then all messages are updated.
|
||||||
|
//
|
||||||
|
// Consumes:
|
||||||
|
// - application/json
|
||||||
|
//
|
||||||
|
// Produces:
|
||||||
|
// - text/plain
|
||||||
|
//
|
||||||
|
// Schemes: http, https
|
||||||
|
//
|
||||||
|
// Responses:
|
||||||
|
// 200: OKResponse
|
||||||
|
// 400: ErrorResponse
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(r.Body)
|
||||||
|
|
||||||
|
var data struct {
|
||||||
|
Read bool
|
||||||
|
IDs []string
|
||||||
|
}
|
||||||
|
|
||||||
|
err := decoder.Decode(&data)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := data.IDs
|
||||||
|
|
||||||
|
if len(ids) == 0 {
|
||||||
|
if data.Read {
|
||||||
|
err := storage.MarkAllRead()
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err := storage.MarkAllUnread()
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if data.Read {
|
||||||
|
for _, id := range ids {
|
||||||
|
if err := storage.MarkRead(id); err != nil {
|
||||||
|
httpError(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, id := range ids {
|
||||||
|
if err := storage.MarkUnread(id); err != nil {
|
||||||
|
httpError(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "text/plain")
|
||||||
|
_, _ = w.Write([]byte("ok"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// swagger:parameters DeleteMessagesParams
|
||||||
|
type deleteMessagesParams struct {
|
||||||
|
// Delete request
|
||||||
|
// in: body
|
||||||
|
Body struct {
|
||||||
|
// Array of message database IDs
|
||||||
|
//
|
||||||
|
// required: false
|
||||||
|
// example: ["4oRBnPtCXgAqZniRhzLNmS", "hXayS6wnCgNnt6aFTvmOF6"]
|
||||||
|
IDs []string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteMessages (method: DELETE) deletes all messages matching IDS.
|
||||||
|
func DeleteMessages(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// swagger:route DELETE /api/v1/messages messages DeleteMessagesParams
|
||||||
|
//
|
||||||
|
// # Delete messages
|
||||||
|
//
|
||||||
|
// Delete individual or all messages. If no IDs are provided then all messages are deleted.
|
||||||
|
//
|
||||||
|
// Consumes:
|
||||||
|
// - application/json
|
||||||
|
//
|
||||||
|
// Produces:
|
||||||
|
// - text/plain
|
||||||
|
//
|
||||||
|
// Schemes: http, https
|
||||||
|
//
|
||||||
|
// Responses:
|
||||||
|
// 200: OKResponse
|
||||||
|
// 400: ErrorResponse
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(r.Body)
|
||||||
|
var data struct {
|
||||||
|
IDs []string
|
||||||
|
}
|
||||||
|
err := decoder.Decode(&data)
|
||||||
|
if err != nil || len(data.IDs) == 0 {
|
||||||
|
if err := storage.DeleteAllMessages(); err != nil {
|
||||||
|
httpError(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := storage.DeleteMessages(data.IDs); err != nil {
|
||||||
|
httpError(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "text/plain")
|
||||||
|
_, _ = w.Write([]byte("ok"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// swagger:parameters SearchParams
|
||||||
|
type searchParams struct {
|
||||||
|
// Search query
|
||||||
|
//
|
||||||
|
// in: query
|
||||||
|
// required: true
|
||||||
|
// type: string
|
||||||
|
// example: search words
|
||||||
|
Query string `json:"query"`
|
||||||
|
|
||||||
|
// Pagination offset
|
||||||
|
//
|
||||||
|
// in: query
|
||||||
|
// required: false
|
||||||
|
// type integer
|
||||||
|
// example: 100
|
||||||
|
Start string `json:"start"`
|
||||||
|
|
||||||
|
// Limit results
|
||||||
|
//
|
||||||
|
// in: query
|
||||||
|
// required: false
|
||||||
|
// type integer
|
||||||
|
// example: 50
|
||||||
|
Limit string `json:"limit"`
|
||||||
|
|
||||||
|
// [Timezone identifier](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) used only for `before:` & `after:` searches (eg: "Pacific/Auckland").
|
||||||
|
//
|
||||||
|
// in: query
|
||||||
|
// required: false
|
||||||
|
// type string
|
||||||
|
TZ string `json:"tz"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search returns the latest messages as JSON
|
||||||
|
func Search(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// swagger:route GET /api/v1/search messages SearchParams
|
||||||
|
//
|
||||||
|
// # Search messages
|
||||||
|
//
|
||||||
|
// Returns messages matching [a search](https://mailpit.axllent.org/docs/usage/search-filters/), sorted by received date (descending).
|
||||||
|
//
|
||||||
|
// Produces:
|
||||||
|
// - application/json
|
||||||
|
//
|
||||||
|
// Schemes: http, https
|
||||||
|
//
|
||||||
|
// Responses:
|
||||||
|
// 200: MessagesSummaryResponse
|
||||||
|
// 400: ErrorResponse
|
||||||
|
|
||||||
|
search := strings.TrimSpace(r.URL.Query().Get("query"))
|
||||||
|
if search == "" {
|
||||||
|
httpError(w, "Error: no search query")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
start, beforeTS, limit := getStartLimit(r)
|
||||||
|
|
||||||
|
messages, results, err := storage.Search(search, r.URL.Query().Get("tz"), start, beforeTS, limit)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stats := storage.StatsGet()
|
||||||
|
|
||||||
|
var res MessagesSummary
|
||||||
|
|
||||||
|
res.Start = start
|
||||||
|
res.Messages = messages
|
||||||
|
res.Count = float64(len(messages)) // legacy - now undocumented in API specs
|
||||||
|
res.Total = stats.Total // total messages in mailbox
|
||||||
|
res.MessagesCount = float64(results)
|
||||||
|
res.Unread = stats.Unread
|
||||||
|
res.Tags = stats.Tags
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "application/json")
|
||||||
|
if err := json.NewEncoder(w).Encode(res); err != nil {
|
||||||
|
httpError(w, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// swagger:parameters DeleteSearchParams
|
||||||
|
type deleteSearchParams struct {
|
||||||
|
// Search query
|
||||||
|
//
|
||||||
|
// in: query
|
||||||
|
// required: true
|
||||||
|
// type: string
|
||||||
|
// example: search words
|
||||||
|
Query string `json:"query"`
|
||||||
|
|
||||||
|
// [Timezone identifier](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) used only for `before:` & `after:` searches (eg: "Pacific/Auckland").
|
||||||
|
//
|
||||||
|
// in: query
|
||||||
|
// required: false
|
||||||
|
// type string
|
||||||
|
TZ string `json:"tz"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSearch will delete all messages matching a search
|
||||||
|
func DeleteSearch(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// swagger:route DELETE /api/v1/search messages DeleteSearchParams
|
||||||
|
//
|
||||||
|
// # Delete messages by search
|
||||||
|
//
|
||||||
|
// Delete all messages matching [a search](https://mailpit.axllent.org/docs/usage/search-filters/).
|
||||||
|
//
|
||||||
|
// Produces:
|
||||||
|
// - application/json
|
||||||
|
//
|
||||||
|
// Schemes: http, https
|
||||||
|
//
|
||||||
|
// Responses:
|
||||||
|
// 200: OKResponse
|
||||||
|
// 400: ErrorResponse
|
||||||
|
|
||||||
|
search := strings.TrimSpace(r.URL.Query().Get("query"))
|
||||||
|
if search == "" {
|
||||||
|
httpError(w, "Error: no search query")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := storage.DeleteSearch(search, r.URL.Query().Get("tz")); err != nil {
|
||||||
|
httpError(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "text/plain")
|
||||||
|
_, _ = w.Write([]byte("ok"))
|
||||||
|
}
|
238
server/apiv1/other.go
Normal file
238
server/apiv1/other.go
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
package apiv1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/axllent/mailpit/config"
|
||||||
|
"github.com/axllent/mailpit/internal/htmlcheck"
|
||||||
|
"github.com/axllent/mailpit/internal/linkcheck"
|
||||||
|
"github.com/axllent/mailpit/internal/spamassassin"
|
||||||
|
"github.com/axllent/mailpit/internal/storage"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/jhillyerd/enmime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// swagger:parameters HTMLCheckParams
|
||||||
|
type htmlCheckParams struct {
|
||||||
|
// Message database ID or "latest"
|
||||||
|
//
|
||||||
|
// in: path
|
||||||
|
// description: Message database ID or "latest"
|
||||||
|
// required: true
|
||||||
|
// example: 4oRBnPtCXgAqZniRhzLNmS
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTMLCheckResponse summary response
|
||||||
|
type HTMLCheckResponse = htmlcheck.Response
|
||||||
|
|
||||||
|
// HTMLCheck returns a summary of the HTML client support
|
||||||
|
func HTMLCheck(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// swagger:route GET /api/v1/message/{ID}/html-check Other HTMLCheckParams
|
||||||
|
//
|
||||||
|
// # HTML check
|
||||||
|
//
|
||||||
|
// Returns the summary of the message HTML checker.
|
||||||
|
//
|
||||||
|
// The ID can be set to `latest` to return the latest message.
|
||||||
|
//
|
||||||
|
// Produces:
|
||||||
|
// - application/json
|
||||||
|
//
|
||||||
|
// Schemes: http, https
|
||||||
|
//
|
||||||
|
// Responses:
|
||||||
|
// 200: HTMLCheckResponse
|
||||||
|
// 400: ErrorResponse
|
||||||
|
// 404: NotFoundResponse
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
id := vars["id"]
|
||||||
|
|
||||||
|
if id == "latest" {
|
||||||
|
var err error
|
||||||
|
id, err = storage.LatestID(r)
|
||||||
|
if err != nil {
|
||||||
|
fourOFour(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := storage.GetMessageRaw(id)
|
||||||
|
if err != nil {
|
||||||
|
fourOFour(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e := bytes.NewReader(raw)
|
||||||
|
|
||||||
|
parser := enmime.NewParser(enmime.DisableCharacterDetection(true))
|
||||||
|
|
||||||
|
msg, err := parser.ReadEnvelope(e)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.HTML == "" {
|
||||||
|
httpError(w, "message does not contain HTML")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
checks, err := htmlcheck.RunTests(msg.HTML)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "application/json")
|
||||||
|
if err := json.NewEncoder(w).Encode(checks); err != nil {
|
||||||
|
httpError(w, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// swagger:parameters LinkCheckParams
|
||||||
|
type linkCheckParams struct {
|
||||||
|
// Message database ID or "latest"
|
||||||
|
//
|
||||||
|
// in: path
|
||||||
|
// required: true
|
||||||
|
// example: 4oRBnPtCXgAqZniRhzLNmS
|
||||||
|
ID string
|
||||||
|
|
||||||
|
// Follow redirects
|
||||||
|
//
|
||||||
|
// in: query
|
||||||
|
// required: false
|
||||||
|
// default: false
|
||||||
|
// example: false
|
||||||
|
Follow string `json:"follow"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LinkCheckResponse summary response
|
||||||
|
type LinkCheckResponse = linkcheck.Response
|
||||||
|
|
||||||
|
// LinkCheck returns a summary of links in the email
|
||||||
|
func LinkCheck(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// swagger:route GET /api/v1/message/{ID}/link-check Other LinkCheckParams
|
||||||
|
//
|
||||||
|
// # Link check
|
||||||
|
//
|
||||||
|
// Returns the summary of the message Link checker.
|
||||||
|
//
|
||||||
|
// The ID can be set to `latest` to return the latest message.
|
||||||
|
//
|
||||||
|
// Produces:
|
||||||
|
// - application/json
|
||||||
|
//
|
||||||
|
// Schemes: http, https
|
||||||
|
//
|
||||||
|
// Responses:
|
||||||
|
// 200: LinkCheckResponse
|
||||||
|
// 400: ErrorResponse
|
||||||
|
// 404: NotFoundResponse
|
||||||
|
|
||||||
|
if config.DemoMode {
|
||||||
|
httpError(w, "this functionality has been disabled for demonstration purposes")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
id := vars["id"]
|
||||||
|
|
||||||
|
if id == "latest" {
|
||||||
|
var err error
|
||||||
|
id, err = storage.LatestID(r)
|
||||||
|
if err != nil {
|
||||||
|
fourOFour(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := storage.GetMessage(id)
|
||||||
|
if err != nil {
|
||||||
|
fourOFour(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
f := r.URL.Query().Get("follow")
|
||||||
|
followRedirects := f == "true" || f == "1"
|
||||||
|
|
||||||
|
summary, err := linkcheck.RunTests(msg, followRedirects)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "application/json")
|
||||||
|
if err := json.NewEncoder(w).Encode(summary); err != nil {
|
||||||
|
httpError(w, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// swagger:parameters SpamAssassinCheckParams
|
||||||
|
type spamAssassinCheckParams struct {
|
||||||
|
// Message database ID or "latest"
|
||||||
|
//
|
||||||
|
// in: path
|
||||||
|
// required: true
|
||||||
|
// example: 4oRBnPtCXgAqZniRhzLNmS
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SpamAssassinResponse summary response
|
||||||
|
type SpamAssassinResponse = spamassassin.Result
|
||||||
|
|
||||||
|
// SpamAssassinCheck returns a summary of SpamAssassin results (if enabled)
|
||||||
|
func SpamAssassinCheck(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// swagger:route GET /api/v1/message/{ID}/sa-check Other SpamAssassinCheckParams
|
||||||
|
//
|
||||||
|
// # SpamAssassin check
|
||||||
|
//
|
||||||
|
// Returns the SpamAssassin summary (if enabled) of the message.
|
||||||
|
//
|
||||||
|
// The ID can be set to `latest` to return the latest message.
|
||||||
|
//
|
||||||
|
// Produces:
|
||||||
|
// - application/json
|
||||||
|
//
|
||||||
|
// Schemes: http, https
|
||||||
|
//
|
||||||
|
// Responses:
|
||||||
|
// 200: SpamAssassinResponse
|
||||||
|
// 400: ErrorResponse
|
||||||
|
// 404: NotFoundResponse
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
id := vars["id"]
|
||||||
|
|
||||||
|
if id == "latest" {
|
||||||
|
var err error
|
||||||
|
id, err = storage.LatestID(r)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(404)
|
||||||
|
fmt.Fprint(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := storage.GetMessageRaw(id)
|
||||||
|
if err != nil {
|
||||||
|
fourOFour(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
summary, err := spamassassin.Check(msg)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "application/json")
|
||||||
|
if err := json.NewEncoder(w).Encode(summary); err != nil {
|
||||||
|
httpError(w, err.Error())
|
||||||
|
}
|
||||||
|
}
|
@ -17,25 +17,48 @@ import (
|
|||||||
"github.com/lithammer/shortuuid/v4"
|
"github.com/lithammer/shortuuid/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// swagger:parameters ReleaseMessageParams
|
||||||
|
type releaseMessageParams struct {
|
||||||
|
// Message database ID
|
||||||
|
//
|
||||||
|
// in: path
|
||||||
|
// description: Message database ID
|
||||||
|
// required: true
|
||||||
|
// example: 4oRBnPtCXgAqZniRhzLNmS
|
||||||
|
ID string
|
||||||
|
|
||||||
|
// in: body
|
||||||
|
Body struct {
|
||||||
|
// Array of email addresses to relay the message to
|
||||||
|
//
|
||||||
|
// required: true
|
||||||
|
// example: ["user1@example.com", "user2@example.com"]
|
||||||
|
To []string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ReleaseMessage (method: POST) will release a message via a pre-configured external SMTP server.
|
// ReleaseMessage (method: POST) will release a message via a pre-configured external SMTP server.
|
||||||
func ReleaseMessage(w http.ResponseWriter, r *http.Request) {
|
func ReleaseMessage(w http.ResponseWriter, r *http.Request) {
|
||||||
// swagger:route POST /api/v1/message/{ID}/release message ReleaseMessage
|
// swagger:route POST /api/v1/message/{ID}/release message ReleaseMessageParams
|
||||||
//
|
//
|
||||||
// # Release message
|
// # Release message
|
||||||
//
|
//
|
||||||
// Release a message via a pre-configured external SMTP server. This is only enabled if message relaying has been configured.
|
// Release a message via a pre-configured external SMTP server. This is only enabled if message relaying has been configured.
|
||||||
//
|
//
|
||||||
|
// The ID can be set to `latest` to reference the latest message.
|
||||||
|
//
|
||||||
// Consumes:
|
// Consumes:
|
||||||
// - application/json
|
// - application/json
|
||||||
//
|
//
|
||||||
// Produces:
|
// Produces:
|
||||||
// - text/plain
|
// - text/plain
|
||||||
//
|
//
|
||||||
// Schemes: http, https
|
// Schemes: http, https
|
||||||
//
|
//
|
||||||
// Responses:
|
// Responses:
|
||||||
// 200: OKResponse
|
// 200: OKResponse
|
||||||
// default: ErrorResponse
|
// 400: ErrorResponse
|
||||||
|
// 404: NotFoundResponse
|
||||||
|
|
||||||
if config.DemoMode {
|
if config.DemoMode {
|
||||||
httpError(w, "this functionality has been disabled for demonstration purposes")
|
httpError(w, "this functionality has been disabled for demonstration purposes")
|
||||||
@ -54,7 +77,9 @@ func ReleaseMessage(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
decoder := json.NewDecoder(r.Body)
|
decoder := json.NewDecoder(r.Body)
|
||||||
|
|
||||||
data := releaseMessageRequestBody{}
|
var data struct {
|
||||||
|
To []string
|
||||||
|
}
|
||||||
|
|
||||||
if err := decoder.Decode(&data); err != nil {
|
if err := decoder.Decode(&data); err != nil {
|
||||||
httpError(w, err.Error())
|
httpError(w, err.Error())
|
||||||
|
@ -17,7 +17,7 @@ import (
|
|||||||
"github.com/jhillyerd/enmime"
|
"github.com/jhillyerd/enmime"
|
||||||
)
|
)
|
||||||
|
|
||||||
// swagger:parameters SendMessage
|
// swagger:parameters SendMessageParams
|
||||||
type sendMessageParams struct {
|
type sendMessageParams struct {
|
||||||
// in: body
|
// in: body
|
||||||
Body *SendRequest
|
Body *SendRequest
|
||||||
@ -108,13 +108,6 @@ type SendRequest struct {
|
|||||||
Headers map[string]string
|
Headers map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendMessageConfirmation struct
|
|
||||||
type SendMessageConfirmation struct {
|
|
||||||
// Database ID
|
|
||||||
// example: iAfZVVe2UQFNSG5BAjgYwa
|
|
||||||
ID string
|
|
||||||
}
|
|
||||||
|
|
||||||
// JSONErrorMessage struct
|
// JSONErrorMessage struct
|
||||||
type JSONErrorMessage struct {
|
type JSONErrorMessage struct {
|
||||||
// Error message
|
// Error message
|
||||||
@ -122,25 +115,41 @@ type JSONErrorMessage struct {
|
|||||||
Error string
|
Error 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendMessageConfirmation struct
|
||||||
|
type SendMessageConfirmation struct {
|
||||||
|
// Database ID
|
||||||
|
// example: iAfZVVe2UQfNSG5BAjgYwa
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
|
||||||
// SendMessageHandler handles HTTP requests to send a new message
|
// SendMessageHandler handles HTTP requests to send a new message
|
||||||
func SendMessageHandler(w http.ResponseWriter, r *http.Request) {
|
func SendMessageHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
// swagger:route POST /api/v1/send message SendMessage
|
// swagger:route POST /api/v1/send message SendMessageParams
|
||||||
//
|
//
|
||||||
// # Send a message
|
// # Send a message
|
||||||
//
|
//
|
||||||
// Send a message via the HTTP API.
|
// Send a message via the HTTP API.
|
||||||
//
|
//
|
||||||
// Consumes:
|
// Consumes:
|
||||||
// - application/json
|
// - application/json
|
||||||
//
|
//
|
||||||
// Produces:
|
// Produces:
|
||||||
// - application/json
|
// - application/json
|
||||||
//
|
//
|
||||||
// Schemes: http, https
|
// Schemes: http, https
|
||||||
//
|
//
|
||||||
// Responses:
|
// Responses:
|
||||||
// 200: sendMessageResponse
|
// 200: sendMessageResponse
|
||||||
// default: jsonErrorResponse
|
// 400: jsonErrorResponse
|
||||||
|
|
||||||
if config.DemoMode {
|
if config.DemoMode {
|
||||||
httpJSONError(w, "this functionality has been disabled for demonstration purposes")
|
httpJSONError(w, "this functionality has been disabled for demonstration purposes")
|
||||||
|
@ -1,39 +1,9 @@
|
|||||||
package apiv1
|
package apiv1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/axllent/mailpit/internal/htmlcheck"
|
|
||||||
"github.com/axllent/mailpit/internal/linkcheck"
|
|
||||||
"github.com/axllent/mailpit/internal/spamassassin"
|
|
||||||
"github.com/axllent/mailpit/internal/storage"
|
"github.com/axllent/mailpit/internal/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MessagesSummary is a summary of a list of messages
|
|
||||||
type MessagesSummary struct {
|
|
||||||
// Total number of messages in mailbox
|
|
||||||
Total float64 `json:"total"`
|
|
||||||
|
|
||||||
// Total number of unread messages in mailbox
|
|
||||||
Unread float64 `json:"unread"`
|
|
||||||
|
|
||||||
// Legacy - now undocumented in API specs but left for backwards compatibility.
|
|
||||||
// Removed from API documentation 2023-07-12
|
|
||||||
// swagger:ignore
|
|
||||||
Count float64 `json:"count"`
|
|
||||||
|
|
||||||
// Total number of messages matching current query
|
|
||||||
MessagesCount float64 `json:"messages_count"`
|
|
||||||
|
|
||||||
// Pagination offset
|
|
||||||
Start int `json:"start"`
|
|
||||||
|
|
||||||
// All current tags
|
|
||||||
Tags []string `json:"tags"`
|
|
||||||
|
|
||||||
// Messages summary
|
|
||||||
// in: body
|
|
||||||
Messages []storage.MessageSummary `json:"messages"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// The following structs & aliases are provided for easy import
|
// The following structs & aliases are provided for easy import
|
||||||
// and understanding of the JSON structure.
|
// and understanding of the JSON structure.
|
||||||
|
|
||||||
@ -45,12 +15,3 @@ type Message = storage.Message
|
|||||||
|
|
||||||
// Attachment summary
|
// Attachment summary
|
||||||
type Attachment = storage.Attachment
|
type Attachment = storage.Attachment
|
||||||
|
|
||||||
// HTMLCheckResponse summary
|
|
||||||
type HTMLCheckResponse = htmlcheck.Response
|
|
||||||
|
|
||||||
// LinkCheckResponse summary
|
|
||||||
type LinkCheckResponse = linkcheck.Response
|
|
||||||
|
|
||||||
// SpamAssassinResponse summary
|
|
||||||
type SpamAssassinResponse = spamassassin.Result
|
|
||||||
|
@ -1,177 +1,8 @@
|
|||||||
package apiv1
|
package apiv1
|
||||||
|
|
||||||
import "github.com/axllent/mailpit/internal/stats"
|
|
||||||
|
|
||||||
// These structs are for the purpose of defining swagger HTTP parameters & responses
|
// These structs are for the purpose of defining swagger HTTP parameters & responses
|
||||||
|
|
||||||
// Application information
|
// Binary data response which inherits the attachment's content type.
|
||||||
// swagger:response InfoResponse
|
|
||||||
type infoResponse struct {
|
|
||||||
// Application information
|
|
||||||
//
|
|
||||||
// in: body
|
|
||||||
Body stats.AppInformation
|
|
||||||
}
|
|
||||||
|
|
||||||
// Web UI configuration
|
|
||||||
// swagger:response WebUIConfigurationResponse
|
|
||||||
type webUIConfigurationResponse struct {
|
|
||||||
// Web UI configuration settings
|
|
||||||
//
|
|
||||||
// in: body
|
|
||||||
Body webUIConfiguration
|
|
||||||
}
|
|
||||||
|
|
||||||
// Message summary
|
|
||||||
// swagger:response MessagesSummaryResponse
|
|
||||||
type messagesSummaryResponse struct {
|
|
||||||
// The message summary
|
|
||||||
// in: body
|
|
||||||
Body MessagesSummary
|
|
||||||
}
|
|
||||||
|
|
||||||
// Message headers
|
|
||||||
// swagger:model MessageHeaders
|
|
||||||
type messageHeaders map[string][]string
|
|
||||||
|
|
||||||
// swagger:parameters DeleteMessages
|
|
||||||
type deleteMessagesParams struct {
|
|
||||||
// in: body
|
|
||||||
Body *deleteMessagesRequestBody
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete request
|
|
||||||
// swagger:model DeleteRequest
|
|
||||||
type deleteMessagesRequestBody struct {
|
|
||||||
// Array of message database IDs
|
|
||||||
//
|
|
||||||
// required: false
|
|
||||||
// example: ["5dec4247-812e-4b77-9101-e25ad406e9ea", "8ac66bbc-2d9a-4c41-ad99-00aa75fa674e"]
|
|
||||||
IDs []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// swagger:parameters SetReadStatus
|
|
||||||
type setReadStatusParams struct {
|
|
||||||
// in: body
|
|
||||||
Body *setReadStatusRequestBody
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set read status request
|
|
||||||
// swagger:model setReadStatusRequestBody
|
|
||||||
type setReadStatusRequestBody struct {
|
|
||||||
// Read status
|
|
||||||
//
|
|
||||||
// required: false
|
|
||||||
// default: false
|
|
||||||
// example: true
|
|
||||||
Read bool
|
|
||||||
|
|
||||||
// Array of message database IDs
|
|
||||||
//
|
|
||||||
// required: false
|
|
||||||
// example: ["5dec4247-812e-4b77-9101-e25ad406e9ea", "8ac66bbc-2d9a-4c41-ad99-00aa75fa674e"]
|
|
||||||
IDs []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// swagger:parameters SetTags
|
|
||||||
type setTagsParams struct {
|
|
||||||
// in: body
|
|
||||||
Body *setTagsRequestBody
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set tags request
|
|
||||||
// swagger:model setTagsRequestBody
|
|
||||||
type setTagsRequestBody struct {
|
|
||||||
// Array of tag names to set
|
|
||||||
//
|
|
||||||
// required: true
|
|
||||||
// example: ["Tag 1", "Tag 2"]
|
|
||||||
Tags []string
|
|
||||||
|
|
||||||
// Array of message database IDs
|
|
||||||
//
|
|
||||||
// required: true
|
|
||||||
// example: ["5dec4247-812e-4b77-9101-e25ad406e9ea", "8ac66bbc-2d9a-4c41-ad99-00aa75fa674e"]
|
|
||||||
IDs []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// swagger:parameters RenameTag
|
|
||||||
type renameTagParams struct {
|
|
||||||
// in: body
|
|
||||||
Body *renameTagRequestBody
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rename tag request
|
|
||||||
// swagger:model renameTagRequestBody
|
|
||||||
type renameTagRequestBody struct {
|
|
||||||
// New name
|
|
||||||
//
|
|
||||||
// required: true
|
|
||||||
// example: New name
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
// swagger:parameters ReleaseMessage
|
|
||||||
type releaseMessageParams struct {
|
|
||||||
// Message database ID
|
|
||||||
//
|
|
||||||
// in: path
|
|
||||||
// description: Message database ID
|
|
||||||
// required: true
|
|
||||||
ID string
|
|
||||||
|
|
||||||
// in: body
|
|
||||||
Body *releaseMessageRequestBody
|
|
||||||
}
|
|
||||||
|
|
||||||
// Release request
|
|
||||||
// swagger:model releaseMessageRequestBody
|
|
||||||
type releaseMessageRequestBody struct {
|
|
||||||
// Array of email addresses to relay the message to
|
|
||||||
// required: true
|
|
||||||
// example: ["user1@example.com", "user2@example.com"]
|
|
||||||
To []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// swagger:parameters HTMLCheck
|
|
||||||
type htmlCheckParams struct {
|
|
||||||
// Message database ID or "latest"
|
|
||||||
//
|
|
||||||
// in: path
|
|
||||||
// description: Message database ID or "latest"
|
|
||||||
// required: true
|
|
||||||
ID string
|
|
||||||
}
|
|
||||||
|
|
||||||
// swagger:parameters LinkCheck
|
|
||||||
type linkCheckParams struct {
|
|
||||||
// Message database ID or "latest"
|
|
||||||
//
|
|
||||||
// in: path
|
|
||||||
// description: Message database ID or "latest"
|
|
||||||
// required: true
|
|
||||||
ID string
|
|
||||||
|
|
||||||
// Follow redirects
|
|
||||||
//
|
|
||||||
// in: query
|
|
||||||
// description: Follow redirects
|
|
||||||
// required: false
|
|
||||||
// default: false
|
|
||||||
Follow string `json:"follow"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// swagger:parameters SpamAssassinCheck
|
|
||||||
type spamAssassinCheckParams struct {
|
|
||||||
// Message database ID or "latest"
|
|
||||||
//
|
|
||||||
// in: path
|
|
||||||
// description: Message database ID or "latest"
|
|
||||||
// required: true
|
|
||||||
ID string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Binary data response inherits the attachment's content type.
|
|
||||||
// swagger:response BinaryResponse
|
// swagger:response BinaryResponse
|
||||||
type binaryResponse string
|
type binaryResponse string
|
||||||
|
|
||||||
@ -183,11 +14,15 @@ type textResponse string
|
|||||||
// swagger:response HTMLResponse
|
// swagger:response HTMLResponse
|
||||||
type htmlResponse string
|
type htmlResponse string
|
||||||
|
|
||||||
// HTTP error response will return with a >= 400 response code
|
// Server error will return with a 400 status code
|
||||||
|
// with the error message in the body
|
||||||
// swagger:response ErrorResponse
|
// swagger:response ErrorResponse
|
||||||
// example: invalid request
|
|
||||||
type errorResponse string
|
type errorResponse string
|
||||||
|
|
||||||
|
// Not found error will return a 404 status code
|
||||||
|
// swagger:response NotFoundResponse
|
||||||
|
type notFoundResponse string
|
||||||
|
|
||||||
// Plain text "ok" response
|
// Plain text "ok" response
|
||||||
// swagger:response OKResponse
|
// swagger:response OKResponse
|
||||||
type okResponse string
|
type okResponse string
|
||||||
@ -196,15 +31,6 @@ type okResponse string
|
|||||||
// 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
|
// JSON error response
|
||||||
// swagger:response jsonErrorResponse
|
// swagger:response jsonErrorResponse
|
||||||
type jsonErrorResponse struct {
|
type jsonErrorResponse struct {
|
||||||
|
@ -18,13 +18,13 @@ func GetAllTags(w http.ResponseWriter, _ *http.Request) {
|
|||||||
// Returns a JSON array of all unique message tags.
|
// Returns a JSON array of all unique message tags.
|
||||||
//
|
//
|
||||||
// Produces:
|
// Produces:
|
||||||
// - application/json
|
// - application/json
|
||||||
//
|
//
|
||||||
// Schemes: http, https
|
// Schemes: http, https
|
||||||
//
|
//
|
||||||
// Responses:
|
// Responses:
|
||||||
// 200: ArrayResponse
|
// 200: ArrayResponse
|
||||||
// default: ErrorResponse
|
// 400: ErrorResponse
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "application/json")
|
w.Header().Add("Content-Type", "application/json")
|
||||||
if err := json.NewEncoder(w).Encode(storage.GetAllTags()); err != nil {
|
if err := json.NewEncoder(w).Encode(storage.GetAllTags()); err != nil {
|
||||||
@ -32,25 +32,43 @@ func GetAllTags(w http.ResponseWriter, _ *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// swagger:parameters SetTagsParams
|
||||||
|
type setTagsParams struct {
|
||||||
|
// in: body
|
||||||
|
Body struct {
|
||||||
|
// Array of tag names to set
|
||||||
|
//
|
||||||
|
// required: true
|
||||||
|
// example: ["Tag 1", "Tag 2"]
|
||||||
|
Tags []string
|
||||||
|
|
||||||
|
// Array of message database IDs
|
||||||
|
//
|
||||||
|
// required: true
|
||||||
|
// example: ["4oRBnPtCXgAqZniRhzLNmS", "hXayS6wnCgNnt6aFTvmOF6"]
|
||||||
|
IDs []string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SetMessageTags (method: PUT) will set the tags for all provided IDs
|
// SetMessageTags (method: PUT) will set the tags for all provided IDs
|
||||||
func SetMessageTags(w http.ResponseWriter, r *http.Request) {
|
func SetMessageTags(w http.ResponseWriter, r *http.Request) {
|
||||||
// swagger:route PUT /api/v1/tags tags SetTags
|
// swagger:route PUT /api/v1/tags tags SetTagsParams
|
||||||
//
|
//
|
||||||
// # Set message tags
|
// # Set message tags
|
||||||
//
|
//
|
||||||
// This will overwrite any existing tags for selected message database IDs. To remove all tags from a message, pass an empty tags array.
|
// This will overwrite any existing tags for selected message database IDs. To remove all tags from a message, pass an empty tags array.
|
||||||
//
|
//
|
||||||
// Consumes:
|
// Consumes:
|
||||||
// - application/json
|
// - application/json
|
||||||
//
|
//
|
||||||
// Produces:
|
// Produces:
|
||||||
// - text/plain
|
// - text/plain
|
||||||
//
|
//
|
||||||
// Schemes: http, https
|
// Schemes: http, https
|
||||||
//
|
//
|
||||||
// Responses:
|
// Responses:
|
||||||
// 200: OKResponse
|
// 200: OKResponse
|
||||||
// default: ErrorResponse
|
// 400: ErrorResponse
|
||||||
|
|
||||||
decoder := json.NewDecoder(r.Body)
|
decoder := json.NewDecoder(r.Body)
|
||||||
|
|
||||||
@ -80,29 +98,42 @@ func SetMessageTags(w http.ResponseWriter, r *http.Request) {
|
|||||||
_, _ = w.Write([]byte("ok"))
|
_, _ = w.Write([]byte("ok"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// swagger:parameters RenameTagParams
|
||||||
|
type renameTagParams struct {
|
||||||
|
// The url-encoded tag name to rename
|
||||||
|
//
|
||||||
|
// in: path
|
||||||
|
// required: true
|
||||||
|
// type: string
|
||||||
|
// example: Old name
|
||||||
|
Tag string
|
||||||
|
|
||||||
|
// in: body
|
||||||
|
Body struct {
|
||||||
|
// New name
|
||||||
|
//
|
||||||
|
// required: true
|
||||||
|
// example: New name
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// RenameTag (method: PUT) used to rename a tag
|
// RenameTag (method: PUT) used to rename a tag
|
||||||
func RenameTag(w http.ResponseWriter, r *http.Request) {
|
func RenameTag(w http.ResponseWriter, r *http.Request) {
|
||||||
// swagger:route PUT /api/v1/tags/{tag} tags RenameTag
|
// swagger:route PUT /api/v1/tags/{Tag} tags RenameTagParams
|
||||||
//
|
//
|
||||||
// # Rename a tag
|
// # Rename a tag
|
||||||
//
|
//
|
||||||
// Renames a tag.
|
// Renames an existing tag.
|
||||||
//
|
//
|
||||||
// Produces:
|
// Produces:
|
||||||
// - text/plain
|
// - text/plain
|
||||||
//
|
//
|
||||||
// Schemes: http, https
|
// Schemes: http, https
|
||||||
//
|
//
|
||||||
// Parameters:
|
|
||||||
// + name: tag
|
|
||||||
// in: path
|
|
||||||
// description: The url-encoded tag name to rename
|
|
||||||
// required: true
|
|
||||||
// type: string
|
|
||||||
//
|
|
||||||
// Responses:
|
// Responses:
|
||||||
// 200: OKResponse
|
// 200: OKResponse
|
||||||
// default: ErrorResponse
|
// 400: ErrorResponse
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
|
|
||||||
@ -131,29 +162,32 @@ func RenameTag(w http.ResponseWriter, r *http.Request) {
|
|||||||
_, _ = w.Write([]byte("ok"))
|
_, _ = w.Write([]byte("ok"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// swagger:parameters DeleteTagParams
|
||||||
|
type deleteTagParams struct {
|
||||||
|
// The url-encoded tag name to delete
|
||||||
|
//
|
||||||
|
// in: path
|
||||||
|
// required: true
|
||||||
|
// example: My tag
|
||||||
|
Tag string
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteTag (method: DELETE) used to delete a tag
|
// DeleteTag (method: DELETE) used to delete a tag
|
||||||
func DeleteTag(w http.ResponseWriter, r *http.Request) {
|
func DeleteTag(w http.ResponseWriter, r *http.Request) {
|
||||||
// swagger:route DELETE /api/v1/tags/{tag} tags DeleteTag
|
// swagger:route DELETE /api/v1/tags/{Tag} tags DeleteTagParams
|
||||||
//
|
//
|
||||||
// # Delete a tag
|
// # Delete a tag
|
||||||
//
|
//
|
||||||
// Deletes a tag. This will not delete any messages with this tag.
|
// Deletes a tag. This will not delete any messages with the tag, but will remove the tag from any messages containing the tag.
|
||||||
//
|
//
|
||||||
// Produces:
|
// Produces:
|
||||||
// - text/plain
|
// - text/plain
|
||||||
//
|
//
|
||||||
// Schemes: http, https
|
// Schemes: http, https
|
||||||
//
|
//
|
||||||
// Parameters:
|
|
||||||
// + name: tag
|
|
||||||
// in: path
|
|
||||||
// description: The url-encoded tag name to delete
|
|
||||||
// required: true
|
|
||||||
// type: string
|
|
||||||
//
|
|
||||||
// Responses:
|
// Responses:
|
||||||
// 200: OKResponse
|
// 200: OKResponse
|
||||||
// default: ErrorResponse
|
// 400: ErrorResponse
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
|
|
||||||
|
159
server/apiv1/testing.go
Normal file
159
server/apiv1/testing.go
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
package apiv1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/axllent/mailpit/config"
|
||||||
|
"github.com/axllent/mailpit/internal/storage"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
// swagger:parameters GetMessageHTMLParams
|
||||||
|
type getMessageHTMLParams struct {
|
||||||
|
// Message database ID or "latest"
|
||||||
|
//
|
||||||
|
// in: path
|
||||||
|
// required: true
|
||||||
|
// example: 4oRBnPtCXgAqZniRhzLNmS
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMessageHTML (method: GET) returns a rendered version of a message's HTML part
|
||||||
|
func GetMessageHTML(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// swagger:route GET /view/{ID}.html testing GetMessageHTMLParams
|
||||||
|
//
|
||||||
|
// # Render message HTML part
|
||||||
|
//
|
||||||
|
// Renders just the message's HTML part which can be used for UI integration testing.
|
||||||
|
// Attached inline images are modified to link to the API provided they exist.
|
||||||
|
// Note that is the message does not contain a HTML part then an 404 error is returned.
|
||||||
|
//
|
||||||
|
// The ID can be set to `latest` to return the latest message.
|
||||||
|
//
|
||||||
|
// Produces:
|
||||||
|
// - text/html
|
||||||
|
//
|
||||||
|
// Schemes: http, https
|
||||||
|
//
|
||||||
|
// Responses:
|
||||||
|
// 200: HTMLResponse
|
||||||
|
// 400: ErrorResponse
|
||||||
|
// 404: NotFoundResponse
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
|
||||||
|
id := vars["id"]
|
||||||
|
|
||||||
|
if id == "latest" {
|
||||||
|
var err error
|
||||||
|
id, err = storage.LatestID(r)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(404)
|
||||||
|
fmt.Fprint(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := storage.GetMessage(id)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(404)
|
||||||
|
fmt.Fprint(w, "Message not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if msg.HTML == "" {
|
||||||
|
w.WriteHeader(404)
|
||||||
|
fmt.Fprint(w, "This message does not contain a HTML part")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
html := linkInlineImages(msg)
|
||||||
|
w.Header().Add("Content-Type", "text/html; charset=utf-8")
|
||||||
|
_, _ = w.Write([]byte(html))
|
||||||
|
}
|
||||||
|
|
||||||
|
// swagger:parameters GetMessageTextParams
|
||||||
|
type getMessageTextParams struct {
|
||||||
|
// Message database ID or "latest"
|
||||||
|
//
|
||||||
|
// in: path
|
||||||
|
// required: true
|
||||||
|
// example: 4oRBnPtCXgAqZniRhzLNmS
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMessageText (method: GET) returns a message's text part
|
||||||
|
func GetMessageText(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// swagger:route GET /view/{ID}.txt testing GetMessageTextParams
|
||||||
|
//
|
||||||
|
// # Render message text part
|
||||||
|
//
|
||||||
|
// Renders just the message's text part which can be used for UI integration testing.
|
||||||
|
//
|
||||||
|
// The ID can be set to `latest` to return the latest message.
|
||||||
|
//
|
||||||
|
// Produces:
|
||||||
|
// - text/plain
|
||||||
|
//
|
||||||
|
// Schemes: http, https
|
||||||
|
//
|
||||||
|
// Responses:
|
||||||
|
// 200: TextResponse
|
||||||
|
// 400: ErrorResponse
|
||||||
|
// 404: NotFoundResponse
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
|
||||||
|
id := vars["id"]
|
||||||
|
|
||||||
|
if id == "latest" {
|
||||||
|
var err error
|
||||||
|
id, err = storage.LatestID(r)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(404)
|
||||||
|
fmt.Fprint(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := storage.GetMessage(id)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(404)
|
||||||
|
fmt.Fprint(w, "Message not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
_, _ = w.Write([]byte(msg.Text))
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will rewrite all inline image paths to API URLs
|
||||||
|
func linkInlineImages(msg *storage.Message) string {
|
||||||
|
html := msg.HTML
|
||||||
|
|
||||||
|
for _, a := range msg.Inline {
|
||||||
|
if a.ContentID != "" {
|
||||||
|
re := regexp.MustCompile(`(?i)(=["\']?)(cid:` + regexp.QuoteMeta(a.ContentID) + `)(["|\'|\\s|\\/|>|;])`)
|
||||||
|
u := config.Webroot + "api/v1/message/" + msg.ID + "/part/" + a.PartID
|
||||||
|
matches := re.FindAllStringSubmatch(html, -1)
|
||||||
|
for _, m := range matches {
|
||||||
|
html = strings.ReplaceAll(html, m[0], m[1]+u+m[3])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, a := range msg.Attachments {
|
||||||
|
if a.ContentID != "" {
|
||||||
|
re := regexp.MustCompile(`(?i)(=["\']?)(cid:` + regexp.QuoteMeta(a.ContentID) + `)(["|\'|\\s|\\/|>|;])`)
|
||||||
|
u := config.Webroot + "api/v1/message/" + msg.ID + "/part/" + a.PartID
|
||||||
|
matches := re.FindAllStringSubmatch(html, -1)
|
||||||
|
for _, m := range matches {
|
||||||
|
html = strings.ReplaceAll(html, m[0], m[1]+u+m[3])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return html
|
||||||
|
}
|
@ -22,35 +22,43 @@ var (
|
|||||||
thumbHeight = 120
|
thumbHeight = 120
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// swagger:parameters ThumbnailParams
|
||||||
|
type thumbnailParams struct {
|
||||||
|
// Message database ID or "latest"
|
||||||
|
//
|
||||||
|
// in: path
|
||||||
|
// required: true
|
||||||
|
// example: 4oRBnPtCXgAqZniRhzLNmS
|
||||||
|
ID string
|
||||||
|
|
||||||
|
// Attachment part ID
|
||||||
|
//
|
||||||
|
// in: path
|
||||||
|
// required: true
|
||||||
|
// example: 2
|
||||||
|
PartID string
|
||||||
|
}
|
||||||
|
|
||||||
// Thumbnail returns a thumbnail image for an attachment (images only)
|
// Thumbnail returns a thumbnail image for an attachment (images only)
|
||||||
func Thumbnail(w http.ResponseWriter, r *http.Request) {
|
func Thumbnail(w http.ResponseWriter, r *http.Request) {
|
||||||
// swagger:route GET /api/v1/message/{ID}/part/{PartID}/thumb message Thumbnail
|
// swagger:route GET /api/v1/message/{ID}/part/{PartID}/thumb message ThumbnailParams
|
||||||
//
|
//
|
||||||
// # Get an attachment image thumbnail
|
// # Get an attachment image thumbnail
|
||||||
//
|
//
|
||||||
// This will return a cropped 180x120 JPEG thumbnail of an image attachment.
|
// This will return a cropped 180x120 JPEG thumbnail of an image attachment.
|
||||||
// If the image is smaller than 180x120 then the image is padded. If the attachment is not an image then a blank image is returned.
|
// If the image is smaller than 180x120 then the image is padded. If the attachment is not an image then a blank image is returned.
|
||||||
//
|
//
|
||||||
|
// The ID can be set to `latest` to return the latest message.
|
||||||
|
//
|
||||||
// Produces:
|
// Produces:
|
||||||
// - image/jpeg
|
// - image/jpeg
|
||||||
//
|
//
|
||||||
// Schemes: http, https
|
// Schemes: http, https
|
||||||
//
|
//
|
||||||
// Parameters:
|
|
||||||
// + name: ID
|
|
||||||
// in: path
|
|
||||||
// description: Database ID
|
|
||||||
// required: true
|
|
||||||
// type: string
|
|
||||||
// + name: PartID
|
|
||||||
// in: path
|
|
||||||
// description: Attachment part ID
|
|
||||||
// required: true
|
|
||||||
// type: string
|
|
||||||
//
|
|
||||||
// Responses:
|
// Responses:
|
||||||
// 200: BinaryResponse
|
// 200: BinaryResponse
|
||||||
// default: ErrorResponse
|
// 400: ErrorResponse
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
|
|
||||||
id := vars["id"]
|
id := vars["id"]
|
||||||
|
@ -1,15 +1,12 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/axllent/mailpit/config"
|
"github.com/axllent/mailpit/config"
|
||||||
"github.com/axllent/mailpit/internal/storage"
|
"github.com/axllent/mailpit/internal/storage"
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// RedirectToLatestMessage (method: GET) redirects the web UI to the latest message
|
// RedirectToLatestMessage (method: GET) redirects the web UI to the latest message
|
||||||
@ -44,142 +41,3 @@ func RedirectToLatestMessage(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
http.Redirect(w, r, uri, 302)
|
http.Redirect(w, r, uri, 302)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMessageHTML (method: GET) returns a rendered version of a message's HTML part
|
|
||||||
func GetMessageHTML(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// swagger:route GET /view/{ID}.html testing GetMessageHTML
|
|
||||||
//
|
|
||||||
// # Render message HTML part
|
|
||||||
//
|
|
||||||
// Renders just the message's HTML part which can be used for UI integration testing.
|
|
||||||
// Attached inline images are modified to link to the API provided they exist.
|
|
||||||
// Note that is the message does not contain a HTML part then an 404 error is returned.
|
|
||||||
//
|
|
||||||
// The ID can be set to `latest` to return the latest message.
|
|
||||||
//
|
|
||||||
// Produces:
|
|
||||||
// - text/html
|
|
||||||
//
|
|
||||||
// Schemes: http, https
|
|
||||||
//
|
|
||||||
// Parameters:
|
|
||||||
// + name: ID
|
|
||||||
// in: path
|
|
||||||
// description: Database ID or latest
|
|
||||||
// required: true
|
|
||||||
// type: string
|
|
||||||
//
|
|
||||||
// Responses:
|
|
||||||
// 200: HTMLResponse
|
|
||||||
// default: ErrorResponse
|
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
|
|
||||||
id := vars["id"]
|
|
||||||
|
|
||||||
if id == "latest" {
|
|
||||||
var err error
|
|
||||||
id, err = storage.LatestID(r)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(404)
|
|
||||||
fmt.Fprint(w, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
msg, err := storage.GetMessage(id)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(404)
|
|
||||||
fmt.Fprint(w, "Message not found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if msg.HTML == "" {
|
|
||||||
w.WriteHeader(404)
|
|
||||||
fmt.Fprint(w, "This message does not contain a HTML part")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
html := linkInlineImages(msg)
|
|
||||||
w.Header().Add("Content-Type", "text/html; charset=utf-8")
|
|
||||||
_, _ = w.Write([]byte(html))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMessageText (method: GET) returns a message's text part
|
|
||||||
func GetMessageText(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// swagger:route GET /view/{ID}.txt testing GetMessageText
|
|
||||||
//
|
|
||||||
// # Render message text part
|
|
||||||
//
|
|
||||||
// Renders just the message's text part which can be used for UI integration testing.
|
|
||||||
//
|
|
||||||
// The ID can be set to `latest` to return the latest message.
|
|
||||||
//
|
|
||||||
// Produces:
|
|
||||||
// - text/plain
|
|
||||||
//
|
|
||||||
// Schemes: http, https
|
|
||||||
//
|
|
||||||
// Parameters:
|
|
||||||
// + name: ID
|
|
||||||
// in: path
|
|
||||||
// description: Database ID or latest
|
|
||||||
// required: true
|
|
||||||
// type: string
|
|
||||||
//
|
|
||||||
// Responses:
|
|
||||||
// 200: TextResponse
|
|
||||||
// default: ErrorResponse
|
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
|
|
||||||
id := vars["id"]
|
|
||||||
|
|
||||||
if id == "latest" {
|
|
||||||
var err error
|
|
||||||
id, err = storage.LatestID(r)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(404)
|
|
||||||
fmt.Fprint(w, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
msg, err := storage.GetMessage(id)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(404)
|
|
||||||
fmt.Fprint(w, "Message not found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "text/plain; charset=utf-8")
|
|
||||||
_, _ = w.Write([]byte(msg.Text))
|
|
||||||
}
|
|
||||||
|
|
||||||
// This will rewrite all inline image paths to API URLs
|
|
||||||
func linkInlineImages(msg *storage.Message) string {
|
|
||||||
html := msg.HTML
|
|
||||||
|
|
||||||
for _, a := range msg.Inline {
|
|
||||||
if a.ContentID != "" {
|
|
||||||
re := regexp.MustCompile(`(?i)(=["\']?)(cid:` + regexp.QuoteMeta(a.ContentID) + `)(["|\'|\\s|\\/|>|;])`)
|
|
||||||
u := config.Webroot + "api/v1/message/" + msg.ID + "/part/" + a.PartID
|
|
||||||
matches := re.FindAllStringSubmatch(html, -1)
|
|
||||||
for _, m := range matches {
|
|
||||||
html = strings.ReplaceAll(html, m[0], m[1]+u+m[3])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, a := range msg.Attachments {
|
|
||||||
if a.ContentID != "" {
|
|
||||||
re := regexp.MustCompile(`(?i)(=["\']?)(cid:` + regexp.QuoteMeta(a.ContentID) + `)(["|\'|\\s|\\/|>|;])`)
|
|
||||||
u := config.Webroot + "api/v1/message/" + msg.ID + "/part/" + a.PartID
|
|
||||||
matches := re.FindAllStringSubmatch(html, -1)
|
|
||||||
for _, m := range matches {
|
|
||||||
html = strings.ReplaceAll(html, m[0], m[1]+u+m[3])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return html
|
|
||||||
}
|
|
||||||
|
@ -81,8 +81,8 @@ func Listen() {
|
|||||||
r.HandleFunc(config.Webroot+"view/latest", middleWareFunc(handlers.RedirectToLatestMessage)).Methods("GET")
|
r.HandleFunc(config.Webroot+"view/latest", middleWareFunc(handlers.RedirectToLatestMessage)).Methods("GET")
|
||||||
|
|
||||||
// frontend testing
|
// frontend testing
|
||||||
r.HandleFunc(config.Webroot+"view/{id}.html", middleWareFunc(handlers.GetMessageHTML)).Methods("GET")
|
r.HandleFunc(config.Webroot+"view/{id}.html", middleWareFunc(apiv1.GetMessageHTML)).Methods("GET")
|
||||||
r.HandleFunc(config.Webroot+"view/{id}.txt", middleWareFunc(handlers.GetMessageText)).Methods("GET")
|
r.HandleFunc(config.Webroot+"view/{id}.txt", middleWareFunc(apiv1.GetMessageText)).Methods("GET")
|
||||||
|
|
||||||
// web UI via virtual index.html
|
// web UI via virtual index.html
|
||||||
r.PathPrefix(config.Webroot + "view/").Handler(middleWareFunc(index)).Methods("GET")
|
r.PathPrefix(config.Webroot + "view/").Handler(middleWareFunc(index)).Methods("GET")
|
||||||
|
@ -40,9 +40,9 @@
|
|||||||
"operationId": "AppInformation",
|
"operationId": "AppInformation",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"$ref": "#/responses/InfoResponse"
|
"$ref": "#/responses/AppInfoResponse"
|
||||||
},
|
},
|
||||||
"default": {
|
"400": {
|
||||||
"$ref": "#/responses/ErrorResponse"
|
"$ref": "#/responses/ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -62,10 +62,11 @@
|
|||||||
"message"
|
"message"
|
||||||
],
|
],
|
||||||
"summary": "Get message summary",
|
"summary": "Get message summary",
|
||||||
"operationId": "Message",
|
"operationId": "GetMessageParams",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
"example": "4oRBnPtCXgAqZniRhzLNmS",
|
||||||
"description": "Message database ID or \"latest\"",
|
"description": "Message database ID or \"latest\"",
|
||||||
"name": "ID",
|
"name": "ID",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
@ -79,15 +80,18 @@
|
|||||||
"$ref": "#/definitions/Message"
|
"$ref": "#/definitions/Message"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"default": {
|
"400": {
|
||||||
"$ref": "#/responses/ErrorResponse"
|
"$ref": "#/responses/ErrorResponse"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/responses/NotFoundResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/message/{ID}/headers": {
|
"/api/v1/message/{ID}/headers": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Returns the message headers as an array.\n\nThe ID can be set to `latest` to return the latest message headers.",
|
"description": "Returns the message headers as an array. Note that header keys are returned alphabetically.\n\nThe ID can be set to `latest` to return the latest message headers.",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@ -99,10 +103,11 @@
|
|||||||
"message"
|
"message"
|
||||||
],
|
],
|
||||||
"summary": "Get message headers",
|
"summary": "Get message headers",
|
||||||
"operationId": "Headers",
|
"operationId": "GetHeadersParams",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
"example": "4oRBnPtCXgAqZniRhzLNmS",
|
||||||
"description": "Message database ID or \"latest\"",
|
"description": "Message database ID or \"latest\"",
|
||||||
"name": "ID",
|
"name": "ID",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
@ -111,20 +116,23 @@
|
|||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "MessageHeaders",
|
"description": "MessageHeadersResponse",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/MessageHeaders"
|
"$ref": "#/definitions/MessageHeadersResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"default": {
|
"400": {
|
||||||
"$ref": "#/responses/ErrorResponse"
|
"$ref": "#/responses/ErrorResponse"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/responses/NotFoundResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/message/{ID}/html-check": {
|
"/api/v1/message/{ID}/html-check": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Returns the summary of the message HTML checker.",
|
"description": "Returns the summary of the message HTML checker.\n\nThe ID can be set to `latest` to return the latest message.",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@ -136,10 +144,11 @@
|
|||||||
"Other"
|
"Other"
|
||||||
],
|
],
|
||||||
"summary": "HTML check",
|
"summary": "HTML check",
|
||||||
"operationId": "HTMLCheck",
|
"operationId": "HTMLCheckParams",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
"example": "4oRBnPtCXgAqZniRhzLNmS",
|
||||||
"description": "Message database ID or \"latest\"",
|
"description": "Message database ID or \"latest\"",
|
||||||
"name": "ID",
|
"name": "ID",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
@ -153,15 +162,18 @@
|
|||||||
"$ref": "#/definitions/HTMLCheckResponse"
|
"$ref": "#/definitions/HTMLCheckResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"default": {
|
"400": {
|
||||||
"$ref": "#/responses/ErrorResponse"
|
"$ref": "#/responses/ErrorResponse"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/responses/NotFoundResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/message/{ID}/link-check": {
|
"/api/v1/message/{ID}/link-check": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Returns the summary of the message Link checker.",
|
"description": "Returns the summary of the message Link checker.\n\nThe ID can be set to `latest` to return the latest message.",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@ -173,10 +185,11 @@
|
|||||||
"Other"
|
"Other"
|
||||||
],
|
],
|
||||||
"summary": "Link check",
|
"summary": "Link check",
|
||||||
"operationId": "LinkCheck",
|
"operationId": "LinkCheckParams",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
"example": "4oRBnPtCXgAqZniRhzLNmS",
|
||||||
"description": "Message database ID or \"latest\"",
|
"description": "Message database ID or \"latest\"",
|
||||||
"name": "ID",
|
"name": "ID",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
@ -185,6 +198,7 @@
|
|||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "false",
|
"default": "false",
|
||||||
|
"example": "false",
|
||||||
"x-go-name": "Follow",
|
"x-go-name": "Follow",
|
||||||
"description": "Follow redirects",
|
"description": "Follow redirects",
|
||||||
"name": "follow",
|
"name": "follow",
|
||||||
@ -198,15 +212,18 @@
|
|||||||
"$ref": "#/definitions/LinkCheckResponse"
|
"$ref": "#/definitions/LinkCheckResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"default": {
|
"400": {
|
||||||
"$ref": "#/responses/ErrorResponse"
|
"$ref": "#/responses/ErrorResponse"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/responses/NotFoundResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/message/{ID}/part/{PartID}": {
|
"/api/v1/message/{ID}/part/{PartID}": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "This will return the attachment part using the appropriate Content-Type.",
|
"description": "This will return the attachment part using the appropriate Content-Type.\n\nThe ID can be set to `latest` to reference the latest message.",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/*",
|
"application/*",
|
||||||
"image/*",
|
"image/*",
|
||||||
@ -220,17 +237,19 @@
|
|||||||
"message"
|
"message"
|
||||||
],
|
],
|
||||||
"summary": "Get message attachment",
|
"summary": "Get message attachment",
|
||||||
"operationId": "Attachment",
|
"operationId": "AttachmentParams",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Message database ID",
|
"example": "4oRBnPtCXgAqZniRhzLNmS",
|
||||||
|
"description": "Message database ID or \"latest\"",
|
||||||
"name": "ID",
|
"name": "ID",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
"example": "2",
|
||||||
"description": "Attachment part ID",
|
"description": "Attachment part ID",
|
||||||
"name": "PartID",
|
"name": "PartID",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
@ -241,15 +260,18 @@
|
|||||||
"200": {
|
"200": {
|
||||||
"$ref": "#/responses/BinaryResponse"
|
"$ref": "#/responses/BinaryResponse"
|
||||||
},
|
},
|
||||||
"default": {
|
"400": {
|
||||||
"$ref": "#/responses/ErrorResponse"
|
"$ref": "#/responses/ErrorResponse"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/responses/NotFoundResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/message/{ID}/part/{PartID}/thumb": {
|
"/api/v1/message/{ID}/part/{PartID}/thumb": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "This will return a cropped 180x120 JPEG thumbnail of an image attachment.\nIf the image is smaller than 180x120 then the image is padded. If the attachment is not an image then a blank image is returned.",
|
"description": "This will return a cropped 180x120 JPEG thumbnail of an image attachment.\nIf the image is smaller than 180x120 then the image is padded. If the attachment is not an image then a blank image is returned.\n\nThe ID can be set to `latest` to return the latest message.",
|
||||||
"produces": [
|
"produces": [
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
],
|
],
|
||||||
@ -261,17 +283,19 @@
|
|||||||
"message"
|
"message"
|
||||||
],
|
],
|
||||||
"summary": "Get an attachment image thumbnail",
|
"summary": "Get an attachment image thumbnail",
|
||||||
"operationId": "Thumbnail",
|
"operationId": "ThumbnailParams",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Database ID",
|
"example": "4oRBnPtCXgAqZniRhzLNmS",
|
||||||
|
"description": "Message database ID or \"latest\"",
|
||||||
"name": "ID",
|
"name": "ID",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
"example": "2",
|
||||||
"description": "Attachment part ID",
|
"description": "Attachment part ID",
|
||||||
"name": "PartID",
|
"name": "PartID",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
@ -282,7 +306,7 @@
|
|||||||
"200": {
|
"200": {
|
||||||
"$ref": "#/responses/BinaryResponse"
|
"$ref": "#/responses/BinaryResponse"
|
||||||
},
|
},
|
||||||
"default": {
|
"400": {
|
||||||
"$ref": "#/responses/ErrorResponse"
|
"$ref": "#/responses/ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -302,10 +326,11 @@
|
|||||||
"message"
|
"message"
|
||||||
],
|
],
|
||||||
"summary": "Get message source",
|
"summary": "Get message source",
|
||||||
"operationId": "Raw",
|
"operationId": "DownloadRawParams",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
"example": "4oRBnPtCXgAqZniRhzLNmS",
|
||||||
"description": "Message database ID or \"latest\"",
|
"description": "Message database ID or \"latest\"",
|
||||||
"name": "ID",
|
"name": "ID",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
@ -316,15 +341,18 @@
|
|||||||
"200": {
|
"200": {
|
||||||
"$ref": "#/responses/TextResponse"
|
"$ref": "#/responses/TextResponse"
|
||||||
},
|
},
|
||||||
"default": {
|
"400": {
|
||||||
"$ref": "#/responses/ErrorResponse"
|
"$ref": "#/responses/ErrorResponse"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/responses/NotFoundResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/message/{ID}/release": {
|
"/api/v1/message/{ID}/release": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Release a message via a pre-configured external SMTP server. This is only enabled if message relaying has been configured.",
|
"description": "Release a message via a pre-configured external SMTP server. This is only enabled if message relaying has been configured.\n\nThe ID can be set to `latest` to reference the latest message.",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@ -339,10 +367,11 @@
|
|||||||
"message"
|
"message"
|
||||||
],
|
],
|
||||||
"summary": "Release message",
|
"summary": "Release message",
|
||||||
"operationId": "ReleaseMessage",
|
"operationId": "ReleaseMessageParams",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
"example": "4oRBnPtCXgAqZniRhzLNmS",
|
||||||
"description": "Message database ID",
|
"description": "Message database ID",
|
||||||
"name": "ID",
|
"name": "ID",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
@ -352,7 +381,23 @@
|
|||||||
"name": "Body",
|
"name": "Body",
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/releaseMessageRequestBody"
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"To"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"To": {
|
||||||
|
"description": "Array of email addresses to relay the message to",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"example": [
|
||||||
|
"user1@example.com",
|
||||||
|
"user2@example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -360,15 +405,18 @@
|
|||||||
"200": {
|
"200": {
|
||||||
"$ref": "#/responses/OKResponse"
|
"$ref": "#/responses/OKResponse"
|
||||||
},
|
},
|
||||||
"default": {
|
"400": {
|
||||||
"$ref": "#/responses/ErrorResponse"
|
"$ref": "#/responses/ErrorResponse"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/responses/NotFoundResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/message/{ID}/sa-check": {
|
"/api/v1/message/{ID}/sa-check": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Returns the SpamAssassin summary (if enabled) of the message.",
|
"description": "Returns the SpamAssassin summary (if enabled) of the message.\n\nThe ID can be set to `latest` to return the latest message.",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@ -380,10 +428,11 @@
|
|||||||
"Other"
|
"Other"
|
||||||
],
|
],
|
||||||
"summary": "SpamAssassin check",
|
"summary": "SpamAssassin check",
|
||||||
"operationId": "SpamAssassinCheck",
|
"operationId": "SpamAssassinCheckParams",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
"example": "4oRBnPtCXgAqZniRhzLNmS",
|
||||||
"description": "Message database ID or \"latest\"",
|
"description": "Message database ID or \"latest\"",
|
||||||
"name": "ID",
|
"name": "ID",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
@ -397,8 +446,11 @@
|
|||||||
"$ref": "#/definitions/SpamAssassinResponse"
|
"$ref": "#/definitions/SpamAssassinResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"default": {
|
"400": {
|
||||||
"$ref": "#/responses/ErrorResponse"
|
"$ref": "#/responses/ErrorResponse"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/responses/NotFoundResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -417,19 +469,25 @@
|
|||||||
"messages"
|
"messages"
|
||||||
],
|
],
|
||||||
"summary": "List messages",
|
"summary": "List messages",
|
||||||
"operationId": "GetMessages",
|
"operationId": "GetMessagesParams",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
|
"example": 100,
|
||||||
|
"x-go-name": "Start",
|
||||||
"description": "Pagination offset",
|
"description": "Pagination offset",
|
||||||
"name": "start",
|
"name": "start",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
"default": 50,
|
"default": 50,
|
||||||
"description": "Limit results",
|
"example": 50,
|
||||||
|
"x-go-name": "Limit",
|
||||||
|
"description": "Limit number of results",
|
||||||
"name": "limit",
|
"name": "limit",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
}
|
}
|
||||||
@ -438,7 +496,7 @@
|
|||||||
"200": {
|
"200": {
|
||||||
"$ref": "#/responses/MessagesSummaryResponse"
|
"$ref": "#/responses/MessagesSummaryResponse"
|
||||||
},
|
},
|
||||||
"default": {
|
"400": {
|
||||||
"$ref": "#/responses/ErrorResponse"
|
"$ref": "#/responses/ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -459,13 +517,32 @@
|
|||||||
"messages"
|
"messages"
|
||||||
],
|
],
|
||||||
"summary": "Set read status",
|
"summary": "Set read status",
|
||||||
"operationId": "SetReadStatus",
|
"operationId": "SetReadStatusParams",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "Body",
|
"name": "Body",
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/setReadStatusRequestBody"
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"IDs": {
|
||||||
|
"description": "Array of message database IDs",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"example": [
|
||||||
|
"4oRBnPtCXgAqZniRhzLNmS",
|
||||||
|
"hXayS6wnCgNnt6aFTvmOF6"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Read": {
|
||||||
|
"description": "Read status",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"example": true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -473,7 +550,7 @@
|
|||||||
"200": {
|
"200": {
|
||||||
"$ref": "#/responses/OKResponse"
|
"$ref": "#/responses/OKResponse"
|
||||||
},
|
},
|
||||||
"default": {
|
"400": {
|
||||||
"$ref": "#/responses/ErrorResponse"
|
"$ref": "#/responses/ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -494,13 +571,27 @@
|
|||||||
"messages"
|
"messages"
|
||||||
],
|
],
|
||||||
"summary": "Delete messages",
|
"summary": "Delete messages",
|
||||||
"operationId": "DeleteMessages",
|
"operationId": "DeleteMessagesParams",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
|
"description": "Delete request",
|
||||||
"name": "Body",
|
"name": "Body",
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/DeleteRequest"
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"IDs": {
|
||||||
|
"description": "Array of message database IDs",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"example": [
|
||||||
|
"4oRBnPtCXgAqZniRhzLNmS",
|
||||||
|
"hXayS6wnCgNnt6aFTvmOF6"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -508,7 +599,7 @@
|
|||||||
"200": {
|
"200": {
|
||||||
"$ref": "#/responses/OKResponse"
|
"$ref": "#/responses/OKResponse"
|
||||||
},
|
},
|
||||||
"default": {
|
"400": {
|
||||||
"$ref": "#/responses/ErrorResponse"
|
"$ref": "#/responses/ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -528,32 +619,37 @@
|
|||||||
"messages"
|
"messages"
|
||||||
],
|
],
|
||||||
"summary": "Search messages",
|
"summary": "Search messages",
|
||||||
"operationId": "MessagesSummary",
|
"operationId": "SearchParams",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
"example": "search words",
|
||||||
|
"x-go-name": "Query",
|
||||||
"description": "Search query",
|
"description": "Search query",
|
||||||
"name": "query",
|
"name": "query",
|
||||||
"in": "query",
|
"in": "query",
|
||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "string",
|
||||||
"default": 0,
|
"example": "100",
|
||||||
|
"x-go-name": "Start",
|
||||||
"description": "Pagination offset",
|
"description": "Pagination offset",
|
||||||
"name": "start",
|
"name": "start",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "string",
|
||||||
"default": 50,
|
"example": "50",
|
||||||
|
"x-go-name": "Limit",
|
||||||
"description": "Limit results",
|
"description": "Limit results",
|
||||||
"name": "limit",
|
"name": "limit",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "[Timezone identifier](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) used specifically for `before:` \u0026 `after:` searches (eg: \"Pacific/Auckland\").",
|
"x-go-name": "TZ",
|
||||||
|
"description": "[Timezone identifier](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) used only for `before:` \u0026 `after:` searches (eg: \"Pacific/Auckland\").",
|
||||||
"name": "tz",
|
"name": "tz",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
}
|
}
|
||||||
@ -562,7 +658,7 @@
|
|||||||
"200": {
|
"200": {
|
||||||
"$ref": "#/responses/MessagesSummaryResponse"
|
"$ref": "#/responses/MessagesSummaryResponse"
|
||||||
},
|
},
|
||||||
"default": {
|
"400": {
|
||||||
"$ref": "#/responses/ErrorResponse"
|
"$ref": "#/responses/ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -580,10 +676,12 @@
|
|||||||
"messages"
|
"messages"
|
||||||
],
|
],
|
||||||
"summary": "Delete messages by search",
|
"summary": "Delete messages by search",
|
||||||
"operationId": "DeleteSearch",
|
"operationId": "DeleteSearchParams",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
"example": "search words",
|
||||||
|
"x-go-name": "Query",
|
||||||
"description": "Search query",
|
"description": "Search query",
|
||||||
"name": "query",
|
"name": "query",
|
||||||
"in": "query",
|
"in": "query",
|
||||||
@ -591,7 +689,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "[Timezone identifier](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) used specifically for `before:` \u0026 `after:` searches (eg: \"Pacific/Auckland\").",
|
"x-go-name": "TZ",
|
||||||
|
"description": "[Timezone identifier](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) used only for `before:` \u0026 `after:` searches (eg: \"Pacific/Auckland\").",
|
||||||
"name": "tz",
|
"name": "tz",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
}
|
}
|
||||||
@ -600,7 +699,7 @@
|
|||||||
"200": {
|
"200": {
|
||||||
"$ref": "#/responses/OKResponse"
|
"$ref": "#/responses/OKResponse"
|
||||||
},
|
},
|
||||||
"default": {
|
"400": {
|
||||||
"$ref": "#/responses/ErrorResponse"
|
"$ref": "#/responses/ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -623,7 +722,7 @@
|
|||||||
"message"
|
"message"
|
||||||
],
|
],
|
||||||
"summary": "Send a message",
|
"summary": "Send a message",
|
||||||
"operationId": "SendMessage",
|
"operationId": "SendMessageParams",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "Body",
|
"name": "Body",
|
||||||
@ -637,7 +736,7 @@
|
|||||||
"200": {
|
"200": {
|
||||||
"$ref": "#/responses/sendMessageResponse"
|
"$ref": "#/responses/sendMessageResponse"
|
||||||
},
|
},
|
||||||
"default": {
|
"400": {
|
||||||
"$ref": "#/responses/jsonErrorResponse"
|
"$ref": "#/responses/jsonErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -662,7 +761,7 @@
|
|||||||
"200": {
|
"200": {
|
||||||
"$ref": "#/responses/ArrayResponse"
|
"$ref": "#/responses/ArrayResponse"
|
||||||
},
|
},
|
||||||
"default": {
|
"400": {
|
||||||
"$ref": "#/responses/ErrorResponse"
|
"$ref": "#/responses/ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -683,13 +782,41 @@
|
|||||||
"tags"
|
"tags"
|
||||||
],
|
],
|
||||||
"summary": "Set message tags",
|
"summary": "Set message tags",
|
||||||
"operationId": "SetTags",
|
"operationId": "SetTagsParams",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "Body",
|
"name": "Body",
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/setTagsRequestBody"
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"Tags",
|
||||||
|
"IDs"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"IDs": {
|
||||||
|
"description": "Array of message database IDs",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"example": [
|
||||||
|
"4oRBnPtCXgAqZniRhzLNmS",
|
||||||
|
"hXayS6wnCgNnt6aFTvmOF6"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Tags": {
|
||||||
|
"description": "Array of tag names to set",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"example": [
|
||||||
|
"Tag 1",
|
||||||
|
"Tag 2"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -697,15 +824,15 @@
|
|||||||
"200": {
|
"200": {
|
||||||
"$ref": "#/responses/OKResponse"
|
"$ref": "#/responses/OKResponse"
|
||||||
},
|
},
|
||||||
"default": {
|
"400": {
|
||||||
"$ref": "#/responses/ErrorResponse"
|
"$ref": "#/responses/ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/tags/{tag}": {
|
"/api/v1/tags/{Tag}": {
|
||||||
"put": {
|
"put": {
|
||||||
"description": "Renames a tag.",
|
"description": "Renames an existing tag.",
|
||||||
"produces": [
|
"produces": [
|
||||||
"text/plain"
|
"text/plain"
|
||||||
],
|
],
|
||||||
@ -717,34 +844,45 @@
|
|||||||
"tags"
|
"tags"
|
||||||
],
|
],
|
||||||
"summary": "Rename a tag",
|
"summary": "Rename a tag",
|
||||||
"operationId": "RenameTag",
|
"operationId": "RenameTagParams",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"example": "Old name",
|
||||||
|
"description": "The url-encoded tag name to rename",
|
||||||
|
"name": "Tag",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Body",
|
"name": "Body",
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/renameTagRequestBody"
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"Name"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Name": {
|
||||||
|
"description": "New name",
|
||||||
|
"type": "string",
|
||||||
|
"example": "New name"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"description": "The url-encoded tag name to rename",
|
|
||||||
"name": "tag",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"$ref": "#/responses/OKResponse"
|
"$ref": "#/responses/OKResponse"
|
||||||
},
|
},
|
||||||
"default": {
|
"400": {
|
||||||
"$ref": "#/responses/ErrorResponse"
|
"$ref": "#/responses/ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"delete": {
|
"delete": {
|
||||||
"description": "Deletes a tag. This will not delete any messages with this tag.",
|
"description": "Deletes a tag. This will not delete any messages with the tag, but will remove the tag from any messages containing the tag.",
|
||||||
"produces": [
|
"produces": [
|
||||||
"text/plain"
|
"text/plain"
|
||||||
],
|
],
|
||||||
@ -756,12 +894,13 @@
|
|||||||
"tags"
|
"tags"
|
||||||
],
|
],
|
||||||
"summary": "Delete a tag",
|
"summary": "Delete a tag",
|
||||||
"operationId": "DeleteTag",
|
"operationId": "DeleteTagParams",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
"example": "My tag",
|
||||||
"description": "The url-encoded tag name to delete",
|
"description": "The url-encoded tag name to delete",
|
||||||
"name": "tag",
|
"name": "Tag",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"required": true
|
"required": true
|
||||||
}
|
}
|
||||||
@ -770,7 +909,7 @@
|
|||||||
"200": {
|
"200": {
|
||||||
"$ref": "#/responses/OKResponse"
|
"$ref": "#/responses/OKResponse"
|
||||||
},
|
},
|
||||||
"default": {
|
"400": {
|
||||||
"$ref": "#/responses/ErrorResponse"
|
"$ref": "#/responses/ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -795,7 +934,7 @@
|
|||||||
"200": {
|
"200": {
|
||||||
"$ref": "#/responses/WebUIConfigurationResponse"
|
"$ref": "#/responses/WebUIConfigurationResponse"
|
||||||
},
|
},
|
||||||
"default": {
|
"400": {
|
||||||
"$ref": "#/responses/ErrorResponse"
|
"$ref": "#/responses/ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -815,11 +954,12 @@
|
|||||||
"testing"
|
"testing"
|
||||||
],
|
],
|
||||||
"summary": "Render message HTML part",
|
"summary": "Render message HTML part",
|
||||||
"operationId": "GetMessageHTML",
|
"operationId": "GetMessageHTMLParams",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Database ID or latest",
|
"example": "4oRBnPtCXgAqZniRhzLNmS",
|
||||||
|
"description": "Message database ID or \"latest\"",
|
||||||
"name": "ID",
|
"name": "ID",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"required": true
|
"required": true
|
||||||
@ -829,8 +969,11 @@
|
|||||||
"200": {
|
"200": {
|
||||||
"$ref": "#/responses/HTMLResponse"
|
"$ref": "#/responses/HTMLResponse"
|
||||||
},
|
},
|
||||||
"default": {
|
"400": {
|
||||||
"$ref": "#/responses/ErrorResponse"
|
"$ref": "#/responses/ErrorResponse"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/responses/NotFoundResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -849,11 +992,12 @@
|
|||||||
"testing"
|
"testing"
|
||||||
],
|
],
|
||||||
"summary": "Render message text part",
|
"summary": "Render message text part",
|
||||||
"operationId": "GetMessageText",
|
"operationId": "GetMessageTextParams",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Database ID or latest",
|
"example": "4oRBnPtCXgAqZniRhzLNmS",
|
||||||
|
"description": "Message database ID or \"latest\"",
|
||||||
"name": "ID",
|
"name": "ID",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"required": true
|
"required": true
|
||||||
@ -863,8 +1007,11 @@
|
|||||||
"200": {
|
"200": {
|
||||||
"$ref": "#/responses/TextResponse"
|
"$ref": "#/responses/TextResponse"
|
||||||
},
|
},
|
||||||
"default": {
|
"400": {
|
||||||
"$ref": "#/responses/ErrorResponse"
|
"$ref": "#/responses/ErrorResponse"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/responses/NotFoundResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -996,25 +1143,6 @@
|
|||||||
},
|
},
|
||||||
"x-go-package": "github.com/axllent/mailpit/internal/storage"
|
"x-go-package": "github.com/axllent/mailpit/internal/storage"
|
||||||
},
|
},
|
||||||
"DeleteRequest": {
|
|
||||||
"description": "Delete request",
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"IDs": {
|
|
||||||
"description": "Array of message database IDs",
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"example": [
|
|
||||||
"5dec4247-812e-4b77-9101-e25ad406e9ea",
|
|
||||||
"8ac66bbc-2d9a-4c41-ad99-00aa75fa674e"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"x-go-name": "deleteMessagesRequestBody",
|
|
||||||
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
|
|
||||||
},
|
|
||||||
"HTMLCheckResponse": {
|
"HTMLCheckResponse": {
|
||||||
"description": "Response represents the HTML check response struct",
|
"description": "Response represents the HTML check response struct",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@ -1337,7 +1465,7 @@
|
|||||||
},
|
},
|
||||||
"x-go-package": "github.com/axllent/mailpit/internal/storage"
|
"x-go-package": "github.com/axllent/mailpit/internal/storage"
|
||||||
},
|
},
|
||||||
"MessageHeaders": {
|
"MessageHeadersResponse": {
|
||||||
"description": "Message headers",
|
"description": "Message headers",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
@ -1503,7 +1631,7 @@
|
|||||||
"ID": {
|
"ID": {
|
||||||
"description": "Database ID",
|
"description": "Database ID",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "iAfZVVe2UQFNSG5BAjgYwa"
|
"example": "iAfZVVe2UQfNSG5BAjgYwa"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
|
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
|
||||||
@ -1745,102 +1873,15 @@
|
|||||||
},
|
},
|
||||||
"x-go-name": "webUIConfiguration",
|
"x-go-name": "webUIConfiguration",
|
||||||
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
|
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
|
||||||
},
|
|
||||||
"releaseMessageRequestBody": {
|
|
||||||
"description": "Release request",
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"To"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"To": {
|
|
||||||
"description": "Array of email addresses to relay the message to",
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"example": [
|
|
||||||
"user1@example.com",
|
|
||||||
"user2@example.com"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
|
|
||||||
},
|
|
||||||
"renameTagRequestBody": {
|
|
||||||
"description": "Rename tag request",
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"Name"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"Name": {
|
|
||||||
"description": "New name",
|
|
||||||
"type": "string",
|
|
||||||
"example": "New name"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
|
|
||||||
},
|
|
||||||
"setReadStatusRequestBody": {
|
|
||||||
"description": "Set read status request",
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"IDs": {
|
|
||||||
"description": "Array of message database IDs",
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"example": [
|
|
||||||
"5dec4247-812e-4b77-9101-e25ad406e9ea",
|
|
||||||
"8ac66bbc-2d9a-4c41-ad99-00aa75fa674e"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Read": {
|
|
||||||
"description": "Read status",
|
|
||||||
"type": "boolean",
|
|
||||||
"default": false,
|
|
||||||
"example": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
|
|
||||||
},
|
|
||||||
"setTagsRequestBody": {
|
|
||||||
"description": "Set tags request",
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"Tags",
|
|
||||||
"IDs"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"IDs": {
|
|
||||||
"description": "Array of message database IDs",
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"example": [
|
|
||||||
"5dec4247-812e-4b77-9101-e25ad406e9ea",
|
|
||||||
"8ac66bbc-2d9a-4c41-ad99-00aa75fa674e"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Tags": {
|
|
||||||
"description": "Array of tag names to set",
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"example": [
|
|
||||||
"Tag 1",
|
|
||||||
"Tag 2"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
|
"AppInfoResponse": {
|
||||||
|
"description": "Application information",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/AppInformation"
|
||||||
|
}
|
||||||
|
},
|
||||||
"ArrayResponse": {
|
"ArrayResponse": {
|
||||||
"description": "Plain JSON array response",
|
"description": "Plain JSON array response",
|
||||||
"schema": {
|
"schema": {
|
||||||
@ -1851,13 +1892,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"BinaryResponse": {
|
"BinaryResponse": {
|
||||||
"description": "Binary data response inherits the attachment's content type.",
|
"description": "Binary data response which inherits the attachment's content type.",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ErrorResponse": {
|
"ErrorResponse": {
|
||||||
"description": "HTTP error response will return with a \u003e= 400 response code",
|
"description": "Server error will return with a 400 status code\nwith the error message in the body",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@ -1868,18 +1909,18 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"InfoResponse": {
|
|
||||||
"description": "Application information",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/AppInformation"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"MessagesSummaryResponse": {
|
"MessagesSummaryResponse": {
|
||||||
"description": "Message summary",
|
"description": "Summary of messages",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/MessagesSummary"
|
"$ref": "#/definitions/MessagesSummary"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"NotFoundResponse": {
|
||||||
|
"description": "Not found error will return a 404 status code",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
"OKResponse": {
|
"OKResponse": {
|
||||||
"description": "Plain text \"ok\" response",
|
"description": "Plain text \"ok\" response",
|
||||||
"schema": {
|
"schema": {
|
||||||
@ -1893,7 +1934,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"WebUIConfigurationResponse": {
|
"WebUIConfigurationResponse": {
|
||||||
"description": "Web UI configuration",
|
"description": "Web UI configuration response",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/WebUIConfiguration"
|
"$ref": "#/definitions/WebUIConfiguration"
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user