Skip to content

Commit

Permalink
Merge branch 'release/v1.2.8'
Browse files Browse the repository at this point in the history
  • Loading branch information
axllent committed Nov 13, 2022
2 parents fc0a735 + b09d7ac commit 0fde942
Show file tree
Hide file tree
Showing 27 changed files with 619 additions and 134 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

Notable changes to Mailpit will be documented in this file.

## v1.2.8

### Bugfix
- Return empty arrays rather than null for message To, CC, BCC, Inlines & Attachments

### Feature
- Message tags and auto-tagging


## v1.2.7

### Feature
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Mailpit is inspired by [MailHog](#why-rewrite-mailhog), but much, much faster.
- SMTP server (default `0.0.0.0:1025`)
- Web UI to view emails (formatted HTML, highlighted HTML source, text, raw source and MIME attachments including image thumbnails)
- Advanced mail search ([see wiki](https://github.com/axllent/mailpit/wiki/Mail-search))
- Message tagging ([see wiki](https://github.com/axllent/mailpit/wiki/Tagging))
- Real-time web UI updates using web sockets for new mail
- Optional browser notifications for new mail (HTTPS only)
- Configurable automatic email pruning (default keeps the most recent 500 emails)
Expand All @@ -30,7 +31,7 @@ Mailpit is inspired by [MailHog](#why-rewrite-mailhog), but much, much faster.
- Optional SMTP with STARTTLS & SMTP authentication ([see wiki](https://github.com/axllent/mailpit/wiki/SMTP-with-STARTTLS-and-authentication))
- Optional HTTPS for web UI ([see wiki](https://github.com/axllent/mailpit/wiki/HTTPS))
- Optional basic authentication for web UI ([see wiki](https://github.com/axllent/mailpit/wiki/Basic-authentication))
- A simple REST API allowing ([see docs](docs/apiv1/README.md))
- A simple REST API ([see docs](docs/apiv1/README.md))
- Multi-architecture [Docker images](https://github.com/axllent/mailpit/wiki/Docker-images)


Expand All @@ -42,7 +43,7 @@ Linux & Mac users can install it directly to `/usr/local/bin/mailpit` with:
sudo bash < <(curl -sL https://raw.githubusercontent.com/axllent/mailpit/develop/install.sh)
```

Or download a pre-built binary in the [releases](https://github.com/axllent/mailpit/releases/latest). The `mailpit` can be placed in your `$PATH`, or simply run as `./mailpit`. See `mailpit -h` for options, or see [the wiki](https://github.com/axllent/mailpit/wiki/Runtime-options) for additional information.
Or download a static binary from the [releases](https://github.com/axllent/mailpit/releases/latest). The `mailpit` binary can be placed in your `$PATH`, or simply run as `./mailpit`. See `mailpit -h` for options, or see [the wiki](https://github.com/axllent/mailpit/wiki/Runtime-options) for additional information.

To build Mailpit from source see [building from source](https://github.com/axllent/mailpit/wiki/Building-from-source).

Expand Down
4 changes: 4 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ func init() {
if len(os.Getenv("MP_MAX_MESSAGES")) > 0 {
config.MaxMessages, _ = strconv.Atoi(os.Getenv("MP_MAX_MESSAGES"))
}
if len(os.Getenv("MP_TAG")) > 0 {
config.SMTPCLITags = os.Getenv("MP_TAG")
}
if len(os.Getenv("MP_UI_AUTH_FILE")) > 0 {
config.UIAuthFile = os.Getenv("MP_UI_AUTH_FILE")
}
Expand Down Expand Up @@ -139,6 +142,7 @@ func init() {
rootCmd.Flags().StringVar(&config.SMTPAuthFile, "smtp-auth-file", config.SMTPAuthFile, "A password file for SMTP authentication")
rootCmd.Flags().StringVar(&config.SMTPSSLCert, "smtp-ssl-cert", config.SMTPSSLCert, "SSL certificate for SMTP - requires smtp-ssl-key")
rootCmd.Flags().StringVar(&config.SMTPSSLKey, "smtp-ssl-key", config.SMTPSSLKey, "SSL key for SMTP - requires smtp-ssl-cert")
rootCmd.Flags().StringVarP(&config.SMTPCLITags, "tag", "t", "", "Tag new messages matching filters")

rootCmd.Flags().BoolVarP(&config.QuietLogging, "quiet", "q", false, "Quiet logging (errors only)")
rootCmd.Flags().BoolVarP(&config.VerboseLogging, "verbose", "v", false, "Verbose logging")
Expand Down
52 changes: 47 additions & 5 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ package config
import (
"errors"
"fmt"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"strings"

"github.com/mattn/go-shellwords"
"github.com/tg123/go-htpasswd"
)

Expand Down Expand Up @@ -61,6 +62,15 @@ var (
// SMTPAuth used for euthentication
SMTPAuth *htpasswd.File

// SMTPCLITags is used to map the CLI args
SMTPCLITags string

// TagRegexp is the allowed tag characters
TagRegexp = regexp.MustCompile(`^([a-zA-Z0-9\-\ \_]){3,}$`)

// SMTPTags are expressions to apply tags to new mail
SMTPTags []Tag

// ContentSecurityPolicy for HTTP server
ContentSecurityPolicy = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-src 'self'; img-src * data: blob:; font-src 'self' data:; media-src 'self'; connect-src 'self' ws: wss:; object-src 'none'; base-uri 'self';"

Expand All @@ -74,6 +84,12 @@ var (
RepoBinaryName = "mailpit"
)

// Tag struct
type Tag struct {
Tag string
Match string
}

// VerifyConfig wil do some basic checking
func VerifyConfig() error {
if DataFile != "" && isDir(DataFile) {
Expand Down Expand Up @@ -148,12 +164,38 @@ func VerifyConfig() error {
return fmt.Errorf("Webroot cannot contain spaces (%s)", Webroot)
}

s, err := url.JoinPath("/", Webroot, "/")
if err != nil {
return err
}
s := path.Join("/", Webroot, "/")
Webroot = s

SMTPTags = []Tag{}

p := shellwords.NewParser()

if SMTPCLITags != "" {
args, err := p.Parse(SMTPCLITags)
if err != nil {
return fmt.Errorf("Error parsing tags (%s)", err)
}

for _, a := range args {
t := strings.Split(a, "=")
if len(t) > 1 {
tag := strings.TrimSpace(t[0])
if !TagRegexp.MatchString(tag) || len(tag) == 0 {
return fmt.Errorf("Invalid tag (%s) - can only contain spaces, letters, numbers, - & _", tag)
}
match := strings.TrimSpace(strings.ToLower(strings.Join(t[1:], "=")))
if len(match) == 0 {
return fmt.Errorf("Invalid tag match (%s) - no search detected", tag)
}
SMTPTags = append(SMTPTags, Tag{Tag: tag, Match: match})
} else {
return fmt.Errorf("Error parsing tags (%s)", a)
}
}

}

return nil
}

Expand Down
6 changes: 3 additions & 3 deletions docs/apiv1/Message.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ Returns a JSON summary of the message and attachments.
"Address": "[email protected]"
}
],
"Cc": null,
"Bcc": null,
"Cc": [],
"Bcc": [],
"Subject": "Message subject",
"Date": "2016-09-07T16:46:00+13:00",
"Text": "Plain text MIME part of the email",
Expand Down Expand Up @@ -57,7 +57,7 @@ Returns a JSON summary of the message and attachments.

- `Read` - always true (message marked read on open)
- `From` - Name & Address, or null
- `To`, `CC`, `BCC` - Array of Names & Address, or null
- `To`, `CC`, `BCC` - Array of Names & Address
- `Date` - Parsed email local date & time from headers
- `Size` - Total size of raw email
- `Inline`, `Attachments` - Array of attachments and inline images.
Expand Down
4 changes: 2 additions & 2 deletions docs/apiv1/Messages.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ List messages in the mailbox. Messages are returned in the order of latest recei
"Address": "[email protected]"
}
],
"Bcc": null,
"Bcc": [],
"Subject": "Message subject",
"Created": "2022-10-03T21:35:32.228605299+13:00",
"Size": 6144,
Expand All @@ -70,7 +70,7 @@ List messages in the mailbox. Messages are returned in the order of latest recei
- `start` - The offset (default `0`) for pagination
- `Read` - The read/unread status of the message
- `From` - Name & Address, or null if none
- `To`, `CC`, `BCC` - Array of Names & Address, or null if none
- `To`, `CC`, `BCC` - Array of Names & Address
- `Created` - Local date & time the message was received
- `Size` - Total size of raw email in bytes

Expand Down
1 change: 1 addition & 0 deletions docs/apiv1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ The API is split into three main parts:

- [Messages](Messages.md) - Listing, deleting & marking messages as read/unread.
- [Message](Message.md) - Return message data & attachments
- [Tags](Tags.md) - Set message tags
- [Search](Search.md) - Searching messages
4 changes: 2 additions & 2 deletions docs/apiv1/Search.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Matching messages are returned in the order of latest received to oldest.
"Address": "[email protected]"
}
],
"Bcc": null,
"Bcc": [],
"Subject": "Test email",
"Created": "2022-10-03T21:35:32.228605299+13:00",
"Size": 6144,
Expand All @@ -65,5 +65,5 @@ Matching messages are returned in the order of latest received to oldest.
- `count` - Number of messages returned in request
- `start` - The offset (default `0`) for pagination
- `From` - Singular Name & Address, or null if none
- `To`, `CC`, `BCC` - Array of Name & Address, or null if none
- `To`, `CC`, `BCC` - Array of Name & Address
- `Size` - Total size of raw email in bytes
27 changes: 27 additions & 0 deletions docs/apiv1/Tags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Tags

Set message tags.


---
## Update message tags

Set the tags for one or more messages.
If the tags array is empty then all tags are removed from the messages.

**URL** : `api/v1/tags`

**Method** : `PUT`

### Request

```json
{
"ids": ["<ID>","<ID>"...],
"tags": ["<tag>","<tag>"]
}
```

### Response

**Status** : `200`
Binary file modified docs/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"axios": "^0.27.2",
"bootstrap": "^5.2.0",
"bootstrap-icons": "^1.9.1",
"bootstrap5-tags": "^1.4.41",
"moment": "^2.29.4",
"prismjs": "^1.29.0",
"tinycon": "^0.6.8",
Expand Down
35 changes: 34 additions & 1 deletion server/apiv1/api.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package apiv1

import (
"bytes"
"encoding/json"
"fmt"
"net/http"
Expand Down Expand Up @@ -32,6 +33,7 @@ func GetMessages(w http.ResponseWriter, r *http.Request) {
res.Count = len(messages)
res.Total = stats.Total
res.Unread = stats.Unread
res.Tags = stats.Tags

bytes, _ := json.Marshal(res)
w.Header().Add("Content-Type", "application/json")
Expand Down Expand Up @@ -63,6 +65,7 @@ func Search(w http.ResponseWriter, r *http.Request) {
res.Count = len(messages)
res.Total = stats.Total
res.Unread = stats.Unread
res.Tags = stats.Tags

bytes, _ := json.Marshal(res)
w.Header().Add("Content-Type", "application/json")
Expand Down Expand Up @@ -120,7 +123,7 @@ func Headers(w http.ResponseWriter, r *http.Request) {
return
}

reader := strings.NewReader(string(data))
reader := bytes.NewReader(data)
m, err := mail.ReadMessage(reader)
if err != nil {
httpError(w, err.Error())
Expand Down Expand Up @@ -234,6 +237,36 @@ func SetReadStatus(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("ok"))
}

// SetTags (method: PUT) will set the tags for all provided IDs
func SetTags(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)

var data struct {
Tags []string
IDs []string
}

err := decoder.Decode(&data)
if err != nil {
httpError(w, err.Error())
return
}

ids := data.IDs

if len(ids) > 0 {
for _, id := range ids {
if err := storage.SetTags(id, data.Tags); err != nil {
httpError(w, err.Error())
return
}
}
}

w.Header().Add("Content-Type", "text/plain")
_, _ = w.Write([]byte("ok"))
}

// FourOFour returns a basic 404 message
func fourOFour(w http.ResponseWriter) {
w.Header().Set("Referrer-Policy", "no-referrer")
Expand Down
1 change: 1 addition & 0 deletions server/apiv1/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type MessagesSummary struct {
Unread int `json:"unread"`
Count int `json:"count"`
Start int `json:"start"`
Tags []string `json:"tags"`
Messages []MessageSummary `json:"messages"`
}

Expand Down
1 change: 1 addition & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ func defaultRoutes() *mux.Router {
r.HandleFunc(config.Webroot+"api/v1/messages", middleWareFunc(apiv1.GetMessages)).Methods("GET")
r.HandleFunc(config.Webroot+"api/v1/messages", middleWareFunc(apiv1.SetReadStatus)).Methods("PUT")
r.HandleFunc(config.Webroot+"api/v1/messages", middleWareFunc(apiv1.DeleteMessages)).Methods("DELETE")
r.HandleFunc(config.Webroot+"api/v1/tags", middleWareFunc(apiv1.SetTags)).Methods("PUT")
r.HandleFunc(config.Webroot+"api/v1/search", middleWareFunc(apiv1.Search)).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")
Expand Down
Loading

0 comments on commit 0fde942

Please sign in to comment.