diff --git a/data/mailbox.go b/data/mailbox.go deleted file mode 100644 index d8f269f..0000000 --- a/data/mailbox.go +++ /dev/null @@ -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 -} diff --git a/docs/apiv1/Message.md b/docs/apiv1/Message.md index 064e4ad..ff0a74f 100644 --- a/docs/apiv1/Message.md +++ b/docs/apiv1/Message.md @@ -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/` @@ -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//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 diff --git a/sendmail/cmd/cmd.go b/sendmail/cmd/cmd.go index 21969e1..6bf2ef7 100644 --- a/sendmail/cmd/cmd.go +++ b/sendmail/cmd/cmd.go @@ -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) } } diff --git a/server/apiv1/api.go b/server/apiv1/api.go index 049aba0..fc0fe4a 100644 --- a/server/apiv1/api.go +++ b/server/apiv1/api.go @@ -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) diff --git a/server/server.go b/server/server.go index c298967..f5ddb55 100644 --- a/server/server.go +++ b/server/server.go @@ -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") diff --git a/server/server_test.go b/server/server_test.go index 3da915e..ecf4b70 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -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()) } diff --git a/server/websockets/client.go b/server/websockets/client.go index 8499dce..d857d93 100644 --- a/server/websockets/client.go +++ b/server/websockets/client.go @@ -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 } diff --git a/server/websockets/hub.go b/server/websockets/hub.go index b5244fe..c19c6f4 100644 --- a/server/websockets/hub.go +++ b/server/websockets/hub.go @@ -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) diff --git a/storage/database.go b/storage/database.go index b9b2fb9..0d7e452 100644 --- a/storage/database.go +++ b/storage/database.go @@ -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:, from: & subject: // 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)`) - 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, } diff --git a/data/message.go b/storage/structs.go similarity index 90% rename from data/message.go rename to storage/structs.go index a6cf788..f0d68d3 100644 --- a/data/message.go +++ b/storage/structs.go @@ -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{}