1
0
mirror of https://github.com/axllent/mailpit.git synced 2025-01-24 03:47:38 +02:00

Merge branch 'release/1.2.2'

This commit is contained in:
Ralph Slooten 2022-10-13 08:14:42 +13:00
commit a2b6107dd6
11 changed files with 127 additions and 69 deletions

View File

@ -4,9 +4,15 @@ Notable changes to Mailpit will be documented in this file.
## 1.2.2
### API
- Add API endpoint to return message headers
### Libs
- Update go modules
### Testing
- Add API test for raw & message headers
## 1.2.1

View File

@ -1,24 +0,0 @@
package data
import "time"
// MailboxSummary struct
type MailboxSummary struct {
Name string
Slug string
Total int
Unread int
LastMessage time.Time
}
// WebsocketNotification struct for responses
type WebsocketNotification struct {
Type string
Data interface{}
}
// MailboxStats struct for quick mailbox total/read lookups
type MailboxStats struct {
Total int
Unread int
}

View File

@ -1,6 +1,8 @@
# Message
Returns a summary of the message and attachments.
## Message summary
Returns a JSON summary of the message and attachments.
**URL** : `api/v1/message/<ID>`
@ -70,6 +72,39 @@ Returns a summary of the message and attachments.
Returns the attachment using the MIME type provided by the attachment `ContentType`.
---
## Headers
**URL** : `api/v1/message/<ID>/headers`
**Method** : `GET`
Returns all message headers as a JSON array.
Each unique header key contains an array of one or more values (email headers can be listed multiple times.)
```json
{
"Content-Type": [
"multipart/related; type=\"multipart/alternative\"; boundary=\"----=_NextPart_000_0013_01C6A60C.47EEAB80\""
],
"Date": [
"Wed, 12 Jul 2006 23:38:30 +1200"
],
"Delivered-To": [
"user@example.com",
"user-alias@example.com"
],
"From": [
"\"User Name\" \\u003remote@example.com\\u003e"
],
"Message-Id": [
"\\u003c001701c6a5a7$b3205580$0201010a@HomeOfficeSM\\u003e"
],
....
}
```
---
## Raw (source) email

View File

@ -8,12 +8,12 @@ import (
"bytes"
"fmt"
"io/ioutil"
"log"
"net/mail"
"net/smtp"
"os"
"os/user"
"github.com/axllent/mailpit/logger"
flag "github.com/spf13/pflag"
)
@ -80,6 +80,6 @@ func Run() {
err = smtp.SendMail(smtpAddr, nil, fromAddr, recip, body)
if err != nil {
fmt.Fprintln(os.Stderr, "error sending mail")
log.Fatal(err)
logger.Log().Fatal(err)
}
}

View File

