diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2a787625d..547994043 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,17 @@
Notable changes to Mailpit will be documented in this file.
+## [v1.19.0]
+
+### Feature
+- Add ability to rename and delete tags globally
+- Add option to disable auto-tagging for plus-addresses & X-Tags ([#323](https://github.com/axllent/mailpit/issues/323))
+
+### Chore
+- Update node dependencies
+- Update Go dependencies
+
+
## [v1.18.7]
### Feature
diff --git a/Dockerfile b/Dockerfile
index fa7f27fe1..c10460944 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM golang:alpine as builder
+FROM golang:alpine AS builder
ARG VERSION=dev
diff --git a/cmd/root.go b/cmd/root.go
index ccdc9502a..a17d16c3e 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -131,6 +131,7 @@ func init() {
rootCmd.Flags().StringVarP(&config.CLITagsArg, "tag", "t", config.CLITagsArg, "Tag new messages matching filters")
rootCmd.Flags().StringVar(&config.TagsConfig, "tags-config", config.TagsConfig, "Load tags filters from yaml configuration file")
rootCmd.Flags().BoolVar(&tools.TagsTitleCase, "tags-title-case", tools.TagsTitleCase, "TitleCase new tags generated from plus-addresses and X-Tags")
+ rootCmd.Flags().StringVar(&config.TagsDisable, "tags-disable", config.TagsDisable, "Disable auto-tagging, comma separated (eg: plus-addresses,x-tags)")
// Webhook
rootCmd.Flags().StringVar(&config.WebhookURL, "webhook-url", config.WebhookURL, "Send a webhook request for new messages")
@@ -290,6 +291,7 @@ func initConfigFromEnv() {
config.CLITagsArg = os.Getenv("MP_TAG")
config.TagsConfig = os.Getenv("MP_TAGS_CONFIG")
tools.TagsTitleCase = getEnabledFromEnv("MP_TAGS_TITLE_CASE")
+ config.TagsDisable = os.Getenv("MP_TAGS_DISABLE")
// Webhook
if len(os.Getenv("MP_WEBHOOK_URL")) > 0 {
diff --git a/config/config.go b/config/config.go
index ceb80bf73..c5c914b97 100644
--- a/config/config.go
+++ b/config/config.go
@@ -102,6 +102,10 @@ var (
// TagFilters are used to apply tags to new mail
TagFilters []autoTag
+ // TagsDisable accepts a comma-separated list of tag types to disable
+ // including x-tags & plus-addresses
+ TagsDisable string
+
// SMTPRelayConfigFile to parse a yaml file and store config of relay SMTP server
SMTPRelayConfigFile string
@@ -390,7 +394,7 @@ func VerifyConfig() error {
}
}
- // load tag filters
+ // load tag filters & options
TagFilters = []autoTag{}
if err := loadTagsFromArgs(CLITagsArg); err != nil {
return err
@@ -398,6 +402,9 @@ func VerifyConfig() error {
if err := loadTagsFromConfig(TagsConfig); err != nil {
return err
}
+ if err := parseTagsDisable(TagsDisable); err != nil {
+ return err
+ }
if SMTPAllowedRecipients != "" {
restrictRegexp, err := regexp.Compile(SMTPAllowedRecipients)
diff --git a/config/tags.go b/config/tags.go
index cb46f2599..c11ec26a3 100644
--- a/config/tags.go
+++ b/config/tags.go
@@ -11,6 +11,14 @@ import (
"gopkg.in/yaml.v3"
)
+var (
+ // TagsDisablePlus disables message tagging using plus-addresses (user+tag@example.com) - set via verifyConfig()
+ TagsDisablePlus bool
+
+ // TagsDisableXTags disables message tagging via the X-Tags header - set via verifyConfig()
+ TagsDisableXTags bool
+)
+
type yamlTags struct {
Filters []yamlTag `yaml:"filters"`
}
@@ -79,3 +87,25 @@ func loadTagsFromArgs(c string) error {
return nil
}
+
+func parseTagsDisable(s string) error {
+ s = strings.TrimSpace(s)
+ if s == "" {
+ return nil
+ }
+
+ parts := strings.Split(strings.ToLower(s), ",")
+
+ for _, p := range parts {
+ switch strings.TrimSpace(p) {
+ case "x-tags", "xtags":
+ TagsDisableXTags = true
+ case "plus-addresses", "plus-addressing":
+ TagsDisablePlus = true
+ default:
+ return fmt.Errorf("[tags] invalid --tags-disable option: %s", p)
+ }
+ }
+
+ return nil
+}
diff --git a/go.mod b/go.mod
index 647067c18..cb8742213 100644
--- a/go.mod
+++ b/go.mod
@@ -8,7 +8,7 @@ require (
github.com/PuerkitoBio/goquery v1.9.2
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
github.com/axllent/semver v0.0.1
- github.com/gomarkdown/markdown v0.0.0-20240419095408-642f0ee99ae2
+ github.com/gomarkdown/markdown v0.0.0-20240626202925-2eda941fd024
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.3
github.com/jhillyerd/enmime v1.2.0
@@ -55,11 +55,11 @@ require (
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/vanng822/css v1.0.1 // indirect
golang.org/x/crypto v0.24.0 // indirect
- golang.org/x/image v0.17.0 // indirect
+ golang.org/x/image v0.18.0 // indirect
golang.org/x/sys v0.21.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b // indirect
- modernc.org/libc v1.53.3 // indirect
+ modernc.org/libc v1.53.4 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/strutil v1.2.0 // indirect
diff --git a/go.sum b/go.sum
index a19ed5291..7195186b2 100644
--- a/go.sum
+++ b/go.sum
@@ -23,8 +23,8 @@ github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs=
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
-github.com/gomarkdown/markdown v0.0.0-20240419095408-642f0ee99ae2 h1:yEt5djSYb4iNtmV9iJGVday+i4e9u6Mrn5iP64HH5QM=
-github.com/gomarkdown/markdown v0.0.0-20240419095408-642f0ee99ae2/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
+github.com/gomarkdown/markdown v0.0.0-20240626202925-2eda941fd024 h1:saBP362Qm7zDdDXqv61kI4rzhmLFq3Z1gx34xpl6cWE=
+github.com/gomarkdown/markdown v0.0.0-20240626202925-2eda941fd024/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -128,8 +128,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
-golang.org/x/image v0.17.0 h1:nTRVVdajgB8zCMZVsViyzhnMKPwYeroEERRC64JuLco=
-golang.org/x/image v0.17.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
+golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
+golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
@@ -197,16 +197,16 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.21.3 h1:2mhBdWKtivdFlLR1ecKXTljPG1mfvbByX7QKztAIJl8=
modernc.org/cc/v4 v4.21.3/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
-modernc.org/ccgo/v4 v4.18.1 h1:1zF5kPBFq/ZVTulBOKgQPQITdOzzyBUfC51gVYP62E4=
-modernc.org/ccgo/v4 v4.18.1/go.mod h1:ao1fAxf9a2KEOL15WY8+yP3wnpaOpP/QuyFOZ9HJolM=
+modernc.org/ccgo/v4 v4.18.2 h1:PUQPShG4HwghpOekNujL0sFavdkRvmxzTbI4rGJ5mg0=
+modernc.org/ccgo/v4 v4.18.2/go.mod h1:ao1fAxf9a2KEOL15WY8+yP3wnpaOpP/QuyFOZ9HJolM=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b h1:BnN1t+pb1cy61zbvSUV7SeI0PwosMhlAEi/vBY4qxp8=
modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
-modernc.org/libc v1.53.3 h1:9O0aSLZuHPgp49we24NoFFteRgXNLGBAQ3TODrW3XLg=
-modernc.org/libc v1.53.3/go.mod h1:kb+Erju4FfHNE59xd2fNpv5CBeAeej6fHbx8p8xaiyI=
+modernc.org/libc v1.53.4 h1:YAgFS7tGIFBfqje2UOqiXtIwuDUCF8AUonYw0seup34=
+modernc.org/libc v1.53.4/go.mod h1:aGsLofnkcct8lTJnKQnCqJO37ERAXSHamSuWLFoF2Cw=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
diff --git a/internal/storage/messages.go b/internal/storage/messages.go
index 638267232..d4e139ec6 100644
--- a/internal/storage/messages.go
+++ b/internal/storage/messages.go
@@ -112,23 +112,29 @@ func Store(body *[]byte) (string, error) {
return "", err
}
- // extract tags from body matches
- rawTags := findTagsInRawMessage(body)
- // extract plus addresses tags from enmime.Envelope
- plusTags := obj.tagsFromPlusAddresses()
- // extract tags from X-Tags header
- xTags := tools.SetTagCasing(strings.Split(strings.TrimSpace(env.Root.Header.Get("X-Tags")), ","))
- // extract tags from search matches
- searchTags := tagFilterMatches(id)
-
- // combine all tags into one slice
- tags := append(rawTags, plusTags...)
- tags = append(tags, xTags...)
- // sort and extract only unique tags
- tags = sortedUniqueTags(append(tags, searchTags...))
+ // extract tags using pre-set tag filters, empty slice if not set
+ tags := findTagsInRawMessage(body)
+
+ if !config.TagsDisableXTags {
+ xTagsHdr := env.Root.Header.Get("X-Tags")
+ if xTagsHdr != "" {
+ // extract tags from X-Tags header
+ tags = append(tags, tools.SetTagCasing(strings.Split(strings.TrimSpace(xTagsHdr), ","))...)
+ }
+ }
+
+ if !config.TagsDisablePlus {
+ // get tags from plus-addresses
+ tags = append(tags, obj.tagsFromPlusAddresses()...)
+ }
+
+ // extract tags from search matches, and sort and extract unique tags
+ tags = sortedUniqueTags(append(tags, tagFilterMatches(id)...))
+ setTags := []string{}
if len(tags) > 0 {
- if err := SetMessageTags(id, tags); err != nil {
+ setTags, err = SetMessageTags(id, tags)
+ if err != nil {
return "", err
}
}
@@ -144,7 +150,7 @@ func Store(body *[]byte) (string, error) {
c.Attachments = attachments
c.Subject = subject
c.Size = size
- c.Tags = tags
+ c.Tags = setTags
c.Snippet = snippet
websockets.Broadcast("new", c)
@@ -593,7 +599,9 @@ func DeleteMessages(ids []string) error {
}
}
- err = tx.Commit()
+ if err := tx.Commit(); err != nil {
+ return err
+ }
dbLastAction = time.Now()
addDeletedSize(int64(totalSize))
diff --git a/internal/storage/schemas.go b/internal/storage/schemas.go
index 874e45c54..aaf26080f 100644
--- a/internal/storage/schemas.go
+++ b/internal/storage/schemas.go
@@ -137,7 +137,9 @@ func dbApplySchemas() error {
buf := new(bytes.Buffer)
- err = t1.Execute(buf, nil)
+ if err := t1.Execute(buf, nil); err != nil {
+ return err
+ }
if _, err := db.Exec(buf.String()); err != nil {
return err
@@ -197,7 +199,7 @@ func migrateTagsToManyMany() {
if len(toConvert) > 0 {
logger.Log().Infof("[migration] converting %d message tags", len(toConvert))
for id, tags := range toConvert {
- if err := SetMessageTags(id, tags); err != nil {
+ if _, err := SetMessageTags(id, tags); err != nil {
logger.Log().Errorf("[migration] %s", err.Error())
} else {
if _, err := sqlf.Update(tenant("mailbox")).
diff --git a/internal/storage/tags.go b/internal/storage/tags.go
index 9facfc4fe..c87189270 100644
--- a/internal/storage/tags.go
+++ b/internal/storage/tags.go
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"database/sql"
+ "fmt"
"regexp"
"sort"
"strings"
@@ -21,7 +22,7 @@ var (
)
// SetMessageTags will set the tags for a given database ID, removing any not in the array
-func SetMessageTags(id string, tags []string) error {
+func SetMessageTags(id string, tags []string) ([]string, error) {
applyTags := []string{}
for _, t := range tags {
t = tools.CleanTag(t)
@@ -30,6 +31,7 @@ func SetMessageTags(id string, tags []string) error {
}
}
+ tagNames := []string{}
currentTags := getMessageTags(id)
origTagCount := len(currentTags)
@@ -38,9 +40,12 @@ func SetMessageTags(id string, tags []string) error {
continue
}
- if err := AddMessageTag(id, t); err != nil {
- return err
+ name, err := AddMessageTag(id, t)
+ if err != nil {
+ return []string{}, err
}
+
+ tagNames = append(tagNames, name)
}
if origTagCount > 0 {
@@ -49,42 +54,44 @@ func SetMessageTags(id string, tags []string) error {
for _, t := range currentTags {
if !tools.InArray(t, applyTags) {
if err := DeleteMessageTag(id, t); err != nil {
- return err
+ return []string{}, err
}
}
}
}
- return nil
+ return tagNames, nil
}
// AddMessageTag adds a tag to a message
-func AddMessageTag(id, name string) error {
+func AddMessageTag(id, name string) (string, error) {
// prevent two identical tags being added at the same time
addTagMutex.Lock()
var tagID int
+ var foundName sql.NullString
q := sqlf.From(tenant("tags")).
Select("ID").To(&tagID).
+ Select("Name").To(&foundName).
Where("Name = ?", name)
// if tag exists - add tag to message
if err := q.QueryRowAndClose(context.TODO(), db); err == nil {
addTagMutex.Unlock()
// check message does not already have this tag
- var count int
+ var exists int
if err := sqlf.From(tenant("message_tags")).
- Select("COUNT(ID)").To(&count).
+ Select("COUNT(ID)").To(&exists).
Where("ID = ?", id).
Where("TagID = ?", tagID).
QueryRowAndClose(context.Background(), db); err != nil {
- return err
+ return "", err
}
- if count > 0 {
+ if exists > 0 {
// already exists
- return nil
+ return foundName.String, nil
}
logger.Log().Debugf("[tags] adding tag \"%s\" to %s", name, id)
@@ -93,7 +100,7 @@ func AddMessageTag(id, name string) error {
Set("ID", id).
Set("TagID", tagID).
ExecAndClose(context.TODO(), db)
- return err
+ return foundName.String, err
}
// new tag, add to the database
@@ -101,7 +108,7 @@ func AddMessageTag(id, name string) error {
Set("Name", name).
ExecAndClose(context.TODO(), db); err != nil {
addTagMutex.Unlock()
- return err
+ return name, err
}
addTagMutex.Unlock()
@@ -174,6 +181,79 @@ func GetAllTagsCount() map[string]int64 {
return tags
}
+// RenameTag renames a tag
+func RenameTag(from, to string) error {
+ to = tools.CleanTag(to)
+ if to == "" || !config.ValidTagRegexp.MatchString(to) {
+ return fmt.Errorf("invalid tag name: %s", to)
+ }
+
+ if from == to {
+ return nil // ignore
+ }
+
+ var id, existsID int
+
+ q := sqlf.From(tenant("tags")).
+ Select(`ID`).To(&id).
+ Where(`Name = ?`, from).
+ Limit(1)
+ err := q.QueryRowAndClose(context.Background(), db)
+ if err != nil {
+ return fmt.Errorf("tag not found: %s", from)
+ }
+
+ // check if another tag by this name already exists
+ q = sqlf.From(tenant("tags")).
+ Select("ID").To(&existsID).
+ Where(`Name = ?`, to).
+ Where(`ID != ?`, id).
+ Limit(1)
+ err = q.QueryRowAndClose(context.Background(), db)
+ if err == nil || existsID != 0 {
+ return fmt.Errorf("tag already exists: %s", to)
+ }
+
+ q = sqlf.Update(tenant("tags")).
+ Set("Name", to).
+ Where("ID = ?", id)
+ _, err = q.ExecAndClose(context.Background(), db)
+
+ return err
+}
+
+// DeleteTag deleted a tag and removed all references to the tag
+func DeleteTag(tag string) error {
+ var id int
+
+ q := sqlf.From(tenant("tags")).
+ Select(`ID`).To(&id).
+ Where(`Name = ?`, tag).
+ Limit(1)
+ err := q.QueryRowAndClose(context.Background(), db)
+ if err != nil {
+ return fmt.Errorf("tag not found: %s", tag)
+ }
+
+ // delete all references
+ q = sqlf.DeleteFrom(tenant("message_tags")).
+ Where(`TagID = ?`, id)
+ _, err = q.ExecAndClose(context.Background(), db)
+ if err != nil {
+ return fmt.Errorf("error deleting tag references: %s", err.Error())
+ }
+
+ // delete tag
+ q = sqlf.DeleteFrom(tenant("tags")).
+ Where(`ID = ?`, id)
+ _, err = q.ExecAndClose(context.Background(), db)
+ if err != nil {
+ return fmt.Errorf("error deleting tag: %s", err.Error())
+ }
+
+ return nil
+}
+
// PruneUnusedTags will delete all unused tags from the database
func pruneUnusedTags() error {
q := sqlf.From(tenant("tags")).
diff --git a/internal/storage/tags_test.go b/internal/storage/tags_test.go
index 388fe8b93..7b3636a58 100644
--- a/internal/storage/tags_test.go
+++ b/internal/storage/tags_test.go
@@ -24,7 +24,7 @@ func TestTags(t *testing.T) {
}
for i := 0; i < 10; i++ {
- if err := SetMessageTags(ids[i], []string{fmt.Sprintf("Tag-%d", i)}); err != nil {
+ if _, err := SetMessageTags(ids[i], []string{fmt.Sprintf("Tag-%d", i)}); err != nil {
t.Log("error ", err)
t.Fail()
}
@@ -58,7 +58,7 @@ func TestTags(t *testing.T) {
// pad number with 0 to ensure they are returned alphabetically
newTags = append(newTags, fmt.Sprintf("AnotherTag %02d", i))
}
- if err := SetMessageTags(id, newTags); err != nil {
+ if _, err := SetMessageTags(id, newTags); err != nil {
t.Log("error ", err)
t.Fail()
}
@@ -82,7 +82,7 @@ func TestTags(t *testing.T) {
assertEqual(t, "", strings.Join(returnedTags, "|"), "Message tags should be empty")
// apply the same tag twice
- if err := SetMessageTags(id, []string{"Duplicate Tag", "Duplicate Tag"}); err != nil {
+ if _, err := SetMessageTags(id, []string{"Duplicate Tag", "Duplicate Tag"}); err != nil {
t.Log("error ", err)
t.Fail()
}
@@ -94,7 +94,7 @@ func TestTags(t *testing.T) {
}
// apply tag with invalid characters
- if err := SetMessageTags(id, []string{"Dirty! \"Tag\""}); err != nil {
+ if _, err := SetMessageTags(id, []string{"Dirty! \"Tag\""}); err != nil {
t.Log("error ", err)
t.Fail()
}
diff --git a/package-lock.json b/package-lock.json
index a3d91465b..86abc2f4e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -938,36 +938,36 @@
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
},
"node_modules/@vue/compiler-core": {
- "version": "3.4.29",
- "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.29.tgz",
- "integrity": "sha512-TFKiRkKKsRCKvg/jTSSKK7mYLJEQdUiUfykbG49rubC9SfDyvT2JrzTReopWlz2MxqeLyxh9UZhvxEIBgAhtrg==",
+ "version": "3.4.31",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.31.tgz",
+ "integrity": "sha512-skOiodXWTV3DxfDhB4rOf3OGalpITLlgCeOwb+Y9GJpfQ8ErigdBUHomBzvG78JoVE8MJoQsb+qhZiHfKeNeEg==",
"dependencies": {
"@babel/parser": "^7.24.7",
- "@vue/shared": "3.4.29",
+ "@vue/shared": "3.4.31",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.0"
}
},
"node_modules/@vue/compiler-dom": {
- "version": "3.4.29",
- "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.29.tgz",
- "integrity": "sha512-A6+iZ2fKIEGnfPJejdB7b1FlJzgiD+Y/sxxKwJWg1EbJu6ZPgzaPQQ51ESGNv0CP6jm6Z7/pO6Ia8Ze6IKrX7w==",
+ "version": "3.4.31",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.31.tgz",
+ "integrity": "sha512-wK424WMXsG1IGMyDGyLqB+TbmEBFM78hIsOJ9QwUVLGrcSk0ak6zYty7Pj8ftm7nEtdU/DGQxAXp0/lM/2cEpQ==",
"dependencies": {
- "@vue/compiler-core": "3.4.29",
- "@vue/shared": "3.4.29"
+ "@vue/compiler-core": "3.4.31",
+ "@vue/shared": "3.4.31"
}
},
"node_modules/@vue/compiler-sfc": {
- "version": "3.4.29",
- "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.29.tgz",
- "integrity": "sha512-zygDcEtn8ZimDlrEQyLUovoWgKQic6aEQqRXce2WXBvSeHbEbcAsXyCk9oG33ZkyWH4sl9D3tkYc1idoOkdqZQ==",
+ "version": "3.4.31",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.31.tgz",
+ "integrity": "sha512-einJxqEw8IIJxzmnxmJBuK2usI+lJonl53foq+9etB2HAzlPjAS/wa7r0uUpXw5ByX3/0uswVSrjNb17vJm1kQ==",
"dependencies": {
"@babel/parser": "^7.24.7",
- "@vue/compiler-core": "3.4.29",
- "@vue/compiler-dom": "3.4.29",
- "@vue/compiler-ssr": "3.4.29",
- "@vue/shared": "3.4.29",
+ "@vue/compiler-core": "3.4.31",
+ "@vue/compiler-dom": "3.4.31",
+ "@vue/compiler-ssr": "3.4.31",
+ "@vue/shared": "3.4.31",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.10",
"postcss": "^8.4.38",
@@ -975,12 +975,12 @@
}
},
"node_modules/@vue/compiler-ssr": {
- "version": "3.4.29",
- "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.29.tgz",
- "integrity": "sha512-rFbwCmxJ16tDp3N8XCx5xSQzjhidYjXllvEcqX/lopkoznlNPz3jyy0WGJCyhAaVQK677WWFt3YO/WUEkMMUFQ==",
+ "version": "3.4.31",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.31.tgz",
+ "integrity": "sha512-RtefmITAje3fJ8FSg1gwgDhdKhZVntIVbwupdyZDSifZTRMiWxWehAOTCc8/KZDnBOcYQ4/9VWxsTbd3wT0hAA==",
"dependencies": {
- "@vue/compiler-dom": "3.4.29",
- "@vue/shared": "3.4.29"
+ "@vue/compiler-dom": "3.4.31",
+ "@vue/shared": "3.4.31"
}
},
"node_modules/@vue/devtools-api": {
@@ -989,49 +989,49 @@
"integrity": "sha512-0MiMsFma/HqA6g3KLKn+AGpL1kgKhFWszC9U29NfpWK5LE7bjeXxySWJrOJ77hBz+TBrBQ7o4QJqbPbqbs8rJw=="
},
"node_modules/@vue/reactivity": {
- "version": "3.4.29",
- "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.29.tgz",
- "integrity": "sha512-w8+KV+mb1a8ornnGQitnMdLfE0kXmteaxLdccm2XwdFxXst4q/Z7SEboCV5SqJNpZbKFeaRBBJBhW24aJyGINg==",
+ "version": "3.4.31",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.31.tgz",
+ "integrity": "sha512-VGkTani8SOoVkZNds1PfJ/T1SlAIOf8E58PGAhIOUDYPC4GAmFA2u/E14TDAFcf3vVDKunc4QqCe/SHr8xC65Q==",
"dependencies": {
- "@vue/shared": "3.4.29"
+ "@vue/shared": "3.4.31"
}
},
"node_modules/@vue/runtime-core": {
- "version": "3.4.29",
- "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.29.tgz",
- "integrity": "sha512-s8fmX3YVR/Rk5ig0ic0NuzTNjK2M7iLuVSZyMmCzN/+Mjuqqif1JasCtEtmtoJWF32pAtUjyuT2ljNKNLeOmnQ==",
+ "version": "3.4.31",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.31.tgz",
+ "integrity": "sha512-LDkztxeUPazxG/p8c5JDDKPfkCDBkkiNLVNf7XZIUnJ+66GVGkP+TIh34+8LtPisZ+HMWl2zqhIw0xN5MwU1cw==",
"dependencies": {
- "@vue/reactivity": "3.4.29",
- "@vue/shared": "3.4.29"
+ "@vue/reactivity": "3.4.31",
+ "@vue/shared": "3.4.31"
}
},
"node_modules/@vue/runtime-dom": {
- "version": "3.4.29",
- "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.29.tgz",
- "integrity": "sha512-gI10atCrtOLf/2MPPMM+dpz3NGulo9ZZR9d1dWo4fYvm+xkfvRrw1ZmJ7mkWtiJVXSsdmPbcK1p5dZzOCKDN0g==",
+ "version": "3.4.31",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.31.tgz",
+ "integrity": "sha512-2Auws3mB7+lHhTFCg8E9ZWopA6Q6L455EcU7bzcQ4x6Dn4cCPuqj6S2oBZgN2a8vJRS/LSYYxwFFq2Hlx3Fsaw==",
"dependencies": {
- "@vue/reactivity": "3.4.29",
- "@vue/runtime-core": "3.4.29",
- "@vue/shared": "3.4.29",
+ "@vue/reactivity": "3.4.31",
+ "@vue/runtime-core": "3.4.31",
+ "@vue/shared": "3.4.31",
"csstype": "^3.1.3"
}
},
"node_modules/@vue/server-renderer": {
- "version": "3.4.29",
- "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.29.tgz",
- "integrity": "sha512-HMLCmPI2j/k8PVkSBysrA2RxcxC5DgBiCdj7n7H2QtR8bQQPqKAe8qoaxLcInzouBmzwJ+J0x20ygN/B5mYBng==",
+ "version": "3.4.31",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.31.tgz",
+ "integrity": "sha512-D5BLbdvrlR9PE3by9GaUp1gQXlCNadIZytMIb8H2h3FMWJd4oUfkUTEH2wAr3qxoRz25uxbTcbqd3WKlm9EHQA==",
"dependencies": {
- "@vue/compiler-ssr": "3.4.29",
- "@vue/shared": "3.4.29"
+ "@vue/compiler-ssr": "3.4.31",
+ "@vue/shared": "3.4.31"
},
"peerDependencies": {
- "vue": "3.4.29"
+ "vue": "3.4.31"
}
},
"node_modules/@vue/shared": {
- "version": "3.4.29",
- "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz",
- "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA=="
+ "version": "3.4.31",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.31.tgz",
+ "integrity": "sha512-Yp3wtJk//8cO4NItOPpi3QkLExAr/aLBGZMmTtW9WpdwBCJpRM6zj9WgWktXAl8IDIozwNMByT45JP3tO3ACWA=="
},
"node_modules/anymatch": {
"version": "3.1.3",
@@ -1759,12 +1759,15 @@
}
},
"node_modules/is-core-module": {
- "version": "2.13.1",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
- "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
+ "version": "2.14.0",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.14.0.tgz",
+ "integrity": "sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==",
"dev": true,
"dependencies": {
- "hasown": "^2.0.0"
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -2029,9 +2032,12 @@
}
},
"node_modules/object-inspect": {
- "version": "1.13.1",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
- "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
+ "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
+ "engines": {
+ "node": ">= 0.4"
+ },
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -3038,15 +3044,15 @@
"peer": true
},
"node_modules/vue": {
- "version": "3.4.29",
- "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.29.tgz",
- "integrity": "sha512-8QUYfRcYzNlYuzKPfge1UWC6nF9ym0lx7mpGVPJYNhddxEf3DD0+kU07NTL0sXuiT2HuJuKr/iEO8WvXvT0RSQ==",
+ "version": "3.4.31",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.31.tgz",
+ "integrity": "sha512-njqRrOy7W3YLAlVqSKpBebtZpDVg21FPoaq1I7f/+qqBThK9ChAIjkRWgeP6Eat+8C+iia4P3OYqpATP21BCoQ==",
"dependencies": {
- "@vue/compiler-dom": "3.4.29",
- "@vue/compiler-sfc": "3.4.29",
- "@vue/runtime-dom": "3.4.29",
- "@vue/server-renderer": "3.4.29",
- "@vue/shared": "3.4.29"
+ "@vue/compiler-dom": "3.4.31",
+ "@vue/compiler-sfc": "3.4.31",
+ "@vue/runtime-dom": "3.4.31",
+ "@vue/server-renderer": "3.4.31",
+ "@vue/shared": "3.4.31"
},
"peerDependencies": {
"typescript": "*"
@@ -3066,9 +3072,9 @@
}
},
"node_modules/vue-router": {
- "version": "4.3.3",
- "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.3.3.tgz",
- "integrity": "sha512-8Q+u+WP4N2SXY38FDcF2H1dUEbYVHVPtPCPZj/GTZx8RCbiB8AtJP9+YIxn4Vs0svMTNQcLIzka4GH7Utkx9xQ==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.0.tgz",
+ "integrity": "sha512-HB+t2p611aIZraV2aPSRNXf0Z/oLZFrlygJm+sZbdJaW6lcFqEDQwnzUBXn+DApw+/QzDU/I9TeWx9izEjTmsA==",
"dependencies": {
"@vue/devtools-api": "^6.5.1"
},
diff --git a/server/apiv1/api.go b/server/apiv1/api.go
index 905bd5791..80888c0fd 100644
--- a/server/apiv1/api.go
+++ b/server/apiv1/api.go
@@ -523,77 +523,6 @@ func SetReadStatus(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("ok"))
}
-// GetAllTags (method: GET) will get all tags currently in use
-func GetAllTags(w http.ResponseWriter, _ *http.Request) {
- // swagger:route GET /api/v1/tags tags GetAllTags
- //
- // # Get all current tags
- //
- // Returns a JSON array of all unique message tags.
- //
- // Produces:
- // - application/json
- //
- // Schemes: http, https
- //
- // Responses:
- // 200: ArrayResponse
- // default: ErrorResponse
-
- w.Header().Add("Content-Type", "application/json")
- if err := json.NewEncoder(w).Encode(storage.GetAllTags()); err != nil {
- httpError(w, err.Error())
- }
-}
-
-// SetMessageTags (method: PUT) will set the tags for all provided IDs
-func SetMessageTags(w http.ResponseWriter, r *http.Request) {
- // swagger:route PUT /api/v1/tags tags SetTags
- //
- // # Set message tags
- //
- // This will overwrite any existing tags for selected message database IDs. To remove all tags from a message, pass an empty tags array.
- //
- // Consumes:
- // - application/json
- //
- // Produces:
- // - text/plain
- //
- // Schemes: http, https
- //
- // Responses:
- // 200: OKResponse
- // default: ErrorResponse
-
- 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.SetMessageTags(id, data.Tags); err != nil {
- httpError(w, err.Error())
- return
- }
- }
- }
-
- w.Header().Add("Content-Type", "text/plain")
- _, _ = w.Write([]byte("ok"))
-}
-
// ReleaseMessage (method: POST) will release a message via a pre-configured external SMTP server.
func ReleaseMessage(w http.ResponseWriter, r *http.Request) {
// swagger:route POST /api/v1/message/{ID}/release message ReleaseMessage
diff --git a/server/apiv1/swagger.go b/server/apiv1/swagger.go
index b0c5c3005..d9977965c 100644
--- a/server/apiv1/swagger.go
+++ b/server/apiv1/swagger.go
@@ -95,6 +95,22 @@ type setTagsRequestBody struct {
IDs []string
}
+// swagger:parameters RenameTag
+type renameTagParams struct {
+ // in: body
+ Body *renameTagRequestBody
+}
+
+// Rename tag request
+// swagger:model renameTagRequestBody
+type renameTagRequestBody struct {
+ // New name
+ //
+ // required: true
+ // example: New name
+ Name string
+}
+
// swagger:parameters ReleaseMessage
type releaseMessageParams struct {
// Message database ID
diff --git a/server/apiv1/tags.go b/server/apiv1/tags.go
new file mode 100644
index 000000000..f9e154b13
--- /dev/null
+++ b/server/apiv1/tags.go
@@ -0,0 +1,171 @@
+package apiv1
+
+import (
+ "encoding/json"
+ "net/http"
+
+ "github.com/axllent/mailpit/internal/storage"
+ "github.com/axllent/mailpit/server/websockets"
+ "github.com/gorilla/mux"
+)
+
+// GetAllTags (method: GET) will get all tags currently in use
+func GetAllTags(w http.ResponseWriter, _ *http.Request) {
+ // swagger:route GET /api/v1/tags tags GetAllTags
+ //
+ // # Get all current tags
+ //
+ // Returns a JSON array of all unique message tags.
+ //
+ // Produces:
+ // - application/json
+ //
+ // Schemes: http, https
+ //
+ // Responses:
+ // 200: ArrayResponse
+ // default: ErrorResponse
+
+ w.Header().Add("Content-Type", "application/json")
+ if err := json.NewEncoder(w).Encode(storage.GetAllTags()); err != nil {
+ httpError(w, err.Error())
+ }
+}
+
+// SetMessageTags (method: PUT) will set the tags for all provided IDs
+func SetMessageTags(w http.ResponseWriter, r *http.Request) {
+ // swagger:route PUT /api/v1/tags tags SetTags
+ //
+ // # Set message tags
+ //
+ // This will overwrite any existing tags for selected message database IDs. To remove all tags from a message, pass an empty tags array.
+ //
+ // Consumes:
+ // - application/json
+ //
+ // Produces:
+ // - text/plain
+ //
+ // Schemes: http, https
+ //
+ // Responses:
+ // 200: OKResponse
+ // default: ErrorResponse
+
+ 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.SetMessageTags(id, data.Tags); err != nil {
+ httpError(w, err.Error())
+ return
+ }
+ }
+ }
+
+ w.Header().Add("Content-Type", "text/plain")
+ _, _ = w.Write([]byte("ok"))
+}
+
+// RenameTag (method: PUT) used to rename a tag
+func RenameTag(w http.ResponseWriter, r *http.Request) {
+ // swagger:route PUT /api/v1/tags/{tag} tags RenameTag
+ //
+ // # Rename a tag
+ //
+ // Renames a tag.
+ //
+ // Produces:
+ // - text/plain
+ //
+ // Schemes: http, https
+ //
+ // Parameters:
+ // + name: tag
+ // in: path
+ // description: The url-encoded tag name to rename
+ // required: true
+ // type: string
+ //
+ // Responses:
+ // 200: OKResponse
+ // default: ErrorResponse
+
+ vars := mux.Vars(r)
+
+ tag := vars["tag"]
+
+ decoder := json.NewDecoder(r.Body)
+
+ var data struct {
+ Name string
+ }
+
+ err := decoder.Decode(&data)
+ if err != nil {
+ httpError(w, err.Error())
+ return
+ }
+
+ if err := storage.RenameTag(tag, data.Name); err != nil {
+ httpError(w, err.Error())
+ return
+ }
+
+ websockets.Broadcast("prune", nil)
+
+ w.Header().Add("Content-Type", "text/plain")
+ _, _ = w.Write([]byte("ok"))
+}
+
+// DeleteTag (method: DELETE) used to delete a tag
+func DeleteTag(w http.ResponseWriter, r *http.Request) {
+ // swagger:route DELETE /api/v1/tags/{tag} tags DeleteTag
+ //
+ // # Delete a tag
+ //
+ // Deletes a tag. This will not delete any messages with this tag.
+ //
+ // Produces:
+ // - text/plain
+ //
+ // Schemes: http, https
+ //
+ // Parameters:
+ // + name: tag
+ // in: path
+ // description: The url-encoded tag name to delete
+ // required: true
+ // type: string
+ //
+ // Responses:
+ // 200: OKResponse
+ // default: ErrorResponse
+
+ vars := mux.Vars(r)
+
+ tag := vars["tag"]
+
+ if err := storage.DeleteTag(tag); err != nil {
+ httpError(w, err.Error())
+ return
+ }
+
+ websockets.Broadcast("prune", nil)
+
+ w.Header().Add("Content-Type", "text/plain")
+ _, _ = w.Write([]byte("ok"))
+}
diff --git a/server/pop3/pop3_test.go b/server/pop3/pop3_test.go
index 912ab3a30..9012becdd 100644
--- a/server/pop3/pop3_test.go
+++ b/server/pop3/pop3_test.go
@@ -67,7 +67,7 @@ func TestPOP3(t *testing.T) {
return
}
- count, size, err = c.Stat()
+ count, _, err = c.Stat()
if err != nil {
t.Errorf(err.Error())
return
@@ -349,7 +349,7 @@ func insertEmailData(t *testing.T) {
t.Fail()
}
- if err := storage.SetMessageTags(id, []string{fmt.Sprintf("Test tag %03d", i)}); err != nil {
+ if _, err := storage.SetMessageTags(id, []string{fmt.Sprintf("Test tag %03d", i)}); err != nil {
t.Log("error ", err)
t.Fail()
}
diff --git a/server/server.go b/server/server.go
index 8ea160db9..aee94d59d 100644
--- a/server/server.go
+++ b/server/server.go
@@ -132,6 +132,8 @@ func apiRoutes() *mux.Router {
r.HandleFunc(config.Webroot+"api/v1/send", middleWareFunc(apiv1.SendMessageHandler)).Methods("POST")
r.HandleFunc(config.Webroot+"api/v1/tags", middleWareFunc(apiv1.GetAllTags)).Methods("GET")
r.HandleFunc(config.Webroot+"api/v1/tags", middleWareFunc(apiv1.SetMessageTags)).Methods("PUT")
+ r.HandleFunc(config.Webroot+"api/v1/tags/{tag}", middleWareFunc(apiv1.RenameTag)).Methods("PUT")
+ r.HandleFunc(config.Webroot+"api/v1/tags/{tag}", middleWareFunc(apiv1.DeleteTag)).Methods("DELETE")
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}/headers", middleWareFunc(apiv1.GetHeaders)).Methods("GET")
diff --git a/server/server_test.go b/server/server_test.go
index b39b0a63c..da9a53e34 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -383,7 +383,7 @@ func insertEmailData(t *testing.T) {
t.Fail()
}
- if err := storage.SetMessageTags(id, []string{fmt.Sprintf("Test tag %03d", i)}); err != nil {
+ if _, err := storage.SetMessageTags(id, []string{fmt.Sprintf("Test tag %03d", i)}); err != nil {
t.Log("error ", err)
t.Fail()
}
diff --git a/server/ui-src/App.vue b/server/ui-src/App.vue
index 35e336a3e..913f4db6b 100644
--- a/server/ui-src/App.vue
+++ b/server/ui-src/App.vue
@@ -2,6 +2,7 @@
import CommonMixins from './mixins/CommonMixins'
import Favicon from './components/Favicon.vue'
import Notifications from './components/Notifications.vue'
+import EditTags from './components/EditTags.vue'
import { RouterView } from 'vue-router'
import { mailbox } from "./stores/mailbox"
@@ -11,6 +12,7 @@ export default {
components: {
Favicon,
Notifications,
+ EditTags
},
beforeMount() {
@@ -41,4 +43,5 @@ export default {