1
0
mirror of https://github.com/axllent/mailpit.git synced 2025-03-07 15:21:01 +02:00

Merge branch 'release/v1.5.0'

This commit is contained in:
Ralph Slooten 2023-03-31 18:51:20 +13:00
commit c4a695e627
21 changed files with 4229 additions and 221 deletions

@ -2,6 +2,22 @@
Notable changes to Mailpit will be documented in this file. Notable changes to Mailpit will be documented in this file.
## [v1.5.0]
### API
- Return received datetime when message does not contain a date header
### Bugfix
- Fix JavaScript error when adding the first tag manually
### Feature
- OpenAPI / Swagger schema
- Download raw message, HTML/text body parts or attachments via single button
- Rename SSL to TLS, add deprecation warnings to flags & ENV variables referring to SSL
- Options to support auth without STARTTLS, and accept any login
- Option to use message dates as received dates (new messages only)
## [v1.4.0] ## [v1.4.0]
### API ### API
@ -427,6 +443,3 @@ This release includes a major backend storage change (SQLite) that will render a
### Feature ### Feature
- Unread statistics - Unread statistics

@ -6,7 +6,7 @@
![CodeQL](https://github.com/axllent/mailpit/actions/workflows/codeql-analysis.yml/badge.svg) ![CodeQL](https://github.com/axllent/mailpit/actions/workflows/codeql-analysis.yml/badge.svg)
[![Go Report Card](https://goreportcard.com/badge/github.com/axllent/mailpit)](https://goreportcard.com/report/github.com/axllent/mailpit) [![Go Report Card](https://goreportcard.com/badge/github.com/axllent/mailpit)](https://goreportcard.com/report/github.com/axllent/mailpit)
Mailpit is a multi-platform email testing tool 7 API for developers. Mailpit is a multi-platform email testing tool & API for developers.
It acts as both an SMTP server, and provides a web interface to view all captured emails. It acts as both an SMTP server, and provides a web interface to view all captured emails.

@ -4,6 +4,8 @@ Mailpit provides a simple REST API to access and delete stored messages.
If the Mailpit server is set to use Basic Authentication, then API requests must use Basic Authentication too. If the Mailpit server is set to use Basic Authentication, then API requests must use Basic Authentication too.
You can view the Swagger API documentation directly within Mailpit by going to `http://0.0.0.0:8025/api/v1/`.
The API is split into three main parts: The API is split into three main parts:
- [Messages](Messages.md) - Listing, deleting & marking messages as read/unread. - [Messages](Messages.md) - Listing, deleting & marking messages as read/unread.

@ -5,20 +5,25 @@ import { sassPlugin } from 'esbuild-sass-plugin'
const doWatch = process.env.WATCH == 'true' ? true : false; const doWatch = process.env.WATCH == 'true' ? true : false;
const doMinify = process.env.MINIFY == 'true' ? true : false; const doMinify = process.env.MINIFY == 'true' ? true : false;
const ctx = await esbuild.context({ const ctx = await esbuild.context(
entryPoints: ["server/ui-src/app.js"], {
bundle: true, entryPoints: [
minify: doMinify, "server/ui-src/app.js",
sourcemap: false, "server/ui-src/docs.js"
outfile: "server/ui/dist/app.js", ],
plugins: [pluginVue(), sassPlugin()], bundle: true,
loader: { minify: doMinify,
".svg": "file", sourcemap: false,
".woff": "file", outdir: "server/ui/dist/",
".woff2": "file", plugins: [pluginVue(), sassPlugin()],
}, loader: {
logLevel: "info" ".svg": "file",
}) ".woff": "file",
".woff2": "file",
},
logLevel: "info"
}
)
if (doWatch) { if (doWatch) {
await ctx.watch() await ctx.watch()

2717
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -14,6 +14,7 @@
"bootstrap5-tags": "^1.4.41", "bootstrap5-tags": "^1.4.41",
"moment": "^2.29.4", "moment": "^2.29.4",
"prismjs": "^1.29.0", "prismjs": "^1.29.0",
"rapidoc": "^9.3.4",
"tinycon": "^0.6.8", "tinycon": "^0.6.8",
"vue": "^3.2.13" "vue": "^3.2.13"
}, },

@ -16,6 +16,34 @@ import (
// GetMessages returns a paginated list of messages as JSON // GetMessages returns a paginated list of messages as JSON
func GetMessages(w http.ResponseWriter, r *http.Request) { 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, limit := getStartLimit(r) start, limit := getStartLimit(r)
messages, err := storage.List(start, limit) messages, err := storage.List(start, limit)
@ -40,11 +68,38 @@ func GetMessages(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(bytes) _, _ = w.Write(bytes)
} }
// Search returns up to 200 of the latest messages as JSON // Search returns the latest messages as JSON
func Search(w http.ResponseWriter, r *http.Request) { func Search(w http.ResponseWriter, r *http.Request) {
// swagger:route GET /api/v1/search messages MessagesSummary
//
// # Search messages
//
// Returns the latest messages matching a search.
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Parameters:
// + name: query
// in: query
// description: search query
// required: true
// type: string
// + name: limit
// in: query
// description: limit results
// required: false
// type: integer
// default: 50
//
// Responses:
// 200: MessagesSummaryResponse
// default: ErrorResponse
search := strings.TrimSpace(r.URL.Query().Get("query")) search := strings.TrimSpace(r.URL.Query().Get("query"))
if search == "" { if search == "" {
fourOFour(w) httpError(w, "Error: no search query")
return return
} }
@ -72,15 +127,37 @@ func Search(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(bytes) _, _ = w.Write(bytes)
} }
// GetMessage (method: GET) returns the *data.Message as JSON // GetMessage (method: GET) returns the Message as JSON
func GetMessage(w http.ResponseWriter, r *http.Request) { 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.
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Parameters:
// + name: ID
// in: path
// description: message id
// required: true
// type: string
//
// Responses:
// 200: Message
// default: ErrorResponse
vars := mux.Vars(r) vars := mux.Vars(r)
id := vars["id"] id := vars["id"]
msg, err := storage.GetMessage(id) msg, err := storage.GetMessage(id)
if err != nil { if err != nil {
httpError(w, "Message not found") fourOFour(w)
return return
} }
@ -91,6 +168,35 @@ func GetMessage(w http.ResponseWriter, r *http.Request) {
// DownloadAttachment (method: GET) returns the attachment data // DownloadAttachment (method: GET) returns the attachment data
func DownloadAttachment(w http.ResponseWriter, r *http.Request) { 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 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) vars := mux.Vars(r)
id := vars["id"] id := vars["id"]
@ -98,7 +204,7 @@ func DownloadAttachment(w http.ResponseWriter, r *http.Request) {
a, err := storage.GetAttachmentPart(id, partID) a, err := storage.GetAttachmentPart(id, partID)
if err != nil { if err != nil {
httpError(w, err.Error()) fourOFour(w)
return return
} }
fileName := a.FileName fileName := a.FileName
@ -111,15 +217,37 @@ func DownloadAttachment(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(a.Content) _, _ = w.Write(a.Content)
} }
// Headers (method: GET) returns the message headers as JSON // GetHeaders (method: GET) returns the message headers as JSON
func Headers(w http.ResponseWriter, r *http.Request) { 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.
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Parameters:
// + name: ID
// in: path
// description: message id
// required: true
// type: string
//
// Responses:
// 200: MessageHeaders
// default: ErrorResponse
vars := mux.Vars(r) vars := mux.Vars(r)
id := vars["id"] id := vars["id"]
data, err := storage.GetMessageRaw(id) data, err := storage.GetMessageRaw(id)
if err != nil { if err != nil {
httpError(w, err.Error()) fourOFour(w)
return return
} }
@ -130,8 +258,7 @@ func Headers(w http.ResponseWriter, r *http.Request) {
return return
} }
headers := m.Header bytes, _ := json.Marshal(m.Header)
bytes, _ := json.Marshal(headers)
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
_, _ = w.Write(bytes) _, _ = w.Write(bytes)
@ -139,6 +266,28 @@ func Headers(w http.ResponseWriter, r *http.Request) {
// DownloadRaw (method: GET) returns the full email source as plain text // DownloadRaw (method: GET) returns the full email source as plain text
func DownloadRaw(w http.ResponseWriter, r *http.Request) { 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.
//
// Produces:
// - text/plain
//
// Schemes: http, https
//
// Parameters:
// + name: ID
// in: path
// description: message id
// required: true
// type: string
//
// Responses:
// 200: TextResponse
// default: ErrorResponse
vars := mux.Vars(r) vars := mux.Vars(r)
id := vars["id"] id := vars["id"]
@ -147,7 +296,7 @@ func DownloadRaw(w http.ResponseWriter, r *http.Request) {
data, err := storage.GetMessageRaw(id) data, err := storage.GetMessageRaw(id)
if err != nil { if err != nil {
httpError(w, err.Error()) fourOFour(w)
return return
} }
@ -159,8 +308,32 @@ func DownloadRaw(w http.ResponseWriter, r *http.Request) {
} }
// DeleteMessages (method: DELETE) deletes all messages matching IDS. // DeleteMessages (method: DELETE) deletes all messages matching IDS.
// If no IDs are provided then all messages are deleted.
func DeleteMessages(w http.ResponseWriter, r *http.Request) { func DeleteMessages(w http.ResponseWriter, r *http.Request) {
// swagger:route DELETE /api/v1/messages messages Delete
//
// # Delete messages
//
// If no IDs are provided then all messages are deleted.
//
// Consumes:
// - application/json
//
// Produces:
// - text/plain
//
// Schemes: http, https
//
// Parameters:
// + name: ids
// in: body
// description: Message ids to delete
// required: false
// type: DeleteRequest
//
// Responses:
// 200: OKResponse
// default: ErrorResponse
decoder := json.NewDecoder(r.Body) decoder := json.NewDecoder(r.Body)
var data struct { var data struct {
IDs []string IDs []string
@ -185,7 +358,33 @@ func DeleteMessages(w http.ResponseWriter, r *http.Request) {
} }
// SetReadStatus (method: PUT) will update the status to Read/Unread for all provided IDs // 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) { 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
//
// Parameters:
// + name: ids
// in: body
// description: Message ids to update
// required: false
// type: SetReadStatusRequest
//
// Responses:
// 200: OKResponse
// default: ErrorResponse
decoder := json.NewDecoder(r.Body) decoder := json.NewDecoder(r.Body)
var data struct { var data struct {
@ -239,6 +438,31 @@ func SetReadStatus(w http.ResponseWriter, r *http.Request) {
// SetTags (method: PUT) will set the tags for all provided IDs // SetTags (method: PUT) will set the tags for all provided IDs
func SetTags(w http.ResponseWriter, r *http.Request) { func SetTags(w http.ResponseWriter, r *http.Request) {
// swagger:route PUT /api/v1/tags tags SetTags
//
// # Set message tags
//
// To remove all tags from a message, pass an empty tags array.
//
// Consumes:
// - application/json
//
// Produces:
// - text/plain
//
// Schemes: http, https
//
// Parameters:
// + name: ids
// in: body
// description: Message ids to update
// required: true
// type: SetTagsRequest
//
// Responses:
// 200: OKResponse
// default: ErrorResponse
decoder := json.NewDecoder(r.Body) decoder := json.NewDecoder(r.Body)
var data struct { var data struct {

@ -11,19 +11,41 @@ import (
"github.com/axllent/mailpit/utils/updater" "github.com/axllent/mailpit/utils/updater"
) )
type appVersion struct { // Response includes the current and latest Mailpit versions, database info, and memory usage
Version string //
// swagger:model AppInformation
type appInformation struct {
// Current Mailpit version
Version string
// Latest Mailpit version
LatestVersion string LatestVersion string
Database string // Database path
DatabaseSize int64 Database string
Messages int // Database size in bytes
Memory uint64 DatabaseSize int64
// Total number of messages in the database
Messages int
// Current memory usage in bytes
Memory uint64
} }
// AppInfo returns some basic details about the running app, and latest release. // AppInfo returns some basic details about the running app, and latest release.
func AppInfo(w http.ResponseWriter, r *http.Request) { func AppInfo(w http.ResponseWriter, r *http.Request) {
// swagger:route GET /api/v1/info application AppInformation
info := appVersion{} //
// # Get the application information
//
// Returns basic runtime information, message totals and latest release version.
//
// Produces:
// - application/octet-stream
//
// Schemes: http, https
//
// Responses:
// 200: InfoResponse
// default: ErrorResponse
info := appInformation{}
info.Version = config.Version info.Version = config.Version
var m runtime.MemStats var m runtime.MemStats

@ -1,6 +1,30 @@
package apiv1 package apiv1
import "github.com/axllent/mailpit/storage" import (
"github.com/axllent/mailpit/storage"
)
// MessagesSummary is a summary of a list of messages
type MessagesSummary struct {
// Total number of messages in mailbox
Total int `json:"total"`
// Total number of unread messages in mailbox
Unread int `json:"unread"`
// Number of results returned
Count int `json:"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.
@ -8,16 +32,6 @@ import "github.com/axllent/mailpit/storage"
// MessageSummary - summary of a single message // MessageSummary - summary of a single message
type MessageSummary = storage.MessageSummary type MessageSummary = storage.MessageSummary
// MessagesSummary - summary of a list of messages
type MessagesSummary struct {
Total int `json:"total"`
Unread int `json:"unread"`
Count int `json:"count"`
Start int `json:"start"`
Tags []string `json:"tags"`
Messages []MessageSummary `json:"messages"`
}
// Message data // Message data
type Message = storage.Message type Message = storage.Message

@ -0,0 +1,19 @@
consumes:
- application/json
info:
description: |-
OpenAPI 2.0 documentation for [Mailpit](https://github.com/axllent/mailpit).
title: Mailpit API
contact:
name: GitHub
url: https://github.com/axllent/mailpit
license:
name: MIT license
url: https://github.com/axllent/mailpit/blob/develop/LICENSE
version: "v1"
paths: {}
produces:
- application/json
schemes:
- http
swagger: "2.0"

83
server/apiv1/swagger.go Normal file

@ -0,0 +1,83 @@
package apiv1
// These structs are for the purpose of defining swagger HTTP responses
// Application information
// swagger:response InfoResponse
type infoResponse struct {
// Application information
Body appInformation
}
// 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
// Delete request
// swagger:model DeleteRequest
type deleteRequest struct {
// ids
// in:body
IDs []string `json:"ids"`
}
// Set read status request
// swagger:model SetReadStatusRequest
type setReadStatusRequest struct {
// Read status
Read bool `json:"read"`
// ids
// in:body
IDs []string `json:"ids"`
}
// Set tags request
// swagger:model SetTagsRequest
type setTagsRequest struct {
// Tags
// in:body
Tags []string `json:"tags"`
// ids
// in:body
IDs []string `json:"ids"`
}
// Binary data reponse inherits the attachment's content type
// swagger:response BinaryResponse
type binaryResponse struct {
// in: body
Body string
}
// Plain text response
// swagger:response TextResponse
type textResponse struct {
// in: body
Body string
}
// Error reponse
// swagger:response ErrorResponse
type errorResponse struct {
// The error message
// in: body
Body string
}
// Plain text "ok" reponse
// swagger:response OKResponse
type okResponse struct {
// Default reponse
// in: body
Body string
}

@ -24,6 +24,33 @@ var (
// 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
//
// # 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.
//
// Produces:
// - image/jpeg
//
// Schemes: http, https
//
// Parameters:
// + name: ID
// in: path
// description: message 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) vars := mux.Vars(r)
id := vars["id"] id := vars["id"]

@ -85,7 +85,7 @@ func defaultRoutes() *mux.Router {
r.HandleFunc(config.Webroot+"api/v1/message/{id}/part/{partID}", middleWareFunc(apiv1.DownloadAttachment)).Methods("GET") r.HandleFunc(config.Webroot+"api/v1/message/{id}/part/{partID}", middleWareFunc(apiv1.DownloadAttachment)).Methods("GET")
r.HandleFunc(config.Webroot+"api/v1/message/{id}/part/{partID}/thumb", middleWareFunc(apiv1.Thumbnail)).Methods("GET") r.HandleFunc(config.Webroot+"api/v1/message/{id}/part/{partID}/thumb", middleWareFunc(apiv1.Thumbnail)).Methods("GET")
r.HandleFunc(config.Webroot+"api/v1/message/{id}/raw", middleWareFunc(apiv1.DownloadRaw)).Methods("GET") r.HandleFunc(config.Webroot+"api/v1/message/{id}/raw", middleWareFunc(apiv1.DownloadRaw)).Methods("GET")
r.HandleFunc(config.Webroot+"api/v1/message/{id}/headers", middleWareFunc(apiv1.Headers)).Methods("GET") r.HandleFunc(config.Webroot+"api/v1/message/{id}/headers", middleWareFunc(apiv1.GetHeaders)).Methods("GET")
r.HandleFunc(config.Webroot+"api/v1/message/{id}", middleWareFunc(apiv1.GetMessage)).Methods("GET") r.HandleFunc(config.Webroot+"api/v1/message/{id}", middleWareFunc(apiv1.GetMessage)).Methods("GET")
r.HandleFunc(config.Webroot+"api/v1/info", middleWareFunc(apiv1.AppInfo)).Methods("GET") r.HandleFunc(config.Webroot+"api/v1/info", middleWareFunc(apiv1.AppInfo)).Methods("GET")

@ -128,9 +128,8 @@ export default {
self.start = response.data.start; self.start = response.data.start;
self.items = response.data.messages; self.items = response.data.messages;
self.tags = response.data.tags; self.tags = response.data.tags;
if (!self.existingTags.length) { self.existingTags = JSON.parse(JSON.stringify(self.tags));
self.existingTags = JSON.parse(JSON.stringify(self.tags));
}
// if pagination > 0 && results == 0 reload first page (prune) // if pagination > 0 && results == 0 reload first page (prune)
if (response.data.count == 0 && response.data.start > 0) { if (response.data.count == 0 && response.data.start > 0) {
self.start = 0; self.start = 0;
@ -543,6 +542,14 @@ export default {
self.appInfo = response.data; self.appInfo = response.data;
self.modal('AppInfoModal').show(); self.modal('AppInfoModal').show();
}); });
},
downloadMessageBody: function (str, ext) {
let dl = document.createElement('a');
dl.href = "data:text/plain," + encodeURIComponent(str);
dl.target = '_blank';
dl.download = this.message.ID + '.' + ext;
dl.click();
} }
} }
} }
@ -576,10 +583,49 @@ export default {
:href="'#' + messagePrev" title="View previous message"> :href="'#' + messagePrev" title="View previous message">
<i class="bi bi-caret-left-fill"></i> <i class="bi bi-caret-left-fill"></i>
</a> </a>
<a :href="'api/v1/message/' + message.ID + '/raw?dl=1'" class="btn btn-outline-light me-2 float-end" <div class="dropdown float-end" id="DownloadBtn">
title="Download message"> <button type="button" class="btn btn-outline-light dropdown-toggle" data-bs-toggle="dropdown"
<i class="bi bi-file-arrow-down-fill"></i> <span class="d-none d-md-inline">Download</span> aria-expanded="false">
</a> <i class="bi bi-file-arrow-down-fill"></i>
<span class="d-none d-md-inline ms-1">Download</span>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a :href="'api/v1/message/' + message.ID + '/raw?dl=1'" class="dropdown-item"
title="Message source including headers, body and attachments">
Raw message
</a>
</li>
<li v-if="message.HTML">
<button v-on:click="downloadMessageBody(message.HTML, 'html')" class="dropdown-item">
HTML body
</button>
</li>
<li v-if="message.Text">
<button v-on:click="downloadMessageBody(message.Text, 'txt')" class="dropdown-item">
Text body
</button>
</li>
<li v-if="allAttachments(message).length">
<hr class="dropdown-divider">
</li>
<li v-for="part in allAttachments(message)">
<a :href="'api/v1/message/' + message.ID + '/part/' + part.PartID" type="button"
class="row m-0 dropdown-item d-flex" target="_blank"
:title="part.FileName != '' ? part.FileName : '[ unknown ]'" style="min-width: 350px">
<div class="col-auto p-0 pe-1">
<i class="bi" :class="attachmentIcon(part)"></i>
</div>
<div class="col text-truncate p-0 pe-1">
{{ part.FileName != '' ? part.FileName : '[ unknown ]' }}
</div>
<div class="col-auto text-muted small p-0">
{{ getFileSize(part.Size) }}
</div>
</a>
</li>
</ul>
</div>
</div> </div>
<div class="col col-md-9 col-lg-5" v-if="!message"> <div class="col col-md-9 col-lg-5" v-if="!message">
@ -590,13 +636,13 @@ export default {
<span v-if="!total" class="ms-2">Mailpit</span> <span v-if="!total" class="ms-2">Mailpit</span>
</a> </a>
<div v-if="total" class="ms-md-2 d-flex bg-white border rounded-start flex-fill position-relative"> <div v-if="total" class="ms-md-2 d-flex bg-white border rounded-start flex-fill position-relative">
<input type="text" class="form-control border-0" v-model.trim="search" <input type="text" class="form-control border-0" v-model.trim="search" placeholder="Search mailbox">
placeholder="Search mailbox">
<span class="btn btn-link position-absolute end-0 text-muted" v-if="search" <span class="btn btn-link position-absolute end-0 text-muted" v-if="search"
v-on:click="resetSearch"><i class="bi bi-x-circle"></i></span> v-on:click="resetSearch"><i class="bi bi-x-circle"></i></span>
</div> </div>
<button v-if="total" class="btn btn-outline-light" type="submit"><i <button v-if="total" class="btn btn-outline-light" type="submit">
class="bi bi-search"></i></button> <i class="bi bi-search"></i>
</button>
</div> </div>
</form> </form>
</div> </div>
@ -618,6 +664,7 @@ export default {
<option value="100">100</option> <option value="100">100</option>
<option value="200">200</option> <option value="200">200</option>
</select> </select>
<span v-if="searching"> <span v-if="searching">
<b>{{ formatNumber(items.length) }} result<template v-if="items.length != 1">s</template></b> <b>{{ formatNumber(items.length) }} result<template v-if="items.length != 1">s</template></b>
</span> </span>
@ -626,8 +673,8 @@ export default {
{{ formatNumber(start + 1) }}-{{ formatNumber(start + items.length) }} <small>of</small> {{ formatNumber(start + 1) }}-{{ formatNumber(start + items.length) }} <small>of</small>
{{ formatNumber(total) }} {{ formatNumber(total) }}
</small> </small>
<button class="btn btn-outline-light ms-2 me-1" :disabled="!canPrev" v-on:click="viewPrev" <button class="btn btn-outline-light ms-2 me-1" :disabled="!canPrev" v-on:click="viewPrev" v-if="!searching"
v-if="!searching" :title="'View previous ' + limit + ' messages'"> :title="'View previous ' + limit + ' messages'">
<i class="bi bi-caret-left-fill"></i> <i class="bi bi-caret-left-fill"></i>
</button> </button>
<button class="btn btn-outline-light" :disabled="!canNext" v-on:click="viewNext" v-if="!searching" <button class="btn btn-outline-light" :disabled="!canNext" v-on:click="viewNext" v-if="!searching"
@ -679,16 +726,16 @@ export default {
<button class="list-group-item list-group-item-action" :disabled="!selectedHasUnread()" <button class="list-group-item list-group-item-action" :disabled="!selectedHasUnread()"
v-on:click="markSelectedRead"> v-on:click="markSelectedRead">
<i class="bi bi-eye-fill"></i> <i class="bi bi-eye-fill"></i>
Mark selected read Mark read
</button> </button>
<button class="list-group-item list-group-item-action" :disabled="!selectedHasRead()" <button class="list-group-item list-group-item-action" :disabled="!selectedHasRead()"
v-on:click="markSelectedUnread"> v-on:click="markSelectedUnread">
<i class="bi bi-eye-slash"></i> <i class="bi bi-eye-slash"></i>
Mark selected unread Mark unread
</button> </button>
<button class="list-group-item list-group-item-action" v-on:click="deleteMessages"> <button class="list-group-item list-group-item-action" v-on:click="deleteMessages">
<i class="bi bi-trash-fill me-1 text-danger"></i> <i class="bi bi-trash-fill me-1 text-danger"></i>
Delete selected Delete
</button> </button>
<button class="list-group-item list-group-item-action" v-on:click="selected = []"> <button class="list-group-item list-group-item-action" v-on:click="selected = []">
<i class="bi bi-x-circle me-1"></i> <i class="bi bi-x-circle me-1"></i>
@ -735,13 +782,13 @@ export default {
<div class="text-truncate d-lg-none privacy"> <div class="text-truncate d-lg-none privacy">
<span v-if="message.From" :title="message.From.Address">{{ <span v-if="message.From" :title="message.From.Address">{{
message.From.Name ? message.From.Name ?
message.From.Name : message.From.Address message.From.Name : message.From.Address
}}</span> }}</span>
</div> </div>
<div class="text-truncate d-none d-lg-block privacy"> <div class="text-truncate d-none d-lg-block privacy">
<b v-if="message.From" :title="message.From.Address">{{ <b v-if="message.From" :title="message.From.Address">{{
message.From.Name ? message.From.Name ?
message.From.Name : message.From.Address message.From.Name : message.From.Address
}}</b> }}</b>
</div> </div>
<div class="d-none d-lg-block text-truncate text-muted small privacy"> <div class="d-none d-lg-block text-truncate text-muted small privacy">
@ -810,8 +857,7 @@ export default {
</div> </div>
<!-- Modal --> <!-- Modal -->
<div class="modal fade" id="MarkAllReadModal" tabindex="-1" aria-labelledby="MarkAllReadModalLabel" <div class="modal fade" id="MarkAllReadModal" tabindex="-1" aria-labelledby="MarkAllReadModalLabel" aria-hidden="true">
aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
@ -874,6 +920,12 @@ export default {
</a> </a>
<div class="row g-3"> <div class="row g-3">
<div class="col-12">
<a class="btn btn-primary w-100" href="api/v1/" target="_blank">
<i class="bi bi-braces"></i>
OpenAPI / Swagger API documentation
</a>
</div>
<div class="col-sm-6"> <div class="col-sm-6">
<a class="btn btn-primary w-100" href="https://github.com/axllent/mailpit" target="_blank"> <a class="btn btn-primary w-100" href="https://github.com/axllent/mailpit" target="_blank">
<i class="bi bi-github"></i> <i class="bi bi-github"></i>
@ -882,8 +934,7 @@ export default {
</a> </a>
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<a class="btn btn-primary w-100" href="https://github.com/axllent/mailpit/wiki" <a class="btn btn-primary w-100" href="https://github.com/axllent/mailpit/wiki" target="_blank">
target="_blank">
Documentation Documentation
<i class="bi bi-box-arrow-up-right"></i> <i class="bi bi-box-arrow-up-right"></i>
</a> </a>

@ -149,10 +149,6 @@ body.blur {
} }
} }
// .tag.active {
// font-weight: bold;
// }
.form-select.tag-selector { .form-select.tag-selector {
display: none; display: none;
} }
@ -166,6 +162,17 @@ body.blur {
} }
} }
#DownloadBtn {
@include media-breakpoint-down(sm) {
position: static;
.dropdown-menu {
left: 0;
right: 0;
}
}
}
/* PrismJS 1.29.0 - modified! /* PrismJS 1.29.0 - modified!
https://prismjs.com/download.html#themes=prism-coy&languages=markup+css */ https://prismjs.com/download.html#themes=prism-coy&languages=markup+css */
code[class*="language-"], code[class*="language-"],

1
server/ui-src/docs.js Normal file

@ -0,0 +1 @@
import "rapidoc";

@ -2,8 +2,8 @@
<script> <script>
import commonMixins from '../mixins.js'; import commonMixins from '../mixins.js';
import Prism from "prismjs"; import Prism from "prismjs";
import Tags from "bootstrap5-tags";
import Attachments from './Attachments.vue'; import Attachments from './Attachments.vue';
import MessageTags from './MessageTags.vue';
export default { export default {
props: { props: {
@ -12,8 +12,7 @@ export default {
}, },
components: { components: {
Attachments, Attachments
MessageTags
}, },
mixins: [commonMixins], mixins: [commonMixins],
@ -22,36 +21,54 @@ export default {
return { return {
srcURI: false, srcURI: false,
iframes: [], // for resizing iframes: [], // for resizing
tagComponent: false, // to force rerendering of component showTags: false, // to force rerendering of component
messageTags: [],
allTags: [],
} }
}, },
watch: { watch: {
message: { message: {
handler(newQuestion) { handler() {
let self = this; let self = this;
self.tagComponent = false; self.showTags = false;
self.messageTags = self.message.Tags;
self.allTags = self.existingTags;
// delay to select first tab and add HTML highlighting (prev/next) // delay to select first tab and add HTML highlighting (prev/next)
self.$nextTick(function () { self.$nextTick(function () {
self.renderUI(); self.renderUI();
self.tagComponent = true; self.showTags = true;
self.$nextTick(function () {
Tags.init("select[multiple]");
});
}); });
}, },
// force eager callback execution // force eager callback execution
immediate: true immediate: true
},
messageTags() {
// save changed to tags
if (this.showTags) {
this.saveTags();
}
} }
}, },
mounted() { mounted() {
let self = this; let self = this;
self.tagComponent = false; self.showTags = false;
self.allTags = self.existingTags;
window.addEventListener("resize", self.resizeIframes); window.addEventListener("resize", self.resizeIframes);
self.renderUI(); self.renderUI();
var tabEl = document.getElementById('nav-raw-tab'); var tabEl = document.getElementById('nav-raw-tab');
tabEl.addEventListener('shown.bs.tab', function (event) { tabEl.addEventListener('shown.bs.tab', function (event) {
self.srcURI = 'api/v1/message/' + self.message.ID + '/raw'; self.srcURI = 'api/v1/message/' + self.message.ID + '/raw';
}); });
self.tagComponent = true;
self.showTags = true;
self.$nextTick(function () {
Tags.init("select[multiple]");
});
}, },
unmounted: function () { unmounted: function () {
@ -105,6 +122,20 @@ export default {
if (s) { if (s) {
s.style.height = s.contentWindow.document.body.scrollHeight + 50 + 'px'; s.style.height = s.contentWindow.document.body.scrollHeight + 50 + 'px';
} }
},
saveTags: function () {
let self = this;
var data = {
ids: [this.message.ID],
tags: this.messageTags
}
self.put('api/v1/tags', data, function (response) {
self.scrollInPlace = true;
self.$emit('loadMessages');
});
} }
} }
} }
@ -162,31 +193,30 @@ export default {
<th class="small">Date</th> <th class="small">Date</th>
<td>{{ messageDate(message.Date) }}</td> <td>{{ messageDate(message.Date) }}</td>
</tr> </tr>
<MessageTags :message="message" :existingTags="existingTags"
@load-messages="$emit('loadMessages')" v-if="tagComponent"> <tr class="small" v-if="showTags">
</MessageTags> <th>Tags</th>
<td>
<select class="form-select small tag-selector" v-model="messageTags" multiple
data-allow-new="true" data-clear-end="true" data-allow-clear="true"
data-placeholder="Add tags..." data-badge-style="secondary"
data-regex="^([a-zA-Z0-9\-\ \_]){3,}$" data-separator="|,|">
<option value="">Type a tag...</option>
<!-- you need at least one option with the placeholder -->
<option v-for="t in allTags" :value="t">{{ t }}</option>
</select>
<div class="invalid-feedback">Please select a valid tag.</div>
</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="col-md-auto text-md-end mt-md-3"> <div class="col-md-auto d-none d-md-block text-end mt-md-3">
<!-- <p class="text-muted small d-none d-md-block mb-2"><small>{{ messageDate(message.Date) }}</small></p> <div class="mt-2 mt-md-0" v-if="allAttachments(message)">
<p class="text-muted small d-none d-md-block"><small>Size: {{ getFileSize(message.Size) }}</small></p> --> <span class="badge rounded-pill text-bg-secondary p-2">
<div class="dropdown mt-2 mt-md-0" v-if="allAttachments(message)">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown"
aria-expanded="false">
Attachment<span v-if="allAttachments(message).length > 1">s</span> Attachment<span v-if="allAttachments(message).length > 1">s</span>
({{ allAttachments(message).length }}) ({{ allAttachments(message).length }})
</button> </span>
<ul class="dropdown-menu">
<li v-for="part in allAttachments(message)">
<a :href="'api/v1/message/' + message.ID + '/part/' + part.PartID" type="button"
class="dropdown-item" target="_blank">
<i class="bi" :class="attachmentIcon(part)"></i>
{{ part.FileName != '' ? part.FileName : '[ unknown ]' }}
<small class="text-muted ms-2">{{ getFileSize(part.Size) }}</small>
</a>
</li>
</ul>
</div> </div>
</div> </div>
</div> </div>
@ -196,8 +226,8 @@ export default {
<button class="nav-link" id="nav-html-tab" data-bs-toggle="tab" data-bs-target="#nav-html" type="button" <button class="nav-link" id="nav-html-tab" data-bs-toggle="tab" data-bs-target="#nav-html" type="button"
role="tab" aria-controls="nav-html" aria-selected="true" v-if="message.HTML">HTML</button> role="tab" aria-controls="nav-html" aria-selected="true" v-if="message.HTML">HTML</button>
<button class="nav-link" id="nav-html-source-tab" data-bs-toggle="tab" data-bs-target="#nav-html-source" <button class="nav-link" id="nav-html-source-tab" data-bs-toggle="tab" data-bs-target="#nav-html-source"
type="button" role="tab" aria-controls="nav-html-source" aria-selected="false" type="button" role="tab" aria-controls="nav-html-source" aria-selected="false" v-if="message.HTML">HTML
v-if="message.HTML">HTML Source</button> Source</button>
<button class="nav-link" id="nav-plain-text-tab" data-bs-toggle="tab" data-bs-target="#nav-plain-text" <button class="nav-link" id="nav-plain-text-tab" data-bs-toggle="tab" data-bs-target="#nav-plain-text"
type="button" role="tab" aria-controls="nav-plain-text" aria-selected="false" type="button" role="tab" aria-controls="nav-plain-text" aria-selected="false"
:class="message.HTML == '' ? 'show' : ''">Text</button> :class="message.HTML == '' ? 'show' : ''">Text</button>
@ -208,8 +238,8 @@ export default {
<div class="tab-content mb-5" id="nav-tabContent"> <div class="tab-content mb-5" id="nav-tabContent">
<div v-if="message.HTML != ''" class="tab-pane fade show" id="nav-html" role="tabpanel" <div v-if="message.HTML != ''" class="tab-pane fade show" id="nav-html" role="tabpanel"
aria-labelledby="nav-html-tab" tabindex="0"> aria-labelledby="nav-html-tab" tabindex="0">
<iframe target-blank="" class="tab-pane" id="preview-html" :srcdoc="message.HTML" <iframe target-blank="" class="tab-pane" id="preview-html" :srcdoc="message.HTML" v-on:load="resizeIframe"
v-on:load="resizeIframe" seamless frameborder="0" style="width: 100%; height: 100%;"> seamless frameborder="0" style="width: 100%; height: 100%;">
</iframe> </iframe>
<Attachments v-if="allAttachments(message).length" :message="message" <Attachments v-if="allAttachments(message).length" :message="message"
:attachments="allAttachments(message)"></Attachments> :attachments="allAttachments(message)"></Attachments>
@ -218,8 +248,8 @@ export default {
tabindex="0" v-if="message.HTML"> tabindex="0" v-if="message.HTML">
<pre><code class="language-html">{{ message.HTML }}</code></pre> <pre><code class="language-html">{{ message.HTML }}</code></pre>
</div> </div>
<div class="tab-pane fade" id="nav-plain-text" role="tabpanel" aria-labelledby="nav-plain-text-tab" <div class="tab-pane fade" id="nav-plain-text" role="tabpanel" aria-labelledby="nav-plain-text-tab" tabindex="0"
tabindex="0" :class="message.HTML == '' ? 'show' : ''"> :class="message.HTML == '' ? 'show' : ''">
<div class="text-view">{{ message.Text }}</div> <div class="text-view">{{ message.Text }}</div>
<Attachments v-if="allAttachments(message).length" :message="message" <Attachments v-if="allAttachments(message).length" :message="message"
:attachments="allAttachments(message)"></Attachments> :attachments="allAttachments(message)"></Attachments>

@ -1,72 +0,0 @@
<script>
import commonMixins from '../mixins.js';
import Tags from "bootstrap5-tags";
export default {
props: {
message: Object,
existingTags: Array
},
mixins: [commonMixins],
data() {
return {
messageTags: [],
}
},
mounted() {
let self = this;
self.loaded = false;
self.messageTags = self.message.Tags;
// delay until vue has rendered
self.$nextTick(function () {
Tags.init("select[multiple]");
self.$nextTick(function () {
self.loaded = true;
});
});
},
watch: {
messageTags() {
if (this.loaded) {
this.saveTags();
}
}
},
methods: {
saveTags: function () {
let self = this;
var data = {
ids: [this.message.ID],
tags: this.messageTags
}
self.put('api/v1/tags', data, function (response) {
self.scrollInPlace = true;
self.$emit('loadMessages');
});
}
}
}
</script>
<template>
<tr class="small">
<th>Tags</th>
<td>
<select class="form-select small tag-selector" v-model="messageTags" multiple data-allow-new="true"
data-clear-end="true" data-allow-clear="true" data-placeholder="Add tags..."
data-badge-style="secondary" data-regex="^([a-zA-Z0-9\-\ \_]){3,}$" data-separator="|,|">
<option value="">Type a tag...</option><!-- you need at least one option with the placeholder -->
<option v-for="t in existingTags" :value="t">{{ t }}</option>
</select>
<div class="invalid-feedback">Please select a valid tag.</div>
</td>
</tr>
</template>

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Mailpit API v1 documentation</title>
<meta name="referrer" content="no-referrer">
<meta name="robots" content="noindex, nofollow, noarchive">
<link rel="icon" href="../../favicon.svg">
<script src="../../dist/docs.js"></script>
</head>
<body>
<rapi-doc id="thedoc" spec-url="swagger.json" theme="light" layout="column" render-style="read" load-fonts="false"
regular-font="system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', 'Noto Sans', 'Liberation Sans', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'"
mono-font="Courier New, Courier, System, fixed-width" font-size="large" allow-spec-url-load="false"
allow-spec-file-load="false" allow-server-selection="false" allow-search="false" allow-advanced-search="false"
bg-color="#ffffff" nav-bg-color="#e3e8ec" nav-text-color="#212529" nav-hover-bg-color="#fff"
header-color="#2c3e50" primary-color="#2c3e50" text-color="#212529">
<div slot="header">Mailpit API v1 documentation</div>
<a target='_blank' href="../../" slot="logo">
<img src="../../mailpit.svg" width="40" alt="Mailpit" />
</a>
</rapi-doc>
</body>
</html>

@ -0,0 +1,820 @@
{
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"schemes": [
"http"
],
"swagger": "2.0",
"info": {
"description": "OpenAPI 2.0 documentation for [Mailpit](https://github.com/axllent/mailpit).",
"title": "Mailpit API",
"contact": {
"name": "GitHub",
"url": "https://github.com/axllent/mailpit"
},
"license": {
"name": "MIT license",
"url": "https://github.com/axllent/mailpit/blob/develop/LICENSE"
},
"version": "v1"
},
"paths": {
"/api/v1/info": {
"get": {
"description": "Returns basic runtime information, message totals and latest release version.",
"produces": [
"application/octet-stream"
],
"schemes": [
"http",
"https"
],
"tags": [
"application"
],
"summary": "Get the application information",
"operationId": "AppInformation",
"responses": {
"200": {
"$ref": "#/responses/InfoResponse"
},
"default": {
"$ref": "#/responses/ErrorResponse"
}
}
}
},
"/api/v1/message/{ID}": {
"get": {
"description": "Returns the summary of a message, marking the message as read.",
"produces": [
"application/json"
],
"schemes": [
"http",
"https"
],
"tags": [
"message"
],
"summary": "Get message summary",
"operationId": "Message",
"parameters": [
{
"type": "string",
"description": "message id",
"name": "ID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Message",
"schema": {
"$ref": "#/definitions/Message"
}
},
"default": {
"$ref": "#/responses/ErrorResponse"
}
}
}
},
"/api/v1/message/{ID}/headers": {
"get": {
"description": "Returns the message headers as an array.",
"produces": [
"application/json"
],
"schemes": [
"http",
"https"
],
"tags": [
"message"
],
"summary": "Get message headers",
"operationId": "Headers",
"parameters": [
{
"type": "string",
"description": "message id",
"name": "ID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "MessageHeaders",
"schema": {
"$ref": "#/definitions/MessageHeaders"
}
},
"default": {
"$ref": "#/responses/ErrorResponse"
}
}
}
},
"/api/v1/message/{ID}/part/{PartID}": {
"get": {
"description": "This will return the attachment part using the appropriate Content-Type.",
"produces": [
"application/*",
"image/*",
"text/*"
],
"schemes": [
"http",
"https"
],
"tags": [
"message"
],
"summary": "Get message attachment",
"operationId": "Attachment",
"parameters": [
{
"type": "string",
"description": "message id",
"name": "ID",
"in": "path",
"required": true
},
{
"type": "string",
"description": "attachment part id",
"name": "PartID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/responses/BinaryResponse"
},
"default": {
"$ref": "#/responses/ErrorResponse"
}
}
}
},
"/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.",
"produces": [
"image/jpeg"
],
"schemes": [
"http",
"https"
],
"tags": [
"message"
],
"summary": "Get an attachment image thumbnail",
"operationId": "Thumbnail",
"parameters": [
{
"type": "string",
"description": "message id",
"name": "ID",
"in": "path",
"required": true
},
{
"type": "string",
"description": "attachment part id",
"name": "PartID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/responses/BinaryResponse"
},
"default": {
"$ref": "#/responses/ErrorResponse"
}
}
}
},
"/api/v1/message/{ID}/raw": {
"get": {
"description": "Returns the full email source as plain text.",
"produces": [
"text/plain"
],
"schemes": [
"http",
"https"
],
"tags": [
"message"
],
"summary": "Get message source",
"operationId": "Raw",
"parameters": [
{
"type": "string",
"description": "message id",
"name": "ID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/responses/TextResponse"
},
"default": {
"$ref": "#/responses/ErrorResponse"
}
}
}
},
"/api/v1/messages": {
"get": {
"description": "Returns messages from the mailbox ordered from newest to oldest.",
"produces": [
"application/json"
],
"schemes": [
"http",
"https"
],
"tags": [
"messages"
],
"summary": "List messages",
"operationId": "GetMessages",
"parameters": [
{
"type": "integer",
"default": 0,
"description": "pagination offset",
"name": "start",
"in": "query"
},
{
"type": "integer",
"default": 50,
"description": "limit results",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"$ref": "#/responses/MessagesSummaryResponse"
},
"default": {
"$ref": "#/responses/ErrorResponse"
}
}
},
"put": {
"description": "If no IDs are provided then all messages are updated.",
"consumes": [
"application/json"
],
"produces": [
"text/plain"
],
"schemes": [
"http",
"https"
],
"tags": [
"messages"
],
"summary": "Set read status",
"operationId": "SetReadStatus",
"parameters": [
{
"description": "Message ids to update",
"name": "ids",
"in": "body",
"schema": {
"description": "Message ids to update",
"type": "object",
"$ref": "#/definitions/SetReadStatusRequest"
}
}
],
"responses": {
"200": {
"$ref": "#/responses/OKResponse"
},
"default": {
"$ref": "#/responses/ErrorResponse"
}
}
},
"delete": {
"description": "If no IDs are provided then all messages are deleted.",
"consumes": [
"application/json"
],
"produces": [
"text/plain"
],
"schemes": [
"http",
"https"
],
"tags": [
"messages"
],
"summary": "Delete messages",
"operationId": "Delete",
"parameters": [
{
"description": "Message ids to delete",
"name": "ids",
"in": "body",
"schema": {
"description": "Message ids to delete",
"type": "object",
"$ref": "#/definitions/DeleteRequest"
}
}
],
"responses": {
"200": {
"$ref": "#/responses/OKResponse"
},
"default": {
"$ref": "#/responses/ErrorResponse"
}
}
}
},
"/api/v1/search": {
"get": {
"description": "Returns the latest messages matching a search.",
"produces": [
"application/json"
],
"schemes": [
"http",
"https"
],
"tags": [
"messages"
],
"summary": "Search messages",
"operationId": "MessagesSummary",
"parameters": [
{
"type": "string",
"description": "search query",
"name": "query",
"in": "query",
"required": true
},
{
"type": "integer",
"default": 50,
"description": "limit results",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"$ref": "#/responses/MessagesSummaryResponse"
},
"default": {
"$ref": "#/responses/ErrorResponse"
}
}
}
},
"/api/v1/tags": {
"put": {
"description": "To remove all tags from a message, pass an empty tags array.",
"consumes": [
"application/json"
],
"produces": [
"text/plain"
],
"schemes": [
"http",
"https"
],
"tags": [
"tags"
],
"summary": "Set message tags",
"operationId": "SetTags",
"parameters": [
{
"description": "Message ids to update",
"name": "ids",
"in": "body",
"required": true,
"schema": {
"description": "Message ids to update",
"type": "object",
"$ref": "#/definitions/SetTagsRequest"
}
}
],
"responses": {
"200": {
"$ref": "#/responses/OKResponse"
},
"default": {
"$ref": "#/responses/ErrorResponse"
}
}
}
}
},
"definitions": {
"Address": {
"description": "An address such as \"Barry Gibbs \u003cbg@example.com\u003e\" is represented\nas Address{Name: \"Barry Gibbs\", Address: \"bg@example.com\"}.",
"type": "object",
"title": "Address represents a single mail address.",
"properties": {
"Address": {
"type": "string"
},
"Name": {
"type": "string"
}
},
"x-go-package": "net/mail"
},
"AppInformation": {
"description": "Response includes the current and latest Mailpit versions, database info, and memory usage",
"type": "object",
"properties": {
"Database": {
"description": "Database path",
"type": "string"
},
"DatabaseSize": {
"description": "Database size in bytes",
"type": "integer",
"format": "int64"
},
"LatestVersion": {
"description": "Latest Mailpit version",
"type": "string"
},
"Memory": {
"description": "Current memory usage in bytes",
"type": "integer",
"format": "uint64"
},
"Messages": {
"description": "Total number of messages in the database",
"type": "integer",
"format": "int64"
},
"Version": {
"description": "Current Mailpit version",
"type": "string"
}
},
"x-go-name": "appInformation",
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
},
"Attachment": {
"description": "Attachment struct for inline and attachments",
"type": "object",
"properties": {
"ContentID": {
"description": "content id",
"type": "string"
},
"ContentType": {
"description": "content type",
"type": "string"
},
"FileName": {
"description": "file name",
"type": "string"
},
"PartID": {
"description": "attachment part id",
"type": "string"
},
"Size": {
"description": "size in bytes",
"type": "integer",
"format": "int64"
}
},
"x-go-package": "github.com/axllent/mailpit/storage"
},
"DeleteRequest": {
"description": "Delete request",
"type": "object",
"properties": {
"ids": {
"description": "ids\nin:body",
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "IDs"
}
},
"x-go-name": "deleteRequest",
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
},
"Message": {
"description": "Message data excluding physical attachments",
"type": "object",
"properties": {
"Attachments": {
"description": "Message attachments",
"type": "array",
"items": {
"$ref": "#/definitions/Attachment"
}
},
"Bcc": {
"description": "Bcc addresses",
"type": "array",
"items": {
"$ref": "#/definitions/Address"
}
},
"Cc": {
"description": "Cc addresses",
"type": "array",
"items": {
"$ref": "#/definitions/Address"
}
},
"Date": {
"description": "Message date if set, else date received",
"type": "string",
"format": "date-time"
},
"From": {
"$ref": "#/definitions/Address"
},
"HTML": {
"description": "Message body HTML",
"type": "string"
},
"ID": {
"description": "Unique message database id",
"type": "string"
},
"Inline": {
"description": "Inline message attachments",
"type": "array",
"items": {
"$ref": "#/definitions/Attachment"
}
},
"Read": {
"description": "Read status",
"type": "boolean"
},
"Size": {
"description": "Message size in bytes",
"type": "integer",
"format": "int64"
},
"Subject": {
"description": "Message subject",
"type": "string"
},
"Tags": {
"description": "Message tags",
"type": "array",
"items": {
"type": "string"
}
},
"Text": {
"description": "Message body text",
"type": "string"
},
"To": {
"description": "To addresses",
"type": "array",
"items": {
"$ref": "#/definitions/Address"
}
}
},
"x-go-package": "github.com/axllent/mailpit/storage"
},
"MessageHeaders": {
"description": "Message headers",
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
},
"x-go-name": "messageHeaders",
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
},
"MessageSummary": {
"description": "MessageSummary struct for frontend messages",
"type": "object",
"properties": {
"Attachments": {
"description": "Whether the message has any attachments",
"type": "integer",
"format": "int64"
},
"Bcc": {
"description": "Bcc addresses",
"type": "array",
"items": {
"$ref": "#/definitions/Address"
}
},
"Cc": {
"description": "Cc addresses",
"type": "array",
"items": {
"$ref": "#/definitions/Address"
}
},
"Created": {
"description": "Created time",
"type": "string",
"format": "date-time"
},
"From": {
"$ref": "#/definitions/Address"
},
"ID": {
"description": "Unique message database id",
"type": "string"
},
"Read": {
"description": "Read status",
"type": "boolean"
},
"Size": {
"description": "Message size in bytes (total)",
"type": "integer",
"format": "int64"
},
"Subject": {
"description": "Email subject",
"type": "string"
},
"Tags": {
"description": "Message tags",
"type": "array",
"items": {
"type": "string"
}
},
"To": {
"description": "To address",
"type": "array",
"items": {
"$ref": "#/definitions/Address"
}
}
},
"x-go-package": "github.com/axllent/mailpit/storage"
},
"MessagesSummary": {
"description": "MessagesSummary is a summary of a list of messages",
"type": "object",
"properties": {
"count": {
"description": "Number of results returned",
"type": "integer",
"format": "int64",
"x-go-name": "Count"
},
"messages": {
"description": "Messages summary\nin:body",
"type": "array",
"items": {
"$ref": "#/definitions/MessageSummary"
},
"x-go-name": "Messages"
},
"start": {
"description": "Pagination offset",
"type": "integer",
"format": "int64",
"x-go-name": "Start"
},
"tags": {
"description": "All current tags",
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "Tags"
},
"total": {
"description": "Total number of messages in mailbox",
"type": "integer",
"format": "int64",
"x-go-name": "Total"
},
"unread": {
"description": "Total number of unread messages in mailbox",
"type": "integer",
"format": "int64",
"x-go-name": "Unread"
}
},
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
},
"SetReadStatusRequest": {
"description": "Set read status request",
"type": "object",
"properties": {
"ids": {
"description": "ids\nin:body",
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "IDs"
},
"read": {
"description": "Read status",
"type": "boolean",
"x-go-name": "Read"
}
},
"x-go-name": "setReadStatusRequest",
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
},
"SetTagsRequest": {
"description": "Set tags request",
"type": "object",
"properties": {
"ids": {
"description": "ids\nin:body",
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "IDs"
},
"tags": {
"description": "Tags\nin:body",
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "Tags"
}
},
"x-go-name": "setTagsRequest",
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
}
},
"responses": {
"BinaryResponse": {
"description": "Binary data reponse inherits the attachment's content type"
},
"ErrorResponse": {
"description": "Error reponse"
},
"InfoResponse": {
"description": "Application information",
"schema": {
"$ref": "#/definitions/AppInformation"
},
"headers": {
"Body": {
"description": "Application information"
}
}
},
"MessagesSummaryResponse": {
"description": "Message summary",
"schema": {
"$ref": "#/definitions/MessagesSummary"
}
},
"OKResponse": {
"description": "Plain text \"ok\" reponse"
},
"TextResponse": {
"description": "Plain text response"
}
}
}

@ -7,45 +7,81 @@ import (
"github.com/jhillyerd/enmime" "github.com/jhillyerd/enmime"
) )
// Message struct for loading messages. It does not include physical attachments. // Message data excluding physical attachments
//
// swagger:model Message
type Message struct { type Message struct {
ID string // Unique message database id
Read bool ID string
From *mail.Address // Read status
To []*mail.Address Read bool
Cc []*mail.Address // From address
Bcc []*mail.Address From *mail.Address
Subject string // To addresses
Date time.Time To []*mail.Address
Tags []string // Cc addresses
Text string Cc []*mail.Address
HTML string // Bcc addresses
Size int Bcc []*mail.Address
Inline []Attachment // Message subject
Subject string
// Message date if set, else date received
Date time.Time
// Message tags
Tags []string
// Message body text
Text string
// Message body HTML
HTML string
// Message size in bytes
Size int
// Inline message attachments
Inline []Attachment
// Message attachments
Attachments []Attachment Attachments []Attachment
} }
// Attachment struct for inline and attachments // Attachment struct for inline and attachments
//
// swagger:model Attachment
type Attachment struct { type Attachment struct {
PartID string // attachment part id
FileName string PartID string
// file name
FileName string
// content type
ContentType string ContentType string
ContentID string // content id
Size int ContentID string
// size in bytes
Size int
} }
// MessageSummary struct for frontend messages // MessageSummary struct for frontend messages
//
// swagger:model MessageSummary
type MessageSummary struct { type MessageSummary struct {
ID string // Unique message database id
Read bool ID string
From *mail.Address // Read status
To []*mail.Address Read bool
Cc []*mail.Address // From address
Bcc []*mail.Address From *mail.Address
Subject string // To address
Created time.Time To []*mail.Address
Tags []string // Cc addresses
Size int Cc []*mail.Address
// Bcc addresses
Bcc []*mail.Address
// Email subject
Subject string
// Created time
Created time.Time
// Message tags
Tags []string
// Message size in bytes (total)
Size int
// Whether the message has any attachments
Attachments int Attachments int
} }