@ -4,25 +4,25 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/mail"
"strconv"
"strings"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/data"
"github.com/axllent/mailpit/storage"
"github.com/gorilla/mux"
)
// MessagesResult struct
type MessagesResult struct {
Total int `json:"total"`
Unread int `json:"unread"`
Count int `json:"count"`
Start int `json:"start"`
Messages []data.Summary `json:"messages"`
Total int `json:"total"`
Unread int `json:"unread"`
Count int `json:"count"`
Start int `json:"start"`
Messages []storage.Summary `json:"messages"`
}
// Messages returns a paginated list of messages
// Messages returns a paginated list of messages as JSON
func Messages(w http.ResponseWriter, r *http.Request) {
start, limit := getStartLimit(r)
@ -47,7 +47,7 @@ func Messages(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(bytes)
}
// Search returns a max of 200 of the latest messages
// Search returns up to 200 of the latest messages as JSON
func Search(w http.ResponseWriter, r *http.Request) {
search := strings.TrimSpace(r.URL.Query().Get("query"))
if search == "" {
@ -76,7 +76,7 @@ func Search(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(bytes)
}
// Message (method: GET) returns a *data.Message
// Message (method: GET) returns the *data.Message as JSON
func Message(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
@ -115,6 +115,32 @@ 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) {
vars := mux.Vars(r)
id := vars["id"]
data, err := storage.GetMessageRaw(id)
if err != nil {
httpError(w, err.Error())
return
}
reader := strings.NewReader(string(data))
m, err := mail.ReadMessage(reader)
if err != nil {
httpError(w, err.Error())
return
}
headers := m.Header
bytes, _ := json.Marshal(headers)
w.Header().Add("Content-Type", "application/json")
_, _ = w.Write(bytes)
}
// DownloadRaw (method: GET) returns the full email source as plain text
func DownloadRaw(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)

View File

@ -5,7 +5,6 @@ import (
"embed"
"io"
"io/fs"
"log"
"net/http"
"os"
"strings"
@ -47,10 +46,10 @@ func Listen() {
if config.UISSLCert != "" && config.UISSLKey != "" {
logger.Log().Infof("[http] starting secure server on https://%s", config.HTTPListen)
log.Fatal(http.ListenAndServeTLS(config.HTTPListen, config.UISSLCert, config.UISSLKey, nil))
logger.Log().Fatal(http.ListenAndServeTLS(config.HTTPListen, config.UISSLCert, config.UISSLKey, nil))
} else {
logger.Log().Infof("[http] starting server on http://%s", config.HTTPListen)
log.Fatal(http.ListenAndServe(config.HTTPListen, nil))
logger.Log().Fatal(http.ListenAndServe(config.HTTPListen, nil))
}
}
@ -62,9 +61,10 @@ func defaultRoutes() *mux.Router {
r.HandleFunc("/api/v1/messages", middleWareFunc(apiv1.SetReadStatus)).Methods("PUT")
r.HandleFunc("/api/v1/messages", middleWareFunc(apiv1.DeleteMessages)).Methods("DELETE")
r.HandleFunc("/api/v1/search", middleWareFunc(apiv1.Search)).Methods("GET")
r.HandleFunc("/api/v1/message/{id}/raw", middleWareFunc(apiv1.DownloadRaw)).Methods("GET")
r.HandleFunc("/api/v1/message/{id}/part/{partID}", middleWareFunc(apiv1.DownloadAttachment)).Methods("GET")
r.HandleFunc("/api/v1/message/{id}/part/{partID}/thumb", middleWareFunc(apiv1.Thumbnail)).Methods("GET")
r.HandleFunc("/api/v1/message/{id}/raw", middleWareFunc(apiv1.DownloadRaw)).Methods("GET")
r.HandleFunc("/api/v1/message/{id}/headers", middleWareFunc(apiv1.Headers)).Methods("GET")
r.HandleFunc("/api/v1/message/{id}", middleWareFunc(apiv1.Message)).Methods("GET")
r.HandleFunc("/api/v1/info", middleWareFunc(apiv1.AppInfo)).Methods("GET")

View File

@ -54,15 +54,24 @@ func Test_APIv1(t *testing.T) {
}
// read first 10
t.Log("Read first 10 messages")
t.Log("Read first 10 messages including raw & headers")
putIDS := []string{}
for indx, msg := range m.Messages {
if indx == 10 {
break
}
_, err := clientGet(ts.URL + "/api/v1/message/" + msg.ID)
if err != nil {
if _, err := clientGet(ts.URL + "/api/v1/message/" + msg.ID); err != nil {
t.Errorf(err.Error())
}
// test RAW
if _, err := clientGet(ts.URL + "/api/v1/message/" + msg.ID + "/raw"); err != nil {
t.Errorf(err.Error())
}
// test headers
if _, err := clientGet(ts.URL + "/api/v1/message/" + msg.ID + "/headers"); err != nil {
t.Errorf(err.Error())
}

View File

@ -5,11 +5,11 @@
package websockets
import (
"log"
"net/http"
"time"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/logger"
"github.com/gorilla/websocket"
)
@ -117,7 +117,7 @@ func ServeWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
logger.Log().Error(err)
return
}

View File

@ -7,7 +7,6 @@ package websockets
import (
"encoding/json"
"github.com/axllent/mailpit/data"
"github.com/axllent/mailpit/logger"
)
@ -27,6 +26,12 @@ type Hub struct {
unregister chan *Client
}
// WebsocketNotification struct for responses
type WebsocketNotification struct {
Type string
Data interface{}
}
// NewHub returns a new hub configuration
func NewHub() *Hub {
return &Hub{
@ -68,7 +73,7 @@ func Broadcast(t string, msg interface{}) {
return
}
w := data.WebsocketNotification{}
w := WebsocketNotification{}
w.Type = t
w.Data = msg
b, err := json.Marshal(w)

View File

@ -19,7 +19,6 @@ import (
"github.com/GuiaBolso/darwin"
"github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/data"
"github.com/axllent/mailpit/logger"
"github.com/axllent/mailpit/server/websockets"
"github.com/jhillyerd/enmime"
@ -231,7 +230,7 @@ func Store(body []byte) (string, error) {
}
// return summary
c := &data.Summary{}
c := &Summary{}
if err := json.Unmarshal(b, c); err != nil {
return "", err
}
@ -247,8 +246,8 @@ func Store(body []byte) (string, error) {
// List returns a subset of messages from the mailbox,
// sorted latest to oldest
func List(start, limit int) ([]data.Summary, error) {
results := []data.Summary{}
func List(start, limit int) ([]Summary, error) {
results := []Summary{}
q := sqlf.From("mailbox").
Select(`ID, Data, Read`).
@ -260,7 +259,7 @@ func List(start, limit int) ([]data.Summary, error) {
var id string
var summary string
var read int
em := data.Summary{}
em := Summary{}
if err := row.Scan(&id, &summary, &read); err != nil {
logger.Log().Error(err)
@ -291,8 +290,8 @@ func List(start, limit int) ([]data.Summary, error) {
// The search is broken up by segments (exact phrases can be quoted), and interprits specific terms such as:
// is:read, is:unread, has:attachment, to:<term>, from:<term> & subject:<term>
// Negative searches also also included by prefixing the search term with a `-` or `!`
func Search(search string) ([]data.Summary, error) {
results := []data.Summary{}
func Search(search string) ([]Summary, error) {
results := []Summary{}
start := time.Now()
s := strings.ToLower(search)
@ -305,8 +304,7 @@ func Search(search string) ([]data.Summary, error) {
p := shellwords.NewParser()
args, err := p.Parse(s)
if err != nil {
// return errors.New("Your search contains invalid characters")
panic(err)
return results, errors.New("Your search contains invalid characters")
}
// generate the SQL based on arguments
@ -317,7 +315,7 @@ func Search(search string) ([]data.Summary, error) {
var summary string
var read int
var ignore string
em := data.Summary{}
em := Summary{}
if err := row.Scan(&id, &summary, &read, &ignore, &ignore, &ignore, &ignore); err != nil {
logger.Log().Error(err)
@ -348,7 +346,7 @@ func Search(search string) ([]data.Summary, error) {
}
// GetMessage returns a data.Message generated from the mailbox_data collection.
func GetMessage(id string) (*data.Message, error) {
func GetMessage(id string) (*Message, error) {
raw, err := GetMessageRaw(id)
if err != nil {
return nil, err
@ -371,7 +369,7 @@ func GetMessage(id string) (*data.Message, error) {
date, _ := env.Date()
obj := data.Message{
obj := Message{
ID: id,
Read: true,
From: from,
@ -384,28 +382,26 @@ func GetMessage(id string) (*data.Message, error) {
Text: env.Text,
}
html := env.HTML
// strip base tags
var re = regexp.MustCompile(`(?U)<base .*>`)
html = re.ReplaceAllString(html, "")
html := re.ReplaceAllString(env.HTML, "")
obj.HTML = html
for _, i := range env.Inlines {
if i.FileName != "" || i.ContentID != "" {
obj.Inline = append(obj.Inline, data.AttachmentSummary(i))
obj.Inline = append(obj.Inline, AttachmentSummary(i))
}
}
for _, i := range env.OtherParts {
if i.FileName != "" || i.ContentID != "" {
obj.Inline = append(obj.Inline, data.AttachmentSummary(i))
obj.Inline = append(obj.Inline, AttachmentSummary(i))
}
}
for _, a := range env.Attachments {
if a.FileName != "" || a.ContentID != "" {
obj.Attachments = append(obj.Attachments, data.AttachmentSummary(a))
obj.Attachments = append(obj.Attachments, AttachmentSummary(a))
}
}
@ -650,7 +646,7 @@ func DeleteAllMessages() error {
}
// StatsGet returns the total/unread statistics for a mailbox
func StatsGet() data.MailboxStats {
func StatsGet() MailboxStats {
var (
start = time.Now()
total = CountTotal()
@ -661,7 +657,7 @@ func StatsGet() data.MailboxStats {
dbLastAction = time.Now()
return data.MailboxStats{
return MailboxStats{
Total: total,
Unread: unread,
}

View File

@ -1,5 +1,4 @@
// Package data contains the message & mailbox structs
package data
package storage
import (
"net/mail"
@ -48,6 +47,12 @@ type Summary struct {
Attachments int
}
// MailboxStats struct for quick mailbox total/read lookups
type MailboxStats struct {
Total int
Unread int
}
// AttachmentSummary returns a summary of the attachment without any binary data
func AttachmentSummary(a *enmime.Part) Attachment {
o := Attachment{}