1
0
mirror of https://github.com/axllent/mailpit.git synced 2025-02-01 13:08:02 +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

View File

@ -2,6 +2,22 @@
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]
### API
@ -427,6 +443,3 @@ This release includes a major backend storage change (SQLite) that will render a
### Feature
- Unread statistics

View File

@ -6,7 +6,7 @@
![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)
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.

View File

@ -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.
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:
- [Messages](Messages.md) - Listing, deleting & marking messages as read/unread.

View File

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

2717
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -16,6 +16,34 @@ import (
// 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, limit := getStartLimit(r)
messages, err := storage.List(start, limit)
@ -40,11 +68,38 @@ func GetMessages(w http.ResponseWriter, r *http.Request) {
_, _ = 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) {
// 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"))
if search == "" {
fourOFour(w)
httpError(w, "Error: no search query")
return
}
@ -72,15 +127,37 @@ func Search(w http.ResponseWriter, r *http.Request) {
_, _ = 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) {
// 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)
id := vars["id"]
msg, err := storage.GetMessage(id)
if err != nil {
httpError(w, "Message not found")
fourOFour(w)
return
}
@ -91,6 +168,35 @@ func GetMessage(w http.ResponseWriter, r *http.Request) {
// 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 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"]
@ -98,7 +204,7 @@ func DownloadAttachment(w http.ResponseWriter, r *http.Request) {
a, err := storage.GetAttachmentPart(id, partID)
if err != nil {
httpError(w, err.Error())
fourOFour(w)
return
}
fileName := a.FileName
@ -111,15 +217,37 @@ func DownloadAttachment(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(a.Content)
}
// Headers (method: GET) returns the message headers as JSON
func Headers(w http.ResponseWriter, r *http.Request) {
// 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.
//
// 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)
id := vars["id"]
data, err := storage.GetMessageRaw(id)
if err != nil {
httpError(w, err.Error())
fourOFour(w)
return
}
@ -130,8 +258,7 @@ func Headers(w http.ResponseWriter, r *http.Request) {
return
}
headers := m.Header
bytes, _ := json.Marshal(headers)
bytes, _ := json.Marshal(m.Header)
w.Header().Add("Content-Type", "application/json")
_, _ = 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
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)
id := vars["id"]
@ -147,7 +296,7 @@ func DownloadRaw(w http.ResponseWriter, r *http.Request) {
data, err := storage.GetMessageRaw(id)
if err != nil {
httpError(w, err.Error())
fourOFour(w)
return
}
@ -159,8 +308,32 @@ func DownloadRaw(w http.ResponseWriter, r *http.Request) {
}
// 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) {
// 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)
var data struct {
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
// 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
//
// Parameters:
// + name: ids
// in: body
// description: Message ids to update
// required: false
// type: SetReadStatusRequest
//
// Responses:
// 200: OKResponse
// default: ErrorResponse
decoder := json.NewDecoder(r.Body)
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
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)
var data struct {

View File

@ -11,19 +11,41 @@ import (
"github.com/axllent/mailpit/utils/updater"
)
type appVersion struct {
Version string
// Response includes the current and latest Mailpit versions, database info, and memory usage
//
// swagger:model AppInformation
type appInformation struct {
// Current Mailpit version
Version string
// Latest Mailpit version
LatestVersion string
Database string
DatabaseSize int64
Messages int
Memory uint64
// Database path
Database string
// Database size in bytes
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.
func AppInfo(w http.ResponseWriter, r *http.Request) {
info := appVersion{}
// swagger:route GET /api/v1/info application AppInformation
//
// # 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
var m runtime.MemStats

View File

@ -1,6 +1,30 @@
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
// and understanding of the JSON structure.
@ -8,16 +32,6 @@ import "github.com/axllent/mailpit/storage"
// MessageSummary - summary of a single message
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
type Message = storage.Message

View File

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

View File

@ -24,6 +24,33 @@ var (
// 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
//
// # 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)
id := vars["id"]

View File

@ -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}/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}/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/info", middleWareFunc(apiv1.AppInfo)).Methods("GET")

View File

@ -128,9 +128,8 @@ export default {
self.start = response.data.start;
self.items = response.data.messages;
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 (response.data.count == 0 && response.data.start > 0) {
self.start = 0;
@ -543,6 +542,14 @@ export default {
self.appInfo = response.data;
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">
<i class="bi bi-caret-left-fill"></i>
</a>
<a :href="'api/v1/message/' + message.ID + '/raw?dl=1'" class="btn btn-outline-light me-2 float-end"
title="Download message">
<i class="bi bi-file-arrow-down-fill"></i> <span class="d-none d-md-inline">Download</span>
</a>
<div class="dropdown float-end" id="DownloadBtn">
<button type="button" class="btn btn-outline-light dropdown-toggle" data-bs-toggle="dropdown"
aria-expanded="false">
<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 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>
</a>
<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"
placeholder="Search mailbox">
<input type="text" class="form-control border-0" v-model.trim="search" placeholder="Search mailbox">
<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>
</div>
<button v-if="total" class="btn btn-outline-light" type="submit"><i
class="bi bi-search"></i></button>
<button v-if="total" class="btn btn-outline-light" type="submit">
<i class="bi bi-search"></i>
</button>
</div>
</form>
</div>
@ -618,6 +664,7 @@ export default {
<option value="100">100</option>
<option value="200">200</option>
</select>
<span v-if="searching">
<b>{{ formatNumber(items.length) }} result<template v-if="items.length != 1">s</template></b>
</span>
@ -626,8 +673,8 @@ export default {
{{ formatNumber(start + 1) }}-{{ formatNumber(start + items.length) }} <small>of</small>
{{ formatNumber(total) }}
</small>
<button class="btn btn-outline-light ms-2 me-1" :disabled="!canPrev" v-on:click="viewPrev"
v-if="!searching" :title="'View previous ' + limit + ' messages'">
<button class="btn btn-outline-light ms-2 me-1" :disabled="!canPrev" v-on:click="viewPrev" v-if="!searching"
:title="'View previous ' + limit + ' messages'">
<i class="bi bi-caret-left-fill"></i>
</button>
<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()"
v-on:click="markSelectedRead">
<i class="bi bi-eye-fill"></i>
Mark selected read
Mark read
</button>
<button class="list-group-item list-group-item-action" :disabled="!selectedHasRead()"
v-on:click="markSelectedUnread">
<i class="bi bi-eye-slash"></i>
Mark selected unread
Mark unread
</button>
<button class="list-group-item list-group-item-action" v-on:click="deleteMessages">
<i class="bi bi-trash-fill me-1 text-danger"></i>
Delete selected
Delete
</button>
<button class="list-group-item list-group-item-action" v-on:click="selected = []">
<i class="bi bi-x-circle me-1"></i>
@ -735,13 +782,13 @@ export default {
<div class="text-truncate d-lg-none privacy">
<span v-if="message.From" :title="message.From.Address">{{
message.From.Name ?
message.From.Name : message.From.Address
message.From.Name : message.From.Address
}}</span>
</div>
<div class="text-truncate d-none d-lg-block privacy">
<b v-if="message.From" :title="message.From.Address">{{
message.From.Name ?
message.From.Name : message.From.Address
message.From.Name : message.From.Address
}}</b>
</div>
<div class="d-none d-lg-block text-truncate text-muted small privacy">
@ -810,8 +857,7 @@ export default {
</div>
<!-- Modal -->
<div class="modal fade" id="MarkAllReadModal" tabindex="-1" aria-labelledby="MarkAllReadModalLabel"
aria-hidden="true">
<div class="modal fade" id="MarkAllReadModal" tabindex="-1" aria-labelledby="MarkAllReadModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
@ -874,6 +920,12 @@ export default {
</a>
<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">
<a class="btn btn-primary w-100" href="https://github.com/axllent/mailpit" target="_blank">
<i class="bi bi-github"></i>
@ -882,8 +934,7 @@ export default {
</a>
</div>
<div class="col-sm-6">
<a class="btn btn-primary w-100" href="https://github.com/axllent/mailpit/wiki"
target="_blank">
<a class="btn btn-primary w-100" href="https://github.com/axllent/mailpit/wiki" target="_blank">
Documentation
<i class="bi bi-box-arrow-up-right"></i>
</a>

View File

@ -149,10 +149,6 @@ body.blur {
}
}
// .tag.active {
// font-weight: bold;
// }
.form-select.tag-selector {
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!
https://prismjs.com/download.html#themes=prism-coy&languages=markup+css */
code[class*="language-"],

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

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

View File

@ -2,8 +2,8 @@
<script>
import commonMixins from '../mixins.js';
import Prism from "prismjs";
import Tags from "bootstrap5-tags";
import Attachments from './Attachments.vue';
import MessageTags from './MessageTags.vue';
export default {
props: {
@ -12,8 +12,7 @@ export default {
},
components: {
Attachments,
MessageTags
Attachments
},
mixins: [commonMixins],
@ -22,36 +21,54 @@ export default {
return {
srcURI: false,
iframes: [], // for resizing
tagComponent: false, // to force rerendering of component
showTags: false, // to force rerendering of component
messageTags: [],
allTags: [],
}
},
watch: {
message: {
handler(newQuestion) {
handler() {
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)
self.$nextTick(function () {
self.renderUI();
self.tagComponent = true;
self.showTags = true;
self.$nextTick(function () {
Tags.init("select[multiple]");
});
});
},
// force eager callback execution
immediate: true
},
messageTags() {
// save changed to tags
if (this.showTags) {
this.saveTags();
}
}
},
mounted() {
let self = this;
self.tagComponent = false;
self.showTags = false;
self.allTags = self.existingTags;
window.addEventListener("resize", self.resizeIframes);
self.renderUI();
var tabEl = document.getElementById('nav-raw-tab');
tabEl.addEventListener('shown.bs.tab', function (event) {
self.srcURI = 'api/v1/message/' + self.message.ID + '/raw';
});
self.tagComponent = true;
self.showTags = true;
self.$nextTick(function () {
Tags.init("select[multiple]");
});
},
unmounted: function () {
@ -105,6 +122,20 @@ export default {
if (s) {
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>
<td>{{ messageDate(message.Date) }}</td>
</tr>
<MessageTags :message="message" :existingTags="existingTags"
@load-messages="$emit('loadMessages')" v-if="tagComponent">
</MessageTags>
<tr class="small" v-if="showTags">
<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>
</table>
</div>
<div class="col-md-auto text-md-end mt-md-3">
<!-- <p class="text-muted small d-none d-md-block mb-2"><small>{{ messageDate(message.Date) }}</small></p>
<p class="text-muted small d-none d-md-block"><small>Size: {{ getFileSize(message.Size) }}</small></p> -->
<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">
<div class="col-md-auto d-none d-md-block text-end mt-md-3">
<div class="mt-2 mt-md-0" v-if="allAttachments(message)">
<span class="badge rounded-pill text-bg-secondary p-2">
Attachment<span v-if="allAttachments(message).length > 1">s</span>
({{ allAttachments(message).length }})
</button>
<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>
</span>
</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"
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"
type="button" role="tab" aria-controls="nav-html-source" aria-selected="false"
v-if="message.HTML">HTML Source</button>
type="button" role="tab" aria-controls="nav-html-source" aria-selected="false" v-if="message.HTML">HTML
Source</button>
<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"
:class="message.HTML == '' ? 'show' : ''">Text</button>
@ -208,8 +238,8 @@ export default {
<div class="tab-content mb-5" id="nav-tabContent">
<div v-if="message.HTML != ''" class="tab-pane fade show" id="nav-html" role="tabpanel"
aria-labelledby="nav-html-tab" tabindex="0">
<iframe target-blank="" class="tab-pane" id="preview-html" :srcdoc="message.HTML"
v-on:load="resizeIframe" seamless frameborder="0" style="width: 100%; height: 100%;">
<iframe target-blank="" class="tab-pane" id="preview-html" :srcdoc="message.HTML" v-on:load="resizeIframe"
seamless frameborder="0" style="width: 100%; height: 100%;">
</iframe>
<Attachments v-if="allAttachments(message).length" :message="message"
:attachments="allAttachments(message)"></Attachments>
@ -218,8 +248,8 @@ export default {
tabindex="0" v-if="message.HTML">
<pre><code class="language-html">{{ message.HTML }}</code></pre>
</div>
<div class="tab-pane fade" id="nav-plain-text" role="tabpanel" aria-labelledby="nav-plain-text-tab"
tabindex="0" :class="message.HTML == '' ? 'show' : ''">
<div class="tab-pane fade" id="nav-plain-text" role="tabpanel" aria-labelledby="nav-plain-text-tab" tabindex="0"
:class="message.HTML == '' ? 'show' : ''">
<div class="text-view">{{ message.Text }}</div>
<Attachments v-if="allAttachments(message).length" :message="message"
:attachments="allAttachments(message)"></Attachments>

View File

@ -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>

View File

@ -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>

View File

@ -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"
}
}
}

View File

@ -7,45 +7,81 @@ import (
"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 {
ID string
Read bool
From *mail.Address
To []*mail.Address
Cc []*mail.Address
Bcc []*mail.Address
Subject string
Date time.Time
Tags []string
Text string
HTML string
Size int
Inline []Attachment
// Unique message database id
ID string
// Read status
Read bool
// From address
From *mail.Address
// To addresses
To []*mail.Address
// Cc addresses
Cc []*mail.Address
// Bcc addresses
Bcc []*mail.Address
// 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
}
// Attachment struct for inline and attachments
//
// swagger:model Attachment
type Attachment struct {
PartID string
FileName string
// attachment part id
PartID string
// file name
FileName string
// content type
ContentType string
ContentID string
Size int
// content id
ContentID string
// size in bytes
Size int
}
// MessageSummary struct for frontend messages
//
// swagger:model MessageSummary
type MessageSummary struct {
ID string
Read bool
From *mail.Address
To []*mail.Address
Cc []*mail.Address
Bcc []*mail.Address
Subject string
Created time.Time
Tags []string
Size int
// Unique message database id
ID string
// Read status
Read bool
// From address
From *mail.Address
// To address
To []*mail.Address
// Cc addresses
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
}