1
0
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:
Ralph Slooten
2024-11-09 12:33:16 +13:00
parent 8f79fcd0d5
commit 657cada916
16 changed files with 1480 additions and 1345 deletions

View File

@ -2,692 +2,16 @@
package apiv1
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/mail"
"strconv"
"strings"
"github.com/araddon/dateparse"
"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/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
func fourOFour(w http.ResponseWriter) {
w.Header().Set("Referrer-Policy", "no-referrer")

View File

@ -6,8 +6,41 @@ import (
"net/http"
"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
//
// swagger:model WebUIConfiguration
@ -38,6 +71,15 @@ type webUIConfiguration struct {
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.
func WebUIConfig(w http.ResponseWriter, _ *http.Request) {
// swagger:route GET /api/v1/webui application WebUIConfiguration
@ -54,7 +96,8 @@ func WebUIConfig(w http.ResponseWriter, _ *http.Request) {
//
// Responses:
// 200: WebUIConfigurationResponse
// default: ErrorResponse
// 400: ErrorResponse
conf := webUIConfiguration{}
conf.Label = config.Label

View File

@ -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
View 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
View 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
View 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())
}
}

View File

@ -17,14 +17,36 @@ import (
"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.
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 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:
// - application/json
//
@ -35,7 +57,8 @@ func ReleaseMessage(w http.ResponseWriter, r *http.Request) {
//
// Responses:
// 200: OKResponse
// default: ErrorResponse
// 400: ErrorResponse
// 404: NotFoundResponse
if config.DemoMode {
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)
data := releaseMessageRequestBody{}
var data struct {
To []string
}
if err := decoder.Decode(&data); err != nil {
httpError(w, err.Error())

View File

@ -17,7 +17,7 @@ import (
"github.com/jhillyerd/enmime"
)
// swagger:parameters SendMessage
// swagger:parameters SendMessageParams
type sendMessageParams struct {
// in: body
Body *SendRequest
@ -108,13 +108,6 @@ type SendRequest struct {
Headers map[string]string
}
// SendMessageConfirmation struct
type SendMessageConfirmation struct {
// Database ID
// example: iAfZVVe2UQFNSG5BAjgYwa
ID string
}
// JSONErrorMessage struct
type JSONErrorMessage struct {
// Error message
@ -122,9 +115,25 @@ type JSONErrorMessage struct {
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
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
//
@ -140,7 +149,7 @@ func SendMessageHandler(w http.ResponseWriter, r *http.Request) {
//
// Responses:
// 200: sendMessageResponse
// default: jsonErrorResponse
// 400: jsonErrorResponse
if config.DemoMode {
httpJSONError(w, "this functionality has been disabled for demonstration purposes")

View File

@ -1,39 +1,9 @@
package apiv1
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"
)
// 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
// and understanding of the JSON structure.
@ -45,12 +15,3 @@ type Message = storage.Message
// Attachment summary
type Attachment = storage.Attachment
// HTMLCheckResponse summary
type HTMLCheckResponse = htmlcheck.Response
// LinkCheckResponse summary
type LinkCheckResponse = linkcheck.Response
// SpamAssassinResponse summary
type SpamAssassinResponse = spamassassin.Result

View File

@ -1,177 +1,8 @@
package apiv1
import "github.com/axllent/mailpit/internal/stats"
// These structs are for the purpose of defining swagger HTTP parameters & responses
// Application information
// 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.
// Binary data response which inherits the attachment's content type.
// swagger:response BinaryResponse
type binaryResponse string
@ -183,11 +14,15 @@ type textResponse string
// swagger:response HTMLResponse
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
// example: invalid request
type errorResponse string
// Not found error will return a 404 status code
// swagger:response NotFoundResponse
type notFoundResponse string
// Plain text "ok" response
// swagger:response OKResponse
type okResponse string
@ -196,15 +31,6 @@ type okResponse string
// swagger:response ArrayResponse
type arrayResponse []string
// Confirmation message for HTTP send API
// swagger:response sendMessageResponse
type sendMessageResponse struct {
// Response for sending messages via the HTTP API
//
// in: body
Body SendMessageConfirmation
}
// JSON error response
// swagger:response jsonErrorResponse
type jsonErrorResponse struct {

View File

@ -24,7 +24,7 @@ func GetAllTags(w http.ResponseWriter, _ *http.Request) {
//
// Responses:
// 200: ArrayResponse
// default: ErrorResponse
// 400: ErrorResponse
w.Header().Add("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(storage.GetAllTags()); err != nil {
@ -32,9 +32,27 @@ 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
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
//
@ -50,7 +68,7 @@ func SetMessageTags(w http.ResponseWriter, r *http.Request) {
//
// Responses:
// 200: OKResponse
// default: ErrorResponse
// 400: ErrorResponse
decoder := json.NewDecoder(r.Body)
@ -80,29 +98,42 @@ func SetMessageTags(w http.ResponseWriter, r *http.Request) {
_, _ = 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
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
//
// Renames a tag.
// Renames an existing tag.
//
// Produces:
// - text/plain
//
// Schemes: http, https
//
// Parameters:
// + name: tag
// in: path
// description: The url-encoded tag name to rename
// required: true
// type: string
//
// Responses:
// 200: OKResponse
// default: ErrorResponse
// 400: ErrorResponse
vars := mux.Vars(r)
@ -131,29 +162,32 @@ func RenameTag(w http.ResponseWriter, r *http.Request) {
_, _ = 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
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
//
// 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:
// - text/plain
//
// Schemes: http, https
//
// Parameters:
// + name: tag
// in: path
// description: The url-encoded tag name to delete
// required: true
// type: string
//
// Responses:
// 200: OKResponse
// default: ErrorResponse
// 400: ErrorResponse
vars := mux.Vars(r)

159
server/apiv1/testing.go Normal file
View 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
}

View File

@ -22,35 +22,43 @@ var (
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)
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
//
// 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.
//
// The ID can be set to `latest` to return the latest message.
//
// Produces:
// - image/jpeg
//
// 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:
// 200: BinaryResponse
// default: ErrorResponse
// 400: ErrorResponse
vars := mux.Vars(r)
id := vars["id"]

View File

@ -1,15 +1,12 @@
package handlers
import (
"fmt"
"net/http"
"net/url"
"regexp"
"strings"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/internal/storage"
"github.com/gorilla/mux"
)
// 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)
}
// 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
}

View File

@ -81,8 +81,8 @@ func Listen() {
r.HandleFunc(config.Webroot+"view/latest", middleWareFunc(handlers.RedirectToLatestMessage)).Methods("GET")
// frontend testing
r.HandleFunc(config.Webroot+"view/{id}.html", middleWareFunc(handlers.GetMessageHTML)).Methods("GET")
r.HandleFunc(config.Webroot+"view/{id}.txt", middleWareFunc(handlers.GetMessageText)).Methods("GET")
r.HandleFunc(config.Webroot+"view/{id}.html", middleWareFunc(apiv1.GetMessageHTML)).Methods("GET")
r.HandleFunc(config.Webroot+"view/{id}.txt", middleWareFunc(apiv1.GetMessageText)).Methods("GET")
// web UI via virtual index.html
r.PathPrefix(config.Webroot + "view/").Handler(middleWareFunc(index)).Methods("GET")

View File

@ -40,9 +40,9 @@
"operationId": "AppInformation",
"responses": {
"200": {
"$ref": "#/responses/InfoResponse"
"$ref": "#/responses/AppInfoResponse"
},
"default": {
"400": {
"$ref": "#/responses/ErrorResponse"
}
}
@ -62,10 +62,11 @@
"message"
],
"summary": "Get message summary",
"operationId": "Message",
"operationId": "GetMessageParams",
"parameters": [
{
"type": "string",
"example": "4oRBnPtCXgAqZniRhzLNmS",
"description": "Message database ID or \"latest\"",
"name": "ID",
"in": "path",
@ -79,15 +80,18 @@
"$ref": "#/definitions/Message"
}
},
"default": {
"400": {
"$ref": "#/responses/ErrorResponse"
},
"404": {
"$ref": "#/responses/NotFoundResponse"
}
}
}
},
"/api/v1/message/{ID}/headers": {
"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": [
"application/json"
],
@ -99,10 +103,11 @@
"message"
],
"summary": "Get message headers",
"operationId": "Headers",
"operationId": "GetHeadersParams",
"parameters": [
{
"type": "string",
"example": "4oRBnPtCXgAqZniRhzLNmS",
"description": "Message database ID or \"latest\"",
"name": "ID",
"in": "path",
@ -111,20 +116,23 @@
],
"responses": {
"200": {
"description": "MessageHeaders",
"description": "MessageHeadersResponse",
"schema": {
"$ref": "#/definitions/MessageHeaders"
"$ref": "#/definitions/MessageHeadersResponse"
}
},
"default": {
"400": {
"$ref": "#/responses/ErrorResponse"
},
"404": {
"$ref": "#/responses/NotFoundResponse"
}
}
}
},
"/api/v1/message/{ID}/html-check": {
"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": [
"application/json"
],
@ -136,10 +144,11 @@
"Other"
],
"summary": "HTML check",
"operationId": "HTMLCheck",
"operationId": "HTMLCheckParams",
"parameters": [
{
"type": "string",
"example": "4oRBnPtCXgAqZniRhzLNmS",
"description": "Message database ID or \"latest\"",
"name": "ID",
"in": "path",
@ -153,15 +162,18 @@
"$ref": "#/definitions/HTMLCheckResponse"
}
},
"default": {
"400": {
"$ref": "#/responses/ErrorResponse"
},
"404": {
"$ref": "#/responses/NotFoundResponse"
}
}
}
},
"/api/v1/message/{ID}/link-check": {
"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": [
"application/json"
],
@ -173,10 +185,11 @@
"Other"
],
"summary": "Link check",
"operationId": "LinkCheck",
"operationId": "LinkCheckParams",
"parameters": [
{
"type": "string",
"example": "4oRBnPtCXgAqZniRhzLNmS",
"description": "Message database ID or \"latest\"",
"name": "ID",
"in": "path",
@ -185,6 +198,7 @@
{
"type": "string",
"default": "false",
"example": "false",
"x-go-name": "Follow",
"description": "Follow redirects",
"name": "follow",
@ -198,15 +212,18 @@
"$ref": "#/definitions/LinkCheckResponse"
}
},
"default": {
"400": {
"$ref": "#/responses/ErrorResponse"
},
"404": {
"$ref": "#/responses/NotFoundResponse"
}
}
}
},
"/api/v1/message/{ID}/part/{PartID}": {
"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": [
"application/*",
"image/*",
@ -220,17 +237,19 @@
"message"
],
"summary": "Get message attachment",
"operationId": "Attachment",
"operationId": "AttachmentParams",
"parameters": [
{
"type": "string",
"description": "Message database ID",
"example": "4oRBnPtCXgAqZniRhzLNmS",
"description": "Message database ID or \"latest\"",
"name": "ID",
"in": "path",
"required": true
},
{
"type": "string",
"example": "2",
"description": "Attachment part ID",
"name": "PartID",
"in": "path",
@ -241,15 +260,18 @@
"200": {
"$ref": "#/responses/BinaryResponse"
},
"default": {
"400": {
"$ref": "#/responses/ErrorResponse"
},
"404": {
"$ref": "#/responses/NotFoundResponse"
}
}
}
},
"/api/v1/message/{ID}/part/{PartID}/thumb": {
"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": [
"image/jpeg"
],
@ -261,17 +283,19 @@
"message"
],
"summary": "Get an attachment image thumbnail",
"operationId": "Thumbnail",
"operationId": "ThumbnailParams",
"parameters": [
{
"type": "string",
"description": "Database ID",
"example": "4oRBnPtCXgAqZniRhzLNmS",
"description": "Message database ID or \"latest\"",
"name": "ID",
"in": "path",
"required": true
},
{
"type": "string",
"example": "2",
"description": "Attachment part ID",
"name": "PartID",
"in": "path",
@ -282,7 +306,7 @@
"200": {
"$ref": "#/responses/BinaryResponse"
},
"default": {
"400": {
"$ref": "#/responses/ErrorResponse"
}
}
@ -302,10 +326,11 @@
"message"
],
"summary": "Get message source",
"operationId": "Raw",
"operationId": "DownloadRawParams",
"parameters": [
{
"type": "string",
"example": "4oRBnPtCXgAqZniRhzLNmS",
"description": "Message database ID or \"latest\"",
"name": "ID",
"in": "path",
@ -316,15 +341,18 @@
"200": {
"$ref": "#/responses/TextResponse"
},
"default": {
"400": {
"$ref": "#/responses/ErrorResponse"
},
"404": {
"$ref": "#/responses/NotFoundResponse"
}
}
}
},
"/api/v1/message/{ID}/release": {
"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": [
"application/json"
],
@ -339,10 +367,11 @@
"message"
],
"summary": "Release message",
"operationId": "ReleaseMessage",
"operationId": "ReleaseMessageParams",
"parameters": [
{
"type": "string",
"example": "4oRBnPtCXgAqZniRhzLNmS",
"description": "Message database ID",
"name": "ID",
"in": "path",
@ -352,7 +381,23 @@
"name": "Body",
"in": "body",
"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": {
"$ref": "#/responses/OKResponse"
},
"default": {
"400": {
"$ref": "#/responses/ErrorResponse"
},
"404": {
"$ref": "#/responses/NotFoundResponse"
}
}
}
},
"/api/v1/message/{ID}/sa-check": {
"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": [
"application/json"
],
@ -380,10 +428,11 @@
"Other"
],
"summary": "SpamAssassin check",
"operationId": "SpamAssassinCheck",
"operationId": "SpamAssassinCheckParams",
"parameters": [
{
"type": "string",
"example": "4oRBnPtCXgAqZniRhzLNmS",
"description": "Message database ID or \"latest\"",
"name": "ID",
"in": "path",
@ -397,8 +446,11 @@
"$ref": "#/definitions/SpamAssassinResponse"
}
},
"default": {
"400": {
"$ref": "#/responses/ErrorResponse"
},
"404": {
"$ref": "#/responses/NotFoundResponse"
}
}
}
@ -417,19 +469,25 @@
"messages"
],
"summary": "List messages",
"operationId": "GetMessages",
"operationId": "GetMessagesParams",
"parameters": [
{
"type": "integer",
"format": "int64",
"default": 0,
"example": 100,
"x-go-name": "Start",
"description": "Pagination offset",
"name": "start",
"in": "query"
},
{
"type": "integer",
"format": "int64",
"default": 50,
"description": "Limit results",
"example": 50,
"x-go-name": "Limit",
"description": "Limit number of results",
"name": "limit",
"in": "query"
}
@ -438,7 +496,7 @@
"200": {
"$ref": "#/responses/MessagesSummaryResponse"
},
"default": {
"400": {
"$ref": "#/responses/ErrorResponse"
}
}
@ -459,13 +517,32 @@
"messages"
],
"summary": "Set read status",
"operationId": "SetReadStatus",
"operationId": "SetReadStatusParams",
"parameters": [
{
"name": "Body",
"in": "body",
"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": {
"$ref": "#/responses/OKResponse"
},
"default": {
"400": {
"$ref": "#/responses/ErrorResponse"
}
}
@ -494,13 +571,27 @@
"messages"
],
"summary": "Delete messages",
"operationId": "DeleteMessages",
"operationId": "DeleteMessagesParams",
"parameters": [
{
"description": "Delete request",
"name": "Body",
"in": "body",
"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": {
"$ref": "#/responses/OKResponse"
},
"default": {
"400": {
"$ref": "#/responses/ErrorResponse"
}
}
@ -528,32 +619,37 @@
"messages"
],
"summary": "Search messages",
"operationId": "MessagesSummary",
"operationId": "SearchParams",
"parameters": [
{
"type": "string",
"example": "search words",
"x-go-name": "Query",
"description": "Search query",
"name": "query",
"in": "query",
"required": true
},
{
"type": "integer",
"default": 0,
"type": "string",
"example": "100",
"x-go-name": "Start",
"description": "Pagination offset",
"name": "start",
"in": "query"
},
{
"type": "integer",
"default": 50,
"type": "string",
"example": "50",
"x-go-name": "Limit",
"description": "Limit results",
"name": "limit",
"in": "query"
},
{
"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",
"in": "query"
}
@ -562,7 +658,7 @@
"200": {
"$ref": "#/responses/MessagesSummaryResponse"
},
"default": {
"400": {
"$ref": "#/responses/ErrorResponse"
}
}
@ -580,10 +676,12 @@
"messages"
],
"summary": "Delete messages by search",
"operationId": "DeleteSearch",
"operationId": "DeleteSearchParams",
"parameters": [
{
"type": "string",
"example": "search words",
"x-go-name": "Query",
"description": "Search query",
"name": "query",
"in": "query",
@ -591,7 +689,8 @@
},
{
"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",
"in": "query"
}
@ -600,7 +699,7 @@
"200": {
"$ref": "#/responses/OKResponse"
},
"default": {
"400": {
"$ref": "#/responses/ErrorResponse"
}
}
@ -623,7 +722,7 @@
"message"
],
"summary": "Send a message",
"operationId": "SendMessage",
"operationId": "SendMessageParams",
"parameters": [
{
"name": "Body",
@ -637,7 +736,7 @@
"200": {
"$ref": "#/responses/sendMessageResponse"
},
"default": {
"400": {
"$ref": "#/responses/jsonErrorResponse"
}
}
@ -662,7 +761,7 @@
"200": {
"$ref": "#/responses/ArrayResponse"
},
"default": {
"400": {
"$ref": "#/responses/ErrorResponse"
}
}
@ -683,13 +782,41 @@
"tags"
],
"summary": "Set message tags",
"operationId": "SetTags",
"operationId": "SetTagsParams",
"parameters": [
{
"name": "Body",
"in": "body",
"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": {
"$ref": "#/responses/OKResponse"
},
"default": {
"400": {
"$ref": "#/responses/ErrorResponse"
}
}
}
},
"/api/v1/tags/{tag}": {
"/api/v1/tags/{Tag}": {
"put": {
"description": "Renames a tag.",
"description": "Renames an existing tag.",
"produces": [
"text/plain"
],
@ -717,34 +844,45 @@
"tags"
],
"summary": "Rename a tag",
"operationId": "RenameTag",
"operationId": "RenameTagParams",
"parameters": [
{
"type": "string",
"example": "Old name",
"description": "The url-encoded tag name to rename",
"name": "Tag",
"in": "path",
"required": true
},
{
"name": "Body",
"in": "body",
"schema": {
"$ref": "#/definitions/renameTagRequestBody"
}
},
{
"type": "object",
"required": [
"Name"
],
"properties": {
"Name": {
"description": "New name",
"type": "string",
"description": "The url-encoded tag name to rename",
"name": "tag",
"in": "path",
"required": true
"example": "New name"
}
}
}
}
],
"responses": {
"200": {
"$ref": "#/responses/OKResponse"
},
"default": {
"400": {
"$ref": "#/responses/ErrorResponse"
}
}
},
"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": [
"text/plain"
],
@ -756,12 +894,13 @@
"tags"
],
"summary": "Delete a tag",
"operationId": "DeleteTag",
"operationId": "DeleteTagParams",
"parameters": [
{
"type": "string",
"example": "My tag",
"description": "The url-encoded tag name to delete",
"name": "tag",
"name": "Tag",
"in": "path",
"required": true
}
@ -770,7 +909,7 @@
"200": {
"$ref": "#/responses/OKResponse"
},
"default": {
"400": {
"$ref": "#/responses/ErrorResponse"
}
}
@ -795,7 +934,7 @@
"200": {
"$ref": "#/responses/WebUIConfigurationResponse"
},
"default": {
"400": {
"$ref": "#/responses/ErrorResponse"
}
}
@ -815,11 +954,12 @@
"testing"
],
"summary": "Render message HTML part",
"operationId": "GetMessageHTML",
"operationId": "GetMessageHTMLParams",
"parameters": [
{
"type": "string",
"description": "Database ID or latest",
"example": "4oRBnPtCXgAqZniRhzLNmS",
"description": "Message database ID or \"latest\"",
"name": "ID",
"in": "path",
"required": true
@ -829,8 +969,11 @@
"200": {
"$ref": "#/responses/HTMLResponse"
},
"default": {
"400": {
"$ref": "#/responses/ErrorResponse"
},
"404": {
"$ref": "#/responses/NotFoundResponse"
}
}
}
@ -849,11 +992,12 @@
"testing"
],
"summary": "Render message text part",
"operationId": "GetMessageText",
"operationId": "GetMessageTextParams",
"parameters": [
{
"type": "string",
"description": "Database ID or latest",
"example": "4oRBnPtCXgAqZniRhzLNmS",
"description": "Message database ID or \"latest\"",
"name": "ID",
"in": "path",
"required": true
@ -863,8 +1007,11 @@
"200": {
"$ref": "#/responses/TextResponse"
},
"default": {
"400": {
"$ref": "#/responses/ErrorResponse"
},
"404": {
"$ref": "#/responses/NotFoundResponse"
}
}
}
@ -996,25 +1143,6 @@
},
"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": {
"description": "Response represents the HTML check response struct",
"type": "object",
@ -1337,7 +1465,7 @@
},
"x-go-package": "github.com/axllent/mailpit/internal/storage"
},
"MessageHeaders": {
"MessageHeadersResponse": {
"description": "Message headers",
"type": "object",
"additionalProperties": {
@ -1503,7 +1631,7 @@
"ID": {
"description": "Database ID",
"type": "string",
"example": "iAfZVVe2UQFNSG5BAjgYwa"
"example": "iAfZVVe2UQfNSG5BAjgYwa"
}
},
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
@ -1745,102 +1873,15 @@
},
"x-go-name": "webUIConfiguration",
"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": {
"AppInfoResponse": {
"description": "Application information",
"schema": {
"$ref": "#/definitions/AppInformation"
}
},
"ArrayResponse": {
"description": "Plain JSON array response",
"schema": {
@ -1851,13 +1892,13 @@
}
},
"BinaryResponse": {
"description": "Binary data response inherits the attachment's content type.",
"description": "Binary data response which inherits the attachment's content type.",
"schema": {
"type": "string"
}
},
"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": {
"type": "string"
}
@ -1868,18 +1909,18 @@
"type": "string"
}
},
"InfoResponse": {
"description": "Application information",
"schema": {
"$ref": "#/definitions/AppInformation"
}
},
"MessagesSummaryResponse": {
"description": "Message summary",
"description": "Summary of messages",
"schema": {
"$ref": "#/definitions/MessagesSummary"
}
},
"NotFoundResponse": {
"description": "Not found error will return a 404 status code",
"schema": {
"type": "string"
}
},
"OKResponse": {
"description": "Plain text \"ok\" response",
"schema": {
@ -1893,7 +1934,7 @@
}
},
"WebUIConfigurationResponse": {
"description": "Web UI configuration",
"description": "Web UI configuration response",
"schema": {
"$ref": "#/definitions/WebUIConfiguration"
}